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 diff
ou la git status
sortie est présentée par fichier. Lors de l'utilisation, git add
vous 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?
la source
Réponses:
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.
la source
$Id$
dans un fichier. La même chose ne fonctionne pas dans git, car la conception est différente.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:
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:
(mais certaines de ces conditions peuvent être modifiées via des
git log
options 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érentesgit log
options!la source
Le bit déroutant est ici:
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.
la source
« 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
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.
la source
git log
ou 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. (égalementgit log -p <file>
intégré et fait exactement cela)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 :
Cela a été repris des années plus tard en janvier 2011 avec le commit 8fe533 , Git v1.7.4:
En attendant, avec Git 1.4.3 (septembre 2006), Git commence à limiter le contenu non suivi aux dossiers non vides, avec commit 2074cb0 :
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:
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 )
C'est ce qui a rendu
git add --interactive
possible, avec le même Git 1.5.0 ( commit 5cde71d )C'est aussi pourquoi, pour supprimer récursivement tout le contenu d'un répertoire, vous devez passer l'
-r
option, 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)
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):
la source