use strict;
use IkiWiki;
use Encode;
+use URI::Escape q{uri_escape_utf8};
use open qw{:utf8 :std};
-sub import { #{{{
+sub import {
hook(type => "checkconfig", id => "mercurial", call => \&checkconfig);
hook(type => "getsetup", id => "mercurial", call => \&getsetup);
hook(type => "rcs", id => "rcs_update", call => \&rcs_update);
hook(type => "rcs", id => "rcs_recentchanges", call => \&rcs_recentchanges);
hook(type => "rcs", id => "rcs_diff", call => \&rcs_diff);
hook(type => "rcs", id => "rcs_getctime", call => \&rcs_getctime);
-} #}}}
+ hook(type => "rcs", id => "rcs_getmtime", call => \&rcs_getmtime);
+}
-sub checkconfig () { #{{{
- if (! defined $config{diffurl}) {
- $config{diffurl}="";
- }
+sub checkconfig () {
if (exists $config{mercurial_wrapper} && length $config{mercurial_wrapper}) {
push @{$config{wrappers}}, {
wrapper => $config{mercurial_wrapper},
wrappermode => (defined $config{mercurial_wrappermode} ? $config{mercurial_wrappermode} : "06755"),
};
}
-} #}}}
+}
-sub getsetup () { #{{{
+sub getsetup () {
return
+ plugin => {
+ safe => 0, # rcs plugin
+ rebuild => undef,
+ section => "rcs",
+ },
mercurial_wrapper => {
type => "string",
#example => # FIXME add example
- description => "mercurial post-commit executable to generate",
+ description => "mercurial post-commit hook to generate",
safe => 0, # file
rebuild => 0,
},
safe => 1,
rebuild => 1,
},
-} #}}}
+}
+
+sub safe_hg (&@) {
+ # Start a child process safely without resorting to /bin/sh.
+ # Returns command output (in list content) or success state
+ # (in scalar context), or runs the specified data handler.
+
+ my ($error_handler, $data_handler, @cmdline) = @_;
+
+ my $pid = open my $OUT, "-|";
+
+ error("Cannot fork: $!") if !defined $pid;
+
+ if (!$pid) {
+ # In child.
+ # hg commands want to be in wc.
+ chdir $config{srcdir}
+ or error("cannot chdir to $config{srcdir}: $!");
+
+ exec @cmdline or error("Cannot exec '@cmdline': $!");
+ }
+ # In parent.
+
+ my @lines;
+ while (<$OUT>) {
+ chomp;
+
+ if (! defined $data_handler) {
+ push @lines, $_;
+ }
+ else {
+ last unless $data_handler->($_);
+ }
+ }
+
+ close $OUT;
-sub mercurial_log ($) { #{{{
+ $error_handler->("'@cmdline' failed: $!") if $? && $error_handler;
+
+ return wantarray ? @lines : ($? == 0);
+}
+# Convenient wrappers.
+sub run_or_die ($@) { safe_hg(\&error, undef, @_) }
+sub run_or_cry ($@) { safe_hg(sub { warn @_ }, undef, @_) }
+sub run_or_non ($@) { safe_hg(undef, undef, @_) }
+
+sub mercurial_log ($) {
my $out = shift;
my @infos;
close $out;
return @infos;
-} #}}}
+}
-sub rcs_update () { #{{{
- my @cmdline = ("hg", "-q", "-R", "$config{srcdir}", "update");
- if (system(@cmdline) != 0) {
- warn "'@cmdline' failed: $!";
- }
-} #}}}
+sub rcs_update () {
+ run_or_cry('hg', '-q', 'update');
+}
-sub rcs_prepedit ($) { #{{{
+sub rcs_prepedit ($) {
return "";
-} #}}}
+}
-sub rcs_commit ($$$;$$) { #{{{
- my ($file, $message, $rcstoken, $user, $ipaddr) = @_;
+sub rcs_commit (@) {
+ my %params=@_;
- if (defined $user) {
- $user = IkiWiki::possibly_foolish_untaint($user);
- }
- elsif (defined $ipaddr) {
- $user = "Anonymous from ".IkiWiki::possibly_foolish_untaint($ipaddr);
- }
- else {
- $user = "Anonymous";
+ return rcs_commit_helper(@_);
+}
+
+sub rcs_commit_helper (@) {
+ my %params=@_;
+
+ my %env=%ENV;
+ $ENV{HGENCODING} = 'utf-8';
+
+ my $user="Anonymous";
+ if (defined $params{session}) {
+ if (defined $params{session}->param("name")) {
+ $user = $params{session}->param("name");
+ }
+ elsif (defined $params{session}->remote_addr()) {
+ $user = $params{session}->remote_addr();
+ }
+
+ my $nickname=$user;
+ if (defined $params{session}->param("nickname")) {
+ $nickname=encode_utf8($params{session}->param("nickname"));
+ $nickname=~s/\s+/_/g;
+ $nickname=~s/[^-_0-9[:alnum:]]+//g;
+ }
+ $ENV{HGUSER} = encode_utf8($user . ' <' . $nickname . '@web>');
}
- $message = IkiWiki::possibly_foolish_untaint($message);
- if (! length $message) {
- $message = "no message given";
+ if (! length $params{message}) {
+ $params{message} = "no message given";
}
- my @cmdline = ("hg", "-q", "-R", $config{srcdir}, "commit",
- "-m", $message, "-u", $user);
- if (system(@cmdline) != 0) {
- warn "'@cmdline' failed: $!";
+ $params{message} = IkiWiki::possibly_foolish_untaint($params{message});
+
+ my @opts;
+
+ if (exists $params{file}) {
+ push @opts, '--', $params{file};
}
+ # hg commit returns non-zero if nothing really changed.
+ # So we should ignore its exit status (hence run_or_non).
+ run_or_non('hg', 'commit', '-m', $params{message}, '-q', @opts);
+ %ENV=%env;
return undef; # success
-} #}}}
+}
-sub rcs_commit_staged ($$$) {
+sub rcs_commit_staged (@) {
# Commits all staged changes. Changes can be staged using rcs_add,
# rcs_remove, and rcs_rename.
- my ($message, $user, $ipaddr)=@_;
-
- error("rcs_commit_staged not implemented for mercurial"); # TODO
+ return rcs_commit_helper(@_);
}
-sub rcs_add ($) { # {{{
+sub rcs_add ($) {
my ($file) = @_;
- my @cmdline = ("hg", "-q", "-R", "$config{srcdir}", "add", "$config{srcdir}/$file");
- if (system(@cmdline) != 0) {
- warn "'@cmdline' failed: $!";
- }
-} #}}}
+ run_or_cry('hg', 'add', $file);
+}
-sub rcs_remove ($) { # {{{
+sub rcs_remove ($) {
+ # Remove file from archive.
my ($file) = @_;
- error("rcs_remove not implemented for mercurial"); # TODO
-} #}}}
+ run_or_cry('hg', 'remove', '-f', $file);
+}
-sub rcs_rename ($$) { # {{{
+sub rcs_rename ($$) {
my ($src, $dest) = @_;
- error("rcs_rename not implemented for mercurial"); # TODO
-} #}}}
+ run_or_cry('hg', 'rename', '-f', $src, $dest);
+}
-sub rcs_recentchanges ($) { #{{{
+sub rcs_recentchanges ($) {
my ($num) = @_;
+ my %env=%ENV;
+ $ENV{HGENCODING} = 'utf-8';
+
my @cmdline = ("hg", "-R", $config{srcdir}, "log", "-v", "-l", $num,
"--style", "default");
open (my $out, "@cmdline |");
foreach my $info (mercurial_log($out)) {
my @pages = ();
my @message = ();
-
+
foreach my $msgline (split(/\n/, $info->{description})) {
push @message, { line => $msgline };
}
foreach my $file (split / /,$info->{files}) {
- my $diffurl = $config{'diffurl'};
- $diffurl =~ s/\[\[file\]\]/$file/go;
+ my $diffurl = defined $config{diffurl} ? $config{'diffurl'} : "";
+ my $efile = uri_escape_utf8($file);
+ $diffurl =~ s/\[\[file\]\]/$efile/go;
$diffurl =~ s/\[\[r2\]\]/$info->{changeset}/go;
push @pages, {
};
}
+ #"user <email@domain.net>": parse out "user".
my $user = $info->{"user"};
$user =~ s/\s*<.*>\s*$//;
$user =~ s/^\s*//;
+ #"user <nickname@web>": if "@web" hits, set $web_commit=true.
+ my $web_commit = ($info->{'user'} =~ /\@web>/);
+
+ #"user <nickname@web>": if user is a URL (hits "://") and "@web"
+ #was present, parse out nick.
+ my $nickname;
+ if ($user =~ /:\/\// && $web_commit) {
+ $nickname = $info->{'user'};
+ $nickname =~ s/^[^<]*<([^\@]+)\@web>\s*$/$1/;
+ }
+
push @ret, {
rev => $info->{"changeset"},
user => $user,
- committype => "mercurial",
+ nickname => $nickname,
+ committype => $web_commit ? "web" : "hg",
when => str2time($info->{"date"}),
message => [@message],
pages => [@pages],
};
}
+ %ENV=%env;
+
return @ret;
-} #}}}
+}
+
+sub rcs_diff ($;$) {
+ my $rev=shift;
+ my $maxlines=shift;
+ my @lines;
+ my $addlines=sub {
+ my $line=shift;
+ return if defined $maxlines && @lines == $maxlines;
+ push @lines, $line."\n"
+ if (@lines || $line=~/^diff --git/);
+ return 1;
+ };
+ safe_hg(undef, $addlines, "hg", "diff", "-c", $rev, "-g");
+ if (wantarray) {
+ return @lines;
+ }
+ else {
+ return join("", @lines);
+ }
+}
-sub rcs_diff ($) { #{{{
- # TODO
-} #}}}
+{
+my %time_cache;
+
+sub findtimes ($$) {
+ my $file=shift;
+ my $id=shift; # 0 = mtime ; 1 = ctime
+
+ if (! keys %time_cache) {
+ my $date;
+
+ # It doesn't seem possible to specify the format wanted for the
+ # changelog (same format as is generated in git.pm:findtimes(),
+ # though the date differs slightly) without using a style
+ # _file_. There is a "hg log" switch "--template" to directly
+ # control simple output formatting, but in this case, the
+ # {file} directive must be redefined, which can only be done
+ # with "--style".
+ #
+ # If {file} is not redefined, all files are output on a single
+ # line separated with a space. It is not possible to conclude
+ # if the space is part of a filename or just a separator, and
+ # thus impossible to use in this case.
+ #
+ # Some output filters are available in hg, but they are not fit
+ # for this cause (and would slow down the process
+ # unnecessarily).
+
+ eval q{use File::Temp};
+ error $@ if $@;
+ my ($tmpl_fh, $tmpl_filename) = File::Temp::tempfile(UNLINK => 1);
+
+ print $tmpl_fh 'changeset = "{date}\\n{files}\\n"' . "\n";
+ print $tmpl_fh 'file = "{file}\\n"' . "\n";
+
+ foreach my $line (run_or_die('hg', 'log', '--style', $tmpl_filename)) {
+ if (! defined $date && $line =~ /^(\d+)/) {
+ $date=$1;
+ }
+ elsif (! length $line) {
+ $date=undef;
+ }
+ else {
+ my $f=$line;
-sub rcs_getctime ($) { #{{{
- my ($file) = @_;
+ if (! $time_cache{$f}) {
+ $time_cache{$f}[0]=$date; # mtime
+ }
+ $time_cache{$f}[1]=$date; # ctime
+ }
+ }
+ }
- # XXX filename passes through the shell here, should try to avoid
- # that just in case
- my @cmdline = ("hg", "-R", $config{srcdir}, "log", "-v", "-l", '1',
- "--style", "default", "$config{srcdir}/$file");
- open (my $out, "@cmdline |");
+ return exists $time_cache{$file} ? $time_cache{$file}[$id] : 0;
+}
- my @log = mercurial_log($out);
+}
- if (length @log < 1) {
- return 0;
- }
+sub rcs_getctime ($) {
+ my $file = shift;
- eval q{use Date::Parse};
- error($@) if $@;
-
- my $ctime = str2time($log[0]->{"date"});
- return $ctime;
-} #}}}
+ return findtimes($file, 1);
+}
+
+sub rcs_getmtime ($) {
+ my $file = shift;
+
+ return findtimes($file, 0);
+}
1