141

The SHA1 hashes stored in the tree objects (as returned by git ls-tree) do not match the SHA1 hashes of the file content (as returned by sha1sum):

$ git cat-file blob 4716ca912495c805b94a88ef6dc3fb4aff46bf3c | sha1sum
de20247992af0f949ae8df4fa9a37e4a03d7063e  -

How does Git compute file hashes? Does it compress the content before computing the hash?

Matthias Braun
  • 32,039
  • 22
  • 142
  • 171
netvope
  • 7,647
  • 7
  • 32
  • 42
  • 13
    See [assigning Git SHA1's without Git](http://stackoverflow.com/questions/552659/assigning-git-sha1s-without-git) – sanmai Aug 29 '11 at 01:41
  • 1
    For more details, also see http://progit.org/book/ch9-2.html – netvope Aug 29 '11 at 22:33
  • 5
    netvope's link seems to be dead now. I think this is the new location: http://git-scm.com/book/en/Git-Internals-Git-Objects which is §9.2 from http://git-scm.com/book – Rhubbarb Jul 22 '14 at 14:50
  • Related: [What is the file format of a git commit object?](https://stackoverflow.com/q/22968856/55075) – kenorb Dec 11 '18 at 14:26

6 Answers6

138

Git prefixes the object with "blob ", followed by the length (as a human-readable integer), followed by a NUL character

$ echo -e 'blob 14\0Hello, World!' | shasum 8ab686eafeb1f44702738c8b0f24f2567c36da6d

Source: http://alblue.bandlem.com/2011/08/git-tip-of-week-objects.html

Leif Gruenwoldt
  • 13,561
  • 5
  • 60
  • 64
  • 3
    Also worth mentioning that it replaces "\r\n" with "\n", but leaves isolated "\r"s alone. – user420667 May 24 '16 at 17:22
  • 9
    ^correction to above comment: *sometimes* git does the replacement above, depending on one's eol/autocrlf settings. – user420667 May 26 '16 at 17:38
  • 9
    You can also compare this to the output of [`echo 'Hello, World!' | git hash-object --stdin`](https://git-scm.com/docs/git-hash-object#_options). Optionally you can specify `--no-filters` to make sure no crlf conversion happens, or specify `--path=somethi.ng` to let git use the filter specified via `gitattributes` (also @user420667). And `-w` to actually submit the blob to `.git/objects` (if you _are_ in a git repo). – Tobias Kienzler Feb 07 '17 at 07:55
  • 1
    Expressing the equivalence, to make sense: `echo -e 'blob 16\0Hello, \r\nWorld!' | shasum` **==** `echo -e 'Hello, \r\nWorld!' | git hash-object --stdin --no-filters` and it will be also equivalent with `\n` and 15. – Peter Krauss Jun 11 '17 at 21:41
  • Shouldn't the length be `13` and not `14`? – Armen Michaeli Nov 17 '18 at 02:03
  • @amn the nul character \0 gets counted as a character. – Leif Gruenwoldt Nov 17 '18 at 02:20
  • That's strange -- I've digested the text `blob \0` where `` is file size in bytes, followed by the file's contents, and the result matches what `git hash-object --no-filters ` gives me. When I count the null byte as an additional byte for `` the hashes are obviously no longer equal. – Armen Michaeli Nov 17 '18 at 13:15
  • 1
    `echo` appends a newline to the output, which is also passed into git. That's why its 14 characters. To use echo without a newline, write `echo -n 'Hello, World!'` – Bouke Versteegh Sep 01 '19 at 09:14
  • Wouldn't this cause two files with the same contents to have the same hash (because the string being hashed only varies with the contents and length of the file)? – Telescope Aug 13 '23 at 03:49
37

I am only expanding on the answer by @Leif Gruenwoldt and detailing what is in the reference provided by @Leif Gruenwoldt

Do It Yourself..

  • Step 1. Create an empty text document (name does not matter) in your repository
  • Step 2. Stage and Commit the document
  • Step 3. Identify the hash of the blob by executing git ls-tree HEAD
  • Step 4. Find the blob's hash to be e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
  • Step 5. Snap out of your surprise and read below

How does GIT compute its commit hashes

    Commit Hash (SHA1) = SHA1("blob " + <size_of_file> + "\0" + <contents_of_file>)

The text blob⎵ is a constant prefix and \0 is also constant and is the NULL character. The <size_of_file> and <contents_of_file> vary depending on the file.

See: What is the file format of a git commit object?

And thats all folks!

But wait!, did you notice that the <filename> is not a parameter used for the hash computation? Two files could potentially have the same hash if their contents are same indifferent of the date and time they were created and their name. This is one of the reasons Git handles moves and renames better than other version control systems.

Do It Yourself (Ext)

  • Step 6. Create another empty file with a different filename in the same directory
  • Step 7. Compare the hashes of both your files.

Note:

The link does not mention how the tree object is hashed. I am not certain of the algorithm and parameters however from my observation it probably computes a hash based on all the blobs and trees (their hashes probably) it contains

kenorb
  • 155,785
  • 88
  • 678
  • 743
Lordbalmon
  • 1,634
  • 1
  • 14
  • 31
  • `SHA1("blob" + ` - is there additional space character between blob and size? Is size decimal? Is it zero-prefixed? – osgx Jul 18 '16 at 07:28
  • 1
    @osgx There is. The reference and my testing confirms so. I've corrected the answer. Size seems to be number of bytes as integer with no prefix. – Samuel Harmer May 14 '17 at 12:15
17

git hash-object

This is a quick way to verify your test method:

s='abc'
printf "$s" | git hash-object --stdin
printf "blob $(printf "$s" | wc -c)\0$s" | sha1sum

Output:

f2ba8f84ab5c1bce84a7b441cb1959cfc7093b7f
f2ba8f84ab5c1bce84a7b441cb1959cfc7093b7f  -

where sha1sum is in GNU Coreutils.

Then it comes down to understanding the format of each object type. We have already covered the trivial blob, here are the others:

Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
  • As mentioned in a previous answer, the length should rather be calculated as `$(printf "\0$s" | wc -c)`. Note the added empty character. That is, if the string is 'abc' with the added empty character in front the length would yield 4, not 3. Then the results with sha1sum matches git hash-object. – Michael Ekoka Apr 11 '17 at 10:24
  • You're right they do match. It seems that there's a bit of a pernicious side effect from using printf rather than echo -e here. When you apply git hash-object to a file containing the string 'abc' you get 8baef1b...f903 which is what you get when using echo -e rather than printf. Provided that echo -e adds a newline at the end of a string it seems that to match the behavior with printf you can do the same (i.e. s="$s\n"). – Michael Ekoka Apr 11 '17 at 12:04
  • 1
    upvote for using `printf` rather than `echo -e` – go2null Feb 25 '22 at 15:57
4

I needed this for some unit tests in Python 3 so thought I'd leave it here.

def git_blob_hash(data):
    if isinstance(data, str):
        data = data.encode()
    data = b'blob ' + str(len(data)).encode() + b'\0' + data
    h = hashlib.sha1()
    h.update(data)
    return h.hexdigest()

I stick to \n line endings everywhere but in some circumstances Git might also be changing your line endings before calculating this hash so you may need a .replace('\r\n', '\n') in there too.

Samuel Harmer
  • 4,264
  • 5
  • 33
  • 67
3

Based on Leif Gruenwoldt answer, here is a shell function substitute to git hash-object :

git-hash-object () { # substitute when the `git` command is not available
    local type=blob
    [ "$1" = "-t" ] && shift && type=$1 && shift
    # depending on eol/autocrlf settings, you may want to substitute CRLFs by LFs
    # by using `perl -pe 's/\r$//g'` instead of `cat` in the next 2 commands
    local size=$(cat $1 | wc -c | sed 's/ .*$//')
    ( echo -en "$type $size\0"; cat "$1" ) | sha1sum | sed 's/ .*$//'
}

Test:

$ echo 'Hello, World!' > test.txt
$ git hash-object test.txt
8ab686eafeb1f44702738c8b0f24f2567c36da6d
$ git-hash-object test.txt
8ab686eafeb1f44702738c8b0f24f2567c36da6d
Tobias Kienzler
  • 25,759
  • 22
  • 127
  • 221
Lucas Cimon
  • 1,859
  • 2
  • 24
  • 33
0

This is a python3 version for binary hash calculation (the above example is for text)

For purpose of readability put this code in your own def. Also note, the code is a snippet, not a complete script. For your inspiration.

    targetSize: int
exists: bool
if os.path.exists(targetFile):
    exists = True
    targetSize = os.path.getsize(targetFile)
else:
    exists = False
    targetSize = 0
openMode: str
if exists:
    openMode = 'br+'
else:
    openMode = 'bw+'
with open(targetFile, openMode) as newfile:
    if targetSize > 0:
        header: str = f"blob {targetSize}\0"
        headerBytes = header.encode('utf-8')
        headBytesLen = len(headerBytes)
        buffer = bytearray(headBytesLen + targetSize)
        buffer[0:0+headBytesLen] = headerBytes
        buffer[headBytesLen:headBytesLen+targetSize] = newfile.read()
        sha1Hash = hashlib.sha1(buffer).hexdigest()
        if not sha == sha1Hash:
            newfile.truncate()
        else:
            continue
    with requests.get(fullFile) as response2:            
        newfile.write(response2.content)
Egbert Nierop
  • 2,066
  • 1
  • 14
  • 16