Git基礎: commit、branch、merge

1. 概要

Gitはほぼすべてのプロジェクトで使用されているバージョン管理ツールですが、多くの初心者はいくつかのコマンドを覚えているだけで、その本質を理解していません。この記事では、3つの核となる概念を紹介します: commit(スナップショットを保存)、branch(独立した作業ブランチ)、merge(ブランチを統合)。この記事を読んだ後、自信を持って新しいブランチを作成し、正しくcommitし、mergeときにコンフリクトを処理し、mergeとrebaseをいつ使い分けるかを理解できるようになります。

2. Commit – スナップショットを保存する

2.1. Commitとは

各commitは、特定の時点でのファイルの完全な状態の「スナップショット」であり、親commitへのポインタが含まれています。Gitはdiffを保存するのではなく、スナップショットを保存します。これにより、branch/mergeが高速かつ効率的になります。

# リポジトリを初期化し、最初のcommitを実行する
git init order-service
cd order-service
echo "# Order Service" > README.md
git add README.md
git commit -m "docs: Order ServiceのREADMEを追加"

説明:

  • git init 新しいリポジトリを作成し、隠しディレクトリ .git を生成します。
  • git add README.md ファイルを staging area(commit待機エリア)に追加します。
  • git commit -m "..." スナップショットを作成し、変更を説明するメッセージを添付します。

履歴を確認:

git log --oneline
a1b2c3d docs: Order ServiceのREADMEを追加

2.2. 良いcommitメッセージを書く

commitは「この変更は何をするのか、なぜするのか」という質問に答える必要があります。一般的な規約(conventional commits):

feat: 注文作成APIを追加
fix: クーポン適用時に金額計算が間違う問題を修正
refactor: OrderValidatorをOrderServiceから分離

注意: "update"や"fix bug"のような曖昧なcommitメッセージを避けてください。数ヶ月後にgit logを見直すとき、そのcommitが何を修正したのかわかりません。

2.3. git add -pで部分的にcommitする

ファイルに複数の無関係な変更がある場合は、すべてを一度にcommitしないでください。

git add -p src/main/java/com/nonladev/OrderService.java

Gitはどの「hunk」(変更セクション)をstageするかを尋ね、複数の小さなcommitに分割できるため、レビューと必要に応じてrevertが容易になります。

3. Branch – 複数のブランチで独立して作業する

3.1. branchの作成と切り替え

branchは単に特定のcommitを指す軽いポインタです。新しいブランチを作成してもコード全体がコピーされることはなく、単に別のポインタが追加されます。

# 新しいbranchを作成して切り替える
git branch feature/add-payment
git checkout feature/add-payment

# または上記の2つのコマンドを1つにまとめる (Git ≥ 2.23)
git switch -c feature/add-payment

説明:

  • git branch <name> はbranchを作成しますが、現在のbranchに留まります。
  • git checkout <name>またはgit switch <name>はHEADをそのbranchに移動します。
  • git switchの-cは作成と切り替えを同時に行うショートカットです。

3.2. branchのリストを表示

git branch -v
* feature/add-payment  d4e5f6a PaymentService skeletonを追加
  main                 a1b2c3d docs: Order ServiceのREADMEを追加

*は現在のbranch(HEADがいるbranch)を示します。

3.3. 規約に従ったbranchの命名

branchに明確な名前を付けると、チーム全体がそのbranchが何をするかを理解できます:

feature/add-payment       # 新しい機能を追加
fix/order-total-wrong     # バグを修正
refactor/extract-validator
hotfix/prod-null-pointer

3.4. 完了後にbranchを削除

# mergeされたlocal branchを削除
git branch -d feature/add-payment

# mergeされていないbranchを削除 (強制削除、commitを失うので注意)
git branch -D feature/add-payment

-d(小文字)はより安全です。branchがどこにもmergeされていない場合、Gitは警告を表示します。

4. Merge – branchを統合する

4.1. Fast-forward merge

mainから分岐して以降、mainに新しいcommitがない場合、Gitは単にmainポインタをfeatureブランチの最後のcommitに移動させます。これをfast-forwardと呼びます。

git switch main
git merge feature/add-payment
Updating a1b2c3d..d4e5f6a
Fast-forward
 PaymentService.java | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

新しいmerge commitは作成されず、履歴は直線のままです。

4.2. Merge commit(3-way merge)

分岐点以降、両ブランチにそれぞれのcommitがある場合、Gitは2つの親を持つmerge commitを作成し、両ブランチの内容を統合します。

git switch main
git commit -am "fix: アイテム数の検証を修正"   # mainに新しいcommitがある
git switch feature/add-payment
git switch main
git merge feature/add-payment
Merge made by the 'recursive' strategy.
 PaymentService.java | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

Gitは自動的にmerge commitを作成し、デフォルトメッセージMerge branch 'feature/add-payment'が付けられます。

4.3. mergeときにコンフリクトを処理する

コンフリクトは、2つのブランチが同じ行のコードを修正したときに発生します。OrderServiceクラスの例:

// main内
public class OrderService {
    public BigDecimal calculateTotal(Order order) {
        return order.getSubtotal().multiply(BigDecimal.valueOf(1.1)); // 10%の税金を追加
    }
}
// feature/add-payment内
public class OrderService {
    public BigDecimal calculateTotal(Order order) {
        return order.getSubtotal().subtract(order.getDiscount()); // 割引を差し引く
    }
}

mergeするとき、Gitはどの行を保つかを自動的に決定できず、ファイルはマーカーが表示されます:

public class OrderService {
    public BigDecimal calculateTotal(Order order) {
<<<<<<< HEAD
        return order.getSubtotal().multiply(BigDecimal.valueOf(1.1));
=======
        return order.getSubtotal().subtract(order.getDiscount());
>>>>>>> feature/add-payment
    }
}

手動で正しいロジックを組み合わせ(例:割引を先に差し引き、その後税金を追加)、<<<<<<<、=======、>>>>>>>のマーカーを削除してから:

git add OrderService.java
git commit

注意: コンフリクト解決後のcommitは-mが不要です。Gitはデフォルトメッセージ付きでエディタを開き、これがmerge commitであることを明確に記録します。このメッセージをそのままにしておくのが十分です。追加のノートが必要な場合を除き。

4.4. Merge vs Rebase

基準 merge rebase
履歴 保持される、merge commitあり 履歴を書き直す、ブランチはフラット
安全性 古いcommitを編集しない 古いcommitを編集する(ハッシュが変わる)
使用時機 公開共有ブランチ(main、develop) 個人ブランチ、PR作成前に履歴をきれいにしたい場合
# featureブランチを最新のmainの上にrebaseする
git switch feature/add-payment
git rebase main

注意: pushされていて他の人が pull している branch を rebase しないでください。彼らの履歴を混乱させ、彼らが再度 pull するときに大量のコンフリクトを引き起こします。

5. JUnitでmergeシナリオをテストする

Gitはコードではありませんが、2つのbranchのmerge後、ビジネスロジックが壊れていないことを確認するテストを書くことができます。これがCIが常にmerge commitでテストを実行する理由です。セクション4.3でコンフリクト解決後のOrderServiceの例:

public class OrderService {

    public BigDecimal calculateTotal(Order order) {
        BigDecimal afterDiscount = order.getSubtotal().subtract(order.getDiscount());
        return afterDiscount.multiply(BigDecimal.valueOf(1.1));
    }
}
class OrderServiceTest {

    private final OrderService orderService = new OrderService();

    @Test
    void shouldApplyDiscountThenTax() {
        Order order = new Order(
                BigDecimal.valueOf(100),  // 小計
                BigDecimal.valueOf(10)    // 割引
        );

        BigDecimal total = orderService.calculateTotal(order);

        assertThat(total).isEqualByComparingTo(BigDecimal.valueOf(99.0));
    }
}

説明: 小計100から割引10を引くと90になり、それに10%の税金を掛けると99になります。このテストは「安全ネット」であり、2つのロジック(税金と割引)のmergeが相互に打ち消されないことを保証します。

✓ shouldApplyDiscountThenTax()

6. 小さなチーム向けの推奨ワークフロー

  1. 各タスクに対して常にmainから新しいbranchを作成します: git switch -c feature/xxx.
  2. 小さく、頻繁にcommitし、明確なメッセージを使用してください。
  3. Pull Requestを作成する前に、branchを最新のmainで更新してください:
    git switch feature/add-payment
    git fetch origin
    git merge origin/main
  4. ローカルマシンでコンフリクトを解決し(ある場合)、テストを再度実行してください。
  5. pushしてPRを作成し、CIがmainに統合する前にmerge commitでテストを実行させてください。

注意: 複数の人でチーム開発するときは、直接mainにcommitしないでください。常にbranch + PRを使用して、review と CI チェックを先に行ってください。

7. 一般的なミス

  • commitする前にgit addを忘れた: ファイルを編集しても、nothing to commit, working tree cleanと報告されます。これはファイルがまだstageされていないためです。
  • 誤ったbranchをmergeした: mergeする前に、常にgit statusとgit branchで現在位置を確認してください。
  • コード内にコンフリクトマーカーが残っている: <<<<<<<を削除し忘れるとコードがコンパイルされません。常にコンフリクト解決直後にbuild/testしてください。
  • 共有branchへのforce push: 共有されているbranchでrebaseした後のgit push --forceは、同僚の履歴を削除します。--force-with-leaseを使用し、個人用branchでのみ使用してください。

8. まとめ

commit、branch、mergeは、Gitを使う際に毎日使う3つの操作です。commitを理解することはコード履歴を明確にし、branchは並行作業を相互に影響させず、merge(およびコンフリクト解決方法)を理解することで、複数の人からのコードを統合するときに自信を持つことができます。小さなcommitを練習し、規約に従ったbranch命名を行い、常にmainへのmerge前にテストを実行してください。

gitversion-controlbranchmergedevops