Jak przypisać Git SHA1 do pliku bez Gita?

138

Jak rozumiem, kiedy Git przypisuje skrót SHA1 do pliku, ten SHA1 jest unikalny dla pliku na podstawie jego zawartości.

W rezultacie, jeśli plik jest przenoszony z jednego repozytorium do drugiego, SHA1 dla pliku pozostaje taka sama, ponieważ jego zawartość nie uległa zmianie.

Jak Git oblicza skrót SHA1? Czy robi to na pełnej nieskompresowanej zawartości pliku?

Chciałbym emulować przypisywanie SHA1 poza Git.

git-noob
źródło

Odpowiedzi:

255

Oto jak Git oblicza SHA1 dla pliku (lub, mówiąc językiem Gita, „bloba”):

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

Możesz więc łatwo obliczyć to samodzielnie bez instalowania Git. Zauważ, że „\ 0” jest bajtem zerowym, a nie dwuznakowym ciągiem.

Na przykład hash pustego pliku:

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

$ touch empty
$ git hash-object empty
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391

Inny przykład:

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

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

Oto implementacja Pythona:

from hashlib import sha1
def githash(data):
    s = sha1()
    s.update("blob %u\0" % len(data))
    s.update(data)
    return s.hexdigest()
Ferdinand Beyer
źródło
Czy ta odpowiedź zakłada Python 2? Kiedy próbuję tego w Pythonie 3, pojawia się TypeError: Unicode-objects must be encoded before hashingwyjątek w pierwszym s.update()wierszu.
Mark Booth,
3
W Pythonie 3 musisz zakodować dane: s.update(("blob %u\0" % filesize).encode('utf-8'))aby uniknąć TypeError.
Mark Booth,
Kodowanie jako utf-8 zadziała, ale prawdopodobnie lepiej będzie po prostu zbudować je z ciągu bajtów w pierwszej kolejności (kodowanie utf-8 działa, ponieważ żaden ze znaków Unicode nie jest spoza ASCII).
torek
Jedną dodatkową rzeczą wartą wspomnienia jest to, że git hash-object również wydaje się zastępować „\ r \ n” przez „\ n” w treści danych. Może to całkiem dobrze usunąć znaki „\ r”, nie sprawdzałem tego.
user420667
1
Umieściłem tutaj implementację Pythona 2 + 3 (obie w jednej) pliku i generatora skrótów drzewa: github.com/chris3torek/scripts/blob/master/githash.py (hasher drzewa czyta drzewo katalogów).
torek
17

Trochę goodie: w skorupce

echo -en "blob ${#CONTENTS}\0$CONTENTS" | sha1sum
knittl
źródło
1
Porównuję echo -en "blob ${#CONTENTS}\0$CONTENTS" | sha1sumdo wyników git hash-object path-to-filei dają różne wyniki. Jednak echo -e ...daje prawidłowe wyniki, z wyjątkiem tego, że występuje końcowy - ( niegit hash-object tworzy żadnych znaków końcowych). Czy to jest coś, o co powinienem się martwić?
FrustratedWithFormsDesigner
2
@FrustratedWithFormsDesigner: końcowy -jest używany przez, sha1sumjeśli obliczył skrót ze standardowego wejścia, a nie z pliku. Nie ma się o co martwić. Dziwna rzecz -n, która powinna zablokować znak nowej linii zwykle dodawany przez echo. Czy Twój plik ma przypadkiem pustą ostatnią linię, której zapomniałeś dodać do CONTENTSzmiennej?
knittl
Tak, masz rację. Pomyślałem, że wynik sha1sum powinien być tylko hashem , ale nie jest trudno go usunąć sedem lub czymś w tym rodzaju.
FrustratedWithFormsDesigner
@FrustratedWithFormsDesigner: Otrzymasz ten sam wynik, jeśli użyjesz cat file | sha1sumzamiast sha1sum file(chociaż więcej procesów i
potoków
8

Jeśli nie masz zainstalowanego gita, możesz utworzyć funkcję powłoki bash, aby ją łatwo obliczyć.

git_id () { printf 'blob %s\0' "$(ls -l "$1" | awk '{print $5;}')" | cat - "$1" | sha1sum | awk '{print $1}'; }
CB Bailey
źródło
1
Nieco krótsza: (stat --printf="blob %s\0" "$1"; cat "$1") | sha1sum -b | cut -d" " -f1.
sschuberth
4

Spójrz na stronę podręcznika systemowego dla git-hash-object . Możesz go użyć do obliczenia skrótu git dowolnego konkretnego pliku. Myślę , że git przekazuje do algorytmu haszującego coś więcej niż tylko zawartość pliku, ale nie wiem na pewno, a jeśli dostarcza dodatkowe dane, nie wiem, co to jest.

Dale Hagglund
źródło
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

To jest rozwiązanie w języku F #.

forki23
źródło
Nadal mam problemy z umlautami: calcGitSHA1 ("ü"). ShouldBeEqualTo ("0f0f3e3b1ff2bc6722afc3e3812e6b782683896f") Ale moja funkcja daje 0d758c9c7bc06c1e307f05d92d896aaf2c8. Jakieś pomysły, jak obiekt haszujący git obsługuje umlauty?
forki
powinien obsługiwać obiekt blob jako strumień bajtowy, co oznacza, że ​​ü ma prawdopodobnie długość 2 (unicode), właściwość Długość F Length zwróci długość 1 (ponieważ jest to tylko jeden widoczny znak)
knittl
Ale System.Text.Encoding.ASCII.GetBytes ("ü") zwraca tablicę bajtów z 1 elementem.
forki
Użycie UTF8 i 2 jako długości łańcucha daje tablicę bajtów: [98; 108; 111; 98; 32; 50; 0; 195; 188], a zatem SHA1 o wartości 99fe40df261f7d4afd1391fe2739b2c7466fe968. Co też nie jest git SHA1.
forki
1
Nigdy nie wolno stosować skrótów do ciągów znaków. Zamiast tego należy je zastosować do ciągów bajtów (tablic bajtów), które można uzyskać, konwertując ciąg znaków na bajty przy użyciu jawnego kodowania.
dolmen
2

Pełna implementacja 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
źródło
2
To, czego naprawdę chcesz, to kodowanie ASCII. UTF8 działa tylko tutaj, ponieważ jest kompatybilny z ASCII, a „blob x \ 0” zawiera tylko znaki z kodem <= 127.
Ferdinand Beyer
1

W Perlu:

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

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

Jako polecenie powłoki:

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

Oraz w Perlu (zobacz także Git :: PurePerl na 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
źródło
1

Używając Rubiego, możesz zrobić coś takiego:

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
źródło
1

Mały skrypt Bash, który powinien dawać identyczne dane wyjściowe do git hash-object:

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

W 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
źródło
-4

Warto zauważyć, że oczywiście Git dodaje znak nowej linii na końcu danych, zanim zostaną one zaszyfrowane. Plik zawierający tylko „Hello World!” pobiera skrót obiektu blob o wartości 980a0d5 ..., który jest taki sam jak ten:

$ php -r 'echo sha1("blob 13" . chr(0) . "Hello World!\n") , PHP_EOL;'
Szturchać
źródło
4
Ta nowa linia jest dodawana przez twój edytor tekstu, a nie przez git hash-object. Zauważ, że robienie echo "Hello World!" | git hash-object --stdindaje 980a0d5..., podczas gdy używanie echo -ndaje c57eff5...zamiast tego hash of .
bdesham