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 <arthur.petry@bolloretelecom.eu>
Date: Mon Dec 13 11:36:13 2010 +0100
second commit
commit 5a6f05829557ddd488ae75e67462dc7c6f1d906e
Author: Arthur Petry <arthur.petry@bolloretelecom.eu>
Date: Mon Dec 13 11:36:13 2010 +0100
first commit
En plus pratique : gitx
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 <arthur.petry@bolloretelecom.eu> 1292236573 +0100
committer Arthur Petry <arthur.petry@bolloretelecom.eu> 1292236573 +0100
second commit
Remarque : pas de parent pour le premier commit (cf git show -s --pretty=raw 5a6f05
)
$ git ls-tree da721a9c7d96bf9ae20c412eb17c67fc7b0f62f1
040000 tree f504814e893b8a1a2a540911f9d6af3adaffb632 dir
100644 blob 180cf8328022becee9aaa2577a8f84ea2b9f3827 test.txt
$ git ls-tree f504814e893b8a1a2a540911f9d6af3adaffb632
100644 blob d5481f7ef0d9fd4878bd489731838b5cb6961c2e in_dir.txt
$ git show 180cf8328022becee9aaa2577a8f84ea2b9f3827
test2
Remarque : la première version du fichier existe toujours :
$ git show 9daeaf
test
$ 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.
$ 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.
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.
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.
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 <arthur.petry@bolloretelecom.eu> 1292250051 +0100
committer Arthur Petry <arthur.petry@bolloretelecom.eu> 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 <arthur.petry@bolloretelecom.eu> 1292249748 +0100
committer Arthur Petry <arthur.petry@bolloretelecom.eu> 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.
Si 2 branches modifient le même fichier on peut avoir un conflit.
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"
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
=> utiliser grb.
pb des submodules :
ne pas oublier le submodule update --init
limiter à du git.
braid : pour les autres utilisateurs c'est transparent (ou presque)
De Scott Chacon :