"Dear Jacob" Git advice: git add -u
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
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
git clone git://github.com/jhelwig/lighthouseapp-git-hook.git
#!/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"; } }
