Gitの差分|コミットする前に、変わった所を自分の目で読む

git 差分を確認しようとして git diff を叩いたのに、何も表示されない。あるいは大量に流れて読む気が失せる。どちらも同じ原因から来ている。Gitには変更を置く場所が3つあり、git diff はそのうちどの2つを比べるかを引数で決めるコマンドだからである。この記事では、比べる場所とコマンドの対応を先に固めて、そのうえで読みやすくするオプションと、文書ファイルを文字として比べる設定までを順に整理する。

差分が出ないのは、比べている場所が違うから

Gitは変更を3つの場所に分けて持っている。編集しているファイルそのものである作業ツリー、次のコミットに入れる分を置くインデックス、そして記録済みの履歴である。git diff を引数なしで叩いたときに出るのは、このうち作業ツリーとインデックスの差だけである。公式のマニュアルはこの形を「インデックス、つまり次のコミットのための待機領域に対して、自分が加えた変更を見るための形」と説明している。言い換えると、まだ git add していない分だけが出る。

だから git add を済ませた直後に git diff を叩くと、比べる対象が一致しているため何も表示されない。

ワークツリーとステージの間の変更差分なので git add した後に git diff をしても何も表示されません。 出典: qiita.com

このときインデックスに入った分を見るには git diff --staged を使う。--cached は同じ意味の別名で、どちらを書いても動きは変わらない。作業ツリーと履歴をまとめて、つまり git add した分もしていない分も一度に見たいときは git diff HEAD になる。この3つの形の違いを押さえるだけで、「差分が出ない」という状況はほぼ消える。

補足として、git status は差分の中身ではなく、どのファイルがどの場所にいるかを一覧にする。中身を読む前に場所を確認する道具として使うと、git diff に渡す引数を決める手間が減る。

比べる場所ごとのコマンドの対応

引数の形と、実際に比べられる2点の対応を1つの表にしておくと、毎回検索する必要がなくなる。

書き方 比べる2点 使う場面
git diff 作業ツリー と インデックス git add する前に読む
git diff --staged インデックス と HEAD コミットする直前に読む
git diff HEAD 作業ツリー と 直近のコミット 編集の全体量を見る
git diff <ファイル名> そのパスだけ 変更が多いときに絞る
git diff A B 2つのコミットまたはブランチ 相手の枝と自分の枝を比べる
git diff A...B AとBの共通の祖先 と B 分岐後にBで起きたことだけ見る
git diff --no-index a b Git外の2つのファイル 管理外のファイルを比べる

A..B と A...B の違いは間違えやすい。3点の A...B は、公式のマニュアルの説明では git diff $(git merge-base A B) B と同じ意味になる。つまり分岐点からBまでに起きたことだけが出る。2点の .. は、点を並べただけの形と同じで、AとBという2つの終端を比べる。マニュアルはこの点を明示していて、diff は範囲ではなく2つの終端を比べるものであり、.. と ... の記法はコミットの範囲を指す意味ではないと書いている。

--merge-base を付けると、渡したコミットの代わりにそのコミットとHEADの共通の祖先が使われる。git diff --merge-base A は git diff $(git merge-base A HEAD) と同じである。共有ブランチが進んでいる状態で自分の変更だけを読みたいときは、この形が素直だ。

--no-index は、Gitの管理下にない2つのファイルを比べる形である。Gitの作業ツリーの中で、少なくとも片方が作業ツリーの外を指しているときは省略できる。差分の色付けや -w のようなオプションがそのまま使えるので、設定ファイルの比較に使うと便利である。

送る前に、相手との差分を読む

ローカルの作業が終わったあとに読むべき差分は、手元の3つの場所の間ではなく、リモートとの間にある。順番は決まっている。

git fetch origin
git diff origin/main...HEAD
git log --oneline origin/main..HEAD

git fetch はリモートを追う参照を更新するだけで、ローカルのブランチには触らない。だから安全に叩ける。そのうえで git diff origin/main...HEAD を読むと、分岐してから自分が加えた内容だけが出る。相手が進めた分は混ざらない。件数と件名だけ確認したいときは git log --oneline origin/main..HEAD で、こちらは2点の .. が範囲として機能する。差分では範囲にならず、ログでは範囲になる。この非対称は、両者の役割が違うことから来ている。

git pull の前に読むときも同じ形が使える。git fetch してから git diff HEAD...origin/main を読めば、取り込んだときに何が変わるのかが先に分かる。取り込んでから慌てるのと、取り込む前に読むのとでは、戻す手間がまったく違う。

量だけ見る書き方と、名前だけ見る書き方

差分の全文が要らない場面は多い。どのファイルがどれだけ動いたかだけ知りたいときは、出力の形を切り替える。

git diff --stat
git diff --shortstat
git diff --compact-summary
git diff --name-only
git diff --name-status
git diff --diff-filter=MR --name-status

--stat はファイルごとの増減をグラフで並べる。--shortstat はその最後の1行だけを出すので、変更したファイル数と追加行数と削除行数の合計が1行で分かる。--numstat は同じ内容を機械が読みやすい10進数で出し、バイナリファイルは0ではなくハイフン2つになる。--compact-summary は、新規作成や削除、実行権限の増減といった情報を、ファイル名の後ろに短く添える形で表示する。

--name-only はファイル名だけ、--name-status は名前と状態の文字を出す。状態の文字は --diff-filter の説明と共通で、追加がA、コピーがC、削除がD、変更がM、リネームがR、種類の変更がT、未解決がU、不明がX、対応付けが壊れたものがBである。--diff-filter=MR のように並べると、変更とリネームだけが残る。小文字にすると除外の意味になり、--diff-filter=ad は追加と削除を外す。

レビューの前に「触ったファイルの一覧」を出したいとき、この2つの組み合わせは検索より速い。たとえば git diff --name-only HEAD~5 と書けば、直近5コミットで触ったファイル名が並ぶ。

読みにくい差分を、読める形にする

差分が読みにくい原因はだいたい3つに分かれる。空白の揺れ、文脈の不足、行の移動である。それぞれに専用のオプションがある。

git diff -w
git diff --ignore-blank-lines
git diff -U10
git diff -W
git diff --word-diff
git diff --color-moved
git diff --check

空白の揺れは -b と -w で消せる。-b は空白の量の変化を無視し、行末の空白も無視して、1文字以上の空白の並びを同じものとして扱う。-w はもっと強く、片方に空白があって片方にない場合まで含めて無視する。整形ツールを通した直後の差分を読むときに効く。改行コードだけが違う場合は --ignore-cr-at-eol、空行だけの変更を外すなら --ignore-blank-lines、特定のパターンに一致する行だけの変更を外すなら -I<正規表現> を使う。

文脈の不足は -U<n> で解決する。既定の前後3行では判断できないときに -U10 のように増やす。関数の全体を文脈として出す -W は、変更が関数の途中にあるときに効く。逆に離れた塊をつなげて読みたいときは --inter-hunk-context=<行数> を使う。

文章や設定値の1語だけが変わったときは、行単位の差分が読みにくい。--word-diff は語単位で比べ、削除を角括弧とハイフンで、追加を波括弧で囲んで出す。--color-words は色だけで示す形で、--word-diff=color と同じである。--word-diff-regex=. のように渡すと1文字ずつ比べられるので、日本語の文章の差分にも使える。

行が移動しただけの変更は --color-moved で見分ける。オプションを付けずに渡すと zebra という形式が既定になり、移動した行と本当に変わった行が別の色になる。設定として固定したいときは diff.colorMoved に書く。最後の --check は、差分が競合マーカーや空白の誤りを持ち込んでいないかを警告する。既定では行末の空白と、行頭の字下げの中でタブの直前にある空白が誤りとして扱われる。問題が見つかると終了コードが0以外になるので、コミット前の自動確認に組み込める。

差分そのものの作り方を変える手もある。diff.algorithm には default と同じ myers、最小の差分を狙う minimal、patience、そして patience を拡張して出現回数の少ない共通要素にも対応する histogram がある。並べ替えの多いコードで差分が滅茶苦茶に見えるときは、ここを変えると読める形になることがある。

塊ごとに読んで、選んでからコミットする

差分を読む目的は、読むこと自体ではなく、入れる分と入れない分を分けることである。その作業を1つのコマンドでまとめて行えるのが git add -p だ。差分の塊ごとに入れるかどうかを聞いてくるので、修正とリネームと消し忘れのデバッグ行が混ざった作業ツリーから、きれいなコミットを2つ作り、1行を捨てられる。

実際に開発をする時は、なんの変更をしたかを確認してから git add , git commit する癖を付けましょう。 出典: qiita.com

この習慣が効くのは、読みやすさのためだけではない。1つの意図しか含まないコミットだけが、あとから単独で取り消せて、別のブランチへ移せて、二分探索の判定対象として使える。逆に git add . で全部まとめて記録した履歴からは、1つの変更だけを引き抜けない。差分を読む手間は、将来の取り消しの手間と交換になっている。

選び終わったら、コミットの直前にもう一度 git diff --staged を読む。ここで出るのはインデックスとHEADの差、つまりこれから記録される内容そのものである。読む場所を1つに固定しておくと、確認の抜けが減る。対話的な差分表示を整形したい場合は interactive.diffFilter に通すコマンドを書ける。元の差分と行が1対1で対応していることが条件になる。

文書やバイナリの差分を、文字として読む

表計算や文書のファイルは、そのままではバイナリとして扱われて中身の差分が出ない。Gitには、比較の前に文字へ変換する仕組みが用意されている。textconv に変換用のプログラムを指定すると、そのプログラムがファイル名を1つ受け取り、変換結果を標準出力に出す形で動く。設定は .gitattributes で拡張子ごとに割り当てる。

この変換は一方向であることに注意が要る。マニュアルは、変換で情報が失われるため生成された差分はそのまま適用できないと明記していて、変換を行うのは git diff と git log の系統だけである。git format-patch はこの出力を作らない。つまり人が読むためのものであって、パッチとして送るためのものではない。

画面で見比べたいときは git difftool がある。これは git diff の前段として外部の比較ツールを呼ぶコマンドで、git diff と同じオプションと引数を受け取る。-d を付けると変更されたファイルを一時的な場所に写してディレクトリ単位で比べるので、ファイルを1つずつ確認する必要がなくなる。使うツールは diff.tool に書くか、その場で -t で渡す。一覧は git difftool --tool-help で出る。独自のコマンドを呼ぶなら difftool.<tool>.cmd に書き、変換前の一時ファイルが $LOCAL、変換後が $REMOTE として渡される。

どのファイル形式まで画面で扱えるかは道具によって差が出る。ファイル管理側で対応している形式や表示の範囲はできることのページに整理されていて、他の道具と設計がどう違うのかは他のファイル管理との比較に軸ごとにまとめられている。

差分を読む時間ではなく、往復の時間を数える

差分を読む作業そのものは、慣れれば1回あたり数十秒で終わる。時間が消えているのは、その前後にある往復のほうである。ターミナルで git diff --name-only を出す、出てきたファイル名をファイルの一覧から探す、別のディレクトリを開いているエディタに切り替える、開いて中身を確認する、ターミナルに戻って git add -p を叩く。この移動はGitのオプションを覚えても短くならない。窓の配置の問題だからである。

ファイルの一覧とターミナルとAIの応答が同じ窓で同じ作業ディレクトリを共有している状態なら、git diff の出力に並んだファイル名と、その実体が同時に視野に入る。差分を読んでからファイルを開くまでの操作が、切り替えを挟まずにつながる。長い処理を待っている間に席を離れる場合の続き方はiPhone・iPadから続きをに、日本語を含むファイル名の扱いに関係する表示言語の対応は対応言語に載っている。導入の費用感は料金で、対応環境や移行時の疑問はよくある質問で確認できる。

決める順番は3つでよい。まず git diff と git diff --staged と git diff HEAD の3つを、比べる場所として言い分けられるようにする。次に git add . を1週間だけ git add -p に置き換えて、読んでから選ぶ形に慣れる。最後に、共有ブランチへ送る前に git fetch と git diff origin/main...HEAD を通す手順に固定する。ここまでで、差分が出ないという状況と、読まずに送って戻す状況は、どちらも起きなくなる。

よくある質問

git diff を叩いても差分が表示されないのはなぜですか?

引数なしの git diff は作業ツリーとインデックスの差を表示するため、git add を済ませた時点で表示するものがなくなります。ステージに入った分は git diff --staged、git add した分もしていない分もまとめて見るには git diff HEAD を使います。どの2点を比べているかを先に決めるのが解決の早道です。

git diff A..B と git diff A...B はどう違いますか?

3点の A...B は、AとBの共通の祖先とBを比べるので、分岐したあとにBで起きたことだけが出ます。2点の A..B はAとBという2つの終端を比べるだけで、コミットの範囲を意味しません。範囲として使えるのは git log のほうで、git log A..B は範囲として機能します。

空白の変更ばかりで差分が読めないときはどうしますか?

-b を付けると空白の量の変化と行末の空白を無視し、-w を付けると片方にだけ空白がある場合まで無視します。空行だけの変更を外すなら --ignore-blank-lines、改行コードの違いだけなら --ignore-cr-at-eol を使います。整形ツールを通した直後の差分は、この組み合わせで読める形になります。

表計算や文書ファイルの中身を差分として読めますか?

textconv に変換用のプログラムを指定し、.gitattributes で拡張子ごとに割り当てれば、文字に変換してから比べられます。ただし変換は一方向で、生成された差分はそのまま適用できません。人が読むための表示であり、パッチとして送る用途には使えない点に注意してください。

記事一覧へ戻る