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
Links
I was working on a large community-driven website recently, which had always had the requirement to synchronize its user database with another (3rd party) site that we didn't control. The most we could get out of the other site was for them to send us regular XML dumps of the changes (additions, removals, deletions). Predictably, we wrote that as a rake task and added it to cron.
@hourly cd /apps/product/current && export RAILS_ENV=production && rake product:synchronize_database
It worked great, until the client threw in ONE additional little snag; not only should it check for updates every hour, but an admin should be able to log in to the site and request an immediate synchronization. Unfortunately, this task can take anywhere from 10 minutes to 10 hours, depending on the size of the XML file and the number of employees involved. Clearly not a job for a simple backtick or %x{}.
There are a number of different options for running background processes in Rails, but since I had pretty simple requirements (and needed quick results) the best choice for me was clearly Spawn.
You can install the plugin from rubyforge:
script/plugin install http://spawn.rubyforge.org/svn/spawn/
Then implementing the client's request was as simple as adding a button with a remote_function and a controller action with:
spawn(:nice => 7) do exec("cd #{RAILS_ROOT} && export RAILS_ENV=#{RAILS_ENV} && rake product:synchronize_database") end
The :nice option (vital, in my case!) will make sure that your process doesn't monopolize the CPU (just like its shell counterpart). There are only a couple of other options; you can choose to fork or thread your process (fork is the default), and you can wait for it to finish. Neither was necessary in my case. Problem solved and, in typical Rails fashion, it only took a few lines of code!
Even the most experienced developer will sometimes encounter abnormal behavior from what they believe to be perfectly valid code. script/console is fantastic and extremely helpful for some of those situations, but there are times when it's not enough. You may need more context in order to discover the source of your problem, and that's where ruby-debug comes in.
Getting Ready to Debug
In order to debug your Rails application the first thing you need to do is to install the ruby-debug gem.
sudo gem install ruby-debug -y
Next, you need to set a breakpoint in your application by dropping this cleverly named nugget debugger into your code where you would like to begin the analysis of your app. For simplicity's sake I will just put the debugger in the show action of my PropertiesController.
# GET /properties/1
# GET /properties/1.xml
def show
debugger
@property = @manager.properties.find(params[:id], :include => [:municipality, {:assemblies => :assembly_checks}])
...
end
And now the only thing left is to just start the server with debugging enabled.
script/server --debugger
Easy Peazy.
In order for that breakpoint to actually break though, the debugger statement needs to be evaluated. We can accomplish this by navigating our browser to the show action where the debugger is set. You will notice that once we do this our browser becomes unresponsive. This is because the debugger halts the execution of our application at the breakpoint until we tell it to proceed. So while we are waiting on our browser, why don't we take a look at the terminal.
/Users/joshua/Sites/backflow_testing_app/app/controllers/properties_controller.rb:19
@property = @manager.properties.find(params[:id], :include => [:municipality, {:assemblies => :assembly_checks}])
(rdb:2)
That's kind of cool, we have a nice prompt there just waiting for us to use.
Getting a Lay of the Land
By typing l (or list) at the prompt we can take a look at the first ten lines of code surrounding our debugger statement. Each successive l command will display the next ten lines of code in the current file.
(rdb:2) l
[14, 23] in /Users/joshua/Sites/backflow_testing_app/app/controllers/properties_controller.rb
14
15 # GET /properties/1
16 # GET /properties/1.xml
17 def show
18 debugger
=> 19 @property = @manager.properties.find(params[:id], :include => [:municipality, {:assemblies => :assembly_checks}])
20 @testers = User.find(:all, :conditions => ["is_tester = ?", true])
21 @next_test_date = @property.assemblies.first.next_test_date if @property.assemblies.first
22
23 @next_test_date =
(rdb:2) l
[24, 33] in /Users/joshua/Sites/backflow_testing_app/app/controllers/properties_controller.rb
24 respond_to do |format|
25 format.html # show.html.erb
26 format.xml { render :xml => @property }
27 end
28 end
29
30 # GET /properties/new
31 # GET /properties/new.xml
32 def new
33 @property = Property.new
(rdb:2)
l - will move us back up 10 lines in the file and l = will always return us to the current line or the line to be evaluated next. The current line is designated by the =>. If we want to have a larger context to look at we can specify a range of line numbers and pass that to l, like so l 1-40. If you happen to be using Textmate we can even type tmate and view the entire file in all its colorful glory.
So now that we know where we are in terms of where we are in the file, let's get a sense of where we are from a different perspective. Let's see where we are in terms of the stack trace. We can view the stack trace by typing either backtrace or where. This prints the entire stack trace to the screen with each stack frame numbered from the current frame (0) upwards. We can navigate through the stack by either explicitly selecting the frame to jump to (e.g. frame 20) or by moving a relative number of frames up or down from our current frame. For example, if we are inspecting frame 5, up 5 or down 2 will move us to frames 10 and 3 respectively. The =>, as before, will show us our current location.
Step Into, Step Over and Continue
Let's make some progress and move forward through some of our code. Let's first make certain we are on the current frame by typing frame 0. We can step through our code by using one of two methods - either n (next) or s (step). n is the traditional step over and s is the traditional step into - if we want to dive into the guts of our methods. If we want to move more quickly through our code, perhaps to the next breakpoint, we can use the c (continue) command.
So let's give it a try. Let's just move to the next line of code using n and inspect the @propertyobject using the p command to print to screen.
[14, 23] in /Users/joshua/Sites/backflow_testing_app/app/controllers/properties_controller.rb
14
15 # GET /properties/1
16 # GET /properties/1.xml
17 def show
18 debugger
=> 19 @property = @manager.properties.find(params[:id], :include => [:municipality, {:assemblies => :assembly_checks}])
20 @testers = User.find(:all, :conditions => ["is_tester = ?", true])
21 @next_test_date = @property.assemblies.first.next_test_date if @property.assemblies.first
22
23 @next_test_date =
(rdb:2) n
/Users/joshua/Sites/backflow_testing_app/app/controllers/properties_controller.rb:20
@testers = User.find(:all, :conditions => ["is_tester = ?", true])
(rdb:2) p @property
#<Property id: 690789713, manager_id: 476927875, integer: nil, name: "Borders Books", string: nil, store: "0020", contact: "jon", street_address: "1500 West 16thStreet", city: "Oak Brook", state: "IL", zip_code: "60523", phone: "(630)574-0800", fax: "", email: "someone@borders.com", municipality_id: 1847418, previous_manager: nil, notes: "", text: nil, property_deleted: false, boolean: nil, lat: #<BigDecimal:189a430,'0.41853303E2',12(16)>, decimal: nil, lng: #<BigDecimal:189a0ac,'-0.87997248E2',12(16)>, created_at: "2008-06-14 17:50:39", updated_at: "2008-06-14 18:25:36">
One interesting and occasionally helpful thing to note is that we can make assignments while debugging.
(rdb:2) @property.name = "Josh\'s Books" "Josh's Books" (rdb:2)
Setting Breaks while Debugging
We are not limited to the breakpoints we put in our code (i.e. debugger). We can also set conditional or unconditional breakpoints from within the debugger. The b (break) command takes either a file and a line number or a class and a method as arguments. So if we wanted to add a breakpoint on the edit and delete actions of the PropertiesController for instance we could simply enter this.
(rdb:716) break PropertiesController.edit Breakpoint 1 at PropertiesController::edit (rdb:716) break PropertiesController.delete Breakpoint 2 at PropertiesController::delete
And to delete the edit breakpoint simply specify the number and delete it like so.
(rdb:1132) delete 1
Now let's list our single remaining breakpoint with info breakpoints.
(rdb:1132) info breakpoints Num Enb What 2 y at PropertiesController:delete
irb
The debugger offers us a lot but lacks some of the functionality of script/console or irb. It turns out, however, that that's not really a problem, we can get the functionality of irbeasily enough. By simply typing irb at the debugger prompt we can open an irb session. In this session we have the bindings environment set to the current debugging state of your app and are able to alter the behavior of the application. Once we have done whatever we need in irb we can simply type quit and we will be returned to the same line in the debugger as when we entered irb.
Don't Forget the Help
This is by no means an exhaustive examination of the debugger but rather is a brief introduction to the uninitiated. Don't forget to use h (help) and h command for more information. I also would recommend trying out the cheat gem for a quick reference of this and other helpful cheat sheets.
sudo gem install cheat cheat rdebug

