GIT avancé

Table of Contents

Ce fichier est disponible en ligne à http://people.bordeaux.inria.fr/goglin/bidouille-git.org

1 Prérequis

Commandes de bases: show, status, log, commit, add, pull, push, merge, branch. Les bases des branches locales et distantes. Résoudre les conflits dans pull ou merge. Connaître la différence entre les changements staged ou non. Les descripteurs de commits HEAD^^, HEAD~5, etc.

2 Outils graphiques

Visualiser une branche

$ gitk

Toutes les branches

$ gitk --all

Committer en GUI

$ git gui

3 Committer partiellement avec -p/–patch

Sélectionner interactivement les hunks à committer:

$ git commit -p

Avec "s" pour split, on peut diminuer la granularité.

Sélectionner interactivement les hunks à ajouter à l'index:

$ git add -p
$ git commit

Si un fichier a été déjà été ajouté à l'index, il va forcément être committé la prochaine fois. Le destager sans le supprimer:

$ git rm --cached fichier

Ou sinon, committer ce qui va avec ce fichier, puis committer le reste, puis inverser les commits (voir git rebase -i plus loin).

4 Committer partiellement des hunks intriqués

J'ai fait un peu trop de changements mélangés, je dois découper des hunks, -p/–patch ne suffit pas. Comment faire ?

On commence par sauver l'état actuel.

$ git commit -a -m "sauve tout"
$ git branch sauvetout

Puis on revient en arrière et on vire les changement sans rapport.

$ git reset HEAD^
$ nano mes_fichiers_ou_je_vais_enlever_des_changements
$ git commit -a -m "premiere partie des changements"

On va dans la branche sauvée, et on replace son index sur le commit intermédiaire.

$ git checkout sauvetout
$ git reset master
$ git status

Les changements restants sont prêts à être commités comme d'habitude. Puis on pourra virer la branche temporaire.

$ git commit -a -m "le reste des changements"
$ git checkout master
$ git merge sauvetout
$ git branch -d sauvetout

5 Mettre de coté des commits avec git stash

Le stash est une pile de changements.

Empiler des changements (ajoutés à l'index ou pas):

$ git stash

Voir un changement mis de coté:

$ git show stash
$ git show stash@{2}

Si les changements étaient dans l'index au moment du stash, git show stash ne les montre pas, mais ils sont bien enregistrés.

Dépiler et appliquer des changements mis de coté:

$ git stash pop stash@{3}

Ils ne sont pas ajoutés à l'index (même s'ils étaient avant le stash).

En cas de conflit lors de l'application, le stash n'est pas enlevé de la pile. Pour committer et le supprimer de la pile:

$ git add fichierenconflit
$ git commit
$ git stash drop

On ne peut appliquer un stash que si les fichiers cibles sont à jour vis-à-vis de l'index. Donc pour appliquer 2 stashs, il faut

$ git stash pop
$ git add ...
$ git stash pop
$ git add ...

6 Mettre de coté des commits avec des branches

Committer et mettre de coté

$ git commit ...
$ git branch mabranchedesauvegarde
$ git reset --hard HEAD^

S'il y a plusieurs commits et si on est sur master

$ git commit ...
...
$ git commit ...
$ git branch save
$ git reset --hard origin/master

7 Reprendre une vieille branche, avec des merges

$ git merge mabranche
$ git branch -d mabranche

Idem avec pull

$ git pull . mabranche
$ git branch -d mabranche

Si la branche est distante

$ git pull origin mabranche

Mais ca crée un merge, l'historique n'est plus forcément linéaire (notamment si la branche est vieille, basée sur un vieux master).

On veut souvent éviter ça, notamment si la branche n'est pas distante/publique: il n'y a pas de raisons d'exposer publiquement nos merges dû à notre historique de développement privé.

8 Reprendre une vieille branche, avec rebase

Mettre à jour une branche au dessus du dernier master.

$ git checkout mabranche
$ git rebase master

Rebase réapplique tous les commits de mabranche sur master 1 par 1.

Ensuite on pourra la merger en fast-forward, l'historique sera linéaire.

$ git checkout master
$ git merge mabranche
$ git branch -d mabranche

En cas de conflit dans le rebase, bien lire ce qui s'affiche. Voir les fichiers qui ont conflicté puis résoudre les conflits:

$ git status
$ nano monfichier.c
$ git add monfichier.c
$ git rm mon_vieux_fichier_supprimé
...
$ git rebase --continue

Puis recommencer sur le commit suivant.

Virer un commit inutile qui a conflicté pendant le rebase mais qui était déjà dans master:

$ git rebase --skip

Si on est perdu, abandonner le rebase:

$ git rebase --abort

rebase, c'est bien pour les branches privées. Et pour les branches publiques/distantes ? Pas grave si la branche disparait apres ce (faux) merge. Pas bien si d'autres vont continuer à utiliser la branche après.

rebase peut-etre long si la branche est grande (beaucoup de conflits à résoudre sur beaucoup de commits, alors que 1 seul conflit pour un merge).

9 Reprendre une vieille branche, avec cherry-pick

S'il y a un seul commit, on peut éviter de jouer avec les branches

$ git cherry-pick save

Si nécessaire, corriger les conflits puis committer.

$ nano monfichier
$ git add monfichier
$ git commit

Et détruire la branche

git branch -D save

S'il y a N commits, c'est possible mais un peu fastidieux

$ git cherry-pick save^^
$ git cherry-pick save^
$ git cherry-pick save
$ git branch -D save

Avec résolutions de conflit entre temps à chaque fois

cherry-pick permet aussi de prendre plusieurs commits d'un coup avec les options –continue etc.

10 Backporter des commits entre branches

Pour appliquer un commit (qui vient de n'importe quelle branche) sur l'index courant:

$ git cherry-pick <id>

Pour appliquer un commit de master dans une branche stable:

$ git checkout stable
$ git cherry-pick -x master~3
$ nano NEWS
$ git commit --amend NEWS

Ne pas oublier -x pour savoir d'où vient le commit backporté.

11 Pousser une branche sauf les derniers commits

Pusher uniquement mon premier commit de ma branche B locale, pas les 2 suivants, dans la branche B distante:

$ git push origin B^^:B

12 Nettoyer l'historique avant de pusher

Pour ranger les derniers commits, on rebase depuis le père du premier à ranger.

$ git rebase -i HEAD~6
$ git rebase -i premier^

Les commits apparaissent dans l'ordre chronologique. pick signifie qu'ils vont être appliqués. On peut permuter l'ordre d'application en permutant les lignes. Si on supprimer une ligne, le commit sera ignoré et disparaitra donc de l'historique. On peut fusionner le contenu avec ou sans le commit log (s ou f). Arrêter le rebase pour éditer le commit ou le message (e ou r). On peut meme ajouter n'importe quel commit qui n'a rien à voir avec pick et son identifiant. Le message à droite n'est là que pour nous aider à reconnaitre les commits.

Quand ca s'arrete avec e, le commit est déjà appliqué. On peut le modifier avec amend ou en rajouter un avec commit, etc. Puis git rebase –continue pour reprendre la suite des commits à appliquer.

Quand ca s'arrete sur conflit, le commit n'est pas appliqué, il est juste dans l'index. On corrige les conflits avec git add/rm puis on committe/continue avec git rebase avec –continue.

Attention à ne pas faire un rebase avec des merges au milieu, car l'historique va être linéarisé: Les commits des branches seront appliqués en ordre chronologique, (comme des cherry-pick) mais sans aucun merge.

13 Au secours, j'ai effacé un commit ou une branche par erreur !!

Si vous avez effacé une branche par erreur mais connaissez l'identifiant de son dernier commit (dans un git log ou git show par exemple):

$ git branch branche <id>

Si vous avez tapé par erreur une de ces commandes:

$ git reset <id>
$ git reset --hard <id>
$ git branch -D <branch>

ou si vous avez fait une betise dans rebase (enlevé une ligne, …).

On peut revoir l'historique de la tete de branche courante

$ git reflog

Tous les commits qui y sont référencés (et leurs parents) restent accessibles (sauf si prune ou gc, cf plus loin). Donc on peut y retourner ou les réappliquer, les voir dans gitk, …

$ git show <id>
...
$ git log <id>
...
$ gitk <id>
...
$ git reset --hard <id~3>
...
$ git cherry-pick <id^^>

14 Retrouver un commit mechant qui a pété un truc

Si on connait un commit ou tag qui marche et un qui foire.

$ git bisect good <mon_commit_qui_marche>
$ git bisect bad <mon_commit_qui_foire>
... tester ... ca marche ...
$ git bisect good
... tester ... ca marche pas ...
$ git bisect bad
... tester ...

Il se débrouille avec les merges de branches!

L'aider un peu:

$ git bisect start <foire> <marche1> <marche2> ... -- repertoiresourceatester ...

Et on peut automatiser avec un script

$ git bisect run commande

commande doit retourner 0 en cas de succes, et 1 (ou plus, cf le man) en cas d'erreur. commande peut compiler des trucs, stasher/destasher, etc…

15 Ranger et/ou optimiser un repository plein de commits pourris

Certains commits ne sont plus référencés. Par exemple ceux d'avant un rebase. Ou si on supprime une branche avec -D. Ou si on git reset.

Afficher les commits inutilisés:

$ git fsck

Supprimer les commits inutilisés:

$ git prune
$ git prune-packed

Grouper les commits par paquets:

$ git repack -a

Faire un gros nettoyage (utilise les commandes ci-dessus):

$ git gc

git gui signale quand le repo a besoin de nettoyage.

16 Deux checkouts sans dupliquer le clone

Si vous manipulez plusieurs branches souvent (branches stables et trunk), changer de branche tout le temps n'est toujours idéal (automake qui doit recompiler plein de choses, etc). Il vaut mieux utiliser différents checkouts. Mais ca prend de la place, et il faut les synchroniser pour pouvoir cherry-picker.

Checkouter une branche dans un sous-repo:

$ git-new-workdir old new branch

Attention si une même branche est checkoutée à plusieurs endroits, à bien mettre à jour les autres (comme pour un push dans un checkout).

Sur Debian, ca se trouve dans /usr/share/doc/git/contrib/workdir/git-new-workdir qu'on peut le mettre en executable de manière permanente (à travers les upgrades):

$ dpkg-statoverride -add root root 755 chemin

17 Fin des choses discutées le 05/12

18 Résoudre des conflits évidents

Fichier effacé d'un coté et modifié de l'autre

$ git rm fichier

Fichier modifié des deux cotés. Pour garder la version de la branche courante:

$ git checkout --ours -- fichier
$ git add fichier

Pour la version de la branche qu'on a fusionné (ou un cherry-pick, etc):

$ git checkout --theirs -- fichier
$ git add fichier

19 Pousser une branche locale A dans une distante B

Push direct

$ git push origin A:B

La branche destination peut être configurée de manière permanente si on configure push pour le respecter:

$ git config --global --add push.default upstream

Pour pusher et configurer de manière permanente que la branche locale A va dans B sur origin:

$ git push -u origin A:B
...
$ git push origin A

Si vous n'etes pas sur de la branche destination, ajoutez –dry-run à git push!

Si origin/B existe déjà, jusqu'à Git 1.7

$ git branch A --set-upstream origin/B
$ git push origin A

A partir de Git 1.8 (car l'ordre des options ci-dessus est pas clair)

$ git branch A --set-upstream-to=origin/B
$ git push origin A

Sinon ca se configure dans .git/config

[branch "A"]
        remote = origin
        merge = refs/heads/B

Sinon on peut appliquer d'abord dans le B local

$ git checkout B
$ git merge A
$ git push origin B

20 Nettoyer mon tas de branches

Effacer une branche locale déjà fusionnée (i.e. dont les commits sont dans la branche courante ou upstream):

$ git branch -d foo

Effacer une branche locale pas fusionnée:

$ git branch -D foo

Les commits de la branche existent encore, ils peuvent encore être cherry-pickés par commit id, etc. Mais ils pourront disparaître si on fait le ménage (voir plus loin).

Effacer la copie locale d'une branche distante:

$ git branch -rd origin/foo

Mais elle va revenir au prochain fetch si elle existe encore sur l'autre repository.

Effacer une branche distante:

$ git push origin :branch

Pas très utile pour les branches fusionnées: les commits sont dans la branche qui a reçu la fusion, donc la branche elle-même n'est qu'une référence sur un commit.

Supprimer les branches locales dont la distante correspondante n'existe plus:

$ git remote prune origin

21 Effacer plein de modifs

Effacer les changements locaux (index et modifications de fichiers), annuler un cherry-pick pas commité, supprimer un stash appliqué, …

$ git reset --hard

Annuler le dernier commit pas encore poussé:

$ git reset --hard HEAD^

Donne un contenu de fichiers identique à:

$ git revert HEAD^

Mais l'historique après revert contient le commit puis son annulation. revert à utiliser si le commit à annuler est déjà poussé dans la branche distante/publique.

Effacer des commits pas encore pushés:

$ git reset --hard <le_dernier_commit_que_je_veux_garder>

Enlever de l'index ce qui a été ajouté sans être committé, sans effacer les changements des fichiers correspondants:

$ git reset

Annuler des commits sans effacer les changements:

$ git reset <le_dernier_commit_que_je_veux_garder>

Remplacer une branche A par le contenu d'une branche B:

$ git checkout A
$ git reset --hard B

Attention, si l'ancien A est publique et n'est pas un ancêtre de B, on ne pourra plus pousser A en fast-forward.

Tout nettoyer les fichiers pas trackés par le repository (un peu comme make maintainer-clean)

$ git clean -fdx

22 Retrouver le mechant commit qui a changé une ligne

$ git blame fichier

Ajouter -w pour ignorer les indentations.

Si le commit trouvé n'est pas le dernier, blamer le parent:

$ git blame <id>^2 -- fichier

Le faire graphiquement:

$ git gui blame fichier

Bouton droit pour blamer le commit parent

23 Passer a travers du firewall de PlaFRIM

Je ne peux pas cloner depuis plafrim ou ailleurs, arg!

Créer un clone vide sur PlaFRIM et autoriser les autres à modifier sa branche checkoutée (avec un warning):

plafrim$ mkdir repertoireduclone
plafrim$ cd repertoireduclone
plafrim$ git init
plafrim$ git config --global --add receive.denyCurrentBranch warn

Développer sur votre machine puis pousser sur PlaFRIM:

local$ git remote add plafrim git+ssh://plafrim/chemin
local$ git push plafrim master

Tester sur PlaFRIM. Attention, le checkout n'est pas a jour, car la branche checkoutée a été modifiée par le push distant.

plafrim$ git status
... on n'est pas clean ...
plafrim$ git reset --hard

Vérifier qu'il y a bien vos changements.

Tester puis corriger sur PlaFRIM, committer en local, puis rapatrier sur votre machine, nettoyer et pousser sur le repo officiel.

local$ git pull plafrim master
local$ git rebase -i origin/master
...
local$ git push origin master

Mettre à jour PlaFRIM si vous avez fait un rebase, mais ce n'est plus un fast-forward, il faut écraser:

local$ git push -f plafrim master
plafrim$ git reset --hard

Vérifier qu'il y a bien vos changements.

Attention à ne pas pousser depuis local vers PlaFRIM si PlaFRIM a des changements non commités, sinon git reset –hard va les supprimer. Utiliser git stash pour les mettre de coté.

Date: 05 décembre 2014

Author: Brice Goglin

Created: 2019-11-15 or. 17:26

Validate