Como atribuir um SHA1 do Git a um arquivo sem o Git?

138

Pelo que entendi, quando o Git atribui um hash SHA1 a um arquivo, esse SHA1 é exclusivo do arquivo com base em seu conteúdo.

Como resultado, se um arquivo for movido de um repositório para outro, o SHA1 do arquivo permanecerá o mesmo, pois seu conteúdo não foi alterado.

Como o Git calcula o resumo SHA1? Faz isso no conteúdo completo do arquivo descompactado?

Eu gostaria de emular a atribuição de SHA1 fora do Git.

git-noob
fonte

Respostas:

255

É assim que o Git calcula o SHA1 para um arquivo (ou, em termos do Git, um "blob"):

sha1("blob " + filesize + "\0" + data)

Assim, você pode computar facilmente sem ter o Git instalado. Observe que "\ 0" é o byte nulo, não uma seqüência de dois caracteres.

Por exemplo, o hash de um arquivo vazio:

sha1("blob 0\0") = "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"

$ touch empty
$ git hash-object empty
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391

Outro exemplo:

sha1("blob 7\0foobar\n") = "323fae03f4606ea9991df8befbb2fca795e648fa"

$ echo "foobar" > foo.txt
$ git hash-object foo.txt 
323fae03f4606ea9991df8befbb2fca795e648fa

Aqui está uma implementação do Python:

from hashlib import sha1
def githash(data):
    s = sha1()
    s.update("blob %u\0" % len(data))
    s.update(data)
    return s.hexdigest()
Ferdinand Beyer
fonte
Esta resposta está assumindo o Python 2? Quando tento isso no Python 3, recebo uma TypeError: Unicode-objects must be encoded before hashingexceção na primeira s.update()linha.
Mark Booth
3
Com o python 3, você precisa codificar os dados: s.update(("blob %u\0" % filesize).encode('utf-8'))para evitar o TypeError.
Mark Booth
A codificação como utf-8 funcionará, mas provavelmente é melhor compilá-la a partir de uma sequência de bytes (a codificação utf-8 funciona porque nenhum dos caracteres unicode é não ASCII).
Torek
Outra coisa que vale a pena mencionar é que o objeto hash git também parece substituir "\ r \ n" por "\ n" no conteúdo dos dados. Pode muito bem tirar completamente os "\ r" 's, não verifiquei isso.
User420667
1
Coloquei uma implementação Python 2 + 3 (ambas em uma) de um gerador de hash de arquivo e árvore aqui: github.com/chris3torek/scripts/blob/master/githash.py (o hasher da árvore lê uma árvore de diretórios).
Torek
17

Um pouco de guloseima: com casca

echo -en "blob ${#CONTENTS}\0$CONTENTS" | sha1sum
knittl
fonte
1
Estou comparando echo -en "blob ${#CONTENTS}\0$CONTENTS" | sha1sumcom a saída de git hash-object path-to-filee eles produzem resultados diferentes. No entanto, echo -e ...produz os resultados corretos, exceto que há um final - ( nãogit hash-object produz caracteres finais). É com isso que eu deveria me preocupar?
FrustratedWithFormsDesigner
2
@FrustratedWithFormsDesigner: O final -é usado sha1sumse ele computou o hash do stdin e não de um arquivo. Nada para se preocupar. Porém -n, o estranho é que isso deve suprimir a nova linha normalmente anexada por eco. Por acaso, seu arquivo tem uma última linha vazia, que você esqueceu de adicionar à sua CONTENTSvariável?
knittl
Sim, você está correto. E eu pensei que a saída do sha1sum deveria ser apenas o hash, mas não é difícil removê-lo com o sed ou algo assim.
FrustratedWithFormsDesigner
@FrustratedWithFormsDesigner: Você obterá o mesmo resultado se você usar cat file | sha1sumem vez de sha1sum file(mais processos e tubulações embora)
knittl
8

Você pode criar uma função bash shell para calculá-la facilmente se não tiver o git instalado.

git_id () { printf 'blob %s\0' "$(ls -l "$1" | awk '{print $5;}')" | cat - "$1" | sha1sum | awk '{print $1}'; }
CB Bailey
fonte
1
Um pouco menor: (stat --printf="blob %s\0" "$1"; cat "$1") | sha1sum -b | cut -d" " -f1.
precisa saber é o seguinte
4

Dê uma olhada na página de manual do git-hash-object . Você pode usá-lo para calcular o hash git de qualquer arquivo em particular. Eu acho que o git alimenta mais do que apenas o conteúdo do arquivo no algoritmo de hash, mas não sei ao certo, e se ele alimenta dados extras, não sei o que é.

Dale Hagglund
fonte
2
/// Calculates the SHA1 for a given string
let calcSHA1 (text:string) =
    text 
      |> System.Text.Encoding.ASCII.GetBytes
      |> (new System.Security.Cryptography.SHA1CryptoServiceProvider()).ComputeHash
      |> Array.fold (fun acc e -> 
           let t = System.Convert.ToString(e, 16)
           if t.Length = 1 then acc + "0" + t else acc + t) 
           ""
/// Calculates the SHA1 like git
let calcGitSHA1 (text:string) =
    let s = text.Replace("\r\n","\n")
    sprintf "blob %d%c%s" (s.Length) (char 0) s
      |> calcSHA1

Esta é uma solução em F #.

forki23
fonte
Ainda tenho problemas com os trema: calcGitSHA1 ("ü"). ShouldBeEqualTo ("0f0f3e3b1ff2bc6722afc3e3812e6b782683896f") Mas minha função fornece 0d758c9c7c7bc06c1e307f05d92d896aaf0a8a. Alguma idéia de como o git hash-object manipula tremas?
forki23
ela deve lidar com o blob como um bytestream, isso significa que U tem provavelmente comprimento 2 (unicode), propriedade Length do F♯ voltará comprimento 1 (porque é apenas um caractere visível)
knittl
Mas System.Text.Encoding.ASCII.GetBytes ("ü") retorna uma matriz de bytes com 1 elemento.
forki23
Usar UTF8 e 2 como comprimento da string fornece uma matriz de bytes: [98; 108; 111; 98; 32; 50; 0; 195; 188] e, portanto, um SHA1 de 99fe40df261f7d4afd1391fe2739b2c7466fe968. O que também não é o git SHA1.
forki23
1
Você nunca deve aplicar resumos a cadeias de caracteres. Em vez disso, você deve aplicá-las a cadeias de bytes (matrizes de bytes) que você pode obter convertendo uma cadeia de caracteres em bytes usando uma codificação explícita.
dólmen
2

Implementação completa do Python3:

import os
from hashlib import sha1

def hashfile(filepath):
    filesize_bytes = os.path.getsize(filepath)

    s = sha1()
    s.update(b"blob %u\0" % filesize_bytes)

    with open(filepath, 'rb') as f:
        s.update(f.read())

    return s.hexdigest() 
Tomer
fonte
2
O que você realmente deseja é codificação ASCII. UTF8 só funciona aqui porque é compatível com ASCII e "blob x \ 0" contém apenas caracteres com código <= 127. #
Ferdinand Beyer
1

Em Perl:

#!/usr/bin/env perl
use Digest::SHA1;

my $content = do { local $/ = undef; <> };
print Digest::SHA1->new->add('blob '.length($content)."\0".$content)->hexdigest(), "\n";

Como um comando shell:

perl -MDigest::SHA1 -E '$/=undef;$_=<>;say Digest::SHA1->new->add("blob ".length()."\0".$_)->hexdigest' < file
dolmen
fonte
1

E no Perl (veja também Git :: PurePerl em http://search.cpan.org/dist/Git-PurePerl/ )

use strict;
use warnings;
use Digest::SHA1;

my @input = &lt;&gt;;

my $content = join("", @input);

my $git_blob = 'blob' . ' ' . length($content) . "\0" . $content;

my $sha1 = Digest::SHA1->new();

$sha1->add($git_blob);

print $sha1->hexdigest();
Alec the Geek
fonte
1

Usando Ruby, você poderia fazer algo assim:

require 'digest/sha1'

def git_hash(file)
  data = File.read(file)
  size = data.bytesize.to_s
  Digest::SHA1.hexdigest('blob ' + size + "\0" + data)
end
Leif
fonte
1

Um pequeno script Bash que deve produzir uma saída idêntica a git hash-object:

#!/bin/sh
( 
    echo -en 'blob '"$(stat -c%s "$1")"'\0';
    cat "$1" 
) | sha1sum | cut -d\  -f 1
Fordi
fonte
0

Em JavaScript

const crypto = require('crypto')
const bytes = require('utf8-bytes')

function sha1(data) {
    const shasum = crypto.createHash('sha1')
    shasum.update(data)
    return shasum.digest('hex')
}

function shaGit(data) {
    const total_bytes = bytes(data).length
    return sha1(`blob ${total_bytes}\0${data}`)
}
EnZo
fonte
-4

É interessante notar que, obviamente, o Git adiciona um caractere de nova linha ao final dos dados antes de serem hash. Um arquivo contendo nada além de "Hello World!" obtém um hash de blob de 980a0d5 ..., o mesmo que este:

$ php -r 'echo sha1("blob 13" . chr(0) . "Hello World!\n") , PHP_EOL;'
Nudge
fonte
4
Essa nova linha está sendo adicionada pelo seu editor de texto, não por git hash-object. Observe que fazer echo "Hello World!" | git hash-object --stdingive 980a0d5..., enquanto estiver usando, echo -ndá um hash c57eff5....
bdesham