"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.
Helper script for creating posts with WWW::StaticBlog
There's always another yak that needs shaving. Since creating posts with WWW::StaticBlog wasn't fast enough for my tastes, I made a small script to make it even faster!
Before, I would create the directory hirarchy, if I haven't yet made a
post this month, coming up with a file name that has some relavence to
the post contents, but is still sane for a file name, and opening it up
in the editor, creating the Title:, and Author: "headers", and
finally getting down to writing the silly post.
#!/bin/bash post_date="$(date +%Y/%m)" title="$*" if [ "$title" == "" ]; then read -e -p 'Post title: ' title fi filename="$(echo -n "$title" | tr -c '[:alnum:]' '_')" mkdir -p articles/$post_date new_post="articles/$post_date/$filename" echo "Title: $title" > $new_post echo "Author: $(git config user.name)" >> $new_post echo >> $new_post echo >> $new_post git add $new_post vim -c 'set tw=72' '+/^$/+' -c 'nohl' $new_post
By saving this as ~/bin/git-new-post, I automatically get a new Git command.
Now, I can just type git new-post from my site's repository, and I'll be
prompted for the post's title (with readline support), and the directories will
be created for me, the title will be sanitized into a reasonable file name, the
empty post with just the headers will be added to Git, and the file will be
opened up in vim, with the cursor at the end, ready for me to just start
writing.
Optionally, I can provide the title to new-post with git new-post Some title,
and it won't bother prompting me.
Ditching dynamic blog engines.
A while back, I'd ditched WordPress in favor of Mephisto. I'd grown tired of constantly being under attack from spammers, and really disliked that it was one gigantic PHP app.
Once I was on Mephisto, I really liked the syntax highlighting I got with the Ultraviolet Gem, but it requires Oniguruma, which was a pain to setup on my shared host (they have since added it to their hosts). Life was good.
Except that it wasn't. It was a little annoying how long it took pages to come up in the "cold cache" case, and I'd actually gotten a few complaints about that. After this, I decided that I was just going to do away with "dynamic blog engines" entirely.
Enter WWW::StaticBlog. I decided that I was just going to statically generate every page, and upload them to the shared host. No more worrying about spammers trying to "hack" my site, and no more slow page loads waiting for a full Ruby on Rails app to start up.
So far the features of WWW::StaticBlog are:
- Pluggable templating system. (Currently only has a plugin for Template::Toolkit)
- Support for tagging articles, and browsing articles by arbitrary combinations of tags.
- Atom feed for posts.
- Support for purely static content.
Things I plan on adding:
- Author pages, with the ability to browse posts by author.
- Support for more templating engines (Template::Mustache will probably be the next one.)
- Archives pages to view past posts by year/month.
- Only generate the pages that have actually changed, instead of regenerating everything, every time.
The idea behind WWW::StaticBlog is that the host does not need to have support for anything, other than serving up static files, and you'll still have what would look like a dynamically generated site.
As a live example of how to use WWW::StaticBlog, you can check out the Git repository for this site.
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.
Capistrano completion in zsh
I've decided to try out zsh for a while, and while I already get completions for most everything I want, out of the box, I am missing completions for Capistrano tasks.
I had been using brynary's Bash Capistrano completion script. I was able to find a mailing list post about setting up Capistrano task completions for zsh, but it didn't quite work for me. (show_tasks isn't a valid task.) I also didn't like throwing the .cap_tasks file in the top-level of the project. I already had a ~/.zsh_cache/ directory for caching zsh's completions, so I decided to modify the script I found to put the cache file there, instead.
_cap_does_task_list_need_generating () { if [ ! -f cap_tasks ]; then return 0; else accurate=$(stat -f%m ~/.zsh_cache/cap_tasks-$(echo $PWD | sha512sum)) changed=$(stat -f%m config/deploy.rb) return $(expr $accurate '>=' $changed) fi } _cap () { if [ -f config/deploy.rb ]; then if _cap_does_task_list_need_generating; then cap -T | grep '^cap' | cut -d' ' -f2 >! ~/.zsh_cache/cap_tasks-$(echo $PWD | sha512sum) fi compadd `cat ~/.zsh_cache/cap_tasks-$(echo $PWD | sha512sum)` fi } compdef _cap cap
I have the above in ~/.zsh.d/S50_capistrano, which automatically gets loaded on startup.
for zshrc_snipplet in ~/.zsh.d/S[0-9][0-9]*[^~] ; do source $zshrc_snipplet done
Fixing the Oniguruma Gem for use on DreamHost
While looking at how to get syntax highlighted source back up on here after switching to Mephisto, I kept running across references to the Ultraviolet gem. Some of the dependencies are a little old (Oniguruma: Gem, Library), but the output looks very nice, from the examples I'd seen.
The problem comes in, that the Oniguruma gem won't install without you already having the Oniguruma library installed (in a standard system location). This is a pretty well documented problem, with a simple fix.
Unfortunately, I wasn't even able to get the gem to build at all with the original Rakefile that comes with it, and gave up very quickly on trying to fix it. Fortunately, there is a wonderful gem out there called Jeweler. This allowed me to trivially setup a working build environment, drop in the original gem code, and get something up and running.
After adding dir_config to the extconf.rb, you can happily install the Oniguruma gem (provided your LD_LIBRARY_PATH includes wherever you installed the library). This gets to be a problem, when using Passenger on a shared host (such as DreamHost), like I'm trying to do. Fortunately, there's a way to fix this (at least on Linux). When linking in the libraries, you can tell ld to include path information on where to look for them. This is very handy.
Here's the extconf.rb that I ended up going with:
require 'mkmf' onig_dirs = dir_config('onig') onig_libs = onig_dirs.pop ldshared = CONFIG['LDSHARED'] if !onig_libs.nil? onig_libs.split(File::PATH_SEPARATOR).each do |p| ldshared += " -Wl,-rpath,#{p}" end end CONFIG['LDSHARED'] = ldshared have_library("onig") $CFLAGS='-Wall' create_makefile( "oregexp" )
With this, I was able to build the gem, install it locally, and still have it work with passenger. I can't guarantee it's the best way to do it, but it works.
I've put the modified Oniguruma gem on GitHub
Until GitHub gets the gem building back up and running, you'll have to download, and make the .gem file yourself, unfortunately.
git clone git://github.com/jhelwig/oniguruma cd oniguruma rake build gem install pkg/oniguruma-$(rake version | sed -e '/oniguruma/d' -e 's/Current version: //').gem -- --with-onig-dir $HOME
Unhappy with the standard Rails Authorization plugins
# Assign user the "global" role 'administrator' user.has_role 'administrator' # Assign user the role "moderator" for the class Group user.has_role 'moderator', Group # Assigns user the role "member" for the instance (of class Group) user.has_role 'member', club
user.has_role? 'administrator' # => true user.has_role? 'administrator', Group # => false user.has_role? 'administrator', club # => false user.has_role? 'moderator' # => true user.has_role? 'moderator', Group # => true user.has_role? 'moderator', club # => false user.has_role? 'member' # => true user.has_role? 'member', Group # => true user.has_role? 'member', club # => true
user.has_role? 'administrator' # => true user.has_role? 'administrator', Group # => true user.has_role? 'administrator', club # => true user.has_role? 'moderator' # => false user.has_role? 'moderator', Group # => true user.has_role? 'moderator', club # => true user.has_role? 'member' # => false user.has_role? 'member', Group # => false user.has_role? 'member', club # => true
user.has_role :administrator user.has_role? :administrator, Group
user.has_role :administrator => nil, :moderator => Group, :member => club user.has_role? :administrator => Group, :moderator => club # => true user.has_role? :administrator => nil, :moderator => nil # => false
nil, is the same as saying that the scope is global. Secondly,
when doing has_role? with a hash, all role requirements must be
met for has_role? to be true.
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"; } }
Soft resolution of Request Tracker tickets.
etc/RT_SiteConfig.pm:
@InactiveStatus = qw(resolved rejected pending deleted) unless @InactiveStatus;
local/lib/RT/Action/AutoResolve.pm
((I'd love to give credit on where I found the code that this is based on, but
I can't seem to find it on the RT Wiki anymore. If you can find the original,
let me know so I can give proper credit for this.)) with the following content:
package RT::Action::AutoResolve; require RT::Action::Generic; use strict; use vars qw/@ISA/; @ISA=qw(RT::Action::Generic); sub Describe { my $self = shift; return (ref $self ); } sub Prepare { my $self = shift; # if the ticket is already resolved don't re-resolve it. if ( ( $self->TicketObj->Status eq 'resolved' ) ) { return undef; } else { return (1); } } sub Commit { my $self = shift; $self->TicketObj->SetStatus( 'resolved' ); return (1); } 1;
--action argument to
rt-crontool.
Create local/lib/RT/Condition/UntouchedInDays.pm ((Modified from
UntouchedInHours.))
for the --condition used with rt-crontool:
package RT::Condition::UntouchedInDays; require RT::Condition::Generic; use RT::Date; @ISA = qw(RT::Condition::Generic); use strict; use vars qw/@ISA/; sub IsApplicable { my $self = shift; if ((time()-$self->TicketObj->LastUpdatedObj->Unix)/3600/24 >= $self->Argument) { return 1 } else { return 0; } } 1;
local/lib/RT/Search/PendingTicketsInQueue.pm will be used for the
--search. ((I can't seem to find where I got this code from
originally. If you recognize it, or are the original author, please let me
know.))
package RT::Search::PendingTicketsInQueue; use strict; use base qw(RT::Search::Generic); sub Describe { my $self = shift; return ($self->loc("No description for [_1]", ref $self)); } sub Prepare { my $self = shift; $self->TicketsObj->LimitQueue(VALUE => $self->Argument); $self->TicketsObj->LimitStatus(VALUE => 'pending'); return(1); } 1;
rt-pending script that should be called daily to resolve tickets
that have been set as pending for n days (in this example: n =
7). ((I just dropped this script into /etc/cron.daily/, but
I'm using Debian. You should adjust as appropriate for your distribution.))
#! /bin/sh /opt/rt3/bin/rt-crontool --search RT::Search::PendingTicketsInQueue --search-arg 'Help Desk' --condition RT::Condition::UntouchedInDays --condition-arg 7 --action RT::Action::AutoResolve
local/lib/RT/Condition/ReplyToPending.pm ((Heavily based on ReplyToResolved.pm))
package RT::Condition::ReplyToPending; use strict; use base qw(RT::Condition::Generic); sub IsApplicable { my $self = shift; my $ticket = $self->TicketObj; my $transaction = $self->TransactionObj; if ( $transaction->Type eq 'Correspond' && $ticket->Status eq 'pending' && $transaction->Creator != 1 ) { # prevent loop return(1); } else { return(undef); } } 1;
#!/usr/bin/perl use strict; use Unicode::String qw(utf8 latin1); # Replace this with your RT_LIB_PATH use lib "/opt/rt3/lib"; # Replace this with your RT_ETC_PATH use lib "/opt/rt3/etc"; use RT; use RT::Interface::CLI qw( CleanEnv GetCurrentUser ); use RT::ScripCondition; CleanEnv(); RT::LoadConfig(); RT::Init(); ##Drop setgid permissions RT::DropSetGIDPermissions(); ##Get the current user all loaded our $CurrentUser = GetCurrentUser(); unless( $CurrentUser->Id ) { print "No RT user found. Please consult your RT administrator.\n"; exit 1; } my $sc = new RT::ScripCondition($CurrentUser); $sc->Create( Name => 'On Reply to Pending', Description => "Reply to a ticket marked as pending.", ExecModule => 'ReplyToPendingTicket', ApplicableTransTypes => 'Any' );
On Reply to Pending, and the action set to Open
Ticket. This will re-open any tickets as soon as someone replies to
them, but not if anyone comments on them. (Commenting will, however delay the
resolution of a ticket.)
You'll then need to create either a global scrip, or a queue specific scrip for
each queue where you want people to be able to reply to a pending ticket, and
have it re-opened. (Personally I recommend the global scrip method.)
This step requires direct DB access. You'll need to be able to do an insert on
the ScripConditions table. <user_id> should be
replaced with a valid user ID, taken from one of the other entries in the
ScripConditions table.
INSERT INTO ScripConditions( Name, Description, ExecModule, Argument, ApplicableTransTypes, Creator, Created, LastUpdatedBy, LastUpdated ) VALUES ( 'On Pending', 'Whenever a ticket is marked as pending resolution', 'StatusChange', 'pending', 'Status', <user_id>, NOW(), <user_id>, NOW() );
