I am working now with git for about 1 year. The question when to use a git rebase and when to use a git merge always lead to confusion for me. To clarify this I want to show you what a git rebase really is, what advantages we can leverage from it and how to use it safely.
Lets start with the most basic question, what is a git rebase? When we consolidate the man page of it, we get the description:
git-rebase – Forward-port local commits to the updated upstream head
So what does that mean? Lets look at it in a real example. Consider we have a repository with a master branch. In that master branch we made 2 commits.
< src/git-rebase - master > git log --oneline --graph * 976d4b6 added file * 706d823 added file
We are creating now a new feature branch from that master and make a new commit on it.
< src/git-rebase - master > git checkout -b feature-xy Switched to a new branch 'feature-xy' < src/git-rebase - feature-xy > touch file3.txt < src/git-rebase + feature-xy > touch file4.txt < src/git-rebase + feature-xy > git add . < src/git-rebase + feature-xy > git commit -am "developed feature-xy" [feature-xy 6c15242] developed feature-xy 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 file3.txt create mode 100644 file4.txt < src/git-rebase - feature-xy > git log --oneline --graph * 6c15242 developed feature-xy * 976d4b6 added file * 706d823 added file
While we are developing in our feature branch, someone else made updates on the master branch.
< src/git-rebase - feature-xy > git checkout master Switched to branch 'master' < src/git-rebase - master > touch file5.txt < src/git-rebase + master > git add file5.txt < src/git-rebase + master > git commit -am "added file" [master c5df90e] added file 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 file5.txt < src/git-rebase - master > git log --oneline --graph * c5df90e added file * 976d4b6 added file * 706d823 added file
When we now want to merge the latest commits of the master into our feature branch we can make a git rebase.
< src/git-rebase - feature-xy > git rebase master First, rewinding head to replay your work on top of it... Applying: developed feature-xy < src/git-rebase - feature-xy > git log --oneline --graph * 4345feb developed feature-xy * c5df90e added file * 976d4b6 added file * 706d823 added file
So what happened by now? The git command line tool tells us: First, rewinding head to replay your work on top of it… So git first throws away all our local changes of the branch feature-xy. It now applies all commits from the master so that the master branch and our feature-xy branch look the same. Than it takes our local commits on the feature-xy branch and applies them on the top.
Maybe we can understand the definition of the man page now better. In our case it updates first our local branch to the master branch .. to the updated upstream head and than it applies our local commits on top of it Forward-port local commits ..
Caution: When you look close at the commit „developed feature-xy“ you will recognize that the commit hash has changed. This is because it now has a different parent commit and the parent commit influences the commit hashes.
Important: This brings us to the conclusion that we dont want to use git rebase when we already have pushed our changes to the remote. Otherwise git would not recognize that the changes in the commits are the same, as it tracks the differences just by the commit hashes, and would try to merge it what would lead to merge conflicts.
Remember: To update your local branch use git rebase, for everything else use git merge.
Okay, but where is now the benefit to use git rebase instead of git merge for local branches? The benefit is to avoid merge-commits and therefor keep a clean history. Lets have a look at an example where we can see the problems of a git merge. Here is the state of our local copy on the master branch.
< src/git-rebase - master > git log --oneline --graph * c5df90e added file * 976d4b6 added file * 706d823 added file
We are making now some new changes on the master branch.
< src/git-rebase - master > touch file3.txt < src/git-rebase + master > git add . < src/git-rebase + master > git commit -am "added file" [master 73c7334] added file 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 file3.txt < src/git-rebase - master > git log --oneline --graph * 73c7334 added file * c5df90e added file * 976d4b6 added file * 706d823 added file
While we made our local changes someone else has pushed a new commit to the remote master. Thus, when we want to push our changes it is rejected as we have to merge the changes on the remote repository first.
< src/git-rebase - master > git push To ../git-rebase-repo/ ! [rejected] master -> master (fetch first) error: failed to push some refs to '../git-rebase-repo/' hint: Updates were rejected because the remote contains work that you do hint: not have locally. This is usually caused by another repository pushing hint: to the same ref. You may want to first integrate the remote changes hint: (e.g., 'git pull ...') before pushing again. hint: See the 'Note about fast-forwards' in 'git push --help' for details.
To see better what happens, I will now do manually what a git pull normally combines. First I will fetch the latest version of the repository from the remote and than do a git merge with the latest master version of the remote.
< src/git-rebase - master > git fetch remote: Counting objects: 4, done. remote: Compressing objects: 100% (2/2), done. remote: Total 2 (delta 1), reused 0 (delta 0) Unpacking objects: 100% (2/2), done. From ../git-rebase-repo c5df90e..3869277 master -> origin/master < src/git-rebase - master > git merge origin/master Merge made by the 'recursive' strategy. file4.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 file4.txt < src/git-rebase - master > git push Counting objects: 7, done. Delta compression using up to 4 threads. Compressing objects: 100% (4/4), done. Writing objects: 100% (4/4), 461 bytes | 0 bytes/s, done. Total 4 (delta 2), reused 0 (delta 0) To ../git-rebase-repo/ 3869277..11e4ffd master -> master
So now we have merged our local master version with the latest version from the remote repository. When we now look at the history, we see a merge commit and even a split in the graph of our history, though we have worked on the master branch all the time.
< src/git-rebase - master > git log --oneline --graph * 11e4ffd Merge remote-tracking branch 'origin/master' |\ | * 3869277 added file * | 73c7334 added file |/ * c5df90e added file * 976d4b6 added file * 706d823 added file
If we would have updated our local master version with git rebase, the history would be linear and we would have saved us that merged commit. I hope the advantages of git rebase are now clear. We can maintain a clean history without cluttering it through unnecessary merge conflicts and branches in the graph.
Note on git pull: A git pull always first fetches the latest version of the remote repository and then makes a git merge on it. If we want git pull to make a rebase instead of that merge with can append the –rebase flag.
git pull --rebase
I would recommend to set up git to do that by default.
git config branch.autosetuprebase always --global
If you skip the –global option this option is just setup for your current local branch.
Remember
To update your local branch, use git rebase, for everything else, use git merge.
Links / Sources
http://git-scm.com/book/en/Git-Branching
http://git-scm.com/book/en/Git-Branching-Rebasing
Book: Daily Git – Author: Martin Dilger
Follow-Up
In this article I have not mentioned how to use git rebase –interactive to cleanup your local history and edit / squash / ammend commits. This will be the content for another article so stay tuned :)
Schreibe einen Kommentar