Cette activité fait suite à l’activité Pourquoi Git ? et aborde le concept de branche.
On y apprend :
git
gère les branches,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.
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.
$ 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
--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 :
git rev-parse
qui prend en argument un peu n’importe quoi et essaie d’en
faire un SHA-1. Essayer par exemple git rev-parse master
,git reflog
qui affiche proprement les logs.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
.
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
...
$ 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 :
gitk --all
pour visualiser avec gitk
, un vrai programme dans une
vraie fenêtre graphique (voir l’image ci-dessous),git log
mais avec des options particulières permettant de visualiser en
Text User Interface :git log --all --color --decorate --graph --oneline
git lg
, qui est un alias défini dans le
squelette .gitconfig.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.
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.
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
$ 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.
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.
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à :
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.
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
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 :
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 :
Pour gérer ce conflit, il faut :
git add
le fichier conflictuel,git commit
pour valider le merge
. 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.
<<<<<<<
et =======
, on trouve ce que le développeur qui gère le
merge pensait commiter.=======
et >>>>>>>
, on trouve ce que le dépôt a reçu à la place,
depuis la dernière version commune.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).
$ 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é.
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 :
master
suit le
dépôt principal,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.
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.
$ 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(+)
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
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.
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
Pour approfondir ces concepts, voici un lien important.
Sinon, une anecdote :