L'option --patch de Git
Pierre Gradot
Posted on February 9, 2023
Vous avez déjà remarqué que plusieurs commandes Git ont une option --patch
? On peut citer add
, checkout
, commit
, reset
,
restore
ou encore stash
.
Dans la suite de cet article, on va modifier ce fichier et on verra comment utiliser l'option --patch
(-p
en version courte) de certaines commandes pour gérer finement ces modifications.
Définition d'un hunk
Si vous avez suivi les liens donnés pour chacune des commandes, vous aurez sûrement constaté que la description de l'option --patch
commence souvent par :
Interactively select hunks
On retrouve souvent ce terme de "hunk". C'est un terme important de l'appréhender pour comprendre comment Git gère les changements.
On trouve une bonne définition de "hunk" dans la documentation de diff
:
When comparing two files,
diff
finds sequences of lines common to both files, interspersed with groups of differing lines called hunks. Comparing two identical files yields one sequence of common lines and no hunks, because no lines differ. Comparing two entirely different files yields no common lines and one large hunk that contains all lines of both files.
Si vous vous demandez ce que la commande Unix diff
vient faire dans un article parlant de Git, c'est juste que git diff
et git apply
ne sont pas très différentes de diff
et patch
(comme discuté ici ou là).
--patch vs --interactive
On vient de dire que l'option --patch
permet de sélectionner interactivement des hunks (et vous verrez clairement dans les exemples de la suite de cet article pourquoi on dit ça et comment on le fait).
Pourtant, il existe une autre option : --interactive
. Quelle différence entre les 2 ?
Voici ce qu'on obtient en utilisant --interactive
(ici avec git add
,mais c'est totalement similaire avec d'autres commandes) :
git add --interactive
*** Commands ***
1: status 2: update 3: revert 4: add untracked
5: patch 6: diff 7: quit 8: help
What now>
En vrai, --patch
est un raccourci pour lancer le mode interactif et choisir patch
dans ce menu. C'est clairement expliqué dans la documentation de git add
:
-p
--patch
Interactively choose hunks of patch between the index and the work tree and add them to the index. This gives the user a chance to review the difference before adding modified contents to the index.
This effectively runs
add --interactive
, but bypasses the initial command menu and directly jumps to thepatch
subcommand.
Disclaimer
Quand on prononce le mot "patch", on pense instinctivement à des fichiers avec l'extension .patch
, qui décrivent des changements et qu'on peut appliquer à notre dépôt. On ne parlera pas de tels fichiers ici.
En effet, il existe d'autres commandes avec une option --patch
: diff
, log
, show
, diff-index
, diff-tree
, diff-files
. Elles génèrent alors des patchs, qu'on peut récupérer (par exemple dans des fichiers) et ensuite appliquer avec git am
ou git apply
.
Dans cet article, on s'intéresse aux commandes interactives (ce n'est pas le cas des commandes citées à l'instant, qui n'ont pas d'option --interactive
).
Notre application
Place aux exemples maintenant ! Pour cela, partons d'un dépôt avec un unique fichier app.py
pour réaliser une superbe application avec Flask :
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'hello, wordl'
Si vous voulez exécuter ce code (ça ne sert à rien pour comprendre cet article, mais c'est marrant), il suffit de vous placer dans le dossier où est app.py
et de faire :
$ pip install flask
$ flask run
On fait un premier commit de ce fichier pour obtenir l'état initial de notre dépôt :
$ git log --oneline
a512faf (HEAD -> master) Initial
Evolution du code
Continuons le développement de notre application et ajoutons une autre route. Au passage, corrigeons également l'affreuse typo à wordl
(vous l'aviez remarqué hein ?).
Notre fichier ressemble maintenant à ça :
from flask import Flask
app = Flask(__name__)
@app.route('/about')
def about():
return 'This is a great app made with Flask'
@app.route('/')
def index():
return 'hello, world'
On peut voir les modifications apportées au fichier grâce à git diff
:
$ git diff
diff --git a/app.py b/app.py
index 71a502c..198f2d0 100644
--- a/app.py
+++ b/app.py
@@ -2,7 +2,10 @@ from flask import Flask
app = Flask(__name__)
+@app.route('/about')
+def about():
+ return 'This is a great app made with Flask'
@app.route('/')
def index():
- return 'hello, wordl'
+ return 'hello, world'
Commiter séparément les modifications
On a fait deux modifications qui n'ont aucun lien entre elles. D'un côté, on corrige un bug ; de l'autre, on ajoute une nouvelle fonctionnalité. Or, un commit doit idéalement avoir une justification unique. Cela veut dire que commit --all -m "Fix bug + add feature"
n'est pas optimal car il a 2 justifications.
On a donc envie de faire 2 commits d'app.py
, mais comment faire ?
Grâce à l'option --patch
des commandes git add
et git commit
, c'est simple.
Dans la suite, on va voir 2 techniques pour d'abord commiter uniquement la modification pour la correction de la typo. Après ce commit, la modification pour l'ajout de la route sera toujours dans le working tree et on pourra commiter app.py
en entier, comme on le fait généralement.
Solution 1 = add
puis commit
Au lieu de faire git add mon_fichier
, on fait git add --patch mon_fichier
. On peut ainsi choisir quoi faire de chaque hunk. On peut ainsi ajouter sélectivement des hunks à la staging area et laisser dans les autres dans le working tree.
λ git add --patch app.py
diff --git a/app.py b/app.py
index 71a502c..198f2d0 100644
--- a/app.py
+++ b/app.py
@@ -2,7 +2,10 @@ from flask import Flask
app = Flask(__name__)
+@app.route('/about')
+def about():
+ return 'This is a great app made with Flask'
@app.route('/')
def index():
- return 'hello, wordl'
+ return 'hello, world'
(1/1) Stage this hunk [y,n,q,a,d,s,e,?]?
Git pense qu'il n'y a qu'un seul hunk (d'où le (1/1)) et nous demande quoi en faire. On lui répond par la lettre correspondant à l'action souhaitée :
y - stage this hunk
n - do not stage this hunk
q - quit; do not stage this hunk or any of the remaining ones
a - stage this hunk and all later hunks in the file
d - do not stage this hunk or any of the later hunks in the file
s - split the current hunk into smaller hunks
e - manually edit the current hunk
? - print help
On souhaite splitter ce hunk en 2 hunks, pour n'ajouter que la seconde modification. On répond donc s
, puis n
et enfin y
:
(1/1) Stage this hunk [y,n,q,a,d,s,e,?]? s
Split into 2 hunks.
@@ -2,6 +2,9 @@
app = Flask(__name__)
+@app.route('/about')
+def about():
+ return 'This is a great app made with Flask'
@app.route('/')
def index():
(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? n
@@ -5,4 +8,4 @@
@app.route('/')
def index():
- return 'hello, wordl'
+ return 'hello, world'
(2/2) Stage this hunk [y,n,q,a,d,K,g,/,e,?]? y
Vérifions le contenu de la staging area :
$ git diff --staged
diff --git a/app.py b/app.py
index 71a502c..efb2808 100644
--- a/app.py
+++ b/app.py
@@ -5,4 +5,4 @@ app = Flask(__name__)
@app.route('/')
def index():
- return 'hello, wordl'
+ return 'hello, world'
Vérifions aussi le contenu du working tree :
$ git diff
diff --git a/app.py b/app.py
index efb2808..198f2d0 100644
--- a/app.py
+++ b/app.py
@@ -2,6 +2,9 @@ from flask import Flask
app = Flask(__name__)
+@app.route('/about')
+def about():
+ return 'This is a great app made with Flask'
@app.route('/')
def index():
C'est parfait ! Pour terminer, il suffit de commiter le contenu de la staging area avec git commit -m "Fix typo in route /"
.
Solution 2 = commit
direct
On n'est pas obligé de passer par la staging area pour commiter des changements. On peut faire directement :
$ git commit --patch -m "Fix typo in route /"
On suit alors exactement le même cheminement : on splitte le hunk en 2, on ignore le premier hunk résultant, et on ajoute le second.
A l'issue de cette commande, un commit a été fait et il reste la modification pour l'ajout de la nouvelle route :
$ git log --oneline
4a39c0b (HEAD -> master) Fix typo in route /
a512faf Initial
$ git diff
diff --git a/app.py b/app.py
index 13357d9..3b00884 100644
--- a/app.py
+++ b/app.py
@@ -2,6 +2,9 @@ from flask import Flask
app = Flask(__name__)
+@app.route('/about')
+def about():
+ return 'This is a great app made with Flask'
@app.route('/')
def index():
Forcer le split d'un hunk
Des fois, on ne peut pas splitter un hunk. La lettre s
n'est pas dans la liste et on est un peu embêté...
Modifions notre fichier pour être dans un tel cas :
from flask import Flask
app = Flask(__name__)
@app.route('/about')
def about():
return 'This is a great app (made with Flask!)'
@app.route('/test')
def hello():
return 'this is a route for testing'
@app.route('/')
def index():
return 'hello, world'
Imaginons qu'on souhaite supprimer l'ajout de la route /test
, mais conserver la modification de la route /about
. Tentons de faire un git restore
en lui passant l'option --patch
:
$ git restore --patch app.py
diff --git a/app.py b/app.py
index 3b00884..9e0d57f 100644
--- a/app.py
+++ b/app.py
@@ -4,7 +4,11 @@ app = Flask(__name__)
@app.route('/about')
def about():
- return 'This is a great app made with Flask'
+ return 'This is a great app (made with Flask!)'
+
+@app.route('/test')
+def hello():
+ return 'this is a route for testing'
@app.route('/')
def index():
(1/1) Discard this hunk from worktree [y,n,q,a,d,e,?]?
Aïe ! s
n'est pas dans la liste... C'est assez logique : Git voit un groupe de lignes contiguës modifiées, il ne peut pas se douter qu'il s'agit en fait de 2 modifications distinctes.
Pas le choix : il va falloir répondre e
pour éditer manuellement le hunk. L'éditeur de texte configuré pour Git s'ouvre alors et nous donne le contrôle. Dans mon cas, c'est Visual Studio Code et le fichier est .git\addp-hunk-edit.diff
:
# Manual hunk edit mode -- see bottom for a quick guide.
@@ -4,7 +4,11 @@ app = Flask(__name__)
@app.route('/about')
def about():
- return 'This is a great app made with Flask'
+ return 'This is a great app (made with Flask!)'
+
+@app.route('/test')
+def hello():
+ return 'this is a route for testing'
@app.route('/')
def index():
# ---
# To remove '+' lines, make them ' ' lines (context).
# To remove '-' lines, delete them.
# Lines starting with # will be removed.
#
# If the patch applies cleanly, the edited hunk will immediately be
# marked for discarding.
# If it does not apply cleanly, you will be given an opportunity to
# edit again. If all lines of the hunk are removed, then the edit is
# aborted and the hunk is left unchanged.
La question posée était "Discard this hunk from worktree [y,n,q,a,d,e,?]?". On doit donc construire un patch qui permettra d'annuler les changements ajoutant la route /test
.
On modifie le fichier comme suit (je ne mets que la partie utile) :
@@ -4,7 +4,11 @@ app = Flask(__name__)
@app.route('/about')
def about():
return 'This is a great app made with Flask'
+
+@app.route('/test')
+def hello():
+ return 'this is a route for testing'
@app.route('/')
def index():
Quand on ferme le fichier, Git utilise ce patch pour terminer son restore
. Pour être sûr qu'on a bien fait ce qu'on voulait, on peut faire un petit git diff
:
$ git diff
diff --git a/app.py b/app.py
index 3b00884..7a87709 100644
--- a/app.py
+++ b/app.py
@@ -4,7 +4,7 @@ app = Flask(__name__)
@app.route('/about')
def about():
- return 'This is a great app made with Flask'
+ return 'This is a great app (made with Flask!)'
@app.route('/')
def index():
Splendide ! La modification qu'on souhaitait conservée est toujours là.
Conclusion
Voilà, c'est super ! Vous savez maintenant comment gérer finement vos changements en ligne de commande.
Dans ces commandes, on se dit que --patch
aurait pu être --partial
. En fait, on se rend compte que cette option nous laisse gérer finement les patchs que Git utilise en background pour faire ses opérations. Effet :
- Faire un
add
, c'est appliquer un patch à la staging area. - Faire un
commit
, c'est appliquer un patch au dépôt. - Faire un
restore
c'est appliquer un patch à la staging area ou au working tree.
C'était très clair avec notre exemple pour git restore
: on a édité un fichier de patch.
Bon, il s'avère que les éditeurs de code modernes permettent de faire de telles opérations via leurs GUI. Et il faut avouer que c'est souvent plus facile... Dans Visual Studio Code, il suffit de sélectionner des lignes pour décider quoi en faire (les actions sont aussi possibles en faisant un clic-droit sur le texte) :
Mais maintenant, vous savez comment comment faire en ligne de commande si vous êtes privé·e·s de GUI !
Posted on February 9, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.