0

我已经将一堆文件移动到一个单独的分支,以避免混合不同的历史。然后我将新分支上的文件移动到/(以便它们可以在根植于它们最初所在位置的工作树中签出):

git branch -c scripts
git rm -rf scripts/
git commit -m 'scripts -> new branch'
git switch scripts
# git rm -rf files NOT under scripts/
git mv scripts/* .
git commit -m "scripts -> top level of new branch

现在,当然,虽然两个分支上的历史看起来都很干净,但我在查看过去的历史时遇到了麻烦。git log --follow需要一个文件名。

在主分支上,我可以

  • xargs -0 -a <(git ls-files -z) git log --(获取现有文件的日志)

在脚本branch上,它不是那么简单;我想我需要

  • 找到现有文件的所有以前的名称,然后是git log当前 + 旧名称(尽管这可能是很多参数)。如何?
  • git log忽略引用已删除(但未重命名)文件的提交(这似乎不可行)

想法?

4

1 回答 1

2

无论如何,从 Git 的角度来看,您认为这一切都是错误的。(你同样可以说 Git 认为这一切都是错误的——但这已经被硬编码到 Git 中并且不会改变!)

特别是,在 Git中没有文件历史记录之类的东西。在 Git 中,历史就是提交。提交是历史。这就是全部。

作为一个选项,Git 确实具有猜测文件重命名的能力。当您使用git log标志和/或参数告诉它显示不是实际历史的东西时,这很有用,例如:向我展示一个简化的历史,其中只包括从父到子的提交,给定的路径-名称更改了内容。这就是这样git log -- setup做的:告诉git log走修订(提交),但只打印有关提交的信息,在父子之间,某些scripts/*文件已被修改。

旁注:正如您所提到的,该--follow标志git log仅适用于单个文件名(因为实施--follow是一个可怕的黑客攻击)并且有很多缺陷。我相信--follow处理应该被完全抛弃并重写,但这是一项不平凡的任务(轻描淡写)。在某些时候,即使--follow没有完全重写,它也可能开始一次只为一个目录工作;那一点甚至可能是 Git 2.17 左右;我没有对此进行测试,对于这种特殊情况,无论如何它都不是正确的答案。

-M标志不会真正有帮助,因为它不会更改git log用于其限制的文件名。这是您需要了解的其余内容。

提交形成有向无环图或 DAG

每个提交都有编号,带有一个哈希 ID,每个提交存储两件事:

  • 每个文件的完整快照(以提交时的形式);和
  • 元数据:诸如作者和提交者、提交日志消息等信息,以及对 Git 本身至关重要的提交的节点的哈希 ID 。

提交本身形成图的节点(或顶点),哈希 ID 存储为每个提交中的父编号,形成这些节点之间的链接(单向边或弧)。

这个 DAG 是树的泛化。树数据结构将允许分支:

          I--J
         /
...--G--H
         \
          K--L

在这里,每个大写字母代表一个提交。较新的提交出现在右侧。提交之间的边缘是单向(向后看)弧:从提交J,我们可以向后工作I,然后再到H等等G。从L,我们向后工作到K,然后H像以前一样依此类推。

合并提交将此树结构转换为 DAG:

          I--J
         /    \
...--G--H      M--N--...
         \    /
          K--L

合并提交M指向两个提交,因此当 Git 在历史中向后移动时,它必须分支:以某种顺序同时访问Land J

当在没有选项的情况下运行时,它从名称找到git log的当前提示提交HEAD开始并向后工作。在合并时,它遵循两个分支。因为它实际上一次只能处理一个提交,所以它使用优先级队列来处理这个问题。

在没有选项或提交指定参数的情况下运行git log,并且此优先级队列仅以提交的一个条目开始HEAD。从队列中弹出一个条目并显示提交。然后该提交的父级或父级被放入队列中,我们重复。只要以这种方式访问​​的每个提交只有一个父级,队列就不会再增长,并且存在队列的事实不可见的。但是当我们点击合并提交时M,父母双方都会进入优先级队列。现在优先级很重要:显示的下一个提交是队列前面的那个。

默认是按提交者日期顺序访问,较高的日期值(稍后提交)具有较高的优先级。所以如果 的 committer-dateL高于 的J,我们接下来看L,commitK会进入队列。否则我们将看到J下一个,I并将进入队列。队列继续有两个条目,因此git log移动到最高优先级的条目。

分行名称

分支名称喜欢mainmaster只是让您(和 Git)成为图表的入口点。如果没有名称,您将不得不求助于没有人愿意使用的原始哈希 ID。分支名称只是一个可移动的指针,指向图中的某个提交节点。名称本身并不重要:重要的是每个名称都为我们提供了一个提交的哈希 ID ,让我们可以找到其他更早的提交。

使用分支名称运行,git log从该提交开始。使用多个分支名称运行,将通过git log分支名称找到的每个提交放入优先级队列,然后再一次,优先级确定接下来显示哪个提交。

如何git log处理队列并显示提交

请注意,在所有情况下,git log都只是简单地遍历队列,插入父母,并一次显示一个提交。没有选项的默认操作是使用格式显示每个提交--pretty=medium(尽管此默认值是可调整的)。

但是,我们可以限制 git log显示所有提交。我们还可以改变它如何队列提供数据以及它如何对队列进行排序(即,相对优先级是什么)。 给出路径名或路径规范参数两者都可以。 这很重要(因此是斜体)。显示提交的部分更容易描述,因为您可以立即看到它的效果。不过,在我们开始讨论之前,值得快速浏览一下git diff.

两次提交的差异

除非我们使用git checkout或提取整个提交,否则我们通常对提交完整档案快照git switch这一事实并不真正感兴趣。我们通常对两次提交之间的区别更感兴趣。

git diff命令可以向我们展示这一点。例如,给定两个提交EH,我们可以运行git diff E H看看有什么不同。由于 Git 的内部存储格式——文件被去重和压缩存储——Git 可以非常快速地判断 in 中的某个文件与 inE中的同名文件完全相同H,而无需向我们展示任何关于它的内容文件。对于不同的文件 Git 可以玩Spot the Difference游戏并告诉我们发生了什么变化1

如果我们选择相邻的提交——即父子提交,比如GH——我们可以看到在那个特定的提交中发生了什么变化H。这对人类特别有用。提交包含所有更改的文件(因为它包含所有文件),并且日志消息告诉我们为什么进行更改的人进行更改:我们可以查看他们所做的更改是否实现了他们的目标。这只是一个例子;有很多有用的例子。关键是它git diff可以很容易地做到这一点。


1更准确地说,输出git diff是将左侧文件更改为右侧文件的方法它不一定反映我们是如何做到这一点的,只是一种实现它的方式。当更改涉及 Git 无法正确理解的一些语法(例如右括号行)时,这有时很重要。Git 会建议删除错误的括号,因为其他一些括号看起来是一样的——但有时这并不完全正确。


再次记录

git log处理正常的非合并提交时,它同时具有原始提交其(单个)父级的哈希 ID。这意味着它可以轻松运行git diff。事实上,找出某些文件是否发生了变化甚至更容易,因为我们不需要让 Git 来查找差异,这是比较慢的部分:我们只需让 Git 找出是否差异. git log内置的也是如此。(如果我们想要的话,它也内置了完整的差异,但它可以快速完成这部分。)

对于 normal git log,这并不重要:无论如何,它将以适当的--pretty格式显示提交的元数据。但是当我们git log使用路径名运行时,git log首先会过滤掉未列出的文件——它会同时处理这个提交及其父文件——然后比较生成的文件。如果它们都相同,git log 则根本不打印提交

这意味着:

git log -- file/in/question.ext

打印(使用--pretty格式)那些有问题的文件与其 parent 中的副本不同的提交人们喜欢称其为“文件历史记录”,但实际上并非如此:它只是经过过滤的提交历史记录。它崩溃或分离的地方正是你现在遇到问题的地方:对于提交和提交中的同一个文件 来说,究竟意味着什么?GH

Git 不存储重命名,但它能够通过其差异引擎即时重建重命名。当左侧提交 ( G) 有一些在右侧 ( H) 提交中不存在的文件 X 并且右侧提交有一些在左侧提交中不存在的文件 Y 时,Git 将,可选地,检查该文件对以查看内容是否匹配或相似。

精确匹配的查找速度非常快(由于重复数据删除技巧)。“类似”文件越来越难。Using-M启用两种重命名检测,包括 ingit diff in git log。该--follow选项还启用了这种重命名检测——但它是一个可怕的 hack:当使用 时--followgit log只有一个路径名,--follow它的作用是启用-M和捕获重命名并更改它正在寻找的一个文件名。因此,如果G-vs-H重命名了一个文件,那么在git log查看提交G之前,它正在寻找的一个名称是来自 的旧名称G,而不是来自 的新名称H

合并:喂入队列,处理名称

合并提交与常规提交的不同之处在于合并提交有两个(或更多,但通常是两个)父级。这意味着我们需要两个差异。Git 可以做到这一点,但有多个问题。

首先,git log通常不会打扰。如果您在git log没有参数的情况下进行操作,或者甚至-p打开以在每次提交时显示从父级到子级的差异,那么当git log遇到合并提交时,它只会抛出它的虚拟手并声明这太难了。它打印日志消息,但根本不运行任何差异,然后像往常一样将两个(或所有)父级添加到优先级队列中。

其次,如果您添加路径名或路径规范,git log将照常进行过滤。为此,它会将此提交及其所有父项都剥离到您的路径规范中的文件集。然后它会检查此提交中的所有内容是否与任何一个parent 匹配。如果是这样,它会做两件事:

  • 既然有匹配,它只跟随一个 parent。这里的理论是,您试图了解为什么这些文件看起来与您开始的提交(例如,HEAD提交)中的方式相同,而其他父母没有贡献任何东西,那么为什么还要费心寻找呢?

  • 由于这一个匹配,它不会打印这个 commit

因此,在合并时,我们修剪掉所有没有贡献的分支——或者更确切地说,合并父级,它们是 Git 使用的反向分支——然后我们也不打印合并提交。这种特殊的简化永远不会直接可见!

但是,如果合并提交的过滤快照与每个父级不同,git log则将 (a) 打印提交并 (b) 跟踪所有父级。无论过滤是否显示差异,我们也可以强制git log跟随所有父母。--full-history

请注意,如果我们--follow打开了,重命名检测可能会弄得一团糟。假设我们在合并M,与父母JL,我们发现在M,文件xyz.ext被重命名为abc.ext。但它在两个 J L中都有名称,其中一个可能是静止abc.ext的,而另一个是xyz.ext。如果我们访问两个分支,并且重命名发生在我们继续查看Iand之前和之间,那么当我们到达其他提交时,我们将寻找错误的名称。(在这种情况下,这工作的好坏取决于很多因素。)JLK

我们在这里也有几个额外的选择:

  • -m导致git log(和git diff)将合并拆分为多个虚拟非合并提交。也就是说,我们没有考虑 commit M,我们的合并,作为具有两个父级的单个提交,我们有git loggit merge假装有两个提交:M'有父级JM''有父级L。这些现在是普通的单亲提交,可以使用单亲显示方法显示并使用单亲差异方法进行差异化。

  • --first-parent导致忽略git log合并的额外父母。这会影响它的差异——<code>M 将仅与 进行比较,假设是第一个父级——以及修订版 walk:将被视为单父提交,并且其(单独的)父级将被放入队列中。JJM

请注意,任何合并的第一个父级是您(或任何人)在您(或他们)进行合并时所在的分支。

结论

在 Git 中,历史只不过是提交,从分支名称(或其他起点)开始并向后工作。移动文件不会避免“混合历史”:它只是意味着提交中的快照会有所不同。分支名称根本不是历史:它们只是graph 的入口点

Git 没有真正的文件身份。无法确定 commit 中名为 X 的文件是否commit中名为XYA的文件相同或不同。它的默认假设是same-name = same-file。这可以在逐个差异的基础上进行调整。不过,这方面的工具有点粗糙。H

于 2021-04-17T01:41:52.977 回答