Git面接の質問:履歴図でmerge・rebase・revertと復旧を説明する
Quick Overview
Gitの履歴図と実行した設定ファイルを照合し、merge・rebase・merge revert・reflogによる復旧を説明する面接練習です。
「共有済みの merge を取り消したい。あとから追加された文書は残したい」。この場面で reset と答える前に、どのコミットが誰の祖先なのか、どのファイル値を戻し、どの後続変更を残すのかを先に整理しましょう。Git のコマンドを覚えていても、履歴とファイルの状態を同時に予測できなければ、意図しない変更を残してしまいます。
この記事では、retry と timeout の設定を変更する独立した練習用リポジトリで、merge、rebase、merge の revert、ローカルの復旧を実行します。二つの成果物は、名前を付けたコミット図と、コマンド前後のファイル・参照を照合する記録です。Demonstrate Git and build workflow と組み合わせ、操作の理由まで説明する練習に使えます。
公式仕様は Git のマニュアル、観測結果は Git 2.50.1(Apple Git-155)で実行した本稿のローカル例、運用上の判断は編集部の提案です。候補者レポートを使った企業別の出題頻度や採点基準の説明ではありません。練習では実際のプロジェクト履歴を変更せず、外部リモートへの push も行っていません。

最初に、履歴・参照・作業中の変更を分ける
公式仕様: Git のオブジェクトの説明を踏まえると、コミットはスナップショットと親の関係を持ち、ブランチ名はコミットを指す参照です。通常の HEAD は現在のブランチを指し、detached HEAD ではコミットを直接指します。さらに、次のコミット候補を作るインデックスと、手元のファイルである作業ツリーがあります。
面接で「変更を取り消す」と言われたら、何を変える依頼かを確認します。共有ブランチの先端を動かすのか、新しいコミットで過去の変更を打ち消すのか、未コミットのファイルだけを戻すのかで、対象が違います。先に「今の HEAD、変更済みファイル、共有の有無を確認します」と説明すると、その後の操作が、何を守るための選択かを伝えられます。
確認には git status、git diff、git diff --cached、git log --graph --oneline --decorate --all が使えます。ただし、一つの出力だけでローカルとリモートの状態がすべて分かるわけではありません。本稿の例はリモートを持たないため、共有済みという条件は面接上の仮定として扱います。
公式仕様: git-merge は、未コミットの変更がある状態で始めた merge を --abort しても、元の変更を完全に再構成できない場合があると説明しています。今回の再現は作業ツリーをクリーンにして始めました。この条件を外した場合まで、同じ復旧結果を保証しません。
A・B・F1・F2 の状態から、結果を先に予測する
練習の初期コミット A には retry.txt が1、timeout.txt が3と入っています。A から feature を作り、F1 で retry を2、F2 で timeout を10にします。一方 main は A から B へ進み、timeout を5に変更します。
A --- B main
\
F1 --- F2 feature
この図の線は左から右への履歴の進行を表します。実際のコミットオブジェクトが持つ参照は親に向かうので、図の見せ方と内部の参照方向を混同しないでください。F2 の親は F1、F1 と B の親は A です。
| コミット | retry | timeout | 変更の意図 |
|---|---|---|---|
| A | 1 | 3 | 初期状態 |
| B | 1 | 5 | main 側の timeout 変更 |
| F1 | 2 | 3 | feature 側の retry 変更 |
| F2 | 2 | 10 | feature 側の timeout 変更 |
main と feature が同じ timeout を異なる値に変えているため、main 上で feature を merge すると競合しました。ここで正解の値は Git が決めるものではありません。今回の main へ取り込む契約は「retry は2にし、timeout は main の5を保つ」と定めます。二つの値の意味を確認せず、競合マーカーだけ消して完了にしないことがポイントです。
merge は二つの親、rebase は新しい親を確認する
公式仕様: git-merge は履歴を統合します。現在の先端が相手の祖先なら fast-forward が可能ですが、今回の B と F2 は A から分岐しています。競合を解決して作った merge コミット M は、第1親が B、第2親が F2 になりました。
A --- B ---------- M
\ /
F1 --- F2 ------
M を見れば両方の履歴へたどれます。実行では git show -s --format=%P の出力を [BのID, F2のID] と照合し、親の順序まで確認しました。--no-ff は統合点を残す意図を表していますが、この分岐した履歴では元々 fast-forward はできません。オプションがあるから二親になる、とだけ説明しないようにします。
公式仕様: git-rebase は、コミットの変更を別の基点へ再適用します。比較用に F2 から rebase-demo を作り、B を基点に rebase しました。F1 と F2 は F1′ と F2′ になり、F1′ の親は B、F2′ の親は F1′ です。
A --- B --- F1' --- F2' rebase-demo
\
F1 --- F2 feature(元の参照を保持)
この rebase でも timeout の競合が起きました。比較用ブランチは「feature の10を維持する」と定めて解決したため、結果は retry=2、timeout=10です。main に取り込む契約の5とは意図が異なります。一直線の履歴でも timeout=5という取り込み条件は満たさないため、グラフとファイル値を別々に照合します。
実行結果では F1′ と F2′ の ID が元と異なり、各コミットの親は一つでした。元の feature 参照は F2 のままです。rebase-demo だけを操作したからであり、「rebase しても元のブランチが常に残る」という一般的な保証ではありません。
他の人が基点にした共有履歴を書き換えると、その人の履歴調整が必要になります。公式マニュアルもその影響を説明しています。判断の提案: 未共有の自分のブランチを整える用途か、共有済みの履歴を変更する用途かを確認し、後者ではチームの方針と調整を先に示します。
競合マーカーを消せても、設定の正しさは証明できない
練習ではまず merge の競合を確認し、git merge --abort で B に戻りました。戻った先端が B、作業ツリーがクリーンであることを照合しています。その後もう一度 merge し、わざと timeout を3へ解決して M をコミットしました。
M は Git にとって有効な二親のコミットですが、ファイルは retry=2、timeout=3です。main 側の5を保つ契約に違反しています。「競合を解決してコミットできた」と「要求された設定を維持できた」は別です。
このとき面接では、「A は3、B は5、F2 は10です。今回の契約は5なので、3へ戻す解決は誤りです」と三つの状態を並べて説明できます。単に ours や theirs を選ぶのではなく、その結果どのファイル値になるかを確認します。特に rebase 中のラベルは操作の文脈を踏まえて読む必要があります。
M のあとには L を追加し、notes.txt に文書を入れました。取り消しの依頼は「M の変更を戻し、あとから作った L の文書は残す」です。ここからは M を共有したと仮定して、既存の履歴を残す方法を選びます。練習中に実際の共有リモートを操作したわけではありません。
共有済みの merge は、第1親を確認して revert する
公式仕様: git-revert は過去の変更を打ち消す新しいコミットを記録します。merge の場合はどの親を mainline とするかを -m で指定します。-m 1 の1は取り消すコミット数ではなく、第1親を意味します。
今回の M の親は [B, F2] なので、L の上で M を第1親 B に対して revert しました。実行した操作の形は git revert --no-edit -m 1 <Mの実際のID> です。山括弧部分は説明用で、そのまま打つ文字ではありません。実際の親と ID を確認して置き換えます。
A --- B ---------- M --- L --- R
\ /
F1 --- F2 ------
新しい R は L を親に持ち、retry=1、timeout=5でした。notes.txt の文書も残っています。M の B に対する変更は retry の1→2と timeout の5→3なので、その逆を適用した結果です。「全ファイルを A に戻した」わけではありません。
| 状態 | retry | timeout | L の文書 |
|---|---|---|---|
| M:誤った解決 | 2 | 3 | まだない |
| L:文書を追加 | 2 | 3 | ある |
| R:M を第1親に対して revert | 1 | 5 | 残る |
| D:確認済みの修正を追加 | 2 | 5 | 残る |
この値は、本稿の独立したファイル構成で観測したものです。後続コミットが M と同じ行を変更している場合、revert でも競合や意味上の調整が起こり得ます。「revert なら必ず後続変更を自動で保てる」とは言わず、対象の差分と結果を確認しましょう。

revert 後に同じ feature を merge しても戻らない理由
R の上で同じ feature を merge した結果は Already up to date. でした。新しいコミットは作られず、retry は1のままです。「変更を取り消したなら、もう一度取り込めば retry=2になる」と予測すると、この結果を説明できません。
R は M を履歴から消していません。F2 は M の親なので、R の祖先でもあります。実行では git merge-base --is-ancestor を使い、M と F2 の両方が R の祖先であることを確認しました。ファイルの内容を打ち消すコミットを追加したことと、以前の統合を履歴から消すことは違います。
公式仕様: git-revert は、merge の revert が後続 merge の取り込みに影響することも説明しています。以前の merge の祖先に含まれる変更が、同じブランチを指定するだけで再び取り込まれるわけではありません。本稿の「同じ F2」の再 merge は、その狭いケースを実行したものです。
今回は R の上に修正コミット D を追加し、retry を2、timeout を5へそろえました。R を祖先として保持し、文書も維持しています。revert 自体をさらに revert する方法なども考えられますが、誤った解決まで復活させる可能性を含め、望む差分を先に定義する必要があります。本稿で実行した修正経路は D の追加です。
「どのコマンドが最短か」より、「必要な変更が何で、過去のどの判断を保持するか」を説明しましょう。再取り込みの方式は、修正済みブランチの状態やチームの履歴方針によって決まります。
ローカルの reset 後は、まず復旧用の参照を作る
別の未共有ブランチ private-work を D から作り、local.txt を追加して X にコミットしました。クリーンな練習用作業ツリーで git reset --hard HEAD~1 を実行し、そのブランチの先端を D へ戻します。main の先端は D のまま変わりません。
公式仕様: git-reset の --hard は、先端だけでなくインデックスと作業ツリーにも影響します。未コミットの変更を保持するための操作ではありません。本稿の目的は、すでにコミットした X がブランチ先端から外れた場合の復旧を確かめることです。
実行では git reflog show --format=%H private-work の記録から X の ID を確認し、git branch rescue-local <Xの実際のID> で復旧用の参照を作りました。rescue-local:local.txt の内容を照合して、X のコミット済みファイルが読めることを確認しています。
この方法では現在のブランチをすぐ X へ移動する必要がありません。まず参照で残し、差分やファイルを確認したあと、必要なブランチへどう取り込むかを判断できます。reflog の番号を決め打ちせず、X の ID と local.txt の内容を確認してから復旧用の参照を付けましょう。別の操作が入れば、番号の意味も変わります。
公式仕様: git-reflog はローカルな参照の更新履歴で、記録には有効期限や削除の条件があります。期限切れを考慮したバックアップの代わりにはならず、未記録ファイルを今回の方法で復元できるという証拠もありません。本稿は期限切れのオブジェクト、壊れたリポジトリ、他人の clone の reflog からの復旧を確認していません。
detached HEAD も別に確認しました。X を直接チェックアウトすると HEAD はブランチ名を持ちませんでした。コミットを残す目的なら、残したいコミットへブランチを付け、あとから見つけられる状態にする方針を説明します。「detached だからすでに作業が失われた」という意味ではありません。
33 の確認を、コマンド成功と内容確認に分ける
ローカルの再現スクリプトでは33項目を確認しました。競合が意図どおり起きること、merge abort 後の状態、M の親の順序、誤った timeout、R の値と文書の維持、再 merge の無変更、rebase 後の新しい ID と親、reflog を使った X の保全などです。
この件数は、33種類の本番障害を解決したという意味ではありません。一つの小さな履歴を、終了コード・参照・祖先関係・ファイル内容から照合した数です。Git が終了コード0を返したことだけを合格条件にしていません。逆に、競合を期待する操作では終了コード1と競合ファイルを確認しています。
再現では Git のユーザー名・メールを練習用の値に設定し、グローバル設定を読み込まず、固定日時を使いました。出力のコミット ID はこの条件の値で、作者情報や日時が違えば変わります。読者が自分の ID を使っても、親の関係とファイル値が一致すれば同じ検証ができます。
この例は、サブモジュール、署名、保護ブランチ、Git LFS、force push の競合、CI の実行を検証していません。面接でそれらを追加条件として渡されたら、ローカルのグラフが正しいことに加え、公開手順や権限の確認が必要だと説明します。
五つの問題で、履歴と共同作業の説明を練習する
PracHub の次の問題は、Git の操作と周辺の開発判断を練習する入口です。すべてが本稿の merge revert を直接問うわけではありません。特にマージ順序の問題は計算問題、バックエンド基礎の問題は複数分野を含むため、目的を絞って使います。
| PracHub の問題 | 練習する観点 | 本稿との境界 |
|---|---|---|
| Demonstrate Git and build workflow | ブランチ、変更、確認を一連の説明にする | 本稿はリモート push や build を実行していない |
| Define a Git workflow for CI | 共有・レビュー・CI の方針を決める | ローカルの Git 仕様だけではチーム方針は決まらない |
| Minimize Branch Merge Conflicts | 与えられたモデルでマージ順序を考える | 計算問題の最適値と実リポジトリの競合解決は別 |
| Demonstrate software engineering fundamentals | Git を含む基礎を短く説明する | 周辺分野のすべてをこの練習で確認したわけではない |
| Explain Backend Fundamentals and AI Tooling | Git の使い分けを他の基礎と整理する | バックエンドや AI ツール全般の演習ではない |
次は Define a Git workflow for CI を開き、「誰が使う履歴か」「どの検証後に公開するか」「失敗時にどの参照へ戻れるか」の三点を加えて答えてください。履歴図とファイルの期待値を先に置くと、merge と rebase の好みだけで終わらない説明になります。
Sources and Further Reading
-
Git — git-merge:統合、fast-forward、競合、abort の条件。
-
Git — git-rebase:変更の再適用と共有履歴を書き換える影響。
-
Git — git-revert:新しい取り消しコミット、mainline、後続 merge への影響。
-
Git — git-reset:先端・インデックス・作業ツリーへの作用。
-
Git — git-reflog:ローカルの参照更新記録と期限。
-
Git — git-branch:復旧用ブランチをコミットに付ける操作。
-
Pro Git — Git Objects:コミットとスナップショットの構造。
Comments (0)