Que veut dire Linus Torvalds quand il dit que Git ne suit «jamais» un fichier?

284

Citant Linus Torvalds lorsqu'on lui a demandé combien de fichiers Git peut gérer lors de son Tech Talk chez Google en 2007 (43:09):

… Git suit votre contenu. Il ne suit jamais un seul fichier. Vous ne pouvez pas suivre un fichier dans Git. Ce que vous pouvez faire, c'est que vous pouvez suivre un projet qui a un seul fichier, mais si votre projet a un seul fichier, faites-le et vous pouvez le faire, mais si vous suivez 10000 fichiers, Git ne les voit jamais comme des fichiers individuels. Git pense que tout est le contenu complet. Toute l'histoire de Git est basée sur l'histoire de l'ensemble du projet…

(Transcriptions ici .)

Pourtant, quand vous plongez dans le livre Git , la première chose que vous avez dit est qu'un fichier dans Git peut être soit suivi ou non suivi . De plus, il me semble que toute l'expérience de Git est orientée vers la gestion des versions de fichiers. Lors de l'utilisation git diffou la git statussortie est présentée par fichier. Lors de l'utilisation, git addvous pouvez également choisir sur une base par fichier. Vous pouvez même consulter l'historique sur une base de fichiers et c'est rapide comme l'éclair.

Comment interpréter cette affirmation? En termes de suivi de fichiers, en quoi Git est-il différent des autres systèmes de contrôle de source, tels que CVS?

Simón Ramírez Amaya
la source
20
reddit.com/r/git/comments/5xmrkv/what_is_a_snapshot_in_git - "Car là où vous en êtes en ce moment, je pense qu'il est plus important de réaliser qu'il y a une différence entre la façon dont Git présente les fichiers aux utilisateurs et la façon dont il les traite en interne . Tel qu'il est présenté à l'utilisateur, un instantané contient des fichiers complets, pas seulement des différences. Mais en interne, oui, Git utilise des différences pour générer des fichiers pack qui stockent efficacement les révisions. " (Ceci contraste fortement avec, par exemple. Subversion.)
user2864740
5
Git ne suit pas les fichiers, il suit les changements . La plupart des systèmes de contrôle de version suivent les fichiers. Pour illustrer comment / pourquoi cela peut être important, essayez d'archiver un répertoire vide pour git (spolier: vous ne pouvez pas, car c'est un ensemble de modifications "vide").
Elliott Frisch le
12
@ElliottFrisch Cela ne sonne pas bien. Votre description est plus proche de ce que fait par exemple Darcs . Git stocke des instantanés, pas des changesets.
melpomene
4
Je pense qu'il veut dire que Git ne suit pas un fichier directement. Un fichier comprend son nom et son contenu. Git suit le contenu sous forme de blobs. Étant donné un blob uniquement, vous ne pouvez pas dire quel est son nom de fichier correspondant. Il peut s'agir du contenu de plusieurs fichiers avec des noms différents sous des chemins différents. Les liaisons entre un nom de chemin et un blob sont décrites dans un objet arborescent.
ElpieKay
3
Connexes: suivi de Randal Schwartz de la conversation de Linus (également une conférence de Google Tech) - "... Ce que Git est vraiment tout ... Linus a dit ce que Git n'est PAS".
Peter Mortensen

Réponses:

316

Dans CVS, l'historique a été suivi par fichier. Une branche peut être constituée de divers fichiers avec leurs propres révisions, chacune avec son propre numéro de version. CVS était basé sur RCS ( Revision Control System ), qui suivait les fichiers individuels de la même manière.

D'un autre côté, Git prend des instantanés de l'état de l'ensemble du projet. Les fichiers ne sont pas suivis et versionnés indépendamment; une révision dans le référentiel fait référence à un état de l'ensemble du projet, pas à un fichier.

Lorsque Git fait référence au suivi d'un fichier, cela signifie simplement qu'il doit être inclus dans l'historique du projet. Le discours de Linus ne faisait pas référence au suivi des fichiers dans le contexte Git, mais contrastait le modèle CVS et RCS avec le modèle basé sur l'instantané utilisé dans Git.

bk2204
la source
4
Vous pouvez ajouter que c'est pourquoi dans CVS et Subversion, vous pouvez utiliser des balises comme $Id$dans un fichier. La même chose ne fonctionne pas dans git, car la conception est différente.
gerrit
58
Et le contenu n'est pas lié à un fichier comme vous vous en doutez. Essayez de déplacer 80% du code d'un fichier vers un autre. Git détecte automatiquement un déplacement de fichier + 20% de changement, même lorsque vous venez de déplacer du code dans des fichiers existants.
allo
13
@allo En tant qu'effet secondaire, git peut faire une chose que les autres ne peuvent pas: lorsque deux fichiers sont fusionnés et que vous utilisez "git blame -C", git peut regarder les deux historiques. Dans le suivi basé sur des fichiers, vous devez choisir lequel des fichiers originaux est le véritable original, et les autres lignes semblent toutes neuves.
Izkata
1
@allo, Izkata - Et c'est l' entité interrogatrice qui fonctionne tout cela en analysant le contenu du référentiel au moment de la requête (historiques de validation et différences entre les arbres référencés et les blobs), plutôt que d'exiger que l' entité engageante et son utilisateur humain spécifient ou synthétisent correctement ces informations au moment de la validation - ni le développeur de l'outil de mise en pension pour concevoir et implémenter cette capacité et le schéma de métadonnées correspondant avant le déploiement de l'outil. Torvalds a fait valoir qu'une telle analyse ne fera que s'améliorer avec le temps, et toute l' histoire de chaque dépôt git depuis le premier jour en bénéficiera.
Jeremy
1
@allo Yep, et pour souligner le fait que git ne fonctionne pas au niveau d'un fichier, vous n'avez même pas à valider toutes les modifications dans un fichier à la fois; vous pouvez valider des plages de lignes arbitraires tout en laissant d'autres modifications dans le fichier en dehors de la validation. Bien sûr, l'interface utilisateur n'est pas aussi simple, donc la plupart ne le font pas, mais elle a rarement ses utilisations.
Alvin Thompson
103

Je suis d'accord avec Brian m. Réponse de Carlson : Linus fait en effet la distinction, au moins en partie, entre les systèmes de contrôle de version orientés fichier et orientés commit. Mais je pense qu'il y a plus que cela.

Dans mon livre , qui est au point mort et qui pourrait ne jamais être terminé, j'ai essayé de trouver une taxonomie pour les systèmes de contrôle de version. Dans ma taxonomie, le terme qui nous intéresse ici est l' atomicité du système de contrôle de version. Voir ce qui est actuellement page 22. Lorsqu'un VCS a une atomicité au niveau fichier, il y a en fait un historique pour chaque fichier. Le VCS doit se souvenir du nom du fichier et de ce qui lui est arrivé à chaque point.

Git ne fait pas ça. Git n'a qu'un historique de validations - la validation est son unité d'atomicité et l'historique est l'ensemble des validations dans le référentiel. Ce dont un commit se souvient, ce sont les données - toute une arborescence remplie de noms de fichiers et le contenu qui accompagne chacun de ces fichiers - plus quelques métadonnées: par exemple, qui a fait le commit, quand et pourquoi, et l'ID de hachage Git interne du commit parent du commit. (C'est ce parent, et le graphique d'acyclisme dirigé formé en lisant tous les commits et leurs parents, qui est l'historique dans un référentiel.)

Notez qu'un VCS peut être axé sur la validation, tout en stockant les données fichier par fichier. C'est un détail d'implémentation, bien que parfois important, et Git ne le fait pas non plus. Au lieu de cela, chaque validation enregistre une arborescence , avec l'objet arborescent encodant les noms de fichiers , les modes (c'est-à-dire, est-ce que ce fichier est exécutable ou non?), Et un pointeur vers le contenu réel du fichier . Le contenu lui-même est stocké indépendamment, dans un objet blob . Comme un objet commit, un blob obtient un ID de hachage unique à son contenu, mais contrairement à un commit, qui ne peut apparaître qu'une seule fois, le blob peut apparaître dans de nombreux commit. Ainsi, le contenu du fichier sous-jacent dans Git est stocké directement en tant qu'objet blob, puis indirectement dans un objet arborescent dont l'ID de hachage est enregistré (directement ou indirectement) dans l'objet commit.

Lorsque vous demandez à Git de vous montrer l'historique d'un fichier en utilisant:

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

ce que fait Git, c'est de parcourir l' historique des validations , qui est la seule que Git possède, mais sans vous montrer aucune de ces validations à moins que:

  • la validation est une validation sans fusion, et
  • le parent de ce commit a également le fichier, mais le contenu du parent diffère, ou le parent du commit n'a pas du tout le fichier

(mais certaines de ces conditions peuvent être modifiées via des git logoptions supplémentaires , et il y a un effet secondaire très difficile à décrire appelé Simplification de l'historique qui fait que Git omet complètement certains commits de l'historique). L'historique des fichiers que vous voyez ici n'existe pas exactement dans le référentiel, dans un certain sens: au lieu de cela, c'est juste un sous-ensemble synthétique de l'historique réel. Vous obtiendrez un "historique de fichier" différent si vous utilisez différentes git logoptions!

torek
la source
Une autre chose à ajouter est que cela permet à Git de faire des choses comme des clones peu profonds. Il suffit de récupérer le commit de tête et tous les blobs auxquels il se réfère. Il n'a pas besoin de recréer des fichiers en appliquant des ensembles de modifications.
Wes Toleman
@WesToleman: cela facilite définitivement les choses. Mercurial stocke des deltas, avec des réinitialisations occasionnelles, et bien que les gens de Mercurial aient l'intention d'y ajouter des clones peu profonds (ce qui est possible en raison de l'idée de "réinitialisation"), ils ne l'ont pas encore fait (car il s'agit davantage d'un défi technique).
Torek
@torek J'ai un doute concernant votre description de la réponse de Git à une demande d'historique de fichier, mais je pense qu'elle mérite sa propre question: stackoverflow.com/questions/55616349/…
Simón Ramírez Amaya
@torek Merci pour le lien vers votre livre, je n'ai rien vu de tel.
gnarledRoot
17

Le bit déroutant est ici:

Git ne les voit jamais comme des fichiers individuels. Git pense que tout est le contenu complet.

Git utilise souvent des hachages 160 bits à la place des objets dans son propre référentiel. Une arborescence de fichiers est essentiellement une liste de noms et de hachages associés au contenu de chacun (plus quelques métadonnées).

Mais le hachage 160 bits identifie de manière unique le contenu (dans l'univers de la base de données git). Ainsi, un arbre avec des hachages en tant que contenu inclut le contenu dans son état.

Si vous modifiez l'état du contenu d'un fichier, son hachage change. Mais si son hachage change, le hachage associé au contenu du nom de fichier change également. Ce qui à son tour modifie le hachage de "l'arborescence de répertoires".

Lorsqu'une base de données git stocke une arborescence de répertoires, cette arborescence de répertoires implique et inclut tout le contenu de tous les sous-répertoires et de tous les fichiers qu'il contient .

Il est organisé dans une arborescence avec des pointeurs (immuables, réutilisables) vers des blobs ou d'autres arbres, mais il s'agit logiquement d'un instantané unique du contenu entier de l'arborescence entière. La représentation dans la base de données git n'est pas le contenu des données plates, mais logiquement, ce sont toutes ses données et rien d'autre.

Si vous sérialisiez l'arborescence dans un système de fichiers, supprimiez tous les dossiers .git et demandiez à git de rajouter l'arborescence dans sa base de données, vous finiriez par n'ajouter rien à la base de données - l'élément serait déjà là.

Il peut être utile de considérer les hachages de git comme un pointeur compté par référence vers des données immuables.

Si vous avez construit une application autour de cela, un document est un tas de pages, qui ont des couches, des groupes, des objets.

Lorsque vous souhaitez modifier un objet, vous devez créer un groupe entièrement nouveau pour lui. Si vous voulez changer un groupe, vous devez créer un nouveau calque, qui a besoin d'une nouvelle page, qui a besoin d'un nouveau document.

Chaque fois que vous modifiez un seul objet, il génère un nouveau document. L'ancien document continue d'exister. Le nouveau et l'ancien document partagent la plupart de leur contenu - ils ont les mêmes pages (sauf 1). Cette page a les mêmes couches (sauf 1). Cette couche a les mêmes groupes (sauf 1). Ce groupe a les mêmes objets (sauf 1).

Et par même, je veux dire logiquement une copie, mais en termes d'implémentation, c'est juste un autre pointeur compté par référence vers le même objet immuable.

Un dépôt git est un peu comme ça.

Cela signifie qu'un ensemble de modifications git donné contient son message de validation (comme un code de hachage), il contient son arbre de travail et il contient ses modifications parentes.

Ces modifications parentales contiennent leurs modifications parentales, tout le long du chemin.

La partie du dépôt git qui contient l' histoire est cette chaîne de changements. Cette chaîne de modifications le situe à un niveau supérieur à l'arborescence "répertoire" - à partir d'une arborescence "répertoire", vous ne pouvez pas accéder de manière unique à un ensemble de modifications et à la chaîne de modifications.

Pour savoir ce qui arrive à un fichier, vous commencez avec ce fichier dans un ensemble de modifications. Ce changeset a une histoire. Souvent dans cet historique, le même fichier nommé existe, parfois avec le même contenu. Si le contenu est le même, aucun changement n'a été apporté au fichier. Si c'est différent, il y a un changement et il faut travailler pour trouver exactement quoi.

Parfois, le fichier a disparu; mais, l'arborescence "répertoire" peut avoir un autre fichier avec le même contenu (même code de hachage), donc nous pouvons le suivre de cette façon (remarque; c'est pourquoi vous voulez un commit-to-move un fichier séparé d'un commit-to -Éditer). Ou le même nom de fichier, et après avoir vérifié le fichier est assez similaire.

Ainsi, git peut patchwork ensemble un "historique de fichiers".

Mais cet historique de fichier provient d'une analyse efficace de l'ensemble des modifications, et non d'un lien d'une version du fichier à une autre.

Yakk - Adam Nevraumont
la source
12

« git ne suit pas les fichiers » essentiellement signifie que les commits de git se composent d'un instantané d'arborescence de fichiers connectant un chemin dans l'arbre à un « blob » et un commit graphique de suivi de l'histoire de commits . Tout le reste est reconstruit à la volée par des commandes comme "git log" et "git blame". Cette reconstruction peut être informée via diverses options de la difficulté à rechercher des modifications basées sur des fichiers. L'heuristique par défaut peut déterminer quand un blob change de place dans l'arborescence de fichiers sans changement, ou quand un fichier est associé à un autre blob qu'auparavant. Les mécanismes de compression utilisés par Git ne se soucient pas beaucoup des limites de blob / fichier. Si le contenu est déjà quelque part, cela gardera la croissance du référentiel petite sans associer les différents blobs.

Maintenant, c'est le référentiel. Git a également une arborescence de travail, et dans cette arborescence de travail il y a des fichiers suivis et non suivis. Seuls les fichiers suivis sont enregistrés dans l'index (zone de transfert? Cache?) Et seul ce qui y est suivi en fait le référentiel.

L'index est orienté fichier et il existe des commandes orientées fichier pour le manipuler. Mais ce qui finit dans le référentiel, ce ne sont que les validations sous forme d'instantanés d'arborescence de fichiers et les données d'objets blob associées et les ancêtres de la validation.

Étant donné que Git ne suit pas les historiques et les renommages de fichiers et que son efficacité ne dépend pas d'eux, vous devez parfois essayer plusieurs fois avec différentes options jusqu'à ce que Git produise l'historique / les différences / les reproches qui vous intéressent pour les historiques non triviaux.

C'est différent avec des systèmes comme Subversion qui enregistrent plutôt que reconstruisent des histoires. Si ce n'est pas enregistré, vous ne pouvez pas en entendre parler.

J'ai en fait construit un installateur différentiel à un moment donné qui vient de comparer les arbres de versions en les archivant dans Git puis en produisant un script dupliquant leur effet. Étant donné que parfois des arbres entiers ont été déplacés, cela a produit des installateurs différentiels beaucoup plus petits que l'écrasement / la suppression de tout aurait produit.


la source
7

Git ne suit pas directement un fichier, mais suit les instantanés du référentiel, et ces instantanés se composent de fichiers.

Voici une façon de voir les choses.

Dans d'autres systèmes de contrôle de version (SVN, Rational ClearCase), vous pouvez cliquer avec le bouton droit sur un fichier et obtenir son historique des modifications .

Dans Git, aucune commande directe ne fait cela. Voir cette question . Vous serez surpris du nombre de réponses différentes. Il n'y a pas de réponse simple car Git ne suit pas simplement un fichier , pas de la même manière que SVN ou ClearCase.

Double Vision Stout Fat Heavy
la source
5
Je pense que je comprends ce que vous essayez de dire, mais "Dans Git, il n'y a pas de commandement direct qui fait cela" est directement contredit par les réponses à la question que vous avez liée. S'il est vrai que la gestion des versions se produit au niveau de l'ensemble du référentiel, il existe généralement de nombreuses façons de réaliser quoi que ce soit dans Git, donc avoir plusieurs commandes pour afficher l'historique d'un fichier n'est pas une évidence.
Joe Lee-Moyet
J'ai survolé les premières réponses à la question que vous avez liée et toutes utilisent git logou un programme construit en plus (ou un alias qui fait la même chose). Mais même s'il y avait beaucoup de façons différentes, comme Joe le dit, cela est également vrai pour afficher l'historique des succursales. (également git log -p <file>intégré et fait exactement cela)
Voo
Êtes-vous sûr que SVN stocke en interne les modifications par fichier? Je ne l'ai pas utilisé depuis un certain temps déjà, mais je me souviens vaguement avoir des fichiers nommés comme des identifiants de version, plutôt que de refléter la structure des fichiers du projet.
Artur Biesiadowski
3

Par ailleurs, le suivi du «contenu» a conduit à ne pas suivre les répertoires vides.
C'est pourquoi, si vous git rm le dernier fichier d'un dossier, le dossier lui-même est supprimé .

Ce n'était pas toujours le cas, et seul Git 1.4 (mai 2006) a appliqué cette politique de "suivi du contenu" avec commit 443f833 :

git status: ignorer les répertoires vides et ajouter -u pour afficher tous les fichiers non suivis

Par défaut, nous utilisons --others --directorypour afficher des répertoires sans intérêt (pour attirer l'attention de l'utilisateur) sans leur contenu (pour désencombrer la sortie).
Afficher des répertoires vides n'a pas de sens, alors passez --no-empty-directoryquand nous le faisons.

Donner -u(ou --untracked) désactive ce désencombrement pour permettre à l'utilisateur d'obtenir tous les fichiers non suivis.

Cela a été repris des années plus tard en janvier 2011 avec le commit 8fe533 , Git v1.7.4:

Ceci est conforme à la philosophie générale de l'interface utilisateur: git suit le contenu, pas les répertoires vides.

En attendant, avec Git 1.4.3 (septembre 2006), Git commence à limiter le contenu non suivi aux dossiers non vides, avec commit 2074cb0 :

il ne doit pas répertorier le contenu des répertoires complètement non suivis, mais uniquement le nom de ce répertoire (plus un ' /' de fin).

Le suivi du contenu est ce qui a permis à git de blâmer très tôt (Git 1.4.4, oct. 2006, commit cee7f24 ) pour être plus performant:

Plus important encore, sa structure interne est conçue pour prendre en charge plus facilement le mouvement de contenu (alias couper-coller) en permettant à plusieurs chemins d'être empruntés à partir du même commit.

Cela (suivi du contenu) est également ce qui a mis git add dans l'API Git, avec Git 1.5.0 (décembre 2006, commit 366bfcb )

faire de 'git add' une interface conviviale de première classe pour l'index

Cela amène la puissance de l'index à l'avant en utilisant un modèle mental approprié sans parler de l'indice du tout.
Voir par exemple comment toute la discussion technique a été évacuée de la page de manuel git-add.

Tout contenu à engager doit être ajouté ensemble.
Que ce contenu provienne de nouveaux fichiers ou de fichiers modifiés n'a pas d'importance.
Vous avez juste besoin de "l'ajouter", soit avec git-add, soit en fournissant git-commit avec -a(pour les fichiers déjà connus bien sûr).

C'est ce qui a rendu git add --interactivepossible, avec le même Git 1.5.0 ( commit 5cde71d )

Après avoir fait la sélection, répondez avec une ligne vide pour mettre en scène le contenu des fichiers d'arborescence de travail pour les chemins sélectionnés dans l'index.

C'est aussi pourquoi, pour supprimer récursivement tout le contenu d'un répertoire, vous devez passer l' -roption, pas seulement le nom du répertoire comme <path>(toujours Git 1.5.0, commit 9f95069 ).

Voir le contenu du fichier au lieu du fichier lui-même est ce qui permet un scénario de fusion comme celui décrit dans commit 1de70db (Git v2.18.0-rc0, avr.2018)

Envisagez la fusion suivante avec un conflit de changement de nom / ajout:

  • côté A: modifier foo, ajouter sans rapportbar
  • face B: renommer foo->bar(mais ne pas modifier le mode ou le contenu)

Dans ce cas, la fusion à trois voies de foo d'origine, foo de A et B barse traduira par un nom de chemin souhaité baravec le même mode / contenu que A avait pour foo.
Ainsi, A avait le bon mode et le bon contenu pour le fichier, et il avait le bon chemin d'accès présent (à savoir, bar).

Commit 37b65ce , Git v2.21.0-rc0, décembre 2018, a récemment amélioré les résolutions de conflits en collision.
Et commit bbafc9c firther illustre l'importance de considérer le contenu des fichiers , en améliorant la gestion des conflits renommer / renommer (2to1):

  • Au lieu de stocker des fichiers dans collide_path~HEADet collide_path~MERGE, les fichiers sont fusionnés et enregistrés dans deux sens collide_path.
  • Au lieu d'enregistrer la version du fichier renommé qui existait du côté renommé dans l'index (ignorant ainsi toutes les modifications apportées au fichier du côté de l'historique sans le renommer), nous effectuons une fusion de contenu à trois voies sur le renommé chemin, puis stockez-le à l'étape 2 ou à l'étape 3.
  • Notez que puisque la fusion de contenu pour chaque renommage peut avoir des conflits, puis que nous devons fusionner les deux fichiers renommés, nous pouvons nous retrouver avec des marqueurs de conflit imbriqués.
VonC
la source