learning-front

Nivel 9 · El ciclo completo: del commit al deploy

Git avanzado: PRs, conflictos, rebase y stash

Pull requests, resolver merge conflicts, rebase, stash, tags y cuándo (con cuidado) un force push. El flujo de equipo que convierte tus commits en un historial que otra persona puede leer.

En el Nivel 0 aprendiste el flujo de una persona trabajando en solitario: add, commit, push. Con eso ya ganas el ochenta por ciento del día a día. Pero en cuanto aparece un segundo desarrollador, o tú mismo empiezas a gestionar varias funcionalidades a la vez, ese flujo se queda corto.

Lo que cambia en el Nivel 9 no es Git en sí: es el contexto en el que lo usas. El mismo repositorio, pero ahora hay más de una línea de trabajo, hay revisión de código antes de integrar, y a veces dos personas tocan el mismo fichero a la vez. Git tiene herramientas para todo eso. Este capítulo las presenta en orden, del problema al comando.

El Team Builder sale del playground y entra a un repo local real. Para los ejercicios de este nivel necesitas tu Team Builder de React en tu máquina, en un repositorio de git, subido a GitHub. Si vienes construyéndolo desde el Nivel 7, ya lo tienes. Si necesitas un punto de partida limpio, el ejercicio del capítulo te explica cómo crearlo en minutos. A partir de aquí, los ejercicios son flujos reales, no playgrounds: un flujo de equipo necesita un repositorio de verdad.

Una rama por tarea#

Hasta ahora has trabajado siempre en main. Eso funciona cuando hay una persona y una tarea a la vez, pero en cuanto hay dos funcionalidades en marcha al mismo tiempo el historial se convierte en un ruido donde es imposible saber qué pertenece a qué.

La solución es una rama por tarea. Una rama es simplemente un puntero a un commit: un nombre que le das a una línea de trabajo para poder cambiar de contexto sin mezclar nada.

texto
main:    A ── B ── C
                    \
feature:             D ── E

En este diagrama, A, B y C son commits de main. Cuando creaste la rama feature a partir de C, empezaste a añadir tus commits (D, E) sin tocar main. Mientras tanto, si alguien hace otro commit en main, las dos líneas siguen siendo independientes.

Los comandos para trabajar con ramas:

shell
# crea la rama feature/filtro-por-rol y te mueve a ella en un solo paso
git switch -c feature/filtro-por-rol

# comprueba en qué rama estás y cuáles existen
git branch

# vuelve a main (o a cualquier otra rama existente)
git switch main

El nombre de la rama importa para el equipo. La convención más extendida es un prefijo que describe el tipo de trabajo: feature/ para funcionalidades nuevas, fix/ para arreglos, chore/ para tareas de mantenimiento. El sufijo describe la tarea concreta: feature/filtro-por-rol, fix/partidas-reinhardt.

En el Team Builder crearías una rama así para añadir el filtro por rol (tanque, daño, soporte) sin tocar nada de lo que ya funciona en main. Si el filtro resulta ser más complicado de lo esperado, main sigue intacto y en cualquier momento puedes volver a él.

Pull requests: pedir que revisen tu código#

Un pull request (o PR) no es un comando de Git: es una función de GitHub (y de GitLab, Bitbucket y otros forges similares). Es la forma de decirle al equipo: “he terminado esta parte, ¿alguien la revisa antes de que la integre?”.

El flujo es el siguiente. Trabajas en tu rama, haces tus commits, subes la rama al remoto, y entonces abres un PR apuntando a main. GitHub te muestra la diferencia entre tu rama y main, y cualquier miembro del equipo puede comentar línea por línea, pedir cambios o aprobar. Cuando hay acuerdo, el PR se fusiona.

La revisión no es un trámite: es la red de seguridad. Un segundo par de ojos antes de integrar en main atrapa errores que quien escribe el código no ve, alinea al equipo en cómo se resuelven los problemas, y crea un registro escrito de por qué se decidió algo de una manera y no de otra.

Para abrir un PR desde la terminal, la GitHub CLI que ya viste en el Nivel 0 lo hace en un comando:

shell
# sube la rama al remoto y la registra como rama de seguimiento
git push -u origin feature/filtro-por-rol

# abre el PR hacia main con título y descripción
gh pr create \
  --base main \
  --head feature/filtro-por-rol \
  --title "feat: filtro de héroes por rol" \
  --body "Añade un selector de rol que filtra el roster en tiempo real."

También puedes abrirlo desde la web de GitHub, que es lo más habitual al principio. Tras hacer el git push, GitHub muestra un aviso en la página del repositorio con un botón para crear el PR directamente.

Fusionar: merge o rebase#

Cuando el PR recibe aprobación, toca integrar la rama en main. Hay dos formas de hacerlo y producen historiales distintos.

Merge crea un commit de fusión que une las dos líneas. El historial refleja lo que ocurrió de verdad: dos líneas paralelas que se juntaron en un punto.

texto
main:    A ── B ── C ────────── F
                    \          /
feature:             D ── E ──
                              (commit de merge F)

Rebase toma tus commits y los reescribe encima del último commit de la rama destino. La historia queda lineal: parece que siempre trabajaste a partir de lo último que había en main.

texto
main:    A ── B ── C ── D' ── E'
                         (tus commits, reescritos)

Ninguno es universalmente mejor. Merge preserva el contexto histórico. Rebase produce un historial más fácil de leer con git log. Muchos equipos usan rebase para limpiar su rama antes de pedir revisión, y merge para la fusión final en main. Cuando main avanza de forma limpia y sin commits de fusión por medio, se llama fast-forward: Git solo mueve el puntero de main al último commit de tu rama, sin crear ningún commit nuevo.

Conflictos: cuando dos cambios chocan#

Un conflicto ocurre cuando dos ramas modifican el mismo fragmento del mismo fichero. Git puede fusionar cambios automáticamente si afectan a partes distintas, pero cuando chocan en el mismo sitio no puede decidir por ti: te marca el fichero y espera a que lo resuelvas.

Los marcadores tienen este aspecto:

texto
<<<<<<< HEAD
  { nombre: 'Reinhardt', partidas: 312 },
=======
  { nombre: 'Reinhardt', partidas: 321 },
>>>>>>> main

Lo que hay entre <<<<<<< HEAD y ======= es el estado de tu rama actual. Lo que hay entre ======= y >>>>>>> main es el estado de la rama que estás integrando. Tu trabajo es:

shell
# tras editar el fichero y borrar los tres marcadores, márcalo como resuelto
git add src/data/heroes.ts

# cierra el merge con un commit que describe la resolución
git commit -m "merge main: resuelve conflicto en heroes.ts"

La consecuencia de resolver mal es grave: si conservas un marcador sin borrarlo, el fichero queda con ese texto literal. El código puede compilar con errores raros, o directamente romper en producción sin que nadie se dé cuenta hasta que alguien abra ese fichero. Git no detecta que olvidaste borrar los marcadores: es responsabilidad tuya leer lo que quedó.

Rebase y la regla de oro#

git rebase main toma cada commit de tu rama y lo aplica uno a uno sobre el último commit de main. Si hay conflicto en algún commit, Git se detiene, resuelves el conflicto, y continúas:

shell
# mueve tus commits encima del último commit de main
git rebase main

# si hay conflicto, Git se detiene: editas el fichero, borras los marcadores, y luego:
git add src/data/heroes.ts

# le dices a Git que continúe aplicando el resto de commits
git rebase --continue

git rebase -i main (la -i es de interactivo) abre un editor con la lista de tus commits y te deja reordenarlos, combinarlos (squash) o editar sus mensajes antes de que lleguen a main. Es la herramienta para limpiar el historial antes de pedir revisión: conviertes cinco “wip” en dos commits con mensajes que cualquier compañero entiende.

La regla de oro del rebase: nunca hagas rebase de una rama que ya has compartido con otra persona. El rebase reescribe commits: los mismos cambios, pero con hashes nuevos. Si alguien más tiene tus commits con los hashes viejos y tú publicas los nuevos, sus historiales divergen de forma irreconciliable. Tendrán que resetear a mano o hacer cherry-pick de sus propios commits. En la práctica: rebase libre en tu rama local mientras la desarrollas; en cuanto otra persona trabaja en esa rama al mismo tiempo, solo merge.

Stash: aparcar trabajo a medias#

Estás en mitad de añadir estilos al filtro de roles cuando llega un mensaje: hay un dato incorrecto en la lista de héroes que está dando problemas en producción. Necesitas un directorio limpio para cambiar de contexto, pero no quieres hacer un commit a medias.

git stash aparca tus cambios sin commitear en una pila temporal y deja el working directory limpio:

shell
# aparca los cambios actuales del working directory y del staging
git stash

# (tu directorio está limpio; cambias de rama, arreglas el urgente, commiteas)

# recupera el trabajo que habías apartado y lo aplica encima del estado actual
git stash pop

La pila puede tener varios elementos. git stash list muestra todo lo que hay aparcado. git stash pop saca el último que metiste (el más reciente). Si olvidaste que había algo ahí, no pasa nada: el stash no expira solo.

Tags: marcar versiones#

Un tag es un nombre fijo que apunta a un commit concreto. A diferencia de una rama (que avanza a medida que haces commits), un tag no se mueve nunca. Se usa para marcar releases, versiones desplegadas o puntos de referencia en el historial.

shell
# crea un tag anotado con un mensaje descriptivo
git tag -a v1.0.0 -m "Primera versión con filtro por rol. Arreglos de datos en roster."

# git push por defecto NO sube los tags: hay que pedirlo explícitamente
git push --tags

Hay dos tipos de tags. Los ligeros son solo un puntero a un commit, sin metadatos extra. Los anotados (con -a) incluyen el nombre del autor, la fecha y el mensaje: son los que se usan en producción porque llevan toda la información de quién marcó la versión y por qué. En el día a día, siempre anotados.

Force push, con mucho cuidado#

Después de un rebase, si intentas hacer push normal de tu rama, Git lo rechaza:

shell
# error: failed to push some refs
# hint: Updates were rejected because the tip of your current branch is behind

El motivo: el rebase reescribió los commits de tu rama local. El remoto aún tiene los commits con los hashes viejos. Las dos ramas han divergido y Git no puede resolver esa situación con un push estándar.

La solución es un force push, pero con la variante segura:

shell
# sobreescribe la rama remota con tu historial rebaseado,
# pero ABORTA si alguien más subió commits desde tu última sincronización
git push --force-with-lease

# NUNCA uses esto si no sabes exactamente qué vas a pisar
git push --force

--force-with-lease comprueba que el remoto está en el mismo estado que tenías cuando hiciste el rebase. Si alguien subió algo entre medias, el push aborta con un error: no pierdes nada. --force sobrescribe sin comprobar nada: si tu compañero subió tres commits mientras tú rebaseabas, desaparecen sin aviso. Usa siempre --force-with-lease.

Deshacer sin reescribir la historia: revert#

Imagina que acabas de publicar un commit en main y, al cabo de unos minutos, te avisan de que algo se ha roto en producción. El commit tiene un error. Necesitas deshacerlo, y rápido.

La tentación inmediata es git reset: mueve el puntero de la rama hacia atrás y, con --hard, descarta los commits que quedan por encima. Parece limpio. Pero si la rama ya está publicada (ya hiciste git push), borrar commits con reset y luego hacer git push --force es exactamente lo que la regla de oro del rebase prohíbe: reescribes la historia que otros ya tienen. Tus compañeros, el pipeline de CI, el deploy, cualquiera que haya recibido esos commits tendrá su historial roto. Tendrán que resetear a mano.

La herramienta correcta en ese caso es git revert:

shell
# revierte el último commit: crea un commit NUEVO que aplica los cambios inversos
git revert HEAD

# revierte un commit concreto identificado por su hash
# (Git abre el editor para que confirmes el mensaje del nuevo commit)
git revert a3f9c1b

# revierte sin abrir el editor: usa el mensaje por defecto
git revert --no-edit HEAD

git revert no borra nada. Toma los cambios del commit que señalas y genera un commit nuevo que los deshace: si el commit malo añadió una línea, el revert la elimina; si eliminó un fichero, el revert lo restaura. El historial queda entero: el commit malo sigue ahí, y justo después aparece el que lo corrige. No hay reescritura. No hay force-push. No hay problema para el resto del equipo.

Puedes hacer git push normal con el revert, igual que con cualquier otro commit.

La regla práctica es sencilla:

  • Algo ya publicado o en una rama compartida que necesitas deshacer → git revert.
  • Commits locales que aún no has compartido con nadie → puedes usar git reset, con cuidado.

Usar reset más force-push para “borrar” un commit que ya está en main reescribe el historial de producción: lo deja inconsistente y el equipo pierde la trazabilidad de qué pasó. Es justo lo que el capítulo de deploy te lleva a evitar al usar git revert como rollback: te da el mismo resultado (el código vuelve al estado anterior) sin ninguna de esas consecuencias. Por eso es la herramienta de rollback estándar en cualquier equipo.

Comprueba lo que sabes#

Pregunta 1 de 7

¿Qué diferencia hay en el historial resultante entre hacer un `git merge` y un `git rebase`?

Tu turno#

El ejercicio es un flujo de Git real: lo haces en tu repositorio local del Team Builder, no en un editor de esta página. Los ficheros de solución muestran la secuencia exacta de comandos de cada tier con su explicación. Haz el tuyo primero y luego léelos: la comparación entre lo que hiciste y la solución es donde está el aprendizaje.

Ejercicio · hazlo en local

Flujo de equipo profesional con Git

Sobre el repositorio de tu Team Builder de React (el que vienes construyendo desde el Nivel 7, o uno de prueba en local), recorre el ciclo de vida completo de una feature: rama, commits, PR, conflicto y resolución. Las soluciones muestran la secuencia exacta de comandos de cada tier con su explicación. Léelas después de hacer el tuyo: la comparación es donde está el aprendizaje.

Paso 1: Rama + PR + conflicto resuelto con merge

  • Creas la rama `feature/filtro-por-rol` con `git switch -c`, haces al menos dos commits con mensajes descriptivos y la subes al remoto.
  • Abres un Pull Request con `gh pr create` apuntando a `main`.
  • Simulas que `main` avanza mientras el PR está abierto (cambias a main, haces un commit, vuelves a la feature).
  • Integras los cambios de `main` en tu rama con `git merge main`, resuelves el conflicto borrando los marcadores, y cierras el merge con `git add` + `git commit`.
  • Subes la rama actualizada con `git push`.

Cómo hacerlo en local

Clona el repositorio del curso, entra en la carpeta del ejercicio y abre el index.html en tu navegador. Toda tu solución va en solucion.js.

git clone <repo>
cd exercises/nivel-9/git-avanzado
# abre index.html en el navegador y edita solucion.js
Ver soluciones
# Tier ok: rama + PR + conflicto resuelto con merge.
# Demuestra el ciclo básico de trabajo en equipo: aislar el trabajo en una rama,
# abrirlo a revisión con un PR, y absorber cambios de main cuando hay colisión.

# --- 1. Crear la rama de la feature y cambiarte a ella ---

# crea la rama feature/filtro-por-rol y te mueve a ella en un solo paso
git switch -c feature/filtro-por-rol


# --- 2. Hacer cambios y commits en la rama ---

# (aquí editas los ficheros de tu proyecto — por ejemplo, añades el selector de rol
# en HeroCard.tsx o en el componente de filtros — y luego registras el trabajo)

# guarda el estado actual del área de preparación como primer commit de la feature
git add .
git commit -m "añade selector de rol en el filtro de héroes"

# (sigues trabajando: conectas el filtro al estado, pruebas que funciona)

# registra el segundo avance de la feature
git add .
git commit -m "filtra el roster según el rol seleccionado"


# --- 3. Subir la rama al repositorio remoto ---

# sube la rama y la registra como rama de seguimiento remoto (-u)
# a partir de aquí, git push sin argumentos actualiza esta misma rama
git push -u origin feature/filtro-por-rol


# --- 4. Abrir el Pull Request ---

# abre un PR desde feature/filtro-por-rol hacia main con título y descripción
# el flag --fill usa el mensaje del último commit como descripción si no pasas --body
gh pr create \
  --base main \
  --head feature/filtro-por-rol \
  --title "feat: filtro de héroes por rol" \
  --body "Añade un selector de rol (tanque/daño/soporte) que filtra el roster en tiempo real."


# --- 5. Simular que main avanza mientras el PR está abierto ---

# vuelve a main para simular el trabajo de otro miembro del equipo
git switch main

# (otro compañero ha tocado, por ejemplo, la lista de héroes en data/heroes.ts)

# registra ese cambio urgente en main
git add .
git commit -m "arregla dato de partidas de Reinhardt"

# vuelve a tu rama de feature para integrar lo que acaba de llegar a main
git switch feature/filtro-por-rol


# --- 6. Integrar main en tu rama con merge ---

# trae los commits nuevos de main a tu rama creando un commit de merge
# esto preserva exactamente lo que pasó en cada rama, pero añade un commit extra
git merge main

# en este punto Git marca los ficheros con conflicto con los marcadores <<<< ==== >>>>
# abre cada fichero afectado, decide qué fragmento conservar (o combina ambos),
# y elimina los marcadores antes de continuar

# tras resolver los marcadores, marca el fichero como resuelto
git add src/data/heroes.ts

# cierra el merge con un commit que describe la resolución
git commit -m "merge main: resuelve conflicto en heroes.ts"


# --- 7. Actualizar la rama remota ---

# sube los nuevos commits (los tuyos más el de merge) a la rama del PR
git push


# El PR ahora muestra tu feature más los cambios de main integrados y sin conflicto.
# El revisor puede aprobar y hacer merge directamente desde GitHub.

# --- Límite de este tier ---
# La historia del repositorio acaba con un commit de merge extra en cada integración.
# En proyectos largos eso acumula ruido: cada vez que main avanza y hay que integrar,
# aparece un nuevo "merge main en feature/..." que no aporta lógica de negocio.
# El tier "mejor" resuelve esto con rebase, que reescribe tu trabajo encima de main
# y deja una historia completamente lineal.

Por qué este nivel

  • El ciclo base de trabajo en equipo: aislar en una rama, abrir el PR, absorber los cambios de main cuando hay colisión. El merge preserva lo que ocurrió en cada rama, pero añade un commit de fusión cada vez que hay que integrar.
  • Su límite: en proyectos activos, esos commits de merge se acumulan y ensucian el historial. `git log --oneline` mezcla tu trabajo con los "merge main en feature/..." que no aportan lógica de negocio. El tier "mejor" resuelve esto con rebase.