<
Media
>
Article

L'option --patch de Git

7 min
05
/
01
/
2023

Vous avez déjà remarqué que plusieurs commandes Git ont une option <span class="css-span">--patch</span> ? On peut citer <span class="css-span">add</span>, <span class="css-span">checkout</span>, <span class="css-span">commit</span>, <span class="css-span">reset</span>, <span class="css-span">restore</span> ou encore <span class="css-span">stash</span>.

Dans la suite de cet article, on va modifier un fichier déjà versionné et on verra comment utiliser l'option <span class="css-span">--patch</span> (<span class="css-span">-p</span> 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 <span class="css-span">--patch</span> commence souvent par :

Interactively select hunks

On retrouve souvent ce terme de "hunk". C'est un terme important à 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 <span class="css-span">diff</span> vient faire dans un article parlant de Git, c'est juste que <span class="css-span">git diff</span> et <span class="css-span">git apply</span> ne sont pas très différentes de <span class="css-span">diff</span> et <span class="css-span">patch</span> (comme discuté ici ou ).

--patch vs --interactive

On vient de dire que l'option <span class="css-span">--patch</span> 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 : <span class="css-span">--interactive</span>. Quelle différence entre les 2 ?

Voici ce qu'on obtient en utilisant <span class="css-span">--interactive</span> (ici avec <span class="css-span">git add</span>,mais c'est totalement similaire avec d'autres commandes) :

<pre><code>git add --interactive

*** Commands ***  
1: status       2: update       3: revert       4: add untracked  
5: patch        6: diff         7: quit         8: help
What now></code></pre>

En vrai, <span class="css-span">--patch</span> est un raccourci pour lancer le mode interactif et choisir <span class="css-span">patch</span> dans ce menu. C'est clairement expliqué dans la documentation de git add :

<span class="css-span">-p</span></br>
<span class="css-span">--patch</span>
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 <span class="css-span">add --interactive</span>, but bypasses the initial command menu and directly jumps to the <span class="css-span">patch</span> subcommand.

Disclaimer

Quand on prononce le mot "patch", on pense instinctivement à des fichiers avec l'extension <span class="css-span">.patch</span>, 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 <span class="css-span">--patch</span> : <span class="css-span">diff</span>, <span class="css-span">log</span>, <span class="css-span">show</span>, <span class="css-span">diff-index</span>, <span class="css-span">diff-tree</span>, <span class="css-span">diff-files</span>. Elles génèrent alors des patchs, qu'on peut récupérer (par exemple dans des fichiers) et ensuite appliquer avec <span class="css-span">git am</span> ou <span class="css-span">git apply</span>.

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 <span class="css-span">--interactive</span>).

Notre application

Place aux exemples maintenant ! Pour cela, partons d'un dépôt avec un unique fichier <span class="css-span">app.py</span> pour réaliser une superbe application avec Flask :

<pre><code>from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():    
     return 'hello, wordl'</code></pre>

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 <span class="css-span">app.py</span> et de faire :

<pre><code>$ pip install flask
$ flask run</code></pre>

On fait un premier commit de ce fichier pour obtenir l'état initial de notre dépôt :

<pre><code>$ git log --oneline
a512faf (HEAD -> master) Initial</code></pre>

Evolution du code

Continuons le développement de notre application et ajoutons une autre route. Au passage, corrigeons également l'affreuse typo à <span class="css-span">wordl</span> (vous l'aviez remarqué hein ?).

Notre fichier ressemble maintenant à ça :

<pre><code>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'</code></pre>

On peut voir les modifications apportées au fichier grâce à <span class="css-span">git diff</span> :

<pre><code>$ 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'</code></pre>

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 <span class="css-span">commit --all -m "Fix bug + add feature"</span> n'est pas optimal car il a 2 justifications.

On a donc envie de faire 2 commits d'<span class="css-span">app.py</span>, mais comment faire ?

Grâce à l'option <span class="css-span">--patch</span> des commandes <span class="css-span">git add</span> et <span class="css-span">git commit</span>, 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 <span class="css-span">app.py</span> en entier, comme on le fait généralement.

Solution 1 = <span class="css-span">add</span> puis <span class="css-span">commit</span>

Au lieu de faire <span class="css-span">git add mon_fichier</span>, on fait <span class="css-span">git add --patch mon_fichier</span>. 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.

<pre><code>vλ 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,?]?</code></pre>

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 :

<pre><code>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</code></pre>

On souhaite splitter ce hunk en 2 hunks, pour n'ajouter que la seconde modification. On répond donc <span class="css-span">s</span>, puis <span class="css-span">n</span> et enfin <span class="css-span">y</span> :

<pre><code>(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</code></pre>

Vérifions le contenu de la staging area :

<pre><code>$ 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'</code></pre>

Vérifions aussi le contenu du working tree :

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

</code></pre>

C'est parfait ! Pour terminer, il suffit de commiter le contenu de la staging area avec <span class="css-span">git commit -m "Fix typo in route /"</span>.

Solution 2 = <span class="css-span">commit</span> direct

On n'est pas obligé de passer par la staging area pour commiter des changements. On peut faire directement :

<pre><code>$ git commit --patch -m "Fix typo in route /"<code></pre>

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 :

<pre><code>$ 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():</code></pre>

Forcer le split d'un hunk

Des fois, on ne peut pas splitter un hunk. La lettre <span class="css-span">s</span> n'est pas dans la liste et on est un peu embêté...

Modifions notre fichier pour être dans un tel cas :

<pre><code>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'</code></pre>

Imaginons qu'on souhaite supprimer l'ajout de la route <span class="css-span">/test</span>, mais conserver la modification de la route <span class="css-span">/about</span>. Tentons de faire un <span class="css-span">git restore</span> en lui passant l'option <span class="css-span">--patch</span> :

<pre><code>$ 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,?]?</code></pre>

Aïe ! <span class="css-span">s</span> 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 <span class="css-span">.git\addp-hunk-edit.diff</span> :

<pre><code># 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 <span class="css-span">/test</span>.

On modifie le fichier comme suit (je ne mets que la partie utile) :

<pre><code>@@ -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():</code></pre>

Quand on ferme le fichier, Git utilise ce patch pour terminer son <span class="css-span">restore</span>. Pour être sûr qu'on a bien fait ce qu'on voulait, on peut faire un petit <span class="css-span">git diff</span> :

<pre><code>$ 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():</code></pre>

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 <span class="css-span">--patch</span> aurait pu être <span class="css-span">--partial</span>. 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 <span class="css-span">add</span>, c'est appliquer un patch à la staging area.
  • Faire un <span class="css-span">commit</span>, c'est appliquer un patch au dépôt.
  • Faire un <span class="css-span">restore</span> c'est appliquer un patch à la staging area ou au working tree.

C'était très clair avec notre exemple pour <span class="css-span">git restore</span> : 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) :

plusieurs changements dans VSC

Mais maintenant, vous savez comment comment faire en ligne de commande si vous êtes privé·e·s de GUI !

No items found.
ça t’a plu ?
Partage ce contenu
Pierre

Que la vie de Pierre, expert embarqué Younup, serait terne sans les variadic templates et les fold expressions de C++17. Heureusement pour lui, Python a tué l'éternel débat sur l’emplacement de l’accolade : "alors, à la fin de la ligne courante ou au début de la ligne suivante ?"

Homme de terrain, il est aussi à l’aise au guidon de son VTT à sillonner les chemins de forêt, dans une salle de concert de black metal ou les mains dans les soudures de sa carte électronique quand il doit déboguer du code (bon ça, il aime moins quand même !)

Son vœu pieux ? Il hésite encore... Faire disparaitre le C embarqué au profit du C++ embarqué ? Ou stopper la génération sans fin d'entropie de son bureau.