首页 > 解决方案 > 在 git 中从远程创建功能分支的最佳方法是什么?

问题描述

听起来像个愚蠢的问题,对吧?

git checkout master
git pull
git checkout -b myBranchName

除了我总是忘记 git pull 步骤......当这是一个如此常见的用例时,我想必须有一些 chrome 来使其自动化,对吧?某种方式可以跳过本地“主人”?

因为我今天又做了一次。我切换到master,创建了我的功能分支,完成了一堆工作,然后我发现当我推送时,我发现我的功能分支master以一种可怕的冲突方式落后了。

我总是忘记那git pull一步。所以我遇到了最糟糕的无声失败,一切看起来都很好,直到你发现自己被搞砸了。

有没有办法避免这种情况?一种说“不要相信我的本地master分支,而是回到远程创建我的本地未跟踪分支的方式?或者我只是用“哎呀,你忘了git pull”换“哎呀,你忘了git fetch”如果我这样做?

标签: git

解决方案


根据您的工作流程的其余部分,您可能会从完全删除master分支名称中受益。一旦你这样做了,你就不能那个分支上,因为你没有那个分支。

除此之外,如果您从本地创建了一个新的分支名称master但忘记首先更新您的本地master,则有多种解决方法。要了解它们——以及了解何时以及为何 删除本地master作品——这有助于意识到在 Git 中,分支名称几乎是无关紧要的。他们为我们(和 Git)做了一件重要的事情:他们帮助我们找到提交。但重要的不是分支名称!重要的是提交

正是这一事实——名称让我们可以找到提交——使名称变得重要。因此,只要有其他方法可以找到这些提交,名称就变得不重要了。

为了理解这一点,请注意git log输出。git log --oneline以下是 Git 存储库克隆的输出片段示例:

$ git log --oneline
328c109303 (HEAD -> master, origin/master, origin/HEAD) The eighth batch
8b25dee615 Merge branch 'tb/precompose-prefix-too'
006c5f79be Merge branch 'jk/complete-branch-force-delete'
60f8121940 Merge branch 'jv/upload-pack-filter-spec-quotefix'

注意左边的时髦数字(十六进制数字)。这些是实际的提交 ID,因为完整的数字很长,所以稍微缩短了一些。(此时 Git 的 Git 存储库相当大,因此数字现在缩短为 10 个字符而不是 7 个;较小的存储库使用较短的缩短。)

这些数字看起来是随机的,但不是。它们实际上是对每个提交的内容运行加密散列函数的结果。这样,宇宙中的每一个 Git 都会同意这里列表中的顶部提交获得数字328c10930387d301560f7cbcd3351cc485a13381。任何其他提交,无论何时何地,都不能使用这个数字。1

如上面的输出所示,分支名称的作用是保存该分支上最后一次提交master的哈希 ID 。但另一个名称也可能包含该哈希 ID。在这种情况下,另一个名称确实包含它:该名称包含相同的哈希 ID。特殊之处在于它不是分支名称:例如,不会切换到它。origin/masterorigin/mastergit switch

无论如何,一旦你内化了重要的是数字的想法,并且名称master只是查找数字的便捷方式,我们就准备好进入下一节。


1从技术上讲,其他 Git 存储库可以重复使用此编号,但前提是该其他 Git 存储库永远不会与 Git 的 Git 存储库的克隆联系。它们不会以 Star-Trek-Matter-Antimatter-End-Of-The-Universe 的方式爆炸,但如果另一个 Git 存储库以某种方式将哈希 ID 重新用于其他东西,那就不好了。另请参阅新发现的 SHA-1 冲突如何影响 Git?

这些是 SHA-1 数字。Git 人员正在逐渐转向 SHA-256。我们将如何到达那里仍然是一个悬而未决的问题。目前(Git 2.30),您可以创建一个 SHA-256 存储库并使用它,或者创建一个 SHA-1 存储库并使用它;但是你永远不能让讲 SHA-256 的 Git 与讲 SHA-1 的 Git 对话。您甚至无法将旧数据库升级为新数据库,也无法将新数据库降级为旧数据库。这不是一个非常可接受的情况。


分支,在几种意义上

每个提交都有编号:OK。Git 实际上是通过这些哈希 ID 号找到提交及其其他内部对象。好的。但是:那又怎样?

好吧,关于提交,还有几件事需要了解。在不深入探讨的情况下,我们在这里只说它们由两部分组成:所有文件的快照和一些元数据。快照是git checkout检查出来的。元数据是关于提交本身的内容,例如提交者、时间和原因。我们在输出中看到了这个元数据git log

$ git log
commit 328c10930387d301560f7cbcd3351cc485a13381
Author: Junio C Hamano <gitster pobox.com>
Date:   Fri Feb 12 14:13:40 2021 -0800

    The eighth batch
    
    Signed-off-by: Junio C Hamano <gitster pobox.com>
...

这里我们有提交的作者、日期和时间(上周五——我整周都没有更新!),以及 Junio 的日志消息(因为真实信息在所有合并中,所以相当简洁)。您在这里不到的是,提交的元数据为 Git 存储了一些东西,Git 将其称为此提交的级。328c10930387d301560f7cbcd3351cc485a13381is的父级8b25dee6155fd3816f62649da196a4f42cf5584e,Git 在其中修复了与 Unicode 文件名有关的特定于 Mac 的错误。2 (这个提交的父328c109303提交本身就是一个合并提交,因为它不仅有一个,而且有两个父提交,但是在大多数 Git 存储库中,大多数提交不是合并。下面,我没有绘制任何合并提交。)

这种存储前一个提交的哈希 ID 的技巧意味着在 Git 中,一个提交“向后指向”前一个提交,然后再次向后指向,再向前一步。这一遍又一遍地重复,将提交串成一个长长的向后看的链。如果我们画出这样一条链,用大写字母等符号替换真正的提交哈希 ID(以便在我们脆弱的人脑中保持理智),我们会得到如下图:

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

H这个链中的最后一个提交在哪里。CommitH指向较早的 commit G,后者又指向F,依此类推。这一直重复,直到我们到达第一个提交(大概A):那个不会向后指向,因为它不能。所以这就是git log停止的地方,如果我们不尽快切断它。

Git 一如既往地通过它们的哈希 ID 找到每个提交。但是 Git 会在哪里找到链中最后一个提交的哈希 ID, commit H?也许我们可以把它写在纸上,或者白板上。但这很愚蠢:我们有一台电脑。如果我们把它保存在一个文件中呢?更好的是,如果我们让Git自动将它保存在某个文件或数据库中的某个地方呢?

这就是分支名称。一个分支名称,例如master,只包含一个哈希 ID。此哈希 ID 自动成为链中的最后一个提交。所以如果我们有这个:

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

那么我们的意思是该名称 master包含 commit 的哈希 ID H,这是该链中的最后一个。即使在之后 H有提交也是如此,如下所示:

...--F--G--H   <-- master
            \
             I--J   <-- feature

在这里,名称 feature保存了 commit 的哈希 ID J,因此feature指向J. J依次向后指向I,向后指向H,向后指向G,依此类推。

提交H同时是链中以 = 结束的最后一个,以及以masterH = 结束的链中间某处featureJ。提交H都在两个分支上。提交I并且J仅在feature.

Git 中的分支这个词是模棱两可的:它既表示一个分支名称,也表示一组由我目前关心的提交结束的提交。导致的提交链H是“一个分支”,并且是“分支主控”,但分支名称 master实际上只是挑选出commit H,Git 可以从中反向工作。导致的提交链J也是“一个分支”,而分支名称 feature选择了 commit J

但是,像and这样的分支名称的特殊之处在于它们会移动。让我们看看这是如何工作的。masterfeature


2这涉及 macOS 获取两个不同文件的方式,例如,名为schönvs schön,并将它们折叠成一个文件,因为不可能看到两个文件名之间的差异,或者实际上在此处发布。一种是使用 Unicode 代码点 U+0308 的 SCHO COMBINING-DIAERESIS N,另一种是使用 Unicode 代码点 U+00F6 的 SCH LATIN-SMALL-LETTER-O-WITH-DIAERESIS N。在 Linux 机器上,您可以使用这两个不同的字节序列制作两个不同的文件。将此提交传输到 Mac 机器后,尝试提取此提交将失败,因为本地文件系统只会创建一个文件。命令行参数必须转换成正确的形式;当前的 Git 版本有点错误。


分支名称如何随着分支的增长而移动

让我们再次从这个开始,虽然没有明显的原因,我会feature在上面而不是下面画,这一次。你如何绘制这些并不重要,只要你记得提交指向backs 即可。Git 绘制这些,以便更新的提交位于顶部,连接位于页面下方。人们有时会在底部绘制更新的提交。内部箭头,从提交到提交,总是向后走: Git 很难向前走,而向后走很容易,所以 Git 通常是向后工作的。例如,这就是git log倒退的原因。我们不仅希望首先看到最新的提交,Git 也是如此。

             I--J   <-- feature
            /
...--F--G--H   <-- master

我们将选择一个分支并“检查”,使用git checkout,或者,如果我们的 Git 是 2.23 或更高版本,则使用git switch. 这两个命令在这里做同样的事情。假设我们运行git switch master(或git checkout master)。这将使master我们当前的分支 name,并进入我们的工作区,来自 commit的所有文件H。让我们通过将特殊名称附加到 name 来绘制HEADmaster

             I--J   <-- feature
            /
...--F--G--H   <-- master (HEAD)

现在让我们创建一个的分支名称,feature2git checkout -b feature2orgit switch -c feature2或 evengit branch feature2; git checkout feature2或任何东西。他们都做同样的事情:使新的分支名称feature2指向 commit H,并附加到HEAD那里,如下所示:

             I--J   <-- feature
            /
...--F--G--H   <-- feature2 (HEAD), master

现在让我们以通常的方式进行新的提交。它获得了一个全新的哈希 ID,但我们K简称它。 K的父母会H,因为我们习惯H了。所以结果看起来像这样: K

             I--J   <-- feature
            /
...--F--G--H   <-- master
            \
             K   <-- feature2 (HEAD)

请注意 Git 如何移动了 name feature2。它通过在提交之后将K哈希 ID 写入名称 来实现这一点。3feature2K

如果我们进行另一个新的提交L,我们会得到:

             I--J   <-- feature
            /
...--F--G--H   <-- master
            \
             K--L   <-- feature2 (HEAD)

请注意我们所附加的name 是如何每次更新的。其他名称没有改变,并且现有的提交也没有改变——无论如何都不能改变,因为它们是用哈希 ID 方案编号的——但是当我们一个接一个地添加新的提交时,当前的分支名称会移动feature2HEAD

如果我们做了一个错误的提交并想摆脱它,我们可以使用git reset. 这个命令相当复杂,但在我们用来摆脱错误提交的模式下,它的作用很容易绘制。它有点将提交推到一边(不更改它),并使我们当前的名称指向其他提交,如下所示:

             I--J   <-- feature
            /
...--F--G--H   <-- master
            \
             K   <-- feature2 (HEAD)
              \
               L   ???

提交L仍然存在于 Git 数据库中。但是现在,没有名称可以找到它。4 提交似乎消失了。但是,如果您将其哈希 ID 保存在某处,则可以使用另一个git reset来取回它:

             I--J   <-- feature
            /
...--F--G--H   <-- master
            \
             K
              \
               L   <-- feature2 (HEAD)

reset命令可以将当前名称移动到任何 地方(到任何现有提交),因此它非常强大。

(一旦我们放回链L中的最后一个提交,就没有理由再将提交单独绘制在一行上。我保留了额外的一行,以表明我们只是在移动名称。)


3的哈希 IDK包含所有元数据,包括 Git 生成的确切时间K、父提交的哈希 ID H、您的日志消息等。这意味着没有人——不是你,不是 Git,没有人——在实际提交之前不知道哈希 ID是什么。

4为确保在您决定要找回它时可以再次找到它,Git 保留了多个隐藏名称。这些最终会过期,通常在 30 天或更长时间后过期;一旦它们过期,并且提交确实无法找到,git gc将“垃圾收集”它,将它真正扔掉。


这一切与你原来的问题有什么关系

现在让我们回到你原来的问题:

git checkout master
git pull
git checkout -b myBranchName

除了我总是忘记git pull步骤...

我将在这里快速进行:git pull意味着run git fetch,然后运行第二个 Git 命令来处理我刚刚获取的任何提交。 通常的第二件事是将这些新提交添加到master. 所以最终的结果是,如果你做你打算做的事情,你会开始:

...--G--H   <-- master (HEAD), origin/master

git pull运行git fetch,添加一个或两个(或多个)新提交并更新非分支名称origin/master,以找到最后一个:

...--G--H   <-- master (HEAD)
         \
          I--J   <-- origin/master

第二命令最终将名称 master向前移动,几乎就像git reset(但更安全: usinggit reset告诉 Git清除未保存的工作,而您仔细配置的第二个命令会保留未保存的工作,或者如果它会破坏它则停止)。5 结果是:

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

现在,当您创建一个新名称myBranchNamefeature2其他任何内容时,6这个新名称指向 commit J

...--G--H--I--J   <-- master, myBranchName (HEAD), origin/master

如果您忘记了该pull步骤,则新创建的分支名称指向提交H(而且您甚至还没有提交I-J):

...--G--H   <-- master, myBranchName (HEAD), origin/master

稍后,您将运行git fetch(可能作为 a 的一部分)git pull并获取新的提交。到那时,您可能已经做出了自己的新提交K

          I--J   <-- origin/master
         /
...--G--H   <-- master
         \
          K   <-- myBranchName (HEAD)

如果你现在git checkout master让 Git “向前滑动分支名称”(就像git pull这样做的那样),你有:

          I--J   <-- master (HEAD), origin/master
         /
...--G--H
         \
          K   <-- myBranchName

您现有的提交K指向旧的提交H这本身并不是一个错误。让你的提交和你的分支从旧的提交中继承并 没有错。您可以继续工作,然后最终用于git merge组合工作。假设您切换回您的分支并进行一次新的提交L

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

您现在可以切换回您的工作mastergit merge将您的更改K-L(从 commit 看到H)与他们的更改I-J(从 commit 看到)结合起来H,并获得一个新的合并提交 M

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

如果您查看各种 Git 存储库,包括 Git 的 Git 存储库,您有时会看到这种工作流程。它没有任何问题。但有些人不喜欢它,无论出于何种原因。如果您是这些人中的一员,或者与他们一起工作,您可以简单地将您的两个提交——<code>K 和L——复制新的和改进的提交中。

无需深入了解所有机制,git rebase将为您执行此操作。您甚至不必更新您的master. 假设你处于这个位置:

          I--J   <-- origin/master
         /
...--G--H   <-- master
         \
          K--L   <-- myBranchName (HEAD)

您现在可以运行:

git checkout myBranchName
git rebase origin/master

Git 会将提交复制K到一个新的和改进的(?)提交,我们将调用它K'来暗示复制操作,在J. 然后 Git 会将提交复制到 .之后L的新改进(?)提交。制作完这两个副本后,Git 将从旧链的旧端剥离名称,并将其粘贴到新链的新端,如下所示:L'K'myBranchName

               K'-L'  <-- myBranchName (HEAD)
              /
          I--J   <-- origin/master
         /
...--G--H   <-- master
         \
          K--L   ???

提交KL似乎消失了,但提交K'L'看起来非常像原始提交K,而且L- 除了大而丑陋的哈希数,但你还记得上面的任何一个吗?我不知道——它看起来好像 Git 以某种方式神奇地改变了提交。

您现在也可以简单地完全删除该名称master,因为这origin/master是您关心的:它在每个git fetch. 如果我们这样做,并放弃无法找到的提交并理顺所有可以解决的问题,我们会得到:

...--G--H--I--J   <-- origin/master
               \
                K'-L'   <-- myBranchName (HEAD)

这看起来好像你没有忘记git pull早些时候。

请注意,如果您还没有进行任何提交,即,如果您有:

...--G--H   <-- master, myBranchName (HEAD), origin/master

当您记得运行git fetch您忘记的内容时,您现在可以master完全删除(可选)并运行git fetch(强制)以获得:

...--G--H   <-- myBranchName (HEAD)
         \
          I--J   <-- origin/master

如果你现在运行git rebase origin/master,你的 Git 将悄悄地复制任何提交,然后移动名称myBranchName

...--G--H--I--J   <-- myBranchName (HEAD), origin/master

所以git rebase适用于这两种情况。这很方便。


5在真正古老的 Git 版本中,大约 1.5 左右,git pull有时会意外破坏未保存的工作。我不止一次被这样烧!我避免 git pull使用 ,部分原因是因为我喜欢在将要运行的两个命令之间插入命令。git pull

6如果您使用 Windows 或 macOS,我建议避免使用混合大小写名称。 Git认为分支名称中的这种情况很重要。您的操作系统认为文件名中的大小写无关紧要。Git 有时将分支名称存储在文件中,有时将它们存储为文件名,这意味着大小写有时重要,有时不重要。经历令人困惑和痛苦。避免混淆大小写,你就避免了痛苦。


推荐阅读