La gestion de versions avec Git
Ce que ce chapitre apporte
- Expliquer ce qu'apporte un gestionnaire de versions et ce que la copie de dossiers ne permet pas.
- Distinguer les trois zones d'un dépôt Git et le rôle de chacune.
- Décrire ce qu'est un commit et pourquoi l'historique forme un graphe.
- Employer les branches et expliquer ce que calcule une fusion.
- Reconnaître un conflit, comprendre son origine et le résoudre.
- Distinguer un dépôt local d'un dépôt distant, et les opérations associées.
- Choisir un message de commit et un découpage utiles.
projet_v2_final_corrigé ne répondent à aucune de ces trois questions. Ce chapitre présente le modèle de Git, qui y répond, et les opérations qu'il faut comprendre avant de retenir des commandes.
Ce que résout un gestionnaire de versions
Un gestionnaire de versions enregistre l'évolution d'un ensemble de fichiers au cours du temps, en conservant chaque état, son auteur, sa date et sa justification.
Trois besoins le motivent.
Le premier est la traçabilité. Une ligne de code étrange se comprend souvent en retrouvant quand elle a été écrite et pourquoi. Sans historique, l'information est perdue.
Le deuxième est le retour en arrière. Une modification qui casse la production doit pouvoir être annulée en quelques secondes, sans reconstituer l'état antérieur de mémoire.
Le troisième est le travail parallèle. Plusieurs personnes modifient les mêmes fichiers ; il faut réunir leurs travaux sans que l'un écrase l'autre, et savoir quand ce n'est pas possible automatiquement.
Git est distribué : chaque poste possède une copie complète de l'historique. On peut consulter, valider et créer des branches hors ligne. Le serveur ne sert qu'à la synchronisation, et il n'est pas la seule copie.
Les trois zones
C'est le point qui déroute au départ, et tout le reste en découle.
| Zone | Contenu | Commande pour y passer |
|---|---|---|
| Répertoire de travail | les fichiers tels qu'on les édite | modification directe |
| Index, ou zone d'attente | ce qui entrera dans le prochain commit | git add |
| Dépôt local | l'historique des commits | git commit |
Sans lui, un commit contiendrait toujours l'intégralité des modifications en cours, et l'historique deviendrait illisible.
Le commit et le graphe
Un commit enregistre un état complet du projet, accompagné de son auteur, de sa date, d'un message, et d'une ou plusieurs références vers ses parents.
Ce sont les parents qui font la structure. Un commit ordinaire a un parent, ce qui donne une chaîne. Un commit de fusion en a deux, ce qui donne un graphe orienté sans cycle. L'historique n'est donc pas une liste, et beaucoup de confusions viennent de là.
L'identifiant d'un commit est une empreinte calculée sur son contenu, ses parents et ses métadonnées. Deux conséquences : l'identifiant ne peut pas être choisi, et modifier un commit ancien change tous ses descendants, puisque leur empreinte dépend de la sienne.
Les branches
Une branche est un simple pointeur nommé vers un commit. Créer une branche ne copie rien.
Cette légèreté explique l'usage courant : on crée une branche par correctif ou par fonctionnalité, on y travaille, on la fusionne, on la supprime. Le pointeur avance automatiquement à chaque commit.
Ce que calcule une fusion
Fusionner deux branches suppose de trouver leur ancêtre commun le plus récent, puis de comparer chaque branche à cet ancêtre. Git applique ensuite les deux ensembles de modifications. Il n'y a conflit que lorsque les deux branches ont modifié les mêmes lignes du même fichier.
La résolution appartient donc au développeur. Il ouvre le fichier, choisit ou combine, retire les marqueurs, indexe le fichier et termine la fusion. Rien ne se répare tout seul, et rien n'est cassé.
<<<<<<< HEAD
int delai = 30;
=======
int delai = 60;
>>>>>>> correctif-log
Les marqueurs délimitent la version de la branche courante, puis celle de la branche fusionnée. Le fichier reste dans cet état tant que quelqu'un n'a pas tranché.
Un conflit se résout en comprenant les deux intentions. Quand elles s'excluent, la discussion avec l'autre auteur coûte moins cher que le défaut.
Dépôt local et dépôt distant
Le dépôt distant n'a rien de particulier : c'est un dépôt comme les autres, désigné par un nom, généralement origin.
| Opération | Effet |
|---|---|
git clone | copie un dépôt distant en local, historique compris |
git fetch | récupère les commits distants sans toucher au travail en cours |
git pull | fetch suivi d'une fusion dans la branche courante |
git push | envoie les commits locaux vers le distant |
rebase ou commit --amend, changent les empreintes. Sur des commits restés locaux, l'opération est sans risque et souvent utile pour ranger son travail avant de le publier.Sur des commits déjà poussés et récupérés par d'autres, elle crée deux versions divergentes de la même histoire. Les collègues se retrouvent avec des commits en double et des fusions incompréhensibles.
Le découpage et les messages
Un historique ne vaut que par sa lisibilité. Deux habitudes suffisent à la produire.
Un commit, une intention. Un commit qui corrige un défaut et reformate deux cents lignes empêche de lire la correction, et rend impossible son annulation isolée.
Un message qui dit pourquoi. Le diff montre déjà ce qui a changé. Ce qu'il ne montre pas, et ce qu'on cherchera dans six mois, c'est la raison du changement.
correction bug n'apprend rien. On sait déjà qu'il s'agit d'une correction, et le diff dit laquelle.Corrige la perte du dernier enregistrement lors d'une écriture concurrente permet de retrouver ce commit, de comprendre le problème, et de savoir s'il concerne le défaut qu'on est en train d'étudier.
Un fichier .gitignore complète le dispositif en excluant ce qui n'a pas à être versionné : produits de compilation, dépendances téléchargées, fichiers de configuration locale, secrets. Un dépôt qui contient un mot de passe le contient pour toujours, y compris après suppression, puisque l'historique conserve tout.
Exercices type
Quelle différence entre git add et git commit ?
git add place une modification dans l'index, la zone qui prépare le prochain commit. Rien n'est encore enregistré dans l'historique.
git commit transforme le contenu de l'index en un commit, avec auteur, date et message.
L'intérêt de cette séparation est de composer un commit qui ne reprend qu'une partie du travail en cours. On peut avoir modifié cinq fichiers et n'en enregistrer que deux, pour séparer deux intentions différentes.
Pourquoi l'historique est-il un graphe et non une liste ?
Parce qu'un commit de fusion possède deux parents. La ligne d'histoire se sépare quand on crée une branche et se rejoint quand on la fusionne.
Cette structure est un graphe orienté sans cycle : les arêtes vont de l'enfant vers ses parents, et l'on ne peut pas remonter à soi-même.
C'est ce qui permet à Git de calculer une base commune entre deux branches, opération impossible sur une simple liste.
Deux développeurs modifient la même ligne. Que fait Git ?
Il signale un conflit et interrompt la fusion, en insérant dans le fichier des marqueurs qui délimitent les deux versions.
Il ne choisit pas, parce qu'aucune règle automatique ne peut décider laquelle des deux intentions doit l'emporter.
Le développeur ouvre le fichier, décide, retire les marqueurs, indexe le fichier résolu et achève la fusion. Si les deux modifications avaient porté sur des lignes différentes du même fichier, Git aurait fusionné sans rien demander.
Pourquoi ne pas faire un seul gros commit en fin de journée ?
Parce qu'il devient inutilisable pour tout ce à quoi sert un historique.
On ne peut pas annuler une partie du travail sans annuler le reste. On ne peut pas retrouver quand un défaut a été introduit, puisque le commit contient tout. On ne peut pas relire la modification, car le diff mélange plusieurs sujets.
Le découpage utile suit les intentions, pas le temps : un commit par correction, par fonctionnalité, par renommage.
Un mot de passe a été commité puis supprimé au commit suivant. Le dépôt est-il propre ?
Non. L'historique conserve tous les états : le commit qui contenait le mot de passe existe toujours, et quiconque possède le dépôt peut le lire.
La suppression au commit suivant ne fait que retirer le fichier de l'état courant.
Deux actions sont nécessaires, dans cet ordre. Considérer le secret comme compromis et le changer, car il a pu être cloné. Puis, éventuellement, réécrire l'historique pour l'en retirer, opération lourde qui suppose que tout le monde reprenne son dépôt.
La prévention coûte moins cher : un .gitignore sur les fichiers de configuration, et les secrets hors du dépôt.
À quoi sert une branche si l'on travaille seul ?
À isoler une modification incertaine. Si l'essai ne donne rien, on supprime la branche et il ne reste aucune trace dans la branche principale.
Elle permet aussi de traiter une urgence sans abandonner le travail en cours : on repart de la branche principale, on corrige, on livre, puis on revient.
Enfin, elle sépare les intentions dans l'historique. Deux fonctionnalités développées en parallèle sur la même branche produisent des commits entrelacés qu'on ne peut plus ni relire ni annuler séparément.
La méthode
- Regarde
git statusavant toute opération. La plupart des erreurs viennent d'un état mal identifié. - Compose tes commits par intention, en t'aidant de l'index plutôt qu'en enregistrant tout.
- Écris le pourquoi dans le message, le diff dit déjà le quoi.
- Ouvre une branche par correctif ou par fonctionnalité, et supprime-la après fusion.
- Lis les deux versions avant de résoudre un conflit, et discute quand elles s'excluent.
- Ne réécris jamais un historique publié.
- Tiens un
.gitignoredès le premier commit, et garde les secrets hors du dépôt.
En résumé
- Un gestionnaire de versions répond à trois besoins : traçabilité, retour en arrière, travail parallèle.
- Git est distribué : chaque poste détient l'historique complet.
- Trois zones : répertoire de travail, index, dépôt local. L'index permet de choisir ce qu'on enregistre.
- Un commit référence ses parents, ce qui fait de l'historique un graphe orienté sans cycle.
- Une branche est un pointeur vers un commit. Sa création ne copie rien.
- Une fusion compare les deux branches à leur ancêtre commun le plus récent.
- Un conflit survient quand les mêmes lignes ont changé des deux côtés. Il se résout à la main.
- Réécrire un historique déjà publié crée deux versions divergentes de la même histoire.
- L'historique conserve tout : un secret commité reste lisible après suppression.
Et ensuite ? Le code versionné doit s'exécuter quelque part. Le chapitre suivant décrit l'environnement d'exécution que suppose le reste du bloc : la plateforme .NET.