次のようなGitワークフローを採用しているプロジェクトがあるとする。
- 開発ブランチ: master
- トピックブランチ: issues/{番号}
- 変更を加えるときは、開発ブランチからトピックブランチをフォークし、トピックブランチにコミットしていく。
- トピックブランチが出来上がったら、開発ブランチにマージする。
-
ただし、開発ブランチにトピックブランチをマージするときは、squashすること。
- squashとは、複数のコミットを1コミットにまとめること
- あと、マージするときは
--no-ff
でやること。
このようなフローでは次の図のような、変更コミット→マージコミット→変更コミット→マージコミットのリズムができ、コミットグラフがなんとなくキレイに見える。
この運用の難しいところは、注意していないとsquashするのを忘れること、そして、うっかりしているとそのままmasterにマージ&pushしてしまうと対処が面倒という点がある。
こういうことがしばしば起こるようであれば、ワークフローを見直したほうが良いだろう。どういうフローにカイゼンしていったらいいかはとりあえず置いておいて、本稿ではこうしたアクシデントが起こったときに、「どういうふうに応急処置したらいいか?」そして「squashし忘れたトピックブランチをどう正常にマージしたらいいか?」について説明する。
/* ああ、タイトルまとまらなくて長くなってしまった…… */
応急処置
応急処置のゴール
まず、応急処置のゴールとしては、リモートの開発ブランチorigin/master
をアクシデントが起きる前の状態に戻すことだ。
アクシデントの再現
対処法を試せるように、まずはアクシデントの再現から行うことにする。
自分のPCの好きなところに再現環境を作る:
mkdir git-playground
cd git-playground
# リモートリポジトリを作る
git init --bare remote.git
# ローカルリポジトリを作る
git clone $PWD/remote.git local
cd local
git commit -m 'init' --allow-empty
アクシデント発生前の状況を作る:
git checkout -b issues/1
git commit -m 'resolve issue/1' --allow-empty
git checkout master
git merge --no-ff --no-edit issues/1
git checkout -b issues/2
git commit -m 'resolve issue/2' --allow-empty
git checkout master
git merge --no-ff --no-edit issues/2
git push
ここまでやると、Gitのコミット履歴はこのようになっているはずだ:
次にアクシデントを発生させる:
git checkout -b issues/3
echo 'change 1' >> file
git add file
git commit -m 'commit 1'
echo 'change 2' >> file
git add file
git commit -m 'commit 2'
echo 'change 3' >> file
git add file
git commit -m 'commit 3'
git checkout master
git merge --no-ff --no-edit issues/3
git push
ここまでのコマンドで、コミット履歴は次のようになっているだろう:
アクシデントへの応急処置手順
応急処置の第一歩は、戻りたい地点を特定することだ。
具体的には、tigコマンドを使い、戻したい地点のリビジョン番号を特定する。
git checkout master
tig
この例では、1e74b700def6fb71e64c7c4168eec86660429235
がそのリビジョン番号だ。
リビジョン番号を特定したら、念のためgit show
でコミットを確認する:
ession
$ git show 1e74b700def6fb71e64c7c4168eec86660429235
commit 1e74b700def6fb71e64c7c4168eec86660429235
Merge: 656469a 6456507
Author: suin <suinyeze@gmail.com>
Date: Thu May 17 15:47:24 2018 +0900
Merge branch 'issues/2'
戻し先に問題がないようであれば、git reset --hard ${リビジョン番号}
を実行し、そのリビジョン以降のコミットを削除する:
git reset --hard 1e74b700def6fb71e64c7c4168eec86660429235
この状態でコミット履歴を確認すると、ローカルのほうではアクシデント発生前の開発ブランチの状態に戻っているはずだ:
最後に、git push --force
を実行し、リモートの開発ブランチでも、消したいコミットを抹消する。--force
は強制的に履歴を書き換えるため、二次的なアクシデントを防ぐためにもチームには連絡するまで開発ブランチにpush
したりpull
しないようにアナウンスが必要だ。
git push --force
これで応急処置は完了となる。これ以降は、チームメンバが開発ブランチに触れても問題ない。
squashし忘れたトピックブランチを正常にマージする
応急処置ができたので、ここからはリラックスして、squashし忘れたトピックブランチのマージ作業に臨もう。
正常なマージのゴール
ここでの正常なマージのゴールとしては、3つのコミットがsquashされ、他のトピックブランチ同様に--no-ff
でマージされた状態にすることだ。
正常にマージする手順
まずはマージしようとするトピックブランチに切り替える:
git checkout issues/3
トピックブランチには依然として3つコミットがあるので、コミット履歴はこのようになっている:
これら3つのコミットをひとつのコミットにする必要がある。それにはgit rebase -i
する方法があるが、それよりもgit reset --soft
したほうが楽だ。
ここでは、master
ブランチの最新コミット以後のコミットを削除しつつ、変更したファイルはstageしたままにしたいので実行するのは次のコマンドで良い:
git reset --soft master
このコマンドを実行するとコミット履歴はこうなる:
消された3つコミットは、コミットしていない変更(Uncommitted changes)としてstageされた状態になっている。
この状態で、commit
すると3つのコミットが1つのコミットになった履歴ができあがる。
git commit -m 'resolve issue/3'
そして、それをmaster
にマージする:
git checkout master
git merge --no-ff --no-edit issues/3
最後に、master
をpush
して完了となる:
git push