Tuesday, March 12, 2013

Git Course Part III: Branches and Merging

We will go back to work on the tutorial project for this section.
Branches:
We already learned what is a commit. A branch is just a label that we can put to a commit.
It is used normally to reference it, just like the HEAD or the SHA-1 of the commit.
When you work on git, you're normally working on a branch. The first time you create your repository, this branch is called master by default.
However, the name master is not used by git in any way, and you can drop this name if you want to.

Listing and changing branches
To list which branches you have, you do git branch. The following output will appear:
weasley:git_tutorial rafael$ git branch
* master

Since we only have master, this is the only branch we can see for now. The star * tells you which branch you're using.
To create another branch, you do git checkout -b . For example:
weasley:git_tutorial rafael$ git checkout -b my_totally_cool_new_branch
Switched to a new branch 'my_totally_cool_new_branch'
weasley:git_tutorial rafael$ git branch
  master
* my_totally_cool_new_branch

You not only created a new branch, but you also now are on it. To go back to master, you type git checkout master.
Now you know how to create a new branch, but maybe you don't know what does it mean. The truth is, the checkout -b command can receive two parameters, the name of the new branch that you want to create, and the name of which branch you want this branch to be based.  If you omit the second parameter, this new branch will be based on your current branch. Since we were on master, my_totally_cool_new_branch is based on master, and the checkout we did was the equivalent of doing git checkout -b my_totally_cool_new_branch master.
When you create a branch based on another, the new branch will have exactly the same commit history and HEAD commit of the branch it's being based on. The picture bellows show that both master and my_totally_cool_new_branch point exactly to the same commit. In fact, they're just names that reference the same commit.
Creating a new commit.
Now, supposed you create another commit on this new branch, this is what happens:
weasley:git_tutorial rafael$ echo "another cool new line" >> a.txt
weasley:git_tutorial rafael$ git commit -a
[my_totally_cool_new_branch ad36d5a] Another cool new line.
 1 file changed, 1 insertion(+)

Now, master and my_totally_cool_new_branch don't point to the same commit anymore. Since we made the commit on my_totally_new_branch, the branch was updated to point to this new commit. master however was not changed and continues to point to the same commit it was previously pointing to.
Merging
Now imagine I'm done with my_totally_cool_new_branch and I want to get the alterations I made on it back to master.  git gives me two ways to do this. The simpler one is merging.
Merging takes the changes made to another branch and applies them on your current branch. So, in order to get the commit back to master, here is what we do:
weasley:git_tutorial rafael$ git checkout master
Switched to branch 'master'
weasley:git_tutorial rafael$ git merge my_totally_cool_new_branch
Updating ca93f24..ad36d5a
Fast-forward
 a.txt | 1 +
 1 file changed, 1 insertion(+)
weasley:git_tutorial rafael$ git log -n 2
commit ad36d5a48059bccf1ce53b86b9069b0693ef3116
Author: Rafael Adson <rafael@geekie.com.br>
Date:   Tue Mar 12 21:59:42 2013 -0300

    Another cool new line.

commit ca93f245d1f6ba6b9d9e5db0a404f9121b929173
Author: Rafael Adson <rafael@geekie.com.br>
Date:   Tue Mar 5 01:15:37 2013 -0300

    Another commit.

Now, master has the new commit, and was updated to point to the new HEAD commit, as we can see now.
After merging
Since my_totally_cool_new_branch was ahead of master, now both commits are equal.
Now let's simulate two people working on the same project. In order to do this, I created two new branches, where each one of them will work:
weasley:git_tutorial rafael$ git checkout -b b1
Switched to a new branch 'b1'
weasley:git_tutorial rafael$ git checkout master
Switched to branch 'master'
weasley:git_tutorial rafael$ git branch b2
weasley:git_tutorial rafael$ git branch
  b1
  b2
* master
  my_totally_cool_new_branch
Both branches were created based on master, and as of now master, b1 and b2 are exactly equal.
weasley:git_tutorial rafael$ git checkout b1
Switched to branch 'b1'
weasley:git_tutorial rafael$ echo "I'm developer 1" > dev1.txt
weasley:git_tutorial rafael$ git add dev1.txt
weasley:git_tutorial rafael$ git commit --author "Dev 1 <dev1@fiive.net>"
[b1 99d495e] Changes made by dev1.
 Author: Dev 1 <dev1@fiive.net>
 1 file changed, 1 insertion(+)
 create mode 100644 dev1.txt

And let us do some work as developer 2:
easley:git_tutorial rafael$ echo "I'm developer 2" > dev2.txt
weasley:git_tutorial rafael$ git add dev2.txt
weasley:git_tutorial rafael$ git commit
Aborting commit due to empty commit message.
weasley:git_tutorial rafael$ git commit --author "Dev 2 <dev2@fiive.net>"
[b2 d3fca00] Changes made by dev2.
 Author: Dev 2 <dev2@fiive.net>
 1 file changed, 1 insertion(+)
 create mode 100644 dev2.txt

Now if we look into the three commits, here is what we see:
Two developers working on different branches.
The branches have diverged. Now dev2 has things that dev1 has not, and dev1 has things that dev2 has not.
Now, let's say dev1 needs the changes dev2 has made to the code. How does he do? He can just merge from b2, the same way we merged master from my_totally_cool_new_branch:
weasley:git_tutorial rafael$ git checkout b1
Switched to branch 'b1'
weasley:git_tutorial rafael$ git merge b2
Merge made by the 'recursive' strategy.
 dev2.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 dev2.txt
If you're following this tutorial, you should have seen that an editor was opened asking for the message for a new commit. What happened?
Well, here's what you had before:Each branch with its own commits.

You had a commit referenced by b1, and it's parent was master. You also had a commit referenced by b2, and it's parent was also master.
When you tried to merge b2, into b1, this is what git tried to do:
git trying to do merge.
Merge won't change the commits, so now you have both commits that were pointed by the two branches, and they both have parent as their master.
However, this structure is not good, since you don't have a HEAD commit and you need one. That's why git creates another commit, that has two parents (b1 and b2 in the previous pictures) and then updates b1 to point to this new commit.
After merge
Notice that, since the commits done by b1 and b2 did not change, if b2 wants b1 changes it can get it by doing git merge b1 in its branch. The effect of this is that both branches will now point to the same commit (since no commit was changed).

Conflicts on Merging
Now imagine dev1 see the brand new dev2 file and wants to make some changes to it. So here's what he does:
weasley:git_tutorial rafael$ echo "some new changes" >> dev2.txt
weasley:git_tutorial rafael$ git add .
weasley:git_tutorial rafael$ git commit --author "Dev 1"
[b1 62de83b] Changes made on dev2 made by dev1.
 Author: Dev 1 <dev1@fiive.net>
 1 file changed, 1 insertion(+)
In the meantime, dev2 also has made his new changes on this file:
weasley:git_tutorial rafael$ git checkout b2
Switched to branch 'b2'
weasley:git_tutorial rafael$ echo "dev 2 changes" > dev2.txt
weasley:git_tutorial rafael$ git reset --hard
HEAD is now at d3fca00 Changes made by dev2.
weasley:git_tutorial rafael$ echo "dev 2 changes" >> dev2.txt
weasley:git_tutorial rafael$ git commit -a
[b2 aee8aa0] Futher changes.
 1 file changed, 1 insertion(+)

Now suppose dev1 wants dev2 changes to the file.  Since both changed the same file, it's possible that the changes will conflict. This will happen when git can't find out what it should do to the file. In this case:
weasley:git_tutorial rafael$ git merge b2
Auto-merging dev2.txt
CONFLICT (content): Merge conflict in dev2.txt
Automatic merge failed; fix conflicts and then commit the result.

There are two ways to fix this conflict. The first way is to edit both files manually and then generate a new commit. I won't teach you this way because I find it too cumbersome.
The second way is to use git merge tool. You can configure a tool for git to use to help you  decide which changes should be applied. There are a wide range of such tools available. I normally use either FileMerge when I am on Mac OS X or meld when I am using Linux, you're free to choose whichever one you like best. Although configuration of such tool falls out of the scope of this tutorial, I will teach you how to use it.
When a conflict happens, you can type git mergetool.
weasley:git_tutorial rafael$ git mergetool
Merging:
dev2.txt

Normal merge conflict for 'dev2.txt':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (opendiff):

git will then ask for you to mark a resolution in each of the files that conflicted. It will either suggest an action or will ask you to open your diff tool. By pressing enter, the merge tool should open:
opendiff showing conflicts.
Such tools will indicate to you where the problems happens, and will ask for you to take an action. In this case, I will select in the Action select box that I want both changes:
Resolving conflicts using opendiff
And then I'll just save my alterations and quit the merge tool. If there's another conflict, git will jump to that one. If there isn't, you just need now to create the merge commit.
weasley:git_tutorial rafael$ git commit
[b1 697b352] Merge branch 'b2' into b1
And now dev1 can work with his branch again in sync with b2.

Deleting a branch
To delete a branch, you do:
weasley:git_tutorial rafael$ git checkout master
Switched to branch 'master'
weasley:git_tutorial rafael$ git branch -d my_totally_cool_new_branch
Deleted branch my_totally_cool_new_branch (was ad36d5a).
weasley:git_tutorial rafael$ git branch
  b1
  b2
* master

Now my_totally_cool_new_branch does not exist anymore (ahhhhhh!).
This is not always work. git will try to prevent you from losing work if the result of deleting a branch would make that happen. So, for example, if I try to delete b2.
weasley:git_tutorial rafael$ git branch -d b2
error: The branch 'b2' is not fully merged.
If you are sure you want to delete it, run 'git branch -D b2'.

git didn't allow me to delete it, but it did tell me what I need to do if I really want to do that.

Coming up next:
I'll teach you another way of syncing branches called rebase.

No comments:

Post a Comment