Prerequisite

  1. Install git

    sudo apt-get install git-core
    

Initializing gitosis

  1. Grab gitosis

    kamal@dev:~$ git clone git://eagain.net/gitosis.git
    Initialized empty Git repository in /home/kamal/gitosis/.git/
    remote: Generating pack...
    remote: Done counting 549 objects.
    remote: Deltifying 549 objects...
    remote:  100% (549/549) done
    Indexing 549 objects...
    remote: Total 549 (delta 379), reused 136 (delta 95)
     100% (549/549) done
    Resolving 379 deltas...
     100% (379/379) done
    
  2. Install gitosis

    kamal@dev:~/gitosis$ cd gitosis && sudo python setup.py install
    running install
    running bdist_egg
    running egg_info
    creating gitosis.egg-info
    ...
    Installing gitosis-init script to /usr/bin
    Installing gitosis-run-hook script to /usr/bin
    Installing gitosis-serve script to /usr/bin
    
    Installed /usr/lib/python2.4/site-packages/gitosis-0.2-py2.4.egg
    Processing dependencies for gitosis==0.2
    Finished processing dependencies for gitosis==0.2
    
  3. Create the git user on the server

    kamal@dev:~$ sudo adduser \
    >        --system \
    >        --shell /bin/sh \
    >        --gecos 'git version control' \
    >        --group \
    >        --disabled-password \
    >        --home /var/local/git \
    >        git
    Adding system user `git' (UID 106) ...
    Adding new group `git' (GID 109) ...
    Adding new user `git' (UID 106) with group `git' ...
    Creating home directory `/var/local/git' ...
    
  4. Back on your local machine, scp your SSH public key to the server

    MacBook-Pro:~ kamal$ scp ~/.ssh/id_dsa.pub dev.ror.com.my:/tmp
    id_dsa.pub                                    100%  613     0.6KB/s   00:00
    
  5. Back on the server, initialize gitosis

    kamal@dev:~/gitosis$ sudo -H -u git gitosis-init < /tmp/id_dsa.pub
    Initialized empty Git repository in ./
    Initialized empty Git repository in ./
    
  6. Back on your local machine, clone the automatically created gitosis-admin project

    MacBook-Pro:~ kamal$ git clone git@dev.ror.com.my:gitosis-admin.git
    Initialized empty Git repository in /Users/kamal/gitosis-admin/.git/
    remote: Generating pack...
    remote: Done counting 5 objects.
    remote: Deltifying 5 objects...
    remote:  100% (5/5) done
    remote: Total 5 (delta 1), reused 5 (delta 1)
    Indexing 5 objects...
     100% (5/5) done
    Resolving 1 deltas...
     100% (1/1) done
    
  7. Check out the contents of gitosis-admin

    MacBook-Pro:~ kamal$ cd gitosis-admin/
    MacBook-Pro:gitosis-admin kamal$ ls
    gitosis.conf    keydir
    MacBook-Pro:gitosis-admin kamal$ more gitosis.conf
    [gitosis]
    
    [group gitosis-admin]
    writable = gitosis-admin
    members = kamal@MacBook-Pro.local
    
    MacBook-Pro:gitosis-admin kamal$ ls keydir/
    kamal@MacBook-Pro.local.pub
    

    Note: The main configuration file lives in gitosis.conf and the users are identified by their SSH public keys which are in keydir

Adding Users

  1. Adding Aizat as a gitosis-admin member is easy

    MacBook-Pro:gitosis-admin kamal$ cp /tmp/aizat-id_dsa.pub keydir/aizat@Zatto.local.pub
    

    Note: The filename in the keydir has to match the last bit in the public key file plus .pub

  2. Edit gitosis.conf

    [gitosis]
    
    [group gitosis-admin]
    writable = gitosis-admin
    members = kamal@MacBook-Pro.local aizat@Zatto.local
    
  3. Add the new file to git

    MacBook-Pro:gitosis-admin kamal$ git add keydir/aizat\@Zatto.local.pub
    
  4. Commit the changes

    MacBook-Pro:gitosis-admin kamal$ git-commit -a -m "Added Aizat as a gitosis admin"
    Created commit 37c0966: Added Aizat as a gitosis admin
     2 files changed, 2 insertions(+), 1 deletions(-)
     create mode 100644 keydir/aizat@Zatto.local.pub
    
  5. And push to the server

    MacBook-Pro:gitosis-admin kamal$ git push
    updating 'refs/heads/master'
      from 3b5462b027a02a5b29bf43f4ca878350ccc8b913
      to   37c0966f285715cc38bd801a3955bb6f319ccdf4
     Also local refs/remotes/origin/master
    Generating pack...
    Done counting 8 objects.
    Result has 5 objects.
    Deltifying 5 objects...
     100% (5/5) done
    Writing 5 objects...
     100% (5/5) done
    Total 5 (delta 1), reused 0 (delta 0)
    refs/heads/master: 3b5462b027a02a5b29bf43f4ca878350ccc8b913 -> 37c0966f285715cc38bd801a3955bb6f319ccdf4
    
  6. To add non-admin users, the process is the roughly the same. You only need to create a new group in gitosis.conf.

Adding Projects

  1. Add a new group for other users and set a new test project as the writable project

    [gitosis]
    
    [group gitosis-admin]
    writable = gitosis-admin
    members = kamal@MacBook-Pro.local aizat@Zatto.local
    
    [group rsb]
    writable = test_project
    members = @gitosis-admin kegan@MacBook.local saimer@MacBook.local seanx2@Dell.local
    
  2. Commit and push

    MacBook-Pro:gitosis-admin kamal$ git-commit -a -m "Added a new group for RSB and set write perms on a new test project"
    MacBook-Pro:gitosis-admin kamal$ git push
    updating 'refs/heads/master'
      from 37c0966f285715cc38bd801a3955bb6f319ccdf4
      to   2fc5d9c394218fb757ce6fe468b9ea8096e99dcf
     Also local refs/remotes/origin/master
    Generating pack...
    Done counting 8 objects.
    Result has 6 objects.
    Deltifying 6 objects...
     100% (6/6) done
    Writing 6 objects...
     100% (6/6) done
    Total 6 (delta 1), reused 0 (delta 0)
    refs/heads/master: 37c0966f285715cc38bd801a3955bb6f319ccdf4 -> 2fc5d9c394218fb757ce6fe468b9ea8096e99dcf
    
  3. Create the test project

    MacBook-Pro:~ kamal$ mkdir test_project
    MacBook-Pro:~ kamal$ cd test_project/
    MacBook-Pro:test_project kamal$ git init
    Initialized empty Git repository in .git/
    MacBook-Pro:test_project kamal$ git remote add origin git@dev.ror.com.my:test_project.git
    MacBook-Pro:test_project kamal$ echo "This is an test project hosted in git" > README
    MacBook-Pro:test_project kamal$ git add README
    MacBook-Pro:test_project kamal$ git commit -a -m "Added a README file"
    Created initial commit f70c2f7: Added a README file
     1 files changed, 1 insertions(+), 0 deletions(-)
     create mode 100644 README
    

    Note: It is important that you have files to add, otherwise the next command will fail. I think this has to do with git not tracking empty directories?

  4. Push the test_project to the server

    MacBook-Pro:test_project kamal$ git push origin master:refs/heads/master
    updating 'refs/heads/master'
      from 0000000000000000000000000000000000000000
      to   f70c2f74d71fbbb1edd0f5da2554fd208f75051b
     Also local refs/remotes/origin/master
    Generating pack...
    Done counting 3 objects.
    Deltifying 3 objects...
     100% (3/3) done
    Writing 3 objects...
     100% (3/3) done
    Total 3 (delta 0), reused 0 (delta 0)
    refs/heads/master: 0000000000000000000000000000000000000000 -> f70c2f74d71fbbb1edd0f5da2554fd208f75051b
    
  5. Test out by cloning the project

    MacBook-Pro:~ kamal$ git clone git@dev.ror.com.my:test_project.git test_project2
    Initialized empty Git repository in /Users/kamal/test_project2/.git/
    remote: Generating pack...
    remote: Done counting 3 objects.
    remote: Deltifying 3 objects...
    remote:  100% (3/3) done
    remote: Total 3 (delta 0), reused 0 (delta 0)
    Indexing 3 objects...
     100% (3/3) done
    

And you’re done!

In the next article, I will explore migrating existing Subversion repositories to git. Stay tuned!

… namely the UI Developer: me.

I’m not even going into the Rails file structure. However, just know that if you want to make your Rails programmer’s life much, much easier (and to make sure they won’t pee in your shampoo, ever) put your files in the public folder. Needless to say, images go in the public/images folder and stylesheets in public/stylesheets. Simple, yes?

Okay, fluff aside and since sharing is caring, today I’ll be sharing the first three sexy lines of Rails code I picked up!

THE BASIC

What’s a UI dev without his/her stylesheet? So first thing’s first: call that stylesheet!

<%= stylesheet_link_tag 'style' %>

Rails is intelligent enough to know that your stylesheets are indeed stylesheets and they’re housed in the public/stylehseets folder without you actually having to specify! How nifty is that? :)

THE SIMPLE BITS

How linking works in Rails:

<%= link_to('RSB, holla!', 'http://blog.ror.com.my') %>

which is actually your

<a href="http://blog.ror.com.my">RSB, holla!</a>

FUN STUFF

Making those images prettier with image_tag

Because Rails is emazing at compacting code, your generic <img src="images/spiffy.png" alt="spiffy!" class="wrapper" /> now becomes:

<%= image_tag 'spiffy.png', :class => 'wrapper', :alt => 'spiffy!' %>

Alrighties, so we’ve gotten the easy part out of the way :) Next week we’ll dive a little into how partials and conditionals work! :) And no, it’s not rocket science, it’s Ruby magic!

p/s: It is secretly fun harassing Kamal for code logic! :P

Rails Routing and Facebook’s POSTs

If you have done any amount of Facebook development with Rails, you will quickly encounter the number one thing that has pissed off every Rails developer - Everything Is A POST!

With Facebooker’s pseudo-resource route generator, things are not so bad. It allows us declarations such as

  facebook_resources :entries
  facebook_resources :photos

in config/routes.rb which is nice and concise. However, the paths generated are so old school!

  GET    /entries            # index
  GET    /entries/new        # new
  POST   /entries/create     # create
  GET    /entries/1/show     # show
  GET    /entries/1/edit     # edit
  POST   /entries/1/update   # update
  POST   /entries/1/destroy  # destroy

Yuck!

Rails taught us to love HTTP verbs, did it not? The above could be simplified if we took into account the HTTP request method, like so,

  GET    /entries            # index
  GET    /entries/new        # new
  POST   /entries            # create
  GET    /entries/1          # show
  GET    /entries/1/edit     # edit
  PUT    /entries/1          # update
  DELETE /entries/1          # destroy

Unfortunately, you are stuck with just POST, buddy. Don’t even dream about your PUT and DELETE. Is there anything we can do? Fortunately, yes!

We Can Have Our RESTful Routes Back!

So, how do you solve this with Facebook? Facebook very recently added an additional request parameter to disambiguate between GET and POST via fb_sig_request_method. The actual requests from Facebook are still POSTs but we can make use of fb_sig_request_method as an added condition in our routes.

This is how you do it. Stick the following piece of code in lib and require it in a config/initializers file, or if you are using Facebooker as a plugin, modify vendor/plugins/facebooker/lib/facebooker/rails/routing.rb

  def facebook_resources(name_sym)
    name = name_sym.to_s

    with_options :controller => name, :conditions => { :method => :post } do |map|

      map.with_options :conditions => { :fb_sig_request_method => 'GET' } do |get|
        get.named_route("new_#{name.singularize}",  "#{name}/new",       :action => 'new')
        get.named_route(name,                       name,                :action => 'index')
        get.named_route(name.singularize,           "#{name}/:id",       :action => 'show', :id => /\d+/)
        get.named_route("edit_#{name.singularize}", "#{name}/:id/edit",  :action => 'edit', :id => /\d+/)
      end

      map.with_options :conditions => { :fb_sig_request_method => 'POST' } do |post|
        post.named_route("create_#{name.singularize}",  name,                  :action => 'create')
        post.named_route("update_#{name.singularize}",  "#{name}/:id",         :action => 'update',  :id => /\d+/)
        post.named_route("destroy_#{name.singularize}", "#{name}/:id/destroy", :action => 'destroy', :id => /\d+/)
      end

    end
  end

Pastie link

Notice the new :condition => { :fb_sig_request_method => 'GET'} and :condition => { :fb_sig_request_method => 'POST'}? That is the key we need to be able to overload a path like /entries/1 to call #show or #update depending on the condition. As a result, our routes now generate much nicer paths. Check it:

  GET    /entries            # index
  GET    /entries/new        # new
  POST   /entries            # create
  GET    /entries/1          # show
  GET    /entries/1/edit     # edit
  POST   /entries/1          # update
  POST   /entries/1/destroy  # destroy

A patch has been submitted to Facebooker. Hopefully the maintainers will see it fit to merge this in. As an added bonus, the patch also includes singleton resources via

  facebook_resource :entry

to generate the following routes:

  GET    /entry              # show
  GET    /entry/new          # new
  POST   /entry              # create
  GET    /entry/edit         # edit
  POST   /entry/update       # update
  POST   /entry/destroy      # destroy

Note that this is still very much a workaround until Facebook moves all the fb_sig metadata into the request headers and use real GETs and POSTs. But until then, happy programming!

At RSB, we strongly believe in tests - copious amounts of tests. We have so many tests that we had to modify our Capistrano deployment recipes to NOT checkout our spec directory. Okay, I’m exaggerating. We do have decent amount of test, especially on the still-under-development Rails plugin to ease an aspect of Facebook development (we like to call it our secret sauce - look out for that soon).

So, we have autotest to keep us honest. The thing that bugs us is that autotest eats significant CPU resources as it constantly polls the filesystem to check file modification times. Aizat Faiz, one of our newest member to the RSB family, decided to hack autotest to utilize Leopard’s new File System Events with a bit of RubyCocoa glue. Now, autotest is informed of filesystem changes. Beautiful.

Go check out the blog entry, grab the code and paste it in your ~/.autotest file. Your machine will thank you.

We're Hiring
RSB is currently looking for Ruby On Rails developer and Web / Graphic Designer to join our amazing family.