Git ブランチ マージ リベース

Git のブランチの作成や切り替え、マージやリベースの使い方についての覚書です。

関連ページ

作成日:2022年8月26日

ブランチ(branch)とは

branch には「枝」や「支流」などの意味がありますが、Git のブランチは、プロジェクトの異なるバージョンや変更を管理するための仕組みのことで、コミット履歴の流れを枝分かれさせるための機能です。

ブランチを使うことで、複数の作業を同時に進めたり、新機能の開発やバグ修正を別々の場所で行ったりすることができます。

Git ではリポジトリを作成(初期化)した際、自動的に main または master といったデフォルトのブランチが設定されます(バージョンによって名前が異なる場合があります)。

このデフォルトのブランチには最初のコミットが含まれ、ブランチを切り替えるまでコミットはデフォルトのブランチに追加されていきます。

新しいブランチを作成する際には、デフォルトのブランチから分岐する形で作成するのが一般的で、必要に応じて任意の数のブランチを作成することができます。

main ブランチ(デフォルトのブランチ名)

以前は、デフォルトのブランチとして master ブランチが自動的に作成されました。しかし、近年の反人種差別運動を受けて、これに関連して "master" という用語の使用が問題視されるようになり、多くのプロジェクトでデフォルトブランチの名前を main や他のものに変更する動きが広がっています。

以下ではデフォルトのブランチを main としています。

デフォルトのブランチは git config コマンドで設定できます(関連:デフォルトのブランチ名を変更)。

ブランチは単なるポインタ

実際にはブランチは特定のコミット(オブジェクト)を指す単なるポインタ(参照)です。

各コミットはそれぞれ一意のハッシュ値を持っていて、前のコミット(親のコミット)のハッシュ値を参照することでコミット同士は繋がっています。

ブランチは、このコミットのハッシュ値を指し示すポインタとして機能します。

特定のブランチが指すコミットは、そのブランチに関連付けられている最新のコミットです。

また、実際のブランチは一つの特定のコミットを指しますが、概念としてのブランチはそのブランチの指すコミットが繋がっている一連のコミットを指します。

ブランチ main 過去 現在 コミット(オブジェクト) 2931804 コミット 6595577 コミット b1acd1b 概念としてのブランチ(ブランチに関連付けられている一連のコミット)

最初にコミットした時点で、そのコミットを指す 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 オブジェクトへのポインタです。

参考:Git のブランチ機能 - ブランチとは

ブランチの作成

ブランチは git branch コマンドを使って作成することができます。

git branch ブランチ名

例えば、現在、以下のようにデフォルトの main というブランチにいるとします(3回目のコミット)。

HEAD main 2931804 6595577 b1acd1b

ブランチ作成後と現在の状態を確認するため、以下を実行します。

% 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 // ブランチを作成

上記のコマンドは、現在のコミットに対する新たなポインタを作成します。

HEAD main 2931804 6595577 b1acd1b 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 2931804 6595577 b1acd1b experiment HEAD

ログを確認すると、切り替え後新しいコミットをしていないので、現在はまだ、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 2931804 6595577 b1acd1b 84c7ff1 experiment HEAD
戻る(切り替え)

ブランチを切り替えて 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
HEAD main 2931804 6595577 b1acd1b 84c7ff1 experiment

この時点以降に行った変更は、これまでのプロジェクトから分岐した状態になります。

分岐

先述の 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 ブランチは 38f218fb1acd1b65955772931804へのコミットの連なりを指します。

experiment ブランチは 84c7ff1b1acd1b65955772931804へのコミットの連なりを指します。

HEAD main 2931804 6595577 b1acd1b 84c7ff1 experiment 38f218f

ブランチを切り替えてそれぞれの作業を進め、必要に応じてマージすることができます。

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 ブランチを指しています。

main 2931804 6595577 b1acd1b 38f218f 84c7ff1 62d63f0 experiment HEAD

現在の状態は 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 の二つのコミットを親とする新たなコミット(マージコミット)を作ります。

HEAD main 2931804 6595577 b1acd1b 84c7ff1 38f218f 0be6f30 マージコミット 62d63f0 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 のブランチ機能 - ブランチとマージの基本

ブランチの削除

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 をマージ元ブランチの最新コミットに移動させます。

HEAD main 5c7d465 11fdd4f 08a38d2 development

main に development を取り込むには、HEAD を development の最新コミットに移動させるだけ。

HEAD main 5c7d465 11fdd4f 08a38d2 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
main 5c7d465 HEAD

git switch -c で development というブランチを作成して、そのブランチに切り替えます。

% git switch -c development  // ブランチを作成して切り替え
Switched to a new branch 'development'

% git branch  // ブランチをリスト
* development  // 現在のブランチ
  main
main 5c7d465 development HEAD

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 ブランチは最初のコミットの時点を指したままです。

main 5c7d465 11fdd4f 08a38d2 development HEAD

この時点で 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 になります。

HEAD main 5c7d465 11fdd4f 08a38d2 development

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 マージと呼びます。

HEAD main 5c7d465 11fdd4f 08a38d2 development

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)とは同じスナップショットなので差分はありません。

HEAD main 5c7d465 b967289 11fdd4f 08a38d2 development

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 619c7cb HEAD

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
main 619c7cb feature HEAD

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 619c7cb a41ffa7 d9d4c1e feature HEAD

ここでリリースしているバージョン(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 ブランチで作業を始める前(最初のコミット)と同じ状態に戻ります。

HEAD main 619c7cb a41ffa7 d9d4c1e 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 は最初のコミットを指している状態になります。

main 619c7cb a41ffa7 d9d4c1e feature bugfix HEAD

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 を指しています。

main 619c7cb d788715 feature a41ffa7 d9d4c1e bugfix HEAD

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 を指します。

619c7cb d788715 feature a41ffa7 d9d4c1e bugfix main HEAD

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)を指しています。

619c7cb d788715 feature a41ffa7 d9d4c1e bugfix main HEAD

これで 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
619c7cb d788715 feature a41ffa7 d9d4c1e main HEAD

※ bugfix ブランチ上で行った作業は feature ブランチには含まれていません。

もし bugfix ブランチ上で行った作業を取得する必要がれば、いかのいずれかの方法で取得できます。

  1. git merge main で main ブランチの内容を feature ブランチにマージする
  2. そのまま作業を続け、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回コミットしています)。

619c7cb d788715 HEAD feature a41ffa7 d9d4c1e cde9b14 main

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つの間でマージが行われ、新しいコミット(マージコミット)が作成されます。

619c7cb d788715 feature a41ffa7 d9d4c1e cde9b14 main HEAD

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 は三方向のマージ結果から新たなスナップショットを作成し、二つのコミットを直接の親とする新しいコミットを自動作成します。

619c7cb d788715 b8db21e feature a41ffa7 d9d4c1e cde9b14 main HEAD

この時作られたコミットは複数の親を持つ特別なコミットで、マージコミットと呼びます。

% 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 には衝突や競合という意味があります。

以下がコンフリクトを解決するおおまかな手順です。

  1. コンフリクトが発生しているファイルの内容を修正
  2. git add で対象のファイルをステージに追加
  3. 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 になり、上記のコミットを指しています。

main bee9468 HEAD

git switch コマンドに -c オプションを指定して topic というブランチを作成して、切り替えます。

% git switch -c topic
Switched to a new branch 'topic'

topic ブランチが作成され、HEAD が topic を指します(カレントブランチが topic になります)。

main bee9468 topic HEAD

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 bee9468 6c0aa6e topic HEAD

main ブランチに切り替えます。

% git switch main
Switched to branch 'main'

HEAD は main ブランチを指し、main ブランチは最初のコミットを指します。

作業ディレクトリの info.txt は、最初のコミットの状態で1行目のみが記述されています。

HEAD main bee9468 6c0aa6e topic

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 ブランチは最新のコミットに移動します。

bee9468 6c0aa6e HEAD main b881b36 topic

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 はファイル内の競合箇所を特殊なマークアップで囲って表示します。これらの箇所を適切に修正して競合(コンフリクト)を解消します。

info.txt(コンフリクトが発生しているファイル)
information
<<<<<<< HEAD
additional text
=======
topic added
>>>>>>> topic

<<<<<<< HEAD から ======= までが、今自分がいるブランチ(main)で競合している箇所です。

<<<<<<< HEAD
additional text
=======

======= から >>>>>>> topic までが、取り込もうとしたブランチ(topic)で競合している箇所です。

=======
topic added
>>>>>>> topic

コンフリクト発生箇所を修正

コンフリクトを解決するには、どちらの変更を取り込むのか、または両方の変更を取り込むのかを決めて、任意のエディタでファイルを編集します。

以下は VS Code でコンフリクト発生時に表示されるファイルの編集画面の例です。

この例では、どちらも残す(取り込む)ことにして 2、4、6行目の記号のある行を削除し保存します。

<<<<<<< HEAD=======>>>>>>> topic の記号のある行はすべて削除する必要があります。

修正後の info.txt
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
bee9468 6c0aa6e HEAD main b881b36 6716e5f topic

topic ブランチは不要になったので削除できます。

マージの中止(修正が難しい場合)

コンフリクトの発生箇所が多すぎるなどで修正が難しい場合は、マージの途中であっても git merge コマンドに --abort オプションを指定してマージを中止することができます。

マージを中止したい場合、マージを開始したブランチで以下を実行します。これにより、マージが取り消され、変更が元の状態に戻ります。

git merge --abort

リベース(rebase)

リベースは、マージと同様、あるブランチの変更を別のブランチに統合するための方法です。

Git におけるリベース(rebase)は「ベースを再設定する」という意味があり、元々のコミットの履歴を、別のコミットを新しいベース(基準)として再構築する操作を意味します。

通常、Git のブランチを作成すると、そのブランチが派生する元(ベース)となるコミットが存在します。この元となるコミットがベースコミットです(以下の場合 A がベースコミット)。

例えば、feature ブランチを main ブランチにリベースすると、feature ブランチのコミットが、対象の main ブランチの最新のコミット(E)をベースとしてその直後に移動され履歴が一直線になります。

A B C main D E feature feature ブランチを main ブランチの最新のコミットの後に移動

リベース操作では、リベースするブランチ(feature)を指定した新たなベースコミット(E)上に移動させ、そのコミットの直後から新たなコミットを作成し直します。

以下の場合、まず main ブランチの E に B のパッチ(A と B の差分)を当てて B' を新たに作成し、次に C のパッチ(B と C の差分)を当てて C' を新たに作成します。※図の矢印部分がパッチに相当します。

リベースはマージコミットを作成する代わりに、元のブランチでコミットごとに新しいコミットを作成することによって履歴を書き換えます。

A B' 元は B C' 元は C main D E feature B C

ただし、rebase はコミット履歴を書き換える操作であるため、公開リポジトリや共有プロジェクトで使用する際には注意が必要です。

基本的に「公開リポジトリにプッシュしたコミットをリベースしてはいけない」とされています。

Git のブランチ機能 - リベース

マージとリベース

以下はブランチでの変更を取り込む場合にマージとリベースでの違いを確認する例です。

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
main 21e0a65 HEAD

続いて作業用の 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
main 21e0a65 feature HEAD

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 21e0a65 a9eb5d3 09b9ab5 feature HEAD

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
HEAD main 21e0a65 a9eb5d3 09b9ab5 feature

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つのコミットは反映されていません。

21e0a65 a9eb5d3 09b9ab5 main bf182a7 15d748e feature HEAD

この時、feature ブランチに main ブランチに追加された変更(新しい2つのコミット)を取り込むには、マージまたはリベースの2つの方法があります。

マージして取り込む

main ブランチを feature ブランチにマージするには、git merge コマンドを使用します。

feature ブランチに切り替えます。

% git switch feature
Switched to branch 'feature'
21e0a65 a9eb5d3 09b9ab5 main bf182a7 15d748e feature HEAD

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 ブランチに作成されます。

21e0a65 a9eb5d3 09b9ab5 1a54e3c main bf182a7 15d748e feature HEAD

マージは非破壊的な操作なので、既存のブランチは決して変更されません。但し、変更を取り込む必要が生じるたびに 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 ブランチのコミットを再作成します。

21e0a65 a9eb5d3 09b9ab5 main bf182a7 15d748e feature HEAD

共通の祖先から分岐する部分(赤丸)を main ブランチの最新のコミットをベースとなるように移動するイメージですが、正確には単に移動するのではなく、移動するブランチにつながるコミットの間からパッチ(差分)を取り出し、それを順番に適用することで新たにコミットを作り直します。

feature ブランチから main にリベースすると、あたかも最新の main ブランチから feature の開発を進めたかのような歴史を作る(歴史を改変する)ことができます。

21e0a65 7af8885 元は a9eb5d3 f46a952 元は 09b9ab5 main bf182a7 15d748e feature HEAD a9eb5d3 09b9ab5

上記の動作を実際のコマンドで確認します。

git rebase コマンド

現在は以下のような状態です(カレントブランチは feature)。

21e0a65 a9eb5d3 09b9ab5 main bf182a7 15d748e feature HEAD

以下のコマンドを実行して 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 に加えられた変更(コミット)により動作がおかしくなっていないかを確認することができます。

21e0a65 7af8885 元 a9eb5d3 f46a952 元 09b9ab5 main bf182a7 15d748e feature HEAD

但し、リベースはマージコミットを使用する代わりに、元のブランチでコミットごとにまったく新しいコミットを作成することによってプロジェクト履歴を再書き込みします(書き換えます)。

そのため、元の feature ブランチのコミットとリベース後の feature ブランチのコミットは異なるスナップショットを表していて、ハッシュ値も変わっています。

リベースして最新の main の修正を取り込んだ状態で、追加された機能が正しく動作することを確認したら、この変更を main ブランチにマージして取り込みます。

まず、main ブランチに戻ります。

% git switch main
Switched to branch 'main'

HEAD が main ブランチを指します。

21e0a65 7af8885 f46a952 HEAD main bf182a7 15d748e feature

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
21e0a65 7af8885 f46a952 HEAD main bf182a7 15d748e feature

-i オプション(interactive モード)

git rebase コマンドに -i または --interactive オプションを指定すると、対話(interactive)モードでコミットをまとめたり、コミットメッセージを変更することができます。

コミットをまとめる(squash)

git rebase コマンドにオプションを指定せずに実行した場合、リベース対象となっている全てのコミットがリベース先に移動しますが、-i オプションを付けて実行すると移動する予定のそれぞれのコミットについてどのようにするかを聞かれます。

以下はリベース前の状態です。

ecd2a21 4ded216 8c2fb26 main d12a913 eb16833 feature HEAD

以下はこの時点でのログです(先述の例のリベース前と同じですが、コミットハッシュは異なります)。

% 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

以下はリベース前の状態です。

ecd2a21 4ded216 8c2fb26 main d12a913 eb16833 feature HEAD

以下がリベース後の状態です。

ecd2a21 4ded216 と 8c2fb26 が統合 bf36b2b HEAD main d12a913 eb16833 feature

この変更を 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

ecd2a21 bf36b2b HEAD main d12a913 eb16833 feature
コミットを指定してリベース

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
main d8ae5dd 3e9bbb6 72e8b36 91b851e feature HEAD
コミットメッセージを変更

直前のコミットメッセージは 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
main リベースの対象となったコミットのハッシュ値が変更される d8ae5dd メッセージを変更したコミット 54d9057 a0a5530 2713817 feature HEAD
コミットを統合

ブランチを指定する場合と同様に、指定した範囲のコミットを統合する(まとめる)ことができます。

先述の例の続きで、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
main 3つのコミットが1つにまとめられた(ハッシュ値も変更される) d8ae5dd 494358a feature HEAD

リベース中のコンフリクト

マージ同様、リベースでも同じファイルを別々に変更していればコンフリクトが発生します。

マージは二つのブランチの修正を一度に取り込む操作なのでコンフリクトの解決を一度だけ行えば良いのに対して、リベースはリベース先のコミットに順番にパッチを適用していく操作なのでコンフリクトが発生する度に解決する必要があります。

言い換えると、リベースは各コミットごとにコンフリクトを検出し、その度にコンフリクトを解決します。

以下がリベースの場合のコンフリクトを解決する手順です。

  1. コンフリクトが発生しているファイルの内容を修正
  2. git add で対象のファイルをステージに追加
  3. 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 を指しています。

df380d4 b73127d 23d7625 HEAD main f0ee59a topic

topic ブランチを main ブランチにリベースするので、まず topic に切り替えます。

% git switch topic
Switched to branch 'topic'

HEAD は topic を指します。

df380d4 b73127d 23d7625 main f0ee59a topic HEAD

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
df380d4 9b3de67 元 b73127d 9329e72 元 23d7625 main f0ee59a topic HEAD

マージの場合

リベースの場合は、各コミットごとにコンフリクトの対応をしますが、マージの場合はコンフリクトの解決を一度に行います。

例えば、上記をリベースではなく 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