Git: remotos, push, pull, fetch y push rechazado
Remotos con un repo bare local: fetch vs pull, un push rechazado por non-fast-forward, upstream, pull.rebase y clones medidos: 10,936 KB vs 608 KB.
Serie: curso de Git 3/6 (intermedio). Para quien ya hace commits y crea ramas con soltura (clases 1–2) pero trata push y pull como magia. Vas a entender qué es origin/main, por qué se rechaza un push, en qué se diferencia fetch de pull y qué configuración de pull conviene elegir.
Esta clase es para quien acaba de ver su git push rechazado, para quien ejecutó git pull y obtuvo un merge commit que nadie quería, o para quien escuchó "nunca hagas force-push" sin que nadie le explicara por qué. Pasa en el primer proyecto en equipo del bootcamp, en el repositorio que compartes con el cliente como freelancer y, sobre todo, en la primera semana de un trabajo nuevo. Todos los ejemplos funcionan sin conexión: el "servidor" es un repositorio bare en una carpeta local, y dos clones, alice y bob, hacen de dos compañeros de equipo. Los mensajes son los mismos que ves con GitHub, GitLab o Bitbucket, solo que sin autenticación.
Ejecutado con Git 2.54.0 (Apple Git-157) en una Mac con Apple Silicon (M1 Pro), con una identidad de demostración y solo en carpetas de prueba. Las rutas locales de la salida están acortadas a ~/demo/….
Monta un remoto que puedas romper sin miedo
Un remoto es otro repositorio que tu repositorio conoce por un nombre; el nombre habitual es origin. Un repositorio bare no tiene working tree, solo el contenido de .git. Los servidores usan repositorios bare porque ahí nadie edita archivos.
git init --bare -b main remote.git
git clone remote.git alice
Initialized empty Git repository in ~/demo/rm3/remote.git/
Cloning into 'alice'...
warning: You appear to have cloned an empty repository.
done.
El warning es inofensivo. Alice hace commit y push:
cd alice
printf 'orders = []\n' > shop.py; git add shop.py; git commit -qm "Start shop"
git push
git branch -vv
To ~/demo/rm3/remote.git
* [new branch] main -> main
* main edd9011 [origin/main] Start shop
[origin/main] es el upstream (rama de seguimiento) de main: la rama remota con la que push, pull y status comparan por defecto. git clone lo configuró por nosotros.
¿Por qué no hacer push a un repositorio normal (no bare)? Lo intentamos, y el servidor se negó:
remote: error: refusing to update checked out branch: refs/heads/main
remote: error: By default, updating the current branch in a non-bare repository
remote: is denied, because it will make the index and work tree inconsistent
...
! [remote rejected] main -> main (branch is currently checked out)
El upstream y el primer push de una rama nueva
Una rama nueva no tiene upstream hasta que le das uno:
git switch -c feature/cart
# ... commit ...
git push
fatal: The current branch feature/cart has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin feature/cart
To have this happen automatically for branches without a tracking
upstream, see 'push.autoSetupRemote' in 'git help config'.
O haces lo que dice (git push -u origin feature/cart), o configuras push.autoSetupRemote=true una sola vez (Git 2.37+). Con esa configuración, el mismo git push imprimió:
* [new branch] feature/cart -> feature/cart
branch 'feature/cart' set up to track 'origin/feature/cart'.
Otro clásico: hacer push de un nombre de rama que no tienes. La mayoría de los tutoriales viejos dicen master, y los repositorios nuevos usan main:
$ git push origin master
error: src refspec master does not match any
error: failed to push some refs to '~/demo/rm3/remote.git'
"src refspec … does not match any" significa que no hay ninguna rama local con ese nombre. Compruébalo con git branch.
fetch contra pull: qué es realmente origin/main
origin/main es una rama de seguimiento remoto (remote-tracking branch): tu memoria local, de solo lectura, de dónde estaba main en origin la última vez que hablaste con él. Solo cambia cuando haces fetch (o pull, que primero hace fetch).
Alice hace push de un commit. Bob todavía no hizo fetch:
cd ../bob
git status
On branch main
Your branch is up to date with 'origin/main'.
Esta es la trampa. "Up to date" se mide contra el origin/main desactualizado de Bob, no contra el servidor. Después de un fetch:
git fetch
git status -sb
git log --oneline main..origin/main
From ~/demo/rm3/remote
edd9011..8500a27 main -> origin/main
## main...origin/main [behind 1]
8500a27 Add TAX
fetch descargó el commit y movió origin/main; el main de Bob y sus archivos no cambiaron. Puede revisar primero (git log main..origin/main, git diff main origin/main) y luego integrar con git merge, que aquí fue un fast-forward. git pull no es más que git fetch seguido de ese paso de integración.
| Quieres… | Comando | ¿Toca tu rama/archivos? |
|---|---|---|
| Ver lo que subieron tus compañeros, sin riesgo | git fetch |
No |
| Comparar antes de integrar | git log main..origin/main, git diff main origin/main |
No |
| Traer su trabajo a tu rama | git pull (fetch + merge/rebase) |
Sí |
| Aceptar solo un fast-forward | git pull --ff-only |
Sí, o falla |
| Ver qué tiene el servidor ahora mismo | git ls-remote origin |
No |
El push rechazado y cómo integrar
Ahora los dos compañeros trabajan al mismo tiempo. Alice agrega un costo de envío y hace push primero. Bob agrega config.py, hace commit y push:
$ git push
To ~/demo/rm3/remote.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to '~/demo/rm3/remote.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Git se niega porque aceptar el push de Bob haría que el main del servidor se saltara el commit de Alice. Bob ejecuta git pull sin ninguna configuración de pull:
From ~/demo/rm3/remote
8500a27..b2e3cbf main -> origin/main
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint: git config pull.rebase false # merge
hint: git config pull.rebase true # rebase
hint: git config pull.ff only # fast-forward only
...
fatal: Need to specify how to reconcile divergent branches.
Fíjate en las dos primeras líneas: el fetch ya ocurrió. Si Bob vuelve a hacer push ahora, el motivo cambia de (fetch first) a (non-fast-forward), porque Git ya conoce el commit remoto y ve que la rama de Bob está detrás. git status -sb mostró [ahead 1, behind 1].
Copiamos el repositorio de Bob dos veces y probamos las dos respuestas.
Merge (git pull --no-rebase):
* c70294d Merge branch 'main' of ~/demo/rm3/remote
|\
| * b2e3cbf Add shipping fee
* | a556d35 Add currency
|/
* 8500a27 Add TAX
Rebase (git pull --rebase):
Successfully rebased and updated refs/heads/main.
* 50a55ed Add currency
* b2e3cbf Add shipping fee
* 8500a27 Add TAX
* edd9011 Start shop
Con rebase, el commit de Bob se volvió a aplicar encima del de Alice y recibió un ID nuevo (a556d35 → 50a55ed); la historia queda en línea recta y el push pasó (b2e3cbf..50a55ed main -> main). La versión con merge también funciona, pero cada pull así agrega un commit "Merge branch 'main' of …" que no dice nada útil.
| Configuración | Qué hace git pull si hay divergencia |
Sirve para |
|---|---|---|
pull.rebase true |
vuelve a aplicar tus commits locales encima | la mayoría de los equipos; historia limpia del trabajo no publicado |
pull.rebase false |
crea un merge commit | cuando los commits locales ya se compartieron en otro lado |
pull.ff only |
se niega salvo que sea fast-forward | ramas en las que nunca haces commit (p. ej. un checkout de deploy) |
| sin configurar (2.54.0) | fatal: Need to specify how to reconcile divergent branches. |
obligarte a elegir |
El rebase reescribe solo tus commits que todavía no subiste, lo cual es seguro. La clase 4 trata el rebase a fondo y explica por qué nunca debes hacer rebase de commits que otros ya tienen.
Error común: "hazle force-push y listo"
Qué pasa en realidad. El clon de Alice todavía cree que origin/main es b2e3cbf (no hizo fetch del push de Bob). Ella corrige su último commit con amend y hace push. Un push normal se rechaza, así que recurre a la fuerza.
Con --force-with-lease, Git primero comprueba que el servidor todavía tenga lo que Alice vio por última vez:
$ git push --force-with-lease
! [rejected] main -> main (stale info)
error: failed to push some refs to '~/demo/rm3/remote.git'
Con --force a secas, no hay ninguna comprobación:
$ git push --force
+ 50a55ed...56ed907 main -> main (forced update)
El commit 50a55ed de Bob desapareció del main del servidor. La próxima vez que Bob hace pull ve + 50a55ed...56ed907 main -> origin/main (forced update) seguido del error de ramas divergentes, y su trabajo sobrevive solo en su propio clon. (Después restauramos el servidor desde una copia.)
Solución: nunca hagas force-push en una rama compartida. En tu propia rama de feature, cuando tengas que reescribir (después de un rebase), usa git push --force-with-lease. Si haces git fetch justo antes, revisa primero lo que llegó (git log HEAD..origin/<branch>): el fetch actualiza el valor que comprueba el lease, así que ya no te protegería.
Medición: clones completos, superficiales y sin blobs
Los remotos también tienen que ver con cuánto descargas. Generamos un repositorio con 2,000 commits de un archivo CSV que crece 20 filas por commit (semilla fija 42; archivo final de 40,000 filas y 984,486 bytes; pack del servidor de 10.55 MiB después de git gc). Lo clonamos por file:// (que obliga a una transferencia real del pack en vez de hard links), 3 ejecuciones cada uno, Mac con Apple Silicon (M1 Pro), Git 2.54.0:
| Clon | Ejecuciones (ms) | Tamaño de .git |
Commits en la historia |
|---|---|---|---|
| completo | 1169.4 / 1124.1 / 1221.5 | 10,936 KB | 2,000 |
--depth 1 (superficial) |
128.1 / 129.6 / 140.9 | 608 KB | 1 |
--filter=blob:none (sin blobs) |
180.9 / 190.7 / 184.8 | 1,108 KB | 2,000 |
Los clones pequeños no salen gratis más adelante:
- En el clon sin blobs,
git log -p -50tardó 6,792.8 ms la primera vez, porque Git tuvo que descargar bajo demanda 50 versiones viejas del archivo, y.gitcreció a 25,644 KB. El mismo comando tardó 300.3 ms la segunda vez. - En el clon superficial,
git log --onelinemostró un solo commit ygit rev-parse --is-shallow-repositoryimprimiótrue.git fetch --unshallowtardó 1,241.9 ms y llevó.gita 12,104 KB, más o menos el costo del clon completo. - Un
git fetchsin nada nuevo tardó una mediana de 41.5 ms en 5 ejecuciones.
Regla práctica: jobs de CI que compilan un solo commit → --depth 1. Desarrolladores que necesitan la historia pero no cada archivo viejo → sin blobs. Tu clon de trabajo diario → completo.
Límites honestos y notas de versión
- Todo se ejecutó con Git 2.54.0 contra repositorios bare locales. Los servicios en la nube agregan autenticación, protección de ramas y sus propios mensajes de rechazo (por ejemplo, una rama protegida que rechaza un force-push); eso no se ejecutó aquí.
push.autoSetupRemotese agregó en Git 2.37. El error de ramas divergentes pasó a ser un error fatal en la época de 2.33/2.34 (las versiones anteriores solo avisaban y hacían merge); solo comprobamos la redacción de 2.54.0.--force-with-leaseexiste desde Git 1.8.5. Sin un valor esperado explícito, compara contra tuorigin/main; ejecutar antesgit fetchactualiza ese valor y debilita la protección.- El clon parcial (
--filter) necesita soporte del servidor; para el repo bare local configuramosuploadpack.allowFilter true. Los tiempos de clon porfile://no incluyen la latencia de red, así que en el mundo real las diferencias suelen ser mayores.
Ejercicios
git statusdice "Your branch is up to date with 'origin/main'", pero un compañero dice que hizo push hace 10 minutos. ¿Qué comando muestra sus commits sin cambiar tu rama?- Tu push se rechazó con
(non-fast-forward). Tienes dos commits locales que nunca subiste. ¿Qué comando integra el trabajo remoto manteniendo una historia en línea recta?
Respuestas
git fetchy luegogit log --oneline main..origin/main. Status solo compara contra elorigin/mainde tu último fetch.git pull --rebase(o configura una vezgit config pull.rebase true) y luegogit push. Tus dos commits reciben IDs nuevos, lo cual no es problema porque nadie más los tiene.
Anterior: Curso de Git 2: ramas, merge y conflictos · Siguiente: Curso de Git 4: rebase, cherry-pick e historia · Inicio de la serie: Curso de Git 1 · Vuelve a la ruta de estudio para tu primer trabajo.
Preguntas frecuentes
¿Qué diferencia hay entre git fetch y git pull?
git fetch descarga los commits nuevos y actualiza las ramas de seguimiento remoto como origin/main, pero nunca toca tu rama ni tus archivos. git pull ejecuta un fetch y luego integra origin/main en tu rama, con merge o con rebase. En nuestra prueba, git status decía 'up to date' antes del fetch aunque el remoto tenía un commit nuevo; después del fetch decía 'behind 1'.
¿Por qué Git rechazó mi push con 'fetch first' o 'non-fast-forward'?
La rama remota tiene commits que tú no tienes, así que aceptar tu push los tiraría a la basura. Integra primero con git pull --rebase (o --no-rebase para un merge) y vuelve a hacer push. No uses --force en una rama compartida; --force-with-lease rechazó nuestra sobrescritura con '(stale info)' porque el remoto se había movido.
¿Qué significa 'fatal: Need to specify how to reconcile divergent branches'?
Tu rama y la rama remota tienen commits nuevos, y ninguna configuración pull.rebase o pull.ff le dice a Git si hacer merge o rebase. Elige uno por comando (git pull --rebase, --no-rebase, --ff-only) o configúralo una vez, por ejemplo git config pull.rebase true. Cuando apareció este error, la parte del fetch ya se había ejecutado.