Asignando SHA de git de un directory sin git

Entonces, encontré esta pregunta: ¿Cómo asignar un Git SHA1 a un file sin Git?

pero no estoy seguro de cómo hacer este método para un directory. ¿Cómo puedo hash un directory en un progtwig sin usar git para que coincida con el sha1 dado por git?

Esto resultó ser más difícil de lo que esperaba, pero lo tengo trabajando ahora.

Como comenté y Hobbs contestó , calcular un hash de tree no es trivial. Debe hash cada file en cada subtree, calcular el hash para esos subtreees y usar esos valores hash para calcular el hash para un tree de nivel superior.

El código python adjunto parece funcionar al less para algunos casos de testing (por ejemplo, cómputo de hash de tree para la propia fuente git). Incluí, como comentarios, explicaciones de algunas cosas inesperadas que descubrí en el path.

Esto también está ahora en mi repository github "scripts" .

[ Editar : la versión de github ahora tiene algunas correcciones de Python3, y generalmente será más nueva / mejor.]

#! /usr/bin/env python """ Compute git hash values. This is meant to work with both Python2 and Python3, but has only been tested with Python2.7. """ from __future__ import print_function import argparse import os import stat import sys from hashlib import sha1 def strmode(mode): """ Turn internal mode (octal with leading 0s suppressed) into print form (ie, left pad => right justify with 0s as needed). """ return mode.rjust(6, '0') def classify(path): """ Return git classification of a path (as both mode, 100644/100755 etc, and git object type, ie, blob vs tree). Also throw in st_size field since we want it for file blobs. """ # We need the X bit of regular files for the mode, so # might as well just use lstat rather than os.isdir(). st = os.lstat(path) if stat.S_ISLNK(st.st_mode): gitclass = 'blob' mode = '120000' elif stat.S_ISDIR(st.st_mode): gitclass = 'tree' mode = '40000' # note: no leading 0! elif stat.S_ISREG(st.st_mode): # 100755 if any execute permission bit set, else 100644 gitclass = 'blob' mode = '100755' if (st.st_mode & 0111) != 0 else '100644' else: raise ValueError('un-git-able file system entity %s' % fullpath) return mode, gitclass, st.st_size def blob_hash(stream, size): """ Return (as hash instance) the hash of a blob, as read from the given stream. """ hasher = sha1() hasher.update(b'blob %u\0' % size) nread = 0 while True: # We read just 64K at a time to be kind to # runtime storage requirements. data = stream.read(65536) if data == '': break nread += len(data) hasher.update(data) if nread != size: raise ValueError('%s: expected %u bytes, found %u bytes' % (stream.name, size, nread)) return hasher def symlink_hash(path): """ Return (as hash instance) the hash of a symlink. Caller must use hexdigest() or digest() as needed on the result. """ hasher = sha1() # XXX os.readlink produces a string, even though the # underlying data read from the inode (which git will hash) # are raw bytes. It's not clear what happens if the raw # data bytes are not decode-able into Unicode; it might # be nice to have a raw_readlink. data = os.readlink(path).encode('utf8') hasher.update(b'blob %u\0' % len(data)) hasher.update(data) return hasher def tree_hash(path, args): """ Return the hash of a tree. We need to know all files and sub-trees. Since order matters, we must walk the sub-trees and files in their natural (byte) order, so we cannot use os.walk. This is also slightly defective in that it does not know about .gitignore files (we can't just read them since git retains files that are in the index, even if they would be ignonetworking by a .gitignore directive). We also do not (cannot) deal with submodules here. """ # Annoyingly, the tree object encodes its size, which requires # two passes, one to find the size and one to compute the hash. contents = os.listdir(path) tsize = 0 to_skip = ('.', '..') if args.keep_dot_git else ('.', '..', '.git') pass1 = [] for entry in contents: if entry not in to_skip: fullpath = os.path.join(path, entry) mode, gitclass, esize = classify(fullpath) # git stores as mode<sp><entry-name>\0<digest-bytes> encoded_form = entry.encode('utf8') tsize += len(mode) + 1 + len(encoded_form) + 1 + 20 pass1.append((fullpath, mode, gitclass, esize, encoded_form)) # Git's cache sorts foo/bar before fooXbar but after foo-bar, # because it actually stores foo/bar as the literal string # "foo/bar" in the index, rather than using recursion. That is, # a directory name should sort as if it ends with '/' rather than # with '\0'. Sort pass1 contents with funky sorting. # # (i[4] is the utf-8 encoded form of the name, i[1] is the # mode which is '40000' for directories.) pass1.sort(key = lambda i: i[4] + '/' if i[1] == '40000' else i[4]) args.depth += 1 hasher = sha1() hasher.update(b'tree %u\0' % tsize) for (fullpath, mode, gitclass, esize, encoded_form) in pass1: sub_hash = generic_hash(fullpath, mode, esize, args) if args.debug: # and args.depth == 0: print('%s%s %s %s\t%s' % (' ' * args.depth, strmode(mode), gitclass, sub_hash.hexdigest(), encoded_form.decode('utf8'))) # Annoyingly, git stores the tree hash as 20 bytes, rather # than 40 ASCII characters. This is why we return the # hash instance (so we can use .digest() directly). # The format here is <mode><sp><path>\0<raw-hash>. hasher.update(b'%s %s\0' % (mode, encoded_form)) hasher.update(sub_hash.digest()) args.depth -= 1 return hasher def generic_hash(path, mode, size, args): """ Hash an object based on its mode. """ if mode == '120000': hasher = symlink_hash(path) elif mode == '40000': hasher = tree_hash(path, args) else: # 100755 if any execute permission bit set, else 100644 with open(path, 'rb') as stream: hasher = blob_hash(stream, size) return hasher def main(): """ Parse arguments and invoke hashers. """ parser = argparse.ArgumentParser('compute git hashes') parser.add_argument('-d', '--debug', action='store_true') parser.add_argument('-k', '--keep-dot-git', action='store_true') parser.add_argument('path', nargs='+') args = parser.parse_args() args.depth = -1 # for debug print status = 0 for path in args.path: try: try: mode, gitclass, size = classify(path) except ValueError: print('%s: unhashable!' % path) status = 1 continue hasher = generic_hash(path, mode, size, args) result = hasher.hexdigest() if args.debug: print('%s %s %s\t%s' % (strmode(mode), gitclass, result, path)) else: print('%s: %s hash = %s' % (path, gitclass, result)) except OSError as err: print(str(err)) status = 1 sys.exit(status) if __name__ == '__main__': try: sys.exit(main()) except KeyboardInterrupt: sys.exit('\nInterrupted') 

¿Cómo puedo hash un directory en un progtwig sin usar git para que coincida con el sha1 dado por git?

No, en absoluto, git no tiene directorys "hash". Comprime los files contenidos y tiene una representación del tree; ver los documentos de almacenamiento de objects git .

El estado de todos los files en un directory en git está representado por un object "tree", descrito en esta respuesta SO y esta sección del libro Git . Para calcular el hash del object de tree, tendrá que producir el tree en sí.

Para cada elemento en el directory necesita cuatro cosas:

  1. Su nombre. Los objects se almacenan en el tree orderados por nombre (si todos no estaban de acuerdo con un order canónico, entonces todos podrían tener representaciones diferentes, y hashes diferentes, del mismo tree).
  2. Su modo. Los modos se basan en modos Unix (el campo st_mode de struct stat ), pero se limitan a unos pocos valores : de uso primario son 040000 para directorys, 100644 para files no ejecutables, 100755 para files ejecutables y 120000 para enlaces simbólicos.
  3. El hash del object que representa ese elemento. Para un file, este es su hash blob . Para un enlace simbólico, es el hash de un blob que contiene el objective de enlace simbólico. Para un subdirectory, es el hash del object de tree de ese directory, por lo que este es un algorithm recursivo.
  4. El tipo de object mencionado en 3.

Si recostack esta información para todas las inputs en el directory y escribe los datos en el formatting correcto, en el order correcto, tendrá un object de tree válido. Si calcula el SHA de este object, debe get el mismo resultado que git y GitHub.