phd wrote an answer我AFK的时候你也应该看看,但是我想完成这个,所以:
我是 git 新手,我能从 githooks 手册页中摘录的只是我可以用钩子准备消息,但不能替换它。
情况并非如此——prepare-commit-msg 钩子可以对消息文件做任何它喜欢的事情,包括完全替换它的内容。但是,您可能会将 消息文件(通常只是 .git/COMMIT_EDITMSG)与 git log 稍后显示的内容(不是.git/COMMIT_EDITMSG)混为一谈。 p>
要了解正在发生的事情(以及您需要做什么),您需要了解 Git 在提交中实际放入的内容以及提交的工作方式。
首先,您所做的每个提交至少在逻辑上包含1一个完整、独立的快照,与其他所有提交分开。也就是说,通过从某个顶级目录开始并枚举其中的文件和目录,可以找到一些源代码文件和目录树。2 Git 提交所有文件,包括子目录中的。3
因此,如果您有 Git 存储库,则可以运行:
git log
查看各种提交,然后通过哈希 ID 选择一个(例如用鼠标剪切和粘贴)并运行:
git ls-tree -r <hash-id>
您会看到该特定提交包含每个文件,而不仅仅是与之前提交不同的文件。
尽管如此,git show <hash-id> 会向您显示该提交中的更改,就好像提交只存储了更改。提交不存储更改——它完整地存储所有内容——但git show 显示 更改。 git show 实现这一点的方式是将提交与其前一个提交进行比较。
提交的前身是提交的父。因此,提交是该父级的 child。对于每个文件,如果父提交中的文件与子提交中的文件匹配,git show 不会说明该文件。如果文件不匹配,git show 会生成一组指令来更改父版本以使其成为子版本。 Git 在 git show 操作时生成此差异列表 *,这意味着您可以将各种标志传递给 git show 以更改其计算和呈现差异的方式。
让我们看一下来自 Git 存储库的一个实际的、原始的提交对象,以便具体说明:
$ git rev-parse HEAD
e3a80781f5932f5fea12a49eb06f3ade4ed8945c
$ git cat-file -p e3a80781f5932f5fea12a49eb06f3ade4ed8945c | sed 's/@/ /'
tree 8e229ef2136e53a530ef74802f83d3b29a225439
parent 66023bbd78fe93c4704b3df754f9f7dc619ebaad
author Junio C Hamano <gitster pobox.com> 1519245935 -0800
committer Junio C Hamano <gitster pobox.com> 1519245935 -0800
Fourth batch for 2.17
此提交的日志消息是最后一行。它在 commit 对象 中,哈希 ID 为 e3a80781f5932f5fea12a49eb06f3ade4ed8945c。如果我在那个提交上运行git show,Git 会告诉我Documentation/RelNotes/2.17.0.txt,但实际上,提交中的文件是tree 8e229ef2136e53a530ef74802f83d3b29a225439 中的文件。如果我运行git ls-tree -r 8e229ef2136e53a530ef74802f83d3b29a225439,它会产生 3222 行输出:
$ git ls-tree -r 8e229ef2136e53a530ef74802f83d3b29a225439 | wc
3222 12900 259436
所以提交中有超过三千个文件。其中 3221 个文件与 parent 中的版本 100% 相同,即 66023bbd78fe93c4704b3df754f9f7dc619ebaad,其中也有 3222 个文件。
无论如何,这里的关键部分是:
- 提交是 Git 对象: 四种类型之一。完整集添加了tree、blob(仅文件数据:文件的name,如果有,则改为在树对象中) , 和 注释标签。最后一个在这里无关紧要。
- 每个提交都有一组父提交(通常只有一个)。
- 每次提交都会保存一棵树。该树列出了文件名及其 blob 哈希 ID。您可以尝试
git ls-tree(并阅读其文档)以了解它们的工作原理,但在此级别上,细节无关紧要。
- 每个提交还具有关联但用户提供的元数据:作者和提交者(姓名、电子邮件和时间戳),以及从您的挂钩可以编辑的消息文件中复制的日志消息。
因此,进行提交是一个涉及构建树对象以用作快照的过程,然后添加元数据以进行新的提交。新的提交获得一个新的、唯一的哈希 ID。 (树 ID 不一定是唯一的:如果您进行的新提交与之前的提交具有 完全相同 树,有时这是明智的做法,您最终会重新使用旧的树。)
1最终,Git 确实开始使用与其他版本控制系统相同的增量压缩。但这种情况发生在提交完成一个完整的独立快照之后很久。
2这是一个近似值。有关详细信息,请参阅下一节。
3Git 不保存任何目录:它只提交个文件。某个目录的存在是通过在其中包含一个文件来暗示的。如果需要,Git 将在稍后检查提交并发现它必须这样做才能将文件放在那里时重新创建目录。
Git 如何进行提交,或者树对象中的内容
你特别提到你正在运行git commit <em>filename</em>:
我的想法是我可以使用 git commit 提交一个文件,然后 git 从源文件中获取相关消息...
Git 不会根据传递给 git commit 的参数构建树。
相反,Git 有一个单独的东西4,它调用一个 index、一个 暂存区 和一个 缓存 em>,这取决于谁在调用以及他们希望强调索引的哪个方面。该索引是树对象的来源。
这意味着索引最初包含来自当前提交的所有文件。当您运行 git add <em>path</em> 时,Git 会将工作树中 path 中的文件复制到索引中,并覆盖之前存在的文件。
要为提交创建树,Git 通常只调用git write-tree,它将索引内容打包为树。如果这棵树与某些现有树相同,则重新使用旧树;如果是新的,那就是新的;无论哪种方式,它都是 树,由索引中的任何内容组成。
编写完树后,Git 可以将其与当前提交的哈希 ID 结合起来,以获取提交对象的 tree 和 parent 行。 Git 添加你的身份和当前时间作为作者和提交者,你的日志消息作为日志消息,并写出新的提交。最后,Git 将新提交的 ID 写入当前分支名称,这样新提交就是分支的新提示。
但是,当您使用git commit <em>path</em> 时,情况会发生变化。现在细节取决于你是运行git commit --only <em>path</em> 还是git commit --include <em>path</em>。不过,Git 仍然会从 an 索引构建树。
4事实上,每个工作树都有一个索引。但是,默认情况下,只有一个工作树。但也有临时索引,我们稍后会看到。
git commit <em>path</em> 和临时索引
当你运行git commit <em>path</em> 时,Git 必须建立一个 临时 索引,独立于普通索引。它从复制一些东西开始。它复制的内容取决于 --only 与 --include。
使用--only,Git 通过读取当前提交的内容来创建临时索引,即HEAD 提交,而不是通过读取普通索引的内容。使用--include,Git 通过读取普通索引的内容来创建临时索引。
在临时索引中,Git 然后将给定 path 的任何条目替换为从工作树中的文件版本生成的条目。如果 path 不在临时索引中,Git 会将其添加为新文件。无论哪种方式,这条路径现在都在临时索引中。
Git 现在使用临时索引而不是常规索引进行新的提交。新提交像往常一样进入存储库,更新当前分支名称,以便分支的提示提交是新提交。像往常一样,新提交的父级是旧的提示提交。但是既然提交完成了,Git 就有点进退两难了。
索引—— 索引,正常的索引——通常应该在“工作树上的工作”周期开始时与当前提交匹配。临时索引确实与新提交匹配,因为新提交是使用临时索引完成的。但是临时索引几乎可以肯定在某些方面与 索引不同。因此,下一步行动再次取决于--include 与--only:
-
如果您使用--include,临时索引从普通索引开始。临时索引与新提交匹配。所以临时索引变成了真正的索引。
此操作反映正常提交:Git 使用名为 .git/index.lock 的临时锁定文件,以确保在执行所有提交工作时没有任何更改。对于不带路径参数的普通提交,临时锁文件和真实索引除了某些时间戳外,内容相同,所以Git只是将锁文件重命名为索引文件路径名,就大功告成了。所以这处理了 no-path-arguments 情况和 --include with path arguments 情况。
-
如果您使用--only,Git 会使用它复制到临时索引中的条目更新普通索引,而保留普通索引的其余条目。这样,您专门提交的文件在当前(正常)索引中的格式与它们在当前提交中的格式相同。当前(正常)索引中的所有其他文件都与您运行 git commit 之前一样:它们仍然匹配或不匹配 HEAD 提交(其 other 条目,用于文件没有在命令行上给出,都匹配父提交),它们仍然匹配或不匹配工作树中的文件,所有这些都没有改变。
这一切对您的 prepare-commit-msg 挂钩意味着什么
与 Git 中的所有内容一样,您必须动态发现发生了什么变化。
您根本不应该查看工作树。您可能已通过git commit(没有路径名参数)调用,在这种情况下,使用的索引将是普通索引。您可能是通过git commit --include 或git commit --only 调用的,在这种情况下,正在使用的索引将是一个临时索引。
要找出索引(无论使用哪个索引)和 HEAD 提交之间的哪些文件不同,请使用 Git 提供的不同引擎之一。
一般来说,在您编写的任何代码中,除了您自己之外的其他用户,您应该使用 Git 所谓的管道命令。在这种情况下,所需的命令是git diff-index。另见Which are the plumbing and porcelain commands?
使用git diff-index -r HEAD 会将当前提交与当前索引文件中的任何内容进行比较,由$GIT_INDEX_FILE 和由于git worktree add 确定的任何替代工作树情况确定。方便的是,您无需在此处进行任何调整。但是如果用户调用了git commit --amend,你真的应该与当前提交的父级进行比较。没有很好的方法来确定是否是这种情况。5
git diff-index 的输出默认为如下所示:
:100644 100644 f5debcd2b4f05c50d5e70efc95d10d95ca6372cd e736da45f71a37b46d5d46056b74070f0f3d488a M wt-status.c
您可以在这里使用--name-status 修剪掉大部分不感兴趣的部分,它会生成:
$ git diff-index -r --name-status HEAD
M wt-status.c
注意状态字母后面的分隔符是制表符,但是如果你写一个shell循环的形式:
git diff-index -r --name-status HEAD | while read status path; do ...
一般来说你可能没问题。为了使其真正健壮,请使用有趣的路径名进行测试,包括空格和全局字符。 bash 或其他智能语言中的脚本可以使用-z 标志来更合理地编码。详情请见the documentation。
请注意,此处的文件可能是Added 或Deleted,而不仅仅是Modified。使用git diff-index 将使您免于检查Rnamed;使用git diff 不会,因为它会读取用户的配置,可能会设置diff.renames。您还应该准备好处理Type-change,以防有人用文件替换了符号链接,反之亦然。
一旦你有一个修改过的文件列表,或者如果你愿意,可以与获取列表交错(但这更复杂——你需要保留并使用:<mode> 的东西来进行健壮的逐行解码) ,您可以检查实际差异。例如:
$ git diff-index --cached -p HEAD -- wt-status.c
diff --git a/wt-status.c b/wt-status.c
index f5debcd2b..e736da45f 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -1,3 +1,4 @@
+
#include "cache.h"
#include "wt-status.h"
#include "object.h"
表明我只是在此处的文件顶部添加了一个空行。 (您需要 --cached 让 Git 从索引中查看 blob 内容,而不是查看工作树文件。您不需要带有初始 -r --name-status 变体的 --cached,尽管包含它是无害的它。这是git diff-index的一个烦人的功能。)
在收集所有git diff-index 输出并对其进行解析以发现您的日志消息文本后,您将准备好将新的提交日志消息写入日志消息文件。
5应该有。这是 Git 提交钩子的一个主题:它们没有提供足够的信息。更高版本的 Git 可能会为钩子添加更多参数,或设置特定的环境变量。例如,您可以在进程树中挖掘以尝试找到调用您的钩子的git commit 命令,然后查看它们的/proc 条目或ps 输出以找到它们的参数,但这非常丑陋和错误- 容易,并且不太可能在 Windows 上工作。