git - 在 git 中从远程创建功能分支的最佳方法是什么?
问题描述
听起来像个愚蠢的问题,对吧?
git checkout master
git pull
git checkout -b myBranchName
除了我总是忘记 git pull 步骤......当这是一个如此常见的用例时,我想必须有一些 chrome 来使其自动化,对吧?某种方式可以跳过本地“主人”?
因为我今天又做了一次。我切换到master
,创建了我的功能分支,完成了一堆工作,然后我发现当我推送时,我发现我的功能分支master
以一种可怕的冲突方式落后了。
我总是忘记那git pull
一步。所以我遇到了最糟糕的无声失败,一切看起来都很好,直到你发现自己被搞砸了。
有没有办法避免这种情况?一种说“不要相信我的本地master
分支,而是回到远程创建我的本地未跟踪分支的方式?或者我只是用“哎呀,你忘了git pull
”换“哎呀,你忘了git fetch
”如果我这样做?
解决方案
根据您的工作流程的其余部分,您可能会从完全删除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/master
origin/master
git 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 将其称为此提交的父级。328c10930387d301560f7cbcd3351cc485a13381
is的父级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
同时是链中以 = 结束的最后一个,以及以master
H
= 结束的链中间某处feature
J
。提交H
都在两个分支上。提交I
并且J
仅在feature
.
Git 中的分支这个词是模棱两可的:它既表示一个分支名称,也表示一组由我目前关心的提交结束的提交。导致的提交链H
是“一个分支”,并且是“分支主控”,但分支名称 master
实际上只是挑选出commit H
,Git 可以从中反向工作。导致的提交链J
也是“一个分支”,而分支名称 feature
选择了 commit J
。
但是,像and这样的分支名称的特殊之处在于它们会移动。让我们看看这是如何工作的。master
feature
2这涉及 macOS 获取两个不同文件的方式,例如,名为schön
vs 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 来绘制HEAD
它master
:
I--J <-- feature
/
...--F--G--H <-- master (HEAD)
现在让我们创建一个新的分支名称,feature2
用git checkout -b feature2
orgit 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 写入名称 来实现这一点。3feature2
K
如果我们进行另一个新的提交L
,我们会得到:
I--J <-- feature
/
...--F--G--H <-- master
\
K--L <-- feature2 (HEAD)
请注意我们所附加的name 是如何每次更新的。其他名称没有改变,并且现有的提交也没有改变——无论如何都不能改变,因为它们是用哈希 ID 方案编号的——但是当我们一个接一个地添加新的提交时,当前的分支名称会移动。feature2
HEAD
如果我们做了一个错误的提交并想摆脱它,我们可以使用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
现在,当您创建一个新名称myBranchName
或feature2
其他任何内容时,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)
您现在可以切换回您的工作master
,git 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 ???
提交K
并L
似乎消失了,但提交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 有时将分支名称存储在文件中,有时将它们存储为文件名,这意味着大小写有时重要,有时不重要。经历令人困惑和痛苦。避免混淆大小写,你就避免了痛苦。
推荐阅读
- php - PHP循环获取HTML下拉列表中的选项组
- c# - 从一个按钮的“单击”方法访问其他按钮
- ios - 你能用 WINDOWS 构建一个 react-native iOS 应用吗?
- python - 如何使用一个键作为行索引,另一个键作为列索引,将双索引字典转换为 Excel 文件?
- c++ - 在转换中使用仿函数(带/不带构造函数)
- validation - 如何在 aweglue 中添加数据质量检查实用程序。?
- mysql - 使用 MySQL 查询子集 JSON 对象
- java - 为什么转义序列 \b 在 jdoodle 中产生一个点作为输出?
- jmeter - 为什么 Jmeter 不记录启动 Localhost 的应用程序 url?
- python-3.x - bokeh 和 nginx 中的活动端口和应用程序名称列表