Highlights from Git 2.44

Git 2.44のハイライト

Image of Ishikawa Setsuna
Author

オープンソースのGitプロジェクトは、新しく加わった34人を含む総勢85人以上のコントリビューターによる新機能の追加とバグ修正が行われたGit 2.44をリリースしました。前回 Git の最新情報をお伝えしたのは、2.43 がリリースされた時でした。

今回の最新リリースを記念して、前回から導入された最も興味深い機能や変更点を GitHub がいくつか紹介します。

マルチパックの再利用によるパック生成の高速化

GitHub との間でリポジトリをプッシュしたりプルしたりする時に Git の出力を詳しく見たことがある人1なら、出力の最後にpack-reused という数字が表示されていることに気づいたかもしれません:

$ git clone git@github.com:git/git.git
Cloning into 'git'...
remote: Enumerating objects: 361232, done.
remote: Counting objects: 100% (942/942), done.
remote: Compressing objects: 100% (453/453), done.
remote: Total 361232 (delta 598), reused 773 (delta 487), pack-reused 360290
[...]

この数字(上の図ではpack-reused 360290 )を見て、どういう意味なんだろうと思ったことがあるのなら、もう何も気にしないでください!

一般的に、この数字は、GitHub が新しいパックをその場で生成するのではなく、すでに存在するパックの一部を(多かれ少なかれ)そのままクローン作成者にストリーミングすることで、どれだけのパックを送信できたかを示しています。Gitがオブジェクトをクライアントに送ったり(フェッチ/クローン時)、サーバーに送ったり(プッシュ時)、自分自身に送ったり(リパック時)するとき、Gitは転送されるオブジェクトのセットを含むパックファイルを生成する必要があります。このパックに含まれるオブジェクトの多くについて、Gitはそれらのオブジェクトを探し出し、開いて解析し、必要に応じて既存のオブジェクトと組み合わせてデルタチェーンを形成します。

パック内のすべてのオブジェクトに対してこのプロセスを繰り返すことで、Gitは似た内容を持つオブジェクトを見つけ出し、ペアにしてスペースを節約するため、よりコンパクトな結果が得られます。少量のデータをGitHubにプッシュする場合は、この検索は無視できる程度で、それほど時間はかかりません。しかし、クローン作成中にリポジトリ内の到達可能なオブジェクトをすべてロードしてデルタ化し直すのは、特に数万以上のクローンを作成する場合、法外なコストがかかる可能性があります。

時間を節約するために、Gitはショートカットを使います。オブジェクトの転送に使われるワイヤーフォーマットは、ディスク上の.pack ファイル($GIT_DIR/objects/pack )と同じ表現を使うので、クライアントに送る新しいパックを生成するときに、既存のパックファイルのセクションをバイト単位で再利用できます。

上記の例では、まさにそれが起こっています。pack-reused 360290 の出力は、GitHub がパックを再オープンして新しいデルタを検索せずに、ディスク上の 360,290 個のオブジェクトを再利用できたことを示しています。プロセスは、残りのオブジェクトに対してのみ行われました(この場合、361,232から再利用された量を差し引いた900強のオブジェクトが遅いプロセスをたどったことになります)。

パックをそのまま再利用することはとてもお得に聞こえるでしょう?そのとおりですが、Gitがこの最適化を利用できる頻度にはいくつかの制限があります:

  • パックファイルは、同じオブジェクトを二度以上含めることはできません。単一パックの再利用であれば、これは簡単なことです(再利用元のパックにもオブジェクトの重複したコピーを含めることはできないため)。しかし、複数パックの再利用の実装は難しくなります。
  • ある種のデルタ(デルタとベースの間のバイト数によってベースを識別する)は、デルタとベースの間に省略されたセクションがある場合、オフセットを変更して”パッチ”する必要があります。

逐語的パック再利用を最大限に利用するためには、リポジトリはオブジェクトの大部分を単一のパックファイルにまとめる必要があります。多くのリポジトリにとって、これは大きな問題ではありませんが、何億ものオブジェクトを含む大規模なリポジトリでは、法外なコストがかかることもあります。

Git 2.44では、複数のパック間でオブジェクトを再利用できるようになりました。到達可能性ビットマップを使ったマルチパックインデックスを使う場合(これについては、ブログ記事 Scaling monorepo maintenance(モノレポメンテナンスの拡張)を参照ください)、Git は複数のパックにまたがってこの最適化を利用できるようになり、リポジトリを単一パックにリパックする必要がなくなりました。

正確な詳細については、マルチパックの再利用に特化したブログ記事で今後取り上げます。とりあえず、GitHub にプッシュしたときにターミナルに新しい行が出力されることに気づくかもしれません:

$ git push
Enumerating objects: 350175, done.
Counting objects: 100% (832/832), done.
Compressing objects: 100% (132/132), done.
Total 350175 (delta 735), reused 700 (delta 700), pack-reused 349343 (from 36)
[...]

pack-reused だけでなく、その隣に (from 36) という情報が追加されていることにお気づきでしょうか。これは、オブジェクトが再利用されたパックの数を示しています。

これを自分で試すには、ローカルにインストールした Git をアップグレードして、GitHub にプッシュする前に以下のコマンドを実行してください。

$ git config --global pack.allowPackReuse multi
$ git multi-pack-index write --bitmap

[ソース]

git replayによるリベースの高速化 (およびその他)

このシリーズを読んだことがある人なら、Git の最近の開発である、マージバックエンドを最初から書き直したmerge-ort についての記事をご存知でしょう。このシリーズを初めてご覧になる方は(まずは歓迎しましょう!)Git 2.33 からのハイライトの記事から始めるのがよいでしょう。

merge-ort は、Gitの数十バージョン近く前に導入され、前身であるrecursiveバックエンドに関する長年の問題を解決することを目的としていました。recursiveバックエンドは修正が難しいことで有名で、大量のリネームを含むマージをうまく処理するのが困難でした。

merge-ort バックエンドは、(既存の挙動に関して、既存のバックエンドをそのまま置き換えて)正しく、性能が高く、変更が容易な構造化された実装を提供することで、このような問題に対処するために導入されました。Git 2.34 (興味のある方は、ここからご覧ください)では、merge-ort がデフォルトのマージバックエンドとなりました。つまり、特別な設定をせずにGit 2.34以降を使用している場合は、ほぼ間違いなくmerge-ort を使っていることになります。最近バージョンのGitでは、マージやリベースのファイル間の衝突を解決するためにmerge-ort バックエンドを使っています。merge-ort が導入され広く使われるようになり、マージやリベースの計算が大幅に高速化されるでしょう。

ただし、merge-ort を使うと、リポジトリの完全なチェックアウトを必要とせずにマージやリベースを計算することもできます。マージを実行するために merge-treeコマンドは--write-tree オプションを使い、リポジトリのチェックアウト済みバージョンを必要とせずにmerge-ort でマージを計算します。

リベースは別の話です。既存のgit rebase サブコマンドは、歴史的な設計上の決定や仮定を多く含んでおり、merge-ort との統合は簡単なものではなく、後方互換性の保証を壊さない限りはパフォーマンスを妨げてしまいます。2

git replay は、このような課題に対処するために存在します。これは、はるかにパフォーマンスが高いだけでなく、git rebaseの代替手段を提供します:

  • ベアリポジトリで動作する。
  • (ベアリポジトリ以外では)現在チェックアウトしているブランチ以外のブランチをリベースできる。
  • 複数のブランチを同時に操作できる。

などなど。GitHubでは、GitHub.comで行われるすべてのマージ(そして最近ではすべてのリベース)を強化するために、1年以上にわたってmerge-ort を使用し、両方の操作に大幅なパフォーマンス向上をもたらしています。

リポジトリ内でスクリプトを書いたり、git rebase に比べてパフォーマンスを上げることに興味があったり、あるいは単に Git プロジェクトの最新・最高の開発を試してみたいと思っているのなら、git replayが便利であることが分かるかもしれません。どちらの立場であれ、 git replay についてもっと知ることができます。

[ソース]


  • リベースの話題のついでに、--autosquash について話しましょう。このオプションを使用したことがなくても心配はいりません。ここで簡単に紹介します。リベースするとき、Git は件名がfixup! [...]squash! [...]amend! [...] で始まるコミットを組み合わせようとします。ここで、[...] は他のコミットのログメッセージです。Gitはこれらを組み合わせ、fixup! [...] のコミット(など)をfixup! 以外のコミットの隣に置くようにTodoリストを再編成します。動詞によって、Git は変更を組み合わせたり、コミットメッセージを変更したり、連続したコミットメッセージをマージして、作業を簡単に編集できるようにします。しかし、以前のバージョンの Git では、これらのオプションはgit rebase --interactive (略してgit rebase -i) を使った対話的リベースにしか対応していませんでした。fixup! のコミット (または同等のもの) を書いて、それを履歴の適切な位置に素早く適用したい場合は、(a)git rebase -i を実行して$EDITOR を閉じるか、(b)GIT_SEQUENCE_EDITOR=true git rebase -i を実行する必要がありました。

    Git 2.44 では、--interactive 以外のリベースでも自動スカッシュが機能するようになりました。つまり、git rebase を実行し、fixup! をそれぞれの場所に適用することができるようになりました。Todo リストを調べたりGIT_SEQUENCE_EDITOR 環境変数を変更する必要はありません。

    [ソース]

  • Gitを長く使っている人(あるいは初心者)なら、以下のようなhint: で始まるメッセージを見たことがあるでしょう:
    hint: Updates were rejected because the tag already exists in the remote. 
    hint: Disable this message with "git config advice.pushAlreadyExists false" 
    

    ヒントが示すように、git config advice.pushAlreadyExists false を実行すれば、Git にメッセージを表示しないように指示できます。しかし、アドバイスが役に立つとしたらどうでしょう?(例えば)--force を使用せずに、同じ名前のタグがすでに存在するリモートにタグをプッシュしようとした場合、警告が表示されて欲しいと思うかもしれません。その場合も、”Disable this message with […]”というヒントの部分は表示したくありません。

    Git 2.44では、git config advice.pushAlreadyExists true を設定して、そのヒントを受け取りたいことを示すことができるようになりました。そうすれば、Gitはヒントを表示し続け、”Disable this message with […]” の部分は表示しなくなります。

    [ソース]

  • 簡単なクイズ:git for-each-ref--no-sort オプションを指定するとどうなるでしょうか?”すべての参照がアルファベット順以外で一覧されるわけではない”と思ったなら、おめでとうございます。あなたはベテランのGitユーザです。--no-sortはその名前にも関わらず、ソートされた順番でgit for-each-ref の結果を提供するため、任意の順序を想定した特定の最適化を利用できませんでした。

    技術的な詳細に興味がある場合は、以下のリンクでより学ぶことができます。数字だけが必要な場合、あなたは幸運です。私のマシンで、参照数の多いリポジトリに対して、git for-each-ref --no-sortは標準的なgit for-each-refより20%以上優れています。

    [ソース]

  • Gitのドキュメントに多くの時間を費やした人は”pathspec”という用語に遭遇し、これが何かと疑問に思ったことがあるでしょう。Git用語で”pathspec”は、Gitコマンドと組み合わせて使用する場合の”ファイルパスを制限する方法”に大まかに対応します。ドキュメントにはたくさんの例がありますが、注目すべき例をいくつか挙げましょう: git show ':^Documentation/' (「Documentationディレクトリの変更を除いた最後のコミットを表示する」)、git show ':(icase)**/*sha256*' (「大文字小文字にかかわらず、パスに ‘sha256’ が含まれるファイルを表示する」)、git show ':(attr:~binary)' (「.gitattributesbinary 属性が設定されていないファイルを表示する」)。

    Git 2.44 では、git addattr pathspec マジックを理解するようになりました。つまり、git add ':(attr:~binary)' のように、すべてのテキストファイル/バイナリ以外のファイルをインデックスにステージできるようになりました。

    Git 2.44 では、builtin_objectmode という新しい pathspec 属性も導入されています。この新しいpathspecマジックを使用すると、モードごとにパスのフィルタリングができます(例えば、実行不可能なファイルには100644 、実行可能なファイルには100755 、サブモジュールには160000 、など)。builtin_ プレフィックスは、.gitattributes ファイルに値を設定せずに、この pathspec マジックを使用できることを示しています。つまり、git add ':(builtin_objectmode=100755)' のように、作業コピーにすべての実行可能ファイルを追加できます。

    [ソースソース]

全体

これは最新リリースからの変更点のほんの一例です。詳細については、Gitリポジトリ内の2.44以前のバージョンのリリースノートを確認してください。

注釈


  1. このブログ記事(特に脚注!)を読んでいるのであれば、それをしている確率はかなり高いでしょう。
  2. 興味のある人は、なぜgit rebase を拡張する代わりにgit replay を使用した理由についての議論をこちらのメーリングリストを確認してください。

The postGit 2.44 のハイライトappeared first onGitHub Blog.