6
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

git リポジトリを構造を保ったまま分割する

Last updated at Posted at 2019-11-27

リポジトリの一部だけを残したい

あるリポジトリを構造を保ったまま別リポジトリとして独立させたいと考えた。そんなことはよくある作業だと思ったが、ぴったりな記事がなかったので書いておく。

やりたいのは greple というコマンドの標準モジュールのひとつ (subst) を、外部モジュールとして独立させることだ。なぜ、そうしたかったかというと、依存する特有のモジュールが増えて、それを本体の依存関係に入れたくなかったからだ。

ちなみに、App::Greple::subst は原稿の校正用モジュールで、用語統一のために使っている。以前から使っていたものだが、今回はこれを大幅に改造して使いやすくした。詳細についてはまた改めて。(追記:その後、以下の記事に書きました)

というわけなので、リポジトリのディレクトリ構造はそのままにして、こういう構造から:

.
├── Build.PL
├── Changes
├── Example.md
├── LICENSE
├── MANIFEST
├── MANIFEST.SKIP
├── META.json
├── README.md
├── cpanfile
├── lib/
│   └── App/
│       ├── Greple/
│       │   ├── Common.pm
│       │   ├── Filter.pm
│       │   ├── Grep.pm
│       │   ├── Pattern/
│       │   │   └── Holder.pm
│       │   ├── Pattern.pm
│       │   ├── PgpDecryptor.pm
│       │   ├── Regions.pm
│       │   ├── Util.pm
│       │   ├── colors.pm
│       │   ├── debug.pm
│       │   ├── dig.pm
│       │   ├── find.pm
│       │   ├── line.pm
│       │   ├── perl.pm
│       │   ├── pgp.pm
│       │   └── subst.pm
│       └── Greple.pm
├── script/
│   └── greple*
...

特定のファイルを切り出したいだけだ:

.
└── lib/
    └── App/
        └── Greple/
            └── subst.pm

メタファイルは新しく作るので必要ないが、ヒストリーは維持したい。その必要がなければ、単に新しいリポジトリを作ってコピーすればいいだけだ。

新しいブランチを作る

とりあえず、元のリポジトリで作業用の新しいブランチを作る。まず clone する例が多いような気がするが、使いもしないデータをわざわざコピーする必要もなかろう。

また、後になって今回の作業を何度か再現してみたが、そのためには元のリポジトリに作業用ブランチが残っていた方が何かと便利だった。

git checkout -b subst

filter-branch --subdirectory-filter は使えない

とりあえず、リポジトリを書き換えるには filter-branch コマンドを使うのが王道であることには間違いなさそうだ。

よく出てくるのが filter-branch --subdirectory-filter を使う方法だ。リポジトリのディレクトリの一部を独立したリポジトリにしたいことはよくあるのだろう。しかし、先に書いたように、今回は全体の構造を保ったまま分割したいので、これは使えない。

filter-branch --tree-filter を使う

--subdirectory-filter が適さない場合には、--tree-filter を使う方法が紹介されていることが多い。大概、間違って登録してしまったファイルを歴史から抹殺するというような使い方だ。

これはかなり乱暴なコマンドで、コミットヒストリーを一つ一つファイルシステム上に展開して、そこで指定したコマンドを実行した結果を再度登録していくというものだ。今回の場合、subst.pm というファイル以外をすべて削除すればいいので find コマンドを使って、それ以外のファイルをすべて削除する。--prune-empty というオプションは、空のコミットをなかったことにするものだ。

git filter-branch -f --prune-empty --tree-filter 'find . -not -name subst.pm -delete'

コミットヒストリが500近くあると、実行に4分半かかった。

real    4m25.242s
user    2m8.542s
sys     1m12.992s

filter-branch --index-filter を使う

ファイルの内容を書き換えるような修正をしたい場合には --tree-filter が必要だが、今回の場合はファイルを消したいだけなので、ファイルそのものにアクセスできる必要はない。その場合には --index-filter というオプションが使えることがわかった。

これはステージングされたインデックスを操作するので、ファイルを消去するためには git rm --cached コマンドを使用する。git ls-files で取得したリストから subst.pm を除いてすべて削除する1

git filter-branch --prune-empty -f --index-filter 'git rm --cached -f `git ls-files | sed "/subst.pm/d"`' HEAD

こうすることで、先の --tree-filter と同じ結果が得られる。ファイルを展開しないので、1分強で実行は終わった。ファイル名に空白が含まれている場合には、このままではうまく行かないかもしれないのでご注意を。

real    1m8.790s
user    0m25.648s
sys     0m29.289s

--index-filter が一番なのか?

ここまでの結果では filter-branch --index-filter を使うのが一番よさそうだ。しかし、できれば残したいファイルを指定できた方が理にかなっているし、git mv した場合にはそれを辿ってくれた方が嬉しい。もっといい方法があったら教えてください。

新しいリポジトリを初期化する

ここからは、使う環境によって違う話だ。今回は Perl のリポジトリを Minilla で初期化する。

minil new App::Greple::subst

こうすると新しい git リポジトリを作って commit する前の状態で止まる。lib 以下は必要ないので reset して消去してしまおう。

cd App-Greple-subst/
git reset lib
rm -fr lib

この状態で commit してもいいのだが、このままにして次に進む。
気持ち悪ければ、全体を reset してしまって、後で add しても構わない。

元のリポジトリを merge する

今回は、隣のディレクトリに元のリポジトリがあるので、そこを remote に指定する。

git remote add greple ../greple/

そして、ブランチを指定して fetch すると最小限のデータだけを持ってくるので、それを merge する。

git fetch greple subst
git merge greple/subst

これで過去のヒストリがコピーされた。この時点で Minilla で作成したファイルを commit してあげると、きれいなヒストリができる。

git commit -m 'minil new App::Greple::subst'

先にコミットしてしまうと merge に失敗する。その場合は --allow-unrelated-histories を指定する。

終わったら remote を削除して縁を切る。

git remote remove greple

まとめ

  • git リポジトリの一部ファイルだけを残したい場合には、それ以外のファイルを削除するしか方法はなさそう。
    • 本当か?
  • コミットヒストリを修正するには git filter-branch を使用する。
  • ファイルの内容にアクセスする必要がなければ --tree-filter よりも --index-filter オプションを使用した方が高速。
  • 特定ファイル以外を削除するには git ls-files の結果を加工する。
    • 若干苦し紛れ感あるので、もっといい方法が望まれる。
  • 対象ファイルを減らすと何も変更しないコミットができるので、それを削除するためには --prune-empty を使用する。
  1. . をエスケープしなきゃダメだろって?鋭いですね。でも、そんなファイルは存在しないことがわかってるので、このケースであれば気にしないで進めるのが得策です。特に、ここは3重の引用符の中にあるので、バックスラッシュが何個必要なのか俄にはわかりません。使うとすれば [.] でしょうか。

6
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?