Git – annexes

Git : les annexes 📎

Pour aller plus loin, hors séance ; chaque partie se lit seule

Git – annexes

Sommaire

Partie De quoi ça parle
Les internes de git ce qu'il y a dans .git/ : objets, branches, HEAD ; aprĂšs GIT1
rebase rejouer ses commits au lieu de fusionner ; aprĂšs GIT2
reflog retrouver un commit « perdu » ; aprÚs GIT2
IntĂ©grer d'autres dĂ©pĂŽts cherry-pick, histoires sans ancĂȘtre commun ; aprĂšs GIT2
gh GitHub en ligne de commande : installer, se connecter, forker, ouvrir une PR ; aprĂšs GIT2
Git – annexe : les internes

Les internes de git 🔬

Ce qu'il y a dans .git/, et pourquoi ça explique tout le reste

Annexe · à lire aprÚs GIT1, en 20 minutes, un terminal ouvert

Git – annexe : les internes

Pourquoi ouvrir le capot ?

Git a la rĂ©putation d'ĂȘtre compliquĂ©. À l'usage, ce qui est compliquĂ©, c'est de retenir des commandes sans savoir sur quoi elles agissent. Or le modĂšle en dessous est petit :

  • une base d'objets immuables, adressĂ©s par leur hash ;
  • des pointeurs (les branches, HEAD) vers ces objets ;
  • un journal des dĂ©placements de ces pointeurs (le reflog).

C'est tout. Nous allons le vérifier avec deux commandes de plomberie, git cat-file et git hash-object, dans le dépÎt carnet de la séance (le vÎtre fera aussi bien l'affaire).

Git – annexe : les internes

.git/ : le dépÎt, c'est ce dossier

$ ls -F .git
COMMIT_EDITMSG  HEAD  ORIG_HEAD  config  description  hooks/  index  info/  logs/  objects/  refs/
Entrée RÎle
objects/ la base : tous les fichiers, arborescences et commits, pour toujours
refs/heads/ les branches (un fichier par branche) ; refs/tags/, refs/remotes/
HEAD « oĂč suis-je » : le nom de la branche courante
index la zone de préparation, en binaire
logs/ le reflog
config remotes, options locales

Le working tree, ce sont vos fichiers à cÎté ; .git/ est l'historique. Supprimez .git/ et le projet redevient un simple dossier.

Git – annexe : les internes

Les objets : trois types, un hash

$ find .git/objects -type f | head -3
.git/objects/a9/bfd1f59efa6179f9a3d33fc937cee75158081e
.git/objects/98/4f5b63bb64c72453b18338a8130e0e16c10a50
.git/objects/c5/2f0cf1c8177960488734248311264a2539ee65
$ git cat-file -t a9bfd1f       # -t : le type
commit
$ git cat-file -t 984f5b6
blob
$ git cat-file -t c52f0cf
tree

Chaque objet est stockĂ© (compressĂ©) dans un fichier dont le nom est le SHA-1 de son contenu : 2 caractĂšres de dossier + 38 de fichier. MĂȘme contenu, mĂȘme hash, mĂȘme fichier : git ne stocke jamais deux fois la mĂȘme chose.

Git – annexe : les internes

Le blob : le contenu d'un fichier, sans son nom

$ git cat-file -p 984f5b6       # -p : le contenu, joliment
# CrĂȘpes

- 250 g de farine
- 3 Ɠufs
- 50 cl de lait

Cuisson : 3 min par face, Ă  feu vif.

Un blob est le contenu brut d'un fichier. Il ne connaĂźt ni son nom, ni ses droits, ni sa place dans l'arborescence : ce sont les trees qui portent cela. Renommer un fichier ne crĂ©e donc aucun nouveau blob ; et deux fichiers identiques dans deux dossiers partagent le mĂȘme blob.

Git – annexe : les internes

Le tree : un dossier

$ git cat-file -p c52f0cf                    # le tree racine du commit a9bfd1f
100644 blob 6f64d556a15d5aea3a3d1141dd91fa3fc5be8374	README.md
040000 tree b16b1c10d65458fd4be56ce9c835a371148f266e	recettes
$ git cat-file -p b16b1c1                    # le sous-dossier recettes/
100644 blob 984f5b63bb64c72453b18338a8130e0e16c10a50	crepes.md
100644 blob bd17b34bda037f60d92fa002a2bf5c787c1743c8	tarte.md

Un tree est une liste de lignes « mode, type, hash, nom » : un blob par fichier, un tree par sous-dossier. C'est exactement un dossier, photographié. Le hash d'un tree dépend de tout ce qu'il contient : un octet changé dans tarte.md change le hash de recettes, donc celui de la racine.

Git – annexe : les internes

Le commit : une photo, son parent, un auteur

$ git cat-file -p a9bfd1f
tree c52f0cf1c8177960488734248311264a2539ee65
parent 093450a4632ed7c6f08ed84c9b912c924e418ade
author Raphael Prasquier <raphael@imagine-app.fr> 1789034400 +0200
committer Raphael Prasquier <raphael@imagine-app.fr> 1789034400 +0200

PrĂ©cise la cuisson des crĂȘpes

Quatre lignes et un message. Le commit ne contient aucun diff : il pointe vers un tree (l'état complet du projet) et vers son parent. Un commit de fusion a simplement deux lignes parent.

Le hash du commit couvre tout cela, parent inclus : modifier un commit ancien change son hash, donc celui de tous ses descendants. C'est pour cela que « réécrire l'historique » est un vrai mot.

Git – annexe : les internes

hash-object : fabriquer un blob Ă  la main

$ echo "bonjour" | git hash-object --stdin
1cd909e05d33f0f6bc4ea1caf19b5749b434ceb3
$ echo "bonjour" | git hash-object -w --stdin      # -w : et écris-le dans .git/objects
1cd909e05d33f0f6bc4ea1caf19b5749b434ceb3
$ git cat-file -p 1cd909e
bonjour

Sans -w, git calcule seulement le hash ; le mĂȘme contenu donnera le mĂȘme hash sur votre machine, la mienne, et celle de GitHub. Avec -w, l'objet existe dans la base sans qu'aucun commit ne le rĂ©fĂ©rence : c'est ce que fait git add, qui Ă©crit le blob et note son hash dans l'index. Le commit, ensuite, ne fait qu'Ă©crire les trees et l'objet commit.

Git – annexe : les internes

Un commit est un snapshot, pas un diff

$ git cat-file -p 093450a^{tree}          # le tree du commit précédent
100644 blob 6f64d556a15d5aea3a3d1141dd91fa3fc5be8374	README.md
040000 tree 70e5859e6a2701295669403bf8aa65d4e8a2e567	recettes
$ git cat-file -p a9bfd1f^{tree}          # le tree du commit suivant
100644 blob 6f64d556a15d5aea3a3d1141dd91fa3fc5be8374	README.md
040000 tree b16b1c10d65458fd4be56ce9c835a371148f266e	recettes

Entre les deux commits, seule crepes.md a changĂ© : README.md a le mĂȘme blob (6f64d55) dans les deux photos, alors que le tree recettes a changĂ©. Chaque commit dĂ©crit l'Ă©tat complet ; git ne paie que ce qui diffĂšre, par partage des objets.

Le diff que montre git show est calculé à la demande, entre deux trees. En clair : git "ne sait pas" ce qu'un commit a changé, il le recalcule, et c'est instantané.

Git – annexe : les internes

Une branche : un fichier de 41 octets

$ ls .git/refs/heads
cuisson  garniture  main  sauce
$ cat .git/refs/heads/main
a9bfd1f59efa6179f9a3d33fc937cee75158081e
$ wc -c .git/refs/heads/main
41 .git/refs/heads/main                # 40 caractĂšres de hash + un retour Ă  la ligne

Voilà toute la branche. git branch sauce écrit un fichier ; git commit sur main remplace le hash par celui du nouveau commit ; git branch -d supprime le fichier. Les commits, eux, ne bougent pas : ils sont dans objects/.

💡 Sur un dĂ©pĂŽt avec beaucoup de branches, git les regroupe dans .git/packed-refs ; le fichier individuel peut alors manquer, git rev-parse main reste la façon fiable de lire le hash.

Git – annexe : les internes

HEAD : un pointeur vers un pointeur

$ cat .git/HEAD
ref: refs/heads/main                   # HEAD "symbolique" : je suis sur la branche main
$ git switch --detach 7ac8892
$ cat .git/HEAD
7ac88920ed4cb2ad2a54a214062ce350d6db7522   # HEAD "détachée" : je suis sur un commit

HEAD désigne normalement une branche ; c'est pour cela qu'un commit fait avancer la branche : git écrit le nouveau hash dans le fichier que HEAD désigne. En HEAD détachée, HEAD contient directement un hash : commiter met à jour HEAD
 et aucune branche.

Tout git switch se résume donc à : réécrire .git/HEAD, puis mettre le working tree et l'index en accord avec le tree du commit visé.

Git – annexe : les internes

Le reflog : le journal des pointeurs

$ head -3 .git/logs/HEAD               # (colonnes raccourcies ici)
0000000
 a7d3639
 Raphael <
> 1789023600 +0200  commit (initial): Crée le carnet de recettes
a7d3639
 7ac8892
 Raphael <
> 1789027200 +0200  commit: Ajoute la recette des crĂȘpes
7ac8892
 093450a
 Raphael <
> 1789030800 +0200  commit: Ajoute la tarte aux pommes et le sommaire
$ git reflog                           # la mĂȘme chose, lisible, du plus rĂ©cent au plus ancien

Chaque ligne : ancien hash, nouveau hash, qui, quand, pourquoi. Il y a un journal pour HEAD (logs/HEAD) et un par branche (logs/refs/heads/main). C'est local : un clone ne l'emporte pas.

Un reset --hard ne détruit rien : il écrit un ancien hash dans la branche, et note l'opération ici. Les commits « perdus » restent dans objects/ tant que git gc ne les a pas jugés inaccessibles et trop vieux (30 jours par défaut).

Git – annexe : les internes

Les packfiles : quand les objets sont rangés

$ git count-objects -v
count: 29                # objets "en vrac", un fichier chacun
size: 116
$ git gc
$ git count-objects -v
count: 0
in-pack: 29              # les mĂȘmes, dans un seul fichier .pack (+ son index .idx)
$ ls .git/objects/pack
pack-957df6de77d1cddbdb2f1c97a959daca57c70847.idx
pack-957df6de77d1cddbdb2f1c97a959daca57c70847.pack

Des blobs entiers pour chaque version d'un gros fichier finiraient par coûter cher. Périodiquement (ou au push/clone), git empaquette les objets et, dans le pack seulement, stocke certains comme des deltas par rapport à d'autres. C'est un détail de stockage : le modÚle reste « un commit = une photo », et cat-file ne voit pas la différence.

Git – annexe : les internes

Pourquoi c'est important

  • Le trio de GIT1 prend tout son sens : le working tree est un dossier ordinaire, l'index un fichier qui liste des hashes de blobs, HEAD un pointeur. add Ă©crit des blobs, commit Ă©crit des trees et un commit, switch réécrit HEAD.
  • Le reflog cesse d'ĂȘtre magique : c'est un fichier texte, et « perdre un commit » veut seulement dire qu'aucun pointeur ne le dĂ©signe plus.
  • Les messages d'erreur se lisent : « detached HEAD », « would be overwritten by checkout », « rejected, non-fast-forward » parlent tous de pointeurs et de trees.
  • GIT2 : git worktree extrait plusieurs commits dans plusieurs dossiers avec le mĂȘme .git/ ; rebase recrĂ©e des commits (nouveaux hashes, mĂȘme trees) ; origin/main n'est qu'un fichier de plus dans refs/remotes/.

Pour aller plus loin : Pro Git, chapitre 10 « Les tripes de Git », https://git-scm.com/book/fr/v2.

Git – annexe : rebase

rebase

Rejouer ses commits au lieu de fusionner ; Ă  lire aprĂšs GIT2

Git – annexe : rebase

rebase : rejouer au lieu de fusionner 🔁

MĂȘme situation que pour un merge commit : main et sauce ont avancĂ© chacune de leur cĂŽtĂ©. Au lieu de les rĂ©unir par un commit Ă  deux parents, git rebase main (depuis sauce) rejoue les commits de sauce au bout de main.

git merge sauce git rebase main 093450aa9bfd1f12ed6ffmerge 178222f 093450aa9bfd1f12ed6ffbe10b51 178222f deux parents la copie l'original, abandonné
  • mĂȘme contenu Ă  l'arrivĂ©e, mais une histoire en ligne droite, plus facile Ă  lire ;
  • les commits rejouĂ©s sont des copies : mĂȘme modification, nouveau parent, donc nouveau hash ; l'original n'est plus pointĂ© par aucune branche (le reflog le garde).
Git – annexe : rebase

rebase en pratique

$ git switch sauce
$ git rebase main                   ← rejoue sauce au bout de main
Successfully rebased and updated refs/heads/sauce.
$ git switch main
$ git merge sauce                   ← plus rien Ă  rĂ©unir : fast-forward
Fast-forward
  • conflit pendant un rebase : on rĂ©sout comme pour un merge, git add, puis git rebase --continue (pas commit) ; git rebase --abort remet tout comme avant ;
  • merge garde l'histoire telle qu'elle s'est passĂ©e, rebase la rend lisible : les deux sont corrects ;
  • git pull --rebase : fetch puis rebase sur origin/main, au lieu du merge de git pull.

⚠ La rĂšgle d'or : on ne rebase que des commits qui n'existent que chez vous. Pourquoi ? C'est la slide suivante.

Git – annexe : rebase

Pourquoi la rĂšgle d'or ?

AprĂšs un rebase, vos commits ont de nouveaux hashes : ce sont des copies. Si un collĂšgue avait dĂ©jĂ  tirĂ© les anciens, il aurait deux versions des mĂȘmes commits, et son prochain pull les mĂ©langerait.

  • votre branche Ă  vous, que personne d'autre n'a tirĂ©e : rebase autorisĂ© ;
  • main, ou toute branche partagĂ©e : jamais de rebase ;
  • si tout part de travers : git rebase --abort, ou git reflog pour retrouver l'Ă©tat d'avant.
Git – annexe : rebase

Mettre sa PR Ă  jour par rebase

En GIT2, nous avons mis la branche de PR à jour par git merge upstream/main. Beaucoup d'équipes préfÚrent le rebase, qui garde la PR en ligne droite au-dessus de main :

$ git fetch upstream
$ git rebase upstream/main            ← conflit ? corriger, git add, git rebase --continue
$ git push                            ← refusĂ© : votre branche a Ă©tĂ© réécrite
$ git push --force-with-lease         ← Ă©crase votre branche sur le fork
 si personne n'y a poussĂ© entre-temps
  • c'est permis par la rĂšgle d'or : votre branche de PR n'est tirĂ©e par personne d'autre ;
  • ⚠ --force nu Ă©crase sans regarder ; prenez l'habitude de --force-with-lease.

Pour toute question, vous pouvez me contacter par email : raphael@imagine-app.fr.

Git – annexe : reflog

reflog

Retrouver un commit « perdu » ; à lire aprÚs GIT2

Git – annexe : reflog

reflog : le filet de sécurité

$ git reset --hard HEAD~2
HEAD is now at 12ed6ff Ajoute les garnitures des crĂȘpes
$ git reflog
12ed6ff HEAD@{0}: reset: moving to HEAD~2
dd98832 HEAD@{1}: commit (merge): Merge branch 'cuisson'
$ git reset --hard HEAD@{1}          ← il est revenu
  • git note chaque dĂ©placement de HEAD et le garde des semaines, y compris vers des commits que git log ne montre plus ;
  • un commit n'est donc jamais perdu tant qu'il figure dans le reflog ;
  • ce qui se perd vraiment, c'est ce qui n'a jamais Ă©tĂ© commitĂ© (restore, reset --hard sur des modifications en cours).
Git – annexe : reflog

đŸ–ïž À essayer — perdre puis retrouver (5 min)

Dans un dépÎt d'essai (pas dans un projet qui compte) :

  1. git log --oneline : notez le hash du commit du haut ;
  2. git reset --hard HEAD~1 ; git log --oneline : il a disparu, et le fichier avec ;
  3. git reflog : retrouvez-le (ligne HEAD@{1}) ;
  4. git reset --hard HEAD@{1} ; git log --oneline : il est revenu ;
  5. git switch -c avant-reset HEAD@{2} : une branche posée sur un ancien état, pour le garder.

Retenez bien : un commit n'est jamais perdu tant qu'il figure dans le reflog (30 à 90 jours). Ce qui se perd, c'est ce qui n'a jamais été commité.

Pour toute question, vous pouvez me contacter par email : raphael@imagine-app.fr.

Git – annexe : autres dĂ©pĂŽts

Intégrer d'autres dépÎts

Un seul commit (cherry-pick), ou deux projets sans ancĂȘtre commun ; Ă  lire aprĂšs GIT2

Git – annexe : autres dĂ©pĂŽts

Un seul commit : cherry-pick 🍒

Parfois, on ne veut pas toute la branche d'une collĂšgue, seulement un commit (un correctif, typiquement). Camille (git-carnet-camille) a corrigĂ© les crĂȘpes, mais son essai de gaufres n'est pas prĂȘt :

$ git remote add camille https://github.com/raphaelpra/git-carnet-camille.git
$ git fetch camille
$ git log --oneline main..camille/main
2a064e0 Essai de gaufres (en cours)              ← celui-ci, non merci
b6c47e3 Corrige le nombre d'Ɠufs des crĂȘpes      ← celui-ci, oui
12ed6ff Ajoute les garnitures des crĂȘpes
$ git cherry-pick b6c47e3
[main 8c1d8fa] Corrige le nombre d'Ɠufs des crĂȘpes
  • cherry-pick rejoue la modification d'un commit au bout de votre branche : c'est une copie, avec un nouveau hash (le vĂŽtre sera diffĂ©rent de 8c1d8fa) ;
  • conflit possible : on rĂ©sout, git add, git cherry-pick --continue (ou --abort) ;
  • ⚠ Ă  l'usage, un commit copiĂ© vit deux fois dans l'histoire : pratique pour un correctif, pĂ©nible si on en abuse.
Git – annexe : autres dĂ©pĂŽts

Deux projets sans ancĂȘtre commun

Vous voulez réunir deux dépÎts nés séparément (votre code et celui d'un binÎme, un prototype et le vrai projet) :

$ git fetch voisin
$ git merge voisin/main
fatal: refusing to merge unrelated histories
$ git merge voisin/main --allow-unrelated-histories
merge à deux parents
 et deux racines main (votre projet) voisin/main (le sien)

Git refuse par dĂ©faut, car c'est presque toujours une erreur (le README créé par GitHub de la recette GIT1). Avec l'option, il crĂ©e un merge commit qui a deux racines : les deux historiques sont conservĂ©s en entier. Les fichiers de mĂȘme nom entrent en conflit, comme d'habitude.

Git – annexe : autres dĂ©pĂŽts

đŸ–ïž À essayer — fusionner deux projets (7 min)

Camille tient de son cÎté un carnet de pùtisserie, né à part : https://github.com/raphaelpra/git-patisserie. Faisons-le entrer dans votre clone de git-carnet :

  1. git remote add patisserie https://github.com/raphaelpra/git-patisserie.git
  2. git fetch patisserie, puis git branch -r et git remote -v ;
  3. git log --oneline --graph --all : combien d'arbres voyez-vous ? pourquoi ?
  4. git show patisserie/main:recettes/macarons.md : sa recette, sans quitter votre branche ;
  5. git switch -c fusion puis git merge patisserie/main : lisez le refus ; recommencez avec --allow-unrelated-histories ; conflit sur README.md : résolvez-le (gardez les deux titres), add, commit ;
  6. ls recettes/ : les crĂȘpes et les macarons ; puis rangez : git switch main, git branch -D fusion, git remote remove patisserie.

Question : aprÚs l'étape 6, les commits de Camille sont-ils encore dans votre .git ?

Pour toute question, vous pouvez me contacter par email : raphael@imagine-app.fr.

Git – annexe : gh

gh

GitHub en ligne de commande ; Ă  lire aprĂšs GIT2

Git – annexe : gh

Installer gh, puis se connecter

gh est l'outil officiel de GitHub en ligne de commande ; il s'installe à part de git. Rien de ce qu'on fait en cours n'en a besoin, mais il simplifie l'authentification (surtout sur Mac) et évite des allers-retours sur le site.

SystĂšme Installation
Windows winget install --id GitHub.cli dans un terminal, puis fermez et rouvrez Git Bash (sinon : l'installateur .msi sur https://cli.github.com)
macOS brew install gh (il faut Homebrew ; sinon : le .pkg sur https://cli.github.com)
Linux le paquet de votre distribution, cf. https://github.com/cli/cli#installation
$ gh --version                     ← vĂ©rifie l'installation
$ gh auth login                    ← rĂ©pondez : GitHub.com, HTTPS, puis « Login with a web browser »
$ gh auth status                   ← « Logged in to github.com account vous »

💡 AprĂšs gh auth login, git utilise gh pour s'authentifier en HTTPS : plus de mot de passe ni de jeton Ă  gĂ©rer.

Git – annexe : gh

Ce que gh fait Ă  la place du site

$ gh repo fork raphaelpra/git-atelier-2026 --clone   ← fork + clone + remote upstream, en une commande
$ gh pr create --fill                                ← ouvre la PR de la branche courante
$ gh pr list                                         ← les PR ouvertes du dĂ©pĂŽt
$ gh pr checkout 12                                  ← rĂ©cupĂšre la branche de la PR n°12, pour la tester
$ gh pr view --web                                   ← ouvre la PR dans le navigateur

Tout ce que fait gh se fait aussi sur le site : c'est une question de confort. Les commandes git, elles, ne changent pas.

Pour toute question, vous pouvez me contacter par email : raphael@imagine-app.fr.