La mayoría de la confusión con Git —"¿por qué desapareció mi rama?", "¿por qué rebase es peligroso pero merge no?", "¿por qué borrar una rama da miedo?"— proviene de imaginar Git como una pila de diffs aplicados en orden, como un historial de deshacer muy literal. Eso no es lo que es.
Git es una base de datos direccionable por contenido de unos pocos tipos de objetos, vinculados entre sí en un grafo por hashes. Cada comando que realmente ejecutas —commit, branch, merge, rebase, tag— es una operación específica y bastante mecánica en ese grafo. Conceptos básicos de Git para equipos de Node cubre el flujo de trabajo basado en trunk y dirigido por etiquetas que recomienda este manual; esta página es el modelo subyacente —la razón por la que las reglas de ese flujo de trabajo (nunca forzar el push a main, cortar hotfixes de etiquetas, cherry-pick en lugar de merge en release/*) funcionan como lo hacen.
Git almacena cuatro tipos de objetos —blobs, trees, commits y tags—, cada uno direccionado por el hash SHA de su propio contenido, vinculados en un grafo dirigido acíclico; las ramas y las etiquetas son solo punteros con nombre dentro de ese grafo.
Por qué es importante: Casi todas las operaciones de Git "seguras vs. peligrosas" son seguras o peligrosas específicamente por lo que hacen a este grafo; comprender el grafo convierte las reglas memorizadas en reglas razonadas.
Conceptos clave:blob, tree, commit, ref, DAG (grafo dirigido acíclico), almacenamiento direccionable por contenido.
Cuándo usar este modelo: Para decidir entre merge y rebase, entender por qué el force-push es destructivo, razonar por qué los commits de una rama eliminada no desaparecen inmediatamente, o explicar por qué las ramas de hotfix se cortan de una etiqueta en lugar de main.
Limitaciones / Compromisos: El modelo explica qué hacen las operaciones de Git al historial; no reemplaza las convenciones de flujo de trabajo reales de un equipo, que aún deben acordarse y aplicarse (consulta Mejores prácticas de Git y GitHub).
Temas relacionados: desarrollo basado en trunk, cherry-picking, protección de ramas, etiquetas de lanzamiento impulsadas por CI.
El almacenamiento de Git se construye a partir de cuatro tipos de objetos, y cada uno de ellos se identifica por el hash SHA-1 (o, en repositorios más nuevos, SHA-256) de su propio contenido, no por un nombre de archivo, no por un número de secuencia, sino por el contenido mismo.
Un blob almacena el contenido de un archivo sin procesar, solo bytes, sin nombre de archivo adjunto. Un tree almacena una lista de directorios: nombres, modos y el hash del blob o sub-tree al que apunta cada nombre. Un commit almacena un puntero a un tree (el estado de todo el repositorio en ese momento), un puntero a su commit padre (o dos, para una fusión), un autor y un mensaje. Una tag (el tipo anotado) es su propio objeto pequeño que apunta a un commit, típicamente con un mensaje y una firma.
La consecuencia que sorprende a las personas que vienen de un modelo mental de "secuencia de diffs": un commit es una instantánea completa, no un diff. Cuando ejecutas git commit, Git escribe un objeto tree que representa el proyecto entero en ese instante; simplemente lo hace de manera eficiente, porque cualquier archivo, subdirectorio o tree completo que sea idéntico en bytes a uno de un commit anterior ya tiene un objeto con ese hash exacto, por lo que Git lo reutiliza en lugar de almacenarlo dos veces. El diff que ves en git show o git log -p se calcula bajo demanda comparando dos instantáneas; no es lo que realmente se almacena.
Una analogía simple: imagina un archivador donde cada carpeta y cada documento se etiqueta no por nombre, sino por una suma de verificación de su contenido exacto, y los documentos idénticos en cualquier parte del archivador comparten una copia física, sin importar cuántas carpetas lo referencien. Un commit es una tarjeta que dice "el archivador se veía exactamente así —consulta la etiqueta X para la carpeta de nivel superior— y la tarjeta anterior era Y". Ese es todo el modelo.
Debido a que los objetos se vinculan entre sí por hash, el historial completo de un repositorio forma un grafo dirigido acíclico (DAG): los commits apuntan hacia atrás a sus padres, nunca hacia adelante, y nunca en un ciclo.
v2.5.0 (tag) v2.6.0 (tag)
│ │
...──A────B───────C────D────E─────────F──... main
\ /
C1───C2───C3───────────┘ feature/refund-api (merged)
hotfix/2.5.1 se ramifica desde la etiqueta v2.5.0, no desde la punta actual de main:
C (v2.5.0)
\
H1───H2 hotfix/2.5.1
Una rama (main, feature/refund-api) no es un contenedor que guarda un conjunto de commits; es un puntero único y móvil (una "ref") a un commit, actualizado automáticamente para apuntar a cada nuevo commit a medida que lo creas. Crear una rama es casi gratis precisamente por esto: git branch feature/x escribe un pequeño archivo que contiene un hash de commit, nada más. Cada commit en el grafo pertenece al grafo mismo, accesible desde donde sea que sea accesible, no "poseído" por el puntero de rama que se encuentre en él en este momento.
Un commit de fusión es el único lugar donde la forma del DAG se vuelve realmente importante: es un objeto commit con dos punteros padre en lugar de uno, lo que hace que el grafo sea un DAG en lugar de una cadena simple. git checkout -b hotfix/2.5.1 v2.5.0, de Conceptos básicos de Git para equipos de Node, funciona porque una etiqueta es un punto permanente y bien conocido en este mismo grafo; ramificarse desde ella significa que el hotfix comienza exactamente desde lo que se envió como v2.5.0, no desde donde main se encuentre hoy, lo que ya puede contener trabajo no lanzado.
Rebase y merge resuelven el mismo problema —"traer el trabajo de otra rama a la mía"— a través de operaciones de grafo genuinamente diferentes, no solo comandos diferentes para el mismo resultado. Un merge agrega un nuevo commit de dos padres encima de ambos historiales, dejando cada commit original intacto. Un rebase reproduce los commits de tu rama uno por uno sobre una nueva base, lo que significa que Git calcula el diff de cada uno, luego crea un objeto commit completamente nuevo con ese diff aplicado sobre la nueva base —mismo contenido, diferente padre, diferente hash. Los commits originales aún existen en el grafo (hasta que se recolectan como basura); el puntero de la rama rebasada ahora simplemente apunta a las nuevas copias.
Ese comportamiento de rebase —commits nuevos, no originales editados— es exactamente por qué "nunca rebasees (o hagas force-push) una rama compartida" es una regla real y no solo una precaución por sí misma. Si dos personas tienen una rama extraída y una hace force-push de una versión rebasada, la ref ahora apunta a un conjunto diferente de objetos commit con hashes diferentes a los que el historial local de la otra persona aún apunta; Git no tiene forma de conciliar "el mismo trabajo, diferente hash" automáticamente, y el resultado es un historial duplicado o conflictivo. Esta es precisamente la razón por la que Mejores prácticas de Git y GitHub prohíbe el force-push a main, release/* y hotfix/*: esas ramas son lo suficientemente compartidas como para que una ref reescrita huérfane silenciosamente la vista del historial de otra persona.
Eliminar una rama solo elimina el puntero, no los commits en sí; estos se vuelven inaccesibles (ninguna ref apunta a ellos) pero permanecen en el almacén de objetos del repositorio hasta que la recolección de basura los elimine, razón por la cual git reflog a menudo puede recuperar una rama que fue eliminada por error, a veces semanas después.
La fusión con squash de una rama de características (el valor predeterminado de este manual para fusionar en main, consulta Conceptos básicos de Git para equipos de Node) es un colapso específico y deliberado de este modelo: en lugar de un commit de fusión de dos padres que conserva cada commit intermedio, Git calcula un diff combinado en toda la rama y lo escribe como un único commit nuevo con un padre. Esa es precisamente la razón por la que hacer cherry-pick de una característica con squash a una rama release/* es simple: hay exactamente un SHA de commit para hacer cherry-pick en lugar de varios.
A escala, Git no mantiene cada objeto como un archivo suelto separado para siempre; periódicamente empaqueta muchos objetos en archivos pack comprimidos y elimina aquellos que son inaccesibles y han pasado un período de gracia, que es el mecanismo real detrás de git gc. Nada de eso cambia el modelo, solo su eficiencia de almacenamiento una vez que un repositorio acumula años de historial.
Estrategia
Qué hace al grafo
Fortaleza
Mejor ajuste
Commit de fusión
Agrega un commit de dos padres; originales intactos
Preserva el historial exacto, seguro en ramas compartidas
Ramas de larga duración, release/* de vuelta en main
Rebase
Reproduce commits como nuevos objetos sobre una nueva base
Historial lineal y legible
Ramas de características locales antes de ser compartidas/pushed
Fusión con squash
Colapsa toda una rama en un nuevo commit
Fácil de hacer cherry-pick, historial limpio de main
Ramas de características que se fusionan en main (predeterminado de este manual)
Cherry-pick
Copia el diff de un commit existente en otra rama como un nuevo commit
Mueve exactamente un cambio, independiente del resto de su rama
Backporting de un hotfix o un solo commit a release/*
"Un commit almacena un diff del commit anterior." Almacena una instantánea completa de todo el árbol del proyecto en ese momento; el diff que se muestra se calcula comparando dos instantáneas sobre la marcha, no se lee del almacenamiento.
"Una rama es un contenedor que contiene un conjunto de commits." Una rama es un único puntero a un commit; los commits en sí pertenecen al grafo y simplemente son accesibles desde ese puntero, no son propiedad de él.
"Eliminar una rama elimina sus commits inmediatamente." Elimina el puntero; los commits se vuelven inaccesibles pero persisten en el almacén de objetos hasta la recolección de basura, por lo que git reflog a menudo puede recuperarlos.
"Rebase edita los commits originales para moverlos." Crea objetos commit completamente nuevos con nuevos hashes que contienen los mismos cambios; los originales permanecen en el grafo hasta que nada los apunta y finalmente se recolectan.
"Las etiquetas y las ramas son básicamente el mismo tipo de cosa." Un puntero de rama avanza automáticamente con cada nuevo commit que se realiza en él; una etiqueta está destinada a ser una referencia fija a un commit específico y normalmente nunca se mueve una vez creada.
"Force-pushing es solo una versión más fuerte de un push normal." Un push normal solo tiene éxito si es un fast-forward; force-push anula esa verificación y puede mover una ref compartida lejos de commits de los que otros aún dependen, huérfanos su vista del historial.
¿Qué es el modelo de objetos de Git, en una frase?
Un almacén direccionable por contenido de cuatro tipos de objetos —blobs, trees, commits, tags—, cada uno identificado por el hash de su propio contenido y vinculado en un grafo dirigido acíclico al que las ramas y las etiquetas simplemente apuntan.
¿Es un commit de Git una instantánea o un diff?
Una instantánea completa de todo el árbol del proyecto en ese momento, que hace referencia a un commit padre, no un diff. Git lo almacena de manera eficiente reutilizando cualquier archivo o subárbol que sea idéntico en bytes a uno ya almacenado, pero conceptualmente cada commit es una imagen completa del repositorio.
¿Qué es realmente una rama, mecánicamente?
Un pequeño archivo que contiene un hash de commit, nada más. Hacer commit en una rama simplemente actualiza ese puntero al hash del nuevo commit; los commits a los que apunta pertenecen al grafo compartido, no a la rama en sí.
¿Por qué se considera "gratis" crear una rama en Git?
Porque solo escribe un puntero a un commit existente; no se copian archivos, no se duplica el historial. El costo de una rama es esencialmente cero, independientemente de cuán grande sea el historial del repositorio.
¿Qué hace que el historial de Git sea un "DAG" específicamente?
Cada commit apunta hacia atrás a sus padres y nunca hacia adelante o en un ciclo, y los commits de fusión —los únicos commits con dos padres— son lo que le da al grafo una estructura de ramificación en lugar de ser una sola línea recta.
¿Cuál es la verdadera diferencia mecánica entre merge y rebase?
Un merge agrega un nuevo commit con dos padres encima de ambos historiales existentes, dejando cada commit original intacto. Un rebase reproduce los diffs de tus commits sobre una nueva base, produciendo objetos commit completamente nuevos con nuevos hashes; los antiguos aún existen hasta que se recolectan como basura.
¿Por qué rebasar una rama compartida causa problemas a los colaboradores?
Porque rebase produce nuevos objetos commit con hashes diferentes a los originales, por lo que un rebase con force-push mueve la ref de la rama para apuntar a commits que el historial local de un colaborador no reconoce como el mismo trabajo; Git no puede conciliar eso automáticamente.
Si elimino una rama por error, ¿el trabajo realmente se pierde?
Generalmente no de inmediato; eliminar una rama solo elimina el puntero, y los commits a los que apuntaba permanecen en el almacén de objetos como objetos inaccesibles hasta que la recolección de basura los elimine. git reflog con frecuencia encuentra el último hash de commit para que la rama pueda recrearse.
¿Por qué una rama de hotfix en el flujo de trabajo de este manual comienza desde una etiqueta en lugar de desde `main`?
Una etiqueta es un punto fijo en el grafo que representa exactamente lo que se lanzó como esa versión, mientras que main ya puede contener trabajo no lanzado cuando se necesita un hotfix. Ramificarse desde la etiqueta garantiza que el hotfix comience exactamente desde lo que realmente se está ejecutando en producción.
¿Qué hace realmente la fusión con squash al grafo de commits?
Calcula un diff combinado en cada commit de la rama de características y lo escribe como un único commit nuevo con un padre en la rama de destino, en lugar de preservar cada commit intermedio o agregar un commit de fusión de dos padres.
¿Por qué es más sencillo hacer cherry-pick de un hotfix a `release/*` después de una fusión con squash?
Debido a que la fusión con squash colapsó toda la característica en un solo commit, hay exactamente un SHA para hacer cherry-pick; una característica no fusionada requeriría seleccionar varios commits, o seleccionar un commit de fusión de una manera que necesita un indicador adicional para especificar qué diff de padre usar.
¿Por qué es específicamente peligroso hacer force-push a `main`, mecánicamente?
Force-push anula la verificación normal de Git de solo fast-forward y puede mover la ref main para apuntar a un commit diferente al que esperan los historiales locales de los colaboradores, huérfanos cualquier trabajo construido sobre los commits que se alejaron.