Git について

公式ドキュメントに書いてある内容ではありますが、今まで Git の公式ドキュメントをまじめに読んだことがなかったので1回くらいまじめに読んでおこうと思ったので読んでまとめます。

ちなみに本当は Git Internals の章(第10章)を読もうと思ったのがきっかけでしたが、そもそも Git のドキュメントを読んだことなくないか?と反省したので、今回あらためて上から下まで読んでみようと思って読んだ次第でした。

ところで日本語ドキュメントあるの読みやすくて助かる~。英語でも読めますけどやっぱり心理的負荷が違うので……

git log

git log の -S オプションでその文字列でのフィルタができる。

もうひとつ、`-S`オプションというとても便利なフィルタがあります。 このオプションは任意の文字列を引数にでき、その文字列が追加・削除されたコミットのみを抜き出してくれます。 仮に、とある関数の呼び出しをコードに追加・削除したコミットのなかから、最新のものが欲しいとしましょう。こうすれば探すことができます。

$ git log -Sfunction_name

git tag

lightweight と annotated があるのは知っていたが、Git 公式としては一般的には annotated タグの使用を推奨しているとのこと。

また内部的にも扱いが違うようで、lightweight は特定のコミットへのポインタだが、annotated は Git データベース内に完全なオブジェクトとして格納される。GPG で検証もできるらしい。

Git alias

git config コマンドでエイリアスが貼れる。git co みたいな感じにしかならず gco とまでは省略できない。が、公式が提供していることは頭に入れておいて損はなさそう。

このテクニックは、「こんなことできたらいいな」というコマンドを作る際にも便利です。 たとえば、ステージを解除するときにどうしたらいいかいつも迷うという人なら、 こんなふうに自分で unstage エイリアスを追加してしまえばいいのです。

$ git config --global alias.unstage 'reset HEAD --'
こうすれば、次のふたつのコマンドが同じ意味となります。

$ git unstage fileA
$ git reset HEAD -- fileA
少しはわかりやすくなりましたね。あるいは、こんなふうに last コマンドを追加することもできます。

$ git config --global alias.last 'log -1 HEAD'
こうすれば、直近のコミットの情報を見ることができます。

git commit/branch internals

git add すると、

  • SHA-1 checksum を計算し、
  • そのバージョンのファイルを Git ディレクトリに格納し、
  • そのチェックサムをステージングエリアに追加する。
ステージング・エリアは、普通はGitディレクトリに含まれる、次のコミットに何が含まれるかに関しての情報を蓄えた一つのファイルです。

git commit を実行してコミットを作る際、

  • 各サブディレクトリのチェックサムを計算して、その tree object を git リポジトリに格納する。
  • commit object を作る。これは、commit のメタデータと root tree へのポインタを保持している。

git における branch とは、commit に対するポインタのことに過ぎない。ブランチの新規作成は特定のコミットを参照する SHA-1 checksum だけを記録したファイルを作成するだけの作業なので、非常に低コストである。

git switch か git checkout か

switch のほうが推奨なのかなーと思っていたが、2025年7月時点で公式ドキュメントを読んでいる感じだとそのように明記はされていない。

むしろブランチ切り替えには checkout を使いましょう、と書いてある。

To switch to an existing branch, you run the git checkout command.

git switch のドキュメントには以下のように強めの注意書きもある。 checkout 側は読む限りそんな記述を発見できない。

THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.

無論将来的には switch や restore に置き換わっていくのだと思うのだが、現時点ではこれら2つのコマンドはある日突然挙動を破壊的に変更されても文句は言えないという認識。CI とかに組み込むのであれば checkout のほうが無難か?という気もするがいまいちわからない。

git merge

毎回忘れるのだが、コンフリクトした場合、上側に merge コマンドを打った時の HEAD の内容が表示される。

<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
 please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

tracking branch と upstream branch

remote tracking branch から local branch にチェックアウトした際にローカルにできるブランチを tracking branch, それが追跡する remote branch のことを upstream branch という。なんとなく聞いたことはあったが定義をちゃんと見たのは初めてだったかも。

リモート追跡ブランチからローカルブランチにチェックアウトすると、“追跡ブランチ” というブランチが自動的に作成されます(そしてそれが追跡するブランチを`‘上流ブランチ’'といいます)。 追跡ブランチとは、リモートブランチと直接のつながりを持つローカルブランチのことです。 追跡ブランチ上で git pull を実行すると、Git は自動的に取得元のサーバーとブランチを判断します。

rebase --onto

まず rebase の定義から。

ふたつのブランチ (現在いるブランチとリベース先のブランチ) の共通の先祖に移動し、現在のブランチ上の各コミットの diff を取得して一時ファイルに保存し、現在のブランチの指す先をリベース先のブランチと同じコミットに移動させ、そして先ほどの変更を順に適用していきます。

git rebase <upstream> <branch> と書いたら、

  • 内部的に git switch <branch> を実行して、current branch を <branch> に変更する
  • current branch にて実行された commit のうち <upstream> にないものを一時領域に退避する
  • current branch を <upstream> まで reset する。即ち git reset --hard <upstream> と同一の内容を実施する
  • 一時領域に退避した内容を current branch に一つずつ apply する

という動作を取る。注意すべきこととして、<upstream> は一切の変更を受けない。

なお、ブランチ名を1つ省略した場合、git rebase <upstream> と解釈し、最初の switch が省略される。

git rebase --onto <newbase> <upstream> <branch> というコマンドもある。これは、上掲した手順の中で reset の手順を git reset --hard <newbase> に置き換えたもの、と理解される。

git clean

コマンドリファレンスに書いてあるとおり、untracked files をまとめて削除する。ビルド結果を削除してクリーンな状態でビルドを実行したいときなどに使う。削除されたデータは戻ってこないので、気が変わりそうなら git stash のほうが安全。

git Server protocols

4つある。ちなみに、リモートリポジトリは .git ディレクトリのみからなるベアリポジトリとして構築するのが普通。working directory が必要ないので、このようにする。

Local

リモートリポジトリをディスク上の別のディレクトリとする。チーム全員がアクセスできる NFS を持っている場合などに使える。ほえー。

HTTP

smart HTTP と dumb HTTP の2つがある。

今日日基本的に使われるのは smart のほう。git プロトコルが提供する匿名での読み込み機能と、SSH プロトコルが提供する認証・暗号化を経た書き込み機能の両方がこれひとつで実現できる。git-http-backend という CGI スクリプトが付属しているので、これを使えば構築できるらしい。

なお smart HTTP に応答しない場合、git client は dumb HTTP にフォールバックするらしい。

SSH

特定の企業内でのみ利用する場合はこれになるだろう。

Git

専用ポートとして9418番を使う。認証を行わない。

プロジェクト運営について

ある程度の分量を割いてドキュメントが書かれていることは注目に値すると思う。これとこれ。

メーリングリストなどで運営されているプロジェクトに参加するでもない限り git format-patch とか git am といったコマンドを使うことは(おそらく)ないと思うが、そんなコマンドがあるんだ、というのは面白い話だと思う。Linux カーネルとかはそういうふうにしてパッチの適用をしているのだろうか?

SHA-1 の衝突確率について

ちゃんと書いてあった。気になったことは確かにあったのだが、心配しなくてよいということが改めて説明されていた。いやー読んで良かった。

SHA-1 に関するちょっとしたメモ
「リポジトリ内のふたつのオブジェクトがたまたま同じ SHA-1 ハッシュ値を持ってしまったらどうするの?」と心配する人も多いでしょう。 実際、どうなるのでしょう?

すでにリポジトリに存在するオブジェクトと同じ SHA-1 値を持つオブジェクトをコミットしてした場合、Git はすでにそのオブジェクトがデータベースに格納されているものと判断します。 そのオブジェクトを後からどこかで取得しようとすると、常に最初のオブジェクトのデータが手元にやってきます (訳注: つまり、後からコミットした内容は存在しないことになってしまう)。

しかし、そんなことはまず起こりえないということを知っておくべきでしょう。SHA-1 ダイジェストの大きさは 20 バイト (160 ビット) です。ランダムなハッシュ値がつけられた中で、たった一つの衝突が 50% の確率で発生するために必要なオブジェクトの数は約 2^80 となります (衝突の可能性の計算式は p = (n(n-1)/2) * (1/2^160) です)。 2^80 は、ほぼ 1.2 x 10^24 、つまり一兆二千億のそのまた一兆倍です。 これは、地球上にあるすべての砂粒の数の千二百倍にあたります。

SHA-1 の衝突を見るにはどうしたらいいのか、ひとつの例をごらんに入れましょう。 地球上の人類 65 億人が全員プログラムを書いていたとします。そしてその全員が、Linux カーネルのこれまでの開発履歴 (360 万の Git オブジェクト) と同等のコードを一秒で書き上げ、馬鹿でかい単一の Git リポジトリにプッシュしていくとします。これを2年ほど続けると、SHA-1 オブジェクトの衝突がひとつでも発生する可能性がやっと 50% になります。 それよりも「あなたの所属する開発チームの全メンバーが、同じ夜にそれぞれまったく無関係の事件で全員オオカミに殺されてしまう」可能性のほうがよっぽど高いことでしょう。

しょうもない注意だが、ここで言う「衝突の可能性」は近似が入っている。アドレス空間の大きさを N := 2^160 とするとき、異なる2つの入力が衝突する確率はそれぞれ 1/N となるから、ペア数 n をかけて得られる n(n-1)/2) * (1/2^160) というのは期待衝突数とでも言うべきものになる。衝突確率が1よりも極めて小さいのであればこれを衝突確率の近似として使える。

それか、厳密な値 1 - (1-1/N) * (1-2/N) * ... (1 - (k + 1)/N) を以下のように近似してもよいだろう;x << 1 ならば e^x \simeq 1 + x なので、近似したい確率を 1 - e^{-k(k-1)/2N} と近似できる。こうすると k = \sqrt{2N ln 2} のように計算できる。

ダブルドットとトリプルドット

そのへんで調べて出てきたコマンドにかかれていた事はあった気がするが、ちゃんとドキュメントに戻って調べたのは今回が初めてだったかも。

experiment ブランチの内容のうち、まだ master ブランチにマージされていないものを調べることになりました。 対象となるコミットのログを見るには、Git に `master..experiment` と指示します。
範囲指定選択の主な構文であとひとつ残っているのがトリプルドット構文です。これは、ふたつの参照のうちどちらか一方からのみたどれるコミット (つまり、両方からたどれるコミットは含まない) を指定します。

ただあまり直感的な構文ではないような気もする。たぶんそのうちまた忘れる内容の筆頭だと思う。

intertactive staging

git add -i で interactive staging できる。

git add -p で hunk ごとに commit できることは知っているのだがよくサボってしまう。反省。

git stash

私は割と脳みそに stash するという ~~エンジニアにあるまじき~~ 勤勉性を発揮する事があるのであまり使っていなかったのですがオプションを一通り見ておく意味はありそうなので。

git stash apply は pop と異なりスタックから stash の内容を削除しない。また、基本的に git stash apply した場合の内容は unstaged になるが、git stash --index とすれば stage への index 処理も再適用される。

pop ではなくて単純に削除したいという場合は drop でよい。

stash save の際に、

  • --keep-index とすると、git add で index に追加された内容は stash されない。
  • --include-untracked (-u) とすれば、 untracked files も stash してくれる。デフォルトだと untracked files は stash されない。
  • --patch は対話的に stash してくれる。

git stash branch とすれば、新しい branch を作成してそこに stash apply してくれる。新規ブランチに逃がしたい場合は役に立ちそう。

filter-branch

「いっけなーい、間違ってパスワードを commit しちゃったので消したゾ♡」みたいなのができる。消す commit を積んだだけでは当然過去の commit を辿れるので意味がない。ちゃんと filter-branch で tree すべての commit に対して一括で適用すること。

あとは commit author のメールアドレスの変更といったユースケースもある。

git reset

公式ドキュメントにちゃんと説明がされていた。もっと早く読めばよかったな。

  • 大前提として、Git が取り扱うワークツリーは HEAD(Commit 済) と Staging Index と Working Directory の3つからなる。
  • git add で Working Directory の変更を Index に動かせる。
  • git commit で Index の内容を Commit オブジェクトにまとめ、HEAD が移動する。

git reset <commit> した場合、

  • HEAD が指し示すブランチを <commit> に動かす(--soft を指定した場合はここで止まる)
  • Index の内容を <commit> のそれで置き換える(--mixed を指定した場合はここで止まる)
  • Working Directory の内容を <commit> で置き換える(--hard を指定した場合はこの挙動になる。ファイルが消える可能性がある)

git checkout は、git reset --hard とよく似ているが、HEAD が指し示すブランチではなく HEAD それ自体を移動させるという点で git reset --hard とことなるものである、と理解するのがよい。

感想

もっと早く読んでおけば良かったかも。