Cómo hacer una copy del repository GitHub en cada commit utilizando el package GitPython en Python

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'):