首页 > 解决方案 > 我可以将一个本地 git 存储库与两个远程存储库一起使用吗?

问题描述

git 新手,想确定我明白我在做什么。

目前,我有一个本地仓库推送到公共远程仓库。我想将某些文件和目录从公共远程仓库中分离出来并添加到私有远程仓库中。

我的计划是将这些文件和目录添加到.gitignore,创建一个名为 的新分支privateRepo,然后将其链接到私有远程仓库。之后,切换到分支,privateRepo添加/提交/推送本地内容。

git checkout -b privateRepo
git remote add privateRepo <url>
git switch privateRepo
git add <files & dir>
git commit -m "message"
git push

在此之后,我可以使用git switch main返回到我的主分支并推送而不影响privateRepo.

这个对吗?

如果我想从privateRepo本地存储库中提取,这会带来任何问题吗?

标签: gitgithub

解决方案


可以按照你的提议去做。我建议不要这样做,因为:

  • 它不像我认为的那样起作用;和
  • 很容易意外地将错误的提交发送到错误的“其他 Git 存储库”,从而永远发布您的私人文件。

为了理解这一点,我们需要了解存储库是什么以及为您做什么的基础知识。存储库主要是两个数据库的集合:

  • 一个数据库保存提交和其他支持 Git 对象。这是“对象数据库”。它是一个简单的键值存储,其中键是哈希ID——提交哈希 ID 和其他支持对象哈希 ID——而值是对象(提交和提交用于永久存储文件的东西)。

  • 另一个数据库保存名称。对象数据库中的键——散列 ID——对于人类来说太大太丑了。所以我们不使用:我们使用名称。Git 保留第二个数据库,以便它可以从名称转换为哈希 ID,以查找提交(和其他内部对象)。例如,人类可以处理名称,例如mastermain作为分支名称。

当您使用多个 Git 存储库并将它们与git fetchor交叉连接时git push(请注意,这git pull是一个方便的命令,意思是run git fetch,然后运行第二个 Git 命令,所以它实际上git fetch是伪装的),您正在做的是在之间传输提交存储库。您在这里几乎没有选择:您要么发送整个提交,要么不发送。你要么收到一个完整的提交,要么一个都没有。1 如果您确实发送或接收了一个提交,您也会发送或接收您或他们没有的所有其前身(祖先)提交。2

这样做的结果是,如果您不小心将一个提交发送到错误的(即公共)存储库,您可能会发送所有提交。现在,他们拥有您曾经提交的每个私人文件的每个版本。你可以尝试收回这一点——GitHub 让这有点困难,因为你必须联系 GitHub 支持——但是在这些提交可见的窗口期间,任何人和每个人都可以从公共存储库中复制它们。

相反,如果您将文件拆分为“公共可用性文件”(您将其放在一个存储库中,然后将该存储库作为公共存储库与 GitHub 共享)以及进入不同存储库的“私有文件”,然后共享该存储库仅将 GitHub 用作私有存储库,整个事情更易于管理。

正如Ôrel 在评论中提到的那样,您可以使用 Git 的submodules来协调这两个存储库。子模块有它们自己的头痛和缺点,以至于人们有时称它们为sob模块,但它们确实实现了适当的公共/私有分离。


1 Git 正在开发一种称为部分克隆的新设施,其中这个一般的全有或全无原则被小心地拆开,就像一个Jenga 塔。但是,拉错部分,整个事情就会崩溃。这不是——至少目前——不是针对您正在谈论的那种事情,我不建议为此使用它,除非您打算在 Git 本身上添加它。(理论上它可以用于这种目的。)

2浅克隆也可以小心地违反这条规则。浅克隆比部分克隆代码更成熟,但它们仍然没有做你想要的那种事情。


关于存储库的更多信息

许多人认为 Git 是关于文件的。不是:这真的是关于commits。但是,提交确实保存文件。或者他们会认为 Git 是关于分支的。这不是:它是关于提交的。但是,分支名称确实可以帮助您(和 Git)找到提交。所以分支名称和文件很重要。但这实际上是关于提交的。

因为提交存储在对象数据库中,所以它们被编号,带有那些又大又丑的哈希 ID。每个唯一的对象都有一个唯一的编号。特别是提交编号在宇宙中的每个 Git 存储库中必须是唯一的,这意味着数字必须很大(目前有 2 160个可能的数字,并且在相对不久的将来可能会变成 2 256 ,因为结果是 2 160太小)。这就是为什么它们如此庞大、丑陋和随机的原因,尽管实际上它们完全是非随机的:它们是加密哈希函数的输出(目前是 SHA-1;SHA-256 是计划的未来)。

但是,每次提交都会存储件事:

  • 每个提交都存储每个文件的完整快照,以您(或任何人)提交时文件的形式。它们以一种特殊的、只读的、仅限 Git 的、压缩的和去重复的格式(作为数据库中的对象)存储,而不是作为普通的计算机文件。

  • 每个提交都存储一些元数据,或有关提交本身的信息:例如,谁提交、何时提交以及为什么提交(一条日志消息)。此元数据与对象数据库中的所有内容一样是只读的。(只读质量来自 Git 的散列技巧,也是允许文件重复数据删除所必需的。)

在任何给定提交的元数据中,Git 存储先前提交列表的原始哈希 ID。通常这个列表只有一个元素长。我们称这个单一的先前提交为提交的级。这些父 ID 形成了向后看的链:

... <-F <-G <-H

这里H代表链中最后一次提交的哈希 ID。CommitH在其中包含所有文件的完整快照,以及一些元数据。中的元数据H向您显示您(或任何人)进行了提交。他们保留数据,例如您的日志消息。而且,它们表明提交H有一个父级,无论哈希 IDG代表什么。

CommitG当然,也有快照和元数据。使用H,Git 可以找到G并提取两个快照并进行比较。任何相同的东西都没有改变(这对于 Git 来说很容易看到,因为通过散列进行了重复数据删除)。无论有什么不同,Git 都需要在这里运行git diff以找出发生了什么变化。如果您提出要求,Git 会执行此操作,当您查看 commit 时,您会看到从变为的内容。GHH

在查看了提交H之后,像git log现在这样的命令会后退一跳来提交G。CommitG有元数据,包括较早的 commit 的哈希 ID F。CommitF有一个快照,因此 Git 可以比较F-vs-G以查看更改的内容,从而将G更改显示给您,即使G 快照。然后git log可以向后移动一跳到F,然后重复。只有当 Git 回到第一次提交时,该过程才会结束,这是第一次提交,没有父提交,或者当你厌倦了阅读git log输出并让它退出时。

使用提交:工作树和索引/暂存区

但是这里有一个大问题。如果快照中的内容,即永久保存的文件,是只读的——更糟糕的是,它的格式只有 Git 本身才能读取——我们将如何使用它?

这个问题的答案很简单。在一个非裸仓库(也就是大部分)中,Git 添加了一个工作区,称为工作树工作树。要使用提交,Git 只需将提交中的所有文件复制到您的工作树中。现在您可以查看您的文件并完成您的工作。

重要的是要意识到这些文件不在 Git 中。这些是普通的计算机文件,您的所有普通计算机程序都可以读取和写入并且通常可以使用它们。它们可能来自Git。但在这一点上,它们不再Git 中了。它们只是文件。

当您使用它们时,它们可能会偏离Git中的内容。它们的内容发生了变化。在某些时候,您可能希望获取所有更新的文件并将它们永久存储在一个新的提交中。在其他非 Git 版本控制系统中,这很容易:例如,您运行,hg commitMercurial 会找出您更改的内容并进行新的提交。在 Git 中,这并不容易。

相反,Git 为每个文件添加了一个隐藏的额外“副本”。我在这里用引号说“复制”是因为每个文件的这个额外的“副本”是预先 Git 化的:它以 Git 内部使用的压缩、去重格式存储。由于所有这些文件最初都来自某个提交,因此它们都是重复的,因此它们都不占用任何空间。3

当你告诉 Git现在进行新的提交时,Git 只会查看这些隐藏的额外副本。因此,在您进行新的提交之前,您必须运行git add. 什么git add是:

  • 通读某个文件的工作树副本;
  • 压缩并通常 Git-ify 它,提出一个内部对象哈希 ID;
  • 如果这是某些现有文件的副本,请丢弃现在建立的临时文件并使用副本;
  • 否则,为将来的提交做准备并存储它。

无论哪种方式,该git add步骤都会获取更新的文件并使其准备好进入下一次提交。这将替换那里的副本,准备好进入下一次提交。或者,如果文件是全新的——如果之前没有同名的文件——那么git add就没有什么可替换的,而是添加一个新文件,现在有一个副本,准备好进入下一次提交。

在所有情况下,在 之前 git add,Git 已经准备好所有文件以进入新的提交。 之后 git add,Git 准备好所有文件以进入新的提交。所以 Git总是准备好所有文件。什么git add是用更新的文件(如果需要,新的副本,或者如果可能的话,重新使用旧的“副本”)替换一个、两个或多个准备好的“副本”并添加任何新文件。

在 Git中,这个额外准备就绪的尚未提交的东西所在的区域有三个名称。这可能是因为主名称index没有意义。另一个主要名称staging area是指您如何使用索引。第三个名字,缓存,大部分已经失效,但仍然出现在像git rm --cached. 我倾向于使用“索引”这个名称,但“暂存区”可能已成为最常见的名称,这绝对是您使用它的方式:您通过将文件安排在“暂存区”上/中来“暂存”文件,准备好被“拍照”到他们将永远存在的提交中。

这个暂存区域或索引位于当前提交和您的工作树之间。Git 的索引很像一个提交,最初是一个提交设置的,后来变成一个新的提交,但实际提交和 Git 的索引之间的主要区别在于您可以替换文件、添加文件和删除文件来自Git 的索引。您不能对提交执行此操作:提交一旦完成,就一成不变。

当您最终运行时,Git 只是将索引git commit的文件打包。这些成为新提交的快照。所以你必须更新索引。人们经常受到.git commit -a

无论如何,我发现将索引视为提交和工作树之间的索引是有帮助的,如下所示:

  HEAD         index      work-tree
---------    ---------    ---------
README.md    README.md    README.md
main.py      main.py      main.py
                          new.txt

在这里,我们检查了一些包含两个文件的提交,所以HEAD——当前提交——有这两个文件,Git 的索引有这两个文件,我们的工作树有这两个文件。与 HEAD 和索引副本不同,我们实际上可以看到和使用工作树副本,但是这三个副本都存在。

然后我们在工作树中创建了一个新文件, 。new.txt它不存在于 中HEAD,也不存在于 Git 的索引中。现在我们来看看一个有趣的特例。


3它们为名称、哈希 ID 和 Git 内部使用的一堆缓存数据占用了一些空间。所需的空间量取决于名称长度和索引格式(有多个索引格式编号),但它通常非常小,每个文件大约 100 个字节。


跟踪、未跟踪和忽略的文件

我们在工作树中创建的新文件不在 Git 中。我们可以看到的另外两个文件也不是,但是这两个文件确实在 Git、提交中具有副本并且在索引中准备HEAD了提议的新提交。new.txt不在索引中:它甚至不在提议的下一次提交中。

此时,new.txt就是 Git 所说的未跟踪文件。未跟踪文件被定义为当前不在 Git 索引中的任何文件

例如,假设我们现在运行:

git rm --cached main.py

产生这个:

  HEAD         index      work-tree
---------    ---------    ---------
README.md    README.md    README.md
main.py                   main.py
                          new.txt

我们没有更改提交(我们无法更改其内容),但现在main.py不在Git 的索引中。我们工作树中的main.py已从tracked变为untracked

如果我们git add main.py new.txt现在运行,Git 将:

  • 读取 的内容,压缩它们,发现它是重复的,然后在索引中main.py重新使用旧的;main.py
  • 阅读 的内容new.txt,压缩它们,可能会发现它是新的,并制作一个新的 Git 化副本以准备提交,并将其放入索引中;

现在我们将拥有:

  HEAD         index      work-tree
---------    ---------    ---------
README.md    README.md    README.md
main.py      main.py      main.py
             new.txt      new.txt

现在我们没有未跟踪的文件。

文件的跟踪性是可变的。 我们所要做的就是创建或删除索引条目和/或工作树文件。两者中的那些文件都是跟踪文件;那些仅在工作树中的文件是未跟踪的文件。这就是全部——<strong>差不多。

为什么.gitignore不意味着人们认为的意思

Git 索引中的文件永远不会被忽略

中的条目.gitignore列出了文件名,部分地告诉 Git,如果该文件未被跟踪,请不要抱怨它。git status命令非常有用,因为它会告诉我们索引中的内容和索引中的内容git add,例如我们忘记的内容。这包括工作树中但不在索引中的任何文件。

但是有些东西——比如 Python 2.x——会生成很多不应该提交的工作树文件。如果git status一直抱怨你所有的*.pyc文件,git status可能会变得无法使用:“哦,我忘了git add这件事”的有用信息将被埋在无用​​的“这里有 5000 个*.pyc文件你可以添加”消息中。

为了防止这种情况,我们在一个文件中列出了*.pyc glob 模式。.gitignore这使得git status对这些文件闭嘴。

它还有另一种效果。我们可以运行git add .git add --all类似的方法来做一个集体“添加一切”。当我们使用它时,Git 将跳过任何现有的未跟踪文件,这些文件也列在.gitignore. 但是 Git 只跳过未跟踪的文件,而不是跟踪的文件。被跟踪的文件——那些在 Git 的索引中的文件——得到更新(这实际上通常是人们想要的。)

所以,.gitignore是错误的名字。该文件应命名为.git-do-not-complain-about-these-files-when-they-are-untracked-and-also-if-they-are-untracked-and-I-use-an-en-masse-git-add-command-do-not-add-them-to-the-index-after-all. 但是这个名字很荒谬,几个稍短的版本也是如此。所以它只是被称为.gitignore

这里的关键是它不会忽略索引中的文件。对于要提交的文件,它必须在 Git 的 index中。如果它在提交中并且我们检查了提交,那么该文件现在位于 Git 的 index中。因此,一旦文件被提交,它往往会潜入索引中,即使我们不时将其取出:任何时候我们检查包含该文件的提交它都会回到索引中。然后它的上市.gitignore没有任何影响。

为什么(以及何时)避免git commit -a

什么git commit -a是使用快捷方式。它:

  • git add 为你跑,然后
  • 做这git commit一步。

您还可以执行以下操作:

git commit --only main.py

和:

git commit --include main.py

这些都操作索引,然后进行提交。但是,它们非常棘手:它们使 Git在提交期间制作一两个额外的临时索引文件。这些额外的索引文件可能会混淆预提交挂钩。他们是否、何时以及在多大程度上弄乱了这些钩子取决于几件事,包括钩子编写者编写钩子的仔细程度,他们是否知道这个 Git 技巧,以及你--only是否--include使用git commit. 4

也就是说,git commit -a通常工作正常(见脚注 4)。但大致相当于先跑git add -u,再跑git commit。该-u选项git add将只更新已知文件,不添加新文件。当我们使用git add .集体添加我们更新的所有内容时,这包括添加新文件。无法添加文件的-a选项。git commit

除此之外,git commit -a简直就是懒惰。有时懒惰是一种美德,所以这并不意味着永远不要使用它。但是git status,紧随其后的是仔细git add的操作——甚至可能是git add -p操作——然后是另一个git status,然后是git diff --cached(或git diff --staged——这些做完全相同的事情),将帮助您安排只提交您想要的更改。阅读 diff 可以让您在另一个窗口中编写一个好的提交消息,或者至少做笔记,然后您可以将其粘贴到提交中。这可以让您做出良好、谨慎的提交。为此,您通常必须避免git commit -a. 所以这至少是一个坏习惯,即使懒惰偶尔是一种美德。


4git commit -a内部使用--include模式。这比--only表单的破坏性更小,因为--include只需要两个索引文件,而不是三个,并且提交期间使用的一个是提交成功后成为新索引的那个。额外文件仅用于回滚情况。表单需要三个:--only一个用于提交,一个用于回滚,一个用于成功;这三者都有——至少可能是——不同的内容。


推荐阅读