リポジトリの一部だけを残したい
あるリポジトリを構造を保ったまま別リポジトリとして独立させたいと考えた。そんなことはよくある作業だと思ったが、ぴったりな記事がなかったので書いておく。
やりたいのは 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
を使用する。
-
.
をエスケープしなきゃダメだろって?鋭いですね。でも、そんなファイルは存在しないことがわかってるので、このケースであれば気にしないで進めるのが得策です。特に、ここは3重の引用符の中にあるので、バックスラッシュが何個必要なのか俄にはわかりません。使うとすれば[.]
でしょうか。 ↩