Git rebase会影响远程分支还是本地分支?

11

我正在处理名为“role”的分支。远程主分支经常被更新,因此在将我的更改提交到远程分支后,我需要将该分支与最新的主分支合并。下面是我的步骤:

# from local
git add .
git commit 'Add feature'
git push origin role

# rebase
git pull --rebase origin master

rebase只影响本地分支还是远程分支或两者都有影响?如果仅重新命名本地分支,是否应在重新命名后提交到源?


你尝试过运行 git status 吗? - jonrsharpe
@jonrsharpe,我测试时,主分支自上次与我的分支合并以来没有更改,因此rebase没有做任何事情。只是想了解这个过程。 - ddd
那么这不就回答了你的问题吗?不,本地变基不会改变远程。 - jonrsharpe
@jonrsharpe 所以基本上,在变基之后我必须再次推送到远程分支? - ddd
我知道这与主题无关,但是你在提交行中缺少“-m”。 - Ariel Voskov
我认为正确的做法是先在本地分支上执行 git commit,然后再执行 git pull --rebase origin master。接着将本地分支推送到远程仓库 git push origin role。这样做对吗? - ddd
1个回答

62

总结:是的,但你的问题不太正确。 :-)

这里有很多要注意的东西。首先,Git术语并不是很好理解——“远程”,“跟踪”,“分支”和“远程跟踪分支”都有非常不同的含义!—而你又使用了另一组术语,这使事情变得更加棘手。因此,在回答我认为你想问的问题之前,让我们定义一下这些术语。(另请参见gitglossary,尽管其中并不包含所有这些术语。)

  • 远程仓库是一个短名称,如origin。它主要用作URL的占位符,这样您就不必一直输入一些长URL,并且还构成了远程跟踪分支的名称(稍后我们将定义)。

  • 分支是一个模糊的术语:请参见 我们所说的“分支”到底是什么意思?。在这种情况下,我们首先使用它来表示像master这样的分支名称。在Git中,分支名称只是特定提交的名称,我们称之为该分支的tip,但是像master这样的本地(普通)分支名称具有特殊属性,我们很快就会介绍。

  • 远程跟踪分支是您的Git根据您的Git调用其他Git时看到的内容为您分配和更新的名称。这些是像origin/master这样的名称。这些远程跟踪分支名称没有普通(本地)分支名称的特殊属性。

  • 跟踪是Git用作缩写词的可怕动词,表示更现代和冗长的方式来表达同样的意思。可以设置本地分支“跟踪”远程跟踪分支。更现代的方法是将远程跟踪分支设置为其上游的本地分支。设置上游不会做任何您无法“手动”完成的事情,但使Git自动为您执行此操作非常方便。

    通常,我们会将远程跟踪分支origin/master设置为本地分支master的上游。同样,我们会将origin/role设置为本地分支role的上游。此时记住,本地分支和远程跟踪分支都在您自己的 Git中。它们实际上都是本地的!远程跟踪分支之所以被称为“远程跟踪”,只是因为它们是自动更新的

接下来,请记住,当您使用git fetchgit push(以及git pull,但请参见结尾的旁注)时,您的Git会调用另一个Git。另一个Git有自己独立的仓库。您的Git获取其他Git的方式是通过某个URL,并且该URL存储在“remote”名称下,例如origin:您的Git通过Internet-phone呼叫origin的Git并与其他Git通信。您的Git从它们那里获取东西(git fetch)或将东西发送给它们(git push),您的Git和它们的Git都完全在本地工作,您的Git在您的仓库上工作,它们的Git在他们的仓库上工作。

一切都是本地的

这就是为什么在 Git 中,至少初步看来,“一切都是本地的”。实际上并不存在真正的远程实体,只有本地实体以及通过这些特殊的互联网电话呼叫进行本地工作时多个 Git 交换数据的情况。幸运的是,由于 fetch 和 push 是主要的联系点,所以很容易保持清晰:在 fetch 或 push 之前一切都是本地的。这些是你的 Git 和另一个 Git 传输数据并进行(本地!)更改的点。
因此,现在我们可以回答你的问题:
引用:

rebase 只影响本地分支还是远程分支还是两者都有影响?

这个很简单:如果你没有推送,那么你只能影响本地事物。
引用:

如果只有本地分支被 rebase,是否应该在 rebase 后再次提交到 origin?

这段文字有一个嵌入式的误解。当你进行提交时,你仍然只影响本地事物。要将本地内容发送到其他地方,需要将它们推送(push)出去(或者,如果您的计算机可以充当服务器,则让您的计算机回答别人的git fetch电话——但让我们不要谈论这个,至少现在不要!)。
这里有一个很大的复杂性,因为 git fetchgit push 不是对称的。

推送和获取不对称

当你运行git fetch时,你的Git会调用他们的Git并从他们那里获取新的内容。你的Git将新的内容(提交和其他需要这些提交的内容)保存在你的存储库中,然后更新你的远程跟踪分支名称。这些名称与你自己的本地分支名称完全不同:你的origin/master与你的master不同,你的origin/role也与你的role不同。你的origin/*分支名称唯一的作用就是记住你的Git在他们的Git上看到了什么,所以每当你的Git在他们的Git上看到新的内容时,改变它们是非常安全的。
当你运行git push时,你的Git会调用他们的Git,发送你的东西,然后请求他们更改他们的masterrole他们没有ddd/masterddd/role:你的Git要求他们更改他们自己的,唯一的masterrole如果他们在他们的masterrole中有新的内容,而你尚未将其合并到你要推送的内容中,那么你将要求他们放弃这些新内容,取而代之使用你的内容。
现在是介绍本地分支名称的特殊属性,并了解分支名称和提交如何工作,以及了解git rebase实际上是如何工作的。这也是回顾其他问题“分支”确切意思是什么?的好时机。

分支通常如何增长,以及rebase对此的暴力影响

Git主要关注提交。提交是Git的存在意义:它们是永恒不变的快照,记录了您提交的任何内容。没有一次提交可以被永久更改,尽管现有的提交可以被复制到新的(不同的)提交中,方法是提取旧的快照,进行更改 - 无论是什么更改 - 并在某个新位置创建新的快照。但是这里的“位置”是什么意思?这就是提交图表的作用。

每个提交都有一个唯一的ID。这是Git在各种情况下显示的大而丑陋的commit face0ff...数字和字母组合。(实际上,每个Git对象都有一个这样的ID,但现在我们只关心提交对象。)

每个提交都保存了与其快照一起的所有内容,包括您通过git add添加或保留的内容,您的名称、电子邮件地址、日志消息以及 - 这是关键项目 - 上一个提交的ID。

这意味着提交形成了(向后)链:

A <- B <- C   <-- master

分支名称,例如“master”,指向“tip”提交。 tip提交是链中最新的提交。 最近的提交指回它的父(较早的)提交,父提交指向另一个父提交,如此循环。 当我们到达第一个提交时,该链条才会结束,因为第一个提交没有父提交。
由于新提交指回较旧的提交,因此每当我们进行新提交时,我们需要Git“推进”分支名称,以指向我们刚刚创建的新提交。 也就是说,如果我们有如上所示的,并且我们创建了一个新的D,我们需要Git将“master”移动到现在指向D:
A <- B <- C <- D   <-- master

这是本地分支名称的特殊属性。当我们在该分支上进行提交时,它们会自动前进到我们刚刚创建的新提交。
这导致了一个简单的分支命名规则(但很快就被打破了):它们以一种方式前进,以保留所有旧的提交。我们向分支添加一个新提交,新提交指向旧提交,并且我们只是将其添加到分支中。(请注意,我们现在正在谈论一个作为提交集合而不是指向恰好一个提交的名称的分支!)
Rebase故意违反了这个简单的规则。让我们看看创建分支时会发生什么。我们也停止绘制所有内部箭头,只记得它们都指向后面:当我们从较新的提交向较旧的提交倒退时,我们总是向左移动。
A--B--C--D        <-- master
       \
        E--F--G   <-- role

这里总共有七个提交,其中三个仅在role分支上,一个——提交D——仅在master分支上,还有三个——A-B-C链——在两个分支上。(这是Git的另一个奇怪之处:提交通常在许多分支上。)
当我们使用git rebase时,通常是要移动提交,使其直接位于其他分支(新)尖端之后。例如,在这种特殊情况下,我们想将E-F-G链移动到D之后,而不是C之后。但是提交不能更改,只能复制。 因此,这就是git rebase所做的事情:它复制提交。我们将E-F-G复制到新的提交E'-F'-G'中,这些提交与E-F-G非常相似,但它们在图形中的位置不同:它们位于D之后。
A--B--C--D           <-- master
       \  \
        \  E'-F'-G'  <-- role
         \
          E--F--G    [previous role, now abandoned]

Rebase命令首先复制提交,然后将分支名称role从旧的tip提交G上剥离并粘贴到新的副本G'上。这样会失去旧的E-F-G链,而采用新的E'-F'-G'链。新的提交略有不同,如果没有其他不同,那么E'与E不同之处在于E'的父级是D,而不是C,因此具有不同的ID。
因此,当我们推送时必须强制推送。如果我们已经将原始的E-F-G推送到origin,我们就给了它们原始的E-F-G的精确副本。这些副本位于C之后,而不是D之后。现在,我们将再次执行git push origin role,因此我们的Git将呼叫其他Git,并交付E'-F'-G',然后要求它们将其role设置为指向提交G'。

他们会说“不”!

如果他们将自己的role设置为指向G',他们将会失去提交E-F-G。现在,他们的Git找到G的唯一方式是查看(他们的)名称role,如果他们更改role以指向G',他们将失去与G的链接。

当然,这正是我们想让他们做的!但默认情况下,他们会说“不,如果我这样做,我将失去一些提交。”(这个默认值的原因很简单:他们不知道或不关心是我们给了他们G,他们只知道,现在,他们将失去G)。因此,我们必须将我们的Git的礼貌请求“请移动role”改为更有力的命令:“移动role!”

理想情况下,我们甚至可以说:“我们认为您的role名称为G,如果是这样,它应该改为名称G';但是请告诉我们如果我们对您的role有误,好吗?”Git称此为--force-with-lease。它不支持每个Git版本,并且如果您确定只有您更改其role,则不需要它;但是需要某种强制操作,因为您需要让他们的Git放弃旧的(已复制)提交,就像您的Git一样。

另外: git pull

git pull 命令旨在提供便利。每当你运行 git fetch,通常可以从远程获取一堆新的提交,但这些新的提交都会出现在其远程跟踪分支下。一旦你拥有了这些提交,通常希望对它们进行一些操作。你可以使用两种主要方式来处理它们: git mergegit rebase。因此,git pull 就是将 git fetch 与立即执行的 git mergegit rebase 结合起来。
主要问题在于,在获取并检查任何新提交之前,你不一定知道确切想要对它们执行什么操作。使用 git pull 会让你提前决定如何处理它们。想象一下:“我要伸手进这个黑暗的壁橱里,拿出一瓶饮料,无论是什么,我都要喝掉。”如果你拿出一瓶啤酒,那很好;但如果你拿出一瓶漂白剂呢?
在任何情况下,上述所有内容仍适用于git pull,因为它只是git fetch后跟第二个命令。 fetch通过存储在远程名称下的URL调用另一个Git。无论是git merge还是git rebase,第二步都在本地工作。关于git pull特别令人困惑的是,因为它先于远程跟踪分支名称的发明而存在,所以它有自己的特殊语法。这就是为什么即使您从origin获取,然后在origin/master上进行变基,您也要写“pull(作为rebase)origin master”,而不是“pull(然后在)origin/master上进行变基”。

4
这绝对是我在StackOverflow上收到的最长回答。解释得非常好。我希望我能给它超过1分的赞。 - ddd
1
这个答案应该得到更多的投票。谢谢,Torek先生。 - Hasan Ammori
据我所知,之前的角色“放弃”的分支 - 实际上只是那些提交 - 不能被删除,因为它们是不可变的?所以它们永远存在是正常的? - evolross
1
@evolross:它们将一直保留在您的存储库中,直到:(1)没有(正常的)过程,甚至git reflog也无法找到它们;(2)git gc决定,它们已经被遗弃并且足够老了,应该被垃圾回收。Git会定期自动运行git gc来清理垃圾。很难预测这实际上发生的时间,但如果您看不到它们,那么保留它们也没有真正的危害-或者至少是这个理论。 - torek
请注意,git gcgit fsck仍然可以找到它们,因为这是它们的工作:扫描整个存储库,而不考虑对象可达性。两者之间的区别在于,git gc会继续删除剩余的对象,而git fsck会检查正在使用和已死亡的对象的有效性。两个命令都相当慢:如果您在大型存储库上手动运行它们,请准备等待。 - torek
真希望我几年前就读到这篇文章,谢谢你,谢谢!你以一种阅读git文档和数十篇博客所无法做到的方式为我澄清了git。 - pcdev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接