Introduction to Git, Part 2

This entry is part 2 of 2 in the series Introduction to Git

Introduction to Git

In Part 1 of this series I introduced the basics of using Git – creating repositories, adding files, committing files, and using the Git’s log and diff tools to view a timeline of your changes. This part will move on from that to cover some slightly more advanced topics: reverting changes, creating branches, and merging changes from one branch into another.

Reverting Changes after You’ve Screwed Up

Let’s take a classic example involving some unintentionally deleted work. You’ve edited a source file to make a simple change and you’re are about to hit Ctrl + S (assuming you’re not using vi) to save your work when the palm of your hand brushes across your laptop’s touch pad which causes a large chunk of the file to be selected. Then you inadvertently hit some other key which causes the selection to wipe out your work, and before you know what has happened, Ctrl + S saves your work. This is easy to recover from if you’re using Git.

Reverting a File to a Previously Committed Version

This scenario assumes you have not yet committed the broken file. Using the config.php file from the first part, let’s say somehow you’ve wiped out most of the database configuration array leaving it looking something like this:

<?php
$database = array(
    "database"  => "new_project");

You can run git diff to see what has changed; the driver, host, username, and password values are all gone.

-    "driver"   => "mysql", 
-    "host"     => "locahost", 
-    "username" => "Foo", 
-    "password" => "pass",

How do you restore the file? With git checkout config.php. The simplest way to undo some changes is to restore a file to the state it was in at your last commit.

Sometimes you’ll want to undo some changes and restore a file back to how it was many commits ago. If you only want to restore a single file rather than all the files that are in a commit, you can use git checkout followed by the hash of the commit which contains the version of the file you want to restore, and the filename.

Let’s say you ran git log config.php and it turned out that commit c1d55debc7be8f50e363df462f84672ad029b703 contained the version of foo.php you want to restore. Now you can run:

sean@beerhaus:~/new_project$ git checkout c1d55debc7be8f50e363df462f84672ad029b703 config.php
sean@beerhaus:~/new_project$ git commit config.php -m "Reverted to previous revision."
[master b125806] Reverted to previous revision.
 1 files changed, 1 insertions(+), 1 deletions(-)

Your working copy of config.php will be overwritten with the version from the earlier commit. Be careful when running your checkout command though, because it overwrites the file and you can’t restore a working copy… you can only restore versions that have been committed and are being tracked by Git.

Reverting Everything to a Previous Commit

Another way to revert changes in Git is to revert everything to a specific commit. When you revert to a commit you revert every file in it, whether the commit contains one or one hundred files.

Let’s create a timeline of fake changes to illustrate reverting entire commits. Create a file named foo.php in your test repository.

<?php
// this is our original, pristine version

Add the new file and commit:

sean@beerhaus:~/new_project$ git add foo.php
sean@beerhaus:~/new_project$ git commit -am "This is the original version."
[master 4d7add0] This is the original version.
 1 files changed, 2 insertions(+), 0 deletions(-)
 create mode 100644 foo.php

Make a second change to foo.php that will look like this:

<?php
// this is our original, pristine version
// this is a version that might be useful

Commit the changes again.

sean@beerhaus:~/new_project$ git commit -am "Added a comment."
[master 9e87750] Added a comment.
 1 files changed, 1 insertions(+), 0 deletions(-)

Make one more change that will be something you don’t want to keep:

<?php 
while (true) {
    exec("yes");
}
sean@beerhaus:~/new_project$ git commit -am "Added a really bad version of the file."
[master d4cb9dd] Added a really bad version of the file.
 1 files changed, 3 insertions(+), 2 deletions(-)

Once you have these three changes committed, take a look at the log. Since you’re mainly after the commit hashes here, you can pass a formatting option to git log to remove some of the extra information.

sean@beerhaus:~/new_project$ git log --pretty=oneline foo.php
d4cb9ddbcb72cccf61e8682ecc2485a9a7a57a29 Added a really bad version of the file.
9e877501c2ad74d177a77b88eb66bfa8be7c18bd Added a comment.
4d7add037b4b38faba3392be988e0e9813daa8a7 This is the original version.

To undo the last commit, you can use git revert. The command expects an argument referring to the bad commit that needs to be reverted; since you want to undo only the most recent commit, you can use HEAD for the argument. In this context, HEAD is simply a reference to the last commit recorded.

sean@beerhaus:~/new_project$ git revert HEAD

The default editor will open allowing you to enter a message. In my case, a suggested message is already provided.

Revert "Added a really bad version of the file."
    
This reverts commit d4cb9ddbcb72cccf61e8682ecc2485a9a7a57a29.

# Please enter the commit message for your changes. Lines starting 
# with '#' will be ignored, and an empty message aborts the commit. 
# On branch master 
# Changes to be committed: 
#   (use "git reset HEAD ..." to unstage) 
# 
#       modified:   foo.php 
# 

Once git revert completes, take another look at your log.

sean@beerhaus:~/new_project$ git log --pretty=oneline foo.php
264208850a690ba1dc6de13c701390d395706830 Revert "Added a really bad version of the file."
d4cb9ddbcb72cccf61e8682ecc2485a9a7a57a29 Added a really bad version of the file.
9e877501c2ad74d177a77b88eb66bfa8be7c18bd Added a comment.
4d7add037b4b38faba3392be988e0e9813daa8a7 This is the original version.

A new commit has been made in which the contents of foo.php appears as it was before the bad commit.

Now let’s try this again, only this time you’ll revert your codebase to 9e877501c2ad74d177a77b88eb66bfa8be7c18bd.

sean@beerhaus:~/new_project$ git revert 9e877501c2ad74d177a77b88eb66bfa8be7c18bd

Again, the editor opens for you to specify a commit message. Once you have done so, show the log once more.

sean@beerhaus:~/new_project$ git log --pretty=oneline foo.php
522ef8c7b70b1dcb9c21ecb51f1f025323b18da2 Revert "Added a comment."
264208850a690ba1dc6de13c701390d395706830 Revert "Added a really bad version of the file."
d4cb9ddbcb72cccf61e8682ecc2485a9a7a57a29 Added a really bad version of the file.
9e877501c2ad74d177a77b88eb66bfa8be7c18bd Added a comment.
4d7add037b4b38faba3392be988e0e9813daa8a7 This is the original version.

Branching and Merging

Branching and merging are concepts common to many version control systems. The idea is that you have your main line of development, sometimes referred to as trunk or stable. This is where you want to keep all the source code in a working, releasable, stable state. You don’t want to inflict all sorts of experimental code on this main line, not because you want to discourage new ideas or experimentation, but because you want this line to be clean and reliable. There should be another line for experimentation purposes, and this is what branches are.

Using Branches

A branch is like a copy of your main, stable development line. You can do whatever you want in the confines of your own branch and it will not affect anything in the main line until you merge your branch’s code back into the main branch.

In Git, you’ve already been using a branch since you first create the repository. This branch is called “master” and you’ve been using it the whole time. Don’t believe me? Run git branch and behold its output.

sean@beerhaus:~/new_project$ git branch
* master

The asterisk points to the branch which you are currently working on.

In most cases, master serves as the stable or main development line I mentioned before, but it doesn’t have to. You can easily create your own branch named “stable” and designate that as the main branch instead. The easiest way to create a branch is with git branch <name>.

Let’s create a branch in the repository named “experimental”.

sean@beerhaus:~/new_project$ git branch experimental
sean@beerhaus:~/new_project$ git branch
  experimental
* master

Notice the asterisk is still showing master as the active branch. You’ve only created the new branch; you’ve not yet switched to it. To switch to the new branch, use git checkout.

sean@beerhaus:~/new_project$ git checkout experimental
Switched to branch 'experimental'
sean@beerhaus:~/new_project$ git branch
* experimental
  master

Alternatively, you could have used git checkout -b experiment to create the new branch and switch you to it all in one shot. I prefer to use this shortcut instead of the two command process myself.

Since you were using the master branch when you created the experimental branch, the experimental branch will be a direct copy of master at the time the branch was created.

Now open foo.php in your editor and add a comment so that it looks like this:

<?php
// this is our original, pristine version
// comment from the experimental branch

Then commit the change.

sean@beerhaus:~/new_project$ git commit -am "Added branch comment."
[experimental 8632135] Added branch comment.
 1 files changed, 1 insertions(+), 0 deletions(-)

Once you’ve committed that change, switch back to the master branch.

sean@beerhaus:~/new_project$ git checkout master
Switched to branch 'master'

Take a look at foo.php and you’ll notice the changes you made in your experimental branch are not present. This is because the two branches are two different working environments. The changes from one branch will not appear in another branch until they’ve been merged.

Merging Branches

The git merge command takes the form git merge <source>, where <source> is the branch that you are merging changes from. The branch in which you run the merge command is implied as the destination.

So let’s bring your branches from the experimental branch into the master branch.

sean@beerhaus:~/new_project$ git merge experimental
Updating 522ef8c..8632135
Fast-forward
 foo.php |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)

When you look at foo.php this time you’ll see the changes that was made in the experimental branch.

Now let’s do this in the other direction; while you are still in master, make a change to foo.php that you’ll merge into the experimental branch.

sean@beerhaus:~/new_project$ git commit -am "Added function to master."
[master cbe5cba] Added function to master.
 1 files changed, 4 insertions(+), 0 deletions(-)

Switch to your experimental branch and merge the changes from master.

sean@beerhaus:~/new_project$ git checkout experimental
Switched to branch 'experimental'
sean@beerhaus:~/new_project$ git merge master
Updating 8632135..cbe5cba
Fast-forward
 foo.php |    4 ++++
 1 files changed, 4 insertions(+), 0 deletions(-)

Take a look at foo.php and see the function you added in master is now in the experimental branch.

Handling Merge Conflicts

Eventually you’ll encounter conflicts while merging. A merge conflict happens when you are merging changes from one branch to another and Git cannot complete the merge without human intervention. This happens when you’ve edited line 80 of a file in one branch, Fred edits line 80 of the same file in another branch, and then you try to merge Fred’s changes with your changes. Git knows that the same section of code has been changed, but it’s up to us humans to figure out which change should be kept.

You can create a merge conflict situation very easily to prove the point. Switch to your master branch with Git, and open foo.php in your editor.

sean@beerhaus:~/new_project$ git checkout master
Switched to branch 'master'

Add this function at line 9:

function master() {
    print "This is the master branch.n";
}

Save the file and commit your changes.

sean@beerhaus:~/new_project$ git commit -am "Added master function."
[master 4f97280] Added master function.
 1 files changed, 4 insertions(+), 0 deletions(-)

Switch back to the experimental branch.

sean@beerhaus:~/new_project$ git checkout experimental
Switched to branch 'experimental'

Open foo.php again in your editor and add this function to line 9 as well:

function experimental() {
    print "This is the experimental branch.n";
}

Commit the changes.

sean@beerhaus:~/new_project$ git commit -am "Added experimental function."
[experimental c5358cc] Added experimental function.
 1 files changed, 4 insertions(+), 0 deletions(-)

Now while you’re still in the experimental branch, merge the changes from master. You’ve edited the same lines of the same file in both branches, so you can expect some kind of conflict.

sean@beerhaus:~/new_project$ git merge master
Auto-merging foo.php
CONFLICT (content): Merge conflict in foo.php
Automatic merge failed; fix conflicts and then commit the result.

Now you have to open the file and manually work out the conflict. In foo.php you’ll see something like this:

<<<<<<< HEAD
function experimental() {
    print "This is the experimental branch.n";
=======
function master() {
    print "This is the master branch.n";
>>>>>>> master
}

The first section starting with <<<<<<< HEAD and ending with ======= is your last commit (HEAD). The section from ======= to >>>>>>> master are the incoming changes from the master branch.

Let’s assume you’ve decided that you are going to keep the changes from master and throw away the experimental changes. All you have to do is delete the conflict symbols and the lines that came from experimental. That section of foo.php should now have just the master function.

<?php
// this is our original, pristine version
// comment from the experimental branch

function bar() {
    print "This is bar.n";
}

function master() {
    print "This is the master branch.n";
}

Once that’s done, save the file and commit the resolved conflict.

sean@beerhaus:~/new_project$ git commit -am "Fixed a merge conflict from master to experimental."
[experimental 3871a1d] Fixed a merge conflict from master to experimental.

This was just a simple example. In the real world of professional software development you can have 20 different developers all merging changes from their individual branches into the master. Each time that happens, you’ll want to merge the newest changes from master back to your individual branch. Conflicts will happen, and while Git is a great tool to have, it cannot fix all of your problems in a situation like this. Sometimes good old fashioned human communication is the best tool to avoid conflicts of this size.

Summary

And that is how it’s done. You’ve seen how to recover from any mistakes you make along the way, how to create branches so you can work in parallel with other developers on the same project or to just try out new ideas without jeopardizing the stability of your main line, and finally how to merge other people’s edits into your own branch. The three topics covered in this piece – reverting mistakes, branching, and merging – along with part one of this series has provided you with the know-how to start using Git to manage your projects. If you’ve never used version control software before, I hope I’ve convinced you to start, and if you are still using CVS of Subversion, maybe you can give Git a try.

Introduction to Git

<< Introduction to Git, Part 1

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Dave

    That’s a really nice tutorial. It’d be great to see (in part 3?) a bit about using git’s mergetool to merge conflicts.

    Also, it’s generally best not to supply a commit message inline when comititng a conflict fix; if you just do “git commit” then your editor will be pre-filled with a commit message detailing the files that had conflicts.

  • http://zonarix.com websage

    Good Tutorial again. We are all keen on part 3!

  • http://Dissectionbydavid.wordpress.com David M

    Awesome work! So well explained. I’ve been wanting to go from SVN to Git – I’ll be keeping these 2 parts on-screen to be sure!

    I am curious, though, why you have not been using ‘shortened’ hashes in your tutorial? Just curious.

    Thank you for freely contributing to the benefit of us all,
    David