はじめに
Git Advent Calendar 2023 2日目の投稿です。よろしくおねがいします。
前回の記事ではgit init
からgit commit
までやりました。今回も初心者向けに、開発シーンでよく使うブランチを切ってマージするところまでGitを使ってみようと思います。
(今回も自分向けの頭の整理も兼ねてます)
準備
- 「git/sample」リポジトリを用意
- リポジトリに「hello」と書き込んだ「sample.txt」をコミットしておく
(前回の内容を進めていると上記の内容になってます)
git log
した状態(git log
はコミットの履歴を確認するコマンドです)
~/Desktop/git/sample (master)
$ git log
commit 9db45bbb0a8487fde3a648f5eb507768613f8018 (HEAD -> master)
Author: hogehoge <fugafuga>
Date: Wed Nov 8 10:51:13 2023 +0900
はじめてのコミット
ブランチとは
ブランチとは簡単に言うとある開発ラインから別の開発ラインへと分岐するための方法です。ここで、Gitがどのようにファイルを管理するかについて説明しつつ、ブランチが何なのか説明してみます。
(イメージがついてると以降の理解の助けになるかなと思って書きます。)
Gitは以下の4種類のオブジェクトを使ってファイルを管理しています。
-
ブロブ
- ある時点のファイル。
-
ツリー
- 1階層分のフォルダ情報。
- ブロブとツリーを参照する。
-
コミット
- 変更のメタデータ(ファイルの作成者とか、日付とか)。
- ツリーとコミットを参照する。
-
タグ
- 特定のオブジェクトに対して人が読みやすくなるようにした名前。
- コミットを参照する。
オブジェクトはすべてSHA1ハッシュ(40桁の16進数。例えば「a564b5f376a8cacf10b87369d122743bbd82fc19」みたいな値)の一意な名前で管理されます。
例えば、リポジトリの直下に「hello world」と書かれたファイルAと、「bye world」と書かれたファイルBを配置して、コミットしたときのオブジェクト間の関係を図示してみると以下のようになります。
コミットがツリーを、ツリーがブロブを参照してリポジトリの中身を表現しています。
つまり、コミットを参照することである時点のリポジトリの中身を知れます。
ここで、リポジトリの最新のコミットを参照するのがブランチです。
(おいおい、ちょっと待てよ、ブランチは開発ラインを分岐するための方法ちゃうんかい、どこがやねんと思った方、もう少し辛抱して下さい)
ブランチを複数作るとどうなるでしょうか。
同じコミットを参照するように新たにanother
ブランチを作ると以下の図のようになります。
今は同じコミットを参照している状態ですが、ここでanother
ブランチに対して作業をしてコミットをしてみると以下の図のようになります。
ブランチは最新のコミットを参照し続けるので、another
ブランチは最新のコミットを参照するようになり、master
ブランチは1つ前のコミットを参照したままの状態になります。
このようにして、master
ブランチはある時点のリポジトリの状態を表現することを保ったまま、another
ブランチではanother
ブランチ専用で作業を続けることができます(これを指して冒頭でブランチは開発ラインを分ける方法と言いました)。
リポジトリのデフォルトのブランチはmaster
という名前が付けられてて、ほとんどの開発ではこのmaster
を最も信頼できるソースコードの履歴として使っていると思います。
そして、他のブランチは、開発用だったりバグ修正用だったり様々な状況の開発ラインとして利用して、最後にmaster
に集める、、なんてことをすると思います。
ブランチを作る(ある開発ラインから別の開発ラインへと分岐する)理由は様々あります。
- 作業者ごとに開発できるようにする
- ソースコードのバージョンを分ける
…など
色々書きましたが、実務ではとりあえずはブランチを作ることとは自分だけの作業領域ができる理解でいいと思います。
git branch
では、実際にブランチを作ってみましょう。
準備として下記のコマンドを打って3回コミットしておきます(コミット何個かあった方がおもしろそうなので)。
説明:文字を「sample.txt」に書き込んでコミット
~/Desktop/git/sample (master)
$ echo "hello world" > sample.txt
~/Desktop/git/sample (master)
$ git add .
~/Desktop/git/sample (master)
$ git commit -m"2回目のコミット"
[master 437e558] 2回目のコミット
1 file changed, 1 insertion(+), 1 deletion(-)
説明:文字を「sample.txt」に書き込んでコミット
~/Desktop/git/sample (master)
$ echo "hello hello world" > sample.txt
~/Desktop/git/sample (master)
$ git add .
~/Desktop/git/sample (master)
$ git commit -m"3回目のコミット"
[master c4a4731] 3回目のコミット
1 file changed, 1 insertion(+), 1 deletion(-)
説明:文字を「sample.txt」に書き込んでコミット
~/Desktop/git/sample (master)
$ echo "hello hello world release" > sample.txt
~/Desktop/git/sample (master)
$ git add .
~/Desktop/git/sample (master)
$ git commit -m"1回目のリリース"
[master 630817a] 1回目のリリース
1 file changed, 1 insertion(+), 1 deletion(-)
説明していないコマンドとオプションをいくつか使っているので説明しておきます。
-
echo
はGitのコマンドではなくコンソールに文字を表示するLinuxのコマンドです。表示した文字は>
でファイルに上書きで転送することができます。 -
git add .
の.
は作業フォルダで編集があったすべてのファイルがインデックスに登録されます。ファイル名打つのが面倒な時かつすべてステージしてよい時に使ってます。
※ここで改行コードのwarningが出るかもしれませんが、いったんは気にしなくていいです。 -
git commit -m <msg>
で指定された<msg> をコミットメッセージとして使用します。エディタでの入力を省略できます。
こうすることで以下のコミットの履歴ができます。この図はコミットツリーと呼ばれます。
図の説明です。
- 丸がコミットです。
- 冒頭の図では、コミットの参照としてツリーが、ツリーの参照としてブロブがついていました。が、コミットが分かればある時点のリポジトリの状態がわかるのでツリーやブロブは省略して書いてます。
- また、参照の向きを表す矢印はどちらの方向か推測できるので省略してます。
上の図はコマンドでも確認することができて、確認するにはgit log --graph
を使います。
~/Desktop/git/sample (master)
$ git log --graph
* commit 5b09212c8a6602f39c9abe12266eff6c3f94fd35 (HEAD -> master)
| Author: hogehoge <fugafuga>
| Date: Fri Nov 10 09:52:31 2023 +0900
|
| 1回目のリリース
|
* commit 9baeecead59dcc40b594db13a9cfc0189f9545cf
| Author: hogehoge <fugafuga>
| Date: Fri Nov 10 09:51:52 2023 +0900
|
| 3回目のコミット
|
* commit d1d0866818415586c47819f032e80b9ac2a3dff4
| Author: hogehoge <fugafuga>
| Date: Fri Nov 10 09:50:15 2023 +0900
|
| 2回目のコミット
|
* commit 677b7f568e7a1563213e0de75677949449108703
Author: hogehoge <fugafuga>
Date: Fri Nov 10 09:48:03 2023 +0900
はじめてのコミット
新しいコミットから古いコミットへさかのぼる形です。
では、さっそくブランチを作りましょう。
git branch <branchname>
で現在のコミットから分岐して新たなブランチができます。また、ブランチの一覧を確認するにはgit branch
です。
~/Desktop/git/sample (master)
$ git branch another
~/Desktop/git/sample (master)
$ git branch
another
* master
git branch
で表示した際に*
がついているブランチが現在作業しているブランチです(ここではカレントブランチと言います)。
仮にこの後コミットをすると、カレントブランチであるmaster
ブランチにコミットがくっつきます。言い換えると、次のコミットはmaster
の参照先になります。
another
ブランチで作業を続けましょう。ブランチを切り替えるにはgit checkout <branchname>
を使います。
~/Desktop/git/sample (master)
$ git checkout another
Switched to branch 'another'
~/Desktop/git/sample (another)
$ git branch
* another
master
カレントブランチをあらわす*
がanother
ブランチに切り替わりました。
ちなみに、少し話がそれますがカレントブランチはGitではHEAD
で表示されます。
少しだけHEAD
の話をします。git log --oneline
をしてみましょう(--oneline
は各コミットのコミットメッセージの1行目だけのgit log
が表示されます)。
~/Desktop/git/sample (another)
$ git log --oneline
5b09212 (HEAD -> another, master) 1回目のリリース
9baeece 3回目のコミット
d1d0866 2回目のコミット
677b7f5 はじめてのコミット
(HEAD -> another, master)
がログに付与されています。この->
の意味は、「カレントブランチ(HEAD
)はanother
ブランチを参照している」という意味になります。
HEAD
も参照なので、図であらわすと以下のようになります。
git checkout <branchname>
で指定のブランチにHEAD
の参照が切り替わるわけです。
、、すみません、話が脱線しました。
さて、話を戻します。繰り返しの図になりますが、今は以下の状況でした。
ここで再度コミットを何度かしてみるとどうなるでしょうか。
説明:「sample.txt」の中身を更新してコミット
~/Desktop/git/sample (another)
$ echo "another hello" > sample.txt
~/Desktop/git/sample (another)
$ git add .
~/Desktop/git/sample (another)
$ git commit -m""anotherブランチで1回目のコミット
[another ec679ef] anotherブランチで1回目のコミット
1 file changed, 1 insertion(+), 1 deletion(-)
説明:「sample.txt」の中身を更新してコミット
~/Desktop/git/sample (another)
$ echo "another hello world" > sample.txt
~/Desktop/git/sample (another)
$ git add .
~/Desktop/git/sample (another)
$ git commit -m"anotherブランチで2回目のコミット"
[another 96a71fa] anotherブランチで2回目のコミット
1 file changed, 1 insertion(+), 1 deletion(-)
git log
~/Desktop/git/sample (another)
$ git log
commit 96a71fa7be0f180a0118ef8d090e9d51bb3d37df (HEAD -> another)
Author: hogehoge <fugafuga>
Date: Fri Nov 10 09:59:48 2023 +0900
anotherブランチで2回目のコミット
commit ec679efe389eb8167f70e9739e1959d27eff4a71
Author: hogehoge <fugafuga>
Date: Fri Nov 10 09:58:43 2023 +0900
anotherブランチで1回目のコミット
commit 5b09212c8a6602f39c9abe12266eff6c3f94fd35 (master)
Author: hogehoge <fugafuga>
Date: Fri Nov 10 09:52:31 2023 +0900
1回目のリリース
commit 9baeecead59dcc40b594db13a9cfc0189f9545cf
Author: hogehoge <fugafuga>
Date: Fri Nov 10 09:51:52 2023 +0900
3回目のコミット
commit d1d0866818415586c47819f032e80b9ac2a3dff4
Author: hogehoge <fugafuga>
Date: Fri Nov 10 09:50:15 2023 +0900
2回目のコミット
commit 677b7f568e7a1563213e0de75677949449108703
Author: hogehoge <fugafuga>
Date: Fri Nov 10 09:48:03 2023 +0900
はじめてのコミット
このようにして、master
ブランチでは最新のコミットがハッシュ値5b0921
を指し示し、another
ブランチでは開発を進めてハッシュ値96a71f
までコミットをすることができました。
ブランチを作って開発を進めることが出来ましたね。
git merge
次に、another
ブランチのコミットをmaster
ブランチに取り込んでみましょう。分岐したコミットの履歴を統合させることをマージと言います。
git merge <branchname>
でカレントブランチにbranchname
の履歴を取り込めます。カレントブランチへ取り込むことに注意です。
マージされたことが分かるようにここでは--no-ff
オプションを使ってマージします(後述します)。
~/Desktop/git/sample (another)
$ git checkout master
Switched to branch 'master'
~/Desktop/git/sample (master)
$ git merge --no-ff another
Merge made by the 'recursive' strategy.
sample.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
コミットメッセージを入力するエディタが表示されるので任意のコメントを入力してコミットしてください(ここではデフォルトの内容でいきます)。
git log --oneline --graph
をするとマージの状況が分かります。
~/Desktop/git/sample (master)
$ git log --oneline --graph
* 3124c16 (HEAD -> master) Merge branch 'another'
|\
| * 96a71fa (another) anotherブランチで2回目のコミット
| * ec679ef anotherブランチで1回目のコミット
|/
* 5b09212 1回目のリリース
* 9baeece 3回目のコミット
* d1d0866 2回目のコミット
* 677b7f5 はじめてのコミット
master
ブランチ上でanother
ブランチを取り込む操作をしたので、master
ブランチにanother
ブランチの内容を取り込むコミットが1つ生成されてます。
ファイルの内容を確認すると、another
ブランチの内容が反映されていることが分かります(コミットID96a71fa
の内容です)。
~/Desktop/git/sample (master)
$ cat sample.txt
another hello world
※cat
はファイルの中身を確認したいときに利用されるLinuxのコマンドです。
ここで、マージするときにつけた--no-ff
オプションについて説明しておきます。
Gitはマージするときにまず、縮退マージと呼ばれる以下の2つのマージを試みます。
-
Already up-to-date
- マージする側のブランチ(ここでいうと
master
)に、マージされる側のブランチ(ここでいうとanother
)からのコミットがすべて含まれている場合にGitから言われる。マージする内容がなく、コミットは追加されない。
- マージする側のブランチ(ここでいうと
-
Fast-forward
- マージされる側のブランチ(
another
)に、マージする側のブランチ(master
)のコミットがすべて含まれている場合におこなわれる。この際は、マージする側のブランチにマージされる側のコミットを付け足して、カレントブランチ(HEAD
)は付け足した最新のコミットに移動する。また、マージのコミットが作られない。
- マージされる側のブランチ(
今回は↑に書いた、Fast-forwardに当てはまります(another
ブランチがmaster
ブランチのコミットをすべて持っている状態)。
そのため、オプションをつけずに(--no-ff
をつけずに)git merge another
を行った場合は、Fast-forwardが行われてmaster
ブランチにanother
コミットで付け足した2つのコミットがくっつくだけの処理となります。
--no-ff
とはFast-forwardせずにマージせよ、というオプションになります。
マージが行われたことが分かりにくいので今回は--no-ff
をつけてマージコミットを作りました。
競合
次に、2つのブランチで同一箇所を修正してマージしてみましょう。
説明:masterブランチ上で「sample.txt」の中身を更新してコミット
~/Desktop/git/sample (master)
$ echo "master branch hello" > sample.txt
~/Desktop/git/sample (master)
$ git add .
~/Desktop/git/sample (master)
$ git commit -m"masterブランチでコミット"
[master 1cff7a9] masterブランチでコミット
1 file changed, 1 insertion(+), 1 deletion(-)
説明:anotherブランチ上で「sample.txt」の中身を更新してコミット
~/Desktop/git/sample (master)
$ git checkout another
Switched to branch 'another'
~/Desktop/git/sample (another)
$ echo "another branch hello" > sample.txt
~/Desktop/git/sample (another)
$ git add .
~/Desktop/git/sample (another)
$ git commit -m"anotherブランチでコミット"
[another 9a8b34b] anotherブランチでコミット
1 file changed, 1 insertion(+), 1 deletion(-)
master
ブランチとanother
ブランチで同じファイルに修正を行いました。
コミットツリーは以下のようになっています。
git show-branch
コマンドを使ってみます。
これはブランチが分岐してから現在までのコミットを表示するコマンドです。各ブランチのコミットをさくっと知りたいときに便利です。
~/Desktop/git/sample (another)
$ git show-branch
* [another] anotherブランチでコミット
! [master] masterブランチでコミット
--
* [another] anotherブランチでコミット
+ [master] masterブランチでコミット
*+ [another^] anotherブランチで2回目のコミット
--
の上が各ブランチの最新のコミット、--
の下が分岐後にブランチごとに何のコミットがあったかおおよそ時系列ごとに表示しています。すべてのブランチのログを見るgit log -all
の結果と一緒にみることで、先ほどマージした後にmaster
とanother
でそれぞれコミットがおこなわれたことが分かります。
~/Desktop/git/sample (another)
$ git log --oneline --all
21085b7 (HEAD -> another) anotherブランチでコミット
1cff7a9 (master) masterブランチでコミット
3124c16 Merge branch 'another' ←マージコミット
96a71fa anotherブランチで2回目のコミット
ec679ef anotherブランチで1回目のコミット
5b09212 1回目のリリース
9baeece 3回目のコミット
d1d0866 2回目のコミット
677b7f5 はじめてのコミット
それでは、master
ブランチにanother
ブランチをマージしてみましょう。
~/Desktop/git/sample (another)
$ git checkout master
Switched to branch 'master'
~/Desktop/git/sample (master)
$ git merge --no-ff another
Auto-merging sample.txt
CONFLICT (content): Merge conflict in sample.txt
Automatic merge failed; fix conflicts and then commit the result.
~/Desktop/git/sample (master|MERGING)
$
メッセージを日本語に訳すと「競合が発生してマージは失敗したよ。競合を解消して結果をコミットしてね。」とのことです。
先ほどのマージではanother
ブランチのみを修正してmaster
ブランチとマージしたので、競合が発生せず自動的にマージされました。しかし、今回は双方のブランチで修正をおこなっており、自動的にマージができず競合がおこっている状態です。
この場合は人間が解消して手動でマージコミットを作ってあげる必要があります。
また、Git Bushでは(master|MERGING)
のようにブランチ名の後ろにマージ中であることが表示されます。
私はこの競合が怖くてマージ操作ができなかったので、先に競合した際にマージ前の状況に戻る方法から書いておきます。マージ前の状況に戻るにはgit merge --abort
です。
~/Desktop/git/sample (master|MERGING)
$ git merge --abort
~/Desktop/git/sample (master)
注意点
ちなみに、マージは作業フォルダで作業されるので、マージ前に作業フォルダで作業中のファイルが無いようにした方がいいです。git merge --abort
が困難になる可能性があります。
では、競合を解消してみましょう。どこで競合が起きたかはGitがファイルに直接書いています(気になる方は「sample.txt」を開いてみてください)。
git diff
コマンドでどこに競合が発生したか確認してみましょう。
~/Desktop/git/sample (master|MERGING)
$ git diff
diff --cc sample.txt
index 81f4d78,2eb1448..0000000
--- a/sample.txt
+++ b/sample.txt
@@@ -1,1 -1,1 +1,5 @@@
++<<<<<<< HEAD
+master branch hello
++=======
+ another branch hello
++>>>>>>> another
マージする側(master
)の変更が<<<<<<<
と=======
の間、マージされる側(another
)の変更が=======
と>>>>>>>
の間に書いてあります。競合の解消は人間がどうしたいか決めてファイルを編集します。
ここではanother
の方を取り込むことにしましょう。今回は面倒なので、echo
コマンドで上書きます(実際は、修正者にどういう意図で編集したか確認したりしてよく考えてやりましょう)。
手動でファイルを修正してgit add
とgit commit
でマージコミットを作るとマージが完了します。
説明:競合が発生しているファイルを上書きで修正してコミット
~/Desktop/git/sample (master|MERGING)
$ echo " another branch hello" > sample.txt
~/Desktop/git/sample (master|MERGING)
$ git add .
~/Desktop/git/sample (master|MERGING)
$ git commit
[master a97dcd5] Merge branch 'another'
マージを2回おこなったのでgit log --oneline --graph
は以下のようになります。
~/Desktop/git/sample (master)
$ git log --oneline --graph
* a97dcd5 (HEAD -> master) Merge branch 'another'
|\
| * 21085b7 (another) anotherブランチでコミット
* | 1cff7a9 masterブランチでコミット
* | 3124c16 Merge branch 'another'
|\ \
| |/
| * 96a71fa anotherブランチで2回目のコミット
| * ec679ef anotherブランチで1回目のコミット
|/
* 5b09212 1回目のリリース
* 9baeece 3回目のコミット
* d1d0866 2回目のコミット
* 677b7f5 はじめてのコミット
また、マージの完了後にマージをした内容を破棄したい場合はgit reset --hard ORIG_HEAD
するとマージ前の状況に戻れます。
補足
ところで、マージをした時にMerge made by the 'recursive' strategy.
とでました。
~/Desktop/git/sample (master)
$ git merge --no-ff another
Merge made by the 'recursive' strategy.
sample.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
recursiveマージについて、簡単に補足しておきます。
マージの際はマージコミットを作り出すためのマージのアルゴリズムを選択できます(これをマージ戦略といいます)。
recursiveマージはマージ戦略の1つです。同時に2つのブランチのみ扱えて、すべてのマージ起点のもとになった共通的な位置に一時的なマージを作り出して、そこをマージ基点として通常の3wayマージをおこなう戦略です。
ところで3wayマージって?
以下の説明を、簡略化して和訳してみます(参照してくださいでもいいんですけど自分のために和訳してメモとして載せてます)。
マージのジレンマ
2人以上の開発者が同じファイルにそれぞれ変更を加え、あとでそのバージョンをマージしようとすると競合が起こる可能性があります。
しばらくの間、バージョン管理システムを忘れて、具体的なケースを考えてみましょう。2人の開発者、太郎と花子が同じ既存の「index.html」ファイルに変更を加え、最終的にそれぞれ1つの新たなリビジョンを作成したとします。この2つのリビジョンの変更を失うことなくマージするにはどうすればよいでしょうか?
<html>
<head>
<title>サンプル</title>
</head>
<body>
<ul class="animals">
<li>Mouse</li>
<li>Cat</li>
<li>Horse</li>
</ul>
</body>
</html>
<html>
<head>
<title>サンプル</title>
</head>
<body>
<ul class="animals">
<li>Moose</li>
<li>Cat</li>
<li>Dog</li>
</ul>
</body>
</html>
両者とも<ul>
ブロックの動物リストを編集していることがわかりますね。もし、この2つのリビジョンから最終的なマージのバージョンを作ることになった場合、あなたならどうするでしょうか?
以下のマージのジレンマが発生します。
- 太郎が
Moose
をMouse
に変えたのでしょうか?それとも、花子がMouse
をMoose
に変えたのでしょうか? -
Horse
とDog
はそれぞれの開発者が加えたのでしょうか?それとも、2人の開発者のどちらかもしくはどちらもがリストの最後の項目を以前の項目から更新したのでしょうか?
この2つのリビジョンの情報では、上記を知ることは不可能です。このシナリオは、2つのリビジョンを静的に比較して手動でマージさせる2wayマージと呼ばれます。
3wayマージ
上のように各開発者がそれぞれどのような変更をおこなったか手動でたどる代わりに、3wayマージがあります。最新のバージョン管理システムでは「最も近い共通の先祖」(マージ基点と言います)を自動的に見つけることができます。このマージ基点も合わせた3つのリビジョンを使って最終的なマージのリビジョンを作成することを「3wayマージ」と呼びます。
例えば、マージ基点が以下だったとしましょう。
<html>
<head>
<title>サンプル</title>
</head>
<body>
<ul class="animals">
<li>Moose</li>
<li>Cat</li>
</ul>
</body>
</html>
マージ基点を特定すれば、望ましい最終バージョンを作成するために何を変更したか見つけ出すのは簡単です。
マージ基点と太郎と花子のリビジョンの比較を見ると、マージ後の最終バージョンがどうあるべきかを理解するのがずっと簡単になります。
マージのジレンマの「1. 太郎がMoose
をMouse
に変えたのでしょうか?それとも、花子がMouse
をMoose
に変えたのでしょうか?」については、太郎が変更をしたことが明らかになります。
また、「2. Horse
とDog
はそれぞれの開発者が加えたのでしょうか?それとも、2人の開発者のどちらかもしくはどちらもがリストの最後の項目を以前の項目から更新したのでしょうか?」については2人の開発者それぞれがHorse
とDog
を追加したことが分かります。
これを踏まえると、マージコミットとして最終的なリビジョンを作成することができます。
要するに
3wayは3つのコミットを表しています。
①マージ元ブランチのポインタに該当するコミット
②マージ先ブランチのポインタに該当するコミット
③①、②の共通祖先となるコミット
「①と③の差分」と、「②と③の差分」をマージ元ブランチに取り込む方法を「3wayマージ」と言います。(↓の説明をお借りしました。)
ちなみに、今回使用したバージョン2.22ではrecursive
がデフォルト戦略として選択されますが、最新のバージョンのデフォルト戦略はort
というやつでした。
まとめ
今回はgit branch
をしてブランチを2本用意して、git merge
でブランチ間をマージしてみました。
間違いがあればコメントでご指摘いただけるとうれしいです
参考