2 package IkiWiki::Plugin::mercurial;
8 use URI::Escape q{uri_escape_utf8};
9 use open qw{:utf8 :std};
12 hook(type => "checkconfig", id => "mercurial", call => \&checkconfig);
13 hook(type => "getsetup", id => "mercurial", call => \&getsetup);
14 hook(type => "rcs", id => "rcs_update", call => \&rcs_update);
15 hook(type => "rcs", id => "rcs_prepedit", call => \&rcs_prepedit);
16 hook(type => "rcs", id => "rcs_commit", call => \&rcs_commit);
17 hook(type => "rcs", id => "rcs_commit_staged", call => \&rcs_commit_staged);
18 hook(type => "rcs", id => "rcs_add", call => \&rcs_add);
19 hook(type => "rcs", id => "rcs_remove", call => \&rcs_remove);
20 hook(type => "rcs", id => "rcs_rename", call => \&rcs_rename);
21 hook(type => "rcs", id => "rcs_recentchanges", call => \&rcs_recentchanges);
22 hook(type => "rcs", id => "rcs_diff", call => \&rcs_diff);
23 hook(type => "rcs", id => "rcs_getctime", call => \&rcs_getctime);
24 hook(type => "rcs", id => "rcs_getmtime", call => \&rcs_getmtime);
28 if (exists $config{mercurial_wrapper} && length $config{mercurial_wrapper}) {
29 push @{$config{wrappers}}, {
30 wrapper => $config{mercurial_wrapper},
31 wrappermode => (defined $config{mercurial_wrappermode} ? $config{mercurial_wrappermode} : "06755"),
39 safe => 0, # rcs plugin
43 mercurial_wrapper => {
45 #example => # FIXME add example
46 description => "mercurial post-commit hook to generate",
50 mercurial_wrappermode => {
53 description => "mode for mercurial_wrapper (can safely be made suid)",
59 example => "http://example.com:8000/log/tip/[[file]]",
60 description => "url to hg serve'd repository, to show file history ([[file]] substituted)",
66 example => "http://localhost:8000/?fd=[[r2]];file=[[file]]",
67 description => "url to hg serve'd repository, to show diff ([[file]] and [[r2]] substituted)",
74 # Start a child process safely without resorting to /bin/sh.
75 # Returns command output (in list content) or success state
76 # (in scalar context), or runs the specified data handler.
78 my ($error_handler, $data_handler, @cmdline) = @_;
80 my $pid = open my $OUT, "-|";
82 error("Cannot fork: $!") if !defined $pid;
86 # hg commands want to be in wc.
88 or error("cannot chdir to $config{srcdir}: $!");
90 exec @cmdline or error("Cannot exec '@cmdline': $!");
98 if (! defined $data_handler) {
102 last unless $data_handler->($_);
108 $error_handler->("'@cmdline' failed: $!") if $? && $error_handler;
110 return wantarray ? @lines : ($? == 0);
112 # Convenient wrappers.
113 sub run_or_die ($@) { safe_hg(\&error, undef, @_) }
114 sub run_or_cry ($@) { safe_hg(sub { warn @_ }, undef, @_) }
115 sub run_or_non ($@) { safe_hg(undef, undef, @_) }
117 sub mercurial_log ($) {
125 if (/^description:/) {
126 $key = "description";
129 # slurp everything as the description text
130 # until the next changeset
132 if (/^changeset: /) {
142 $infos[$#infos]{$key} = $value;
146 ($key, $value) = split /: +/, $line, 2;
148 if ($key eq "changeset") {
151 # remove the revision index, which is strictly
152 # local to the repository
156 $infos[$#infos]{$key} = $value;
164 run_or_cry('hg', '-q', 'update');
167 sub rcs_prepedit ($) {
174 return rcs_commit_helper(@_);
177 sub rcs_commit_helper (@) {
181 $ENV{HGENCODING} = 'utf-8';
183 my $user="Anonymous";
184 if (defined $params{session}) {
185 if (defined $params{session}->param("name")) {
186 $user = IkiWiki::cloak($params{session}->param("name"));
188 elsif (defined $params{session}->remote_addr()) {
189 $user = IkiWiki::cloak($params{session}->remote_addr());
193 if (defined $params{session}->param("nickname")) {
194 $nickname=encode_utf8($params{session}->param("nickname"));
195 $nickname=~s/\s+/_/g;
196 $nickname=~s/[^-_0-9[:alnum:]]+//g;
198 $ENV{HGUSER} = encode_utf8($user . ' <' . $nickname . '@web>');
201 if (! length $params{message}) {
202 $params{message} = "no message given";
205 $params{message} = IkiWiki::possibly_foolish_untaint($params{message});
209 if (exists $params{file}) {
210 push @opts, '--', $params{file};
212 # hg commit returns non-zero if nothing really changed.
213 # So we should ignore its exit status (hence run_or_non).
214 run_or_non('hg', 'commit', '-m', $params{message}, '-q', @opts);
217 return undef; # success
220 sub rcs_commit_staged (@) {
221 # Commits all staged changes. Changes can be staged using rcs_add,
222 # rcs_remove, and rcs_rename.
223 return rcs_commit_helper(@_);
229 run_or_cry('hg', 'add', $file);
233 # Remove file from archive.
236 run_or_cry('hg', 'remove', '-f', $file);
239 sub rcs_rename ($$) {
240 my ($src, $dest) = @_;
242 run_or_cry('hg', 'rename', '-f', $src, $dest);
245 sub rcs_recentchanges ($) {
249 $ENV{HGENCODING} = 'utf-8';
251 my @cmdline = ("hg", "-R", $config{srcdir}, "log", "-v", "-l", $num,
252 "--style", "default");
253 open (my $out, "@cmdline |");
255 eval q{use Date::Parse};
259 foreach my $info (mercurial_log($out)) {
263 foreach my $msgline (split(/\n/, $info->{description})) {
264 push @message, { line => $msgline };
267 foreach my $file (split / /,$info->{files}) {
268 my $diffurl = defined $config{diffurl} ? $config{'diffurl'} : "";
269 my $efile = uri_escape_utf8($file);
270 $diffurl =~ s/\[\[file\]\]/$efile/go;
271 $diffurl =~ s/\[\[r2\]\]/$info->{changeset}/go;
274 page => pagename($file),
279 #"user <email@domain.net>": parse out "user".
280 my $user = $info->{"user"};
281 $user =~ s/\s*<.*>\s*$//;
284 #"user <nickname@web>": if "@web" hits, set $web_commit=true.
285 my $web_commit = ($info->{'user'} =~ /\@web>/);
287 #"user <nickname@web>": if user is a URL (hits "://") and "@web"
288 #was present, parse out nick.
290 if ($user =~ /:\/\// && $web_commit) {
291 $nickname = $info->{'user'};
292 $nickname =~ s/^[^<]*<([^\@]+)\@web>\s*$/$1/;
296 rev => $info->{"changeset"},
298 nickname => $nickname,
299 committype => $web_commit ? "web" : "hg",
300 when => str2time($info->{"date"}),
301 message => [@message],
317 return if defined $maxlines && @lines == $maxlines;
318 push @lines, $line."\n"
319 if (@lines || $line=~/^diff --git/);
322 safe_hg(undef, $addlines, "hg", "diff", "-c", $rev, "-g");
327 return join("", @lines);
336 my $id=shift; # 0 = mtime ; 1 = ctime
338 if (! keys %time_cache) {
341 # It doesn't seem possible to specify the format wanted for the
342 # changelog (same format as is generated in git.pm:findtimes(),
343 # though the date differs slightly) without using a style
344 # _file_. There is a "hg log" switch "--template" to directly
345 # control simple output formatting, but in this case, the
346 # {file} directive must be redefined, which can only be done
349 # If {file} is not redefined, all files are output on a single
350 # line separated with a space. It is not possible to conclude
351 # if the space is part of a filename or just a separator, and
352 # thus impossible to use in this case.
354 # Some output filters are available in hg, but they are not fit
355 # for this cause (and would slow down the process
358 eval q{use File::Temp};
360 my ($tmpl_fh, $tmpl_filename) = File::Temp::tempfile(UNLINK => 1);
362 print $tmpl_fh 'changeset = "{date}\\n{files}\\n"' . "\n";
363 print $tmpl_fh 'file = "{file}\\n"' . "\n";
365 foreach my $line (run_or_die('hg', 'log', '--style', $tmpl_filename)) {
366 if (! defined $date && $line =~ /^(\d+)/) {
369 elsif (! length $line) {
375 if (! $time_cache{$f}) {
376 $time_cache{$f}[0]=$date; # mtime
378 $time_cache{$f}[1]=$date; # ctime
383 return exists $time_cache{$file} ? $time_cache{$file}[$id] : 0;
388 sub rcs_getctime ($) {
391 return findtimes($file, 1);
394 sub rcs_getmtime ($) {
397 return findtimes($file, 0);