X-Git-Url: https://sipb.mit.edu/gitweb.cgi/ikiwiki.git/blobdiff_plain/f41448d9caf591ba369bf319297d15552be94678..7aa10a2952a0d0ee8543f575966f7e4a780c993c:/IkiWiki/Plugin/mercurial.pm diff --git a/IkiWiki/Plugin/mercurial.pm b/IkiWiki/Plugin/mercurial.pm index d2c34fa6a..8da4ceb07 100644 --- a/IkiWiki/Plugin/mercurial.pm +++ b/IkiWiki/Plugin/mercurial.pm @@ -5,9 +5,10 @@ use warnings; 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); @@ -20,23 +21,29 @@ sub import { #{{{ 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 () { #{{{ +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, }, @@ -61,9 +68,53 @@ sub getsetup () { #{{{ 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; + + $error_handler->("'@cmdline' failed: $!") if $? && $error_handler; -sub mercurial_log ($) { #{{{ + 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; @@ -107,78 +158,96 @@ sub mercurial_log ($) { #{{{ 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 |"); @@ -190,14 +259,15 @@ sub rcs_recentchanges ($) { #{{{ 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 = defined $config{diffurl} ? $config{'diffurl'} : ""; - $diffurl =~ s/\[\[file\]\]/$file/go; + my $efile = uri_escape_utf8($file); + $diffurl =~ s/\[\[file\]\]/$efile/go; $diffurl =~ s/\[\[r2\]\]/$info->{changeset}/go; push @pages, { @@ -206,47 +276,125 @@ sub rcs_recentchanges ($) { #{{{ }; } + #"user ": parse out "user". my $user = $info->{"user"}; $user =~ s/\s*<.*>\s*$//; $user =~ s/^\s*//; + #"user ": if "@web" hits, set $web_commit=true. + my $web_commit = ($info->{'user'} =~ /\@web>/); + + #"user ": 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 ($) { #{{{ - # TODO -} #}}} +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_getctime ($) { #{{{ - my ($file) = @_; +{ +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; - # 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 |"); + if (! $time_cache{$f}) { + $time_cache{$f}[0]=$date; # mtime + } + $time_cache{$f}[1]=$date; # ctime + } + } + } - my @log = mercurial_log($out); + return exists $time_cache{$file} ? $time_cache{$file}[$id] : 0; +} - if (length @log < 1) { - return 0; - } +} - eval q{use Date::Parse}; - error($@) if $@; - - my $ctime = str2time($log[0]->{"date"}); - return $ctime; -} #}}} +sub rcs_getctime ($) { + my $file = shift; + + return findtimes($file, 1); +} + +sub rcs_getmtime ($) { + my $file = shift; + + return findtimes($file, 0); +} 1