Git ブランチ マージ リベース
Git のブランチの作成や切り替え、マージやリベースの使い方についての覚書です。
関連ページ
- Git の基本的な使い方
- Git リセット git reset コマンド
- Git スタッシュ(stash) 一時的に変更を退避
- Git hunk 変更の一部をステージしてコミット git add -p
- VS Code で Git を使う(ソース管理機能)
- Git の初期設定と更新(Mac)
作成日:2022年8月26日
ブランチ(branch)とは
branch には「枝」や「支流」などの意味がありますが、Git のブランチは、プロジェクトの異なるバージョンや変更を管理するための仕組みのことで、コミット履歴の流れを枝分かれさせるための機能です。
ブランチを使うことで、複数の作業を同時に進めたり、新機能の開発やバグ修正を別々の場所で行ったりすることができます。
Git ではリポジトリを作成(初期化)した際、自動的に main または master といったデフォルトのブランチが設定されます(バージョンによって名前が異なる場合があります)。
このデフォルトのブランチには最初のコミットが含まれ、ブランチを切り替えるまでコミットはデフォルトのブランチに追加されていきます。
新しいブランチを作成する際には、デフォルトのブランチから分岐する形で作成するのが一般的で、必要に応じて任意の数のブランチを作成することができます。
main ブランチ(デフォルトのブランチ名)
以前は、デフォルトのブランチとして master ブランチが自動的に作成されました。しかし、近年の反人種差別運動を受けて、これに関連して "master" という用語の使用が問題視されるようになり、多くのプロジェクトでデフォルトブランチの名前を main や他のものに変更する動きが広がっています。
以下ではデフォルトのブランチを main としています。
デフォルトのブランチは git config コマンドで設定できます(関連:デフォルトのブランチ名を変更)。
ブランチは単なるポインタ
実際にはブランチは特定のコミット(オブジェクト)を指す単なるポインタ(参照)です。
各コミットはそれぞれ一意のハッシュ値を持っていて、前のコミット(親のコミット)のハッシュ値を参照することでコミット同士は繋がっています。
ブランチは、このコミットのハッシュ値を指し示すポインタとして機能します。
特定のブランチが指すコミットは、そのブランチに関連付けられている最新のコミットです。
また、実際のブランチは一つの特定のコミットを指しますが、概念としてのブランチはそのブランチの指すコミットが繋がっている一連のコミットを指します。
最初にコミットした時点で、そのコミットを指す main ブランチが作られ、そのコミットがその main ブランチに関連付けられた最新のコミットとなります。
新しいコミットが行われると、そのコミットが main ブランチに関連付けられ、ブランチのポインタが更新されます(ポインタは最新のコミットを指すように自動的に進んでいきます)。
コミットオブジェクト
コミットオブジェクトはコミットすると作成されるオブジェクトです(上記図の丸の部分)。
コミットオブジェクトには tree オブジェクトへのポインタやそのコミットの直接の親となるコミットへのポインタ、コミットメッセージなどのメタデータが含まれています。
コミットオブジェクトの内容は git cat-file -p コマンドで確認することができます。
以下は上記図の3つ目(右端)のコミットの内容を確認する例ですが、parent には直接の親となる2つ目のコミットへのポインタ(ハッシュ値)が書かれています。
% git cat-file -p b1acd1b tree 8759e1f7cb2183c13789eeccbe2e0d31ab65263a parent 65955773eb9f32f979d0a7cfc32051d96766bf6c // 2つ目のコミットのハッシュ値 author foo <foo@example.com> 1691721109 +0900 committer foo <foo@example.com> 1691721109 +0900
以下は2つ目のコミットです。parent には直接の親となる最初のコミットへのポインタが書かれています。
% git cat-file -p 6595577 tree e7a5aa673c587da7c0362d43e0d73b203537ef67 parent 2931804dad879ad2dc24006787b9e399ba63f4fc // 最初のコミットのハッシュ値 author foo <foo@example.com> 1691717547 +0900 committer foo <foo@example.com> 1691717547 +0900
以下は最初のコミットオブジェクトの例です。最初のコミットには親がないので、parent はありません。
% git cat-file -p 2931804 tree 09a13b897d3d0f528d487c704da540cb952d7606 author foo <foo@example.com> 1691716179 +0900 committer foo <foo@example.com> 1691716179 +0900
tree はディレクトリの中身の一覧とどのファイルがどの blob オブジェクトに対応するかを表す tree オブジェクトへのポインタです。
HEAD
HEAD は現在作業中のブランチの最新のコミットを指し示すポインタです。
HEAD は「現在地」を示すポインタと考えることができます。新しいコミットが行われると、HEAD はその新しいコミットを指すように更新されます。
言い換えれば、どのコミットが現在の作業ツリー(プロジェクトのファイルやディレクトリ)に反映されているかを指し示します。
Git は初期化の際にデフォルトで main ブランチを作成し、HEAD は main を指します。
ブランチを切り替えると、HEAD もそれに合わせて移動します。つまり、異なるブランチに切り替えることで、HEAD が指すブランチが変わり、そのブランチが指すコミットも変わります。
以下は HEAD がどのようなものかを確認する例です。
まず、Git で管理する my-project という名前の空のプロジェクトを作成します。
% git init my-project // Git で管理するディレクトリ(プロジェクト)を作成 Initialized empty Git repository in /.../my-project/.git/ % cd my-project //作成したプロジェクトのディレクトリに移動
初期化直後の .git ディレクトリの中身を確認すると、HEAD というファイル(.git/HEAD)があります。
内容を確認すると ref: refs/heads/main と書かれていますが、refs/heads/ ディレクトリはこの時点では空で、refs/heads/main というファイルはまだ存在しません。
% tree .git //.git ディレクトリの中身をツリー表示 .git ├── HEAD ├── config ├── description ├── hooks │ ├ 中略 │ └── update.sample ├── info │ └── exclude ├── objects │ ├── info │ └── pack └── refs ├── heads // この時点では空 └── tags % cat .git/HEAD // HEAD の内容を表示 ref: refs/heads/main
初期化直後はまだコミットがないので main ブランチは何も参照していない状態です。
新規にファイル foo.txt を作成してコミットし、コミット履歴を確認すると以下のように表示されます。
git log コマンドのレスポンスの HEAD -> main は HEAD が main を指していること意味します。
% touch foo.txt % git add foo.txt % git commit -m "first commit" [main (root-commit) 2931804] first commit 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 foo.txt % git log // コミット履歴を確認 commit 2931804dad879ad2dc24006787b9e399ba63f4fc (HEAD -> main) Author: foo <foo@example.com> Date: Fri Aug 11 10:09:39 2023 +0900 first commit
.git/HEAD ファイルを確認すると先程と同じ参照先(ref: refs/heads/main)が記述されています。
最初のコミットを実行すると、.git/refs/heads ディレクトリに main という名前のファイル(.git/HEAD に記述されている参照先ファイル)が追加されます。このファイルにはハッシュ値が書かれていて、git log の出力の直近のコミットのハッシュ値と同じであることが確認できます。
% cat .git/HEAD ref: refs/heads/main % tree .git .git ├── HEAD ├── ・・・中略・・・ └── refs ├── heads │ └── main // 追加される └── tags % cat .git/refs/heads/main 2931804dad879ad2dc24006787b9e399ba63f4fc // コミットのハッシュ値
HEAD が main を指し、main がコミットを指しています。
続いて、foo.txt にテキストを追加して変更をコミットし、コミット履歴を確認します。
% echo hello git from foo! >> foo.txt % git commit -a -m "text added to foo.txt" // ステージングを省略してコミット [main 6595577] text added to foo.txt 1 file changed, 1 insertion(+) % git log // コミット履歴を確認 commit 65955773eb9f32f979d0a7cfc32051d96766bf6c (HEAD -> main) Author: foo <foo@example.com> Date: Fri Aug 11 10:32:27 2023 +0900 text added to foo.txt commit 2931804dad879ad2dc24006787b9e399ba63f4fc Author: foo <foo@example.com> Date: Fri Aug 11 10:09:39 2023 +0900 first commit
.git/HEAD ファイルには前回同様 .git/refs/heads/main と記述されていて、.git/refs/heads/main は直近の(2回目の)コミットのハッシュ値に更新されています。
% cat .git/HEAD ref: refs/heads/main % cat .git/refs/heads/main 65955773eb9f32f979d0a7cfc32051d96766bf6c
HEAD は main を指し、main が2回目のコミットを指します。
その後コミットを繰り返すたびに、HEAD とブランチは新しいコミットに自動的に移動していきます。
% echo extra text >> foo.txt % git commit -a -m "extra text added to foo.txt" [main b1acd1b] extra text added to foo.txt 1 file changed, 1 insertion(+) % git log --oneline b1acd1b (HEAD -> main) extra text added to foo.txt 6595577 text added to foo.txt 2931804 first commit
上記で確認した .git/HEAD ファイルや .git/refs/heads/xxxx ファイルは作業で使うことはないですが、以下のような関係になっています。
HEAD と .git/HEAD ファイル
HEAD は「現在位置」を示すポインタで、その参照先は .git/HEAD ファイルに記述されています。
.git/HEAD ファイルには以下のように記述されています。
- ブランチの場合:ref: refs/heads/ブランチ名(例: ref: refs/heads/main)
- コミットの場合:コミットのハッシュ値(例: 65955773eb9f32f979d0a7cfc32051d96766bf6c)
ブランチ と .git/refs/heads/xxxx ファイル
ブランチのポイント先(参照先)のコミットは 「.git/refs/heads/ブランチ名」というファイルに記述されています。
detached HEAD 状態
通常 HEAD はブランチを経由してコミットを指していますが、操作によっては HEAD が直接コミットを指す場合があり、この状態を detached HEAD 状態と呼びます。
detached は切り離されたという意味で、HEAD がブランチから切り離されいることを意味し、detached HEAD 状態は、HEAD がブランチではなく直接コミットを指している(ブランチがない)状態です。
detached HEAD は git checkout コマンドで特定のコミットをチェックアウトした場合やブランチが存在しない場合に発生します。
以下は1つ前のコミット(6595577)をチェックアウトする(取り出す)例で、その時点でのスナップショットが作業ディレクトリ(ワーキングツリー)にロードされます。
この場合、特定のコミットを指定してチェックアウトしたので HEAD はブランチではなく指定されたコミットを直接指すようになり、ブランチがない状態になります。
そのため detached HEAD 状態となり You are in 'detached HEAD' state...
というメッセージが表示されています(9行目から)。
% git log --oneline b1acd1b (HEAD -> main) extra text added to foo.txt 6595577 text added to foo.txt 2931804 first commit % git checkout 6595577 // 1つ前のコミットをチェックアウト Note: switching to '6595577'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by switching back to a branch. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -c with the switch command. Example: git switch -c <new-branch-name> Or undo this operation with: git switch - Turn off this advice by setting config variable advice.detachedHead to false HEAD is now at 6595577 text added to foo.txt
.git/HEAD は HEAD はブランチではなく1つ前のコミットを指し、 .git/refs/heads/main は直近のコミットをポイントしたままです。
% cat .git/HEAD 65955773eb9f32f979d0a7cfc32051d96766bf6c // HEAD はブランチではなくコミットをポイント % cat .git/refs/heads/main b1acd1b8c78adc0535b445a85b61571249026839 // ブランチは同じコミットに留まります
また、この状態でステータスを確認すると HEAD detached at xxxxx
と表示されます。
% git status HEAD detached at 6595577 nothing to commit, working tree clean
detached HEAD 状態は以下のように HEAD がブランチではなく直接コミットを指している状態です。
この状態では、プロジェクトの現在の状態に影響を与えることなくコードを閲覧したり、実験的な変更を行い、それをコミットすることができます。
また、この状態で行ったコミットは、通常のブランチに影響を与えずに破棄することもできます。つまり、これにより、ブランチに影響を与えることなく、作業内容を試すことができます。
この状態で行ったコミットを破棄して、開発を続けるには git switch -
を実行してプロジェクトの「現在の」状態に戻る(前のブランチに戻る)必要があります。
この例の場合は main ブランチにいたので git switch main
でも同じことです。
% git switch - // 前のブランチに戻る Previous HEAD position was 6595577 text added to foo.txt Switched to branch 'main'
この状態で作成したコミットを保持したい場合は、今チェックアウトしているコミット上で新しいブランチを作成することができます。
新しいブランチを作成するには、switch コマンドに -c オプションを使用してブランチ名を指定します。
git switch -c 新しいブランチ名
detached HEAD 状態では、一時的な変更や実験的な作業を行うことができますが、それらの変更をブランチに統合せずに保持したい場合は、新しいブランチを作成してコミットを保存することができます。
要約すると、detached HEAD 状態から抜けるには、新しいブランチを作成するか、既存のブランチに切り替える必要があります。
ブランチの作成
ブランチは git branch
コマンドを使って作成することができます。
git branch ブランチ名
例えば、現在、以下のようにデフォルトの main というブランチにいるとします(3回目のコミット)。
ブランチ作成後と現在の状態を確認するため、以下を実行します。
% git status On branch main //現在 main ブランチにいる nothing to commit, working tree clean % git log --oneline -1 b1acd1b (HEAD -> main) extra text added to foo.txt // HEAD は main をポイント % git branch //ブランチの一覧を表示 * main // 現在のブランチは main のみ
以下を実行すると experiment というブランチが作成されます。
% git branch experiment // ブランチを作成
上記のコマンドは、現在のコミットに対する新たなポインタを作成します。
上記の操作は単に新規ブランチを作成しただけで、現在見ているブランチは main のままです。
git branch
コマンドはオプションを何も指定せずに実行すると、ブランチの一覧を表示します。その際、現在のブランチにはアスタリスクがついて表示されます。
また、現在いるブランチのことをカレントブランチと呼んだりします。
% git status On branch main //現在 main ブランチにいる nothing to commit, working tree clean % git branch experiment // 新しいブランチ * main // カレントブランチ(現在いるブランチ)
また、ログを確認すると、現在 HEAD は main と experiment の両方のブランチを指していて、main と experiment は同じコミットを指しています。
main と experiment の両方のブランチが、コミット b1acd1b の横に表示されていることがわかります。
% git log --oneline -1 b1acd1b (HEAD -> main, experiment) extra text added to foo.txt
.git/HEAD ファイルは refs/heads/main を参照しています。
% cat .git/HEAD ref: refs/heads/main
ブランチの切り替え
新しく作成したブランチへコミットの追加を開始するには、git switch コマンドを使ってブランチを切り替える必要があります。
% git switch experiment // ブランチを切り替え Switched to branch 'experiment'
以下を実行すると、カレントブランチが切り替わっているのがわかります。
% git status On branch experiment //現在 experiment ブランチにいる nothing to commit, working tree clean % git branch * experiment // カレントブランチ main
これで、HEAD が main から experiment を指すようになります。
ログを確認すると、切り替え後新しいコミットをしていないので、現在はまだ、main と experiment ブランチは同じコミットを指しています(切り替え前と experiment と main の順番は変わっていますが、同じコミットを指していることに変わりはありません)。
% git log --oneline -1 b1acd1b (HEAD -> experiment, main) extra text added to foo.txt
.git/HEAD ファイルは refs/heads/experiment を参照しています。
% cat .git/HEAD ref: refs/heads/experiment
作成と切り替えを同時に行う
git switch
に -c
オプションをつけて実行すると、ブランチの作成と切り替えを同時に行えます。
git switch -c ブランチ名 // ブランチの作成と切り替えを同時に行う
上記のコマンドは以下と同じことです。
git branch ブランチ名 // ブランチを作成 git switch ブランチ名 // ブランチを切り替え
experiment というブランチを作成して、カレントブランチを experiment ブランチに切り替えるには以下のようにまとめて記述することができます。
% git switch -c experiment
切り替えたブランチでコミット
main ブランチから experiment ブランチに切り替えた状態(HEAD のポイント先が main から experiment に変わった状態)で変更を加え、コミットすると HEAD が指す experiment ブランチがコミットによって移動します。
experiment 以外のブランチ(この例の場合は main)はそのままの状態で取り残されます。
% echo Hello from bar > bar.txt // ファイルを追加 % git add bar.txt % git commit -m "bar.txt added at experiment branch" // コミット [experiment 84c7ff1] bar.txt added at experiment branch 1 file changed, 1 insertion(+) create mode 100644 bar.txt
ログを確認すると、HEAD は experiment ブランチを指し、main ブランチは前のコミットに残ります。
% git log --oneline 84c7ff1 (HEAD -> experiment) bar.txt added at experiment branch b1acd1b (main) extra text added to foo.txt 6595577 text added to foo.txt 2931804 first commit
experiment ブランチは最新のコミットに移動しますが、main ブランチはそのまま動きません。
戻る(切り替え)
ブランチを切り替えて main に戻ってみます。
git switch コマンドを使って main に切り替えます。
% git switch main Switched to branch 'main'
上記のコマンドにより、HEAD は main を指します。
そして作業ディレクトリ内のファイルを main が指すスナップショットの状態に戻します。
これは experimantal ブランチで行った作業を巻き戻したことになり、作業ディレクトリのファイルは main ブランチでの最後のコミット(b1acd1b)の状態になります。そのため、作業ディレクトリには experimantal ブランチで追加したファイル bar.txt はありません。
% git log --oneline b1acd1b (HEAD -> main) extra text added to foo.txt 6595577 text added to foo.txt 2931804 first commit
この時点以降に行った変更は、これまでのプロジェクトから分岐した状態になります。
分岐
先述の main に戻った状態で、変更を加えてコミットするとプロジェクトの歴史が二つに分かれます。
% git status On branch main //現在は main ブランチ nothing to commit, working tree clean % echo goodbye! >> foo.txt // ファイルを変更 % git commit -a -m "goodbye added to foo.txt" // コミット [main 38f218f] goodbye added to foo.txt 1 file changed, 1 insertion(+)
% git log --oneline 38f218f (HEAD -> main) goodbye added to foo.txt b1acd1b extra text added to foo.txt 6595577 text added to foo.txt 2931804 first commit
新たに作られたコミットは、main が指していたコミット(b1acd1b)が親なのでそのコミットに連なり、ここで歴史が分岐します。
main ブランチは 38f218f
→b1acd1b
→6595577
→2931804
へのコミットの連なりを指します。
experiment ブランチは 84c7ff1
→b1acd1b
→6595577
→2931804
へのコミットの連なりを指します。
ブランチを切り替えてそれぞれの作業を進め、必要に応じてマージすることができます。
git log コマンドの --all オプション
git log コマンドを使用すると、現在のブランチのコミット履歴が表示されますが、--all オプションを使用することで他のブランチのコミット履歴も一緒に表示することができます。
% git log --oneline --all 38f218f (HEAD -> main) goodbye added to foo.txt 84c7ff1 (experiment) bar.txt added at experiment branch // experiment のコミット b1acd1b extra text added to foo.txt 6595577 text added to foo.txt 2931804 first commit
git log コマンドの --graph オプション
--graph オプションは、コミット履歴をグラフィカルな形式で表示するためのオプションで、コミット間の関係性やマージの流れが視覚的にわかりやすく表示されます。
以下は --all と --graph オプションをあわせて指定した例です。
5行目の b1acd1b から 84c7ff1 と 38f218f に分岐しているのがわかります。
*
は各コミットを表す記号です。
% git log --oneline --all --graph * 38f218f (HEAD -> main) goodbye added to foo.txt | * 84c7ff1 (experiment) bar.txt added at experiment branch |/ * b1acd1b extra text added to foo.txt * 6595577 text added to foo.txt * 2931804 first commit
マージ(merge)
あるブランチの変更をもう一つの別のブランチに取り込むことをマージ(merge)と呼びます。
マージを行うにはマージ先のブランチに切り替えてから git merge コマンドを使用します。
以下のコマンドは現在いるブランチ(マージ先のブランチ)に指定したブランチの変更を取り込みます。
git merge マージ元のブランチ名
先のサンプルの続きで、experiment ブランチで作業します(ファイルを変更してコミットを実行)。
% git switch experiment // experiment ブランチに切り替え Switched to branch 'experiment' % echo Hello again! >> bar.txt // ファイルを変更 % git commit -a -m "additional text for bar.txt" // コミット [experiment 62d63f0] additional text for bar.txt 1 file changed, 1 insertion(+) % git log --oneline 62d63f0 (HEAD -> experiment) additional text for bar.txt 84c7ff1 bar.txt added at experiment branch b1acd1b extra text added to foo.txt 6595577 text added to foo.txt 2931804 first commit
現在、main ブランチと experiment ブランチは分岐していて、以下のような状態になっています。
また、HEAD は experiment ブランチを指しています。
現在の状態は git log --oneline --all --graph
コマンドを実行するとわかりやすいです。
分岐後、main ブランチでは foo.txt が変更され、experiment ブランチでは bar.txt が追加され、更に変更されているのがコミットメッセージからわかります。
% git log --oneline --all --graph * 62d63f0 (HEAD -> experiment) additional text for bar.txt // experiment ブランチ * 84c7ff1 bar.txt added at experiment brancht // experiment ブランチ | * 38f218f (main) goodbye added to foo.txt // main ブランチ |/ * b1acd1b extra text added to foo.txt // 共通の祖先(分岐前) * 6595577 text added to foo.txt * 2931804 first commit
experiment ブランチでの作業が完了したこととして main ブランチに取り込んでみます。
この例の場合、main が指すコミットが experiment の直接の祖先ではないので、Git は両方のブランチの共通の祖先からの修正を両方のブランチから取り込んで、二つのコミットを親とする新たなコミット(マージコミット)を作ります。
但し、二つのブランチで同じファイルの同じ場所を修正していた場合、コンフリクト(衝突)が発生し、Git は自動でマージすることができないので手動で対応する必要があります。
main ブランチに experiment ブランチを取り込むので main ブランチに切り替えます。
% git switch main //main ブランチに切り替え Switched to branch 'main'
git merge コマンドを実行します。
% git merge experiment
この例のマージの場合、マージコミットが作成されるので以下のようなコミットメッセージの編集画面が表示されます。※ git commit 同様、 -m オプションを使えば、コミットメッセージを直接インラインでコマンドに指定することもできます。
メッセージを編集するには、i キーを押してインサートモードに切り替えます。
1行目のデフォルトのメッセージをそのまま使う場合は、インサートモードに切り替えずに :wq と入力し、return キーを押して保存します。
Merge branch 'experiment' # Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit.
マージの目的と変更内容などを記述して、編集が終了したら esc キーを押し、:wq と入力し、return キーを押して保存するとマージが実行されます(#で始まる行はコミットメッセージには含まれません)。
Merge branch 'experiment' // マージの目的と変更内容などを記述 Integrate recent changes from the 'experiment' branch into the main branch. # Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit. ~ ~ -- INSERT -- // i キーを押してインサートモードに切り替え
マージが問題なく完了すれば、以下のようなレスポンスが表示されます。
% git merge experiment Merge made by the 'ort' strategy. bar.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 bar.txt
git log に --graph オプションを指定して実行すると、以下のように表示され、experiment ブランチが main にマージされているのが確認できます。
% git log --oneline --graph * 0be6f30 (HEAD -> main) Merge branch 'experiment' Integrate recent changes from the 'experiment' branch into the main branch. |\ | * 62d63f0 (experiment) additional text for bar.txt // experiment | * 84c7ff1 bar.txt added at experiment branch // experiment * | 38f218f goodbye added to foo.txt // main |/ * b1acd1b extra text added to foo.txt * 6595577 text added to foo.txt * 2931804 first commit
main と experiment の二つのコミットを親とする新たなコミット(マージコミット)を作ります。
マージにより作成されたコミットオブジェクトを確認すると、2つの親コミットがあるのがわかります。
% git cat-file -p 0be6f30 // コミットオブジェクト tree 7141fcd21b6a9881800b2525abf6aa26d69eebc4 parent 38f218f066af7c2dbd5440de3c4660966976c55d // 直接の親コミット parent 62d63f04a02991b9f2ff04751f93bfd080f796a8 // 直接の親コミット author foo <foo@example.com> 1692150959 +0900 committer foo <foo@example.com> 1692150959 +0900 Merge branch 'experiment' Integrate recent changes from the 'experiment' branch into the main branch.
experiment ブランチの作業内容は main ブランチに取り込まれたので、experiment ブランチを削除することができます。
ブランチの削除
git branch に -d オプションを指定すると、マージ済みのブランチを削除することができます。
マージ済みのブランチの削除はラベルが削除されるだけなので、それが指していたコミットはそのまま残ります。ブランチを削除しても履歴を失うことはありません。
ブランチを削除してもコミットハッシュを指定してブランチを付けたり、差分を表示するなどの作業が可能です。
以下は experiment ブランチを削除する例です。レスポンスにはそのブランチがどのコミットを指していたかが表示されます。
% git branch -d experiment Deleted branch experiment (was 62d63f0).
これで experiment ブランチは削除されました。
% git log --oneline --graph --all * 0be6f30 (HEAD -> main) Merge branch 'experiment' Integrate recent changes from the 'experiment' branch into the main branch. |\ | * 62d63f0 additional text for bar.txt | * 84c7ff1 bar.txt added at experiment branch * | 38f218f goodbye added to foo.txt |/ * b1acd1b extra text added to foo.txt * 6595577 text added to foo.txt * 2931804 first commit % git branch // 全てのブランチを表示 * main
ブランチにマージされていない変更(コミット)が残っている場合、ブランチを削除してしまうとそのコミットにアクセスする手段がなくなってしまいます(コミットハッシュを覚えていればアクセス可能)。
git branch に -d オプションを指定した削除は、ブランチにマージされていない変更が残っている場合は Git が削除を拒否し、以下のようなエラーメッセージが表示されます。
% git branch -d test error: The branch 'test' is not fully merged. If you are sure you want to delete it, run 'git branch -D test'.
エラーメッセージに書いてあるように、-D オプションを指定すると強制的にブランチを削除することができます。
Fast Forward マージ
マージ先のブランチ(main)が指すコミットがマージ元(development)のコミットの直接の祖先である場合、マージ先のブランチが指すコミットが移動するだけで済みます。
言い換えると、マージ元ブランチ(development)が、マージ先ブランチ(main)のコミット歴をそのまま含む場合、Git は新たなコミットを作成せず、単にマージ先ブランチの HEAD をマージ元ブランチの最新コミットに移動させます。
main に development を取り込むには、HEAD を development の最新コミットに移動させるだけ。
簡単なサンプルで確認します。
この例では project という名前のプロジェクトに2つのファイル(foo.txt と bar.txt)を作成し、Git で初期化して最初のコミットを実行しておきます。
% mkdir project % cd project % echo this is foo. > foo.txt % echo this is bar. > bar.txt % git init // プロジェクトを Git で初期化 Initialized empty Git repository in /.../project/.git/ % git add . % git commit -m "Initial commit" // 最初のコミット [main (root-commit) 5c7d465] Initial commit 2 files changed, 2 insertions(+) create mode 100644 bar.txt create mode 100644 foo.txt
git switch -c で development というブランチを作成して、そのブランチに切り替えます。
% git switch -c development // ブランチを作成して切り替え Switched to a new branch 'development' % git branch // ブランチをリスト * development // 現在のブランチ main
development ブランチで作業をします。
baz.txt を追加してコミットし、その後 baz.txt を編集してコミットします。
% echo thi is baz. > baz.txt // baz.txt を追加してコミット % git add baz.txt % git commit -m "baz.txt added for development" [development 11fdd4f] baz.txt added for development 1 file changed, 1 insertion(+) create mode 100644 baz.txt % echo some text >> baz.txt // baz.txt を変更してコミット % git commit -a -m "text added to baz.txt for development" [development 08a38d2] text added to baz.txt for development 1 file changed, 1 insertion(+) % git log --oneline --all // ログを表示 08a38d2 (HEAD -> development) text added to baz.txt for development 11fdd4f baz.txt added for development 5c7d465 (main) Initial commit
この時点での2つのブランチの状態を図にすると以下のようになっています。
development ブランチを作成して切り替えたので、HEAD は development ブランチを指し、コミットのたびに新しいコミットに自動的に移動しています。
また、最初のコミットの時点で main ブランチから development ブランチに切り替えたので、main ブランチは最初のコミットの時点を指したままです。
この時点で development ブランチでの作業が一段落したので、main ブランチに development ブランチ上で行った変更を取り込みます(マージします)。
マージを行うには git merge
コマンドを使用します。
git merge マージ元のブランチ名
このコマンドは現在いるブランチに指定したマージ元のブランチの変更を取り込みます。
変更を取り込む main ブランチに切り替えます。
main ブランチに切り替えると、この例の場合、作業ディレクトリは最初のコミットの時点での状態になります。そのため、development ブランチ上で追加した baz.txt は作業ディレクトリには存在しません。
% git switch main // main に切り替え Switched to branch 'main' % ls // ファイルを確認(development で追加した baz.txt はない) bar.txt foo.txt % git log --oneline --all 08a38d2 (development) text added to baz.txt for development 11fdd4f baz.txt added for development 5c7d465 (HEAD -> main) Initial commit // HEAD は main を指している % git branch development * main // 現在いるブランチ
HEAD が main を指し、カレントブランチが main になります。
git merge
コマンドを使ってマージを実行します。
※ --no-ff オプションを指定すると、Fast Forward マージせずに、マージコミットを作ってマージすることもできます。
% git merge development Updating 5c7d465..08a38d2 Fast-forward // Fast-forward マージと表示されている baz.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 baz.txt
Fast Forward マージの場合、新しいコミットは作成されないので、コミットメッセージの入力は求められません。
マージ後、main ブランチを確認すると、development ブランチで追加した baz.txt が main ブランチに取り込まれています。
% git status On branch main nothing to commit, working tree clean % ls // development で追加した baz.txt が取り込まれている bar.txt baz.txt foo.txt
ログを確認すると、現在 HEAD は main と development の両方のブランチを指していて、main と development は同じコミット(08a38d2)を指しています。
% git log --oneline --all 08a38d2 (HEAD -> main, development) text added to baz.txt for development 11fdd4f baz.txt added for development 5c7d465 Initial commit
この例のように、修正を取り込みたいブランチが、現在作業中のブランチの直接の祖先であり、且つそこから歴史が分岐していない場合、修正を取り込みたいブランチの指す先を現在作業中のブランチの指す先に移動するだけで済みます。
このようなマージを Fast-Forward マージと呼びます。
development ブランチは main にマージされたので、削除することができます。
% git branch -d development Deleted branch development (was 08a38d2). % git log --oneline --all --graph * 08a38d2 (HEAD -> main) text added to baz.txt for development * 11fdd4f baz.txt added for development * 5c7d465 Initial commit
--no-ff オプション
git merge コマンドに --no-ff オプションを指定すると、Fast Forward マージを強制的に無効にして通常のマージコミットを作成する方法でマージすることができます。
Fast Forward マージでは、マージ元ブランチのコミット履歴が直接マージ先ブランチに追加されるため、マージ元ブランチの変更の追跡が難しくなりますが、--no-ff オプションを使用すると、マージコミットが明示的に作成されるため、マージ元ブランチの変更がどこでマージされたのかが明確になります。
一方で、--no-ff オプションを使用することで、マージコミットが増えるため、履歴が多くなります。
以下は --no-ff オプションを指定して、Fast Forward マージを強制的に無効にして通常のマージコミットを作成する例です。
% git merge --no-ff development
上記を実行すると、マージコミットが作成されるので、コミットメッセージの入力画面になります。
Merge branch 'development' # Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit. ~ ~ ~
インサートモードに切り替えてメッセージを作成して保存するとマージが実行されます。
Merge branch 'development' Non Fast Forward Merge Sample # Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit. ~ ~ ~ -- INSERT --
% git merge --no-ff development // メッセージを作成するとレスポンスが表示される Merge made by the 'ort' strategy. baz.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 baz.txt
ログを見ると、新たにマージコミットが作成されているのが確認できます。
% git log --oneline --all --graph * b967289 (HEAD -> main) Merge branch 'development' Non Fast Forward Merge Sample |\ | * 08a38d2 (development) text added to baz.txt for development | * 11fdd4f baz.txt added for development |/ * 5c7d465 Initial commit
新たに作成されたマージコミットは、もともと main が指していたコミット(5c7d465)と development が指しているコミット(08a38d2)の2つのコミットを親に持ちます。
新たに作成されたマージコミットと development が指しているコミット(08a38d2)とは同じスナップショットなので差分はありません。
development ブランチは不要になったので削除できます。
ブランチとマージの操作
Git では一般的に main ブランチはプロジェクトの安定したバージョンを表すブランチで、このブランチには動作が確認された(リリース済みの)コードが格納されます。
そのため、通常、main ブランチでは作業せず、実施する作業に対応したブランチを作成して、そのブランチ上で作業を行います。
そして動作が確認できたら、ブランチで作業した内容を main ブランチにマージして取り込みます。
以下では単純なサンプルを作成してマージの動作を確認します。
greeting-project というプロジェクトを作成し、Git で管理を開始します。
初期化の際に main ブランチが設定されるので、このブランチを安定バージョンとしてリリースします。
% mkdir greeting-project // プロジェクトのディレクトリを作成 % cd greeting-project % echo Hello! > hello.txt % echo Goodbye! > goodbye.txt % git init // プロジェクトを Git で初期化 Initialized empty Git repository in /.../greeting-project/.git/ % git add . // 全てのファイルをステージに追加 % git commit -m "Initial commit" // 最初のコミット [main (root-commit) 619c7cb] Initial commit 2 files changed, 2 insertions(+) create mode 100644 goodbye.txt create mode 100644 hello.txt
main ブランチの他に新しい機能を開発するために feature という作業用のブランチを作成します。
git switch -c コマンドでブランチを作成して、同時にそのブランチに切り替えます。
% git switch -c feature // ブランチを作成して切り替え Switched to a new branch 'feature' % git log --oneline // HEAD は feature と main を指している 619c7cb (HEAD -> feature, main) Initial commit % git branch // ブランチのリストを表示 * feature // 現在いるブランチ main
feature ブランチで作業をします(以下では2回コミットしています)。
% echo See You Soon! > see-you.txt // see-you.txt を追加してコミット % git add see-you.txt % git commit -m "see-you.txt added at feature" [feature a41ffa7] see-you.txt added at feature 1 file changed, 1 insertion(+) create mode 100644 see-you.txt % echo Thank You. > thanks.txt // thanks.txt を追加してコミット % git add thanks.txt % git commit -m "thanks.txt added at feature" [feature d9d4c1e] thanks.txt added at feature 1 file changed, 1 insertion(+) create mode 100644 thanks.txt % git log --oneline --all // 全てのブランチのログを表示 d9d4c1e (HEAD -> feature) thanks.txt added at feature // HEAD は featur を指している a41ffa7 see-you.txt added at feature 619c7cb (main) Initial commit
この時点での feature と main ブランチの状態を図にすると以下のようになっています。
feature ブランチを作成して切り替えているので、HEAD は feature ブランチを指し、コミットするたびに新しいコミットに自動的に移動しています。
また、最初のコミットの時点で main ブランチから feature ブランチに切り替えたので、main ブランチは最初のコミットを指したままです。
ここでリリースしているバージョン(main)で問題が発覚して早急に対応する必要があるとします。
feature での作業が完了していれば、main にマージできますが、まだ完了していないので feature での作業は一時中断します。
基本的に main では作業しないので、問題修正用のブランチを作成して切り替えて修正作業を行います。
まず main ブランチに切り替えます。
% git switch main // main に切り替え Switched to branch 'main' % git log --oneline 619c7cb (HEAD -> main) Initial commit // HEAD が main を指している
HEAD が main を指します。
作業ディレクトリは feature ブランチで作業を始める前(最初のコミット)と同じ状態に戻ります。
続いて問題修正用の bugfix というブランチを作成して切り替えます。
% git switch -c bugfix Switched to a new branch 'bugfix' % git log --oneline --all d9d4c1e (feature) thanks.txt added at feature a41ffa7 see-you.txt added at feature 619c7cb (HEAD -> bugfix, main) Initial commit // HEAD は bugfix と main を指している
bugfix ブランチを作成して切り替えたので以下のように HEAD は bugfix と main を指し、bugfix と main は最初のコミットを指している状態になります。
bugfix ブランチで修正作業を行います(以下では1回コミットしています)。
% echo bug fixed >> hello.txt // hello.txt に1行追加して問題修正とする % git commit -a -m "bug fixed for hello.txt" [bugfix d788715] bug fixed for hello.txt 1 file changed, 1 insertion(+) % git log --oneline --all d788715 (HEAD -> bugfix) bug fixed for hello.txt d9d4c1e (feature) thanks.txt added at feature a41ffa7 see-you.txt added at feature 619c7cb (main) Initial commit
HEAD は bugfix を指しています。
bugfix での修正が問題なければ、main ブランチに bugfix ブランチ上で行った修正を取り込みます。
main ブランチに bugfix ブランチを取り込むには、main ブランチに切り替えて git merge コマンドを使ってマージします。
まず、main ブランチに切り替えます。
% git switch main // main に切り替え Switched to branch 'main' % git log --oneline --all d788715 (bugfix) bug fixed for hello.txt d9d4c1e (feature) thanks.txt added at feature a41ffa7 see-you.txt added at feature 619c7cb (HEAD -> main) Initial commit // HEAD は main を指している
HEAD が main を指します。
git merge コマンドに bugfix を指定してマージを実行します。
% git merge bugfix Updating 619c7cb..d788715 Fast-forward // Fast-forward マージ hello.txt | 1 + 1 file changed, 1 insertion(+)
修正を取り込みたいブランチ(main)が、現在作業中のブランチ(bugfix)の直接の祖先であり、かつそこから歴史が分岐していない場合、単に bugfix の指すコミットまで main を動かせばよいので、このようなマージを Fast-forward マージと呼びます。
% git log --oneline --all d788715 (HEAD -> main, bugfix) bug fixed for hello.txt d9d4c1e (feature) thanks.txt added at feature a41ffa7 see-you.txt added at feature 619c7cb Initial commit
HEAD は main と bugfix の両方のブランチを指していて、main と bugfix は修正作業のコミット(d788715)を指しています。
これで main ブランチの指すスナップショットに bugfix で修正した内容が反映されました。
また、main ブランチが bugfix と同じ場所を指しているので、bugfix ブランチは削除できます。
削除するには git branch コマンドに -d オプションを指定します。
% git branch -d bugfix // bugfix ブランチを削除 Deleted branch bugfix (was d788715). % git log --oneline --all --graph * d788715 (HEAD -> main) bug fixed for hello.txt | * d9d4c1e (feature) thanks.txt added at feature | * a41ffa7 see-you.txt added at feature |/ * 619c7cb Initial commit
※ bugfix ブランチ上で行った作業は feature ブランチには含まれていません。
もし bugfix ブランチ上で行った作業を取得する必要がれば、いかのいずれかの方法で取得できます。
- git merge main で main ブランチの内容を feature ブランチにマージする
- そのまま作業を続け、feature ブランチの内容を main に適用することになった時点でマージする
この例では、feature ブランチに切り替えてこのまま作業を続けます(実際には、1. の main ブランチの内容を feature ブランチにマージして、feature ブランチの作業を進めた方が後々楽だと思います)。
% git switch feature // feature ブランチに切り替え Switched to branch 'feature' % echo Have a nice day! >> goodbye.txt % git commit -a -m "text added for goodbye.txt" // コミット [feature cde9b14] text added for goodbye.txt 1 file changed, 1 insertion(+) % git log --oneline --all --graph * cde9b14 (HEAD -> feature) text added for goodbye.txt * d9d4c1e thanks.txt added at feature * a41ffa7 see-you.txt added at feature | * d788715 (main) bug fixed for hello.txt |/ * 619c7cb Initial commit
feature ブランチに切り替え後、作業を行った状態です(以下では1回コミットしています)。
feature ブランチでの作業が完了したとします。feature ブランチでの変更を main ブランチに取り込むには、マージ先の main ブランチに切り替えてから git merge コマンドを実行します。
まず、マージ先の main ブランチに切り替えます。
% git switch main Switched to branch 'main' % git log --oneline --all --graph * cde9b14 (feature) text added for goodbye.txt * d9d4c1e thanks.txt added at feature * a41ffa7 see-you.txt added at feature | * d788715 (HEAD -> main) bug fixed for hello.txt |/ * 619c7cb Initial commit
main ブランチに feature ブランチをマージする場合、マージ先のコミットがマージ元のコミットの直系の先祖ではないため、各ブランチが指す2つのスナップショット(cde9b14 と d788715)とそれらの共通の先祖(619c7cb)との3つの間でマージが行われ、新しいコミット(マージコミット)が作成されます。
git merge コマンドに feature を指定してマージを実行します。
% git merge feature
上記を実行すると、Git のマージコミットメッセージを入力するためのテキストエディタが開きます。
-m オプションを使えば、コミットメッセージを直接インラインでコマンドに指定することもできます。
Merge branch 'feature' # Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit.
メッセージを編集するには、i キーを押してインサートモードに切り替え、例えば以下のようなメッセージを作成します(#で始まる行はマージコミットメッセージには含まれません)。
Merge branch 'feature' Integrate recent changes from the 'feature' branch into the main branch. # Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit. ~ ~ -- INSERT --
編集が終了したら esc キーを押し、:wq と入力し、return キーを押して保存するとマージが実行され、以下のようなレスポンスが表示されます。
% git merge feature //実行したコマンド(以下がそのレスポンス) Merge made by the 'ort' strategy. goodbye.txt | 1 + see-you.txt | 1 + thanks.txt | 1 + 3 files changed, 3 insertions(+) create mode 100644 see-you.txt create mode 100644 thanks.txt
% git log --oneline --all --graph * b8db21e (HEAD -> main) Merge branch 'feature' Integrate recent changes from the 'feature' branch into the main branch. //マージコミットメッセージ |\ | * cde9b14 (feature) text added for goodbye.txt | * d9d4c1e thanks.txt added at feature | * a41ffa7 see-you.txt added at feature * | d788715 bug fixed for hello.txt |/ * 619c7cb Initial commit
この場合、単にブランチのポインタを先に進めるのではなく、Git は三方向のマージ結果から新たなスナップショットを作成し、二つのコミットを直接の親とする新しいコミットを自動作成します。
この時作られたコミットは複数の親を持つ特別なコミットで、マージコミットと呼びます。
% git cat-file -p b8db21e tree 7b17c87ba8e31b717460049466179d93c76805b6 parent d7887150e3c1dde672c2d7eec65130d2f9e812de // 直接の親 parent cde9b14e30a368a483086bd2df5c199b34947e1c // 直接の親 author foo <foo@example.com> 1692095462 +0900 committer foo <foo@example.com> 1692095462 +0900 Merge branch 'feature' Integrate recent changes from the 'feature' branch into the main branch.
マージが完了すれば、main ブランチの作業ディレクトリには bugfix ブランチで修正作業と feature ブランチでの作業が取り込まれているのが確認できます。
問題がなければ、feature ブランチは不要なので削除することができます。
% git branch -d feature Deleted branch feature (was cde9b14). % git log --oneline --all --graph * b8db21e (HEAD -> main) Merge branch 'feature' Integrate recent changes from the 'feature' branch into the main branch. |\ | * cde9b14 text added for goodbye.txt | * d9d4c1e thanks.txt added at feature | * a41ffa7 see-you.txt added at feature * | d788715 bug fixed for hello.txt |/ * 619c7cb Initial commit
コンフリクト(conflict)
同じファイルをふたつのブランチで別々に変更してそれをマージしようとすると、Git はそれをどのようにマージすれば良いかわからず、自動的にマージすることができません。
この状態をコンフリクト(conflict)と呼びます。英語の conflict には衝突や競合という意味があります。
以下がコンフリクトを解決するおおまかな手順です。
- コンフリクトが発生しているファイルの内容を修正
- git add で対象のファイルをステージに追加
- git commit コマンドでコミット
以下では意図的にコンフリクトを発生させて、コンフリクトを解決する手順を確認します。
my-project2 というディレクトリの中に info.txt というファイルを作成し、1行目にテキストを記述しておきます。
% mkdir my-project2 % cd my-project2 % echo information > info.txt //1行目にテキストを記述 % git init // Git で初期化 Initialized empty Git repository in /.../my-project2/.git/ % git add info.txt % git commit -m "Initial Commit" // info.txt を追加(コミット) [main (root-commit) bee9468] Initial Commit 1 file changed, 1 insertion(+) create mode 100644 info.txt % git log --oneline bee9468 (HEAD -> main) Initial Commit
現在のブランチはデフォルトの main になり、上記のコミットを指しています。
git switch コマンドに -c オプションを指定して topic というブランチを作成して、切り替えます。
% git switch -c topic Switched to a new branch 'topic'
topic ブランチが作成され、HEAD が topic を指します(カレントブランチが topic になります)。
topic ブランチで info.txt に「topic added」というテキストを追加してコミットします。
% echo topic added >> info.txt % git commit -a -m "topic added to info.txt" [topic 6c0aa6e] topic added to info.txt 1 file changed, 1 insertion(+)
topic ブランチは最新のコミットに移動します(main ブランチはそのまま動きません)。
main ブランチに切り替えます。
% git switch main Switched to branch 'main'
HEAD は main ブランチを指し、main ブランチは最初のコミットを指します。
作業ディレクトリの info.txt は、最初のコミットの状態で1行目のみが記述されています。
main ブランチで info.txt に「additional text」という topic ブランチでの変更とは異なるテキストを追加してコミットします(topic ブランチの変更箇所と同じなのでマージの際にコンフリクトが発生します)。
% echo additional text >> info.txt % git commit -a -m "additional text added to info.txt" [main b881b36] additional text added to info.txt 1 file changed, 1 insertion(+)
main ブランチは最新のコミットに移動します。
main ブランチに topic ブランチで行った変更をマージします。
現在 main ブランチにいるので、git merge topic
を実行して topic ブランチでの変更を取り込みます。
但し、main と topic ブランチの両方で、同じファイル info.txt の同じ箇所を修正しているので、この状態で main から topic をマージしようとすると、Git はコンフリクト(競合)を検出し、以下のようにユーザに対応を求めてきます。レスポンスには、コンフリクトが発生しているファイルと解決方法が表示されています。
% git merge topic // main ブランチに topic ブランチでの変更をマージ Auto-merging info.txt CONFLICT (content): Merge conflict in info.txt // info.txt でコンフリクト発生 Automatic merge failed; fix conflicts and then commit the result. // 自動マージが失敗しました。競合している箇所を修正して、その結果をコミットしてください
コンフリクトが発生しているファイルを開いて確認すると、この例の場合は以下のようになっています。
Git はファイル内の競合箇所を特殊なマークアップで囲って表示します。これらの箇所を適切に修正して競合(コンフリクト)を解消します。
information <<<<<<< HEAD additional text ======= topic added >>>>>>> topic
<<<<<<< HEAD
から =======
までが、今自分がいるブランチ(main)で競合している箇所です。
<<<<<<< HEAD additional text =======
=======
から >>>>>>> topic
までが、取り込もうとしたブランチ(topic)で競合している箇所です。
======= topic added >>>>>>> topic
コンフリクト発生箇所を修正
コンフリクトを解決するには、どちらの変更を取り込むのか、または両方の変更を取り込むのかを決めて、任意のエディタでファイルを編集します。
以下は VS Code でコンフリクト発生時に表示されるファイルの編集画面の例です。
この例では、どちらも残す(取り込む)ことにして 2、4、6行目の記号のある行を削除し保存します。
※ <<<<<<< HEAD
や =======
、>>>>>>> topic
の記号のある行はすべて削除する必要があります。
information additional text topic added
git add コマンドを実行
git add コマンドを実行して修正したファイルをステージに追加します。そして git status コマンドで状態を確認します。
以下では「All conflicts fixed」とあるので、競合(コンフリクト)が解決されているのが確認できます。
但し、まだマージが完了していないのでコミットを実行する必要があります。
% git add info.txt // 修正したファイルをステージに追加 % git status // 状態を確認 On branch main All conflicts fixed but you are still merging. (use "git commit" to conclude merge) // マージを完了するには git commit を実行 Changes to be committed: modified: info.txt
git commit コマンドを実行
コミットを実行します。
% git commit -m "info.txtのコンフリクトを修正" [main 6716e5f] info.txtのコンフリクトを修正
git log で確認してマージされていれば完了です。
% git log --oneline --graph * 6716e5f (HEAD -> main) info.txtのコンフリクトを修正 |\ | * 6c0aa6e (topic) topic added to info.txt * | b881b36 additional text added to info.txt |/ * bee9468 Initial Commit
topic ブランチは不要になったので削除できます。
マージの中止(修正が難しい場合)
コンフリクトの発生箇所が多すぎるなどで修正が難しい場合は、マージの途中であっても git merge コマンドに --abort
オプションを指定してマージを中止することができます。
マージを中止したい場合、マージを開始したブランチで以下を実行します。これにより、マージが取り消され、変更が元の状態に戻ります。
git merge --abort
リベース(rebase)
リベースは、マージと同様、あるブランチの変更を別のブランチに統合するための方法です。
Git におけるリベース(rebase)は「ベースを再設定する」という意味があり、元々のコミットの履歴を、別のコミットを新しいベース(基準)として再構築する操作を意味します。
通常、Git のブランチを作成すると、そのブランチが派生する元(ベース)となるコミットが存在します。この元となるコミットがベースコミットです(以下の場合 A がベースコミット)。
例えば、feature ブランチを main ブランチにリベースすると、feature ブランチのコミットが、対象の main ブランチの最新のコミット(E)をベースとしてその直後に移動され履歴が一直線になります。
リベース操作では、リベースするブランチ(feature)を指定した新たなベースコミット(E)上に移動させ、そのコミットの直後から新たなコミットを作成し直します。
以下の場合、まず main ブランチの E に B のパッチ(A と B の差分)を当てて B' を新たに作成し、次に C のパッチ(B と C の差分)を当てて C' を新たに作成します。※図の矢印部分がパッチに相当します。
リベースはマージコミットを作成する代わりに、元のブランチでコミットごとに新しいコミットを作成することによって履歴を書き換えます。
ただし、rebase はコミット履歴を書き換える操作であるため、公開リポジトリや共有プロジェクトで使用する際には注意が必要です。
基本的に「公開リポジトリにプッシュしたコミットをリベースしてはいけない」とされています。
マージとリベース
以下はブランチでの変更を取り込む場合にマージとリベースでの違いを確認する例です。
main ブランチから派生した feature ブランチで作業をしていて、その間に main ブランチで変更があり、その変更を feature ブランチに取り込みます。
まずサンプルのプロジェクトを Git で初期化して、ファイルを作成し、ベースコミットを作成します。
% git init my-project3 // 空のディレクトリにリポジトリを作成(初期化) Initialized empty Git repository in /.../my-project3/.git/ % cd my-project3 % echo sample text > sample.txt // ファイルを作成 % git add sample.txt % git commit -m "Initial Commit" //コミット(このコミットがベースコミットになる) [main (root-commit) 21e0a65] Initial Commit 1 file changed, 1 insertion(+) create mode 100644 sample.txt % git log --oneline 21e0a65 (HEAD -> main) Initial Commit
続いて作業用の feature というブランチを作成します。
% git switch -c feature // feature というブランチを作成してそこへ切り替え Switched to a new branch 'feature' % git log --oneline 21e0a65 (HEAD -> feature, main) Initial Commit % git branch * feature main
feature ブランチで作業をします(コミットを2回実行)。
% echo feature1 text > feature1.txt % git add feature1.txt % git commit -m "feature1.txt added" [feature a9eb5d3] feature1.txt added 1 file changed, 1 insertion(+) create mode 100644 feature1.txt % echo additional text >> feature1.txt % git commit -a -m "text added to feature1.txt" [feature 09b9ab5] text added to feature1.txt 1 file changed, 1 insertion(+) % git log --oneline 09b9ab5 (HEAD -> feature) text added to feature1.txt a9eb5d3 feature1.txt added 21e0a65 (main) Initial Commit
main ブランチに切り替えます。
% git switch main Switched to branch 'main' % git log --oneline --all 09b9ab5 (feature) text added to feature1.txt a9eb5d3 feature1.txt added 21e0a65 (HEAD -> main) Initial Commit
main ブランチで作業を行います(コミットを2回実行)。
% echo abc >> sample.txt % git commit -a -m "abc added to sample.txt" [main bf182a7] abc added to sample.txt 1 file changed, 1 insertion(+) % echo 123 >> sample.txt % git commit -a -m "123 added to sample.txt" [main 15d748e] 123 added to sample.txt 1 file changed, 1 insertion(+) % git log --oneline 15d748e (HEAD -> main) 123 added to sample.txt bf182a7 abc added to sample.txt 21e0a65 Initial Commit % git log --oneline --all --graph * 15d748e (HEAD -> main) 123 added to sample.txt * bf182a7 abc added to sample.txt | * 09b9ab5 (feature) text added to feature1.txt | * a9eb5d3 feature1.txt added |/ * 21e0a65 Initial Commit
歴史が分岐し、feature ブランチには main ブランチに追加された新しい2つのコミットは反映されていません。
この時、feature ブランチに main ブランチに追加された変更(新しい2つのコミット)を取り込むには、マージまたはリベースの2つの方法があります。
マージして取り込む
main ブランチを feature ブランチにマージするには、git merge コマンドを使用します。
feature ブランチに切り替えます。
% git switch feature Switched to branch 'feature'
main ブランチを feature ブランチにマージします。
% git merge main -m "Marge main to feature" Merge made by the 'ort' strategy. sample.txt | 2 ++ 1 file changed, 2 insertions(+) % git log --oneline --all --graph * 1a54e3c (HEAD -> feature) Marge main to feature // feature ブランチ |\ | * 15d748e (main) 123 added to sample.txt // main ブランチ | * bf182a7 abc added to sample.txt // main ブランチ * | 09b9ab5 text added to feature1.txt // feature ブランチ * | a9eb5d3 feature1.txt added // feature ブランチ |/ * 21e0a65 Initial Commit
この場合、両方のブランチの履歴を連結する新しいマージコミットが feature ブランチに作成されます。
マージは非破壊的な操作なので、既存のブランチは決して変更されません。但し、変更を取り込む必要が生じるたびに feature ブランチに無関係なマージコミットが作成されることになります。
リベースの動作と比較するため、今実行したマージを git reset --hard コマンドで取り消してマージ前の状態に戻します。
% git reset --hard HEAD~ // マージを取り消す HEAD is now at 09b9ab5 text added to feature1.txt % git log --oneline --all --graph * 15d748e (main) 123 added to sample.txt * bf182a7 abc added to sample.txt | * 09b9ab5 (HEAD -> feature) text added to feature1.txt | * a9eb5d3 feature1.txt added |/ * 21e0a65 Initial Commit
リベースして取り込む
以下はリベース(rebase)を使う例です。
feature ブランチを main ブランチにリベースすると、main ブランチで行った変更のパッチ(差分)を取得し、それを feature ブランチの先端(下図の赤丸)に適用します。
リベース(rebase)のベース(base)は、この操作が行われる基準となるコミットを指します。
リベース操作を行う際に、指定したブランチを「ベース」として、現在のブランチのコミットを順番にパッチ(差分)を再適用していくイメージです。
以下の場合、feature ブランチのベースは共通の祖先のコミット(21e0a65)ですが、リベースして main ブランチの最新のコミット(15d748e)をベースとして feature ブランチのコミットを再作成します。
共通の祖先から分岐する部分(赤丸)を main ブランチの最新のコミットをベースとなるように移動するイメージですが、正確には単に移動するのではなく、移動するブランチにつながるコミットの間からパッチ(差分)を取り出し、それを順番に適用することで新たにコミットを作り直します。
feature ブランチから main にリベースすると、あたかも最新の main ブランチから feature の開発を進めたかのような歴史を作る(歴史を改変する)ことができます。
上記の動作を実際のコマンドで確認します。
git rebase コマンド
現在は以下のような状態です(カレントブランチは feature)。
以下のコマンドを実行して feature ブランチを main ブランチにリベースします。
% git rebase main Successfully rebased and updated refs/heads/feature.
ログを確認すると以下のようになっています。
先程のマージと異なり、枝分かれしていたブランチが1本になっています。main ブランチの後ろに feature ブランチが結合されているような状態です。マージコミットも作成されません。
% git log --oneline --all --graph * f46a952 (HEAD -> feature) text added to feature1.txt // feature ブランチ * 7af8885 feature1.txt added // feature ブランチ * 15d748e (main) 123 added to sample.txt // main ブランチ * bf182a7 abc added to sample.txt // main ブランチ * 21e0a65 Initial Commit
これにより、main に feature ブランチでの変更を取り込む前に、 feature ブランチでの変更が、main に加えられた変更(コミット)により動作がおかしくなっていないかを確認することができます。
但し、リベースはマージコミットを使用する代わりに、元のブランチでコミットごとにまったく新しいコミットを作成することによってプロジェクト履歴を再書き込みします(書き換えます)。
そのため、元の feature ブランチのコミットとリベース後の feature ブランチのコミットは異なるスナップショットを表していて、ハッシュ値も変わっています。
リベースして最新の main の修正を取り込んだ状態で、追加された機能が正しく動作することを確認したら、この変更を main ブランチにマージして取り込みます。
まず、main ブランチに戻ります。
% git switch main Switched to branch 'main'
HEAD が main ブランチを指します。
main ブランチに feature ブランチをマージします。
% git merge feature Updating 7af8885..f46a952 Fast-forward // First-forward でマージされる feature1.txt | 1 + 1 file changed, 1 insertion(+)
この状態で git merge コマンドを実行すると First-forward マージとなります。
% git log --oneline --graph * f46a952 (HEAD -> main, feature) text added to feature1.txt * 7af8885 feature1.txt added * 15d748e 123 added to sample.txt * bf182a7 abc added to sample.txt * 21e0a65 Initial Commit
-i オプション(interactive モード)
git rebase コマンドに -i または --interactive オプションを指定すると、対話(interactive)モードでコミットをまとめたり、コミットメッセージを変更することができます。
コミットをまとめる(squash)
git rebase コマンドにオプションを指定せずに実行した場合、リベース対象となっている全てのコミットがリベース先に移動しますが、-i オプションを付けて実行すると移動する予定のそれぞれのコミットについてどのようにするかを聞かれます。
以下はリベース前の状態です。
以下はこの時点でのログです(先述の例のリベース前と同じですが、コミットハッシュは異なります)。
% git branch * feature // 現在は feature ブランチ main % git log --oneline --all --graph // ログを表示 * eb16833 (main) 123 added to sample.txt * d12a913 abc added to sample.txt | * 8c2fb26 (HEAD -> feature) text added to feature1.txt | * 4ded216 feature1.txt added |/ * ecd2a21 Initial Commit
-i オプションを指定して git rebase コマンドを実行すると、
% git rebase -i main
対話モードになり、エディタが開いて以下のような画面が表示さます。
この例の場合は、main と feature の共通祖先であるコミット(ecd2a21)から feature が指すコミットまでに2つのコミットがあるので、それらを表す2行が先頭に表示されています。
各行にはデフォルトのコマンド(pick)とコミットハッシュ、コミットメッセージが表示されています。
各行を編集してそれぞれに対するコマンドを指定することができます。
pick 4ded216 feature1.txt added pick 8c2fb26 text added to feature1.txt # Rebase eb16833..8c2fb26 onto eb16833 (2 commands) # # Commands: # p, pick <commit> = use commit # r, reword <commit> = use commit, but edit the commit message # e, edit <commit> = use commit, but stop for amending # s, squash <commit> = use commit, but meld into previous commit # f, fixup [-C | -c] <commit> = like "squash" but keep only the previous # commit's log message, unless -C is used, in which case # keep only this commit's message; -c is same as -C but # opens the editor # x, exec <command> = run command (the rest of the line) using shell # b, break = stop here (continue rebase later with 'git rebase --continue') # d, drop <commit> = remove commit # l, label <label> = label current HEAD with a name # t, reset <label> = reset HEAD to a label # m, merge [-C <commit> | -c <commit>] <label> [# <oneline>] # create a merge commit using the original merge commit's # message (or the oneline, if no original merge commit was # specified); use -c <commit> to reword the commit message # u, update-ref <ref> = track a placeholder for the <ref> to be updated # to this position in the new commits. The <ref> is # updated at the end of the rebase # # These lines can be re-ordered; they are executed from top to bottom.
表示された画面のコメント部分に書かれていますが、以下のようなコマンドを指定できます。
コマンド | 短縮形 | 意味 |
---|---|---|
pick | p | そのままこのコミットを使う(デフォルト) |
reword | r | このコミットを採用するが、コミットメッセージを変更する |
squash | s | 一個前のコミットとまとめる(統合する) |
fixup | f | squash と同様、一個前のコミットと統合するが前のコミットのメッセージのみを保持する |
コミットをまとめるには pick を squash(または s)に変更することでそのコミットを1つ前のコミットにに含める(統合する)ことができます。
編集するには i キーを押して INSERT モードにします。
例えば、以下のように編集して保存すると2つのコミットを1つにまとめることができます。保存するには esc キーでコマンドモードにして、:wq と入力します。
pick 4ded216 feature1.txt added squash 8c2fb26 text added to feature1.txt
以下のようなコミットメッセージを編集する画面になるので、必要に応じてコミットメッセージを変更して保存します。
# This is a combination of 2 commits. # This is the 1st commit message: feature1.txt added # This is the commit message #2: text added to feature1.txt # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # # Date: Sat Aug 19 20:07:31 2023 +0900 # # interactive rebase in progress; onto eb16833 # Last commands done (2 commands done): # pick 4ded216 feature1.txt added # squash 8c2fb26 text added to feature1.txt # No commands remaining. # You are currently rebasing branch 'feature' on 'eb16833'. # # Changes to be committed: # new file: feature1.txt #
コミットメッセージを保存すると、以下のように表示されてリベースが完了します。
% git rebase -i main [detached HEAD bf36b2b] feature1.txt added Date: Sat Aug 19 20:07:31 2023 +0900 1 file changed, 2 insertions(+) create mode 100644 feature1.txt Successfully rebased and updated refs/heads/feature.
上記レスポンスの [detached HEAD bf36b2b] は、リベースの一環として一時的に HEAD がブランチから外れた状態であることを示していて、リベース操作が完了すると、新しいコミットのチェーンが作成され、通常のブランチに統合されることで、HEAD は再び通常のブランチに関連付けられた状態に戻ります。
ログを確認すると以下のようになっていて、feature ブランチの2つのコミットが統合されているのが確認できます。
% git log --oneline --all --graph * bf36b2b (HEAD -> feature) feature1.txt added * eb16833 (main) 123 added to sample.txt * d12a913 abc added to sample.txt * ecd2a21 Initial Commit
以下はリベース前の状態です。
以下がリベース後の状態です。
この変更を main ブランチにマージして取り込むには、main ブランチに切り替えて feature ブランチをマージします。
% git switch main // main ブランチに切り替え Switched to branch 'main' % git merge feature // feature ブランチをマージ Updating eb16833..bf36b2b Fast-forward // Fast-forward マージになります feature1.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 feature1.txt % git log --oneline --all --graph * bf36b2b (HEAD -> main, feature) feature1.txt added * eb16833 123 added to sample.txt * d12a913 abc added to sample.txt * ecd2a21 Initial Commit
コミットを指定してリベース
git rebase -i
コマンドではブランチを指定して統合する以外に、コミットを指定することもできます。
この場合、指定したコミットから現在のブランチの最新コミットまでの間のコミット履歴がリベース操作の対象となり、この範囲内のコミットの順序を変更したり、コミットメッセージを編集することができます。
git rebase -i <commit-hash>
※ リベースの操作を行うとその対象のコミットのハッシュ値が変更されます。
また、git rebase -i は、指定したコミットから現在の HEAD(最新のコミット)までの範囲内のコミットを対象としてリベース操作を行うため、1つの特定のコミットのみを対象にすることはできないようです。
動作を確認するためのサンプルのプロジェクトを作成します。
main ブランチでファイルを作成してコミット後、feature ブランチを作成して切り替え、3つのコミットを実行しています。
% git init my-project4 // プロジェクトを初期化 Initialized empty Git repository in /.../my-project4/.git/ % cd my-project4 % echo hello > main.txt // main ブランチでファイルを作成 % git add main.txt % git commit -m "Initial Commit" // main ブランチでコミット [main (root-commit) d8ae5dd] Initial Commit 1 file changed, 1 insertion(+) create mode 100644 main.txt % git switch -c feature // feature ブランチを作成して切り替え Switched to a new branch 'feature' % echo abc > feature1.txt % echo 123 > feature2.txt % git add . % git commit -m "feature1.txt and feature2.txt added" // feature ブランチでコミット [feature 3e9bbb6] feature1.txt and feature2.txt added 2 files changed, 2 insertions(+) create mode 100644 feature1.txt create mode 100644 feature2.txt % echo def > feature1.txt % git commit -a -m "change feature1.txt" // feature ブランチでコミット [feature 72e8b36] change feature1.txt 1 file changed, 1 insertion(+), 1 deletion(-) % echo 456 >> feature2.txt % git commit -a -m "text added to feature2.txt" // feature ブランチでコミット [feature 91b851e] text added to feature2.txt 1 file changed, 1 insertion(+) % git log --oneline // ログを表示 91b851e (HEAD -> feature) text added to feature2.txt 72e8b36 change feature1.txt 3e9bbb6 feature1.txt and feature2.txt added d8ae5dd (main) Initial Commit
コミットメッセージを変更
直前のコミットメッセージは git commit --amend コマンドで変更できますが、直前のコミット以外の場合はリベースを使ってコミットメッセージを変更することができます。
git rebase -i コマンドに指定するコミットのハッシュは変更したいコミットの一つ前のコミットハッシュを指定します。
% git log --oneline 91b851e (HEAD -> feature) text added to feature2.txt 72e8b36 change feature1.txt 3e9bbb6 feature1.txt and feature2.txt added d8ae5dd (main) Initial Commit
例えば、上記のようなログがある場合、3e9bbb6 のコミットメッセージを変更するには、その前のコミットハッシュ(d8ae5dd)を指定します。
% git rebase -i d8ae5dd
コミットメッセージを変更したいコミットの先頭に記述されているコマンドを pick から reword(または r)に変更して保存します。
pick 3e9bbb6 feature1.txt and feature2.txt added pick 72e8b36 change feature1.txt pick 91b851e text added to feature2.txt # Rebase d8ae5dd..91b851e onto d8ae5dd (3 commands) # # Commands: # p, pick <commit> = use commit # r, reword <commit> = use commit, but edit the commit message # e, edit <commit> = use commit, but stop for amending # s, squash <commit> = use commit, but meld into previous commit # f, fixup [-C | -c] <commit> = like "squash" but keep only the previous # commit's log message, unless -C is used, in which case # keep only this commit's message; -c is same as -C but # opens the editor # x, exec <command> = run command (the rest of the line) using shell # b, break = stop here (continue rebase later with 'git rebase --continue') # d, drop <commit> = remove commit # l, label <label> = label current HEAD with a name # t, reset <label> = reset HEAD to a label # m, merge [-C <commit> | -c <commit>] <label> [# <oneline>] # create a merge commit using the original merge commit's # message (or the oneline, if no original merge commit was # specified); use -c <commit> to reword the commit message # u, update-ref <ref> = track a placeholder for the <ref> to be updated # to this position in the new commits. The <ref> is # updated at the end of the rebase #
この例では1行目の 3e9bbb6 を変更するように pick から r に変更して保存します。
r 3e9bbb6 feature1.txt and feature2.txt added pick 72e8b36 change feature1.txt pick 91b851e text added to feature2.txt
以下が表示されるのでコミットメッセージを編集して変更します。
feature1.txt and feature2.txt added # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # # Date: Sun Aug 20 09:08:43 2023 +0900 # # interactive rebase in progress; onto d8ae5dd # Last command done (1 command done): # reword 3e9bbb6 feature1.txt and feature2.txt added # Next commands to do (2 remaining commands): # pick 72e8b36 change feature1.txt # pick 91b851e text added to feature2.txt # You are currently editing a commit while rebasing branch 'feature' on 'd8ae5dd'. # # Changes to be committed: # new file: feature1.txt # new file: feature2.txt #
例えば、以下のように変更して保存します。
First commit at feature branch
以下のようにコマンドのレスポンスに Successfully rebased and updated と表示されれば完了です。
% git rebase -i d8ae5dd [detached HEAD 54d9057] First commit at feature branch Date: Sun Aug 20 09:08:43 2023 +0900 2 files changed, 2 insertions(+) create mode 100644 feature1.txt create mode 100644 feature2.txt Successfully rebased and updated refs/heads/feature.
ログを確認するとコミットメッセージが変更されているのがわかります。
※ コミットメッセージを変更したコミットだけではなく、リベースの対象となったコミットのハッシュ値も変更されているのが確認できます。
% git log --oneline 2713817 (HEAD -> feature) text added to feature2.txt a0a5530 change feature1.txt 54d9057 First commit at feature branch // 変更されている(※コミットハッシュも変更される) d8ae5dd (main) Initial Commit
コミットを統合
ブランチを指定する場合と同様に、指定した範囲のコミットを統合する(まとめる)ことができます。
先述の例の続きで、feature ブランチのコミットを1つに統合してみます。以下が現在のログです。
% git log --oneline 2713817 (HEAD -> feature) text added to feature2.txt a0a5530 change feature1.txt 54d9057 First commit at feature branch d8ae5dd (main) Initial Commit
統合する対象のコミットの1つ前のコミットを指定して git rebase -i コマンドを実行します。
% git rebase -i d8ae5dd
以下が表示されるので、2つ目と3つ目のコミットの pick を squash(または s)に変更します。
pick 54d9057 First commit at feature branch pick a0a5530 change feature1.txt pick 2713817 text added to feature2.txt # Rebase d8ae5dd..2713817 onto d8ae5dd (3 commands) # # Commands: # p, pick <commit> = use commit # r, reword <commit> = use commit, but edit the commit message # e, edit <commit> = use commit, but stop for amending # s, squash <commit> = use commit, but meld into previous commit # f, fixup [-C | -c] <commit> = like "squash" but keep only the previous # commit's log message, unless -C is used, in which case # keep only this commit's message; -c is same as -C but # opens the editor # x, exec <command> = run command (the rest of the line) using shell # b, break = stop here (continue rebase later with 'git rebase --continue') # d, drop <commit> = remove commit # l, label <label> = label current HEAD with a name # t, reset <label> = reset HEAD to a label # m, merge [-C <commit> | -c <commit>] <label> [# <oneline>] # create a merge commit using the original merge commit's # message (or the oneline, if no original merge commit was # specified); use -c <commit> to reword the commit message # u, update-ref <ref> = track a placeholder for the <ref> to be updated # to this position in the new commits. The <ref> is # updated at the end of the rebase #
以下のように変更したら保存します。
pick 54d9057 First commit at feature branch s a0a5530 change feature1.txt s 2713817 text added to feature2.txt
統合したコミットのコミットメッセージを作成する画面が表示されます。
# This is a combination of 3 commits. # This is the 1st commit message: First commit at feature branch # This is the commit message #2: change feature1.txt # This is the commit message #3: text added to feature2.txt # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # # Date: Sun Aug 20 09:08:43 2023 +0900 # # interactive rebase in progress; onto d8ae5dd # Last commands done (3 commands done): # squash a0a5530 change feature1.txt # squash 2713817 text added to feature2.txt # No commands remaining. # You are currently rebasing branch 'feature' on 'd8ae5dd'. # # Changes to be committed: # new file: feature1.txt # new file: feature2.txt
例えば、以下のように編集して保存します。
Created 2 files and edited for feature branch # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # # Date: Sun Aug 20 09:08:43 2023 +0900 # # interactive rebase in progress; onto d8ae5dd # Last commands done (3 commands done): # squash a0a5530 change feature1.txt # squash 2713817 text added to feature2.txt # No commands remaining. # You are currently rebasing branch 'feature' on 'd8ae5dd'. # # Changes to be committed: # new file: feature1.txt # new file: feature2.txt
リベースが完了します。
% git rebase -i d8ae5dd [detached HEAD 494358a] Created 2 files and edited for feature branch Date: Sun Aug 20 09:08:43 2023 +0900 2 files changed, 3 insertions(+) create mode 100644 feature1.txt create mode 100644 feature2.txt Successfully rebased and updated refs/heads/feature.
ログを確認すると feature ブランチの3つのコミットが1つに統合されています。
% git log --oneline 494358a (HEAD -> feature) Created 2 files and edited for feature branch d8ae5dd (main) Initial Commit
リベース中のコンフリクト
マージ同様、リベースでも同じファイルを別々に変更していればコンフリクトが発生します。
マージは二つのブランチの修正を一度に取り込む操作なのでコンフリクトの解決を一度だけ行えば良いのに対して、リベースはリベース先のコミットに順番にパッチを適用していく操作なのでコンフリクトが発生する度に解決する必要があります。
言い換えると、リベースは各コミットごとにコンフリクトを検出し、その度にコンフリクトを解決します。
以下がリベースの場合のコンフリクトを解決する手順です。
- コンフリクトが発生しているファイルの内容を修正
- git add で対象のファイルをステージに追加
- git rebase --continue を実行(コミットメッセージを作成)
以下では意図的にコンフリクトを発生させて、コンフリクトを解決する手順を確認します。
// プロジェクトを作成して初期化 % git init my-project5 Initialized empty Git repository in /.../my-project5/.git/ % cd my-project5 // main ブランチでファイル abc.txt と xyz.txt を作成してコミット % echo abc > abc.txt % echo xyz > xyz.txt % git add . % git commit -m "Initial Commit" [main (root-commit) df380d4] Initial Commit 2 files changed, 2 insertions(+) create mode 100644 abc.txt create mode 100644 xyz.txt // topic ブランチを作成して切り替え % git switch -c topic Switched to a new branch 'topic' // topic ブランチで abc.txt を変更 % echo 123 >> abc.txt % git commit -a -m "123 added to abc.txt" [topic b73127d] 123 added to abc.txt 1 file changed, 1 insertion(+) // topic ブランチで xyz.txt を変更 % echo 456 >> xyz.txt % git commit -a -m "456 added to xyz.txt" [topic 23d7625] 456 added to xyz.txt 1 file changed, 1 insertion(+) // main ブランチに切り替え % git switch main Switched to branch 'main' // main ブランチで abc.txt と xyz.txt を変更 % echo def >> abc.txt % echo ghi >> xyz.txt % git add . % git commit -m "abc.txt and xyz.txt changed" [main f0ee59a] abc.txt and xyz.txt changed 2 files changed, 2 insertions(+)
この時点でログを確認すると以下のようになっています。
% git log --oneline --all --graph // ログを確認 * f0ee59a (HEAD -> main) abc.txt and xyz.txt changed | * 23d7625 (topic) 456 added to xyz.txt | * b73127d 123 added to abc.txt |/ * df380d4 Initial Commit
現在以下のような状態になっていて HEAD は main を指しています。
topic ブランチを main ブランチにリベースするので、まず topic に切り替えます。
% git switch topic Switched to branch 'topic'
HEAD は topic を指します。
topic ブランチを main ブランチにリベースします。
この例の場合、コンフリクトは topic の2つのコミットで発生します。リベースでは各コミットごとにコンフリクトを解決する必要があるので、以下ではまず最初のコンフリクトが表示されます。
% git rebase main Auto-merging abc.txt CONFLICT (content): Merge conflict in abc.txt // abc.txt でコンフリクト error: could not apply b73127d... 123 added to abc.txt hint: Resolve all conflicts manually, mark them as resolved with hint: "git add/rm <conflicted_files>", then run "git rebase --continue". hint: You can instead skip this commit: run "git rebase --skip". hint: To abort and get back to the state before "git rebase", run "git rebase --abort". Could not apply b73127d... 123 added to abc.txt
abc.txt にコンフリクトが発生しているので、abc.txt を開くと以下のように競合の箇所が表示されます。
abc <<<<<<< HEAD def ======= 123 >>>>>>> b73127d (123 added to abc.txt)
この例では両方の変更を取り込むことにして以下のように編集(修正)して保存します。
abc def 123
修正した abc.txt をステージに追加します。
% git add abc.txt
git rebase --continue を実行します。
% git rebase --continue
内部的には、ステージに追加した abc.txt をコミットするので、コミットメッセージの編集画面が表示されます。
123 added to abc.txt # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # # interactive rebase in progress; onto f0ee59a # Last command done (1 command done): # pick b73127d 123 added to abc.txt # Next command to do (1 remaining command): # pick 23d7625 456 added to xyz.txt # You are currently rebasing branch 'topic' on 'f0ee59a'. # # Changes to be committed: # modified: abc.txt #
編集して保存します。
123 and def added to abc.txt # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # # interactive rebase in progress; onto f0ee59a # Last command done (1 command done): # pick b73127d 123 added to abc.txt # Next command to do (1 remaining command): # pick 23d7625 456 added to xyz.txt # You are currently rebasing branch 'topic' on 'f0ee59a'. # # Changes to be committed: # modified: abc.txt #
1つ目のコンフリクトの対応は終わりましたが、この例の場合、コンフリクトが発生するコミットがもう1つあるので以下のようなレスポンが表示されます。
xyz.txt でもコンフリクトが発生しているとあるので、xyz.txt を開いて競合している部分を修正します。
% git rebase --continue // 実行した git rebase --continue コマンドのレスポンス [detached HEAD 9b3de67] 123 and def added to abc.txt 1 file changed, 4 insertions(+) Auto-merging xyz.txt CONFLICT (content): Merge conflict in xyz.txt // xyz.txt でコンフリクト error: could not apply 23d7625... 456 added to xyz.txt hint: Resolve all conflicts manually, mark them as resolved with hint: "git add/rm <conflicted_files>", then run "git rebase --continue". hint: You can instead skip this commit: run "git rebase --skip". hint: To abort and get back to the state before "git rebase", run "git rebase --abort". Could not apply 23d7625... 456 added to xyz.txt
xyz.txt を開くと以下のように表示されるので、競合している部分を修正します。
xyz <<<<<<< HEAD ghi ======= 456 >>>>>>> 23d7625 (456 added to xyz.txt)
この場合も両方の変更を取り込むことにして以下のように編集(修正)して保存します。
xyz ghi 456
修正した xyz.txt をステージします。
% git add xyz.txt
git rebase --continue を実行します。
% git rebase --continue
コミットメッセージの編集画面が表示されるので編集して保存します。手順は同じなので省略します。
以下のように Successfully rebased and updated ... と表示されれば完了です。
% git rebase --continue [detached HEAD 9329e72] 456 and ghi added to xyz.txt 1 file changed, 1 insertion(+) Successfully rebased and updated refs/heads/topic.
ログを確認すると、リベースされています。
% git log --oneline --all --graph * 9329e72 (HEAD -> topic) 456 and ghi added to xyz.txt * 9b3de67 123 and def added to abc.txt * f0ee59a (main) abc.txt and xyz.txt changed * df380d4 Initial Commit
マージの場合
リベースの場合は、各コミットごとにコンフリクトの対応をしますが、マージの場合はコンフリクトの解決を一度に行います。
例えば、上記をリベースではなく topic ブランチを main ブランチに取り込むためメージすると、以下のように2箇所のコンフリクトがまとめて表示されます。
% git merge topic Auto-merging abc.txt CONFLICT (content): Merge conflict in abc.txt // コンフリクト Auto-merging xyz.txt CONFLICT (content): Merge conflict in xyz.txt // コンフリクト Automatic merge failed; fix conflicts and then commit the result.
この場合、abc.txt と xyz.txt を修正して、それらをステージに追加し、コミットします。
リベースの中止
コンフリクトの内容や修正方法がわからない場合、リベースの途中であっても git rebase コマンドに --abort
オプションを指定してリベースを中止することができます。
以下を実行するとリベース操作が取り消され、リベース前の状態に戻ります。
% git rebase --abort