Come assegnare un Git SHA1 a un file senza Git?


138

A quanto ho capito quando Git assegna un hash SHA1 a un file, questo SHA1 è univoco per il file in base al suo contenuto.

Di conseguenza se un file si sposta da un repository a un altro, SHA1 per il file rimane lo stesso in quanto il suo contenuto non è cambiato.

In che modo Git calcola il digest SHA1? Lo fa sul contenuto completo del file non compresso?

Vorrei emulare l'assegnazione di SHA1 al di fuori di Git.




Risposte:


256

Ecco come Git calcola SHA1 per un file (o, in termini Git, un "blob"):

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

Quindi puoi facilmente calcolarlo da solo senza aver installato Git. Si noti che "\ 0" è il byte NULL, non una stringa di due caratteri.

Ad esempio, l'hash di un file vuoto:

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

$ touch empty
$ git hash-object empty
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391

Un altro esempio:

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

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

Ecco un'implementazione di Python:

from hashlib import sha1
def githash(data):
    s = sha1()
    s.update("blob %u\0" % len(data))
    s.update(data)
    return s.hexdigest()

Questa risposta sta assumendo Python 2? Quando provo questo su Python 3 ottengo TypeError: Unicode-objects must be encoded before hashingun'eccezione sulla prima s.update()riga.
Mark Booth,

3
Con python 3 devi codificare i dati: s.update(("blob %u\0" % filesize).encode('utf-8'))per evitare il file TypeError.
Mark Booth,

La codifica come utf-8 funzionerà, ma probabilmente è meglio crearla da una stringa di byte in primo luogo (la codifica utf-8 funziona perché nessuno dei caratteri unicode è non ASCII).
Torek,

Un'altra cosa degna di nota è che git hash-object sembra anche sostituire "\ r \ n" con "\ n" nel contenuto dei dati. Potrebbe benissimo eliminare completamente il "\ r", non l'ho verificato.
user420667,

1
Ho messo un'implementazione Python 2 + 3 (entrambe in una) di un generatore di hash di file e alberi qui: github.com/chris3torek/scripts/blob/master/githash.py (l'hash dell'albero legge un albero di directory).
torek,

17

Un piccolo tesoro: in guscio

echo -en "blob ${#CONTENTS}\0$CONTENTS" | sha1sum

1
Sto confrontando echo -en "blob ${#CONTENTS}\0$CONTENTS" | sha1sumcon l'output di git hash-object path-to-filee producono risultati diversi. Tuttavia, echo -e ...produce i risultati corretti, tranne per il fatto che esiste un finale - ( nongit hash-object produce caratteri finali). È qualcosa di cui dovrei preoccuparmi?
FrustratedWithFormsDesigner,

2
@FrustratedWithFormsDesigner: il trailing -viene utilizzato sha1sumse ha calcolato l'hash da stdin e non da un file. Nulla di cui preoccuparsi. Una cosa strana però riguardo al -n, che dovrebbe sopprimere la nuova riga normalmente aggiunta dall'eco. Il tuo file ha per caso un'ultima riga vuota, che hai dimenticato di aggiungere nella tua CONTENTSvariabile?
Knittl,

Sì, hai ragione. E avevo pensato che l'output di sha1sum dovesse essere solo l'hash, ma non è difficile rimuoverlo con sed o qualcosa del genere.
FrustratedWithFormsDesigner,

@FrustratedWithFormsDesigner: Otterrai lo stesso output se usi cat file | sha1suminvece di sha1sum file(più processi e tubazioni)
knittl

9

Puoi fare una funzione di shell bash per calcolarla abbastanza facilmente se non hai installato git.

git_id () { printf 'blob %s\0' "$(ls -l "$1" | awk '{print $5;}')" | cat - "$1" | sha1sum | awk '{print $1}'; }

1
Un po 'più corta: (stat --printf="blob %s\0" "$1"; cat "$1") | sha1sum -b | cut -d" " -f1.
sschuberth,

4

Dai un'occhiata alla pagina man per git-hash-object . Puoi usarlo per calcolare l'hash git di qualsiasi file particolare. Io penso che si nutre git più che solo il contenuto del file nella algoritmo di hash, ma non lo so per certo, e se non si nutrono di dati aggiuntivi, non so cosa sia.


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

Questa è una soluzione in F #.


Ho ancora problemi con le umlaut: calcGitSHA1 ("ü"). ShouldBeEqualTo ("0f0f3e3b1ff2bc6722afc3e3812e6b782683896f") Ma la mia funzione dà 0d758c9c7bc06c1e307f05d92d896aafca Qualche idea su come git hash-object gestisce le umlaut?
forki23,

dovrebbe gestire il BLOB come un bytestream, ciò significa che probabilmente ü ha lunghezza 2 (unicode), la proprietà Length di F♯ restituirà la lunghezza 1 (perché è solo un carattere visibile)
knittl

Ma System.Text.Encoding.ASCII.GetBytes ("ü") restituisce una matrice di byte con 1 elemento.
forki23,

L'uso di UTF8 e 2 come lunghezza della stringa fornisce un array di byte: [98; 108; 111; 98; 32; 50; 0; 195; 188] e quindi uno SHA1 di 99fe40df261f7d4afd1391fe2739b2c7466fe968. Che non è anche il git SHA1.
forki23,

1
Non devi mai applicare digest alle stringhe di caratteri. Invece è necessario applicarli alle stringhe di byte (array di byte) che è possibile ottenere convertendo una stringa di caratteri in byte utilizzando una codifica esplicita.
dolmen,

2

Implementazione completa di 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() 

2
Quello che vuoi veramente è la codifica ASCII. UTF8 funziona qui solo perché è compatibile con ASCII e "blob x \ 0" contiene solo caratteri con codice <= 127.
Ferdinand Beyer

1

In 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";

Come comando shell:

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

1

E in Perl (vedi anche Git :: PurePerl su 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();

1

Usando Ruby, potresti fare qualcosa del genere:

require 'digest/sha1'

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

1

Un piccolo script Bash che dovrebbe produrre un output identico a git hash-object:

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

0

In 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}`)
}

-4

È interessante notare che ovviamente Git aggiunge un carattere di nuova riga alla fine dei dati prima che vengano sottoposti a hash. Un file che non contiene altro che "Hello World!" ottiene un hash BLOB di 980a0d5 ..., uguale a questo:

$ php -r 'echo sha1("blob 13" . chr(0) . "Hello World!\n") , PHP_EOL;'

4
Quella nuova riga viene aggiunta dall'editor di testo, non da git hash-object. Nota che fare echo "Hello World!" | git hash-object --stdin980a0d5..., mentre usa echo -ndà invece un hash c57eff5....
bdesham,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.