¿Cómo se soluciona una fusión incorrecta y se reproducen las confirmaciones correctas en una combinación fija?

407

Accidentalmente cometí un archivo no deseado ( filename.origal resolver una fusión) en mi repositorio hace varias confirmaciones, sin que me diera cuenta hasta ahora. Quiero eliminar completamente el archivo del historial del repositorio.

¿Es posible reescribir el historial de cambios de manera que filename.orignunca se haya agregado al repositorio en primer lugar?

Grant Limberg
fuente

Respuestas:

297

No use esta receta si su situación no es la descrita en la pregunta. Esta receta es para arreglar una mala fusión y reproducir tus buenas confirmaciones en una fusión fija.

Aunque filter-branchhará lo que quiera, es un comando bastante complejo y probablemente elegiría hacerlo git rebase. Probablemente sea una preferencia personal. filter-branchpuede hacerlo en un solo comando, un poco más complejo, mientras que la rebasesolución está realizando las operaciones lógicas equivalentes paso a paso.

Prueba la siguiente receta:

# create and check out a temporary branch at the location of the bad merge
git checkout -b tmpfix <sha1-of-merge>

# remove the incorrectly added file
git rm somefile.orig

# commit the amended merge
git commit --amend

# go back to the master branch
git checkout master

# replant the master branch onto the corrected merge
git rebase tmpfix

# delete the temporary branch
git branch -d tmpfix

(Tenga en cuenta que en realidad no necesita una bifurcación temporal, puede hacerlo con un 'CABEZAL separado', pero debe tomar nota de la identificación de confirmación generada por el git commit --amendpaso para suministrar al git rebasecomando en lugar de usar la bifurcación temporal nombre.)

CB Bailey
fuente
66
¿No git rebase -isería un más rápido y aún así de fácil? $ git rebase -i <sh1-of-merge> Marque el correcto como "editar" $ git rm somefile.orig $ git commit --amend $ git rebase --continue Sin embargo, por alguna razón, todavía tengo ese archivo en algún lugar el último vez que hice eso. Probablemente falta algo.
Wernight
12
git rebase -ies muy útil, especialmente cuando tiene que realizar varias operaciones de rebase-y, pero es un dolor describir con precisión cuando no está apuntando sobre el hombro de alguien y puede ver lo que está haciendo con su editor. Uso vim, pero no todos estarían contentos con: "ggjcesquash <Esc> jddjp: wq" e instrucciones como "Mueva la línea superior después de la segunda línea actual y cambie la primera palabra en la línea cuatro a 'editar' ahora guarde y dejar de fumar "parece rápidamente más complejo que los pasos reales. Normalmente terminas con algunas --amendy --continueacciones, también.
CB Bailey
3
Hice esto, pero se aplicó una nueva confirmación sobre la enmendada, con el mismo mensaje. Aparentemente, git hizo una fusión de 3 vías entre la confirmación anterior sin modificaciones que contiene el archivo no deseado y la confirmación fija de la otra rama, por lo que creó una nueva confirmación sobre la anterior, para volver a aplicar el archivo.
66
@UncleCJ: ¿Se agregó su archivo en una confirmación de fusión? Esto es importante. Esta receta está diseñada para hacer frente a una mala confirmación de fusión. No funcionará si su archivo no deseado se agregó en una confirmación normal en el historial.
CB Bailey
1
¡Estoy sorprendido de cómo podría hacer todo esto usando smartgit y sin terminal! Gracias por la receta!
cregox
209

Introducción: tienes 5 soluciones disponibles

El cartel original dice:

Accidentalmente cometí un archivo no deseado ... en mi repositorio hace varias confirmaciones ... Quiero eliminar completamente el archivo del historial del repositorio.

¿Es posible reescribir el historial de cambios de manera que filename.orignunca se haya agregado al repositorio en primer lugar?

Hay muchas formas diferentes de eliminar completamente el historial de un archivo de git:

  1. La modificación se compromete.
  2. Restablecimientos duros (posiblemente más un rebase).
  3. Rebase no interactivo.
  4. Rebases interactivos.
  5. Filtrando ramas.

En el caso del póster original, enmendar el commit no es realmente una opción en sí misma, ya que luego realizó varios commits adicionales, pero en aras de la integridad, también explicaré cómo hacerlo, para cualquier otra persona que solo quiera para modificar su compromiso anterior.

Tenga en cuenta que todas estas soluciones implican alterar / reescribir el historial / confirmaciones de una manera u otra, por lo que cualquier persona con copias antiguas de las confirmaciones tendrá que hacer un trabajo adicional para volver a sincronizar su historial con el nuevo historial.


Solución 1: compromisos de modificación

Si accidentalmente realizó un cambio (como agregar un archivo) en su confirmación anterior, y no desea que el historial de ese cambio ya exista, simplemente puede modificar la confirmación anterior para eliminar el archivo:

git rm <file>
git commit --amend --no-edit

Solución 2: restablecimiento completo (posiblemente más una rebase)

Al igual que la solución n. ° 1, si solo desea deshacerse de su confirmación anterior, también tiene la opción de simplemente hacer un restablecimiento completo a su padre:

git reset --hard HEAD^

Ese comando restablecerá su rama a la primera confirmación principal anterior .

Sin embargo , si, como el póster original, ha realizado varias confirmaciones después de la confirmación a la que desea deshacer el cambio, aún puede usar restablecimientos duros para modificarlo, pero hacerlo también implica el uso de un rebase. Estos son los pasos que puede seguir para modificar una confirmación más atrás en el historial:

# Create a new branch at the commit you want to amend
git checkout -b temp <commit>

# Amend the commit
git rm <file>
git commit --amend --no-edit

# Rebase your previous branch onto this new commit, starting from the old-commit
git rebase --preserve-merges --onto temp <old-commit> master

# Verify your changes
git diff master@{1}

Solución 3: Rebase no interactiva

Esto funcionará si solo desea eliminar una confirmación del historial por completo:

# Create a new branch at the parent-commit of the commit that you want to remove
git branch temp <parent-commit>

# Rebase onto the parent-commit, starting from the commit-to-remove
git rebase --preserve-merges --onto temp <commit-to-remove> master

# Or use `-p` insteda of the longer `--preserve-merges`
git rebase -p --onto temp <commit-to-remove> master

# Verify your changes
git diff master@{1}

Solución 4: bases interactivas

Esta solución le permitirá lograr lo mismo que las soluciones n. ° 2 y n. ° 3, es decir, modificar o eliminar confirmaciones más atrás en el historial que su confirmación anterior inmediata, por lo que la solución que elija utilizar dependerá de usted. Los rebases interactivos no son adecuados para rebases de cientos de commits, por razones de rendimiento, por lo que usaría rebases no interactivos o la solución de ramificación de filtro (ver más abajo) en ese tipo de situaciones.

Para comenzar el rebase interactivo, use lo siguiente:

git rebase --interactive <commit-to-amend-or-remove>~

# Or `-i` instead of the longer `--interactive`
git rebase -i <commit-to-amend-or-remove>~

Esto hará que git rebobine el historial de confirmación al padre de la confirmación que desea modificar o eliminar. Luego le presentará una lista de las confirmaciones rebobinadas en orden inverso en cualquier editor que git esté configurado para usar (esto es Vim por defecto):

pick 00ddaac Add symlinks for executables
pick 03fa071 Set `push.default` to `simple`
pick 7668f34 Modify Bash config to use Homebrew recommended PATH
pick 475593a Add global .gitignore file for OS X
pick 1b7f496 Add alias for Dr Java to Bash config (OS X)

La confirmación que desea modificar o eliminar estará en la parte superior de esta lista. Para eliminarlo, simplemente elimine su línea en la lista. De lo contrario, reemplace "pick" con "edit" en la línea, así:

edit 00ddaac Add symlinks for executables
pick 03fa071 Set `push.default` to `simple`

A continuación, ingrese git rebase --continue. Si elige eliminar el compromiso por completo, entonces todo lo que necesita hacer (aparte de la verificación, consulte el paso final para esta solución). Si, por otro lado, desea modificar el compromiso, entonces git volverá a aplicar el compromiso y luego pausará el rebase.

Stopped at 00ddaacab0a85d9989217dd9fe9e1b317ed069ac... Add symlinks
You can amend the commit now, with

        git commit --amend

Once you are satisfied with your changes, run

        git rebase --continue

En este punto, puede eliminar el archivo y modificar la confirmación, luego continuar con el rebase:

git rm <file>
git commit --amend --no-edit
git rebase --continue

Eso es. Como paso final, tanto si modificó el commit como si lo eliminó por completo, siempre es una buena idea verificar que no se realizaron otros cambios inesperados en su rama al diferirlo con su estado antes del rebase:

git diff master@{1}

Solución 5: Filtrar ramas

Finalmente, esta solución es mejor si desea eliminar por completo todos los rastros de la existencia de un archivo del historial, y ninguna de las otras soluciones está a la altura de la tarea.

git filter-branch --index-filter \
'git rm --cached --ignore-unmatch <file>'

Eso eliminará <file>de todas las confirmaciones, comenzando desde la confirmación raíz. Si, en cambio, solo desea reescribir el rango de confirmación HEAD~5..HEAD, puede pasarlo como un argumento adicional filter-branch, como se señala en esta respuesta :

git filter-branch --index-filter \
'git rm --cached --ignore-unmatch <file>' HEAD~5..HEAD

Nuevamente, una vez que filter-branchse completa, generalmente es una buena idea verificar que no haya otros cambios inesperados al diferenciar su rama con su estado anterior antes de la operación de filtrado:

git diff master@{1}

Alternativa de filtro-rama: BFG Repo Cleaner

He oído que la herramienta BFG Repo Cleaner se ejecuta más rápido que git filter-branch, por lo que es posible que también desee comprobar eso como una opción. Incluso se menciona oficialmente en la documentación de rama de filtro como una alternativa viable:

git-filter-branch le permite realizar reescrituras complejas con scripts de shell de su historial de Git, pero probablemente no necesite esta flexibilidad si simplemente está eliminando datos no deseados como archivos grandes o contraseñas. Para esas operaciones, es posible que desee considerar The BFG Repo-Cleaner , una alternativa basada en JVM a git-filter-branch, generalmente al menos 10-50x más rápido para esos casos de uso, y con características bastante diferentes:

  • Cualquier versión particular de un archivo se limpia exactamente una vez . El BFG, a diferencia de git-filter-branch, no le da la oportunidad de manejar un archivo de manera diferente en función de dónde o cuándo se confirmó en su historial. Esta restricción brinda el beneficio de rendimiento central de The BFG, y se adapta bien a la tarea de limpiar los datos incorrectos: no le importa dónde están los datos incorrectos, solo quiere que se vayan .

  • Por defecto, el BFG aprovecha al máximo las máquinas multinúcleo, limpiando los árboles de archivos de confirmación en paralelo. git-filter-branch limpia los commits secuencialmente (es decir, de una sola hebra), aunque es posible escribir filtros que incluyan su propio paralelismo en los scripts ejecutados contra cada commit.

  • Las opciones de comando son mucho más restrictivas que la rama git-filter, y están dedicadas solo a las tareas de eliminar datos no deseados, por ejemplo:--strip-blobs-bigger-than 1M .

Recursos adicionales

  1. Pro Git § 6.4 Herramientas Git - Reescribiendo el historial .
  2. git-filter-branch (1) Página del manual .
  3. Página del manual de git-commit (1) .
  4. Página de manual de git-reset (1) .
  5. Página del manual de git-rebase (1) .
  6. El BFG Repo Cleaner (ver también esta respuesta del propio creador ).
Comunidad
fuente
¿ filter-branchCausa recalcular los hashes? Si un equipo trabaja con un repositorio donde se debe filtrar un archivo grande, ¿cómo lo hacen para que todos terminen con el mismo estado del repositorio?
YakovL
@YakovL. Todo recalcula los hashes. En realidad, los commits son inmutables. Crea una historia completamente nueva y mueve el puntero de su rama hacia ella. La única forma de garantizar que todos tengan el mismo historial es mediante un restablecimiento completo.
Mad Physicist
118

Si no ha cometido nada desde entonces, solo git rmel archivo y git commit --amend.

Si usted tiene

git filter-branch \
--index-filter 'git rm --cached --ignore-unmatch path/to/file/filename.orig' merge-point..HEAD

pasará por cada cambio de merge-pointa HEAD, borrará filename.orig y reescribirá el cambio. Usar --ignore-unmatchsignifica que el comando no fallará si por alguna razón filename.orig falta en un cambio. Esa es la forma recomendada de la sección de Ejemplos en la página del comando man git-filter-branch .

Nota para usuarios de Windows: la ruta del archivo debe usar barras diagonales

Schwern
fuente
3
¡Gracias! git filter-branch funcionó para mí donde el ejemplo de rebase dado como respuesta no lo hizo: los pasos parecían funcionar, pero luego el empuje falló. Hice un tirón, luego empujó con éxito, pero el archivo todavía estaba alrededor. Intenté rehacer los pasos de rebase y luego todo fue complicado con conflictos de fusión. Sin embargo, utilicé un comando filter-branch ligeramente diferente, el "Un método mejorado" que se proporciona aquí: github.com/guides/completely-remove-a-file-from-all-revisions git filter-branch -f --index- filtro 'git update-index --remove filename' <introduction-revision-sha1>
..HEAD
1
No estoy seguro de cuál es el método mejorado . La documentación oficial de Git git-filter-branchparece dar la primera.
Wernight
55
Eche un vistazo a zyxware.com/articles/4027/… Me parece la solución más completa y directa que involucrafilter-branch
leontalbot
2
@atomicules, si intenta empujar el repositorio local al remoto, git insistirá en tirar primero del remoto, porque tiene cambios que no tiene localmente. Puede usar --force flag para empujar al control remoto - eliminará los archivos de allí por completo. Pero tenga cuidado, asegúrese de no forzar la sobrescritura de otra cosa que no sea solo los archivos.
sol0mka
1
Recuerde usar "y no 'cuando use Windows, o recibirá un error de "mala revisión" redactado de manera inútil.
cz
49

Esta es la mejor manera:
http://github.com/guides/completely-remove-a-file-from-all-revisions

Solo asegúrese de hacer una copia de seguridad de las copias de los archivos primero.

EDITAR

La edición de Neon desafortunadamente fue rechazada durante la revisión.
Vea la publicación de Neons a continuación, ¡puede contener información útil!


Por ejemplo, para eliminar todos los *.gzarchivos comprometidos accidentalmente en el repositorio de git:

$ du -sh .git ==> e.g. 100M
$ git filter-branch --index-filter 'git rm --cached --ignore-unmatch *.gz' HEAD
$ git push origin master --force
$ rm -rf .git/refs/original/
$ git reflog expire --expire=now --all
$ git gc --prune=now
$ git gc --aggressive --prune=now

¿Eso todavía no funcionó para mí? (Actualmente estoy en git versión 1.7.6.1)

$ du -sh .git ==> e.g. 100M

No estoy seguro de por qué, ya que solo tenía UNA rama maestra. De todos modos, finalmente conseguí que mi repositorio de git se limpiara realmente al ingresar a un nuevo repositorio de git vacío y vacío, por ejemplo

$ git init --bare /path/to/newcleanrepo.git
$ git push /path/to/newcleanrepo.git master
$ du -sh /path/to/newcleanrepo.git ==> e.g. 5M 

(¡si!)

Luego cloné eso en un nuevo directorio y moví su carpeta .git a esta. p.ej

$ mv .git ../large_dot_git
$ git clone /path/to/newcleanrepo.git ../tmpdir
$ mv ../tmpdir/.git .
$ du -sh .git ==> e.g. 5M 

(¡Sí! ¡Finalmente limpiado!)

Después de verificar que todo está bien, puede eliminar los directorios ../large_dot_gity ../tmpdir(tal vez en un par de semanas o un mes a partir de ahora, por si acaso ...)

Darren
fuente
1
Esto funcionó para mí antes de "¿Eso todavía no funcionó para mí?" comentario
shadi
Gran respuesta, pero sugiero agregar --prune-emptyal comando filter-branch.
ideasman42
27

Reescribir el historial de Git exige cambiar todos los ID de confirmación afectados, por lo que todos los que estén trabajando en el proyecto deberán eliminar sus copias antiguas del repositorio y hacer un nuevo clon después de haber limpiado el historial. Cuantas más personas molesten, más necesitará una buena razón para hacerlo: su archivo superfluo no está causando realmente un problema, pero si solo usted está trabajando en el proyecto, que también podría limpiar el historial Git si quieres ¡a!

Para hacerlo lo más fácil posible, recomendaría usar el BFG Repo-Cleaner , una alternativa más simple y más rápida a la git-filter-branchdiseñada específicamente para eliminar archivos del historial de Git. Una forma de facilitarle la vida aquí es que en realidad maneja todas las referencias por defecto (todas las etiquetas, ramas, etc.) pero también es 10 - 50 veces más rápido.

Debe seguir cuidadosamente los pasos aquí: http://rtyley.github.com/bfg-repo-cleaner/#usage , pero el bit central es solo esto: descargue el jar BFG (requiere Java 6 o superior) y ejecute este comando :

$ java -jar bfg.jar --delete-files filename.orig my-repo.git

Se analizará todo el historial del repositorio y se eliminará cualquier archivo llamado filename.orig(que no esté en su último commit ). ¡Esto es considerablemente más fácil que usar git-filter-branchpara hacer lo mismo!

Divulgación completa: soy el autor del BFG Repo-Cleaner.

Roberto Tyley
fuente
44
Esta es una herramienta excelente: un solo comando, produce una salida muy clara y proporciona un archivo de registro que coincide con cada confirmación anterior con la nueva . No me gusta instalar Java pero vale la pena.
mikemaccana
Esto es lo único que funcionó para mí, pero eso es porque no estaba trabajando correctamente git filter-branch. :-)
Kevin LaBranche
14
You should probably clone your repository first.

Remove your file from all branches history:
git filter-branch --tree-filter 'rm -f filename.orig' -- --all

Remove your file just from the current branch:
git filter-branch --tree-filter 'rm -f filename.orig' -- --HEAD    

Lastly you should run to remove empty commits:
git filter-branch -f --prune-empty -- --all
paulalexandru
fuente
1
Si bien todas las respuestas parecen estar en la pista de la rama de filtro, esta resalta cómo limpiar TODAS las ramas en su historial.
Cameron Lowell Palmer el
4

Solo para agregar eso a la solución de Charles Bailey, simplemente utilicé un git rebase -i para eliminar archivos no deseados de un commit anterior y funcionó de maravilla. Los pasos:

# Pick your commit with 'e'
$ git rebase -i

# Perform as many removes as necessary
$ git rm project/code/file.txt

# amend the commit
$ git commit --amend

# continue with rebase
$ git rebase --continue
Sverrir Sigmundarson
fuente
4

La forma más sencilla que encontré fue sugerida por leontalbot(como comentario), que es una publicación publicada por Anoopjohn . Creo que vale la pena su propio espacio como respuesta:

(Lo convertí en un script bash)

#!/bin/bash
if [[ $1 == "" ]]; then
    echo "Usage: $0 FILE_OR_DIR [remote]";
    echo "FILE_OR_DIR: the file or directory you want to remove from history"
    echo "if 'remote' argument is set, it will also push to remote repository."
    exit;
fi
FOLDERNAME_OR_FILENAME=$1;

#The important part starts here: ------------------------

git filter-branch -f --index-filter "git rm -rf --cached --ignore-unmatch $FOLDERNAME_OR_FILENAME" -- --all
rm -rf .git/refs/original/
git reflog expire --expire=now --all
git gc --prune=now
git gc --aggressive --prune=now

if [[ $2 == "remote" ]]; then
    git push --all --force
fi
echo "Done."

Todos los créditos van a Annopjohn, y a leontalbotpara señalarlo.

NOTA

Tenga en cuenta que el script no incluye validaciones, así que asegúrese de no cometer errores y de tener una copia de seguridad en caso de que algo salga mal. A mí me funcionó, pero puede que no funcione en tu situación. ÚSELO CON PRECAUCIÓN (siga el enlace si desea saber qué está pasando).

lepe
fuente
3

Definitivamente, git filter-branches el camino a seguir.

Lamentablemente, esto no será suficiente para eliminar por completo filename.origde su repositorio, ya que todavía se puede hacer referencia a él mediante etiquetas, entradas de registro, controles remotos, etc.

Recomiendo eliminar todas estas referencias también y luego llamar al recolector de basura. Puede usar el git forget-blobscript de este sitio web para hacer todo esto en un solo paso.

git forget-blob filename.orig

nachoparker
fuente
1

Si es la última confirmación que desea limpiar, probé con git versión 2.14.3 (Apple Git-98):

touch empty
git init
git add empty
git commit -m init

# 92K   .git
du -hs .git

dd if=/dev/random of=./random bs=1m count=5
git add random
git commit -m mistake

# 5.1M  .git
du -hs .git

git reset --hard HEAD^
git reflog expire --expire=now --all
git gc --prune=now

# 92K   .git
du -hs .git
clarkttfu
fuente
git reflog expire --expire=now --all; git gc --prune=nowEs algo muy malo que hacer. A menos que se esté quedando sin espacio en disco, deje que git basura recolecte estas confirmaciones después de unas semanas
avmohan
Gracias por señalar eso. Mi repositorio se envió con muchos archivos binarios grandes y el repositorio está respaldado por completo todas las noches. Así que solo quería todo lo posible;)
clarkttfu
-1

También puedes usar:

git reset HEAD file/path

paolo granada lim
fuente
3
Si el archivo se ha agregado a una confirmación, esto ni siquiera elimina el archivo del índice, solo restablece el índice a la versión HEAD del archivo.
CB Bailey