Git 没有为此内置任何内容,因此您必须编写代码。
在运行git clone 后立即尝试对任何特定文件执行此操作存在巨大问题,但您添加了以下注释:
总历史会更好,但我只能解决我的一个分支。我希望能够在不与 Git 交互的情况下查找全部历史记录。
在这种情况下,有一条明显的前进道路。我将为您概述一个想法,但您必须编写代码。如果你对 Git 有很多了解,请跳到底部关于使用 post-commit 钩子的部分。如果没有,请先阅读其余部分。通过编写 post-commit 钩子,您将学到很多关于 Git 的知识,但您可能还需要其他部分。
首先,请记住未跟踪的文件是什么
如果你打算使用 Git,Git 会强制你了解它的三个部分:
工作树。这很简单:这是你工作的地方。工作树中的文件以通常的形式存储,您可以在其中查看和使用它们。
index,它还有两个名字,因为它在 Git 中非常重要:它也称为 暂存区,有时也称为 缓存时间>。索引中的文件采用特殊的仅 Git 格式。这里的关键是您可以替换索引中的文件,因此它们是可写的。
提交。提交是永久的、只读的和不可破坏的。1 Git 中的提交是历史:没有“文件历史”之类的东西。每个提交都是一个完整的快照,其内容独立于其他所有快照。 Git 通过保存(提交)索引的内容来创建新的快照。
未跟踪的文件是不在索引中的文件。这是 Git 简单明了的罕见情况。 :-) 如果您在工作树中有一个不在索引中的文件,则它是未跟踪的。您的所有landing.html.<em>suffix</em> 文件都将不被跟踪。
1提交的持久性取决于它们的可达性。正如下面关于提交的部分所述,Git finds 从分支名称(或任何其他标识提交的名称)开始提交。这些提交通过它们的哈希 ID 标识它们的父级,因此父级可以从分支提示中访问。父母会识别更多父母,因此这些父母也是可以访问的。 Git 很少(因为它需要很长时间)计算可到达提交集的传递闭包——实际上是可到达的对象——并将其与对象数据库的全部内容进行比较。此时,无法访问的对象可能会被垃圾收集(丢弃),具体取决于其他条件。
清廉取决于它们是只读的和散列的。如果对象内部发生某种变化,它将不再匹配其(加密)哈希 ID,Git 会知道它已损坏。
关于提交的一些注意事项
(这些都不是直接相关的,但记住这一切很有用。)
像所有 Git 的内部对象一样,提交由它们的哈希 ID 标识(命名)。对象的哈希 ID(包括每次提交)是其内容的加密校验和。每个提交的实际内容非常小,因为存储的快照是通过一个名为 tree 的单独 Git 对象完成的:Git 将索引变成一棵树,然后保存树的哈希 ID 以及您的提交元数据(您的姓名和电子邮件地址、一些时间戳、您的日志消息和提交的 父 哈希 ID)作为提交对象。
存在分支,以及存储库中的历史记录,因为提交存储父 ID。像 master 这样的分支 name 只保存一 (1) 个提交哈希 ID。 Git 将此称为 tip commit,根据定义,它是分支上的 last 提交,即最新的。要查找历史记录,Git 会查看提示提交的父提交,即倒数第二个。然后Git查看parent的parent,也就是倒数第三个;等等。因此,生成的提交链就是分支,由分支 name 找到,它仅标识最尖端的提交:
D--E <-- master
/
A--B--C
\
F--G <-- develop
提交A 到E 都在分支master 上,提交A 到C 加上F 和G 都在分支develop 上。 请注意,有些提交在多个分支上。存储在存储库中的历史记录是只是存储在存储库中的所有提交的总和.请注意,names、master 和 develop 在此处仅标识 一个 提交。
如果您愿意,您可以创建一个具有单个线性分支的存储库,其中每个提交都与之前的提交完全无关。更有用(但仍然故意歪曲),您可以创建一个存储库,其中每个 other 提交都有一个 不同 项目,这样如果您签出第一个提交,您获得项目 A 的初步尝试。如果您签出第二次提交,您将获得项目 B 的初始尝试。第三次commit是A的第二次commit;第四个commit是B的第二个commit;等等。也就是说,一个偶数提交N就是Project B,commit N/2;奇数提交是 ProjectA,提交 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
commit M 是master 上的合并提交,此时有人将develop 合并到master。提交M 有两个 父母,而不仅仅是一个:你会将M 与E 或G 进行比较吗?与此同时,分支在C 处分叉,所以C 有——此刻;我们可以随时添加更多!——两个孩子。您会将C 与D 或C 与F 进行比较吗?这些是真正棘手的部分,你可以通过“只用我的和一个分支解决”来避免。
提交
您无疑已经知道,提交的过程包括执行以下步骤:
- 检查一些分支名称:这使其提示提交成为当前提交。关于这一点有一些重要的事实:特别是它如何影响索引和工作树。我们稍后再讨论。
- 在工作树中进行更改。工作树中的文件有它们普通的读/写形式,所以这很容易。
- 运行
git add。这实际上是将更新后的文件从工作树复制到索引中,替换未编辑的索引文件。
- 运行
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 中签出的分支)现在将 new 提交标识为其提示。最后,这就是你可以做你想做的事的地方,git commit 运行一个 post-commit 钩子。
上面的内容可能会让人困惑,所以让我们举个例子,一个简单的三提交存储库变成四提交存储库:
A--B--C <-- master (HEAD)
名称master 指向提交C。你git checkout master,进行一些更改,git add 和git commit 并创建新的提交D。新提交指向 C 作为其父级:
A--B--C <-- master (HEAD)
\
D
然后 Git 快速将名称 master 向右滑动,使其指向新的提交 D:
A--B--C
\
D <-- master (HEAD)
之后,我们通常会将绘图拉直,使其再次看起来像一条简单的线。
请注意,您可以运行git commit --amend,这会使新提交以当前提交的父级 作为其父级。也就是说,我们可以让D 指向B,而不是让D 指向C:
A--B--C
\
D <-- master (HEAD)
这使历史记录转到D -> B -> A,跳过C(它已变得无法访问,最终将被垃圾收集)。换句话说,我们实际上并没有改变历史——C 仍然在那里,只是不再在我们的历史链接中——但它看起来我们已经改变了。如果您以后会使用git commit --amend,请在以后的 Git 挂钩中记住这一点。
(Git 的 git rebase 具有类似的效果,但更加剧烈:它将 多个 提交复制到新的提交,放弃原来的提交。)
2如果通过诡计和诡计(或仅通过运行使用诡计和诡计的git filter-branch),您设法做出与现有的逐位相同的新提交提交——它具有相同的作者和提交者、相同的时间戳、相同的父节点、相同的源快照和相同的日志消息——然后您将重用 old 提交的哈希 ID。但那又怎样?您刚刚做了一个与旧提交完全相同的新提交。它具有相同的作者,同时制作,具有相同的历史,并且具有相同的日志消息。它是旧的提交。
这里有一个奇怪的案例,当两个分支名称都指向同一个提示提交时,在两个不同的分支名称签出上非常快(在一秒钟内)进行两个相同的提交。这会导致分支名称最终指向一个共享的新提交,即使您希望它们指向两个不同的提交,并且如果进程跨越了一个时钟滴答,它们也会如此。从图论的意义上说,结果是正确的,并且有效;但令人惊讶。
填写空白,或者更确切地说,填写索引和工作树
我提到上面的第 1 步 - git checkout <em>branch-name</em> 步骤 - 对索引和工作树有重要影响。请注意,当 Git 在上面进行新提交时,它首先使用 git write-tree 写出索引以创建树对象。这意味着索引必须从当前提交开始匹配。3
git checkout 命令通过将当前(签出前)提交与目标(签出后)提交进行比较来实现此目的。当前提交有一些文件,而目标提交有另一组文件,大概至少有点不同。 Checkout 将从当前索引和工作树中删除那些必须删除的文件。它会将所有必须添加的文件添加到当前索引和工作树中。它将在索引和工作树中替换任何必须换出的文件,以从旧提交转到新提交。
因此,在git checkout 之后,索引和工作树将(除了根本不在索引中的未跟踪文件)与刚刚成为当前提交的目标提交匹配。
还要注意,当您运行git commit 时,这会使用当前索引进行新的提交。结果是一旦新的提交完成,当前的提交和索引再次匹配。所以我们得到了一个关于 Git 的基本(虽然有点灵活,见脚注 3)真相:索引通常匹配当前提交,直到你开始 git adding 从工作树复制文件。
3实际上,结账时允许有一些差异。详情请见Checkout another branch when there are uncommitted changes on the current branch。
使用 post-commit 钩子来获得你想要的东西
Git 在git commit 成功完成后立即运行您的提交后挂钩。这个git commit 做了一个新的提交,例如在我们将三提交存储库转换为四提交存储库的示例中提交D。
新提交有一个父,例如C。现在您有机会比较父母与孩子:
git diff --name-status HEAD^ HEAD
例如。 (HEAD 是当前的,即子级,提交,HEAD^ 表示查看HEAD 的第一个父级。请记住这里有多个父级的合并提交:你例如,可以使用HEAD^2 查看合并的 second 父级。我不确定git merge 是否运行提交后挂钩,当git merge进行合并提交,尽管我怀疑它确实如此。)git diff --name-status 的输出告诉您它打印的每个文件发生了什么;详情请参阅the git diff documentation。4
此时,如果landing.html等文件发生了变化(状态M),或者创建了新文件(状态A),可以在下一个版本下复制该文件编号,并使用提交日志消息主题 (git log -1 --pretty=format:%s HEAD)。如果文件没有更改,您将不会得到任何输出——git diff 什么也没说,因为没有什么可说的——所以你不要复制。
结果,随着时间的推移,您将在您的工作树中构建您想要作为您的历史记录的未跟踪文件,按您提交这些提交的顺序编号。为了使编号有意义,您甚至可以检查您在哪个分支上(如果有的话——在“分离 HEAD”模式下,例如当您查看历史提交时,HEAD 根本不附加到分支名称)。请注意,您可以使用git rev-parse --abbrev-ref HEAD 或git symbolic-ref --short HEAD 来获取分支名称。5
4对于脚本,你真的应该使用git diff-tree,这样更可预测。例如,它不遵守每个用户的配置控制,因此它对每个人的行为都相同。 git diff 将查看您的 diff.renames 设置、diff.renameLimit 等等,以及差异输出着色选项,所有这些都可能与脚本编写混淆。
5两者的区别在于git symbolic-ref会失败(退出非零),并且不产生标准输出(但会默认写入stderr),如果 HEAD 分离。对于这种情况,git rev-parse 只会打印 HEAD。