Git:我如何列出提交中已更改的文件,包括每个文件(blob)的SHA-1哈希?

4

我的要求,简述:

我有一个提交记录,例如 HEAD111abc111,我想要一种优雅的方式来打印所有已修改的文件和仅已修改的文件以及它们的 SHA-1 哈希值。我该怎么做?

下面是使用 git-cat-file 的一个思路,它几乎可以工作,但它要么列出所有文件(包括未更改的文件),要么必须在批处理模式下使用。最初在批处理模式下使用似乎很有前途,但我无法使其正常工作。请参见下面关于我尝试过的使用 git-ls-tree 等方法的内容。

关于我的优先事项,请参见本问题下面的说明,或查看我自己编写的答案(我不会接受它,但也许你可以重构它)。

具体示例:

设置示例:

为了背景,让我们看一下我的 Git 工作树的样子:

$ ls

alice.txt
bob.c
carol.h
main.c

$ git status -s

# Nothing prints, the working copy is clean and untouched.

我现在只需要更改两个文件:

$ echo "Add one line." >> bob.c

$ echo "Add one line." >> carol.h

$ git add .    # Add (stage) both changed files.

$ git status -s

M  bob.c
M  carol.h

$ git commit -m "Two changed files."

[master 111abc111] Two changed files.
 2 files changed, 2 insertions(+), 0 deletions(-)

这个功能基本上满足我的要求:

$ git cat-file -p 111abc111:./

100644 blob 99c2e88ad312f1eac63afc908f64c370fac9d947    .gitignore
100644 blob 607f8ea764981fb3f92a8d91abc2b154d99bc39c    alice.txt
100755 blob 5a297bd6931c1a70abbcab919815324258c08b0f    bob.c
100644 blob c6c2dfd18d26c1cf71b21e9d4c0892157dd6ec33    carol.h
100755 blob d0802cd238a3e83f186bc5c24be7e23dfc69205f    main.c

上述命令的问题在于它列出了指定路径(./)下的所有文件,而不仅仅是修改过的文件。我只想显示bob.ccarol.h
第二个问题是使用111abc111:./指定树对象只会显示该目录中的文件(blobs),而不会显示子目录中的文件。子目录将以以下方式显示:
040000 tree b98f38763b689e8197c6129726d41169fceeaaa0    subdir

可能的想法:

我刚刚删除了一些我尝试过的内容。

我怀疑关键在于使用git-diff来制作一个“git对象”(包括blob)的列表,这些对象在指定的提交中发生了变化,然后以某种格式将该列表传递给git-cat-file。因此,像这样的魔术命令可能会起作用:

$ git diff 111abc111^ 111abc111 --magic-options-go-here | git cat-file --batch-check='%(objectname) %(objectsize)'

关键是找到--magic-options-go-here的值。我也不确定git-cat-file是否是我想要的管道符号右侧的内容,我可能需要其他东西。

谢谢。

编辑:我的优先事项

我更感兴趣的是“Git对象”,即在Git中存储并由SHA-1哈希标识的实体,如树、块、提交,以及我没有考虑过的其他东西,如标签。我对文件名和如果您检出提交后实际看起来的方式不太感兴趣。

我想要查看SHA-1哈希,以便我可以看到“哦,这个合并提交指向不同分支中的那个树”。对于大型仓库中的分支、合并和变基,每个提交对象都包含大量未更改的树和块,它们只是指针(引用),而它们所引用的东西在概念上可能会非常远。当您仅更改一行代码时,进行git-commit,然后进行git-push,并推送50 MiB的数据时,这一点可能会变得明显。在内部,Git只需解引用大量指针并创建新的增量、packfiles等即可。在工作目录(文件系统)中感觉像小变化的事情,在Git仓库二进制格式中实际上可能代表大量数据。


1
你不能通过使用 git diff --raw 命令得到你需要的大部分内容吗? - Hasturkun
5个回答

3
就像这样简单:
git show --stat --name-only 'YOUR_COMMIT_HASH'

1
我认为这个可以满足你的需求:

git diff --stat --name-only $COMMIT^ $COMMIT \
  | xargs git ls-tree --full-tree  $COMMIT

这可以被放入别名中:
# Usage: git changed-files <commit>
# List files changed in a commit.
git config --local --add alias.changed-files '!f() { git diff --stat --name-only $1^ $1 | xargs git ls-tree --full-tree  $1 ; }; f'

这是我的一个代码库的输出示例:
$ git changed-files d3a3029ca7489cb168d493de3d695809e84ffb0f
100644 blob 39855d9b6918f1c02f33115e357d7beeed1aaab8    libstdc++-v3/ChangeLog
100644 blob d0257c07e1fe92da339512d2457ac2ad43b12686    libstdc++-v3/include/std/optional
100644 blob 86b58ccf225597a64995878edc68c8666fa2c675    libstdc++-v3/include/std/type_traits
100644 blob 020cb26453f465ac49afb87f77e4833d0fb3aa16    libstdc++-v3/testsuite/20_util/optional/cons/value_neg.cc

它可以被优化,以显示两个任意提交之间的差异,当给定两个参数时:
# Usage: git changed-files <commit> [<commit>]
# List files changed in a commit (or between two commits).
git config --local --add alias.changed-files '!f() { git diff --stat --name-only ${2:-$1^} $1 | xargs git ls-tree --full-tree  $1 ; }; f'

谢谢,这在非常简单的情况下有效,并且有时确实有效。我有点惊讶于xargs可以处理带有空格的路径,至少对我来说在一个平台上是这样的。这不是真正基于git的解决方案,这可能是可以接受的,但可能不太可移植。由于不使用git-cat-file,它具有一些限制,例如无法显示blob的大小(-s选项)。它似乎也容易受到重命名和删除文件的影响。 - SerMetAla
基本限制在于你的管道仅传递文件名,实际上并未传递“git对象”,后者更具意义并可保留blob、tree、commit和delta之间的实际逻辑关系。例如,使用git-diff正确地将或不将跟随重命名。 - SerMetAla
使用GNU xargs,您可以使用“xargs --delim=\n”来在每个文件名之间断开换行符,忽略空格,但这使其不太便携。对于POSIX,您可以执行“xargs -I %@ git ls-tree --full-tree $1 %@”。但是,如果您想要一个纯git解决方案,您将不得不自己编写一个在更低级别上工作的解决方案。 - Jonathan Wakely
我的解决方案似乎满足了你在问题中提出的所有要求(“我想要一种优雅的方法,可以打印出所有修改过的文件以及仅修改过的文件,并附上它们的SHA-1哈希值”),但是既然你现在改变了目标,我不确定如何改进答案! - Jonathan Wakely
我不打算改变目标,我认为我没有清楚地表达原始问题。我添加了自己可怕的hackish答案来强调我的优先事项。您的解决方案非常适用于“修改文件”的最狭义定义,即未添加、未删除、未重命名、未重命名且重写90%等。它是通过文件名传递的修改文件。我的hackish答案(我也不打算接受)则采取相反的方法,它深深地基于git对象,并比较组成两个提交的对象。 - SerMetAla

0

OP的回答非常丑陋:

如果我需要接受这个答案,那么我会很难过。但我想写下它,因为我已经考虑过了:

步骤1:在$COMMIT^1上执行git-ls-tree

$ git ls-tree -r $COMMIT^1:./

这将为您提供树状结构中该提交的每个文件和目录的长列表。 -r选项使其递归,因此它显示每个文件和目录。

将此输出存储在某个地方。

步骤2:对$COMMIT执行git-ls-tree

$ git ls-tree -r $COMMIT:./

再次,将输出存储在某个地方。

步骤3:编写Python脚本以删除所有未更改的行

使用上面的两个(非常大的)STDOUT转储,编写一个Python脚本,仅当SHA-1出现在$COMMIT^1$COMMIT中时才删除行。

任何SHA-1出现在两者中的内容都是未更改的“git对象”。无论文件名是否更改,无论它是文件(或树、标签或其他我没有意识到的东西),如果SHA-1更改或是新的,则这是$COMMIT^1$COMMIT之间的更改。

这很不专业,但它不会错过任何类型的更改。它可能会稍微过度报告更改。

如果没有人有基于Git的神奇答案,那么我可能会编写这个Python脚本并在这里发布。


使用Bash和GNU join以及GNU sort,类似于:join -j 3 <(git ls-tree -r $COMMIT^:../ | sort -k 3) <(git ls-tree -r $COMMIT:../ | sort -k 3 ) -v 1 - Jonathan Wakely
这是一个不错的建议,但我个人会使用Python,然后使用set对象。我喜欢编写Python,它非常便携,并且set对象恰好是正确的东西。我想要的答案是两个集合的xor(如果我使用这个可怕的hack解决方案)。 - SerMetAla
当然,最终它会更灵活,但在你开始编写Python版本之前,我已经编写了该管道,因此它可以作为概念验证。 - Jonathan Wakely

0

第一个问题在于定义“修改”对象:相对于什么来说是“修改”?每个提交都是一个快照;快照并不能告诉你发生了什么变化。要查找变化,必须选择一些额外的快照。

使用带有两个提交(或树)哈希值的git diff可以得到答案:在这两个哈希值之间进行了修改。使用commit^意味着将一个提交与其直接前任进行比较,这通常是普通提交的正确答案。但对于合并提交来说,情况就更加棘手,因为它们—按定义!—有两个或多个直接前任。

请注意,如果您想将潜在的提交或注释标签哈希转换为树哈希(以识别顶级树哈希 ID),则使用 git rev-parse 是正确的方法:git rev-parse $hash^{tree} 验证了无论对象 $hash 标识什么,都可以跟随到找到一个树对象的点,然后 git rev-parse 发出树的哈希 ID。这对于检测是否使用了 -s ours 运行了 git merge 很有用,例如:如果是这样,合并提交的树哈希与合并提交的第一个父提交的树哈希匹配。请参阅 gitrevisions 文档 以获取特定操作的多种拼写方式,例如使用 ^{} 查找标签对象下面的对象(不管底层对象的类型如何)与使用 ^{commit} 查找提交并在不是提交时失败。这些后缀操作适用于大多数有效的语法,但不适用于 :/search,因此有时最好使用两步过程:首先将一些任意用户提供的字符串解析为哈希,然后使用 ${hash}${suffix}

在找到您想要的对象(可能包括或停留在顶层树对象)后,您确实可以使用git ls-tree(带有或不带有递归-r)。但现在,既然已经定义了“修改”的含义,您必须选择哪些修改算作“修改”。例如,如果提交P和C之间的差异仅在于文件path/to/script模式100644变为100755,那怎么办?或者,如果path/to/script曾经是一个常规文件,现在是一个符号链接呢?如果某个路径曾用于命名文件,但现在是一个充满文件的目录,反之亦然呢?

在编程方面,您可以使用 --name-only 或者 --name-statusgit diff 命令一起使用。而且,通过使用带有 -M 参数的 git diff 命令,您可以启用 Git 的“树形重命名检测”功能。

但现在需要考虑的是,git rev-parsegit ls-tree 是“管道”命令,它们以明确定义和可编写脚本的方式运行。另一方面,git diff 不是这样的:“瓷器”命令,因此它遵循每个用户和每个代码仓库的配置。这意味着在代码仓库 A 中使用 git diff 默认情况下可以找到重命名文件,而在代码仓库 B 中可能无法找到。

这意味着如果你在考虑使用git diff作为一个可能的工具,你应该转而考虑其plumbing变体:git diff-filesgit diff-indexgit diff-tree。如果你已经决定比较提交所附加的树,则git diff-tree是明显的赢家。

它使用起来有点麻烦,但是它被设计成可以通过脚本驱动,并且特别是从Python中你可以使用-z-r选项运行git diff-tree,并将输出视为一系列b'\0'分隔的记录。请注意,git diff-tree将自动使用常规提交的父提交作为两个输入中的第一个:

$ git diff-tree -r HEAD
b7bd9486b055c3f967a870311e704e3bb0654e4f
:100644 100644 2b45b6ff5cb3bf2980ad911b8c84179f27d8d72c f2e261abf38dba4e608de1ca40a805f2b0e3818c M      Documentation/RelNotes/2.19.0.txt

对于合并提交,git diff-tree 默认执行组合差异,其中来自任何一个父提交的未更改文件都会被抑制。为了避免这种情况,可以提供两个明确的哈希值(提交或树哈希值),或者使用-m将合并拆分为多个虚拟提交,每个提交只有一个父提交,并针对每个父提交获取差异。

0
直接的方法是使用--raw格式,git show可以实现您想要的人性化操作,它会将所有父提交中的差异合并在一起:
$ git show --pretty= --raw @^
::100644 100644 100644 d1ab6625f6 dfb6c554ac 00abe95315 MM      builtin/commit-graph.c
::100644 100644 100644 f013a84e29 417b7eac9c 3da52847e4 MM      commit-graph.c
::100755 100755 100755 9bf920ae17 786b5f73ef 117dca317e MM      t/t5318-commit-graph.sh
::100755 100755 100755 53b2e6b455 e2017bc24b 6dda4c1f1c MM      t/t5324-split-commit-graph.sh
$

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