🌲

Gitの参照周りについて整理する

2024/05/07に公開
2

Gitの参照周りの概念や用語があまり理解できていなかったので整理してみようと思った。

まとめ

Git References

Commit Hash

  • コミットの識別子
  • 以下データのSHA1ハッシュを計算したもの
    • 一番上のGit TreeオブジェクトのSHA1ハッシュ値
    • 親コミットのハッシュ値
    • Author
    • Committer
    • タイムスタンプ
  • タイムスタンプを含むので同じデータに対するコミットでも時刻によりハッシュ値は違うものになる
  • Git公式の用語はなさそう[1]
    • GitHubではCommit ID[2]と呼ばれる
dc948eb27d5150c72b09750e856c015024666bd1
  • git rev-parse コマンドでrefをcommit hashとして解決してくれる
$ git rev-parse main
dc948eb27d5150c72b09750e856c015024666bd1

参考

Reference

  • Commit Hashへのエイリアス
  • refと呼ばれる
  • 実態は .git/refs に保存されている
.git/refs/
  -- heads/          ローカルブランチ refs/heads/mainなど
  -- remotes/origin/ リモートブランチ refs/remotes/origin/mainなど
  -- tags/           タグ refs/tags/v0.0.1など

Short name

単にmainやHEADと指定するとGitは ${GIT_DIR}/<refname> > refs/<refname> > refs/tags/refname > refs/heads/<refname>> refs/remotes/<refname> という順番で同名のファイルがないかを探して見つかるとそのrefとして解決してくれる。

  • git rev-parse --symbolic-full-nameでどういう参照として解決されるかが確認できる
# ローカルブランチ
$ git rev-parse --symbolic-full-name main
refs/heads/main

# リモートブランチ
$ git rev-parse --symbolic-full-name  origin/main
refs/remotes/origin/main

# タグ
$ git tag
v0.0.1
$ git rev-parse --symbolic-full-name v0.0.1
refs/tags/v0.0.1

Packed ref

  • .git/refsディレクトリの内容を圧縮したもの
  • レポジトリが大きいと git gc の際に圧縮されることがある
  • 能動的に実行するには git pack-refs --all を実行する(unpackする方法はgitでは提供されていない)

Special ref

  • 主にGitが内部で利用している特殊な参照
  • .git以下にファイルとして保存されるが、内容はそれぞれの種類ごとに異なる
  • 普段の作業でユーザが利用するのはHEADくらい
  • 参照 概要
    HEAD 現在のブランチ。これが指しているコミットに新たなコミットが追加される。通常はSymbolic refだが、detached状態の場合はCommit Hashになる。
    FETCH_HEAD 直近でフェッチされたブランチ。git pull = git fetch + git merge FETCH_HEAD
    ORIG_HEAD git merge や git rebase など大きな変更がある場合に直前のHEADの位置を保存するために作られる。git reset --hard ORIG_HEADで元に戻すことができる。

Symbolic ref

  • 他のrefを指すref
  • HEADは通常symbolic ref
$ cat .git/HEAD
ref: refs/heads/main

Refspec

  • リモートサーバー上のブランチとリモート追跡ブランチ(remote-tracking branch)をマッピングを表記するフォーマット
[+]<src>:<dst>

という形式をしている。<src> はリモートサーバー上のブランチ、 <dst> はリモート追跡ブランチを指す。 fetchしたときにデフォルトではfast-forwardマージができないとfetchが失敗するが、 + をつけると強制的にリモート追跡ブランチを更新する。

Gitのブランチのローカルとリモート

あるブランチには3種類ある
(1) ローカルブランチ ローカルマシン上の refs/heads/
(2) リモート追跡ブランチ ローカルマシン上の refs/remotes/<origin name>/
(3) リモートサーバ上のブランチ サーバマシン上の refs/heads/
Refspecは(2)と(3)のマッピングを定義するもの。(1)と(2)のマッピングはconfigの branch.<branch-name>.merge で定義される。

git clonegit remote add origin <url>した際に

fetch = +refs/heads/*:refs/remotes/origin/*

という設定が .git/config に書き込まれるが、これはfetchしたときにすべてのリモートサーバ上のブランチをリモート追跡ブランチとしてfetchするという意味になる。

参考

Reflog

CHANGELOG 2025/2/16

HEADの変更ログだけを意味していると記載してましたが、HEADだけではないと@nosuke23さんよりコメントで指摘をいただきましたので修正しました。

  • Reference logs
  • refの変更ログ
  • ログのエントリはある時点でrefが参照していたコミットを指す
  • git reflog コマンドでログの確認やログの操作ができる
  • ログの実態は.git/logs/以下に保存されている

git reflogコマンド

サブコマンドやrefを引数として指定できるが、何も指定しない場合ではshowサブコマンド、HEADをrefとして指定した場合と同じ扱いになる。つまり、下記の(1)、(2)のコマンドは等価になる。

git reflog # (1)
git reflog show HEAD # (2)

git reflog show [<ref>]コマンドは指定されたrefに対するreflogを閲覧するコマンド。
他のサブコマンドとして reflogを持つref一覧を出力するgit reflog list, 古いログを削除するgit reflog expireなどがある。

mainブランチで2つコミット、topicブランチを作成して切り替えた場合、git reflogは以下のような出力になる。

$ git reflog
d43dd39 (HEAD -> topic, main) HEAD@{0}: checkout: moving from main to topic
d43dd39 (HEAD -> topic, main) HEAD@{1}: commit: B
48669b2 HEAD@{2}: commit (initial): A

ここで、各行のShort SHA-1はHEAD変更後の参照先のコミットのハッシュになっている。
git reflog <branch>で、ブランチの変更ログも確認できる。

$ git reflog main
d43dd39 (HEAD -> topic, main) main@{0}: commit: B
48669b2 main@{1}: commit (initial): A

$ git reflog topic
d43dd39 (HEAD -> topic, main) topic@{0}: branch: Created from HEAD

以前の状態に戻ることができる

誤ったgit操作をしてしまった場合に、Reflogを使って、誤った操作の前の状態に戻ることができる。
例えば、git reset --hardしてしまっても、reflogにはgit reset前のHEADの参照先コミットが残っているので、もう一度git resetを使って元に戻すことができる。

$ git reset --hard HEAD^ # 誤ってgit resetしてしまった
$ git reflog
f68d5ca HEAD@{0}: reset: moving to HEAD^ # <-- git reset後のHEAD
23a0caa HEAD@{1}: commit: E              # <-- git reset直前のHEAD
f68d5ca HEAD@{2}: commit: D
607bed5 HEAD@{3}: commit: C

# 直前のHEADの状態に戻ることができる
$ git reset --hard HEAD@{1}
# or
$ git reset --hard 23a0caa

参考

Revision

CHANGELOG 2025/2/16

revisionとgitrevisionsを混同していましたが、revisionはcommitを一意に示すものであると @nosuke23 さんよりコメントで指摘をいただきましたので修正しました。

  • 一意のコミットを指す(=コミットと同義)

参考

gitrevisions

  • いろいろなコマンドが引数としてrevision parameterを受け取るが、コマンドや表記によってそれが指すものが変わる
    • 単一のコミット
    • 指定されたコミットから到達可能な複数のコミット (例: git log)
    • ツリー(tree)やブロブ(blob) (例: git show, git push)
  • git rev-parse コマンドでどのようなCommit Hashとして解決されるかが確認できる

  • Short SHA-1

    • 他に同じプレフィックスをもつSHA1が存在しなければ、SHA1ハッシュの一部でもフルSHA1ハッシュとして解決される
    • git rev-parseコマンドでフルSHA1が取得できる
    $ git rev-parse ae13719
    ae137196659150446b94f8dacc5549d50ccf8414
    
  • @

    • @が単独で利用されると、HEADのエイリアスを意味する
    $ git rev-parse @
    a1ace7478533b9fa20938f0f231f43275b7c1ddd
    
    $ git rev-parse HEAD
    a1ace7478533b9fa20938f0f231f43275b7c1ddd
    
  • <refname>@{<n>}

    • refnamen番前を表す
    $ git log --oneline --format='%h %s'
    a1ace74 C  # <-- HEAD
    7045d92 B  # <-- HEAD@{1}
    60e5dac A  # <-- HEAD@{2}
    $ git rev-parse HEAD@{2}
    60e5dacad70de9cc1577580dfe370dc103c0f1af
    
  • <rev>@~<n>, <rev>@^<n>

    • あるコミットからの相対的なコミットを参照するref
    • ~{n} では第一の親コミットをn番目前までたどる
    • mergeによって親コミットが複数あるときに、第一の親コミット以外を選択したい場合は ^{n} で第nの親を選択する
    HEAD~1 # HEADが指すコミットから1つ前のコミット(2つ親がある場合は第一の親コミット)
    HEAD^2 # HEADが指すコミットの第二の親コミットを指す
    main^2~2 # HEADが指すコミットから1つ前のさらに2つ前のコミット(第二の親ルート)
    
    Gに対して
    - Dは第一の親コミット
    - Fは第二の親コミット
    
    HEAD^2~2  HEAD~1
    HEAD~3    HEAD^1
        ▼     ▼
    --A--B--C--D--G <--HEAD, main
          \      /
          E----F <-- HEAD^2
    
  • <rev>:<path>

    • <rev>tree-ishオブジェクトを指すとき、そのオブジェクト内のpathにあるブロブかツリー

    • git show HEAD:README.mdはHEADが指すコミットのREADME.mdファイルの内容を表示する

    • 特定のコミットであるファイルがどういう状態だったかを確認するのに利用できる

    • rev-parseから得られるSHA-1 Hashがブロブを指している

    $ git rev-parse HEAD:README.md
    f80e74b29035d60c1a82c6b3aadf65edb35ca3b1
    
    $ git cat-file -t f80e74b29035d60c1a82c6b3aadf65edb35ca3b1
    blob
    
  • Commit Ranges

    • git loggit rev-listはリビジョンを受け取るとそこからたどり着けるコミットの一覧を返す
    • <rev>
      • revからたどり着けるコミットのリスト
    • ^<rev>
      • revからたどり着けるコミットを除外する
    • Double dot <rev1>..<rev2>
      • rev1からはたどり着けず、rev2からのみたどり着けるコミットのリスト
      • ^<rev1> <rev2> と同じ
    • Triple dot <rev1>...<rev2>
      • rev1もしくはrev2のどちらかしかからたどり着けないコミットのリスト

参考

おわりに

Gitの多くのコマンドでコミットを参照することがあるが、Commit SHA-1 Hash値以外にも参照の仕方を知っていると役立つと思う。

全体の参考

脚注
  1. gitで特定のcommitバージョン/リビジョンを指すコレをなんと呼ぶか問題 ↩︎

  2. GitHub 用語集 ↩︎

GitHubで編集を提案

Discussion

nosuke23nosuke23

reflogは文字通りただのrefのログ。git reflog <branch>とすればブランチの参照履歴が表示される。revisionはcommitを一意に示すもので、セットやレンジで扱えるってだけ。gitの情報は公式のリファレンス以外参考にしないほうがいい。用語集などは目的的な説明に終始しているので、触りとしてはいいが実用的じゃない

rutamu30rutamu30

返信が遅くなってしまいすみません。ご指摘ありがとうございます。
reflog, revisionあたりは改めて公式ドキュメントを見直して修正したいと思います。
たしかに、用語集より逆引き的な方が役立つとは思いました。勉強になります。。