Citant Linus Torvalds lorsqu'on lui a demandé combien de fichiers Git peut gérer pendant 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, lorsque vous plongez dans le livre Git , la première chose qu'on vous dit est qu'un fichier dans Git peut être 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 de git diff
ou git status
la sortie est présentée par fichier. Lors de l'utilisation de git add
vous pouvez également choisir 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?
Dans CVS, l'historique a été suivi par fichier. Une branche peut être constituée de différents 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.
Je suis d'accord avec réponse de brian m. Carlson : Linus fait en effet une 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 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 commits - le commit est son unité d'atomicité, et l'historique est l'ensemble des commits 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 du commit du commit. (C'est ce parent, et le graphique d'acyclisme dirigé formé en lisant tous les commits et leurs parents, que est l'historique dans un référentiel.)
Notez qu'un VCS peut être orienté commit, tout en conservant 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 commit enregistre une arborescence , avec le nom du fichier de codage des objets arborescents , modes (c.-à-d., ce fichier est-il 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 Git fait vraiment, c'est parcourir l'historique de commit , qui est le seul historique de Git, mais pas montrant vous l'un de ces commits sauf:
(mais certaines de ces conditions peuvent être modifiées via des options git log
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 certaines validations de la marche 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 options git log
!
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). Donc, un arbre avec des hachages comme contenu inclut le conten 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 logiquement, il s'agit 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 de référence compté 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'historique est cette chaîne de changements. Cette chaîne de modifications le fait à 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 des changements.
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.
"git ne suit pas les fichiers" signifie fondamentalement que les validations de git consistent en un instantané d'arborescence de fichiers reliant un chemin dans l'arborescence à un "blob" et un graphique de validation retraçant l'historique 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 enregistre plutôt que reconstruit les historiques. 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.
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.
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 443f8 :
git status: ignorer les répertoires vides et ajouter -u pour afficher tous les fichiers non suivis
Par défaut, nous utilisons
--others --directory
Pour afficher les répertoires sans intérêt (pour attirer l'attention de l'utilisateur) sans leur contenu (pour désencombrer la sortie).
L'affichage de répertoires vides n'a pas de sens, alors passez--no-empty-directory
Lorsque 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é répété des années plus tard en janvier 2011 avec commit 8fe5 , 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 2074cb :
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, octobre 2006, commit cee7f24 ) être plus performant:
Plus important encore, sa structure interne est conçue pour prendre en charge le contenu le mouvement (alias couper-coller) plus facilement en permettant à plus d'un chemin d'être emprunté le même commit.
C'est ce (suivi du contenu) qui a également 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 le contenu provienne de nouveaux fichiers ou de fichiers modifiés n'a pas d'importance.
Il vous suffit de "l'ajouter", soit avec git-add, soit en fournissant git-commit avec-a
(Pour les fichiers déjà connus seulement bien sûr).
C'est ce qui a rendu possible git add --interactive
, 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'option -r
, 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: modifiez
foo
, ajoutez des éléments indépendantsbar
- côté B: renommez
foo->bar
(mais ne modifiez pas le mode ou le contenu)Dans ce cas, la fusion à trois voies du foo d'origine, du foo de A et du
bar
de B entraînera un chemin d'accès souhaité debar
avec le même mode/contenu que A avait pourfoo
.
Ainsi, A avait le bon mode et le bon contenu pour le fichier, et il avait le bon chemin d'accès présent (à savoirbar
).
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 du fichier , en améliorant la gestion de renommer/renommer (2to1) conflits:
- Au lieu de stocker des fichiers dans
collide_path~HEAD
Etcollide_path~MERGE
, Les fichiers sont fusionnés dans les deux sens et enregistrés danscollide_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.