¿Cuáles son los commands de git para iterar a través de files que se presionan?

Estoy tratando de implementar un gancho de git pre-push en python para validar los files antes de que sean enviados al repository remoto.

He escrito previamente un gancho de git pre-commit para validar los files antes de que se hayan comprometido con el repository local y para get una list de los files en la confirmación, ejecuté git diff-index --cached --name-status HEAD .

Para el script de pre-push , ¿qué commands de git puedo ejecutar para repetir todas las confirmaciones que están a punto de ser enviadas, y luego recorrer todos los files en las confirmaciones individuales para poder validarlos?

Hasta ahora estoy usando el command: git diff --name-status @{u}..

EDITAR: Creo que también es importante tener en count que los mismos files podrían modificarse a través de múltiples commits que están a punto de ser enviados, por lo que sería bueno no terminar validando el mismo file varias veces.

SOLUCIÓN FINAL:

Aquí está el código que terminé usando gracias a la respuesta de @Vampire y @Torek …

 #!/usr/bin/env python # read the args provided by git from stdin that are in the following format... # <local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF # the line above represents a branch being pushed # Note: multiple branches may be pushed at once lines = sys.stdin.read().splitlines() for line in lines: local_ref, local_sha1, remote_ref, remote_sha1 = line.split() if remote_sha1 == "0000000000000000000000000000000000000000": print_error("Local branch '%s' cannot be found on the remote repo - push only the branch without any commits first!" % local_ref) sys.exit(1) # get changed files changed_files = subprocess.check_output(["git", "diff", "--name-status", local_sha1, remote_sha1], universal_newlines=True) # get the non deleted files while getting rid of M\t or A\t (etc) characters from the diff output non_deleted_files = [ f[2:] for f in changed_files.split("\n") if f and not f.startswith("D") ] # validation here... if validation_failed: sys.exit(1) # terminate the push sys.exit(0) 

Problema 1: confirma

Obtener la list de confirmaciones solo es moderadamente difícil ya que la mayoría necesita ejecutar git rev-list . Sin embargo, hay algunos casos extremos aquí. Como dice la documentation de githooks :

La información sobre lo que se debe empujar se proporciona en la input estándar del gancho con líneas del formulario:

<local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF

Por ejemplo, si se ejecutara el command git push origin master:foreign el gancho recibiría una línea como la siguiente:

refs/heads/master 67890 refs/heads/foreign 12345

aunque se suministrarían los SHA-1 completos de 40 caracteres. Si la reference externa aún no existe, <remote SHA-1> será 40 0 . Si se va a eliminar una reference, la <local ref> se suministrará como (delete) y la <local SHA-1> será 40 0 . Si la confirmación local fue especificada por algo que no sea un nombre que podría expandirse (como HEAD ~ o SHA-1), se proporcionará tal como se proporcionó originalmente.

Por lo tanto, debe leer cada línea stdin y analizarla en sus componentes, y luego decidir:

  • ¿Es esto una actualización de twig en absoluto? (Es decir, ¿la reference remota tiene el formulario refs/heads/* , como una coincidencia global?) De lo contrario, ¿desea verificar las confirmaciones?
  • ¿Se está creando o destruyendo la reference? Si es así, ¿qué deberías hacer?
  • ¿Tiene el object especificado por el hash extranjero? (De lo contrario, y el impulso tiene éxito, es posible que falle), esto networkingucirá el número de objects de compromiso, pero no puede decir cuáles. Además, no puede enumerar correctamente qué confirmaciones locales se transferirán: usted sabe lo que está preguntando a ellos para establecer su nombre, pero no sabes cuál es el compromiso que tú y ellos tienen en común, ya que no puedes atravesar su historial).

Suponiendo que ha determinado las respuestas a estos, digamos que son "no", "omítalo" y "rechaza localmente los impulsos que no son analizables", vamos a enumerar las confirmaciones, y esa es solo la salida de:

 git rev-list remotehash..localhash 

con lo que podrías hacer:

 proc = subprocess.Popen(['git', 'rev-list', '{}..{}'.format(remotehash, localhash)], stdout=subprocess.PIPE) text = proc.stdout.read() if proc.wait(): raise ... # some appropriate error, as Git failed here if not isinstance(text, str): # ie, if python3 text = text.decode('utf-8') # convert bytes to str lines = text.split('\n') # now work with each commit hash 

Tenga en count que esta llamada git rev-list fallará ( saldrá con un estado distinto de cero) si el hash remoto o local es ceros totales, o si el hash remoto es para un object que no existe en su repository local (puede verificar esto usando git rev-parse --verify --quiet y verificando el estado de retorno, o tal vez use el error aquí como su indicación de que no puede verificar los commits, aunque hay otras opciones cuando se crea una nueva bifurcación).

Tenga en count que debe ejecutar git rev-list arriba para cada reference que se actualizará. Es posible que se envíen los mismos commits, o algún subset de los mismos commits, para diferentes references. Por ejemplo:

 git push origin HEAD:br1 HEAD:br2 HEAD~3:br3 

solicitaría que la actualización remota tenga tres twigs br1 a br3 , establezca br1 y br2 en la misma confirmación que HEAD y establezca br3 en la confirmación tres pasos desde HEAD . No sabemos (y no podemos) qué confirmaciones son realmente nuevas -el gancho de pre-recepción del otro extremo podría resolverlo, pero no podemos br1 pero si las br1 y br2 del control remoto se están actualizando desde HEAD~3 a HEAD , y el br3 del control remoto se está actualizando de HEAD~2 hacia atrás a HEAD~3 , a lo sumo los commits HEAD~1 a HEAD pueden ser nuevos. Si también desea comprobar HEAD~2 , ya que ahora es probable que aparezca en br1 y br2 en el otro repository (aunque ya estaba en br3 allí), también depende de usted.

Problema 2: files

Ahora tienes el problema más difícil. Mencionaste en una edición que:

EDITAR: Creo que también es importante tener en count que los mismos files podrían modificarse a través de múltiples commits que están a punto de ser enviados, por lo que sería bueno no terminar validando el mismo file varias veces.

Cada compromiso que se enviará tiene una instantánea completa del repository. Es decir, cada confirmación tiene cada file. No tengo idea de qué validation pretendes ejecutar, pero estás en lo cierto: si estás enviando, digamos, seis confirmaciones totales, es bastante probable que la mayoría de los files en las seis confirmaciones sean los mismos, y solo unos pocos files se modifiquen. . Sin embargo, el file foo.py podría modificarse en la confirmación 1234567 (con respecto a la confirmación primaria de 1234567 ), y luego modificarse nuevamente en la confirmación fedcba9 , y probablemente debería verificar ambas versiones .

Además, cuando un commit es un commit de fusión , tiene (al less) dos padres diferentes. ¿Debe verificar un file si es diferente de cualquiera de los padres? ¿O debería comprobarlo solo si difiere de ambos padres, indicando que contiene cambios de "ambos lados" de la fusión? Si solo tiene cambios desde "un lado", es probable que el file esté "verificado previamente" por las verificaciones que se hayan realizado para el compromiso que se encuentra en el otro lado, y por lo tanto, puede que no sea necesario volver a verificarlo (aunque, por supuesto, depende del tipo de comprobación).

(Para una fusión de pulpo, es decir, una fusión con más de dos padres, esta pregunta se vuelve mucho más difícil de considerar).

Es relativamente fácil ver qué files se cambian en una confirmación, con respecto a sus padres o padres: simplemente ejecute git diff-tree con las opciones apropiadas (especialmente, -r para recurse en los subtreees de la confirmación). El formatting de salida pnetworkingeterminado es bastante parseable por máquina, aunque es posible que desee agregar -z para que sea más fácil de manejar directamente dentro de Python. Si está haciendo esto de uno en uno, lo cual es posible, probablemente también desee --no-commit-id para que no necesite leer y omita el encabezado de confirmación.

Depende de usted si desea habilitar la detección de cambio de nombre y, de ser así, en qué umbral. Dependiendo, una vez más, de lo que está haciendo exactamente para verificar los files, dejar la detección de renombrado a menudo es mejor: de esa manera "verá" un file renombrado como una eliminación de la ruta anterior y una adición de la nueva ruta.

La salida de git diff-tree -r --no-commit-id en una confirmación particular se ve así:

 :000000 100644 0000000000000000000000000000000000000000 b0b4c36f9780eaa600232fec1adee9e6ba23efe5 A Documentation/RelNotes/2.13.0.txt :100755 100755 6a208e92bf30c849028268b5fca54b902f671bbd 817d1cf7ef2a2a99ab11e5a88a27dfea673fec79 M GIT-VERSION-GEN :120000 120000 d09c3d51093ac9e4da65e8a127b17ac9023520b5 125bf78f3b9ed2f1444e1873ed02cce9f0f4c5b8 M RelNotes 

Los identificadores de hash son los hashes de blob antiguos y nuevos; los códigos de letras y nombres de ruta están documentados. A continuación, puede recuperar el contenido del file utilizando git cat-file -p en la nueva ID hash. Si su Git es lo suficientemente nuevo, incluso puede get cualquier filtrado basado en .gitattributes y la conversión de final de línea aplicada al agregar --textconv --filters , y --path=<path> (o usar la ruta del file junto con la confirmación ID, en lugar de --path=... , para nombrar el hash del object que se extraerá). O simplemente puede usar la forma del object almacenado en el repository, si los filters no son importantes.

Sin embargo, dependiendo de lo que esté comprobando, es posible que deba extraer la confirmación completa en un tree de trabajo temporal. (Por ejemplo, un analizador estático podría querer ejecutar cualquier import ). En este caso, simplemente ejecute git checkout , usando la variable de entorno GIT_INDEX_FILE (pase esto por subprocess como de costumbre) para especificar un file de índice temporal para que el principal el índice no está perturbado Especifique un tree de trabajo alternativo con --work-tree= o mediante la variable de entorno GIT_WORK_TREE . En cualquier caso, el git diff-tree le dirá qué files se han modificado y, por lo tanto, se deben verificar. (Puede usar shutil.rmtree para deshacerse del tree de trabajo temporal una vez que se completen las testings).

Si va a verificar las asignaciones de fusión, preste especial atención a la descripción de las diferencias combinadas realizadas para las fusiones , ya que requerirán un tratamiento algo diferente (o dividir la fusión con -m ).

Editar: algún código para mostrar lo que quiero decir

Aquí hay un poco de código para get todas las inputs y mostrar cada confirmación que se agrega a cada twig en el extranjero. Tenga en count que la list de confirmaciones agregadas estará vacía si las confirmaciones solo se eliminan . Esto también se testing muy poco, y no está pensado para ser robusto, fácil de mantener, buen estilo, etc., como un ejemplo mínimo.

 import re, subprocess, sys lines = sys.stdin.read().splitlines() for line in lines: localref, localhash, foreignref, foreignhash = line.split() if not foreignref.startswith('refs/heads/'): print('skip {}'.format(foreignref)) continue if re.match('0+$', localhash): print('deleting {}, do nothing'.format(foreignref)) continue if re.match('0+$', foreignhash): print('creating {}, too hard for now'.format(foreignref)) continue proc = subprocess.Popen(['git', 'rev-parse', '--quiet', '--verify', foreignhash], stdout=subprocess.PIPE) _ = proc.stdout.read() status = proc.wait() if status: print('we do not have {} for {}, try ' 'git fetch'.format(foreignhash, foreignref)) # can try to run git fetch here ourselves, but for now: continue print('sending these commits for {}:'.format(foreignref)) subprocess.call(['git', 'rev-list', '{}..{}'.format(localhash, foreignhash)]) 

Usar @{u}.. es poco útil, ya que diferirá el flujo ascendente de HEAD contra HEAD si hay una stream ascendente definida. Pero esto no necesariamente tiene que ver con lo que se empuja, ya que puede empujar cualquier twig o en realidad cualquier commit-ish, independientemente de lo que se comtesting actualmente y de cualquier twig remota que desee, independientemente de la configuration de subida.

Según la documentation de githooks , obtienes el nombre y la location remotos como parameters para tu script y en stdin obtienes una línea por cada "cosa" presionada con la reference local y remota y el sha local y remoto. Por lo tanto, necesita iterar sobre stdin y diff el sha remoto al que se empuja contra el sha local que empuja para get los files que son diferentes.