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 branch2
primero 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 stash
para guardarlos; Esta es una de las cosas para las que está diseñada. Tenga en cuenta que git stash save
o en git stash push
realidad 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 apply
después de cambiar.
Barra lateral: git stash save
es la sintaxis anterior; git stash push
se introdujo en Git versión 2.13, para solucionar algunos problemas con los argumentos git stash
y 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 stash
o git commit
; o, si sus cambios son triviales para recrear, use git checkout -f
para forzarlo. Esta respuesta trata sobre cuándo Git te permitirá git checkout branch2
aunque 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 add
s, git rm
s escalonados y tal, supongamos que estás en branch1
. A git checkout branch2
tendría que hacer esto:
- Por cada archivo que está en
branch1
y no en branch2
, 1 quitar ese archivo.
- Para cada archivo que está dentro
branch2
y no dentro branch1
, cree ese archivo (con el contenido apropiado).
- Para cada archivo que se encuentre en ambas ramas, si la versión en
branch2
es 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
branch2
es "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
branch1
git rev-parse branch1:P
branch1
P
git rev-parse
branch-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 branch1
y 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 inboth
coincide con el que está adentro branch2
, aunque estemos en branch1
. Este cambio no está organizado para commit, que es lo que se git status --short
muestra 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 M
s aquí significan: el archivo por etapas difiere del HEAD
archivo 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 checkout
no permitirá el pago:
$ git checkout branch2
error: Your local changes ...
Vamos a configurar la branch2
versió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 checkout
lo que a perdería esa copia y git checkout
se 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 add
un 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 add
al í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.txt
eso 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 checkout
documentación como podría esperar, sino en la git read-tree
documentación, en la sección titulada "Combinación de dos árboles" ).
git checkout -m
, que combina sus cambios de árbol de trabajo e índice en el nuevo pago.