Git公開用に第２のブランチ＆リポジトリを作りつつ履歴は極力残すドン！

## やりたいこと：おいらもGithubに進出だドン！

いざGithubへ、そんなとき、次のようなこと、悩みませんか

- 個人or社内開発のリポジトリはOnedriveや社内クラウドなどのクローズド環境に置いている（クローズドリポ）。
- 公開用ソースは、それとは別のリポジトリ(Github等)にしたい(パブリックリポ)。
- パブリックリポでは、公開するファイルを削りたい。
- 見せても良い開発履歴は、クローズドリポからパブリックリポへ極力残したい。

ここで、今回の方法では、クローズドリポに紐づいているクローズドブランチと、パブリックリポに紐づいているパブリックブランチがそれぞれ別物としてあって、クローズドブランチで日々の開発を勧めつつ、パブリックブランチに定期的にマージすることにします。

## もうだめカッ・・・

さて、上記の目的のために検索すると、`.gitignore`を書き換えて、`.gitattributes`に`merge=ours`追加して、という情報が出てきます。が、私の環境ではうまくいかなかった。具体的には、パブリックリポを作った後、クローズドリポで開発を進めて、再度パブリックリポへマージしようとすると、コンフリクトの嵐。原因は、パブリックリポでは一部のファイルもなくなっているし、場合によって履歴のコミットIDが変わっているため。

また、`merge=ours`が動作するにはある条件が必要なようで[^1]、動作しないことが結構多い。そうするとパブリックリポ向けの.gitignoreに書いてあろうがmergeによってファイルが取り込まれてしまうことがありました。

## なんとか目的を達成だドン！

先に例題のコマンド載せます。私の冗長な文章よむよりもコマンド見た方が早いって人も多いと思うんで。クローズドブランチを`master`、パブリックブランチを`pub`とします。`close.txt`が公開したくないファイルで、`pub`ブランチからはそれを除きます。

```
mkdir gittest
cd gittest

#(1)prepare closed branch "master", and do commit 3 times.
git init
echo "bigdata" > .gitignore
git add .gitignore
git commit -a -m "master1"
echo "master close2" > close.txt
git add close.txt
git commit -a -m "master2"
echo "master open3" > open.txt
echo "master close3" > close.txt
git add open.txt
git commit -a -m "master3"
git log --oneline -n 5 open.txt
git log --oneline -n 5 close.txt
cat *.txt

#(2)make public branch "pub"
git branch pub
git checkout pub
git log --oneline -n 5 open.txt
git log --oneline -n 5 close.txt
cat *.txt

#(3)remove "close.txt", which is unpublic file, on public "pub" branch
git filter-branch -f --index-filter "git rm -f --cached --ignore-unmatch close.txt" --prune-empty HEAD
echo "close.txt" >> .gitignore
git reflog expire --expire=now HEAD
git gc --aggressive --prune=now
git log --oneline -n 5 open.txt
git log --oneline -n 5 close.txt
cat *.txt

#(4)advance to develop on closed "master" branch
git checkout master
echo "master open4" > open.txt
echo "master close4" > close.txt
git commit -a -m "master4"
echo "master open5" > open.txt
echo "master close5" > close.txt
git commit -a -m "master5"
git log --oneline -n 5 open.txt
git log --oneline -n 5 close.txt
cat *.txt

#(5)make temporary branch "tmp"
git branch tmp
git checkout tmp
cat *.txt

#(6)remove unpublic files on "tmp" branch
git filter-branch -f --index-filter "git rm -f --cached --ignore-unmatch close.txt" --prune-empty HEAD
git reflog expire --expire=now HEAD
git gc --aggressive --prune=now
git log --oneline -n 5 open.txt
git log --oneline -n 5 close.txt
cat *.txt

#(7)merge the developed files into public "pub" branch
git checkout pub
git merge -Xtheirs tmp -m "pub5"
git log --oneline -n 5 open.txt
git log --oneline -n 5 close.txt
cat *.txt

#(8)remove temporary branch
git branch -d tmp
```

ここで、開発が進むたびに(4)から(8)が繰り返されます。

つまるところ、次のことをやっています。

- 公開したくないファイルは`git filter-branch`で履歴からも削除
- 二度目以降のマージではコンフリクトを防ぐために、temporaryなbranchで`git filter-branch`してからパブリックブランチにマージする

ここで、経験則でしかないのですが、**同じ履歴を持つブランチに同じ引数でgit filter-branchした場合には、書き換わったコミットIDに再現性がある(同じになる)**という性質を使っています。ここで、ある時点まで同じ履歴をもっていれば、片方のブランチがそれ以降の履歴を持っていても、大丈夫です。この経験則を利用して、マージ時に`tmp`ブランチでの`git filter-branch`にて書き換わったコミットIDが、実は`pub`ブランチで最初に`git filter-branch`して書き換えたコミットIDと同一になり、マージ可能になります。

もしコミットIDが違っちゃうと、同じ履歴とみなされず、`fatal: refusing to merge unrelated histories`エラーが出て、optionで`--allow-unrelated-histories`をつけるという話になってきますが、今回はそれも必要ありません。

## 注意だドン！

Windows環境では、PowerShellなら問題ないですが、WSL(Windows10 1903)上で動かすと、ファイル操作が遅いためか時々おかしな動作になります。ファイルが消えて居なかったり。消したはずのファイルが残っていたり。よって、上記コマンドの例題のコマンドも途中で失敗することがあります。特に`git merge`や`git filter-branch`で消したはずのファイルが消えてなかったりとかでErrorになります。ぶっちゃけこれで相当ハマりました。

WSL2でこの辺り改善されるといいですね。

## おわりに

git mergeマジ卍むずかしい（言ってみたかっただけ）。

殆ど経験則だけで書いているので、ご指摘大歓迎です。

## Reference

[^1][StackOverFlow](https://stackoverflow.com/questions/5465122/gitattributes-individual-merge-strategy-for-a-file)の回答にある"**The merge driver is only called in non-trivial cases, i.e. if both master and test have touched setup**"や、[StackOverFlow-2](https://stackoverflow.com/questions/41640602/gitattributes-merge-strategy-not-working)の回答にある"**Git only invokes a three-way merge driver if there are \*two diffs to combine.**"あたりが、どうやら的を射てるのかな。要は、マージドライバーの"ours"が呼ばれるには条件があって、それはマージ元とマージ先の両方のブランチで編集されている必要があると。

1. [git filter-branchで過去の全てのcommitから画像ファイルの追加/変更をなかったことにしてリポジトリを軽量化する](https://dskd.jp/archives/46.html)
1. [Git ファイルの履歴を完全に削除する](https://gist.github.com/ktx2207/3167fa69531bdd6b44f1)
1. [ブランチごとに特定ファイルの内容を変える](https://www.granfairs.com/blog/cto/change-branch-settings)
1. [Gitでブランチをリモートに送る時の注意点](https://rcmdnk.com/blog/2014/01/31/computer-git/)
1. [gitで空ブランチを作る](https://qiita.com/fami-ki-chi/items/d49aca6721750f68b5af)
1. [git merge 時に内容が異なるファイルをマージ対象から除外する](https://qiita.com/haman29/items/cafd59233b2affe653fb)
1. [git の merge, rebase, revert で衝突した際の ours, theirs はどんな状態になるか](https://qiita.com/nirasan/items/27cdd75e8117bf41d2be)