Git 2.55 の注目すべき新機能と変更点
Git 2.55 は、大規模リポジトリの維持コストを削減し、メタデータの書き込み効率を向上させる「増分型マルチパックインデックス」機能を git repack コマンドに正式実装した。
キーポイント
増分型マルチパックインデックスの実装
Git 2.55 では、git repack に --write-midx=incremental オプションが追加され、リポジトリ全体を再構築するのではなく、新しい層を追加するだけでインデックスを更新できるようになった。
メタデータ書き込みコストの削減
従来の単一ファイル方式では小さな更新でも大規模な書き換えが必要だったが、増分型チェーン方式により、メンテナンス実行時のメタデータ再書き込み量を最小限に抑えることが可能になる。
幾何学的リパックとの連携
増分モードと --geometric オプションを組み合わせることで、新しい層を追加しつつ不要な層を統合する自動最適化が可能となり、チェーンの無限成長を防ぐ仕組みが強化された。
MIDX チェーンの動的統合ルール
新しい層の累積オブジェクト数が、直前の古い層の数の 1/f(f は repack.midxSplitFactor)を超えた場合、それらの層は自動的に結合されます。
幾何学的リパック候補の拡大
tip MIDX レイヤーが設定された閾値 (repack.midxNewLayerThreshold) に達すると、既存のレイヤーからパックも幾何学的リパックの候補に含められるようになります。
分割位置の最適化ロジック
新しい層と古い層を結合する際、左側の隣接パックが結合対象の合計サイズより十分大きいか(2 倍以上など)を判定し、幾何数列の性質を維持できる分割点を探します。
メタデータのみを対象としたコンパクト化
Git は新しい MIDX レイヤーを生成する際、オブジェクト自体を再パックせず、既存のパックファイルを参照するメタデータのみを統合してサイズを削減します。
影響分析・編集コメントを表示
影響分析
この更新は、GitHub や大規模オープンソースプロジェクトなど、膨大な履歴とオブジェクトを持つリポジトリの運用において、メンテナンス作業のボトルネックを解消する重要なステップです。特に、頻繁なコミットやブランチ操作がある環境では、インデックス生成にかかるオーバーヘッドが劇的に減少し、CI/CD パイプラインや開発者のワークフロー効率に直接寄与します。
編集コメント
Git の内部構造を深く理解している開発者にとって、リポジトリの規模が拡大してもパフォーマンスが劣化しないための重要な技術的進歩と言えます。特に大規模チームや継続的インテグレーション環境では、この機能の導入を検討する価値があります。
オープンソースの Git プロジェクトは、100 人以上のコントリビューター(そのうち 33 名は新規)による機能追加とバグ修正を盛り込んだ Git 2.55 をリリースしました。直近の Git の最新動向についてお伝えしたのは、2.54 がリリースされた時が最後でした。
今回の最新リリースを祝うため、GitHub がこれまでで最も興味深い機能や変更点に焦点を当てて解説します。
インクリメンタル・マルチパックインデックスを使用したリパック
このシリーズの既読者の方には、インクリメンタル・マルチパックインデックス(incremental multi-pack indexes)およびインクリメンタル・マルチパック到達可能性ビットマップ(incremental multi-pack reachability bitmaps)に関する当社の報道を覚えておられる方もいらっしゃるかもしれません。もし復習が必要であれば、ここでは簡潔に説明します。
Git はリポジトリの内容を、コミット、ツリー、ブロッブといった個別のオブジェクトとして保存します。これらのオブジェクトは通常、圧縮されたオブジェクトの集合体であるパックファイル(packfiles)内に存在します。各パックファイルには対応するパックインデックスがあり、これにより Git はパック内の任意のオブジェクトを高速に検索できます。しかし、大規模なリポジトリでは通常、単一のパックファイルしか持ちません。時間とともに、フェッチ、プッシュ、メンテナンスタスク、およびリパックによって多数のパックが残ってしまうのです。
マルチパックインデックス(MIDX)は、Git に対して複数のパック全体をまたぐ単一のインデックスを提供します。各パックの個別インデックスを開いて検索する代わりに、Git は MIDX に特定のオブジェクトがどのパックに含まれ、オフセットがどこにあるかを問い合わせることができます。これは大規模リポジトリにおいて特に有用であり、GitHub のリポジトリメンテナンス戦略を支える重要な構成要素の一つとなっています。
Git 2.47 でインクリメンタル MIDX フォーマットを導入した際に解説した通り、リポジトリはすべてのパックをカバーする単一の MIDX としてではなく、MIDX のレイヤーのチェーンとして保存することができます。単一ファイルの MIDX は読み込みがシンプルで効率的ですが、重要なメンテナンスコストがあります。そのファイルはカバーするすべてのパックを含んでいるため、すでに大きなリポジトリであっても、小さな更新だけで大規模な書き込みが必要になる可能性があります。
インクリメンタル MIDX は、MIDX レイヤーのチェーンを保存することでこの問題を解決します。各レイヤーはいくつかのパックのカバー範囲を持ち、チェーンファイルはそれらのレイヤーの順序を記録しています。チェーンの末尾に新しいレイヤーを追加しても、古いレイヤーは無効化されないため、Git はリポジトリ全体をカバーする単一の MIDX を書き換えることなく、新しく作成されたパックをインデックスできます。
Git 2.55 では、git repack コマンドがこれらのインクリメンタル MIDX チェーンを直接記述する方法を学びました:
$ git repack --write-midx=incremental
他のオプションなしでは、このモードは追加専用です。Git は repack によって作成されたパックに対して新しいレイヤーを書き込み、既存のレイヤーには手を付けません。これは、メンテナンス実行中に書き換えられるメタデータの量を最小限に抑えたい場合にすでに有用です。
しかし、追加専用のチェーンが永遠に成長し続けることはできません。各メンテナンス実行で新しいレイヤーが追加される場合、最終的にはチェーン自体が維持すべき対象となってしまいます。Git 2.55 では、--write-midx=incremental を幾何学的リパック (geometric repacking) と組み合わせて使用することもサポートしています:
$ git repack --write-midx=incremental --geometric=2 -d
これらのモードを併用すると、各 repack は新しいティップ層を作成し、隣接する層を統合すべきかどうかを判断します。デフォルトのルールは repack.midxSplitFactor によって制御され、新しい層に蓄積されたオブジェクト数が次の古い層に対して十分に大きくなると、Git はそれらの層を単一の置換層としてマージします。そうでない場合、古い層はそのまま untouched に保たれます。
高レベルでは、アルゴリズムは以下の通り動作します。以下において、NN は repack.midxNewLayerThreshold の値を、ff は repack.midxSplitFactor の値を表します:
MIDX 化されていないパックを幾何学的な repacking の候補として選択します。ティップ MIDX 層が少なくとも NN 個のパックを持っている場合、それらも候補に含めます。
その候補セットに対して通常の幾何学的 repacking ルールを適用し、結果得られたパックをカバーする新しいティップ MIDX 層を書き出します。
新しい層(複数ある場合はそれら)の累積オブジェクト数が、より深い次の層のオブジェクト数の 1/f1/f を超える間、隣接する MIDX 層を compact 化します。
これらの要素がどのように組み合わさるかを理解するために、すでに増分 MIDX チェーンを持つリポジトリから始めましょう。古い層は左側にあり、ティップ層は右側にあります。一方、通常のリポジトリ活動により新しいパックが生成され続けています。これらのパックはまだどの MIDX 層にもカバーされていないため、次のメンテナンス実行では2つのタスクがあります:何を repack するかを決定することと、MIDX チェーンのどこまでを書き換えるかを決定することです。
通常、MIDX化されていないパックは幾何学的再パックの候補となる唯一の対象です。Git は既存のレイヤーを乱すことなく、新しいパックと新しいティップ MIDX レイヤーを作成できます。下の図はより興味深いケースを示しています。現在のティップレイヤーに十分な数のパックが蓄積され、設定された repack.midxNewLayerThreshold に達している場合です。この閾値を満たすと、ティップレイヤーからのパックも新たに作成されるパックと共に幾何学的再パックの候補に加わります。
幾何学的再パックは次に、最新の候補パックに関する局所的な問いを投げかけます。すなわち、「Git が 풫\mathcal{P}(一連のパックの接尾辞)をロールアップした際に、その幾何級数性を維持するために、풫\mathcal{P} の直左にあるパックが十分に大きいか?」という問いです。下の最初の試行では、풫\mathcal{P} には現在のティップレイヤーから最も小さなパックと、新たに MIDX化されていないパックが含まれています。しかし、分割の直左にあるパックはオブジェクト数が 30,000 個に過ぎず、これは 풫\mathcal{P} のサイズの 2 倍よりも小さいため、この分割位置は右側すぎます。

Git は分割されたパックをより早期に移動させ、同じ質問を再度行います。これで 풫\mathcal{P} には、トップレイヤーからもう一つのパックが含まれるようになります。左側に隣接するパックは 100,000 個のオブジェクトを含んでおり、これは選択されたサフィックスのサイズ少なくとも 2 倍です。これが幾何学的不変量が成立するポイントであり、Git はまさにこれらのパックを新しいパックに統合できます。

この新しいパックの書き込みが完了すると、Git は前回のトップレイヤーから生き残ったパックと新たに作成された統合パックを基に、新しい MIDX レイヤー(MIDX: Multi-pack Index)をトップに追加します。この時点で、パックファイル自体は良好な状態ですが、MIDX チェーンにはまだ小さく隣接するレイヤーが蓄積されすぎている可能性があります。Git は「新しきものは古きものより」という直感を MIDX レイヤー自身にも適用します:新しいレイヤーが隣接するレイヤーに対して十分な大きさであれば、そのメタデータを統合して置換用のレイヤーを作成します。

この統合ステップは意図的にメタデータのみを対象としています。Git はこれらのレイヤーからオブジェクトを再パックすることはありません;同じパックファイルをカバーする新しい MIDX レイヤーを作成するだけです。その後、さらに古いレイヤーへと処理を進めます。ここで、統合されたレイヤーはまだ深いレイヤーの半分より小さいため、Git は停止します。古いレイヤーはそのまま untouched に保たれ、これがこのメンテナンスがインクリメンタルであるための鍵となる性質です。
その結果は、2 つの極端なアプローチの間での妥協点となります。単一ファイルの MIDX は参照計算の複雑さを最小化しますが、メンテナンス時に大規模な書き換えが必要になる可能性があります。一方、純粋に追加のみを行うインクリメンタル MIDX は各書き込みを最小化しますが、チェーンが無限に成長する可能性があります。幾何学的インクリメンタル再パックでは、レイヤー数がオブジェクト総数に対して対数的に保たれつつ、最も新しく小さなレイヤーほど古く大きなレイヤーよりも頻繁に書き換えられるように保証されます。
これにより、Git の既存の repack 機構とも統合されます。MIDX チーンでまだカバーされていない新規作成されたパックは、常に幾何学的再パックの対象候補となります。より深い MIDX レイヤーにあるパックはそのまま放置されます。tip(最上位)MIDX レイヤーのパックは、その tip レイヤーが少なくとも repack.midxNewLayerThreshold 個のパックを持つようになるまで、候補セットには加わりません。もし tip レイヤーがまだこの閾値より小さい場合、Git はそれを完全に disturbance せず、新規作成されたパックのために新しいレイヤーを単に追加します。
継続的に新しいオブジェクトを受け取るリポジトリの場合、これは通常のメンテナンスでリポジトリのパックメタデータをインクリメンタルに更新可能になることを意味し、各メンテナンス実行でオブジェクトストア全体をカバーする単一の MIDX を書き換える必要がなくなります。
[source]
Fixing up earlier commits with git history
レビュー用にコミットシリーズを送る前に整えたことがある人なら、おそらくこんな経験があるでしょう:作業ツリー内の変更が、ブランチの先頭ではなく、より早いコミットに属していることに気づくことです。
今日では、これに対処する一般的な方法は、fixup コミットを作成してそれを自動修正(autosquash)することです:
$ git commit --fixup=<commit>
$ git rebase --autosquash ^<branch>
これは機能しますが、意図ではなくメカニズムを記述することを求められます。Git 2.55 では、Git 2.54 で導入された実験的な git history コマンドを発展させ、新しい fixup サブコマンドを追加しました。これにより、インデックスに現在ステージングされている変更をより早いコミットに適用できます:
$ git history fixup <commit>
以下に小さな例を示します。最初のコミットでパンケーキのレシピが導入され、その後にいくつかの追加コミットが続きます。その後、レシピにメープルシロップが不足していることに気づきます。その 1 行の変更をステージングした後、git history fixup はそれを元のレシピコミットに折りたたみ、下位のコミットを上書きして再実行します。

ここでステージングされた変更は、ターゲットとなるコミット自体の一部になります。デフォルトでは、ターゲットコミットのメッセージと著作者情報は保持されます(--reedit-message オプションを指定しない限り)。Git はその後のコミットを書き換えるため、ブランチの末尾は修正が正しい位置にある同等の履歴で終わります。
Git の歴史の他の部分と同様に、このコマンドはまだ実験的なものです。また、意図的に保守的な設計となっています。fixup はインデックスから読み取る必要があるため、作業ツリーが必要であり、裸のリポジトリでは動作しません。ステージされた変更を適用すると競合が発生する場合は、状態を持つ書き換えの途中にあなたが残されるのではなく、コマンドは中止します。
[source]
氷山の一角…
これで最も大きな変更について詳しく取り上げたので、今回のリリースにおけるその他の新機能やアップデートの一部を見てみましょう。
このシリーズの既読者の方々は、Git 2.54 で紹介した設定ベースフック(config-based hooks)を覚えていらっしゃるかもしれません。これにより、フックを実行可能なファイルとして$GIT_DIR/hooks に置くだけでなく、Git の設定内で定義できるようになりました。フックは、コミット作成前やプッシュ受信後など、ワークフローの特定のポイントで Git が実行するスクリプトです。これらを設定内に移動させることで、各リポジトリの hooks ディレクトリにスクリプトをコピーすることなく、これらのフックを共有しやすくし、組み合わせやすく、選択的に無効化できるようになります。
Git 2.55 は、互換性のある設定フックを並列実行できるようにすることで、その取り組みを拡張します。例えば、プロジェクトにはリンティング用とユニットテスト用の独立した pre-commit フックが存在する場合がありますが、両方が hook..parallel = true を宣言している場合、Git はそれらを同時に実行できます。並列ジョブの数は、hook.jobs でグローバルに制御するか、イベントごとに hook..jobs で制御するか、または git hook run -j コマンドで指定することができます。コミットメッセージフックやインデックスや作業ツリーを検査する他のフックなど、共有状態を必要とするフックは、引き続き逐次実行されます。
[source]
git status を実行してターミナルで長い一時停止に直面した経験があるなら、Git の組み込みファイルシステムモニターを使用して処理を高速化したことがあるかもしれません。core.fsmonitor が有効になっている場合、git status などのコマンドは作業ツリー全体をスキャンするのではなく、長期間実行されるデーモンに対してどのパスが変更されたかを問い合わせることができます。
これまで、この組み込みデーモンは macOS と Windows のみで利用可能でした。Git 2.55 では Linux でのサポートが追加され、実装には inotify が使用されます。これは特権を必要とせずに動作しますが、ディレクトリごとに 1 つの監視が必要となるため、非常に大規模なリポジトリでは fs.inotify.max_user_watches リミットを引き上げる必要がある場合があります。他のプラットフォームと同様に、デーモンはネットワークマウントされたリポジトリに対して慎重に動作し、それらはオプトイン方式のままです。
[source]
到達可能性ビットマップは、Git が「このコミットからどのオブジェクトが到達可能か」といった質問に、オブジェクトグラフを最初からすべてたどることなく回答するために使用するトリックの一つです。これによりオブジェクトの走査が高速化されますが、Git には still、git repack --write-midx-bitmaps のようなメンテナンスタスク中にこれらのビットマップを構築・更新する必要があります。
Git 2.55 では、不要な再帰的ツリー処理を回避し、すでに計算済みの選択済みビットマップを再利用し、オブジェクトの位置情報をキャッシュし、XOR 演算を行う前にビットマップをソートすることで、この生成パスを高速化しました。パッチシリーズからのベンチマークによると、これらの一般的な改善により、ある大規模リポジトリにおけるビットマップ生成時間が約 612 秒から約 294 秒に短縮されました。
同じシリーズはまた、関連する参照をグループ化することで、Git が走査中に事前計算されたビット配列を結合し、同じオブジェクトを繰り返し再発見する必要がなくなるようにした疑似マージビットマップも改善しています。あるベンチマークでは、疑似マージにより git rev-list --objects --use-bitmap-index の完全な走査がほぼ 20 倍高速化されましたが、以前はビットマップ生成時間がほぼ 2 倍に増加していました。これらの変更後、疑似マージは走査の高速化効果のほとんどを維持しつつ、ビットマップ生成パスへの追加負荷を大幅に削減しました。
[source, source]
部分的クローンやフィルタ付きパック、または Git が意図的に一部のオブジェクトを省略する他のワークフローを使用している場合でも、パックサイズは依然として重要です。Git 2.51 で導入された git pack-objects の --path-walk モードは、2 回目の圧縮パスを実行する前にパス単位でオブジェクトをグループ化し、パスの局所性が重要となる場合により良い差分(deltas)を生成することができます。
Git 2.55 では、blob:none、blob:limit=、tree:0、object:type=、sparse:、および互換性のある combine: フィルタを含むフィルタと --path-walk を組み合わせることが可能になりました。これにより、より多くの部分的クローンやフィルタ付きパックのワークフローで --path-walk を使用したパッキングが可能になります。Git 自身のリポジトリにおけるあるベンチマークでは、blob なしのパスウォークによる再パックが、新鮮な差分計算に時間がかかるという代償を払うことで、約 16% 小さいパックを生成しました。
[source]
Git は、標準入力からリビジョンをプレティフォーマットするための新しい実験的コマンド git format-rev を学習しました。履歴の範囲を辿る git log とは異なり、git format-rev はコミットを 1 つずつ遭遇する場合や、他のテキストに埋め込まれている場合に設計されています。
例えば、あるディレクトリ内の各パスを最後に修正したコミットを出力するために git last-modified を使用している場合を考えましょう。どのコミットがそれを行ったかだけでなく、誰が最後に各パスを修正したのかを知りたい場合はどうでしょうか?その出力を以下のようなパイプ処理に通すことで、コミットを著者名に置き換えることができます。
$ git last-modified | perl -F'\t' -lane '
chomp($F[0] = qx(git show -s --format=%an $F[0]));
print join "\t", @F
'
Junio C Hamano builtin/commit.c
[...]
That works, but it has to start a new Git process for each row just to format the commit. In Git 2.55, git format-rev can handle that part as a normal pipeline:
$ git last-modified |
git format-rev --stdin-mode=text --format=%an
Junio C Hamano builtin/commit.c
[...]
The command's text mode can also rewrite full commit object names found in freeform text, which makes it useful for commit-message hooks or other scripting workflows.
[source]
When you push your repository somewhere, you may have noticed output that starts with remote::
$ git push origin main
Enumerating objects: 5, done.
[...]
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
When a client fetches or pushes, Git can multiplex different streams over the same connection: one sideband carries packfile data (the actual objects being transferred), another carries progress messages that the client usually prints to stderr, and a third carries errors from the remote.
Those progress messages are useful, but they also come from the other side of the connection and may be printed directly to your terminal. Before Git 2.55, that meant a server could include arbitrary terminal control sequences in sideband output, including sequences that move the cursor or erase text. Git now masks most of those control characters by default while still allowing ANSI color sequences, so colored progress output continues to work.
[source]
ファイルの編集途中、誤ったブランチから始めたことに気づいたとしましょう。移動先のブランチが同じパスを変更している場合、通常の git checkout では移動を拒否し、作業内容が上書きされるリスクがあります。git checkout -m は、「ローカルの編集内容を一緒に持ち運ぶ」ことを試みるバージョンの操作です。
しかし、相手側もその同じパスに対して変更を加えている場合はどうなるでしょうか? 以前は、git checkout -m を実行すると、生じた競合を即座に解決する機会が一度だけ与えられていました。Git 2.55 では内部で autostash(自動スタッシュ)を使用することで安全性が高まり、競合が発生したローカル変更はスタッシュエントリとして保存されます。これにより、その場で解決することも、後で再適用することも可能になります。
[source]
一部のプロジェクトでは、同じブランチをプライマリホストと 1 つ以上のミラーなど複数の場所に公開する必要があります。git fetch では、リモートグループが長らく利用可能であり、これは複数のリモートを空白区切りリストとして設定することで構成されます。Git 2.55 では、git push でも同様の短縮記法を利用できるようになりました:
$ git config remotes.publish "github gitlab mirror"
$ git push publish main
これは、グループ内の各リモートに対して順次プッシュする操作と同等です。原子性を保証できるのは単一の転送接続のみであるため、グループへのプッシュでは --atomic オプションはサポートされません。
[source]
git log --graph は、ブランチ構造を可視化するのに優れていますが、グラフ自体が読み取るには広すぎてしまうまでです。多くの並列なブランチを持つリポジトリでは、コミット件名に到達する前に、グラフのレーンが端末の大部分を占有してしまいます。
Git 2.55 では、git log --graph および関連コマンドに --graph-lane-limit= が追加されました。制限を超えたレーンは ~ に置き換えられるため、非常に広い履歴を持つリポジトリでもグラフ出力を管理しやすくなります。
[source]
ブランチ上の最新の 10 コミットを一覧表示したい場合、それは簡単です:git log -n 10 がまさにその役割を果たします。では、最も古い 10 コミットを取得するにはどうすればよいでしょうか?もし「おそらく git log --reverse --10 ではないだろう」とお考えなら、おめでとうございます:あなたはベテランの Git ユーザーです!履歴を逆順にしてから 10 コミットを印刷するのではなく、Git は最新の 10 コミットを取得してその順序を逆にします。
git log --reverse | tail -10 のように範囲全体を後処理することで達成することもできますが、それでもシェルが破棄しようとするすべてのコミットを Git に印刷・フォーマットさせる必要があります。Git 2.55 では、git rev-list および git log コマンド群に新しい --max-count-oldest= オプションが追加され、これにより範囲内で最も古い n 個のコミットを選択できるようになりました。
[source]
フェッチ実行中、クライアントとサーバーは、クライアントが既に保持しているコミットを「have」行として広告することでネゴシエーションを行います。これにより、サーバーはクライアントが既に到達可能なオブジェクトの送信を回避できます。しかし、参照(ref)が多数存在するリポジトリでは、ネゴシエーションアルゴリズムが共通履歴を見つける上で特に重要な参照をスキップしてしまう可能性があります。
Git 2.55 では、ネゴシエーションに参加する参照を制御するための新しい機能が追加されました。新たに導入された include および restrict オプションと、対応する remote.* 設定により、特定の参照を送信対象の「have」行として必須にしたり、ネゴシエーションを特定の参照セットに限定したりすることが可能になりました。
[source]
…残りの氷山の一角
これは最新リリースからの変更の一部に過ぎません。詳細は、2.55 のリリースノート、または Git リポジトリ内の過去の任意のバージョンのノートをご覧ください。
本記事「Highlights from Git 2.55」は、最初に The GitHub Blog で公開されました。
原文を表示
The open source Git project just released Git 2.55 with features and bug fixes from over 100 contributors, 33 of them new. We last caught up with you on the latest in Git back when 2.54 was released.
To celebrate this most recent release, here is GitHub’s look at some of the most interesting features and changes introduced since last time.
Repacking with incremental multi-pack indexes
Returning readers of this series may recall our coverage of incremental multi-pack indexes and incremental multi-pack reachability bitmaps. In case you could use a refresher, here’s the short version.
Git stores the contents of your repository as individual objects: commits, trees, and blobs. Those objects usually live in packfiles, which are compressed collections of objects. A packfile has a corresponding pack index that lets Git locate any object inside the pack quickly. But large repositories do not usually have just one packfile: over time, fetches, pushes, maintenance tasks, and repacks can leave many packs behind.
A multi-pack index (or MIDX) gives Git a single index over many packs. Instead of opening and searching each pack’s individual index, Git can ask the MIDX which pack contains a given object and at which offset. This is especially useful for large repositories, and it is one of the building blocks behind GitHub’s repository maintenance strategy.
As we covered when Git 2.47 introduced the incremental MIDX format, a repository can store its MIDX as a chain of layers instead of as a single MIDX covering every pack. A single-file MIDX is simple and efficient to read, but it has an important maintenance cost; since that file includes every pack it covers, even a small update can require a large write in an already-large repository.
Incremental MIDXs address that by storing a chain of MIDX layers. Each layer covers some collection of packs, and the chain file records the order of those layers. Appending a new layer to the tip of the chain does not invalidate the older layers, so Git can index newly created packs without rewriting a single MIDX that covers the entire repository.
Git 2.55 teaches git repack how to write those incremental MIDX chains directly:
$ git repack --write-midx=incremental
Without any other options, that mode is append-only: Git writes a new layer for the packs created by the repack and leaves the existing layers alone. That is already useful when you want to minimize how much metadata gets rewritten during a maintenance run.
But an append-only chain cannot grow forever. If each maintenance run adds a new layer, then eventually the chain itself becomes the thing you need to maintain. Git 2.55 also supports combining --write-midx=incremental with geometric repacking:
$ git repack --write-midx=incremental --geometric=2 -d
When those modes are used together, each repack creates a new tip layer, then decides whether adjacent layers should be compacted together. The default rule is controlled by repack.midxSplitFactor: if the accumulated object count in newer layers grows large enough relative to the next older layer, Git merges those layers into a single replacement layer. Otherwise, the older layers are left untouched.
At a high level, the algorithm works like this. Below, NN refers to the repack.midxNewLayerThreshold value, and ff refers to the repack.midxSplitFactor value:
Pick the un-MIDX’d packs as geometric repacking candidates. If the tip MIDX layer has at least NN packs, include those as candidates too.
Apply the usual geometric repacking rule to that candidate set, and write a new tip MIDX layer covering the resulting packs.
Compact adjacent MIDX layers while the accumulated object count of the newer layer(s) exceeds 1/f1/f of the next deeper layer’s object count.
To see how the pieces fit together, let’s start with a repository that already has an incremental MIDX chain. The older layers are on the left, and the tip layer is on the right. Meanwhile, normal repository activity keeps producing new packs. Those packs are not covered by any MIDX layer yet, which means the next maintenance run has two jobs: decide what to repack, and decide how much of the MIDX chain to rewrite.

Ordinarily, those un-MIDX’d packs are the only geometric repacking candidates: Git can write a new pack and a new tip MIDX layer without disturbing any existing layer. The figure below shows the more interesting case, where the current tip layer has accumulated enough packs to meet the configured repack.midxNewLayerThreshold. Once that threshold is met, packs from the tip layer can join the newly written packs as geometric repacking candidates.
Geometric repacking then asks a local question about the newest candidate packs. Geometric repacking then asks a local question about the newest candidate packs: is the pack immediately to the left of some suffix of packs (풫\mathcal{P}) large enough to preserve the geometric progression if Git rolls up 풫\mathcal{P}? In the first attempt below, 풫\mathcal{P} contains the smallest pack from the current tip layer along with the new un-MIDX’d packs. But the pack to the left of the split is only 30,000 objects, which is smaller than twice the size of 풫\mathcal{P}, so this split is too far to the right.

So Git moves the split one pack earlier and asks the same question again. Now 풫\mathcal{P} includes one more pack from the tip layer. The pack immediately to the left has 100,000 objects, which is at least twice the size of the selected suffix. That is the point where the geometric invariant holds, so Git can roll up exactly those packs into a new pack.

After writing that new pack, Git writes a new tip MIDX layer over the surviving pack from the previous tip layer and the newly written roll-up pack. At this point, the packfiles themselves are in good shape, but the MIDX chain may still have accumulated too many small adjacent layers. Git applies the same “newer compared to older” instinct to the MIDX layers themselves: if the newer layer is large enough relative to its neighbor, compact their metadata into a replacement layer.

That compaction step is deliberately metadata-only. Git does not repack the objects from those layers again; it writes a new MIDX layer that covers the same packfiles. Then it considers the next older layer. Here, the compacted layer is still smaller than half of the deeper layer, so Git stops. The older layer remains untouched, which is the key property that keeps this maintenance incremental.

The result is a compromise between two extremes. A single-file MIDX minimizes lookup complexity, but can require large rewrites during maintenance. A purely append-only incremental MIDX minimizes each write but allows the chain to grow without bound. Geometric incremental repacking keeps the number of layers logarithmic in the total number of objects, while ensuring that the newest, smallest layers are rewritten more often than older, larger ones.
This also integrates with Git’s existing repack machinery. Newly written packs that are not yet covered by the MIDX chain are always candidates for the geometric repack; packs in deeper MIDX layers are left alone. Packs in the tip MIDX layer join the candidate set only after the tip layer has at least repack.midxNewLayerThreshold packs. If the tip layer is still smaller than that threshold, Git skips disturbing it entirely and simply appends a new layer for the newly written packs.
For repositories that receive a steady stream of new objects, this means routine maintenance can update the repository’s pack metadata incrementally, without forcing each maintenance run to rewrite a single MIDX covering the entire object store.
[source]
Fixing up earlier commits with git history
Anyone who has polished a commit series before sending it for review has probably had this experience: you notice that a change in your working tree really belongs in an earlier commit, not at the tip of the branch.
Today, one common way to handle that is to create a fixup commit and then autosquash it:
$ git commit --fixup=<commit>
$ git rebase --autosquash <commit>^
That works, but it asks you to spell out the mechanism instead of the intent. Git 2.55 builds on the experimental git history command, which Git 2.54 introduced, by adding a new fixup subcommand. It applies the changes currently staged in the index to an earlier commit:
$ git history fixup <commit>
Here is a small example. The first commit introduced a pancake recipe, followed by a few more commits on top. Later, we realize that the recipe was missing maple syrup. After staging that one-line change, git history fixup <commit> folds it into the original recipe commit and replays the descendant commits on top.

Here the staged change becomes part of the target commit itself. The target commit keeps its message and authorship by default, unless you pass --reedit-message, and Git rewrites the commits that follow so the branch ends at an equivalent history with the fix in the right place.
Like the rest of git history, this command is still experimental. It is also intentionally conservative. Because fixup reads from the index, it needs a working tree and cannot operate in a bare repository; if applying the staged change would produce a conflict, the command aborts instead of leaving you in the middle of a stateful rewrite.
[source]
The tip of the iceberg…
Now that we’ve covered the largest changes in more detail, let’s take a look at a selection of some other new features and updates in this release.
Returning readers of this series may remember our coverage of config-based hooks from Git 2.54, which let you define hooks in your Git configuration rather than only as executable files in $GIT_DIR/hooks. Hooks are the scripts Git runs at well-known points in your workflow, like before creating a commit or after receiving a push. Moving them into configuration makes those hooks easier to share, compose, and selectively disable without copying scripts into each repository’s hooks directory.
Git 2.55 extends that work by allowing compatible configured hooks to run in parallel. For example, a project might have independent pre-commit hooks for linting and unit tests; if both declare hook.<name>.parallel = true, Git can run them at the same time. The number of concurrent jobs can be controlled globally with hook.jobs, per event with hook.<event>.jobs, or on the command line with git hook run -j. Hooks that need shared state, like commit-message hooks or other hooks that inspect the index or working tree, continue to run serially.
[source]
If you have ever run git status only to be greeted by a long pause at your terminal, you may have used Git’s built-in filesystem monitor to speed things back up. When core.fsmonitor is enabled, commands like git status can ask a long-running daemon which paths have changed instead of scanning the entire working tree.
Until now, that built-in daemon was available only on macOS and Windows. Git 2.55 adds support for Linux, where the implementation uses inotify. That works without elevated privileges, but requires one watch per directory, so very large repositories may need to raise the fs.inotify.max_user_watches limit. As on other platforms, the daemon is conservative around network-mounted repositories, which remain opt-in.
[source]
Reachability bitmaps are one of the tricks Git uses to answer questions like “which objects are reachable from this commit?” without walking the entire object graph from scratch. They make object traversals faster, but Git still has to build and update those bitmaps during maintenance tasks like git repack --write-midx-bitmaps.
Git 2.55 makes that generation path faster by avoiding unnecessary tree recursion, reusing already-computed selected bitmaps, caching object positions, and sorting bitmaps before XORing them together. In benchmarks from the patch series, those general improvements reduced bitmap generation time in one large repository from about 612 seconds to about 294 seconds.
The same series also improves pseudo-merge bitmaps, which group related references together so Git can combine precomputed bit arrays during a traversal instead of rediscovering the same objects repeatedly. In one benchmark, pseudo-merges made a full git rev-list --objects --use-bitmap-index traversal nearly 20 times faster, but previously nearly doubled bitmap generation time. After these changes, pseudo-merges keep most of their traversal speedup while adding much less work to the bitmap generation path.
[source, source]
If you use partial clones, filtered packs, or other workflows where Git intentionally omits some objects, pack size still matters. The git pack-objects --path-walk mode, introduced in Git 2.51, groups objects by path before performing a second compression pass, which can produce better deltas when path locality matters.
In Git 2.55, --path-walk can be combined with filters including blob:none, blob:limit=<n>, tree:0, object:type=<type>, sparse:<oid>, and compatible combine: filters. That makes packing using --path-walk available in more partial-clone and filtered-pack workflows. In one benchmark on Git’s own repository, a blob-less path-walk repack produced a pack roughly 16% smaller, at the cost of a slower fresh-delta computation.
[source]
Git learned a new experimental command, git format-rev, for pretty-formatting revisions from standard input. Unlike git log, which walks a range of history, git format-rev is designed for cases where you encounter commits one at a time or embedded in other text.
For example, suppose you’re using git last-modified to print the commit that last modified each path in some directory. What if you wanted to know who last modified each path, not just which commit did it? You could replace those commits with author names by piping its output through something like this:
$ git last-modified | perl -F'\t' -lane '
chomp($F[0] = qx(git show -s --format=%an $F[0]));
print join "\t", @F
'
Junio C Hamano builtin/commit.c
[...]
That works, but it has to start a new Git process for each row just to format the commit. In Git 2.55, git format-rev can handle that part as a normal pipeline:
$ git last-modified |
git format-rev --stdin-mode=text --format=%an
Junio C Hamano builtin/commit.c
[...]
The command’s text mode can also rewrite full commit object names found in freeform text, which makes it useful for commit-message hooks or other scripting workflows.
[source]
When you push your repository somewhere, you may have noticed output that starts with remote::
$ git push origin main
Enumerating objects: 5, done.
[...]
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
When a client fetches or pushes, Git can multiplex different streams over the same connection: one sideband carries packfile data (the actual objects being transferred), another carries progress messages that the client usually prints to stderr, and a third carries errors from the remote.
Those progress messages are useful, but they also come from the other side of the connection and may be printed directly to your terminal. Before Git 2.55, that meant a server could include arbitrary terminal control sequences in sideband output, including sequences that move the cursor or erase text. Git now masks most of those control characters by default while still allowing ANSI color sequences, so colored progress output continues to work.
[source]
Suppose you are halfway through editing a file when you realize that you started from the wrong branch. If the branch you want to switch to changed the same path, a plain git checkout <branch> will refuse to move and risk clobbering your work. git checkout -m <branch> is the “try to carry my local edits with me” version of that operation.
But what happens when the other side has modifications against that same path? Previously, git checkout -m gave you one chance to resolve the resulting conflicts immediately. Git 2.55 makes that safer by using an autostash internally, so the conflicted local changes are saved as a stash entry that you can either resolve right away or reapply later.
[source]
Some projects need to publish the same branch to more than one place, like a primary host and one or more mirrors. Remote groups have long been available to git fetch, where a group is configured with remotes.<name> as a whitespace-separated list of remotes. Git 2.55 lets git push use the same shorthand:
$ git config remotes.publish "github gitlab mirror"
$ git push publish main
This is equivalent to pushing to each remote in the group in sequence. Since atomicity can only be guaranteed for a single transport connection, --atomic is not supported when pushing to a group.
[source]
git log --graph is great for visualizing branch structure, right up until the graph itself gets too wide to read. In repositories with many parallel branches, the graph lanes can consume most of the terminal before you get to the commit subject.
Git 2.55 adds --graph-lane-limit=<n> to git log --graph and related commands. Lanes beyond the limit are replaced with ~, making graph output more manageable in repositories with very wide histories.
[source]
Suppose you want to list the 10 most recent commits on your branch. That is easy enough: git log -n 10 does exactly that. But what if you want the 10 oldest commits? If you are thinking, “it surely isn’t git log --reverse --10,” then congratulations: you’re a veteran Git user! Instead of reversing the history and then printing 10 commits, Git takes the 10 most recent commits and reverses their order.
You can get there by post-processing the whole range (for example with git log --reverse <range> | tail -10) but doing so still asks Git to print and format all of the commits that the shell is going to throw away. Git 2.55 adds a new --max-count-oldest=<n> option to git rev-list and the git log family of commands, which selects the oldest n commits in a range instead.
[source]
During a fetch, the client and server negotiate by having the client advertise commits it already has as have lines. That lets the server avoid sending objects the client can already reach. But in repositories with many references, the negotiation algorithm may skip a ref that is especially important for finding common history.
Git 2.55 adds new controls for which references participate in negotiation. The new include and restrict options, along with corresponding remote.* configuration, allow users to require certain refs to be sent as have lines or to limit negotiation to a specific set of refs.
[source]
…the rest of the iceberg
That’s just a sample of changes from the latest release. For more, check out the release notes for 2.55, or any previous version in the Git repository.
The post Highlights from Git 2.55 appeared first on The GitHub Blog.
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み