Verifique otra rama cuando haya cambios no confirmados en la rama actual

350

La mayoría de las veces cuando trato de pagar otra rama existente, Git no me permite si tengo algunos cambios no confirmados en la rama actual. Así que primero tendré que cometer o esconder esos cambios.

Sin embargo, ocasionalmente Git me permite pagar otra rama sin comprometer o esconder esos cambios, y llevará esos cambios a la rama que pago.

¿Cuál es la regla aquí? ¿Importa si los cambios son organizados o no? Llevar los cambios a otra rama no tiene ningún sentido para mí, ¿por qué git lo permite a veces? Es decir, ¿es útil en algunas situaciones?

Xufeng
fuente

Respuestas:

352

Notas preliminares

La observación aquí es que, después de comenzar a trabajar branch1(olvidando o sin darse cuenta de que sería bueno cambiar branch2primero a una rama diferente ), ejecuta:

git checkout branch2

A veces, Git dice "¡OK, ahora estás en branch2!" A veces, Git dice "No puedo hacer eso, perdería algunos de sus cambios".

Si Git no te permite hacerlo, debes confirmar tus cambios para guardarlos en un lugar permanente. Es posible que desee utilizar git stashpara guardarlos; Esta es una de las cosas para las que está diseñada. Tenga en cuenta que git stash saveo en git stash pushrealidad significa "Confirmar todos los cambios, pero en ninguna rama en absoluto, luego eliminarlos de donde estoy ahora". Eso hace posible cambiar: ahora no tiene cambios en curso. Puede luego git stash applydespués de cambiar.

Barra lateral: git stash savees la sintaxis anterior; git stash pushse introdujo en Git versión 2.13, para solucionar algunos problemas con los argumentos git stashy permitir nuevas opciones. Ambos hacen lo mismo, cuando se usan de manera básica.

¡Puedes dejar de leer aquí, si quieres!

Si Git no le permite cambiar, ya tiene un remedio: use git stasho git commit; o, si sus cambios son triviales para recrear, use git checkout -fpara forzarlo. Esta respuesta trata sobre cuándo Git te permitirá git checkout branch2aunque hayas comenzado a hacer algunos cambios. ¿Por qué funciona a veces , y no otras veces?

La regla aquí es simple en un sentido y complicada / difícil de explicar en otro:

Puede cambiar ramas con cambios no confirmados en el árbol de trabajo si y solo si dicho cambio no requiere bloquear esos cambios.

Es decir, y tenga en cuenta que esto todavía está simplificado; hay algunos casos de esquina extra difíciles con git adds, git rms escalonados y tal, supongamos que estás en branch1. A git checkout branch2tendría que hacer esto:

  • Por cada archivo que está en branch1y no en branch2, 1 quitar ese archivo.
  • Para cada archivo que está dentro branch2y no dentro branch1, cree ese archivo (con el contenido apropiado).
  • Para cada archivo que se encuentre en ambas ramas, si la versión en branch2es diferente, actualice la versión del árbol de trabajo.

Cada uno de estos pasos podría golpear algo en su árbol de trabajo:

  • Eliminar un archivo es "seguro" si la versión en el árbol de trabajo es la misma que la versión confirmada en branch1; es "inseguro" si ha realizado cambios.
  • Crear un archivo de la forma en que aparece branch2es "seguro" si no existe ahora. 2 Es "inseguro" si existe ahora pero tiene el contenido "incorrecto".
  • Y, por supuesto, reemplazar la versión del árbol de trabajo de un archivo con una versión diferente es "seguro" si la versión del árbol de trabajo ya está comprometida branch1.

La creación de una nueva rama ( git checkout -b newbranch) siempre se considera "segura": no se agregarán, eliminarán ni alterarán archivos en el árbol de trabajo como parte de este proceso, y el índice / área de ensayo tampoco se modifica. (Advertencia: es seguro al crear una nueva sucursal sin cambiar el punto de partida de la nueva sucursal; pero si agrega otro argumento, por ejemplo git checkout -b newbranch different-start-point, esto podría tener que cambiar las cosas, para pasar a different-start-point. Git aplicará las reglas de seguridad de pago como de costumbre) .)


1 Esto requiere que definamos lo que significa que un archivo esté en una rama, lo que a su vez requiere definir la rama de la palabra correctamente. (Vea también ¿Qué queremos decir exactamente con "rama"? ) Aquí, lo que realmente quiero decir es el compromiso al que se resuelve el nombre de la rama: un archivo cuya ruta está dentro si produce un hash. Ese archivo no es en si recibe un mensaje de error. La existencia de una ruta en su índice o árbol de trabajo no es relevante al responder esta pregunta en particular. Por lo tanto, el secreto aquí es examinar el resultado de cadaP branch1git rev-parse branch1:Pbranch1Pgit rev-parsebranch-name:path. Esto falla porque el archivo está "en" como máximo una rama o nos da dos ID hash. Si las dos ID de hash son iguales , el archivo es el mismo en ambas ramas. No se requiere cambio. Si las ID de hash difieren, el archivo es diferente en las dos ramas y debe cambiarse para cambiar de rama.

La noción clave aquí es que los archivos en commits se congelan para siempre. Los archivos que editará obviamente no están congelados. Estamos, al menos inicialmente, mirando solo las discrepancias entre dos confirmaciones congeladas. Desafortunadamente, nosotros, o Git, también tenemos que lidiar con los archivos que no están en la confirmación de la que va a cambiar y están en la confirmación a la que va a cambiar. Esto lleva a las complicaciones restantes, ya que los archivos también pueden existir en el índice y / o en el árbol de trabajo, sin tener que existir estas dos confirmaciones congeladas particulares con las que estamos trabajando.

2 Podría considerarse "más o menos seguro" si ya existe con el "contenido correcto", para que Git no tenga que crearlo después de todo. Recuerdo que al menos algunas versiones de Git lo permiten, pero las pruebas ahora muestran que se considera "inseguro" en Git 1.8.5.4. El mismo argumento se aplicaría a un archivo modificado que se modifica para que coincida con la rama to-be-switch-to. Una vez más, 1.8.5.4 solo dice "sería sobrescrito". Vea también el final de las notas técnicas: mi memoria puede estar defectuosa ya que no creo que las reglas del árbol de lectura hayan cambiado desde que comencé a usar Git en la versión 1.5.


¿Importa si los cambios son organizados o no?

Si, de alguna manera. En particular, puede realizar un cambio, luego "desmodificar" el archivo del árbol de trabajo. Aquí hay un archivo en dos ramas, que es diferente en branch1y branch2:

$ git show branch1:inboth
this file is in both branches
$ git show branch2:inboth
this file is in both branches
but it has more stuff in branch2 now
$ git checkout branch1
Switched to branch 'branch1'
$ echo 'but it has more stuff in branch2 now' >> inboth

En este punto, el archivo del árbol de trabajo inbothcoincide con el que está adentro branch2, aunque estemos en branch1. Este cambio no está organizado para commit, que es lo que se git status --shortmuestra aquí:

$ git status --short
 M inboth

El espacio entonces M significa "modificado pero no por etapas" (o más precisamente, la copia del árbol de trabajo difiere de la copia por etapas / índice).

$ git checkout branch2
error: Your local changes ...

OK, ahora vamos a organizar la copia del árbol de trabajo, que ya sabemos que también coincide con la copia branch2.

$ git add inboth
$ git status --short
M  inboth
$ git checkout branch2
Switched to branch 'branch2'

Aquí las copias preparadas y en funcionamiento coincidían con lo que había dentro branch2, por lo que se permitió el pago.

Intentemos otro paso:

$ git checkout branch1
Switched to branch 'branch1'
$ cat inboth
this file is in both branches

El cambio que realicé ahora se pierde del área de preparación (porque el proceso de pago escribe a través del área de preparación). Este es un poco un caso de esquina. El cambio no se ha ido, pero el hecho de que lo haya organizado, se ha ido.

Creemos una tercera variante del archivo, diferente de cualquiera de las ramas de copia, luego configuremos la copia de trabajo para que coincida con la versión de rama actual:

$ echo 'staged version different from all' > inboth
$ git add inboth
$ git show branch1:inboth > inboth
$ git status --short
MM inboth

Los dos Ms aquí significan: el archivo por etapas difiere del HEADarchivo y el archivo del árbol de trabajo difiere del archivo por etapas. La versión del árbol de trabajo coincide con la versión branch1(aka HEAD):

$ git diff HEAD
$

Pero git checkoutno permitirá el pago:

$ git checkout branch2
error: Your local changes ...

Vamos a configurar la branch2versión como la versión de trabajo:

$ git show branch2:inboth > inboth
$ git status --short
MM inboth
$ git diff HEAD
diff --git a/inboth b/inboth
index ecb07f7..aee20fb 100644
--- a/inboth
+++ b/inboth
@@ -1 +1,2 @@
 this file is in both branches
+but it has more stuff in branch2 now
$ git diff branch2 -- inboth
$ git checkout branch2
error: Your local changes ...

A pesar de que la copia de trabajo actual coincide con la de adentro branch2, el archivo por etapas no lo hace, por git checkoutlo que a perdería esa copia y git checkoutse rechazará.

Notas técnicas: solo para los curiosos :-)

El mecanismo de implementación subyacente para todo esto es el índice de Git . El índice, también llamado el "área de ensayo", es donde construye el siguiente compromiso: comienza coincidiendo con el compromiso actual, es decir, lo que haya desprotegido ahora, y luego cada vez que crea git addun archivo, reemplaza la versión del índice con lo que tengas en tu árbol de trabajo.

Recuerde, el árbol de trabajo es donde trabaja en sus archivos. Aquí, tienen su forma normal, en lugar de alguna forma especial de solo útil para Git como lo hacen en commits y en el índice. Entonces extrae un archivo de una confirmación, a través del índice, y luego en el árbol de trabajo. Después de cambiarlo, lo llevas git addal índice. De hecho, hay tres lugares para cada archivo: la confirmación actual, el índice y el árbol de trabajo.

Cuando ejecutas git checkout branch2, lo que Git hace debajo de las cubiertas es comparar la confirmación de propinasbranch2 con lo que esté en la confirmación actual y en el índice ahora. Cualquier archivo que coincida con lo que hay ahora, Git puede dejarlo solo. Todo está intacto. Cualquier archivo que sea igual en ambos commits , Git también puede dejarlo solo, y estos son los que le permiten cambiar de rama.

Gran parte de Git, incluido el cambio de confirmación, es relativamente rápido debido a este índice. Lo que realmente está en el índice no es cada archivo en sí, sino el hash de cada archivo . La copia del archivo en sí se almacena como lo que Git llama un objeto blob , en el repositorio. Esto es similar a cómo se almacenan los archivos en las confirmaciones: las confirmaciones en realidad no contienen los archivos , solo llevan a Git a la identificación hash de cada archivo. Por lo tanto, Git puede comparar las identificaciones hash, actualmente cadenas de 160 bits de longitud, para decidir si las confirmaciones X e Y tienen el mismo archivo o no. Luego puede comparar esas identificaciones hash con la identificación hash en el índice, también.

Esto es lo que lleva a todos los casos de esquina de bicho raro anteriores. Tenemos commits X e Y que tienen archivo path/to/name.txt, y tenemos una entrada de índice para path/to/name.txt. Tal vez los tres hashes coinciden. Tal vez dos de ellos coinciden y uno no. Tal vez los tres son diferentes. Y, también podríamos tener another/file.txteso solo en X o solo en Y y ahora está o no en el índice. Cada uno de estos diversos casos requiere su propia consideración por separado: ¿Git necesita copiar el archivo de commit a index, o eliminarlo de index, para cambiar de X a Y ? Si es así, también tiene quecopie el archivo en el árbol de trabajo o elimínelo del árbol de trabajo. Y si ese es el caso, las versiones del índice y del árbol de trabajo deberían coincidir mejor con al menos una de las versiones comprometidas; de lo contrario, Git estará tropezando con algunos datos.

(Las reglas completas para todo esto se describen en, no en la git checkoutdocumentación como podría esperar, sino en la git read-treedocumentación, en la sección titulada "Combinación de dos árboles" ).

torek
fuente
3
... también hay git checkout -m, que combina sus cambios de árbol de trabajo e índice en el nuevo pago.
jthill
1
Gracias por esta excelente explicación! Pero, ¿dónde puedo encontrar la información en los documentos oficiales? ¿O están incompletos? Si es así, ¿cuál es la referencia autorizada para git (ojalá que no sea su código fuente)?
max
1
(1) no puede, y (2) el código fuente. El principal problema es que Git está en constante evolución. Por ejemplo, en este momento, hay un gran impulso para aumentar o deshacerse de SHA-1 con o en favor de SHA-256. Sin embargo, esta parte particular de Git ha sido bastante estable durante mucho tiempo, y el mecanismo subyacente es sencillo: Git compara el índice actual con los compromisos actuales y de destino y decide qué archivos cambiar (si los hay) en función del compromiso de destino. , luego prueba la "limpieza" de los archivos del árbol de trabajo si la entrada de índice debería reemplazarse.
torek
66
Respuesta corta: hay una regla, pero es demasiado obtuso para el usuario promedio tener alguna esperanza de entender y mucho menos recordar, por lo tanto, en lugar de confiar en la herramienta para comportarse de manera inteligible, debe confiar en la convención disciplinada de verificar solo cuando La sucursal actual está comprometida y limpia. No veo cómo esto responde a la pregunta de cuándo sería útil llevar los cambios pendientes a otra rama, pero es posible que me haya perdido porque me cuesta entenderlo.
Neutrino
2
@HawkeyeParker: esta respuesta ha sufrido numerosas ediciones, y no estoy seguro de que ninguna de ellas la haya mejorado mucho, pero intentaré agregar algo sobre lo que significa que un archivo esté "en una rama". En última instancia, esto va a ser tambaleante porque la noción de "rama" aquí no está definida adecuadamente en primer lugar, pero ese es otro elemento más.
torek
51

Tienes dos opciones: esconder tus cambios:

git stash

luego para recuperarlos:

git stash apply

o coloque sus cambios en una rama para que pueda obtener la rama remota y luego fusionar sus cambios en ella. Esa es una de las mejores cosas de git: puedes hacer una rama, comprometerte con ella y luego buscar otros cambios en la rama en la que estabas.

Dices que no tiene ningún sentido, pero solo lo estás haciendo para que puedas fusionarlos a voluntad después de hacer la extracción. Obviamente, su otra opción es comprometerse con su copia de la rama y luego hacer la extracción. La presunción es que o no quieres hacer eso (en cuyo caso me desconcierta que no quieras una rama) o tienes miedo a los conflictos.

Robar
fuente
1
¿No es el comando correcto git stash apply? Aquí los documentos.
Thomas8
1
Justo lo que estaba buscando, cambiar temporalmente a diferentes ramas, buscar algo y volver al mismo estado de la rama en la que estoy trabajando. Gracias Rob!
Naishta
1
Sí, esta es la forma correcta de hacer esto. Aprecio los detalles en la respuesta aceptada, pero eso está haciendo las cosas más difíciles de lo necesario.
Michael Leonard
55
Además, si no tiene ninguna necesidad de mantener el alijo, puede usarlo git stash popy lo eliminará de su lista si se aplica con éxito.
Michael Leonard
1
mejor uso git stash pop, a menos que tenga la intención de mantener un registro de escondites en su historial de repositorios
Damilola Olowookere
14

Si la nueva rama contiene ediciones que son diferentes de la rama actual para ese archivo modificado en particular, entonces no le permitirá cambiar de rama hasta que el cambio se confirme o se oculte. Si el archivo modificado es el mismo en ambas ramas (es decir, la versión confirmada de ese archivo), puede cambiar libremente.

Ejemplo:

$ echo 'hello world' > file.txt
$ git add file.txt
$ git commit -m "adding file.txt"

$ git checkout -b experiment
$ echo 'goodbye world' >> file.txt
$ git add file.txt
$ git commit -m "added text"
     # experiment now contains changes that master doesn't have
     # any future changes to this file will keep you from changing branches
     # until the changes are stashed or committed

$ echo "and we're back" >> file.txt  # making additional changes
$ git checkout master
error: Your local changes to the following files would be overwritten by checkout:
    file.txt
Please, commit your changes or stash them before you can switch branches.
Aborting

Esto se aplica tanto a los archivos no rastreados como a los archivos rastreados. Aquí hay un ejemplo para un archivo sin seguimiento.

Ejemplo:

$ git checkout -b experimental  # creates new branch 'experimental'
$ echo 'hello world' > file.txt
$ git add file.txt
$ git commit -m "added file.txt"

$ git checkout master # master does not have file.txt
$ echo 'goodbye world' > file.txt
$ git checkout experimental
error: The following untracked working tree files would be overwritten by checkout:
    file.txt
Please move or remove them before you can switch branches.
Aborting

Un buen ejemplo de por qué QUIERES moverte entre las ramas mientras haces cambios sería si estuvieras realizando algunos experimentos en master, quisieras comprometerlos, pero no masterizar todavía ...

$ echo 'experimental change' >> file.txt # change to existing tracked file
   # I want to save these, but not on master

$ git checkout -b experiment
M       file.txt
Switched to branch 'experiment'
$ git add file.txt
$ git commit -m "possible modification for file.txt"
Gordolio
fuente
En realidad todavía no lo entiendo. En su primer ejemplo, después de agregar "y estamos de regreso", dice que el cambio local se sobrescribirá, ¿qué cambio local exactamente? "y estamos de vuelta"? ¿Por qué git no solo lleva este cambio a master para que en master el archivo contenga "hola mundo" y "y volvamos"
Xufeng
En el primer ejemplo, master solo tiene comprometido 'hello world'. el experimento ha comprometido 'hello world \ ngoodbye world'. Para que se produzca el cambio de rama, file.txt necesita ser modificado, el problema es que hay cambios no confirmados "hola mundo \ ngoodbye world \ y estamos de regreso".
Gordolio
1

La respuesta correcta es

git checkout -m origin/master

Fusiona los cambios de la rama maestra de origen con los cambios locales, incluso sin confirmar.

JD1731
fuente
0

En caso de que no desee que se realicen estos cambios, hágalo git reset --hard.

A continuación, puede pagar a la sucursal deseada, pero recuerde que se perderán los cambios no confirmados.

Kacpero
fuente