Une présentation de Christophe Porteneuve à BLEND Web Mix 2014
var christophe = {
age: 36.9863013698630136,
city: 'Paris',
company: 'Delicious Insights',
trainings: ['Git Total', 'JS Total', 'Node.js'],
gitSince: '2008-03-28'
};
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
.
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
…
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é.
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>
…
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>
…
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"+}>
…
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…
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
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
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
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'
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';
…
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 $
.
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
.
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'"
--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
Besoin de voir les commits uniques de 2 branches/tags ? (en gros, remonter juste à leur ancêtre commun). Utilisez A...B
.
![]() |
![]()
![]() |
git blame
, c’est comme le H de Hawaii : ça ne sert à rien.
Préférez chercher sur les contenus actifs des diffs : c’est plus pertinent et ça marche aussi pour des lignes virées !
Options -S
* (insertions OU suppressions) et -G
(tous diffs).
$ git lg -S "Donec sed" -1 -p index.html
...
* 6d731f3 - Content tweaks (Christophe Porteneuve 1 year, 1 month ago)
|
| diff --git a/index.html b/index.html…
…
| @@ -60,18 +60,18 @@
…
| <h2>Three-point rebasing</h2>
| - <p>Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa.</p>
| + <p>Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa.</p>
| <p><a class="btn" href="#">View details »</a></p>
…
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();
+}
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
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.
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)
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 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
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) $
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
…
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
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
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) $
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) $
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)
La plupart des conflits sont simples à arbitrer.
Il faut juste la bonne méthodo :
git status
tout de suite (voir qui cloche)git mergetool
)git add
pour marquer le fichier comme résolugit commit
.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
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
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 *) $
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.
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
.
On fait des super formations de ouf sur Git.
Christophe Porteneuve
Retrouvez les slides sur bit.ly/gitprotips