如何仅克隆 Git 存储库的子目录?

我有我的 Git 存储库,它的根目录有两个子目录:

/finisht
/static

SVN 中时/finisht在一个位置检出,而/static在其他地方检出,如下所示:

svn co svn+ssh://admin@domain.com/home/admin/repos/finisht/static static

有没有办法用 Git 做到这一点?

答案

您尝试执行的操作称为稀疏签出 ,该功能已在 git 1.7.0 中添加(2012 年 2 月)。进行稀疏克隆的步骤如下:

mkdir <repo>
cd <repo>
git init
git remote add -f origin <url>

这将使用您的遥控器创建一个空的存储库,并获取所有对象,但不将其检出。然后做:

git config core.sparseCheckout true

现在,您需要定义要实际检出的文件 / 文件夹。通过在.git/info/sparse-checkout列出它们来完成此操作,例如:

echo "some/dir/" >> .git/info/sparse-checkout
echo "another/sub/tree" >> .git/info/sparse-checkout

最后但并非最不重要的一点是,使用远程状态更新空仓库:

git pull origin master

现在,您将在文件系统上 “检出” some/diranother/sub/tree的文件(这些路径仍然存在),并且不存在其他路径。

您可能想看一下扩展教程,并且应该阅读稀疏签出的官方文档

作为功能:

function git_sparse_clone() (
  rurl="$1" localdir="$2" && shift 2

  mkdir -p "$localdir"
  cd "$localdir"

  git init
  git remote add -f origin "$rurl"

  git config core.sparseCheckout true

  # Loops over remaining args
  for i; do
    echo "$i" >> .git/info/sparse-checkout
  done

  git pull origin master
)

用法:

git_sparse_clone "http://github.com/tj/n" "./local/location" "/bin"

请注意,这仍将从服务器下载整个存储库 - 仅减少结帐的大小。目前,无法仅克隆单个目录。但是,如果不需要存储库的历史记录,则至少可以通过创建浅表克隆来节省带宽。有关如何组合浅表克隆和稀疏校验的信息,请参见下面的udondan 答案

编辑 :从 Git 2.19 开始,这最终是可能的,如该答案所示

考虑提高答案。

注意:在 Git 2.19 中,仅实现了客户端支持,仍然缺少服务器端支持,因此仅在克隆本地存储库时有效。还要注意,大型 GitHub 托管者(例如 GitHub)实际上并不使用 Git 服务器,而是使用自己的实现,因此,即使在 Git 服务器中显示了支持,也并不意味着它可以自动在 Git 托管者上使用。 (OTOH,由于他们不使用 Git 服务器,因此可以在自己的实现中更快地实现它,然后再将其显示在 Git 服务器中。)


不,这在 Git 中是不可能的。

在 Git 中实现这样的工作将是一项巨大的工作,这将意味着不再能够保证客户端存储库的完整性。如果您有兴趣,请在 git 邮件列表上搜索有关 “稀疏克隆” 和 “稀疏访存” 的讨论。

通常,Git 社区的共识是,如果您有几个始终独立检出的目录,则它们实际上是两个不同的项目,应该位于两个不同的存储库中。您可以使用Git 子模块将它们重新粘合在一起。

您可以结合使用稀疏签出浅表克隆功能。 浅表克隆会切断历史记录,而稀疏检出只会提取与模式匹配的文件。

git init <repo>
cd <repo>
git remote add origin <url>
git config core.sparsecheckout true
echo "finisht/*" >> .git/info/sparse-checkout
git pull --depth=1 origin master

您需要最低 git 1.9 才能运行。我自己仅使用 2.2.0 和 2.2.2 进行了测试。

这样,您仍然可以推送 ,而git archive无法实现。

来自 Git 2.19 的git clone --filter

该选项实际上将跳过从服务器获取不需要的对象的操作。还包括Git 2.20 中的--filter=tree:0和 Git 2.24 中添加的--filter=combine复合过滤器,我们最终得到:

git clone \
  --depth 1 \
  --filter=combine:blob:none+tree:0 \
  --no-checkout \
  "file://$(pwd)/server_repo" \
  local_repo \
;
cd local_repo
git checkout master -- mydir/

服务器应配置为:

git config --local uploadpack.allowfilter 1
git config --local uploadpack.allowanysha1inwant 1

对 Git 远程协议进行了扩展,以支持v2.19.0此功能,并实际上跳过了提取不需要的对象的操作,但是当时没有服务器支持。但是它已经可以在本地测试。

命令细目:

  • --filter=blob:none跳过所有 blob,但仍获取所有树对象
  • --filter=tree:0跳过不需要的树: https --filter=tree:0
  • --depth 1已经意味着--single-branch ,另请参见: 如何在 Git 中克隆单个分支?
  • file://$(path)才能克服git clone协议的恶作剧: 如何用相对路径浅克隆本地 git 存储库?
  • --filter=combine:FILTER1+FILTER2是一次使用多个过滤器的语法,由于某些原因尝试通过--filter失败,并显示:“无法组合多个过滤器规格”。它是在 Git 2.24 中的 e987df5fe62b8b29be4cdcdeb3704681ada2b29e“列表对象过滤器:实现复合过滤器” 中添加的

--filter的格式记录在man git-rev-list

Git 树上的文档:

测试一下

#!/usr/bin/env bash
set -eu

list-objects() (
  git rev-list --all --objects
  echo "master commit SHA: $(git log -1 --format="%H")"
  echo "mybranch commit SHA: $(git log -1 --format="%H")"
  git ls-tree master
  git ls-tree mybranch | grep mybranch
  git ls-tree master~ | grep root
)

# Reproducibility.
export GIT_COMMITTER_NAME='a'
export GIT_COMMITTER_EMAIL='a'
export GIT_AUTHOR_NAME='a'
export GIT_AUTHOR_EMAIL='a'
export GIT_COMMITTER_DATE='2000-01-01T00:00:00+0000'
export GIT_AUTHOR_DATE='2000-01-01T00:00:00+0000'

rm -rf server_repo local_repo
mkdir server_repo
cd server_repo

# Create repo.
git init --quiet
git config --local uploadpack.allowfilter 1
git config --local uploadpack.allowanysha1inwant 1

# First commit.
# Directories present in all branches.
mkdir d1 d2
printf 'd1/a' > ./d1/a
printf 'd1/b' > ./d1/b
printf 'd2/a' > ./d2/a
printf 'd2/b' > ./d2/b
# Present only in root.
mkdir 'root'
printf 'root' > ./root/root
git add .
git commit -m 'root' --quiet

# Second commit only on master.
git rm --quiet -r ./root
mkdir 'master'
printf 'master' > ./master/master
git add .
git commit -m 'master commit' --quiet

# Second commit only on mybranch.
git checkout -b mybranch --quiet master~
git rm --quiet -r ./root
mkdir 'mybranch'
printf 'mybranch' > ./mybranch/mybranch
git add .
git commit -m 'mybranch commit' --quiet

echo "# List and identify all objects"
list-objects
echo

# Restore master.
git checkout --quiet master
cd ..

# Clone. Don't checkout for now, only .git/ dir.
git clone --depth 1 --quiet --no-checkout --filter=blob:none "file://$(pwd)/server_repo" local_repo
cd local_repo

# List missing objects from master.
echo "# Missing objects after --no-checkout"
git rev-list --all --quiet --objects --missing=print
echo

echo "# Git checkout fails without internet"
mv ../server_repo ../server_repo.off
! git checkout master
echo

echo "# Git checkout fetches the missing directory from internet"
mv ../server_repo.off ../server_repo
git checkout master -- d1/
echo

echo "# Missing objects after checking out d1"
git rev-list --all --quiet --objects --missing=print

GitHub 上游

Git v2.19.0 中的输出:

# List and identify all objects
c6fcdfaf2b1462f809aecdad83a186eeec00f9c1
fc5e97944480982cfc180a6d6634699921ee63ec
7251a83be9a03161acde7b71a8fda9be19f47128
62d67bce3c672fe2b9065f372726a11e57bade7e
b64bf435a3e54c5208a1b70b7bcb0fc627463a75 d1
308150e8fddde043f3dbbb8573abb6af1df96e63 d1/a
f70a17f51b7b30fec48a32e4f19ac15e261fd1a4 d1/b
84de03c312dc741d0f2a66df7b2f168d823e122a d2
0975df9b39e23c15f63db194df7f45c76528bccb d2/a
41484c13520fcbb6e7243a26fdb1fc9405c08520 d2/b
7d5230379e4652f1b1da7ed1e78e0b8253e03ba3 master
8b25206ff90e9432f6f1a8600f87a7bd695a24af master/master
ef29f15c9a7c5417944cc09711b6a9ee51b01d89
19f7a4ca4a038aff89d803f017f76d2b66063043 mybranch
1b671b190e293aa091239b8b5e8c149411d00523 mybranch/mybranch
c3760bb1a0ece87cdbaf9a563c77a45e30a4e30e
a0234da53ec608b54813b4271fbf00ba5318b99f root
93ca1422a8da0a9effc465eccbcb17e23015542d root/root
master commit SHA: fc5e97944480982cfc180a6d6634699921ee63ec
mybranch commit SHA: fc5e97944480982cfc180a6d6634699921ee63ec
040000 tree b64bf435a3e54c5208a1b70b7bcb0fc627463a75    d1
040000 tree 84de03c312dc741d0f2a66df7b2f168d823e122a    d2
040000 tree 7d5230379e4652f1b1da7ed1e78e0b8253e03ba3    master
040000 tree 19f7a4ca4a038aff89d803f017f76d2b66063043    mybranch
040000 tree a0234da53ec608b54813b4271fbf00ba5318b99f    root

# Missing objects after --no-checkout
?f70a17f51b7b30fec48a32e4f19ac15e261fd1a4
?8b25206ff90e9432f6f1a8600f87a7bd695a24af
?41484c13520fcbb6e7243a26fdb1fc9405c08520
?0975df9b39e23c15f63db194df7f45c76528bccb
?308150e8fddde043f3dbbb8573abb6af1df96e63

# Git checkout fails without internet
fatal: '/home/ciro/bak/git/test-git-web-interface/other-test-repos/partial-clone.tmp/server_repo' does not appear to be a git repository
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

# Git checkout fetches the missing directory from internet
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), 45 bytes | 45.00 KiB/s, done.
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), 45 bytes | 45.00 KiB/s, done.

# Missing objects after checking out d1
?8b25206ff90e9432f6f1a8600f87a7bd695a24af
?41484c13520fcbb6e7243a26fdb1fc9405c08520
?0975df9b39e23c15f63db194df7f45c76528bccb

结论: d1/外部的所有斑点均缺失。例如0975df9b39e23c15f63db194df7f45c76528bccb (签出d1/a后不存在d2/b )。

请注意, root/rootmybranch/mybranch也丢失了,但是--depth 1将其隐藏在丢失文件的列表中。如果删除--depth 1 ,它们将显示在丢失文件的列表中。

我有一个梦想

此功能可能会彻底改变 Git。

想象一下,将您企业的所有代码库都放在一个回购中,而没有repo这样的丑陋第三方工具

想象一下, 将巨大的 blob 直接存储在仓库中,而无需任何丑陋的第三方扩展

想象一下,如果 GitHub 允许每个文件 / 目录元数据(例如星号和权限),那么您可以将所有个人内容存储在一个回购协议下。

想象一下子模块是否与常规目录完全一样 :仅请求树状 SHA,然后采用类似 DNS 的机制即可解决您的请求 ,首先查看本地~/.git ,然后查找更近的服务器(您企业的镜像 / 缓存),然后结束在 GitHub 上。

对于只想从 github 下载文件 / 文件夹的其他用户,只需使用:

svn export <repo>/trunk/<folder>

例如

svn export https://github.com/lodash/lodash.com/trunk/docs

(是的,这里是 svn。显然在 2016 年,您仍然需要 svn 来简单下载一些 github 文件)

礼貌: 从 GitHub 存储库下载单个文件夹或目录

重要提示 - 确保更新 github URL,并用 '/ trunk /' 替换/tree/master/

作为 bash 脚本:

git-download(){
    folder=${@/tree\/master/trunk}
    folder=${folder/blob\/master/trunk}
    svn export $folder
}

注意此方法下载一个文件夹,不克隆 / 签出它。您不能将更改推回存储库。另一方面 - 与稀疏结帐或浅结帐相比,下载量较小。

如果您从未打算与要克隆的存储库进行交互,则可以执行完整的git 克隆,并使用git filter-branch --subdirectory-filter重写存储库。这样,至少将保留历史。

看起来要简单得多:

git archive --remote=<repo_url> <branch> <path> | tar xvf -

Git 1.7.0 具有 “稀疏签出” 功能。请参见git config联机帮助页中的 “core.sparseCheckout”, git 读取树联机帮助页中的 “Sparse checkout” 和git update-index联机帮助页中的 “Skip-worktree 位”。

该接口不如 SVN 方便(例如,在初始克隆时无法进行稀疏签出),但是现在可以使用可用于构建更简单接口的基本功能。

无法仅使用 Git 克隆子目录,但以下是一些解决方法。

过滤器分支

您可能想要重写存储库,使其看起来好像trunk/public_html/已经是其项目根,并丢弃所有其他历史记录(使用filter-branch ),尝试使用已经签出的分支:

git filter-branch --subdirectory-filter trunk/public_html -- --all

注意: --将过滤器分支选项与修订选项分开,而--all重写所有分支和标记。将保留所有信息,包括原始提交时间或合并信息。此命令支持refs/replace/名称空间中的.git/info/grafts文件和 ref,因此,如果您定义了任何嫁接或替换refs ,则运行此命令会使它们永久化。

警告!重写的历史记录将为所有对象使用不同的对象名称,并且不会与原始分支收敛。您将无法轻松地将重写的分支推入并分发到原始分支的顶部。如果您不了解全部含义,请不要使用此命令;如果简单的一次提交就可以解决您的问题,请不要使用此命令。


稀疏结帐

这是使用稀疏签出方法的简单步骤,该方法将稀疏地填充工作目录,因此您可以告诉 Git 值得签出工作目录中的哪个文件夹或文件。

  1. 照常克隆存储库( --no-checkout是可选的):

    git clone --no-checkout git@foo/bar.git
    cd bar

    如果您的存储库已经克隆,则可以跳过此步骤。

    提示:对于大型存储库,请考虑使用浅克隆--depth 1 )仅签出最新修订版或 / 和--single-branch和仅--single-branch

  2. 启用sparseCheckout选项:

    git config core.sparseCheckout true
  3. 指定用于稀疏签出的文件夹(末尾没有空格):

    echo "trunk/public_html/*"> .git/info/sparse-checkout

    或编辑.git/info/sparse-checkout

  4. 检出分支(例如master ):

    git checkout master

现在,您应该已经在当前目录中选择了文件夹。

如果目录或过滤分支的级别过多,则可以考虑使用符号链接。


我刚刚为GitHub 编写了一个脚本

用法:

python get_git_sub_dir.py path/to/sub/dir <RECURSIVE>