目次:

用語定義

円錐モード(cone mode)

sparse-checkout で、目的の、ファイル群のサブセットを指定するための 2 つのモードのうちの 1 つ。 円錐モード(cone mode)では、 ユーザーはディレクトリを指定します(そのディレクトリ下にある全てのものと、先頭のディレクトリ内にある全てのものの両方を取得します)。 一方、 非コーン・モードでは、 ユーザーは gitignore スタイルのパターンを指定します。 sparse-checkout init|set への --[no-]cone オプションによって制御されます。

SKIP_WORKTREE

追跡中のファイルがスパース仕様にマッチせず、 作業ツリーから削除されると、 インデックス内の当該ファイルには SKIP_WORKTREE ビットが設定されます。 追跡中のファイルに SKIP_WORKTREE ビットが設定されているにもかかわらず、 その後に当該ファイルがユーザーによって作業ツリーに書き込まれた場合、 SKIP_WORKTREE ビットは後続の Git 操作の開始時にクリアされることに注意してください。

ほとんどのスパース・チェックアウトの利用者はこの実装の詳細を知らないため、 一般にユーザー向けの説明やコマンド・フラグではこの用語の使用を避ける必要があります。 残念ながら、 この低レベルな詳細は sparse-checkout サブコマンドが使用される前に公開されており、 この記事の執筆時点でもまださまざまな場所で公開されたままです。

sparse-checkout

作業ツリーに存在するファイルを、 全ての追跡中ファイルのサブセットに減らすために使用される git のサブコマンド。 また、 ユーザーの希望するサブセットに対応するスパース・パターンを追跡するために使用される $GIT_DIR/info ディレクトリ内のファイル名。

sparse cone

円錐モード(cone mode)参照

スパース・ディレクトリ

ディレクトリに対応するインデックス内のエントリ。 通常はそのディレクトリ下にあるすべてのファイルの代わりにインデックスに現れます。 「スパースインデックス」(sparse-index)も参照してください。 混乱を引き起こす可能性があるのは、「スパース・ディレクトリ」がスパース仕様と一致しないことです。 つまり、 ディレクトリが作業ツリーに存在しないことです。 将来的に名前が変更される可能性があります(例えば「skipped directory」)。

スパース・インデックス

ディレクトリ下にあるすべてのファイルの代わりにディレクトリ・エントリを記録することでインデックスをスパース(疎らに)する(つまり、 そのディレクトリは、遺憾ながら「スパース・ディレクトリ」とも呼ばれる、「スキップされたディレクトリ」になります)、 sparse-checkout の特別なモードで、これを複数のディレクトリに対して行う可能性があります。 init|set|reapply への --[no-]sparse-index オプションによって制御されます。

スパース・パターン(sparsity patterns)

対象のファイルの組を定義するために使用される $GIT_DIR/info/sparse-checkout のパターン。 警告: スパース・パターンという用語(または短い「パターン」) は、 次の 2 つの理由から簡単に乱用されます: (1) 円錐モード(cone mode)の利用者はパターンではなくディレクトリを指定する(ディレクトリの指定はパターンに変換されますが、「パターン」という言葉を使用すると、ユーザーは非円錐モード(non-cone mode)について話していると考えるかもしれません)ため、 (2) 作業ツリーまたはインデックス内のスパース仕様が一時的にスパース・パターンと異なる事があるかもしれません(「スパース仕様 V.S. スパース・パターン」参照)。

スパース仕様(sparse specification)

利用者の注目領域内のパスの組。 これは通常、 スパース・パターンにマッチする追跡中のファイルだけですが、 スパース仕様が一時的に異なり、 追加のファイルが含まれる場合があります。 (「スパース仕様 V.S. スパース・パターン」も参照下さい)

  • 履歴を扱う場合、スパース仕様は正確にスパース・パターンにマッチするファイルの組です。

  • 作業ツリーと対話する場合、 スパース仕様は、 クリアされた SKIP_WORKTREE ビットを持つ追跡中ファイルの組、 または作業コピーに存在する追跡中ファイルです。

  • インデックスの結果を変更または表示する場合、 スパース仕様は、クリアされた SKIP_WORKTREE ビットを持つファイルの組、 または HEAD とはインデックスが異なるファイルの組です。

  • インデックスと作業コピーを使用する場合、スパース仕様は上記のパスを合わせたものです。

vivifying

コマンドが追跡中のファイルを作業ツリーに復元する(そして、 たぶん、そのファイルのインデックス内の SKIP_WORKTREE ビットもクリアする)ことを、ファイルの「vivifying」(復活)と呼びます。

sparse-checkouts(まばらなチェックアウト)の目的

sparse-checkout は、 ユーザーのファイル群のサブセットを操作できるようにするために存在します。

sparse-checkouts は、「追跡中のファイル」を 2 つのカテゴリ — スパース・サブセットとその他すべて — に再分割するものと考えることができます。 その実装としては、 インデックス内の「残りすべて」を SKIP_WORKTREE ビットでマークし、 作業ツリーから除外します。 SKIP_WORKTREE ビットをマークしたファイルは引き続き追跡中ですが、 作業ツリーには存在しないだけです。

以前は、スパース・チェックアウトは「SKIP_WORKTREE とは、 そのファイルが作業ツリーから欠落しているが、 ファイルの内容が HEAD と一致するかのように装うことを意味する」と定義していました。 それはインチキ(実際には、 作業ツリーで欠落しているファイルが HEAD ではなくインデックスと一致することを意味していました)であるだけでなく、いくつかのコマンドにしかまともな動作を提供しない低レベルのものだった。 その指導原理がユーザーの期待に反するような結果をもたらすことは驚くほど多く、 よくない概念モデルであった。 しかし、それは長年にわたって存在したので、 今でもコードベースの片隅に残っているかもしれません。

とにかく、 「ファイル群のサブセットを操作する」という考え方は非常に単純ですが、 一部の Git サブコマンドの動作に影響を与える、 複数の異なる高レベルのユースケースが存在します。 さらに、これらのユースケースの 1 つだけを考慮したとしても、 スパース・チェックアウトはさまざまなサブコマンドを 6 種類以上の異なる方法で変更できます。 まずは、以下のような高レベルのユースケースを検討してみましょう。

A)

ユーザーはリポジトリのスパース(まばら)な部分「のみ」に関心があります

A*)

ユーザーは、これまでにダウンロードしたリポジトリのスパース(まばら)な部分「のみ」に関心があります

B)

ユーザーはスパース(まばら)な作業ツリーを望んでいますが、 より大きな全体で作業しています

C)

sparse-checkout は、 Git のための特別に作成された内部仮想ファイル・システムと連携できるようにする舞台裏の実装詳細です。 ユーザーは実際には、遅延的に取り込まれた「完全な」作業ツリーを操作しているため、 スパース・チェックアウトは遅延取り込み部分で役立ちます。

これらのそれぞれをもう少し詳しく説明する価値があるかもしれません:

(振る舞いA)

ユーザーはリポジトリのスパース(まばら)な部分「のみ」に関心があります

ユーザーは、 リポジトリに他のものが存在することを知っているかもしれませんが、 気にしません。 ユーザーはリポジトリの他の部分には興味がなく、 関心のある領域内の変更についてのみ知りたいと考えています。 履歴の他のファイル達(たとえば diff/log/grep/ など)を表示することは、有用性を損なう事であり、 履歴の他の部分の変更達が、 ユーザーが興味を持っている変更を矮小化する可能性があるため、 大きな迷惑になる可能性がある。

これらのユーザーの中には、部分(partial)クローンを、(スパース仕様内の BLOB をダウンロードした方法で、)スパース・チェックアウトとともに使用し、 非接続開発を行うことを希望してこのユースケースにたどり着いた人もいます。 これらのユーザーは通常、 リポジトリの他の部分を気にしないだけでなく、 Git コマンドがそれらの部分を操作しようとするのを妨げるものであると考えています。 コマンドがスパース仕様の範囲外の履歴内のパスにアクセスしようとすると、 部分クローンはオンデマンドで追加の BLOB をダウンロードしようとしますが失敗し、 その後ユーザーのコマンドも失敗します。 (これは場合によっては避けられないかもしれません。たとえば、 git merge でスパース仕様の外側で調整するための重要な変更がある場合など、 ユーザーがネットワークに接続することを強制される頻度を制限する必要があります。)

また、 常にネットワークに接続していても構わない部分クローンを使用しているユーザーであっても、 他のさまざまなコマンド(マージまたはプル後に出力される diffstat など)の副作用として BLOB をダウンロードする必要があるため、 ローカル・リポジトリのサイズが不必要に増大するのではないかという懸念につながる可能性があります[10]。

(振る舞いA*)

ユーザーは、 これまでにダウンロードしたリポジトリのスパース(まばら)な部分「のみ」に関心があります(これは最初のユースケースのバリエーションです)

このバリエーションは、 履歴クエリ操作のスパース仕様が、「スパース・パターン」から「既にダウンロードした BLOB に限定されたスパース・パターン」に変更された「振る舞いA」とみなすことができます。

(振る舞いB)

ユーザーはスパース(まばら)な作業ツリーを望んでいますが、 より大きな全体で作業しています

Stolee はこのユースケースを以下のように説明しています[11]:

「また、 私はそれが大きな全体の一部であることを認識しているユーザーにも焦点を当てています。 彼らは大規模なリポジトリを操作していることを知っていますが、 自分たちが貢献するために必要な部分は何かに焦点を当てています。 複数の「担当」(roles)が、 まったく異なりほとんど結合していないコードベース部分を使用することが予想されます。 他の「設計」ユーザーの中には、 ツリー全体にわたって操作したり、 必要に応じてコードベースの異なるセクション間を行き来したりします。 この状況では、 スパースチェックアウト定義、 特に git log にあまりにも多くの機能の範囲を設定することには慎重であるべきで、それはコードベースの見方があなたの「観点」に依存すると非常に混乱する可能性があるためです。

また、 プロジェクト間の複雑な依存関係により、 最終的に「振る舞いB」が必要になる場合もあります。 スパース・チェックアウト(まばらなチェックアウト)を使用する最初の試みには、 通常、 直接関心のあるディレクトリと、 それらのディレクトリがリポジトリ内で依存しているものが含まれます。 しかし、 ここにそれを台無しにするものがあります(there’s a monkey wrench here)。 統合テストを行う場合、 序列が反転します。 統合テストを実行するには、 関心のあるものとそのツリー内の依存関係だけが必要なのではなく、 あなたが興味のあるものに依存するもの、 またはその依存関係のいずれかに依存するもの全ても必要で…そして、その展開されたグループのツリー内の依存関係がすべて必要です。 これにより、 あなたのスパース・チェックアウト(まばらなチェックアウト)をほぼ密なチェックアウトに簡単に変えることができます。

当然ながら、 これはチェックアウトがスパース(まばら)である利点を殺す傾向があります。 この難問に対する解決策がいくつかあります。 それは、 (たぶん、 あなたがどこかの CI キャッシュでプルしたリポジトリ内の依存関係のビルド・バージョンがあるでしょうから、 )リポジトリ内の依存関係を取得しないようにするか、 あるいは、 ユーザーが統合テストを直に実行せずにコード・レビューを送信するときに CI サーバー上で実行するよう指示する事です。 あるいはその両方を行います。 リポジトリ内の依存関係をもみ消すか、 依存しているものをもみ消すかに関係なく、 リポジトリの他のもみ消された部分の問い合わせを行って認識する必要があるのは確かで、 特に依存関係が複雑である場合や、 比較的頻繁に変更される場合には注意が必要です。 したがって、 そのような用途では、 スパース・チェックアウトを使用して、 直接に構築や変更するものを制限できますが、 これらのユーザーは、 スパース・チェックアウト・パスによって履歴内のバージョンの問い合わせを制限することを必ずしも望んでいる訳ではありません。

単純にパフォーマンス上の回避策として、 「振る舞いA」ではなく「振る舞いB」に興味を持つ人もいるかもしれません。 非円錐モードを使用している場合は、 非円錐モード固有の等比級数的パフォーマンス増大の問題に対処する必要があります。 このモードでは、 パスがスパース仕様にマッチするかどうかをチェックするすべての操作にコストがかかる可能性があります。 そのため、 これらのユーザーは、 作業コピーを操作するときにのみ高価なチェックにお金を払うつもりであり、 遅いコマンドを使用するよりも履歴クエリから「無関係な」結果を取得することを好む可能性があります。

(振る舞いC)

sparse-checkout は、特殊な VFS をサポートする実装詳細です。

このユースケースは、 実際に完全なチェックアウトまたは密なチェックアウトをユーザーに提示しようとするという点で、 スパース・チェックアウトの従来の定義に少々反しています。 ただし、 このユースケースでは、 同一の基礎技術基盤を新しいやりかたで利用することで、 ユーザーにパフォーマンス上の利点をもたらします。 基本的なアイデアとしては、 ある企業で社内用に Git 対応仮想ファイル・システムを持っていて、 そのすべてのファイル・システム・アクセスの傍受により、 部分クローンを介してオンデマンドでアクセスされたファイルを取り出したりしたり書き込んだりすることで、 すべてのファイルが作業ツリーに存在するかのように見せることができるというものです。 VFS はスパース・チェックアウトを使用して、 Git が多くのファイルに書き込んだり注意を払うのを防ぎ、 ユーザー・アクセスと作業ツリー内のファイルの変更に基づいてスパース・チェックアウト・パターン自体を手動で更新します。 このような VFS の詳細な説明については、コミット ecc7c8841d("repo_read_index: add config to expect files outside sparse patterns", 2022-02-25) と [17] のリンクを参照してください。

ここでの最大の違いは、 ユーザーはスパース・チェックアウト機構が使用中であることにまったく気づいていないことです。 スパース・パターンはユーザーによって指定されるのではなく、 VFS の完全な制御下にあります(そして、 パターンは VFS によって頻繁かつ動的に更新されます)。 ユーザーはチェックアウトが高密度(まばらでない)であると認識するため、 コマンドはすべてのファイルが存在するかのように動作する必要があります。

主たる関心事に関するユースケース

このドキュメントの残りの大部分は、 振る舞いA と 振る舞いB に焦点を当てます。 他の 2 つのケースと、それらに焦点を当てない理由についてもいくつか書いておきます:

(振る舞いA*)

このユースケースをサポートすることは難しく、 多大な労力がかかると予想されます。 現時点では実装の予定はありませんが、 将来的には代替手段となる可能性があります。 追加の代替手段の存在を知ることは、 コマンド・ライン・フラグの選択に影響を与える可能性があるため(たとえば、単なる2値フラグではなくトライステート・フラグやクアッドステート・フラグが必要かどうか等)、 少なくとも注意は払っておくことが重要です。

さらに、 振る舞いA に関する以下の説明は、 おそらくこのユースケースでも依然として有効であると思いますが、 唯一の例外は、 スパース仕様を再定義して、 既にダウンロードされた BLOB に制限することです。 難しいのは、その変更された定義を尊重できるコマンドを作成することです。

(振る舞いC)

この使用例は、 初期のスパース・チェックアウトに関する文書化された前提条件の一部に違反しています(SKIP_WORKTREE としてマークされたファイルは、 作業ツリーに存在するものとしてユーザーに表示されるため)。 この違反は、 スパース・チェックアウトに関連するさまざまな動作がこのユースケースにあまり適していないことを意味する可能性があり、 これを処理するにはドキュメントとコードの両方に調整が必要になる可能性があります。 ただし、 このユースケースは、 いくつかの例外を除いてすべてが高密度(まばらでない)チェックアウトのように動作するという点で、 おそらくサポートする最も単純なモデルでもあります(たとえば、VFS が必要に応じて残りを遅延的に書き込むことがわかっているため、 ブランチ・チェックアウトと switch は書き込む内容が少なくなります)。

一般に公開されて試せる VFS 関連のコードがないため、 そのようなユースケースをテストできる人の数は限られています。

振る舞いC のユースケースに注目する主な理由は、 振る舞いA と 振る舞いB をより適切にサポートするために修正を行う際に、 このユースケースの人々が元の非スパース処理を受けられるように調整する必要がある追加の場所が存在する可能性があるためです。 例については、 ecc7c8841d ("repo_read_index: add config to expect files outside sparse patterns", 2022-02-25)を参照してください。 振る舞いC に注目する 2 番目の理由は、 振る舞いC を利用する人々が自分が 振る舞いB 陣営の一員であると思い込み、 実際の 振る舞いB の人々のために問題を解決するパッチを提案しないようにするためです。

思いっきり単純化した概念モデル

上記の振る舞いの違いを思いっきり単純化すると以下のようになります:

振る舞い A

ワークツリーと履歴の操作をスパース仕様に制限する

振る舞い B

ワークツリーの操作をスパース仕様に制限します。 履歴操作がすべてのファイルにわたって機能するようにします

振る舞い C

ワークツリーまたは履歴の操作をスパース仕様に制限しないでください。ただし、ブランチのチェックアウトやスイッチは例外で、インデックスにマッチするファイルの書き込みを回避し、代わり後で遅延して行えるようにします。

期待される振る舞い

前述したように、 ファイルのサブセットを操作するという単純な考え方にもかかわらず、 このような機能を適切に操作するには、 さまざまなサブコマンドにさまざまな動作変更を加える必要があります。 さまざまな例については、[1,2,3,4,5,6,7,8,9,10] を参照してください。 特に [2] では、スパース・チェックアウト・コンテキストで個別に正しく動作する他のコマンドを単に組み合わせただけでは、 上位レベルのコマンドが正しく動作することを意味しないことが分かります。 場合によってはさらに調整が必要になる場合があります。 したがって、これらの違いを理解することは有益です。

  • 高レベルのユースケースに関係なく、コマンドは同一の振る舞いをします。

    • スパース仕様内のファイルのみを参照するコマンド

      • diff (--cached なし、または REVISION 引数なし)

      • grep (--cached なし、または REVISION 引数なし)

      • diff-files

    • スパース・パターンにマッチするファイルを作業ツリーに復元し、それらのパターンにマッチしない未変更のファイルを削除するコマンド:

      • switch

      • checkout (switch風の部分)

      • read-tree

      • reset --hard

    • 競合するファイルを作業ツリーに書き込むコマンドは、スパース・パターンにマッチしないファイルを作業ツリーへ書き込むのを省略します:

      • merge

      • rebase

      • cherry-pick

      • revert

      • amapply --cached はおそらくこのセクションにあるはずですが、 バグがあります(下記「Known bugs」セクションを参照)

      これらのコマンドの動作は、使用されているマージ戦略によって多少異なります:

      • ort は上記のとおり振る舞います

      • recursive はファイルを不必要に復活(vivify)させないよう努めますが、 競合無しでファイルを復活(vivify)させることもあります。

      • octopusresolve は、 最初の親を基準にしてマージで変更されたファイルを常に復活(vivify)させますが、 これはむしろ最適ではありません。

      これらのコマンドは、 操作の開始時を基準にして、スパース仕様の外側のインデックスを更新する事に注意することも重要ですが、 しかし、 これらのコマンドは多くの場合、 直前または直後にコミットを行うため、 操作の終了までスパース仕様の範囲外のインデックスは変更されません。 もちろん、 その操作が競合に直面するかコミットを行わない場合、 これらの操作は明らかにスパース仕様の範囲外でインデックスを変更する可能性があります。

      最後に、 これらのコマンドの少なくとも最初の 4 つは、 (前のセクションのコマンドと同様に、)スパース仕様とスパース・パターンの間の差異を削除しようとしていることに注意することが重要です。

    • コミットは完全ツリー(full-tree)である必要があるため、 常にスパース性を無視するコマンド群:

      • archive

      • bundle

      • commit

      • format-patch

      • fast-export

      • fast-import

      • commit-tree

    • 変更されたファイルを作業ツリーに書き込むコマンド(競合するかどうか、 そしてそれらのパスがスパース・パターンとマッチするかどうか):

      • stash

      • apply (--index 無し、 または --cached 無し)

  • 振る舞いA と 振る舞いB で若干異なる可能性があるコマンド:

    このカテゴリのコマンドは、 この 2 つの振る舞いにおいてはほとんど同一の動作ですが、 おしゃべり度(verbosity)や警告やエラー・メッセージの種類が異なる場合があります。

    • 追跡中のファイルを変更するコマンド:

      • add

      • rm

      • mv

      • update-index

      ファイルが「追跡中」(tracked)カテゴリと「非追跡」(untracked)カテゴリの間を移動できるということは、 一部のコマンドでは非追跡ファイルを異なる方法で処理する必要があることを意味します。 ただし、 非追跡ファイルを別の方法で処理する必要がある場合、 追加のコマンドも変更する必要がある場合があります:

      • status

      • clean

      特に、 status は、 スパース仕様の範囲外にある非追跡ファイルを間違っている状態として報告する必要がある場合があります (特に、 ユーザーがファイルを git add しようとして、git add に強制的にエラーが表示されるのを避けるため)。

      clean がどのように変更するか(あるいは変更するかどうか)は正確にはわかりませんが、 非追跡ファイルにも影響を与えるもう 1 つのコマンドです。

      update-index は少し特殊かもしれません。 --[no-]skip-worktree フラグは、 その性質上、スパース仕様を無視する必要がある場合があります。 また、現在の --[no-]ignore-skip-worktree-entries のデフォルトは完全にインチキです。

    • インデックスと作業ツリーの両方のパスを手動で調整するためのコマンド

      • restore

      • checkout の restore風な部分

      これらのコマンドは、 デフォルトではスパース仕様でのみ動作し、 すべてのファイルで動作するには特別なフラグが必要であるという点で add/rm/mv に似ています。

      また、 これらのコマンドには現在多くの問題が出ていることに注意してください(下記「Known bugs」セクション参照)

  • 振る舞いA と 振る舞いB で大きく異なるコマンド:

    • 履歴を問い合わせ(query)するコマンド

      • diff (--cached または REVISION 引数を伴う)

      • grep (--cached または REVISION 引数を伴う)

      • show (コミット引数が与えられた場合)

      • blame (1 つ以上の -C フラグが渡された場合にのみこの問題が発生します)

        • と、 annotate

      • log

      • whatchanged

      • ls-files

      • diff-index

      • diff-tree

      • ls-tree

      注意: log と whatchanged の場合、 リビジョン・ウォーキングのロジックは影響を受けませんが、 パッチの表示はコマンドのスコープをスパース・チェックアウトに設定することによって影響を受けます。 (リビジョン・ウォーキングが影響を受けないという事実が、rev-list, shortlog, show-branch, bisect がこのリストに含まれていない理由です。)

      ls-files は、 たとえば git ls-files -t が、 何がスパースで何がスパースでないのかを確認するためによく使用されるという点で少し特殊かもしれません。 あるいは、 -t は常に完全なツリーに対して機能するようにすべきかもしれませんね?

  • 私にはどこに分類すべきか分からないコマンド

    • range-diff

      これは logformat-patch のようなものでしょうか?

    • cherry

      range-diff 参照

  • sparse-checkouts の影響を受けないコマンド

    • shortlog

    • show-branch

    • rev-list

    • bisect

    • branch

    • describe

    • fetch

    • gc

    • init

    • maintenance

    • notes

    • pull (merge と rebase には必要な変更があります)

    • push

    • submodule

    • tag

    • config

    • filter-branch (sparse-checkout 設定なしで、 個別のチェックアウトで動作します)

    • pack-refs

    • prune

    • remote

    • repack

    • replace

    • bugreport

    • count-objects

    • fsck

    • gitweb

    • help

    • instaweb

    • merge-tree (ワークツリーやインデックスには影響せず、マージは常に完全なツリー(full-tree)を計算します)

    • rerere

    • verify-commit

    • verify-tag

    • commit-graph

    • hash-object

    • index-pack

    • mktag

    • mktree

    • multi-pack-index

    • pack-objects

    • prune-packed

    • symbolic-ref

    • unpack-objects

    • update-ref

    • write-tree (インデックスを操作します。 スパース・ディレクトリ・エントリを使用するように最適化される可能性があります)

    • for-each-ref

    • get-tar-commit-id

    • ls-remote

    • merge-base (マージは完全ツリー(full tree)を計算するため、 マージ・ベースも同様である必要があります)

    • name-rev

    • pack-redundant

    • rev-parse

    • show-index

    • show-ref

    • unpack-file

    • var

    • verify-pack

    • git help --all の「Interacting with Others」セクションにあるすべて>

    • git help --all の「Low-level Commands / Syncing Repositories」セクションにあるすべて>

    • git help --all の「Low-level Commands / Internal Helpers」セクションにあるすべて>

    • git help --all の「External commands」セクションにあるすべて>

  • 影響を受ける可能性のあるコマンド群だけど、 こんなの気にする人いる?

    • merge-file

    • merge-index

    • gitk?

振る舞いクラス

上記により、 振る舞いクラスがいくつあります:

"restrict"(制限)

このクラスのコマンドは、 スパース仕様内の作業ツリー内のファイルの読み取りまたは書き込みのみを行います。

(switch や、 reset --hard など、)新しいコミットに移動するとき、 これらのコマンドは操作の開始時点でスパース仕様の範囲外にあるインデックス・ファイルを更新する可能性がありますが、 操作の終了までにそれらのインデックス・ファイルは再び HEAD と一致するため、 これらのファイルは再びスパース仕様の範囲外になります。

パスが明示的に指定されている場合、 これらのパスはスパース仕様と交錯し、 そのようなパス上でのみ動作します。 (例: git restore [--staged] -- '*.png'git reset -p -- '*.md' )

これらのコマンドの一部では、 その操作の最後で、 スパース仕様とスパース・パターンの間の一時的な差異を除去しようとする場合があります(詳細については「"Sparse specification vs. sparsity patterns」を参照してください。 ただし、 これは基本的に、 スパース・パターンにマッチしない未変更のファイルを削除し、それらのファイルを SKIP_WORKTREE としてマークすることを意味するか、 または、 スパース・パターンにマッチするファイルを復活(vivifying)させ、 それらのファイルを !SKIP_WORKTREE としてマークします)。

"restrict modulo conflicts"(競合外の制限)

このクラスのコマンドは通常、 以下の点を除いて "restrict" クラスと同様に振る舞います:

  1. これらはスパース仕様を無視し、競合するファイル達を作業ツリーに書き込みます(したがって、 そのようなファイル達を含めるようにスパース仕様を一時的に拡張します。)

  2. これらは、 新しいコミットへ移動させるコマンドとグループ化されています。 これは、 新しいコミットへ移動させるのは多くの例外があることがわかっていても、 コマンドはコミットを作成してからそのコミットに移動させることが多いためです。 (たとえば、ユーザーは空になったコミットを rebase したり、 競合する cherry-pick を使用したり、 merge --no-commit を実行したり、 apply --indexam --no-commit のようなものです。) そのため、 これらのコマンドはスパース仕様外のインデックス・ファイルに変更を加えることはできますが、 そのようなファイルには SKIP_WORKTREE のマークが付けられます。

"restrict also specially applied to untracked files"(非追跡ファイルにも特別に適用される制限)

このクラスのコマンドは通常、 "restrict"(制限)クラスと同様に動作しますが、 非追跡ファイルも別の方法で処理する必要がある点が異なります。 多くの場合、 これらのコマンドは「追跡」(tracked)と「非追跡」(untracked)の間で状態が変化するファイルを処理するためです。 多くの場合、 これは、 コマンドが何も行わなかった場合にエラー・メッセージを出力することを意味しますが、 その引数は、 スパース・パターンが除外していなければ追跡状態が変更された可能性があるファイルを参照している可能性があります。

"no restrict"(制限しない)

このクラスのコマンドは、スパース仕様を完全に無視します。

"restrict or no restrict dependent upon behavior A vs. behavior B"(振る舞いA と 振る舞いB に応じて、 制限する または 制限しない)

このクラスのコマンドは、 振る舞いB の人々にとっては "no restrict"(制限なし) のように動作し、 振る舞いA の人々にとっては "restrict"(制限) のように振る舞います。ただし、 "restrict"(制限)のように振る舞うと、 履歴クエリがスパース・チェックアウト仕様によって制限されている、 という、 ある種の警告が表示される場合があります。

サブコマンドごとのデフォルト

注意: 目的の振る舞いを実現するためのコマンドに応じて、異なるデフォルトがあることに注意してください:

  • デフォルトで "restrict"(制限)に設定されているコマンド

    • diff-files

    • diff (--cached なし、または REVISION 引数なし)

    • grep (--cached なし、または REVISION 引数なし)

    • switch

    • checkout (switch風の部分)

    • reset (<commit>)

    • restore

    • checkout (restore風の部分)

    • checkout-index

    • reset (pathspec を伴う)

    この振る舞いは理にかなっています。 つまり、これらは作業ツリーと対話します。

  • デフォルトで "restrict modulo conflicts"(競合外の制限) に設定されているコマンド:

    • merge

    • rebase

    • cherry-pick

    • revert

    • am

    • apply --index (これは am --no-commit のようなものです)

    • read-tree (-m または -u を使用する場合; --no-commit マージのようなもの)

    • reset (<tree-ish> 、read-tree との類似性による)

    これらも作業ツリーと対話しますが、 (a)競合を解決できるように、 または、 (b)コミットなしのマージ操作のようなもの、 であるため、 微妙に異なる振る舞いが必要です。

    ( am および apply に関する下記「Known bugs」セクションも参照してください)

  • デフォルトで "no restrict" (制限なし)に設定されているコマンド:

    • archive

    • bundle

    • commit

    • format-patch

    • fast-export

    • fast-import

    • commit-tree

    • stash

    • apply (--index 無し)

    これらのデフォルトは完全に異なるため、 更に詳細な説明が必要でしょう:

    最初のグループのコマンド(format-patch、fast-export、bundle、archive など)の場合、 これらは履歴を交信するためのコマンドであり、 リポジトリのサブセットに制限すると壊れます。 そのため、 これらはフルパスで動作するため、 オーバーライドするための --restrict オプションはありません。 これらのコマンドの一部は、 エクスポートされる内容を手動で制限するためのパスを受け付ける場合がありますが、 それは非常に明示的である必要があります。

    stash の場合、 ユーザーの変更が失われないように、 ファイルを復活(vivify)させる必要があります。

    --index なしで適用する場合、 そのコマンドはインデックスなしで作業ツリー(または、 --cached を渡した場合は作業ツリーなしのインデックス)を更新する必要があります。 これらの更新をスパース仕様で制限すると、 ユーザーからの変更が失われます。

  • デフォルトで「非追跡ファイルにも特別に適用される制限」が設定されているコマンド:

    • add

    • rm

    • mv

    • update-index

    • status

    • clean (?)

    上記コマンドの上から 3 つの実装は最初、 「制限なし」(no restrict)でしたが、 以下のとおり、ユーザビリティにいくつかの重大な問題がありました:

    • git add <somefile> が尊重され、 かつ、 スパース仕様外にある場合、 その後のコマンド実行でファイルがランダムに消える可能性があります(さまざまなコマンドがスパース仕様外の未変更のファイルを自動的にクリーンアップするため)。

    • git rm '*.jpg' がユーザーの関心の範囲外のファイルを削除すると、 ユーザーは非常に不快な思いをする可能性があります。

    • git mv は、 コーンの範囲に出入りするときに同様の驚きをもたらすため、 デフォルトで制限(restrict)するのが最善です

    そこで、 私達は addrm をデフォルトの "restrict" に切り替えました。 これにより、 ユーザビリティの問題が大幅に軽減され、 頻度も減りましたが、 以下のようなコマンドが原因で依然として苦情が寄せられていました:

    git add <file-outside-sparse-specification>
    git rm <file-outside-sparse-specification>

    これは何も声を上げず何も行わない事でしょう。 ユーザビリティを適切に保つために、 そのような場合には代わりにエラーを出力する必要があります。

    update-index はマッチするように更新する必要があり、 非追跡パスを特別に処理するには、 status と、 おそらく clean も更新する必要があります。

    ここで、 エラーまたは追加の警告の詳細さ(verboseness)の点で、 振る舞いA と 振る舞いB の間に違いがある可能性があります。

  • 「振る舞いA や 振る舞いB に応じて制限するか、あるいは制限しない」に該当するコマンド:

    • diff (--cached または REVISION 引数を伴う)

    • grep (--cached または REVISION 引数を伴う)

    • show (コミット引数が与えられた場合)

    • blame (1 つ以上の -C フラグが渡された場合にのみ、この問題が発生します)

      • と、 annotate

    • log

      • と、その派生: shortlog, gitk, show-branch, whatchanged, rev-list

    • ls-files

    • diff-index

    • diff-tree

    • ls-tree

    現時点では、 デフォルトで「制限なし」(no restrict)が必要なため、 これらに対してデフォルトの 振る舞いB を設定します。

    注意: これらのコマンドのうち、 diff と grep もまた、 デフォルトの「制限」(restrict)で別のリストに表示されますが、これは作業ツリーの検索に限定されている場合に限られることに注意してください。 作業ツリーと履歴の区別は 振る舞いB の操作の基本であるため、 これは期待どうりのものです。 ただし、 --cached を指定した diff と grep の場合、「制限」(restrict)振る舞いを行う場合、 スパース仕様とスパース・パターンの違いを処理することが重要であることに注意してください。

    これらの長期的なデフォルトとしては、「制限」(restrict)の方が合理的かもしれません[12]。 また、これらのコマンドの「制限」(restrict)をサポートするには、 実装にかなりの作業が必要となる可能性があり、 複数のリリースにまたがって実装作業が行われる可能性があります。 その振る舞いが、 それをサポートするコマンドのデフォルトである場合、 振る舞いB のユーザーは、 必要な動作を得るために、 git のバージョンに応じて、 コマンドにフラグを徐々に追加する方法を学ぶ必要があります。 この段階的な対応は苦痛を伴うため、 私達は、 少なくとも完全に実装されるまでは回避するべきです。

スパース仕様 V.S. スパース・パターン

正常に動作する状況では、 スパース仕様は $GIT_DIR/info/sparse-checkout ファイルによって直接指定されます。 ただし、 以下のいくつかの理由で過渡的にそこから逸脱する可能性があります:

  • 競合を解決する必要がある時(マージすると競合したファイルが復活(vivify)します)

  • ファイルを暗黙的に復活(vivify)させる Git コマンドの実行中(例: git stash apply)

  • ファイルを明示的に復活(vivify)させる Git コマンドの実行中(例: git checkout --ignore-skip-worktree-bits FILENAME)

  • これらのファイルに書き込む他のコマンド(おそらくユーザーが他の場所からコピーしたもの)

上記の最後の項目については、 作業ツリーに存在するファイルの SKIP_WORKTREE ビットが自動的にクリアされることに注意してください。 これはコミット 82386b4496("Merge branch en/present-despite-skipped", 2022-03-09)以降で当てはまります。

ただし、 このような状況は以下の理由により、過渡的なものです:

  • このような過渡的な差異は、 unpack_trees() を呼び出すコマンド(checkout、 merge、 reset 等)の副作用として自動的に取り除かれる可能性があります。

  • ユーザーは git sparse-checkout reapply を実行して、 このような過渡的な差異の修正を要求することもできます。 さまざまな場所でこのコマンドの実行が推奨されています。

  • これらの違いを暗黙的に修正する追加のコマンドも大歓迎です。 私達は、 将来、さらにこのようなコマンドを追加する可能性があります。

ステージされてない変更または競合のあるファイルの削除は避けますが、 そうでない場合は、 これらの過渡的な差異を積極的に修正しようとします。 ユーザーがこれらの差異を維持したい場合は、 git sparse-checkoutset または add サブコマンドを実行して、 意図するスパース仕様を反映するべきです。

ただし、 「関連するファイルのサブセット」に限定された履歴に対してクエリを実行する必要がある場合、 そのような過渡的に拡張されたスパース仕様は無視されます。 これには以下のように一組の理由があります:

  • git grepexpression REVISION のようなことを実行するときに必要な振る舞いは、 ユーザーが git checkout REVISION && git grepexpression (「REVISION:」プレフィックスを適用外)に期待するものとほぼ同じであり、 これには以下のような影響があります:

  • REVISION には現在のインデックスにないパスが含まれている可能性があるため、 それらのパスの SKIP_WORKTREE 設定を調べる事ができるパスはありません。

  • checkout はスパース仕様の過渡的な差異を削除しようとするコマンドの 1 つであるため、 SKIP_WORKTREE を調べようとするのではなく、 修正されたスパース仕様 (つまり $GIT_DIR/info/sparse-checkout) を使用するのが合理的です。

したがって、 過渡的に拡張された(または制限された)スパース仕様は作業ツリーには適用されますが、 スパース・パターンを使用する履歴クエリには常に適用されません(これに関する初期の議論については、[16] を参照してください。)

作業ツリーに存在する追加フ​​ァイルに基づいて過渡的に拡張された作業ツリーのスパース仕様と同様に、 インデックス内で変更される追加ファイルも考慮する必要があります。 特に、 ユーザーが、(HEAD に関連した、)スパース・パターンにマッチしないファイルへの変更をステージングしており、 そのファイルが作業ツリーには存在しない場合でも、 スパース仕様のファイル部分を考慮する必要があります。 具体的には、 インデックスに関連する問い合わせを実行します(例: git diff --cached [REVISION]git diff-index [REVISION]git restore --staged --source=REVISION -- PATHS など)。 過渡的に拡張されたスパースは、 振る舞いB ではインデックス操作を履歴と一緒にまとめ、 フルのツリー(full-tree)で操作する傾向があるため、 通常、 インデックスの指定は 振る舞いA でのみ問題になります。

実装時の疑問

  • オプション --scope={sparse,all} は、 私達以外の人々にとっても良い書き方に思えますか? もっと良い選択肢があるでしょうか?

    • 使用中の名前、またはパッチに登場する名前、または以前に提案された名前:

      • --sparse/--dense

      • --ignore-skip-worktree-bits

      • --ignore-skip-worktree-entries

      • --ignore-sparsity

      • --[no-]restrict-to-sparse-paths

      • --full-tree/--sparse-tree

      • --[no-]restrict

      • --scope={sparse,all}

      • --focus/--unfocus

      • --limit/--unlimited

    • `--scope={sparse,all}` という表記に少し傾倒している根拠:

      • 私達は多くのコマンドで機能する名前が必要なので、 それには競合しない名前が必要です

      • 考えられるユースケースが 3 つ以上あることはわかっているので、 2値のように見えるフラグは避けるのが最善です。

      • --scope={sparse,all} はそれほど長くはなく、 比較的説明的に思えます。

      • add/rm/mv で使用される --sparse は、 grep や log などとはまったく逆です。 これらのコマンドの --sparse の意味を変更すると、 後方性互換性は修正されますが、 既存のスクリプトが壊れる可能性があります。 新しい名前のペアを使用すると、 これらのコマンドの --sparse を非推奨のエイリアスとして扱うことができます。

      • リビジョン機構を使用するコマンドには他に --sparse/--dense のペアがあるため、 その名前を使用すると混乱が生じる可能性があります

      • また、 pack-objects と show-branch の両方に --sparse があります。 これとは競合はしませんが、 --sparse がオーバーロードされていることを示唆しています。

      • --ignore-skip-worktree-bits という名前は二重否定であり、 非常に長文で、 多くのユーザーには馴染みのない実装の詳細を指しています。 おそらくそれをさらに否定する必要があります。 とんでもなく長くなります。 (ただし、 --ignore-skip-worktree-bits--no-restrict の非推奨のエイリアスにすることは可能です。)

    • 設定オプション (sparse.scope?) が追加される場合、 値と説明はどうあるべきか? 「sparse」(振る舞いA)、「worktree-sparse-history-dense」(振る舞いB)、「dense」(振る舞いC)? 振る舞いA と 振る舞いB の場合でも、 一部のコマンドはフル・ツリー(full-tree)であり、 かつ、 他のコマンドはまばらに(sparsely)操作するようにするため、 混乱が生じる危険性があります。 そのため、文言をよりユースケースに結び付けて、 何らかの方法でそれを説明する必要があるかもしれません。 また、現時点で着目している主な違いは、 履歴問い合わせコマンド(log/diff/grep)だけです。 以前の構成提案はこちら: [13]

    • --no-expand は ls-files の --sparse オプションの別名として適しているでしょうか? (--sparse は、 --scope=sparse にも --scope=all にもマップされません。 なぜなら、 非コーン・モードでは何も行わず、 コーン・モードでは技術的にはスパース仕様の範囲外であるスパース・ディレクトリ・エントリが表示されるためです。)

    • 振る舞いAにおいて:

      • ls-files の --no-expand はデフォルトの --scope=all をオーバーライドしましょうか? それとも追加のフラグが必要でしょうか?

      • ls-files の -t オプションは --scope=all を意味を含みますか?

      • update-index の --[no-]skip-worktree オプションは --scope=all の意味を含みますか?

    • sparse-checkout: 振る舞いA が完全に実装されたら、 人々がそれをデフォルトに切り替えるのを容易にするための暫定措置を講じるべきでしょうか? つまり、 まだスパース・チェックアウトを行っていない場合は、 sparse-checkout init/set によって --set-scope=(sparse|worktree-sparse-history-dense|dense) フラグを取得する必要があります(これにより、 指定の設定に従って sparse.scope を設定します)、 フラグが指定されていない場合はエラーを投げますか? このエラーは、 デフォルトが将来変更される可能性があることをユーザーに警告し、 最終的なデフォルトの切り替えがシームレスになるように、 必要なものを指定することに慣れてもらうのに最適です。

実装の 目標/計画

  • この文書全般について賛同を得ること。

  • 「実装時の疑問」セクション(上記)に対する答えを見つけ出すこと

  • 「既知のバグ」セクション(下記)のバグを修正すること

  • 部分クローン(partial clone)のスパース仕様内の BLOB を埋め戻し(backfilling)するための何らかの方法を提供すること

[上記の上 2 つがまだ解決されていないため、 以下はまだ思い付きレベルの話ではあります]

  • update-index: デフォルトを --no-ignore-skip-worktree-entries に切り替えて、 「ああん、バグがある? それじゃ、 ユーザーにこのバグをトリガーしないようにリクエストするフラグを追加しよう」という、 この愚かなフラグを削除

  • フラグと構成(config)について

    • add/rm/mv の --sparse--scope=all の非推奨のエイリアスにします

    • checkout-index/checkout/restore の --ignore-skip-worktree-bits--scope=all の非推奨のエイリアスにします

    • 構成オプション(sparse.scope?)を新規作成し、「Cliff Notes」の概要に関連付けます

    • 履歴問い合わせする各コマンドに --scope=sparse (および --scope=all) フラグを追加します。 重要: diff 機構の変更によって format-patch、 fast-export などが台無しにならないようにしてください。

既知のバグ

このリストは以前はもっと長かったのですが([1,2,3,4,5,6,7,8,9] を参照)、 私達の鋭意ある取り組みによりだいぶ短くなりました。

  1. 振る舞いA は Git では十分にサポートされていません。 (以前はどちらの振る舞いも十分にサポートされていなかったのですが、 2 つのうち振る舞いBの実装がより容易でした。)

  2. am と apply について:

    apply は、 --index または --cached を指定しないと、 作業コピーに存在するファイルに依存し、 また、 それらに無条件に書き込みます。 そのため、 最初にファイルの存在を確認し、 SKIP_WORKTREE であることが判明した場合は、 ビットをクリアしてパスを復活(vivify)させてから作業を実行する必要があります。 現時点では、 エラーが投げられるだけです。

    --cached または --index を指定して apply を実行すると、 SKIP_WORKTREE ビットは保持されません。 ファイルに競合がある場合はこれで問題ありませんが、 そうでない場合は、 SKIP_WORKTREE ビットを --cached と、 おそらく --index に対しても保存するべきです。

    am は、 競合がない場合、 ファイルを復活(vivify)させ、 SKIP_WORKTREE ビットの保存に失敗します。 競合があり、 -3 が指定されていない場合、 ファイルは復活(vivify)後、 パッチが適用されないというメッセージが表示されます。 競合があり、 -3 が指定されている場合、 ファイルを復活(vivify)させ、 それらの復活(vivify)したファイルがマージによって上書きされると警告します。

  3. reset --hard について:

    reset --hard は、 以下のような混乱を招くエラー・メッセージを表示します(それは正しく動作しますが、ユーザーに動作しなかったと誤解させます):

    $ touch addme
    $ git add addme
    $ git ls-files -t
    H addme
    H tracked
    S tracked-but-maybe-skipped
    $ git reset --hard                           # usually works great
    error: Path 'addme' not uptodate; will not remove from working tree.
    HEAD is now at bdbbb6f third
    $ git ls-files -t
    H tracked
    S tracked-but-maybe-skipped
    $ ls -1
    tracked

    git replace --hard は、 エラー・メッセージとは裏腹に、 インデックスと作業ツリーから addme を削除しましたが、 それは reset --hard の振る舞いに沿って行われたものです。

  4. read-tree

    read-tree は、 インデックスに読み込む 「どのエントリ」にも SKIP_WORKTREE ビットを適用しないため、 すべてのファイルが突然「削除」されたように見えます。

  5. Checkout、 restore について:

    以下のコマンドは、 パスとリビジョンの引数を適切に処理しません:

    $ ls
    tracked
    $ git ls-files -t
    H tracked
    S tracked-but-maybe-skipped
    $ git status --porcelain
    $ git checkout -- '*skipped'
    error: pathspec '*skipped' did not match any file(s) known to git
    $ git ls-files -- '*skipped'
    tracked-but-maybe-skipped
    $ git checkout HEAD -- '*skipped'
    error: pathspec '*skipped' did not match any file(s) known to git
    $ git ls-tree HEAD | grep skipped
    100644 blob 276f5a64354b791b13840f02047738c77ad0584f    tracked-but-maybe-skipped
    $ git status --porcelain
    $ git checkout HEAD~1 -- '*skipped'
    $ git ls-files -t
    H tracked
    H tracked-but-maybe-skipped
    $ git status --porcelain
    M  tracked-but-maybe-skipped
    $ git checkout HEAD -- '*skipped'
    $ git status --porcelain
    $

    注意: リビジョンなしの checkout (または restore --staged)では、 ls-files でそのようなファイルが確かに存在することが示されている場合でも、 インデックスから復元(restore)するファイルが見つからないことに注意してください。

    同様の問題が HEAD (restore の場合は --source=HEAD)でも発生しますが、 HEAD~1 が指定されると突然機能するようになります。 その後、 以前は機能しなかったとしても、 HEAD を指定しても機能するようになります。

    ディレクトリにも問題があります:

    $ git sparse-checkout set nomatches
    $ git status
    On branch main
    You are in a sparse checkout with 0% of tracked files present.
    
    nothing to commit, working tree clean
    $ git checkout .
    error: pathspec '.' did not match any file(s) known to git
    $ git checkout HEAD~1 .
    Updated 1 path from 58916d9
    $ git ls-files -t
    S tracked
    H tracked-but-maybe-skipped
  6. checkout と restore --staged の続き:

    以下のコマンドは、 操作のスコープをスパース仕様に対して正しく設定しておらず、 重要な SKIP_WORKTREE ビットを設定しないため、 事態はさらに悪化します:

    $ git restore --source OLDREV --staged outside-sparse-cone/
    $ git status --porcelain
    MD outside-sparse-cone/file1
    MD outside-sparse-cone/file2
    MD outside-sparse-cone/file3

    --scope=all モードを git list に追加して、 スパース仕様の範囲外で動作できるようにすることもできますが、 その場合は SKIP_WORKTREE ビットを適切に設定することが重要になります。

  7. パフォーマンスの問題ついて。 以下参照:

    FKr8JhyuuTMK1RDw@mail.gmail.com/">https://lore.kernel.org/git/CABPp-BEkJQoKZsQGCYioyga_uoDQ6iBeW+FKr8JhyuuTMK1RDw@mail.gmail.com/

Reference Emails

sparse-checkout で発生したさまざまなバグを詳しく説明したメール達:

  1. (振る舞いA と 振る舞いB の説明のオリジナル)

  2. (スパース・チェックアウトの stash アプ​​リケーションを修正。 振る舞いの違いによるバグ)

  3. (Present-despite-skipped entries)

  4. (Clone --no-checkout interaction)

  5. (update_sparsity() の必要性と read-tree -mu HEAD の回避)

  6. (SKIP_WORKTREE は推奨事項であり、 必須ではありません)

  7. (worktree add は現在のワークツリーからスパース設定をコピーする必要があります)

  8. (add、rm、mv では否定的なサプライズを避けてください)

  9. (円錐外から円錐内へ移動)

  10. (スパース仕様外のオブジェクトを不必要にダウンロードする)

    https://lore.kernel.org/git/CAOLTT8QfwOi9yx_qZZgyGa8iL8kHWutEED7ok_jxwTcYT_hf9Q@mail.gmail.com/ . (高レベルのユースケースに関する Stolee のコメント)

  11. 最終的にデフォルトの 振る舞いA に切り替えることについてコメントしている人々:

  12. 以前の構成名の提案と説明

  13. わき道の議論: デフォルトのスパース仕様メカニズムとして円錐モードに切り替えます:

  14. grep の動作に関する長いメール。 検索すべき対象の網羅について:

  15. スパース・パターンと SKIP_WORKTREE および、 履歴操作を説明する電子メール。 "We do not check" で始まる括弧(parenthetical)内のコメントを検索してください。

    GCg@mail.gmail.com/">https://lore.kernel.org/git/CABPp-BFsCPPNOZ92JQRJeGyNd0e-TCW-LcLyr0i_+VSQJP+GCg@mail.gmail.com/

  16. https://lore.kernel.org/git/20220207190320.2960362-1-jonathantanmy@google.com/