问题描述
我的问题是:为什么一个分支/提交被合并,直到我在提交中找到一个合并 code>( .git / MERGE )。
git apply -3 中的 -3 ,可以将其拼写为使用索引字符串放在 git diff 输出中,以便根据需要构建合并基础。当使用 git cherry-pick 和 git revert 执行此操作时,要将它们合并(而不是简单的修补程序), Git使用樱桃选择或恢复提交的父提交结束。这里值得注意的是,Git只在每个文件 的基础上执行此操作,在之后处理修补程序就像一个简单的修补程序失败一样。只有当提交是当前(HEAD)提交的祖先时,使用父提交文件作为三向合并的基本版本才会有帮助。如果它实际上不是这样一个祖先,那么将从base生成的差异与应用的补丁相结合可能没有帮助。尽管如此,Git会将其作为后备。
像往常一样维基百科,我刚刚发现了一些细微的不准确之处 - 比如,可能有两个以上的DAG LCA--但没有时间去处理它,并且这不是一个糟糕的概述。
来抑制这种快进行为。在这种情况下,即使快进是可能的, git merge 会进行新的合并提交。您没有合并作为动词动作(没有工作要做),但是您做获得新合并提交,并且Git更新您的工作树以匹配。
壁球合并不是合并
请注意,我们已经覆盖了两个这里有三种情况:
- 动词形式加形容词/名词形式:正常合并
- 形容词/名词形式的合并,但没有动词:可以快进的合并,但是您运行了 git merge --no-ff
缺少的第三种情况是动词无名词:我们如何获得合并的动作,结合变化,没有名词/合并形式 commit ?这就是squash merges进来的地方。运行 git merge --squash< commit-specifier> 告诉Git照常执行合并操作,但是不是来记录其他分支/提交ID,以便最终 git commit 进行正常的非合并单亲提交。
就是这样 - 就是这样!它只是在最后进行正常的非合并提交。奇怪的是,它会迫使你做出承诺,而不是单独做出承诺。 (没有必要这样做的根本原因,我不知道为什么Git作者选择这样做。)但这些都是机制,而不是策略 :他们告诉你如何进行各种提交,而不是应该做什么,或者什么时候,或者最重要,为什么。
你可以告诉 git merge 它只能 继续执行: git merge --ff-only 。如果新提交可以快进, git merge 更新即可。否则,它只是失败。我做了一个别名, git mff ,这样做,因为通常我想要 git fetch ,然后看看我需要合并,rebase,完全建立一个新的分支,或者其他什么。如果我可以快进,我不需要做任何事情,所以如果 git mff 起作用,我就完成了。 p>
使用什么样的合并,何时,为什么
为什么问题很难,而且和所有的哲学问题一样,没有一个正确的答案(但肯定是一堆错误的:-))。考虑一下这样一个事实:每次你使用 git merge 时,你都可以做一些不同的事情并获得相同的源代码与你最新的提交一起去。对于 git merge 有三个成功的结果(也就是说,您不需要合并 git merge --abort 结束它,而是结束它):
- 快进:没有新的提交;源是现有提交的源。
- true merge:new commit;源代码是合并源代码。
- squash merge:new commit;源代码是合并源代码。
这三者之间的唯一区别(除了明显的无新提交第一个)是记录它们留在提交图中。快进显然会离开 no 记录:图表与之前没有变化,因为你什么都没加。如果这就是你想要的,那就是你应该使用的。在一个你正在关注别人的工作并从未做过任何事情的仓库中,这可能就是你想要的。这也是默认情况下你会得到的,Git会为你工作。
如果你定期进行合并,这留下了合并的记录。所有现有的提交保持完全一样,Git增加了一个新的提交,其中有两个父母。任何后来的人都会看到谁做了什么,什么时候,如何等等。如果这是你想要的,这就是你应该做的。当然,有些工具(比如 git log )会显示谁做了什么,什么时候等,通过显示所有历史的完整图片,可能会掩盖Big图片视图与所有的小细节。换句话说,这就是正面和负面。
如果你做了一个压缩合并,那么就会留下 no 记录合并。你做了一个新的提交,它提取每个合并即动词动作动作,但新提交不是合并即名词。任何后来的人都会看到所有进入的工作,但不是从哪里来的。诸如 git log 之类的工具不能显示细节,而且您和其他人都只会获得 的大图片。再次,这既是正面的也是负面的。但是下面的方面可能会更大一些,因为如果你以后发现你需要那些细节,那么它们就不存在。它们不仅存在于 git log 视图中,它们也不存在于将来的 git merge 中。
如果您从未 变化,这可能不是问题。如果您打算完全删除该分支,放弃所有个人更改 并保留单个集体压缩合并更改,则执行 git的坏部分合并--squash 基本上为零不良值。如果你打算继续在那个分支上工作,并且在以后再次合并,这个特殊的坏点值会大大增加。
如果你正在做压扁合并, code> git log 输出看起来更好(显示更多的大图而不是用太多的细节掩盖它),请注意,有各种 git log 关于的选择性选项,它提交它显示。特别是, - first-commit 避免完全遍历合并分支,只显示合并本身,然后继续执行提交图的主线。例如,您也可以使用 - simplified-by-decoration 来省略所有标记的提交。
那么,在您的reflogs中也是如此;但您的推荐日志是私人的,并最终过期,所以我们会忽略它们。
这里假设他们 - 他们通过重新绑定或删除已发布的提交,不会倒回他们的提交图。如果他们确实删除了已发布的提交,那么Git默认会将这些提交合并回来,就好像它们是您自己的工作一样。这是发布Git存储库的任何人都应该认真考虑倒回这样的提交的原因之一。
$ b
假设没有奇妙的章鱼合并。 p>
I was wondering how all the tools know what branches/commits are merged until I found a "Merge" header in the commit.
My question is: why a git merge --squash does not add that header, while git merge does?
In other words: why do I see a merge edge when merging with git merge while there is no edge with git merge --squash?
Thank you.
some amended information:
With "merge-header" I mean the second line in git log after merging:
commit 7777777 Merge: 0123456 9876543 Author: Some Body <...> Date: Fri ....
... while a git merge --squash will not produce that line and my assumption is that git gui tools reads that header to be able to draw that 'merge edge' see the following image.
My question is again the same, see above.
TL;DR: git merge --squash merges (verb), but does not make a merge (noun).
The "merge header" you mention is not in the commits in that form. Instead, it's just something that git log prints when it comes across a merge commit. To really cement this idea, let's look at what merging does—what "to merge" means as a verb—and what a merge is. More properly, let's look at what a merge commit is, or what "merge" means as an adjective modifying the word "commit". We'll be lazy, like good Git users, though, and shorten it from "merge commit" to "a merge", making it a noun.
Merge as an noun
Let's look at the noun usage first, since that's simpler. In Git, a merge commit is simply a commit with at least two parent commits. It really is that simple. Every commit has some number of parents. A root commit has zero parents (and most repositories have only one root commit, which is the first commit ever made, which obviously cannot have had a parent commit). Most ordinary commits have just one parent, and all the other commits have two or more parents and are therefore merge commits. Then we shorten the phrase "merge commit" to just "a merge", changing "merge" from an adjective (modifying "commit") to a noun (meaning "a merge commit").
If a merge commit is a commit with two parents, then git merge must make merge commits, and to merge should mean "to make a merge commit"—and it does, but only sometimes. We will see when—and more importantly, why—soon.
Other VCSes may stop at two. Mercurial does, for instance: no hg commit ever has more than two parents. Git, however, allows any commit to have any number of parents. A Git commit with three or more parents is called an octopus merge, and normally these commits are made using git merge -s octopus or equivalent, e.g., git merge topic1 topic2 topic3: running git merge with extra commit specifiers implies -s octopus. These do nothing that cannot be done using a series of two-parent merges, so Git's octopus merges do not give it more power than Mercurial, but octopus merges are sometimes convenient, and good for showing off your Git Fu. :-)
Merge as a verb
The verb form of to merge is much more complicated—or at least, it is in Git. We can distinguish between two major forms of merging as well: there's the act of merging source code changes, and then there's the act of merging branches. (This brings in the question of "what is a branch". There's a lot more to this question than you might think, so see What exactly do we mean by "branch"?) Many Git commands can do the verb kind of merging, including git cherry-pick and git revert, both of which are essentially a form of git apply -3. Of course git merge is the most obvious way to do it, and when it does, you get the verb form of merging changes.
Merging changes
Merging source changes is more properly called "three way merging". For more about this, see the Wikipedia article or VonC's answer to Why is a 3-way merge advantageous over a 2-way merge? The details can get quite complex, but the goal of this merge process is simple enough: we want to combine changes made to some common base, i.e., given some starting point B, we find changes C and C and combine them to make a single new change C, then add that change to the base B to get a new version.
In Git, the act of merging sources—of doing the three-way merge itself—uses Git's index, also called the "staging area". Normally, the index has just one entry for each file that will go into the next commit. When you git add a file, you tell Git to update the staged version of the file, replacing the one in the current index, or if the file was previously "untracked", adding the file to the index so that it is now tracked and also staged for the next commit. During the merge process, however, the index has up to three entries for each file: one from the merge base version, one entry for the file-version to be treated as change #1 (also called "ours"), and one for change #2 ("theirs"). If Git is able to combine the changes on its own, it replaces the three entries with one regular (staged-for-commit) entry. Otherwise it stops with a conflict, leaving the conflict marked in the work-tree version of the file. You must resolve the conflict yourself (presumably editing the file in the process), and use git add to replace the three special conflicted-merge index versions with the one normal staged version.
Once all conflicts are resolved, Git will be able to make a new commit.
Making a merge commit
The last thing a normal, merge-making git merge does is to make the merge commit. Again, if there were no conflicts, Git can just do this on its own: git merge merges the changes, git adds each merge-result to update the staged files, and runs git commit for you. Or, if there were conflicts, you fix them, you run git add, and you run git commit. In all cases, it's actually git commit, rather than git merge itself, that makes the merge commit.
This last part is actually very easy, since the index / staging-area is all set up. Git just makes a commit as usual, except that instead of giving it the current (HEAD) commit's ID as its one single parent, it gives it at least two parent commit IDs: the first one is the HEAD commit as usual, and the rest come from a file left behind by git merge (.git/MERGE).
The -3 in git apply -3, which can be spelled out as --3way, directs git apply to use the index string in git diff output to construct a merge base if needed. When doing this with git cherry-pick and git revert, to turn them into merges (instead of straightforward patches), Git winds up using the parent commit of the cherry-picked or reverted commit. It's worth noting here that Git does this only on a per file basis, after treating the patch just as a simple patch has failed. Using the parent commit's file as a base version for a three-way merge will normally help only if that commit is an ancestor of the current (HEAD) commit. If it's not actually such an ancestor, combining the diff generated from "base" to HEAD with the patch being applied is probably not helpful. Still, Git will do it as a fallback.
As usual for Wikipedia, I spotted some minor inaccuracies in it just now—for instance, it's possible to have more than two DAG LCAs—but don't have time to work on it, and it's not a bad overview.
Often, it never bothers to make the conflicted entries in the first place. Git will, if possible, short-cut-away even the git diff phase. Suppose for instance that the base commit has four files in it: u.txt is unchanged from base in either commit, one.txt is changed from base to HEAD but not from base to the other commit, two.txt is changed from base to the other commit but not from base to HEAD, and three.txt is changed in both. Git will simply copy u.txt straight through, take one.txt from HEAD, take two.txt from the other commit, and only bother to generate diffs, then try to merge them, for three.txt. This goes pretty fast, but does mean that if you have your own special three-way-merge program for these files, it never gets run for u.txt, one.txt, and two.txt, only for three.txt.
I am not sure off-hand whether Git makes the multiple index entries before attempting the merging of diffs, or after attempting and failing. It does, however, have to make all three entries before running custom merge drivers.
Non-merge "merges"
The above sequence—check out some commit (usually a branch tip), run git merge on another commit (usually some other branch tip), find a suitable merge base, make two sets of diffs, combine the diffs, and commit the result—is how normal merges work, and how Git makes merge commits. We merge (as a verb) the changes, and make a merge (adjective) commit (or "make a merge", noun). But, as we noted earlier, git merge doesn't always do this.
Fast-forwards
Sometimes git merge says it's "doing a fast-forward merge". This is a little bit of a misnomer, because "fast-forwarding" is more accurately considered a property of a branch label change, in Git. There are two other commands that use this property, git fetch and git push, which distinguish between a normal (or "fast-forward") branch update and a "forced" update. A proper discussion of fast-forwarding requires getting into the details of the commit graph, so all I will say here is that it occurs when you move a branch label from commit O (old) to commit N (new), and commit N has commit O as an ancestor.
When git merge detects that your merge argument is one of these cases—that HEAD is an ancestor of this other commit—it will normally invoke this fast-forward operation instead. In this case, Git just uses the commit you told it to merge. There's no new commit at all, just the re-use of some existing commit. Git does not make a merge commit, nor does it do any merging-as-a-verb. It just changes you over to the new commit, almost as if by git reset --hard: moving the current branch label and updating the work-tree.
You can suppress this fast-forward action with --no-ff. In this case, git merge will make a new merge commit even if a fast-forward is possible. You get no merge-as-a-verb action (there's no work to do) but you do get a new merge commit, and Git updates your work-tree to match.
Squash merge is not a merge
Note that we have covered two of three cases here:
- verb form plus adjective/noun form: a normal merge
- adjective/noun form of merge, but no verb: a merge that was fast-forward-able, but you ran git merge --no-ff
The missing third case is verb-without-noun: how do we get the action of a merge, combining changes, without the noun/adjective form of a merge commit? This is where "squash merges" come in. Running git merge --squash <commit-specifier> tells Git to do the merge action as usual, but not to record the other branch / commit-ID, so that the final git commit makes a normal, non-merge, single-parent commit.
That's really it—that's all it does! It just makes a normal, non-merge commit at the end. Oddly, it forces you to make that commit, instead of making it on its own. (There is no fundamental reason that it has to do this, and I don't know why the Git authors chose to make it behave this way.) But these are all mechanisms, not policies: they tell you how to make various kinds of commits, but not which ones you should make, or when, or—most important—why.
You can tell git merge that it should only proceed if it can fast-forward: git merge --ff-only. If the new commit is fast-forward-able, git merge updates to it. Otherwise it simply fails. I made an alias, git mff, that does this, since normally I want to git fetch and then see whether I need to merge, rebase, make a new branch entirely, or whatever. If I can fast-forward, I don't need to do anything else, so if git mff works, I'm done.
What kind of merge to use, when, why
The why question is hard, and like all philosophy questions, has no one right answer (but definitely a bunch of wrong ones :-) ). Consider this fact: Every time you use git merge at all, you could have done something different and gotten the same source code to go with your newest commit. There are three successful outcomes for a git merge (that is, a merge where you do not git merge --abort to end it, but rather conclude it successfully):
- fast-forward: no new commit; the source is that of an existing commit.
- true merge: new commit; the source is the combined source.
- squash merge: new commit; the source is the combined source.
The only difference between these three (aside from the obvious "no new commit at all" for the first one) is the record they leave behind in the commit graph. A fast-forward obviously leaves no record: the graph is unchanged from before, because you added nothing. If that's what you want, that's what you should use. In a repository where you are following someone else's work and never doing anything of your own, this is probably what you want. It is also what you will get by default, and Git will "just work" for you.
If you do a regular merge, that leaves a record of the merge. All the existing commits remain exactly as they are, and Git adds one new commit, with two parents. Anyone coming along later will see just who did what, when, how, etc. If this is what you want, this is what you should do. Of course, some tools (like git log) will show who did what, when, etc., which—by showing a complete picture of all of history—may obscure the Big Picture view with all the little details. That's both the up-side and the down-side, in other words.
If you do a squash merge, that leaves no record of the merge. You make a new commit that picks up every merge-as-a-verb action, but the new commit is not a merge-as-a-noun. Anyone coming along later will see all the work that went in, but not where it came from. Tools like git log cannot show the little details, and you—and everyone else—will get only a Big Picture. Again, that is both the up-side and the down-side. But the down-side is perhaps a bit bigger, because if you find that you need those details later, they are not there. They are not only not there in the git log view, they are also not there for a future git merge.
If you are never going to do a future git merge of the squashed-in changes, that might not be a problem. If you plan to delete that branch entirely, giving up all the individual changes as individuals and keeping only the single collective squash-merge change, the "bad" part of doing the git merge --squash has essentially zero "badness value". If you intend to keep working on that branch, though, and merge it again later, that particular badness value increases hugely.
If you are doing squash merges specifically to make git log output "look nicer" (show more of a Big Picture instead of obscuring it with too many details), note that there are various git log options designed to be selective about which commits it shows. In particular, --first-commit avoids traversing merged-in branches entirely, showing only the merge itself and then continuing down the "main line" of the commit graph. You can also use --simplify-by-decoration to omit all but tagged commits, for instance.
Well, also in your reflogs; but your reflogs are private, and eventually expire, so we'll just ignore them.
This assumes that they—whoever "they" are—do not "rewind" their commit graph, by rebasing or removing published commits. If they do remove published commits, your Git will by default merge those commits back in, as if they were your own work. This is one reason anyone publishing a Git repository should think hard about "rewinding" such commits.
Assuming no fancy octopus merges.
这篇关于git`merge --squash`不会添加“合并”头提交的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!