webコーダーのsaco @sacocco_sacoya です。
もりけん塾で、gitコンフリクト解消ができるようになるハンズオンに挑戦しました。
このハンズオンで学んだことのメモを記します。
リベースとは
まずリベースが何かがわからなかったため確認しました。
リベースとは、
「指定したコミットを、ブランチを変えて作り直したり、ひとまとめにしたりして、ログを綺麗にするコマンド」〜(略)〜
「指定コミットを作り直して、ログを掃除するためのコマンド」
https://www.sejuku.net/blog/71919
ログを掃除をする、とか綺麗にするためのもの、といったことが伺えました。
chatGPTからの返答では、
リベース
はブランチの履歴を変更するため、公開されているブランチや他の人が作業しているブランチに対して行うと問題を引き起こす可能性があることに注意が必要です。
個人的なブランチやまだプッシュしていないブランチに対して使用する場合が最も適しています。
とのことです。
公開済みのブランチをリベースすることは危険ということも伺えます。
リベースの使い方
リベースには2種類の使い方がある。
- 別々のブランチで伸ばしていた開発コミットを繋げ直す
- 複数のコミットを1コミットにまとめる
という使い方。
①別々のブランチで伸ばしていた開発コミットを繋げ直したいとき
git rebase [つなぐ元にするブランチ or コミットID]
②複数のコミットを1コミットにまとめたいとき
git rebase -i [ひとまとめにする地点の1つ前のコミットID]
-iはinteractiveの略で、相互作用、という意味。
もりけんさんからのアドバイス
リベースというのは、その名の通り、どのブランチをベースにしてその後のコミットを積んでいくかを決める作業。
リベース時にコンフリクトが出るのはよくあること。
1つコンフリクトを解消しても、さらにコンフリクトが起こることもあるので、都度コンフリクトを解消していく必要がある。
各ブランチのコミットで同じファイルを編集している場合はコンフリクトが起きる。
マージとは
あるブランチに対して、別のブランチで行われた変更を取り込む(統合する)ための機能。
マージ時には、マージコミットが作成される。
マージはプルによっても暗黙的に実行される。
↓使用手順の例
- マージしたいブランチに移動
- 修正用ブランチを作成
- 修正用ブランチをマージ、プッシュ
コンフリクトが発生した場合、手動でコンフリクトを解消し、コミットを完了する必要がある。
マージでのコンフリクトの比較内容は、HEADと指定したブランチの最新のコミット。
コンフリクトとは
「衝突」を意味する英単語であり、Gitではブランチをマージする際に変更点が重なってしまうことを指す。
コンフリクトは、同じファイルの同じ行同士が、別ブランチのコミットで変更されている場合に生じる。
コンフリクトが生じると、マージは失敗する。
マージに失敗した対象ファイルは、開発者からのコンフリクトの解消またはキャンセルを待っている状態となる。
コンフリクトした内容を、どちらで採用するかをGitに教えてあげるのがコンフリクト解消。
コンフリクトの挙動確認
コンフリクトの挙動を確認するため、まずmainブランチでindex.htmlを作成し、以下のコードを記述してプッシュしました。
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Document</title>
</head>
<body>
</body>
</html>
次に git checkout feature/aとして新たにブランチを作成し、index.htmlに以下のコードを記述してコミットしました
mainブランチと違う点として、<body>直下にコメントを追加しています。
(別ブランチで同ファイル同行に差分を発生させるため)
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Document</title>
</head>
<body>
<!-- 追加部分 -->
これはfeature/aで作られました
<main>
</main>
</body>
</html>
ここまででコンフリクト発生の土台が出来ました。
このまま(feature/aのまま)git merge mainとコマンドを入力してマージを試みます。
すると以下のようにコンフリクトが発生しました。
Auto-merging index.html
CONFLICT (add/add): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
ファイルを確認すると以下のようになっています。
<<<<<<< HEADという記述で始まっている、緑色で囲まれた箇所がmerge先の変更(現在checkoutしているブランチでの変更)。
>>>>>>> main(ブランチ名)という記述で始まっている、青色の箇所がmerge元の変更となります。
それぞれの=======がここまで、の区切り。
コンフリクトの解消方法
コンフリクトが発生したら、「現在チェックアウトしているブランチでの変更」を取り込むか、「merge元の変更」を取り込むかを選択する必要があります。
コマンドで<<<<<<< HEAD部分を反映する方法
git checkout --ours index.html
ours = 私達の。現在チェックアウトしているブランチでの変更を取り込む。
コマンドで>>>>>>>(merge元ブランチ名)を反映する方法
git checkout --theirs
theirs = 彼らの。merge元のブランチでの変更を取り込む。
コマンドで解決せずとも、手作業で調整することも可能。
手作業で調整する場合は、>>>>>>>や=======の記述も手作業で消してあげる必要がある。
コンフリクト解消後の確認
コンフリクトしていた内容の、<<<<<<< HEAD側(現在チェックアウトしているブランチ)の内容を反映させました。
git statusで現状を確認すると、以下のようになっています。
Unmarged path
コンフリクトが発生した箇所が列挙されている。
both added
Gitで発生するマージコンフリクトの一種。
2つのブランチが同一のベースから分岐して以降、それぞれのブランチで新しいファイルが同じ場所に追加された時にboth addedという状況が発生する。
both = 両方という意味
ここでgit addし、git statusで状況を確認します。
All conflicts〜という部分は、「すべてのコンフリクトは解消したが、マージが完了していません」とメッセージが表示されています。
このメッセージは、過去にマージしようとしたことをGitが記録から判別して表示してくれるもののようです。
use “git”〜の部分は、「マージを完了するには「git commit」を行う」という意味です。
マージを試みようとしたブランチでコンフリクトが解消されたあとは、コミットを行うだけでコミットされた内容をマージしてくれるようです。
(本来は自動で作成されるマージコミットを手動で行っている)
Changes to be committed〜の部分は、コミットされる変更のあったファイルを示しています。
このあと、git commit -m “fix: conflict”としてコミットを行いました。
git logで履歴を確認すると、以下のようになっています。
このログの詳細は、上から
- コミットID(ハッシュ) / 現在の作業ディレクトリ((HEAD -> feature/a))
(HEAD は自分が今作業している場所を示すポインタ) - マージの親コミットとして2つのコミットが示されている
- 日付
という情報が確認できます。
コンフリクトを解消し、マージコミットを手動で作成できたので、git push origin headとコマンドを入力し、作成したfeature/aブランチをリモートリポジトリに反映させます。
※ git push origin headは現在作業中のブランチ(head)の最新のコミットをoriginというリモートリポジトリに反映させること。
プッシュをすると、プルリクエストを作成するためのURLを教えてくれるので、ここにアクセスすることで、プルリクエストを作成することも可能になります。
mergeでコンフリクトを解決する
再度mergeでコンフリクト解消する挙動確認をするため、git checkout mainとし、index.htmlを以下のように書き換え、git add→git commit→git push origin headとコマンドを入力し、mainブランチを進めました。
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Document</title>
</head>
<body>
<div>main変更箇所</div>
</body>
</html>
そして再度feature/aブランチに戻ります(git checkout –で1つ前にcheckoutしていたブランチに戻る)。
feature/aブランチは、mainで変更があったことをまだ知りません。
feature/aブランチで以下のようにindex.htmlのコードを書き換え、git add→git commit→git push origin headとコマンドを入力します。
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Document</title>
</head>
<body>
<!-- 書き換え部分 -->
<p>feature/aの修正</p>
</body>
</html>
ここでgithubのfeature/aブランチのプルリクエスト画面を確認すると、
「This branch has conflicts that must be resolved」このブランチには解決する必要のある競合が存在します、と表示されています。
この状態を解決するには、ローカルでmainブランチを最新に(git pull)したあと、プルリクエストを出しているブランチにcheckoutし、git merge main (もしくはgit rebase main) で変更を取り込み、前述の通り解決してpushし直します。
※ 今回の例では、自分自身のローカルブランチであるmainでリモートのmainを更新したのでpullの必要がないが、現場ではpullが必要となる。
git rebaseでコンフリクトを解決する方法
先ほどはgit mergeでコンフリクトを解消しましたが、コンフリクトはrebaseでも発生する場合があります。
rebaseでコンフリクトを解消する方法を確認するため、feature/aブランチに居る状態で git rebase mainとコマンドを入力しました。
すると以下のようにコンフリクトが発生します。
コンフリクトが発生したことが判明した状態で、ファイルを確認すると↓の状態になっています。
HEAD(現在の変更)がmainブランチ、0299e8e (feat: add index.html)が入力側のブランチのコミットであることに注目します。
mergeでコンフリクトを解決した際↓は、HEADがfeature/aとなっており、mainが入力側の変更でした。
rebase中は内部的にHEADがmainを指すことがありますが、リベースが完了するとHEADは再びfeature/aブランチへ戻ります。
rebaseの挙動確認
下記のようなブランチの状態があるとします。
developブランチに居る状態でgit rebase mainを行うと、developブランチでのコミットがmainブランチの先頭に移動します。
mainブランチの先にfeature/aブランチでのコミットが追加され、
その後、mainブランチのHEADと、featureブランチのそれぞれのコミットで差分の比較が行われていきます。
mainブランチとfeatureブランチで同じファイルの同じ部分を変更している場合、コンフリクトが発生する可能性があり、都度手動でコンフリクトを解消する必要があります。
例えば上記の例でmainブランチのHEAD(add1)とadd2の比較でコンフリクトが発生し、それが解消したら、次はmainブランチのHEAD(add1)とadd3との比較が行われるといった流れです。
すべての比較が完了したらrebaseが完了し、ローカルのfeature/aブランチが更新されている状態となります。
コンフリクトしている内容の両方の状態を取り入れる方法
mainでの変更を取り込みつつ、feature/aブランチでのコミットも活かすには、すべて手動で解決していきます。
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Document</title>
</head>
<body>
<main>
<div>main変更箇所</div>
これはfeature/aで作られました
<p>feature/aの修正</p>
</main>
</body>
</html>
>>>>>HEADなどの記述を手動で消去して、上記のコードとなるように調整しました。
このあと、忘れずにgit add .をします。
その後、git rebase –continueでrebaseを前に進めます。
git rebase –continueを行うと以下の様にエディタが立ち上がります。
この内容で問題なければ :wq! を入力し、すべてのコミット比較が終了したことになります。
rebase –continueを終えたあとは、強制的にpushを行う必要があります。
rebaseを終えたブランチの強制push
rebaseした後は、変更をpushしてリモートブランチを上書きします。
rebaseしたものをリモートにpushする際は必ず強制pushをする必要がありますが、該当のリモートブランチで他の人が変更をpushしていたらすべて上書きされてしまいます。
このような誤った上書きを避けるために使うのが、git –force-with-leaseです。
git –force / git –force-with-lease とは
force=力。git –forceは、リモートリポジトリに対してローカルのブランチの現在の状態を強制的に反映するために使われるコマンド。
ローカルのブランチ履歴がリモートのそれと異なる場合(リベースや履歴の書き換え後など)、通常のプッシュでは拒否されるような状況でもリモートの履歴を上書きしてしまう。
フォースプッシュは他の開発者のコミットを強制的に上書きしてしまう危険性があるため、通常はチーム全体での確認が必要となる。
そのため、安全な手段として git –force-with-leaseというコマンドでの対処が取られる場合が多い。
git –force-with-lease 安全なpush
git –force-with-leaseは–forceよりも安全なコマンドで、ローカルの情報が最新である(自分が最後にフェッチまたはプルしたあとに他の誰かがリモートブランチに変更をプッシュしていない)ことを確認した上でのみ(このコマンドで確認してくれる)、強制プッシュが許可される。
もし他の開発者が新しくコミットをプッシュしていた場合は、プッシュが拒否される。
安全なpushを行うため、git –force-with-leaseコマンドを使用してpushしました。
(git push --force-with-lease origin head
)
最後の行にforced updateとあり、強制更新されたことがわかります。
ここでgithubを確認すると、以下のようになっていました。
コンフリクトが解消されて、mergeできる状態になりました。
以上がrebase時のコンフリクト解消の挙動確認です。
まとめ・感想
- リベースとは、指定のコミットをあるブランチにつなぎ直したり、コミットをひとまとめにすることでコミット履歴を綺麗にできるもの
- マージとは、あるブランチに対して、別のブランチで行われた変更を取り込む(統合する)ための機能。
- コンフリクトとは、マージ時やリベース時などに、同じファイルの同じ行同士が、別ブランチのコミットで変更されている場合に生じる。
- コンフリクトが生じるとマージやリベースは失敗するので、どちらのコードが正しいかをgitに教えてあげる必要がある。
- rebaseで発生したコンフリクトを解消した後はgit addをし、git rebase –continueをする
- rebaseが成功した後はgit –force-with-leaseでrebaseしたブランチを安全にpushする。
今回はgitコンフリクト解消ができるようになるハンズオンに挑戦しました。
このハンズオンでrebase、merge、conflictの挙動を確認することができました。
rebaseの理解がとくに難しかったですが、gitが提示してくれるメッセージをひとつずつ読み解いたり、色々と記事を読んでいくことで理解していくことができました。
もりけん@terrace_tech さんと塾生のはせがわさんがrebaseの挙動の確認方法を教えてくださり、大変勉強になりました。ありがとうございました。
いつか業務に活かせるタイミングが来れば良いなと思いました。
本日は以上です。