リベースとコンフリクトの解消
目標
リベースがコミットを移動するのではなく書き換えるという事実をハッシュで確認し、衝突の解決と中断、インタラクティブリベースのsquash/dropまで、手を動かして試します。出力物は/root/gitx3/の下に置きます。
なぜ重要なのか
コミットオブジェクトの中には、親のハッシュが入っています。親が変わると、コミットのハッシュは必ず変わります。この1つの文から、残りがすべて導かれます。他の人がそのコミットの上に作業を積んでいたなら、書き換えは、その人の履歴を壊します。そのため、黄金律の正確な形は、「すでにプッシュしていたらやってはいけない」ではなく、「他の人の作業がその上に積まれているコミットは、書き換えない」です。衝突が繰り返される理由も、同じ構造から来ています。マージは3つの地点だけを比較しますが、リベースはコミットを1つずつ再適用し、そのたびに3-wayマージを行うので、リベースは1回のマージではなく、N回のマージです。
ステップ
グローバルなgitの作成者情報がありません。新しいリポジトリごとに、ローカルのuser.email / user.nameを設定してください。
/root/gitx3/repoにリポジトリを作成します。mainにコミットを3つ積み、そこからtopicブランチを作ってコミットを2つ追加したあと、再びmainに戻ってコミットを1つさらに積み、分岐した状態を作ります(topicの総コミット数は4つ以上)。そして、/root/gitx3/diverged.txtに3行を書きます:main_ahead=1、topic_ahead=2、merge_base=<두 브랜치의 공통 조상 해시>(プレースホルダーは2つのブランチの共通祖先のハッシュです)。topicをmainの上にリベースします。リベースの前に、topicの先頭のハッシュを記録しておき、/root/gitx3/rebased.txtに、before=<리베이스 전 해시>とafter=<리베이스 후 해시>の2行を書きます(プレースホルダーは、リベース前のハッシュと、リベース後のハッシュです)。2つの値は互いに異なる必要があり、afterは現在のtopicの先頭と一致する必要があり、リベースが進行中の状態で残っていてはいけません。mainでtopicを早送りでマージします。結果:mainのマージコミットは0個、mainの総コミット数は6つ以上、mainとtopicが同じコミットを指している必要があります。/root/gitx3/conflictに新しいリポジトリを作成します。mainにshared.txtを置き、conflict-topicブランチでそのファイルを修正して、from-topicという文字列が入るようにコミットします。次に、mainでも同じ箇所を修正して、from-mainが入るようにコミットします。これで、conflict-topicをmainの上にリベースすると、衝突が起きます。2つの変更を両方とも残すように解決してください。完了条件: リベースが終わっていること、インデックスに未解決のパスがないこと、<<<<<<<のような衝突マーカーがファイルに残っていないこと、shared.txtにfrom-mainとfrom-topicが両方あること、そして、ワーキングツリーがきれいであることです。/root/gitx3/abortに新しいリポジトリを作成し、riskyブランチとmainが、同じファイルを互いに異なる内容に修正するようにコミットします。リベースを始める前に、riskyの先頭のハッシュを/root/gitx3/abort-before.txtに保存します(ハッシュの文字列だけ)。そのあと、リベースを開始して衝突に遭遇し、中断します。完了条件: リベースが進行中でないこと、riskyの先頭が記録しておいたハッシュと完全に同じであること、reflogにrebaseの記録があることです。/root/gitx3/squashに新しいリポジトリを作成し、feature/threeブランチにコミットを3つ積みます(それぞれs1.txt、s2.txt、s3.txtを追加)。インタラクティブリベースで、3つを1つにまとめます。完了条件:main..feature/threeのコミット数が1つであること、そのコミットのタイトルにfeat: combinedが含まれること、s1.txt/s2.txt/s3.txtが3つとも存在することです。/root/gitx3/dropに新しいリポジトリを作成し、feature/fourブランチにコミットを4つ積みます:d1.txt、d2.txt、そして、タイトルがdebug: temp logで、debug.txtを追加するコミット、最後にd3.txtです。インタラクティブリベースで、デバッグ用のコミットだけを捨てます。完了条件:main..feature/fourが3つであること、debug: temp logのコミットがないこと、debug.txtがないこと、d1/d2/d3.txtがすべて存在することです。/root/gitx3/rebase.mdを書きます。必ず含めるもの:--force-with-leaseの説明と、--forceとの違い、どんなときにリベースしてはいけないか(韓国語で「共有」「プッシュ」「他の人」のいずれかを意味する語を含めてください)、なぜハッシュが変わるのか(韓国語で「ハッシュ」「書き換え」のいずれかを意味する語を含めてください)、そして、merge_commits_on_main=の行に、/root/gitx3/repoのmainのマージコミットの数を、実際に数えて書きます。
参考
- 先行しているコミット数の数え方:
git rev-list --count topic..main(mainが先行している数)、git rev-list --count main..topic(topicが先行している数)、共通祖先はgit merge-base main topicです。 - エディターがない環境でのインタラクティブリベース: リストは
GIT_SEQUENCE_EDITOR='sed -i "2,3s/^pick/fixup/"' git rebase -i mainのように、コミットメッセージの編集はGIT_EDITOR=trueで回避できます。squashの代わりにfixupでまとめたあと、git commit --amend -m "feat: combined"でタイトルを決めても構いません。 - dropは、リストの該当行の
pickをdropに変えるか、行を削除すれば済みます。 - 衝突の解決後は、
git add <파일>のあとにgit rebase --continueです(プレースホルダーはファイル名です)。エディターが開く場合は、先頭にGIT_EDITOR=trueを付けてください。 - よくある間違い1: 衝突を
--oursや--theirsで片側だけ選んで済ませてしまうことです。相手の作業が静かに消えます。しかも、リベース中は、再生されるコミット側がtheirsなので、意味が直感と逆に感じられます。 - よくある間違い2: ステップ2で、先にリベースをしてから
beforeのハッシュを探そうとすることです。リベース後はgit reflogを見る必要があるので、開始前にgit rev-parse topicを先に保存しておいてください。
分岐した2つのブランチを作る
/root/gitx3/repoにリポジトリを作成します。mainにコミットを3つ積み、そこからtopicブランチを作ってコミットを2つ追加したあと、再びmainに戻ってコミットを1つさらに積み、分岐した状態を作ります(topicの総コミット数は4つ以上)。そして、/root/gitx3/diverged.txtに3行を書きます: main_ahead=1、topic_ahead=2、merge_base=<두 브랜치의 공통 조상 해시>(プレースホルダーは2つのブランチの共通祖先のハッシュです)。
/root/gitx3/repoに、mainとtopicが互いに異なるコミットを持つように作ります。分岐のあと、両側にそれぞれコミットがあってはじめて「分岐した」状態です。先行している数は、git rev-list --count A..Bで数えられます。
topicをmainの上に書き換える
topicをmainの上にリベースします。リベースの前に、topicの先頭のハッシュを記録しておき、/root/gitx3/rebased.txtに、before=<리베이스 전 해시>とafter=<리베이스 후 해시>の2行を書きます(プレースホルダーは、リベース前のハッシュと、リベース後のハッシュです)。2つの値は互いに異なる必要があり、afterは現在のtopicの先頭と一致する必要があり、リベースが進行中の状態で残っていてはいけません。
リベースの前に、topicの先頭のハッシュを先に記録しておくと、比較できます。リベースが終わると、共通祖先がmainの先頭と同じになり、親が変わったため、ハッシュも変わります。
早送りで一直線にする
mainでtopicを早送りでマージします。結果: mainのマージコミットは0個、mainの総コミット数は6つ以上、mainとtopicが同じコミットを指している必要があります。
リベースのあとは、mainがtopicの祖先になるため、マージコミットなしでマージされます。--ff-onlyを使うと、条件が合わないときに拒否してくれるので安全です。
衝突を両方保存して解決する
/root/gitx3/conflictに新しいリポジトリを作成します。mainにshared.txtを置き、conflict-topicブランチでそのファイルを修正して、from-topicという文字列が入るようにコミットします。次に、mainでも同じ箇所を修正して、from-mainが入るようにコミットします。これで、conflict-topicをmainの上にリベースすると、衝突が起きます。2つの変更を両方とも残すように解決してください。完了条件: リベースが終わっていること、インデックスに未解決のパスがないこと、<<<<<<<のような衝突マーカーがファイルに残っていないこと、shared.txtにfrom-mainとfrom-topicが両方あること、そして、ワーキングツリーがきれいであることです。
/root/gitx3/conflictで、同じファイルの同じ箇所を両側が変更するように作ってから、リベースします。解決は、片側を選ぶことではなく、2つの変更を両方残すことで、衝突マーカーまで消して、はじめて終わりです。
リベースを中断して元の状態に戻す
/root/gitx3/abortに新しいリポジトリを作成し、riskyブランチとmainが、同じファイルを互いに異なる内容に修正するようにコミットします。リベースを始める前に、riskyの先頭のハッシュを/root/gitx3/abort-before.txtに保存します(ハッシュの文字列だけ)。そのあと、リベースを開始して衝突に遭遇し、中断します。完了条件: リベースが進行中でないこと、riskyの先頭が記録しておいたハッシュと完全に同じであること、reflogにrebaseの記録があることです。
/root/gitx3/abortで、開始前のブランチの先頭のハッシュをファイルに書いておき、衝突が起きるリベースを開始してから、中断します。中断は、開始前の状態に正確に戻します。
コミット3つを1つにまとめる
/root/gitx3/squashに新しいリポジトリを作成し、feature/threeブランチにコミットを3つ積みます(それぞれs1.txt、s2.txt、s3.txtを追加)。インタラクティブリベースで、3つを1つにまとめます。完了条件: main..feature/threeのコミット数が1つであること、そのコミットのタイトルにfeat: combinedが含まれること、s1.txt/s2.txt/s3.txtが3つとも存在することです。
/root/gitx3/squashのfeature/threeを、mainを基準にインタラクティブリベースします。まとめても、変更内容は捨てられないため、3つのファイルがすべて残る必要があります。エディターを開くのが難しければ、GIT_SEQUENCE_EDITORでリストを変更できます。
コミットを1つ捨てる
/root/gitx3/dropに新しいリポジトリを作成し、feature/fourブランチにコミットを4つ積みます: d1.txt、d2.txt、そして、タイトルがdebug: temp logで、debug.txtを追加するコミット、最後にd3.txtです。インタラクティブリベースで、デバッグ用のコミットだけを捨てます。完了条件: main..feature/fourが3つであること、debug: temp logのコミットがないこと、debug.txtがないこと、d1/d2/d3.txtがすべて存在することです。
/root/gitx3/dropのfeature/fourで、デバッグ用のコミットだけをなくします。コミットをdropすると、そのコミットが追加したファイルも一緒に消えます。残りの3つのコミットのファイルは、残っている必要があります。
黄金律をまとめる
/root/gitx3/rebase.mdを書きます。必ず含めるもの: --force-with-leaseの説明と、--forceとの違い、どんなときにリベースしてはいけないか(韓国語で「共有」「プッシュ」「他の人」のいずれかを意味する語を含めてください)、なぜハッシュが変わるのか(韓国語で「ハッシュ」「書き換え」のいずれかを意味する語を含めてください)、そして、merge_commits_on_main=の行に、/root/gitx3/repoのmainのマージコミットの数を、実際に数えて書きます。
/root/gitx3/rebase.mdに、--force-with-leaseと--forceの違い、どんなときにリベースしてはいけないか、なぜハッシュが変わるかを書き、merge_commits_on_main=の行に、ステップ3の結果を実際に数えて書きます。