Dans la première partie de cette mini-série Apprendre Git, on a découvert l’outil de contrôle de version Git.

On l’a installé, configuré brièvement et on a fait nos premiers commits. On a même vu comment restaurer des fichiers en cas de souci.

Dans cette deuxième partie de Apprendre Git, on va parler des branches.

Qu’est-ce qu’une branche ?

Les branches sont un concept fondamental de Git. Pour faire simple, une branche est simplement une version indépendante de votre projet avec son propre historique de commits.

Une branche se crée à partir d’un certain commit d’une certaine branche (en général le dernier) et permet de développer sa fonctionnalité en toute sécurité car les commits suivants que vous y ferez ne seront pas mélangés avec les commits de la branche principale ou des autres branches.

Vous pouvez créer des branches à partir de n’importe quelle autre branche. Un projet Git a donc une structure arborescente, comme le montre ce schéma.

Apprendre Git - Schéma illustrant l'arborescence des branches Git

Sur le schéma, on peut voir que la branche 1 a été créée à partir du commit B de la branche master, et la branche 2 a été crée à partir du commit C. La branche 2 aurait aussi pu être créée depuis le commit C1 de la branche 1, par exemple.

A partir de ces moments, les historiques de commits des branches divergent. La branche master contient les commits A, B, C et D, alors que la branche 1 contient les commits A, B, C1, D1, E1 et la branche 2 contient les commits A, B, C, D2. Aucune des deux branches 1 ou 2 ne contient le commit D.

Manipuler les branches avec git branch

Retournons sur notre projet, et voyons où nous en sommes avec git status.

`git status` montre l'état de notre dépôt

Notre copie de travail est propre.

Maintenant, imaginons que nous voulons tester plusieurs patterns de navigation. On ne veux pas que chaque essai pollue la branche master, on va donc créer une branche pour y développer notre fonctionnalité.

Pour créer une branche, on utilise simplement git branch [nom_de_ma_branche].

On va donc créer notre première branche :

git branch inline-navigation

Puis, on va lister les branches disponibles avec git branch --list ou le raccourci git branch -l ou encore git branch, tout simplement. Voilà ce qu’on obtient :

`git branch` a bien créé notre branche.

git branch est une commande assez simple qui permet de créer des branches ou de les lister, mais aussi de renommer des branches (avec -m, pour move) ou de supprimer des branches (avec -d pour delete).

On a bien créé notre branche. Par contre, vous remarquerez qu’on est toujours sur la branche master, qui a une petite étoile devant et qui est surlignée.

Si on modifie notre code, c’est sur la branche master qu’on préparera un commit. Nous, on veut travailler sur la branche inline-navigation.

Naviguer avec git checkout

Maintenant, on va basculer sur notre nouvelle branche en utilisant git checkout.

git checkout inline-navigation

Voilà, on est bien sur notre branche inline-navigation. Maintenant, on peut travailler en toute sécurité, en étant sûr de ne pas mettre notre code dans master, qui est normalement censé être la dernière version stable de notre code.

Alors, techniquement, on aurait pu commencer à travailler dans master, puis utiliser git branch et git checkout pour changer de branche. Nos modifications auraient été conservées dans la nouvelle branche.

Par contre, naviguer depuis une branche avec des modifications non-validées vers une autre branche existante est plus compliqué. Dans ce cas, Git nous prévient en nous demandant de remiser (mettre de côté avec git stash) ou abandonner nos modifications avant de changer.

Dans tous les cas, le plus prudent est quand même de changer de branche avant de commencer les développements pour éviter les commits accidentels.

A noter aussi : on peut utiliser un raccourci pour basculer dans une branche ET la créer simultanément : git checkout -b [nom-de-la-branche]. C’est très pratique.

Notre premier commit

On va simplement ajouter une liste de liens, qu’on va placer en display: inline-block;

<!-- in index.html -->
<!DOCTYPE html>
<html lang="fr">
<head>
...
</head>
<body>
    <main>
        <div class="wrapper">
            <h1>Introduction à Git</h1>
            <nav class="main-navigation">
                <ul>
                    <li><a href="/about">About</a></li>
                    <li><a href="/services">Services</a></li>
                    <li><a href="/contact">Contact</a></li>
                </ul>
            </nav>
        </div>
    </main>
</body>
</html>

h1 {
    text-align: center
}
nav li {
    display: inline-block;
}

On va valider ces changements.

git add .
git commit -m "Added basic inline navigation"

Naviguer avec git switch

Maintenant, vous pouvez vous amuser à basculer de la branche inline-navigation vers master et vice-versa, et voir votre code se mettre à jour automatiquement.

En changeant de branche, le code se met à jour.

Vous remarquerez que j’ai utilisé git switch et non git checkout. Techniquement, il n’y a pas de différence. Les deux commandes permettent de mettre à jour votre copie de travail et de basculer sur une autre branche.

En fait, git checkout permet de changer de branche ou de restaurer des fichiers. Pour plus de simplicité, les développeurs du logiciel ont décidé de diviser ces deux fonctionnalité en git switch et git restore dont on a parlé dans la première partie.

D’ailleurs, je n’utilise plus git checkout maintenant. J’utilise exclusivement git switch ou git restore.

En coulisses, git switch (ou git checkout) met à jour votre copie de travail au dernier commit de la branche de destination (la tête ou HEAD), et change de branche.

Fusionner vos branches avec git merge

Imaginons que nous avons testé plusieurs types de navigation dans plusieurs branches, et qu’au final, votre responsable vous annonce que c’est la navigation en ligne de la branche inline-navigation qui est validée.

Maintenant, on doit l’incorporer dans le code en production représenté par la branche master. On va donc fusionner notre branche inline-navigation dans notre branche master.

Pour faire cela, on se place dans la branche de destination master en utilisant git switch et on utilise la commande git merge pour fusionner une autre branche dans la branche courante.

git switch master
git merge inline-navigation
`git merge` fusionne une branche dans la branche courante.

git merge a réussi sans souci à fusionner nos deux branches. Git a détecté 7 lignes ajoutées dans index.html et 3 dans style.css. Aussi, la commande a automatiquement ajouté le commit de inline-navigation contenant toutes nos modifications, comme on peut le voir si on fait un petit git log.

`git log` montre bien le nouveau commit dans la branche `master`

Le log nous indique même quelle branche a été fusionnée, et quel(s) commit(s) a(ont) été appliqué(s).

Aucun changement n’a été fait sur la branche master pendant que nous travaillions sur inline-navigation, donc la fusion a pu se dérouler sans souci, car elle consistait simplement à ajouter des commits.

On peut représenter notre opération comme ceci :

La fusion a simplement ajouté les commits de `inline-navigation` à `master`

Ce type de fusion est appellé un fast-forward (avance-rapide). Vous pouvez d’ailleurs en voir la mention sur le résultat de la commande git merge.

Pour simplifier, on peut dire que notre branche master a “rattrapé” les commits qu’elle avait “en retard”. Par défaut Git va toujours essayer de faire un fast-forward quand vous allez tenter de fusionner.

Fusionner des changements dans les deux branches

Le fast-forward comme précédemment, c’est le cas le plus simple. Si vous travaillez seul sur un dépôt local comme on est en train de le faire, vous serez souvent dans cette situation. C’est-à-dire que tant que vous travaillez sur une branche, aucune modification n’est effectuée dans les autres.

Maintenant, si vous collaborez sur un projet à partir d’un dépôt distant, cette situation risque d’être plus rare.

En effet, vos collègues vont probablement faire la même chose que vous de leur côté : créer une branche à partir de master, faire leurs modifications et fusionner leur branche dans master. Voire travailler dans master directement !

Autrement dit, vos collègues ne vont pas attendre que vous ayez fini de travailler sur votre branche pour modifier ou corriger des petits bugs sur master.

On verra dans la troisième partie de cette série comment travailler avec les dépôts distants, ne vous inquiétez pas.

Pour le moment, on va imaginer que vous continuez de travailler sur votre navigation, mais qu’un autre collègue (ou vous!) a fait une modification sur master.

Plus précisément, il a ajouté un élément de menu: un lien vers la page produits.

<!-- in index.html -->
...
<nav class="main-navigation">
    <ul>
        <li><a href="/about">About</a></li>
        <li><a href="/products">Products</a></li> <!-- new -->
        <li><a href="/services">Services</a></li>
        <li><a href="/contact">Contact</a></li>
    </ul>
</nav>
...

Ce lien est donc présent dans le dernier commit (HEAD) de la branche master, mais pas dans celui de la branche inline-navigation.

Dans la branche inline-navigation, on va simplement ajouter quelques styles.

...
nav ul {
    text-align: center;
    padding: 0;
}
nav li {
    display: inline-block;
    text-transform: uppercase;
    font-size: larger;
    margin-left: 8px;
}
nav li:first-of-type {
    margin-left: 0;
}

Puis, on va valider ces changements. On a vu que pour ajouter tous nos fichiers modifiés en staging, on peut utiliser git add . .

Astuce ! On peut aussi combiner cet ajout au staging ET notre commit en une seule commande en utilisant les flags -am, comme ceci :

git commit -am "Added more navigation styles"
`git commit -am` permet de combiner l'ajout et le commit avec message

Maintenant, on a des modifications différentes dans master ET dans inline-navigation.

On va fusionner nos changements dans master. On se place donc dans master avec git switch et on va fusionner notre branche inline-navigation avec git merge.

Ici, `git merge` fusionne deux branches avec des modifications

Ici, git merge m’a demandé de donner un message de commit, car il a automatiquement crée un commit de fusion. Mon système m’a juste ouvert l’éditeur de texte nano pour éditer mon message.

Un commit a été créé et si on regarde l’état de notre code, on a bien notre nouvel élément de navigation ET nos nouveaux styles.

Tout a fonctionné sans souci !

Dans notre cas, Git a comparé les commits en tête des branches avec le commit ancêtre commun le plus proche, pour déterminer l’ordre et la nature des modifications. Il a détecté du code nouveau dans chacune des branches et a pu fusionner nos modifications sans souci, car ces modifications ne concernent pas les mêmes lignes dans les mêmes fichiers.

Vous remarquerez que Git nous annonce avoir utilisé une stratégie ort. C’est simplement la stratégie récursive par défaut. On ne parlera pas des stratégies dans cette série, car dans 99% des cas vous n’avez pas à vous en soucier. Git choisit toujours la meilleure automatiquement !

On peut schématiser cette opération comme ceci :

Schéma résumant une fusion classique

Ou alors comme ça :

Schéma résumant les opérations sur le code lors d'une fusion

Résoudre les conflits

Ces deux derniers cas n’ont pas posé de souci. Vu que les modifications effectuées dans les branches étaient bien distinctes.

Mais que se passe-t-il si les modifications dans les deux branches concernent le même code ?

Voyons ça maintenant. Imaginons que dans master, un collègue ait modifié le titre de la page en Débuter avec Git, et ait validé son changement.

De votre côté, vous avez continué à travailler sur votre navigation, mais avez aussi changé le titre en Apprendre Git.

On a donc le titre Débuter avec Git dans master et Apprendre Git dans inline-navigation.

Tentons la fusion. On se place sur la branche master, et on essaie de fusionner inline-navigation.

git switch master
git merge --no-commit inline-navigation

Ici, j’utilise le flag --no-commit pour expliciter à Git de ne pas créer le commit de fusion. On le fera nous-même. C’est utile quand on n’est pas sûr de notre coup ! Sinon, Git essaie automatiquement de mettre le résultat de la fusion en staging et de créer un commit.

`git merge` a échoué. On a un conflit dans `index.html`

Notre fusion a échoué !

Git a détecté un conflit dans index.html. Plus précisément, il a détecté des changements concernant la même ligne et ne peut pas décider automatiquement quelles sont les lignes de code à garder.

Dans ce cas, il nous faut nous-même résoudre le conflit. Pour nous aider, Git a ajouté des marqueurs dans le fichier en question. git diff nous montre l’état de notre fichier.

Le HEAD nous signale quel est le changement courant (dans la branche dans laquelle nous fusionnons, soit master), et inline-navigation nous signale le changement dans la branche que nous tentons de fusionner (les incoming changes ou changement entrants).

VSCode et son intégration avec Git est un peu plus clair grâce aux couleurs et nous permet même de régler le conflit très vite en cliquant sur les boutons Accept Current Change ou Accept Incoming Changes.

L'intégration VSCode nous surligne les changements et propose des boutons de résolution

Si vous ne pouvez pas résoudre le conflit, vous pouvez annuler la fusion avec :

git merge --abort

Git va alors restaurer votre copie de travail au dernier état propre connu. Sinon, vous pouvez soit cliquer le bouton pour accepter les changements dans VSCode, soit simplement éditer le fichier manuellement avec le bon titre.

Quand votre code est ok, vous pouvez poursuivre la fusion en créant votre commit. Si vous n’avez pas utilisé le flag --no-commit, vous pouvez aussi utiliser git merge --continue pour continuer la procédure automatique.

Après avoir réglé le conflit, on crée un commit.

Sans le flag --no-commit, Git tente de mettre en staging et créer automatiquement un commit. En cas de conflit, vous aurez surement des fichiers déjà en staging et vous aurez juste à vous occuper de vos conflits. Le commit est donc préparé pour vous.

Eviter les conflits

Vous voyez, ce n’était pas si compliqué !

Git nous aide beaucoup. Les messages du terminal sont clairs et il nous suggère toujours la marche à suivre. Si on suit ses instructions en général tout se passe bien.

Cependant, si vous collaborez sur le même dépôt, des conflits vont inévitablement arriver.

Pour les éviter ou les garder faciles à régler, pensez à mettre à jour votre code avant de travailler ! Votre dépôt local contient surement une version périmée du projet. Mettez votre branche principale à jour, puis créez une branche à partir de la dernière version.

Aussi, essayer de faire de petits commits. Essayez de ne pas travailler sur plusieurs choses à la fois. Si possible un commit doit correspondre à un seul changement, même s’il affecte plusieurs fichiers.

Conclusion

Apprendre Git n’est pas si compliqué si on procède pas à pas ! Dans la première partie la série, on s’est concentré sur les bases : initialiser le dépôt et faire nos premiers commits.

Dans cette partie 2, on a étudié le fonctionnement des branches. Comprendre les branches est essentiel pour bien travailler avec Git. Si vous avez bien compris ce qu’on a fait aujourd’hui, vous avez toutes les compétences nécessaires pour développer proprement vos projets locaux.

Dans la partie 3 de cette mini-série Apprendre Git, on va mettre en pratique tout ce qu’on a appris et voir comment utiliser Git pour collaborer et travailler à plusieurs sur des dépôts distants.