L'option --patch de Git
Tags: git, programming, todayilearned
Published: 2023-02-09 14:51:05

Vous avez déjà remarqué que plusieurs commandes Git ont une option `--patch` ? On peut citer [`add`](https://git-scm.com/docs/git-add#Documentation/git-add.txt---patch), [`checkout`](https://git-scm.com/docs/git-checkout#Documentation/git-checkout.txt-emgitcheckoutem-p--patchlttree-ishgt--ltpathspecgt82308203), [`commit`](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---patch), [`reset`](https://git-scm.com/docs/git-reset#Documentation/git-reset.txt-emgitresetem--patch-plttree-ishgt--ltpathspecgt82308203),
[`restore`](https://git-scm.com/docs/git-restore#Documentation/git-restore.txt--p) ou encore [`stash`](https://git-scm.com/docs/git-stash#Documentation/git-stash.txt---patch).

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`](https://www.gnu.org/software/diffutils/manual/html_node/Hunks.html) :

> 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](https://unix.stackexchange.com/questions/356652/is-git-diff-related-to-diff) ou [là](
https://stackoverflow.com/questions/4857310/how-to-get-diff-working-like-git-diff)).

### --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) :

```bash
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`](https://git-scm.com/docs/git-add#Documentation/git-add.txt---patch) :

> `-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 the `patch` 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](https://git-scm.com/docs/git-diff#_generating_patch_text_with_p) 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`](https://git-scm.com/docs/git-am) ou [`git apply`](https://git-scm.com/docs/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](https://www.younup.fr/blog/heroku-pour-deployer-votre-application-python-flask-dans-le-cloud) :

```python
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 :

```bash
$ pip install flask
$ flask run
```

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

```bash
$ 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 :

```python
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` :

```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_.

```diff
λ 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](https://git-scm.com/docs/git-add#Documentation/git-add.txt-patch) :

```raw
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` :

```diff
(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_ :

```diff
$ 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_ :

```diff
$ 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 :

```bash
$ 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 :

```bash
$ git log --oneline
4a39c0b (HEAD -> master) Fix typo in route /
a512faf Initial
```

```diff
$ 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é](https://drewdeponte.com/blog/git-add-patch-wont-split/)...

Modifions notre fichier pour être dans un tel cas :

```python
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` :

```diff
$ 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` :

```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) :

```diff
@@ -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` :

```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) :

![plusieurs changements dans VSC](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tjjw741szxlxnji9vkan.png)

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