Git ProTips

Faire du Git plus vite et mieux

Une présentation de Christophe Porteneuve à BLEND Web Mix 2014

TÉKITOA


var christophe = {
  age:        36.9863013698630136,
  city:       'Paris',
  company:    'Delicious Insights',
  trainings:  ['Git Total', 'JS Total', 'Node.js'],
  gitSince:   '2008-03-28'
};
          

github/tdd

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é

Voyez le détail des nouveaux fichiers, en profondeur.


$ git status
…
Untracked files:
  (use "git add ..." to include in what will be committed)

  vendor/

$ git config --global status.showUntrackedFiles all
$ git status
…
Untracked files:
  (use "git add ..." to include in what will be committed)

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

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é.

Voir le stage

Version diff


$ git diff --staged
diff --git c/index.html i/index.html
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:pathspec
<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <title>Git ProTips</title>
…
            

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>
…
            

Affiner le diff

Ligne à ligne, ça manque parfois de granularité…


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

$ git diff --color-words=.
…
<!doctype html>
<html lang="fr">
…
            

Une bonne fois pour toutes :


$ git config --global diff.wordRegex .
$ git diff --word-diff
…
<!doctype html>
<html{+ lang="fr"+}>
…
            

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>© 2014 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.


$ git reset index.html
Unstaged changes after reset:
M index.html
            

Annuler tout le stage :


$ git reset
Unstaged changes after reset:
M index.html
            

Unstage partiel

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


$ git reset -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>© 2014 Ma Boîte</footer>Unstage this hunk [y,n,q,a,d,/,K,g,e,?]?  y
            

Renommages et suppressions

git add . ne suffit pas avant Git 2.0 : il ne prend en compte que le working directory, donc pas les suppressions.


Changes not staged for commit:
…
  deleted:    index.html

Untracked files:
…
  home.html
            

Pour gérer les connus et les untracked, on utilise -A (--all) :


$ git add -A && git status
On branch master
Changes to be committed:
…
  renamed:    index.html -> home.html
            

Retoucher

le dernier commit

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

Oublié de versionner une dépendance ?


$ git add vendor/scripts/underscore.min.js
$ git commit --amend --no-edit
            

Versionné un fichier sensible ?


$ git rm --cached config/database.yml
$ echo config/database.yml >> .gitignore && git add .gitignore
$ git commit --amend --no-edit
            

Foiré le message ?


$ git commit --amend -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 <tdd@tddsworld.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 save -u '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

Le log par défaut est bien trop verbeux, sans graphe, etc.


$ git config --global alias.lg "log --graph \
  --pretty=tformat:'%Cred%h%Creset -%C(auto)%d%Creset %s \
  %Cgreen(%an %ar)%Creset'"
            
Exemple d‘affichage de git lg

Filtrer et limiter

--grep sur les messages complets

--author sur les noms/e-mails des auteurs

-- pathspec… sur les chemins (répertoires, fichiers)

-n limite le nombre de lignes après filtrage


$ git lg --author=patter --grep '^Merge' -10 -- activerecord activemodel
            
Filtrage simple de log

Loguer la divergence

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

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

Depuis la 1.8.4 (avril 2013), on 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 :

Avant 2.0 : toutes les branches trackées* de même nom**

Depuis 2.0 : 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
            
* push.default = matching — de même nom = nom distant identique au local
** 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 @{u}
            

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)

Démo

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 preserve
            

Démo

Des branches nickel

Nouvelle branche

La plupart du temps, quand on crée une branche, c’est pour bosser dessus direct.


(master) $ git branch feature
(master) $ git checkout feature
(feature) $
            

(master) $ git checkout -b feature
(feature) $
            

Notez que pour commencer à collaborer à une branche uniquement distante jusque-là, un checkout simple suffit.


(master) $ git branch -a
* master
  origin/master
  origin/topic
(master) $ git checkout topic
Branch topic set up to track remote branch topic from origin.
Switched to a new branch 'topic'
(topic) $
            

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


(2014-octobre u+1) $ git branch -avv
* 2014-octobre                 abaca0f [origin/2014-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
  v2014                        521350a [origin/v2014: behind 2] Backport changement cible lien plugins Backbone vers backplug.io
  v2015                        27b1791 [origin/v2015] MàJ docs annotés
  remotes/origin/2014-octobre  10ad1b1 MàJ code source annoté
  remotes/origin/bs3           49bc984 Tweaks en cours de session
  remotes/origin/bs3-basis     650f025 Tweak export connectivity
…
            

Qui contient quoi ?

Alors comme ça, le 73abc4f est la source du bug, et tu veux savoir où il faudra propager le fix ?


(master) $ git branch -a --contains 73abc4f
* master
  stats
  origin/master
  origin/3-2-stable
  origin/stats
            

« Bon, il reste quoi de fusionnable dans master ? »


(master) $ git branch -a --no-merged
  stats
  origin/fix/143
  origin/fix/148
  origin/stats
  origin/max/experiment-web-audio
            

Fusions

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

La branche est un descendant ? Empêchez le fast-forward :


(master) $ git merge --no-ff fix/143
            

On veut un message détaillant les commits fusionnés ?


(master) $ git merge --log stats
            

Ou pour du systématique :


(master) $ git config --global merge.log true
            

Un tiret c’est tout

Très souvent, quand on merge, rebase, cherry-pick ou simplement checkout, c’est depuis/sur la branche précédente (celle où on était juste avant).

Tout comme le cd - du shell, on peut filer - (tiret) comme argument : raccourci récent pour le plus classique @{-1}.


(master) $ git rebase master experiment
(experiment) $ git checkout -
(master) $
            

(fix/148) $ git checkout 3-2-stable
(3-2-stable) $ git merge -
(3-2-stable) $ git checkout master
(master) $ git cherry-pick -
(master) $
            

Branch-worthy

T’aurais juré que ça allait prendre 1 commit et 10 minutes…

…et voilà 3 commits et 2 heures que tu y es. Et c’est pas fini !

« J’aurais dû faire une branche… »


  (master) $ git branch fix/158
  (master) $ git reset --soft HEAD~3
(master +) $ git checkout fix/158
 (fix/158) $
            

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).


(master) $ git cherry-pick 3-2-stable
            

On liste les commits candidats avec git cherry :


(master) $ git cherry -v HEAD topic
+ 3abb73d7cc8d8655f8b99816fed56c6030c28551 /img/ -> /images/
- ba05b8d03de5540181af34a10f1b07debb0ea5fc Stats JS
+ 363f53d53d78384f29dc68d900b04ac0b56d20f6 Nav stats
            

Ou avec git log --cherry (je préfère) :


(master) $ 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 :

  1. git status tout de suite (voir qui cloche)
  2. Examen du 1er fichier conflictuel (éditeur ou git mergetool)
  3. Arbitrage des conflits du fichier*
  4. git add pour marquer le fichier comme résolu
  5. S’il reste des fichiers conflictuels, retour en (2).
  6. Sinon, finalisation avec git commit.
* C‘est ça qui est souvent simple mais peut parfois nécessiter plus d’infos

Aspect du conflit

Par défaut, style merge :


<<<<<<< HEAD
SVN est un outil de gestion de source largement répandu
et extrêmement pratique.
=======
SVN est un outil de gestion de source largement répandu
malgré sa profonde stupidité et la plaie de son usage.
>>>>>>> truth
            

Il est souvent pratique de voir la version pré-divergence :


<<<<<<< HEAD
SVN est un outil de gestion de source largement répandu
et extrêmement pratique.
||||||| merged common ancestors
SVN est un outil de gestion de source largement répandu.
=======
SVN est un outil de gestion de source largement répandu
malgré sa profonde stupidité et la plaie de son usage.
>>>>>>> truth
            

$ git config --global merge.conflictStyle diff3
            

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.

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


(master *) $ git show :2:intro.md
# ou : git show HEAD:intro.md
# ou : git show master:intro.md
            

Côté source du code fusionné :


(master *) $ 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) :


(master *) $ git show :1:intro.md
            

Log de la divergence

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 :


(master *) $ 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…
(master *) $
            

Lâcher l’affaire ?

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


(master *) $ git merge --abort
Anciennement : git reset --merge
(master) $
            

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

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.

Activé par rerere.enabled à true ou la présence de .git/rr-cache. Auto-stage un fichier résolu si rerere.autoupdate et à true.

Démo

Envie d’en savoir plus ?

On fait des super formations de ouf sur Git.

Merci !

Et que Git soit avec vous


Christophe Porteneuve

@porteneuve

Retrouvez les slides sur bit.ly/gitprotips