为什么git hash-object返回的哈希值与openssl sha1不同?

62

背景:我从code.google下载了一个文件(Audirvana 0.7.1.zip)到我的Macbook Pro(Mac OS X 10.6.6)。

我想要验证这个特定文件的校验和,它的SHA-1值被发布为862456662a11e2f386ff0b24fdabcb4f6c1c446a。 git hash-object给出了不同的哈希值,但是openssl sha1返回了预期的862456662a11e2f386ff0b24fdabcb4f6c1c446a。

以下实验似乎排除了任何可能的下载损坏或换行符差异,并表明实际上有两种不同的算法在起作用:

$ echo A > foo.txt
$ cat foo.txt
A
$ git hash-object foo.txt 
f70f10e4db19068f79bc43844b49f3eece45c4e8
$ openssl sha1 foo.txt 
SHA1(foo.txt)= 7d157d7c000ae27db146575c08ce30df893d3a64

发生了什么?


这个网址http://progit.org/book/ch9-2.html上有一篇很好的文章。 - Josh Lee
书籍位置已更改:http://git-scm.com/book/ch9-2.html#Object-Storage(我无法在SO上编辑评论)。 - riezebosch
可能是Assigning Git SHA1's without Git的重复问题 || https://dev59.com/BGw05IYBdhLWcg3weBpF - Ciro Santilli OurBigBook.com
5个回答

82

你看到了差异是因为git hash-object不仅仅是对文件中的字节进行哈希 - 它在文件内容前面添加了字符串"blob",紧随其后的是文件大小和一个NUL字符,然后再进行哈希。关于此更多详细信息,请参阅 Stack Overflow 上的另一个答案:

或者,您可以尝试类似以下命令来验证:

$ echo -n hello | git hash-object --stdin
b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0

$ printf 'blob 5\0hello' > test.txt
$ openssl sha1 test.txt
SHA1(test.txt)= b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0

4
为什么 Git 的作者选择了这种行为? - liori
1
liori:我只能猜测。我已经添加了一个答案,展示了它在一个特殊情况下的使用方式,但我怀疑那不是唯一的原因。 - araqnid
3
我猜这是为了确保没有与提交或树等相同对象名称(SHA1sum)的数据块存在,每个对象在计算哈希值之前至少会被添加它们的类型。 - Mark Longair
5
同时,“blob <filesize>\0”(或类似的内容)在文件开头,意味着你可以通过解压缩对象文件的前几个字节快速确定对象的类型。有关压缩以及实际写入磁盘的内容,请参阅Pro Git中Git对象的这一章节的详细介绍。 - Mark Longair
2
@liori:Git 以这种方式使用 SHA-1 是有道理的,因为它的目的是对文件树进行版本控制,而不是像 sha1sum 或 md5sum 这样的命令行实用程序。 - twcamper
2
@liori:Git中的所有“对象”类型(blob、commit、tag和tree)都由哈希命名。有一个命令cat-file -t,例如git cat-file -t a7bb6fb0,可以告诉你以a7bb6fb0开头的“对象”的“类型”...这是因为实际的对象(存储在仓库中,压缩)以“blob”或“tree”等开头。您可以使用类似python -c "import zlib,sys;print repr(zlib.decompress(sys.stdin.read()))" < .git/objects/a7/bb6fb0*的命令查看对象。总之,git的名称是git“对象”的哈希值,而不仅仅是内部的blob。 - ShreevatsaR

6
SHA1摘要是在头字符串后跟文件数据计算的。头部由对象类型、空格和对象长度(以十进制字节为单位)组成。这个头部通过一个空字节与数据分开。
因此:
$ git hash-object foo.txt
f70f10e4db19068f79bc43844b49f3eece45c4e8
$ ( perl -e '$size = (-s shift); print "blob $size\x00"' foo.txt \
               && cat foo.txt ) | openssl sha1
f70f10e4db19068f79bc43844b49f3eece45c4e8

这会导致“空”树和“空”blob具有不同的ID。即:
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391始终表示“空文件” 4b825dc642cb6eb9a060e54bf8d69288fbee4904始终表示“空目录”
你会发现,实际上可以在没有注册对象的新git存储库中执行“git ls-tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904”,因为它被识别为特殊情况并且从未实际存储(使用现代Git版本)。相比之下,如果向存储库添加一个空文件,则将存储一个blob“e69de29bb2d1d6434b8b29ae775ad8c2e48c5391”。

3

Git 将对象存储为 [对象类型,对象长度,分隔符 (\0),内容]。在您的情况下:

$ echo "A" | git hash-object --stdin
f70f10e4db19068f79bc43844b49f3eece45c4e8

尝试按以下方式计算哈希值:
$ echo -e "blob 2\0A" | shasum 
f70f10e4db19068f79bc43844b49f3eece45c4e8  -

请注意,在bash shell中使用-e选项,并根据需要调整新行的长度。

2

0

注意过滤器!

git 实际上是在计算 sha 值之前对文件进行过滤。通常情况下,\r\n 的换行符会被转换为 \n。这就是为什么你可能会在使用 git hash-objectgit hash-object --no-filters 时得到不同的结果。一些其他的东西也可能被过滤掉,而 .gitattributes 可能会对结果产生影响。

以下是一个在 Windows 命令提示符中的小例子:

在一个新文件夹中创建测试文件:

$ echo this is a test $Id$ > test1.txt
$ echo this is a test $Id: ffbf88668784c14e809c8c449d799b654d7a5fc5 $ > test2.txt

现在使用 git hash-object

$ git hash-object test1.txt
0c3a75d8155d54c2367e290cf7f33434805410be

$ git hash-object test2.txt
60fff1b8ec47ed41254719681e32369d640d6a0f

$ git hash-object --no-filters test2.txt
2f68d9b80a38fb800f039ef9062c764d2a4d4352

不同的文件导致不同的哈希值:没问题,但是 `git` 在某种程度上会过滤文件,因为 `--no-filters` 有影响。
现在在文件夹中创建一个 git 存储库和 .gitattributes。
$ git init .
Initialized empty Git repository in ~/.git

$ echo *.txt ident > .gitattributes

$ git hash-object test1.txt
0c3a75d8155d54c2367e290cf7f33434805410be

$ git hash-object test2.txt
0c3a75d8155d54c2367e290cf7f33434805410be

$ git hash-object --no-filters test2.txt
2f68d9b80a38fb800f039ef9062c764d2a4d4352

现在test1和test2具有相同的哈希值!但是--no-filters选项仍然给出相同的值。

结论:您可以使用git和openssl获得相同的哈希值,但需要确保您的文件不受git过滤器的影响。


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