git version-control

¿Qué quiere decir Linus Torvalds cuando dice que Git "nunca" rastrea un archivo?



version-control (6)

Citando a Linus Torvalds cuando se le preguntó cuántos archivos puede manejar Git durante su Tech Talk en Google en 2007 (43:09):

... Git rastrea tu contenido. Nunca rastrea un solo archivo. No puedes seguir un archivo en Git. Lo que puedes hacer es rastrear un proyecto que tiene un solo archivo, pero si tu proyecto tiene un solo archivo, hazlo y puedes hacerlo, pero si rastreas 10,000 archivos, Git nunca los ve como archivos individuales. Git piensa todo como contenido completo. Toda la historia en Git se basa en la historia de todo el proyecto ...

(Transcripciones here ).

Sin embargo, cuando te sumerges en el libro de Git , lo primero que te dicen es que un archivo en Git puede ser rastreado o no. Además, me parece que toda la experiencia de Git está orientada hacia la versión del archivo. Cuando se utiliza git diff git status salida de git status se presenta por archivo. Al usar git add , también puedes elegir por archivo. Incluso puede revisar el historial a base de archivos y es increíblemente rápido.

¿Cómo debe interpretarse esta afirmación? En términos de seguimiento de archivos, ¿en qué se diferencia Git de otros sistemas de control de origen, como CVS?


"git no rastrea archivos" básicamente significa que las confirmaciones de git consisten en una instantánea del árbol de archivos que conecta una ruta en el árbol a un "blob" y un gráfico de confirmación que rastrea el historial de confirmaciones . Todo lo demás se reconstruye sobre la marcha mediante comandos como "git log" y "git blame". Esta reconstrucción puede explicarse a través de varias opciones lo difícil que debería ser buscar cambios basados ​​en archivos. La heurística predeterminada puede determinar cuándo un blob cambia de lugar en el árbol de archivos sin cambios, o cuando un archivo está asociado con un blob diferente al anterior. A los mecanismos de compresión que usa Git no les importa mucho los límites de blob / archivo. Si el contenido ya está en algún lugar, esto mantendrá pequeño el crecimiento del repositorio sin asociar los distintos blobs.

Ahora que es el repositorio. Git también tiene un árbol de trabajo, y en este árbol de trabajo hay archivos rastreados y sin seguimiento. Solo los archivos rastreados se graban en el índice (área de almacenamiento? ¿Caché?) Y solo lo que se rastrea allí se convierte en el repositorio.

El índice está orientado a archivos y hay algunos comandos orientados a archivos para manipularlo. Pero lo que termina en el repositorio es simplemente confirmaciones en forma de instantáneas del árbol de archivos y los datos de blob asociados y los ancestros de la confirmación.

Como Git no rastrea los historiales de archivos y los renombramientos y su eficiencia no depende de ellos, a veces hay que intentarlo varias veces con diferentes opciones hasta que Git produzca la historia / diferencias / culpas que le interesan para las historias no triviales.

Eso es diferente con los sistemas como Subversion que registran en lugar de reconstruir historias. Si no está registrado, no puedes escucharlo.

De hecho, construí un instalador diferencial al mismo tiempo que simplemente comparaba los árboles de lanzamiento al registrarlos en Git y luego producir un script que duplicaba su efecto. Como a veces se movían árboles enteros, esto producía instaladores diferenciales mucho más pequeños que sobrescribir / eliminar todo lo que se hubiera producido.


El bit confuso está aquí:

Git nunca los ve como archivos individuales. Git piensa todo como contenido completo.

Git a menudo utiliza hashes de 160 bits en lugar de objetos en su propio repositorio. Un árbol de archivos es básicamente una lista de nombres y hashes asociados con el contenido de cada uno (más algunos metadatos).

Pero el hash de 160 bits identifica de forma única el contenido (dentro del universo de la base de datos git). Así que un árbol con hashes como contenido incluye el contenido en su estado.

Si cambia el estado del contenido de un archivo, su hash cambia. Pero si su hash cambia, el hash asociado con el contenido del nombre del archivo también cambia. Lo que a su vez cambia el hash del "árbol de directorios".

Cuando una base de datos git almacena un árbol de directorios, ese árbol de directorios implica e incluye todo el contenido de todos los subdirectorios y todos los archivos que contiene.

Está organizado en una estructura de árbol con punteros (inmutables, reutilizables) a blobs u otros árboles, pero lógicamente es una instantánea única de todo el contenido de todo el árbol. La representación en la base de datos de git no es el contenido de datos planos, pero lógicamente es todos sus datos y nada más.

Si serializó el árbol a un sistema de archivos, eliminó todas las carpetas .git y le dijo a git que volviera a agregar el árbol a su base de datos, terminaría sin agregar nada a la base de datos; el elemento ya estaría allí.

Puede ser útil pensar en los hashes de git como un indicador de referencia para datos inmutables.

Si creó una aplicación alrededor de eso, un documento es un grupo de páginas, que tienen capas, que tienen grupos, que tienen objetos.

Cuando desee cambiar un objeto, debe crear un grupo completamente nuevo para él. Si desea cambiar un grupo, debe crear una nueva capa, que necesita una nueva página, que necesita un nuevo documento.

Cada vez que cambias un solo objeto, genera un nuevo documento. El documento antiguo sigue existiendo. El documento nuevo y antiguo comparte la mayor parte de su contenido: tienen las mismas páginas (excepto 1). Esa página tiene las mismas capas (excepto 1). Esa capa tiene los mismos grupos (excepto 1). Ese grupo tiene los mismos objetos (excepto 1).

Y por lo mismo, me refiero lógicamente a una copia, pero en cuanto a la implementación es solo otra referencia que apunta al puntero del mismo objeto inmutable.

Un git repo es muy parecido a eso.

Esto significa que un conjunto de cambios de git dado contiene su mensaje de confirmación (como un código hash), contiene su árbol de trabajo y contiene sus cambios principales.

Esos cambios principales contienen sus cambios principales, todo el camino de vuelta.

La parte del repositorio de git que contiene la historia es esa cadena de cambios. Esa cadena de cambios lo hace en un nivel por encima del árbol de "directorio"; desde un árbol de "directorio", no se puede obtener de forma única un conjunto de cambios y la cadena de cambios.

Para averiguar qué le sucede a un archivo, comience con ese archivo en un conjunto de cambios. Ese conjunto de cambios tiene una historia. A menudo, en esa historia, existe el mismo archivo nombrado, a veces con el mismo contenido. Si el contenido es el mismo, no hubo cambios en el archivo. Si es diferente, hay un cambio, y se debe trabajar para resolver exactamente qué.

A veces el archivo se ha ido; pero, el árbol del "directorio" puede tener otro archivo con el mismo contenido (el mismo código hash), por lo que podemos rastrearlo de esa manera (nota: esta es la razón por la que quiere un archivo de confirmación para mover un archivo separado de un mensaje de confirmación). -editar). O el mismo nombre de archivo, y después de verificar el archivo es lo suficientemente similar.

Así git puede remendar juntos un "historial de archivos".

Pero este historial de archivos proviene de un análisis eficiente del "conjunto de cambios completo", no de un enlace de una versión del archivo a otra.


El seguimiento del "contenido", por cierto, es lo que llevó a no rastrear directorios vacíos.
Es por eso que, si obtiene el último archivo de una carpeta, la carpeta se eliminará .

Ese no fue siempre el caso, y solo Git 1.4 (mayo de 2006) hizo cumplir esa política de "seguimiento de contenido" con el compromiso 443f833 :

estado de git: omita los directorios vacíos y agregue -u para mostrar todos los archivos sin seguimiento

De forma predeterminada, utilizamos --others --directory para mostrar directorios poco interesantes (para llamar la atención del usuario) sin su contenido (para despejar la salida).
Mostrar directorios vacíos no tiene sentido, así que pase --no-empty-directory cuando lo hagamos.

Al dar -u (o - sin --untracked ) se desactiva esta separación para permitir que el usuario obtenga todos los archivos sin seguimiento.

Eso se hizo eco años más tarde en enero de 2011 con el commit 8fe533 , Git v1.7.4:

Esto está de acuerdo con la filosofía general de la interfaz de usuario: git rastrea el contenido, no los directorios vacíos.

Mientras tanto, con Git 1.4.3 (septiembre de 2006), Git comienza a limitar el contenido no rastreado a carpetas no vacías, con el compromiso de 2074cb0 :

no debe enumerar los contenidos de directorios completamente sin seguimiento, sino solo el nombre de ese directorio (más un '' / '').

El seguimiento del contenido es lo que permitió a git culpar, muy pronto (Git 1.4.4, octubre de 2006, cometer cee7f24 ) tener un mejor rendimiento:

Más importante aún, su estructura interna está diseñada para soportar el movimiento de contenido (también conocido como cortar y pegar) más fácilmente al permitir que se tomen más de una ruta desde el mismo commit.

Eso (seguimiento del contenido) es también lo que puso git add en la API de Git, con Git 1.5.0 (diciembre de 2006, confirmación 366bfcb )

Haz de ''git add'' una interfaz fácil de usar de primera clase para el índice

Esto trae el poder del índice por adelantado utilizando un modelo mental adecuado sin hablar del índice en absoluto.
Vea, por ejemplo, cómo se ha evacuado toda la discusión técnica de la página de manual de git-add.

Cualquier contenido que se comprometa debe ser agregado juntos.
Si ese contenido proviene de nuevos archivos o archivos modificados no importa.
Solo tiene que "agregarlo", ya sea con git-add, o proporcionando git-commit con -a (para archivos ya conocidos, por supuesto).

Eso es lo que hizo que git add --interactive sea ​​posible, con el mismo Git 1.5.0 ( commit 5cde71d )

Después de realizar la selección, responda con una línea vacía para organizar el contenido de los archivos del árbol de trabajo para las rutas seleccionadas en el índice.

Por eso también, para eliminar recursivamente todos los contenidos de un directorio, debe pasar la opción -r , no solo el nombre del directorio como <path> (aún Git 1.5.0, confirme 9f95069 ).

Ver el contenido del archivo en lugar del archivo en sí es lo que permite un escenario de fusión como el que se describe en commit 1de70db (Git v2.18.0-rc0, abril de 2018)

Considere la siguiente fusión con un conflicto de renombrar / agregar:

  • lado A: modificar foo , agregar bar no relacionada
  • lado B: renombrar foo->bar (pero no modifique el modo o los contenidos)

En este caso, la combinación de tres vías del foo original, el foo de A y la bar de B dará como resultado la ruta de acceso deseada de la bar con el mismo modo / contenido que A tenía para foo .
Por lo tanto, A tenía el modo y los contenidos correctos para el archivo, y tenía el nombre de ruta correcto presente (a saber, bar ).

Commit 37b65ce , Git v2.21.0-rc0, diciembre de 2018, recientemente mejorado las resoluciones de conflictos en conflicto.
Y la confirmación bbafc9c muestra la importancia de considerar el contenido del archivo, al mejorar el manejo de los conflictos de cambio de nombre / cambio de nombre (2a1):

  • En lugar de almacenar archivos en collide_path~HEAD y collide_path~MERGE , los archivos se fusionan en dos direcciones y se graban en collide_path .
  • En lugar de registrar la versión del archivo renombrado que existía en el lado renombrado en el índice (ignorando así todos los cambios que se hicieron en el archivo en el lado de la historia sin cambiar el nombre), hacemos una combinación de contenido de tres vías en el renombrado camino, luego guárdelo en la etapa 2 o en la etapa 3.
  • Tenga en cuenta que dado que la combinación de contenido para cada cambio de nombre puede tener conflictos, y luego tenemos que combinar los dos archivos con el nuevo nombre, podemos terminar con marcadores de conflicto anidados.

En CVS, el historial se rastreaba por archivo. Una rama puede consistir en varios archivos con sus propias revisiones, cada una con su propio número de versión. CVS se basó en RCS ( Sistema de control de revisión ), que rastreaba archivos individuales de una manera similar.

Por otro lado, Git toma instantáneas del estado de todo el proyecto. Los archivos no son rastreados y versionados independientemente; una revisión en el repositorio se refiere a un estado de todo el proyecto, no a un archivo.

Cuando Git se refiere al seguimiento de un archivo, significa simplemente que debe incluirse en el historial del proyecto. La charla de Linus no se refería al seguimiento de archivos en el contexto de Git, sino que contrastaba el modelo CVS y RCS con el modelo basado en instantáneas utilizado en Git.


Estoy de acuerdo con Brian m. La respuesta de Carlson : Linus está distinguiendo, al menos en parte, entre sistemas de control de versiones orientados a archivos y sistemas de control de versiones. Pero creo que hay más que eso.

En mi libro , que está estancado y puede que nunca se termine, traté de encontrar una taxonomy para los sistemas de control de versiones. En mi taxonomía, el término para lo que nos interesa aquí es la atomicidad del sistema de control de versiones. Vea lo que está actualmente en la página 22. Cuando un VCS tiene atomicidad a nivel de archivo, de hecho hay un historial para cada archivo. El VCS debe recordar el nombre del archivo y lo que se le ocurrió en cada punto.

Git no hace eso. Git solo tiene un historial de confirmaciones: la confirmación es su unidad de atomicidad y la historia es el conjunto de confirmaciones en el repositorio. Lo que un compromiso recuerda son los datos, todo un árbol lleno de nombres de archivos y el contenido que acompaña a cada uno de esos archivos, además de algunos metadatos: por ejemplo, quién realizó el compromiso, cuándo y por qué, y el ID interno de Git hash. del commit del padre del commit. (Es este padre, y el gráfico de ciclo de lectura dirigido formado por la lectura de todas las confirmaciones y sus padres, es la historia en un repositorio).

Tenga en cuenta que un VCS puede estar orientado a compromisos, pero aún así almacenar datos archivo por archivo. Ese es un detalle de implementación, aunque a veces es importante, y Git tampoco lo hace. En su lugar, cada confirmación registra un árbol , con el objeto de árbol que codifica los nombres de archivo, los modos (es decir, ¿es este archivo ejecutable o no?) Y un puntero al contenido del archivo real . El contenido en sí se almacena de forma independiente, en un objeto blob . Al igual que un objeto de confirmación, un blob obtiene un ID de hash que es exclusivo de su contenido, pero a diferencia de un commit, que solo puede aparecer una vez, el blob puede aparecer en muchas confirmaciones. Por lo tanto, el contenido del archivo subyacente en Git se almacena directamente como un blob, y luego indirectamente en un objeto de árbol cuyo ID de hash se registra (directa o indirectamente) en el objeto de confirmación.

Cuando le pides a Git que te muestre el historial de un archivo usando:

git log [--follow] [starting-point] [--] path/to/file

Lo que Git realmente está haciendo es recorrer el historial de cometer , que es el único que tiene Git, pero no te muestra ninguno de estos comités a menos que:

  • el commit es un commit no-merge, y
  • el padre de ese compromiso también tiene el archivo, pero el contenido en el padre difiere, o el padre del compromiso no tiene el archivo en absoluto

(pero algunas de estas condiciones pueden modificarse mediante opciones de git log adicionales, y hay un efecto secundario muy difícil de describir llamado Simplificación de la historia que hace que Git omita algunas confirmaciones de la historia). El historial de archivos que ve aquí no existe exactamente en el repositorio, en cierto sentido: en cambio, es solo un subconjunto sintético de la historia real. ¡Obtendrá un "historial de archivos" diferente si usa diferentes opciones de git log !


Git no rastrea un archivo directamente, sino que realiza un seguimiento de las instantáneas del repositorio, y estas instantáneas consisten en archivos.

Aquí hay una forma de verlo.

En otros sistemas de control de versiones (SVN, Rational ClearCase), puede hacer clic derecho en un archivo y obtener su historial de cambios .

En Git, no hay un comando directo que haga esto. Vea esta pregunta . Te sorprenderás de cuántas respuestas diferentes hay. No hay una respuesta simple porque Git no solo rastrea un archivo , no de la forma en que SVN o ClearCase lo hacen.