Git diff patch 入門:使い方からコードレビューへの応用まで


Git diff patch 入門:使い方からコードレビューへの応用まで

はじめに

ソフトウェア開発において、バージョン管理システムGitはもはや不可欠なツールです。Gitを使うことで、コードの変更履歴を記録し、複数人での共同開発を効率的に行うことができます。そのGitの中核をなす機能の一つが、「差分」を扱うことです。具体的には、ある時点のコードと別の時点のコードの間の違いを検出するgit diffコマンド、そしてその差分情報をファイルとして保存した「パッチ」を作成・適用する機能です。

多くの開発者は、GitHubやGitLab、Bitbucketなどのプラットフォーム上で提供されるプルリクエスト(Pull Request)やマージリクエスト(Merge Request)を通じてコードレビューや共同開発を行っています。これらのプラットフォームは非常に便利で強力ですが、その裏側ではGitのdiffやpatchの概念が深く関わっています。そして、Gitのdiffやpatchを直接操作するスキルは、以下のような様々な状況で強力な力を発揮します。

  • インターネット接続がない環境でコード変更を共有したい
  • プロジェクトの標準的なワークフローがメーリングリストベースである
  • Pull Request/Merge Requestの機能だけでは対応できない、よりきめ細やかな差分操作を行いたい
  • 一時的な変更を別のブランチやリポジトリに簡単に適用したい
  • Gitの内部的な動作原理をより深く理解したい

この記事では、Gitのdiffコマンドの基本的な使い方から始まり、差分情報をファイルとして保存した「パッチ」の作成、適用、さらにはパッチを使ったコードレビューのワークフローまで、詳細かつ実践的に解説します。約5000語をかけて、Git diff patchの世界を網羅的に探求していきます。

この記事を読むことで、あなたは以下のことができるようになります。

  • Gitの様々な状態(ワーキングツリー、インデックス、HEAD、特定のコミット)間の差分を確認する
  • Unified Diffフォーマットの構造と意味を理解する
  • 任意のコード変更をパッチファイルとしてエクスポートする
  • パッチファイルを別のリポジトリやブランチに適用する
  • パッチ適用時のコンフリクトに対処する
  • git format-patchコマンドを使ってメール形式のパッチを作成する
  • パッチを使ったコードレビューの基本的な流れと利点、欠点を理解する
  • Git diff patchに関する一般的なトラブルシューティングができるようになる

それでは、Git diff patchの世界への旅を始めましょう。

Git diff の基本

まずは、Gitの差分を表示する最も基本的なコマンドであるgit diffから見ていきます。git diffコマンドは、Gitリポジトリ内の異なる二つの状態間の差分を表示するために使われます。ここでいう「状態」とは、主に以下のものを指します。

  1. ワーキングツリー (Working Tree / Working Directory): 現在ファイルシステム上に表示されている、編集中のファイルの状態です。まだgit addされていない変更が含まれます。
  2. インデックス (Index / Staging Area): 次のコミットに含まれる予定の変更を一時的に保持する領域です。git addされた変更がここに置かれます。
  3. HEAD: 現在チェックアウトしているブランチの最新コミットを指します。通常、これは最後にコミットされた時点の状態を表します。
  4. 特定のコミット (Commit): 過去の任意のコミット時点の状態です。コミットハッシュやタグ名などで指定します。

git diffコマンドは、これらの状態の組み合わせによって様々な差分を表示できます。

git diff の基本的な使い方

最も一般的なgit diffコマンドの使い方は以下の通りです。

  • ワーキングツリー vs. インデックス:
    bash
    git diff

    これは、ファイルシステム上の現在の状態(ワーキングツリー)と、git addでステージングされている状態(インデックス)との間の差分を表示します。つまり、まだステージングされていない変更を確認できます。

  • インデックス vs. HEAD:
    bash
    git diff --cached
    # または git diff --staged

    これは、git addでステージングされている状態(インデックス)と、最新のコミット(HEAD)との間の差分を表示します。つまり、次にコミットされる内容を確認できます。

  • ワーキングツリー vs. HEAD:
    bash
    git diff HEAD

    これは、ファイルシステム上の現在の状態(ワーキングツリー)と、最新のコミット(HEAD)との間の差分を表示します。つまり、最後にコミットしてから行った全ての変更(ステージング済みと未ステージングの両方)を確認できます。これはgit diffgit diff --cachedの結果を合わせたものと考えることもできます。

特定のファイルに対するdiff

リポジトリ全体ではなく、特定のファイルやディレクトリのみの差分を見たい場合は、コマンドの最後にパスを指定します。

bash
git diff <ファイルパス>
git diff <ディレクトリパス>
git diff --cached <ファイルパス>
git diff HEAD <ファイルパス>

例:src/main.c というファイルの、ワーキングツリーとインデックスの差分を見る。
bash
git diff src/main.c

特定のコミット間のdiff

特定の二つのコミット間の差分を見たい場合は、二つのコミットID(またはブランチ名、タグ名など)を指定します。

bash
git diff <コミットID1> <コミットID2>

このコマンドは、<コミットID1>の状態から<コミットID2>の状態への変更を表示します。

例:
* develop ブランチの先端と main ブランチの先端の差分を見る
bash
git diff develop main

* コミット a1b2c3de4f5g6h の差分を見る
bash
git diff a1b2c3d e4f5g6h

* 最新コミット(HEAD)と一つ前のコミット(HEAD~1)の差分を見る
bash
git diff HEAD~1 HEAD
# または単に git show HEAD

git show <commit> コマンドは、そのコミット自体の情報(コミットメッセージ、作者、日付など)と、そのコミットで導入された変更(親コミットとの差分)を表示します。これは git diff <parent_commit> <commit> と同じ差分を表示します。

また、特定のコミットからHEADまでの全ての差分を見たい場合は、以下のようにします。
“`bash
git diff <コミットID> HEAD

HEAD は省略可能

git diff <コミットID>
``
これは、
<コミットID>` 以降、HEADに至るまでの全てのコミットで導入された変更をまとめた差分として表示します。

diffの表示をカスタマイズするオプション

git diffコマンドには、差分の表示方法を調整するための様々なオプションがあります。

  • --stat: 変更されたファイルとそのファイル内の変更行数の概要を表示します。詳細なdiffは表示されません。
    bash
    git diff --stat

  • --word-diff: 行単位ではなく、単語単位での差分を表示します。テキストファイル内の微細な変更を確認するのに役立ちます。追加された単語や削除された単語が特殊な記号や色で強調表示されます。
    bash
    git diff --word-diff

  • --color: 差分を色付きで表示します(通常はデフォルトで有効ですが、明示的に指定することもできます)。--no-color で色を無効にできます。

  • -U<n> または --unified=<n>: 差分のコンテキスト行数(変更された行の前後に追加で表示される変更されていない行の数)を指定します。デフォルトは3行です。コンテキスト行数を減らすと、パッチファイルが小さくなりますが、変更箇所の特定が難しくなる場合があります。
    bash
    git diff -U1 # コンテキスト行数を1に設定

  • --binary: バイナリファイルの差分をGitが扱える形式で表示します。通常、バイナリファイルは差分が表示されず、「Binary files … differ」というメッセージだけが表示されますが、このオプションを使うことで、バイナリパッチを作成できるようになります。

  • --no-index: Gitリポジトリの一部ではない二つのファイルの差分を表示します。これは、二つの任意のファイルを比較したい場合に便利です。
    bash
    git diff --no-index <ファイルパス1> <ファイルパス2>

これらのオプションを組み合わせることで、見たい差分情報を効率的に取得できます。

Diff フォーマットの理解

git diffコマンドのデフォルトの出力形式は「Unified Diffフォーマット」と呼ばれます。これは、多くの差分表示ツールやパッチファイルで標準的に使用されている形式です。パッチの作成や適用、手動編集を行うためには、このフォーマットを理解することが不可欠です。

Unified Diffフォーマットは、以下のような要素で構成されています。

  1. ヘッダー (Header): 差分対象のファイルやコミット情報、タイムスタンプなどが含まれます。
    diff
    diff --git a/file1.txt b/file1.txt
    index abc123d..def456e 100644
    --- a/file1.txt
    +++ b/file1.txt

    • diff --git a/file1.txt b/file1.txt: 差分対象のファイルが file1.txt であることを示します。a/ は変更前のファイル、b/ は変更後のファイルを表す慣習的なプレフィックスです。
    • index abc123d..def456e 100644: オブジェクトID(ハッシュ)とそのファイルモード(ここでは通常のファイル権限)を示します。abc123d は変更前のファイルのインデックス(またはコミット)でのオブジェクトID、def456e は変更後のオブジェクトIDです。
    • --- a/file1.txt: 変更前のファイルを示します。パッチ適用時にこのパスを使って元のファイルを特定します。
    • +++ b/file1.txt: 変更後のファイルを示します。パッチ適用後に作成または更新されるファイルのパスです。
  2. チャンクヘッダー (Chunk Header): 差分があるブロック(チャンク)の開始位置と行数情報を示します。
    diff
    @@ -1,5 +1,6 @@

    • @@ ... @@: これがチャンクヘッダーであることを示します。
    • -1,5: 変更前のファイルにおける、このチャンクの開始行番号とそこから続く行数を示します。ここでは、1行目から5行分(つまり1~5行目)がこのチャンクの対象であることを意味します。
    • +1,6: 変更後のファイルにおける、このチャンクの開始行番号とそこから続く行数を示します。ここでは、1行目から6行分(つまり1~6行目)がこのチャンクの対象であることを意味します。
    • この例では、変更前の5行が、変更後には6行になっている(つまり1行増えている)ことが分かります。
  3. 差分の行 (Diff Lines): 実際の変更内容を示します。行頭の記号でその行の状態を表します。

    • (スペース): 変更されていないコンテキスト行です。差分箇所を特定しやすくするために、変更された行の前後数行が表示されます(デフォルトは3行)。
    • - (マイナス): 変更前のファイルに存在し、変更後のファイルから削除された行です。
    • + (プラス): 変更後のファイルに新しく追加された行です。

例:
diff
diff --git a/sample.txt b/sample.txt
index fedcba9..0123456 100644
--- a/sample.txt
+++ b/sample.txt
@@ -1,4 +1,5 @@
This is the first line.
This is the second line.
-This line will be removed.
This is the fourth line.
+This is a new line added.

このdiffは、sample.txtというファイルに対して行われた変更を示しています。
* 1行目と2行目は変更されていません()。
* 3行目(- This line will be removed.) は削除されました。
* その結果、4行目(- This is the fourth line. だったものが、変更後のファイルでは3行目になります)は、新しい行が追加された後に表示されます。
* 新しい行(+ This is a new line added.) が追加されました。
* チャンクヘッダーを見ると、変更前は1行目から4行まで(計4行)、変更後は1行目から5行まで(計5行)になったことが分かります。これは、1行削除され、1行追加された結果として、全体の行数が1行増えたことを示しています。

Unified Diffフォーマットを理解することは、単にdiffを見るだけでなく、パッチファイルを読み解き、必要に応じて手動で編集するためにも非常に重要です。

パッチの作成 (git diff > patchfile)

git diffコマンドの出力をファイルに保存することで、「パッチファイル」を作成できます。パッチファイルは、ある状態から別の状態へ変更するための指示書のようなものです。このファイルを使うことで、差分を適用したい他のリポジトリやブランチに、元の変更内容を簡単に再現させることができます。

パッチファイルが必要になる主な理由:

  • 変更の共有: インターネットに接続できない環境や、共同作業者が同じリポジトリにアクセスできない場合に、変更をファイルとして受け渡しできます。
  • コードレビュー: メーリングリストベースのプロジェクトや、より直接的なレビューを希望する場合に、変更内容をパッチファイルとして送付し、レビューを依頼できます。
  • 一時的な変更のバックアップ/移動: あるブランチで行った一時的な変更をパッチとして保存しておき、後で別のブランチに適用したり、元のブランチに戻したりできます。
  • 外部へのコントリビュート: オープンソースプロジェクトなどでは、変更内容をパッチファイルとして作成し、プロジェクトのメーリングリストに投稿するというワークフローが一般的です(特にLinuxカーネル開発など)。

git diff を使ったパッチファイルの作成方法

最も簡単な方法は、git diffコマンドの出力をリダイレクトすることです。

bash
git diff [オプション] [状態指定1] [状態指定2] [--] [パス] > /path/to/your/patchfile.patch

例:

  1. ワーキングツリーの全ての変更をパッチにする:
    bash
    git diff > my_changes.patch

    これは、まだステージングされていない(git addしていない)全ての変更をパッチファイルにします。

  2. ステージング済みの全ての変更をパッチにする:
    bash
    git diff --cached > staged_changes.patch
    # または git diff --staged > staged_changes.patch

    これは、git addでステージングされたが、まだコミットされていない全ての変更をパッチファイルにします。

  3. 最後にコミットしてから行った全ての変更(ステージング済み + 未ステージング)をパッチにする:
    bash
    git diff HEAD > all_unstaged_since_last_commit.patch

  4. 特定のコミット (a1b2c3d) から HEAD までの全ての変更をパッチにする:
    bash
    git diff a1b2c3d HEAD > changes_since_a1b2c3d.patch
    # または git diff a1b2c3d > changes_since_a1b2c3d.patch

    これは、a1b2c3d コミットに含まれていないが、その後HEADまでのコミットに含まれている全ての変更、およびHEADのワーキングツリー/インデックスにある変更(もしあれば)をまとめてパッチにします。通常、特定のコミット以降の「コミットされた変更」をパッチにする場合は、後述のgit format-patchを使用することが多いですが、git diff <commit> HEAD も有効な使い方です。

  5. 特定の二つのコミット (abc123ddef456e) の間の変更をパッチにする:
    bash
    git diff abc123d def456e > diff_between_commits.patch

    これは、abc123d の状態から def456e の状態にするための変更をパッチにします。

  6. 特定のファイル (src/utils.py) の変更のみをパッチにする:
    bash
    git diff HEAD~1 HEAD -- src/utils.py > utils_change.patch
    # または現在のワーキングツリーの変更のみなら
    git diff src/utils.py > utils_change_unstaged.patch

    パスを指定する前に -- を挟むのは、パスがブランチ名やコミットIDなどと誤解されるのを防ぐための慣習です。

パッチファイルの命名規則

特に厳密な決まりはありませんが、パッチファイルには.patch または .diff という拡張子を付けるのが一般的です。また、ファイル名に変更内容や作成者、日付などを含めると管理しやすくなります(例: feature_x_implementation_v1.patch, bugfix_issue_123.diff)。

git format-patch との違い

git diff > patchfile は、単にgit diffコマンドの出力をファイルに保存するだけです。これはUnified Diffフォーマットのテキストファイルになります。

一方、git format-patchコマンドは、一つ以上のコミットをメール形式のパッチシリーズとして作成するために設計されています。git format-patchは以下の点でgit diff > patchfileと異なります。

  • コミット情報: git format-patchで作成されたパッチファイルには、コミットメッセージ、作者、コミットハッシュなどの情報が含まれます。これは、後述するgit amコマンドで適用する際に、元のコミット情報を再現するために使用されます。
  • ファイル分割: git format-patchは、指定されたコミット範囲の各コミットに対して個別のパッチファイルを作成します(デフォルト)。これにより、一連の変更を個々の論理的なまとまりとして管理しやすくなります。
  • メールヘッダー: メール形式のパッチファイルとして、SubjectやFromなどのメールヘッダーが含まれます。これはメーリングリストへの投稿に適しています。
  • 用途: git diff > patchfile は、インデックスやワーキングツリーの変更、あるいは二つの任意の時点の差分を単純なdiffファイルとして保存するのに適しています。git format-patch は、既にコミット済みの変更を、元のコミット構造を保ったまま、特にメールを介して共有するのに適しています。

通常、他の人に適用してもらいたいコミット済みの変更をパッチとして渡す場合は、git format-patchを使用するのが推奨されます。単純な差分確認や、コミットにする前のワーキングツリーの変更を共有する場合は、git diff > patchfileも有用です。

パッチの適用 (git apply, git am)

作成したパッチファイルは、別のGitリポジトリや同じリポジトリの別のブランチに適用することで、元の変更を再現できます。パッチを適用するためのコマンドとして、主にgit applygit amの二つがあります。

git apply コマンド

git applyコマンドは、Unified Diffフォーマットのパッチファイルをワーキングツリーおよびインデックスに適用します。これは、git diff > patchfileで作成したパッチや、git format-patchで作成したパッチ(ただしコミット情報は無視されます)を適用するのに使えます。

基本的な使い方:

bash
git apply /path/to/your/patchfile.patch

このコマンドを実行すると、パッチファイルに記述された変更が現在のワーキングツリーに適用されます。変更は自動的にステージングされるわけではありません。適用後、git statusで変更を確認し、必要であればgit addしてステージング、git commitでコミットします。

git applyの主なオプション:

  • --check: パッチを実際に適用する前に、適用可能かどうか(コンフリクトが発生しないかなど)を確認します。問題がなければ何も出力せず終了し、問題があればエラーメッセージを表示します。パッチ適用前に必ず --check を実行するのが良いプラクティスです。
    bash
    git apply --check my_changes.patch

  • --reverse: パッチの変更内容を元に戻す(パッチを剥がす)ために使用します。
    bash
    git apply --reverse my_changes.patch

  • --index: パッチをワーキングツリーだけでなく、インデックスにも適用します。これにより、変更が自動的にステージングされた状態になります。
    bash
    git apply --index my_changes.patch

  • --reject: パッチ適用時にコンフリクトが発生した場合、パッチ全体を中断するのではなく、コンフリクトした部分のみを.rejファイルとして残し、それ以外の部分は適用します。コンフリクト箇所の.rejファイルを手動で解決する必要があります。
    bash
    git apply --reject my_changes.patch

  • --3way: 3-wayマージ戦略を使用してパッチを適用します。これは、パッチが作成されたベースとなるコミット(パッチファイルには通常含まれませんが、Gitが内部的に推測しようとします)と、現在のHEAD、そしてパッチ内容という3つの情報を使ってマージを試みる方法です。これにより、パッチの適用元と適用先のブランチが少し乖離している場合でも、コンフリクトをより適切に解決できる可能性があります。ただし、このオプションを使うには、パッチが3-wayマージに必要な情報(特に元のファイル名)を含んでいる必要があります。
    bash
    git apply --3way my_changes.patch

git applyの限界:

git applyはファイルシステムレベルでパッチを適用します。パッチファイルに含まれるコミット情報(作者、コミットメッセージなど)は無視されます。そのため、git applyでパッチを適用した場合、改めて自分でコミットを作成する必要があります。この性質から、git applyは主にワーキングツリーやインデックスの変更を共有・適用するのに適しています。

git am コマンド

git amコマンドは、「mbox形式」(Unixのメールボックス形式)のメール形式パッチファイルを適用するために使用されます。この形式のパッチは通常、git format-patchコマンドで作成されます。git amの最大の特徴は、パッチに含まれるコミット情報(作者、コミットメッセージ、コミット日付など)を読み取り、それを基に新しいコミットを作成することです。これは、メーリングリストを介したパッチ交換ワークフローで中心的に使用されます。

基本的な使い方:

“`bash
git am /path/to/your/patchfile.patch

またはパッチシリーズが複数ファイルに分かれている場合、ディレクトリを指定

git am /path/to/directory_containing_patches/
``
パッチファイルが複数ある場合(
git format-patchのデフォルト)、git am`はそれらを順番に適用し、各パッチから一つずつコミットを作成します。

git amの主なオプション:

  • --signoff (-s): 適用するコミットメッセージに Signed-off-by: 行を追加します。これは、変更内容をレビューし承認したことを示すために、プロジェクトによっては必須とされる慣習です(例: Linuxカーネル)。
    bash
    git am --signoff my_formatted_patch.patch

  • --continue: パッチ適用中にコンフリクトが発生した場合、手動でコンフリクトを解決し、git addでステージングした後、git am --continueを実行して適用プロセスを再開します。

  • --abort: パッチ適用中にコンフリクトが発生した場合、適用プロセスを完全に中止し、パッチ適用前の状態に戻します。
  • --skip: パッチ適用中にコンフリクトが発生した場合、コンフリクトしたパッチをスキップして、次のパッチの適用に進みます。

  • --3way: git apply --3way と同様に、3-wayマージ戦略を使用してパッチ適用時のコンフリクトを解決しようとします。git amは内部的にgit apply --3wayを使用することが多いため、コンフリクト発生時にこのオプションが役立ちます。

git applygit am の使い分け

  • git apply:

    • Unified Diffフォーマットのパッチを扱う。
    • ワーキングツリー/インデックスに適用する。
    • コミット情報は無視される。
    • 単純な差分ファイルの適用、ワーキングツリーの変更共有、git diff > patchfile で作成したパッチの適用に適している。
  • git am:

    • メール形式(mbox形式)のパッチを扱う。
    • パッチ内のコミット情報を読み取り、新しいコミットを作成する。
    • git format-patch で作成したパッチの適用、メーリングリストベースのワークフローに適している。

基本的には、git format-patchで作成されたパッチはgit amで、git diff > patchfileで作成されたパッチはgit applyで適用すると覚えておけば良いでしょう。ただし、git applyもメール形式パッチをある程度は扱えますが、コミット情報は失われます。

パッチファイルの編集

パッチファイルは単なるテキストファイルなので、テキストエディタを使って内容を直接編集することができます。これは、以下のような場合に役立ちます。

  • パッチから一部の変更だけを削除したい: 作成したパッチに含まれる変更の中で、特定のファイルや特定の変更箇所だけを除外したい場合。
  • パッチの適用パスを修正したい: パッチが作成されたリポジトリと、適用先のリポジトリでファイルのパス構造が異なる場合(例: src/file.c が適用先では source/file.c になっているなど)。
  • 差分内容を微調整したい: パッチ適用前に、差分のごく一部を手動で修正したい場合。

パッチファイルを編集する際の注意点:

  • Unified Diffフォーマットを崩さないこと: フォーマットを間違えると、git applygit amがパッチファイルを正しく解釈できなくなります。ヘッダー(---, +++)、チャンクヘッダー(@@ ... @@)、行頭記号(+, -, )の書式は厳密に維持する必要があります。
  • チャンクヘッダーと内容の一致: チャンクヘッダーに記載されている開始行番号と行数(-1,5, +1,6など)は、実際の差分内容と一致している必要があります。手動で差分行を削除したり追加したりした場合、対応するチャンクヘッダーの行数情報も正確に修正する必要があります。これは非常に面倒でエラーを起こしやすい作業です。
  • 編集の複雑さ: 編集が複雑になる場合は、パッチを編集するよりも、元のリポジトリでGitのインタラクティブな機能(git add -p, git reset -p など)を使って変更内容を調整し、改めてパッチを作成し直す方が安全で効率的な場合が多いです。

例:パッチファイル my_changes.patch から、README.md への変更を削除したい場合。

  1. パッチファイルをテキストエディタで開く。
  2. README.md に関するヘッダー行 (diff --git a/README.md b/README.md から次のファイルのヘッダー行、またはファイルの最後まで) を見つける。
  3. そのヘッダー行から、対応する---+++@@ヘッダー、そして全ての差分行(+, -, )を含めて、README.mdに関する全てのセクションを削除する。
  4. ファイルを保存する。

チャンクヘッダーを編集する例:元のチャンクが @@ -10,5 +10,6 @@ で、内容として5行が削除、6行が追加されている場合を考えます。もし、そのチャンクから削除されるべきだった1行を誤って残すようにパッチを編集した場合(つまり、削除行を-からに変更した場合)、パッチの実際の削除行は4行、追加行は6行になります。この場合、チャンクヘッダーは @@ -10,4 +10,6 @@ に修正する必要があります。このような手動計算はエラーの原因になりやすいです。

パッチを使ったワークフロー

パッチを使ったワークフローは、モダンなPull Request/Merge Request中心のワークフローとは異なりますが、特定の状況では非常に有効です。

1. オフライン環境での開発と共有

開発者Aがインターネットに接続できない環境で作業し、開発者Bにコード変更を渡したい場合。

  1. 開発者Aは、ローカルで行った変更をコミットするか、ワーキングツリーのままパッチを作成する。コミット済みの場合はgit format-patch、ワーキングツリーの場合はgit diff > patchfileを使う。
    bash
    # 例: 最新コミットからの変更をパッチに
    git format-patch HEAD~1
    # 例: ワーキングツリーの変更をパッチに
    git diff > my_offline_work.patch
  2. 開発者Aは、USBメモリやローカルネットワークなどを介してパッチファイルを開発者Bに渡す。
  3. 開発者Bは、パッチを受け取り、自身のGitリポジトリでパッチを適用する。
    • git format-patchで作成されたパッチの場合:
      bash
      git am my_offline_work.patch
    • git diff > patchfileで作成されたパッチの場合:
      bash
      git apply my_offline_work.patch
  4. 必要に応じて、開発者Bはパッチを適用した変更をレビューし、修正を加え、コミットしてメインのリポジトリにプッシュする。

2. メーリングリストベースのコードレビュー

Linuxカーネルなど、多くのオープンソースプロジェクトでは、メーリングリストを介したパッチによるコードレビューが一般的なワークフローです。

  1. 開発者は、変更内容を一つの論理的なまとまりごとにコミットしていく。
  2. レビューを依頼したいコミット範囲に対してgit format-patchを実行し、メール形式のパッチファイルを生成する。通常、-n オプションでパッチの通し番号を付けたり、--stdout で標準出力に出力してメール本文に貼り付けたり、またはパッチファイルを添付したりします。
    bash
    git format-patch -2 # 最新2コミットをパッチに
    git format-patch HEAD~5..HEAD # HEADから5コミット前までのパッチ
    git format-patch my_feature_branch origin/main # my_feature_branch にあるが origin/main にはないコミットをパッチに
  3. 生成されたパッチファイルを、プロジェクトのメーリングリストにメールとして送信する。メールの件名には、通常 [PATCH] というプレフィックスと、変更内容を簡潔に表すタイトルを付ける慣習があります。本文には、なぜこの変更が必要なのか、どのような問題を解決するのかといった説明を加えます。
  4. レビュー担当者(メンテナーなど)はメーリングリストからパッチメールを受け取る。
  5. レビュー担当者は、パッチメールをローカルに保存し、git amを使って自身のローカルリポジトリにパッチを適用する。
    bash
    git am /path/to/downloaded_patch.patch
  6. パッチ適用後、コードの内容をレビューする。コードの確認、ビルド、テスト実行などを行う。
  7. レビューの結果、修正が必要な場合は、メーリングリストへの返信としてコメントを送る。修正の提案や問題点の指摘などを行う。
  8. 開発者はレビューコメントに基づいてコードを修正し、必要であればコミットをamendしたり、新しいコミットを追加したりして、修正版のパッチを作成し、再度メーリングリストに送る(通常、件名に [PATCH v2] のようにバージョン番号を付けて送ります)。
  9. このプロセスを繰り返し、パッチが承認されるまでレビューと修正を行います。
  10. パッチが承認されたら、レビュー担当者はそのパッチを正式にプロジェクトのリポジトリにマージ(通常はgit amで適用されたコミットをそのまま含む形で)します。承認の印として、レビュー担当者はパッチに Reviewed-by:Tested-by:、そして最終的にマージする人が Acked-by:Signed-off-by: といった署名行を追加することがあります。

このワークフローは、分散開発の理念を体現しており、特定のプラットフォームに依存せず、メールという普遍的な手段で変更をやり取りできる利点があります。一方で、レビュープロセスが非同期になりがちで、特に大量の変更やコンフリクトが発生した場合の管理が複雑になるという側面もあります。

3. 一時的な変更の保存・共有

ある作業中に、別のタスクで一時的に必要な変更ができたが、まだコミットするほどではない、という場合があります。git stashも便利ですが、パッチとして保存するのも一つの方法です。

  1. ワーキングツリー/インデックスの変更をパッチに保存する。
    bash
    git diff HEAD > my_temp_work.patch
  2. 現在の作業をリセットする。
    bash
    git reset --hard HEAD
    git clean -fdx
  3. 別のタスクを行う。
  4. 一時保存した変更が必要になったら、パッチを適用する。
    bash
    git apply my_temp_work.patch

    これにより、my_temp_work.patch に保存されていた変更がワーキングツリーに戻ります。

高度な Diff/Patch のテクニック

バイナリファイルのDiff/Patch

通常、git diffはテキストファイルの差分を表示しますが、バイナリファイルについては「Binary files … differ」とだけ表示されます。しかし、--binaryオプションを使うことで、Git独自のバイナリ差分形式でバイナリファイルの差分を表示・パッチ化できます。

bash
git diff --binary <commit1> <commit2> -- <binary_filepath> > binary_change.patch

このパッチファイルは、テキストエディタで見ても人間には理解できない形式ですが、git apply --binarygit am コマンドはこれを正しく解釈し、バイナリファイルの変更を適用できます。

バイナリファイルの変更を含むコミットをgit format-patchでパッチ化した場合も、デフォルトでバイナリ差分が含まれます。これをgit amで適用すれば、バイナリファイルも正しく更新されます。

サブリポジトリ(Submodule)のDiff

Git Submoduleを使用しているリポジトリでgit diffを実行すると、サブリポジトリのコミットIDの変更が表示されます。

diff
diff --git a/submodule_dir b/submodule_dir
index abc123d..def456e 160000
--- a/submodule_dir
+++ b/submodule_dir
@@ -1 +1 @@
-Submodule commit abc123d
+Submodule commit def456e

この 160000 というファイルモードは、そのエントリがサブリポジトリであることを示します。パッチにはサブリポジトリ自体の内容の差分は含まれず、親リポジトリが参照するサブリポジトリのコミットの差分のみが含まれます。

サブリポジトリ自体の変更のパッチを作成するには、サブリポジトリのディレクトリに移動してそこでgit format-patchなどを実行する必要があります。

git format-patch (メール形式パッチの作成)

前述したように、git format-patchはコミット済みの変更をメール形式のパッチファイルとして作成するのに特化しています。これはメーリングリストベースのワークフローで非常に重要なコマンドです。

基本的な使い方とオプション:

  • 最後のN個のコミットをパッチにする:
    bash
    git format-patch -n

    例: 最新2コミットをパッチに (0001-xxx.patch, 0002-yyy.patch のようにファイルが作成される)
    bash
    git format-patch -2

  • 特定のコミットからの全コミットをパッチにする:
    bash
    git format-patch <commit-id>

    例: コミット a1b2c3d 以降全てのコミットをパッチに
    bash
    git format-patch a1b2c3d

  • 特定のコミット範囲をパッチにする:
    bash
    git format-patch <commit-id-from>..<commit-id-to>

    例: コミット a1b2c3d から e4f5g6h までのコミットをパッチに
    bash
    git format-patch a1b2c3d..e4f5g6h

    <commit-id-from> は範囲に含まれず、<commit-id-to> は含まれます。つまり、これは commit-to の親から commit-to までの差分ではなく、commit-from の次に導入された変更から commit-to までの各コミットを個別のパッチにします。

  • あるブランチにはあるが、別のブランチにはないコミットをパッチにする:
    bash
    git format-patch <base-branch>..<feature-branch>

    例: main ブランチにはなく、my-feature ブランチにあるコミットをパッチに
    bash
    git format-patch main..my-feature

  • 標準出力にパッチを出力する (--stdout): 個別のファイルを作成せず、全てのパッチを結合して標準出力に表示します。これを直接メールの本文に貼り付けたり、一つのファイルにリダイレクトしたりできます。
    bash
    git format-patch -2 --stdout > patches.txt

  • メールヘッダーや件名プレフィックスをカスタマイズする:

    • --subject-prefix=<prefix>: 件名のプレフィックスを変更します(デフォルトは PATCH)。例: --subject-prefix="REVIEW"
    • --add-header=<header>: 追加のメールヘッダー(例: Cc: [email protected])を追加します。
    • --to=<email>: To: ヘッダーを追加します。
    • --cc=<email>: Cc: ヘッダーを追加します。

git format-patch で作成されたパッチファイルには、以下のようなメールヘッダーが含まれます。
“`
From: Your Name your.email@example.com
Date: Mon, 1 Jan 2024 10:00:00 +0900
Subject: [PATCH 1/2] Add new feature
To: [email protected] # –to オプションで指定した場合
Cc: [email protected] # –cc オプションで指定した場合

[PATCH] Add new feature # 通常の件名(コミットメッセージの1行目などから生成)

… コミットメッセージ本文 …


… diff content …
``
この形式になっていることで、
git am`がメールヘッダーから差分内容を正しく解析し、コミット情報(作者、日付、メッセージなど)を復元できます。

git stash と diff/patch の連携

git stashは、作業途中の変更を一時的に退避させる便利な機能です。実は、stashの内容もパッチ形式で見たり、パッチとして出力したりできます。

  • 最新の stash の内容を diff として表示:
    bash
    git stash show -p

    これは、git stash apply したときにワーキングツリーに適用される変更内容をUnified Diff形式で表示します。

  • 特定の stash の内容を diff として表示:
    bash
    git stash show -p stash@{n}

    stash@{n}git stash list で確認できるstash IDです(例: stash@{0}, stash@{1})。

  • stash の内容をパッチファイルとして出力:
    bash
    git stash show -p > stash_patch.patch

    このように出力したパッチファイルは、git apply コマンドで適用できます。これは、特定のstashの内容を別のブランチやリポジトリに適用したい場合に便利です。

コードレビューへの応用

前述のメーリングリストワークフローで触れたように、パッチはコードレビューの強力な手段となり得ます。Pull Request/Merge Requestが主流となっている現代でも、パッチレビューには独自の利点があります。

パッチを使ったレビューの利点

  1. プラットフォーム非依存: 特定のWebサービス(GitHubなど)に依存しません。Gitとメールが使えればレビュー可能です。これは、クローズドな環境や、独自のインフラを使っている場合に特に有効です。
  2. シンプルな形式: パッチファイルはテキストベースであり、構造がシンプルです。Diffフォーマットを理解していれば、どのツールでも開いて内容を確認できます。
  3. オフラインレビュー: パッチファイルをダウンロードすれば、インターネット接続がない環境でもじっくりコードレビューを行うことができます。
  4. メール連携: メールは普遍的な通信手段であり、パッチを添付したり、本文に貼り付けたりして簡単に送受信できます。レビューコメントもメールの返信として行われるため、メールスレッドで議論の履歴が管理されます。
  5. Gitの低レベル理解: パッチを扱うことで、Gitが内部的にどのように変更を管理しているか、より深く理解できます。これは、トラブルシューティングや高度なGit操作を行う上で役立ちます。

パッチレビューのワークフロー(詳細)

レビュー依頼者側:

  1. 変更をコミットする: レビューしてほしい変更を、論理的な単位でコミットにまとめる。コミットメッセージは明確に書く。
  2. パッチを作成する: レビュー対象のコミット範囲に対してgit format-patchを実行する。
    bash
    git format-patch <base_commit> # または他のコミット指定方法

    -n オプションで通し番号を付けると、レビュー担当者が適用しやすくなります。
  3. パッチを送付する:
    • メーリングリスト宛に、パッチファイルを添付するか、--stdout で出力した内容をメール本文に貼り付けて送信する。
    • 件名に[PATCH][PATCH v2] (修正版の場合) などのプレフィックスを付ける。
    • メール本文に、パッチの概要、なぜこの変更が必要なのか、どのようにテストしたのか、レビューしてほしい特定の箇所などの説明を加える。
    • 場合によっては、担当者やレビューしてほしい人にTo:Cc:で指定する。

レビュー担当者側:

  1. パッチを受け取る: メールクライアントなどからパッチファイル(またはメール本文のパッチ内容)を取得する。
  2. パッチを適用する (git am または git apply):
    • 通常はgit format-patchで作成されたメール形式パッチなので、git amを使用する。
    • パッチ適用前に--checkで適用可能か確認するのが安全。
      bash
      git am --check /path/to/patchfile.patch
    • 問題なければ適用する。コンフリクトが発生した場合は、Gitの指示に従って手動で解決するか、--abortで中断する。
      bash
      git am /path/to/patchfile.patch
  3. コードをレビューする: パッチが適用されたローカルリポジトリで、コードの内容を確認する。
    • ファイル差分を確認する(git diff HEAD~1 など)。
    • コード全体を読んで品質、設計、セキュリティなどをチェックする。
    • 必要であればコードをビルドし、テストを実行する。
  4. レビューコメントを送る:
    • レビュー結果をメールで依頼者に返信する。元のパッチメールに返信する形式が一般的です。
    • 肯定的なフィードバック(Acked-by:, Reviewed-by: などの署名行を追加する提案)や、修正提案、質問、問題点などを具体的に記述する。
    • コードの特定の行に言及したい場合は、パッチの行番号やコンテキスト行を引用してコメントすると分かりやすい。
    • 修正提案自体を小さなパッチとして作成し、返信メールに添付することも可能です。

レビューコメントに対する修正と再送:

  1. 依頼者はレビューコメントを確認する。
  2. ローカルでコードを修正する。既存のコミットを修正する場合はgit commit --amendを使う。新しい変更を追加する場合は新しいコミットを作成する。
  3. 修正したコミットに対して再度git format-patchを実行する。この際、件名のプレフィックスを[PATCH v2]のようにバージョンアップするのが慣例です。
  4. 新しいパッチをメーリングリストに再送する。メール本文で、v1からの変更点や、レビューコメントへの対応について説明すると、レビュー担当者が変更を確認しやすくなります。
  5. このサイクルを、パッチが承認されるまで繰り返す。

パッチレビューが適しているケース

  • メーリングリストを主なコミュニケーション手段としているプロジェクト(Linuxカーネル、Git自体など)。
  • Pull Request/Merge Requestを提供するプラットフォームを使用できない、または使用したくない環境。
  • 小規模な変更や、レビュー担当者がオフラインでのレビューを希望する場合。
  • Gitの低レベルな操作を学習したい、または必要とする場合。

大規模プロジェクトでのパッチレビューの限界

  • 可視性: メールスレッドでの議論は追いにくくなることがある。特に複数のパッチシリーズが並行して流れている場合。
  • コンフリクト解決の複雑さ: 適用先のブランチが進んでいてコンフリクトが多発する場合、手動での解決がPull Requestのマージツールに比べて煩雑になる。
  • CI/CD連携: パッチがメールで送られるため、自動ビルドやテスト、静的解析などのCI/CDパイプラインとの連携がPull Requestほど容易ではない(ただし、ツールや仕組みによっては可能)。
  • レビュー担当者の負担: パッチの適用やコンフリクト解決など、レビュー担当者側の手作業が増える傾向がある。

現代の開発ではPull Request/Merge Requestが主流ですが、これはWeb UI上で差分表示、コメント、ステータス確認などを一元管理できるため、特にチーム開発やCI/CD連携において効率的です。しかし、その裏側ではGitのdiffやpatchの技術が使われています。パッチレビューのワークフローを理解することは、Gitの機能を深く理解し、様々な状況に対応できる能力を高める上で非常に価値があります。

トラブルシューティング

パッチの適用は、特に元のリポジトリと適用先のリポジトリで履歴が乖離している場合、コンフリクトなどの問題が発生しやすい操作です。

パッチ適用時のコンフリクト (Patch failed)

git applygit am を実行した際に、Gitがパッチの内容を現在のワーキングツリーに適用できない場合、コンフリクトが発生し、「Patch failed」のようなエラーメッセージが表示されます。

原因:
* パッチが作成された時点のコードの状態と、現在パッチを適用しようとしている時点のコードの状態が大きく異なる。
* 同じファイルの同じ箇所が、パッチ内容と現在のブランチの両方で変更されている。
* パッチ内で参照されているファイルが、適用先のリポジトリに存在しない、またはパスが異なる。

対処法:

  1. コンフリクトの発生箇所の確認: Gitはどのファイルでコンフリクトが発生したかをメッセージで示します。git status コマンドでも確認できます。
  2. コンフリクトの解決:
    • git apply --reject を使った場合: コンフリクトした変更は.rejファイルとして元のファイルの隣に作成されます。パッチの他の部分は適用されます。.rejファイルを見て、パッチで加えようとしていた変更内容を確認し、元のファイルに手動でその変更を組み込みます。
    • git am を使った場合: git am はコンフリクト発生時に停止します。コンフリクトしたファイルには、Gitのマージコンフリクトマーカー(<<<<<<<, =======, >>>>>>>)が挿入されます。これらのマーカーを参考に、手動でコードを編集してコンフリクトを解消します。
    • 手動解決のステップ (git am の場合):
      a. git status でコンフリクトしたファイルを確認する。
      b. テキストエディタでコンフリクトしたファイルを開き、<<<<<<<, =======, >>>>>>> マーカーを含むブロックを編集して、最終的に含めたいコードの状態にする。マーカー自体は削除する。
      c. コンフリクトを解消したファイルをgit addでステージングする。
      d. 全てのコンフリクトを解消し、ステージングしたら、git am --continue を実行してパッチ適用プロセスを再開する。
    • 3-wayマージ (git apply --3waygit am のデフォルト設定): パッチが3-wayマージに必要な情報を含んでいる場合、--3wayオプションがコンフリクト解決を助けることがあります。しかし、それでも自動的に解決できない場合は手動解決が必要です。
  3. パッチ適用の中止: コンフリクト解決が難しい場合や、パッチ適用自体を取りやめたい場合は、git am --abort (git am の場合) または git reset --hard / git clean -fdx (git apply の場合、パッチ適用前の状態に戻るには手動でのファイル復旧が必要なことも) を実行して、パッチ適用前の状態に戻します。

パスの不一致

パッチファイル内のファイルパス(a/path/to/file.c, b/path/to/file.c など)が、適用先リポジトリの実際のファイルパスと異なる場合、パッチ適用に失敗します。

対処法:
* git applyp<n> オプション: パッチ内のパスから先頭の <n> 個のディレクトリコンポーネントを取り除いて適用します。例えば、パッチファイルに a/src/file.c と書かれているが、適用先ではファイルが src/file.c である場合、-p1 オプションを使います (a/ を取り除く)。パッチファイルが a/dir1/dir2/file.c で、適用先が dir2/file.c なら -p2 を使います (a/dir1/ を取り除く)。
bash
git apply -p1 my_patch_with_offset.patch

git am にも同様の -p<n> オプションがあります。
* パッチファイルの手動編集: シンプルなパスの置換であれば、パッチファイルをテキストエディタで開き、全てのファイルパス(--- a/..., +++ b/..., diff --git a/... b/... の箇所)を一括置換するという手もあります。ただし、フォーマットを崩さないよう細心の注意が必要です。

改行コードの問題

パッチファイルを作成した環境と適用する環境で改行コード(CRLF vs LF)の扱いが異なる場合、パッチ適用に失敗したり、意図しない差分が発生したりすることがあります。Gitは通常改行コードの正規化を適切に行いますが、設定によっては問題が起こり得ます。

対処法:
* Gitの設定 core.autocrlfcore.eol を確認し、プロジェクト内で統一された改行コード設定を使用する。
* パッチを作成する前に、Gitがファイルの改行コードを正しく扱っているか確認する。
* パッチファイル自体を、適用先の環境に適した改行コードに変換するツール(dos2unix, unix2dos など)を使って変換する。

文字コードの問題

パッチファイルやソースコードファイルで使用されている文字コードが統一されていない場合も問題の原因となります。

対処法:
* プロジェクト全体で共通の文字コード(UTF-8が推奨されます)を使用する。
* Gitの設定 core.editor や使用するテキストエディタがUTF-8を正しく扱えるように設定する。
* パッチファイルが特定の文字コードで作成されている場合、適用先の環境でその文字コードを正しく扱えるか確認する。

まとめ

この記事では、Gitにおける差分(diff)とパッチ(patch)の概念について、その基本的な使い方から、Unified Diffフォーマットの理解、パッチの作成・適用方法、さらにはパッチファイルを使ったコードレビューのワークフローやトラブルシューティングまで、幅広く詳細に解説しました。

Gitのdiffコマンドは、ワーキングツリー、インデックス、コミットといった様々な状態間の違いを視覚化する強力なツールです。その出力をファイルに保存したパッチは、コード変更を共有、バックアップ、あるいはレビューするために用いられる、テキストベースの指示書です。git applyはファイルシステムレベルでパッチを適用し、git amはコミット情報を保持したままメール形式パッチを適用し、新しいコミットを作成します。

メーリングリストを介したパッチレビューワークフローは、特定のプラットフォームに依存しない分散開発の典型的なスタイルであり、Gitが本来持っている思想を色濃く反映しています。Pull Request/Merge Requestが主流となった現代においても、パッチを直接扱うスキルは、オフライン環境での作業、特定のプロジェクトへのコントリビュート、あるいは単にGitの内部的な動作を深く理解する上で非常に価値があります。

パッチの適用はコンフリクトなどの問題を引き起こす可能性もありますが、--check, --reject, --3way といったオプションや、手動でのコンフリクト解決、パスオフセットの指定(-p<n>)などの対処法を学ぶことで、これらの問題に対処できるようになります。

Git diff patchの世界は奥深く、本記事で紹介した内容もその一部に過ぎません。しかし、ここで解説した基本的な概念と操作を習得することで、あなたはGitを使った開発ワークフローにおいて、より柔軟かつ強力な力を手に入れることができるでしょう。

今後の学習として、Gitの公式ドキュメントや、パッチベースのワークフローを採用しているプロジェクト(Linuxカーネルなど)の開発プロセスやパッチ投稿ガイドラインなどを参照すると、さらに理解を深めることができます。

Git diff patchを使いこなし、あなたの開発ライフをより豊かにしてください。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール