GitのTwo dotsとThree dots

こんにちは、ヌーラボの中村です。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

コミット履歴画面git log master

ファイルの差分

「ファイル差分」の画面は、dot なしで git diff した結果を表示する実装になっています。

  • git diff 1a93496c66 6d107886db

ファイルの差分画面git diff 1a93496c66 6d107886db

プルリクエスト

コミット履歴

「プルリクエスト」の「コミット履歴」の画面では、最新のベースブランチから最新のトピックブランチまでのログを出力します。

  • git log master..TEST1-2/fuga

プルリクエスト、コミット履歴画面git log master..TEST1-2/fuga

ファイル差分

「プルリクエスト」の「ファイル差分」の画面では、マージベースと最新のトピックブランチの差分を出力します。

  • git diff master...TEST1-2/fuga

プルリクエスト、ファイル差分画面

まとめ

git サブコマンドの diff と log で使用される Two dots(..) とThree dots(...)の違いについて説明しました。また、これらが Backlog の裏側にどう組み込まれているのかを説明しました。

git コマンドはオプションが多くて複雑ですが、ホスティングする側としてもキャッチアップの必要な箇所が多く、日々闘いを続けています。
この記事を読んで、「おや…?」と思った方、ぜひ Backlog の Git チームで一緒に働いてみませんか?

参考資料

公式ドキュメント

解説ブログなど(図あり)

開発メンバー募集中

より良いチームワークを生み出す

チームの創造力を高めるコラボレーションツール

製品をみる