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


283

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 rastrear un archivo en Git. Lo que puede hacer es rastrear un proyecto que tiene un solo archivo, pero si su proyecto tiene un solo archivo, asegúrese de hacerlo y puede hacerlo, pero si rastrea 10,000 archivos, Git nunca los verá como archivos individuales. Git piensa todo como el contenido completo. Toda la historia en Git se basa en la historia de todo el proyecto ...

(Transcripciones aquí ).

Sin embargo, cuando se sumerge en el libro de Git , lo primero que le dicen es que un archivo en Git puede ser o bien un seguimiento o sin seguimiento . Además, me parece que toda la experiencia de Git está orientada a la versión de archivos. Cuando se usa git diffo la git statussalida se presenta por archivo. Al usarlo git add, también puede elegir por archivo. Incluso puede revisar el historial en un archivo y es muy rápido.

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


20
reddit.com/r/git/comments/5xmrkv/what_is_a_snapshot_in_git - "Por dónde estás en este momento, sospecho que lo más importante es darse cuenta de que hay una diferencia entre cómo Git presenta los archivos a los usuarios y cómo los trata internamente . Tal como se presentó al usuario, una instantánea contiene archivos completos, no solo diffs. Pero internamente, sí, Git usa diffs para generar paquetes que almacenan revisiones de manera eficiente ". (Esto es un fuerte contraste con, por ejemplo, Subversion.)
usuario2864740

55
Git no rastrea archivos, rastrea conjuntos de cambios . La mayoría de los sistemas de control de versiones rastrean archivos. Como ejemplo de cómo / por qué esto puede importar, intente registrar un directorio vacío para git (spolier: no puede, porque ese es un conjunto de cambios "vacío").
Elliott Frisch

12
@ElliottFrisch Eso no suena bien. Su descripción está más cerca de lo que, por ejemplo, hace Darcs . Git almacena instantáneas, no conjuntos de cambios.
melpomene

44
Creo que quiere decir que Git no rastrea un archivo directamente. Un archivo incluye su nombre y contenido. Git rastrea los contenidos como blobs. Solo con un blob, no se puede saber cuál es su nombre de archivo correspondiente. Podría ser el contenido de múltiples archivos con diferentes nombres bajo diferentes rutas. Los enlaces entre un nombre de ruta y un blob se describen en un objeto de árbol.
ElpieKay

3
Relacionado: Randal Schwartz ' seguimiento de la charla de Linus (también una charla de Google Tech) - "... De qué se trata Git realmente ... Linus dijo lo que Git NO es".
Peter Mortensen

Respuestas:


316

En CVS, el historial se rastreó por archivo. Una rama puede constar de varios archivos con sus propias revisiones, cada una con su propio número de versión. CVS se basó en RCS ( Revision Control System ), que rastreó archivos individuales de 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 al estado de todo el proyecto, no a un archivo.

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


44
Podría agregar que es por eso que en CVS y Subversion, puede usar etiquetas como $Id$en un archivo. Lo mismo no funciona en git, porque el diseño es diferente.
gerrit

58
Y el contenido no está vinculado a un archivo como cabría esperar. Intente mover el 80% del código de un archivo a otro. Git detecta automáticamente un movimiento de archivo + 20% de cambio, incluso cuando acaba de mover el código en los archivos existentes.
allo

13
@allo Como efecto secundario de eso, git puede hacer una cosa que los demás no pueden hacer: cuando dos archivos se fusionan y usas "git blame -C", git puede mirar hacia abajo en ambas historias. En el seguimiento basado en archivos, debe elegir cuál de los archivos originales es el original real, y todas las demás líneas aparecen completamente nuevas.
Izkata

1
@allo, Izkata: y es la entidad de consulta la que resuelve todo esto analizando el contenido del repositorio en el momento de la consulta (historiales de confirmación y diferencias entre árboles y blobs a los que se hace referencia), en lugar de exigir que la entidad de compromiso y su usuario humano especifiquen o sinteticen correctamente esta información en el momento de la confirmación, ni el desarrollador de la herramienta de repositorio para diseñar e implementar esta capacidad y el esquema de metadatos correspondiente antes de implementar la herramienta. Torvalds argumentó que dicho análisis solo mejorará con el tiempo, y toda la historia de cada repositorio git desde el primer día se beneficiará.
Jeremy

1
@allo Sí, y para recordar el hecho de que git no funciona a nivel de archivo, ni siquiera tiene que confirmar todos los cambios en un archivo a la vez; puede confirmar rangos arbitrarios de líneas mientras deja otros cambios en el archivo fuera de la confirmación. Por supuesto, la interfaz de usuario para eso no es tan simple, así que la mayoría no lo hace, pero rara vez tiene sus usos.
Alvin Thompson

103

Estoy de acuerdo con brian m. La respuesta de Carlson : Linus realmente distingue, al menos en parte, entre los sistemas de control de versiones orientados a archivos y los de confirmación. Pero creo que hay más que eso.

En mi libro , que está estancado y puede que nunca termine, traté de encontrar una taxonomía 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 actualmente es la página 22. Cuando un VCS tiene una 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 commits: el commit es su unidad de atomicidad, y el historial es el conjunto de commits en el repositorio. Lo que recuerda un commit son los datos, un árbol completo 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 commit, cuándo y por qué, y el ID de hash interno de Git del commit del padre commit. (Es este padre, y el gráfico de aciclismo dirigido formado al leer todos los commits y sus padres, ese es el historial en un repositorio).

Tenga en cuenta que un VCS puede estar orientado a la confirmación, pero aún así almacenar datos archivo por archivo. Es un detalle de implementación, aunque a veces es importante, y Git tampoco lo hace. En cambio, cada confirmación registra un árbol , con el objeto del árbol que codifica nombres de archivos , modos (es decir, ¿este archivo es ejecutable o no?), Y un puntero al contenido real del archivo . El contenido en sí se almacena de forma independiente, en un objeto blob . Al igual que un objeto commit, un blob obtiene una identificación hash que es exclusiva de su contenido, pero a diferencia de un commit, que solo puede aparecer una vez, el blob puede aparecer en muchos commits. Entonces, el contenido del archivo subyacente en Git se almacena directamente como un blob, y luego indirectamente en un objeto de árbol cuya 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 confirmaciones , que es el único historial que tiene Git, pero no mostrarle ninguna de estas confirmaciones a menos que:

  • el commit es un commit sin fusión, y
  • el padre de ese compromiso también tiene el archivo, pero el contenido en el padre es diferente, o el padre del compromiso no tiene el archivo en absoluto

(pero algunas de estas condiciones se pueden modificar a través de git logopciones adicionales , y hay un efecto secundario muy difícil de describir llamado Simplificación del historial que hace que Git omita algunas confirmaciones del recorrido del historial por completo). El historial de archivos que ve aquí no existe exactamente en el repositorio, en cierto sentido: en cambio, es solo un subconjunto sintético del historial real. ¡Obtendrá un "historial de archivos" diferente si usa diferentes git logopciones!


Otra cosa para agregar es que esto le permite a Git hacer cosas como clones poco profundos. Solo necesita recuperar el commit principal y todos los blobs a los que se refiere. No necesita recrear archivos aplicando conjuntos de cambios.
Wes Toleman

@WesToleman: definitivamente lo hace más fácil. Mercurial almacena deltas, con reinicios ocasionales, y aunque la gente de Mercurial intenta agregar clones poco profundos allí (lo cual es posible debido a la idea de "reinicio"), aún no lo han hecho (porque es más un desafío técnico).
torek

@torek Tengo una duda con respecto a su descripción sobre Git respondiendo a una solicitud de historial de archivos, pero creo que merece su propia pregunta: stackoverflow.com/questions/55616349/…
Simón Ramírez Amaya

@torek Gracias por el enlace a tu libro, no he visto nada igual.
GnarledRoot

17

Lo confuso está aquí:

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

Git a menudo usa 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 exclusiva el contenido (dentro del universo de la base de datos git). Entonces, 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 del contenido completo de todo el árbol. La representación en la base de datos git no es el contenido de datos planos, pero lógicamente son todos sus datos y nada más.

Si serializa el árbol en un sistema de archivos, elimina todas las carpetas .git y le dice a git que agregue el árbol nuevamente 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 puntero contado de referencia a datos inmutables.

Si creó una aplicación en torno a 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 cambia un solo objeto, genera un nuevo documento. El documento antiguo sigue existiendo. El documento nuevo y el antiguo comparten 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 términos de implementación es solo otro puntero contado de referencia al mismo objeto inmutable.

Un repositorio git se parece mucho a eso.

Esto significa que un conjunto de cambios 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 regreso.

La parte del repositorio de git que contiene la historia es esa cadena de cambios. Esa cadena de cambios lo hace en un nivel superior al árbol de "directorio": desde un árbol de "directorio", no se puede acceder de forma exclusiva a un conjunto de cambios y a 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 ese historial, existe el mismo archivo con nombre, 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 determinar exactamente qué.

A veces el archivo se ha ido; pero, el árbol de "directorio" podría tener otro archivo con el mismo contenido (mismo código hash), por lo que podemos rastrearlo de esa manera (nota; es por eso que desea un commit-to-move un archivo separado de un commit-to -editar). O el mismo nombre de archivo, y después de verificar el archivo es lo suficientemente similar.

Entonces git puede juntar un "historial de archivos".

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


12

"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 se puede decir a través de varias opciones de 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 cuándo un archivo está asociado con un blob diferente al anterior. Los mecanismos de compresión que usa Git no se preocupan mucho por los límites de blob / archivo. Si el contenido ya está en algún lugar, esto mantendrá el crecimiento del repositorio pequeño sin asociar los diversos blobs.

Ahora ese es el repositorio. Git también tiene un árbol de trabajo, y en este árbol de trabajo hay archivos rastreados y no rastreados. Solo los archivos rastreados se registran en el índice (área de almacenamiento provisional? Caché?) Y solo lo que se rastrea allí ingresa al repositorio.

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

Dado que Git no rastrea los historiales de archivos y los cambios de nombre y su eficiencia no depende de ellos, a veces debe intentarlo varias veces con diferentes opciones hasta que Git produzca el historial / diferencias / culpas que le interesan para historiales no triviales.

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

Realmente construí un instalador diferencial en un momento que solo comparó los árboles de lanzamiento al registrarlos en Git y luego producir un script que duplica su efecto. Como a veces se movieron árboles enteros, esto produjo instaladores diferenciales mucho más pequeños que sobrescribir / eliminar todo lo que habría producido.


7

Git no rastrea un archivo directamente, pero rastrea instantáneas del repositorio, y estas instantáneas consisten en archivos.

Aquí hay una manera 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. Ver esta pregunta . Se sorprenderá de cuántas respuestas diferentes hay. No hay una respuesta simple porque Git no solo rastrea un archivo , no de la manera en que lo hace SVN o ClearCase.


55
Creo que entiendo lo que estás tratando de decir, pero "en Git, no hay un comando directo que haga esto" se contradice directamente con las respuestas a la pregunta a la que te has vinculado. Si bien es cierto que el control de versiones ocurre a nivel de todo el repositorio, generalmente hay muchas formas de lograr cualquier cosa en Git, por lo que tener múltiples comandos para mostrar el historial de un archivo no es evidencia de mucho.
Joe Lee-Moyet

Leí las primeras respuestas de la pregunta que vinculaste y todas usan git logo algún programa construido sobre eso (o algún alias que hace lo mismo). Pero incluso si hubiera muchas formas diferentes, como Joe dice, eso también es cierto para mostrar el historial de la sucursal. (también git log -p <file>está integrado y hace exactamente eso)
Voo

¿Estás seguro de que SVN almacena internamente los cambios por archivo? No lo he usado en algún tiempo, pero recuerdo vagamente tener archivos nombrados como identificadores de versión, en lugar de reflejar la estructura del archivo del proyecto.
Artur Biesiadowski

3

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

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

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

Por defecto, utilizamos --others --directorypara 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 pasa --no-empty-directorycuando lo hagamos.

Dar -u(o --untracked) deshabilita este desorden para permitir que el usuario obtenga todos los archivos no rastreados.

Eso se hizo eco años después, en enero de 2011, con commit 8fe533 , Git v1.7.4:

Esto está de acuerdo con la filosofía general de la interfaz de usuario: git rastrea contenido, no 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 commit 2074cb0 :

no debe enumerar el contenido de directorios completamente no rastreados, sino solo el nombre de ese directorio (más un ' /').

El seguimiento del contenido es lo que permitió a git culpar, desde el principio (Git 1.4.4, octubre de 2006, commit cee7f24 ) sea más eficiente :

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.

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

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

Esto trae el poder del índice por adelantado usando un modelo mental apropiado 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 a comprometerse debe agregarse juntos.
No importa si ese contenido proviene de archivos nuevos o modificados.
Solo necesita "agregarlo", ya sea con git-add, o proporcionando git-commit con -a(para archivos ya conocidos, por supuesto).

Eso es lo que hizo git add --interactiveposible, con el mismo Git 1.5.0 ( commit 5cde71d )

Después de hacer 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.

Esa es también la razón por la cual, para eliminar recursivamente todo el contenido de un directorio, debe pasar la -ropción, no solo el nombre del directorio como <path>(todavía Git 1.5.0, commit 9f95069 ).

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

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

  • lado A: modificar foo, agregar no relacionadobar
  • lado B: renombrar foo->bar(pero no modificar el modo o el contenido)

En este caso, las tres vías de combinación de foo original, foo de A y B barse traducirá en una ruta deseada de barlos mismos de modo que A / contenidos tenía para foo.
Por lo tanto, A tenía el modo y el contenido correctos para el archivo, y tenía el nombre de ruta correcto presente (es decir, bar).

Commit 37b65ce , Git v2.21.0-rc0, diciembre de 2018, recientemente mejoró la resolución de conflictos en conflicto.
Y commit bbafc9c firther ilustra la importancia de considerar el contenido del archivo , al mejorar el manejo de los conflictos rename / rename (2to1):

  • En lugar de almacenar archivos en collide_path~HEADy collide_path~MERGE, los archivos se combinan 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í los cambios que se hicieron al archivo en el lado del historial sin el cambio de nombre), hacemos una fusión de contenido de tres vías en el renombrado ruta, luego almacene eso en la etapa 2 o la etapa 3.
  • Tenga en cuenta que, dado que la fusión de contenido para cada cambio de nombre puede tener conflictos, y luego tenemos que fusionar los dos archivos renombrados, podemos terminar con marcadores de conflicto anidados.
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.