He estado tratando de hacer una copy de un repository de GitHub en cada confirmación de su historial utilizando el package de GitPython en Python y me estoy encontrando con este error cuando aparece a mitad de mi código.
git.exc.GitCommandError: Cmd('git') failed due to: exit code(128) cmdline: git reset --mixed HEAD~1 -- stderr: 'fatal: Failed to resolve 'HEAD~1' as a valid revision.'
Este es el código que he estado ejecutando:
from git import * import os, shutil repo = Repo(repo_path) commits = list(repo.iter_commits('master')) for c in commits: # reset to previous commit repo.head.reset('HEAD~1', index = True, working_tree = True) # unique SHA key sha = c.name_rev.split()[0] shutil.copytree(repo_path, destination_path)
¿Podría ser este error debido a una fusión? Si es así, ¿cómo puedo solucionarlo de tal manera que pueda get todas las confirmaciones en la twig principal del repository?
Antes de comenzar una respuesta, diré: no me queda claro por qué estás haciendo algo de esto. Podría, por ejemplo, usar git archive
para crear un git archive
tar o zip de cualquier confirmación dada. Por ejemplo:
git archive -o foo.tar v2.3.1
foo.tar
un file foo.tar
de la revisión labelda v2.3.1
. Para hacer que muchos files tar o zip estén fuera de todas las revisiones accesibles desde el master
, puede escribir:
git rev-list master | while read hash; do git archive -o /path/to/$hash.zip $hash done
y termine con esto.
¿Podría ser este error debido a una fusión?
Sí, podría.
Si es así, ¿cómo puedo solucionarlo de tal manera que pueda get todas las confirmaciones en la twig principal del repository?
Cuidado: los commits en master
probablemente incluyan muchos commits que también están en otras twigs.
Cuando haces esto:
commits = list(repo.iter_commits('master'))
obtienes una list completa de cada compromiso al que se puede acceder desde el master
nombres, empezando por el más reciente. Supongamos que los puntos master
comprometen en un gráfico que se parece a este, por ejemplo. En lugar de cada identificador hash de confirmación real, usaré una sola letra mayúscula para representar las confirmaciones:
A--B--C------G <-- master \ / D--E--F <--- develop
Este repository tiene siete (¡cuéntelos!) Confirmaciones. Los siete commits están activados, es decir, accesibles desde , branch master
. Seis de los siete commits se develop
en la twig. El master
nombre identifica el compromiso G
, que es un compromiso de fusión. El nombre develop
identifica commit F
, que no es.
Cuando haces esto:
repo.head.reset('HEAD~1', index = True, working_tree = True)
usted tiene que decirle a Git que resuelva la confirmación actual , que es una de estas siete, a su primer padre , y luego cambie la idea del depósito de "confirmación actual" a la confirmación que acaba de encontrar. Digamos que comienzas con HEAD
(el compromiso actual) cometiendo G
Entonces HEAD~1
es commit C
Aquí las cosas se complican un poco. El object repo.head
representa el propio HEAD
Git, que siempre es uno de los dos elementos diferentes. En este caso, sin embargo, es claramente una reference simbólica, apuntando al master
. No he probado esto, pero parece casi seguro que GitPython reproduce fielmente el comportamiento de Git aquí, y hace el equivalente de git reset
con uno de --soft
, --mixed
, o --hard
dependiendo de tus parameters, y los tuyos son aquellos para --hard
(curiosamente el command mostrado aquí que falla usa --mixed
; o su código no coincide con su publicación, o más probablemente, GitPython usa un paso adicional). Entonces, ¿qué termina haciendo esto? Hacer que el nombre del punto master
el nuevo C
confirmado:
A--B--C <-- master \ D--E--F <-- develop
¿Dónde cometió G
ir? Bueno, en realidad en ninguna parte, pero ahora está "perdido": es difícil de encontrar, y después de un período de caducidad, se eliminará por completo. Entonces commit G
se ha ido efectivamente. (Podría resucitarse, si conocemos su hash: podríamos forzar al master
a señalarlo de nuevo con otro git reset
o equivalente. Su list de confirmaciones en commits
variables aún enumera su hash, entonces esa es una de las muchas maneras en que podríamos encontrarlo y resucitarlo.)
Ahora hace su código de cuerpo principal de bucle, trabajando con commit C
:
sha = c.name_rev.split()[0] shutil.copytree(repo_path, destination_path)
Has pasado por uno de los siete commits en tu list, haciendo una copy de commit C
mientras crees que fue commit G
(el primer commit en repo.iter_commits('master')
es commit G
ya que ese es el master
puntos-to )
Ahora está listo para dar vueltas para trabajar en el segundo. El repository , sin embargo, ahora tiene solo seis commits, y puntos master
para confirmar C
Ahora haz otro git reset --hard
, borrando commit C
de la image, dejándonos con:
A--B <-- master \ D--E--F <-- develop
Ahora haces algo con commit B
(mientras que c
for c in commits
está en el segundo commit de los siete, enumerados en algún order, no está claro qué order utiliza repo.iter_commits
, pero probablemente ejecute git rev-list
y, por lo tanto, obtiene el order pnetworkingeterminado; si es así, consulte la documentation de git rev-list
).
Ahora haces otro git reset --hard
. Esta vez, el compromiso B
no se olvida: el compromiso D
restring. Pero el master
termina señalando para cometer A
:
A <-- master \ B--D--E--F <-- develop
Usted hace lo suyo con el compromiso A
, mientras que el for c in commits
está en el tercer compromiso de siete.
Ahora le pides a Git que encuentre el primer compromiso de los padres de A … pero A
no tiene un primer padre, o ninguno de los padres. Commit A
es el primer compromiso que se haya cometido; es una confirmación de raíz . En este punto, el git reset
simplemente falla. Ha iterado sobre las cuatro confirmaciones a las que se puede acceder desde el master
siguiendo solo los enlaces del primer padre. Los otros tres commits que son alcanzables desde master
requieren, en un punto, seguir al segundo padre. También eliminó dos de los cuatro commits que visitó; dos permanecen solo porque son accesibles desde otro nombre.
Tenga en count que podría tener el mismo gráfico pero sin que el nombre se develop
más:
A--B--C------G <-- master \ / D--E--F
En este caso, el primer git reset
que borra G
también borra el acceso a la cadena DEF
, porque G
era la key de ese acceso: ahora es G^2
, que es el segundo padre de la confirmación G
, que encuentra F
Es F
que encuentra E
y E
que encuentra D
; por lo que perder G
pierde todo esto, y esto acaba saliendo:
A--B--C <-- master
visible. (Como antes, todos los commits "borrados" se quedan por un período de gracia y pueden resucitar siempre que puedas encontrarlos de nuevo).
… cómo lo soluciono
Use un algorithm completamente diferente y / o elija sus commits sabiamente. El hecho de que haya siete (o cualquier otro número) de confirmaciones que sean accesibles desde algún nombre de twig, no significa que los siete (o lo que sea) estén vinculados como primeros padres .
Tenga en count que incluso en una configuration completamente lineal, como por ejemplo:
A--B <-- master
Tendrás una list de dos commits (en el order B
luego A
), pero solo puedes ejecutar git reset HEAD~1
una vez , para pasar de B
a A
Una vez que estás en A
, no puedes dar un paso atrás. Debe retroceder una vez less de lo que hace con commits, en esta situación. También debe hacer lo suyo, sea lo que sea, con el compromiso primero .
No es inmediatamente obvio para mí cómo GitPython trata con un "HEAD separado", aunque si quieres acceder a los files directamente desde el código Python, no tiene mucho sentido usar un HEAD separado. Pero si va a ejecutar shutils.copytree
, puede escribir todo esto en el script de shell, que es mucho más simple: Git está lleno de scripts de shell, y está diseñado para funcionar bien con ellos, y requiere un intérprete de shell existir para que Git funcione, de modo que si tienes Git, tienes un intérprete de shell.
'fatal: Failed to resolve 'HEAD~1' as a valid revision.'
significa que git no puede encontrar la confirmación previa, esto sucede solo cuando existe una confirmación previa.
Esto es seguro porque ejecuta su script varias veces.
GitPython interactúa con su repository de la misma manera que lo haría con la command-line, por lo que si ejecuta un script que restablece todo el repository al primer commit, su repository almacenará un único commit.
Entonces, la próxima vez que lo ejecute, nada sucederá excepto este error.
Le aconsejo que primero clone un repository existente en un directory temporal, como:
import git git.Git().clone("git://foobar.git", "path/to/cloned_repo")
O desde el directory local (si no necesita repos en línea):
git.Git().clone("path/to/source_repo/", "path/to/cloned_repo")
PD:
commits = list(repo.iter_commits('master')) for c in commits:
Sería así como:
for commit in repo.iter_commits('master'):