Git Pro Tips • Édition 2020

Une présentation de Christophe Porteneuve chez France TV Éditions Numériques

whoami


const christophe = {
  family: { wife: 'Élodie', sons: ['Maxence', 'Elliott'] },
  city: 'Paris, FR',
  company: 'Delicious Insights',
  trainings: ['Git Total', 'ES Total', 'Web Apps Modernes', 'Node.js', 'Webpack', 'Archi. CSS'],
  webDevSince: 1995,
  mightBeKnownFor: [
    'dotJS',
    'Paris Web',
    'NodeSchool Paris',
    'Prototype.js',
    'Prototype and Script.aculo.us',
  ],
}
          

« Faire du SVN avec Git »

L’éternelle histoire du changement de paradigme…

Ou : trimballer sa culture existante dans son nouvel environnement.

Erreur tragique mais ô combien courante

  • Java ➜ Ruby
  • Java ➜ JavaScript / Node
  • SVN ➜ Git
  • SGBD/R ➜ CouchDB/MongoDB
  • etc.

Objectifs & Philosophie

  • Maintenabilité
    • Graphe lisible + pertinent
    • Commits atomiques, homogènes, compréhensibles, peu défectueux
    • Workflow propre
  • Tolérance à l’erreur
  • Tolérance à l’humain 😉

Des commits atomiques

qui chatoyent

Atomiques pourquoi ?

Arrêtez de foutre n’importe quoi dans vos commits.

1 commit = 1 périmètre réduit, d’un coup, ni plus, ni moins.

Pour y arriver, il faut maîtriser add et reset, mais aussi diff, show et bien entendu commit.

Un statut détaillé


              git config --global alias.st status
              git config --global status.showUntrackedFiles all
              git config --global color.ui auto
            

              git st
              …
              Untracked files:
                (use "git add <file>..." to include in what will be committed)

                vendor/scripts/bootstrap.min.js
                vendor/scripts/jquery.min.js
                vendor/scripts/underscore.js
            

Un diff sympa


              git config --global diff.mnemonicPrefix true
              git config --global diff.renames true
              git config --global diff.submodule log
              git config --global diff.wordRegex .

              git config --global color.diff.meta yellow bold
              git config --global color.diff.frag magenta bold
              git config --global color.diff.whitespace red reverse

              npm install -g diff-so-fancy
              git config --global pager.diff diff-so-fancy
              git config --global pager.show diff-so-fancy
            

Comprendre le stage

Le stage ou l’index : ce qui est validé pour partir au commit.

Permet de sculpter finement le commit à venir.

git add pathspec… = « prends pathspec en photo et mets ça dans le colis du prochain commit ».

Synonyme : git stage. Rien à voir avec svn add.

Obligatoire pour versionner un nouveau fichier (untracked), contrairement à quand le fichier est déjà versionné.

(À ce sujet, si vous voulez voir un nouveau fichier dans votre diff sans le stager d’entrée de jeu, git add -N)

Voir le stage

Version diff :


                  git diff --staged
                  diff --git c/index.html i
                  index 5237399..85e642f 100644
                  --- c/index.html
                  +++ i/index.html
                  @@ -1,5 +1,5 @@
                  <!doctype html>
                  -<html>
                  +<html lang="fr">
                

Version snapshot (tout le fichier) :


                  git show :0:index.html
                  <!doctype html>
                  <html lang="fr">
                  <head>
                  …
                
(attention, le diff par défaut est entre le stage et le WD)

Dépolluer le diff

Les whitespaces, le plus souvent, OSEF.


                  git diff
                  …
                  <!doctype html>
                  -<html>
                  +<html lang="fr">
                  <head>
                  -  <meta charset="utf-8">
                  -  <title>Git ProTips</title>
                  +    <meta charset="utf-8">
                  +    <title>Git  ProTips</title>
                  …
              

                  git diff -w
                  …
                   <!doctype html>
                  -<html>
                  +<html lang="fr">
                   <head>
                  …
                

                  git diff -w --word-diff
                  …
                  <!doctype html>
                  <html{+ lang="fr"+}>
                  …
                

Mais aussi façon agrégats : git diff --stat et git diff --dirstat

Fragments de fichiers

Qui a dit qu’on devait stager tout le fichier d’un coup ?


              git add -p index.html
              …
              <!doctype html>
              -<html>
              +<html lang="fr">
              <head>
              …
              Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? y
              …
                <h1>Git ProTips</h1>
              +  <footer>© 2020 Ma Boîte</footer>
              …
              Stage this hunk [y,n,q,a,d,/,K,g,e,?]?  n
            

Juste critique parce que dans la vraie vie, on a toujours 2–3 sujets distincts en cours dans un même fichier…

Unstage

Sortir un snapshot du stage : git reset (historique) ou git restore --staged (2.23+, août 2019).


              git restore --staged index.html
            

Annuler tout le stage :


              git reset
              Unstaged changes after reset:
              M      index.html
            

La petite vidéo en français qui va bien

Unstage partiel

C’est comme pour l’ajout : on peut n’unstager que certains fragments.


              git restore --staged -p index.html
              …
              …
              <!doctype html>
              -<html>
              +<html lang="fr">
              <head>
              …
              Unstage this hunk [y,n,q,a,d,/,j,J,g,e,?]? n
              …
                <h1>Git ProTips</h1>
              +  <footer>© 2020 Ma Boîte</footer>
              …
              Unstage this hunk [y,n,q,a,d,/,K,g,e,?]?  y
            

Retoucher le dernier commit

git commit --amend remplace par l’état courant.

git config --global alias.oops commit --amend --no-edit

Oublié de versionner une dépendance ?


              git add vendor/scripts/underscore.min.js
              git oops
            

Versionné un fichier sensible ?


              git rm --cached config/database.yml
              echo config/database.yml >> .gitignore && git add .gitignore
              git oops
            

Foiré le message ?


              git oops -m 'Le message ni énervé ni bourré de fautes'
            

Visualiser un commit, un snapshot

git show [object] permet d’afficher au mieux un commit (par défaut HEAD), une arbo, un snapshot (blob)…


              git show # ou explicitement : git show HEAD
              commit 8a5a383
              Author: Christophe Porteneuve <christophe@delicious-insights.com>
              Date:   Sun Oct 26 15:04:17 2014 +0100

                  Premier index

              diff --git a/index.html b/index.html
              …
            

Le contenu de app/initialize.js en branche legacy ?


              git show legacy:app/initialize.js
              'use strict';
              …
            

Le stash… mais bien

Histoire d’avoir les untracked et un message utile :


(master *+%) $ git stash push -u -m 'migration BS3'
Saved working directory and index state On master: migration BS3
HEAD is now at 8a5a383 Trackers GA asynchrones
(master $) $
            

Pour que votre prompt* vous rappelle que vous avez du stash, pensez à activer la variable d’environnement GIT_PS1_SHOWSTASHSTATE, qui y ajoutera un $.

Attention ça stashe

Pour récupérer le stash, évitez apply, préférez un pop :


(master $) $ git stash pop --index
…
(master *+%) $
            

pop tente l’apply, et s’il marche enchaîne avec drop. Rien de pire que de laisser traîner un stash réintégré…

Même s’il stocke le stage par défaut, le stash ne le restaure pas par défaut, pour éviter des « fusions auto » dans le stage. Pas très cohérent avec ce que fait par exemple merge, mais bon… Donc tentez toujours d’abord --index.

Des logs utiles

Passer la 5ème avec git log

Un format pertinent


              LOG_FORMAT='%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%an %ad)%Creset'
              git config --global alias.lg "log --graph --date=relative --pretty=tformat:'$LOG_FORMAT'"
              git config --global log.abbrevCommit true
              git config --global log.follow true
              git config --global grep.extendedRegexp true
            
Exemple d‘affichage de git lg

Filtrer et limiter

Par références aval/amont


              git lg [ref…] [^neg-ref…] # Liste spécifique aval
              git lg ref.. # Amont
              git lg [--branches[=glob]|--tags[=glob]|--all] [--exclude=glob] # Wildcards aval
              git lg -n # Nombre de commits listés
            

Par métadonnées


              git lg --grep 'fragment du message complet' [--invert-grep] [--all-match] [-i]
              git lg --author 'fragment du noms e-mail de l’auteur·e' [-i]
              git lg --[no-]merges
              git lg -- pathspec…
            

Évidemment combinable à l’infini


$ git lg --author=patter --grep '^Merge' -10 -- activerecord activemodel
            

Loguer la divergence

Besoin de voir les commits uniques de 2 branches/tags ? (en gros, remonter juste à leur ancêtre commun).
Utilisez A...B.


              git lg [--right-only|--left-only] ref1...ref2
            
Le graphe complet

                  git lg master..bootstrap3-to-rebase
                
Le graphe mal listé

                  git lg master...bootstrap3-to-rebase
                
Le listing limité à la divergence

Fragments & fonctions

Il y a un filtrage de fou sur le log : par fragment de fichier, notamment par corps de fonction. On utilise soit un intervalle (ex. -L 1,100:index.html) soit en fournissant une regex pour la fonction englobante :


git lg -L :getCheckIns:app/lib/persistence.js
* 12164bc - Refactoring gestion Check-In Details, et gestion corner-cases (Christophe Porteneuve 2 days ago)
|
| diff --git a/spa-final/app/lib/persistence.js b/spa-final/app/lib/persistence.js
| --- a/spa-final/app/lib/persistence.js
| +++ b/spa-final/app/lib/persistence.js
| @@ -81,4 +86,7 @@
|  function getCheckIns() {
| -  return collection.toJSON();
| +  return collection.map(modelWithCid);
|  }
|
* d714350 - Initial import (Christophe Porteneuve 1 year ago)

  diff --git a/app/lib/persistence.js b/app/lib/persistence.js
  --- /dev/null
  +++ b/app/lib/persistence.js
  @@ -0,0 +34,4 @@
  +function getCheckIns() {
  +  return collection.toJSON();
  +}
            

Des pushes choisis

Et pas trop fréquents

Faut pas pousser

les autres branches

Par défaut, sur un git push seul, Git va tenter l’actuelle si trackée de même nom*

Ce qu’on veut : l’actuelle, quel que soit le nom distant.


              git config --global push.default upstream
            

Et par ailleurs, on aimerait bien auto-pusher les tags annotés locaux qui deviendraient disponibles sur le remote :


              git config --global push.followTags true
            
* push.default = simple

Push initial

Une bonne 1ère impression

La première fois que vous poussez une branche que vous voulez tracker ensuite, pensez à caler à la volée le tracking :


(stats-v3) $ git push -u origin stats
Counting objects: 5, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 488 bytes | 0 bytes/s, done.
Total 5 (delta 0), reused 0 (delta 0)
To git@github.com:tdd/private-tests.git
 * [new branch]      stats -> stats
Branch stats set up to track remote branch stats from origin.
            

Nettoyer avant push

Le bordel OK, mais chez toi

Réflexe pré-push : nettoyer ton historique local, lequel est forcément plus ou moins en bordel.


              git lg @{u}..
              git rebase -i
            

Le rebase interactif nous permet de mettre au propre nos travaux locaux avant de partager tout ça avec les copains.

Raison de plus pour ne pas faire de pushes trop souvent. On est pas en SVN, les copains ! On pond des commits souvent (10–30 ×/j), mais on push plus rarement (2–3 ×/j)

Pull ≠ Merge !

C’est vrai, quoi, merde.

Par défaut, git pull finit par un merge. C’est super con.

Quand tu pull, tu ne fusionnes pas une branche tierce chez toi : tu récupères les mises à jour sur ta branche courante.

En plus, ça pourrit le graphe :

Un graphe dégueulassé par le pull qui merge

Pull = Rebase

Un pull devrait plutôt rejouer notre taf local sur la branche distante à jour : par définition, un rebase.

Il faut juste faire attention à ne pas inliner par inadvertance un merge au sein du travail local.

Une bonne fois pour toutes :


              git config --global pull.rebase merges
            

Des branches nickel

Mieux voir les branches

Deux options utiles pour git branch :

-a liste les locales et les distantes

-vv ajoute les 1ères lignes de commit et, pour les trackées, l'état du tracking


(2019-octobre u+1) $ git branch -avv
* 2019-octobre                 abaca0f [origin/2019-octobre: ahead 1] Retrait vieilles demos
  legacy                       41b5bf7 [origin/legacy] Script Bash de packaging + déploiement du fichier Zip de debrief (exécutable par Chris seul vu les droits SSH requis)
  master                       0208acb [origin/master] Fix .groc.json
  v2018                        521350a [origin/v2018: behind 2] Backport changement cible lien plugins Backbone vers backplug.io
  v2017                        27b1791 [origin/v2017: gone] MàJ docs annotés
  remotes/origin/2017-octobre  10ad1b1 MàJ code source annoté
  remotes/origin/bs3           49bc984 Tweaks en cours de session
  remotes/origin/bs3-basis     650f025 Tweak export connectivity
…
            

Fusions

On ne fusionne que pour rapatrier de façon visible (bosse dans le graphe)
un périmètre fonctionnel identifié (bugfix, feature, story, etc.).


              git config --global core.whitespace '-trailing' # OSEF des écarts de whitespace en fin de ligne
              git config --global merge.conflictStyle diff3   # J’aime bien avoir le chunk « ancêtre commun »
              git config --global merge.log true              # Ou un nombre de commits, par défaut 25
              git config --global merge.ff false              # Un merge doit rarement être fast-forward
            

Le process classique et sûr :


              git status             # Quelle est l’étendue des dommages ?
              git mergetool path     # Si on en a (+ veut) un, pour le fichier concerné
              git add path           # Fichier par fichier, après l’avoir arbitré
              git commit [--no-edit] # Une fois que tout est staged
            

Cherry-picking

Récupérer un commit unique, sans son historique. Parfait pour les fixes et tout ce qu’on trouve dans une branche de release, à réintégrer ailleurs (ex. master).


              git cherry-pick -x 3-2-stable
            

On liste les commits candidats avec git log --cherry (je préfère à git cherry) :


              git lg --cherry HEAD...topic
              * 363f53d - (topic) Nav stats (Christophe Porteneuve 1 year, 1 month ago)
              = ba05b8d - Stats JS (Christophe Porteneuve 1 year, 1 month ago)
              * 3abb73d - /img/ -> /images/ (Christophe Porteneuve 1 year, 1 month ago)
            

Arbitrer un conflit

La plupart des conflits sont simples à arbitrer. Il faut juste la bonne méthodo :


              # Qui est le prochain coupable ?
              git status
              # On édite le fichier en direct, ou si on a un bon outil dédié configuré…
              git mergetool path
              # Une fois que les conflits sont réglés*, on le dit :
              git add path
              # Si on a mal arbitré (notammment mergetool) ou que le style de marqueurs nous gêne :
              git checkout --conflict=(merge|diff3) path
              # Besoin de voir un des 3 snapshots sans mergetool ?
              git show (ref|MERGE_HEAD|HEAD|:1):path
              # Envie de récupérer le fichier tel quel depuis une des branches, ou ailleurs ?
              git checkout ref [--] path
              # Ou la version verbeuse : git restore --source=ref --staged --worktree [--] path
            
* C‘est ça qui est souvent simple mais peut parfois nécessiter plus d’infos

Voir les snapshots

Parfois le diff ne suffit pas, notamment quand le code en conflit fait référence à d’autres parties du code qui ont, elles, bien fusionné, noyant le contexte. Disons qu’on a fait un git merge truth dans master.

On peut alors ressortir les snapshots côté récipient :


              git show :2:intro.md
              # ou : git show HEAD:intro.md
              # ou : git show master:intro.md
            

Côté source du code fusionné :


              git show :3:intro.md
              # ou : git show MERGE_HEAD:intro.md
              # ou : git show truth:intro.md
            

Dans l’ancêtre commun (avant la divergence) :


              git show :1:intro.md
            

Log de la divergence lors d’un conflit

Très rarement, même les snapshots ne suffisent pas à comprendre le problème, et on veut retracer le cheminement du code depuis l’ancêtre commun jusqu’aux têtes de branches.

C’est faisable à tout moment avec la syntaxe A...B déjà vue, mais lors d’une fusion Git a déjà tout le contexte, il suffit de faire un --merge (les variantes --left-only et --right-only marchent toujours) :


              git lg --merge -p intro.md
              * 9d8dafd - (truth) Truth (Christophe Porteneuve 12 minutes ago)
              …et ici le diff…
              * f068b20 - (HEAD, master) Disinfo (Christophe Porteneuve 12 minutes ago)
              …et ici le diff…
            

Lâcher l’affaire ?

Si vraiment tu n’y arrives pas pour le moment (manque d’infos), annule la fusion proprement :


              git merge --abort
              # Anciennement : git reset --merge
            

Nettement plus propre et moins dangereux qu’un git reset --hard. Préserve tes modifs locales pré-fusion.

Note que les sous-commandes --abort, --skip et --continue sont disponibles pour merge, rebase et cherry-pick.

Solve once

Merge anywhere

rerere : reuse recorded resolution

Prend deux empreintes complètes pour chaque conflit (dénuée du chemin, etc.) : une au conflit, une au commit.

Assiste les conflits ultérieurs sur le dépôt en ré-utilisant ces empreintes en cas de résolution antérieure.


              git config --global rerere.enabled true
              git config --global rerere.autoupdate true # seulement pour les gens attentifs !
            

Activé aussi par la présence de .git/rr-cache

Tous les détails et la notion de merges de contrôle.

Envie d’en savoir plus ?

Quelques-uns de nos articles Git : sur
merge vs. rebase, rerere, le log, reset, bisect, les workflows, les hooks, les submodules

Notre formation Git qui envoie du pâté (en inter-entreprises comme en intra chez vous !)

On a des cours vidéos sur Git, dont un gratuit indispensable qu’on retrouve également sur YouTube.

Merci !

Et que Git soit avec vous


Christophe Porteneuve

@porteneuve

Les slides sont sur bit.ly/ftv-git