2 package IkiWiki::Plugin::mercurial;
8 use open qw{:utf8 :std};
11 hook(type => "checkconfig", id => "mercurial", call => \&checkconfig);
12 hook(type => "getsetup", id => "mercurial", call => \&getsetup);
13 hook(type => "rcs", id => "rcs_update", call => \&rcs_update);
14 hook(type => "rcs", id => "rcs_prepedit", call => \&rcs_prepedit);
15 hook(type => "rcs", id => "rcs_commit", call => \&rcs_commit);
16 hook(type => "rcs", id => "rcs_commit_staged", call => \&rcs_commit_staged);
17 hook(type => "rcs", id => "rcs_add", call => \&rcs_add);
18 hook(type => "rcs", id => "rcs_remove", call => \&rcs_remove);
19 hook(type => "rcs", id => "rcs_rename", call => \&rcs_rename);
20 hook(type => "rcs", id => "rcs_recentchanges", call => \&rcs_recentchanges);
21 hook(type => "rcs", id => "rcs_diff", call => \&rcs_diff);
22 hook(type => "rcs", id => "rcs_getctime", call => \&rcs_getctime);
23 hook(type => "rcs", id => "rcs_getmtime", call => \&rcs_getmtime);
27 if (exists $config{mercurial_wrapper} && length $config{mercurial_wrapper}) {
28 push @{$config{wrappers}}, {
29 wrapper => $config{mercurial_wrapper},
30 wrappermode => (defined $config{mercurial_wrappermode} ? $config{mercurial_wrappermode} : "06755"),
38 safe => 0, # rcs plugin
42 mercurial_wrapper => {
44 #example => # FIXME add example
45 description => "mercurial post-commit hook to generate",
49 mercurial_wrappermode => {
52 description => "mode for mercurial_wrapper (can safely be made suid)",
58 example => "http://example.com:8000/log/tip/[[file]]",
59 description => "url to hg serve'd repository, to show file history ([[file]] substituted)",
65 example => "http://localhost:8000/?fd=[[r2]];file=[[file]]",
66 description => "url to hg serve'd repository, to show diff ([[file]] and [[r2]] substituted)",
73 # Start a child process safely without resorting to /bin/sh.
74 # Returns command output (in list content) or success state
75 # (in scalar context), or runs the specified data handler.
77 my ($error_handler, $data_handler, @cmdline) = @_;
79 my $pid = open my $OUT, "-|";
81 error("Cannot fork: $!") if !defined $pid;
85 # hg commands want to be in wc.
87 or error("cannot chdir to $config{srcdir}: $!");
89 exec @cmdline or error("Cannot exec '@cmdline': $!");
97 if (! defined $data_handler) {
101 last unless $data_handler->($_);
107 $error_handler->("'@cmdline' failed: $!") if $? && $error_handler;
109 return wantarray ? @lines : ($? == 0);
111 # Convenient wrappers.
112 sub run_or_die ($@) { safe_hg(\&error, undef, @_) }
113 sub run_or_cry ($@) { safe_hg(sub { warn @_ }, undef, @_) }
114 sub run_or_non ($@) { safe_hg(undef, undef, @_) }
116 sub mercurial_log ($) {
124 if (/^description:/) {
125 $key = "description";
128 # slurp everything as the description text
129 # until the next changeset
131 if (/^changeset: /) {
141 $infos[$#infos]{$key} = $value;
145 ($key, $value) = split /: +/, $line, 2;
147 if ($key eq "changeset") {
150 # remove the revision index, which is strictly
151 # local to the repository
155 $infos[$#infos]{$key} = $value;
163 run_or_cry('hg', '-q', 'update');
166 sub rcs_prepedit ($) {
173 return rcs_commit_helper(@_);
176 sub rcs_commit_helper (@) {
180 $ENV{HGENCODING} = 'utf-8';
182 my $user="Anonymous";
183 if (defined $params{session}) {
184 if (defined $params{session}->param("name")) {
185 $user = $params{session}->param("name");
187 elsif (defined $params{session}->remote_addr()) {
188 $user = $params{session}->remote_addr();
192 if (defined $params{session}->param("nickname")) {
193 $nickname=encode_utf8($params{session}->param("nickname"));
194 $nickname=~s/\s+/_/g;
195 $nickname=~s/[^-_0-9[:alnum:]]+//g;
197 $ENV{HGUSER} = encode_utf8($user . ' <' . $nickname . '@web>');
200 if (! length $params{message}) {
201 $params{message} = "no message given";
204 $params{message} = IkiWiki::possibly_foolish_untaint($params{message});
208 if (exists $params{file}) {
209 push @opts, '--', $params{file};
211 # hg commit returns non-zero if nothing really changed.
212 # So we should ignore its exit status (hence run_or_non).
213 run_or_non('hg', 'commit', '-m', $params{message}, '-q', @opts);
216 return undef; # success
219 sub rcs_commit_staged (@) {
220 # Commits all staged changes. Changes can be staged using rcs_add,
221 # rcs_remove, and rcs_rename.
222 return rcs_commit_helper(@_);
228 run_or_cry('hg', 'add', $file);
232 # Remove file from archive.
235 run_or_cry('hg', 'remove', '-f', $file);
238 sub rcs_rename ($$) {
239 my ($src, $dest) = @_;
241 run_or_cry('hg', 'rename', '-f', $src, $dest);
244 sub rcs_recentchanges ($) {
248 $ENV{HGENCODING} = 'utf-8';
250 my @cmdline = ("hg", "-R", $config{srcdir}, "log", "-v", "-l", $num,
251 "--style", "default");
252 open (my $out, "@cmdline |");
254 eval q{use Date::Parse};
258 foreach my $info (mercurial_log($out)) {
262 foreach my $msgline (split(/\n/, $info->{description})) {
263 push @message, { line => $msgline };
266 foreach my $file (split / /,$info->{files}) {
267 my $diffurl = defined $config{diffurl} ? $config{'diffurl'} : "";
268 $diffurl =~ s/\[\[file\]\]/$file/go;
269 $diffurl =~ s/\[\[r2\]\]/$info->{changeset}/go;
272 page => pagename($file),
277 #"user <email@domain.net>": parse out "user".
278 my $user = $info->{"user"};
279 $user =~ s/\s*<.*>\s*$//;
282 #"user <nickname@web>": if "@web" hits, set $web_commit=true.
283 my $web_commit = ($info->{'user'} =~ /\@web>/);
285 #"user <nickname@web>": if user is a URL (hits "://") and "@web"
286 #was present, parse out nick.
288 if ($user =~ /:\/\// && $web_commit) {
289 $nickname = $info->{'user'};
290 $nickname =~ s/^[^<]*<([^\@]+)\@web>\s*$/$1/;
294 rev => $info->{"changeset"},
296 nickname => $nickname,
297 committype => $web_commit ? "web" : "hg",
298 when => str2time($info->{"date"}),
299 message => [@message],
315 return if defined $maxlines && @lines == $maxlines;
316 push @lines, $line."\n"
317 if (@lines || $line=~/^diff --git/);
320 safe_hg(undef, $addlines, "hg", "diff", "-c", $rev, "-g");
325 return join("", @lines);
334 my $id=shift; # 0 = mtime ; 1 = ctime
336 if (! keys %time_cache) {
339 # It doesn't seem possible to specify the format wanted for the
340 # changelog (same format as is generated in git.pm:findtimes(),
341 # though the date differs slightly) without using a style
342 # _file_. There is a "hg log" switch "--template" to directly
343 # control simple output formatting, but in this case, the
344 # {file} directive must be redefined, which can only be done
347 # If {file} is not redefined, all files are output on a single
348 # line separated with a space. It is not possible to conclude
349 # if the space is part of a filename or just a separator, and
350 # thus impossible to use in this case.
352 # Some output filters are available in hg, but they are not fit
353 # for this cause (and would slow down the process
356 eval q{use File::Temp};
358 my ($tmpl_fh, $tmpl_filename) = File::Temp::tempfile(UNLINK => 1);
360 print $tmpl_fh 'changeset = "{date}\\n{files}\\n"' . "\n";
361 print $tmpl_fh 'file = "{file}\\n"' . "\n";
363 foreach my $line (run_or_die('hg', 'log', '--style', $tmpl_filename)) {
364 if (! defined $date && $line =~ /^(\d+)/) {
367 elsif (! length $line) {
373 if (! $time_cache{$f}) {
374 $time_cache{$f}[0]=$date; # mtime
376 $time_cache{$f}[1]=$date; # ctime
381 return exists $time_cache{$file} ? $time_cache{$file}[$id] : 0;
386 sub rcs_getctime ($) {
389 return findtimes($file, 1);
392 sub rcs_getmtime ($) {
395 return findtimes($file, 0);