Index: perl/lib/BarnOwl/Complete/Client.pm
===================================================================
--- perl/lib/BarnOwl/Complete/Client.pm	(revision dab89e2891d01e3a2c5ff346f0bd09e212b27c43)
+++ perl/lib/BarnOwl/Complete/Client.pm	(revision dc8f6e0953e232434c4d8f6fa01a608ed7bdb1da)
@@ -6,5 +6,5 @@
 package BarnOwl::Complete::Client;
 
-use BarnOwl::Completion::Util qw(complete_flags);
+use BarnOwl::Completion::Util qw(complete_flags complete_file);
 use BarnOwl::Complete::Filter qw(complete_filter_name complete_filter_expr);
 
@@ -167,4 +167,32 @@
 }
 
+sub complete_bindkey {
+    my $ctx = shift;
+    # bindkey KEYMAP KEYSEQ command COMMAND
+    #   0      1       2      3        4
+    if ($ctx->word == 1) {
+        return complete_keymap();
+    } elsif ($ctx->word == 2) {
+        return;
+    } elsif ($ctx->word == 3) {
+        return ('command');
+    } else {
+        my $new_ctx = $ctx->shift_words(4);
+        return BarnOwl::Completion::get_completions($new_ctx);
+    }
+}
+
+sub complete_print {
+    my $ctx = shift;
+    return unless $ctx->word == 1;
+    return complete_variable();
+}
+
+sub complete_one_file_arg {
+    my $ctx = shift;
+    return unless $ctx->word == 1;
+    return complete_file($ctx->words->[1]);
+}
+
 BarnOwl::Completion::register_completer(help    => \&complete_help);
 BarnOwl::Completion::register_completer(filter  => \&complete_filter);
@@ -178,4 +206,12 @@
 BarnOwl::Completion::register_completer(unset   => \&complete_set);
 BarnOwl::Completion::register_completer(startup => \&complete_startup);
+BarnOwl::Completion::register_completer(bindkey => \&complete_bindkey);
+BarnOwl::Completion::register_completer(print   => \&complete_print);
+
+BarnOwl::Completion::register_completer(source      => \&complete_one_file_arg);
+BarnOwl::Completion::register_completer('load-subs' => \&complete_one_file_arg);
+BarnOwl::Completion::register_completer(loadsubs    => \&complete_one_file_arg);
+BarnOwl::Completion::register_completer(loadloginsubs    => \&complete_one_file_arg);
+BarnOwl::Completion::register_completer(dump        => \&complete_one_file_arg);
 
 1;
Index: perl/lib/BarnOwl/Completion.pm
===================================================================
--- perl/lib/BarnOwl/Completion.pm	(revision 48d130b1d845e04389262e8cbfdd3064726b9468)
+++ perl/lib/BarnOwl/Completion.pm	(revision 880311dcb6439ff7571d1769b15e5a0491179f5c)
@@ -31,8 +31,9 @@
     my @words = get_completions($ctx);
     return unless @words;
-    my $prefix = common_prefix(@words);
+    my $prefix = common_prefix(map {completion_value($_)} @words);
 
     if($prefix) {
-        insert_completion($ctx, $prefix, scalar @words == 1);
+        insert_completion($ctx, $prefix,
+                          scalar @words == 1 && completion_done($words[0]));
     }
 
@@ -44,10 +45,48 @@
 }
 
+=head1 COMPLETIONS
+
+A COMPLETION is either a simple string, or a reference to an array
+containing two or more values.
+
+In the former case, the string use used for both the text to display,
+as well as the result of the completion, and is assumed to be a full
+completion.
+
+An arrayref completion consists of
+
+    [$display_text, $replacement_value[, $completion_done] ].
+
+$display_text will be printed in the case of ambiguous completions,
+$replacement_value will be used to substitute the value in. If there
+is only a single completion for a given word, a space will be appended
+after the completion iff $completion_done is true (or missing).
+
+=cut
+
+sub completion_text {
+    my $c = shift;
+    return $c unless ref($c) eq 'ARRAY';
+    return $c->[0];
+}
+
+sub completion_value {
+    my $c = shift;
+    return $c unless ref($c) eq 'ARRAY';
+    return $c->[1];
+}
+
+sub completion_done {
+    my $c = shift;
+    return 1 if ref($c) ne 'ARRAY' or @$c < 3;
+    return $c->[2];
+}
+
 sub insert_completion {
     my $ctx = shift;
-    my $completion = BarnOwl::quote(shift);
-    my $unique = shift;
+    my $completion = BarnOwl::quote(completion_value(shift));
+    my $done = shift;
 
-    if($unique) {
+    if($done) {
         $completion .= " ";
     }
@@ -66,5 +105,5 @@
 sub show_completions {
     my @words = @_;
-    my $all = BarnOwl::quote(@words);
+    my $all = BarnOwl::quote(map {completion_text($_)} @words);
     my $width = BarnOwl::getnumcols();
     if (length($all) > $width-1) {
@@ -98,5 +137,5 @@
         my $word = $ctx->words->[$ctx->word];
         if(exists($completers{$cmd})) {
-            return grep {$_ =~ m{^\Q$word\E}} $completers{$cmd}->($ctx);
+            return grep {completion_value($_) =~ m{^\Q$word\E}} $completers{$cmd}->($ctx);
         }
         return;
Index: perl/lib/BarnOwl/Completion/Util.pm
===================================================================
--- perl/lib/BarnOwl/Completion/Util.pm	(revision 69c27e6211624978c76b0f196e79d063e4d143d3)
+++ perl/lib/BarnOwl/Completion/Util.pm	(revision e6cec013ec75e5ee143ad87023528b9b132d4e52)
@@ -5,7 +5,10 @@
 
 use base qw(Exporter);
-our @EXPORT_OK = qw(complete_flags);
+our @EXPORT_OK = qw(complete_flags complete_file);
 
 use Getopt::Long;
+use Cwd qw(abs_path);
+use File::Basename qw(dirname basename);
+
 
 sub complete_flags {
@@ -67,2 +70,63 @@
     }
 }
+
+sub expand_tilde {
+    # Taken from The Perl Cookbook, recipe 7.3
+    my $path = shift;
+    $path =~ s{ ^ ~ ( [^/]* ) }
+                { $1
+                  ? (getpwnam($1))[7]
+                  : ( $ENV{HOME} || $ENV{LOGDIR}
+                      || (getpwuid($>))[7]
+                     )
+              }ex;
+    return $path;
+}
+
+sub splitfile {
+    my $path = shift;
+    if ($path =~ m{^(.*/)([^/]*)$}) {
+        return ($1, $2);
+    } else {
+        return ('', $path);
+    }
+}
+
+sub complete_file {
+    my $string = shift;
+
+    return ['~/', '~/', 0] if $string eq '~';
+
+    my $path = abs_path(expand_tilde($string));
+    my $dir;
+    if ($string =~ m{/$} || $string eq '' || basename($string) eq '.') {
+        $dir = $path;
+    } else {
+        $dir = dirname($path);
+    }
+    return unless -d $dir;
+
+    my ($pfx, $base) = splitfile($string);
+    
+    opendir(my $dh, $dir) or return;
+    my @dirs = readdir($dh);
+    close($dh);
+
+    my @out;
+    for my $d (@dirs) {
+        # Skip dotfiles unless explicitly requested
+        if($d =~ m{^[.]} && $base !~ m{^[.]}) {
+            next;
+        }
+        
+        my ($text, $value, $done) = ($d, "${pfx}${d}", 1);
+
+        if (-d "$dir/$d") {
+            $text .= "/";
+            $value .= "/";
+            $done = 0;
+        }
+        push @out, [$text, $value, $done];
+    }
+    return @out;
+}
Index: perl/modules/IRC/lib/BarnOwl/Module/IRC.pm
===================================================================
--- perl/modules/IRC/lib/BarnOwl/Module/IRC.pm	(revision 618a9803a0d96a593db35163d5f984957a0b7316)
+++ perl/modules/IRC/lib/BarnOwl/Module/IRC.pm	(revision 416241f7c28243f9c5890da26c47ca93147f4414)
@@ -137,6 +137,41 @@
 }
 
-use constant OPTIONAL_CHANNEL => 1;
-use constant REQUIRE_CHANNEL => 2;
+=head2 mk_irc_command SUB FLAGS
+
+Return a subroutine that can be bound as a an IRC command. The
+subroutine will be called with arguments (COMMAND-NAME,
+IRC-CONNECTION, [CHANNEL], ARGV...).
+
+C<IRC-CONNECTION> and C<CHANNEL> will be inferred from arguments to
+the command and the current message if appropriate.
+
+The bitwise C<or> of zero or more C<FLAGS> can be passed in as a
+second argument to alter the behavior of the returned commands:
+
+=over 4
+
+=item C<CHANNEL_ARG>
+
+This command accepts the name of a channel. Pass in the C<CHANNEL>
+argument listed above, and die if no channel argument can be found.
+
+=item C<CHANNEL_OPTIONAL>
+
+Pass the channel argument, but don't die if not present. Only relevant
+with C<CHANNEL_ARG>.
+
+=item C<ALLOW_DISCONNECTED>
+
+C<IRC-CONNECTION> may be a disconnected connection object that is
+currently pending a reconnect.
+
+=back
+
+=cut
+
+use constant CHANNEL_ARG        => 1;
+use constant CHANNEL_OPTIONAL   => 2;
+
+use constant ALLOW_DISCONNECTED => 4;
 
 sub register_commands {
@@ -167,5 +202,5 @@
 
     BarnOwl::new_command(
-        'irc-disconnect' => \&cmd_disconnect,
+        'irc-disconnect' => mk_irc_command( \&cmd_disconnect, ALLOW_DISCONNECTED ),
         {
             summary => 'Disconnect from an IRC server',
@@ -192,5 +227,5 @@
 
     BarnOwl::new_command(
-        'irc-mode' => mk_irc_command( \&cmd_mode, OPTIONAL_CHANNEL ),
+        'irc-mode' => mk_irc_command( \&cmd_mode, CHANNEL_OPTIONAL|CHANNEL_ARG ),
         {
             summary => 'Change an IRC channel or user mode',
@@ -216,5 +251,5 @@
 
     BarnOwl::new_command(
-        'irc-part' => mk_irc_command( \&cmd_part, REQUIRE_CHANNEL ),
+        'irc-part' => mk_irc_command( \&cmd_part, CHANNEL_ARG ),
         {
             summary => 'Leave an IRC channel',
@@ -241,5 +276,5 @@
 
     BarnOwl::new_command(
-        'irc-names' => mk_irc_command( \&cmd_names, REQUIRE_CHANNEL ),
+        'irc-names' => mk_irc_command( \&cmd_names, CHANNEL_ARG ),
         {
             summary => 'View the list of users in a channel',
@@ -294,5 +329,5 @@
 
     BarnOwl::new_command(
-        'irc-topic' => mk_irc_command( \&cmd_topic, REQUIRE_CHANNEL ),
+        'irc-topic' => mk_irc_command( \&cmd_topic, CHANNEL_ARG ),
         {
             summary => 'View or change the topic of an IRC channel',
@@ -391,25 +426,13 @@
 
 sub cmd_disconnect {
-    # Such a hack
-    local *get_connection_by_alias = sub {
-        my $key = shift;
-        return $ircnets{$key} if exists $ircnets{$key};
-        return $reconnect{$key}{conn} if exists $reconnect{$key};
-        die("No such ircnet: $key\n");
-    };
-
-    mk_irc_command(
-        sub {
-            my $cmd = shift;
-            my $conn = shift;
-            if ($conn->conn->connected) {
-                $conn->conn->disconnect;
-            } elsif ($reconnect{$conn->alias}) {
-                BarnOwl::admin_message('IRC',
-                                       "[" . $conn->alias . "] Reconnect cancelled");
-                delete $reconnect{$conn->alias};
-            }
-        }
-    )->(@_);
+    my $cmd = shift;
+    my $conn = shift;
+    if ($conn->conn->connected) {
+        $conn->conn->disconnect;
+    } elsif ($reconnect{$conn->alias}) {
+        BarnOwl::admin_message('IRC',
+                               "[" . $conn->alias . "] Reconnect cancelled");
+        $conn->cancel_reconnect;
+    }
 }
 
@@ -564,5 +587,5 @@
 sub mk_irc_command {
     my $sub = shift;
-    my $use_channel = shift || 0;
+    my $flags = shift || 0;
     return sub {
         my $cmd = shift;
@@ -578,7 +601,8 @@
 
         if(defined($alias)) {
-            $conn = get_connection_by_alias($alias);
-        }
-        if($use_channel) {
+            $conn = get_connection_by_alias($alias,
+                                            $flags & ALLOW_DISCONNECTED);
+        }
+        if($flags & CHANNEL_ARG) {
             $channel = $ARGV[0];
             if(defined($channel) && $channel =~ /^#/) {
@@ -594,10 +618,13 @@
         }
 
-        if(!$channel && $use_channel == REQUIRE_CHANNEL) {
+        if(!$channel &&
+           ($flags & CHANNEL_ARG) &&
+           !($flags & CHANNEL_OPTIONAL)) {
             die("Usage: $cmd <channel>\n");
         }
         if(!$conn) {
             if($m && $m->type eq 'IRC') {
-                $conn = get_connection_by_alias($m->network);
+                $conn = get_connection_by_alias($m->network,
+                                               $flags & ALLOW_DISCONNECTED);
             }
         }
@@ -608,5 +635,5 @@
             die("You must specify an IRC network using -a.\n");
         }
-        if($use_channel) {
+        if($flags & CHANNEL_ARG) {
             $sub->($cmd, $conn, $channel, @ARGV);
         } else {
@@ -618,6 +645,9 @@
 sub get_connection_by_alias {
     my $key = shift;
-    die("No such ircnet: $key\n") unless exists $ircnets{$key};
-    return $ircnets{$key};
+    my $allow_disconnected = shift;
+
+    return $ircnets{$key} if exists $ircnets{$key};
+    return $reconnect{$key} if $allow_disconnected && exists $reconnect{$key};
+    die("No such ircnet: $key\n")
 }
 
Index: perl/modules/IRC/lib/BarnOwl/Module/IRC/Connection.pm
===================================================================
--- perl/modules/IRC/lib/BarnOwl/Module/IRC/Connection.pm	(revision 618a9803a0d96a593db35163d5f984957a0b7316)
+++ perl/modules/IRC/lib/BarnOwl/Module/IRC/Connection.pm	(revision 416241f7c28243f9c5890da26c47ca93147f4414)
@@ -23,4 +23,5 @@
 
 use BarnOwl;
+use Scalar::Util qw(weaken);
 
 BEGIN {
@@ -315,12 +316,20 @@
     my $interval = shift || 5;
     delete $BarnOwl::Module::IRC::ircnets{$self->alias};
-    $BarnOwl::Module::IRC::reconnect{$self->alias} =
+    $BarnOwl::Module::IRC::reconnect{$self->alias} = $self;
+    my $weak = $self;
+    weaken($weak);
+    $self->{reconnect_timer} = 
         BarnOwl::Timer->new( {
             after => $interval,
             cb    => sub {
-                $self->reconnect( $interval );
+                $weak->reconnect( $interval ) if $weak;
             },
         } );
-    $BarnOwl::Module::IRC::reconnect{$self->alias}{conn} = $self;
+}
+
+sub cancel_reconnect {
+    my $self = shift;
+    delete $BarnOwl::Module::IRC::reconnect{$self->alias};
+    delete $self->{reconnect_timer};
 }
 
@@ -329,5 +338,5 @@
     my $msg = shift;
     BarnOwl::admin_message("IRC", $msg);
-    delete $BarnOwl::Module::IRC::reconnect{$self->alias};
+    $self->cancel_reconnect;
     $BarnOwl::Module::IRC::ircnets{$self->alias} = $self;
     my $fd = $self->getSocket()->fileno();
Index: perlconfig.c
===================================================================
--- perlconfig.c	(revision e3068de13716f360b58ba2e2cdc99f7b7e4b9a5d)
+++ perlconfig.c	(revision 901b9311d60876dabd9b3787c0d53cd68fcb77d3)
@@ -638,5 +638,5 @@
 
   if (SvTRUE(ERRSV)) {
-    owl_function_error("Error in calback: '%s'", SvPV_nolen(ERRSV));
+    owl_function_error("Error in callback: '%s'", SvPV_nolen(ERRSV));
     sv_setsv (ERRSV, &PL_sv_undef);
   }
Index: scripts/do-release
===================================================================
--- scripts/do-release	(revision 01846cebe79d07863da17547f59fd0d0eb97c1d2)
+++ scripts/do-release	(revision d771d1b92b8b63462925ff8f0ab7fc081ffd81b8)
@@ -31,4 +31,13 @@
 if [ ! "$force" ] && [ "$VERS" != "$(head -1 ChangeLog)" ]; then
     die "No ChangeLog entry for version $VERS, aborting."
+fi
+
+head=$(git symbolic-ref HEAD)
+head=${head#refs/heads/}
+
+git rev-parse --verify -q $head >/dev/null 2>&1
+git rev-parse --verify -q origin/$head >/dev/null 2>&1
+if [ -n "$(git rev-list $head..origin/$head)" ]; then
+    die "$head is not up to date. Aborting."
 fi
 
Index: t/completion.t
===================================================================
--- t/completion.t	(revision 1167bf1098ad511a9390361dd5b88a090b872377)
+++ t/completion.t	(revision e6cec013ec75e5ee143ad87023528b9b132d4e52)
@@ -289,4 +289,84 @@
               \&complete_filter_expr);
 
+# Test complete_files
+use BarnOwl::Completion::Util qw(complete_file);
+use File::Temp;
+use File::Path qw(mkpath);
+
+my $tmpdir = File::Temp::tempdir(CLEANUP => 1);
+
+# Make sure $tmpdir does not have a trailing /
+$tmpdir =~ s{/$}{};
+$ENV{HOME} = $tmpdir;
+
+sub touch {
+    my $path = shift;
+    system("touch", "$path");
+}
+
+mkpath("$tmpdir/.owl/",
+       "$tmpdir/.owl/modules/",
+       "$tmpdir/Public/",
+       "$tmpdir/Private/",
+       "$tmpdir/.ours",
+       "$tmpdir/www");
+touch("$tmpdir/.zephyr.subs");
+touch("$tmpdir/wheee");
+touch("$tmpdir/.owl/startup");
+
+sub completion_value {
+    my $c = shift;
+    return $c unless ref($c) eq 'ARRAY';
+    return $c->[1];
+}
+
+sub test_file {
+    my $spec  = shift;
+    my $pfx   = shift;
+    my $dirs  = shift;
+    my $files = shift;
+
+    my $expect = [ sort {$a->[1] cmp $b->[1]} 
+        ((map {["$_/", defined($pfx)?"$pfx/$_/":"$_/", 0]} @$dirs),
+         (map {["$_",  defined($pfx)?"$pfx/$_" :$_   , 1]} @$files))
+       ];
+
+    local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+    my @got = complete_file($spec);
+
+    @got = grep {completion_value($_) =~ m{^\Q$spec\E}} @got;
+    @got = sort {completion_value($a) cmp completion_value($b)} @got;
+
+    use Data::Dumper;
+    is_deeply(\@got, $expect);
+}
+
+is_deeply([complete_file("~")], [["~/", "~/", 0]]);
+
+chdir($tmpdir);
+test_file("$tmpdir/", $tmpdir,
+          [qw(Public Private www)],
+          [qw(wheee)]);
+
+test_file("./", ".",
+          [qw(Public Private www)],
+          [qw(wheee)]);
+
+test_file("", undef, [qw(Public Private www)], [qw(wheee)]);
+
+test_file("./.owl/", "./.owl",
+          [qw(modules)],
+          [qw(startup)]);
+
+test_file("~/", "~",
+          [qw(Public Private www)],
+          [qw(wheee)]);
+
+test_file("P", undef, [qw(Public Private)], []);
+
+test_file("$tmpdir/.", $tmpdir,
+          [qw(. .. .owl .ours)],
+          [qw(.zephyr.subs)]);
 1;
 
