SquashマージをGitフローに採用しているプロジェクトで、誤って「リモート開発ブランチ」に「トピックブランチ」をmerge & pushしてしまったとき、どのように「リモート開発ブランチ」のコミット履歴を訂正したら良いか?

次のような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

最後に、masterpushして完了となる:

git push

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.