Loading
BLOG 開発者ブログ

2020年3月26日

「マージコミットの revert で予期しない挙動をした話」

今や開発の場で欠かせないバージョン管理システム Git。
その機能のひとつである revert の注意点を実体験の共有を通して解説します。

 

イントロ

こんにちは。モバイルソリューショングループの yamazaki.h です。
新機能開発用ブランチの命名には毎度首を捻ったりしております。

今回の記事では Git での失敗談とマージの仕組みの検証をした結果を共有いたします。

 

コンテンツ

  1. 失敗談
  2. 検証
  3. 考察
  4. 対策

 

失敗談

まず初めにその時のブランチ構成を図示します。
機能ををまとめる develop ブランチから開発用の feature/change* ブランチが切られています。

何をしてしまったかというと以下の図を踏まえて解説します。

機能実装を進め、 feature/change1 ブランチの実装と動作確認が完了し、マージリクエストを作成。
レビューも通り、マージリクエストが許可されましたが、このマージリクエストの向き先が実は develop ではなく master に向いてしまっていたのです。
つまり feature/change1 ブランチの実装内容を、 master に入れてしまったわけです。
ただ、すぐに気付き revert を実施。 master は元どおりに。
その後の開発も続行し、 feature/change* で各機能を実装。
それぞれが完了し、 develop にマージ。
develop で様々な動作確認やテストを実施し、いよいよ master へマージを実行。

これにより何が起きたかと言いますと、結果として feature/change1 で実装した内容が master から消えていました。
図の赤枠部分です。

 

検証

上記を再現します。
まず初めに master, develop, feature/change* ブランチを用意します。
feature/change1 でファイルを作成し、コミットプッシュを行います。
次に feature/change2 でも同様にコミットプッシュを行います。
feature/change1 を master へ入れるマージリクエストを作成し許可、 master に開発途中の内容が混ざってしまった状態になります。
ここで master ブランチで以下の revert コマンドを実行します。

オプションについての説明は割愛します。

そして今度は develop へ feature/change* のマージを行います。
次に develop を master に入れるマージリクエストを作成し許可します。

master のコミットログを見てみます。

赤く囲った部分の順番を確認してください。古い順に、

  1. feature/change1 で機能実装のコミット
  2. feature/change1 を master にマージしたコミット
  3. マージで起きた変更点を打ち消すコミット(revert)

という順番になっています。

 

考察

ここでrevertコマンドのリファレンスを引用します。

Given one or more existing commits, revert the changes that the related patches introduce, and record some new commits that record them.

(引用元:Git – git-revert Documentation

訳すと、1つ以上のコミットに対し、それの変更を元に戻すパッチを適用する新たなコミットを作成する、とあります。
つまり、コミットを打ち消すコミットを作成するコマンドが revert であることがわかります。

また、マージコミットに対する revert コマンドについて GitHub 上で言及された内容を引用すると、

Reverting a regular commit just effectively undoes what that commit did, and is fairly straightforward. But reverting a merge commit also undoes the _data_ that the commit changed, but it does absolutely nothing to the effects on _history_ that the merge had.
So the merge will still exist, and it will still be seen as joining the two branches together, and future merges will see that merge as the last shared state – and the revert that reverted the merge brought in will not affect that at all.
So a “revert” undoes the data changes, but it’s very much _not_ an “undo” in the sense that it doesn’t undo the effects of a commit on the repository history.
So if you think of “revert” as “undo”, then you’re going to always miss this part of reverts. Yes, it undoes the data, but no, it doesn’t undo history.

(引用元:git/revert-a-fualty-merge.txt

要約すると、マージコミットに対する revert はあくまで「コード上の変更を打ち消すコミットを作成する」コマンドである、とのこと。

つまり「マージしたという事実の取り消し」は含まれないことが示されています。
マージした事実がある以上、ソースを変更したコミットというのはマージ済みであり、今後のマージの対象外になります。
そのため、 develop で試験した機能は実装されたように見え、 master へのマージのタイミングで消えてしまう、という事象が発生したわけです。

 

対策

前述した GitHub 上の内容にはそうした状況での対応方法も記述されています。

  1. revert 自体を revert し直す。
    「実装したコードの revert 」を revert することで master に入った変更を取り込んでしまう方法です。

  2. revert 後の master から新規のブランチを切り直して消してしまった実装を再度実装する。
    イメージとしては hotfix ブランチに似たような対応になります。

  3. 実装ブランチと同等になるようなブランチを作成と再実装を行い、マージを行う
    根元を揃えることで流れが追いやすくなります。

これらを見る限り、元どおり綺麗な状態に戻す方法は無いようです。
ちなみに私の実体験では master というブランチの利用方法上これらを実践するのも叶わず、 develop のマージが終わったのちに develop から新規ブランチを作成、機能を再度実装し、 develop へマージ、 master へマージという手順を踏むことが必要となりました。

前述の内容を踏まえた上で挙げられる対策は

  • あたりまえだけど、マージ先を間違えない
  • あたりまえだけど、マージを許可する際にはブランチの再確認を行う

つまり、当たり前のチェックを欠かさないようにすることが必要となります。

 

まとめ

技術的な話からずれてしまう結論となりましたが、メンタルや時期など様々な要因でこうしたことが起きかねないのでgitの操作などの特に大切な作業の時は細かく確認するよう気を付けましょう。

自戒も込めての記事となりました。読んでいただきありがとうございました。
この記事が Git 操作で困惑した方の助けになれば幸いです。