将其他存储库子目录的稀疏签出推送到我们的存储库

Pushing sparse checkout of other repository subdirectory to our repository

我有两个存储库

Repo1             
  |_______ folder1
  |_______ folder2
  |_______ folder3

Repo2             
  |_______ folder21
  |_______ folder22
  |_______ folder23

我想link在repo1中的repo2的floder22。

这样做...我试过一些像this

git clone repos1
cd repos1
git remote add repos2 <github link of repos2>
git remote -v 
git config core.sparseCheckout true
echo "Folder22/*" > .git/info/sparse-checkout
% Comment(open .git/info/sparse-checkout folder using editor and add all the folders that are to be tracked. So Now sparse-checkout file looks like
Folder22/*
Folder1/*
Folder2/*
Folder3/*)
git pull origin master
git pull repos2 master --allow-unrelated-histories

到目前为止,我可以检出到 repo1 和 repo2 中的任何分支或任何提交。 这里的问题是当我们在 repos1 中进行一些提交并尝试推送 repos1 的最新更改时,远程 repos1 看起来像

Repo1 
  |_______ folder21
  |_______ folder22
  |_______ folder23            
  |_______ folder1
  |_______ folder2
  |_______ folder3

而不是

Repo1 

  |_______ folder22           
  |_______ folder1
  |_______ folder2
  |_______ folder3

你能帮帮我吗?

谢谢

你从一个非常糟糕的地方开始了整个问题。

Git 不推送文件。 Git 推送 提交 。 Git 也不 存储 文件,不是直接存储:它存储 提交 。每个提交都是 每个文件的完整快照

从 提交中提取稀疏签出。 commit 包含每个文件,但是稀疏检出操作会选择 哪些文件 在 commit[=581= 中](全部都是!)实际上来自 提交

任何 检出总是必须复制提交中的文件,从提交中复制出来。这是因为存储在提交中的文件采用特殊的 read-only、Git-only 格式、压缩和 de-duplicated 针对其他文件(在相同的 and/or 其他提交中).因此,您计算机上的其他程序无法使用这些文件。所以要使用一个提交,即使只是为了阅读它,Git也必须提取提交。这与您将在任何存档上使用的提取类型相同,这是有道理的:毕竟每个提交 一个存档。

因此,定期检出就像从某个存档中提取 所有 文件,而稀疏检出就像细读档案的文件列表并选择并选择只提取 一些 那些文件。如果您从这些知识开始,您的立足点就会好一些。 您不能推送签出(稀疏或其他)因为签出本身不是提交并且Git仅推送提交.

还有更多要知道

既然您知道提交是一个存档,下面是关于提交的其他信息:每个提交都有一个编号。这个数字非常大并且 似乎 随机(尽管它实际上完全是 non-random:它是提交内容的加密校验和)。 Git 称其为 哈希 IDobject ID;通常用 hexadecimal 表示。这个数字是 Git 找到 特定提交的方式。

虽然每个提交都包含每个文件的完整存档(压缩和de-duplicated),但每个提交还包含一些元数据,或有关提交本身的信息。例如,此元数据包括提交人的姓名和电子邮件地址。您将在 git log 输出中看到大部分此类元数据。

Git 放入每个提交中的元数据的一个关键位是一些较早 提交或一组提交的原始哈希 ID。也就是说,每个 new 提交都会记住一些较旧(现有)提交的哈希 ID。

特别是,我们检查 一些致力于此的承诺。这填充了 工作树 ——保存提取的结帐的地方——并且,因为 Git 有点特殊,还填充了 Git 调用的内容,不同的是, index暂存区,以及最近很少见的缓存。 (姓氏 cache 大多以标志的形式出现:例如 git rm --cached。)使用稀疏结帐时,Git 仅填写 部分 工作树,但完全填充索引。

每个文件的索引副本都是Git的内部格式:压缩和de-duplicated。由于来自 当前提交 的每个文件的索引副本——您刚刚签出的那个——已经 在 Git 存储库 中(在该提交中),它们必然都是重复的。因此,他们都是de-duplicated,所以他们没有space。这使得索引副本几乎是免费的:制作索引副本的成本是缓存的一点点 space(粗略平均每个文件不到 100 字节左右,所以 1000 个文件平均不到 100 kiB ),但 Git 无论如何都需要 space 用于其他目的,因此您必须支付它,无论是使用稀疏结帐还是完整结帐。

当您进行 new 提交时,使用 git commit、Git 将:

  • 将索引中的所有文件变成一个新的快照:这进行得相当快,因为​​索引中的文件副本已经在内部Git-only格式;
  • 从您或您的配置中获取任何 元数据 Git 需要但不会自行提供:例如,Git 读取您的用户来自 user.name 的姓名和来自 user.email;
  • 的电子邮件地址
  • 将元数据——包括你之前签出的当前提交的哈希ID——与快照打包在一起,并将所有这些作为一个新提交写出。

写出新提交的行为会产生新提交的新的、唯一的哈希 ID。 Git然后存储这个新的哈希ID在当前分支nae.

结果是当前分支 name 前进到包含新提交,而新提交现在指向 是 [=581] 的提交=] 该分支的最新消息,就在刚才。换句话说,如果旧提交链结束于哈希为 H:

的提交
        ... <-H

然后我们刚刚添加了一个新的提交。让我们调用新提交的哈希 ID I:

         ... <-H <-I

I 现在是这个链中的最新/最新提交,我们会找到一些分支名称,例如 master。当然,commit H 也存储了一些较早提交的 hash Id,所以让我们绘制分支名称和较早提交,我们可以调用 G:

    ... <-G <-H <-I   <-- master

较早的提交 G 存储一些 even-earlier 提交的哈希 ID F:

... <-F <-G <-H <-I   <-- master

然后继续(或来回)到 有史以来第一次提交确实没有指向任何进一步倒退,仅仅是因为它不能

为了更好地理解分支名称,请记住 git checkout 从提交中提取文件。当它这样做时,Git 将 记住 你现在 使用 这个分支名称。让我们在某个存储库中绘制一些提交:

...--G--H   <-- master

现在让我们向这个存储库添加两个分支名称branch1branch2。他们还将 select 最新提交 H:

...--G--H   <-- branch1, branch2, master

我们需要知道我们使用的是哪个名称。如果我们 运行 git checkout master,Git 从提交 H 中填充它的索引和我们的工作树,并将特殊名称 HEAD 绑定到名称 master:

...--G--H   <-- branch1, branch2, master (HEAD)

如果我们现在 运行 git checkout branch1, Git 删除 提交 H 的所有文件,并替换它们与...一起提交 H 的文件,因为 branch1 仍然 select 提交 H。 Git 实际上注意到了这一点并且没有打扰 removing-and-replacing 任何事情,但是附加的 HEAD 移动到名称 branch1:

...--G--H   <-- branch1 (HEAD), branch2, master

现在让我们进行新的提交。我们将修改一些文件 and/or 创建一些新文件,然后使用 git add 告诉 Git 将更新的或新的文件 复制到 Git 中index,又名暂存区。更新文件更新,新文件新建。它们的内容被压缩并且 de-duplicated: Git 检查内容是否曾出现在任何 earlier 提交中,如果是,re-uses旧内容,而不是新的压缩数据。否则 Git 缓存新的压缩数据,准备提交,并且在任何一种情况下,Git 更新这些文件的索引条目。

现在我们运行git commit。 Git 将索引文件打包成一个快照,添加元数据,并写出新的提交 I。更新哪个分支名称?看图:找到HEAD后面的名字。因此,如果我们绘制新的一组提交,它看起来像这样:

          I   <-- branch1 (HEAD)
         /
...--G--H   <-- branch2, master

如果我们再进行一次新提交,我们将得到:

          I--J   <-- branch1 (HEAD)
         /
...--G--H   <-- branch2, master

如果我们现在 运行 git checkout branch2, Git 从它的索引和我们的工作树中删除所有提交 J 的文件,并填写它的索引和我们的工作树以及来自 H 的所有文件。或者,如果我们使用稀疏结帐,它会用它的索引来完成整个事情,而稀疏的事情会用我们的工作树来完成。无论哪种方式,我们现在再次提交 H

          I--J   <-- branch1
         /
...--G--H   <-- branch2 (HEAD), master

如果我们现在进行两次 更多 新提交,这些新提交会导致名称 branch2 前进:

          I--J   <-- branch1
         /
...--G--H   <-- master
         \
          K--L   <-- branch2 (HEAD)

请注意,当我们开始时,所有提交(从 H 开始的所有内容)都在 所有三个分支 上。从那时起,我们添加了四个提交:两个在 branch1 上,两个在 branch2 上。 H 之前的所有提交 仍在所有三个分支上 。提交 I-J 目前仅在 branch1 上进行,而提交 K-L 目前仅在 branch2 上进行,但我们即将对其进行更改。

你现在需要明白git merge

既然您知道提交和分支名称是如何工作的,您就可以开始 git merge

我们现在运行git checkout master。第一步像往常一样从提交 H 中填充 Git 的索引和我们的工作树(如果需要,首先从提交 L 中删除文件)。所以我们现在有这个:

          I--J   <-- branch1
         /
...--G--H   <-- master (HEAD)
         \
          K--L   <-- branch2

如果我们现在 运行 git merge branch1,Git 现在将找到 三个 提交:

  • 第一个(或者在某些方面,第二个)提交是我们当前的提交 H
  • 第二个(或者在某些方面,第三个)提交是我们告诉 Git 找到的那个:branch1 指向提交 J,所以这是“其他”提交.
  • Git 现在使用这两个提交来查找 最佳共享提交: 两个分支 上的提交,并且比同时在两个分支上的任何其他提交都要好。此处提交的“优点”取决于它与两个分支提示提交的接近程度HJ.

我们已经知道两个分支上的提交都是通过 H 的提交。最近的此类提交 H 就是提交 H 本身。最接近 J 的此类提交也是 H。因此,除了作为当前或 HEAD 提交之外,提交 H 也是此特定合并的合并基础。这使得这种合并成为一种特殊情况!

当合并基础是 HEAD 提交时,Git 将在您不阻止它的情况下执行它所谓的 fast-forward 合并。 Fast-forwarding 在技术上是分支名称移动的 属性,但是当您使用 git merge 执行此操作时,Git 将其称为 fast-forward 合并。 (在其他情况下 Git 称它为 fast-forward,没有 merge 这个词。)Git 实际上通过做一个简单的 git checkout 的另一个提交 同时拖动当前分支名称 ,并且不更改分支名称。结果是:

          I--J   <-- branch1, master (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

请注意名字 master 如何“向前移动”(在这些图中向右)提交 J。我们现在有两个分支名称 select 相同提交的情况。

但现在我们将 运行:

git merge branch2

Git 必须再次定位这三个提交,其中最重要的一个是合并基础。合并基础是最好的 shared 提交。共享哪些提交?和以前一样,它仍然是 H 以上的那些。其中哪一个是 最好的,即最接近 JL?毫不奇怪,它再次提交 H

所以合并基础是提交 H。这一次,Git 必须进行真正的合并:合并基础 H 不是当前提交 J.

合并的目标合并工作。也就是说,Git想弄清楚我们当前分支master上的“我们改变了什么”,分别是“他们改变了什么”(不管他们是谁) 在他们的分支上 branch2。但是每个提交都包含一个快照,而不是一些更改集。

要从像 J 这样的快照中查找更改,Git 必须 比较 此快照与其他提交。如果您考虑一下,这里明显的候选者是合并基础提交 H:

git diff --find-renames <hash-of-H> <hash-of-J>   # what we changed

Git 然后可以进行相同类型的比较,从相同的提交 H 开始,但是转到 他们的 提交 L :

git diff --find-renames <hash-of-H> <hash-of-L>   # what they changed

这两个 git diff 命令的输出显示了如何进行 我们的 更改,以及如何进行 他们的 更改,如果我们从提交 H 开始。因此,在保存了 进行 这些更改所需的工作后,Git 现在可以......(考虑一下!)

...检查提交 H,合并基础。检查提交 H 后,Git 可以将 两组更改 应用到所有不同的文件。在这些更改不冲突的情况下,Git 以两个更改结束。如果(如果)这些 冲突,Git 将声明一个 合并冲突 并让我们收拾残局。

注意这里有一些不错的 short-cuts Git 可以使用。假设从 HJ,我们更改了文件 README.md 并添加了 totally-new 文件 xyz.py。他们更改了 README.md 并修改了现有文件 main.py。当 Git 组合 这些变化时:

  • 它必须合并 我们在README.md 上所做的工作。这里可能会有冲突,看我们改了什么,他们改了什么。如果没有,那就太好了。
  • 它将以我们的 xyz.py 版本结束,因为那是全新的。这通常会重复所有 totally-new 个文件。
  • 它将以他们的 main.py 版本结束,因为我们没有触及 main.py

一般来说,如果我们触及了一些文件而他们没有,Git将采取我们的改变/我们的文件的版本。如果 他们 触及了一些文件而我们没有,Git 将采用 他们 更改/他们的文件版本。 Git 只需要努力处理我们都接触过的任何文件。这往往会使合并进行得非常快,具体取决于有多少文件进行了多少更改。但原则上,Git 正在将组合更改应用于合并基础提交中的文件。

合并完成后,如果没有合并冲突,Git 将自动进行 new 提交。这个新提交将当前分支名称向前拖动,像往常一样将其移动到那个新提交。这个新提交像往常一样有所有文件的快照:快照是合并我们的更改和他们对合并库中文件的更改的结果。

这个新合并提交的 只有 特别之处,事实上,它不是链接回提交 J,而是链接回 both 提交涉及合并:

          I--J   <-- branch1
         /    \
...--G--H      M   <-- master (HEAD)
         \    /
          K--L   <-- branch2

请注意,Git 不会费心将合并链接到合并基础(Git 自动计算合并基础;它可以 re-compute 稍后从两个分支提示,并且将得到相同的结果)。

链接到两个分支提示的原因是为了有效处理以后合并。假设我们现在 git checkout branch2 并添加一些提交,然后再次 git checkout master

          I--J   <-- branch1
         /    \
...--G--H      M   <-- master (HEAD)
         \    /
          K--L---N--O   <-- branch2

如果我们现在运行 git merge branch2,哪个提交是合并基础?尝试一步一步解决这个问题:

  • master selects 提交 M,但那只是在 master 上,所以我们必须后退一步。返回一步得到 提交 JL.
  • branch2 selects 提交 O,但那只在 branch2 上,所以我们必须返回。向后退一步让我们到达 N,它仍然只在 branch2 上,所以我们再次返回到 L.
  • Lmaster 上!我们通过返回一跳到达那里。 L 也在 branch2 上;我们通过返回两跳到达那里。没有 更接近 的提交:提交 K 在两个分支上但更远,提交 Jmasterbranch1 但不在 branch2 上,提交 IJ 有相同的问题(不在 branch2 上),并且提交 H 在所有分支上但是甚至更远 K。如果我们继续前进,我们只会走得更远。

所以提交L是这次的合并基础。我们的下一个 git merge 将比较(比较)L 中的快照与 M 中的快照,以查看“我们”发生了什么变化。这将显示我们从提交 J (branch1) 合并时保留的所有内容。然后它将比较 LO 以查看“他们”在 branch2 上发生了什么变化,而这正是我们需要合并的内容。因此,通过组合这两组更改并根据结果进行新的提交,我们得到了正确的合并:

          I--J   <-- branch1
         /    \
...--G--H      M------P   <-- master (HEAD)
         \    /      /
          K--L---N--O   <-- branch2

新提交 P,在 master,导致提交 N-Omaster 上,并获取从 L 到 [=145] 的更改=],进入 branch2

评论

  • A commit 包含快照和元数据。我们通过哈希 ID 找到提交,尽管我们经常通过分支名称找到哈希 ID。 (其他时候我们找到哈希 ID,通常是通过 分支名称反向工作——我们只在必要时使用原始哈希 ID,因为它们非常麻烦且不利于人类。)

  • A 分支名称 select我们认为是分支的最后一次提交.这意味着随着分支名称的移动,“在”某些分支上的提交集会随着时间动态变化。

  • A merge commit链接两个分支,之后其中一个分支名称可能变得不必要。例如,在上面,一旦我们完成了它,我们就从未使用过 branch1:如果我们不打算向它添加更多提交,我们可以删除它。

  • 合并行为使用了三个 提交中的内容。一次提交是您当前的提交,一次是您在命令行中命名的提交,第三次——或者第一次,真的,因为它将被 git diff 编辑两次,一次针对 HEAD,然后一次针对另一个提交——是合并基础.

  • 稀疏签出对任何提交中的内容没有影响:它只会影响提取到您的工作树的内容。

git pull

git pull 命令实际上只是 shorthand 用于 运行 两个 Git 命令。第一个是 git fetch(总是)。在 git fetch 运行 之后,您通常希望对通过 git fetch 获取的任何提交 做一些事情 ,因为 git fetch 意味着调用其他 Git 软件,与其他 Git 存储库对话,并从其他 Git 获取提交。现在您有 提交,您可能想 对它们做一些事情.

命令git pull运行s可配置。您可以选择 git mergegit rebase。我们在这里只介绍了 git merge,因为这是您现在正在使用的。

合并不相关的历史记录

你的实际命令是:

git pull repos2 master --allow-unrelated-histories

--allow-unrelated-histories 旗帜是一个危险信号。

记住 git merge 的工作原理,找到合并基础。 Git 使用存储库中的一组提交及其链接来执行此操作。我们有:

          I--J   <-- one (HEAD)
         /
...--G--H
         \
          K--L   <-- two

或多或少,合并基础是提交 H

不过,在您的情况下,您有存储库 repos1,它有一些从 root 提交 开始的提交链——最开始的提交,即没有 parent——并在某个时刻结束:

A--B--C--D   <-- master (HEAD), origin/master

然后你有了存储库 repos2,它有一些其他提交链,从它自己的 开始单独的根提交:

E--F--G--H   <-- repos2/master

然后您指示 Git 将提交 D、您的 master 与提交 H 合并。但是如果我们从 DH 向后计算,两条线永远不会相交:

A--B--C--D   <-- master (HEAD)

E--F--G--H   <-- repos2/master

相反,我们遇到了两个死胡同,例如提交 AE

自Git 2.9 版以来,git merge 拒绝合并此类历史记录。 没有合并基础。没有共同的起点!合并是什么意思?

但是,

Git 曾经有一个答案(在 2.9 之前),而 --allow-unrelated-histories 告诉 Git 使用其旧的(通常是错误的)答案。 Git 假装 在两条链之前有一个提交:

  A--B--C--D   <-- master (HEAD)
 /
α
 \
  E--F--G--H   <-- repos2/master

这个伪提交α(Git为此使用empty tree),所以当Git 运行s:

git diff --find-renames α <hash-of-D>

commit D 中的所有“我们的”文件都是 new,当 Git 运行s:

git diff --find-renames α <hash-of-H>

提交 H 中的所有“他们的”文件也是新的。

“添加新文件file1”与“添加新文件file1”的组合。无和“添加新文件file2”的组合是“添加新文件file2”。所以只要提交 DH 中的所有文件 names 都是 different,这个合并就会顺利进行, Git 将进行新的合并提交 I:

A--B--C--D
          \
           I   <-- master (HEAD)
          /
E--F--G--H   <-- repos2/master

新提交包含包含所有文件的快照。

我们终于可以解决您的问题了

The problem here is when we made some commits in repos1 and try to push the latest changes of repos1 ...

提交 I 之后(其中包含来自两个提交 DH 所有文件 ),我们可以看到发生了什么当你做出进一步的承诺时。您启用了稀疏结帐模式,因此您的 工作树 仅显示您 select 使用稀疏结帐设置编辑的文件。但是提交 I 所有文件 。 Git 的索引也是如此。因此,您所做的新提交 JK 具有 所有文件:

A--B--C--D
          \
           I--J--K   <-- master (HEAD)
          /
E--F--G--H   <-- repos2/master

你可能没有把它们都签出来,但它们都在那里。

当你 运行 git push 时,你 Git 调用其他 Git (通常是另一台计算机上的软件,并与另一个存储库对话)和您向他们发送您拥有但他们没有的任何提交,这是这个特定 git push 所需要的。然后你要求他们 Git 设置他们的 分支名称之一 来记录新的提交。

因为提交是完整的-read-only,never-changing 所有文件的快照,并且提交I所有文件,提交JK也是如此,他们得到所有文件并检查提交K显示所有文件。

如果您希望提交 I 的文件少于所有文件,您需要在提交前删除 一些文件。请注意,即使您这样做,提交 I 也会链接回更早的提交 H,其中有... 所有文件。所以他们的 Git 存储库 将获得 所有文件 。提交 I and/or J and/or K 他们的 档案中可能有更少的文件,但只要你让将 E-F-G-H 提交到存储库并将它们附加到此处,您将发送所有文件。

你能做些什么

你有很多选择:

  • 一种是允许所有文件通过(你现在正在做的)。

  • 另一种是使用git merge --squash --no-commit,然后删除不需要的文件。这将允许您避免连接历史 E-F-G-H 提交,这意味着您不会引入其他文件,但也会丢失历史记录(因为提交 历史);就是这样。

  • 另一种方法是用 folder22/* 文件的 副本 填充您的工作树,这些文件来自已检出的 repos2 克隆:这不会获取这些文件的历史记录,但就是这样。

  • 还有一个方法是获取 repos2 克隆并将其复制到一个新的(不同的,不兼容的)存储库,其中历史记录仅包含 folder22/* 文件。这很重要(但如果您知道如何工作,也不是特别困难 git filter-branch)。这让你有一段历史。这不是那些文件的原始历史,但事情就是这样:原始历史与 repos2.

    中的所有其他文件不可磨灭地交织在一起

可能会有更多选择。您将必须根据您对 Git 的新知识来复习所有内容,然后选择前进的道路。