这个答案很长,因为这里发生了很多事情。不过,TL;DR 摘要可能就是您想要的--full-history
.
这里有多个单独的问题需要解决:
- 短语“显示更改”,或者您在其中看到的内容
git log -p
or git show
,经常会导致人们在解释 Git 存储的内容时走上错误的道路。
- The
git log
命令有时会对你撒谎(尤其是在合并方面),纯粹是为了不让你被无用的信息淹没。
- What
git merge
确实可能有点棘手。原则上很简单,但大多数人并不能立即理解。
普通提交
首先让我们看一下 Git 最常见的普通提交。在 Git 中,提交是一个snapshot(按文件名的文件内容)。您做出一项承诺,然后更改一些内容,然后git add
更改一个或两个文件并进行第二次提交,第二次提交已所有相同的文件作为第一次提交,除了那些被git add
.
值得将它们绘制为 Git 的一部分提交图。请注意,每个提交都有其自己唯一的哈希 ID(那些难以记住的字符串之一,例如93aefc
or badf00d
or cafedad
),加上一个IDparent(或之前的)提交。父提交哈希让 Git 以向后的方式将这些东西串在一起:
... <-E <-F <-G ...
其中每个大写字母代表一个哈希 ID,箭头涵盖了每个提交“指向”其父级的想法。通常我们不需要绘制内部箭头(它们最终不是很有趣),所以我将它们绘制为:
...--E--F--G <-- master
The name master
然而,仍然值得一个箭头,因为该箭头指向的提交会随着时间的推移而改变。
如果我们选择像这样的提交G
并查看它without using git log -p
or git show
,我们将完整地看到每个文件,就像它存储在提交中一样。事实上,这就是我们使用时发生的情况git checkout
检查一下:我们将所有文件完整地提取到工作树中,以便我们可以查看并处理它们。但是当我们用git log -p
or git show
,Git 并没有向我们展示一切;它只告诉我们什么changed.
为此,Git 提取提交and它的父提交,然后运行一个大的git diff
在这对上。无论父母之间有什么不同F
和孩子G
,就是这样changed,所以这就是git log -p
or git show
向您展示。
合并提交
这对于普通的单亲提交来说一切都很好,但它不适用于merge承诺。合并提交只是具有两个(或更多,但我们不会担心这种情况)父提交的任何提交。你可以通过成功地获得这些git merge
,我们可以这样画。我们从两个分支开始(从某个起点分叉出来):
H--I <-- development (HEAD)
/
...--E--F--G <-- master
and then we run git merge master
.1 Git now tries to combine the two branches. If it succeeds, it makes one new commit that has two parents:
H--I--J <-- development (HEAD)
/ /
...--E--F--G <-- master
名字development
现在指向新的合并提交J
。括号内的(HEAD)
这里表示这是我们的当前分支。这告诉我们哪个name被移动:我们进行一个新的提交——包括任何新的合并提交——并且development
是更改为指向新提交的分支名称。
如果我们不担心如何contents确定合并提交(各种提交的文件),这一切都非常简单。合并提交就像任何其他提交一样:它有一个完整的快照,一堆包含内容的文件。我们检查合并提交,这些内容像往常一样进入我们的工作树。
但是当我们去view合并提交……好吧,Git 通常会将提交与其父提交进行比较。合并有two父母,每个分支一名。 Git 应该对比哪一个来显示更改?
Here, git log
and git show
采取不同的方法。当您查看提交时git log
, 表明什么都没有默认情况下。它不会选择I
-vs-J
,并且它不会选择G
-vs-J
任何一个!它什么也没显示,因为git log -p
.
1In some Git workflows, merging from master into any other branch is discouraged. It can work, though, and since you did, let's run with it.
查看合并提交
The git show
命令做了一些不同的、更好的事情。它运行two git diff
s,一个用于I
-vs-J
和一个用于G
-vs-J
。然后它尝试combine两个差异,仅向您显示发生的变化both。也就是说,在哪里J
不同于I
但不是在一个特别的有趣的这样,Git 就抑制了差异。在哪里J
不同于G
但不是在一个特别的有趣的这样,Git 也抑制了这种差异。这可能是最有用的模式,所以就是这样git show
显示。它仍然很不完美,但是你在这里所做的一切都不是完美的。
你(们)能做到git log
通过添加来做同样的事情--cc
to the git log -p
选项。或者,您可以更改方式either git log
or git show
通过使用显示合并提交-m
(注意用一个破折号表示-m
, 两个为--cc
, 顺便一提)。
The -m
选项告诉 Git 为了查看目的,它应该split合并。现在Git比较I
to J
,向您展示通过合并带来的所有内容G
。然后Git比较G
到分离的额外版本J
,向您展示通过合并带来的所有内容I
。产生的差异通常非常大,但是(或因为)它向您展示了一切.
有更多方法可以尝试查找某些文件发生了什么,但我们需要稍等一下才能找到您的:
git log -- path/to/file1
命令。正如我们所看到的git log
skipping合并,它可能会在这里跳过更多的事情(但有一些方法可以阻止 Git 这样做)。
实际进行合并:Git 如何构建合并的内容
让我们再看看合并前的图:
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 all earlier commits) is on both branches. Commit E
is special, though: it's the most recent2 commit that's on both branches. Commit E
is what Git calls the merge base.
为了执行合并,Git 有效地运行两个git diff
命令:
git diff E I
git diff E G
第一个对各种文件进行一组更改,这些更改是“我们在分支上所做的事情”development
“。实际上,它是H
and I
如果它们被视为补丁。第二个对各种文件产生一组可能不同的更改,“我们在master
“,和以前一样,它实际上是F
and G
作为补丁。
然后 Git 尝试合并这两个差异。它们之间完全独立的东西,需要both更改集,将它们应用于提交的内容E
,并使用它作为结果。只要两个变更集触及同一行same文件中,Git 尝试查看它是否只能获取该更改的一份副本。如果两者都修正了文件第 33 行某个单词的拼写README
, Git 可以直接取one copy的拼写修复。但是,只要两个更改集触及同一文件的同一行,就可以创建一个不同的更改时,Git 声明“合并冲突”,将其隐喻的双手举到空中,并使得you解决由此产生的混乱。
如果你(或任何进行合并的人)愿意,他们可以阻止 Git 提交合并结果,即使 Git 认为一切进展顺利:git merge --no-commit master
使 Git 在合并所有内容后停止。此时,您可以在编辑器中打开工作树文件,change他们,写回他们,git add
更改后的文件,以及git commit
合并将并非来自合并的内容放入合并中any三个输入(基础和两个分支尖端)。
无论如何,理解这一切的关键是概念合并基地犯罪。如果您坐下来绘制提交图,合并基础通常非常明显,除非图失控(实际上这种情况经常发生)。您还可以让 Git 为您找到合并基础,或者在某些情况下为复数合并基础:
git merge-base --all master development
这会打印出哈希 ID。在我们假设的情况下,这将是提交的哈希 IDE
。一旦你有了它,你就可以运行git diff
手动查看每个文件发生了什么。或者您可以运行单独的文件git diff
:
git diff E development -- path/to/file1
git diff E master -- path/to/file1
请注意,如果您替换名称master
and development
以及提交的哈希 IDwere当前的before你做了一个git merge
,这甚至有效after合并。这会告诉你 Git 认为它应该结合起来做什么path/to/file1
。反过来,这会告诉你 Git 是否没有看到更改,或者是谁进行了合并overrodeGit,或错误地处理了冲突合并。
一旦进行合并,后续合并将找到不同的合并基础:
H--I--J----K <-- development
/ /
...--E--F--G--L--M <-- master
现在,我们查看两个分支提示,并按照合并的两个分叉,向后追溯历史(向左方向)J
,找到我们可以到达的第一个提交both分支提示。开始于K
,我们回到J
,然后对两者I
and G
。开始于M
,我们回到L
,然后到G
。我们发现G
在两个分支上,所以提交G
是新的合并基础。 Git 将运行:
git diff G K
git diff G M
获取两个更改集以应用于合并基础提交G
.
2"Most recent" here refers to commit graph order, rather than time stamps, although it's probably also the commit with the newest time stamp that is on both branches.
Git 日志和简化又是谎言
我们已经看到了git log -p
只是跳过合并提交。你根本看不到任何差异,就好像合并完全是魔法一样。但是当你跑步时:
git log -- path/to/file1
还有一些更阴险的事情发生了。尽管相当不透明,但在(长)git log文档 https://www.kernel.org/pub/software/scm/git/docs/git-log.html在标题为历史简化 https://www.kernel.org/pub/software/scm/git/docs/git-log.html#_history_simplification.
在我们上面的例子中,假设git log
正在步行从K
向后。它发现J
,这是一个合并。然后它会检查两者I
and G
,将每个比较J
排除除您正在查看的一个文件之外的所有文件后。也就是说,只是比较path/to/file1
在三个提交中。
如果合并的一侧doesn't显示任何更改path/to/file1
,这意味着合并结果J
与输入没有什么不同(I
or G
)。 Git 称之为“TREESAME”。如果合并J
,在被剥离到这个文件后,匹配I
or G
同样地精简,然后J
是树一样的I
or G
(或者也许两者兼而有之)。在这种情况下,Git 选择 TREESAME 父级或任何一个 TREESAME 父级并查找only在那条路上。假设它选择了I
(沿着顶行)而不是G
(沿着底部)。
在我们的例子中,这意味着如果有人在合并过程中犯了错误,就会丢失本应进行的更改J
from F
, git log
从不表现出来。这log
命令查看K
, then J
,然后看着但掉落G
,并且只看I
, then H
, then E
,然后是任何早期的提交。它从不考虑提交F
根本!所以我们看不到变化path/to/file1
from F
.
这里的逻辑很简单;我将引用git log
直接记录但添加一些强调:
[默认模式] 将历史记录简化为解释树的最终状态的最简单的历史记录。最简单,因为它修剪了一些侧枝如果最终结果相同 ...
自从发生变化以来F
被删除,Git 声明它们不相关。你不需要见他们!我们将完全忽略合并的那一侧!
你可以完全击败这个--full-history
。这告诉 Gitnot修剪合并的任一侧:它应该查看两个历史记录。这会发现提交F
为你。添加-m -p
还应该找到where更改已被删除,因为它会发现all涉及文件的提交:
git log -m -p --full-history -- path/to/file1
如果有更改(在提交中F
在我们的示例中)并且不再存在,它们丢失的方式只有两种: