How to
- Introducción
- Trastear con repositorios remotos
- Problemas al subir una rama
- Modificar algo en el repositorio remoto
- Git pull, ese comando problemático
- Renombrar una rama
- Mantener el entorno limpio: cuando las ramas no dejan ver el bosque
- Modificar un tag
Introducción
Un repositorio de código, dentro del contexto de los sistema de control de versiones, es un sistema para almacenar código y metadatos, como pueden ser el histórico de los archivos.
Git es un sistema de control de versiones distribuido, en el que cada repositorio guarda una copia completa del código y los metadatos, pudiendo funcionar de manera independiente; en contraposición están los sistemas centralizados, en los que existe un único repositorio de código y los clientes se comunican con él para obtener o subir código, pero en ningún caso tienen una copia de los metadatos.
Se denomina "flujo de trabajo" al cómo colaboran entre sí los repositorios de un sistema de control de versiones distribuido. El más común de ellos es el "flujo de trabajo centralizado", en el que hay un repositorio central o de referencia, por ejemplo un repositorio en Bitbucket o GitHub; los desarrolladores se clonan ese repositorio en su local, obteniendo así un repositorio privado sobre el que pueden trabajar; hacen cambios en su repositorio local, como por ejemplo desarrollador una nueva feature, y envian esos cambios al repositorio central; y actualizan el repositorio privado con los cambios que otros desarrolladores han enviado al repositorio central.
El tener un "sistema de control de versiones distribuido" y acabar usando un "flujo de trabajo centralizado" puede sonar contradictorio o exagerado ¿Por qué complicarse? ¿Por qué no usar simplemente un sistema de control de versiones centralizado como SVN?
En un sistema de control de versiones centralizado cualquier operación, desde hacer un nuevo commit a consultar el log, implica comunicaciones con el servidor donde está alojado el repositorio, lo cual es mucho más lento que consultando una copia local como en Git; además de la lentitud, una perdida de conexión con el servidor central implica no poder ejecutar ningún comando; y la gestión de conflictos es más complicada, ya que habrá que resolverlos sobre cambios que todavía no se han podido commitear.
Personalmente he trabajado con SVN y Git y me quedo sin ninguna duda con Git, pero como todo la elección dependerá de gustos y necesidades.
Adicionalmente, un sistema de control de versiones distribuido permite evolucionar el flujo de trabajo dependiendo de como evolucione o crezca el proyecto; por poner un ejemplo, en el kernel de Linux usan el "flujo de trabajo dictador y tenientes".
Tras hacer git clone <url repositorio>
en el repositorio recién clonado habrá dos conjuntos de ramas: ramas locales y ramas remotas,
En un GUI | En consola |
---|---|
Las ramas remotas son referencias a ramas que existen en el repositorio remoto que se ha clonado, al que por defecto se le da el nombre de origin
, y que se actualizan con git fetch
si ha habido cambios en el repositorio remoto, o cuando este acepta los cambios que se le proponen al hacer git push
.
Un ejemplo de rama remota sería origin/master
, y haría referencia a la rama master
en el repositorio remoto con nombre origin
.
Una rama remota puede ser el "upstream" de una rama local, pudiéndose entender "lo que hay rio arriba" como que la rama local mira o sigue a la rama remota: cuando se hace git pull
se intenta actualizar la rama local con lo que haya en su upstream; cuando se hace git push
se intenta actualizar el upstream con los contenidos de la rama local. Y en ambos casos digo intentar porqué si la rama local ha divergido respecto del upstream, sera necesarias algunas acciones que se irán viendo a lo largo del post.
A la hora de visualizarlo, se puede hacer igual que con las ramas locales, solo que ahora habrá unas cuantas ramas más.
Con un GUI
O por consola
$ git log --graph --format='%C(auto)%h %s%n%C(auto)%d%n' --all
Las ramas de los repositorios remotos añaden un nuevo nivel de complejidad a la gestión de las ramas: hay ramas como pippin-merry
que han adelantado a su upstream origin/pippin-merry
; ramas como aragorn-legolas-gimli
que se han quedado atrás respecto a su upstream origin/pippin-merry
; y ramas como master
que han divergido respecto a su upstream origin/master
; también habrá ramas remotas que no serán seguidas por ninguna rama local o ramas locales que todavía no se han subido al repositorio remoto y que no tengan su equivalente en origin
.
Y para terminar la introducción, algo muy importante:
🚨 No hay que editar nada que ya este en el repositorio remoto y que este siendo usado por otros desarrolladores, ya que se puede liar parda.
Imagina que:
- Mortadelo hace
git push
amaster
con un commit que tiene un bug. - Filemón se actualiza su rama con
master
, obteniendo así el commit con el bug. - Mortadelo se da cuenta de su error, y sin avisar a nadie: borra el commit con el bug de
master
; intenta hacergit push
amaster
, pero no puede por no se que error extraño; así que hacegit push -f
. - Filemón integra su rama en
master
, intenta subirla, y o bien vuelve a añadir amaster
el commit que Mortadelo había borrado, o bien se encuentra con conflictos y tiene que perder un buen rato en resolverlos.
El tener que usar git push -f
(o borrar la rama remota y volver a crearla con commits diferentes) es algo que debe hacer saltar todas las alarmas, y hacer pensar muy mucho si realmente es necesario o hay otras opciones.
Dicho esto, si la rama es tuya, y estas seguro de que nadie más la ha usado ni ha usado ninguno de sus commits, es licito modificar los commits que hagan falta y usar git push -f
, porque a todos nos ha pasado que queramos cambiar algo que hemos subido por error.
git push -f
es una herramienta más de Git, y si sigue hay después de tantos años por algo será, pero hay que usarla con mucho cuidado.
Trastear con repositorios remotos
Personalmente, como más aprendo sobre las cosas es trasteando con ellas, y cómo más aprendí sobre los repositorios remotos de Git fue creándome un repositorio en local y clonándolo.
En un minuto es posibles tener un área de pruebas donde ver como afectan los comando git push
y git pull
al repositorio remoto, crear situaciones para probar las cosas, probar cosas antes de aplicarlas al repositorio remoto de verdad, etc.
También se puede crear un repositorio privado en GitHub o similares, pero me parece mucho más rápido y ágil probar en local.
El proceso sería más o menos así
# Creo una carpeta donde poner los repositorios para las pruebas.
~$ mkdir -p sandbox/remotes
~$ cd sandbox/remotes/
# Creo una carpeta donde creare el repositorio remoto.
~/sandbox/remotes$ mkdir origin
~/sandbox/remotes$ cd origin/
# Inicio el repositorio.
~/sandbox/remotes/origin$ git init
Inicializado repositorio Git vacío en ~/sandbox/remotes/origin/.git/
# Le añado algún commit.
~/sandbox/remotes/origin$ echo "hola git" > README.md
~/sandbox/remotes/origin$ git add .
~/sandbox/remotes/origin$ git commit -am "creado archivo index.html"
[master (commit-raíz) 246ceaf] creado archivo README.md
1 file changed, 1 insertion(+)
create mode 100644 README.md
# Me voy a otra rama para evitar los problemas del tipo
# "remote: error: Por defecto, actualizar la rama actual
# en un repositorio no vacío".
# https://stackoverflow.com/a/2933656/1587302
~/sandbox/remotes/origin$ git checkout -b no-editar
# Voy atras.
~/sandbox/remotes/origin$ cd ..
# Clono el repositorio origin en la carpeta local.
~/sandbox/remotes$ git clone origin local
Clonando en 'local'...
hecho.
# Entro en él y veo que tengo un clone de "origin".
~/sandbox/remotes$ cd local/
~/sandbox/remotes$ git checkout master
Rama 'master' configurada para hacer seguimiento a la rama remota 'master' de 'origin'.
Cambiado a nueva rama 'master'
~/sandbox/remotes/local$ git status
En la rama master
Tu rama está actualizada con 'origin/master'.
nada para hacer commit, el árbol de trabajo está limpio
~/sandbox/remotes/local$ cat README.md
hola git
Problemas al subir una rama
Hago push pero lo cambios no se suben; al intentar hacer push sale un error; hago push pero no pasa nada; etc
Hay varios motivos por los que git push
puede dar error:
- No se tienen permisos para escribir es un repositorio.
Por poner un ejemplo, es posible clonarse cualquier repositorio público de GitHub, pero al hacer
git push
se obtendrá un error como esteremote: Permission to 30-seconds/30-seconds-of-code.git denied to Pepito.
- No se tienen permisos para escribir en una rama.
Es posible restringir los permisos de escritura a ramas sensibles como
master
, de tal forma que sólo ciertos usuarios puedan escribir sobre ellas o solo se pueda escribir sobre ellas al darle a Merge en las "Pull request" o "Merge request". - Todavía no existe la rama remota.
- La rama local está detrás o no está al día con la rama remota
- La rama local ha divergido respecto a la rama remota y no es posible actualizarla.
Para los dos primeros casos habrá que consultar al administrador del repositorio.
En el tercer caso, git push
da error porque se le pide actualizar algo que no existe, y la solución es tan simple como crearla siguiendo las instrucciones que da Git en el mensaje de error.
La siguientes veces que se ejecute git push
ya no se obtendrá el error, porque ya sabrá que debe actualizar.
Aunque esto es algo que solo suelen experimentar los usuarios de consola, porque muchos muchos GUI por debajo controlan este caso y crean la rama remata si no existe.
# Intento hacer push de una rama que no existe y obtengo un error.
$ git push
fatal: La rama actual nueva-feature no tiene una rama upstream.
Para realizar un push de la rama actual y configurar el remoto como upstream, use
git push --set-upstream origin nueva-feature
# Así que la creo.
$ git push --set-upstream origin HEAD
Total 0 (delta 0), reusado 0 (delta 0)
To ~/sandbox/remotes/origin
* [new branch] HEAD -> nueva-feature
Rama 'nueva-feature' configurada para hacer seguimiento a la rama remota 'nueva-feature' de 'origin'.
$ git status
En la rama nueva-feature
Tu rama está actualizada con 'origin/nueva-feature'.
nada para hacer commit, el árbol de trabajo está limpio
En el ejemplo se utiliza git push --set-upstream origin HEAD
para crear la rama en lugar del comando sugerido por Git git push --set-upstream origin nueva-feature
.
Es un pequeño atajo no tener que pensar en el nombre de la rama a la hora de crearla.
Respecto a los dos últimos casos, si git push
da error, es importante hacer git fetch
para actualizar las ramas remotas, porque puede pasar que todo parezca estar en orden al ejecutar git status
, pero que no lo este porque otro desarrollador haya hecho cambios en el repositorio remoto desde la última vez se actualizo el repositorio local, y las referencias a las ramas remotas no estén actualizadas.
Si la rama local está detrás de la rama remota no hay cambios que subir, y aunque el mensaje puede ser un poco agresivo es normal que no pase nada.
Si se actualiza la rama seguirá sin pasar nada, pero el mensaje no será tan agresivo.
# Hago push y obtengo un mensaje un poco agresivo
$ git push
To ~/sandbox/remotes/origin
! [rejected] master -> master (non-fast-forward)
error: falló el push de algunas referencias a '~/sandbox/remotes/origin'
ayuda: Actualizaciones fueron rechazadas porque la punta de tu rama actual está
ayuda: detrás de su contraparte remota. Integra los cambios remotos (es decir
ayuda: 'git pull ...') antes de hacer push de nuevo.
ayuda: Mira 'Note about fast-forwards' en 'git push --help' para más detalles.
# Compruebo el estado y veo que mi rama se ha quedado detras
$ git status
En la rama master
Tu rama está detrás de 'origin/master' por 1 commit, y puede ser avanzada rápido.
(usa "git pull" para actualizar tu rama local)
nada para hacer commit, el árbol de trabajo está limpio
# Actualizo la rama
$ git pull
Actualizando 246ceaf..47be068
Fast-forward
index.html | 1 +
1 file changed, 1 insertion(+)
# Y el git push segirá sin hacer nada, aunque el mensaje
# no es tan agresivo
$ git push
Everything up-to-date
Y si la rama remota y la rama local han evolucionado de manera distinta, es que han divergido.
# Compruebo el estado de la rama y veo que está por delante
# de la rama remota, así que debería poder hacer push.
$ git status
En la rama master
Tu rama está adelantada a 'origin/master' por 1 commit.
(usa "git push" para publicar tus commits locales)
nada para hacer commit, el árbol de trabajo está limpio
# Pero falla.
$ git push
To sandbox/remotes/origin
! [rejected] master -> master (fetch first)
error: falló el push de algunas referencias a 'sandbox/remotes/origin'
ayuda: Actualizaciones fueron rechazadas porque el remoto contiene trabajo que
ayuda: no existe localmente. Esto es causado usualmente por otro repositorio
ayuda: realizando push a la misma ref. Quizás quiera integrar primero los cambios
ayuda: remotos (ej. 'git pull ...') antes de volver a hacer push.
ayuda: Vea 'Notes about fast-forwards0 en git push --help' para detalles.
# Actualizo las ramas remotas y veo que la rama remota
# se ha actualizado.
$ git fetch
remote: Enumerando objetos: 5, listo.
remote: Contando objetos: 100% (5/5), listo.
remote: Total 3 (delta 0), reusado 0 (delta 0)
Desempaquetando objetos: 100% (3/3), 277 bytes | 277.00 KiB/s, listo.
Desde sandbox/remotes/origin
246ceaf..792a7bf master -> origin/master
# Intento obtener información más concreta y veo que
# las ramas han divergido.
$ git status
En la rama master
Tu rama y 'origin/master' han divergido,
y tienen 1 y 1 commits diferentes cada una respectivamente.
(usa "git pull" para fusionar la rama remota en la tuya)
nada para hacer commit, el árbol de trabajo está limpio
En el ejemplo se puede ver que la rama local master
y la rama remota origin/master
partieron de commit creado archivo index.html
y que en ambas se han hecho commits.
git push
solo funciona cuando los commits que se suben salen del último commit de la rama remota, es decir, se produce un fast forward, en caso contrario falla y habrá que actualizar la rama local con el upstream.
La forma de salir del paso depende de como se integren las ramas en el proyecto:
- Si se usan rebases, el comando sería
git rebase <upstream>
, por ejemplogit rebase origin/master
si por ejemplo mi el upstream esorigin/master
. - Si se usan merges
git pull
.
Una vez integrada, y si no se da la casualidad de que justo se ha vuelto a actualizar la rama remota entre medias, se podrá hacer git push
.
Modificar algo en el repositorio remoto
He visto después de hacer push que el mensaje de un commit tiene una errata; he mergeado la rama que no era y he hecho push; he subido algo por error; me acabo de dar cuenta de que he subido un bug; etc.
Como se vio en la introducción
🚨 No hay que editar nada que ya este en el repositorio remoto y que este siendo usado por otros desarrolladores, ya que se puede liar parda.
Pero ¿quién no la ha cometido errores alguna vez?
Como también se dijo en la introducción, si la rama es tuya, y estas seguro de que nadie más la ha usado ni ha usado ninguno de sus commits, es licito modificar los commits que hagan falta y usar git push -f
$ git commit -am "Hañado titúlo"
[mi-rama 13c9acf] Hañado titúlo
1 file changed, 1 insertion(+)
$ git push
Enumerando objetos: 5, listo.
Contando objetos: 100% (5/5), listo.
Compresión delta usando hasta 12 hilos
Comprimiendo objetos: 100% (2/2), listo.
Escribiendo objetos: 100% (3/3), 314 bytes | 314.00 KiB/s, listo.
Total 3 (delta 0), reusado 0 (delta 0)
To sandbox/remotes/origin
207ba5a..13c9acf mi-rama -> mi-rama
# Me doy cuenta de que he metido algunos errores en el mensaje
# y los corrijo.
$ git amend -m "Añado título"
[mi-rama ec27a36] Añado título
Date: Sat May 8 21:05:27 2021 +0200
1 file changed, 1 insertion(+)
# Pero el push falla porque he modificado un commit que
# ya estaba en el repositorio remoto.
$ git push
To sandbox/remotes/origin
! [rejected] mi-rama -> mi-rama (non-fast-forward)
error: falló el push de algunas referencias a 'sandbox/remotes/origin'
ayuda: Actualizaciones fueron rechazadas porque la punta de tu rama actual está
ayuda: detrás de su contraparte remota. Integra los cambios remotos (es decir
ayuda: 'git pull ...') antes de hacer push de nuevo.
ayuda: Mira 'Note about fast-forwards' en 'git push --help' para más detalles.
# Compruebo el estado.
$ git status
En la rama mi-rama
Tu rama y 'origin/mi-rama' han divergido,
y tienen 1 y 1 commits diferentes cada una respectivamente.
(usa "git pull" para fusionar la rama remota en la tuya)
nada para hacer commit, el árbol de trabajo está limpio
# Estando muy muy seguro de que no va a afectar a otras personas.
$ git push -f
Enumerando objetos: 5, listo.
Contando objetos: 100% (5/5), listo.
Compresión delta usando hasta 12 hilos
Comprimiendo objetos: 100% (2/2), listo.
Escribiendo objetos: 100% (3/3), 318 bytes | 318.00 KiB/s, listo.
Total 3 (delta 0), reusado 0 (delta 0)
To ~/sandbox/remotes/origin
+ 13c9acf...ec27a36 mi-rama -> mi-rama (forced update)
$ git status
En la rama mi-rama
Tu rama está actualizada con 'origin/mi-rama'.
nada para hacer commit, el árbol de trabajo está limpio
Si el git push
se hizo a una rama compartida, las acciones a tomar dependen de la situación:
- Si los commits son para corregir una errata en un mensaje, lo mejor es dejarlo correr, porque arreglarlo no merece la pena.
- Si es algo que hay que deshacer, lo más correcto sería usar
git revert
para revertir un commit individual o un merge commit, probar bien los cambios, y después hacergit push
. - Si es algo como un bug, siempre se pueden hacer commits para corregirlo.
Aunque lo más correcto no siempre es lo más cómodo, para que mentir.
Si se ha hecho git push
a una rama compartida con otros desarrolladores y ninguno ha usado ninguno de los commits que se han subido (no han hecho git pull
en la rama comprometida, ni git pull origin <rama>
, ni git cherry-pick
de ninguno de los commits) se puede:
- Avisar al resto de desarrolladores de que se ha subido algo por error y que hasta nuevo aviso no hagan ni
git pull
en la rama,git pull origin <rama>
nigit cherry-pick
de los commits recién subidos. - Hacer lo cambios que se tengan que hacer y hacer
git push -f
. - Pedir al resto de desarrolladores que se actualicen las ramas remotas con
git fetch
y decir que ya se puede trabajar con normalidad.
¿Y si algún otro desarrollador ya ha hecho uso de alguno de los commits subidos?
Siempre es posible seguir los pasos de arriba y que el resto de desarrolladores rehagan parte del trabajo, pero igual es demasiado trastorno y no merece la pena.
¿Y si se ha modificado algún commit en local y se decide que lo mejor es dar marcha atrás porque no se puede hacer git push -f
?
Se puede volver a lo que había antes de modificar el commit siguiendo los mismos pasos que para recuperar una rama borrada por error.
Git pull, ese comando problemático
"git pull" cada vez hace una cosa distinta; después de hacer "git pull" la aplicación ya no funciona.
git pull
sirve para actualizar una rama local con una rama del repositorio remoto.
Por debajo primero hace git fetch
, actualizando todas las ramas del repositorio remoto, y después a git merge <rama-remota>
.
Ciertamente es muy cómodo, pero hay que tener todas las consideraciones que al mergear una rama (pueden surgir conflictos, hay revisar que todo sigue funcionando después de un merge commit, etc) y alguna más.
La forma más verbosa de invocar a git pull
sería git pull origin <rama>
, que por debajo vendría a hacer:
-
git fetch origin
, que actualizaría todas las ramas remotas (las ramasorigin/<rama>
que se mencionaban en la introducción). -
git merge origin/<rama>
para mergear la rama remotaorigin/<rama>
en la rama local.
Es una forma muy útil para por ejemplo actualizar la rama local en la que se está desarrollando una nueva feature con la última versión de master
en el repositorio remoto.
$ git status
En la rama mi-feature
nada para hacer commit, el árbol de trabajo está limpio
# Actualizo mi-feature con la rama master del repositorio remoto.
$ git pull origin master
# Internamente hace git fetch.
remote: Enumerando objetos: 5, listo.
remote: Contando objetos: 100% (5/5), listo.
remote: Comprimiendo objetos: 100% (2/2), listo.
remote: Total 3 (delta 0), reusado 0 (delta 0)
Desempaquetando objetos: 100% (3/3), 291 bytes | 291.00 KiB/s, listo.
# La rama master ha cambiado en el repositorio remoto
# así que actualiza origin/master.
Desde ~/sandbox/remotes/origin
* branch master -> FETCH_HEAD
792a7bf..1a16ce3 master -> origin/master
# Hace el merge.
Merge made by the 'recursive' strategy.
index.html | 2 ++
1 file changed, 2 insertions(+)
# Reviso la el úlitmo commit para ver que ha sido un merge.
$ git log -1
commit 73365ef70d893c3b3b9680e0676bda0c78c36505 (HEAD -> mi-feature)
Merge: 504b167 1a16ce3
Merge branch 'master' of ~/sandbox/remotes/origin into mi-feature
Otra forma es git pull
a secas, bastante útil para actualizar la rama local con el upstream.
Por debajo vendría a hacer:
-
git fetch <remoto>
, donde<remoto>
sería el repositorio remoto del upstream (si el upstream fueseorigin/master
haríagit fetch origin
). -
git merge <upstream>
, para mergear el upstream en la rama local (si el upstream fueseorigin/master
haríagit merge master
). Si la rama no tuviese upstream indicaría como añadirlo.
# Hago git status y veo que no estoy siguiendo
# a ninguna rama remota.
$ git status
En la rama mi-feature
nada para hacer commit, el árbol de trabajo está limpio
# git pull hace internamente el git fetch, y luego falla,
# porque la rama actual no sigue a ninguna rama remota
$ git pull
remote: Enumerando objetos: 5, listo.
remote: Contando objetos: 100% (5/5), listo.
remote: Comprimiendo objetos: 100% (2/2), listo.
remote: Total 3 (delta 0), reusado 0 (delta 0)
Desempaquetando objetos: 100% (3/3), 290 bytes | 290.00 KiB/s, listo.
Desde ~/sandbox/remotes/origin
792a7bf..01d5c54 master -> origin/master
No hay información de rastreo para la rama actual.
Por favor especifica a qué rama quieres fusionar.
Ver git-pull(1) para detalles.
git pull <remoto> <rama>
Si deseas configurar el rastreo de información para esta rama, puedes hacerlo con:
git branch --set-upstream-to=origin/<rama> mi-feature
# Cambio a otra rama que si sigue a una rama remota.
$ git checkout master
Cambiado a rama 'master'
Tu rama está detrás de 'origin/master' por 1 commit, y puede ser avanzada rápido.
(usa "git pull" para actualizar tu rama local)
# Hago git pull.
# Internamente hace el git fecth, pero como no hay cambios
# en el repositorio remoto no imprime nada.
$ git pull
Actualizando 792a7bf..01d5c54
Fast-forward
index.html | 1 +
1 file changed, 1 insertion(+)
Todas las opciones que se pueden aplicar a git merge
se pueden aplicar también a git pull
.
Si por ejemplo se está en la rama master
y se quiere actualizar con el repositorio remoto puede ser útil ejecutar git pull --ff-only
; haciéndolo así git pull
fallaría si las ramas han divergido, evitando así que la rama master
se diferente a la de otros desarrolladores.
Se puede ejecutar git pull --no-ff --no-commit origin hotfix
para que si el merge resultase en un merge commit tener la posibilidad de revisar los cambios antes de completar el commit, por si hubiese alguna incompatibilidad entre las dos ramas.
Renombrar una rama
Corregir un texto incorrecto, se copio la clave del issute tracker que no era, etc.
Las acciones para renombrar ramas solo renombran ramas locales, no actúan sobre los upstream; como tal no existe la opción de renombrar una rama en un repositorio remoto, aunque si se puede borrar la rama con el nombre antiguo y crear una rama con el nombre nuevo. Adicionalmente habría que actualizar el upstream de la rama, porque no se actualiza solo.
# Hago un git status y veo que estoy en la rama dovolop
# y que estoy siguiendo a la rama remota origin/dovolop.
$ git status
En la rama dovolop
Tu rama está actualizada con 'origin/dovolop'.
nada para hacer commit, el árbol de trabajo está limpio
# Otra opción para ver esto es mirarlo es
$ git branch -vv -a
* dovolop d63cc7b [origin/dovolop] crear hoja de estilos home.css
master d63cc7b [origin/master] crear hoja de estilos home.css
remotes/origin/dovolop d63cc7b crear hoja de estilos home.css
remotes/origin/master d63cc7b crear hoja de estilos home.css
# El nombre debería ser develop, así que renombro la rama.
$ git branch -m develop
# Si verifico el estado veo que he renombrado la rama local,
# pero no la remota, y que mi rama local todavía sigue
# a origin/dovolop.
$ git branch -a -vv
* develop d63cc7b [origin/dovolop] crear hoja de estilos home.css
master d63cc7b [origin/master] crear hoja de estilos home.css
remotes/origin/dovolop d63cc7b crear hoja de estilos home.css
remotes/origin/master d63cc7b crear hoja de estilos home.css
# "Actualizo" la rama remota borrando la vieja y creando la nueva.
$ git push origin :dovolop develop
Total 0 (delta 0), reusado 0 (delta 0)
To ~/sandbox/git
- [deleted] dovolop
* [new branch] develop -> develop
# Si consulto el estado veo que la rama remota se ha renombrado
# y que mi rama local sigue a una rama "desaparecida" o "gone".
$ git branch -a -vv
* develop d63cc7b [origin/dovolop: desaparecido] crear hoja de estilos home.css
master d63cc7b [origin/master] crear hoja de estilos home.css
remotes/origin/develop d63cc7b crear hoja de estilos home.css
remotes/origin/master d63cc7b crear hoja de estilos home.css
# Así que tengo que actualizar el upstream para que siga
# a la rama renombrada.
$ git push --set-upstream origin develop
Rama 'develop' configurada para hacer seguimiento a la rama remota 'develop' de 'origin'.
Everything up-to-date
# Y ya está todo en orden.
$ git branch -a -vv
* develop d63cc7b [origin/develop] crear hoja de estilos home.css
master d63cc7b [origin/master] crear hoja de estilos home.css
remotes/origin/develop d63cc7b crear hoja de estilos home.css
remotes/origin/master d63cc7b crear hoja de estilos home.css
🚨 Los comandos anteriores pueden afectar a otros desarrolladores.
Si el Filemón tenía una rama siguiendo a dovolop
y Mortadelo la "renombra" en el repositorio remoto, pueden ocurrir varias cosas: que Filemón no sea capaz de actualizarse con los commits que haga Mortadelo, ya que ha perdido la referencia la rama remota; o que Filemón haga nuevos commits y al subirlos al repositorio remoto cree una nueva sin darse cuenta.
# Mortadelo comprueba el estado de las ramas.
$ git branch -vv -a
* dovolop d63cc7b [origin/dovolop] crear hoja de estilos home.css
master d63cc7b [origin/master] crear hoja de estilos home.css
remotes/origin/dovolop d63cc7b crear hoja de estilos home.css
remotes/origin/master d63cc7b crear hoja de estilos home.css
# Hace un fetch y no le presta mucha antención, porque
# no es extraño que se creen o borren ramas.
$ git fetch
Desde ~/sandbox/git
- [eliminado] (nada) -> origin/dovolop
* [nueva rama] develop -> origin/develop
# Si Filemón comprobase el estado de la rama vería algo raro,
# pero supongamos que no lo hace.
$ git branch -vv -a
* dovolop d63cc7b [origin/dovolop: desaparecido] crear hoja de estilos home.css
feature d63cc7b [origin/feature: desaparecido] crear hoja de estilos home.css
master d63cc7b [origin/master] crear hoja de estilos home.css
remotes/origin/develop d63cc7b crear hoja de estilos home.css
remotes/origin/master d63cc7b crear hoja de estilos home.css
# Hace algunos cambios y los commitea.
$ git commit -am "algunos cambios"
[dovolop 190e822] algunos cambios
1 file changed, 1 insertion(+)
# Sube el nuevo commit al repositorio remoto...
# y ve que se ha creado una nueva rama, aunque el esperaba
# que se hubiese actualizado la rama dovolop
$ git push
Enumerando objetos: 5, listo.
Contando objetos: 100% (5/5), listo.
Compresión delta usando hasta 12 hilos
Comprimiendo objetos: 100% (3/3), listo.
Escribiendo objetos: 100% (3/3), 305 bytes | 305.00 KiB/s, listo.
Total 3 (delta 1), reusado 0 (delta 0)
To ~/sandbox/git
* [new branch] dovolop -> dovolop
# Y sin darse cuenta ha recreado la rama que mi Mortadelo
# con tanto esfuerzo renombro.
$ git branch -a -vv
* dovolop 190e822 [origin/dovolop] algunos cambios
master d63cc7b [origin/master] crear hoja de estilos home.css
remotes/origin/develop d63cc7b crear hoja de estilos home.css
remotes/origin/dovolop 190e822 algunos cambios
remotes/origin/master d63cc7b crear hoja de estilos home.css
Mantener el entorno limpio: cuando las ramas no dejan ver el bosque
"git branch" y tengo muchas ramas que ya no se usan; "git branch -a " muestra ramas del repositorio remoto que ya fueron borradas; "git status" dice que el upstream ha desaparecido o "is gone".
En repositorios muy activos y con muchos desarrolladores es fácil que git branch
y git brach -a
devuelvan decenas de resultados, de los cuales la inmensa mayoría serán ramas que no se han usado en meses.
Git nunca borra nada a menos que se le diga explícitamente, lo cual esta bien porque nunca se pierde información, pero tiene la contra de que se puede acabar con información que ya no es relevante y que dificulta encontrar lo que se esta buscando, como cuando se tiene el escritorio tan lleno de archivos, carpetas y enlaces directos que lleva un rato encontrar algo que no uses todos los días.
Ramas del repositorio remoto que ya no existen
Las más fáciles de limpiar son las ramas remotas que ya no existen en el repositorio remoto, es decir aquellas que cuando:
- Pin subió la rama
feature/tiger
al repositorio remeto. - Pon hizo un
git fetch
, el cual creo la ramaorigin/feature/tiger
. Si ejecutase hiciesegit branch -a
mostraría, entre otras,origin/feature/tiger
- Pin borro
feature/tiger
. - Pon hizo
git fetch
, que no cambio nada. Si ejecutase hiciesegit branch -a
seguiría mostrandoorigin/feature/tiger
.
Se le puede indicar a Git que borre esas ramas llamando a git fetch
o a git pull
con la opción --prune
, para indicar que tiene que podar las ramas que ya no existen.
Si se usa un GUI, puede que ya haga el prune automáticamente, o puede que tenga una opción para configurarlo.
Es posible configurar Git para que por defecto haga prune , ahorrando así tener que usar la opción --prune
en cada llamada, para ello, añadir lo siguiente al archivo ~/.gitconfig
[fetch]
prune = true
Un ejemplo paso a paso
### Pin publica la rama feature/tiger.
# Pon hace un fetch.
$ git fetch
remote: Enumerando objetos: 5, listo.
remote: Contando objetos: 100% (5/5), listo.
remote: Comprimiendo objetos: 100% (2/2), listo.
remote: Total 3 (delta 0), reusado 0 (delta 0)
Desempaquetando objetos: 100% (3/3), 294 bytes | 294.00 KiB/s, listo.
Desde ~/sandbox/remotes/origin
* [nueva rama] feature/tiger -> origin/feature/tiger
$ git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/feature/tiger
remotes/origin/master
### Pin borra del repositorio remoto feature/tiger.
# Pon hago un fecth y no pasa nada.
$ git fetch
# La rama origin/feature/tiger sigue existiendo.
$ git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/feature/tiger
remotes/origin/master
# Si hiciese fetch con la opción --prune
# origin/feature/tiger se borraría.
$ git fetch --prune
Desde ~/sandbox/remotes/origin
- [eliminado] (nada) -> origin/feature/tiger
$ git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/master
Ramas locales que se han quedado sin rama remota
- Pin subió la rama
feature/duck
- Pon hizo un
git fech
ygit checkout feature/duck
, lo cual creo la rama localfeature/duck
cuyo upstream esorigin/feature/duck
. - Pin mergeó
feature/duck
en master y la borro del repositorio remoto para no deja una rama que ya terminado su ciclo de vida. - Pon hizo
git fecth
con la opción de autoprune, con lo cual Git borro la ramaorigin/feature/duck
, pero nofeature/duck
, no vaya a ser que por alguna razón se quiera conservar en local, o que la rama local hubiese divergido de la rama remota y se pierdan commits. - Pon ejecuta
git status
y ve el siguiente mensaje Tu rama está basada en 'origin/feature/duck', pero upstream ha desaparecido.
La manera de limpiar estar ramas es borrarlas a mano: git branch -d feature/duck
Pero con el tiempo se vuelve una tarea repetitiva y pesada, sobre todo cuando hay varias ramas en ese estado.
Una forma de hacerlo menos pesado es definiendo un par de alias en ~/.gitconfig
[alias]
clear-pruned-branches = "!f() { git branch -vv | grep -E ': (desaparecido|gone)\\]' | grep -v '*' | awk '{print $1}' | xargs -r git branch -d; }; f"
fetch-and-clear = !sh -c 'git fetch && git clear-pruned-branches'
git clear-pruned-branches
borraría las ramas desaparecidas que no estén mergeadas en la rama actual (git branch -d
).
Si alguna rama no se puede borrar porque no está mergeada mostrará un mensaje tal que error: La rama 'feature/gorilla' no ha sido fusionada completamente.
, y tal vez haya que prestarle un poco más de atención para no borrar algún commit que se olvido subir.
git fetch-and-clear
serviría como atajo para evitar tener que escribir git fecth && git clear-pruned-branches
.
Para mantener el sistema limpio se puede usar git clear-pruned-branches
o usar git fetch-and-clear
en lugar de git fetch
.
Un ejemplo paso a paso
### Pin publica la rama feature/duck.
# Pon hace un fetch y va a la ramara a cotillear.
$ git fetch
Desde ~/sandbox/remotes/origin
* [nueva rama] duck -> origin/duck
$ git checkout duck
Rama 'duck' configurada para hacer seguimiento a la rama remota 'duck' de 'origin'.
Cambiado a nueva rama 'duck'
$ git status
En la rama duck
Tu rama está actualizada con 'origin/duck'.
nada para hacer commit, el árbol de trabajo está limpio
### Pin borra del repositorio remoto feature/duck.
# Pon hace un fecth y como tiene el autoprune activo
# borra la rama remota.
$ git fetch
Desde ~/sandbox/remotes/origin
- [eliminado] (nada) -> origin/duck
# git status dice que el upstream ha desaparecido.
$ git status
En la rama duck
Tu rama está basada en 'origin/duck', pero upstream ha desaparecido.
(usa "git branch --unset-upstream" para arreglar)
nada para hacer commit, el árbol de trabajo está limpio
# Para borrarla tendría que ir a otra rama y borrarla
$ git checkout master
Cambiado a rama 'master'
Tu rama está actualizada con 'origin/master'.
$ git branch -d duck
Eliminada la rama duck (era 01d5c54)..
Ramas remotas que han sido mergeadas pero no borradas
- Pin subió la rama
feature/dolphin
. - Mergeó la rama
feature/dolphine
enmaster
pero no la borro. - La rama sigue ahí 3 meses después.
Muchos sistemas de control de versiones como GitHub o Bitbucket tienen una opción para borrar la rama al mergear si se usan Pull Request o Merge Request, pero no es raro que se olvide marcarla, o que el merge se haga en local y se olvide borrar la rama después.
Una solución es ir al repositorio remoto y borrar una por una, asegurándose de borrar solo las ramas mergeadas para no perder ningún commit
Pero de nuevo es una tarea manual y tediosa, que se puede simplificar con un comando de consola
# Ir a la rama de integración, master en ese caso
git checkout master \
`# Actualizarla a la última versión` \
&& git pull --ff-only \
`# Listar todas ramas remotas mergeadas en master` \
&& git branch -r --merged \
`# Excluir del listado las ramas que no hay que borrar;` \
`# con expresiones regulares se exluyen las que contienen el texto` \
`# "master", "production" o "RC"` \
| grep -vE 'master|production|RC' \
`# Quitar del texto el nombre del repositorio remoto` \
| sed 's/origin\///' \
`# Borrar las ramas del repositorio remoto` \
| xargs -r -n 1 git push --delete origin
No es un comando precisamente sencillo, y personalmente siempre me da un poco de respecto ejecutarlo.
Por ello, primero listo las ramas que se irían a borrar.
# Ir a la rama de integración, master en ese caso
git checkout master \
`# Actualizarla a la última versión` \
&& git pull --ff-only \
`# Listar todas ramas remotas mergeadas en master` \
&& git branch -r --merged \
`# Excluir del listado las ramas que no hay que borrar` \
`# con expresiones regulares se exluyen las que contienen el texto` \
`# "master", "production" o "RC"` \
| grep -vE 'master|production|RC' \
`# Quitar del texto el nombre del repositorio remoto` \
| sed 's/origin\///'
Las reviso, y si todo me cuadra ya lanzo el comando completo.
Y como medida de seguridad adicional, siempre se puede clonar el proyecto original para tener un backup.
git clone <repository-url> <project-name>-bck-"$(date)"
# Hago 'git branch' y veo que hay muchas ramas,
# muchas de las cuales creo que ya no se usan.
$ git branch
* contenido-home
master
rama-dos
rama-nueve
rama-seis
rama-siete
rama-tres
rama-uno
seccion-contacto
# Intento borrar alguna que ya se haya borrado del repositorio
# remoto,pero nada.
# (fetch-and-clear es un alias descrito algo más arriba en
# "Ramas locales que se han quedado sin rama remota")
$ git fetch-and-clear
$ git checkout master
# Miro cuantas ramas hay mergeadas en master y hay unas cuantas...
$ git branch -r --merged
origin/HEAD -> origin/master
origin/master
origin/production
origin/rama-cinco
origin/rama-cuatro
origin/rama-diez
origin/rama-dos
origin/rama-nueve
origin/rama-ocho
origin/rama-seis
origin/rama-siete
origin/rama-tres
origin/rama-uno
origin/seccion-contacto
# Asi que quiero borrarlas todas menos 'production'.
# Como borrarlas una a una sería demasiado tedioso uso un script.
$ git branch -r --merged \
`# Excluir del listado las ramas que no hay que borrar;` \
`# con expresiones regulares se exluyen las que contienen el texto` \
`# "master", "production" o "RC"` \
| grep -vE 'master|production|RC' \
`# Quitar del texto el nombre del repositorio remoto` \
| sed 's/origin\///' \
`# Borrar las ramas del repositorio remoto` \
| xargs -r -n 1 git push --delete origin
To ~/sandbox/remotes/origin
- [deleted] rama-cinco
To ~/sandbox/remotes/origin
- [deleted] rama-cuatro
To ~/sandbox/remotes/origin
- [deleted] rama-diez
To ~/sandbox/remotes/origin
- [deleted] rama-dos
To ~/sandbox/remotes/origin
- [deleted] rama-nueve
To ~/sandbox/remotes/origin
- [deleted] rama-ocho
To ~/sandbox/remotes/origin
- [deleted] rama-seis
To ~/sandbox/remotes/origin
- [deleted] rama-siete
To ~/sandbox/remotes/origin
- [deleted] rama-tres
To ~/sandbox/remotes/origin
- [deleted] rama-uno
To ~/sandbox/remotes/origin
- [deleted] seccion-contacto
# Listo todas las ramas y veo que se han borrado muchas
# del repositorio remoto, pero todavía quedan muchas en local.
$ git branch -a
contenido-home
* master
rama-dos
rama-nueve
rama-seis
rama-siete
rama-tres
rama-uno
seccion-contacto
remotes/origin/HEAD -> origin/master
remotes/origin/contenido-home
remotes/origin/master
remotes/origin/production
# Puede ser que me falte hacer el prune y borrar las ramas locales
# que se han quedado sin rama remota.
$ git fetch-and-clear
Eliminada la rama rama-dos (era 211751a)..
Eliminada la rama rama-nueve (era 211751a)..
Eliminada la rama rama-seis (era 211751a)..
Eliminada la rama rama-siete (era 211751a)..
Eliminada la rama rama-tres (era 211751a)..
Eliminada la rama rama-uno (era 211751a)..
Eliminada la rama seccion-contacto (era 2656b99)..
# Reviso por última vez y veo que está más limpio.
$ git branch -a
contenido-home
* master
remotes/origin/HEAD -> origin/master
remotes/origin/contenido-home
remotes/origin/master
remotes/origin/production
Modificar un tag
He creado un tag en el commit que no era; me he equivocado en el nombre del tag; he creado un tag del tipo que no era; etc
Si no se ha subido todavía al repositorio remoto, basta con borrarlo y volver a crearlo.
🚨 Si ya se ha subido al repositorio remoto... se podría reemplazar, pero eso tendría consecuencias para otros desarrolladores.
Los tags no están pensados para ser modificados, así que si alguien modifica un tag en el repositorio remoto, ya sea haciendo git push -f
o borrándolo y recreándolo, el resto de desarrolladores seguirían teniendo en sus locales la versión original del tag, ya que git fetch
no modifica tags.
Y puede ser un problema si en algún momento hay que revertir alguna rama a ese tag o hay que sacar un fix para esa versión, ya que dependiendo de quien haga la operación los resultados serían distintos.
En la documentación de Git se analiza este caso, y vienen a proponer dos soluciones.
La primera es crear un nuevo tag, añadiéndole por ejemplo un sufijo (si el tag original era 1.0.0
llamar al nuevo 1.0.0-the-good-one
).
La segunda opción es modificar el tag en el repositorio remoto y escribir al resto de desarrolladores explicando lo ocurrido y describiendo los pasos que hay que seguir para obtener el tag correcto (borrar el tag en local con git tag --delete TAG
, hacer git fetch
para obtener la nueva versión, y verificar que tienen el tag correcto con git rev-parse TAG
o git show TAG
)
Aquí un ejemplo práctico.
# Moratadelo crea un nuevo tag y lo sube al repositorio remoto.
mortadelo@tia$ git commit -am "Implementado algoritmo secreto"
[master 2da5ad7] Implementado algoritmo secreto
1 file changed, 1 insertion(+)
mortadelo@tia$ git push
Enumerando objetos: 5, listo.
Contando objetos: 100% (5/5), listo.
Compresión delta usando hasta 12 hilos
Comprimiendo objetos: 100% (2/2), listo.
Escribiendo objetos: 100% (3/3), 285 bytes | 285.00 KiB/s, listo.
Total 3 (delta 0), reusado 0 (delta 0)
To ~/sandbox/remotes/origin/
409db7e..2da5ad7 master -> master
mortadelo@tia$ git tag -a "1.0.0" -m "Implementado algoritmo secreto"
mortadelo@tia$ git push origin 1.0.0
Enumerando objetos: 1, listo.
Contando objetos: 100% (1/1), listo.
Escribiendo objetos: 100% (1/1), 169 bytes | 169.00 KiB/s, listo.
Total 1 (delta 0), reusado 0 (delta 0)
To ~/sandbox/remotes/origin/
* [new tag] 1.0.0 -> 1.0.0
# Se lo comenta a Filemón, que hace git fectch para traerse todas
# las novedades
filemon@tia$ git fetch
remote: Enumerando objetos: 6, listo.
remote: Contando objetos: 100% (6/6), listo.
remote: Comprimiendo objetos: 100% (3/3), listo.
remote: Total 4 (delta 0), reusado 0 (delta 0)
Desempaquetando objetos: 100% (4/4), 402 bytes | 402.00 KiB/s, listo.
Desde ~/sandbox/remotes/origin
409db7e..2da5ad7 master -> origin/master
* [nuevo tag] 1.0.0 -> 1.0.0
# Por algúna razón Mortadelo se da cuenta de que hay un error
# en código y decide actualizarlo.
mortadelo@tia$ git commit \
-am "Implementación sin errores del algoritmo secreto"
[master e985b90] Implementación sin errores del algoritmo secreto
1 file changed, 1 insertion(+)
mortadelo@tia$ git push
Enumerando objetos: 5, listo.
Contando objetos: 100% (5/5), listo.
Compresión delta usando hasta 12 hilos
Comprimiendo objetos: 100% (2/2), listo.
Escribiendo objetos: 100% (3/3), 311 bytes | 311.00 KiB/s, listo.
Total 3 (delta 0), reusado 0 (delta 0)
To ~/sandbox/remotes/origin/
2da5ad7..e985b90 master -> master
# Mortadelo no quiere que el primer tag del proyecto tenga
# ese bug, así que intenta recrearlo, pero Git le da error.
mortadelo@tia$ git tag -a "1.0.0" -m "Implementado algoritmo secreto"
fatal: el tag '1.0.0' ya existe
# Decidido a cubrir sus huellas opta por borrar el tag
# y crear el bueno.
mortadelo@tia$ git tag --delete 1.0.0
Etiqueta '1.0.0' eliminada (era b017cde)
mortadelo@tia$ git tag -a "1.0.0" -m "Implementado algoritmo secreto"
# Intenta pushearlo, pero el repositorio remoto lo rechaza
mortadelo@tia$ git push origin 1.0.0
To ~/sandbox/remotes/origin/
! [rejected] 1.0.0 -> 1.0.0 (already exists)
error: falló el push de algunas referencias a '~/sandbox/remotes/origin/'
ayuda: Actualizaciones fueron rechazadas porque el tag ya existe en el remoto.
# Resuelto quema todas las naves con un git push -f
mortadelo@tia$ git push --force origin 1.0.0
Enumerando objetos: 1, listo.
Contando objetos: 100% (1/1), listo.
Escribiendo objetos: 100% (1/1), 171 bytes | 171.00 KiB/s, listo.
Total 1 (delta 0), reusado 0 (delta 0)
To ~/sandbox/remotes/origin/
+ b017cde...7a1b39e 1.0.0 -> 1.0.0 (forced update)
# Revisa la información del tag para comprobar que es la esperada.
mortadelo@tia$ git show 1.0.0
tag 1.0.0
Tagger: mortadelo <mortadelo@tia.es>
Date: Sun Sep 19 17:19:08 2021 +0200
Implementado algoritmo secreto
commit e985b904626ff98b77b2478d15abd929d4b6237d (HEAD -> master, tag: 1.0.0, origin/master, origin/HEAD)
Author: mortadelo <mortadelo@tia.es>
Date: Sun Sep 19 17:18:34 2021 +0200
Implementación sin errores del algoritmo secreto
# Filemón, hace git fetch, pero el tag no se actualiza.
filemon@tia$ git fetch
remote: Enumerando objetos: 6, listo.
remote: Contando objetos: 100% (6/6), listo.
remote: Comprimiendo objetos: 100% (3/3), listo.
remote: Total 4 (delta 0), reusado 0 (delta 0)
Desempaquetando objetos: 100% (4/4), 430 bytes | 430.00 KiB/s, listo.
Desde ~/sandbox/remotes/origin
2da5ad7..e985b90 master -> origin/master
# Revisa su tag en local y es distinto al de Mortadelo,
# ya que apunta a la versión con el bug.
filmemon@tia$ git show 1.0.0
tag 1.0.0
Tagger: mortadelo <mortadelo@tia.es>
Date: Sun Sep 19 17:14:32 2021 +0200
Implementado algoritmo secreto
commit 2da5ad761654cd67bbd8bdfe1a9ac9891f04970f (tag: 1.0.0)
Author: mortadelo <mortadelo@tia.es>
Date: Sun Sep 19 17:11:17 2021 +0200
Implementado algoritmo secreto
# Para tener el mismo tag que Mortadelo tendría que
# borrar el tag en local.
filmemon@tia$ git tag --delete 1.0.0
Etiqueta '1.0.0' eliminada (era b017cde)
# Y hacer git fetch para obtener el nuevo tag
filmemon@tia$ git fetch
Desde /home/claudio/sandbox/remotes/origin
* [nuevo tag] 1.0.0 -> 1.0.0
# Revisa la información y ya ve lo mismo que Mortadelo.
filmemon@tia$ git show 1.0.0
tag 1.0.0
Tagger: mortadelo <mortadelo@tia.es>
Date: Sun Sep 19 17:19:08 2021 +0200
Implementado algoritmo secreto
commit e985b904626ff98b77b2478d15abd929d4b6237d (HEAD -> master, tag: 1.0.0, origin/master, origin/HEAD)
Author: mortadelo <mortadelo@tia.es>
Date: Sun Sep 19 17:18:34 2021 +0200
Implementación sin errores del algoritmo secreto
Si has llegado hasta aquí, gracias 🙂
Este era el último post de la serie; espero que este post, así como los anteriores, te hayan resultado útiles 👋
Créditos
Cover: https://www.pexels.com/photo/photography-of-forest-during-daytime-1068508/
Generación de diagramas: https://excalidraw.com/
Top comments (0)