"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.

Helper script for creating posts with WWW::StaticBlog

Written by Jacob Helwig on 2010-04-10 @ 13:40 PDT (Last updated: 2010-04-10 @ 13:43 PDT)

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.

Written by jhelwig on 2010-04-02 @ 21:45 PDT

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

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.

Capistrano completion in zsh

Written by jhelwig on 2009-10-06 @ 23:03 PDT (Last updated: 2010-04-04 @ 09:50 PDT)

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

Written by jhelwig on 2009-10-04 @ 10:33 PDT (Last updated: 2010-04-02 @ 21:00 PDT)

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

Written by jhelwig on 2009-09-30 @ 14:37 PDT

Recently, I've decided to start learning Ruby on Rails (2.3.4). Things have been going along more-or-less smoothly (I'm still not sure whether or not I hate ActiveRecord, or can tolerate it, but that's a post for another time.). That is, until I started looking into the various plugins/frameworks for doing Authorization in Rails. After searching around for a bit, the two main contenders I found were rails-authorization-plugin, and acl9. acl9 seemed to be, by far, the more commonly recommended of the two. I wasn't really impressed with the Apache style allow/deny used by acl9. Just wasn't what I was looking for with my project, so I started looking at rails-authorization-plugin. This is where I ran into trouble. rails-authorization-plugin allows you to assign roles for objects, and have an optional scope. For example:
# 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
So far, so good. Now, let's check what roles this user has:
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
Wait? What? That's right: Inheritance of roles flows from instance, to class, to global. If you have a role for any instance, you also have it globally. This is completely backwards from what I would have expected, and from what I wanted. Ok, time to go back to looking at acl9. Guess what? It does the exact same thing. Given these findings, I did what any developer, with access to Git would do. I forked the code to make it do what I want. Thus my fork of rails-authorization-plugin (and it's tests) was born. I decided to base my changes off of rails-authroization plugin, simply because I didn't like the Apache style allow/deny syntax of acl9. Now, given the initial role assignments above, we get the following:
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
Inheritance is handled as follows: Global -> Class -> Instance. If a role is assigned at a "higher" level (further left), then it applies at all levels "lower" than it (further right). While I was in there monkeying around, I decided that I didn't like strings as role names. They seemed a little more special to me than just plain old strings. Now we can use symbols as role names:
user.has_role :administrator
user.has_role? :administratorGroup
Things still didn't quite seem right though. The whole role to scope mapping seemed like it deserved to be emphasized a little more, and assigning multiple roles at once was a little clunky. Now we can use hashes for assignment, and lookup:
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
This actually demonstrates two things. First using a scope of 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

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";

    }
}

Soft resolution of Request Tracker tickets.

Written by jhelwig on 2007-09-06 @ 05:51 PDT

We wanted to be able to close a ticket, without actually "closing" it. Thus the "pending" ticket status was born. We can set a ticket's status as "pending", and have it automatically marked as "closed" n days later, if there haven't been any replies in that time. I have received permission from my employer to release this under the GNU GPLv2. Now that the background info is done with, here's the solution I came up with to be able to have a soft resolution of tickets. First up: Add the status itself to the list of inactive statuses, since supposedly the ticket is done with, or the requester just doesn't care enough anymore to be bothered with replying to it. Put the following in etc/RT_SiteConfig.pm:
@InactiveStatus = qw(resolved rejected pending deleted) unless @InactiveStatus;
You'll then need to create 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;
This will be used as the --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;
Now that the pre-requisites are in place, we can create the 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
That finishes up the portion to mark the tickets as resolved, after the given number of days have passed, with no updates to a ticket. Next up is re-opening the ticket if someone replies to it. Now we'll create the pre-requisites necessary to setup the queue, and global scrips relating to pending tickets. 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;
You can use the following template ((Also taken from the ReplyToResolved RT Wiki site.)) to make the conditions, and actions described here available through the web scrip interface:
#!/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'
           );
Now that these steps are done, you can create a global scrip with the condition set to 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()
);
This sets up the "On Pending" scrip condition, so that you'll be able to create a scrip such as "On Pending Reply to Requesters with template XXXXXX", and have a custom message sent out to inform the requester that the ticket will close itself in n days, unless they reply to it.