git subtreeの仕組み
git subtree
git-subtreeは、複数のリポジトリを一つのリポジトリに纏めるためのツールで、gitのcontribに含まれている。ここではgit-subtreeの仕組みについて説明する。
前提となるgitのデータ構造についてはPro Git 10.2 Gitの内側 - Gitオブジェクトを参照のこと。
事前準備
取り込み元リポジトリの内容を参照するため、 git remote add
でリモートリポジトリを登録し、git fetch
して対象のコミットを参照できるようにしておく。
git subtree add
git subtree add
は、取り込み元の内容を指定したサブディレクトリ以下に取り込む。
雰囲気で説明すると、取り込み元のディレクトリツリーをサブディレクトリ以下に mv したものを、 git merge --allow-unrelated-histories
でマージして取り込んでいる感じ。
具体例で説明しよう。事前準備で登録したリモートリポジトリの履歴に「取り込み元commit」が含まれているとする。
ここで git subtree add –prefix=sub_dir 取り込み元commit
すると、次の図のようにツリーオブジェクトとマージコミットが作られ、HEADが更新される。
「マージcommit」では、「取り込み先commit」から見ると指定したサブディレクトリ以下にファイルが追加されており、「取り込み元commit」から見るとファイルが指定したサブディレクトリ以下に移動されている。
$ git diff --name-status 取り込み先commit..マージcommit
A sub_dir/file_g
A sub_dir/file_r
A sub_dir/file_y
$ git diff --name-status 取り込み元commit..マージcommit
A file_b
R100 file_g sub_dir/file_g
R100 file_r sub_dir/file_r
R100 file_y sub_dir/file_y
git subtree add
したときに実行されている内容は以下の通り。
-
git read-tree --prefix=sub_dir 取り込み元commit
で取り込み元のディレクトリツリーをsub_dir以下に移動しつつ、インデックスに取り込み -
git checkout -- sub_dir
でsub_dir以下をインデックスから作業ツリーに反映 -
git write-tree
でトップレベルのツリーオブジェクトを生成 -
git commit-tree 生成したツリーオブジェクト -p 取り込み先commit -p 取り込み元commit
で親を2つ指定してマージコミットを生成 -
git reset 生成したマージコミット
で HEAD を更新
マージコミットにより「取り込み元commit」に連なるコミットグラフをそのまま取り込むため、git log
で表示されるコミットハッシュは取り込み元リポジトリのものと同じになる[1]。
また、git diff --name-status
でR100と表示されていた通り一致度100%でリネームとして判定される[2]ので、git blame
ではマージコミットより手前の取り込み元リポジトリ側の履歴も辿ってくれる。
git subtree merge
git subtree merge --prefix=sub_dir 取り込み元commit
でsubtree add済みのsub_dirにさらなる取り込みを行える。
処理としては git merge --no-ff -Xsubtree=sub_dir 取り込み元commit
でディレクトリがずれていることを踏まえてマージしている。これにより、既にsubtree addあるいはsubtree mergeで取り込み済みのコミット履歴よりも先の履歴がいい感じに取り込まれる。
試してみる
サンプルリポジトリを作成して実際に試してみよう。
取り込み元リポジトリ作成
/tmp$ mkdir remote
/tmp$ cd $_
/tmp/remote$ git init
Initialized empty Git repository in /private/tmp/remote/.git/
/tmp/remote$ for c in y r g; do echo ${c} > file_${c}; done
/tmp/remote$ git add .
/tmp/remote$ git commit -m "initialize target repository"
[main (root-commit) 7705f16] initialize target repository
3 files changed, 3 insertions(+)
create mode 100644 file_g
create mode 100644 file_r
create mode 100644 file_y
/tmp/remote$ COMMIT_1=$(git rev-parse HEAD)
/tmp/remote$ echo yyy >> file_y
/tmp/remote$ git add .
/tmp/remote$ git commit -m "update file_y in target repository"
[main c932ae2] update file_y in target repository
1 file changed, 1 insertion(+)
/tmp/remote$ COMMIT_2=$(git rev-parse HEAD)
/tmp/remote$ REMOTE_URL=$(pwd)
/tmp/remote$ cd -
/tmp
取り込み先リポジトリ作成
/tmp$ mkdir local
/tmp$ cd $_
/tmp/local$ git init
Initialized empty Git repository in /private/tmp/local/.git/
/tmp/local$ echo b > file_b
/tmp/local$ git add .
/tmp/local$ git commit -m "initialize local repository"
[main (root-commit) 6196cdb] initialize local repository
1 file changed, 1 insertion(+)
create mode 100644 file_b
/tmp/local$ COMMIT_3=$(git rev-parse HEAD)
リモート設定
/tmp/local$ git remote add target ${REMOTE_URL}
/tmp/local$ git fetch target
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 8 (delta 1), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (8/8), 522 bytes | 130.00 KiB/s, done.
From /tmp/remote
* [new branch] main -> target/main
git subtree add
/tmp/local$ ls
file_b
/tmp/local$ git log --oneline --graph
* 6196cdb (HEAD -> main) initialize local repository
/tmp/local$ git log --oneline --graph ${COMMIT_1}
* 7705f16 initialize target repository
/tmp/local$ git subtree add --prefix=sub_dir ${COMMIT_1}
Added dir 'sub_dir'
/tmp/local$ git log --oneline --graph
* 3e9d760 (HEAD -> main) Add 'sub_dir/' from commit '7705f165ca49a966f8d9278ecb96552d10e76359'
|\
| * 7705f16 initialize target repository
* 6196cdb initialize local repository
/tmp/local$ ls *
file_b
sub_dir:
file_g file_r file_y
/tmp/local$ git diff --name-status ${COMMIT_3}..HEAD
A sub_dir/file_g
A sub_dir/file_r
A sub_dir/file_y
/tmp/local$ git diff --name-status ${COMMIT_1}..HEAD
A file_b
R100 file_g sub_dir/file_g
R100 file_r sub_dir/file_r
R100 file_y sub_dir/file_y
git subtree merge
/tmp/local$ git log --oneline --graph
* 3e9d760 (HEAD -> main) Add 'sub_dir/' from commit '7705f165ca49a966f8d9278ecb96552d10e76359'
|\
| * 7705f16 initialize target repository
* 6196cdb initialize local repository
/tmp/local$ git log --oneline --graph ${COMMIT_2}
* c932ae2 (target/main) update file_y in target repository
* 7705f16 initialize target repository
/tmp/local$ git subtree merge --prefix=sub_dir ${COMMIT_2}
Merge made by the 'ort' strategy.
sub_dir/file_y | 1 +
1 file changed, 1 insertion(+)
/tmp/local$ git log --oneline --graph
* 29f8850 (HEAD -> main) Merge commit 'c932ae27c75bebd66a1844e463a655c4ce823638'
|\
| * c932ae2 (target/main) update file_y in target repository
* | 3e9d760 Add 'sub_dir/' from commit '7705f165ca49a966f8d9278ecb96552d10e76359'
|\|
| * 7705f16 initialize target repository
* 6196cdb initialize local repository
/tmp/local$ git diff --name-status HEAD^1..HEAD
M sub_dir/file_y
-
オプション
--squash
を指定しなかった場合 ↩︎
Discussion