1 I submitted some changes that added 5 "Yes"es and 2 "Fast"s to Mercurial at [[/rcs]], but some functionality is still missing as compared to e.g. `git.pm`, with which it should be able to be equivalent.
3 To do this, a more basic rewrite would simplify things. I inline the complete file below with comments. I don't expect anyone to take the time to read it all at once, but I'd be glad if those interested in the Mercurial backend could do some beta testing.
5 * [This specific revision at my hg repo](http://46.239.104.5:81/hg/program/ikiwiki/file/4994ba5e36fa/Plugin/mercurial.pm) ([raw version](http://46.239.104.5:81/hg/program/ikiwiki/raw-file/4994ba5e36fa/Plugin/mercurial.pm)).
7 * [My default branch](http://510x.se/hg/program/ikiwiki/file/default/Plugin/mercurial.pm) (where updates will be made, will mention here if anything happens) ([raw version](http://510x.se/hg/program/ikiwiki/raw-file/default/Plugin/mercurial.pm)).
9 (I've stripped the `hgrc`-generation from the linked versions, so it should work to just drop them on top of the old `mercurial.pm`).
11 I break out my comments from the code to make them more readable. I comment all the changes as compared to current upstream. --[[Daniel Andersson]]
13 > So, sorry it took me so long (summer vacation), but I've finally
14 > gotten around to looking at this. Based mostly just on the comments,
15 > it does not seem mergeable as-is, yet. Red flags for me include:
17 > * This is a big rewrite, and the main idea seems to be to copy git.pm
18 > and hack on it until it works, which I think is unlikely to be ideal
19 > as git and mercurial are not really similar at the level used here.
20 > * There have been no changes in your hg repo to the code since you
21 > originally committed it. Either it's perfect, or it's not been tested..
22 > * `hg_local_dirstate_shelve` writes to a temp file in the srcdir,
23 > which is hardly clean or ideal.
24 > * Relies on mercurial bookmarks extension that seems to need to be
26 > * There are some places where code was taken from git.pm and the
27 > comment asks if it even makes sense for mercurial, which obviously
28 > would need to be cleaned up.
29 > * The `rcs_receive` support especially is very ambitious to try to add to
30 > the mercurial code. Does mercurial support anonymous pushes at all? How
31 > would ikiwiki be run to handle such a push? How would it tell
32 > mercurial not to accept a push if it made prohibited changes?
34 > I'm glad we already got so many standalone improvements into
35 > mercurial.pm. That's a better approach than rewriting the world, unless
36 > the world is badly broken.
43 package IkiWiki::Plugin::mercurial;
49 use open qw{:utf8 :std};
52 Pattern to validate hg sha1 sums. hg usually truncates the hash to 12
53 characters and prepends a local revision number for output, but internally
54 it keeps a 40 character hash. Will use the long version in this code.
56 my $sha1_pattern = qr/[0-9a-fA-F]{40}/;
58 Message to skip in recent changes
60 my $dummy_commit_msg = 'dummy commit';
62 *TODO:* `$hg_dir` not really implemented yet, until a srcdir/repository distinction is
63 made as for e.g. Git. Used in `rcs_receive`, and for attachments in `hg_parse_changes`. See comments in those places, though.
68 hook(type => "checkconfig", id => "mercurial", call => \&checkconfig);
69 hook(type => "getsetup", id => "mercurial", call => \&getsetup);
70 hook(type => "rcs", id => "rcs_update", call => \&rcs_update);
71 hook(type => "rcs", id => "rcs_prepedit", call => \&rcs_prepedit);
72 hook(type => "rcs", id => "rcs_commit", call => \&rcs_commit);
73 hook(type => "rcs", id => "rcs_commit_staged", call => \&rcs_commit_staged);
74 hook(type => "rcs", id => "rcs_add", call => \&rcs_add);
75 hook(type => "rcs", id => "rcs_remove", call => \&rcs_remove);
76 hook(type => "rcs", id => "rcs_rename", call => \&rcs_rename);
77 hook(type => "rcs", id => "rcs_recentchanges", call => \&rcs_recentchanges);
78 hook(type => "rcs", id => "rcs_diff", call => \&rcs_diff);
79 hook(type => "rcs", id => "rcs_getctime", call => \&rcs_getctime);
80 hook(type => "rcs", id => "rcs_getmtime", call => \&rcs_getmtime);
81 hook(type => "rcs", id => "rcs_preprevert", call => \&rcs_preprevert);
82 hook(type => "rcs", id => "rcs_revert", call => \&rcs_revert);
84 This last hook is "unsanctioned" from [[Auto-setup and maintain Mercurial wrapper hooks]]. Will try to solve its function
87 hook(type => "rcs", id => "rcs_wrapper_postcall", call => \&rcs_wrapper_postcall);
91 if (exists $config{mercurial_wrapper} && length $config{mercurial_wrapper}) {
92 push @{$config{wrappers}}, {
93 wrapper => $config{mercurial_wrapper},
94 wrappermode => (defined $config{mercurial_wrappermode} ? $config{mercurial_wrappermode} : "06755"),
96 Next line part of [[Auto-setup and maintain Mercurial wrapper hooks]].
98 wrapper_postcall => (defined $config{mercurial_wrapper_hgrc_update} ? $config{mercurial_wrapper_hgrc_update} : "1"),
106 safe => 0, # rcs plugin
110 mercurial_wrapper => {
112 #example => # FIXME add example
113 description => "mercurial post-commit hook to generate",
117 mercurial_wrappermode => {
120 description => "mode for mercurial_wrapper (can safely be made suid)",
124 mercurial_wrapper_hgrc_update => {
127 description => "updates existing hgrc to reflect path changes for mercurial_wrapper",
133 example => "http://example.com:8000/log/tip/\[[file]]",
134 description => "url to hg serve'd repository, to show file history (\[[file]] substituted)",
140 example => "http://localhost:8000/?fd=\[[r2]];file=\[[file]]",
141 description => "url to hg serve'd repository, to show diff (\[[file]] and \[[r2]] substituted)",
148 # Start a child process safely without resorting to /bin/sh.
149 # Returns command output (in list content) or success state
150 # (in scalar context), or runs the specified data handler.
152 my ($error_handler, $data_handler, @cmdline) = @_;
154 my $pid = open my $OUT, "-|";
156 error("Cannot fork: $!") if !defined $pid;
160 # hg commands want to be in wc.
162 This `$hg_dir` logic means nothing and could be stripped until srcdir/repdir distinction is made (it's stripped in upstream `mercurial.pm` right now).
164 if (! defined $hg_dir) {
165 chdir $config{srcdir}
166 or error("cannot chdir to $config{srcdir}: $!");
169 chdir $hg_dir or error("cannot chdir to $hg_dir: $!");
172 exec @cmdline or error("Cannot exec '@cmdline': $!");
180 if (! defined $data_handler) {
184 last unless $data_handler->($_);
190 $error_handler->("'@cmdline' failed: $!") if $? && $error_handler;
192 return wantarray ? @lines : ($? == 0);
194 # Convenient wrappers.
195 sub run_or_die ($@) { safe_hg(\&error, undef, @_) }
196 sub run_or_cry ($@) { safe_hg(sub { warn @_ }, undef, @_) }
197 sub run_or_non ($@) { safe_hg(undef, undef, @_) }
200 To handle uncommited local changes ("ULC"s for short), I use logic similar to the (non-standard) "shelve" extension to Mercurial. By taking a diff before resetting to last commit, making changes and then applying diff again, one can do things Mercurial otherwise refuses, which is necessary later.
202 This function creates this diff.
204 sub hg_local_dirstate_shelve ($) {
205 # Creates a diff snapshot of uncommited changes existing the srcdir.
206 # Takes a string (preferably revision) as input to create a unique and
207 # identifiable diff name.
208 my $tempdiffname = "diff_".shift;
210 if (my @tempdiff = run_or_die('hg', 'diff', '-g')) {
212 writefile($tempdiffname, $config{srcdir},
215 $tempdiffpath = $config{srcdir}.'/'.$tempdiffname;
217 return $tempdiffpath;
220 This function restores the diff.
222 sub hg_local_dirstate_unshelve ($) {
223 # Applies diff snapshot to revert back to initial dir state. If diff
224 # revert succeeds, the diff is removed. Otherwise it stays to not
225 # eradicate the local changes if they were important. This clutters the
226 # directory though. Better ways to handle this are welcome. A true way
227 # around this dance is to have a separate repository for local changes
228 # and push ready commits to the srcdir instead.
229 if (my $tempdiffpath = shift) {
230 if (run_or_cry('hg', 'import', '--no-commit', $tempdiffpath)) {
231 unlink($tempdiffpath);
237 This makes online diffing possible. A similar approach as in `git.pm`, which is [discussed to some length in a comment there](http://source.ikiwiki.branchable.com/?p=source.git;a=blob;f=IkiWiki/Plugin/git.pm;h=cf7fbe9b7c43ee53180612d0411e6202074fb9e0;hb=refs/heads/master#l211), is taken.
239 sub merge_past ($$$) {
240 my ($sha1, $file, $message) = @_;
242 # Undo stack for cleanup in case of an error
244 # File content with conflict markers
249 # Hide local changes from Mercurial by renaming the modified
250 # file. Relative paths must be converted to absolute for
252 my ($target, $hidden) = (
253 "$config{srcdir}/${file}",
254 "$config{srcdir}/${file}.${sha1}"
256 rename($target, $hidden)
257 or error("rename '$target' to '$hidden' failed: $!");
258 # Ensure to restore the renamed file on error.
260 return if ! -e "$hidden"; # already renamed
261 rename($hidden, $target)
262 or warn "rename '$hidden' to '$target' failed: $!";
266 Take a snapshot of srcdir to be able to restore uncommited local changes ("ULCs") afterwards.
268 * This must happen _after_ the merging commit in Mercurial, there is no way around it. By design hg refuses to commit merges if there are other changes to tracked content present, no matter how much you beg.
270 * ULCs to the file being edited are special: they can't be diffed here since `editpage.pm` already has overwritten the file. When the web edit session started though, the ULC version (not the commited
271 version) was read into the form, so in a way, the web user _has already merged_ with the ULC. It is not saved in commit history, but that is the exact consequence of "uncommited" changes. If an ULC is done between the time the web edit started and was submitted, then it is lost, though. All in all, one shouldn't be editing the srcdir directly when web edits of the same file are allowed. Clone the repo and push changes instead.
273 Much of these issues disappear, I believe, if one works with a master repo which only is pushed to.
275 my $tempdiffpath = hg_local_dirstate_shelve($sha1);
277 # Ensure uniqueness of bookmarks.
278 my $bookmark_upstream_head = "current_head_$sha1";
279 my $bookmark_edit_base = "edit_base_$sha1";
281 # Git and Mercurial differ in the branch concept. Mercurial's
282 # "bookmarks" are closer in function in this regard.
284 Bookmarks aren't standard until Mercurial 1.8 ([2011--02--10](http://selenic.com/hg/rev/d4ab9486e514)), but they've been bundled with Mercurial since ~2008, so they can be enabled by writing a `hgrc`, which is also being worked on.
286 # Create a bookmark at current tip.
287 push @undo, sub { run_or_cry('hg', 'bookmark', '--delete',
288 $bookmark_upstream_head) };
289 run_or_die('hg', 'bookmark', $bookmark_upstream_head);
291 # Create a bookmark at the revision from which the edit was
292 # started and switch to it, discarding changes (they are stored
293 # in $tempdiff and the hidden file at the moment).
294 push @undo, sub { run_or_cry('hg', 'bookmark', '--delete',
295 $bookmark_edit_base) };
296 run_or_die('hg', 'bookmark', '-r', $sha1, $bookmark_edit_base);
297 run_or_die('hg', 'update', ,'-C', $bookmark_edit_base);
299 # Reveal the modified file.
300 rename($hidden, $target)
301 or error("rename '$hidden' to '$target' failed: $!");
303 # Commit at the bookmarked revision, creating a new head.
304 run_or_cry('hg', 'commit', '-m', $message);
306 # Attempt to merge the newly created head with upstream head.
307 # '--tool internal:merge' to avoid spawning a GUI merger.
309 (*Semi-TODO:* How do you make this command quiet? On failed merge, it
310 always writes to STDERR and clutters the web server log.)
312 if (!run_or_non('hg', 'merge', '--tool', 'internal:merge',
313 $bookmark_upstream_head)) {
314 # ..., otherwise return file with conflict markers.
315 $conflict = readfile($target);
317 # The hardcore reset approach. Keep your hands inside
319 run_or_die('hg', 'rollback');
320 run_or_die('hg', 'update', '-C',
321 $bookmark_upstream_head);
323 hg_local_dirstate_unshelve($tempdiffpath);
326 Other approaches tried here:
328 1. Clean up merge attempt,
330 run_or_die('hg', 'update', '-C', $bookmark_upstream_head);
332 2. Redo "merge", using only upstream head versions,
334 run_or_die('hg', 'merge', '--tool', 'internal:local', $bookmark_edit_base);
336 3. dummy commit to close head.
338 run_or_non('hg', 'commit', '-m', $message);
340 This creates a cluttered and erroneous history. We
341 tell Mercurial to merge, even though we in practice
342 discard. This creates problems when trying to revert
347 1. Discard merge attempt and switch to temp head,
349 run_or_die('hg', 'update', '-C', $bookmark_edit_base);
351 2. close the temp head (why do they call the command that in practice closes heads "--close-branch"?),
353 run_or_non('hg', 'commit', '--close-branch', '-m', $message);
355 3. restore working directory to pre-fiddling status.
357 run_or_die('hg', 'update', $bookmark_upstream_head);
359 ...but this requires the same amount of forks as the
360 above method, and confuses other parts of ikiwiki
361 since the upstream head is now the third newest
362 revision. Maybe that particular problem is solvable
363 by setting a global default bookmark that follows the
364 main tip. It will leave clutter in the revision
365 history, though. Two extra commits that in practice
366 don't hold relevant information will be recorded for
367 each failed merge attempt.
369 To only create one extra commit, one could imagine
370 adding `--close-branch` to the commit that initially
371 created the new head (since there is no problem
372 merging with closed heads), but it's not possible to
373 close and create a head at the same time, apparently.
379 # Process undo stack (in reverse order). By policy, cleanup actions
380 # should normally print a warning on failure.
381 while (my $handle = pop @undo) {
385 error("Mercurial merge failed!\n$failure\n") if $failure;
387 return ($conflict, $tempdiffpath);
390 sub hg_commit_info ($;$;$) {
391 # Return an array of commit info hashes of num commits starting from
394 This could be optimized by using a lookup cache similar to
395 `findtimes()`. By adding `KeyAttr => ['node']` to `XMLin()` options, one
396 could use the revision ID as key and do a single massive history
397 lookup and later just check if the given revision already exists as a
398 key. Right now I'm at the "don't optimize it yet" stage, though.
400 This uses Mercurial's built-in `--style xml` and parses it with `XML::Simple`. Mercurial's log output is otherwise somewhat cumbersome to get good stuff out of, so this XML solution is quite good, I think. It adds module dependency, but XML::Simple seems fairly standard (but what do I know, I've used 1 Perl installation in my life).
405 my ($sha1, $num, $file) = @_;
409 if ($sha1 =~ m/^($sha1_pattern)$/) {
410 push @opts, ('-r'. $1.':0');
412 elsif ($sha1 =~ m/^($sha1_pattern):($sha1_pattern)$/) {
413 push @opts, ('-r', $1.':'.$2);
416 push @opts, ('--limit', $num) if defined $num;
417 push @opts, ('--', $file) if defined $file;
420 $ENV{HGENCODING} = 'utf-8';
421 my @xml = run_or_cry('hg', 'log', '-v', '--style', 'xml', @opts);
424 # hg returns empty string if file is not in repository.
425 return undef if !@xml;
427 Some places it is clear that I'm coding ad-hoc Perl. I don't know if this is a reasonably efficient way to give input to `XMLin`, but it works.
429 # Want to preserve linebreaks in multiline comments.
431 my $xmllog = XMLin("@xml",
432 ForceArray => ['logentry', 'parent', 'copy', 'path']);
436 foreach my $rev (@{$xmllog->{logentry}}) {
438 # In Mercurial, "rev" is technically the strictly local
439 # revision number. What ikiwiki wants is what is called
440 # "node": a globally defined SHA1 checksum.
441 $c_info{rev} = $rev->{node};
442 foreach my $parent (@{$rev->{parent}}) {
443 push @{$c_info{parents}}, {rev => $parent->{node}};
445 $c_info{user} = $rev->{author}{content};
446 # Mercurial itself parses out and stores an email address if
447 # present in author name. If not, hg sets email to author name.
448 if ( $rev->{author}{content} ne $rev->{author}{email} &&
449 $rev->{author}{email} =~ m/^([^\@]+)\@(.*)$/ ) {
451 $c_info{nickname} = $1;
452 $c_info{web_commit} = "1";
455 # Mercurial gives date in ISO 8601, well handled by str2time().
456 $c_info{when} = str2time($rev->{date});
457 # Mercurial doesn't allow empty commit messages, so there
458 # should always be a single defined message.
459 $c_info{message} = $rev->{msg}{content};
460 # Inside "paths" sits a single array "path" that contains
461 # multiple paths. Crystal clear :-)
462 foreach my $path (@{$rev->{paths}{path}}) {
463 push @{$c_info{files}}, {
464 # Mercurial doesn't track file permissions as
465 # Git do, so that's missing here.
466 'file' => $path->{content},
467 'status' => $path->{action},
470 # There also exists an XML branch "copies"->"copy", containing
471 # source and dest of files that have been copied with "hg cp".
472 # The copy action is also registered in "paths" as a removal of
473 # source and addition of dest, so it's not needed here.
474 push @c_infos, {%c_info};
478 return wantarray ? @c_infos : $c_infos[0];
482 # Return head sha1sum (of given file).
483 my $file = shift || q{--};
485 # Non-existing file doesn't give error, just empty string.
486 my $f_info = hg_commit_info(undef, 1, $file);
488 if ($f_info->{rev}) {
489 ($sha1) = $f_info->{rev} =~ m/($sha1_pattern)/;
492 debug("Empty sha1sum for '$file'.");
494 return defined $sha1 ? $sha1 : q{};
498 run_or_cry('hg', '-q', 'update');
501 sub rcs_prepedit ($) {
502 # Return the commit sha1sum of the file when editing begins.
503 # This will be later used in rcs_commit if a merge is required.
506 return hg_sha1($file);
510 # Try to commit the page; returns undef on _success_ and
511 # a version of the page with the rcs's conflict markers on
515 # Check to see if the page has been changed by someone else since
516 # rcs_prepedit was called.
517 my $cur = hg_sha1($params{file});
518 my ($prev) = $params{token} =~ /^($sha1_pattern)$/; # untaint
520 if (defined $cur && defined $prev && $cur ne $prev) {
522 If there was a conflict, the file with conflict markers is returned. Else, the path to the tempdiff, which is to be run to restore previous local state after `rcs_commit_staged`, is returned.
524 my ($conflict, $tempdiffpath) =
525 merge_past($prev, $params{file}, $dummy_commit_msg);
526 return defined $conflict
531 tempdiffpath => $tempdiffpath);
534 return rcs_commit_helper(@_);
537 sub rcs_commit_helper (@) {
541 $ENV{HGENCODING} = 'utf-8';
543 my $user="Anonymous";
545 if (defined $params{session}) {
546 if (defined $params{session}->param("name")) {
547 $user = $params{session}->param("name");
549 elsif (defined $params{session}->remote_addr()) {
550 $user = $params{session}->remote_addr();
553 if (defined $params{session}->param("nickname")) {
554 $nickname=encode_utf8($params{session}->param("nickname"));
555 $nickname=~s/\s+/_/g;
556 $nickname=~s/[^-_0-9[:alnum:]]+//g;
558 $ENV{HGUSER} = encode_utf8($user . ' <' . $nickname . '@web>');
561 if (! length $params{message}) {
562 $params{message} = "no message given";
565 $params{message} = IkiWiki::possibly_foolish_untaint($params{message});
569 Mercurial rejects file arguments when performing a merging commit. It
570 only does "all or nothing" commits by design when merging, so given file arguments must be discarded. It should not pose a problem.
572 if (exists $params{file} && ! defined $params{merge}) {
573 push @opts, '--', $params{file};
576 # hg commit returns non-zero if nothing really changed.
577 # So we should ignore its exit status (hence run_or_non).
578 run_or_non('hg', 'commit', '-m', $params{message}, '-q', @opts);
580 If there were uncommited local changes in srcdir before a merge was done, they are restored here.
582 if (defined $params{tempdiffpath}) {
583 hg_local_dirstate_unshelve($params{tempdiffpath});
587 return undef; # success
590 sub rcs_commit_staged (@) {
591 # Commits all staged changes. Changes can be staged using rcs_add,
592 # rcs_remove, and rcs_rename.
593 return rcs_commit_helper(@_);
599 run_or_cry('hg', 'add', $file);
603 # Remove file from archive.
606 run_or_cry('hg', 'remove', '-f', $file);
609 sub rcs_rename ($$) {
610 my ($src, $dest) = @_;
612 run_or_cry('hg', 'rename', '-f', $src, $dest);
615 sub rcs_recentchanges ($) {
620 foreach my $c_info (hg_commit_info(undef, $num, undef)) {
622 for my $page (@{$c_info->{files}}) {
623 my $diffurl=defined $config{diffurl} ?
624 $config{diffurl} : '';
625 # These substitutions enable defining keywords \[[file]]
626 # and \[[r2]] (backward compatibility) in the setup file
627 # that will be exchanged with filename and revision
629 $diffurl =~ s/\[\[file\]\]/$page->{file}/go;
630 $diffurl =~ s/\[\[r2\]\]/$c_info->{rev}/go;
632 # pagename() strips suffixes and returns the
633 # path to the file as it is to be represented
635 page => pagename($page->{file}),
640 # It is expected of ikiwiki to get each comment line as a
643 open my $message, '<', \$c_info->{message};
644 while (<$message>) { push @messagelines, { line => $_ } };
647 rev => $c_info->{rev},
648 user => $c_info->{user},
649 nickname => defined $c_info->{nickname} ?
650 $c_info->{nickname} : $c_info->{user},
651 committype => $c_info->{web_commit} ? "web" : "hg",
652 when => $c_info->{when},
653 message => [@messagelines],
654 pages => [@pagenames],
667 return if defined $maxlines && @lines == $maxlines;
668 push @lines, $line."\n"
669 if (@lines || $line=~/^diff --git/);
672 safe_hg(undef, $addlines, "hg", "diff", "-c", $rev, "-g");
677 return join("", @lines);
684 This is an upstream change I did a week ago or so. Perhaps it can be merged in some clever way with the updated `hg_commit_info` to make one shared lookup cache. Don't know how much would be gained.
688 my $id=shift; # 0 = mtime ; 1 = ctime
690 if (! keys %time_cache) {
693 # It doesn't seem possible to specify the format wanted for the
694 # changelog (same format as is generated in git.pm:findtimes(),
695 # though the date differs slightly) without using a style
696 # _file_. There is a "hg log" switch "--template" to directly
697 # control simple output formatting, but in this case, the
698 # {file} directive must be redefined, which can only be done
701 # If {file} is not redefined, all files are output on a single
702 # line separated with a space. It is not possible to conclude
703 # if the space is part of a filename or just a separator, and
704 # thus impossible to use in this case.
706 # Some output filters are available in hg, but they are not fit
707 # for this cause (and would slow down the process
710 eval q{use File::Temp};
712 my ($tmpl_fh, $tmpl_filename) = File::Temp::tempfile(UNLINK => 1);
714 print $tmpl_fh 'changeset = "{date}\\n{files}\\n"' . "\n";
715 print $tmpl_fh 'file = "{file}\\n"' . "\n";
717 foreach my $line (run_or_die('hg', 'log', '--style', $tmpl_filename)) {
718 # {date} gives output on the form
720 # where the first number is UTC Unix timestamp with one
721 # decimal (decimal always 0, at least on my system)
722 # followed by local timezone offset from UTC in
724 if (! defined $date && $line =~ /^\d+\.\d[+-]\d*$/) {
725 $line =~ s/^(\d+).*/$1/;
728 elsif (! length $line) {
734 if (! $time_cache{$f}) {
735 $time_cache{$f}[0]=$date; # mtime
737 $time_cache{$f}[1]=$date; # ctime
742 return exists $time_cache{$file} ? $time_cache{$file}[$id] : 0;
747 sub rcs_getctime ($) {
750 return findtimes($file, 1);
753 sub rcs_getmtime ($) {
756 return findtimes($file, 0);
759 The comment just below the function declaration below is taken from `git.pm`. Is it true? Should ikiwiki support sharing its repo with other things? Mercurial-wise that sounds like a world of pain.
761 > Yes, ikiwiki supports this for git and svn. It's useful when you want
762 > a doc/ directory with the wiki for a project. I don't know why
763 > it wouldn't be a useful thing to do with mercurial, but it's not
764 > required. --[[Joey]]
769 # The wiki may not be the only thing in the git repo.
770 # Determine if it is in a subdirectory by examining the srcdir,
771 # and its parents, looking for the .git directory.
773 return @$ret if defined $ret;
776 my $dir=$config{srcdir};
777 while (! -d "$dir/.hg") {
778 $subdir=IkiWiki::basename($dir)."/".$subdir;
779 $dir=IkiWiki::dirname($dir);
781 error("cannot determine root of hg repo");
785 $ret=[$subdir, $dir];
791 sub hg_parse_changes (@) {
792 # Only takes a single info hash as argument in rcs_preprevert, but
793 # should be able to take several in rcs_receive.
794 my @c_infos_raw = shift;
796 my ($subdir, $rootdir) = hg_find_root();
799 foreach my $c_info_raw (@c_infos_raw) {
800 foreach my $path (@{$c_info_raw->{files}}) {
801 my ($file, $action, $temppath);
805 # check that all changed files are in the subdir
806 if (length $subdir && ! ($file =~ s/^$subdir//)) {
807 error sprintf(gettext("you are not allowed to change %s"), $file);
810 if ($path->{status} eq "M") { $action="change" }
811 elsif ($path->{status} eq "A") { $action="add" }
812 elsif ($path->{status} eq "R") { $action="remove" }
813 else { error "unknown status ".$path->{status} }
815 I haven't tested the attachment code below. Is it run when there is an non-trusted file upload?
817 > It's run when an anonymous git push is done. I don't know if there would
818 > be any equivilant with mercurial; if not, it does not makes sense
819 > to implement this at all (this function is only used by `rcs_receive`). --[[Joey]]
821 # extract attachment to temp file
822 if (($action eq 'add' || $action eq 'change') &&
825 eval q{use File::Temp};
829 ($fh, $temppath)=File::Temp::tempfile(undef, UNLINK => 1);
830 my $cmd = "cd $hg_dir && ".
831 "hg diff -g -c $c_info_raw->{rev} > '$temppath'";
832 if (system($cmd) != 0) {
833 error("failed writing temp file '$temppath'.");
848 *TODO:* I don't know what's happening here. I've changed the code to adhere to this file's variables and functions, but it refers to a srcdir _and_ a default repo, which currently isn't available in the Mercurial setup.
850 `rcs_receive` is optional and only runs when running a pre-receive hook. Where `$_` comes from and its format are mysteries to me.
852 Also, a comment in `git.pm` mentions that we don't want to chdir to a subdir "and only see changes in it" - but this isn't true for either Git or Mercurial to my knowledge. It only seems to happen in `git.pm` since the `git log` command in `git_commit_info` ends with "`-- .`" - if it didn't do that, one wouldn't have to chdir for this reason, I believe.
854 In this case we need to stay in default repo instead of srcdir though, so `hg_dir="."` _is_ needed, but not for the abovementioned reason :-) (maybe there's more to it, though).
856 > Implementing some sort of anonymous push handling for mercurial is not something
857 > you can funble your way through like this, if it can be done at all.
859 > Hint: `$_` is being populated by the specific format git sends to a
860 > specific hook script.
867 my ($oldrev, $newrev, $refname) = split(' ', $_, 3);
869 # only allow changes to hg_default_branch
871 *TODO:* What happens here? Some Git voodoo. _If_ `$_` has the exact same format for Mercurial, then the below should work just as well here, I think.
873 if ($refname !~ m|^refs/heads/$config{hg_default_branch}$|) {
874 error sprintf(gettext("you are not allowed to change %s"), $refname);
877 Comment from `git.pm`:
879 # Avoid chdir when running git here, because the changes are in
880 # the default git repo, not the srcdir repo. (Also, if a subdir
881 # is involved, we don't want to chdir to it and only see
882 # changes in it.) The pre-receive hook already puts us in the
886 hg_parse_changes(hg_commit_info($newrev.":".$oldrev,
894 sub rcs_preprevert ($) {
896 my ($sha1) = $rev =~ /^($sha1_pattern)$/; # untaint
898 The below 4 lines of code are from `git.pm`, but I can't see what they actually do there. Neither Git nor Mercurial only lists changes in working directory when given a command - they always traverse to repository root by themselves. I keep it here for comments, in case I'm missing something.
900 *UPDATE:* See earlier note about `git log` ending in "`-- .`".
902 ## Examine changes from root of git repo, not from any subdir,
903 ## in order to see all changes.
904 #my ($subdir, $rootdir) = git_find_root();
907 my $c_info=hg_commit_info($sha1, 1, undef) or error "unknown commit";
909 # hg revert will fail on merge commits. Add a nice message.
910 if (exists $c_info->{parents} && $c_info->{parents} > 1) {
911 error gettext("you are not allowed to revert a merge");
914 my @c_info_ret=hg_parse_changes($c_info);
916 ### Probably not needed, if earlier comment is correct.
922 # Try to revert the given rev; returns undef on _success_.
924 my ($sha1) = $rev =~ /^($sha1_pattern)$/; # untaint
926 # Save uncommited local changes to diff file. Attempt to restore later.
927 my $tempdiffpath = hg_local_dirstate_shelve($sha1);
929 # Clean dir to latest commit.
930 run_or_die('hg', 'update', '-C');
932 Some voodoo is needed here. `hg backout --tool internal:local -r $sha1` is *almost* good, but if the reversion is done to the directly previous revision, hg automatically commits, which is bad in this case. Instead I generate a reverse diff and pipe it to `import --no-commit`.
934 if (run_or_non("hg diff -c $sha1 --reverse | hg import --no-commit -")) {
935 if ($tempdiffpath) { hg_local_dirstate_unshelve($tempdiffpath) }
939 if ($tempdiffpath) { hg_local_dirstate_unshelve($tempdiffpath) }
940 return sprintf(gettext("Failed to revert commit %s"), $sha1);
944 Below follows code regarding [[Auto-setup and maintain Mercurial wrapper hooks]]. Will try to solve it in another place later, but the code in itself is working.
946 Should perhaps add initiation of the bookmark extension here, to support older Mercurial versions.
948 sub rcs_wrapper_postcall($) {
949 # Update hgrc if it exists. Change post-commit/incoming hooks with the
950 # .ikiwiki suffix to point to the wrapper path given in the setup file.
951 # Work with a tempfile to not delete hgrc if the loop is interrupted
953 # I believe there is a better way to solve this than creating new hooks
954 # and callbacks. Will await discussion on ikiwiki.info.
955 my $hgrc=$config{srcdir}.'/.hg/hgrc';
956 my $backup_suffix='.ikiwiki.bak';
959 my $mercurial_wrapper_abspath=File::Spec->rel2abs($config{mercurial_wrapper}, $config{srcdir});
960 local ($^I, @ARGV)=($backup_suffix, $hgrc);
962 s/^(post-commit|incoming)(\.ikiwiki[ \t]*=[ \t]*).*$/$1$2$mercurial_wrapper_abspath/;
965 unlink($hgrc.$backup_suffix);