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

次のようなGitワークフローを採用しているプロジェクトがあるとする。

* 開発ブランチ: master
* トピックブランチ: issues/{番号}
* 変更を加えるときは、開発ブランチからトピックブランチをフォークし、トピックブランチにコミットしていく。
* トピックブランチが出来上がったら、開発ブランチにマージする。
* __ただし、開発ブランチにトピックブランチをマージするときは、squashすること。__
    * squashとは、複数のコミットを1コミットにまとめること
    * あと、マージするときは`--no-ff`でやること。

このようなフローでは次の図のような、変更コミット→マージコミット→変更コミット→マージコミットのリズムができ、コミットグラフがなんとなくキレイに見える。

![](https://qiita-image-store.s3.amazonaws.com/0/889/47a4ec8c-4fbf-8675-37d4-f6eec1fe44f5.png)

この運用の難しいところは、__注意していないとsquashするのを忘れること__、そして、うっかりしていると__そのままmasterにマージ&pushしてしまうと対処が面倒__という点がある。

![](https://qiita-image-store.s3.amazonaws.com/0/889/9e879a3f-561d-1576-ec86-16043761fcbc.png)

こういうことがしばしば起こるようであれば、ワークフローを見直したほうが良いだろう。どういうフローにカイゼンしていったらいいかはとりあえず置いておいて、本稿ではこうした__アクシデントが起こったときに、「どういうふうに応急処置したらいいか？」そして「squashし忘れたトピックブランチをどう正常にマージしたらいいか？」__について説明する。

/* ああ、タイトルまとまらなくて長くなってしまった…… */

## 応急処置

### 応急処置のゴール

まず、応急処置のゴールとしては、リモートの開発ブランチ`origin/master`をアクシデントが起きる前の状態に戻すことだ。

![](https://qiita-image-store.s3.amazonaws.com/0/889/cad4a721-9aac-475d-546a-48c607667069.png)

### アクシデントの再現

対処法を試せるように、まずはアクシデントの再現から行うことにする。

自分のPCの好きなところに再現環境を作る:

```bash 
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
```

アクシデント発生前の状況を作る:

```bash
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のコミット履歴はこのようになっているはずだ:

![](https://qiita-image-store.s3.amazonaws.com/0/889/47a4ec8c-4fbf-8675-37d4-f6eec1fe44f5.png)

次にアクシデントを発生させる:

```bash
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
```

ここまでのコマンドで、コミット履歴は次のようになっているだろう:

![](https://qiita-image-store.s3.amazonaws.com/0/889/9e879a3f-561d-1576-ec86-16043761fcbc.png)

### アクシデントへの応急処置手順

応急処置の第一歩は、戻りたい地点を特定することだ。

![](https://qiita-image-store.s3.amazonaws.com/0/889/cad4a721-9aac-475d-546a-48c607667069.png)

具体的には、tigコマンドを使い、戻したい地点のリビジョン番号を特定する。

```bash
git checkout master
tig
```

![](https://qiita-image-store.s3.amazonaws.com/0/889/e4bbf4da-6410-a82f-c886-ed8c631d444c.png)

この例では、`1e74b700def6fb71e64c7c4168eec86660429235`がそのリビジョン番号だ。

リビジョン番号を特定したら、念のため`git show`でコミットを確認する:

```bash
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 ${リビジョン番号}`を実行し、そのリビジョン以降のコミットを削除する:

```bash
git reset --hard 1e74b700def6fb71e64c7c4168eec86660429235
```

この状態でコミット履歴を確認すると、ローカルのほうではアクシデント発生前の開発ブランチの状態に戻っているはずだ:

![](https://qiita-image-store.s3.amazonaws.com/0/889/e44cd0e3-a7c9-86b6-895b-83402d5833b7.png)

最後に、`git push --force`を実行し、リモートの開発ブランチでも、消したいコミットを抹消する。`--force`は強制的に履歴を書き換えるため、二次的なアクシデントを防ぐためにもチームには連絡するまで開発ブランチに`push`したり`pull`しないようにアナウンスが必要だ。

```bash
git push --force
```

これで応急処置は完了となる。これ以降は、チームメンバが開発ブランチに触れても問題ない。

## squashし忘れたトピックブランチを正常にマージする

応急処置ができたので、ここからはリラックスして、squashし忘れたトピックブランチのマージ作業に臨もう。

### 正常なマージのゴール

ここでの正常なマージのゴールとしては、3つのコミットがsquashされ、他のトピックブランチ同様に`--no-ff`でマージされた状態にすることだ。

![](https://qiita-image-store.s3.amazonaws.com/0/889/b94977e9-6b1d-6816-118b-29dc2dfa5cc2.png)


### 正常にマージする手順

まずはマージしようとするトピックブランチに切り替える:

```bash
git checkout issues/3
```

トピックブランチには依然として3つコミットがあるので、コミット履歴はこのようになっている:

![](https://qiita-image-store.s3.amazonaws.com/0/889/90dfacfd-978b-3067-67de-e31f7412a7a9.png)

これら3つのコミットをひとつのコミットにする必要がある。それには`git rebase -i`する方法があるが、それよりも`git reset --soft`したほうが楽だ。

ここでは、`master`ブランチの最新コミット以後のコミットを削除しつつ、変更したファイルはstageしたままにしたいので実行するのは次のコマンドで良い:

```bash
git reset --soft master
```

このコマンドを実行するとコミット履歴はこうなる:

![](https://qiita-image-store.s3.amazonaws.com/0/889/a4a2339a-6c1c-4f5b-2d2b-348a97b15984.png)

消された3つコミットは、コミットしていない変更(Uncommitted changes)としてstageされた状態になっている。

この状態で、`commit`すると3つのコミットが1つのコミットになった履歴ができあがる。

```bash
git commit -m 'resolve issue/3'
```

![](https://qiita-image-store.s3.amazonaws.com/0/889/3ac13152-c251-4640-a5f1-957490b99e2e.png)

そして、それを`master`にマージする:

```bash
git checkout master
git merge --no-ff --no-edit issues/3
```

![](https://qiita-image-store.s3.amazonaws.com/0/889/300ea643-728d-bf40-1863-8995b4cdd03b.png)

最後に、`master`を`push`して完了となる:

```bash
git push
```

![](https://qiita-image-store.s3.amazonaws.com/0/889/b94977e9-6b1d-6816-118b-29dc2dfa5cc2.png)