Skip to content

Instantly share code, notes, and snippets.

@Arthur
Created December 13, 2010 11:01
Show Gist options
  • Save Arthur/738892 to your computer and use it in GitHub Desktop.
Save Arthur/738892 to your computer and use it in GitHub Desktop.
Petit Tuto sur git

Sommaire

git en local

comment ça marche, le log, et la structure (commit/tree/blob), le stockage (.git/objects/)

le staging

les branches, les merge, le rebase

git avec d'autre repository

les submodules ou braid

git en local

une exemple très simple de repository

Un petit exemple :

$ mkdir test_git
$ cd test_git
$ git init
$ echo test > test.txt
$ mkdir dir
$ echo test_in_dir > dir/in_dir.txt
$ git stage .
$ git commit -m "first commit"
$ echo test2 > test.txt
$ git stage .
$ git commit -m "second commit"
$ git log
commit 6053c6f3e185e745352dc3df642ced45812a386e
Author: Arthur Petry <[email protected]>
Date:   Mon Dec 13 11:36:13 2010 +0100

    second commit
commit 5a6f05829557ddd488ae75e67462dc7c6f1d906e
Author: Arthur Petry <[email protected]>
Date:   Mon Dec 13 11:36:13 2010 +0100

    first commit

En plus pratique : gitx

la structure de git

3 types d'objects : le commit, le tree, et le blob, (il y aussi le tag, mais je n'en parle pas ici).

Un commit : un auteur (avec une date), un commiteur (avec une date), un parent (un autre commit), un tree.

$ git show -s --pretty=raw 6053c6f3e185e745352dc3df642ced45812a386e
commit 6053c6f3e185e745352dc3df642ced45812a386e
tree da721a9c7d96bf9ae20c412eb17c67fc7b0f62f1
parent 5a6f05829557ddd488ae75e67462dc7c6f1d906e
author Arthur Petry <[email protected]> 1292236573 +0100
committer Arthur Petry <[email protected]> 1292236573 +0100

    second commit

Remarque : pas de parent pour le premier commit (cf git show -s --pretty=raw 5a6f05)

Un tree : une liste de blob et de tree avec les permissions et le nom du fichier.

$ git ls-tree da721a9c7d96bf9ae20c412eb17c67fc7b0f62f1
040000 tree f504814e893b8a1a2a540911f9d6af3adaffb632  dir
100644 blob 180cf8328022becee9aaa2577a8f84ea2b9f3827  test.txt

$ git ls-tree f504814e893b8a1a2a540911f9d6af3adaffb632
100644 blob d5481f7ef0d9fd4878bd489731838b5cb6961c2e  in_dir.txt

Un blob : le contenu d'un fichier.

$ git show 180cf8328022becee9aaa2577a8f84ea2b9f3827
test2

Remarque : la première version du fichier existe toujours :

$ git show 9daeaf
test

Où est-ce que tout ça est stocké ?

$ ls .git/objects/*
.git/objects/18/0cf8328022becee9aaa2577a8f84ea2b9f3827 # blob test.txt v2
.git/objects/55/559172cbfeae5c3d2c5dd1c97e7af470f5ac4f # tree / v1
.git/objects/5a/6f05829557ddd488ae75e67462dc7c6f1d906e # first commit
.git/objects/60/53c6f3e185e745352dc3df642ced45812a386e # second commit
.git/objects/9d/aeafb9864cf43055ae93beb0afd6c7d144bfa4 # blob test.txt v1
.git/objects/d5/481f7ef0d9fd4878bd489731838b5cb6961c2e # blob in_dir.txt
.git/objects/da/721a9c7d96bf9ae20c412eb17c67fc7b0f62f1 # tree / v2
.git/objects/f5/04814e893b8a1a2a540911f9d6af3adaffb632 # tree /dir

le format de ces fichiers : une entête + plus le contenu gzippé cf. how git stores objects ou un format plus efficace cf. the packfile.

le staging

$ git status
# On branch master
nothing to commit (working directory clean)

On fait quelques modifs :

$ echo "modif" >> test.txt 
$ touch a_new_file.txt
$ git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#	modified:   test.txt
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#	a_new_file.txt
no changes added to commit (use "git add" and/or "git commit -a")

On veut tout prendre en compte :

$ git stage .
$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#	new file:   a_new_file.txt
#	modified:   test.txt
#

Ah non, finalement, je garderais bien le fichier a_new_file.txt pour un autre commit :

$ git reset HEAD a_new_file.txt
$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#	modified:   test.txt
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#	a_new_file.txt

euh je suis perdu, je préfère repartir de zéro :

$ git reset HEAD .
Unstaged changes after reset:
M	test.txt
$ git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#	modified:   test.txt
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#	a_new_file.txt
no changes added to commit (use "git add" and/or "git commit -a")

Je vais prendre en compte le changement sur test.txt

$ git stage test.txt
$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#	modified:   test.txt
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#	a_new_file.txt
$ git commit -m "un commit avec juste test.txt"
[master 7f903a7] un commit avec juste test.txt
 1 files changed, 1 insertions(+), 0 deletions(-)
$ git status
# On branch master
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#	a_new_file.txt
nothing added to commit but untracked files present (use "git add" to track)

Puis celui de a_new_file.txt :

 $ git stage a_new_file.txt
 $ git commit -m "et un autre commit pour a_new_file.txt"
 [master dba6a38] et un autre commit pour a_new_file.txt
  0 files changed, 0 insertions(+), 0 deletions(-)
  create mode 100644 a_new_file.txt
 $ git status
 # On branch master
 nothing to commit (working directory clean)

Donc le staging, c'est souple, et ça permet de préparer le commit.

Mais il n'y a pas que le staging qui permet de faire ce qu'on veut, car même après un commit, on peut revenir en arrière.

Par exemple, si finalement, je préfère commiter les 2 fichiers ensembles :

$git reset HEAD^1^1
Unstaged changes after reset:
M	test.txt
$ git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#	modified:   test.txt
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#	a_new_file.txt
no changes added to commit (use "git add" and/or "git commit -a")
$ git stage . && git commit -m "les 2 ensembles"
[master f2f3b1b] les 2 ensembles
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 a_new_file.txt

En résumé, le staging, c'est pour préparer des beaux commits.

les branches, les merge, le rebase

Par défaut, on est dans la branche master :

$ mkdir branches && cd branches && git init
$ touch master.txt && git add master.txt && git commit -m "master.txt"

(Pour pouvoir revenir facilement à cet état initial, on peut utiliser un tag : git tag first_commit puis faire ensuite un git reset --hard first_commit)

Création d'une autre branche : git checkout -b nom_de_la_branche

$ git checkout -b experimental
Switched to a new branch 'experimental'

Liste des branhces : git branch

$ git branch
* experimental
  master

On peut ajouter une modification dans un branche :

$ touch experience.txt && git add experience.txt && git commit -m "experience.txt"

On peut passer d'un branche à l'autre très rapidement avec git checkout nom_de_la_branche

$ git checkout master
$ ls
master.txt
$ git checkout experimental
$ ls
experience.txt	master.txt

Si c'est possible, ont peut passer d'une branche à l'autre même s'il y a des modifications en cours :

$ echo "I'm the master" >> master.txt # ds experimental
$ git checkout master
$ git add master.txt && git commit -m "I'm the master"

Mais ce n'est pas toujours possible :

$ git checkout experimental
Switched to branch 'experimental'
$ echo "experience_in_file" >> master.txt
$ git checkout master
error: You have local changes to 'master.txt'; cannot switch branches.

Il faut dans ce cas commiter le changement. ou bien utiliser le stash.

$ git stash
Saved working directory and index state WIP on experimental: 9b384d9 experience.txt
HEAD is now at 9b384d9 experience.txt

En gros on mets de côté (dans le stash) les modifs en cours, on peut alors à nouveau changer de branche :

$ git checkout master

Et enventuellement appliqué les modifications (si par exemple on s'était tromper de branche) :

$ git stash apply
Auto-merging master.txt
CONFLICT (content): Merge conflict in master.txt

Mais bon le stash, on a vite fait de s'y perdre (à moins de nommer les stash), il vaut mieux commencer ses modifications dans la bonne branche.

les merges de base.

On prépare 2 branches depuis master :

$ git checkout -b branch1 && touch branch1.txt && git add . && git commit -m "branch1" && git checkout master
$ git checkout -b branch2 && touch branch2.txt && git add . && git commit -m "branch2" && git checkout master

On merge branch2 dans branch1 :

$ git checkout branch1
Switched to branch 'branch1'
$ git merge branch2
Merge made by recursive.
 0 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 branch2.txt
$ git log --graph --pretty=oneline --abbrev-commit
*   bdaa3b6 Merge branch 'branch2' into branch1
|\  
| * 858feec branch2
* | 3862e5b branch1
|/  

On peut maintenant effacer la branch2, vu qu'elle est mergée :

$ git branch -d branch2
Deleted branch branch2 (was 858feec).

Par contre, on a un warning si on tente d'effacer la branch1 :

$ git checkout master
Switched to branch 'master'
$ git branch -d branch1
error: The branch 'branch1' is not fully merged.
If you are sure you want to delete it, run 'git branch -D branch1'.

On peut donc utiliser git branch -d sans trop se poser de question, sans risquer de perdre son travail.

Assez souvent, le merge, est même plus simple que cela, quand il n'y a eu des modifications que dans l'une des 2 branches. Git peut faire dans ce cas, ce qu'il appelle un "Fast-Forward". Exemple :

$ git checkout -b une_super_idee
$ echo "part1" >> une_super_idee.txt && git add . && git commit -m "part1"
$ echo "part2" >> une_super_idee.txt && git add . && git commit -m "part2"
$ git log --graph --pretty=oneline --abbrev-commit
* cfd7c65 part2
* 2d08c7a part1
* a72fd26 I'm the master
* ecef4f4 master.txt

$ git checkout master
$ git merge une_super_idee
Updating a72fd26..cfd7c65
Fast-forward
 une_super_idee.txt |    3 +++
 1 files changed, 3 insertions(+), 0 deletions(-)
 create mode 100644 une_super_idee.txt
$ git log --graph --pretty=oneline --abbrev-commit
* cfd7c65 part2
* 2d08c7a part1
* a72fd26 I'm the master
* ecef4f4 master.txt

Remarque : il est parfois plus lisible d'avoir un vrai merge, même si un fast-forward est possible :

$ git checkout -b une_mega_idee
$ echo "part1" >> une_mega_idee.txt && git add . && git commit -m "part1"
$ echo "part2" >> une_mega_idee.txt && git add . && git commit -m "part2"
$ echo "part3" >> une_mega_idee.txt && git add . && git commit -m "part3"
git log master..HEAD --graph --pretty=oneline --abbrev-commit
* 3ea4d3f part3
* 8f2ec63 part2
* 9d75939 part1

$ git checkout master
$ git merge --no-ff une_mega_idee
$ git branch -d une_mega_idee

$ git log --graph --pretty=oneline --abbrev-commit
*   ded03f5 Merge branch 'une_mega_idee'
|\  
| * 3ea4d3f part3
| * 8f2ec63 part2
| * 9d75939 part1
|/  
* cfd7c65 part2
...

Dans l'historique, les 3 commits sont regroupés, et on a encore le nom de la branche dans le merge, même si celle-ci n'existe plus.

le rebase

Parfois on peut vouloir récrire l'histoire...

2 personnes travaillent de leur côté : Une première fait des anciennes modifs :

$ git checkout master
$ git checkout -b old_modifs
$ touch old_modifs.txt && git add . && git commit -m "old_modifs" && git checkout master
[old_modifs e54b310] old_modifs

Ensuite en partant aussi de master, on fait de nouvelles modifs :

$ git checkout master
$ git checkout -b new_modifs
$ touch new_modifs.txt && git add . && git commit -m "new_modifs" && git checkout master
[new_modifs 70e3569] new_modifs

Et là au moment de mettre en prod les new_modifs, on se souvient de l'existence des old_modifs, un première approche c'est un simple merge :

$ git checkout new_modifs
$ git merge old_modifs 
Merge made by recursive.
$ git log --graph --pretty=oneline --abbrev-commit
*   abbe2d3 Merge branch 'old_modifs' into new_modifs
|\  
| * e54b310 old_modifs
* | 70e3569 new_modifs
|/  
* ecef4f4 master.txt

En voyant ça on se dit que l'historique n'est plus très compréhensible : pourquoi les anciennes modifs apparaissent après les nouvelles ??

Une première solution, c'est d'aller dans les anciennes modifications, de merger les nouvelles, puis de revenir dans new_modifs et de merger le tout :

$ git checkout new_modifs && git reset --hard 70e3569 # on annule le merge précédant.
$ git checkout old_modifs
$ git merge new_modifs
Merge made by recursive.
$ git checkout new_modifs
$ git merge old_modifs
Updating 70e3569..37f28f2
Fast-forward
$ git log --graph --pretty=oneline --abbrev-commit
*   37f28f2 Merge branch 'new_modifs' into old_modifs
|\  
| * 70e3569 new_modifs
* | e54b310 old_modifs
|/  
* ecef4f4 master.txt

Et voilà les modifications sont bien dans le bon ordre, mais bon ce n'était pas très simple...

Une solution plus simple est d'utiliser la commande rebase pour faire comme si on avait fait nos nouvelles modifs en partant des anciennes :

On commence par tout annuler :

$ git checkout new_modifs && git reset --hard 70e3569 # on annule le merge précédant.
$ git log --graph --pretty=oneline --abbrev-commit
* 70e3569 new_modifs
* ecef4f4 master.txt
$ git checkout old_modifs && git reset --hard e54b310
$ git log --graph --pretty=oneline --abbrev-commit
* e54b310 old_modifs
* ecef4f4 master.txt

Puis on fait le rebase :

$ git checkout new_modifs
$ git rebase old_modifs
First, rewinding head to replay your work on top of it...
Applying: new_modifs
$ git log --graph --pretty=oneline --abbrev-commit
* 3dcfcf4 new_modifs
* e54b310 old_modifs
* ecef4f4 master.txt

Donc là l'historique reflête ce que l'on voulait, mais par contre le commit a changé :

Première version :

$ git show -s --pretty=raw 37f28f2
commit 37f28f28fd81f77cd7c774486c515889782b65b5
tree 0a8e9648838723505467ba5ed7e3a8b06b6b9856
parent e54b3102e98678cca96750ca7ba4f95eccf2b016
parent 70e3569630fc8b0469322933711062debad6b89f
author Arthur Petry <[email protected]> 1292250051 +0100
committer Arthur Petry <[email protected]> 1292250051 +0100

    Merge branch 'new_modifs' into old_modifs

Version avec le rebase :

$ git show -s --pretty=raw 3dcfcf4
commit 3dcfcf488f1fe6c95abd946a1ccb43618469a133
tree 0a8e9648838723505467ba5ed7e3a8b06b6b9856
parent e54b3102e98678cca96750ca7ba4f95eccf2b016
author Arthur Petry <[email protected]> 1292249748 +0100
committer Arthur Petry <[email protected]> 1292250995 +0100

    new_modifs

Le code est le même (même tree), mais l'histoire qui amène à ce code est différente. (commit différent, parent différent).

Remarque : on a gardé la date du commit 70e3569 pour l'author, mais le commiter a bien la date du rebase.

conflit

Si 2 branches modifient le même fichier on peut avoir un conflit.

Cas simple, git se débrouille...

Premier exemple simple, où git s'en sort tout seul :

$ git checkout master
$ echo test > file_with_conflict.txt && git add . && git commit -m "echo test > file_with_conflict.txt"

Une modification dans une branche before

$ git checkout -b before
$ echo before > tmp.txt && cat file_with_conflict.txt >> tmp.txt && mv tmp.txt file_with_conflict.txt 
$ cat file_with_conflict.txt 
before
test
$ git add . && git commit -m "before"
[before 114e7ed] before

Une autre, sur le même fichier, dans la branche after

$ git checkout master
$ git checkout -b after
Switched to a new branch 'after'
$ echo after >> file_with_conflict.txt 
$ cat file_with_conflict.txt 
test
after
$ git add . && git commit -m "after"
[after b9a9bb6] after

Et le merge :

$ git checkout before
Switched to branch 'before'
$ git merge after
Auto-merging file_with_conflict.txt
Merge made by recursive.
 file_with_conflict.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
$ git log --graph --pretty=oneline --abbrev-commit
*   7ebd23d Merge branch 'after' into before
|\  
| * b9a9bb6 after
* | 114e7ed before
|/  
* df7b248 echo test > file_with_conflict.txt
* ecef4f4 master.txt
$ cat file_with_conflict.txt 
before
test
after

Git a réussi à faire son "Auto-merging"

parfois git ne sais pas quoi faire :

On modifie le même fichier au même endroit dans 2 branches :

$ git checkout -b after1
Switched to a new branch 'after1'
$ echo after1 >> file_with_conflict.txt && git add . && git commit -m after1 && git checkout master
[after1 9df1ace] after1
$  git checkout -b after2
Switched to a new branch 'after2'
$ echo after2 >> file_with_conflict.txt && git add . && git commit -m after2 && git checkout master
[after2 7d793b7] after2

Et là git ne sait pas comment merger les 2 modifications :

$ git checkout after1
Switched to branch 'after1'
$ git merge after2
Auto-merging file_with_conflict.txt
CONFLICT (content): Merge conflict in file_with_conflict.txt
Automatic merge failed; fix conflicts and then commit the result.
$ git status
# On branch after1
# Unmerged paths:
#   (use "git add/rm <file>..." as appropriate to mark resolution)
#
#	both modified:      file_with_conflict.txt
#
$ cat file_with_conflict.txt 
test
<<<<<<< HEAD
after1
=======
after2
>>>>>>> after2

Là il suffit d'editer le fichier en question, de faire le merge à la main, et d'ajouter le tout :

$ cat file_with_conflict.txt 
test
after1
after2
$ git add file_with_conflict.txt
$ git commit
[after1 b907f66] Merge branch 'after2' into after1

git avec d'autre repository

=> utiliser grb.

les submodules ou braid

pb des submodules : ne pas oublier le submodule update --init limiter à du git.

braid : pour les autres utilisateurs c'est transparent (ou presque)

Références (pour aller plus loin)

De Scott Chacon :

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment