Módulo 9: Laboratorio — rebase, cherry-pick y bisect

Unidad: GIT-11
Nivel: avanzado, opcional tras el itinerario troncal.
Público: alumnado de DAW, DAM, ASIR y grados de informática.
Itinerario: GIT-11, después de ramas, remotos, conflictos y recuperación (reflog).
Prerrequisitos: manejar log, ramas, merge, conflictos, restore, revert y reflog.
Objetivo observable: reorganizar con seguridad el historial de una rama no publicada, trasplantar un commit concreto a otra rama y localizar el commit que introdujo una regresión mediante búsqueda binaria.
Fuente técnica: documentación oficial de git-rebase, git-cherry-pick, git-bisect y git-reflog.
Encaje curricular: contenido transversal avanzado; no depende de GitHub o GitLab.

Laboratorio A — Rebase interactivo en una rama privada

Objetivo

Dejar una rama de funcionalidad preparada para revisión: actualizada sobre main, con dos commits coherentes en lugar de tres provisionales.

Idea clave

git rebase main vuelve a aplicar los commits exclusivos de la rama actual sobre la punta de main. Al hacerlo, crea commits nuevos: cambian sus identificadores.

Úsalo únicamente en trabajo local o en una rama que no esté siendo usada por otras personas. No cambia ningún remoto por sí solo.

Preparación reproducible

git init -b main laboratorio-rebase
cd laboratorio-rebase
git config user.name "Alumno"
git config user.email "alumno@example.test"

printf "# Tienda\n" > README.md
git add README.md
git commit -m "chore: inicia proyecto"

git switch -c feature/formulario
printf "<form></form>\n" > formulario.html
git add formulario.html
git commit -m "feat: añade estructura de formulario"

printf "required\n" >> formulario.html
git add formulario.html
git commit -m "wip: añade validación"

printf "Formulario de contacto.\n" >> README.md
git add README.md
git commit -m "docs: documenta formulario"

git switch main
printf "# Guía de desarrollo\n" > CONTRIBUTING.md
git add CONTRIBUTING.md
git commit -m "docs: añade guía de desarrollo"

git switch feature/formulario
git log --oneline --graph --all

El commit de main toca un archivo distinto al de la rama a propósito: así el rebase de la Parte 1 se aplica limpio y puedes centrarte en el mecanismo. Los conflictos durante un rebase se tratan más abajo, en su propia sección.

Salida esperada, con hashes distintos:

* <hash> docs: documenta formulario
* <hash> wip: añade validación
* <hash> feat: añade estructura de formulario
| * <hash> docs: añade guía de desarrollo
|/
* <hash> chore: inicia proyecto

Parte 1: actualizar la base de la rama

git rebase main

Salida esperada aproximada:

Successfully rebased and updated refs/heads/feature/formulario.

Ahora los tres commits de feature/formulario son nuevos commits sobre la punta actual de main. El directorio de trabajo y el área de preparación deben quedar limpios:

git status
On branch feature/formulario
nothing to commit, working tree clean

Parte 2: ordenar y fusionar commits

git rebase -i main

En el editor, dejar las líneas así:

pick <hash> feat: añade estructura de formulario
squash <hash> wip: añade validación
pick <hash> docs: documenta formulario

Al guardar, Git pedirá el mensaje del commit fusionado. Sustituirlo por:

feat: añade formulario con validación

Comprobación:

git log --oneline main..HEAD

Salida esperada:

<hash> docs: documenta formulario
<hash> feat: añade formulario con validación

Qué cambia Git

  • Directorio de trabajo e índice: durante el proceso pueden contener el estado intermedio del commit que Git está reaplicando; al acabar correctamente quedan en el estado de la nueva punta.
  • Historial local: se sustituyen los commits originales por otros nuevos.
  • Remoto: no cambia. Si esta rama ya se hubiese publicado, habría que coordinarlo y, solo si procede, actualizarla con git push --force-with-lease, nunca con un push --force indiscriminado.

Si aparece un conflicto

git status
# editar los archivos con marcadores <<<<<<<, ======= y >>>>>>>
git add <archivo-resuelto>
git rebase --continue

Alternativas:

git rebase --abort  # vuelve la rama al estado previo al rebase
git rebase --skip   # omite el commit actual; usar solo si se entiende la pérdida

Si se termina un rebase no deseado, localizar la punta anterior:

git reflog
git switch feature/formulario
git reset --hard <hash-anterior>

reset --hard sobrescribe el directorio de trabajo y el área de preparación: usarlo solo tras identificar el commit correcto y comprobar que no hay cambios sin guardar.

Errores frecuentes

  • Hacer rebase sobre una rama compartida: las demás personas conservarán referencias a los commits antiguos.
  • Creer que rebase “mueve” commits: los reaplica y crea otros nuevos.
  • Resolver un conflicto y olvidar git add: git rebase --continue no podrá avanzar.
  • Usar --skip para salir del paso: puede eliminar un cambio funcional necesario.

Ejercicio y criterios

Crea una rama con cuatro commits: dos de funcionalidad, uno de corrección y uno de documentación. Tras actualizarla sobre main, deja tres commits: una funcionalidad, una corrección y documentación.

Se considera correcto si:

  • git log --oneline main..HEAD muestra exactamente tres commits con mensajes claros.
  • El contenido funciona y git status queda limpio.
  • La explicación identifica que los hashes cambiaron.
  • No se ha modificado ningún remoto.

Laboratorio B — Trasplantar un commit con cherry-pick

Objetivo

Llevar a main una corrección concreta que vive dentro de una rama de funcionalidad sin arrastrar el resto de esa rama.

Idea clave

git cherry-pick <hash> toma el cambio que introdujo un commit y lo aplica sobre la rama actual como un commit nuevo.

La palabra importante es nuevo: no mueve el commit ni lo comparte. El contenido del cambio es el mismo, pero el commit resultante tiene otro hash, otra fecha de aplicación y otro padre. Es la misma idea que en rebase —reaplicar, no mover—, aplicada a un commit suelto en lugar de a una rama entera.

Cuándo usarlo, y cuándo no

cherry-pick resuelve bien un caso concreto: necesitas un cambio ya, y la rama que lo contiene no está lista.

Situación Herramienta
Un fix urgente atrapado en una rama a medias cherry-pick
Un commit hecho por error en la rama equivocada cherry-pick al sitio correcto y borrarlo del origen
Llevar unas correcciones a una rama de mantenimiento cherry-pick de un rango
Integrar una rama terminada merge (módulo 5)
Poner la rama al día sobre main rebase (laboratorio A)

Lo que no es: una forma de integrar ramas. Trasplantar commits uno a uno para no aprender merge acaba en historiales duplicados y difíciles de leer.

Preparación reproducible

git init -b main laboratorio-cherry
cd laboratorio-cherry
git config user.name "Alumno"
git config user.email "alumno@example.test"

printf "PRECIO_BASE = 100\nIVA = 0.21\n" > tarifas.py
git add tarifas.py
git commit -m "chore: inicia proyecto"

printf "# Tarifas\n" > README.md
git add README.md
git commit -m "docs: crea README"

git switch -c feat/rediseno
printf "\nDESCUENTO = 0.10\n" >> tarifas.py
git add tarifas.py
git commit -m "feat: añade descuento"

printf "IVA = 0.21  # tipo general vigente\n" > iva.py
git add iva.py
git commit -m "fix: corrige el tipo de IVA aplicado"

printf "\nCOLOR_PRIMARIO = 'azul'\n" >> tarifas.py
git add tarifas.py
git commit -m "feat: aplica nueva paleta"

git log --oneline --graph --all

Salida esperada, con hashes distintos:

* d988099 feat: aplica nueva paleta
* 268f417 fix: corrige el tipo de IVA aplicado
* c1f4c49 feat: añade descuento
* 8bc7e0b docs: crea README
* 7f6b0e4 chore: inicia proyecto

La situación es la habitual: el rediseño va para largo, pero la corrección del IVA hay que publicarla hoy.

Parte 1: trasplantar el commit

git switch main
ls
README.md
tarifas.py

main no tiene iva.py. Trasplanta solo ese commit, usando su hash:

git cherry-pick 268f417
ls
git log --oneline -2
[main 36151b8] fix: corrige el tipo de IVA aplicado
 1 file changed, 1 insertion(+)
 create mode 100644 iva.py
README.md
iva.py
tarifas.py
36151b8 fix: corrige el tipo de IVA aplicado
8bc7e0b docs: crea README

main tiene la corrección y nada más: ni el descuento ni la paleta.

El commit es nuevo: mismo cambio, distinto hash

Compara el original con el trasplantado:

git log --oneline -1 feat/rediseno~1   # 268f417, el original
git log --oneline -1 main              # 36151b8, el nuevo
diff <(git show 268f417 --format="") <(git show 36151b8 --format="")

El diff no devuelve nada: el cambio es idéntico. Pero son dos commits distintos, y git log los mostrará como tales. Interiorizar esto evita la confusión clásica de «¿por qué aparece dos veces la misma corrección?».

Parte 2: dejar rastro del origen con -x

Como el hash cambia, conviene registrar de dónde vino:

git reset --hard HEAD~1
git cherry-pick -x 268f417
git log -1 --format="%B"
fix: corrige el tipo de IVA aplicado

(cherry picked from commit 268f4179c5302a91c3ceb3374a3f7ec848b476e4)

-x añade esa línea al mensaje. En un proyecto con ramas de mantenimiento es la diferencia entre poder rastrear una corrección y tener que adivinarlo. Úsalo siempre al trasplantar entre ramas de larga vida.

Parte 3: cuando hay conflicto

Un trasplante puede chocar con lo que ya hay en el destino:

git switch feat/rediseno
printf "PRECIO_BASE = 100\nIVA = 0.21\nENVIO = 5\n" > tarifas.py
git add tarifas.py && git commit -m "feat: añade coste de envío"

git switch main
printf "PRECIO_BASE = 120\nIVA = 0.21\n" > tarifas.py
git add tarifas.py && git commit -m "feat: sube el precio base"

git cherry-pick <hash-del-coste-de-envío>
Auto-merging tarifas.py
CONFLICT (content): Merge conflict in tarifas.py
error: could not apply d5367d0... feat: añade coste de envío
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git cherry-pick --continue".

El estado y los marcadores son los del módulo 5, con una particularidad:

git status --short
cat tarifas.py
UU tarifas.py
PRECIO_BASE = 120
IVA = 0.21
<<<<<<< HEAD
=======
ENVIO = 5
>>>>>>> d5367d0 (feat: añade coste de envío)

UU es el código corto de «ambas versiones modificadas». Fíjate en que el marcador inferior identifica el commit trasplantado, no una rama: es la pista de que estás en un cherry-pick y no en un merge. Resuelve, prepara y continúa:

printf "PRECIO_BASE = 120\nIVA = 0.21\nENVIO = 5\n" > tarifas.py
git add tarifas.py
git status
On branch main
You are currently cherry-picking commit d5367d0.
  (all conflicts fixed: run "git cherry-pick --continue")
  (use "git cherry-pick --skip" to skip this patch)
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)
git cherry-pick --continue

Como en el rebase, hay tres salidas y conviene conocerlas antes de necesitarlas:

Comando Efecto
git cherry-pick --continue Cierra el trasplante con la resolución preparada
git cherry-pick --abort Cancela y devuelve la rama al estado previo
git cherry-pick --skip Descarta este commit y sigue con el resto del rango

--abort deja el repositorio exactamente como estaba, con el árbol limpio y el archivo con su contenido anterior.

Parte 4: trasplantar un rango

Para llevar varias correcciones a una rama de mantenimiento, se puede indicar un rango. Ojo con la sintaxis: A..B excluye A, así que para incluir el primer commit se escribe A^..B.

git switch produccion
git cherry-pick <hash-primer-fix>^..<hash-último-fix>
[produccion b4895d2] fix: valida el email
 1 file changed, 1 insertion(+)
 create mode 100644 validacion.py
[produccion 52a7892] fix: corrige el mensaje de error
 1 file changed, 1 insertion(+)
 create mode 100644 errores.py
git log --oneline
52a7892 fix: corrige el mensaje de error
b4895d2 fix: valida el email
dcdb3fb chore: inicia proyecto

La rama de producción recibe las dos correcciones y ninguna de las funcionalidades que había entre ellas. Es el caso de uso que justifica el comando.

Qué cambia Git

  • Directorio de trabajo e índice: se aplican los cambios del commit trasplantado; si hay conflicto, quedan con marcadores hasta que se resuelva.
  • Historial local: se añade un commit nuevo en la rama actual. El commit original no se toca ni desaparece de su rama.
  • Remoto: no cambia hasta un push. A diferencia del rebase, aquí no se reescribe nada ya existente, así que publicar no exige coordinación especial.

Y después, ¿qué pasa al integrar la rama?

Es la duda que siempre aparece: si más adelante hago merge de feat/rediseno, ¿se aplicará la corrección dos veces?

Primero, comprueba qué queda pendiente:

git cherry -v main feat/rediseno
+ 273e6fc feat: añade descuento
- 2302bdf fix: corrige el IVA
+ 6df97a4 feat: nueva paleta

git cherry compara los cambios, no los hashes. El signo - marca el commit cuyo contenido ya está en main; los + son los que faltan. Ahora integra:

git merge feat/rediseno -m "merge: integra el rediseño"
cat iva.py
Merge made by the 'ort' strategy.
 descuento.py | 1 +
 tema.py      | 1 +
 2 files changed, 2 insertions(+)
IVA = 0.21

Solo se añaden los dos archivos que faltaban: el cambio no se duplica. Git detecta que ese contenido ya está aplicado. En el grafo verás la corrección como dos commits distintos, uno en cada línea, pero el archivo resultante es correcto.

Con todo, si el commit trasplantado se modifica después en su rama de origen, la reconciliación sí puede dar conflicto. Por eso -x importa: te dice de dónde salió cada cosa cuando toca investigar.

Errores frecuentes

  • Usar cherry-pick en lugar de merge para integrar una rama entera. Duplica commits y ensucia el historial. Para integrar, merge.
  • Olvidar -x al trasplantar entre ramas de larga vida. Meses después nadie sabe de dónde vino esa corrección.
  • Escribir A..B esperando que incluya A. El rango excluye el primer commit; hay que usar A^..B.
  • Trasplantar un commit que depende de otro anterior. El cambio se aplica, pero puede no funcionar porque le falta el commit del que dependía. Comprueba que el resultado funciona, no solo que Git no protestó.
  • Resolver el conflicto y olvidar git cherry-pick --continue. El repositorio se queda a medias; git status te lo recuerda.
  • Confundir el marcador >>>>>>> <hash> (mensaje) con una rama. En un cherry-pick el marcador identifica el commit trasplantado.

Ejercicio y criterios

Crea un repositorio con una rama main y una rama feat/informes que contenga cuatro commits: dos de funcionalidad, uno de corrección de un error y otro de documentación.

  1. Trasplanta a main únicamente el commit de corrección, con -x.
  2. Demuestra que el hash cambió y que el contenido del cambio es idéntico.
  3. Provoca un conflicto trasplantando un segundo commit y resuélvelo con --continue. Repite el trasplante desde cero y esta vez cancélalo con --abort, verificando que el repositorio queda como antes.
  4. Crea una rama mantenimiento a partir de un commit antiguo y trasplanta a ella un rango de dos commits con A^..B.
  5. Ejecuta git cherry -v main feat/informes e interpreta los signos.
  6. Integra feat/informes en main con merge y comprueba si el cambio trasplantado se duplicó.

Se considera correcto si:

  • main contiene la corrección y ninguna de las funcionalidades de la rama.
  • El mensaje del commit trasplantado incluye la línea (cherry picked from commit …).
  • Se explica por qué el hash es distinto aunque el cambio sea el mismo.
  • Tras el --abort, git status queda limpio y el contenido coincide con el previo.
  • La rama mantenimiento recibe exactamente los dos commits del rango.
  • Se interpreta correctamente el - de git cherry y se explica por qué el merge final no duplicó el contenido.
  • No se ha modificado ningún remoto.

Laboratorio C — Encontrar una regresión con git bisect

Objetivo

Identificar el primer commit malo entre un commit que se sabe correcto y una versión actual que falla.

Idea clave

git bisect realiza una búsqueda binaria: comprueba un commit intermedio, se marca como good o bad y Git reduce el rango. No reescribe el historial; durante la búsqueda cambia temporalmente el HEAD a los commits que hay que probar.

Preparación reproducible

git init -b main laboratorio-bisect
cd laboratorio-bisect
git config user.name "Alumno"
git config user.email "alumno@example.test"

printf "resultado=4\n" > calculadora.txt
git add calculadora.txt
git commit -m "feat: calcula suma correcta"

printf "# Calculadora\n" > README.md
git add README.md
git commit -m "docs: añade título"

printf "resultado=5\n" > calculadora.txt
git add calculadora.txt
git commit -m "refactor: ajusta cálculo"

printf "Uso interno.\n" >> README.md
git add README.md
git commit -m "docs: añade nota de uso"

printf "Formato revisado.\n" >> README.md
git add README.md
git commit -m "docs: revisa formato"

El criterio de prueba será sencillo: calculadora.txt debe contener exactamente resultado=4.

grep -qx "resultado=4" calculadora.txt

En la punta actual no habrá salida y el comando devolverá un código de error: la regresión está presente.

Búsqueda guiada

git bisect start
git bisect bad
git bisect good HEAD~4

Git cambia a un commit intermedio. En cada parada, ejecutar:

grep -qx "resultado=4" calculadora.txt
  • Si el comando no muestra nada y falla, el commit es malo:

    git bisect bad
  • Si el comando termina correctamente, el commit es bueno:

    git bisect good

Repetir hasta obtener una salida de este tipo:

<hash> is the first bad commit
commit <hash>
Author: Alumno <alumno@example.test>
Date: ...

    refactor: ajusta cálculo

Comprobar el cambio:

git show --stat
git show

Finalizar siempre la sesión:

git bisect reset

Salida esperada:

Previous HEAD position was <hash> ...
Switched to branch 'main'

Qué cambia Git

  • Directorio de trabajo e índice: se actualizan en cada comprobación para reflejar el commit que se está probando.
  • Historial local: no cambia; solo se mueve temporalmente HEAD.
  • Remoto: no cambia.
  • Al terminar: git bisect reset devuelve el repositorio a la rama o commit desde el que se inició el diagnóstico.

Automatización opcional

Cuando existe una prueba automatizada que devuelve 0 si pasa y un valor distinto de 0 si falla:

git bisect start HEAD HEAD~4
git bisect run grep -qx "resultado=4" calculadora.txt
git bisect reset

En un proyecto real, se sustituye grep ... por el comando de pruebas del proyecto, por ejemplo npm test, pytest o mvn test, siempre que su código de salida represente correctamente éxito o fallo.

Errores frecuentes

  • Marcar como good un commit que no se ha probado.

  • Elegir un commit “bueno” que no es anterior al fallo.

  • Confundir un fallo de compilación ajeno a la regresión con un resultado bad. Si no se puede evaluar un commit, usar:

    git bisect skip
  • Olvidar git bisect reset y continuar trabajando con HEAD separado.

Ejercicio y criterios

Introduce deliberadamente una regresión en el cuarto de siete commits de una rama. Define una comprobación reproducible, localiza el primer commit malo y documenta:

  1. El hash y mensaje del primer commit malo.
  2. El último commit bueno utilizado.
  3. La comprobación aplicada.
  4. La salida de git bisect reset.

Se considera correcto si el commit señalado es el que introdujo el fallo, no uno posterior que solo lo conserva.

Resumen final

El alumnado debe saber:

  • decidir cuándo un rebase es apropiado y cuándo no;
  • reorganizar una rama privada con rebase -i;
  • continuar, abortar o recuperar un rebase;
  • trasplantar un commit concreto con cherry-pick y explicar por qué el hash cambia;
  • elegir entre cherry-pick, merge y rebase según el problema;
  • dejar trazabilidad con -x y leer la salida de git cherry;
  • delimitar una regresión entre un commit bueno y uno malo;
  • usar git bisect sin alterar el historial ni el remoto;
  • cerrar siempre una sesión de bisect.

Los tres laboratorios comparten una idea de fondo: Git no mueve commits, los reaplica. rebase lo hace con una rama entera, cherry-pick con uno suelto, y bisect no reaplica nada porque solo diagnostica.

Estado editorial: listo.

Registro de revisión técnica

Revisado el 4 de agosto de 2026 con Git 2.47.1. Los tres laboratorios se han reproducido desde carpetas vacías, con identidad de práctica y configuración aislada.

Corrección aplicada al laboratorio A. La preparación original hacía que el commit de main (docs: añade guía de desarrollo) añadiera texto al final del mismo README.md que modificaba la rama, de modo que git rebase main siempre terminaba en CONFLICT (content): Merge conflict in README.md y no en el Successfully rebased que el documento anunciaba. El alumnado se topaba con un conflicto en la Parte 1, antes de que el documento explicara cómo resolverlo. Se ha cambiado ese commit para que cree CONTRIBUTING.md: el rebase se aplica limpio, la salida coincide con la documentada y la sección de conflictos conserva su papel. Verificado que git rebase main devuelve Successfully rebased and updated refs/heads/feature/formulario. con el árbol limpio, y que git rebase -i main con squash en la segunda línea deja los dos commits esperados.

Laboratorio B, nuevo. Se ha comprobado que:

  • git cherry-pick crea un commit nuevo: el diff del original y el del trasplantado son idénticos y sus hashes distintos.
  • -x añade al mensaje la línea (cherry picked from commit <hash completo>).
  • Ante un conflicto, git status --short marca UU y el marcador inferior identifica el commit trasplantado, no una rama; git status ofrece --continue, --skip y --abort, y --abort restituye el estado previo con el árbol limpio.
  • El rango A^..B trasplanta los dos commits indicados sin arrastrar los intermedios de otra naturaleza.
  • git cherry -v main feat/rediseno marca con - el commit ya aplicado y con + los pendientes.
  • Un merge posterior de la rama de origen no duplica el contenido trasplantado: solo incorpora los archivos que faltaban.

Laboratorio C verificado sin cambios: la búsqueda manual identifica refactor: ajusta cálculo como primer commit malo en dos pasos, git bisect reset devuelve a main, y la versión automatizada con git bisect run grep -qx … llega al mismo commit y termina con bisect found first bad commit.

Los hashes de las salidas son los de esta ejecución y serán distintos en cada repositorio.