Git历史记录转换为常规文件

3
我想知道在Git本身或文本编辑器中是否有一种方法可以直接在文件树中将Git历史记录显示为常规文件。例如,如果我有一个名为landing.html的文件,并有几个先前的提交记录,我希望在当前文件旁边直接显示完整的landind.html.old.v0landind.html.old.v1landind.html.old.v2等历史文件。文件名中可能还包含提交消息,例如landind.html.old.v0.initial-commit。这可以通过向Git添加after-commit钩子来完成吗?或者作为文本编辑器的扩展?我主要使用VSCodeSublime,不想使用额外的工具如gitk

1
你想要“你”的历史记录(从哪个时间点开始呢?这是一个问题);还是想要“全部”的历史记录?如果是后者,你打算如何处理图中的分支和合并? - torek
请参见 https://dev59.com/SnVC5IYBdhLWcg3wfhKL - torek
最好是能够查看全部历史记录,但如果只有我的和一个分支的话我也可以接受。我希望能够在不与Git交互的情况下查找全部历史记录。 - Hartator
2个回答

2
Git本身没有这个功能,所以你需要编写代码来实现。你提到了一个巨大的问题,即在运行`git clone`之后尝试对任何特定文件进行此操作,但你添加了以下备注: “总历史记录会更好,但只有我的和一个分支也可以。我希望能够查找整个历史记录,而无需与Git交互。” 在这种情况下,有明显的前进道路。我将为您概述一种想法,但您需要编写代码。如果您非常了解Git,请跳到关于使用后提交挂钩的底部部分。如果不是,请先阅读其余部分。通过编写后提交挂钩,您将学习很多关于Git的知识,但您可能还需要其他部分。 首先,请记住什么是“未跟踪的文件” 如果您要使用Git,则Git会强制您了解其三个部分: 工作树。这很简单:这是您进行工作的地方。工作树中的文件以通常的形式存储,您可以查看并处理它们。 索引,由于在Git中非常重要,因此具有两个其他名称:它也称为“暂存区”,有时也称为“缓存”。索引中的文件采用特殊的Git-only格式。关键在于您可以替换索引中的文件,因此它们是可写的。 提交。提交是永久的、只读的和不可破坏的。提交在Git中是历史记录:不存在“文件历史记录”这样的东西;每个提交都是一个完整的快照,其内容与每个其他快照无关。Git通过保存(提交)索引的内容来创建新的快照。 未跟踪的文件是不在索引中的文件。这是Git简单明了的罕见情况。如果您在工作树中有一个文件而不在索引中,则它是未跟踪的。所有您的landing.html.<后缀>文件将是未跟踪的。
提交的持久性取决于它们的可达性。如下面关于提交的部分所述,Git通过从分支名称(或任何其他标识提交的名称)开始查找提交。这些提交通过它们的哈希ID标识它们的父级,因此父级可以从分支末端到达。父级标识更多的父级,因此那些也是可达的。Git会很少(因为需要很长时间)计算可达提交的传递闭包 - 实际上是可达对象的传递闭包,并将其与对象数据库的全部内容进行比较。在这一点上,根据其他标准,可能会对不可达对象进行垃圾回收(丢弃)。
不可篡改性取决于它们是只读的并进行了散列。如果某些内容在对象内部发生了变化,它将停止匹配其(加密)哈希ID,Git将知道它已损坏。
提交的一些注释(以下内容都不是直接相关的,但将所有这些内容记在心中很有用)。与Git的所有内部对象一样,提交也是通过它们的哈希ID进行标识(命名)的。对象的哈希ID(包括每个提交)是其内容的加密校验和。每个提交的实际内容非常小,因为存储的快照是通过名为树的单独Git对象完成的:Git将索引转换为树,然后将树的哈希ID保存下来,再加上您的提交元数据(您的名称和电子邮件地址,一些时间戳,您的日志消息以及提交的父哈希ID)作为提交对象。
分支以及存储库中的历史记录存在,因为提交存储了父ID。分支名称(如master)仅仅持有一个提交哈希ID。Git将其称为“尖端提交”,它定义为分支上的最后一个提交,即最新的提交。要查找历史记录,Git会查看尖端提交的父提交,这是倒数第二个。然后Git查看父级的父级,这是倒数第三个;依此类推。因此,所得到的提交链就是该分支,由分支名称找到,它只标识最顶端的提交。
        D--E   <-- master
       /
A--B--C
       \
        F--G   <-- develop

提交记录 AE 都在分支 master 上,提交记录 AC 以及 FG 都在分支 develop 上。需要注意的是,有一些提交记录同时存在于多个分支中。存储在仓库中的历史记录就是所有提交记录的总和。这里的名称 masterdevelop 只标识了一个提交记录。
如果你愿意,可以创建一个只有一个线性分支的仓库,其中每个提交记录都与前一个提交记录没有关系。更有用的是(但仍然是故意歪曲的),你可以创建一个仓库,其中每隔一个提交记录就有一个不同的项目,所以如果你检出第一个提交记录,你会得到项目A的初始尝试。如果你检出第二个提交记录,你会得到项目B的初始尝试。第三个提交记录是A的第二个提交记录;第四个提交记录是B的第二个提交记录;以此类推。换句话说,偶数提交记录 N 是项目B的第N/2个提交记录;奇数提交记录是项目A的第floor((N+1)/2)个提交记录。
关键点在于,提交记录并不是变更集。如果同一个文件在连续的多个提交记录中出现多次,每个提交记录都有自己独立的文件副本。虽然在 Git 的深层结构中它们都共享着一个文件的“真实副本”(对于相同的对象,Git 可以轻松地做到这一点;对于稍有变化的对象,Git 必须将它们放入所谓的“pack”文件中进行增量压缩)。
这意味着,为了讨论文件或某些文件发生的事情,你必须选择一些提交记录,一次比较一对提交记录。显而易见的做法是比较每个父子对。只要提交记录是线性的,这种方法就有效。
... G--H--I--J   <-- develop

这里,G-H对、H-I对和I-J对是有用的比较对象。但假设这是其中的一部分:

        D--E
       /    \
A--B--C      M   <-- master
       \    /
        F--G--H--I--J   <-- develop

在这里,提交M是一个合并提交,它在master上,有人将develop合并到了master。提交M两个父提交,而不是只有一个:您要将ME还是与G进行比较?同时,在C处分叉,所以C在此时有-可能我们随时可以添加更多!-两个子提交。您要将CD还是CF进行比较?这些是真正棘手的部分,您可以通过“仅使用我的和一个分支settling”来避免这些问题。

进行提交

正如您无疑已经知道的那样,进行提交的过程包括执行以下步骤:

  1. 检出某个分支名称:这会使其尖端提交成为当前提交。这里有一些重要的事实:特别是,这对索引和工作树的影响。我们马上回到这个问题。
  2. 在工作树中进行更改。工作树中的文件具有普通的读/写形式,因此这很容易。
  3. 运行git add。这实际上是将更新的文件从工作树复制到索引中,替换未编辑的索引文件。
  4. 运行git commit。这会收集您的提交日志消息,然后创建实际的提交对象。

进行提交的棘手部分是将索引转换为树对象(如果您想要手动执行所有操作,则有一个单独的命令git write-tree)。一旦Git拥有了树对象,它就可以写出提交的文本:

tree <hash>
parent <hash>
author <name> <email> <timestamp>
committer <name> <email> <timestamp>
<log message>

然后将其转换为提交对象(如果愿意,您也可以手动完成此部分,使用git hash-object -w -t commit)。创建对象会通过计算文本的加密校验和来创建对象的哈希ID。只要此提交与每个其他提交不同 - 时间戳加上其余内容确保它是不同的,因为时间始终在增加2 - 它就会获得一个新的、与所有其他提交不同的哈希ID。请注意,parent <hash>行使用当前提交的哈希ID - 您在步骤1中检出的提交。
然后,Git简单地将新提交的哈希ID写入分支名称,以便当前分支 - 您在步骤1中检出的分支 - 现在将其标识为其顶端的提交。最后,这就是您可以做自己想做的事情的地方,git commit运行提交后钩子
以上可能令人困惑,因此让我们举个例子,用一个简单的三次提交存储库变成一个四次提交存储库:
A--B--C   <-- master (HEAD)

名称master指向提交C。你执行git checkout master,进行一些更改,git addgit commit创建新的提交D。新的提交将C作为其父提交:

A--B--C   <-- master (HEAD)
       \
        D

然后 Git 快速将名称 master 向下右滑,指向新的提交 D

A--B--C
       \
        D   <-- master (HEAD)

之后,我们通常会重新整理绘图,使其再次看起来像一条简单的线。

请注意,您可以运行git commit --amend命令,这将使新提交的父级为当前提交的父级。也就是说,我们可以让D指向B,而不是指向C

A--B--C
    \
     D   <-- master (HEAD)

这使得历史记录变成了 D -> B -> A,跳过了 C(它已经变得无法访问并最终将被垃圾回收)。换句话说,我们实际上没有改变历史记录——C仍然存在,只是不再在我们的历史记录中,但看起来好像我们已经改变了。如果你将来会使用 git commit --amend,请记住这一点,在 Git 钩子中使用时要注意。
(Git 的 git rebase 也有类似的效果,但更加激进:它将多个提交复制到新的提交中,放弃原始提交。)
2如果通过欺骗和诡计(或者只是运行 git filter-branch,它使用欺骗和诡计)成功创建一个与现有提交完全相同的新提交——它具有相同的作者和提交者、相同的时间戳、相同的父级、相同的源快照和相同的日志信息——那么您将重用旧提交的哈希 ID。但是怎么样呢?你只是创建了一个与旧提交完全相同的新提交。它具有相同的作者,是在同一时间创建的,具有相同的历史记录和相同的日志信息。它就是旧提交。
在两个不同的分支名称检出时非常快(一秒内)制作两个相同的提交,当两个分支名称都指向相同的头提交时,这里有一个奇怪的案例。这会导致分支名称最终指向单个共享的新提交,尽管您希望它们指向两个不同的提交,如果该过程跨越了时钟滴答,则它们将如此。结果从图形理论意义上来说是正确的,并且有效;但是这令人惊讶。

填空,或者说,填充索引和工作树

我提到了上面的步骤1——git checkout branch-name 步骤——对索引和工作树有重要影响。请注意,当 Git 创建上面的新提交时,它首先通过使用 git write-tree 将索引写出以创建树对象。这意味着索引必须与当前提交匹配。3 git checkout 命令通过比较当前提交(checkout之前)和目标提交(checkout之后)来实现。当前提交有一些文件,目标提交有另一组文件,可能略有不同。Checkout将从当前索引和工作树中删除必须删除的文件。它将把必须添加的任何文件添加到当前索引和工作树中。它将在索引和工作树中替换必须被交换出去的任何文件,以从旧提交转移到新提交。

因此,在git checkout之后,索引和工作树将-除了未在索引中的未跟踪文件-与目标提交匹配,该提交刚刚成为当前提交。

还要注意,当您运行git commit时,这会使用当前索引创建新的提交。结果是,一旦完成新的提交,当前提交和索引再次匹配。因此,我们得到了关于Git的一个基本(尽管略微灵活,参见脚注3)真相:索引通常与当前提交匹配,直到您开始使用git add从工作树中复制文件。


3实际上,允许某些差异在检出之间保留。有关详细信息,请参见在当前分支上有未提交的更改时检出另一个分支


使用后提交钩子获取所需内容

Git在git commit成功完成后立即运行您的后提交钩子。这个git commit已经创建了一个新的提交,比如我们将一个三次提交的仓库转换为一个四次提交的仓库的提交D

新提交有一个父提交,比如C。现在你有机会比较父提交和子提交:

git diff --name-status HEAD^ HEAD

例如。 (HEAD 是当前提交的子提交,HEAD^ 表示 查看 HEAD 的第一个父提交。在这里要记住合并提交,它们有多个父提交:例如,您可以使用 HEAD^2 来查看合并的 第二个 父提交。我不确定,git merge 在进行合并提交时是否运行 post-commit 钩子,尽管我认为它会。) 从 git diff --name-status 输出的内容告诉您每个打印文件发生了什么; 有关详细信息,请参见 git diff 文档4 此时,如果某些文件(如 landing.html)已更改(状态为 M),或者已创建新文件(状态为 A),则可以将该文件的副本复制到下一个版本号下,并使用提交日志消息主题 (git log -1 --pretty=format:%s HEAD)。如果文件没有 更改,则不会输出任何内容——git diff 不会说任何话,因为没有什么可说的——所以您不需要复制。
随着时间的推移,您将在工作树中构建出您想要的未跟踪文件,它们按照您进行这些提交的顺序编号,以成为 您自己的 历史记录。为了使编号有意义,您甚至可以检查您所在的分支(如果有——在“分离的 HEAD”模式下,例如当您查看历史提交时,HEAD 根本没有附加到任何分支名称上)。请注意,您可以使用 git rev-parse --abbrev-ref HEADgit symbolic-ref --short HEAD 来获取分支名称。5

4对于脚本编写,您应该真正使用 git diff-tree,因为它更可预测。它不遵循每个用户的配置控制,例如,因此它对每个人都是相同的。 git diff 将查看您的 diff.renames 设置、您的 diff.renameLimit 等等,以及差异输出着色选项,所有这些都可能会影响脚本编写。

5两者之间的区别在于,如果 HEAD 处于分离状态,则 git symbolic-ref失败(退出非零),并且不会产生标准输出(但默认情况下会写入 stderr)。对于这种情况,git rev-parse 只会打印 HEAD


哇哦,非常感谢您提供如此完整的答案。我一直觉得Git很复杂,从来没有真正完全理解它。我会在这个周末实现一个完整的解决方案。 - Hartator

0
时间轴视图-内置的,不是扩展程序-似乎可以满足您的要求。非常棒,专注于打开编辑器并列出该文件的提交历史记录。单击条目会打开差异。它将打开到当前版本的差异-如果要使单独的文件持久存在,则需要复制所有差异并保存到新文件中。

vscode v1.44 更新

时间轴视图现在已经正式发布并且默认启用(重点强调)。这是一个统一的视图,用于可视化文件的时间序列事件(例如 Git 提交、文件保存、测试运行等)。时间轴视图会自动更新当前活动编辑器的时间轴,默认情况下。您可以通过在视图工具栏中切换眼睛图标来控制此默认行为。此外,与其他视图类似,时间轴视图支持按键输入查找或过滤。

在此版本中,内置的 Git 扩展提供了一个时间轴源,用于提供指定文件的 Git 提交历史记录。选择一个提交将打开引入该提交的更改的差异视图。同时,上下文菜单还提供了复制提交 ID复制提交消息命令。还有一个新的打开时间轴命令在资源管理器文件的上下文菜单中,快速显示所选文件的时间轴。


v1.42和v1.43相关功能预览: https://github.com/microsoft/vscode-docs/blob/vnext/release-notes/v1_42.md#timeline-viewhttps://github.com/microsoft/vscode-docs/blob/vnext/release-notes/v1_43.md#timeline-view

时间轴视图

在这个里程碑中,我们在新的时间轴视图上取得了进展,并有一个早期预览分享。这是用于可视化资源(文件夹)的时间序列事件(例如Git提交、文件保存、测试运行等)的统一视图。要启用时间轴视图,您必须使用Insiders版本,然后添加以下设置:

"timeline.showView": true [默认情况下在v1.44中启用]

git timeline view

在这个早期预览版本中,时间轴视图显示了活动文档的Git提交历史记录,目前仅限于32项。点击其中一个提交将会打开该提交引入更改的差异对比。扩展程序也将能够贡献自己的时间轴来源,这些将显示在统一的时间轴视图中。最终,你还可以选择(过滤)你想要在视图中看到的来源。
请关注,我们还为这个新功能准备了更多内容。你可以通过订阅#84297和观察带有时间轴标签的问题来跟进。如果你有其他类型信息想要在这个视图中看到,欢迎告诉我们!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接