branchとmerge|コマンドライン・Git・変更管理

このページはプロモーション(広告)を含みます。

コマンドライン・Git・変更管理(中級・読む30分+演習40分)

この授業のゴール

仕事でどう使うか

チームの本線(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本の道に並んだ写真だとすると、ブランチはその写真に貼った付箋です。

マージには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 したとき)

  1. feature が main の祖先なら:取り込むものが無い(Already up to date.)。
  2. main が feature の祖先なら:早送り。main を feature と同じcommitに進めるだけで、新しいcommitは作らない。
  3. どちらでもない(分岐している)なら: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してから切り替えます。

まとめ

用語

ブランチ(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 を実行した直後の状態として正しいものはどれか。

  1. ① プロジェクトのファイルがすべて複製される
  2. ② 自動で feature に切り替わる
  3. ③ 今のcommitを指す feature という名前が増え、今いるブランチは変わらない
  4. ④ main が削除される
解答と解説

正解:③

ブランチは名前(付箋)を作るだけで、ファイルは複製されません。切り替えは git switch で行います。

問2 main が c2、feature が c2→c3→c4 と進み、main には新しいcommitがない。git switch main → git merge feature の結果はどれか。

  1. ① 早送りで main が c4 を指し、新しいcommitは作られない
  2. ② 親が2つのマージcommitが必ず作られる
  3. ③ 必ず競合になる
  4. ④ feature が c2 に戻る
解答と解説

正解:①

main(c2)が feature(c4)の祖先なので、名前を進めるだけの早送りになります。

問3 共通の祖先で 0.5、main で 0.6、feature で 0.5 の設定ファイルを3方向マージすると、結果はどうなるか。

  1. ① 0.5(祖先の値)
  2. ② ファイルが削除される
  3. ③ 競合
  4. ④ 0.6(main の値)
解答と解説

正解:④

feature は祖先から変えていないので、変えた側(main)の 0.6 が採用されます。

問4 feature にいるまま git merge main を実行した。起きることとして正しいものはどれか。

  1. ① feature の変更が main に入る
  2. ② main の変更が feature に入り、main は動かない
  3. ③ 両方のブランチが削除される
  4. ④ 何も起きない
解答と解説

正解:②

git merge X は X を今いるブランチに取り込みます。本線に入れたいなら main に移ってからマージします。

問5 3方向マージで「自分はファイルを編集し、相手は同じファイルを削除した」。正しい扱いはどれか。

  1. ① 競合として人が判断する
  2. ② 編集を採用する
  3. ③ 削除を採用する
  4. ④ 祖先の版に戻す
解答と解説

正解:①

両方が祖先から別々に変えているので競合です。どちらかを黙って採用すると、編集が消えるか、削除の意図が無視されます。

問6 未commitの変更があるまま、別のブランチに切り替えようとした。最も安全な手順はどれか。

  1. ① 気にせず切り替える
  2. ② ブランチを削除して作り直す
  3. ③ git restore で変更を捨ててから切り替える
  4. ④ git status で確かめ、必要な変更はcommitしてから切り替える
解答と解説

正解:④

変更が切り替え先に付いてきたり、上書きを防ぐため拒否されたりします。commitしておけば、どのブランチの作業か明確になります。

次に学ぶこと

次は 競合とレビュー で、3方向マージで両方が別々に変えたとき(競合)の解き方と、変更を他の人に確認してもらうレビューを学びます。

commitや履歴に不安があれば commitと履歴 に戻りましょう。

キャリア学習:データサイエンティスト・機械学習エンジニア・DX推進・研究者を目指す人の学習アプリ。164モジュール・984授業を、ブラウザで動くSQLとPythonの演習で。基礎と各分野の最初のモジュールは無料。全部は2,980円の買い切り。

アプリを無料で開く有料版と無料版の違い