User Tools

Site Tools


projtec:git

Git

Un bon tutoriel pour jouer de manière interactive (https://learngitbranching.js.org/) et une sandbox en bonus (https://learngitbranching.js.org/?NODEMO).

Commençons par créer un dépôt Git “remote” vide, qui se trouve en principe sur un serveur distant. Dans cet exemple, il se trouvera dans le répertoire /tmp/remote de la machine locale.

mkdir /tmp/remote
cd /tmp/remote
git init --bare --shared

Nous pouvons maintenant “clôner” notre dépôt Git en local dans un répertoire “local”.

git clone /tmp/remote /tmp/local
cd /tmp/local

A partir de maintenant nous pouvons commencer à travailler dans le dépôt local.

Tout d'abord, modifions un peu la configuration globale de Git sur la machine :

# mail / user name
git config --global user.name "orel"
git config --global user.email aurelien.esnard@u-bordeaux.fr
# editeur de texte
git config --global core.editor emacs
# coloration syntaxique
git config --global color.diff auto
git config --global color.status auto
git config --global color.branch auto

Pour afficher votre configuration :

git config --global -l

Commandes de base

Ensuite, on va pouvoir commencer à jouer le depôt local (/tmp/local). Tout d'abord, on crée un fichier “date.txt”. Initialement, ce fichier a comme le statut untracked, c'est-à-dire non suivi.

date > date.txt 
git status   

Pour suivre ce fichier dans le dépôt Git, il va falloir tout d'abord l'ajouter à l'index (git add). Puis dans un second temps, on va pouvoir effectuer le commit, ce qui consiste à conserver dans le dépôt local une version de ce fichier. On prendra garde de mettre des commentaires compréhensibles lors des commits.

git add date.txt               
git commit -m "premier commit"
git status

A tout moment, la commande git status permet de vérifier le statut des différents fichiers afin d'éviter les bétises : à utiliser sans modération ! Personnellement, j'aime bien la version courte : git status -s.

Dans un second temps, on modifie le fichier… On va souhaiter committer cette nouvelle version dans le dépôt local. Attention, il ne faut pas oublier de signaler la modification d'un fichier en faisant un git add avant d'effectuer le commit !

date > date.txt                
git status                     # le fichier est indiqué comme "modifié"
git add date.txt               
git commit -m "second commit"  

Synchronisation avec le dépôt distant

Les commits sont uniquement locaux et n'ont aucun effet sur le dépôt distant (remote). D'ailleurs, il n'est pas nécessaire d'être connecté à Internet pour effectuer ces commits ! Pour synchroniser le dépôt local avec le dépôt distant (remote), il faut faire un push. Ainsi tous les commits vont être “poussés” sur ce serveur pour y être archivés.

git push

A l'inverse, pour mettre à jour son dépôt local, quand le dépôt remote a été modifié (par quelqu'un d'autre), il faut faire un pull.

git pull

Log & Diff

La commande log permet de suivre l'historique des “commits”. On notera que chaque commit produit une nouvelle version de l'ensemble du dépôt, identifié par un ID de la forme “83109ce0d335a…4e8bd58f”.

git log

En un peu plus complet :

git log --summary

En un peu plus joli :

git log --graph --oneline --all

Par ailleurs, la commande diff permet de voir quelles modifications ont été effectuées pour tous les fichiers ou pour un fichier particulier :

git diff [file]

Il existe plusieurs variantes de diff :

git diff <commit> <commit> [file] # comparaison explicite entre deux commits
git diff <commit>          [file] # comparaison du workspace avec un commit
git diff                   [file] # comparaison du workspace avec HEAD

avec <commit>, un identifiant de commit qui peut être :

  • un ID explicite comme “83109ce0d335a…4e8bd58f” tel qu'affiché par git log ;
  • HEAD, qui désigne le dernier commit dans la branche courante du dépôt local ;
  • HEAD~1, qui désigne l'avant dernier commit dans la branche courante du dépôt local ;
  • un nom de branche sur le dépôt local comme master ;
  • un nom de branche sur le dépôt distant (origin), comme origin/master.

Une option intéressante permet d'afficher uniquement le nom des fichiers modifiés~:

git diff --name-only

Gestion des conflits

Imaginons maintenant que deux développeurs ont commité des modifications sur leur copie locale respective. Que se passe-t-il au moment de la synchronisation ? On va devoir fusionner ces modifications, ce qui peut engendrer un conflit quand les modifications portent sur le même fichier !

Simulons le problème avec deux copies locales du dépôt : local1 et local2. Dans ce premier cas, nous allons ajouter concurremment deux fichiers différents :

# récupérons deux copies loales du dépôt
git clone /tmp/remote /tmp/local1
git clone /tmp/remote /tmp/local2
 
# ajout de date1.txt dans local1
cd /tmp/local1
date > date1.txt
git add date1.txt
git commit -m "ajout de date1"
 
# ajout date2.txt dans local2
cd /tmp/local2
date > date2.txt
git add date2.txt
git commit -m "ajout de date2"
 
# fusion
cd /tmp/local1
git push          # ok
cd /tmp/local2
git push          # ko (rejected, pull before!)
git pull          # ok après merge automatique...

Regardons maintenant ce qu'il se passe, si on modifie concurrement le même fichier, disons “date1.txt”…

# modification de date1.txt dans local1
cd /tmp/local1
date >> date1.txt
git add date1.txt
git commit -m "modif de date1"
 
# modification de date1.txt dans local2
cd /tmp/local2
date >> date1.txt
git add date1.txt
git commit -m "modif aussi de date1"
 
cd /tmp/local1
git push          # ok
cd /tmp/local2
git push          # ko (rejected, pull before!)
git pull          # conflit, le merge automatique échoue !!!

Pour résoudre le conflit, il faut éditer le fichier “date1.txt” responsable du conflit et faire la fusion à la main… Le fichier en cause marque explicitement la zone de conflit et affiche les deux versions à comparer…

<<<<<<< HEAD
ma version HEAD dans le dépôt local
=======
l'autre version qui vient du dépôt remote
>>>>>>> <commit>

On corrige à la main, on commit et on push à nouveau ;-)

Les branches locales et distantes

Par défaut, nous travaillons sur la branche master du dépôt local que nous synchronisons avec la branche distante origin/master du dépôt distant avec les commandes git pull et git push. Il est possible de créer et d'utiliser d'autres branches. C'est particulièrement utile pour développer une nouvelle fonctionnalité, expérimenter des choses ou réparer un bug, sans perturber le reste du projet.

Nous allons maintenant créer une nouvelle branche hotfix, afin de corriger un bug.

git branch hotfix
git branch
  hotfix
* master

Nous sommes encore dans la branche master comme l'indique “*”. Passons dans la branche hotfix :

git switch hotfix
git branch 
* hotfix
  master

Regardons l'état du dépôt :

git log --graph --oneline
* c908469 (HEAD -> hotfix, origin/master, origin/HEAD, master) bla bla
* d144ffa autre bla bla
(...)

Le dernier commit est bien commun à la branch hotfix, la branch master. Corrigeons le “bug” maintenant :

date >> date.txt
git add date.txt
git commit -m "deux dates dans hotfix"

Une fois le bug corrigé, nous pouvons le commiter dans la branche courante hotfix. Notons que cela n'a aucun impact sur la branche master:

git log --graph --oneline
* c38c81a (HEAD -> hotfix) deux dates dans hotfix
* c908469 (origin/master, origin/HEAD, master) bla bla
* d144ffa autre bla bla
(...)

Pour propager cette modification vers la branche master, il va falloir repasser dans la branche master, faire quelques modifs sur master puis effectuer un merge.

git switch master
date >> date2.txt
git add date2.txt
git commit -m "date2.txt"
git merge hotfix
git log --graph --oneline
*   36db317 (HEAD -> master) Merge branch 'hotfix'
|\  
| * c38c81a (hotfix) deux dates dans hotfix
|/  
* c908469 bla bla
* d144ffa autre bla bla

Pour propager ces modifications dans le dépôt distant on fera des pull/push comme d'habitude.

Si l'on souhaite que sa branche locale hotfix deviennent une branche distante (ou “remote”) :

  git log --graph --oneline
    * c38c81a (HEAD -> hotfix) deux dates dans hotfix
    * c908469 bla bla
    * d144ffa autre bla bla
    (...)
  git switch hotfix
  git push --set-upstream origin hotfix
  git log --graph --oneline
    * c38c81a (HEAD -> hotfix, origin/hotfix) deux dates dans hotfix
    * c908469 bla bla
    * d144ffa autre bla bla
    (...)

La encore on se synchronisera avec la branche distance à l'aide des commandes push/pull:

  date >> date.txt
  git add date.txt
  git commit -m "mise à jour de la date"
  git log --graph --oneline
    * 68b1978 (HEAD -> hotfix) mise à jour de la date
    * c38c81a (origin/hotfix) deux dates dans hotfix
    (...)
  git push
  git log --graph --oneline
    * 68b1978 (HEAD -> hotfix, origin/hotfix) mise à jour de la date
    * c38c81a deux dates dans hotfix
    (...) 

Pour voir toutes les branches (locales et distantes) :

  git fetch -p
  git branch -a

Nota Bene : le fetch permet de mettre à jour son cache local et éventuellement (option -p) d'effacer dans ce cache les références à des branches mortes !

Puis pour supprimer la branche locale, il faut faire ceci :

  git branch -d mybranch

Puis pour supprimer la branche distante (enfin presque…), il faut faire ceci :

  git push origin --delete mybranch

Pour récupérer une branche distante et la suivre via une branche locale :

  git checkout --track origin/mybranch

Considérons le cas suivant… Expliquer ensuite le rebase simple :

  git switch mybranch
  git rebase master        # 
  git switch master
  git merge mybranch       # merge naif
  git branch -d mybranch

Pour aller plus loin :

Les Tags

En bref :

  # création d'un tag
  git tag -a v1.4 -m "my version 1.4"
  # liste des tags
  git tag
  # afficher les infos sur un tag
  git show v1.4
  # push le tag sur le serveur 
  git push origin v1.4
  #  création d'une nouvelle branche locale à partir d'un tag   
  git checkout -b version-1.4 v1.4 # git checkout -b [branchname] [tagname]  

En détail : https://git-scm.com/book/en/v2/Git-Basics-Tagging

Restaurer un fichier supprimé ou une ancienne version

Si l'on souhaite annuler les modifications faites sur un fichier dans le working directory (qui ne sont pas encore commités)… c'est-à-dire, si l'on souhaire récupérer un fichier dans sa dernière version commitée :

git checkout -- myfile

Lorsque le fichier path/to/myfile a été supprimé plus loin dans l'historique, il faut examiner les log en détail afin d'identifier le “commit” juste avant la suppression :

git log --summary
git checkout <commit> -- <path/to/myfile>

Lorsque l'on a commité (en local) des trucs par erreur, il est possible d'annuler facilement un commit à condition de ne pas l'avoir push ! Par exemple, si l'on souhaite annuler le dernier commit, on peut faire :

git reset --hard HEAD~1

Attention, l'option –hard a pour effet de supprimer *définitivement* dans vos fichiers les modifications du dernier commit…

Si on souhaite maintenant annuler le dernier commit sans toucher aux fichiers, par exemple pour corriger un commit incomplet, on peut faire :

git reset HEAD~1  

Dans ce cas, les fichiers impliqués dans le commit annulé apparaissent comme *modified and not staged*… A vous de retravailler ces fichiers et de proposer un nouveau commit ! Une autre solution à ce problème consiste à faire un nouveau commit qui se fusionne avec l'ancien grâce à l'option –amend :

git commit -m "maj de mon dernier commit..." --amend

Autre cas de figure, j'ai commité des modifs sur deux fichiers (*good* et *bad*), et je souhaite annuler partiellement mon commit en annulant les modifs sur le fichier *bad*, sans pour autant perdre les modifications sur ce fichier dans répertoire local…

git reset HEAD~1                  # reset global (HEAD recule)
git add good
git commit -m "update good only"

Ou encore, une solution un peu plus tricky…

git reset HEAD~1 -- bad           # reset partiel (HEAD ne bouge pas)
git commit -m "update good only" --amend

Dans le cas, où l'on a *push* le commit que l'on souhaite supprimer, les chosent se compliquent car elles peuvent maintenant impacter d'autres utilisateurs du dépôt ! Pas trop de choix dans ce cas, il faut faire un *revert* qui se présentera comme un nouveau commit qui annule l'ancien… mais tout restera écrit dans l'historique :-(

git revert HEAD~1
git push

Si l'on souhaite synchroniser à nouveau son dépôt local avec *origin/master* en ignorant les supprimant les derniers commits locaux (non synchronisés), on peut biensûr faire un nouveau git clone, mais il y a plus simple et plus rapide :

git reset --hard origin/master

A utiliser avec prudence ! Néanmoins, vos anciens commits ne sont pas totalement perdus, on peut les retrouver en faisant :

git reflog

D'autres commandes

Pour éviter de devoir toujours taper son mot de passe au moment de push/pull, il est possible de demander à Git de sauvegarder les credentials :

git config credential.helper store

Si l'on souhaite supprimer du dépôt tous les fichiers non suivis, la commande clean permet de faire le ménage. Attention, tout de même à ne pas supprimer des fichiers par erreur ! Personnellement, j'aime bien les options suivantes : en interactif (-i), suppression des directory aussi (-d) et ignore de .gitignore (-x) !

git clean -i -x -d  

Si l'on souhaite faire une archive toto.zip (ou toto.tgz) de son dépôt master pour un projet Git toto :

git archive --format=zip --prefix=toto/ --output /tmp/toto.zip master
git archive --format=tar --prefix=toto/ master | gzip > /tmp/toto.tgz

Statistique : pour connaitre le nombre de commit git par utilisateur…

git shortlog -sn

Utilisation du Stash

Une petite “étagère locale” pour sauvegarder toutes les modifications efectués dans un dépôt sans les commiter, par exemple avant de change de branche… cela conserme également les fichiers “non suivis”.

# ... hack hack hack ...
$ git stash
$ edit emergency fix
$ git commit -a -m "Fix in a hurry"
$ git stash pop
# ... continue hacking ..

Utilisation du Worktree

C'est une alternative au stash pour réaliser des développements concurrents dans des branches différentes…

$ git worktree add /path/to/myproject_mybranch mybranch # or <commitID>
  Preparing worktree (checking out 'mybranch')
  HEAD is now at 88d22d5 ...
  
$ git worktree list
  /path/to/myproject 231b057 [master]
  /path/to/myproject_mybranch 88d22d5 [mybranch]

L'idée c'est que le .git/ n'est pas dupliqué !

Suppression d'une Branche Distante

Pour supprimer une branche locale sur votre ordinateur, la commande est la suivante :

$ git branch -d [nom_de_la_branche]

Ou bien :

$ git branch -D [nom_de_la_branche]

Pour supprimer la branche située sur le dépôt distant (syntaxe récente) :

$ git push origin --delete [nom_de_la_branche]

Pour mettre à jour les changements sur un autre ordinateur, il faut faire :

$ git fetch --all -prune

https://www.journaldunet.fr/web-tech/developpement/1202943-effacer-une-branche-git-a-la-fois-locale-et-distante/

Suppression de Commit Fautifs sur le dépôt distant

Revenir à l'état désiré :

$ git reset --hard <commit hash>

Forcer cet état comme le dernier état du dépôt :

$ git push --force origin master

Attention : Ces commandes sont susceptibles d'engendrer le chaos lorsque plusieurs utilisateurs collaborent sur le même dépôt. https://makandracards.com/git/7347-supprimer-des-commits-fautifs-avec-git

Savane

Pour créer son projet Git hébergé au CREMI, il faut utiliser Savane. Choisir un projet privé ;-)

Pour aller encore plus loin

Documentation

A Lire

projtec/git.txt · Last modified: 2024/03/18 15:06 by 127.0.0.1