git Quick-Start

This tutorial is designed to get you up-and-running with using git in the context of CSC210. It mostly exists to help you with using the git infrastructure that has been provided for your team, it is by no means a replacement for more complete git documentation, which you should consult frequently.

Here are some more comprehensive sources:

Distributed Source Code Management (SCM)

A source control management (SCM) system is a set of software tools designed to help people manage revisions of their code, and share those revisions with others. Different SCMs work under different models. In this course, we will be using git.

git is a distributed source control management system. This means that everyone's copy of the code is a complete copy. No single copy of a repository is inherently the "official" copy. It is up to people working on the code to agree upon a copy to be used as the official copy, and to ensure that their individual copies are correctly synchronized with the official copy.

Each team has been provided with a repository on a remote server. The repository is located in the /p/csc210/teamname.git directory of betaweb.csug.rochester.edu. We highly recommend that you use this repository as the official, shared, team repository.

Making a Clone

A copy of the git repository is called a clone. As stated earlier, each clone is a complete copy of the repository --- it contains all of the revisions, commit messages, branches, and metadata of the repository it is a clone of. As a result, even the original repository is called a clone.

In order to make a clone of an existing repository, one uses the command git clone, with the location of the repository we want to make a clone of as an argument. To clone your shared repository:

    $ git clone username@betaweb.csug.rochester.edu:/p/csc210/teamname.git

A directory teamname should have been created, with all of the contents of the repository inside.

Making Revisions to the code

In git terminology, a revision of the code is called a commit. With each commit is associated a commit message, which the person making the commit writes in order to describe the change that was made in plain human language. Also associated with each commit is a hash, a string that git uniquely identifies with that revision. Finally, a commit is made to a branch (more on branches later).

To make a commit, we must first make changes to the workspace, that is the directory that the repository is in and all subdirectories. This includes adding and modifying files and directories. Next, we add some changes into the staging area. Finally, we commit the changes in the staging area, creating a new revision out of them.

Staging a Commit

To add a change to the staging area, use the git add command as follows:

    $ git add <filename>

This works equally well for new files, as well as files that git already tracks that have been modified.

It is often the case that we only want to include some of the changes we have made in the newest revision. For example, if you are half way into coding your new function, and your teammate calls you up with a critical bug report, it might be the case that you fix the bug without finishing the function. In this case, you would want to make a commit that includes the bug fix, but not your half-finished function. To deal with this issue, you can use the -p flag (think "partial") as follows:

    $ git add -p <filename>

The system will do it's best to break the file into chunks that it thinks you might want to include in the commit, and asks, for each chunk, if you would like it to be added to the staging area. It also gives you the ability to edit the file directly to choose what is staged.

In the event that you stage a change that you do not want to include in the next commit, you must remove the change from the staging area before proceeding. This is accomplished with the reset command:

    $ git reset HEAD <file>

This will unstage the file without modifying it.

Verifying the Staged Changes

It is highly recommended that you verify that the staging area includes precisely what you want to commit before proceeding. git includes two commands to help. The status command:

    $ git status

Will show which branch you are on, how many commits you are ahead of master, which files have staged modifications, which files have unstaged modifications, and displays and files that are in the directory, but that git does not track.

In addition to showing status, git has its own specialized version of the unix diff command:

    $ git diff
    $ git diff <filename>

This command will show a diff between the staging area and what is in the directory of the changed files (or just the ones provided with arguments). The command can also be used to compare the state of the workspace against the last commit, rather than the staging area:

    $ git diff HEAD
    $ git diff HEAD <filename>

As an aside, in git, HEAD refers to the hash associated with the most recent commit on the current branch. One could just as effectively do:

    $ git diff 123kjnkn2j13n21o0u9090

where "123kjnkn2j13n21o0u9090" is the hash of the most recent commit. Similarly, we can compare the state of the workspace against any previous commit by using its hash with git diff.

Committing the Staged Changes

Once the staging area contains precisely what we want included in the next revision, we commit the change as follows:

    $ git commit

The system will prompt you for a "commit message", some text to include with the commit, by opening up a text editor. It is very important, especially when working in teams, to write good commit messages. The standard format for a commit message matches that of an email (git can be used to automatically mail people about commits); the first line concisely explains the commit in 50, characters or less, the second line is blank, and the lines following that explain in more detail the changes that were made. An example commit message is given below.

    Wrote function foo() in src/bar.php

    foo() resides in bar.php, and is used to interface between bar and baz. It
    works by doing faz, baz, and finally foz, as we discussed in our meeting
    on 02/01.

    The following links describe the algorithm that was used:
        http://example.com

When the message is completed, save the file and close the editor, the commit has been made! If you are making a very small change, you can include the commit message inline with the commit command. This should only be done for trivial changes, e.g.:

    $ git commit -m "Fixed whitespace in bar.php"

Working with Branches

Motivation

In the SCRUM model we are using in class, we only want to commit code that has been thoroughly tested and complete in some logically significant way (e.g. it implements a particular feature) to the master branch. However, we would like the ability to commit broken code. For example, suppose you're stuck on a really tricky bug. It would be nice to be able to commit the bug so that when you go visit your friendly TA, he can simply clone your repository onto his computer and look at the code.

A branch is a logical construct that makes thinking about and sharing revisions easier.

Creating, Displaying, and Switching between Branches

When a fresh git repository is made, it includes a single branch: master. We can list all of the branches in the repository with the command

    $ git branch

It will display all of the branches in the repository, with a star next the branch you are currently in. Creating a new branch is similar:

    $ git branch <name of new branch>

The new branch will be identical to the branch that the user was in when he ran the branch command. Note that creating a branch does not switch to it. To switch to a different branch, do

    $ git checkout <name of existing branch>

Aside: subversion users should note that what svn calls checkout is what git calls clone, and git uses checkout to mean something completely different.

Merging Branches

Suppose we are in "master". If we create a new branch "foo", switch to the new branch, and commit a change to it, "foo" and "master" will be out-of-sync. Branch "foo" will have the newly-committed revision, "master" will not. This is desired behavior; the whole idea was to be able to make a commit without disturbing our reliably-working master branch. However, the time will come when we want those shiny new features we committed to the "foo" branch to appear on the "master" branch. This is accomplished through git's merge operation.

In order to merge changes from branch b into branch a, we first switch to branch a, and then tell git to merge the changes from branch b:

    $ git checkout a
    $ git merge b

This will apply all of the commits we made while we were in branch b to branch a.

Merge Conflicts

Suppose a commit was made on branch a that was not made on branch b? What happens? What if the changes conflict? Before we answer these questions, you may be asking "Why in the world would I make a conflicting on the branch I want to merge into?" You wouldn't. However, you are working in teams. Suppose you make branch b from a, and your teammate makes branch c from a. While you are hacking away at branch b, your teammate makes a bunch of commits to c, tests them, and merges them into a. By the time you're ready to merge b into a, it has all kinds of c commits in it that conflict with your code.

To adequately understand how to deal with this issue, we need to first understand how to share code with your teammates. After all, how did those changes your teammate made on branch c make it all the way to your computer to cause the conflict? After doing so, we will revisit this issue.

Sharing Code with your Teammates

So you've cloned your shared repository, started a new private branch, made a bunch of commits, and merged them back into a shared branch. How do you share those changes with your teammates?

In git, there are two ways (okay, there are more than two, but I am only going to mention two) to share commits between clones of repositories. One is push, and the other is pull. They work fairly intuitively, git push pushes sends to a remote location, git pull requests commits from a remote location.

Before we describe them in depth, we need to learn about how git knows where to find the repositories and branches it is pulling from and pushing to.

Remote Repositories

In your cloned repository, take a look at teamname/.git/config. It should look similar to:

[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
[remote "origin"]
    url = username@betaweb.csug.rochester.edu:/p/csc210/team.git
    fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
    remote = origin
    merge = refs/heads/master

The remote section refers to the remote servers that the git command knows how to associate with this instance. "origin" is the default name of the location that was cloned from (though one can change the name by editing this config file).

In this course, you will probably only need your repository to talk with one other repository: your teams shared one on betaweb. However, suppose you were working on a very large project, like the linux kernel. Rather than just having one master branch and people pushing changes to it, or, as we'll discuss later, a single staging branch that people push to, that a project leads merges into master, there could be a whole hierarchy of people, each in charge of a small part of a project (like for instance, a single device driver). The typical way this would work is that you would email that developer and ask him to pull from your repository. He would make a new branch, pull your changes, test them, merge them in, and then repeat with the next person up, until that change makes it all the way up the hierarchy. In order to do this, that developer would need to add your repository so he could pull from it. To do so, he would do something like:

    $ git remote add test user@hostname.com:/var/git/my-repo.git

Remote Branches

It may have occurred to you that when you create a new branch in your local clone of the repository, other clones of the repository do not know about the new branch. This is true, and this is desirable! Now, you don't have to check with all of the other developers (which on a large project, could be hundreds or thousands of people) to make sure that there are no collisions with branch names.

However, we would like the option of telling remote branches about the branches we have created. Before we start, let us list all of the branches on the remote repository:

    $ git branch -r       # lists all branches on remote repos
    $ git branch -a       # lists all branches on local and remote repos

Push --- Sending Changes to the Remote Repository

The push command is used to send changes to a remote branch. It is used as follows:

    $ git push <remote name> <branch name>

In this command, is the name of the branch that we want to push to on the remote repository. So if we currently have our local branch foo checked out, and we do git push origin bar, we will push the changes from our foo branch to the remote repository's bar branch. With the exception of the master branch, the branch names do not need to match.

If there is no branch on the remote repository, then one is created and pushed to. This is the simplest way to create remote branches.

Occasionally a push operation will fail. This means that there are commits to the remote branch in question that conflict with the commits we are trying to push. We will deal with this problem in later sections.

Pull --- Getting Changes from the Remote Repository

The pull command takes two arguments, the name of the remote repository (usually "origin"), and the name of the branch on the remote repository to pull changes from.

Suppose we are in branch foo. If we run the command:

    $ git pull origin master

the system will fetch the commits from the master branch of the origin repository, and attempt to merge them into the current branch of our repository, which in this case is branch foo.

There are 3 cases:

The last case is in fact the case we talked about before.

Checking out a remote branch

Now that we understand how branch and pull work, checking out a remote branch is quite easy. Suppose we want to check out the remote branch origin foo. We first checkout master, then create and checkout a new (local) branch foo, and then pull the remote foo into it.

    $ git checkout master
    $ git branch foo
    $ git checkout foo
    $ git pull origin foo.

Note that we can name our local branch that is the same as origin foo whatever we like.

Reverting to an Old Revision

In addition to tracking progress and sharing work, one of the primary reasons to use an SCM is so that we can revert to an old version of the code. As we said before, each commit is given a unique hash. We will use this hash in reverting to an old revision.

The log command provided by git can be used to show, for all of the commits on the current branch, what the hash of the commit is, when the commit was made, who made the commit, and what the commit message was. It is used as follows:

    $ git log

This is probably the simplest way to determine the hash associated with the commit we would like to revert to. With the proper hash in hand, there are a number of ways to look at the old commit. They are outlined in more detail in the top answer of this StackOverflow post, but we will quickly go over them here.

If you just want to look at an old commit without permanently changing back to it, you simply check it out as if it were a branch:

    $ git checkout <hash>

If you actually want to erase all of the things that have happened since this commit forever (no really, it will be gone), you reset to that commit as follows:

    $ git reset --hard <hash>

Note, you should never do this to remove changes that were pushed to a remote repository. Really, don't do it. In fact, why don't you just avoid this technique altogether?

Finally, if you want to permanently revert to the old change, but not wipe out all of the new commits from the repository, then we use the revert command. This command creates a new commit (rather than deleting old ones) that undoes all of the changes made by the revisions since the commit we want to revert to. This is the preferred way of permanently reverting to an old revision:

$ git revert <hash>

Tips, Tricks, and Recommendations

In this section I give a few more useful git commands, and a couple recommendations for your team.

Looking at past commit messages

We've already talked about the log command. This is useful for tracking who is contributing what, and reminding oneself what has been accomplished in recent history.

Another useful tool is the blame command. Suppose we are looking through a source file, and see a line that doesn't make sense. We may want to know whom to ask about it. We could use git log to look at who made past commits, but if multiple people made commits, we would have to search through them by checking all of them out. Instead, we do:

    $ git blame <filename>

which will show us the current revision of the file, with the name of the person who last modified each line next to it.

Using a Staging Branch

Because we only ever want to commit working code into branch master, and because merges can be complicated, I suggest your team do the following:

Create a branch called "staging", and push it to the remote repository. The only way one should ever make a commit to master is by merging staging into it. Furthermore, the only way one should ever make a commit to staging is by merging another branch into it (or by making small commits after a merge to fix errors that may have been made in merging).

Using this model, the team can always test the code after it has been merged before it gets committed to master. If this model is followed strictly, then whenever staging is merged into master, it will be a fast-forward update, and we will never have a merge conflict when merging into master. Thus, we guarantee that, if the code worked in staging, then when we merge it into master, it will still work. The staging branch acts as a good place to do run the battery of tests again before making the final merge.