apply the big mercurial patch
[ikiwiki.git] / IkiWiki / Plugin / mercurial.pm
1 #!/usr/bin/perl
2 package IkiWiki::Plugin::mercurial;
3
4 use warnings;
5 use strict;
6 use IkiWiki;
7 use Encode;
8 use open qw{:utf8 :std};
9
10 sub import {
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);
24 }
25
26 sub checkconfig () {
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"),
31                 };
32         }
33 }
34
35 sub getsetup () {
36         return
37                 plugin => {
38                         safe => 0, # rcs plugin
39                         rebuild => undef,
40                         section => "rcs",
41                 },
42                 mercurial_wrapper => {
43                         type => "string",
44                         #example => # FIXME add example
45                         description => "mercurial post-commit hook to generate",
46                         safe => 0, # file
47                         rebuild => 0,
48                 },
49                 mercurial_wrappermode => {
50                         type => "string",
51                         example => '06755',
52                         description => "mode for mercurial_wrapper (can safely be made suid)",
53                         safe => 0,
54                         rebuild => 0,
55                 },
56                 historyurl => {
57                         type => "string",
58                         example => "http://example.com:8000/log/tip/[[file]]",
59                         description => "url to hg serve'd repository, to show file history ([[file]] substituted)",
60                         safe => 1,
61                         rebuild => 1,
62                 },
63                 diffurl => {
64                         type => "string",
65                         example => "http://localhost:8000/?fd=[[r2]];file=[[file]]",
66                         description => "url to hg serve'd repository, to show diff ([[file]] and [[r2]] substituted)",
67                         safe => 1,
68                         rebuild => 1,
69                 },
70 }
71
72 sub safe_hg (&@) {
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.
76
77         my ($error_handler, $data_handler, @cmdline) = @_;
78
79         my $pid = open my $OUT, "-|";
80
81         error("Cannot fork: $!") if !defined $pid;
82
83         if (!$pid) {
84                 # In child.
85                 # hg commands want to be in wc.
86                 chdir $config{srcdir}
87                     or error("cannot chdir to $config{srcdir}: $!");
88
89                 exec @cmdline or error("Cannot exec '@cmdline': $!");
90         }
91         # In parent.
92
93         my @lines;
94         while (<$OUT>) {
95                 chomp;
96
97                 if (! defined $data_handler) {
98                         push @lines, $_;
99                 }
100                 else {
101                         last unless $data_handler->($_);
102                 }
103         }
104
105         close $OUT;
106
107         $error_handler->("'@cmdline' failed: $!") if $? && $error_handler;
108
109         return wantarray ? @lines : ($? == 0);
110 }
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, @_) }
115
116 sub mercurial_log ($) {
117         my $out = shift;
118         my @infos;
119
120         while (<$out>) {
121                 my $line = $_;
122                 my ($key, $value);
123
124                 if (/^description:/) {
125                         $key = "description";
126                         $value = "";
127
128                         # slurp everything as the description text 
129                         # until the next changeset
130                         while (<$out>) {
131                                 if (/^changeset: /) {
132                                         $line = $_;
133                                         last;
134                                 }
135
136                                 $value .= $_;
137                         }
138
139                         local $/ = "";
140                         chomp $value;
141                         $infos[$#infos]{$key} = $value;
142                 }
143
144                 chomp $line;
145                 ($key, $value) = split /: +/, $line, 2;
146
147                 if ($key eq "changeset") {
148                         push @infos, {};
149
150                         # remove the revision index, which is strictly 
151                         # local to the repository
152                         $value =~ s/^\d+://;
153                 }
154
155                 $infos[$#infos]{$key} = $value;
156         }
157         close $out;
158
159         return @infos;
160 }
161
162 sub rcs_update () {
163         run_or_cry('hg', '-q', 'update');
164 }
165
166 sub rcs_prepedit ($) {
167         return "";
168 }
169
170 sub rcs_commit (@) {
171         my %params=@_;
172
173         return rcs_commit_helper(@_);
174 }
175
176 sub rcs_commit_helper (@) {
177         my %params=@_;
178
179         my %env=%ENV;
180         $ENV{HGENCODING} = 'utf-8';
181
182         my $user="Anonymous";
183         my $nickname;
184         if (defined $params{session}) {
185                 if (defined $params{session}->param("name")) {
186                         $user = $params{session}->param("name");
187                 }
188                 elsif (defined $params{session}->remote_addr()) {
189                         $user = $params{session}->remote_addr();
190                 }
191
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;
196                 }
197                 $ENV{HGUSER} = encode_utf8($user . ' <' . $nickname . '@web>');
198         }
199
200         if (! length $params{message}) {
201                 $params{message} = "no message given";
202         }
203
204         $params{message} = IkiWiki::possibly_foolish_untaint($params{message});
205
206         my @opts;
207
208         if (exists $params{file}) {
209                 push @opts, '--', $params{file};
210         }
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);
214
215         %ENV=%env;
216         return undef; # success
217 }
218
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(@_);
223 }
224
225 sub rcs_add ($) {
226         my ($file) = @_;
227
228         run_or_cry('hg', 'add', $file);
229 }
230
231 sub rcs_remove ($) {
232         # Remove file from archive.
233         my ($file) = @_;
234
235         run_or_cry('hg', 'remove', '-f', $file);
236 }
237
238 sub rcs_rename ($$) {
239         my ($src, $dest) = @_;
240
241         run_or_cry('hg', 'rename', '-f', $src, $dest);
242 }
243
244 sub rcs_recentchanges ($) {
245         my ($num) = @_;
246
247         my %env=%ENV;
248         $ENV{HGENCODING} = 'utf-8';
249
250         my @cmdline = ("hg", "-R", $config{srcdir}, "log", "-v", "-l", $num,
251                 "--style", "default");
252         open (my $out, "@cmdline |");
253
254         eval q{use Date::Parse};
255         error($@) if $@;
256
257         my @ret;
258         foreach my $info (mercurial_log($out)) {
259                 my @pages = ();
260                 my @message = ();
261         
262                 foreach my $msgline (split(/\n/, $info->{description})) {
263                         push @message, { line => $msgline };
264                 }
265
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;
270
271                         push @pages, {
272                                 page => pagename($file),
273                                 diffurl => $diffurl,
274                         };
275                 }
276
277                 #"user <email@domain.net>": parse out "user".
278                 my $user = $info->{"user"};
279                 $user =~ s/\s*<.*>\s*$//;
280                 $user =~ s/^\s*//;
281
282                 #"user <nickname@web>": if "@web" hits, set $web_commit=true.
283                 my $web_commit = ($info->{'user'} =~ /\@web>/);
284
285                 #"user <nickname@web>": if user is a URL (hits "://") and "@web"
286                 #was present, parse out nick.
287                 my $nickname;
288                 if ($user =~ /:\/\// && $web_commit) {
289                         $nickname = $info->{'user'};
290                         $nickname =~ s/^[^<]*<([^\@]+)\@web>\s*$/$1/;
291                 }
292
293                 push @ret, {
294                         rev        => $info->{"changeset"},
295                         user       => $user,
296                         nickname   => $nickname,
297                         committype => $web_commit ? "web" : "hg",
298                         when       => str2time($info->{"date"}),
299                         message    => [@message],
300                         pages      => [@pages],
301                 };
302         }
303
304         %ENV=%env;
305
306         return @ret;
307 }
308
309 sub rcs_diff ($;$) {
310         # TODO
311 }
312
313 sub rcs_getctime ($) {
314         my ($file) = @_;
315
316         my @cmdline = ("hg", "-R", $config{srcdir}, "log", "-v",
317                 "--style", "default", "$config{srcdir}/$file");
318         open (my $out, "-|", @cmdline);
319
320         my @log = (mercurial_log($out));
321
322         if (@log < 1) {
323                 return 0;
324         }
325
326         eval q{use Date::Parse};
327         error($@) if $@;
328         
329         my $ctime = str2time($log[$#log]->{"date"});
330         return $ctime;
331 }
332
333 sub rcs_getmtime ($) {
334         error "rcs_getmtime is not implemented for mercurial\n"; # TODO
335 }
336
337 1