branchとmerge|コマンドライン・Git・変更管理
このページはプロモーション(広告)を含みます。
コマンドライン・Git・変更管理(中級・読む30分+演習40分)
この授業のゴール
- ブランチが「commitを指す名前」であることを説明し、作業ごとにブランチを作って切り替えられる
- 早送り(fast-forward)になる条件と、マージcommitが作られる条件を履歴の形から判定できる
- 共通の祖先・自分・相手の3つの版から、ファイルごとにマージ結果または競合を判定できる
- Macの練習用リポジトリで branch・switch・merge を実行し、git log --graph で結果を確かめられる
仕事でどう使うか
チームの本線(main ブランチ)は、毎週の集計や本番の処理に使われる「いつでも動く版」です。そこに作りかけの変更を直接入れると、途中の失敗がそのまま全員に影響します。ブランチ(branch)を使うと、本線から枝分かれした場所で試し、完成してから本線に合流(merge:マージ)できます。
| 路線 | この授業の使いどころ |
|---|---|
| DS | 新しい外れ値ルールを feature/outlier で試し、結果を確認してから本線の週次集計に入れる |
| ML | 特徴量の追加やモデル変更を実験ごとのブランチに分け、良かった実験だけを本線に取り込む |
| DX | 業務ルール変更を担当者ごとのブランチで準備し、承認後に本線へ合流させる |
| RS | 共同執筆の章ごと、追加解析ごとにブランチを分け、査読対応の変更を区別する |
前提
前提:
この授業の模擬Gitは、ブランチとマージをファイル単位で判定する単純なモデルです。本物のGitは、ファイルの中の変更の塊(hunk)単位で判定するので、同じファイルの離れた場所を2人が直しても自動で合流できます。違いは「発展」で説明します。Macでの操作は本物のGitです。
身近な状況:本線を止めずに新しいルールを試したい
ハナさんのチームでは、毎週月曜に main ブランチの report.py で集計を出しています。ハナさんは外れ値を除く新ルールを試したいのですが、まだ正しいか自信がありません。同じ週に、同僚のケンさんは main で README の誤字を直します。
| 日 | ハナさん(新ルールの試行) | ケンさん(mainで小さな修正) |
|---|---|---|
| 月 | main から feature/outlier を作る |
月曜の集計は main で通常どおり |
| 火 | rules.txt を変更してcommit | README の誤字を直してcommit |
| 水 | 新ルールの確認が完了 | ― |
| 木 | main に feature/outlier をマージ |
両方の変更が main に入る |
ハナさんの試行中も main は動く版のままで、ケンさんの修正も失われません。これがブランチとマージの役割です。
直感的な説明:ブランチは付箋、マージは合流
commitの鎖を1本の道に並んだ写真だとすると、ブランチはその写真に貼った付箋です。
mainという付箋が最新の写真に貼ってある。git branch featureは、同じ写真にもう1枚付箋を貼るだけ。ファイルはコピーされません。- feature で作業してcommitすると、今いる付箋だけが新しい写真に貼り替わります。
- HEAD は「いまどの付箋を使っているか」を示す印です。
マージには2種類あります。
- 早送り(fast-forward):main の付箋のあとに、feature の写真がまっすぐ続いているだけなら、main の付箋を先へ移すだけで済みます。
- マージcommit:両方が別々に進んでいたら、2つの写真を合わせた新しい写真を作ります。この写真には親が2つあります。
たとえと実際の違い:付箋は自由に貼れますが、Gitでは「切り替える」と作業ツリーのファイルもその写真の内容に入れ替わります。だから、切り替えた先のブランチに無いファイルは、Finderからも見えなくなります(消えたのではなく、別のブランチにあります)。
正確な定義:ブランチ・切り替え・2種類のマージ
ブランチは「あるcommitを指す、動く名前」です。commitすると、HEADが指しているブランチが新しいcommitを指すように進みます。
| コマンド(実Git) | 働き |
|---|---|
git branch |
ブランチの一覧。* が今いるブランチ |
git switch -c 名前 |
今のcommitから新しいブランチを作って移る |
git switch 名前 |
ブランチを移る。作業ツリーとステージがその先のcommitの内容になる |
git merge 名前 |
指定したブランチを、今いるブランチに取り込む |
git branch -d 名前 |
マージ済みのブランチ名を消す(未マージなら拒否されるので安全) |
git log --oneline --graph --all |
全ブランチの履歴を枝分かれの図つきで表示 |
未commitの変更があり、切り替えでそれが上書きされる場合、git switch は拒否します。commitしてから切り替えるのが基本です。
マージの判定(git switch main してから git merge feature したとき)
- feature が main の祖先なら:取り込むものが無い(
Already up to date.)。 - main が feature の祖先なら:早送り。main を feature と同じcommitに進めるだけで、新しいcommitは作らない。
- どちらでもない(分岐している)なら:3方向マージ。共通の祖先(merge base:2本が分かれる直前のcommit)を探し、変更ごとに次の表で決める。
| 共通の祖先 | main(自分 ours) | feature(相手 theirs) | 結果 |
|---|---|---|---|
| X | X | Y | Y(相手だけ変えた) |
| X | Y | X | Y(自分だけ変えた) |
| X | Y | Y | Y(同じ変更) |
| X | Y | Z | 競合(両方が別々に変えた。人が決める) |
競合がなければ、親が2つのマージcommitが自動で作られます。競合の解き方は 競合とレビュー で学びます。
段階付き例題:早送りと3方向マージを見分ける
例1:早送り。main は c2 を指し、feature は c2 から c3, c4 と進んだ。
c1 ── c2 (main)
└── c3 ── c4 (feature)
main(c2)は feature(c4)の祖先です。git merge feature は main を c4 に進めるだけ(早送り)。新しいcommitは増えず、履歴は1本のままです。
例2:3方向マージ。feature が c3 に進む間に、main も c4 に進んだ。
c1 ── c2 ── c4 (main)
└── c3 (feature)
共通の祖先は c2。ファイルごとに判定します。
| ファイル | c2(祖先) | c4(main) | c3(feature) | 結果 |
|---|---|---|---|---|
| README.md | 誤字あり | 誤字修正 | 誤字あり | main の版(自分だけ変更) |
| rules.txt | 外れ値: なし | 外れ値: なし | 外れ値: 3σ | feature の版(相手だけ変更) |
| report.py | 版A | 版A | 版A | そのまま |
競合はないので、親が c4 と c3 のマージcommit c5 が作られ、両方の変更が main に入ります。
Macのターミナルで試す(実Git):~/se01-practice/git-demo で行います。
cd ~/se01-practice/git-demo
git switch -c feature/outlier
echo "外れ値: 3σを超える値を除く" > rules.txt
git add rules.txt
git commit -m "外れ値ルールを追加(3σ基準)"
git switch main
ls
git merge feature/outlier
ls
git log --oneline --graph --all
git switch main 直後の ls には rules.txt がありません(feature/outlier にだけあるため)。マージ後の ls には現れ、Fast-forward と表示されます。
次に分岐を作ります。
git switch -c feature/note
echo "注記: 速報値" >> report.md
git commit -am "レポートに速報値の注記を追加"
git switch main
echo "# 練習用リポジトリ" > README.md
git add README.md
git commit -m "READMEを追加"
git merge --no-edit feature/note
git log --oneline --graph --all
git commit -am は「追跡済みファイルの変更をすべてステージしてcommit」の省略形です(新規ファイルは含まれません)。--no-edit は、マージcommitのメッセージを既定の Merge branch 'feature/note' のまま使い、エディタを開かない指定です。--graph の表示で、線が分かれて再び合流する形が見えれば成功です。最後に git branch -d feature/note でマージ済みのブランチ名を片付けられます。
操作して理解する:付箋の動きと3方向の判定を模擬する
ブラウザ内の模擬です。まず、ブランチが「commitを指す名前」でしかないことを、辞書で確かめます。
# ブランチは「commitを指す名前」にすぎない、を辞書で確かめる(模擬)
parents = {'c1': None, 'c2': 'c1'} # commit -> 親
branches = {'main': 'c2'}
head = 'main'
def commit(new_id): # 今いるブランチの先に commit を足す
parents[new_id] = branches[head]; branches[head] = new_id
branches['feature'] = branches[head] # git branch feature(同じcommitを指すだけ)
head = 'feature' # git switch feature
commit('c3'); commit('c4') # feature で2回 commit
print('feature で作業後:', branches)
head = 'main' # git switch main
def is_ancestor(a, b): # a は b の祖先か
while b is not None:
if a == b: return True
b = parents[b]
return False
print('main は feature の祖先か:', is_ancestor(branches['main'], branches['feature']))
branches['main'] = branches['feature'] # 祖先なので早送り(fast-forward)できる
print('merge 後:', branches)
出力:
feature で作業後: {'main': 'c2', 'feature': 'c4'}
main は feature の祖先か: True
merge 後: {'main': 'c4', 'feature': 'c4'}
次に、3方向マージの判定規則を関数にして、4つのファイルに当てはめます。
# 3方向マージの判定規則(ファイルごと)。None は「ファイルが無い」
def decide(base, ours, theirs):
if ours == theirs: return ours, '両方同じ'
if ours == base: return theirs, '相手だけ変更 → 相手を採用'
if theirs == base: return ours, '自分だけ変更 → 自分を採用'
return None, '両方が別々に変更 → 競合(人が決める)'
files = { # ファイル名: (共通の祖先, main, feature)
'README.md': ('v1', 'v2', 'v1'),
'rules.txt': ('外れ値: なし', '外れ値: なし', '外れ値: 3σ'),
'report.py': ('A', 'B', 'B'),
'config.txt': ('0.5', '0.6', '0.7'),
}
for name, (b, o, t) in files.items():
print(f'{name:11}', decide(b, o, t))
出力:
README.md ('v2', '自分だけ変更 → 自分を採用')
rules.txt ('外れ値: 3σ', '相手だけ変更 → 相手を採用')
report.py ('B', '両方同じ')
config.txt (None, '両方が別々に変更 → 競合(人が決める)')
試してみよう:①config.txt の feature 側を '0.6' に変えると競合が消える理由を説明する。②共通の祖先を使わずに「main と feature が違えば競合」とすると、README.md と rules.txt まで競合になることを確かめる(祖先が必要な理由です)。
発展:本物のGitの判定単位と、マージ以外の統合方法
判定の単位:本物のGitは、ファイルを行の並びとして比べ、変更の塊(hunk)ごとに上の表を当てはめます。同じファイルでも、1行目と50行目を別々の人が直したなら自動で合流します。隣り合う行や同じ行を別々に直したときに競合になります。模擬Gitはファイル全体を1つの塊として判定するので、本物より競合が起きやすくなっています。
共通の祖先の求め方:本物のGitは、両方のブランチから親をたどって最も近い共通のcommitを探します(git merge-base main feature で表示できます)。模擬Gitは連番の最大値で代用しています。
マージ以外の統合:
| 方法 | 履歴の形 | 注意 |
|---|---|---|
| merge(マージcommit) | 枝分かれと合流が残る | 実際の経緯がそのまま残る |
| squash merge | ブランチの複数commitを1つにまとめて本線へ | 本線は読みやすいが、途中の経緯は本線に残らない |
| rebase | ブランチのcommitを本線の先に付け替えて一直線にする | commitが作り直され、IDが変わる。共有済みのブランチには使わない |
どれを使うかはチームの規約で決めます。初学者はまず merge を正しく使えることを目標にします。長く放置したブランチほど本線との差が大きくなり、競合が増えます。ブランチは小さく作って早めにマージするのが実務のコツです。
よくある失敗
失敗1:main で直接作りかけの作業をする
誤り:main で新ルールを書きかけのままcommitし、月曜の集計がその版で動いて数字がおかしくなった。
なぜ誤りか:main は「いつでも動く版」という約束で使われます。試行中の変更が混ざると、他の人の作業や定期処理に影響します。
直し方:作業を始める前に git switch -c feature/作業名 でブランチを作る。git branch で * の位置を確かめてからcommitする。まだ共有していなければ、main に入れてしまったcommitを新しいブランチに移すこともできますが、操作が難しいので、最初からブランチを作る習慣をつけます。
失敗2:マージの向きを逆にする
誤り:feature にいるまま git merge main を実行し、「main に新ルールが入った」と思い込んだ。
branches = {'main': 'c4', 'feature': 'c3'}
head = 'feature' # feature にいるまま
# git merge main → main の変更が「feature に」入り、feature が進む(ここでは結果を c5 とする)
branches[head] = 'c5'
print(branches) # main は c4 のまま
出力:{'main': 'c4', 'feature': 'c5'}。main は動いていません。
なぜ誤りか:git merge X は「X を今いるブランチに取り込む」操作です。
直し方:本線に取り込むときは git switch main → git merge feature。マージの前に git branch で今いる場所を確かめる。なお、feature で git merge main するのは「本線の最新を自分のブランチに取り込んで、競合を先に解いておく」ときに使う正しい操作でもあります。目的に合わせて向きを選びます。
失敗3:切り替えたらファイルが消えた、と慌てる
誤り:git switch main したら rules.txt が見えなくなり、Finderで探し回ったり、作り直したりした。
なぜ誤りか:切り替えると、作業ツリーはそのブランチのcommitの内容になります。rules.txt は feature のcommitにだけあるので、main では見えないのが正常です。git switch feature/outlier で戻れば現れます。
直し方:git log --oneline --graph --all と git branch で、ファイルがどのブランチにあるかを確かめる。また、未commitの変更は切り替え先に付いてくることがあり(ぶつからない場合)、どのブランチの作業かわからなくなります。切り替える前に git status で「変更なし」を確かめ、作業中ならcommitしてから切り替えます。
まとめ
- ブランチはcommitを指す動く名前。作ってもファイルは複製されず、commitすると今いるブランチだけが進む。
- git merge X は X を今いるブランチに取り込む。本線に入れるなら main に移ってからマージする。
- 取り込む側が今の祖先なら早送り、分岐していれば共通の祖先を使う3方向マージになる。
- 3方向マージは「変えた側を採用、両方が別々に変えたら競合」。編集と削除の組み合わせも競合になる。
- 切り替え前に git status で変更なしを確かめる。模擬Gitはファイル単位、本物のGitはhunk単位で判定する。
用語
- ブランチ(branch)
あるcommitを指す動く名前。commitすると今いるブランチが新しいcommitへ進む。
- マージ(merge)
別のブランチの変更を今いるブランチに取り込むこと。
- 早送り(fast-forward)
今のブランチが相手の祖先のとき、名前を相手のcommitまで進めるだけのマージ。新しいcommitを作らない。
- 共通の祖先(merge base)
2つのブランチが分かれる直前の、両方から到達できる最も近いcommit。3方向マージの比較の基準。
- マージcommit(merge commit)
分岐した2つの履歴を合流させるcommit。親を2つ持つ。
よくある質問
なぜブランチを作るのですか? main だけで気をつければよいのでは?
気をつけても、作業途中の版は壊れていることがあります。ブランチがあれば、本線をいつでも動く状態に保ったまま、複数の作業を並行して進め、レビューを経てから合流できます。
ブランチ名はどう付ければよいですか?
feature/outlier、fix/zero-amount のように『種類/内容』を半角英数字とハイフンで付けるのが一般的です。空白や日本語は避けます。チームに規約があればそれに従います。
早送りとマージcommitは、どちらが良いのですか?
優劣ではなく履歴の形の違いです。早送りは履歴が一直線で単純、マージcommitは『ここで合流した』記録が残ります。チームによっては git merge --no-ff で常にマージcommitを作り、作業のまとまりを残す規約もあります。
どこで失敗しやすいですか?
①今いるブランチの勘違い(git branch を見ない)、②マージの向きの逆、③未commitのまま切り替え、④長期間マージしないブランチ(差が広がり競合が増える)の4つです。
条件が変わって、相手のブランチが自分より新しい main から分岐していたら?
どちらも共通の祖先から別々に進んでいれば3方向マージになります。祖先がどのcommitかは git merge-base A B で確認できます。判定の規則は同じです。
仕事では、マージの前に何を確かめるべきですか?
①git branch で今いるブランチ、②git status で変更なし、③git log --oneline --graph --all で取り込む範囲、④取り込む変更の差分(git diff main...feature)とテスト結果です。チーム開発では、この確認をプルリクエストのレビュー(競合とレビュー)で行います。
確認問題
問1 git branch feature を実行した直後の状態として正しいものはどれか。
- ① プロジェクトのファイルがすべて複製される
- ② 自動で feature に切り替わる
- ③ 今のcommitを指す feature という名前が増え、今いるブランチは変わらない
- ④ main が削除される
解答と解説
正解:③
ブランチは名前(付箋)を作るだけで、ファイルは複製されません。切り替えは git switch で行います。
問2 main が c2、feature が c2→c3→c4 と進み、main には新しいcommitがない。git switch main → git merge feature の結果はどれか。
- ① 早送りで main が c4 を指し、新しいcommitは作られない
- ② 親が2つのマージcommitが必ず作られる
- ③ 必ず競合になる
- ④ feature が c2 に戻る
解答と解説
正解:①
main(c2)が feature(c4)の祖先なので、名前を進めるだけの早送りになります。
問3 共通の祖先で 0.5、main で 0.6、feature で 0.5 の設定ファイルを3方向マージすると、結果はどうなるか。
- ① 0.5(祖先の値)
- ② ファイルが削除される
- ③ 競合
- ④ 0.6(main の値)
解答と解説
正解:④
feature は祖先から変えていないので、変えた側(main)の 0.6 が採用されます。
問4 feature にいるまま git merge main を実行した。起きることとして正しいものはどれか。
- ① feature の変更が main に入る
- ② main の変更が feature に入り、main は動かない
- ③ 両方のブランチが削除される
- ④ 何も起きない
解答と解説
正解:②
git merge X は X を今いるブランチに取り込みます。本線に入れたいなら main に移ってからマージします。
問5 3方向マージで「自分はファイルを編集し、相手は同じファイルを削除した」。正しい扱いはどれか。
- ① 競合として人が判断する
- ② 編集を採用する
- ③ 削除を採用する
- ④ 祖先の版に戻す
解答と解説
正解:①
両方が祖先から別々に変えているので競合です。どちらかを黙って採用すると、編集が消えるか、削除の意図が無視されます。
問6 未commitの変更があるまま、別のブランチに切り替えようとした。最も安全な手順はどれか。
- ① 気にせず切り替える
- ② ブランチを削除して作り直す
- ③ git restore で変更を捨ててから切り替える
- ④ git status で確かめ、必要な変更はcommitしてから切り替える
解答と解説
正解:④
変更が切り替え先に付いてきたり、上書きを防ぐため拒否されたりします。commitしておけば、どのブランチの作業か明確になります。
次に学ぶこと
次は 競合とレビュー で、3方向マージで両方が別々に変えたとき(競合)の解き方と、変更を他の人に確認してもらうレビューを学びます。
commitや履歴に不安があれば commitと履歴 に戻りましょう。
キャリア学習:データサイエンティスト・機械学習エンジニア・DX推進・研究者を目指す人の学習アプリ。164モジュール・984授業を、ブラウザで動くSQLとPythonの演習で。基礎と各分野の最初のモジュールは無料。全部は2,980円の買い切り。
アプリを無料で開く有料版と無料版の違い