Git リポジトリの特定のファイルを別の Git リポジトリに移す

Posted on

本来こっちのリポジトリにあるべきファイルがなぜか別のリポジトリにあるという状況に出くわしたので、Git の履歴を含めて別のリポジトリに移せるか調べてみました。

大まかな手順は次のとおりです。

  1. 移行元のリポジトリをクローンして移したいファイルだけ残す
  2. クローンしたリポジトリを移行先にサブツリーマージする
  3. 移行元のリポジトリから履歴を消す

検証した環境の Git のバージョンは 2.39.0 です。

$ git --version
git version 2.39.0

検証用リポジトリのセットアップ

ここでは repo1 を移行元、repo2 を移行先と仮定します。まずは repo1 を作ります。

$ mkdir repo1
$ cd repo1

$ git init
Initialized empty Git repository in /path/to/repo1/.git/

$ touch README.md
$ git add README.md
$ git commit -m "Initial commit"
[main (root-commit) ea19501] Initial commit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README.md

適当なファイルを作ってコミットを重ねます。

$ mkdir config
$ echo "foo: bar" > config/test.yml
$ git add config/test.yml
$ git commit -m "Add config/test.yml"
[main 37a369b] Add config/test.yml
 1 file changed, 1 insertion(+)
 create mode 100644 config/test.yml

$ echo "foo: buz" > config/test.yml
$ git add config/test.yml
$ git commit -m "Update config/test.yml"
[main 41e3378] Update config/test.yml
 1 file changed, 1 insertion(+), 1 deletion(-)

今回は config/ 以下を repo2 に移すことにします。

$ tree .
.
├── README.md
└── config
    └── test.yml

2 directories, 2 files

移行先の repo2 も同様に作っておきます。

$ cd ..
$ mkdir repo2
$ cd repo2
$ git init
Initialized empty Git repository in /path/to/repo2/.git/

$ touch README.md
$ git add README.md
$ git commit -m "Initial commit"
[main (root-commit) 0d71366] Initial commit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README.md

移行元のリポジトリをクローンして移したいファイルだけ残す

ローカルで repo1 をクローンして、作業用の repo1.tmp を作成します。

$ cd ..
$ git clone repo1 repo1.tmp
$ cd repo1.tmp

$ git log --oneline
41e3378 (HEAD -> main, origin/main, origin/HEAD) Update config/test.yml
37a369b Add config/test.yml
ea19501 Initial commit

次に git filter-branch を使って config/ 以下だけ残します。

$ git filter-branch --subdirectory-filter ./config HEAD
WARNING: git-filter-branch has a glut of gotchas generating mangled history
         rewrites.  Hit Ctrl-C before proceeding to abort, then use an
         alternative filtering tool such as 'git filter-repo'
         (https://github.com/newren/git-filter-repo/) instead.  See the
         filter-branch manual page for more details; to squelch this warning,
         set FILTER_BRANCH_SQUELCH_WARNING=1.
Proceeding with filter-branch...

Rewrite 41e337863e8d1d9e5403b7b0483aabd92ea1dba2 (2/2) (0 seconds passed, remaining 0 predicted)
Ref 'refs/heads/main' was rewritten

$ git log --oneline
5b5413f (HEAD -> main) Update config/test.yml
24e4b99 Add config/test.yml

$ tree .
.
└── test.yml

1 directory, 1 file

指定したパス以外のファイルと履歴が消えていることが分かります。歴史を書き換えているので、コミットハッシュが変わっています。

クローンしたリポジトリを移行先にサブツリーマージする

今度は移行先の repo2 のリポジトリで作業します。必要なファイルだけ残した repo1.tmp をリモートリポジトリとして追加します。

$ cd ../repo2
$ git remote add repo1 ../repo1.tmp
$ git fetch repo1
remote: Enumerating objects: 6, done.
remote: Counting objects: 100% (6/6), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 6 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (6/6), 439 bytes | 219.00 KiB/s, done.
From ../repo1.tmp
 * [new branch]      main       -> repo1/main

repo1/main をサブツリーマージして config/ 以下に Git の履歴を取り込みます。

$ git merge -s ours --no-commit --allow-unrelated-histories repo1/main
Automatic merge went well; stopped before committing as requested

$ git read-tree --prefix=config/ -u repo1/main

$ git commit -m "Import config file"
[main f784183] Import config file

$ git log --oneline
f784183 (HEAD -> main) Import config file
0d71366 Initial commit
5b5413f (repo1/main) Update config/test.yml
24e4b99 Add config/test.yml

$ tree .
.
├── README.md
└── config
    └── test.yml

2 directories, 2 files

意図したとおり移行できたら、リモートリポジトリを削除します。

$ git remote remove repo1

移行元のリポジトリから履歴を消す

最後に repo1 のリポジトリに戻って config/ 以下に関する履歴を削除します。

$ cd ../repo1
$ git filter-branch --force --index-filter "git rm -rf --cached --ignore-unmatch config/" --prune-empty -- --all
WARNING: git-filter-branch has a glut of gotchas generating mangled history
         rewrites.  Hit Ctrl-C before proceeding to abort, then use an
         alternative filtering tool such as 'git filter-repo'
         (https://github.com/newren/git-filter-repo/) instead.  See the
         filter-branch manual page for more details; to squelch this warning,
         set FILTER_BRANCH_SQUELCH_WARNING=1.
Proceeding with filter-branch...

Rewrite 37a369bb56e1b809943c63d63e4db4466b2b3ab1 (2/3) (0 seconds passed, remaining 0 predicted)    rm 'config/test.yml'
Rewrite 41e337863e8d1d9e5403b7b0483aabd92ea1dba2 (3/3) (0 seconds passed, remaining 0 predicted)    rm 'config/test.yml'

Ref 'refs/heads/main' was rewritten

$ git log --oneline
ea19501 (HEAD -> main) Initial commit

$ tree .
.
└── README.md

1 directory, 1 file

config/ 以下のファイルと履歴が消えていることが分かります。ただ、チームで開発している場合は影響が大きすぎるので、特別な理由がない限りは git rm したコミットを重ねるほうが良いと思います。

Popular Entries

Recent Entries