本文介绍了这是指“由提交引起的更改".在git中的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我到处都看到这样的内容:"...... pick应用由提交引入的更改..."

Everywhere I see this: "...cherry-pick applies changes introduced by a commit..."

我做到了:在master中创建了此文件:

I did this: created this file in master:

** File 1 **

Content

** Footer **

然后分支到branch2并进行更改:

then branched out to branch2 and committed a change:

** File 1 **

Content
Edit 1

** Footer **

然后是另一个:

** File 1 **

Content
Edit 2
Edit 1

** Footer **

现在,我回到主目录,尝试从branch2中挑选最新提交.我期望只有'Edit2'会被导入,因为与前一个提交相比,这不是该提交引入的更改吗?

Now I went back to master and tried to cherry-pick the latest commit from branch2. I expected that only 'Edit2' will get imported since isn't this a change introduced by that commit, compared to the previous one?

我得到的是以下合并冲突:

What I got instead is the following merge conflict:

** File 1 **

Content
<<<<<<< HEAD
=======
Edit 2
Edit 1
>>>>>>> b634e53...
** Footer **

现在,我显而易见的问题是,我对Cherry-pick的工作方式有什么误解,具体是为什么在这里存在合并冲突,而这对git merge来说是一个快速的前进?

Now my obvious question is what is it that I misunderstand about how cherry-pick works, and concretely why there is a merge conflict here, which would be a fast-forward with git merge?

重要通知:这并不是关于合并冲突的问题,我感兴趣的是Cherry-pick在这里实际上在做什么.而且我并不是问好奇心,而是因为我在工作中使用git遇到了麻烦.

IMPORTANT NOTICE: This is NOT meant as a question about merge conflicts, what I'm interested in is what cherry-pick is actually doing here. And I'm not asking from curiosity/whatever, but because I'm running into troubles using git on my job.

推荐答案

正如几个人在评论中指出的(并指向其他问题的链接)一样,git cherry-pick实际上执行了三种方式的合并. cherry-pick和还原工作是如何进行的?对此进行了描述,但更多的是内容而非机制.

As several people have noted in comments (and made links to other questions), git cherry-pick actually does a three way merge. How do cherry-pick and revert work? describes this, but more in terms of content than mechanism.

我在为什么会与git rebase是交互式的吗?,以及大致的选择和还原轮廓,但是我认为退后一步并询问您做的机制问题是一个好主意.不过,我将其重新构架为以下三个问题:

I describe the source of a particular set of merge conflicts in Why do I get this merge conflict with git rebase interactive?, along with a general outline of cherry-pick and revert, but I think it's a good idea to step back and ask the mechanism question you did. I would re-frame it a bit, though, as these three questions:

  • 提交确实是快照吗?
  • 如果提交是快照,那么git showgit log -p如何将其显示为更改?
  • 如果提交是快照,那么git cherry-pickgit revert如何工作?
  • Is a commit really a snapshot?
  • If a commit is a snapshot, how does git show or git log -p show it as a change?
  • If a commit is a snapshot, how can git cherry-pick or git revert work?

回答最后一个问题需要首先回答一个问题:

Answering the last requires first answering one more question:

  • Git如何执行git merge?

因此,让我们以正确的顺序回答这四个问题.这将相当长,如果愿意,您可以直接跳到最后一部分-但请注意,它建立在第三部分的基础上,第二部分是第二部分的基础.

So, let's take these four questions, in the correct order. This is going to be rather long, and if you like, you can jump straight to the last section—but note that it builds on the third section, which builds on the second, which builds on the first.

是的-尽管从技术上讲,提交指的是快照,而不是 的快照.这非常简单明了.要使用Git,我们通常从运行git clone开始,这将为我们提供一个新的存储库.有时,我们首先创建一个空目录,然后使用git init创建一个 empty 存储库.无论哪种方式,我们现在都有三个实体:

Yes—though, technically, a commit refers to a snapshot, rather than being one. This is pretty simple and straightforward. To use Git, we generally start out by running git clone, which gets us a new repository. Occasionally, we start out by making an empty directory and using git init to create an empty repository. Either way, though, we now have three entities:

  1. 存储库本身,它是一个由 objects 组成的大型数据库,以及一个由 name到哈希ID映射的较小数据库(例如,分支名称),加上许多其他作为单个文件实现的小型数据库(例如,每个reflog一个).

  1. The repository itself, which a big database of objects, plus a smaller database of name to hash ID mappings (for, e.g., branch names), plus lots of other mini-databases implemented as single files (e.g., one per reflog).

Git称为 index ,或 staging区域,有时甚至称为 cache .它被呼叫的内容取决于谁进行呼叫.索引本质上是让Git构建您将进行的 next 提交的地方,尽管它在合并过程中扮演着扩展的角色.

Something Git calls the index, or the staging area, or sometimes the cache. What it gets called depends on who does the calling. The index is essentially where you have Git build the next commit you will make, though it takes on an expanded role during merges.

工作树,您可以在其中实际查看文件并对其进行处理.

The work-tree, which is where you can actually see files and work on / with them.

对象数据库包含四种类型的对象,Git称为 commits blob 带注释的标签.树和Blob主要是实现细节,我们可以在这里忽略带注释的标签:出于我们的目的,这个大数据库的主要功能是保留所有提交.这些提交然后引用保存文件的树和Blob.最后,实际上是快照加上blobs和blobs的组合.尽管如此,每个提交都只有一棵树,而那棵树正是我们获取快照的其余方法,因此,除了大量令人讨厌的实现细节之外,提交本身也可能是快照.

The object database holds four types of objects, which Git calls commits, trees, blobs, and annotated tags. Trees and blobs are mostly implementation detail, and we can ignore annotated tags here: the main function of this big database, for our purposes, is to hold all our commits. These commits then refer to the trees and blobs that hold the files. In the end, it's actually the combination of trees-plus-blobs that is the snapshot. Still, every commit has exactly one tree, and that tree is what gets us the rest of the way to the snapshot, so except for lots of devilish implementation details, the commit itself might as well be a snapshot.

我们还不会深入研究杂草,但是我们可以说该索引通过保存每个文件的压缩,Git格式且大部分为冻结的副本而起作用.从技术上讲,它保存了对实际冻结的副本的引用,并存储为 blob .也就是说,如果您从git clone url开始,则Git已运行git checkout branch作为克隆的最后一步.此checkout填充了分支尖端的提交中的索引,以便该索引具有该提交中每个文件的副本.

We won't go too deep into the weeds yet, but we will say that the index works by holding a compressed, Git-ified, mostly-frozen copy of every file. Technically, it holds a reference to the actually-frozen copy, stored as a blob. That is, if you start by doing git clone url, Git has run git checkout branch as the last step of the clone. This checkout filled-in the index from the commit at the tip of branch, so that the index has a copy of every file in that commit.

实际上,大多数 git checkout操作都从提交中填充 索引.这使您可以查看和使用工作树中的所有文件,但是工作树副本实际上不是提交中的所有文件.提交中的内容是所有这些文件的冻结,压缩,Git修饰,永不更改的Blob快照.这样可以永久保留这些文件的那些版本,或者只要提交本身存在就保留这些版本,这对于存档非常有用,但对于执行任何实际工作却毫无用处.这就是为什么Git将文件解Git到工作树中的原因.

Indeed, most git checkout operations fill in both the index and the work-tree from a commit. This lets you see, and use, all of your files in the work-tree, but the work-tree copies aren't the ones that are actually in the commit. What's in the commit is (are?) frozen, compressed, Git-ified, can-never-be-changed blob snapshots of all of those files. This keeps those versions of those files forever—or for as long as the commit itself exists—and is great for archival, but useless for doing any actual work. That's why Git de-Git-ifies the files into the work-tree.

Git 可以止于此,只需提交和工作树即可. Mercurial(在很多方面都像Git一样)确实在这里停止:您的工作树是您建议的下一次提交.您只需更改工作树中的内容,然后运行hg commit,它就会从您的工作树中进行新的提交.这具有明显的优势,没有麻烦的索引会造成麻烦.但是它也有一些缺点,包括本质上比Git的方法慢.无论如何,Git要做的是从上一个提交的信息保存在索引中,准备再次提交.

Git could stop here, with just commits and work-trees. Mercurial—which is in many ways like Git—does stop here: your work-tree is your proposed next commit. You just change stuff in your work-tree and then run hg commit and it makes the new commit from your work-tree. This has the obvious advantage that there's no pesky index making trouble. But it also has some drawbacks, including being inherently slower than Git's method. In any case, what Git does is to start with the previous commit's information saved in the index, ready to be committed again.

然后,每次您运行git add时,Git都会压缩并Git修改您添加的文件,然后立即更新索引.如果仅更改了几个文件,然后仅git add更改了这几个文件,则Git只需更新一些索引条目.因此,这意味着始终索引中都有下一个快照,采用特殊的仅Git压缩和随时冻结的形式.

Then, each time you run git add, Git compresses and Git-ifies the file you add, and updates the index now. If you change just a few files, and then git add just those few files, Git only has to update a few index entries. So this means that at all times the index has the next snapshot inside it, in the special Git-only compressed and ready-to-freeze form.

这又意味着git commit仅需要冻结索引内容.从技术上讲,它将索引变成一棵新树,为新提交做好了准备.在某些情况下,例如经过一些还原后,或对于git commit --allow-empty,新树实际上将与先前的提交成为相同树,但是您无需了解或关心这个.

This in turn means that git commit simply needs to freeze the index contents. Technically, it turns the index into a new tree, ready for the new commit. In a few cases, such as after some reverts, or for a git commit --allow-empty, the new tree will actually be the same tree as some previous commit, but you don't need to know or care about this.

这时,Git收集了您的日志消息以及每个提交中包含的其他元数据.它将当前时间添加为时间戳,这有助于确保每次提交都是唯一的,并且通常很有用.它使用 current 提交作为新提交的 parent 哈希ID,使用通过保存索引产生的 tree 哈希ID,并写出新的commit对象,它将获得一个新的唯一的提交哈希ID.因此,新提交包含您之前签出的所有提交的实际哈希ID.

At this point, Git collects your log message and the other metadata that goes into each commit. It adds the current time as the time-stamp—this helps make sure that each commit is totally unique, as well as being generally useful. It uses the current commit as the new commit's parent hash ID, uses the tree hash ID produced by saving the index, and writes out the new commit object, which gets a new and unique commit hash ID. The new commit therefore contains the actual hash ID of whatever commit you had checked out earlier.

最后,Git将新提交的哈希ID写入当前分支名称,因此该分支名称现在是指 new 提交,而不是以前的新提交的父对象.也就是说,无论什么分支的尖端,现在的提交都比分支的尖端 落后一步.新技巧是您刚刚进行的提交.

Last, Git writes the new commit's hash ID into the current branch name, so that the branch name now refers to the new commit, rather than to the new commit's parent, as it used to. That is, whatever commit was the tip of the branch, now that commits is one step behind the tip of the branch. The new tip is the commit you just made.

您可以使用git checkout commit -- path从一个特定的提交中提取一个特定的文件.此 still 首先将文件复制到索引中,因此这并不是真正的例外.但是,您还可以使用git checkout将文件仅从索引复制到工作树,并且可以使用git checkout -p选择性地,交互式地 patch 文件.这些变体在处理索引和/或工作树时都有自己的特殊规则集.

You can use git checkout commit -- path to extract one particular file from one particular commit. This still copies the file into the index first, so that's not really an exception. However, you can also use git checkout to copy files just from the index, to the work-tree, and you can use git checkout -p to selectively, interactively patch files, for instance. Each of these variants has its own special set of rules as to what it does with index and/or work-tree.

由于Git从索引创建 新提交,因此经常重新检查文档可能是明智的(尽管很痛苦).幸运的是,git status告诉您很多有关索引中的内容的方法-通过比较当前提交与索引,然后将索引与工作树进行比较,对于每次这样的比较,告诉您什么是不同.因此,很多时候,您不必费神费力地掌握每个Git命令对索引和/或工作树的影响的千差万别的细节:您可以运行该命令,并使用稍后.

Since Git builds new commits from the index, it may be wise—albeit painful—to re-check the documentation often. Fortunately, git status tells you a lot about what's in the index now—by comparing the current commit vs the index, then comparing the index vs the work-tree, and for each such comparison, telling you what's different. So a lot of the time, you don't have to carry around, in your head, all the wildly varying details of each Git command's effect on index and/or work-tree: you can just run the command, and use git status later.

每个提交都包含其父提交的原始哈希ID,这又意味着我们可以始终从某些提交字符串的 last 提交开始,并向后进行向后查找以前的所有提交:

Each commit contains the raw hash ID of its parent commit, which in turn means that we can always start at the last commit of some string of commits, and work backwards to find all the previous commits:

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

我们只需要有一种方法来找到 last 提交.这样的方式是:分支名称(例如此处的master)标识了 last 提交.如果最后一次提交的哈希ID为H,则Git在对象数据库中找到提交H. H存储G的哈希ID,Git从中找到G,它存储F的哈希ID,Git从中找到F,依此类推.

We only need to have a way to find the last commit. That way is: the branch name, such as master here, identifies the last commit. If that last commit's hash ID is H, Git finds commit H in the object database. H stores G's hash ID, from which Git finds G, which stores F's hash ID, from which Git finds F, and so on.

这也是将提交显示为补丁的指导原则.我们让Git查看提交本身,找到其父级,然后提取该提交的快照.然后,我们也让Git提取提交的快照.现在我们有了两个快照,现在我们可以将它们进行比较-照原样从较早的快照中减去较早的快照.不管是什么 ,都必须是该快照中更改的.

This is also the guiding principle behind showing a commit as a patch. We have Git look at the commit itself, find its parent, and extract that commit's snapshot. Then we have Git extract the commit's snapshot too. Now we have two snapshots, and now we can compare them—subtract the earlier one from the later one, as it were. Whatever is different, that must be what changed in that snapshot.

请注意,这仅适用于非合并提交.当我们让Git构建一个 merge 提交时,我们让Git存储了一个而不是两个 父哈希ID.例如,在master上运行git merge feature之后,我们可能会:

Note that this only works for non-merge commits. When we have Git build a merge commit, we have Git store not one but two parent hash IDs. For instance, after running git merge feature while on master, we may have:

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

提交M两个父母:它的第一个父母是I提示刚刚在master上提交了.它的第二个父级是L,它仍然是对feature的提示提交.很难将提交M作为对IL的简单更改来呈现M,并且默认情况下,git log只是不打扰来显示这里有任何变化!

Commit M has two parents: its first parent is I, which was the tip commit on master just a moment ago. Its second parent is L, which is still the tip commit on feature. It's hard—well, impossible, really—to present commit M as a simple change from either I or L, and by default, git log simply doesn't bother to show any changes here!

(您可以告诉git loggit show实际上是 split 合并:显示从IM的差异,然后显示第二个,使用git log -m -pgit show -m将diff从LM分开.git show命令默认情况下会产生Git称为 combined diff 的内容,这很奇怪特殊:它实际上是通过像-m一样运行两个差异,然后忽略它们所说的大部分内容,并仅向您显示这两个方面的一些变化而制成的 em> commits.这与合并的工作方式密切相关:其目的是显示可能存在合并冲突的部分.)

(You can tell both git log and git show to, in effect, split the merge: to show a diff from I to M, and then to show a second, separate diff from L to M, using git log -m -p or git show -m. The git show command produces, by default, what Git calls a combined diff, which is kind of weird and special: it's made by, in effect, running both diffs as for -m, then ignoring most of what they say and showing you only some of those changes that come from both commits. This relates pretty strongly to how merges work: the idea is to show the parts that might have had merge conflicts.)

这使我们想到了一个内在的问题,在开始挑选和还原之前,我们需要解决这个问题.首先,我们需要讨论git merge的机制,即如何为提交M获取快照.

This leads us to our embedded question, which we need to cover before we get to cherry-pick and revert. We need to talk about the mechanics of git merge, i.e., how we got a snapshot for commit M in the first place.

让我们首先注意到,合并(无论如何,大多数合并)的 point 合并工作.当我们先做git checkout master然后是git merge feature时,我们的意思是:我在master上做了一些工作.其他人在feature上做了一些工作.我想将他们所做的工作与我所做的工作结合起来.有一个过程可以进行结合,然后再有一个更简单的过程来保存结果.

Let's start by noting that the point of a merge—well, of most merges, anyway—is to combine work. When we did git checkout master and then git merge feature, we meant: I did some work on master. Someone else did some work on feature. I'd like to combine the work they did with the work I did. There is a process for doing this combining, and then a simpler process for saving the result.

因此,真正合并有两个部分,这会导致像上面的M那样的提交.第一部分是我喜欢的 verb 部分, merge .这部分实际上结合了我们的不同更改.第二部分是进行合并合并提交:在这里,我们将"merge"一词用作名词或形容词.

Thus, there are two parts to a true merge that results in a commit like M above. The first part is what I like to call the verb part, to merge. This part actually combines our different changes. The second part is making a merge, or a merge commit: here we use the word "merge" as a noun or an adjective.

在这里也值得一提的是git merge并不总是合并.该命令本身很复杂,并且具有许多有趣的标志参数,可以通过各种方式对其进行控制.在这里,我们只考虑它确实可以进行实际合并的情况,因为我们正在考虑合并以了解自动选择"和还原".

It's also worth mentioning here that git merge doesn't always make a merge. The command itself is complicated and has lots of fun flag arguments to control it in various ways. Here, we're only going to consider the case where it really does make an actual merge, because we're looking at merge in order to understand cherry-pick and revert.

真正合并的第二部分是较容易的部分.一旦完成了 merging 的合并过程,即合并动词,我们就让Git使用索引中的任何内容以通常的方式进行新的提交.这意味着索引需要以其中的合并内容结束. Git将像往常一样构建树并像往常一样收集日志消息-我们可以使用不太好的默认值merge branch B,或者如果我们感到特别勤奋,则可以构建一个好的树. Git将照常添加我们的姓名,电子邮件地址和时间戳.然后,Git将写出一个提交-但不是在此新提交中仅存储一个父级,而是Git将存储一个额外的 second 父级,即哈希我们在运行git merge时选择的提交ID.

The second part of a real merge is the easier part. Once we've finished the to merge process, the merge-as-a-verb, we have Git make a new commit in the usual way, using whatever is in the index. This means the index needs to end up with the merged content in it. Git will build the tree as usual and collect a log message as usual—we can use the not-so-good default, merge branch B, or construct a good one if we're feeling particularly diligent. Git will add our name, email address, and timestamp as usual. Then Git will write out a commit—but instead of storing, in this new commit, just the one parent, Git will store an extra, second parent, which is the hash ID of the commit we chose when we ran git merge.

例如,对于在master上的git merge feature,第一个父级将是提交I-我们通过运行git checkout master检出的提交.第二个父级将是提交Lfeature指向该提交.这实际上是 a 合并的全部:合并提交只是具有至少两个父级的提交,而标准合并的标准两个父级是,第一个与相同任何提交,第二个是我们通过运行git merge something选择的提交.

For our git merge feature while on master, for instance, the first parent will be commit I—the commit we had checked out by running git checkout master. The second parent will be commit L, the one to which feature points. That's really all there is to a merge: a merge commit is just a commit with at least two parents, and the standard two parents for a standard merge are that the first is the same as for any commit, and the second is the one we picked by running git merge something.

动词合并是较难的部分.我们在上面指出,Git将根据索引中的内容进行 new 提交.因此,我们需要将放入索引,或者将Git放入合并工作的结果.

The merge-as-a-verb is the harder part. We noted above that Git is going to make the new commit from whatever is in the index. So, we need to put into the index, or have Git put into it, the result of combining work.

我们在上面声明,我们对master进行了一些更改,无论它们是什么,都对feature进行了一些更改.但是我们已经看到,Git不会 store 进行更改. Git 存储快照.我们如何从快照变为更改?

We declared above that we made some changes on master, and they—whoever they are—made some changes on feature. But we already saw that Git doesn't store changes. Git stores snapshots. How do we go from snapshot to change?

我们已经知道该问题的答案!.当我们查看git show时,我们已经看到了答案. Git 比较两个快照.因此,对于git merge,我们只需选择正确的快照.但是哪些是正确的快照?

We already know the answer to that question! We saw it when we looked at git show. Git compares two snapshots. So for git merge, we just need to pick the right snapshots. But which ones are the right snapshots?

此问题的答案位于提交图中.在运行git merge之前,图形如下所示:

The answer to this question lies in the commit graph. Before we run git merge, the graph looks like this:

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

我们坐在提交I(master的尖端)上.他们的提交是提交L,这是feature的提示.从I开始,我们可以倒退到H,然后是G,然后是F,然后是E,依此类推.同时,从L开始,我们可以倒退到K,然后是J,然后是F,大概是E,依此类推.

We're sitting on commit I, the tip of master. Their commit is commit L, the tip of feature. From I, we can work backwards to H and then G and then F and then presumably E and so on. Meanwhile, from L, we can work backwards to K and then J and then F and presumably E and so on.

实际上,当我们时,我们在提交F收敛.显然,那么,无论我们进行了什么更改,我们都从F中的快照开始... ...并且他们进行了任何更改,它们均以!因此,要结合我们的两组更改,我们要做的就是:

When we do actually do this work-backwards trick, we converge at commit F. Obviously, then, whatever changes we made, we started with the snapshot in F ... and whatever changes they made, they also started with the snapshot in F! So all we have to do, to combine our two sets of changes, is:

  • FI进行比较:这就是我们所做的更改
  • FL进行比较:这就是他们所做的更改
  • compare F to I: that's what we changed
  • compare F to L: that's what they changed

本质上,我们将只让Git运行两个git diff.一个人会弄清楚我们 发生了什么变化,一个人会弄清楚他们发生了什么变化.提交F是我们的常见起点,或者在版本控制方面来说是合并基础.

We will, in essence, just have Git run two git diffs. One will figure out what we changed, and one will figure out what they changed. Commit F is our common starting point, or in version-control-speak, the merge base.

现在,为了实际完成合并,Git扩展了索引.现在,Git不再拥有每个文件的一个副本,而是拥有每个文件的三个副本的索引.一份副本将来自合并基础F.第二份副本将来自我们的提交I.最后的第三份副本来自其提交L.

Now, to actually accomplish the merge, Git expands the index. Instead of holding one copy of each file, Git will now have the index hold three copies of each file. One copy will come from the merge base F. A second copy will come from our commit I. The last, third, copy comes from their commit L.

同时,Git还逐文件查看了两个差异的结果.只要提交FIL都具有相同的文件,只有以下五种可能性:

Meanwhile, Git also looks at the result of the two diffs, file-by-file. As long as commits F, I, and L all have all the same files, there are only these five possibilities:

  1. 没有人触摸过该文件.只需使用任何版本:它们都是相同的.
  2. 我们更改了文件,但他们没有.只需使用我们的版本即可.
  3. 他们更改了文件,但我们没有.只需使用其版本即可.
  4. 我们和他们俩都更改了文件,但是我们做了相同更改.使用我们的或他们的-两者都相同,所以无论哪个都没关系.
  5. 我们和他们俩都更改了相同文件,但是我们进行了不同更改.
  1. Nobody touched the file. Just use any version: they're all the same.
  2. We changed the file and they didn't. Just use our version.
  3. They changed the file and we didn't. Just use their version.
  4. We and they both changed the file, but we made the same changes. Use either ours or theirs—both are the same, so it doesn't matter which.
  5. We and they both changed the same file, but we made different changes.

案例5是唯一困难的案例.对于所有其他文件,Git知道-或至少假设它知道-正确的结果是什么,因此对于所有其他情况,Git会将有问题的文件的索引槽缩小到只有一个槽(编号为零)来保存该文件.正确的结果.

Case 5 is the only tough one. For all the others, Git knows—or at least assumes it knows—what the right result is, so for all those other cases, Git shrinks the index slots for the file in question back to just one slot (numbered zero) that holds the correct result.

对于情况5,Git将三个输入文件的所有三个副本填充到索引中的三个带编号的插槽中.如果文件名为file.txt,则:1:file.txt保存来自F的合并基础副本,:2:file.txt保存来自提交I的副本,而:3:file.txt保存来自L的副本.然后Git运行一个低级合并驱动程序-我们可以在.gitattributes中设置一个,或使用默认的驱动程序.

For case 5, though, Git stuffs all three copies of the three input files into three numbered slots in the index. If the file is named file.txt, :1:file.txt holds the merge base copy from F, :2:file.txt holds our copy from commit I, and :3:file.txt holds their copy from L. Then Git runs a low-level merge driver—we can set one in .gitattributes, or use the default one.

默认的低级合并将两个差异(从基本到我们以及从基本到他们)进行比较,并尝试通过进行这两组更改来合并它们.每当我们触摸文件中的不同行时,Git都会接受我们的更改.当我们触摸 same 行时,Git声明合并冲突. Git将生成的文件作为file.txt写入工作树,如果存在冲突则使用冲突标记. .如果将merge.conflictStyle设置为diff3,则冲突标记包括插槽1中的 base 文件以及插槽2和3中文件中的行.我更喜欢这种冲突样式而不是默认值,它忽略了slot-1上下文,只显示了slot-2与slot-3的冲突.

The default low-level merge takes the two diffs, from base to ours and from base to theirs, and tries to combine them by taking both sets of changes. Whenever we touch different lines in the file, Git takes our or their change. When we touch the same lines, Git declares a merge conflict. Git writes the resulting file to the work-tree as file.txt, with conflict markers if there were conflicts. If you set merge.conflictStyle to diff3, the conflict markers include the base file from slot 1, as well as the lines from the files in slots 2 and 3. I like this conflict style much better than the default, which omits the slot-1 context and shows just the slot-2 vs slot-3 conflict.

当然,如果存在冲突,Git会声明合并冲突.在这种情况下,它(最终,在处理完所有其他文件之后)在合并的中间停止,将冲突标记混乱留在工作树中,并将file.txt的所有三个副本保留在插槽1中, 2,和3.但是,如果Git能够自行解决两个不同的变更集,它将继续进行,并且 erases 插槽1-3将成功合并的文件写入工作树. ,将工作树文件复制到正常插槽0处的索引中,然后照常处理其余文件.

Of course, if there are conflicts, Git declares the merge conflicted. In this case, it (eventually, after processing all the other files) stops in the middle of the merge, leaving the conflict-marker mess in the work-tree and all three copies of file.txt in the index, in slots 1, 2, and 3. But if Git is able to resolve the two different change-sets on its own, it goes ahead and erases slots 1-3, writes the successfully-merged file to the work-tree, copies the work-tree file into the index at the normal slot zero, and proceeds with the rest of the files as usual.

如果合并停止停止,则是解决混乱的工作.许多人通过编辑有冲突的工作树文件,找出正确的结果,写出工作树文件并运行git add将该文件复制到索引中来完成此操作. copy-into-index步骤删除了阶段1-3条目,并写入了正常的阶段零条目,这样就解决了冲突,我们准备提交.然后,您告诉合并继续进行,或者直接运行git commit,因为git merge --continue无论如何都只是运行git commit.

If the merge does stop, it is your job to fix the mess. Many people do this by editing the conflicted work-tree file, figuring out what the right result is, writing out the work-tree file, and running git add to copy that file into the index. The copy-into-index step removes the stage 1-3 entries and writes the normal stage-zero entry, so that the conflict is resolved and we're ready to commit. Then you tell the merge to continue, or run git commit directly since git merge --continue just runs git commit anyway.

这个合并过程虽然有点复杂,但最终却非常简单:

This to merge process, while a bit complicated, is in the end pretty straightforward:

  • 选择合并基础.
  • 将合并基础与当前提交进行比较,这是我们已经检查出要通过合并进行修改的提交,以查看我们发生了什么变化.
  • 将合并基础与我们选择进行合并的 other 提交进行比较,以查看他们发生了什么变化.
  • 合并更改,将 combined 更改应用于合并基础中的快照.这就是结果,该结果将包含在索引中.可以从合并基本版本开始,因为 combined 更改了 include 我们的更改:除非我们说 ,否则我们不会丢失它们仅获取文件的版本.
  • Pick a merge base.
  • Diff the merge base against the current commit, the one we have checked out that we're going to modify by merging, to see what we changed.
  • Diff the merge base against the other commit, the one we picked to merge, to see what they changed.
  • Combine the changes, applying the combined changes to the snapshot in the merge base. That's the result, which goes in the index. It's OK that we start out with the merge base version, because the combined changes include our changes: we won't lose them unless we say take only their version of the file.

合并合并为动词过程,然后执行合并为名词步骤,进行合并提交,然后合并完成.

This to merge or merge as a verb process is then followed by the merge as noun step, making a merge commit, and the merge is done.

如果三个输入提交都具有相同的文件,那么事情就会变得棘手.我们可以有添加/添加冲突,修改/重命名冲突,修改/删除冲突等等,这些都是我所说的高级冲突.这些还会在中间停止合并,并在适当时填充索引的插槽1-3. -X标志-X ours-X theirs不会影响高级冲突.

If the three input commits don't have all the same files, things get tricky. We can have add/add conflicts, modify/rename conflicts, modify/delete conflicts, and so on, all of which are what I call high level conflicts. These also stop the merge in the middle, leaving slots 1-3 of the index populated as appropriate. The -X flags, -X ours and -X theirs, do not affect high level conflicts.

您可以使用-X ours-X theirs来使Git选择我们的更改"或他们的更改",而不是因冲突而停止.请注意,您将其指定为git merge的参数,因此它适用于所有有冲突的所有文件.发生冲突后,可以使用git merge-file来一次更智能,更选择性地一次处理一个文件,但是Git并没有像应该做的那样简单.

You can use -X ours or -X theirs to make Git choose "our change" or "their change" instead of stopping with a conflict. Note that you specify this as an argument to git merge, so it applies to all files that have conflicts. It's possible to do this one file at a time, after the conflict happens, in a more intelligent and selective way, using git merge-file, but Git does not make this as easy as it should.

至少,Git 认为已成功合并文件. Git仅基于合并的两边触及同一文件的不同行,而这一定是可以的,,但这实际上并不一定是可以的.不过,它在实践中效果很好.

At least, Git thinks the file is successfully merged. Git is basing this on nothing more than the two sides of the merge touched different lines of the same file and that must be OK, when that's not necessarily actually OK at all. It works pretty well in practice, though.

某些人更喜欢合并工具,该工具通常向您显示所有三个输入文件,并允许您使用构造正确的合并结果. (具体取决于工具).合并工具可以简单地从索引中提取这三个输入,因为它们正好位于三个插槽中.

Some people prefer merge tools, which generally show you all three of the input files and allow you to construct the correct merge result somehow, with the how depending on the tool. A merge tool can simply extract those three inputs from the index, since they are right there in the three slots.

这些也是三向合并操作.他们使用提交图,其方式类似于git show的使用方式.尽管它们将 merge作为动词用作合并代码的一部分,但它们却不如git merge花哨.

These are also three-way merge operations. They use the commit graph, in a fashion similar to the way git show uses it. They are not as fancy as git merge, even though they use the merge as a verb part of the merge code.

相反,我们从您可能拥有的任何提交图开始,例如:

Instead, we start with whatever commit graph you might have, e.g.:

...---o--P--C---o--...
      .      .
       .    .
        .  .
 ...--o---o---H   <-- branch (HEAD)

HP之间以及HC之间的实际关系(如果有)不重要.这里唯一重要的是 current (HEAD)提交是H,并且有一些提交C(孩子),(一个,单个)父提交.也就是说,PC直接是我们要选择或还原的提交的父提交.

The actual relationship, if any, between H and P, and between H and C, is not important. The only thing that matters here is that the current (HEAD) commit is H, and that there is some commit C (the child) with a (one, single) parent commit P. That is, P and C are directly the parent-and-commit of the commit we want to pick or revert.

由于我们正在提交H,因此这就是索引和工作树中的内容.我们的HEAD附加到名为 branch 的分支上,并且 branch 指向提交H.现在,Git会做什么对于git cherry-pick hash-of-C很简单:

Since we're on commit H, that's what is in our index and work-tree. Our HEAD is attached to the branch named branch, and branch points to commit H. Now, what Git does for git cherry-pick hash-of-C is simple:

  • 选择提交P作为合并基础.
  • 使用当前的提交H作为我们的提交并将C作为他们的提交,执行标准的三向合并,将合并为动词部分.
  • Choose commit P as the merge base.
  • Do a standard three-way merge, the merge as a verb part, using the current commit H as ours and commit C as theirs.

此动词合并过程在索引中进行,就像git merge一样.当所有操作成功完成时-或者您清理了混乱的情况,如果不成功并且您运行了git cherry-pick --continue,则Git继续制作普通的,非常规的-合并提交.

This merge-as-a-verb process happens in the index, just as for git merge. When it's all done successfully—or you'e cleaned up the mess, if it wasn't successful, and you've run git cherry-pick --continue—Git goes on to make an ordinary, non-merge commit.

如果您回顾动词合并过程,您会发现这意味着:

If you look back at the merge-as-a-verb process, you'll see that this means:

  • diff commit PC:那就是他们所做的更改
  • diff commit PH:那就是我们所做的更改
  • 组合这些差异,并将其应用于P
  • 中的内容
  • diff commit P vs C: that's what they changed
  • diff commit P vs H: that's what we changed
  • combine these differences, applying them to what's in P

所以git cherry-pick 三向合并.只是他们所做的更改git show将会显示的一样!同时,我们所做的更改是将P转换为H所需的一切,而我们要做则需要它,因为我们想保持 > H作为我们的起点,并且仅添加的更改.

So git cherry-pick is a three-way merge. It's just that what they changed is the same thing that git show would show! Meanwhile, what we changed is everything we need to turn P into H—and we do need that, because we want to keep H as our starting point, and only add their changes to that.

但这也是为什么樱桃采摘有时会看到一些奇怪的冲突(我们认为)的原因和方式.它必须随P -vs- C的变化组合整个P -vs- H的变化.如果PH相距很远,那么这些变化可能会很大.

But this is also how and why cherry-pick sometimes sees some strange—we think—conflicts. It has to combine the entire set of P-vs-H changes with the P-vs-C changes. If P and H are very far apart, those changes could be massive.

git revert命令与git cherry-pick一样简单,并且实际上是由Git中的相同源文件实现的.它所做的只是将commit C作为合并基础,并将P作为其提交(同时像往常一样将H用作我们的).也就是说,Git将比较C与要还原的提交,与H进行比较,以查看我们做了什么.然后,它将对C,要还原的提交和P进行比较,以了解他们的所作所为-当然,这与他们实际所做的相反.然后,合并引擎(将 merge合并为动词的部分)将合并这两组更改,将合并的更改应用于C并将结果放入索引和我们的工作树中.合并结果使我们的更改(CH)保持不变,并且撤消其更改(CP是反向差异).

The git revert command is just as simple as git cherry-pick, and in fact, is implemented by the same source files in Git. All it does is use commit C as the merge base and commit P as their commit (while using H as ours as usual). That is, Git will diff C, the commit to revert, vs H, to see what we did. Then it will diff C, the commit to revert, vs P to see what they did—which is, of course, the reverse of what they actually did. Then the merge engine, the part that implements merge as a verb, will combine these two sets of changes, applying the combined changes to C and putting the result into the index and our work-tree. The combined result keeps our changes (C vs H) and undoes their changes (C vs P being a reverse-diff).

如果一切顺利,我们最终将完成一个完全普通的新提交:

If all goes well, we end up with a perfectly ordinary new commit:

...---o--P--C---o--...
      .      .
       .    .
        .  .
 ...--o---o---H--I   <-- branch (HEAD)

HI的差异(我们将在git show中看到的)是PC更改的副本(樱桃-pick)或P -to- C逆转会更改(还原).

The difference from H to I, which is what we will see with git show, is either a copy of the P-to-C changes (cherry-pick) or a reversal of the P-to-C changes (revert).

除非索引和工作树与当前提交匹配,否则Cherry-pick和revert都将拒绝运行,尽管它们确实具有允许它们不同的模式. 允许与众不同"只是调整期望的问题.以及如果选择或还原失败的事实,则可能无法完全恢复.如果工作树和索引与提交匹配,则很容易从失败的操作中恢复,因此这就是存在此要求的原因.

Both cherry-pick and revert refuse to run unless the index and work-tree match the current commit, though they do have modes that allow them to be different. The "allowed to be different" is just a matter of tweaking expectations. and the fact that if the pick or revert fails, it may be impossible to recover cleanly. If the work-tree and index match the commit, it's easy to recover from a failed operation, so that's why this requirement exists.

这篇关于这是指“由提交引起的更改".在git中的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-03 19:40