はじめに
これは「Git リポジトリのあるリビジョンのファイル全部を取得する方法があるんだから、ファイル一覧だけを効率よく取得する方法もどこかにあるのでは?」と思った人間が「結局通常のケースでは ssh して bare リポジトリを直接見るか、ローカルに shallow clone するのが最適解じゃねーか!」と気づくまでの記録です。
結論 (TL;DR)
- ファイル一覧を得たいユーザーがそもそもリモートホストに ssh ログインできる場合
ssh <user>@<host> git --git-dir <repo> ls-tree -r <revision>
- ファイル一覧を得たいユーザーがリモートホストに ssh ログインできない場合(リモートが GitHub の場合など)
-
git clone --bare --depth 1 --branch <revision> <remote-repo> && git --git-dir <repo> ls-tree -r <revision>
(shallow clone) - もしくは
git archive --remote <remote-repo> <revision> | tar -tf -
-
-
ファイル一覧を得たいユーザーは ssh ログインできないが、リモート側・クライアント側双方で Git v2.22 以上を使っていて、リモートリポジトリに対して
git config
する権限のあるアカウントがある場合- リモート側で
git config uploadpack.allowfilter true && git config uploadpack.allowanysha1inwant true
する-
uploadpack.allowfilter
: v2.22 より前のバージョンには--filter=sparse:path=<path>
の脆弱性があるため注意 -
uploadpack.allowanysha1inwant
: リモートリポジトリの任意の object をクライアント側から取得できるようにするつよいオプションなので扱いには注意
-
- クライアント側で
git clone --bare --depth 1 --branch <revision> --filter blob:none <remote-repo> && git --git-dir <repo> ls-tree -r <revision>
- リモート側で
なお、「ssh ログインできる」とは「ssh 先のリモート bare リポジトリに対してある程度自由に操作を行える(シェルが nologin
だったりミドルウェアによる制限がかかっていたりしない)」ことを指します。
下準備
今回はLAN内のリモートホスト example.local
に git/git - GitHub を git clone --bare
して置いてあります。
$ git clone --bare https://github.com/git/git # (in git@example.local)
この bare リポジトリ全体のファイルサイズは 131 MB です。
$ du -sh git.git
131M git.git
このリポジトリの v2.23.0
タグ時点でのファイル一覧を取得したい、という状況を考えます。
そもそもリモートホストに ssh ログインできる場合
bare リポジトリの特定リビジョンのファイル一覧を見るには git ls-tree
を使います。-r
は tree を再帰的に処理するオプションです。また、--git-dir
オプション(または GIT_DIR
環境変数)を指定することで cd
することなく操作対象の Git リポジトリを選択できます。
$ ssh <user>@<host> git --git-dir <repo> ls-tree -r <revision>
実行例は以下の通り。
$ time sh -c 'ssh git@example.local git --git-dir ./git.git ls-tree -r v2.23.0 | tail'
100644 blob a4285ac0eb2840fb3d128b81ab4e19ca3fa673ae xdiff/xinclude.h
100644 blob 2809a28ca960147c285bc5a224ed377a0964663a xdiff/xmacros.h
100644 blob 1659edb45393a6b57ca70654e67784e5d0025c65 xdiff/xmerge.c
100644 blob 3c5601b602a24cb6cd6e063466369b1a90a14f9f xdiff/xpatience.c
100644 blob abeb8fb84e6d73086d612b831963a227e35743b8 xdiff/xprepare.c
100644 blob 947d9fc1bb8cf95719284de6563227485907988f xdiff/xprepare.h
100644 blob 8442bd436efeab81afc25db9d89da082638fcca4 xdiff/xtypes.h
100644 blob cfa6e2220ffd0461ce3293911c86366f6ccb1b05 xdiff/xutils.c
100644 blob fba7bae03c7855ca90aff3f238321581a91a6676 xdiff/xutils.h
100644 blob d594cba3fc9d82d94b9277e886f2bee265e552f6 zlib.c
real 0m0.237s
user 0m0.024s
sys 0m0.003s
処理時間は 0.2 秒でした。流石にこれが一番高速ですね……。
リモートホストに ssh ログインできない場合
shallow clone する
自由に bare リポジトリを触る権限がない人は素直に --depth 1
を付けて shallow clone しましょう。--branch
で対象リビジョンを指定することができます。
$ git clone --bare --depth 1 --branch <revision> <remote-repo> && git --git-dir <repo> ls-tree -r <revision>
実行例は以下の通り。
$ time sh -c 'git clone --bare --depth 1 --branch v2.23.0 git@example.local:git.git && git --git-dir git.git ls-tree -r v2.23.0 | tail'
Cloning into bare repository 'git.git'...
remote: Enumerating objects: 3719, done.
remote: Counting objects: 100% (3719/3719), done.
remote: Compressing objects: 100% (3330/3330), done.
remote: Total 3719 (delta 282), reused 1889 (delta 235)
Receiving objects: 100% (3719/3719), 8.54 MiB | 5.74 MiB/s, done.
Resolving deltas: 100% (282/282), done.
100644 blob a4285ac0eb2840fb3d128b81ab4e19ca3fa673ae xdiff/xinclude.h
100644 blob 2809a28ca960147c285bc5a224ed377a0964663a xdiff/xmacros.h
100644 blob 1659edb45393a6b57ca70654e67784e5d0025c65 xdiff/xmerge.c
100644 blob 3c5601b602a24cb6cd6e063466369b1a90a14f9f xdiff/xpatience.c
100644 blob abeb8fb84e6d73086d612b831963a227e35743b8 xdiff/xprepare.c
100644 blob 947d9fc1bb8cf95719284de6563227485907988f xdiff/xprepare.h
100644 blob 8442bd436efeab81afc25db9d89da082638fcca4 xdiff/xtypes.h
100644 blob cfa6e2220ffd0461ce3293911c86366f6ccb1b05 xdiff/xutils.c
100644 blob fba7bae03c7855ca90aff3f238321581a91a6676 xdiff/xutils.h
100644 blob d594cba3fc9d82d94b9277e886f2bee265e552f6 zlib.c
real 0m2.708s
user 0m0.593s
sys 0m0.161s
処理時間は 2.7 秒、展開後のリポジトリのサイズは 8.8 MB でした。
$ du -sh git.git
8.8M git.git
ちなみに --bare
でなく --no-checkout
オプションを使ってもだいたい同じような速度になります。こちらでは bare でない普通のリポジトリとして clone されるので時と場合に応じて使い分けましょう。
git archive
を使う
git archive
の --remote
オプションと tar
コマンドを組み合わせることでもファイル一覧を得られます。
$ time sh -c 'git archive --remote git@example.local:git.git v2.23.0 | tar -tf - | tail'
xdiff/xinclude.h
xdiff/xmacros.h
xdiff/xmerge.c
xdiff/xpatience.c
xdiff/xprepare.c
xdiff/xprepare.h
xdiff/xtypes.h
xdiff/xutils.c
xdiff/xutils.h
zlib.c
real 0m3.538s
user 0m0.307s
sys 0m0.279s
処理時間は 3.5 秒でした。今回は shallow clone した方が少し高速だったようです。
Git v2.22 以上を使っていてリモートリポジトリに対して git config
する権限のあるアカウントがある場合
これは**「自分はリモートリポジトリの git config
権限を持っている」「ssh 権限がない人でも高速にファイル一覧を取得できるようにしたい」**というようなケースを想定しています。また、サーバー側に v2.22 以上の Git が入っている必要があります。実はもう少し古いバージョンでも可能なのですが脆弱性(後述)が存在するため注意が必要です。
あらかじめリモート側で以下の作業をしておきます。
$ cd git.git # (in git@example.local)
$ git config uploadpack.allowfilter true
$ git config uploadpack.allowanysha1inwant true
git.git/config
はこんな感じになります。
[core]
repositoryformatversion = 0
filemode = true
bare = true
[remote "origin"]
url = https://github.com/git/git
[uploadpack]
allowfilter = true
allowanysha1inwant = true
そして、クライアントで shallow clone する際に --filter=blob:none
オプションを付けます。これにより blob (ファイルの中身)を除外して一覧のみをダウンロードすることができます。
$ git clone --bare --depth 1 --branch <revision> --filter blob:none <remote-repo> && git --git-dir <repo> ls-tree -r <revision>
実行例は以下の通り。
$ time sh -c 'git clone --bare --depth 1 --branch v2.23.0 --filter blob:none git@example.com:git.git && git --git-dir git.git ls-tree -r v2.23.0 | tail'
Cloning into bare repository 'git.git'...
remote: Enumerating objects: 194, done.
remote: Counting objects: 100% (194/194), done.
remote: Compressing objects: 100% (152/152), done.
remote: Total 194 (delta 1), reused 132 (delta 1)
Receiving objects: 100% (194/194), 111.81 KiB | 11.18 MiB/s, done.
Resolving deltas: 100% (1/1), done.
100644 blob a4285ac0eb2840fb3d128b81ab4e19ca3fa673ae xdiff/xinclude.h
100644 blob 2809a28ca960147c285bc5a224ed377a0964663a xdiff/xmacros.h
100644 blob 1659edb45393a6b57ca70654e67784e5d0025c65 xdiff/xmerge.c
100644 blob 3c5601b602a24cb6cd6e063466369b1a90a14f9f xdiff/xpatience.c
100644 blob abeb8fb84e6d73086d612b831963a227e35743b8 xdiff/xprepare.c
100644 blob 947d9fc1bb8cf95719284de6563227485907988f xdiff/xprepare.h
100644 blob 8442bd436efeab81afc25db9d89da082638fcca4 xdiff/xtypes.h
100644 blob cfa6e2220ffd0461ce3293911c86366f6ccb1b05 xdiff/xutils.c
100644 blob fba7bae03c7855ca90aff3f238321581a91a6676 xdiff/xutils.h
100644 blob d594cba3fc9d82d94b9277e886f2bee265e552f6 zlib.c
real 0m0.515s
user 0m0.058s
sys 0m0.017s
処理時間は 0.5 秒、展開後のリポジトリのサイズは 196 KB でした! ファイルの中身を取得しない分高速に処理できています。
$ du -sh git.git
196K git.git
なお、blob を取得していないファイルに対して git cat-file -p
しようとするとこのタイミングで fetch
が発生します。
$ git --git-dir git.git cat-file -p v2.23.0:zlib.c | head
remote: Enumerating objects: 1, done.
remote: Counting objects: 100% (1/1), done.
remote: Total 1 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (1/1), 1.62 KiB | 1.62 MiB/s, done.
/*
* zlib wrappers to make sure we don't silently miss errors
* at init time.
*/
# include "cache.h"
static const char *zerr_to_string(int status)
{
switch (status) {
case Z_MEM_ERROR:
--filter
について
--filter
に blob:none
を指定すると「blob を一切ダウンロードしない」という挙動になりますが、「特定のサイズ以上のファイルを fetch しない (blob:limit=<n>[kmg]
)」「特定の path 下にあるファイルのみ fetch する (sparse:oid=<blob-ish>
)」といった使い方も可能です。これについては GitLab のドキュメントの以下のページが詳しいです。
uploadpack.allowfilter
, uploadpack.allowanysha1inwant
とは
uploadpack.allowfilter
, uploadpack.allowanysha1inwant
はいずれも git upload-pack
(簡単に言うと git fetch
における通信を司るやつ)の挙動を変更するオプションで、uploadpack.allowfilter
はその名の通り --filter
を利用することを許可するオプション、uploadpack.allowanysha1inwant
は任意の object (tree, blob, commit, tag) を want (fetch) できるようにするオプションです。
- Git - git-config Documentation #uploadpack.allowFilter
- Git - git-config Documentation #uploadpack.allowAnySHA1InWant
「これなんでオプトイン(デフォルトで false
)なの?」という疑問が生じますが、おそらくセキュリティ上の都合でしょう(gitnamespaces や #uploadpack.allowReachableSHA1InWant の説明を参照)。「任意の object を fetch できる」というのはもちろん強力なオプションであり、object へのフルアクセスを誰からでも許可しているリポジトリのみこれらのオプションを有効にしましょう。まあそもそもセキュリティ上ヤバいファイルはリポジトリごと分離しておけと上記の公式の説明にも書いてあるのですが……。
また、Git v2.22 以前のバージョンでは uploadpack.allowfilter
を有効にすることで --filter=sparse:path=<path>
というフィルタを使えますが、この処理にはファイルシステムの任意のパスにアクセスできてしまうという問題があるため注意が必要です。
- list-objects-filter: disable 'sparse:path' filters · git/git@e693237
- Arbitrary read access via
git clone --filter=sparse:path=<path>
(#1578) · Issues · GitLab.org / gitaly · GitLab
おわりに
というわけで、**「結局通常のケースでは ssh して bare リポジトリを直接見るか、ローカルに shallow clone するのが最適解じゃねーか!」**ということでした。なんて遠い廻り道………。
でもこれを調べている段階で Git の pack 周りの処理や知られざる Git の機能に触れることができたのでプラスにはなった、たぶん……。