こんにちは、ヌーラボの中村です。Backlog の Git チームで開発やメンテナンスをやっています。
最近Git機能に手を入れる際、Git コマンドの Two dots (..
) と Three dots (...
) がなかなかややこしいなぁと感じました。本記事ではそのあたりをまとめます。
※これはヌーラバー真夏のブログリレー2024の14日目の記事です。
目次
概要
git diff もしくは git logで比較対象のブランチ名を指定するとき、ドットを付けないのか・ドットを2つ(Two dots)にするのか・ドットを3つ(Three dots)にするのか・ドットを4つ以上付けるのか(実際はエラーになりますが……)迷う方も多いのではないでしょうか。私も昔は雰囲気で使っていました。
この記事ではドットの意味と、その内容を深堀って行きます。
git log と git diff のオプションの違い
git diff A..B と git diff A...B の違い
git diff A..B
(Two dots)は、シンプルにコミットAとコミットBの差分を表示します。これはgit diff A B
と同義です。
git diff A...B
(Three dots)は、コミットAとコミットBが枝分かれしているときに使用します。枝分かれしている地点とコミットBの差分を表示します。この枝分かれしている地点を「マージベース」といいます。
git diff A...B
は以下のコマンドと同義になります。
git diff $(git merge-base A B) B
git diff –merge-base A B
これを見ると、マージベースと比較していることがわかります。
git log A..B と git log A...B の違い
……の前に、まずはドットなしの場合の説明をします。
git log A
は、コミットAから始まるコミットログを、git log A B
は、コミットAとコミットBいずれかから到達可能なコミットログを表示します。指定する順番に意味はありません。
また、コミットは^A
などと指定することもでき、^
が付くとnot条件となります。ちなみにgit log –all
はすべてのコミットログを表示します。
git log A..B
(Two dots)は、コミットAからコミットBまでの間のコミットを表示します。古い方を左に指定します。ドットを使う場合、順番に意味があります。これは以下のコマンドと同義です。
git log B ^A
つまり、「AからB」は「Aからは到達不能で、Bからだけ到達可能なコミット」と言い換えることができます。
git log A…B
(Three dots)は、コミットAから始まるコミットログにだけ含まれるコミットと、コミットBから始まるコミットログだけに含まれるコミットを表示します。これは以下のコマンドと同義です。
git log A B --not $(git merge-base --all A B)
つまり、「AまたはBから到達可能で、それぞれのマージベースの手前まで」のものが取得されます。
git diff と git log のオプションは別物
ここまでに書いたように、オプションの「A B
」「A..B
」(ドット2つ)「A...B
」(ドット3つ)は、git diff と git log とでは全然異なる意味を持ちます。実はコマンドのSYNOPSIS(概要)を読むとその違いがわかります。
git diff [<options>] [<commit>] [–] [<path>…]
git diff [<options>] –cached [–merge-base] [<commit>] [–] [<path>…]
git diff [<options>] [–merge-base] <commit> [<commit>…] <commit> [–] [<path>…]
git diff [<options>] <commit>…<commit> [–] [<path>…]
git diff [<options>] <blob> <blob>
git diff [<options>] –no-index [–] <path> <path>
git log [<options>] [<revision-range>] [[–] <path>…]
オプションの太字にした箇所を見ると、git diff は <commit> もしくは <blob> を対象としている一方、git logは <revision-range> を対象にしています。
<revision-range> はリビジョンの範囲を指定するためのもので、「A B
」「A..B
」「A...B
」以外にも色々な指定の仕方があります。詳細な仕様は gitrevisions に記載されています。
実装者の視点
Backlog の Git 機能のバックエンドは主に Go 言語で記述されています。内部的な git の操作は、直接 git コマンドを呼び出す場合もあれば、git2go というライブラリを使うこともあります。
このライブラリを使用する場合、History(git log)を取得するコードは以下のようになり、”...
”をオプションとしてそのまま使うことができません。
// 「git log from..to」の情報(walk)を取得 fromCommit, err := revisionToCommit(from) // リビジョン名からCommitオブジェクトを取得 if err != nil { // 省略 } toCommit, err := revisionToCommit(to) // リビジョン名からCommitオブジェクトを取得 if err != nil { // 省略 } walk, err := r.repo.Walk()
バックエンドのプログラムは、受け取ったブランチがマージベースを指すのかそうでないのかを判断して扱う必要があります。
ここまで書いたように、git diff と git log では Two dots と Three dots で参照するコミットが異なるため、気をつけて実装する必要があります。(実際開発しているとややこしいです)
Backlogの裏側のTwo dotsとThree dots
では実際に Backlog の裏側でどのように Two dots と Three dots を使い分けているのかを示します。
※2024年8月1日時点での仕様になります
コミット履歴
「コミット履歴」の画面は、リビジョンを1つだけ指定してgit log した結果を表示する実装になっています。
git log master
ファイルの差分
「ファイル差分」の画面は、dot なしで git diff した結果を表示する実装になっています。
git diff 1a93496c66 6d107886db
プルリクエスト
コミット履歴
「プルリクエスト」の「コミット履歴」の画面では、最新のベースブランチから最新のトピックブランチまでのログを出力します。
git log master..TEST1-2/fuga
ファイル差分
「プルリクエスト」の「ファイル差分」の画面では、マージベースと最新のトピックブランチの差分を出力します。
git diff master...TEST1-2/fuga
まとめ
git サブコマンドの diff と log で使用される Two dots(..
) とThree dots(...
)の違いについて説明しました。また、これらが Backlog の裏側にどう組み込まれているのかを説明しました。
git コマンドはオプションが多くて複雑ですが、ホスティングする側としてもキャッチアップの必要な箇所が多く、日々闘いを続けています。
この記事を読んで、「おや…?」と思った方、ぜひ Backlog の Git チームで一緒に働いてみませんか?
参考資料
公式ドキュメント
- https://git-scm.com/docs/git-diff
- https://git-scm.com/docs/git-log
- https://git-scm.com/docs/gitrevisions