Kev*_*STS 2 git git-merge git-cherry-pick
:git merge <commit-id>和之间有区别
git cherry-pick <commit-id>吗?其中“ commit-id”是我要进入master分支的新分支的提交的哈希。
除琐碎的情况外,其他所有情况都存在巨大差异(即使在琐碎的情况下,仍然存在差异)。正确理解这一点是一个挑战,但是一旦您做到了,您就可以真正理解Git本身了。
TL; DR主要是ItayB已经说过的:cherry-pick意味着复制一些现有的提交。这种复制的本质是将提交转变为变更集,然后将该相同的变更集重新应用于其他现有提交以进行新提交。新的提交与您复制的提交“执行相同的更改”,但是将该更改应用于其他快照。
此描述有用且实用,但并非100%准确-如果您在选择樱桃时遇到合并冲突,则无济于事。正如那些所暗示的那样,在内部将Cherry-pick作为一种特殊的合并来实现。如果没有合并冲突,则不需要知道这一点。如果可以的话,最好是从对git merge样式合并的正确理解开始。
合并(由完成git merge)比较复杂:它不会复制任何内容。取而代之的是,它进行了类型为merge的新提交。:-)如果不先描述Git commit图,就无法充分解释。它也有两个部分,我希望将其分为两个部分:第一,作为动词合并(合并更改的动作),第二,作为类型合并的提交,或作为名词或形容词合并: Git调用这些是合并或合并提交。
当cherry-pick合并时,它仅执行前半部分,即作为动词动作进行合并,并且有点怪异。如果合并由于冲突而失败,那么结果可能会非常令人困惑。只有通过了解Git如何将合并作为动词过程来解释它们。
Git还有些东西称为快进操作,有时也称为快进合并,这根本不是合并。不幸的是,这也令人困惑。让我们推迟一下。
您可能已经知道的第一件事是,Git主要是关于提交的,每个Git提交都会保存每个文件的完整快照。也就是说,Git的提交不是变更集。如果您修改一个文件(例如,)README.md并进行新的提交,则新的提交将包含完整的每个文件,包括已修改的(全文)README.md。当您检查提交时,使用git show或git log -p,Git会向您显示更改的内容,但是它是通过先提取先前提交的保存文件,然后提取提交的保存文件,然后比较两个快照来完成的。由于仅README.md 更改,因此仅向您显示README.md,即使如此,也只会显示差异-对一个文件的更改集。
反过来,这意味着每个提交都知道其直接祖先或父提交。在Git中,提交具有固定的永久“真实名称”,该名称始终表示特定的提交。这个真实的名称或哈希ID或有时是OID(“ O”代表Object)是Git在git log输出中打印的难看的字母和数字字符串。例如,5d826e972970a784bd7a7bdf587512510097b8c7是Git在Git存储库中的提交。这些东西看起来是随机的(尽管不是),并且通常对人类没有用,但是它们是Git所发现的每次提交。那个特定的提交有一个父级(其他丑陋的哈希ID大),Git将父级的哈希保存在提交中,以便Git可以使用该提交向后看其父级。
结果是,如果我们有一系列提交,它们将形成一个向后看的链。我们(或Git)将从此链的末尾开始,然后倒退,以在存储库中查找历史记录。假设我们有一个只有三个提交的小型存储库。相反,他们的实际散列的ID,这是太大,丑陋打扰,让我们打电话给他们承诺A,B和C,并在他们的父/子关系绘制它们:
A <-B <-C
Run Code Online (Sandbox Code Playgroud)
提交C是最新的,所以它是的子级B。Git C记住了B哈希ID,所以我们说它C 指向 B。当我们进行时B,以前只有一次提交A,所以A它B的父项也B指向A。提交A是一种特殊情况:提交时,没有提交。它没有父代,这就是让Git停止向后追逐的原因。
提交也完全是完全100%只读的:一旦完成,任何提交都将无法更改。这是因为哈希ID实际上是提交的完整内容的加密校验和。即使在任何地方更改,都可以得到一个新的,不同的哈希ID-一个新的,不同的提交。因此,提交快照可以永久保存文件状态,或至少在提交本身继续存在的前提下保存文件状态。(您最初可以将其视为“永远”;忘记或替换提交的机制更加先进,当它不是最新的提交时会变得非常棘手。)
这种只读的质量意味着我们可以更简单地绘制提交字符串,如下所示:
A--B--C
Run Code Online (Sandbox Code Playgroud)
并请记住,链接只能往后退。父母之所以不知道自己的孩子,是因为孩子在父母出生时就不存在,并且一旦父母出生,它就会一直冻结。不过,孩子可以认识其父母,因为孩子是在父母存在并被冻结后出生的。
在上面的简化图中,很容易看出哪个提交是最新的。毕竟,这封信C是紧随其后的。但是Git哈希ID看起来完全是随机的,Git 需要实际的哈希ID。因此,Git在这里所做的就是将最新提交的哈希ID 存储在分支名称中。BC
实际上,这就是分支名称的定义:像这样的名称master仅存储我们要对该分支调用的最新提交的哈希ID 。因此,鉴于A--B--C提交字符串,我们只需添加name master,指向commit即可C:
A--B--C <-- master
Run Code Online (Sandbox Code Playgroud)
分支名称的特殊之处在于,它们与commit不同,它们会更改。它们不仅会改变,而且会自动进行。在Git 中进行新提交的过程包括写出提交的内容(其父哈希ID,作者/提交者信息,已保存的快照,日志消息等),从而为新提交计算新的哈希ID,然后更改分支名称以记录新提交的哈希ID。如果我们创建一个新的提交D上master,Git会不会通过写出D指回C,然后更新master到点D:
A--B--C--D <-- master
Run Code Online (Sandbox Code Playgroud)
假设我们现在创建一个新的分支名称develop。新名称将同时指向承诺D:
A--B--C--D <-- develop, master
Run Code Online (Sandbox Code Playgroud)
E现在让我们进行一次新提交,其父节点为D:
A--B--C--D
\
E
Run Code Online (Sandbox Code Playgroud)
Git应该更新哪个分支名称?我们要master指向E还是develop指向E?这个问题的答案在于特殊名称HEAD。
为了记住我们希望Git更新的分支以及我们现在已经签出的提交,Git具有特殊的名称HEAD,在所有这样的大写字母中都进行了拼写。(由于存在古怪,小写字母在Windows和MacOS上有效,但在不共享此古怪字母的Linux / Unix系统上不起作用,因此最好使用全大写字母的拼写。如果您不喜欢输入单词,您可以使用符号@,它是同义词。)通常,Git 将名称附加HEAD到一个分支名称:
A--B--C--D <-- develop (HEAD), master
Run Code Online (Sandbox Code Playgroud)
在这里,我们在branch上develop,因为那HEAD是附属的。(请注意,所有四个提交都在两个分支上。)如果我们现在进行新提交E,则Git知道要更新的名称:
A--B--C--D <-- master
\
E <-- develop (HEAD)
Run Code Online (Sandbox Code Playgroud)
该名称HEAD保留在分支上;分支名称本身会更改它记住的提交哈希ID;E现在commit 是当前的commit。如果我们现在进行一次新提交,则其父提交将为E,而Git将更新develop。(新的承诺E是只上develop,而提交A-B-C-D仍然在这两个分支!)
一个分离的头只是意味着GIT中做了名HEAD点直接提交一些它连接到一个分支名称代替。在这种情况下,HEAD仍然命名当前提交。您只是不在任何分支上。进行新提交仍会像往常一样创建提交,但是Git不会直接将新提交的新哈希ID写入分支名称中,而是直接将其写入name中HEAD。
(独立的HEAD是正常的,但有一些特殊情况;除非进行某些git rebase操作,否则您将不会将其用于日常开发。您主要将其用于检查历史性提交-不在某些分支名称的开头。我们在这里会忽略它。)
git merge因此,既然我们知道了提交如何链接以及分支名称如何指向其分支上的最后一次提交,那么让我们看一下git merge工作原理。
假设我们对两者都进行了一些提交master,develop因此现在有了一个看起来像这样的图:
G--H <-- master
/
...--D
\
E--F <-- develop
Run Code Online (Sandbox Code Playgroud)
我们将git checkout master使它HEAD附加master指向H,然后运行git merge develop。
此时,Git将向后跟随两个链。就是说,它将开始于,然后向H反向工作。它还将从开始并向后退到。在这一点上,Git找到了一个共享的提交 -这是两个分支上的提交。所有较早的提交也被共享,但这是最好的提交,因为它是与两个分支技巧最接近的提交。GDFED
这最好的共享犯被称为合并基础。因此,在这种情况下,D是master(H)和develop(F)的合并基础。 合并基础提交完全由提交图确定,从当前提交(HEAD= master= commit H)和您在命令行上命名的其他提交(develop= commit F)开始。在此过程中,分支名称的唯一用途是查找提交,之后的所有操作都取决于图形。
找到合并基础之后,git merge现在要做的就是合并更改。但是请记住,我们说提交是快照,而不是变更集。因此,要查找更改,Git必须首先将合并基础提交本身提取到一个临时区域中。
现在,Git提取了合并库,a git diff将在以下位置找到我们所做的更改master:快照中D的快照与HEAD(H)中的快照之间的差异。这是第一个变更集。
Git的现在已经跑第二git diff,找一下他们改变了,就develop:在快照之间的差异D,并在快照F。这是第二个变更集。
因此,git merge找到合并库之后,将执行以下两个git diff命令:
git diff --find-renames <hash-of-D> <hash-of-H> # what we changed
git diff --find-renames <hash-of-D> <hash-of-F> # what they changed
Run Code Online (Sandbox Code Playgroud)
然后,Git合并这两组更改,将合并的更改应用于D(合并基础中)快照中的内容,然后根据结果进行新提交。或更确切地说,只要合并有效,它就会执行所有这些操作;或更准确地说,只要Git认为合并有效,它就会执行所有这些操作。
现在,让我们假设Git认为它可行。我们一会儿会回来合并冲突。
提交应用于合并基础的合并更改的结果是一个新的提交。此新提交具有一个特殊功能:除了照常保存完整快照外,它没有一个,而是有两个父提交。这两个父母中的第一个是您在运行时所进行的提交,git merge第二个是另一个提交。也就是说,新提交I是合并提交:
G--H
/ \
...--D I <-- master (HEAD)
\ /
E--F <-- develop
Run Code Online (Sandbox Code Playgroud)
因为Git存储库中的历史记录是提交的集合,所以这将创建一个新的提交,其历史记录是两个分支。从I,Git的可向后工作,H 并到F,并从这些,以G和E分别,并从那里到D。该名称master现在指向I。名称develop不变:继续指向F。
现在,如果需要,可以安全地删除该名称develop,因为我们(和Git)可以F从commit中找到commit I。另外,我们可以继续开发它,做出更多新的提交:
G--H
/ \
...--D I <-- master
\ /
E--F--J--K--L <-- develop
Run Code Online (Sandbox Code Playgroud)
如果我们现在git checkout master 再和运行git merge develop 再次,Git会做之前做了同一件事:找一个合并的基础上,运行两个git diffS,并提交结果。现在有趣的是,由于提交I,合并基础不再存在D。
您可以命名合并库吗?尝试一下,作为练习:从头开始L并向后工作,列出提交。(记住,只能走倒退:从F,你不能去I,因为这是方向错了你。可以得到E,这是正确的方式,向后)。然后在开始I和工作向后都F和H。您为此清单中的一员develop吗?如果是这样,那就是新合并的合并基础(F即),因此Git会将其用于两个git diff命令。
最后,如果合并成功,我们会得到一个新的合并提交M于master:
G--H
/ \
...--D I--------M <-- master (HEAD)
\ / /
E--F--J--K--L <-- develop
Run Code Online (Sandbox Code Playgroud)
以后的合并,如果我们向添加更多提交develop,将L用作合并基础。
让我们回到这种状态,并附HEAD加到master:
G--H <-- master (HEAD)
/
...--D
\
E--F <-- develop
Run Code Online (Sandbox Code Playgroud)
现在,让我们看看Git是如何实现的git cherry-pick develop。
首先,Git将名称解析develop为提交哈希ID。既然develop指向F,那就是提交F。
提交F是快照,必须将其转换为变更集。Git用做到这一点git diff <hash-of-E> <hash-of-F>。
此时,Git 可以将这些相同的更改应用于中的快照H。这就是我们的高水平,不十分准确的描述所声称的:我们只是将此差异应用到上H。在大多数情况下,会发生什么模样的Git就是这样做的,并且在很老的版本的Git的(即没有人使用了),Git的真的没做。但是在某些情况下它不能正常工作,因此Git现在执行一种奇怪的合并。
在正常的合并中,Git将找到合并基础并运行两个差异。在樱桃选择方式的合并中,Git只是强制合并基础成为要进行樱桃选择的提交的父代。也就是说,由于我们在挑选樱桃F,因此Git强制将merge base提交E。
Git现在git diff --find-renames <hash-of-E> <hash-of-H>可以查看我们所做的更改,并git diff --find-renames <hash-of-E> <hash-of-F>查看它们(commit F)所做的更改。然后,它将两组更改组合在一起,并将结果应用于中的快照E。这样就可以保留您的工作(因为所做的任何更改,您仍然可以更改),同时也可以从中添加更改集F。
如果一切正常(通常这样做),则Git会进行一次新提交,但此新提交是继续进行的普通单亲提交master。这很像F,实际上,Git也从中复制日志消息F,因此让我们称之为新提交F'来记住:
G--H--F' <-- master (HEAD)
/
...--D
\
E--F <-- develop
Run Code Online (Sandbox Code Playgroud)
请注意,和以前一样,它develop都没有移动。但是,我们也没有进行合并提交:新文件F'不会记录F自身。该图未合并;在合并基础的F',并F仍承诺D。
这是cherry-pick和真正的合并之间的完全区别:cherry-pick使用Git的合并机制进行更改合并,但不合并图,仅复制一些现有提交即可。合并中使用的两个变更集是基于精心挑选的提交的父级,而不是基于计算的合并基础。新副本具有新的哈希ID,显然与原始提交没有任何关系。从分支名称master或develop此处开始发现的历史仍然可以追溯到过去。使用真正的合并,新的提交是一个双父合并,并且历史牢固地git merge结合在一起-当然,合并的两组更改是从计算的合并基础形成的,因此它们是不同的更改集。
Git的合并机制,即将两组不同的变更合并在一起的引擎,有时可能而且确实无法进行合并。这种情况发生时,在这两个改变集,都试图改变相同的行相同的文件。
假设Git正在合并更改,并且更改集--ours表示触摸文件A的第17行,文件B的第30行和文件D的第3-6行。同时,变更集--theirs对文件A没有任何说明,但是确实对文件B的第30行,文件C的第12行以及文件D的第10-15行进行了更改。
由于只有我们的触摸文件A,只有他们的触摸文件C,所以Git只能使用我们的A版本和C的版本。我们都触摸文件D,但是我们的触摸文件3-6行,而他们的触摸文件10-15,所以Git可以对文件D进行两项更改。文件B是真正的问题:我们都触及了第30行。
如果我们对第30行进行了相同的更改,Git可以解决此问题:它只需复制一份更改。但是,如果我们对第30行进行了其他更改,Git将因合并冲突而停止。
在这一点上,Git的索引(我在这里没有讨论过)变得至关重要。我将继续不谈论它,只是说Git在其中保留了冲突文件的所有三个版本。同时,文件B也有一个工作树副本,并且在工作树文件中,Git尽最大的努力来组合更改,并使用冲突标记显示问题出在哪里。
作为运行Git的人,您的工作是以任何您喜欢的方式解决每种冲突。解决了所有冲突之后,您便可以使用git add它为新提交更新Git的索引。然后,您可以运行git merge --continue或git cherry-pick --continue,根据导致问题的原因,让Git提交结果;或者,您可以运行git commit,这是做同一件事的老方法。实际上,这些--continue操作主要是git commit为您运行的:提交代码检查是否存在应该完成的冲突,如果是,则进行常规(樱桃选择)提交或合并提交。
运行时,Git会像往常一样找到合并库,但是有时合并库很简单。例如,考虑如下图:git merge othercommit
...--F--G--H <-- develop (HEAD)
\
I--J <-- feature-X
Run Code Online (Sandbox Code Playgroud)
如果你运行git merge feature-X现在的Git通过启动在提交发现的合并基础J和H和做平常向后行走找到第一个共享的承诺。但是,第一个共享提交是提交H本身,就在哪里develop。
Git可以运行以下命令进行真正的合并:
git diff --find-renames <hash-of-H> <hash-of-H> # what we changed
git diff --find-renames <hash-of-H> <hash-of-J> # what they changed
Run Code Online (Sandbox Code Playgroud)
您可以使用强制Git执行此操作git merge --no-ff。但是显然,将提交与自己进行比较不会显示任何变化。--ours两组更改的一部分将为空。合并的结果将与commit中的快照相同J,因此如果我们强制执行真正的合并:
...--F--G--H------J' <-- develop (HEAD)
\ /
I--J <-- feature-X
Run Code Online (Sandbox Code Playgroud)
然后J',J也将匹配。它们将是不同的提交(J'将是合并提交,带有我们的名称和日期以及我们喜欢的任何日志消息),但是它们的快照将是相同的。
如果我们不强制进行真正的合并,则Git会意识到这一点,J'并且J会像这样进行匹配,并且根本不会打扰进行新的提交。相反,它“向后滑动HEAD附加的名称”,而不是向后指向的内部箭头:
...--F--G--H
\
I--J <-- develop (HEAD), feature-X
Run Code Online (Sandbox Code Playgroud)
(在此之后,在图形中绘制纽结毫无意义)。这是一个快进操作,或者用Git特有的术语,是一个快进合并(即使没有实际的合并!)。
cherry-pick只对当前分支进行一次提交。
merge获取整个分支(可能是多次提交)并将其合并到您的分支。
如果您将其合并,则相同<commit-id>- 它不仅需要特定的提交,还需要以下提交(如果有的话)。