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
/
A
\
F
提交记录
A
到
E
都在分支
master
上,提交记录
A
到
C
以及
F
和
G
都在分支
develop
上。需要注意的是,有一些提交记录同时存在于多个分支中。存储在仓库中的历史记录就是所有提交记录的总和。这里的名称
master
和
develop
只标识了一个提交记录。
如果你愿意,可以创建一个只有一个线性分支的仓库,其中每个提交记录都与前一个提交记录没有关系。更有用的是(但仍然是故意歪曲的),你可以创建一个仓库,其中每隔一个提交记录就有一个不同的项目,所以如果你检出第一个提交记录,你会得到项目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
有两个父提交,而不是只有一个:您要将M
与E
还是与G
进行比较?同时,在C
处分叉,所以C
在此时有-可能我们随时可以添加更多!-两个子提交。您要将C
与D
还是C
与F
进行比较?这些是真正棘手的部分,您可以通过“仅使用我的和一个分支settling”来避免这些问题。
进行提交
正如您无疑已经知道的那样,进行提交的过程包括执行以下步骤:
- 检出某个分支名称:这会使其尖端提交成为当前提交。这里有一些重要的事实:特别是,这对索引和工作树的影响。我们马上回到这个问题。
- 在工作树中进行更改。工作树中的文件具有普通的读/写形式,因此这很容易。
- 运行
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中检出的分支 - 现在将其标识为其顶端的
新提交。最后,这就是您可以做自己想做的事情的地方,
git commit
运行
提交后钩子。
以上可能令人困惑,因此让我们举个例子,用一个简单的三次提交存储库变成一个四次提交存储库:
A
名称master
指向提交C
。你执行git checkout master
,进行一些更改,git add
和git commit
创建新的提交D
。新的提交将C
作为其父提交:
A
\
D
然后 Git 快速将名称 master
向下右滑,指向新的提交 D
:
A
\
D <
之后,我们通常会重新整理绘图,使其再次看起来像一条简单的线。
请注意,您可以运行git commit --amend
命令,这将使新提交的父级为当前提交的父级。也就是说,我们可以让D
指向B
,而不是指向C
:
A
\
D <
这使得历史记录变成了
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 HEAD
或
git 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
。