Mueva las confirmaciones más recientes a una nueva rama con Git

4986

Me gustaría mover los últimos commits que he comprometido a master a una nueva rama y volver a master antes de que se realicen esos commits. Desafortunadamente, mi Git-fu no es lo suficientemente fuerte todavía, ¿alguna ayuda?

Es decir, ¿cómo puedo ir de esto?

master A - B - C - D - E

¿a esto?

newbranch     C - D - E
             /
master A - B 
Mark A. Nicolosi
fuente
114
Nota: Hice la pregunta opuesta aquí
Benjol
3
eddmann.com/posts/… este funciona
Sagar Naliyapara
77
¿Se purgaron los comentarios aquí? Pregunto porque durante mi visita bimensual a esta pregunta, siempre me desplazo por ese comentario.
Tejas Kale

Respuestas:

6453

Mudarse a una sucursal existente

Si desea mover sus confirmaciones a una rama existente , se verá así:

git checkout existingbranch
git merge master         # Bring the commits here
git checkout master
git reset --keep HEAD~3  # Move master back by 3 commits.
git checkout existingbranch

La --keepopción conserva los cambios no confirmados que pueda tener en archivos no relacionados, o aborta si esos cambios tendrían que sobrescribirse, de manera similar a lo que git checkouthace. Si aborta, git stashcambia y vuelve a intentarlo, o usa --hardpara perder los cambios (¡incluso de archivos que no cambiaron entre las confirmaciones!)

Mudarse a una nueva sucursal

Este método funciona creando una nueva rama con el primer comando ( git branch newbranch) pero sin cambiar a él. Luego, retrocedemos la rama actual (maestra) y cambiamos a la nueva rama para continuar trabajando.

git branch newbranch      # Create a new branch, containing all current commits
git reset --keep HEAD~3   # Move master back by 3 commits (Make sure you know how many commits you need to go back)
git checkout newbranch    # Go to the new branch that still has the desired commits
# Warning: after this it's not safe to do a rebase in newbranch without extra care.

Pero asegúrese de cuántas confirmaciones para volver. Alternativamente, en lugar de HEAD~3, simplemente puede proporcionar el hash de la confirmación (o la referencia como origin/master) a la que desea volver, por ejemplo:

git reset --keep a1b2c3d4

ADVERTENCIA: con Git versión 2.0 y posteriores, si luego se agrega git rebasela nueva rama a la rama original ( master), es posible que necesite una --no-fork-pointopción explícita durante el rebase para evitar perder las confirmaciones que movió de la rama maestra. Tenerlo lo branch.autosetuprebase alwayshace más probable. Ver la respuesta de John Mellor para más detalles.

Duc Filan
fuente
249
Y, en particular, no intente retroceder más allá del punto en el que presionó por última vez los commits en otro repositorio del que alguien más podría haberse retirado.
Greg Hewgill
107
Me pregunto si puedes explicar POR QUÉ esto funciona. Para mí, está creando una nueva rama, eliminando 3 confirmaciones de la rama anterior en la que aún se encuentra y luego revisando la rama que realizó. Entonces, ¿cómo se muestran mágicamente las confirmaciones que eliminó en la nueva rama?
Jonathan Dumaine
142
@ Jonathan Dumaine: Porque creé la nueva rama antes de eliminar las confirmaciones de la rama anterior. Todavía están allí en la nueva sucursal.
sykora
86
las ramas en git son solo marcadores que apuntan a confirmaciones en la historia, no hay nada que se clone, cree o elimine (excepto los marcadores)
knittl
218
También tenga en cuenta: ¡No haga esto con cambios no confirmados en su copia de trabajo! ¡Esto solo me mordió! :(
Adam Tuttle
1039

Para aquellos que se preguntan por qué funciona (como estaba al principio):

Desea volver a C y mover D y E a la nueva rama. Esto es lo que parece al principio:

A-B-C-D-E (HEAD)
        ↑
      master

Después git branch newBranch:

    newBranch
        ↓
A-B-C-D-E (HEAD)
        ↑
      master

Después git reset --hard HEAD~2:

    newBranch
        ↓
A-B-C-D-E (HEAD)
    ↑
  master

Como una rama es solo un puntero, el maestro señaló la última confirmación. Cuando creó newBranch , simplemente creó un nuevo puntero a la última confirmación. Luego, utilizando git reset, movió el puntero maestro hacia atrás dos confirmaciones. Pero como no movió newBranch , todavía señala la confirmación que hizo originalmente.

Ryan Lundy
fuente
56
También necesitaba hacer un git push origin master --forcecambio para que aparezca en el repositorio principal.
Dženan
99
Esta respuesta hace que se pierdan los commits: la próxima vez git rebase, los 3 commits se descartarán silenciosamente newbranch. Vea mi respuesta para más detalles y alternativas más seguras.
John Mellor
14
@ John, eso no tiene sentido. Rebasar sin saber lo que estás haciendo hace que se pierdan los compromisos. Si perdiste commits, lo siento por ti, pero esta respuesta no perdió tus commits. Tenga en cuenta que origin/masterno aparece en el diagrama anterior. Si presionó origin/mastery luego realizó los cambios anteriores, seguro, las cosas serían divertidas. Pero ese es un problema del tipo "Doctor, me duele cuando hago esto". Y está fuera del alcance de lo que hizo la pregunta original. Le sugiero que escriba su propia pregunta para explorar su escenario en lugar de secuestrarlo.
Ryan Lundy
1
@John, en tu respuesta, dijiste "¡No hagas esto! git branch -t newbranch". Regrese y lea las respuestas nuevamente. Nadie sugirió hacer eso.
Ryan Lundy
1
@ Kyralessa, claro, pero si miras el diagrama en la pregunta, está claro que quieren newbranchbasarse en su masterrama local existente . Después de realizar la respuesta aceptada, cuando el usuario consigue alrededor de ejecutar git rebaseen newbranch, git les recordará que se olvidaron de establecer la rama aguas arriba, por lo que van a ejecutar git branch --set-upstream-to=mastera continuación, git rebasey tienen el mismo problema. También pueden usar git branch -t newbranchen primer lugar.
John Mellor
455

En general...

El método expuesto por sykora es la mejor opción en este caso. Pero a veces no es lo más fácil y no es un método general. Para un método general, use git cherry-pick :

Para lograr lo que OP quiere, es un proceso de 2 pasos:

Paso 1: tenga en cuenta qué confirmaciones del maestro desea en un newbranch

Ejecutar

git checkout master
git log

Tenga en cuenta los hashes de (digamos 3) confirmaciones que desea newbranch. Aquí usaré:
C commit: 9aa1233
D commit: 453ac3d
E commit:612ecb3

Nota: Puede usar los primeros siete caracteres o el hash de confirmación completo

Paso 2 - Póngalos en el newbranch

git checkout newbranch
git cherry-pick 612ecb3
git cherry-pick 453ac3d
git cherry-pick 9aa1233

O (en Git 1.7.2+, use rangos)

git checkout newbranch
git cherry-pick 612ecb3~1..9aa1233

git cherry-pick aplica esos tres commits a newbranch.

Ivan
fuente
13
¿No estaba el OP tratando de pasar de maestro a nuevo rama? Si selecciona Cherry mientras está en master, estaría agregando a la rama master, agregando commits que ya tenía, de hecho. Y no se mueve hacia atrás ni hacia la cabeza de la rama hacia B. ¿O hay algo sutil y genial que no entiendo?
RaveTheTadpole
66
Esto funciona muy bien si accidentalmente comete la rama incorrecta, no maestra, cuando debería haber creado una nueva rama de características.
julianc
55
+1 para un enfoque útil en algunas situaciones. Esto es bueno si solo desea extraer sus propios commits (que se intercalan con otros) en una nueva rama.
Tyler V.
8
Es mejor respuesta. De esta manera puede mover confirmaciones a cualquier rama.
skywinder
8
¿Es importante el orden de la recolección de cerezas?
kon psych
328

Otra forma de hacer esto, usando solo 2 comandos. También mantiene intacto su árbol de trabajo actual.

git checkout -b newbranch # switch to a new branch
git branch -f master HEAD~3 # make master point to some older commit

Versión anterior - antes de aprender sobregit branch -f

git checkout -b newbranch # switch to a new branch
git push . +HEAD~3:master # make master point to some older commit 

Poder pushhacerlo .es un buen truco para saber.

aragaer
fuente
1
Directorio actual. Supongo que esto funcionaría solo si estás en un directorio superior.
aragaer
1
El impulso local es inductor de sonrisa, pero en la reflexión, ¿cómo es diferente git branch -faquí?
jthill
2
@GerardSexton .es el director actual. git puede empujar a REMOTOS o URL de GIT. path to local directoryes compatible con la sintaxis de URL de Git. Consulte la sección URL de GIT en git help clone.
débil
31
No sé por qué esto no tiene una calificación más alta. Muy simple, y sin el pequeño pero potencial peligro de reinicio de git: duro.
Godsmith
44
@ Godsmith Creo que la gente prefiere tres comandos simples a dos comandos un poco más oscuros. Además, las respuestas más votadas obtienen más votos por la naturaleza de mostrarse primero.
JS_Riddler
323

¡La mayoría de las respuestas anteriores son peligrosamente incorrectas!

No hagas esto:

git branch -t newbranch
git reset --hard HEAD~3
git checkout newbranch

¡Como la próxima vez que ejecute git rebase(o git pull --rebase) esos 3 commits serían descartados en silencio newbranch! (ver explicación a continuación)

En cambio, haz esto:

git reset --keep HEAD~3
git checkout -t -b newbranch
git cherry-pick ..HEAD@{2}
  • Primero descarta las 3 confirmaciones más recientes ( --keepes como --hard, pero más seguro, ya que falla en lugar de descartar los cambios no confirmados).
  • Luego se bifurca newbranch.
  • Luego, elige los 3 commits nuevamente newbranch. Dado que una rama ya no hace referencia a ellos, lo hace mediante el uso de refit de git : HEAD@{2}es el compromiso que HEADsolía referirse a 2 operaciones hace, es decir, antes de 1. desprotegido newbranchy 2. utilizado git resetpara descartar los 3 compromisos.

Advertencia: el reflog está habilitado de forma predeterminada, pero si lo ha deshabilitado manualmente (por ejemplo, utilizando un repositorio git "desnudo"), no podrá recuperar los 3 commits después de ejecutarlo git reset --keep HEAD~3.

Una alternativa que no depende del reflog es:

# newbranch will omit the 3 most recent commits.
git checkout -b newbranch HEAD~3
git branch --set-upstream-to=oldbranch
# Cherry-picks the extra commits from oldbranch.
git cherry-pick ..oldbranch
# Discards the 3 most recent commits from oldbranch.
git branch --force oldbranch oldbranch~3

(si lo prefiere, puede escribir @{-1}, la rama previamente extraída, en lugar de oldbranch).


Explicación técnica

¿Por qué git rebasedescartar los 3 commits después del primer ejemplo? Es porque git rebasesin argumentos habilita la --fork-pointopción por defecto, que usa el registro local para tratar de ser robusto contra la rama ascendente que se empuja a la fuerza.

Supongamos que ramificó origen / maestro cuando contenía confirmaciones M1, M2, M3, y luego realizó tres confirmaciones usted mismo:

M1--M2--M3  <-- origin/master
         \
          T1--T2--T3  <-- topic

pero luego alguien reescribe el historial presionando forzar origin / master para eliminar M2:

M1--M3'  <-- origin/master
 \
  M2--M3--T1--T2--T3  <-- topic

Usando su reflog local, git rebasepuede ver que se bifurcó de una encarnación anterior de la rama de origen / maestra, y por lo tanto, las confirmaciones de M2 ​​y M3 no son realmente parte de su rama de tema. Por lo tanto, se supone razonablemente que, dado que M2 se eliminó de la rama ascendente, ya no lo desea en su rama temática una vez que la rama temática se haya modificado:

M1--M3'  <-- origin/master
     \
      T1'--T2'--T3'  <-- topic (rebased)

Este comportamiento tiene sentido y, por lo general, es lo que se debe hacer al rebasar.

Entonces, la razón por la que fallan los siguientes comandos:

git branch -t newbranch
git reset --hard HEAD~3
git checkout newbranch

es porque dejan el reflog en el estado incorrecto. Git ve newbranchque se ha bifurcado de la rama ascendente en una revisión que incluye los 3 commits, luego reset --hardreescribe el historial del upstream para eliminar los commits, por lo que la próxima vez que git rebaselo ejecutes los descarta como cualquier otro commit que se haya eliminado del upstream.

Pero en este caso particular, queremos que esas 3 confirmaciones se consideren como parte de la rama del tema. Para lograr eso, necesitamos bifurcar el flujo ascendente en la revisión anterior que no incluye las 3 confirmaciones. Eso es lo que hacen mis soluciones sugeridas, por lo tanto, ambos dejan el registro en el estado correcto.

Para obtener más detalles, consulte la definición de --fork-pointen los documentos git rebase y git merge-base .

John Mellor
fuente
15
Esta respuesta dice "¡NO hagas esto!" por encima de algo que nadie sugirió hacer.
Ryan Lundy
44
La mayoría de las personas no reescribe la historia publicada, especialmente en master. Entonces no, no están peligrosamente equivocados.
Walf
44
@ Kyralessa, el al que -tte refieres git branchsucede implícitamente si lo has git config --global branch.autosetuprebase alwaysconfigurado. Incluso si no lo hace, ya le expliqué que el mismo problema ocurre si configura el seguimiento después de ejecutar estos comandos, como el OP probablemente intenta hacer dada su pregunta.
John Mellor
2
@RockLee, sí, la forma general de solucionar tales situaciones es crear una rama nueva (newbranch2) desde un punto de partida seguro y luego elegir todas las confirmaciones que desea mantener (desde badnewbranch a newbranch2). La selección de cerezas le dará a los commits nuevos hashes, por lo que podrá volver a crear una nueva base con seguridad en newbranch2 (y ahora puede eliminar badnewbranch).
John Mellor
2
@Walf, entendiste mal: git rebase está diseñado para ser robusto frente a las fuentes que tienen su historia reescrita. Desafortunadamente, los efectos secundarios de esa robustez afectan a todos, incluso si ni ellos ni su corriente arriba reescriben la historia.
John Mellor
150

Solución mucho más simple usando git stash

Aquí hay una solución mucho más simple para los compromisos con la rama incorrecta. Comenzando en la rama masterque tiene tres confirmaciones erróneas:

git reset HEAD~3
git stash
git checkout newbranch
git stash pop

¿Cuándo usar esto?

  • Si su propósito principal es retroceder master
  • Desea conservar los cambios de archivo.
  • No te importan los mensajes en las confirmaciones erróneas
  • Aún no has empujado
  • Quieres que sea fácil de memorizar
  • No desea complicaciones como ramas temporales / nuevas, encontrar y copiar hashes de confirmación y otros dolores de cabeza

Qué hace esto, por número de línea

  1. Deshace las últimas tres confirmaciones (y sus mensajes) master, pero deja intactos todos los archivos de trabajo
  2. Guarda todos los cambios del archivo de trabajo, haciendo que el masterárbol de trabajo sea exactamente igual al estado HEAD ~ 3
  3. Cambia a una sucursal existente newbranch
  4. Aplica los cambios escondidos a su directorio de trabajo y borra el alijo

Ahora puede usar git addy git commitcomo lo haría normalmente. Se agregarán todos los nuevos commits newbranch.

Lo que esto no hace

  • No deja ramas aleatorias temporales que abarrotan tu árbol
  • No conserva los mensajes de confirmación erróneos, por lo que deberá agregar un nuevo mensaje de confirmación a esta nueva confirmación
  • ¡Actualizar! Use la flecha hacia arriba para desplazarse por el búfer de comandos para volver a aplicar la confirmación anterior con su mensaje de confirmación (gracias @ARK)

Metas

El OP declaró que el objetivo era "recuperar el master antes de que se realizaran esas confirmaciones" sin perder los cambios y esta solución lo hace.

Hago esto al menos una vez a la semana cuando accidentalmente hago nuevos commits en masterlugar de develop. Por lo general, solo tengo un commit para revertir, en cuyo caso usar git reset HEAD^en la línea 1 es una forma más simple de revertir solo un commit.

No hagas esto si empujaste los cambios del maestro hacia arriba

Alguien más puede haber sacado esos cambios. Si solo está reescribiendo a su maestro local, no hay ningún impacto cuando se empuja hacia arriba, pero enviar un historial reescrito a los colaboradores puede causar dolores de cabeza.

Golpe
fuente
44
Gracias, estoy muy contento de haber leído tanto para llegar aquí, porque también es un caso de uso bastante común para mí. ¿Somos tan atípicos?
Jim Mack
66
Creo que somos totalmente típicos y "Ups que me comprometí a dominar por error" es el caso de uso más común para la necesidad de revertir un puñado o menos de commits. Por suerte, esta solución es tan simple que la tengo memorizada ahora.
Slam
99
Esta debería ser la respuesta aceptada. Es sencillo, fácil de entender y fácil de recordar
Sina Madani
1
No creo que el escondite sea necesario. Simplemente lo hice sin y funcionó bien.
A Campos
1
También puede recuperar fácilmente sus mensajes de confirmación si los tiene en su historial de CLI (línea de comando). Sucedí que tenía los comandos git addy git commitque usé, así que todo lo que tenía que hacer era presionar la flecha hacia arriba e ingresar algunas veces y ¡boom! Todo había vuelto, pero ahora en la rama correcta.
Luke Gedeon el
30

Esto no los "mueve" en el sentido técnico, pero tiene el mismo efecto:

A--B--C  (branch-foo)
 \    ^-- I wanted them here!
  \
   D--E--F--G  (branch-bar)
      ^--^--^-- Opps wrong branch!

While on branch-bar:
$ git reset --hard D # remember the SHAs for E, F, G (or E and G for a range)

A--B--C  (branch-foo)
 \
  \
   D-(E--F--G) detached
   ^-- (branch-bar)

Switch to branch-foo
$ git cherry-pick E..G

A--B--C--E'--F'--G' (branch-foo)
 \   E--F--G detached (This can be ignored)
  \ /
   D--H--I (branch-bar)

Now you won't need to worry about the detached branch because it is basically
like they are in the trash can waiting for the day it gets garbage collected.
Eventually some time in the far future it will look like:

A--B--C--E'--F'--G'--L--M--N--... (branch-foo)
 \
  \
   D--H--I--J--K--.... (branch-bar)
Sukima
fuente
1
¿No puedes usar rebasepara lo mismo?
Bergi
Sí, podría usar alternativamente rebaseen la rama separada en el escenario anterior.
Sukima
24

Para hacer esto sin reescribir el historial (es decir, si ya ha empujado los commits):

git checkout master
git revert <commitID(s)>
git checkout -b new-branch
git cherry-pick <commitID(s)>

¡Ambas ramas pueden ser empujadas sin fuerza!

teh_senaus
fuente
Pero luego tiene que lidiar con el escenario de reversión, que, dependiendo de su circunstancia, puede ser mucho más complicado. Si revierte una confirmación en la rama, Git seguirá viendo esas confirmaciones como se han llevado a cabo, por lo que para deshacer eso, debe revertir la reversión. Esto quema a bastantes personas, especialmente cuando revierten una fusión e intentan fusionar la rama nuevamente, solo para descubrir que Git cree que ya se ha fusionado esa rama (lo cual es completamente cierto).
Makoto
1
Es por eso que selecciono los commits al final, en una nueva rama. De esa manera, git los ve como nuevos commits, lo que resuelve su problema.
teh_senaus
Esto es más peligroso de lo que parece al principio, ya que está cambiando el estado de la historia del repositorio sin comprender realmente las implicaciones de este estado.
Makoto
55
No sigo su argumento: el punto de esta respuesta es que no está cambiando el historial, simplemente agregando nuevas confirmaciones (que deshacen efectivamente los cambios). Estas nuevas confirmaciones pueden ser empujadas y fusionadas de manera normal.
teh_senaus
13

Acabo de esta situación:

Branch one: A B C D E F     J   L M  
                       \ (Merge)
Branch two:             G I   K     N

Yo actué:

git branch newbranch 
git reset --hard HEAD~8 
git checkout newbranch

Esperaba que commit fuera el HEAD, pero commit L es ahora ...

Para asegurarse de aterrizar en el lugar correcto de la historia, es más fácil trabajar con el hash del commit

git branch newbranch 
git reset --hard #########
git checkout newbranch
Resplandor oscuro
fuente
9

¿Cómo puedo ir de esto?

A - B - C - D - E 
                |
                master

¿a esto?

A - B - C - D - E 
    |           |
    master      newbranch

Con dos comandos

  • git branch -m master newbranch

dando

A - B - C - D - E 
                |
                newbranch

y

  • git branch master B

dando

A - B - C - D - E
    |           |
    master      newbranch
Ivan
fuente
Sí, esto funciona y es bastante fácil. Sourcetree GUI está un poco confundido acerca de los cambios realizados en el shell git, pero después de una recuperación está todo bien nuevamente.
lars k.
Sí, son como en la pregunta. Los primeros dos diagramas pretenden ser equivalentes a los de la pregunta, simplemente rediseñados de la manera que me gustaría con el fin de ilustrar la respuesta. Básicamente cambie el nombre de la rama maestra como newbranch y cree una nueva rama maestra donde lo desee.
Ivan
Creo que esta es la respuesta correcta.
Leonardo Herrera
4

Si solo necesita mover todas sus confirmaciones no apresuradas a una nueva sucursal , entonces solo necesita,

  1. crear una nueva rama de la actual:git branch new-branch-name

  2. empuja tu nueva sucursal :git push origin new-branch-name

  3. revierta su rama anterior (actual) al último estado de inserción / estable:git reset --hard origin/old-branch-name

Algunas personas también tienen otros en upstreamslugar de origin, deberían usarupstream

Shamsul Arefin Sajib
fuente
3

1) Crea una nueva rama, que mueve todos tus cambios a new_branch.

git checkout -b new_branch

2) Luego regresa a la rama anterior.

git checkout master

3) Hacer git rebase

git rebase -i <short-hash-of-B-commit>

4) Luego, el editor abierto contiene los últimos 3 datos de confirmación.

...
pick <C's hash> C
pick <D's hash> D
pick <E's hash> E
...

5) Cambie picka dropen todos esos 3 commits. Luego guarde y cierre el editor.

...
drop <C's hash> C
drop <D's hash> D
drop <E's hash> E
...

6) Ahora se eliminan los últimos 3 commits de la rama actual ( master). Ahora empuje la rama con fuerza, con un +signo antes del nombre de la rama.

git push origin +master
rashok
fuente
2

Puedes hacer esto, solo 3 pasos simples que utilicé.

1) crea una nueva sucursal donde quieras confirmar tu actualización reciente.

git branch <branch name>

2) Encuentre el ID de confirmación reciente para confirmar en una nueva sucursal.

git log

3) Copie esa nota de identificación de confirmación de que la lista de confirmación más reciente tiene lugar en la parte superior. para que pueda encontrar su compromiso. también lo encuentras por mensaje.

git cherry-pick d34bcef232f6c...

También puede proporcionar algún tipo de ID de confirmación.

git cherry-pick d34bcef...86d2aec

Ahora tu trabajo hecho. Si eligió la identificación correcta y la rama correcta, entonces tendrá éxito. Entonces, antes de hacer esto, ten cuidado. De lo contrario, puede ocurrir otro problema.

Ahora puedes empujar tu código

git push

Pankaj Kumar
fuente
0

Otra forma de hacer esto:

[1] Cambie el nombre de la masterrama a su newbranch(suponiendo que esté en la masterrama):

git branch -m newbranch

[2] Crea una masterrama desde el commit que desees:

git checkout -b master <seven_char_commit_id>

por ejemplo, git checkout -b master a34bc22

Saikat
fuente