Current tags: git (3 posts)
Additional tags: development (1) hooks (1) lighthouse (1) tips (1)

"Dear Jacob" Git advice: git add -u

Written by Jacob Helwig on 2010-05-01 @ 22:15 PDT

I recently received a request that I start a "Dear Jacob" advice column for git, and thought that it was a pretty nifty idea. I needed a good excuse to post more frequently, and I do end up answering a lot of questions about Git for the people that I know.

Welcome to the first installment of the "Dear Jacob" Git advice column!

The first tip comes about because of wanting to add all of the modified files to the index, without adding any test, or experimental files that might also be in your tree.

My first thought would be to try git add -a. Unfortunately, that doesn't work at all (even though -a is what you'd want if you were doing git commit in this case). With git add, you actually want -u.

Anyway, the disjunction resulted in the person doing git diff --name-only | xargs git add whenever they wanted to add only the modified files in their repo. They couldn't use git add . to add everything, since they potentially had some test/experiment files that they didn't want to track.

Fun with the upcoming 1.7 release of Git: rebase --interactive --autosquash

Written by jhelwig on 2010-02-07 @ 17:50 PST

The upcoming Git 1.7 has a lot of really nice improvements, and new features. One of the big new features is the --autosquash argument for git rebase --interactive.

If you're anything like me, then you commit a lot, while you're working on something, and use git rebase --interactive judiciously to clean up all these incremental commits into a presentable format. If you're a bit more like me, then you'll often end up doing multiple git rebase --interactive passes to split commits apart, and squash them back into other commits.

Git just gained the ability to make this a little faster. If you know what commit you want to squash something in to you can commit it with a message of "squash! $other_commit_subject". Then if you run git rebase --interactive --autosquash commitish, the line will automatically be set as squash, and placed below the commit with the subject of $other_commit_subject.

For example:

$ vim Foo.txt
$ git commit -am "Change all the 'Bar's to 'Foo's"
[topic 8374d8e] Change all the 'Bar's to 'Foo's
 1 files changed, 2 insertions(+), 2 deletions(-)
$ vim Bar.txt
$ git commit -am "Change all the 'Foo's to 'Bar's"
[topic 2d12ce8] Change all the 'Foo's to 'Bar's
 1 files changed, 1 insertions(+), 1 deletions(-)
$ vim Foo.txt
$ git commit -am "squash! Change all the 'Bar's"
[topic 259a7e6] squash! Change all the 'Bar's
 1 files changed, 2 insertions(+), 1 deletions(-)

If we run git rebase --interactive --autosquash origin/master from here, the pick-list will look like this:

pick 8374d8e Change all the 'Bar's to 'Foo's
squash 259a7e6 squash! Change all the 'Bar's
pick 2d12ce8 Change all the 'Foo's to 'Bar's

# Rebase b6bee12..259a7e6 onto b6bee12
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

When you get to the squash, you'll have a commit message like:

# This is a combination of 2 commits.
# The first commit's message is:

Change all the 'Bar's to 'Foo's

# This is the 2nd commit message:

squash! Change all the 'Bar's

# Please enter the commit message for your ch anges. Lines starting
# with '#' will be ignored, and an empty mess age aborts the commit.
# Not currently on any branch.
# Changes to be committed:
#    modified:   Foo.txt
#

If you were paying attention earlier to the pick-list, you'll notice that there's also a fixup command available. If we had specified fixup!, instead of squash! as the commit message's prefix, then the pick list would have ended up as:

pick 8374d8e Change all the 'Bar's to 'Foo's
fixup cfc6e54 fixup! Change all the 'Bar's
pick 2d12ce8 Change all the 'Foo's to 'Bar's

# Rebase b6bee12..cfc6e54 onto b6bee12
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

With the following in your editor for the combined commit message:

# This is a combination of 2 commits.
# The first commit's message is:

Change all the 'Bar's to 'Foo's

# The 2nd commit message will be skipped:

#    fixup! Change all the 'Bar's

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# Not currently on any branch.
# Changes to be committed:
#    modified:   README.markdown
#

Notice that the fixup! commit's message is already commented out. You can just save out the message as-is, and your original commit message will be kept. Very handy for including changes when you realize that you forgot to add part of an earlier commit.

Here's a few aliases I have setup to make all this easier:

[alias]
    fixup = !sh -c 'git commit -m \"fixup! $(git log -1 --format='\\''%s'\\'' $@)\"' -
    squash = !sh -c 'git commit -m \"squash! $(git log -1 --format='\\''%s'\\'' $@)\"' -
    ri = rebase --interactive --autosquash

Here's how they would be used in our previous example:

$ vim Foo.txt
$ git commit -am "Change all the 'Bar's to 'Foo's"
[topic 8374d8e] Change all the 'Bar's to 'Foo's
 1 files changed, 2 insertions(+), 2 deletions(-)
$ vim Bar.txt
$ git commit -am "Change all the 'Foo's to 'Bar's"
[topic 2d12ce8] Change all the 'Foo's to 'Bar's
 1 files changed, 1 insertions(+), 1 deletions(-)
$ vim Foo.txt
$ git add Foo.txt
$ git squash HEAD~2
[topic 259a7e6] squash! Change all the 'Bar's to 'Foo's
 1 files changed, 2 insertions(+), 1 deletions(-)
$ git ri origin/master

Similarly, git fixup HEAD~2 would create a fixup! commit to be used with git rebase --interactive --autosquash (Aliased as: git ri).

Edit 2010-02-13: Fix alias examples.

Git + Lighthouse

Written by jhelwig on 2008-05-18 @ 10:43 PDT

I've been playing around with Git, CIA.vc, and Lighthouse on a project of mine that's recently been resurrected. There is a pretty good update hook for CIA.vc integration called ciabot.pl that I've been using, without any complaints. Unfortunately, I haven't been able to find anything to integrate Git with Lighthouse that hasn't needed modification out of the box. I tried the (pre|post)-receive hooks provided at Obvious Code, but it had a few issues. It would use the author/committer information of the first rev it saw, and use that for every revision it handled. Lighthouse couldn't understand the "changed-at" time format it was using. The hook also relied on writing out a file with the last rev it saw, so it knew where to pick up, which I didn't really like. So, I decided to write my own in Perl. The latest version is available via Git on GitHub.
git clone git://github.com/jhelwig/lighthouseapp-git-hook.git
The script can either be called straight from an existing update hook, or can be the update hook. It will split out commits on its own given the old and new SHA1.
#!/usr/bin/perl -w

use strict;
binmode STDIN, ':utf8';

use LWP::UserAgent ();

use Date::Format          qw/ time2str /;
use HTTP::Request::Common qw/ POST     /;
use XML::Simple           qw/ XMLout   /;
use YAML                  qw/ Dump     /;

#################################################
# Configuration Options                         #
#################################################
my $git = '/home/jhelwig/bin/git';
my %tokens = (
    # Fallback token to use if one isn't found for an author.
    'default' => '',
    # Individual tokens for author/committer.
    # If author email can't be found, committer's email will be tried.
    'jacob@technosorcery.net' => undef,
    'Another Author' => undef,
);
my $lighthouseapp_account_url = 'http://account-name.lighthouseapp.com';
my $lighthouseapp_project_id = 0;

#################################################

shift @ARGV;
my $old_sha1 = shift @ARGV;
my $new_sha1 = shift @ARGV;

my $rev_list = `$git rev-list --pretty=format:"" $old_sha1..$new_sha1`;
$rev_list =~ s/^commit //gm;

my @revs = reverse(split("\n"$rev_list));

foreach my $rev (@revs) {
    chomp(my $author_name = `$git show --pretty=format:"%an$rev | sed q`);
    chomp(my $author_email = `$git show --pretty=format:"%ae$rev | sed q`);
    chomp(my $author_date = `$git show --pretty=format:"%aD$rev | sed q`);
    chomp(my $committer_name = `$git show --pretty=format:"%cn$rev | sed q`);
    chomp(my $committer_email = `$git show --pretty=format:"%ce$rev | sed q`);
    chomp(my $committer_date = `$git show --pretty=format:"%cD$rev | sed q`);
    chomp(my $changed_at = time2str("%Y-%m-%dT%TZ"`$git show --pretty=format:"%ct$rev | sed q`'GMT'));
    chomp(my $commit_log = `$git log -n1 --pretty=medium $rev | sed '1,4d'`);

    my $body = <<"HERE";
$commit_log

Author:     $author_name <$author_email>
AuthorDate: $author_date
Commit:     $committer_name <$committer_email>
CommitDate: $committer_date
HERE

    chomp(my $commit_subject = `$git show --pretty=format:"%s$rev | sed q`);
    my $changes_yaml = Dump([
        map { [ split(/\s+/$_) ] } split("\n"`$git diff-tree -r --name-status $rev | sed '1d'`)
    ]);

    my $xml = XMLout({ 'changeset' => {
            'title'      => [ $commit_subject, ],
            'body'       => [ $body, ],
            'revision'   => [ $rev, ],
            'changes'    => {
                'type'    => 'yaml',
                'content' => $changes_yaml,
            },
            'changed-at' => {
                'type'    => 'datetime',
                'content' => $changed_at,
            },
        }},
        KeepRoot => 1,
    );

    my $lh_token = defined($tokens{$author_email})
        ? $tokens{$author_email}
        : defined($tokens{$committer_email})
            ? $tokens{$committer_email}
            : $tokens{'default'};

    my $ua = LWP::UserAgent->new();
    $ua->timeout(3);
    $ua->env_proxy();

    my $response = $ua->simple_request(POST(
        "$lighthouseapp_account_url/projects/$lighthouseapp_project_id/changesets.xml",
        'content-type' => 'application/xml',
        'X-LighthouseToken' => $lh_token,
        Content => $xml
    ));

    unless ($response->is_success()) {
        print $response->status_line() . "\n"
            . $response->content() . "\n\n";

    }
}