tite fractale

Les branches avec Git

1. Introduction

Cette activité fait suite à l’activité Pourquoi Git ? et aborde le concept de branche.

On y apprend :

2. Les branches

2.1. En bref

Pour un aperçu, voir l’article Wikipedia.

Attention : le concept d’arbre, ou tree en anglais, que nous avons vu au sujet de git n’est pas lié au concept de branche. Les sous-arbres d’un arbre ne sont pas des branches ! Un arbre est une structure récursive et un sous-arbre non réduit à une feuille (ici un fichier, ou un blob dans la terminologie de git) est aussi un arbre.

2.2. Cas d’utilisation

Les branches servent à distinguer des versions différentes du code source d’un même logiciel.

Avec git, la branche par défaut est master et est en général la branche principale d’un dépôt (ce qui ne veut pas dire grand chose). Ensuite, suivant l’organisation que l’équipe a décidé de mettre en place, on peut avoir :

Une équipe peut aussi décider de n’utiliser le nom master pour aucune des branches.

3. Travaux pratiques

3.1. Préliminaire : stockage des branches

$ mkdir test
$ cd test/
$ git init
Initialized empty Git repository in test/.git/
$ ls .git
HEAD  branches  config  description  hooks  info  objects  refs

Le fichier HEAD est en quelque sorte la tête de lecture principale du dépôt.

$ cat .git/HEAD
ref: refs/heads/master
$ ls .git/refs
heads  tags
$ ls .git/refs/heads/
$ git log
fatal: bad default revision 'HEAD'

Par défaut, cette tête de lecture pointe vers une autre tête de lecture : refs/heads/master, fichier qui contient la référence de la branche master (branche par défaut). Comme ce fichier n’existe pas (dépôt fraîchement initialisé), git attend d’avoir un premier commit pour pouvoir commencer à vraiment travailler.

$ echo bonjour > test.txt
$ git add test.txt
$ git cm "First commit."
[master (root-commit) 0cf7fe0] First commit.
 1 file changed, 1 insertion(+)
 create mode 100644 test.txt
Git nous empêche par défaut de commiter pour rien.
On peut forcer la création d’un commit vide en ajoutant --allow-empty à la commande git commit.

Après ce commit, plusieurs fichiers et répertoires sont créés, mais le plus intéressant ici est refs/heads/master.

$ ls .git/
COMMIT_EDITMSG  config          index           objects/
HEAD            description     info/           refs/
branches/       hooks/          logs/
$ ls .git/refs/heads/
master
$ cat .git/refs/heads/master
0cf7fe05cb39a3dca76f5423c3d2e27c35afb595
$ cat .git/logs/HEAD
000000000000000000... 0cf7fe05... unknown <profgra.org@gmail.com> ... First commit.

Le fichier refs/heads/master a été créé et contient le SHA-1 du commit courant. On voit que cette position est aussi archivée dans les logs.

Pour accéder à ces informations d’assez bas niveau, on peut utiliser les commandes :

Créons maintenant une branche.

$ git branch testbranch
$ ls .git/refs/heads/
master  testbranch
$ cat .git/refs/heads/testbranch
0cf7fe05cb39a3dca76f5423c3d2e27c35afb595
$ cat .git/HEAD
ref: refs/heads/master
$ git branch
* master
  testbranch

La branche testbranch a été créée (une tête de lecture en plus en quelque sorte). Elle pointe vers le commit en cours au moment de sa création. Nous sommes toujours sur master.

$ git checkout testbranch
Switched to branch 'testbranch'
$ cat .git/HEAD
ref: refs/heads/testbranch
$ git branch
  master
* testbranch

Lorsque que l’on change de branche, la tête de lecture principale se cale sur la tête de lecture testbranch.

Pour encore plus d’infos sur vos branches (SHA-1 et dernier message de commit), git branch -vv.

3.2. Mise en place

Ici on met en scène deux développeurs, nommés Dev1 et Dev2, qui travailleraient chacun sur un branche (nommées dev1 et dev2). Ce n’est jamais le cas dans la pratique (chaque développeur aurait son dépôt sur sa machine de travail), mais puisque l’architecture de git est très symétrique, la petite mécanique en jeu est exactement la même.

Il faut imaginer que le travail que nous allons faire sur les branches dev1 et dev2 peut se faire :

Tout dépend de la façon dont l’équipe de développeurs (parfois une seule personne) s’est organisée.

Avant de créer la moindre branche, créons un premier commit, mais pas sur n’importe quel fichier, sur test_compter.txt (si vous n’avez pas wget, essayez avec curl) :

$ mkdir test  # ou sur le dépôt précédent
$ cd test
$ git init
Initialized empty Git repository in /tmp/test/.git/
$ wget http://profgra.org/lycee/squelettes/test_compter.txt                  (git)-[master]
--2013-10-17 22:25:33--  http://profgra.org/lycee/squelettes/test_compter.txt
Resolving profgra.org (profgra.org)... 178.32.28.117, 2001:41d0:8:c936::1
Connecting to profgra.org (profgra.org)|178.32.28.117|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 42 [text/plain]
Saving to: `test_compter.txt'
$ git add test_compter.txt                                                   (git)-[master]
$ git cm "First commit."                                                     (git)-[master]
[master (root-commit) 6ce297e] First commit.
 1 file changed, 15 insertions(+)
 create mode 100644 test_compter.txt
$ git branch -vv
...

3.3. Première branche

$ git branch dev1                                                            (git)-[master]
$ git co dev1                                                                (git)-[master]
Switched to branch 'dev1'
$ git branch -vv
...

On édite le fichier :

 1 dev 1
 2 1
 3 2
 4 3         # changement ici (ne pas taper ces commentaires dans le fichier)
 5
 6 dev 2
 7 1
 8 2
 9
10 dev 3
11 1
12 2
13
14 tous
15 1
16 2
17 3          # changement là

Et on commit ces changements.

$ git add test_compter.txt                                                     (git)-[dev1]
$ git cm "dev1 3"                                                              (git)-[dev1]
[dev1 c13d655] dev1 3
 1 file changed, 2 insertions(+)
$ git branch -vv
...

C’est le moment de voir graphiquement ce qu’il se passe. Pour cela, plusieurs commandes :

premier commit de Dev1

3.4. Deuxième branche

Pour cette expérience, il faut la créer depuis master.

$ git co master                                                                (git)-[dev1]
Switched to branch 'master'
$ git co -b dev2               # git co -b crée la branche et nous y amène   (git)-[master]
Switched to a new branch 'dev2'
$ git branch                                                                   (git)-[dev2]
  dev1
* dev2
  master
$ git sl                                                                       (git)-[dev2]
6ce297e First commit.
$ git branch -vv
...

On édite le fichier :

 1 dev 1
 2 1
 3 2
 4
 5 dev 2
 6 1
 7 2
 8 3        # changement ici
 9
10 dev 3
11 1
12 2
13
14 tous
15 1
16 2
17 3         # changement là

Et on commit.

$ git add test_compter.txt                                                     (git)-[dev2]
$ git cm "dev2 3"                                                              (git)-[dev2]
[dev2 5eeaff6] dev2 3
 1 file changed, 2 insertions(+)
$ git branch -vv
...

C’est le moment de voir graphiquement ce qu’il se passe.

un commit pour chaque dev

3.5. Premier merge

git merge une_branche applique les changements des commits qui sont sur une_branche à la branche courante.

$ git merge master                                                             (git)-[dev2]
Already up-to-date.

Il n’y a rien de nouveau sur master qui ne serait pas dans dev2. Faisons plutôt le contraire : rapatrier sur master ce qui a été fait sur dev1.

$ git co master                                                                (git)-[dev2]
Switched to branch 'master'
$ git merge dev1                                                             (git)-[master]
Updating 6ce297e..c13d655
Fast-forward
test_compter.txt |    2 ++
 1 file changed, 2 insertions(+)
$ git branch -vv
...

Noter le fast-forward. Il est possible de l’empêcher en ajoutant --no-ff à la commande git merge.

C’est le moment de voir graphiquement ce qu’il se passe.

premier merge

Vérification du contenu :

$ cat test_compter.txt                                                       (git)-[master]
dev 1
1
2
3

dev 2
1
2

dev 3
1
2

tous
1
2
3

3.6. Deuxième merge

$ git merge dev2                                                             (git)-[master]
Auto-merging test_compter.txt
$ git branch -vv
...

Ici, git nous demande de rédiger un message, car le merge n’est pas trivial. Certains préfèrent toujours ajouter un commit de merge, pour marquer le coup (empêcher le fast-forward).

1 Merge branch 'dev2'
2
3 # Please enter a commit message to explain why this merge is necessary,
4 # especially if it merges an updated upstream into a topic branch.
5 #
6 # Lines starting with '#' will be ignored, and an empty message aborts
7 # the commit.

git ajoute donc un commit, à la fois spécial (deux parents), à la fois ordinaire.

Merge made by the 'recursive' strategy.
test_compter.txt |    1 +
 1 file changed, 1 insertion(+)

C’est le moment de voir graphiquement ce qu’il se passe.

deuxième merge

Vérification du contenu :

$ cat test_compter.txt                                                       (git)-[master]
dev 1
1
2
3

dev 2
1
2
3

dev 3
1
2

tous
1
2
3

Les changements dans les sections de chaque développeur ont été fusionnés, et le changement de la section commune ne pose pas de problème puisque les développeurs sont d’accord.

3.7. Troisième merge

On constate que Dev1 n’a pas encore récupéré les modifications de Dev2.

$ git co dev1                                                                (git)-[master]
Switched to branch 'dev1'
$ cat test_compter.txt                                                         (git)-[dev2]
dev 1
1
2
3

dev 2
1
2

dev 3
1
2

tous
1
2
3

Graphiquement, on en est là :

deuxième merge

Dev1 peut récupérer les modifications de Dev2 sans dialoguer directement avec la branche dev2, mais via master.

$ git merge master                                                             (git)-[dev1]
Updating c13d655..1c9faaf
Fast-forward
test_compter.txt |    1 +
 1 file changed, 1 insertion(+)
$ git branch -vv
...

Les commits de dev1 sont tous dans master. C’est le fast-forward.

troisième merge

Vérifions que Dev1 a ainsi récupèré les modifications de Dev2 :

$ cat test_compter.txt                                                         (git)-[dev1]
dev 1
1
2
3

dev 2
1
2
3

dev 3
1
2

tous
1
2
3

3.8. Conflit

3.8.1. Mise en place

Dev1 compte jusqu’à 4, mais « signe » cette action :

 1 dev 1
 2 1
 3 2
 4 3
 5 4         # changement ici
 6
 7 dev 2
 8 1
 9 2
10 3
11
12 dev 3
13 1
14 2
15
16 tous
17 1
18 2
19 3
20 4 dev1    # et changement là, avec une « signature »

Commit.

$ git add test_compter.txt                                                     (git)-[dev1]
$ git cm "dev1 4"                                                              (git)-[dev1]
[dev1 9195cf7] dev1 4
 1 file changed, 2 insertions(+)

Au tour de Dev2, de récupérer, et d’écrire une ligne conflictuelle.

$ git co dev2                                                                  (git)-[dev1]
Switched to branch 'dev2'
$ cat test_compter.txt                                                         (git)-[dev2]
dev 1
1
2

dev 2
1
2
3

dev 3
1
2

tous
1
2
3

Récupération :

$ git merge master                                                             (git)-[dev2]
Updating 5eeaff6..1c9faaf
Fast-forward
test_compter.txt |    1 +
 1 file changed, 1 insertion(+)
$ cat test_compter.txt                                                         (git)-[dev2]
dev 1
1
2
3

dev 2
1
2
3

dev 3
1
2

tous
1
2
3

Ligne conflictuelle :

 1 dev 1
 2 1
 3 2
 4 3
 5
 6 dev 2
 7 1
 8 2
 9 3
10 4           # pas celle-ci…
11
12 dev 3
13 1
14 2
15
16 tous
17 1
18 2
19 3
20 4 dev2      # mais celle-là.

Commit.

$ git add test_compter.txt                                                     (git)-[dev2]
$ git cm "dev2 4"                                                              (git)-[dev2]
[dev2 732a8e4] dev2 4
 1 file changed, 2 insertions(+)

C’est le moment de voir graphiquement l’état du dépôt :

situation avant le conflit

Nous allons fusionner le travail de Dev1 et de Dev2, sachant pertinemment que ce merge est voué au conflit :

$ git merge dev1                                                               (git)-[dev2]
Auto-merging test_compter.txt
CONFLICT (content): Merge conflict in test_compter.txt
Automatic merge failed; fix conflicts and then commit the result.
$                                                                            -[dev2|merge] u

Il est possible de configurer son terminal pour avoir les infos sur la droite de l’écran. Ici, on voit que le dépôt local est en situation de conflit.

Sinon, graphiquement :

situation pendant le conflit

Pour gérer ce conflit, il faut :

3.8.2. Édition

 1 dev 1
 2 1
 3 2
 4 3
 5 4
 6
 7 dev 2
 8 1
 9 2
10 3
11 4
12
13 dev 3
14 1
15 2
16
17 tous
18 1
19 2
20 3
21 <<<<<<< HEAD
22 4 dev2
23 =======
24 4 dev1
25 >>>>>>> dev1

On s’aperçoit que git a ajouté les marqueurs <<<<<<< et >>>>>>> (avec les références en jeu juste avant le conflit) et ======= pour séparer les zones.

Il faut supprimer ces marqueurs (qui ne correspondent à aucune instruction dans un langage courant) et décider :

Ici le commit du bas opère la fusion :

print("bonjoure")
       +
       |\
       | +--------------+
       |                |
       +                +
print("bonjour")   log("bonjoure")
       +                +
       |                |
       + +--------------+
       |/
       +
log("bonjour")  # fusion difficile à automatiser

Ici, on choisit la dernière option :

 1 dev 1
 2 1
 3 2
 4 3
 5 4
 6
 7 dev 2
 8 1
 9 2
10 3
11 4
12
13 dev 3
14 1
15 2
16
17 tous
18 1
19 2
20 3
21 4 dev1 et dev2, ensemble

Sinon, il y a la commante git mergetool, qui lance un éditeur adapté à la résolution de conflits (si disponible sur la machine).

3.8.3. Validation

$ git add test_compter.txt                                                   -[dev2|merge] u
$ git commit                                                                 -[dev2|merge] u

Ici git va proposer un message standard, que l’on peut agrémenter d’un commentaire si besoin.

 1 Merge branch 'dev1' into dev2
 2
 3 Conflicts:
 4     test_compter.txt
 5 #
 6 # It looks like you may be committing a merge.
 7 # If this is not correct, please remove the file
 8 #   .git/MERGE_HEAD
 9 # and try again.
10
11
12 # Please enter the commit message for your changes. Lines starting
13 # with '#' will be ignored, and an empty message aborts the commit.
14 # On branch dev2
15 # Changes to be committed:
16 #
17 #   modified:   test_compter.txt
18 #

git crée finalement un commit pour ce merge.

[dev2 ea94de7] Merge branch 'dev1' into dev2
$                                                                              (git)-[dev2]

C’est le moment de voir graphiquement ce qu’il s’est passé.

situation une fois le conflit résolu

4. Utiliser les branches

4.1. Statégies

Comme dit plus haut dans la section Cas d’utilisation, la stratégie de gestion des branches est propre à chaque équipe. À défaut de stratégie évoluée, en voici une simple :

  1. si votre dépôt n’est pas le dépôt principal, la branche master suit le dépôt principal,
  2. les autres branches peuvent :
    • vous servir à mettre en place une nouvelle fonctionnalité, par exemple en vue de la fermeture d’une demande, d’un ticket (en anglais une issue),
    • maintenir une ancienne version,
    • suivre le dépôt d’un collègue,

Il est conseillé de ne jamais travailler directement sur master, mais de créer une branche pour toute activité. Aussi, en l’absence de gestion fine de droits sur un dépôt, s’il n’y a pas de mainteneur et si toute l’équipe peut commiter, il est possible de commiter d’une branche d’un dépôt local vers la branche master du dépôt principal. Voir la section Quelques commandes ci-dessous.

4.2. Quelques commandes

4.2.1. Schéma général

     origin   A---B---C (master)    -+          <--+
    (online)                         |             |
                                     |   fetch     | push origin ma_branche:master
                                     | + merge     |
    dev n°1   A---B---C (master)  <--+             | (rebase préalable si besoin)
    (local)        \                               |
                    D---E (ma_branche)    ---------+

    dev n°2   A---B---C (master)
    (local)        \                     IDEM avec origin (ou autre dev⁽¹⁾)
                    F---G (typos)

(1) C’est un des avantage des systèmes distribués, décentralisés.

4.2.2. Commit

$ git clone http://profgra.org/git/test.git
Cloning into 'test'...
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
$ cd test
$ git co -b testbranch                                (git)-[master]
Switched to a new branch 'testbranch'
$ echo tout le monde >> test.txt                  (git)-[testbranch]
$ git add test.txt                                (git)-[testbranch]
$ git cm "Commit sur testbranch."                 (git)-[testbranch]
[testbranch 27d7a12] Commit sur testbranch.
 1 file changed, 1 insertion(+)

4.2.3. Push depuis une branche

Voici la commande pour pousser :

$ git push origin testbranch:master               (git)-[testbranch]
Counting objects: 5, done.
Writing objects: 100% (3/3), 275 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
To http://profgra.org/git/test.git
   9f2db14..27d7a12  testbranch -> master
porte avec inscription PUSH

4.2.4. Fetch puis Merge, ou Pull

Ensuite, la branche master du dépôt local récupère le travail sur testbranch via le dépôt principal. Cela assure que le transfert vers le dépôt principal s’est bien déroulé et que la branche master locale est bien synchronisée avec la branche master distante.

$ git co master
Switched to branch 'master'
$ git pull origin master  # pull car on est sûr que tout se passe bien
From http://profgra.org/git/test
 * branch            master     -> FETCH_HEAD
Updating 742b3cf..1211be2
Fast-forward
 test.txt |    1 +
 1 file changed, 1 insertion(+)

Bien sûr, si vous ne travaillez jamais directement sur la branche locale master elle n’avancera qu’à coup de fast-forwards.

porte avec inscription PULL

4.2.5. Rebase

La première phrase de la page du manuel est un peu obscure :

git-rebase - Forward-port local commits to the updated upstream head

Le passage intéressant est recopié ici :

Assume the following history exists and the current branch is "topic":

      A---B---C topic
     /
D---E---F---G master

From this point, the result of either of the following commands:

git rebase master
git rebase master topic

would be:

              A'--B'--C' topic
             /
D---E---F---G master

4.2.6. Approfondissement

Pour approfondir ces concepts, voici un lien important.

Sinon, une anecdote :




Christophe Gragnic, le 02/12/2020, 22h09'30".






Page générée le 27/05/2021, 09h06'59" (source).
historique de la page
historique global