git 加入和替换 x 分支到 master 分支的命令是什么?
What are the git commands to join and replace an x branch to the master branch?
用什么git命令替换,用什么git命令把一个x分支加入master分支?
git merge
命令就是答案。
我试着向你解释命令。
从git merge
的定义开始:"merge"命令用于集成来自另一个分支的更改。
注意:此集成的目标(即接收更改的分支)始终是当前签出的 HEAD branch.While Git 可以自动执行大多数集成,一些更改将导致必须由用户解决的冲突。
现在一些有用的选项:
--no-ff
创建合并提交,即使快进是可能的。
--squash
将所有集成更改合并到一个提交中,而不是将它们保留为单独的提交。
--abort
当发生冲突时,此选项可用于中止合并并恢复项目开始合并前的状态。
现在在你的情况下(如果我理解正确的话)你想将 x 分支合并到主分支。
所以序列可以是这样的:
git checkout master
切换到master分支。
现在,如果您在远程存储库上工作是一个好习惯:
git pull
更新存储库的本地副本。
然后你就可以安全地进行合并了:
git merge xBranch
--squash 选项是合并时的一个好习惯,但没有也可以。
现在您可能需要解决一些冲突。然后您可以使用 git add filename
和 git commit
命令进行简单的提交,最后,如果您的存储库是远程的,您可以执行 git push
。
TL;DR
在命令行中,使用 git merge
,或者 git merge --no-ff
。不过,在您开始之前还有很多事情要了解。
长
如果您从 b运行ches 的角度考虑 Git,您可能走错了路。 Git 与 b运行ches 无关。有时我喜欢说 Git 没有 b运行ches,1 并且如果 b运行ch 你的意思是像 Mercurial 或 Subversion b运行ch,或 b运行ches 之类的东西,几乎来自你熟悉的任何其他版本控制系统,这实际上是真的,因为与其他版本控制系统相比,b运行ches Git 确实很奇怪。
那么,Git 是关于什么的呢?它存储什么?答案是:Git 存储 提交 。提交是Git存储的基本单位。2 当然,提交确实存储文件,所以这就是你获取文件的方式。但是提交是 Git 中的核心人物。您应该从提交的角度考虑 Git。 B运行ches——或者更确切地说,是人们说 b运行ch 时所指的一种东西——是由 by 提交。 b运行ch names,这是人们说b运行ch的另一个意思,只是让你找到 提交。
为了理解所有这些,画一些图很有帮助。
1这样做的目的是为了引起他们的注意。我不确定这实际上是一个好策略。
2这有点像原子是"everyday stuff"物理存在的基本单位。你可以将原子分解成组成部分——电子、质子、中子——但它们本身并不能提供适当的存在。在你拥有铁(26 个质子的原子核,根据需要加上中子和电子)或铜(29 个质子)或氧(8 个质子)之前,你必须将它们组合成原子。 (然后你也必须把原子连接成分子,所以不要把这个比喻推得太重。)
绘制提交图片
在我们画出像样的提交图之前,我们需要了解一些事情:
- 提交的名称是什么?
- 提交中有什么?
任何提交的名称都是其哈希 ID。这个哈希 ID 是一个很大的丑陋的字母和数字字符串,如 083378cc35c4dbcc607e4cdd24a5fca440163d17
、3 是通过创建提交找到的。一旦它被分配给 that commit——无论你或其他人创建了什么提交——hash ID 现在意味着 that commit,并且 仅 提交。没有其他提交可以有那个 ID!4 此外,all Git software 到处都会同意 你的提交,使用那个哈希ID,得到那个哈希ID。
这些哈希 ID 的坏处在于它们又大又丑,人类无法处理它们(除了像 cut-and-paste 之类的东西:如果 Git 向您展示哈希 ID,您可以用鼠标抓住它,并将其粘贴到第二个 Git 命令中)。所以我们没有,而且我们通常 没有。但它们是提交的真实名称。
与此同时,内部 提交分为两部分。其中一部分是 快照: 您的每个文件的完整副本。另一部分是元数据,这是关于提交的信息。元数据包括制作它的人的名字:你,或来自你的 user.name
设置的任何人。它们包括此人的电子邮件地址,来自您的 user.email
设置。它们包括 date-and-time-stamp 的 当 您或任何人进行提交时。它们包括一条提交日志消息,向 future-you 和其他人解释 为什么 您进行了此提交。不过,对于 Git 本身最重要的是,元数据包括提交的 parent 提交的原始哈希 ID。提交的 parent 是此提交 之前 的提交。
如果我们使用单个大写字母代表提交的哈希 ID——提交 A
是第一次提交,B
是第二次提交,依此类推——我们可以绘制像这样的小型 three-commit 存储库:
A <-B <-C
这里 C
是我们所做的最后 提交。在 C
中,Git 存储了早期提交 B
的实际哈希 ID。 Commit C
有我们所有文件的完整快照,以压缩和 read-only 方式存储。它具有通常的元数据:制作者、制作时间和原因,以及 B
的哈希 ID。所以,如果我们能找到提交 C
,我们就可以用它来找到 B
。我们说commit C
指向 B
.
同时,当我们制作 B
时,我们是从提交 A
开始的,所以 B
有所有 f 的完整快照les——大概其中一些与 C
中的不同——以及所有常见的元数据。这一次,提交 B
指向提交 A
,虽然:A
是 B
的 parent。所以从 B
我们可以找到 A
.
Commit A
有点特别。它具有相同的快照和日志消息等,但它具有 no parent。是sui generis. It somehow appeared from nothing, some sort of spontaneous generation。或者,更简单地说,它只是没有 parent。 Git 将这种提交称为 root 提交 并且每个 nonempty 存储库至少有一个(通常只有一个)。
但仅此而已!我们刚刚成功绘制了三个提交的图片。只要我们不关心提交中的 snapshots 或它们的实际哈希 ID,这个简单:
A <-B <-C
事情成功了。 backwards-pointing 箭头是因为这就是 Git 的真正工作方式:它从 结束 开始,然后返回 wards,从提交到提交, 直到你厌倦了寻找并让 Git 停止,或者它到达根提交,它 不能 进一步返回。
因为很难在任何方向画出好的箭头(有一些是各种字体,但它们在某些显示器上显示得很奇怪)我现在想偷懒并停止绘制内部箭头:
A--B--C
只要记住他们指向backwards.
3从技术上讲,它是一个 160-bit-long 的字符串,但我们倾向于将其视为字母和数字。这些哈希值来自 Secure Hash Algorithm 1 或 SHA-1。 Git 人员计划转向另一种算法,并且会有某种 t运行sition 期间,每个提交都会获得 两个 名称,并且会有很多混乱。但是 Git 大多数情况下会自动处理。
4哈希冲突的可能性,这不会偶然发生,但现在是故意设计的,这就是为什么Git 正在从 SHA-1 转向另一种 more-secure 算法。
B运行ch names 只是让我们 find commits
请注意,要使用我们的小型 three-commit 存储库,我们必须首先找到 last 提交。我们可以列出每个提交,找到所有箭头,并将它们组合在一起以确定最后一个。我们也许可以按日期:列出所有提交并找到具有 latest 时间戳的那个。我们可以在这里做很多事情。但是 Git 实际上 所做的 是使用 b运行ch 名称。
请记住,哈希 ID 看起来 运行dom。再加上不同的提交是在不同的计算机上进行的,有时一台计算机中设置了错误的 time,这样日期就搞砸了。从现实生活中添加任意数量的其他混淆因素,并没有明显的方法来 find last 提交。但是,如果我们让 b运行ch name 告诉我们哪个提交是 last,那么,那真的很简单……而且很有用!让我们画这个:
A--B--C <-- master
与指向较早提交的提交一样,我们说 b运行ch 名称 指向 最后一次提交。不仅如此,我们 define 这样的事情: A b运行ch 名称包含我们将声称是最后一次提交的提交的哈希 ID。 现在我们已经完成了,我们可以有多个 "last" 提交,即使提交继续。
也就是说,让我们使用我们的 three-commit 存储库并创建一个 new b运行ch 名称,develop
,并创建 develop
指向现有提交 C
:
A--B--C <-- master, develop
现在让我们添加一个新 提交D
。我们将一些文件写入新提交,输入我们的姓名和电子邮件地址以及当前日期和时间,并设置新提交 D
以指向现有提交 C
:
A--B--C <-- master, develop
\
D
并且,作为这个 git commit
操作的最后一个技巧,我们将 Git 将 D
的实际哈希 ID(无论它是什么)写入 名字develop
:
A--B--C <-- master
\
D <-- develop
名称 develop
已停止指向 C
并开始指向 D
。
让我们创建另一个名称 feature
,它不指向 D
,而是指向 C
:
A--B--C <-- master, feature
\
D <-- develop
这里有点混乱,所以让我们附加一个特殊的名称,HEAD
,正好是这些其他名称中的一个,以记住我们正在使用哪个 名称:
A--B--C <-- master, feature (HEAD)
\
D <-- develop
现在让我们以通常的方式进行新的提交E
。新提交 E
应该指向我们正在使用的 commit,即 C
,因为 feature
指向 C
。所以我们得到:
E <-- feature (HEAD)
/
A--B--C <-- master
\
D <-- develop
如果我们在不更改 b运行ches 的情况下添加另一个新提交 F
,我们将得到:
E--F <-- feature (HEAD)
/
A--B--C <-- master
\
D <-- develop
请注意,每次我们进行 new 提交时,b运行ch name 都会移动 ward。特殊名称HEAD
还在心疼它。 现有 提交中的 None 已完全更改。 new 提交只是指向以前的 b运行ch 提示,现在 b运行ch 提示 is 新提交。
切换b运行ches
如果我们现在 git checkout master
,我们要求 Git 从提交 F
返回 切换到提交 C
再次,像这样:
E--F <-- feature
/
A--B--C <-- master (HEAD)
\
D <-- develop
请注意 commits 的 none 已更改,并且 b运行ch names[=] 的 none 607=] 这次移动了,但是 HEAD
现在附加到 master
,所以提交 C
是当前的。这就是我们所拥有的,我们将在 work-area.
中看到来自 C
的文件
还要注意,提交中的文件,就像提交本身一样,一直被冻结。没有什么也没有人可以改变其中任何一个。它们采用特殊的 read-only、Git-only 压缩和冻结格式。5 因为它们 是 read-only,每个提交存储每个文件的完整副本这一事实很便宜:不同提交中的文件副本与其他提交中的副本匹配,并且由于提交内容被冻结,Git 可以 re-use 较早的副本。我们只会添加 new 提交,并且新提交会自动提交 re-use 现有的冻结文件。他们只存储 new 版本的文件作为副本。6
为了使用 我们的文件,Git 将它们复制出来。当您使用 Git 时,您使用的文件位于 Git 称为您的 work-tree 或 工作树 或任意数量的类似名称。 (Git 也曾使用短语 working directory,但这很容易与 pwd
命令或 print working 混淆directory, prints. 所以 work-tree 是一个更好的名字。)
现在您知道了 git checkout
的作用——或者至少,它的部分作用。 git checkout
命令实际上可以做很多 不同的 事情,可能太多了,所以在 Git 2.23 中 Git 人们添加了一个新命令, git switch
。切换到 b运行ch 意味着 将 HEAD 附加到它,并取出文件以便我可以查看和使用它们。 文件本身来自 tip 提交 的那个 b运行ch,它是哈希 ID 存储在中的 b运行ch(或者更准确地说,b 运行通道名称).
5我喜欢叫他们freeze-dried.
6最终,当 Git packs 其内部 objects。这对正常使用来说都是不可见的:你不直接查看 Git objects,你只是 git checkout
提交和 Git 解冻并重新水化其中的文件,进入一个可用的表格。
哪些提交在哪些 b运行ches 中?
这就是 Git 与 b运行ches 的真正怪异之处。在大多数其他版本控制系统中,如果您在 一些 b运行ch 上进行提交,它 保持 在那个 b运行通道。也就是说,如果您有这样的图表:
I--J <-- feature1
/
...--G--H <-- master
\
K--L <-- feature2
您可以轻松而正确地说 feature1
包含提交 I
和 J
,而 feature2
包含提交 K
和 L
.在一些 not-Git VCS 中,这将是真实的 永远 。在 Git 中,通常 不 正确。此外,导致并包括提交 H
的提交又如何呢?提交 H
是 master
的提示——它是 last 提交 b运行ch,目前——但在 Git, 提交 H
现在在 所有三个 b运行 上。
也就是说,从 feature1
的最后一次提交开始,即 J
,我们可以返回 wards: J
,然后 I
,然后是 H
,然后是 G
,依此类推。所以 b运行ch feature1
包含 所有这些提交,它的 tip 是 J
.
类似地,从 feature2
的顶端开始,即 L
,我们可以返回 wards:L
,然后 K
,然后是 H
——但不是 I
或 J
——然后是 G
,依此类推。所以 b运行ch feature2
包含所有这些提交;它唯一不包含的是 feature1
.
独有的两个
正在合并
Git 的 merge 可能是 Git 获得其主要力量的地方。提交是 Git 存在的 raison d'être,但 git merge
可能是 你 使用 Git 的原因之一。 7
我们在运行git merge
的时候一般是这样的,至少在命令行下是这样的:
git checkout somebranch # or now, git switch somebranch
git merge otherbranch
看来我们要合并 b运行ches。但不是!与 Git 中的许多其他内容一样,b运行ch 操作实际上是基于 commits。让我们再次从这张图开始,但是把名字 master
去掉合身。我们将选择一个特征来附加我们的 HEAD,并合并另一个:
I--J <-- feature1 (HEAD)
/
...--G--H
\
K--L <-- feature2
这由 提交 完成。你 运行 git merge feature2
,所以 Git 找到提交 L
。您 在 feature1
,因此 Git 使用 HEAD
查找当前提交:即 J
。所以 Git 应该合并 J
和 L
。这是我们想要合并的两个 提示 提交。
Git 现在使用 提交图 ——我们一直在这里画的东西——从 J
回到 I
并回到 H
,然后从 L
到 K
再到 H
。沿着两个 b运行ches 返回 wards 后,第一个 on both b运行ches 的提交显然是 H
。不仅如此,这显然也是 最佳 常见提交。
最常见的提交,Git自动找到,是任何git merge
的合并基础手术。 Git 现在需要弄清楚并合并两件事:
- 我们改变了什么?即
H
中的快照与J
中的快照有什么区别?
- 他们改变了什么?即
H
中的快照与L
中的快照有何区别?
要找到这些,Git 运行s,实际上,两个 git diff
命令。 A git diff
提取快照并进行比较。当文件匹配时,git diff
什么都不说。当它们不同时,git diff
比较文件,line-by-line,并找出哪些 行 匹配,哪些不同,并告诉我们差异。
所以,给定:
git diff --find-renames <hash-of-H> <hash-of-J>
Git 会发现 我们 发生了什么变化,从 H
中的快照到 J
中的快照。然后,做第二个 git diff
:
git diff --find-renames <hash-of-H> <hash-of-L>
Git 会发现 他们 改变了什么。
合并过程,或者我喜欢将其称为合并动词,是获取这些差异并将它们合并的过程。我们——或者更确切地说,Git——现在将组合更改应用到来自合并基础的快照。也就是说,在将我们的所有更改与他们的所有更改组合之后,Git 将这些更改应用于 H
中的快照。 保留我们的更改——新快照将匹配我们的J
,到那个程度——但添加他们的更改:新快照将匹配我们的 J
,但随着他们的变化 添加 .
一旦完成所有这些组合,merge-as-a-verb 过程本身就完成了,Git 将进行 合并提交 。合并提交就像任何其他提交一样,除了它记录 both tip commits 作为它的 two parents。换句话说,这个新的提交 M
将记录我们的提交 J
和他们的提交 L
,作为它的两个 parents:
I--J
/ \
...--G--H M <-- feature1 (HEAD)
\ /
K--L <-- feature2
与其他所有提交一样,M
有一个快照。它是通过组合我们的更改和他们的更改并将其应用于合并基础而制成的。与其他所有提交一样,M
有一条日志消息。 Git 将生成一个 not-very-good,由短语 merge b运行ch Y into X 或 merge b运行ch Y 或类似的东西。8 only 这个合并的真正特别之处在于它有两个 parents.9
7好吧,可能还有像 GitHub 这样无处不在的网站。但是这些站点的存在是由 Git 的分布式提交模型和 Git 合并的方式实现的。 Mercurial 也做这一切,以一种对大多数人来说更容易使用的方式,但是随着 Bitbucket 从 mainly-Hg 移动到 mainly-Git,Git 似乎赢得了 VCS war, 至少现在是这样。
8放一个更好的也不错。不过,人们传统上不这样做。如果 X
是文字字符串 master
,Git 会抑制 into X
部分。 git pull
命令为 merge branch Y
部分添加了 URL。 URL 可能随着时间的推移变得无效,and/or b运行ch 名称可能随着时间的推移而改变,这就是为什么这个消息不是很好。
9从技术上讲,合并提交是具有两个 或更多 parent 的任何提交。 Git 支持所谓的 octopus merge,它具有两个以上的 parent。但是,这并没有做任何你不能用普通 two-parent 合并做的事情,所以即使,例如,Mercurial 没有章鱼合并,Mercurial 可以实现 Git 可以做的一切。但这产生了提交的分类方案:root 提交 是 no parent,常规提交是 no parent一个 parent,一个 merge commit 是一个有两个或更多 parent 的提交。除了 parent 的数量——以及由此产生的raph——这些提交都是一样的,真的:它们都只存储元数据和快照。
合并后删除 b运行ch 名称
这里又是after-merge图片:
I--J
/ \
...--G--H M <-- feature1 (HEAD)
\ /
K--L <-- feature2
注意 b运行ch name, feature1
, HEAD
依附的, 以通常的方式移动到指向新提交 M
。但是由于 M
指向 both J
and L
,我们可以使用它来查找提交L
。然后我们可以使用 commit L
来查找 commit K
。我们不再需要 name feature2
来查找提交 L
。所以我们可以删除它:
I--J
/ \
...--G--H M <-- feature1 (HEAD)
\ /
K--L
b运行ch 名称 feature2
现在 消失了 。 feature2
上的提交也在 feature1
上;现在它们 仅 在 feature1
上。 找到一些提交的 b运行ch 名称集会随时间动态变化。 所以 包含的 b运行ch 名称 提交,例如 K
或 L
,可能会更改!
这就是为什么您应该根据 提交 来考虑 Git 存储库。 b运行ch 名称 意义不大。10
10好吧,b运行ch 名字可以有你想要的意思。 Glory! 但与 Humpty Dumpty 一样,如果你强行用词表达 你 想要的意思,而 他们 不想要,你可能会让听众感到困惑我不知道。
并非所有 git merge
命令合并
上图中有一个技巧。我们从这个开始:
I--J <-- feature1 (HEAD)
/
...--G--H
\
K--L <-- feature2
但是如果我们从这个开始呢:
I--J <-- feature1
/
...--G--H <-- master (HEAD)
\
K--L <-- feature2
和运行git merge feature2
?现在我们要求 Git 将 feature2
合并到 master
,即将提交 L
合并到提交 H
。因此,让我们从 b运行ch 技巧开始,然后返回 wards,找到第一个共同提交。
从H
开始,我们先别动。从 L
,让我们退回到 K
,然后再退回到 H
。啊哈!我们找到了共享提交,best 可以从两个 b运行ches.
访问
如果我们现在进行真正的合并,我们将比较 H
中的快照,合并基础,以及 H
中的快照,b运行ch tip master
,看看我们改变了什么。然后我们将 H
中的快照(合并基础)与 L
中的快照进行比较,以查看 它们 发生了什么变化。
将 H
与自身进行比较会发生什么?试试看。在任何 Git 存储库中选择一个提交,使用 git log
获取其哈希 ID,然后 运行 git diff <hash> <hash>
。会发生什么?
答案当然是差异空。没有文件被改变。没有什么可以结合的!这绝对是 gua运行teed,因为合并基础 是 b运行ch tip.
所以,在这种情况下,git merge
需要一个 short-cut,除非你告诉它不要这样做。你可以 运行 git merge --no-ff
到 force 一个真正的合并,这给你这个:
I--J <-- feature1
/
...--G--H------M <-- master (HEAD)
\ /
K--L <-- feature2
也就是说,Git 像往常一样进行新的合并提交,其中两个 parent 是提示提交,H
和 L
。像往常一样,当前的 b运行ch 名称会指向新的提交。
如果您不强制执行它,Git会执行Git所谓的fast-forward合并,产生这个:
I--J <-- feature1
/
...--G--H
\
K--L <-- master (HEAD), feature2
也就是说,Git 不会 进行新的提交 M
。它只是将名称 master
for ward 移动到与内部 backwards-pointing 箭头相反的方向 - 到达提交 L
。现在两个 b运行ch 名称,master
和 feature2
,都标识现有提交 L
.
Git 将执行 fast-forward,而不是实际合并,当两个 b运行ch 提示 的合并基础是 当前 HEAD
提交。你也可以找到Git说没有什么可以合并的情况:
...--o--o <-- branch1
\
o--o <-- branch2 (HEAD)
运行 git merge branch1
产生投诉,但什么都不做,因为 branch1
标识的提交已经包含在 branch2
.
中
结合并不总是顺利
当您使用 git merge
时,它会进行真正的合并——找到一个合并基础和 运行 两个 git diff
等等 –Git 将 合并这些change-sets。通常,Git 可以自己完成此操作,因为您的更改和他们的更改不会重叠。但有时这些变化 确实 重叠,或者至少相互接触 (abut)。在这种情况下,Git 会把它比喻性的双手高高举起说:这太难了!我放弃!帮我斯波克!
一般来说,您在这种情况下所做的是完成合并:找出这些更改的正确组合是什么。这篇文章太长,无法详述所有细节,但是 Git 给您留下了混乱,在索引和您的 work-tree 中,您必须清理它。那你运行git merge --continue
或 git commit
,完成合并过程并进行最终合并提交。
有时,Git 认为一切顺利,但最终合并实际上并不正确。在这种情况下,由您决定如何处理。您可以只做一个后续提交来修复任何错误,或者您可以 "undo" 合并或做其他棘手的事情。11 没有一种方法是完全令人满意的,但最简单的——只需添加一个后续提交,并附上一条很好的日志消息来准确解释发生了什么——我认为这通常是最好的方法。
通常,一切正常......但测试东西是明智的。如果你有一个CI系统,那可能会测试结果。
11例如,您可以使用 git commit --amend
来制作 evil merge。请注意,--amend
实际上并没有更改任何 现有的 提交——它确实不能这样做——但它所做的对于常规提交来说工作正常,但可以产生这些 so-called 邪恶合并,当用于合并提交时。
用什么git命令替换,用什么git命令把一个x分支加入master分支?
git merge
命令就是答案。
我试着向你解释命令。
从git merge
的定义开始:"merge"命令用于集成来自另一个分支的更改。
注意:此集成的目标(即接收更改的分支)始终是当前签出的 HEAD branch.While Git 可以自动执行大多数集成,一些更改将导致必须由用户解决的冲突。
现在一些有用的选项:
--no-ff
创建合并提交,即使快进是可能的。
--squash
将所有集成更改合并到一个提交中,而不是将它们保留为单独的提交。
--abort
当发生冲突时,此选项可用于中止合并并恢复项目开始合并前的状态。
现在在你的情况下(如果我理解正确的话)你想将 x 分支合并到主分支。 所以序列可以是这样的:
git checkout master
切换到master分支。 现在,如果您在远程存储库上工作是一个好习惯:
git pull
更新存储库的本地副本。 然后你就可以安全地进行合并了:
git merge xBranch
--squash 选项是合并时的一个好习惯,但没有也可以。
现在您可能需要解决一些冲突。然后您可以使用 git add filename
和 git commit
命令进行简单的提交,最后,如果您的存储库是远程的,您可以执行 git push
。
TL;DR
在命令行中,使用 git merge
,或者 git merge --no-ff
。不过,在您开始之前还有很多事情要了解。
长
如果您从 b运行ches 的角度考虑 Git,您可能走错了路。 Git 与 b运行ches 无关。有时我喜欢说 Git 没有 b运行ches,1 并且如果 b运行ch 你的意思是像 Mercurial 或 Subversion b运行ch,或 b运行ches 之类的东西,几乎来自你熟悉的任何其他版本控制系统,这实际上是真的,因为与其他版本控制系统相比,b运行ches Git 确实很奇怪。
那么,Git 是关于什么的呢?它存储什么?答案是:Git 存储 提交 。提交是Git存储的基本单位。2 当然,提交确实存储文件,所以这就是你获取文件的方式。但是提交是 Git 中的核心人物。您应该从提交的角度考虑 Git。 B运行ches——或者更确切地说,是人们说 b运行ch 时所指的一种东西——是由 by 提交。 b运行ch names,这是人们说b运行ch的另一个意思,只是让你找到 提交。
为了理解所有这些,画一些图很有帮助。
1这样做的目的是为了引起他们的注意。我不确定这实际上是一个好策略。
2这有点像原子是"everyday stuff"物理存在的基本单位。你可以将原子分解成组成部分——电子、质子、中子——但它们本身并不能提供适当的存在。在你拥有铁(26 个质子的原子核,根据需要加上中子和电子)或铜(29 个质子)或氧(8 个质子)之前,你必须将它们组合成原子。 (然后你也必须把原子连接成分子,所以不要把这个比喻推得太重。)
绘制提交图片
在我们画出像样的提交图之前,我们需要了解一些事情:
- 提交的名称是什么?
- 提交中有什么?
任何提交的名称都是其哈希 ID。这个哈希 ID 是一个很大的丑陋的字母和数字字符串,如 083378cc35c4dbcc607e4cdd24a5fca440163d17
、3 是通过创建提交找到的。一旦它被分配给 that commit——无论你或其他人创建了什么提交——hash ID 现在意味着 that commit,并且 仅 提交。没有其他提交可以有那个 ID!4 此外,all Git software 到处都会同意 你的提交,使用那个哈希ID,得到那个哈希ID。
这些哈希 ID 的坏处在于它们又大又丑,人类无法处理它们(除了像 cut-and-paste 之类的东西:如果 Git 向您展示哈希 ID,您可以用鼠标抓住它,并将其粘贴到第二个 Git 命令中)。所以我们没有,而且我们通常 没有。但它们是提交的真实名称。
与此同时,内部 提交分为两部分。其中一部分是 快照: 您的每个文件的完整副本。另一部分是元数据,这是关于提交的信息。元数据包括制作它的人的名字:你,或来自你的 user.name
设置的任何人。它们包括此人的电子邮件地址,来自您的 user.email
设置。它们包括 date-and-time-stamp 的 当 您或任何人进行提交时。它们包括一条提交日志消息,向 future-you 和其他人解释 为什么 您进行了此提交。不过,对于 Git 本身最重要的是,元数据包括提交的 parent 提交的原始哈希 ID。提交的 parent 是此提交 之前 的提交。
如果我们使用单个大写字母代表提交的哈希 ID——提交 A
是第一次提交,B
是第二次提交,依此类推——我们可以绘制像这样的小型 three-commit 存储库:
A <-B <-C
这里 C
是我们所做的最后 提交。在 C
中,Git 存储了早期提交 B
的实际哈希 ID。 Commit C
有我们所有文件的完整快照,以压缩和 read-only 方式存储。它具有通常的元数据:制作者、制作时间和原因,以及 B
的哈希 ID。所以,如果我们能找到提交 C
,我们就可以用它来找到 B
。我们说commit C
指向 B
.
同时,当我们制作 B
时,我们是从提交 A
开始的,所以 B
有所有 f 的完整快照les——大概其中一些与 C
中的不同——以及所有常见的元数据。这一次,提交 B
指向提交 A
,虽然:A
是 B
的 parent。所以从 B
我们可以找到 A
.
Commit A
有点特别。它具有相同的快照和日志消息等,但它具有 no parent。是sui generis. It somehow appeared from nothing, some sort of spontaneous generation。或者,更简单地说,它只是没有 parent。 Git 将这种提交称为 root 提交 并且每个 nonempty 存储库至少有一个(通常只有一个)。
但仅此而已!我们刚刚成功绘制了三个提交的图片。只要我们不关心提交中的 snapshots 或它们的实际哈希 ID,这个简单:
A <-B <-C
事情成功了。 backwards-pointing 箭头是因为这就是 Git 的真正工作方式:它从 结束 开始,然后返回 wards,从提交到提交, 直到你厌倦了寻找并让 Git 停止,或者它到达根提交,它 不能 进一步返回。
因为很难在任何方向画出好的箭头(有一些是各种字体,但它们在某些显示器上显示得很奇怪)我现在想偷懒并停止绘制内部箭头:
A--B--C
只要记住他们指向backwards.
3从技术上讲,它是一个 160-bit-long 的字符串,但我们倾向于将其视为字母和数字。这些哈希值来自 Secure Hash Algorithm 1 或 SHA-1。 Git 人员计划转向另一种算法,并且会有某种 t运行sition 期间,每个提交都会获得 两个 名称,并且会有很多混乱。但是 Git 大多数情况下会自动处理。
4哈希冲突的可能性,这不会偶然发生,但现在是故意设计的,这就是为什么Git 正在从 SHA-1 转向另一种 more-secure 算法。
B运行ch names 只是让我们 find commits
请注意,要使用我们的小型 three-commit 存储库,我们必须首先找到 last 提交。我们可以列出每个提交,找到所有箭头,并将它们组合在一起以确定最后一个。我们也许可以按日期:列出所有提交并找到具有 latest 时间戳的那个。我们可以在这里做很多事情。但是 Git 实际上 所做的 是使用 b运行ch 名称。
请记住,哈希 ID 看起来 运行dom。再加上不同的提交是在不同的计算机上进行的,有时一台计算机中设置了错误的 time,这样日期就搞砸了。从现实生活中添加任意数量的其他混淆因素,并没有明显的方法来 find last 提交。但是,如果我们让 b运行ch name 告诉我们哪个提交是 last,那么,那真的很简单……而且很有用!让我们画这个:
A--B--C <-- master
与指向较早提交的提交一样,我们说 b运行ch 名称 指向 最后一次提交。不仅如此,我们 define 这样的事情: A b运行ch 名称包含我们将声称是最后一次提交的提交的哈希 ID。 现在我们已经完成了,我们可以有多个 "last" 提交,即使提交继续。
也就是说,让我们使用我们的 three-commit 存储库并创建一个 new b运行ch 名称,develop
,并创建 develop
指向现有提交 C
:
A--B--C <-- master, develop
现在让我们添加一个新 提交D
。我们将一些文件写入新提交,输入我们的姓名和电子邮件地址以及当前日期和时间,并设置新提交 D
以指向现有提交 C
:
A--B--C <-- master, develop
\
D
并且,作为这个 git commit
操作的最后一个技巧,我们将 Git 将 D
的实际哈希 ID(无论它是什么)写入 名字develop
:
A--B--C <-- master
\
D <-- develop
名称 develop
已停止指向 C
并开始指向 D
。
让我们创建另一个名称 feature
,它不指向 D
,而是指向 C
:
A--B--C <-- master, feature
\
D <-- develop
这里有点混乱,所以让我们附加一个特殊的名称,HEAD
,正好是这些其他名称中的一个,以记住我们正在使用哪个 名称:
A--B--C <-- master, feature (HEAD)
\
D <-- develop
现在让我们以通常的方式进行新的提交E
。新提交 E
应该指向我们正在使用的 commit,即 C
,因为 feature
指向 C
。所以我们得到:
E <-- feature (HEAD)
/
A--B--C <-- master
\
D <-- develop
如果我们在不更改 b运行ches 的情况下添加另一个新提交 F
,我们将得到:
E--F <-- feature (HEAD)
/
A--B--C <-- master
\
D <-- develop
请注意,每次我们进行 new 提交时,b运行ch name 都会移动 ward。特殊名称HEAD
还在心疼它。 现有 提交中的 None 已完全更改。 new 提交只是指向以前的 b运行ch 提示,现在 b运行ch 提示 is 新提交。
切换b运行ches
如果我们现在 git checkout master
,我们要求 Git 从提交 F
返回 切换到提交 C
再次,像这样:
E--F <-- feature
/
A--B--C <-- master (HEAD)
\
D <-- develop
请注意 commits 的 none 已更改,并且 b运行ch names[=] 的 none 607=] 这次移动了,但是 HEAD
现在附加到 master
,所以提交 C
是当前的。这就是我们所拥有的,我们将在 work-area.
C
的文件
还要注意,提交中的文件,就像提交本身一样,一直被冻结。没有什么也没有人可以改变其中任何一个。它们采用特殊的 read-only、Git-only 压缩和冻结格式。5 因为它们 是 read-only,每个提交存储每个文件的完整副本这一事实很便宜:不同提交中的文件副本与其他提交中的副本匹配,并且由于提交内容被冻结,Git 可以 re-use 较早的副本。我们只会添加 new 提交,并且新提交会自动提交 re-use 现有的冻结文件。他们只存储 new 版本的文件作为副本。6
为了使用 我们的文件,Git 将它们复制出来。当您使用 Git 时,您使用的文件位于 Git 称为您的 work-tree 或 工作树 或任意数量的类似名称。 (Git 也曾使用短语 working directory,但这很容易与 pwd
命令或 print working 混淆directory, prints. 所以 work-tree 是一个更好的名字。)
现在您知道了 git checkout
的作用——或者至少,它的部分作用。 git checkout
命令实际上可以做很多 不同的 事情,可能太多了,所以在 Git 2.23 中 Git 人们添加了一个新命令, git switch
。切换到 b运行ch 意味着 将 HEAD 附加到它,并取出文件以便我可以查看和使用它们。 文件本身来自 tip 提交 的那个 b运行ch,它是哈希 ID 存储在中的 b运行ch(或者更准确地说,b 运行通道名称).
5我喜欢叫他们freeze-dried.
6最终,当 Git packs 其内部 objects。这对正常使用来说都是不可见的:你不直接查看 Git objects,你只是 git checkout
提交和 Git 解冻并重新水化其中的文件,进入一个可用的表格。
哪些提交在哪些 b运行ches 中?
这就是 Git 与 b运行ches 的真正怪异之处。在大多数其他版本控制系统中,如果您在 一些 b运行ch 上进行提交,它 保持 在那个 b运行通道。也就是说,如果您有这样的图表:
I--J <-- feature1
/
...--G--H <-- master
\
K--L <-- feature2
您可以轻松而正确地说 feature1
包含提交 I
和 J
,而 feature2
包含提交 K
和 L
.在一些 not-Git VCS 中,这将是真实的 永远 。在 Git 中,通常 不 正确。此外,导致并包括提交 H
的提交又如何呢?提交 H
是 master
的提示——它是 last 提交 b运行ch,目前——但在 Git, 提交 H
现在在 所有三个 b运行 上。
也就是说,从 feature1
的最后一次提交开始,即 J
,我们可以返回 wards: J
,然后 I
,然后是 H
,然后是 G
,依此类推。所以 b运行ch feature1
包含 所有这些提交,它的 tip 是 J
.
类似地,从 feature2
的顶端开始,即 L
,我们可以返回 wards:L
,然后 K
,然后是 H
——但不是 I
或 J
——然后是 G
,依此类推。所以 b运行ch feature2
包含所有这些提交;它唯一不包含的是 feature1
.
正在合并
Git 的 merge 可能是 Git 获得其主要力量的地方。提交是 Git 存在的 raison d'être,但 git merge
可能是 你 使用 Git 的原因之一。 7
我们在运行git merge
的时候一般是这样的,至少在命令行下是这样的:
git checkout somebranch # or now, git switch somebranch
git merge otherbranch
看来我们要合并 b运行ches。但不是!与 Git 中的许多其他内容一样,b运行ch 操作实际上是基于 commits。让我们再次从这张图开始,但是把名字 master
去掉合身。我们将选择一个特征来附加我们的 HEAD,并合并另一个:
I--J <-- feature1 (HEAD)
/
...--G--H
\
K--L <-- feature2
这由 提交 完成。你 运行 git merge feature2
,所以 Git 找到提交 L
。您 在 feature1
,因此 Git 使用 HEAD
查找当前提交:即 J
。所以 Git 应该合并 J
和 L
。这是我们想要合并的两个 提示 提交。
Git 现在使用 提交图 ——我们一直在这里画的东西——从 J
回到 I
并回到 H
,然后从 L
到 K
再到 H
。沿着两个 b运行ches 返回 wards 后,第一个 on both b运行ches 的提交显然是 H
。不仅如此,这显然也是 最佳 常见提交。
最常见的提交,Git自动找到,是任何git merge
的合并基础手术。 Git 现在需要弄清楚并合并两件事:
- 我们改变了什么?即
H
中的快照与J
中的快照有什么区别? - 他们改变了什么?即
H
中的快照与L
中的快照有何区别?
要找到这些,Git 运行s,实际上,两个 git diff
命令。 A git diff
提取快照并进行比较。当文件匹配时,git diff
什么都不说。当它们不同时,git diff
比较文件,line-by-line,并找出哪些 行 匹配,哪些不同,并告诉我们差异。
所以,给定:
git diff --find-renames <hash-of-H> <hash-of-J>
Git 会发现 我们 发生了什么变化,从 H
中的快照到 J
中的快照。然后,做第二个 git diff
:
git diff --find-renames <hash-of-H> <hash-of-L>
Git 会发现 他们 改变了什么。
合并过程,或者我喜欢将其称为合并动词,是获取这些差异并将它们合并的过程。我们——或者更确切地说,Git——现在将组合更改应用到来自合并基础的快照。也就是说,在将我们的所有更改与他们的所有更改组合之后,Git 将这些更改应用于 H
中的快照。 保留我们的更改——新快照将匹配我们的J
,到那个程度——但添加他们的更改:新快照将匹配我们的 J
,但随着他们的变化 添加 .
一旦完成所有这些组合,merge-as-a-verb 过程本身就完成了,Git 将进行 合并提交 。合并提交就像任何其他提交一样,除了它记录 both tip commits 作为它的 two parents。换句话说,这个新的提交 M
将记录我们的提交 J
和他们的提交 L
,作为它的两个 parents:
I--J
/ \
...--G--H M <-- feature1 (HEAD)
\ /
K--L <-- feature2
与其他所有提交一样,M
有一个快照。它是通过组合我们的更改和他们的更改并将其应用于合并基础而制成的。与其他所有提交一样,M
有一条日志消息。 Git 将生成一个 not-very-good,由短语 merge b运行ch Y into X 或 merge b运行ch Y 或类似的东西。8 only 这个合并的真正特别之处在于它有两个 parents.9
7好吧,可能还有像 GitHub 这样无处不在的网站。但是这些站点的存在是由 Git 的分布式提交模型和 Git 合并的方式实现的。 Mercurial 也做这一切,以一种对大多数人来说更容易使用的方式,但是随着 Bitbucket 从 mainly-Hg 移动到 mainly-Git,Git 似乎赢得了 VCS war, 至少现在是这样。
8放一个更好的也不错。不过,人们传统上不这样做。如果 X
是文字字符串 master
,Git 会抑制 into X
部分。 git pull
命令为 merge branch Y
部分添加了 URL。 URL 可能随着时间的推移变得无效,and/or b运行ch 名称可能随着时间的推移而改变,这就是为什么这个消息不是很好。
9从技术上讲,合并提交是具有两个 或更多 parent 的任何提交。 Git 支持所谓的 octopus merge,它具有两个以上的 parent。但是,这并没有做任何你不能用普通 two-parent 合并做的事情,所以即使,例如,Mercurial 没有章鱼合并,Mercurial 可以实现 Git 可以做的一切。但这产生了提交的分类方案:root 提交 是 no parent,常规提交是 no parent一个 parent,一个 merge commit 是一个有两个或更多 parent 的提交。除了 parent 的数量——以及由此产生的raph——这些提交都是一样的,真的:它们都只存储元数据和快照。
合并后删除 b运行ch 名称
这里又是after-merge图片:
I--J
/ \
...--G--H M <-- feature1 (HEAD)
\ /
K--L <-- feature2
注意 b运行ch name, feature1
, HEAD
依附的, 以通常的方式移动到指向新提交 M
。但是由于 M
指向 both J
and L
,我们可以使用它来查找提交L
。然后我们可以使用 commit L
来查找 commit K
。我们不再需要 name feature2
来查找提交 L
。所以我们可以删除它:
I--J
/ \
...--G--H M <-- feature1 (HEAD)
\ /
K--L
b运行ch 名称 feature2
现在 消失了 。 feature2
上的提交也在 feature1
上;现在它们 仅 在 feature1
上。 找到一些提交的 b运行ch 名称集会随时间动态变化。 所以 包含的 b运行ch 名称 提交,例如 K
或 L
,可能会更改!
这就是为什么您应该根据 提交 来考虑 Git 存储库。 b运行ch 名称 意义不大。10
10好吧,b运行ch 名字可以有你想要的意思。 Glory! 但与 Humpty Dumpty 一样,如果你强行用词表达 你 想要的意思,而 他们 不想要,你可能会让听众感到困惑我不知道。
并非所有 git merge
命令合并
上图中有一个技巧。我们从这个开始:
I--J <-- feature1 (HEAD)
/
...--G--H
\
K--L <-- feature2
但是如果我们从这个开始呢:
I--J <-- feature1
/
...--G--H <-- master (HEAD)
\
K--L <-- feature2
和运行git merge feature2
?现在我们要求 Git 将 feature2
合并到 master
,即将提交 L
合并到提交 H
。因此,让我们从 b运行ch 技巧开始,然后返回 wards,找到第一个共同提交。
从H
开始,我们先别动。从 L
,让我们退回到 K
,然后再退回到 H
。啊哈!我们找到了共享提交,best 可以从两个 b运行ches.
如果我们现在进行真正的合并,我们将比较 H
中的快照,合并基础,以及 H
中的快照,b运行ch tip master
,看看我们改变了什么。然后我们将 H
中的快照(合并基础)与 L
中的快照进行比较,以查看 它们 发生了什么变化。
将 H
与自身进行比较会发生什么?试试看。在任何 Git 存储库中选择一个提交,使用 git log
获取其哈希 ID,然后 运行 git diff <hash> <hash>
。会发生什么?
答案当然是差异空。没有文件被改变。没有什么可以结合的!这绝对是 gua运行teed,因为合并基础 是 b运行ch tip.
所以,在这种情况下,git merge
需要一个 short-cut,除非你告诉它不要这样做。你可以 运行 git merge --no-ff
到 force 一个真正的合并,这给你这个:
I--J <-- feature1
/
...--G--H------M <-- master (HEAD)
\ /
K--L <-- feature2
也就是说,Git 像往常一样进行新的合并提交,其中两个 parent 是提示提交,H
和 L
。像往常一样,当前的 b运行ch 名称会指向新的提交。
如果您不强制执行它,Git会执行Git所谓的fast-forward合并,产生这个:
I--J <-- feature1
/
...--G--H
\
K--L <-- master (HEAD), feature2
也就是说,Git 不会 进行新的提交 M
。它只是将名称 master
for ward 移动到与内部 backwards-pointing 箭头相反的方向 - 到达提交 L
。现在两个 b运行ch 名称,master
和 feature2
,都标识现有提交 L
.
Git 将执行 fast-forward,而不是实际合并,当两个 b运行ch 提示 的合并基础是 当前 HEAD
提交。你也可以找到Git说没有什么可以合并的情况:
...--o--o <-- branch1
\
o--o <-- branch2 (HEAD)
运行 git merge branch1
产生投诉,但什么都不做,因为 branch1
标识的提交已经包含在 branch2
.
结合并不总是顺利
当您使用 git merge
时,它会进行真正的合并——找到一个合并基础和 运行 两个 git diff
等等 –Git 将 合并这些change-sets。通常,Git 可以自己完成此操作,因为您的更改和他们的更改不会重叠。但有时这些变化 确实 重叠,或者至少相互接触 (abut)。在这种情况下,Git 会把它比喻性的双手高高举起说:这太难了!我放弃!帮我斯波克!
一般来说,您在这种情况下所做的是完成合并:找出这些更改的正确组合是什么。这篇文章太长,无法详述所有细节,但是 Git 给您留下了混乱,在索引和您的 work-tree 中,您必须清理它。那你运行git merge --continue
或 git commit
,完成合并过程并进行最终合并提交。
有时,Git 认为一切顺利,但最终合并实际上并不正确。在这种情况下,由您决定如何处理。您可以只做一个后续提交来修复任何错误,或者您可以 "undo" 合并或做其他棘手的事情。11 没有一种方法是完全令人满意的,但最简单的——只需添加一个后续提交,并附上一条很好的日志消息来准确解释发生了什么——我认为这通常是最好的方法。
通常,一切正常......但测试东西是明智的。如果你有一个CI系统,那可能会测试结果。
11例如,您可以使用 git commit --amend
来制作 evil merge。请注意,--amend
实际上并没有更改任何 现有的 提交——它确实不能这样做——但它所做的对于常规提交来说工作正常,但可以产生这些 so-called 邪恶合并,当用于合并提交时。