Git Branches

Utile de savoir que : Créer une branche ne pèse donc presque rien en mémoire, et changer de branche est presque instantanné.

Pour la suite, on se place dans le dépôt repo1/ créé dans la page Premiers pas.

Principales commandes pour manipuler les branches :
CommandeSignification
git branch List branches
git branch testing Creates a new branch (doesn't switch to that branch)
git checkout testing Switches to branch testing (the HEAD pointer goes to testing)
git checkout -b testing Create a branch and switch to it

La branche courante

Sous git, on est toujours sur une branche. Quand on initialise un dépôt, git crée une branche par défaut, appelée main (avant 2020, la branche par défaut s'appellait master).
git branch permet de lister les branches du dépôt courant :
git branch
* main
Git indique qu'il n'y a que la branche main ; l'astérisque indique que c'est la branche sur laquelle on se trouve.

Créer une branche et faire un commit

On se place dans le dépôt repo1/ créé dans la page Premiers pas.
# créer une branche nommée "branche1"
git branch branche1

git branch
  branche1
* main
# changer de branche
git checkout branche1
Les 2 étapes précédentes auraient pu être faites en une instruction :
git checkout -b branch1
Dans les deux cas, on peut vérifier qu'on se trouve sur branch1
git branch
* branche1
  main
Dans cette branche, on crée un nouveau fichier fichier-branche1.txt, on l'ajoute à git et on commit :
echo "Fichier créé dans branche1" > fichier-branche1.txt
git add -A
git commit -m "Création de fichier-branche1.txt"
git slog donne des indications intéressantes :
git slog
* 052f4d7 - (2019-01-20 11:33:43 +0100) Création de fichier-branche1.txt - Thierry (HEAD -> branch1)
| 
* 8c0ae1e - (2019-01-20 03:14:20 +0100) Ajout d'une troisième ligne dans test1.txt - Thierry (main)
| 
* 20dae34 - (2019-01-20 03:14:20 +0100) Ajout d'une seconde ligne dans test1.txt - Thierry
| 
* d3a94de - (2019-01-20 03:14:20 +0100) Création de test1.txt - Thierry
Le pointeur HEAD pointe bien vers l'état courant du répertoire de travail.
Le pointeur main pointe vers le dernier commit fait sur la branche main.
Le pointeur branch1 pointe vers le dernier commit fait sur la branche branch1

Retour sur main

On retourne sur la branche main :
git checkout main
On peut s'apercevoir de deux choses :

Stashing

Pour passer d'une branche à l'autre, il faut avoir son répertoire de travail propre, ne pas avoir de fichiers dans l'état modified. Pour changer de branche en laissant en l'état son travail courant, on utilise git stash.

Très bien expliqué : https://git-scm.com/book/en/v2/Git-Tools-Stashing-and-Cleaning
git stash list

# stash
git stash
git stash -u                # --include-untracked
git stash --keep-index      # include staged content and leave it in the index.
git stash -a                # --all (include ignored files)
git stash --patch           # interactive - ask which files to stash
git stash branch new_branch # create a branch, checks out the commit you were on when you stashed,
                            # reapplies your work there, and then drops the stash if apply = ok

# unstash
git stash apply
git stash apply stash@{2}   # if several stashes - see git stash list
git stash apply --index     # reapply also the staged changes

git stash drop stash@{0}
git stash pop               # apply the stash and then immediately drop it

Branch merge

Fast-forward merge

On va merger la branche branch1 dans main.
On est dans un cas particulièrement simple car on n'a effectué aucune modification dans main.
main pointe donc sur un ancêtre direct de branch1.
Git détecte cette situation et peut merger les deux branches simplement en déplaçant le pointeur main.

Ce type de merge s'appelle fast-forward.
git merge branch1 
Mise à jour 8c0ae1e..052f4d7
Fast-forward
 fichier-branche1.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 fichier-branche1.txt
En listant les fichiers, on peut voir que le fichier fichier-branche1.txt est présent ; on peut voir en faisant slog que le pointeur main a été modifié.

Three-way merge

On va créer une situation plus compliquée : Le merge fast-forward ne sera plus possible car une branche ne sera plus ancêtre direct de l'autre.
A chaque étape, on peut vérifier comment les choses évoluent avec git status, git slog, et en listant les fichiers du répertoire.

Sur la branche main :
echo "Fichier créé dans main" > fichier-main.txt
git add -A
git commit -m "Création de fichier-main.txt"
On va sur la branche branch1 et on modifie fichier-branche1.txt :
git checkout branch1 
echo "Une deuxième ligne ajoutée dans branch1" >> fichier-branche1.txt
git commit -am "Ajout d'une 2ème ligne dans fichier-branche1.txt"
git slog est intéressant :
git slog 
* 43bf826 - (2019-01-20 12:47:59 +0100) Ajout d'une 2ème ligne dans fichier-branche1.txt - Thierry (HEAD -> branch1)
|   
| * 549446f - (2019-01-20 12:40:16 +0100) Création de fichier-main.txt - Thierry (main)
|/  
| 
* 052f4d7 - (2019-01-20 11:33:43 +0100) Création de fichier-branche1.txt - Thierry
Les 2 branches ont divergé. L'ancêtre commun est le commit 052f4d7.
Mais un même fichier n'a pas été modifié dans les 2 branches, donc git peut faire le merge automatiquement.

On est toujours sur branch1, on fait le merge :
git merge main
Merge made by the 'recursive' strategy.
 fichier-main.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 fichier-main.txt
On voit que git n'a pas fait un fast-forward merge mais a utilisé la stratégie 'recursive'.
Ce genre de merge donne lieu à un commit, et git ouvre un éditeur pour demander le message du commit, avec une valeur par défaut proposée.
L'éditeur utilisé correspond à celui spécifié par core.editor dans la config.
Comme indiqué dans le texte, seules les lignes ne commençant pas par # sont conservées dans le message du merge.
En enregistrant ce texte éventuellement modifié et en quittant l'éditeur, git fait le commit de merge, ce que l'on peut voir par git slog :
git slog 
*   723fe55 - (2019-01-20 17:08:21 +0100) Merge branch 'main' into branch1 - Thierry (HEAD -> branch1)
|\  
| | 
| * 549446f - (2019-01-20 12:40:16 +0100) Création de fichier-main.txt - Thierry (main)
| | 
* | 43bf826 - (2019-01-20 12:47:59 +0100) Ajout d'une 2ème ligne dans fichier-branche1.txt - Thierry
|/  
| 
* 052f4d7 - (2019-01-20 11:33:43 +0100) Création de fichier-branche1.txt - Thierry

Merge avec conflit sur un fichier

On va maintenant modifier le même fichier dans deux branches différentes et merger.

Sur branch1, modifier le fichier test1.txt de manière à avoir :
Une première ligne modifiée dans branch1
Une seconde ligne
Du contenu ajouté dans branch1
Une troisième ligne
Faire le commit dans branch1 et basculer sur main
git commit -am "Modification de test1.txt dans branch1"

git checkout main
Sur main, éditer le fichier test1.txt. On voit que les modifications faites sur branch1 ne sont pas présentes.
Modifier ce fichier de manière à avoir :
Une première ligne modifiée dans main
Une seconde ligne
Du contenu ajouté dans main
Une troisième ligne
Faire le commit :
git commit -am "Modification de test1.txt dans main"
On peut maintenant faire le merge avec conflit
git merge branch1
Fusion automatique de test1.txt
CONFLIT (contenu) : Conflit de fusion dans test1.txt
La fusion automatique a échoué ; réglez les conflits et validez le résultat.
La phrase important du message est "réglez les conflits et validez le résultat". git status donne plus d'information :
git st
Sur la branche main
Vous avez des chemins non fusionnés.
  (réglez les conflits puis lancez "git commit")
  (utilisez "git merge --abort" pour annuler la fusion)

Modifications qui seront validées :

	modifié :         fichier-branche1.txt

Chemins non fusionnés :
  (utilisez "git add ..." pour marquer comme résolu)

	modifié des deux côtés :  test1.txt
git merge --abort permet de revenir à l'état d'avant la tentative de merge.

Pour régler le merge, il faut éditer le fichier conflictuel test1.txt
Il contient :
<<<<<<< HEAD
Une première ligne modifiée dans main
Une seconde ligne
Du contenu ajouté dans main
=======
Une première ligne modifiée dans branch1
Une seconde ligne
Du contenu ajouté dans branch1
>>>>>>> branch1
Une troisième ligne
Git a donc modifié ce fichier et identifié les conflits.
Chaque conflit à régler (un seul conflit dans notre cas) est encadré par les lignes commençant par <<<<<<< et >>>>>>>.
Git a mis les 2 versions en conflit à la suite, en les séparant par =======.
Les parties non conflictuelles du fichier sont recopiées telles quelles (Une troisième ligne dans notre cas).

Donc git nous dit : Pour résoudre le conflit, il faut donc utiliser notre intelligence d'humain pour corriger le fichier et en obtenir une version satisfaisante, puis l'enregistrer.
La on peut par exemple faire :
Une première ligne modifiée dans main et dans branch1
Une seconde ligne
Du contenu ajouté dans main
Du contenu ajouté dans branch1
Une troisième ligne
(noter qu'on a supprimé les lignes de marquage ajoutées par git).

On peut maintenant terminer la résolution du conflit comme un commit normal :
git commit -am "Résolution manuelle du conflit"
git slog montre que le conflit est résolu :
*   4494d4b - (2019-01-20 18:00:30 +0100) Résolution manuelle du conflit - Thierry (HEAD -> main)
|\  
| | 
| * 8b04dc1 - (2019-01-20 17:24:35 +0100) Modification de test1.txt dans branch1 - Thierry (branch1)
| |   
| *   99c8465 - (2019-01-20 17:14:52 +0100) Merge branch 'main' into branch1 - Thierry
| |\  
| | | 
| * | 43bf826 - (2019-01-20 12:47:59 +0100) Ajout d'une 2ème ligne dans fichier-branche1.txt - Thierry
| | | 
* | | 66abec5 - (2019-01-20 17:29:21 +0100) Modification de test1.txt dans main - Thierry
| |/  
|/|   
| | 
* | 549446f - (2019-01-20 12:40:16 +0100) Création de fichier-main.txt - Thierry
|/  
| 
* 052f4d7 - (2019-01-20 11:33:43 +0100) Création de fichier-branche1.txt - Thierry
La suite est incomplète

Branches distantes

Par exemple, si on a l'autorisation de pusher sur le dépôt origin :
git push origin content-git

Renommer une branche distante

git push  <REMOTENAME> <LOCALBRANCHNAME>:<REMOTEBRANCHNAME>

Effacer une branche distante

ATTENTION : similaire à la syntaxe pour renommer une branche distante mais sans <LOCALBRANCHNAME>
git push  <REMOTENAME> :<BRANCHNAME>
A comprendre comme : push rien du tout dans <BRANCHNAME>
Par exemple
git push origin :content-git
Tous les commits effectués dans content-git seront supprimés sur le dépôt distant, qui se retrouve dans le même état qu'avant avoir pushé la content-git sur origin.

(Source : https://help.github.com/articles/pushing-to-a-remote/)

Lier une branche distante par défaut à une branche locale

On se place sur la branche content-git, on fait des modifications et on commit.
Si on veut pusher en faisant simplement :
git push
git nous dit :
fatal: La branche courante content-git n'a pas de branche amont.
Pour pousser la branche courante et définir la distante comme amont, utilisez

    git push --set-upstream origin content-git
OK, on fait :
git push --set-upstream origin content-git
git affiche plusieurs lignes qui finissent par :
La branche 'content-git' est paramétrée pour suivre la branche distante 'content-git' depuis 'origin'.
La prochaine fois qu'on fait
git push
depuis la branche locale content-git, git pushera sur la branche distante origin content-git

De la même manière, si on retourne sur la branche main et qu'on fait
git push --set-upstream origin main
alors git pushera sur la branche distante origin main quand on est sur la branche locale main.