Git 说分支已合并,但更改显然不存在
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/43927066/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Git says branch is merged, but changes are apparently not present
提问by Robert Dodier
I've worked myself into a situation that is not making sense to me. I'll try to describe it as best I can.
我已经让自己陷入了一种对我来说毫无意义的境地。我会尽力描述它。
I have a development branch and I've merged master into it via git checkout develpment && git merge master
. I didn't get any merge conflicts here.
我有一个开发分支,我已经通过git checkout develpment && git merge master
. 我在这里没有遇到任何合并冲突。
There is a specific commit that I'm interested in, let's say it's abcd123
. When I run git branch --contains abcd123
, it reports that both development
and master
contain abcd123
. When I do git log
, it shows abcd123
in the list of commits, both on development
and on master
.
有一个我感兴趣的特定提交,假设它是abcd123
. 当我运行git branch --contains abcd123
,它报告这两个development
和master
包含abcd123
。当我这样做时git log
,它会显示abcd123
在提交列表中,包括 ondevelopment
和 on master
。
git show abcd123
shows that contains changes to two files. But I can't seem to find these changes. When I look at the files, I don't see those changes, neither on development
nor on master
. When I inspect git log -- path/to/file1
, I don't see abcd123
, same for git log -- path/to/file2
.
git show abcd123
显示包含对两个文件的更改。但我似乎无法找到这些变化。当我查看文件时,我没有看到这些更改,无论是 ondevelopment
还是 on master
。当我检查时git log -- path/to/file1
,我没有看到abcd123
,对于git log -- path/to/file2
.
What's going on here? How can the commit be present, but the changes are apparently not there?
这里发生了什么?提交如何存在,但更改显然不存在?
It is possible that abcd123
was originally introduced in another branch (other than development
) that was merged into master
. I don't know if that could make a difference.
可能abcd123
最初development
是在合并到master
. 我不知道这是否会有所作为。
By the way, when I try git checkout master && git merge development
(after merging master
into development
as shown above) I get a lot of merge conflicts, including file1 and file2. So that seems to show that master
was not actually merged into development
-- shouldn't git merge development
succeed if git merge master
was already executed? To cause more confusion, git branch --merged development
says that master
has been merged into development
. I guess that is consistent with git merge master
....
顺便说一句,当我尝试git checkout master && git merge development
(合并后master
为development
如上图所示),我得到了很多的合并冲突,包括文件1和文件2的。所以这似乎表明master
实际上并没有合并到development
-git merge development
如果git merge master
已经执行,应该不会成功?为了引起更多混乱,git branch --merged development
表示master
已合并到development
. 我想这与git merge master
...一致
Any advice at this point is much appreciated.
非常感谢此时的任何建议。
EDIT: At this point it appears that the problem is due to a merge that failed or was messed up in some way. If anyone is still reading, I think the direction that torek's answer is going seems most fruitful.
编辑:此时看来问题是由于合并失败或以某种方式搞砸了。如果有人还在阅读,我认为 torek 的答案的方向似乎最有成效。
回答by torek
This answer is long, because there is a lot going on here. The TL;DR summary, though, is probably that you want --full-history
.
这个答案很长,因为这里发生了很多事情。不过,TL;DR 摘要可能正是您想要的--full-history
。
There are multiple separate issues here that need to be untangled:
这里有多个单独的问题需要解决:
- The phrase "show changes", or what you see in
git log -p
orgit show
, often leads people down the wrong path in interpreting what Git stores. - The
git log
command sometimes lies to you (especially around merges), purely in the interest of not overwhelming you with useless information. - What
git merge
does can be a bit tricky. It's straightforward in principle, but most people don't get it right away.
- 短语“显示更改”,或者您在
git log -p
或 中看到的内容git show
,通常会导致人们在解释 Git 存储的内容时走上错误的道路。 - 该
git log
命令有时你撒谎(特别是围绕合并),纯粹是不是无用的信息铺天盖地,你的兴趣。 - 做什么
git merge
可能有点棘手。原则上很简单,但大多数人不会马上明白。
Ordinary commits
普通提交
Let's look first at Git's most common, ordinary commits. A commit, in Git, is a snapshot(of file-contents by file names). You make one commit, then you change a few things and git add
a changed file or two and make a second commit, and the second commit has all the same filesas the first commit, except for the ones overwritten by the git add
.
让我们先来看看 Git 最常见的普通提交。在 Git 中,提交是一个快照(按文件名的文件内容)。您进行一次提交,然后更改一些内容和git add
一两个更改的文件并进行第二次提交,第二次提交的所有文件与第一次提交相同,除了被git add
.
It's worth drawing these as parts of Git's commit graph. Note that each commit has its own unique hash ID (one of those impossible-to-remember strings like 93aefc
or badf00d
or cafedad
), plus the ID of a parent(or previous) commit. The parent commit hash lets Git string these things together, in a backwards fashion:
值得将它们绘制为 Git提交图的一部分。请注意,每个提交都有自己唯一的哈希 ID(像93aefc
orbadf00d
或那样难以记住的字符串之一cafedad
),加上父(或之前)提交的 ID 。父提交哈希让 Git 以一种向后的方式将这些东西串在一起:
... <-E <-F <-G ...
where each uppercase letter stands in for a hash ID, and the arrows cover the idea that each commit "points back" to its parent. Normally we don't need to draw in the internal arrows (they're not very interesting in the end) so I draw these as:
其中每个大写字母代表一个哈希 ID,箭头涵盖了每个提交“指向”其父级的想法。通常我们不需要绘制内部箭头(它们最终不是很有趣)所以我将它们绘制为:
...--E--F--G <-- master
The namemaster
, however, still deserves an arrow, because the commit to which this arrow points will change over time.
该名称master
,但是,仍然值得一支箭,因为承诺此箭头指向会随着时间而改变。
If we pick a commit like G
and view it withoutusing git log -p
or git show
, we will see every file in full, exactly as it is stored in the commit. In fact, that's what happens when we use git checkout
to check it out: we extract all the files in full, into the work-tree, so that we can see and work on them. But when we view it with git log -p
or git show
, Git doesn't show us everything; it only shows us what changed.
如果我们选择像这样的提交G
并查看它而不使用git log -p
或git show
,我们将完整地看到每个文件,就像它存储在提交中一样。事实上,这就是我们git checkout
用来检查它时发生的情况:我们将所有文件完整提取到工作树中,以便我们可以查看和处理它们。但是当我们用git log -p
or查看它时git show
,Git 不会向我们展示一切;它只向我们展示了发生了什么变化。
To do this, Git extracts both the commit andits parent commit, and then runs a big git diff
on the pair. Whatever is different between the parent F
and the child G
, that's what changed, so that's what git log -p
or git show
shows you.
为此,Git 提取提交及其父提交,然后git diff
在这对提交上运行一个 big 。无论父母F
和孩子之间有什么不同G
,那就是改变了,所以这就是git log -p
或git show
向您展示的。
Merge commits
合并提交
This is all well and good for ordinary, single-parent commits, but it doesn't work for mergecommits. A merge commit is simply any commit with two (or more, but we won't worry about this case) parent commits. You get these by doing a successful git merge
, and we might draw that like this. We start with the two branches (which fork off from some starting-point):
这对于普通的单亲提交都很好,但它不适用于合并提交。合并提交只是具有两个(或更多,但我们不会担心这种情况)父提交的任何提交。你通过做一个成功的 得到这些git merge
,我们可能会像这样画。我们从两个分支开始(从某个起点分叉出来):
H--I <-- development (HEAD)
/
...--E--F--G <-- master
and then we run git merge master
.1Git now tries to combine the two branches. If it succeeds, it makes one new commit that has twoparents:
然后我们运行git merge master
。1Git 现在尝试合并这两个分支。如果成功,它会进行一个具有两个父项的新提交:
H--I--J <-- development (HEAD)
/ /
...--E--F--G <-- master
The name development
now points to the new merge commit J
. The parenthesized (HEAD)
here denotes that this is our current branch. That tells us which namegets moved: we make a new commit—including any new merge commit—and development
is the branch-name that changes to point to the new commit.
该名称development
现在指向新的合并提交J
。(HEAD)
这里的括号表示这是我们当前的分支。这告诉我们哪个名称被移动了:我们进行了一个新的提交——包括任何新的合并提交——并且development
是更改为指向新提交的分支名称。
If we don't worry about how the contents(the various committed files) of the merge commit are determined, this is all pretty straightforward. The merge commit is like any other commit: it has a complete snapshot, a bunch of files with contents. We check out the merge commit, and those contents get in our work-tree as usual.
如果我们不担心合并提交的内容(各种提交的文件)是如何确定的,这一切都非常简单。合并提交就像任何其他提交:它有一个完整的快照,一堆包含内容的文件。我们检查合并提交,这些内容像往常一样进入我们的工作树。
But when we go to viewthe merge commit ... well, Git normally diffs a commit against its parent. The merge has twoparents, one for each branch. Which one should Git diff against, to show you changes?
但是当我们去查看合并提交时……好吧,Git 通常会将提交与其父提交进行比较。合并有两个父级,每个分支一个。Git 应该与哪一个进行比较,以向您展示更改?
Here, git log
and git show
take different approaches. When you view the commit with git log
, it shows nothing at allby default. It won't choose I
-vs-J
, and it won't choose G
-vs-J
either! It just shows nothing at all, for git log -p
.
在这里,git log
并git show
采取不同的方法。当您使用 来查看提交时git log
,默认情况下它根本不显示任何内容。它不会选择I
-vs- J
,也不会选择G
-vs- J
!它只是什么都不显示,因为git log -p
.
1In some Git workflows, merging frommaster into any other branch is discouraged. It can work, though, and since you did, let's run with it.
1在某些 Git 工作流程中,不鼓励从master合并到任何其他分支。不过,它可以工作,既然你做到了,让我们运行它。
Viewing merge commits
查看合并提交
The git show
command does something different and better. It runs twogit diff
s, one for I
-vs-J
and one for G
-vs-J
. It then tries to combinethe two diffs, showing you only what changed in both. That is, where J
is different from I
but not in a particularly interestingway, Git suppresses the difference. Where J
is different from G
but not in a particularly interestingway, Git suppresses this difference as well. This is probably the most useful mode, so it's what git show
shows. It's still quite imperfect, but nothing you can do here is perfect for all purposes.
该git show
命令做了一些不同的和更好的事情。它运行两个git diff
s,一个用于I
-vs- J
,另一个用于G
-vs- J
。然后它会尝试组合这两个差异,仅向您显示两者中的变化。也就是说,在J
不同于I
但不是特别有趣的方式的地方,Git 抑制了差异。凡J
不同于G
但不是在一个特别有趣的方式,Git的抑制这种差异也是如此。这可能是最有用的模式,所以这就是git show
显示的内容。它仍然很不完美,但是您在这里所做的一切都不是万能的。
You can make git log
do this same thing by adding --cc
to the git log -p
options. Or, you can change how eithergit log
orgit show
shows a merge commit by using -m
(note one dash for -m
, two for --cc
, by the way).
您可以git log
通过添加这样做同样的事情--cc
的git log -p
选项。或者,您可以更改任何git log
或git show
合并使用显示提交-m
(注:对于一个破折号-m
,二为--cc
,顺便说一句)。
The -m
option tells Git that for viewing purposes, it should splitthe merge. Now Git compares I
to J
, to show you everything you brought in through merging G
. Then Git compares G
to the split-off extra version of J
, to show you everything you brought in through merging I
. The resulting diff is usually very large but (or because) it shows you everything.
该-m
选项告诉 Git,为了查看目的,它应该拆分合并。现在Git的比较I
来J
,向你展示你在通过合并所带来的一切G
。然后Git的比较G
来的拆过额外的版本J
,向你展示你在通过合并所带来的一切I
。结果差异通常非常大,但(或因为)它向您展示了一切。
There are more ways to try to find what happened to some file, but we need to hold off a moment before getting to your:
有更多方法可以尝试找出某个文件发生了什么情况,但我们需要稍等片刻,然后才能找到您的:
git log -- path/to/file1
command. Just as we saw git log
skippingmerges, it may skip even more things here (but there are ways to stop Git from doing that).
命令。就像我们看到git log
跳过合并一样,这里可能会跳过更多的东西(但有一些方法可以阻止 Git 这样做)。
Actually making merges: how Git builds the merge's contents
实际进行合并:Git 如何构建合并的内容
Let's look at that pre-merge graph again:
让我们再看看那个预合并图:
H--I <-- development (HEAD)
/
...--E--F--G <-- master
Here, there are two commits on branch development
that are not on branch master
, and two commits on master
that are not on development
. Commit E
(along with allearlier commits) is on bothbranches. Commit E
is special, though: it's the most recent2commit that's on both branches. Commit E
is what Git calls the merge base.
在这里, branch 上有两个提交development
不在 branch 上master
,并且有两个提交master
不在 上development
。提交E
(以及所有较早的提交)在两个分支上。但是,提交E
很特别:它是两个分支上最近的2 次提交。提交E
是 Git 所说的合并基础。
To perform a merge, Git effectively runs two git diff
commands:
为了执行合并,Git 有效地运行了两个git diff
命令:
git diff E I
git diff E G
The first produces a set of changes to various files, which are "what we did on branch development
". It is, in effect, the sum of H
and I
if they are treated as patches. The second produces a—probably different—set of changes to various files, "what we did on master
", and as before it's effectively the sum of F
and G
as patches.
第一个对各种文件产生一组更改,这些更改是“我们在分支上所做的development
”。实际上,它是H
和I
如果它们被视为补丁的总和。第二个产生,可能不同的设定“我们所做的更改各种文件的master
”,和之前有效的总和F
,并G
作为补丁。
Git then tries to combine these two diffs. Whatever is completely independent between them, it takes bothsets of changes, applies them to the contents of commit E
, and uses that as the result. Wherever the two change-sets touch the same line in the samefile, Git tries to see if it can just take one copy of that change. If both fixed the spelling of a word on line 33 of file README
, Git can just take one copyof the spelling fix. But wherever the two change-sets touch the same line of the same file, but make a differentchange, Git declares a "merge conflict", throws its metaphorical hands in the air, and makes youfix up the resulting mess.
Git 然后尝试结合这两个差异。无论是他们之间完全独立的,它需要两个组变化的,它们适用于犯下的内容E
,并将其用作结果。无论两个变更集在同一个文件中的同一行接触到哪里,Git 都会尝试查看它是否可以只获取该变更的一个副本。如果两者都修复了 file 的第 33 行单词的拼写README
,Git 只能获取一份拼写修复的副本。但是,只要两个变更集接触同一个文件的同一行,但进行不同的更改,Git 就会声明“合并冲突”,将其比喻性的双手抛在空中,并让您修复由此产生的混乱。
If you (or whoever does the merge) wants to, they can stop Git from committing the merge result even if Git thinks it all went swimmingly: git merge --no-commit master
makes Git stop after combining everything. At this point, you can open work-tree files in your editor, changethem, write them back, git add
the changed file, and git commit
the merge to put something in the merge that did not come from anyof the three inputs (base and two branch-tips).
如果您(或进行合并的任何人)愿意,他们可以阻止 Git 提交合并结果,即使 Git 认为一切进展顺利:git merge --no-commit master
在合并所有内容后让 Git 停止。此时,您可以在编辑器中打开工作树文件,更改它们,将它们写回,git add
更改后的文件和git commit
合并以将不是来自三个输入(基本和两个分支)中的任何一个的内容放入合并中-提示)。
In any case, the key to understanding all of this is the concept of the merge basecommit. If you sit down and draw the commit graph, the merge base is usually pretty obvious unless the graph gets way out of hand (which happens a lot, actually). You can also have Git find the merge base for you—or merge bases, plural, in some cases:
无论如何,理解所有这些的关键是合并基础提交的概念。如果您坐下来绘制提交图,合并基础通常非常明显,除非图失控(实际上这种情况经常发生)。你也可以让 Git 为你找到合并基数——或者在某些情况下合并基数,复数:
git merge-base --all master development
This prints out a hash ID. In our hypothetical case here, that would be the hash ID of commit E
. Once you have that, you can run git diff
manually, to see what happened to every file. Or you can run an individual-file git diff
:
这会打印出一个哈希 ID。在我们这里的假设案例中,这将是 commit 的哈希 ID E
。一旦你有了它,你就可以git diff
手动运行,看看每个文件发生了什么。或者你可以运行一个单独的文件git diff
:
git diff E development -- path/to/file1
git diff E master -- path/to/file1
Note that if you replace the names master
and development
with the hash IDs of the commits that werecurrent beforeyou did a git merge
, this works even afterthe merge. That will tell you what Git thought it should combine for path/to/file1
. That, in turn, will tell you whether Git did not see the change, or whether whoever made the merge overrodeGit, or handled a conflicting merge incorrectly.
请注意,如果您在执行a之前将名称master
和当前development
提交的哈希 ID替换为,则即使在合并之后也可以使用。这会告诉你 Git 认为它应该结合什么。反过来,这将告诉您 Git 是否没有看到更改,或者进行合并的人是否覆盖了Git,或者错误地处理了冲突合并。git merge
path/to/file1
Once you have a merge, a subsequent merge will find a different merge base:
合并后,后续合并将找到不同的合并基础:
H--I--J----K <-- development
/ /
...--E--F--G--L--M <-- master
We look now at both branch tips and work our way backwards through history (in the leftward direction), following both forks of a merge like J
, to find the first commit we can get to from bothbranch tips. Starting at K
, we go back to J
, then to both I
and G
. Starting at M
, we go back to L
, then to G
. We find G
to be on both branches, so commit G
is the new merge base. Git will run:
我们现在查看两个分支提示,并通过历史(向左方向)向后工作,遵循像 一样的合并的两个分支J
,以找到我们可以从两个分支提示中获得的第一个提交。从 开始K
,我们回到J
,然后到I
和G
。开始M
,我们回到L
,然后到G
。我们发现G
在两个分支上,所以 commitG
是新的合并基础。Git 将运行:
git diff G K
git diff G M
to get the two change-sets to apply to merge-base commit G
.
将两个更改集应用于 merge-base commit G
。
2"Most recent" here refers to commit graphorder, rather than time stamps, although it's probably also the commit with the newest time stamp that is on both branches.
2这里的“最近”指的是提交图顺序,而不是时间戳,尽管它可能也是两个分支上具有最新时间戳的提交。
Git log and simplifications that lie, again
Git 日志和简化再次撒谎
We already saw that git log -p
just skips right over merge commits. You don't see any diff at all, as if the merge were totally magic. But when you run:
我们已经看到git log -p
直接跳过合并提交。您根本看不到任何差异,就好像合并完全是魔术一样。但是当你运行时:
git log -- path/to/file1
something else, even more insidious, happens. This is described, albeit rather opaquely, in the (long) git log
documentationunder the section titled History Simplification.
其他更阴险的事情发生了。这在标题为History Simplification的部分下的(长)git log
文档中描述,尽管相当不透明。
In our example above, suppose git log
is walking from K
backwards. It finds J
, which is a merge. It then inspects both I
and G
, comparing each to J
after excluding all but the one file you are looking at.That is, it's just comparing path/to/file1
in the three commits.
在我们上面的例子中,假设git log
是从K
后面走。它发现J
,这是一个合并。然后检查I
和G
,J
在排除除您正在查看的一个文件之外的所有文件后将每个文件与 和进行比较。也就是说,它只是path/to/file1
在三个提交中进行比较。
If one side of the merge doesn'tshow any change to path/to/file1
, that means the merge result J
was no different from the input (I
or G
). Git calls this "TREESAME". If the merge J
, after being stripped down to this one file, matches I
or G
similarly stripped-down, then J
is TREESAME to I
or G
(or perhaps both). In this case, Git picks the, or any one of the, TREESAME parent(s) and looks onlyat that path. Let's say it picks I
(along the top row) rather than G
(along the bottom).
如果合并的一侧没有显示对 的任何更改path/to/file1
,则意味着合并结果J
与输入(I
或G
)没有区别。Git 称之为“TREESAME”。如果合并J
,在被剥离到这个文件之后,匹配I
或G
类似地被剥离,那么J
TREESAME 到I
或G
(或可能两者)。在这种情况下,Git 会选择 TREESAME 父级或其中任何一个,并且只查看该路径。假设它选择I
(沿着顶行)而不是G
(沿着底部)。
What this means in our case is that if someone dropped the ball during a merge, losing a change that was supposed to come in to J
from F
, git log
never shows it. The log
command looks at K
, then J
, then looks at but drops G
, and looks only at I
, then H
, then E
, and then any earlier commits. It never looks at commit F
at all! So we don't see the change to path/to/file1
from F
.
这意味着在我们的情况是,如果有人在合并过程中放弃了球,失去了本来应该在给来改变J
从F
,git log
从不显示它。该log
命令查看K
, then J
,然后查看 but drops G
,并且只查看I
, then H
, then E
,然后是任何更早的提交。它从不考虑提交F
!所以我们看不到path/to/file1
from的变化F
。
The logic here is simple; I'll quote the git log
documentation directly but add some emphasis:
这里的逻辑很简单;我会git log
直接引用文档,但要强调一下:
[Default mode] Simplifies the history to the simplest history explaining the final state of the tree. Simplest because it prunes some side branches if the end result is the same...
[默认模式] 将历史简化为解释树最终状态的最简单历史。最简单,因为如果最终结果相同,它会修剪一些侧枝......
Since the changes in F
were dropped, Git declares them to be irrelevant. You don't need to see them! We'll just ignore that side of the merge entirely!
由于 中的更改F
被删除,Git 声明它们是不相关的。你不需要看到他们!我们将完全忽略合并的那一侧!
You can defeat this completely with --full-history
. That tells Git notto prune either side of a merge: it should look down both histories. This will find commit F
for you. Adding -m -p
should also find wherethe changes were dropped, since it will find allcommits that touch the file:
你可以用 完全打败它--full-history
。这告诉 Git不要修剪合并的任何一侧:它应该查看两个历史记录。这将为F
您找到提交。添加-m -p
应该也发现其中的变化被丢弃,因为它会发现所有的那一抹文件提交:
git log -m -p --full-history -- path/to/file1
If the changes were there (in commit F
in our example) and are no longer, there are only two ways they were lost:
如果更改在那里(在F
我们的示例中提交)并且不再存在,那么它们丢失的方式只有两种:
They were reverted (either with
git revert
, or manually) in an ordinary commit. You would see this as a commit that touchespath/to/file1
in the history even without-m
;-p
will show you the diff.Or, they were lost by being dropped during a merge. You would see the merge even without
-m
, but not know for sure that whoever did the merge dropped the ball here; but-m -p
will show both parent diffs, including the one that should have(but did not) take the change.
它们
git revert
在普通提交中被恢复(使用或手动)。path/to/file1
即使没有-m
,您也会将此视为触及历史的提交;-p
将向您展示差异。或者,它们因在合并期间被丢弃而丢失。即使没有
-m
,您也会看到合并,但不确定进行合并的人是否将球丢在这里;但-m -p
将显示两个父差异,包括应该(但没有)进行更改的差异。