git
Quick-StartThis 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:
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.
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.
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.
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.
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
.
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"
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.
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.
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.
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.
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.
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
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
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, 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
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.
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:
git
informs us that
"Everything is up to date".git
performs a fast-forward; it simply applies those new
commits to localhost foo.git mergetool
, which provides
on-screen instructions for resolving the conflicts. A new commit, one which
resolves merge conflicts and performs the merge, is created by git mergetool
,
and would have to be made eventually if git mergetool
is not used.The last case is in fact the case we talked about before.
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.
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>
In this section I give a few more useful git commands, and a couple recommendations for your team.
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.
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.