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

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

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

[greple]: https://github.com/kaz-utashiro/greple
[subst]: https://github.com/kaz-utashiro/greple-subst
[App::Greple::subst]: https://github.com/kaz-utashiro/greple-subst

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

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

https://qiita.com/kaz-utashiro/items/85add653a71a7e01c415

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

```
.
├── 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 する例が多いような気がするが、使いもしないデータをわざわざコピーする必要もなかろう。

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

```sh
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` というオプションは、空のコミットをなかったことにするものだ。

```sh
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` を除いてすべて削除する[^sed]。

[^sed]: `.` をエスケープしなきゃダメだろって？鋭いですね。でも、そんなファイルは存在しないことがわかってるので、このケースであれば気にしないで進めるのが得策です。特に、ここは3重の引用符の中にあるので、バックスラッシュが何個必要なのか俄にはわかりません。使うとすれば `[.]` でしょうか。

```sh
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 して消去してしまおう。

```sh
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 してあげると、きれいなヒストリができる。

```sh
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` を使用する。