Git is a very powerful source control tool that is quickly gaining traction, especially in the Rails community. Getting started with Git, however, can be quite overwhelming thanks to its 100+ commands. The goal of this article is not to provide a comprehensive guide any means. Instead, this is simply a collection of useful tools and configurations that I have incorporated into my day-to-day workflow, using Git with Rails development using Mac OS X and TextMate.
Installation Essentials
Mac OS X Precompiled git packages: There are easy installers for both Leopard (10.5) and Tiger (10.4), just download, open and install.
Git From Source: If you would rather install Git from source, first download the latest package at git.or.cz. Open the file and cd into its folder, then:
make prefix=/usr/local all sudo make prefix=/usr/local install which git
If the last command returns '/usr/local/bin/git' then you're golden — if not, then you may need to add /usr/local/bin to your path.
Note: When you install from source, you also get the added benefit of being able to use Git to update itself.
TextMate Bundle: Tim Harper, with the help of various other contributors, has been nice enough to publish a very helpful TextMate bundle hosted on gitorious.org. If you feel like contributing (or just looking through the repository) go ahead and snoop around. To install the TextMate bundle:
# may need to create the Bundles directory if it does not exist cd ~/Library/Application\ Support/TextMate/Bundles git clone git://gitorious.org/git-tmbundle/mainline.git Git.tmbundle
In TextMate, go to Preferences > Advanced Tab > Shell Variables and set the TM_GIT variable to point to your installation of Git (ie /usr/local/bin/git). Once installed, this bundle will enable you to do all the common Git tasks such as pull, push, commit, stash, etc. all from within TextMate. It is worth mentioning that it also supports the ability to resolve conflicts with FileMerge, visualize branch history with gitk, and visualize history with gitnub.
Configuration Tips and Tricks
To set up Git's global configuration:
git config --global user.name "Name" git config --global user.email "email" git config --global color.status auto git config --global color.diff auto git config --global color.branch auto git config --global merge.tool opendiff git config --global apply.whitespace nowarn
To alias checkout to co (ie git checkout branch_name becomes git co branch_name):
git config --global alias.co checkout
You can keep all those pesky Mac OS X .DS_Store files from being committed once and for all in a global .gitignore file. The .gitignore file is where you specify all files and patterns you would like Git to ignore.
git config --global core.excludesfile ~/.gitignore echo ".DS_Store" >> ~/.gitignore
At the end of all this, my ~/.gitconfig looks like the following (you could also edit this file directly):
[user] name = user_name email = user_name@example.com [alias] co = checkout [apply] whitespace = nowarn [color] status = auto diff = auto branch = auto [merge] tool = opendiff [core] excludesfile = /Users/anemic/.gitignore
To change fonts in gitk for better readability on Mac OS X, simply edit ~/.gitk and change it to:
set mainfont {Monaco 10}
set textfont {Monaco 10}
set uifont {Monaco 10}
You can set up Git to use TextMate instead of the default editor vim, simply add this to ~/.bash_login or ~/.profile :
export GIT_EDITOR="mate -w"
If you would like to set up some more aliases for less typing on the command line, you can throw these into your ~/.bash_login or ~/.profile as well, for example:
alias gst='git status' alias gl='git pull' alias gp='git push' alias gd='git diff | mate' alias gc='git commit -v' alias gca='git commit -v -a' alias gb='git branch' alias gba='git branch -a'
Initial Repository Setup
Git is a distributed control system, meaning that repositories can exist in many different locations. In the event of a failure of a single repository, other copies exist and can be cloned to minimize data loss. If you are working with a team you may find it useful to use a centralized server as a "main" repository for all users to push to and pull from.
To demonstrate this, I will first set up a standard Rails application:
rails git_test cd git_test
I mentioned .gitignore files earler but only created a global .gitignore. We now can add local .gitignore files to this repository. These can be added to any directory, but I prefer to add one to RAILS_ROOT and another to RAILS_ROOT/log:
mate .gitignore # add in TextMate (will ignore all .DS_STORE file in project) .DS_Store # save and close TextMate mate log/.gitignore # add in TextMate (will ignore all .log files in RAILS_ROOT/log) *.log # save and close TextMate
We already set the global .gitignore to not track .DS_Store files, but these local .gitignore files will be pushed up to the main repository and will be tracked so that every clone will ignore the same files.
We now need to initialize the Git repository, add all tracked files and commit all files to the local repository.
git init git add . git commit -m 'initial import, not tracking any .DS_Store or log/*.log files'
To set up a remote repository, first create a directory on the remote server (ie mkdir preferred_repo_name.git). In this case I will create git_test.git. Copy the .git directory from your local repository to this newly created directory on the remote server:
scp -rp .git user@remoteserver.com:/path/to/repository/git_test.git
Now you need to set up your local repository to track the remote repository so that you are able to pull and push changes:
git remote add origin user@remoteserver.com:/path/to/repository/git_test.git
You are now ready to notify your team members that the repository is set up and ready for them to clone at the specified URL:
git clone user@remoteserver.com:/path/to/repository/git_test.git desired_local_repo_name # desired_local_repo_name is optional and will default to what ever is before .git if not specified hack... hack.. hack... git commit -m "message for change log" git pull git push
Note for Subversion users: clone is like checking out a branch, but instead of just checking out HEAD you get the entire repository history.
Your local repository will pull in any changes from remote that occurred since your last pull, and your local changes are pushed up to the remote server so team members can now pull your changes and push their own.
Merging and Branching
Merging and branching is very easy and inexpensive in Git thanks to how it tracks objects, not files:
git co master git merge [your_branch] git push upstream A-B-C-D-E A-B-C-D-E-F-G \ ----> \ your branch C-D-E G
Now you are ready to go and make changes in your own branch until your next merge into master.
Tagging
One very useful tool in Git is tagging. Tagging allows you to preserve a snapshot of the repository at that time and use it as a reference to easily revert to if necessary.
git tag v1.0.0 -m 'finally a stable release'
To check out or revert to revision v1.0.0 at a later date:
git branch v1.0.0 origin/v1.0.0
This will check out revision v1.0.0 from the remote repository into your local branch v1.0.0. The first advice I would give to a Git beginner is to put a tag on the head revision before making significant changes (just in case).
Troubleshooting and Flexibility
What should you do if you find that you are no longer able to push or pull from the remote repository (ie your local repository has some how gotten corrupted)? First, check to see if you can still run git log. If so, create a patch of all the changes you have made since your last push:
git log git diff [sha1-before-change-from-git-log] [sha1-after-change-from-git-log] > my.patch mv my.patch ~/ cd .. rm -rf git_test git clone user@remoteserver.com:/path/to/repository/git_test.git cd git_test mv ~/my.patch ./ git apply my.patch
This will make a patch of all the changes, create a newly cloned localrepository and then apply the patch. You should now have a fresh copy with your changes and can continue working without any loss. The only drawback is that all the changes made between sha1-before-change and sha1-after-change have now been lumped into one single commit. To combat this, you could make a patch for each commit and then apply each one at a time, committing after each patch.
This is also useful if you are not particularly comfortable with rebase and wanted to take out a prior commit or reorder some commits.
git diff [sha1-before-change] [sha1-after-change] | patch -p1 -R
This way you are able to leave the history intact and remove all changes from sha1-before-change to sha1-after-change.
If you must cause a "rift in the space-time continuum" there is git rebase. This is where the diagram above showing commits and pulls goes right out the window and you are able to order the way Git applies the changes to you local branch.
Warning: git rebase is a bit like juggling knives — if done correctly it can be spectacular but there is an inherent risk of harm if mistakes are made. If you end up losing a finger or an entire limb there is always git gc to resolve this, but that goes beyond the scope of this article.
Another note worth mentioning is that git rebase should never be run on a remote branch or any branch that is being pulled from, as it will cause conflicts for everyone who already has a working copy. It is meant to be run on a local, private branch where you would like to be able to switch up or modify history in a way to make a better, more cohesive patch. If you do find the need to use git rebase, always put a tag (perhaps called BACKUP) on the head of the branch before doing so. That way, no matter what you do, you can easily restore your history by resetting the branch to that tag:
git reset BACKUP
Git's reset command comes to the rescue if you suddenly find your changes FUBARed the code base. If you just want to revert back to HEAD and toss out those changes, you can:
git reset --hard HEAD~3 # will permanently scrap the last three commits and revert your branch git reset --hard HEAD~5 # will permanently scrap the last five commits and revert your branch
When you only want to merge a particular change from a branch (instead of merging the whole branch), Git allows you to just pick one commit from a different branch, apply the changes in revision sha1-rev and commit them to the current branch:
# in the branch you would like to take changes from git log # find the sha1-rev you would like to apply and take note git checkout branch_you_want_changes_applied_to git cherry-pick -x your-sha1-rev
Lets say you are working on a new, not-yet-stable feature. Suddenly you get an urgent request to make a bug fix but you are not ready to commit your other changes. No worries, just stash your local changes away for later reapplication:
# bat phone rings git stash git checkout master # fix bug git commit -a git push # problem solved, back to what you where doing before git checkout [original_branch_you_where_working_on] git stash apply # if you have many stashes you can use 'git stash list' to see all the available stashes to apply

