Cerramos el módulo 2 con una promesa: te dijimos que ya sabías lo que era una rama, aunque no lo supieras del todo. "Un fichero con un hash dentro", decíamos. Esta lección va a cumplir esa promesa hasta el último byte.
Las ramas son, sin exagerar, la razón por la que Git ganó. Otros sistemas de control de versiones tenían ramas antes que Git, pero eran caras, lentas y daban miedo. En Git son tan baratas que crear una es literalmente escribir 41 bytes en el disco. Ese cambio de coste no es un detalle técnico: es lo que hace posible que un equipo como el de Ana, Bruno y Carla abra una rama para cada idea, cada corrección y cada experimento sin pensárselo dos veces.
Pero para usarlas con confianza hay que entender qué son realmente, no la metáfora del árbol que sale en todos los tutoriales. En esta lección vamos a abrir el capó: veremos el fichero, veremos el hash, veremos qué es HEAD y por qué apunta a una rama y no a un commit, y entenderemos qué significa exactamente que dos ramas "diverjan". Todavía no vamos a crear ninguna rama —eso es la lección siguiente—: aquí toca comprender la maquinaria.
Contenido
- El problema que resuelven las ramas
- Qué es realmente una rama: 41 bytes
HEAD: dónde estás ahora mismo- Por qué
HEADapunta a una rama y no a un commit - Cómo avanza el puntero al confirmar
- Por qué las ramas de Git son baratas (y las de SVN no lo eran)
- El grafo de commits: la estructura real
- Divergencia y ancestro común
- Las ramas que ya tienes sin saberlo
- El problema que resuelven las ramas
Recordemos el callejón en el que dejamos al equipo. Ana y Bruno trabajaban los dos sobre main, cada uno en su portátil. Ana desarrollaba el contador de tareas; Bruno, el filtro de pendientes. Ninguno de los dos podía ver el trabajo del otro y, cuando quisieran juntarlo, se encontrarían con dos versiones de app.js que habían evolucionado por separado.
Sin ramas, un desarrollador tiene exactamente tres opciones malas:
| Opción | Qué implica | Por qué es mala |
|---|---|---|
| Confirmar todo en la única línea | Cada commit va directo al proyecto principal | El proyecto queda roto a medias mientras la funcionalidad no esté terminada |
| No confirmar hasta terminar | Días de trabajo sin ningún punto de guardado | Se pierde todo el valor de Git: no hay historial, no hay vuelta atrás |
| Copiar la carpeta entera | gestor-tareas-v2, gestor-tareas-prueba… |
Es el versionado manual que veíamos en la lección 01-01, con todos sus problemas |
Una rama resuelve las tres a la vez: permite confirmar con frecuencia en una línea de trabajo aislada que no molesta a nadie, y que después se puede integrar en el proyecto principal cuando esté lista.
La pregunta es: ¿cómo consigue Git que eso sea barato? La respuesta está en el modelo de datos que ya estudiaste.
- Qué es realmente una rama: 41 bytes
Vamos al repositorio de Ana y miremos dentro de .git/. Recuerda del modelo de datos que ahí dentro está toda la base de datos del repositorio.
Un único fichero, llamado main. Veamos su contenido:
Eso es todo. Un hash SHA-1 de 40 caracteres y un salto de línea final. Comprobémoslo con precisión de contable:
Cuarenta y un bytes. Cuarenta caracteres hexadecimales más el \n del final. Eso es una rama en Git: un fichero de texto plano cuyo contenido es el hash de un commit.
No hay metadatos, no hay lista de commits, no hay copia de los ficheros, no hay nada más. La rama main no "contiene" los cuatro commits del proyecto: solo apunta al último de ellos, y el resto se alcanza siguiendo los enlaces de padre que cada commit lleva dentro.
La forma limpia de consultarlo
Leer ficheros de .git/ a mano está muy bien para entender, pero para el día a día Git tiene un comando que hace lo mismo de forma fiable (y que además funciona aunque la referencia esté "empaquetada", cosa que veremos en un momento):
El mismo hash. git rev-parse es el comando que traduce cualquier forma de nombrar un commit a su hash completo. Ya lo usaste en el módulo 1 con git rev-parse HEAD; funciona igual con nombres de rama, con HEAD~2, con etiquetas y con todo lo que vimos en la lección 02-06.
Y si quieres confirmar que ese hash es efectivamente un commit y no otra cosa:
tree 8c4f2a1e9b7d3f5a6c8e0b2d4f6a8c0e2b4d6f8a parent 4e7f2a9c8b1d5e3f7a2c9d4b6e8f1a3c5d7b9e2f author Ana Ferrer <[email protected]> 1753876800 +0200 committer Ana Ferrer <[email protected]> 1753876800 +0200 Documentar la instalación en el README
Nada nuevo: es el objeto commit que ya sabes leer. Lo importante es la conclusión.
Una rama es un puntero móvil a un commit. No es una carpeta, no es una copia, no es un contenedor. Es un nombre legible que guarda un hash.
El caso de las referencias empaquetadas
Un aviso para que no te asustes si un día el fichero no aparece. Git, para no tener miles de ficheros diminutos en repositorios grandes, agrupa las referencias que no cambian a menudo en un único fichero:
# pack-refs with: peeled fully-peeled sorted c5d9b1e7a3f2d8b4e6c1a9f5d3b7e2c8a4f6d1b9 refs/heads/main
Si git gc se ha ejecutado (Git lo hace solo de vez en cuando), puede que .git/refs/heads/main haya desaparecido y su información esté aquí. El significado es idéntico: nombre de rama → hash. Por eso git rev-parse es más fiable que cat: mira en los dos sitios.
HEAD: dónde estás ahora mismo
HEAD: dónde estás ahora mismoSi una rama es un puntero a un commit, hace falta algo que indique en qué rama estás trabajando. Ese algo es HEAD, y también es un fichero:
Fíjate bien, porque aquí está la clave de toda la lección: HEAD no contiene un hash. Contiene la cadena ref: refs/heads/main, que es una referencia simbólica: apunta a otra referencia, no a un objeto.
La cadena de indirección completa es esta:
graph LR
HEAD[".git/HEAD<br/>ref: refs/heads/main"] --> RAMA[".git/refs/heads/main<br/>c5d9b1e…"]
RAMA --> COMMIT["objeto commit c5d9b1e<br/>tree + parent + autor + mensaje"]
COMMIT --> TREE["objeto tree<br/>index.html, estilos.css,<br/>app.js, README.md"]
Tres saltos: HEAD → rama → commit → árbol de ficheros. Cuando ejecutas git status y lees On branch main, Git está literalmente leyendo la primera línea de ese fichero.
También hay un comando para consultarlo sin abrir ficheros:
Y si lo que quieres es solo el nombre corto de la rama, que es lo que se usa en scripts y en los prompts de la terminal:
HEAD como forma de nombrar un commit
Ya has usado HEAD muchas veces —git diff HEAD, HEAD~1, git show HEAD— como si fuera el commit actual. Y funciona, porque Git resuelve la cadena entera automáticamente:
Mismo hash que git rev-parse main, porque HEAD apunta a main y main apunta a ese commit. Pero ojo, son cosas distintas:
| Expresión | Qué devuelve | Qué es en el disco |
|---|---|---|
git rev-parse HEAD |
El hash del commit actual (resolviendo toda la cadena) | .git/HEAD → contiene una referencia simbólica |
git symbolic-ref HEAD |
El nombre de la rama actual | El contenido literal de .git/HEAD |
git rev-parse main |
El hash al que apunta la rama main |
.git/refs/heads/main |
- Por qué
HEAD apunta a una rama y no a un commit
HEAD apunta a una rama y no a un commitEsta es la pregunta que separa a quien "usa" Git de quien lo entiende. ¿Por qué esa indirección? ¿Por qué HEAD no guarda directamente el hash del commit actual, que sería más simple?
Porque HEAD tiene que saber qué rama debe avanzar cuando confirmas.
Imagina que HEAD guardase directamente c5d9b1e. Ana hace un commit nuevo. Git crea el objeto commit… ¿y ahora qué? ¿Qué rama tiene que actualizar? ¿main? ¿Alguna otra que también apuntase ahí? No tendría forma de saberlo.
Al guardar ref: refs/heads/main, Git sabe exactamente dos cosas:
- El commit padre del nuevo commit es aquel al que apunta
main(resolviendo la cadena). - La rama que debe actualizarse después de crear el commit es
main.
Esa indirección es lo que convierte una rama en un puntero móvil en lugar de una simple etiqueta. Y es también la razón de que exista un estado especial llamado detached HEAD, en el que HEAD sí contiene un hash directamente y por tanto no hay ninguna rama que avanzar. Lo veremos con detalle en la lección siguiente.
- Cómo avanza el puntero al confirmar
Veamos la mecánica exacta. Ana está en main con el repositorio tal como quedó al final del módulo 2:
c5d9b1e (HEAD -> main) Documentar la instalación en el README 4e7f2a9 Añadir borrado de tareas al listado 8b6d3c2 Añadir estilos base del listado 1a4c8d6 Estructura inicial del gestor de tareas
Ese (HEAD -> main) que has visto tantas veces ya no es un adorno: es Git dibujándote la cadena de indirección. Se lee literalmente como "HEAD apunta a main, y main está aquí".
Estado en disco antes de confirmar:
Ahora Ana confirma un cambio cualquiera:
[main a3f5c9e] Corregir el enlace de la documentación en el README 1 file changed, 1 insertion(+), 1 deletion(-)
Git ha hecho, en este orden:
- Ha escrito los objetos blob de los ficheros modificados y los tree correspondientes.
- Ha creado un objeto commit cuyo campo
parentesc5d9b1e(el commit al que apuntabamain, obtenido resolviendoHEAD). - Ha sobrescrito el fichero
.git/refs/heads/maincon el hash nuevo.
Estado en disco después:
HEAD no se ha tocado. Sigue diciendo exactamente lo mismo. Lo que ha cambiado es el fichero de la rama. Compruébalo:
Esta es la operación fundamental de Git y es la misma siempre: crear un commit y mover 41 bytes. Que haya una rama o cincuenta no cambia nada.
Y aquí conviene recordar algo del módulo 1: los commits son inmutables, pero las ramas no lo son. Un commit, una vez creado, nunca cambia —cambiarlo produciría otro hash y por tanto otro commit—. Las ramas, en cambio, son punteros que se mueven constantemente. Esa asimetría es la base de todo lo que viene en este módulo y en el 5.
- Por qué las ramas de Git son baratas (y las de SVN no lo eran)
Ahora que sabes que una rama son 41 bytes, la comparación con los sistemas anteriores se explica sola.
En Subversion (SVN), el sistema centralizado que dominó antes de Git y que ya comparamos en la lección 01-01, no existía el concepto de rama como tal. Una rama era una copia de un directorio dentro del propio repositorio, por convención en una carpeta llamada branches/:
repositorio/ ├── trunk/ ← la línea principal ├── branches/ │ ├── contador-tareas/ ← una copia completa del proyecto │ └── filtro-pendientes/ ← otra copia completa └── tags/
Crear una rama era ejecutar svn copy y esperar. Es cierto que SVN implementaba copias perezosas y no duplicaba físicamente todos los bytes, pero la operación seguía requiriendo comunicación con el servidor, seguía siendo lenta en proyectos grandes y, sobre todo, seguía teniendo un coste conceptual enorme: fusionar una rama de vuelta al tronco era una operación delicada, propensa a errores y que muchos equipos evitaban activamente.
La comparación en detalle:
| Aspecto | Subversion | Git |
|---|---|---|
| Qué es una rama | Una copia de un directorio en el repositorio | Un fichero de 41 bytes con un hash |
| Coste de crearla | Operación en el servidor, segundos o minutos | Escribir 41 bytes en local, milisegundos |
| ¿Requiere red? | Sí | No |
| Cambiar de rama | Actualizar la copia de trabajo desde el servidor | Reescribir los ficheros del directorio de trabajo, en local |
| Fusionar | Históricamente frágil; hasta la 1.5 había que llevar el seguimiento a mano | Operación de primera clase, con seguimiento automático de ancestros |
| Práctica habitual del equipo | Pocas ramas, de larga duración, temidas | Muchas ramas, de vida corta, rutinarias |
Mídelo tú mismo cuando crees tu primera rama en la lección siguiente: la operación es instantánea porque no se copia nada. Todos los commits, todos los árboles y todos los blobs ya están en la base de datos de objetos, compartidos por todas las ramas. Lo único nuevo es el nombre.
Y este cambio de coste tuvo una consecuencia cultural enorme. Cuando crear una rama cuesta cero, dejas de preguntarte si merece la pena: abres una para probar una idea de diez minutos, y si no funciona la borras. Esa es la mentalidad que este módulo pretende instalarte.
- El grafo de commits: la estructura real
Hasta ahora hemos visto el historial como una lista, porque solo había una rama. Con varias ramas, el historial es lo que siempre fue en realidad: un grafo dirigido acíclico (DAG, por sus siglas en inglés).
Descompongamos ese nombre, que suena peor de lo que es:
- Grafo: nodos (los commits) unidos por aristas (los enlaces
parent). - Dirigido: las aristas tienen sentido, y el sentido es hacia atrás. Cada commit conoce a su padre; ningún commit conoce a sus hijos. Por eso
git logrecorre el historial hacia el pasado y nunca hacia el futuro. - Acíclico: no hay ciclos. Es imposible que un commit sea su propio antepasado, porque su hash depende del hash del padre; un ciclo exigiría conocer un hash antes de calcularlo.
Así se ve el repositorio de Ana ahora mismo, con una sola línea:
gitGraph commit id: "1a4c8d6" commit id: "8b6d3c2" commit id: "4e7f2a9" commit id: "c5d9b1e"
Y así se verá dentro de dos lecciones, cuando el trabajo de Ana y el de Bruno convivan en el mismo repositorio en ramas distintas:
gitGraph commit id: "1a4c8d6" commit id: "8b6d3c2" commit id: "4e7f2a9" commit id: "c5d9b1e" branch contador-tareas checkout contador-tareas commit id: "6f2b9d4" commit id: "9d1e4b7" checkout main branch filtro-pendientes checkout filtro-pendientes commit id: "3d5b8e1" commit id: "7c1f4a9" commit id: "b2e6d3f"
Fíjate en un detalle importante del diagrama: los tres puntos de partida (main, contador-tareas, filtro-pendientes) comparten los cuatro primeros commits. No hay tres copias de 1a4c8d6: hay un único objeto commit en .git/objects/ alcanzable desde tres nombres distintos. Eso es lo que hace que las ramas no ocupen espacio.
En la terminal, ese mismo grafo se ve con el comando que ya conoces del módulo 2 y que a partir de ahora será tu herramienta principal:
* b2e6d3f (filtro-pendientes) Corregir el foco del campo tras añadir una tarea * 7c1f4a9 Aplicar estilo a las tareas completadas * 3d5b8e1 Añadir filtro de tareas pendientes | * 9d1e4b7 (contador-tareas) Marcar tareas como completadas al hacer clic | * 6f2b9d4 Añadir el contador de tareas pendientes |/ * c5d9b1e (HEAD -> main) Documentar la instalación en el README * 4e7f2a9 Añadir borrado de tareas al listado * 8b6d3c2 Añadir estilos base del listado * 1a4c8d6 Estructura inicial del gestor de tareas
Presta atención a --all. Sin esa opción, git log solo muestra lo alcanzable desde HEAD, es decir, solo los cuatro commits de main: el trabajo de las otras ramas sería invisible. Con --all, Git parte de todas las referencias. Es un descuido tan habitual que merece la pena que lo memorices ahora.
Y fíjate en la línea |/: ahí es donde las dos líneas de trabajo se juntan hacia atrás. Ese punto tiene nombre propio y es el protagonista del apartado siguiente.
- Divergencia y ancestro común
Dos ramas divergen cuando cada una tiene commits que la otra no tiene. Dicho con precisión: cuando ninguna de las dos es alcanzable desde la otra siguiendo enlaces de padre.
Es importante distinguir tres situaciones distintas, porque determinan cómo se comportará la fusión que veremos en la lección 03-03:
| Situación | Descripción | Ejemplo |
|---|---|---|
| Rama por detrás | Todos los commits de A están en B, pero B tiene más | main está en c5d9b1e; contador-tareas tiene dos commits encima |
| Rama por delante | Todos los commits de B están en A, pero A tiene más | Lo mismo visto desde el otro lado |
| Ramas divergentes | Cada una tiene commits propios desde que se separaron | contador-tareas y filtro-pendientes entre sí |
En el grafo de arriba, contador-tareas tiene dos commits que filtro-pendientes no tiene, y filtro-pendientes tiene tres que contador-tareas no tiene. Han divergido.
El ancestro común
Cuando dos ramas divergen, el punto donde se separaron se llama ancestro común (merge base, en la terminología de Git). Es el commit más reciente alcanzable desde ambas ramas.
En nuestro grafo, el ancestro común de contador-tareas y filtro-pendientes es c5d9b1e: es el último commit que las dos tienen en su historia.
graph RL
A["1a4c8d6"]
B["8b6d3c2"] --> A
C["4e7f2a9"] --> B
D["c5d9b1e<br/><b>ancestro común</b>"] --> C
E["6f2b9d4"] --> D
F["9d1e4b7<br/>contador-tareas"] --> E
G["3d5b8e1"] --> D
H["7c1f4a9"] --> G
I["b2e6d3f<br/>filtro-pendientes"] --> H
(Las flechas apuntan hacia el pasado, como en la realidad: cada commit señala a su padre.)
¿Por qué importa tanto este concepto? Porque es la pieza que hace posible fusionar. Para juntar dos ramas divergentes, Git necesita saber, para cada línea de cada fichero, quién la cambió: si solo cambió en una rama, se coge esa versión; si cambió en las dos de forma distinta, hay conflicto. Y para saber "quién la cambió" hace falta un punto de referencia neutral: el ancestro común.
Con tres versiones de cada fichero —la del ancestro, la de una rama y la de la otra— Git puede razonar. Con solo dos, no podría distinguir un cambio de una eliminación. De ahí el nombre de fusión a tres bandas (three-way merge) que veremos en la lección 03-03.
Anticipemos la lógica con un ejemplo concreto sobre una línea de README.md:
| Versión | Contenido de la línea | Conclusión de Git |
|---|---|---|
Ancestro c5d9b1e |
# Gestor de Tareas |
Punto de partida |
| Rama de Ana | # Gestor de Tareas |
No la ha tocado |
| Rama de Bruno | # Gestor de Tareas del Equipo |
La ha cambiado él |
Resultado: se coge la versión de Bruno, sin conflicto y sin preguntar. Git sabe que Ana no tocó esa línea porque coincide con el ancestro. Si las dos versiones fueran distintas del ancestro y distintas entre sí, tendríamos un conflicto y haría falta una persona: eso es la lección 03-05.
- Las ramas que ya tienes sin saberlo
Terminemos atando dos cabos que llevas arrastrando desde el módulo 1.
Primero: main no tiene nada de especial. Es una rama exactamente igual que cualquier otra, con el mismo fichero de 41 bytes en el mismo directorio. Git no le da ningún trato preferente; su importancia es puramente una convención del equipo. De hecho, su nombre lo elegiste tú en la lección 01-06, al poner init.defaultBranch = main (por eso el proyecto de Ana no se llama master).
Segundo: la rama inicial existe antes de tener contenido. Cuando Ana ejecutó git init en la lección 02-01, git status dijo On branch main aunque .git/refs/heads/ estaba vacío. Ahora sabes exactamente por qué:
HEAD apunta a una rama que todavía no existe como fichero. Git llama a esto una rama sin nacer (unborn branch). El fichero .git/refs/heads/main se crea en el momento del primer commit, y por eso ese commit es especial: no tiene padre, y git commit lo anuncia con (root-commit).
Es una consecuencia elegante del diseño: como HEAD guarda un nombre y no un hash, puede apuntar perfectamente a algo que aún no existe.
Errores Comunes y Consejos
Error 1: creer que una rama "contiene" commits. Es la confusión más frecuente y la fuente de casi todos los malentendidos posteriores. Una rama es un puntero a un commit; el resto del historial se deduce siguiendo los padres. Consecuencia práctica: un mismo commit puede estar en diez ramas a la vez sin ocupar más espacio, y borrar una rama no borra ningún commit (solo el puntero, como veremos en la lección 03-06).
Error 2: creer que borrar una rama borra el trabajo. Se deduce del punto anterior. Borrar .git/refs/heads/experimento elimina 41 bytes. Los commits siguen en .git/objects/, y si no quedan referencias que los alcancen acabarán siendo eliminados por el recolector de basura, pero no de inmediato ni de forma silenciosa. Hay red de seguridad.
Error 3: olvidar --all en git log --graph. Sin --all, Git te muestra únicamente lo alcanzable desde HEAD y te da la falsa impresión de que el resto de ramas no tiene nada. Acostúmbrate a escribir git log --oneline --graph --all como una sola unidad; en la lección 06-04 lo convertiremos en un alias.
Error 4: confundir HEAD con la rama actual. HEAD es el fichero que indica cuál es la rama actual. En el 99 % de los casos puedes usarlos de forma intercambiable, pero cuando entres en detached HEAD (lección siguiente) la diferencia se vuelve crítica: ahí hay HEAD pero no hay rama.
Consejo 1: cuidado con HEAD en mayúsculas. En Linux y macOS, head (minúsculas) no es lo mismo que HEAD y Git te dará un error de referencia desconocida. En Windows y en macOS con sistema de ficheros no sensible a mayúsculas puede funcionar por accidente, lo que hace el fallo más traicionero cuando el script llega a otra máquina. Escríbelo siempre en mayúsculas.
Consejo 2: no edites los ficheros de .git/refs/ a mano. Los hemos leído para entender la maquinaria, y leerlos es perfectamente seguro. Escribirlos no: Git tiene bloqueos, registros de cambios (el reflog, que veremos en el módulo 9) y validaciones que te saltarías. Para mover una rama a mano existe git update-ref.
Consejo 3: instala un indicador de rama en tu terminal. Saber en todo momento en qué rama estás evita la mitad de los sustos de este módulo. Muchas configuraciones de shell modernas lo traen; si no, git branch --show-current es el comando que se suele usar para construirlo.
Ejercicios
Ejercicio 1: la autopsia de una rama
Sin usar git log ni git branch, y usando únicamente comandos de "fontanería" y lectura de ficheros, averigua en tu propio repositorio:
- En qué rama estás.
- A qué commit apunta esa rama.
- Cuál es el mensaje de ese commit.
- Cuál es el hash de su commit padre.
Ejercicio 2: verificar que HEAD no se mueve
Demuestra empíricamente lo que afirma el apartado 5: que al confirmar cambia el fichero de la rama pero no .git/HEAD. Diseña la secuencia de comandos que lo prueba y explica qué esperas ver en cada paso.
Ejercicio 3: razonar sobre el grafo
Dado este historial, responde sin ejecutar nada:
* d4e5f6a (rama-b) Ajustar el pie de página * c3f8a21 Añadir el pie de página | * b1a2c3d (HEAD -> rama-a) Corregir el título | * a9f8e7d Añadir la cabecera |/ * 8f1a3b7 (main) Estructura inicial
- ¿Cuál es el ancestro común de
rama-ayrama-b? - ¿Cuántos commits tiene
rama-aque no tengarama-b? - ¿Aparecería
c3f8a21en la salida degit log --onelinesin--all? ¿Por qué? - ¿Han divergido
mainyrama-a? - ¿Cuánto ocupan en disco los ficheros de las tres ramas, en total?
Soluciones
Solución 1:
HEAD contiene una referencia simbólica, y la última parte de la ruta es el nombre de la rama: main.
# 2. A qué commit apunta esa rama
cat .git/refs/heads/main
# → c5d9b1e7a3f2d8b4e6c1a9f5d3b7e2c8a4f6d1b9Si el fichero no existiera porque las referencias están empaquetadas, la alternativa fiable es git rev-parse main, o buscar la línea correspondiente en .git/packed-refs.
tree 8c4f2a1e9b7d3f5a6c8e0b2d4f6a8c0e2b4d6f8a parent 4e7f2a9c8b1d5e3f7a2c9d4b6e8f1a3c5d7b9e2f author Ana Ferrer <[email protected]> 1753876800 +0200 committer Ana Ferrer <[email protected]> 1753876800 +0200 Documentar la instalación en el README
El mensaje es Documentar la instalación en el README y el padre es 4e7f2a9…. Fíjate en que has recorrido a mano exactamente el mismo camino que recorre git log: HEAD → rama → commit → parent.
Solución 2:
# Hacemos un cambio cualquiera y lo confirmamos
echo "" >> README.md
git commit -am "Añadir una línea en blanco al README".git/HEAD es idéntico; .git/refs/heads/main ha cambiado. Confirmar no mueve HEAD: mueve la rama a la que HEAD apunta. Esa es exactamente la razón de que HEAD guarde un nombre y no un hash.
(Si quieres deshacer el commit de prueba, git reset --hard HEAD~1 lo devuelve todo al estado anterior; lo veremos en profundidad en la lección 09-02.)
Solución 3:
-
El ancestro común es
8f1a3b7. Es el commit más reciente alcanzable desde las dos ramas:rama-allega a él pora9f8e7d, yrama-bporc3f8a21. -
Dos commits:
a9f8e7dyb1a2c3d. Son los que están en la línea derama-apor encima del punto de separación. -
No aparecería. Sin
--all,git logparte deHEAD, que apunta arama-a. Desdeb1a2c3dse llega aa9f8e7dy de ahí a8f1a3b7, pero nunca ac3f8a21: las flechas van hacia el pasado y no hay ningún camino desderama-ahasta la línea derama-b. -
No han divergido.
mainestá en8f1a3b7, que es alcanzable desderama-a.mainestá simplemente por detrás: no tiene ningún commit propio querama-ano tenga. Esta situación es justo la que permitirá una fusión fast-forward en la lección 03-03. -
123 bytes: tres ficheros de 41 bytes cada uno (
main,rama-ayrama-b). Los seis commits, sus árboles y sus blobs se almacenan una sola vez en.git/objects/y los comparten las tres ramas. Ese es todo el "coste" de tener tres líneas de trabajo abiertas.
Conclusión
Hemos abierto el capó y dentro no había magia, sino un diseño muy simple llevado hasta sus últimas consecuencias:
- Una rama es un fichero de 41 bytes en
.git/refs/heads/(o una línea en.git/packed-refs) que contiene el hash de un commit. Nada más. Se consulta de forma fiable congit rev-parse <rama>. HEADes otro fichero que normalmente contiene una referencia simbólica del tiporef: refs/heads/main. Apunta a una rama, no a un commit, y esa indirección es deliberada: es lo que permite a Git saber qué puntero mover al confirmar.- Confirmar mueve la rama, no
HEAD. El objeto commit se crea con el commit actual como padre y el fichero de la rama se sobrescribe con el hash nuevo. Los commits son inmutables; las ramas son móviles. - Las ramas son baratas porque no copian nada. Frente al
svn copyde Subversion, que exigía servidor y tiempo, aquí todo el historial vive en una base de datos de objetos compartida y una rama solo añade un nombre. Ese cambio de coste es cultural, no solo técnico. - El historial es un grafo dirigido acíclico: los commits apuntan hacia atrás, a sus padres.
git log --oneline --graph --alles la forma de verlo; no olvides el--all. - Dos ramas divergen cuando cada una tiene commits propios desde su punto de separación. Ese punto es el ancestro común, y es la pieza que hará posible la fusión: con tres versiones de un fichero —ancestro, rama A y rama B— Git puede deducir quién cambió qué.
Lo que viene
Ya sabes qué es una rama. Toca crearlas y moverse entre ellas, que es donde aparecen las preguntas prácticas: ¿qué diferencia hay entre git branch y git switch -c? ¿Por qué existe git switch si ya existía git checkout? ¿Qué pasa con los cambios que tengo a medias si cambio de rama? ¿Y qué demonios significa ese aviso de HEAD detached at 4e7f2a9 que a todo el mundo le da miedo?
En la siguiente lección, Creando y Cambiando Ramas, Ana abrirá por fin funcionalidad/contador-tareas y Bruno funcionalidad/filtro-pendientes, y el repositorio dejará de tener una única línea de trabajo. Todo lo que has aprendido aquí —el fichero de 41 bytes, la indirección de HEAD, el avance del puntero— vas a verlo ocurrir en directo.
Dominando Git: De Principiante a Avanzado
Módulo 1: Introducción a Git
- ¿Qué es Git?
- Instalando Git
- Terminología Básica de Git
- El Modelo de Datos de Git
- Configurando Git
- Configuración Inicial
Módulo 2: Operaciones Básicas de Git
- Creando un Repositorio
- Clonando un Repositorio
- Flujo de Trabajo Básico de Git
- Preparando y Confirmando Cambios
- Inspeccionando Cambios con git diff
- Visualizando el Historial de Confirmaciones
Módulo 3: Ramas y Fusión
- Entendiendo las Ramas
- Creando y Cambiando Ramas
- Fusionando Ramas
- Estrategias de Fusión
- Resolviendo Conflictos de Fusión
- Gestión de Ramas
Módulo 4: Trabajando con Repositorios Remotos
- Entendiendo los Repositorios Remotos
- Agregando un Repositorio Remoto
- Autenticación con Repositorios Remotos
- Obteniendo y Extrayendo Cambios
- Enviando Cambios
- Rastreando Ramas
Módulo 5: Operaciones Avanzadas de Git
- Rebase
- Rebase Interactivo
- Cherry-Picking de Confirmaciones
- Guardando Cambios Temporales
- Etiquetando Confirmaciones
- Revirtiendo Confirmaciones
Módulo 6: Herramientas y Técnicas de Git
- Usando Git Hooks
- Git Bisect
- Git Blame
- Git Log y Alias
- Submódulos de Git
- Múltiples Copias de Trabajo con git worktree
Módulo 7: Estrategias de Colaboración y Flujo de Trabajo
- Forks y Pull Requests
- Revisiones de Código con Git
- Flujo de Trabajo Git Flow
- GitHub Flow
- Trunk Based Development
- Integración Continua con Git
Módulo 8: Mejores Prácticas y Consejos de Git
- Escribiendo Buenos Mensajes de Confirmación
- Manteniendo un Historial Limpio
- Ignorando Archivos con .gitignore
- Atributos de Fichero con .gitattributes
- Mejores Prácticas de Seguridad
- Consejos de Rendimiento
Módulo 9: Solución de Problemas y Depuración
- Problemas Comunes de Git
- Deshaciendo Cambios
- Resolviendo Divergencias con el Remoto
- Recuperando Confirmaciones Perdidas
- Tratando con Repositorios Corruptos
- Técnicas Avanzadas de Depuración
