[英]Completely update local repo from remote
我有一个小项目,我一直在使用两台不同的计算机进行工作。 我git push
送到 github (遥控器被称为origin
),但有时我已经在一台计算机上工作了一周,然后才回到另一台计算机。 当我回来时,我只想要一个完整的更新。 所有的旧树枝都被剪掉了,所有的新树枝都被拔掉了,等等。
我可以完全删除本地项目,然后git clone
origin
存储库。 感觉很脏,但它不是一个大项目,所以需要几秒钟,并且基本上是用两个命令完成的。
在git
本身中是否有类似的快速简便的方法来执行此操作?
我看到了这个线程和一些类似的线程,但是所有的答案似乎要么使用脚本,要么逐个分支地工作,这比我认为的可能更乏味。
这里要记住的是,“分支”——更具体地说,是分支名称——对 Git 没有真正的意义。 它们对你来说很重要,但就 Git 而言,每个名称只是一种查找特定提交的方法。 什么混帐真正关心的是信息库中的提交。
每个提交都有自己独特的大而丑陋的哈希 ID。 例如,您可以在git log
输出中看到这些 ID。 Git 真正的魅力在于,每个地方的每个Git 存储库都会同意,如果您存储库中的一个特定提交(或他们的一个)具有某个哈希 ID H ,则任何其他地方都不会拥有该哈希 ID。 1因此,当两个 Git 存储库“相遇”时,通过git fetch
或git push
,它们只需要比较哈希 ID 。 您在 GitHub 上的 GitHub 存储库有一些提交,其中一些哈希 ID H列在其名称master
。 你的 Git 调用他们的 Git。 他们的 Git 说:我的名字master
对应于哈希 ID H。你的 Git 检查:我有 H 吗? 如果你的 Git 已经有了H ,那就大功告成了。 如果没有,您的 Git 会要求他们的 Git 发送提交H 。
你自己的 Git 有你自己的名字。 其中之一可能是master
。 它有一些哈希 ID。 你的master
在这里拥有什么哈希 ID 并不重要,对你的 Git 进行fetch
操作的唯一重要的事情是:我有提交 H 吗? 您的 Git始终可以通过原始哈希 ID 直接查找其所有内部 Git 对象。 你要么已经有H ,要么没有。 由于没有Git的任何地方都可以永远使用散列ID H代表什么,但这个承诺,所有的Git所要做的就是检查这一件事。
如果你毕竟没有H ,你的 Git 会让他们的 Git 发送提交H 。 现在,关于每个提交的另一件事是:每个提交记录一些父提交哈希 ID。 提交的父项或父项是“就在”此提交之前的提交(或对于合并,两个或更多提交)。 也就是说,给定一长串提交,一次一个,每个提交都存储前一个提交的哈希 ID,在一个向后指向的链中:
... <-F <-G <-H
所以如果你要得到H ,他们的 Git 现在会为你提供G 。 你的 Git 会问自己:我已经有 G 了吗? 如果没有,你的 Git 说好的,也给我。 如果你有G ,你的 Git 会说不,谢谢,我已经有了。 对于他们拥有而您没有的每一次提交,这都会重复。 最终, git fetch
有一个它必须发送的所有提交的列表,以及这个提交列表将扩展的所有提交。
在这一点上,他们的 Git 打包了他们所需的提交子集——以及相关的快照等等——知道你有哪些提交,因为你的 Git 告诉他们我已经有了那个。 他们的Git可以压缩所有文件和这样的,一切是那样的内提交使用这些信息。 所以你最终通过网络获得的数据集比他们只是向你发送所有内容要小得多。
1从技术上讲,两个不同的提交可以具有相同的哈希 ID,但前提是它们永远不会“相遇”。 也就是说,如果您将 Git 连接到某个其他存储库,并且它有一些带有哈希 ID H 的提交,而您的 Git 具有不同的H ,则两个 Git 都会认为这是同一个提交,并且不会将其发送给另一个。 只要您的 Git 和他们的 Git 从未相遇并尝试交换提交,这不会导致任何问题。 在实践中,直到你有超过约10个17物体这种散列冲突不会成为甚至远程可能。 在这一点上,它与您的计算机的存储系统随机出现故障的机会相似,这同样是灾难性的。 如果有人小心地设计哈希冲突,这可能是一个问题。 有关详细信息,请参阅新发现的 SHA-1 冲突如何影响 Git?
我们在上面绘制了一个简单的提交链,以提交H
结束:
...--F--G--H
(其中字母代表真实的哈希 ID,它们看起来完全随机,但实际上是完全确定的)。 给定最后一次提交H
的 ID,我们只需要 Git 查找H
。 在H
,Git 找到哈希 ID G
,这让它可以查找G
。 在G
,Git 找到哈希 ID F
,这让它可以查找F
,依此类推。 这让 Git 从最后一次提交一直倒退到有史以来的第一次提交。
这甚至适用于存在分支提交结构的情况:
I--J
/
...--G--H
\
K--L
现在有两个最后的提交。 J
是一个结构中的最后一次提交, L
是另一个结构中的最后一次提交。 这两个结构——我们应该称它们为分支吗?——在它们返回H
相遇,然后它们一直共享提交到时间的开始(大概是提交A
)。
在真实的存储库中,我们可能有数千或数百万次提交。 有一大堆旧的哈希 ID。 您或 Git 将如何快速找到最后一次提交? 你可以——在维护命令中,Git确实——列出每个提交,并找出哪些是“最后”的。 这需要一段时间:几秒钟,或者在非常大的存储库中,有时几分钟。 这显然太慢了。 另外,谁想使用哈希 ID? 人类当然不会。
因此,Git 为我们提供了使用名称来记住一 (1) 个哈希 ID 的能力。 我们可以选择名称branch1
来记住哈希 ID J
,选择名称branch2
来记住哈希 ID L
:
I--J <-- branch1
/
...--G--H
\
K--L <-- branch2
如果需要,我们可以使用名称master
来记住哈希 ID H
:
I--J <-- branch1
/
...--G--H <-- master
\
K--L <-- branch2
在H
之后有提交并不重要。 H
是master
的最后一次提交。 这就是分支的完整定义。 就是这样:分支名称只是一个指针,在 Git 中; 这只是保存一个哈希 ID 的一种方式,根据定义,无论名称包含什么哈希 ID,都是该分支中的最后一次提交。 2
因此,分支branch1
在提交J
处结束,并自动包含您可以通过从J
开始并向后工作而获得的每个提交。 科branch2
端在提交L
,包括之前的所有提交L
,使用Git再次合作倒退。 Git总是向后工作。 如果它由于某种原因需要向前工作,它首先向后工作并记住哈希ID,然后向前遍历记住的列表。 而且,提交可以并且经常是在多个分支上。
当您的 Git 从他们的 Git 获得新的提交时,您的 Git 需要设置一些名称来记住他们的Git 在其分支名称中的哈希 ID。 但是他们的 Git 会在git fetch
开始时告诉你这些东西。 你运行git fetch origin
,Git 在 origin 上——在名称origin
下列出的 URL 上——说:我的master
持有 H,我的develop
持有 L,...。 你的 Git 刚刚得到了整个列表。
然后,当 fetch 运行时,您的 Git 会选择他们拥有但您没有的任何提交,并让他们的 Git 将它们发送过来。 这会向您的存储库添加新的提交,而不会删除任何提交 - 从字面上看,实际上无法删除任何提交,因为它们只会向您发送新的(对您来说是新的)东西。 当所有这些都完成后,你肯定有这些提交。
所以现在你的 Git 获取他们所有的分支名称,并重命名这些名称。 你的 Git 将他们的master
变成了你的origin/master
。 你的 Git 将他们的develop
变成你的origin/develop
。 这适用于他们所有的分支名称。 这些是 Git 的远程跟踪名称,因为它们会记住(“跟踪”)您的 Git 在其 Git 中看到的分支名称和哈希 ID,位于您的远程名称origin
。
所以,假设你在运行一个新的git fetch
之前有这个:
...--G--H <-- master, origin/master
您和他们都只有一个分支,名为master
,并且这两个名称都标识了 commit H
。 然后你运行git fetch
。 他们的master
现在指向新的提交J
,并且他们有一个分支名称develop
指向提交L
:
I--J <-- origin/master
/
...--G--H <-- master
\
K--L <-- origin/develop
你不必做任何事情,但如果你愿意,你可以让你的 Git移动你的名字master
指向提交J
。 有很多方法可以做到这一点,但通常,最简单的方法是在必要时首先使用git checkout master
。 3这将特殊名称HEAD
附加到名称master
,以便 Git 知道将新哈希 ID 写入当前分支的操作使用哪个分支名称:
I--J <-- origin/master
/
...--G--H <-- master (HEAD)
\
K--L <-- origin/develop
git checkout
操作还会安排您的索引(又名staging area )和工作树(又名工作树),让您查看和/或处理由分支名称标识的提交。 也就是说, H
现在是您当前的提交,而master
是您当前的分支。 我们不会在这里详细介绍索引和工作树,但它们非常重要:它们是您构建下一次提交的地方,以及您如何处理文件,Git 将这些文件存储在一个特殊的提交中,阅读-only,仅限 Git 格式。
无论如何,既然你处于这种特殊情况,你可以告诉 Git 做一个快进的非真正合并的“合并”操作,让你的master
赶上他们的origin/master
:
git merge --ff-only origin/master
这需要您当前的分支名称 - master
,来自我们刚刚在必要时执行的结帐 - 并在可能的情况下执行快进操作。 如果它不能,它不会进行合并,它只是说它不能快进,然后退出。 从这里开始,它可以快进而不是合并,它这样做:
I--J <-- master (HEAD), origin/master
/
...--G--H
\
K--L <-- origin/develop
您现在已经提交了J
,并且可以在您的工作树中查看(并使用)它的文件。 您的名称master
现在标识与其名称origin/master
相同的提交,并且您仍然拥有他们拥有的所有提交,并且您的 Git 对于其远程跟踪名称仍然拥有其分支的名称。
2要向分支添加提交,请使用 Git 执行此操作:
git checkout branch1
。现在分支名称标识了分支中的最后一次提交,就像在您进行新提交之前一样。 新提交是分支中的最后一次提交!
如图:
...--G--H <-- branch (HEAD)
变成:
...--G--H <-- branch (HEAD)
\
I
片刻,但随后 Git 立即将I
的哈希 ID 写入名称branch
。 Git 知道branch
是正确的名称,因为特殊名称HEAD
附加到名称branch
。 所以现在我们有:
...--G--H
\
I <-- branch (HEAD)
没有理由不把它们都画成一条直线。
3在 Git 2.23 及更高版本中,您可以使用git switch
而不是git checkout
。 这样做的原因是git checkout
作为一个命令,它可以做太多不同的工作。 所以在 Git 2.23 中,它被分成两个单独的命令: git switch
做一半的工作,而git restore
做另一半。 如果你有一个旧的 Git,或者习惯了旧的做事方式,旧的git checkout
命令仍然像往常一样工作。
请注意,如果他们删除了分支名称,您的 Git 仍会保留您对他们名称的记忆。 也就是说,假设他们认为提交KL
毫无价值,并且完全放弃他们的develop
名称。 您的存储库中有这个:
...--G--H--I--J <-- master (HEAD), origin/master
\
K--L <-- origin/develop
然后运行git fetch
并让 Git 调用origin
处的 Git。 他们列出了他们的master
识别提交J
的事实,这就是他们的分支。 你的 Git 说啊,我已经提交了J
并且他们没有向你发送提交并且两个 Git 断开连接。 你的 Git 会更新你的origin/master
,将它从指向J
更改为指向J
,这不会改变它,所以这里什么也没发生。 然后你的 Git 完成了,你的origin/develop
仍然记得提交L
,即使他们不再有develop
了。
如果你不想要这个——如果你想摆脱你的origin/develop
——你只需告诉你的 Git 在它获取时进行修剪。 由于您的 Git 获得了所有分支的完整列表,您的 Git 可以看到他们不再有develop
。 所以你的 Git 现在将删除你的origin/develop
,给你留下:
...--G--H--I--J <-- master (HEAD), origin/master
\
K--L [abandoned]
要进行此修剪,请运行git fetch --prune
。 要使所有git fetch
操作在可能时自动修剪, fetch.prune
配置为true
:
git config --global fetch.prune true
例如。
请注意,提交仍然存在,至少有一段时间。 没有名字可以找到它们,你的 Git 最终会删除它们。 4废弃提交的删除过程实际上是由一个维护命令git gc
,你可以运行它,但它需要很长时间:几秒钟,甚至几分钟。 Git 在后台自动为您运行它,当 Git 似乎是一个可能有利可图的冒险时,所以几乎没有任何理由自己运行它。
4当您放弃自己的提交时,您的 Git 往往会在一个或多个reflog 条目中记住它们的哈希 ID 至少 30 天。 这使被放弃的提交至少存活 30 天,以防你想要它们回来。 但是,在这种情况下,不再有 reflog 条目,所以这个“最终”是在下一个git gc
实际运行时。 不过,很难预测什么时候会发生。
回顾我们的图表,他们有两个分支名称,我们有一个:
...--G--H--I--J <-- master (HEAD), origin/master
\
K--L <-- origin/develop
我们不需要在这里develop
自己的分支名称。 如果我们想在最后添加提交,我们只需要一个。 我们可以做一个:
...--G--H--I--J <-- master, origin/master
\
K--L <-- develop (HEAD), origin/develop
然后进行新的提交:
...--G--H--I--J <-- master, origin/master
\
K--L <-- origin/develop
\
M--N <-- develop (HEAD)
现在,我们需要发送我们的新提交给他们,因为我们使用git push
。 这很像git fetch
:我们通过哈希 ID 向他们提供他们没有的提交。 但它以不同的方式结束。 向他们发送了我们的提交MN
,然后我们要求他们将他们的分支名称develop
指向提交N
。 如果他们接受,我们会更新我们自己的origin/develop
:
...--G--H--I--J <-- master, origin/master
\
K--L
\
M--N <-- develop (HEAD), origin/develop
Commit L
不再有任何名称指向它,因此我们可以理顺绘图中的扭结。 但是我们也可以切换回我们的名字master
并删除我们的develop
:
...--G--H--I--J <-- master (HEAD), origin/master
\
K--L--M--N <-- origin/develop
提交都还在那里。 我们使用名称origin/develop
找到它们。 没有理由再使用“ develop
这个名称找到它们了。 所以一旦我们用完它,我们就停止使用它并删除它。 然后,如果他们添加更多提交并且我们git fetch
,我们拥有的唯一名称会自动更新:
...--G--H--I--J <-- master (HEAD), origin/master
\
K--L--M--N--O <-- origin/develop
如果我们发现我们需要更多的提交补充,我们git checkout develop
再次创造我们的名字develop
从我们的origin/develop
:
...--G--H--I--J <-- master, origin/master
\
K--L--M--N--O <-- develop (HEAD), origin/develop
我们准备添加新的提交,然后像往常一样git push
。
如果我们要添加新的提交,我们只需要我们自己的名字。 否则,他们的名字——我们的远程跟踪名字——就足够了。 我们只是使用这些,我们就完成了。
我们甚至可以使用 Git 的分离 HEAD模式查看他们的提交。 假设我们已经推送了O
并删除了我们的develop
以便我们有:
...--G--H--I--J <-- master (HEAD), origin/master
\
K--L--M--N--O <-- origin/develop
现在他们添加了一个新的提交P
。 我们使用git fetch
来获取它:
...--G--H--I--J <-- master (HEAD), origin/master
\
K--L--M--N--O--P <-- origin/develop
我们现在可以git checkout origin/develop
。 由于origin/develop
不是分支名称——它是一个远程跟踪名称——我们的 Git 将使用其分离的 HEAD模式。 在这种模式下,特殊名称HEAD
只保存我们正在浏览的提交的原始哈希 ID:
...--G--H--I--J <-- master, origin/master
\
K--L--M--N--O--P <-- HEAD, origin/develop
如果我们在这里进行新的提交Q
,名称HEAD
前进以指向我们的新提交:
...--G--H--I--J <-- master, origin/master
\
K--L--M--N--O--P <-- origin/develop
\
Q <-- HEAD
现在我们真的应该创建一个分支名称来记住Q
的哈希 ID,因为如果我们离开这个提交(比如回到P
或J
),我们将忘记哈希 ID。 谁能记得那些事? 好吧, Git可以记住它们。 我们只需要创建一个name 。 这就是分支名称的用途:记住最后一次提交。 如果Q
是最后一次提交,我们为它取一个新名称。 我们可以随意称呼它:
git checkout -b feature
现在我们有:
...--G--H--I--J <-- master, origin/master
\
K--L--M--N--O--P <-- origin/develop
\
Q <-- feature (HEAD)
git checkout -b
操作创建我们选择的名称,并将HEAD
附加到名称。 我们选择的提交是我们使用的提交: HEAD
过去直接指向的那个提交。 现在HEAD
附加到新名称feature
,并且名称(分支名称)指向提交。
通常,您首先创建指向P
的名称,然后提交使Q
。 但是如果你忘记了,这就是你恢复的方式: git status
说detached HEAD你对自己说,哎呀,我现在应该创建一个分支名称。 您运行checkout -b
,或在 Git 2.23 及更高版本中运行git switch -c
来执行此操作。
您的分支名称用于记住上次提交的哈希 ID。 在需要时创建它们。 否则,不要打扰名字。 使用prune选项剪掉死的origin/*
名称。
您的 Git 想要使用至少一个名称,因此您可以让它这样做:例如,让它使用master
。 然后在git fetch
之后做一个快进。 如果您从未真正在存储库中完成自己的工作,那么您只需坚持使用master
并让git merge --ff-only origin/master
带您到他们的更新。
或者,您甚至可以使用 detached-HEAD 模式: git checkout origin/master
,然后删除名称master
。 你实际上并不需要它。 分离的HEAD
名称加上远程跟踪名称将起作用。 在git fetch
更新您的origin/master
,您可以再次git checkout origin/master
移动分离的 HEAD。 这可能会让一些 Git 用户感到惊讶,所以如果你确实使用了这种方法,而其他人想要接管这个 Git 存储库,你可以警告他们——但你的 Git 存储库是给你的,而不是给他们的。
因此,如果我正确理解您的问题,您可以使用:
git fetch [remote]
从远程获取更改但不更新跟踪分支; 或者
git fetch --prune [remote]
删除从远程存储库中删除的引用。
还要看:
git pull [remote]
从远程获取更改并将当前分支与其上游合并。
您所描述的是我处理所有项目的方式。 [好吧,这并不完全正确,所以请继续阅读。]
在这种情况下,我并没有真正与自己“合作”。 在任何给定时间只有一台计算机负责,通常(如您所说)一次持续数天; 然后我切换回另一台计算机。 潜在的现实通常是我在旅行。 在旅行之前,我将“精通”切换到笔记本电脑; 当我回到家时,我将“掌握”切换回台式计算机。
在这种安排中,我仅使用 github 作为中介; 那里的仓库是私有的(在 github 允许免费私有仓库之前,我为此使用了 bitbucket)。 好吧,不仅仅是; 如果我或我的电脑“被公共汽车撞到”,有一个异地遥控器也很好。
所以我会说:是的,做你所描述的。
现在,至于隐含的问题
我只想要一个完整的更新。 所有旧树枝都被修剪掉,所有新树枝都被拔掉,等等。
...将所有分支在一行中推送到远程的方法只是git push --all
,但至于拉取,不,不完全是单行版本 - 至少,不是我的怀疑你的意思。 即使制作一个新的克隆也不是单行版本。 当您执行克隆或全部拉取时,您确实获得了整个存储库,包括所有远程分支; 但是不会自动创建远程分支对应的本地分支。 这就是为什么有这样的 Stack Overflow 问题和答案的原因:
...和这个:
“git pull --all”可以更新我所有的本地分支吗?
因此,如果您乐于做这些问题的答案中推荐的那种事情,那么您的 O(1) 更新就是如此。
脚注:回想一下我说过“那不是真的”。 我有另一种工作方式。 以另一种方式,我在两台计算机之间同步工作树文件夹,使用Finder或rsync
(我使用 Mac)作为中介。 我仍然使用 GitHub 作为异地备份,但我只是通过同步将精通从一台计算机转移到另一台计算机。 事实上,我可以在传输时使用 Finder 副本,但我大部分时间都使用同步软件。 对此没有任何困难,因为 git repo 只是一堆文件夹和文件,就像其他任何文件一样:它可以很好地从一台计算机同步/复制到另一台计算机。 这样,您就可以获得所有本地分支,因为整个本地git 只是从一台计算机复制到另一台计算机。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.