Como o git calcula hashes de arquivos?

124

Os hashes SHA1 armazenados nos objetos da árvore (retornados por git ls-tree) não correspondem aos hashes SHA1 do conteúdo do arquivo (retornados por sha1sum)

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

Como o git calcula hashes de arquivos? Comprime o conteúdo antes de calcular o hash?

netvope
fonte
1
Para obter mais detalhes, consulte também progit.org/book/ch9-2.html
netvope 29/08/11
5
O link do netvope parece estar morto agora. Eu acho que este é o novo local: git-scm.com/book/en/Git-Internals-Git-Objects que é §9.2 do git-scm.com/book
Rhubbarb

Respostas:

122

Git prefixa o objeto com "blob", seguido pelo comprimento (como um número inteiro legível por humanos), seguido por um caractere NUL

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

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

Leif Gruenwoldt
fonte
2
Também vale mencionar que ele substitui "\ r \ n" por "\ n", mas deixa "\ r" s isolados.
User420667
8
^ correção para o comentário acima: algumas vezes o git faz a substituição acima, dependendo das configurações de eol / autocrlf.
User420667
5
Você também pode comparar isso com a saída de echo 'Hello, World!' | git hash-object --stdin. Opcionalmente, você pode especificar --no-filterspara garantir que nenhuma conversão crlf aconteça ou especificar --path=somethi.ngpara permitir que o git use o filtro especificado via gitattributes(também @ user420667). E -wpara realmente enviar o blob para .git/objects(se você estiver em um repositório git).
Tobias Kienzler
Que traduz a equivalência, a fazer sentido: echo -e 'blob 16\0Hello, \r\nWorld!' | shasum == echo -e 'Hello, \r\nWorld!' | git hash-object --stdin --no-filters e será também equivalente com \ne 15.
Peter Krauss
1
echoanexa uma nova linha à saída, que também é passada para o git. É por isso que seus 14 caracteres. Para usar o eco sem uma nova linha, escrevaecho -n 'Hello, World!'
Bouke Versteegh
36

Estou apenas expandindo a resposta @Leif Gruenwoldte detalhando o que está na referência fornecida por@Leif Gruenwoldt

Faça Você Mesmo..

  • Etapa 1. Crie um documento de texto vazio (o nome não importa) em seu repositório
  • Etapa 2. Preparar e confirmar o documento
  • Etapa 3. Identifique o hash do blob executando git ls-tree HEAD
  • Etapa 4. Encontre o hash do blob a ser e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
  • Etapa 5. Saia da surpresa e leia abaixo

Como o GIT calcula seus hashes de confirmação

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

O texto blob⎵é um prefixo constante e \0também é constante e é o NULLcaractere. O <size_of_file>e <contents_of_file>varia dependendo do arquivo.

Consulte: Qual é o formato do arquivo de um objeto de confirmação git?

E isso é tudo!

Mas espere! , você percebeu que <filename>não é um parâmetro usado para o cálculo de hash? Dois arquivos podem ter o mesmo hash se o conteúdo for indiferente à data e hora em que foram criados e ao nome. Essa é uma das razões pelas quais o Git manipula movimentos e renomeia melhor do que outros sistemas de controle de versão.

Faça Você Mesmo (Ext)

  • Etapa 6. Crie outro arquivo vazio com um diferente filenameno mesmo diretório
  • Etapa 7. Compare os hashes dos dois arquivos.

Nota:

O link não menciona como o treeobjeto está em hash. Eu não tenho certeza do algoritmo e dos parâmetros, no entanto, pela minha observação, provavelmente calcula um hash com base em todos os blobse trees(provavelmente seus hashes) que ele contém

Lordbalmon
fonte
SHA1("blob" + <size_of_file>- existe um caractere de espaço adicional entre blob e tamanho? O tamanho é decimal? É o prefixo zero?
Osgx
1
@osgx Existe. A referência e meus testes confirmam isso. Eu corrigi a resposta. Tamanho parece ser o número de bytes como número inteiro sem prefixo.
Samuel Harmer 14/05
13

git hash-object

Esta é uma maneira rápida de verificar seu método de teste:

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

Resultado:

f2ba8f84ab5c1bce84a7b441cb1959cfc7093b7f
f2ba8f84ab5c1bce84a7b441cb1959cfc7093b7f  -

onde sha1sumestá no GNU Coreutils.

Então, tudo se resume a entender o formato de cada tipo de objeto. Já cobrimos o trivial blob, aqui estão os outros:

Ciro Santilli adicionou uma nova foto
fonte
Conforme mencionado em uma resposta anterior, o comprimento deve ser calculado como $(printf "\0$s" | wc -c). Observe o caractere vazio adicionado. Ou seja, se a string for 'abc' com o caractere vazio adicionado na frente, o comprimento renderá 4, não 3. Em seguida, os resultados com sha1sum corresponderão ao objeto hash git.
Michael Ekoka
Você está certo de que eles combinam. Parece que há um efeito colateral pernicioso do uso de printf em vez de eco aqui. Quando você aplica git hash-object a um arquivo que contém a string 'abc', obtém 8baef1b ... f903, que é o que você obtém ao usar echo -e em vez de printf. Desde que echo -e adicione uma nova linha no final de uma string, parece que, para combinar o comportamento com printf, você pode fazer o mesmo (por exemplo, s = "$ s \ n").
Michael Ekoka
3

Com base na resposta de Leif Gruenwoldt , aqui está um substituto da função shell para 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/ .*$//'
}

Teste:

$ echo 'Hello, World!' > test.txt
$ git hash-object test.txt
8ab686eafeb1f44702738c8b0f24f2567c36da6d
$ git-hash-object test.txt
8ab686eafeb1f44702738c8b0f24f2567c36da6d
Lucas Cimon
fonte
3

Eu precisava disso para alguns testes de unidade no Python 3, então pensei em deixá-lo aqui.

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()

Eu mantenho \nfinais de linha em todos os lugares, mas em algumas circunstâncias o Git também pode estar alterando seus finais de linha antes de calcular esse hash, para que você possa precisar de um .replace('\r\n', '\n')também.

Samuel Harmer
fonte