]> git.vanrenterghem.biz Git - git.ikiwiki.info.git/blob - IkiWiki/Rcs/monotone.pm
be6b36c66e4816f67875d51caba86f49f3582bd3
[git.ikiwiki.info.git] / IkiWiki / Rcs / monotone.pm
1 #!/usr/bin/perl
2 use warnings;
3 use strict;
4 use IkiWiki;
5 use Monotone;
6 use Date::Parse qw(str2time);
7 use Date::Format qw(time2str);
9 package IkiWiki;
11 my $sha1_pattern = qr/[0-9a-fA-F]{40}/; # pattern to validate sha1sums
13 sub check_config() { #{{{
14         if (!defined($config{mtnrootdir})) {
15                 $config{mtnrootdir} = $config{srcdir};
16         }
17         if (! -d "$config{mtnrootdir}/_MTN") {
18                 error("Ikiwiki srcdir does not seem to be a Monotone workspace (or set the mtnrootdir)!");
19         }
20         
21         if (!defined($config{mtnmergerc})) {
22                 $config{mtnmergerc} = "$config{mtnrootdir}/_MTN/mergerc";
23         }
24         
25         chdir $config{srcdir}
26             or error("Cannot chdir to $config{srcdir}: $!");
27 } #}}}
29 sub get_rev () { #{{{
30         my $sha1 = `mtn --root=$config{mtnrootdir} automate get_base_revision_id`;
32         ($sha1) = $sha1 =~ m/($sha1_pattern)/; # sha1 is untainted now
33         if (! $sha1) {
34                 debug("Unable to get base revision for '$config{srcdir}'.")
35         }
37         return $sha1;
38 } #}}}
40 sub get_rev_auto ($) { #{{{
41         my $automator=shift;
43         my @results = $automator->call("get_base_revision_id");
45         my $sha1 = $results[0];
46         ($sha1) = $sha1 =~ m/($sha1_pattern)/; # sha1 is untainted now
47         if (! $sha1) {
48                 debug("Unable to get base revision for '$config{srcdir}'.")
49         }
51         return $sha1;
52 } #}}}
54 sub mtn_merge ($$$$) { #{{{
55         my $leftRev=shift;
56         my $rightRev=shift;
57         my $branch=shift;
58         my $author=shift;
59     
60         my $mergeRev;
62         my $mergerc = $config{mtnmergerc};
63     
64         my $child = open(MTNMERGE, "-|");
65         if (! $child) {
66                 open STDERR, ">&STDOUT";
67                 exec("mtn", "--root=$config{mtnrootdir}", "--rcfile",
68                      $mergerc, "explicit_merge", $leftRev, $rightRev,
69                      $branch, "--author", $author, "--key", 
70                      $config{mtnkey}) || error("mtn merge failed to run");
71         }
73         while (<MTNMERGE>) {
74                 if (/^mtn.\s.merged.\s($sha1_pattern)$/) {
75                         $mergeRev=$1;
76                 }
77         }
78         
79         close MTNMERGE || return undef;
81         debug("merged $leftRev, $rightRev to make $mergeRev");
83         return $mergeRev;
84 } #}}}
86 sub commit_file_to_new_rev($$$$$$$$) { #{{{
87         my $automator=shift;
88         my $wsfilename=shift;
89         my $oldFileID=shift;
90         my $newFileContents=shift;
91         my $oldrev=shift;
92         my $branch=shift;
93         my $author=shift;
94         my $message=shift;
95         
96         #store the file
97         my ($out, $err) = $automator->call("put_file", $oldFileID, $newFileContents);
98         my ($newFileID) = ($out =~ m/^($sha1_pattern)$/);
99         error("Failed to store file data for $wsfilename in repository")
100                 if (! defined $newFileID || length $newFileID != 40);
102         # get the mtn filename rather than the workspace filename
103         ($out, $err) = $automator->call("get_corresponding_path", $oldrev, $wsfilename, $oldrev);
104         my ($filename) = ($out =~ m/^file "(.*)"$/);
105         error("Couldn't find monotone repository path for file $wsfilename") if (! $filename);
106         debug("Converted ws filename of $wsfilename to repos filename of $filename");
108         # then stick in a new revision for this file
109         my $manifest = "format_version \"1\"\n\n".
110                        "new_manifest [0000000000000000000000000000000000000000]\n\n".
111                        "old_revision [$oldrev]\n\n".
112                        "patch \"$filename\"\n".
113                        " from [$oldFileID]\n".
114                        "   to [$newFileID]\n";
115         ($out, $err) = $automator->call("put_revision", $manifest);
116         my ($newRevID) = ($out =~ m/^($sha1_pattern)$/);
117         error("Unable to make new monotone repository revision")
118                 if (! defined $newRevID || length $newRevID != 40);
119         debug("put revision: $newRevID");
120         
121         # now we need to add certs for this revision...
122         # author, branch, changelog, date
123         $automator->call("cert", $newRevID, "author", $author);
124         $automator->call("cert", $newRevID, "branch", $branch);
125         $automator->call("cert", $newRevID, "changelog", $message);
126         $automator->call("cert", $newRevID, "date",
127                 time2str("%Y-%m-%dT%T", time, "UTC"));
128         
129         debug("Added certs for rev: $newRevID");
130         return $newRevID;
131 } #}}}
133 sub check_mergerc () { #{{{
134         my $mergerc = $config{mtnmergerc};
135         if (! -r $mergerc ) {
136                 debug("$mergerc doesn't exist. Creating file with default mergers.");
137                 open (my $out, ">", $mergerc) or error("can't open $mergerc: $!");
138                 print $out <DATA>;
139                 close $out;
140         }
141 } #}}}
143 sub read_certs ($$) { #{{{
144         my $automator=shift;
145         my $rev=shift;
146         my @results = $automator->call("certs", $rev);
147         my @ret;
149         my $line = $results[0];
150         while ($line =~ m/\s+key\s"(.*?)"\nsignature\s"(ok|bad|unknown)"\n\s+name\s"(.*?)"\n\s+value\s"(.*?)"\n\s+trust\s"(trusted|untrusted)"\n/sg) {
151                 push @ret, {
152                         key => $1,
153                         signature => $2,
154                         name => $3,
155                         value => $4,
156                         trust => $5,
157                 };
158         }
160         return @ret;
161 } #}}}
163 sub get_changed_files ($$) { #{{{
164         my $automator=shift;
165         my $rev=shift;
166         
167         my @results = $automator->call("get_revision", $rev);
168         my $changes=$results[0];
170         my @ret;
171         my %seen = ();
172         
173         while ($changes =~ m/\s*(add_file|patch|delete|rename)\s"(.*?)(?<!\\)"\n/sg) {
174                 my $file = $2;
175                 # don't add the same file multiple times
176                 if (! $seen{$file}) {
177                         push @ret, $file;
178                         $seen{$file} = 1;
179                 }
180         }
181         
182         return @ret;
183 } #}}}
185 sub rcs_update () { #{{{
186         check_config();
188         if (defined($config{mtnsync}) && $config{mtnsync}) {
189                 if (system("mtn", "--root=$config{mtnrootdir}", "sync",
190                            "--quiet", "--ticker=none", 
191                            "--key", $config{mtnkey}) != 0) {
192                         debug("monotone sync failed before update");
193                 }
194         }
196         if (system("mtn", "--root=$config{mtnrootdir}", "update", "--quiet") != 0) {
197                 debug("monotone update failed");
198         }
199 } #}}}
201 sub rcs_prepedit ($) { #{{{
202         my $file=shift;
204         check_config();
206         # For monotone, return the revision of the file when
207         # editing begins.
208         return get_rev();
209 } #}}}
211 sub rcs_commit ($$$;$$) { #{{{
212         # Tries to commit the page; returns undef on _success_ and
213         # a version of the page with the rcs's conflict markers on failure.
214         # The file is relative to the srcdir.
215         my $file=shift;
216         my $message=shift;
217         my $rcstoken=shift;
218         my $user=shift;
219         my $ipaddr=shift;
220         my $author;
222         if (defined $user) {
223                 $author="Web user: " . $user;
224         }
225         elsif (defined $ipaddr) {
226                 $author="Web IP: " . $ipaddr;
227         }
228         else {
229                 $author="Web: Anonymous";
230         }
232         check_config();
234         my ($oldrev)= $rcstoken=~ m/^($sha1_pattern)$/; # untaint
235         my $rev = get_rev();
236         if (defined $rev && defined $oldrev && $rev ne $oldrev) {
237                 my $automator = Monotone->new();
238                 $automator->open_args("--root", $config{mtnrootdir}, "--key", $config{mtnkey});
240                 # Something has been committed, has this file changed?
241                 my ($out, $err);
242                 #$automator->setOpts("-r", $oldrev, "-r", $rev);
243                 #my ($out, $err) = $automator->call("content_diff", $file);
244                 #debug("Problem committing $file") if ($err ne "");
245                 # FIXME: use of $file in these backticks is not wise from a
246                 # security POV. Probably safe, but should be avoided
247                 # anyway.
248                 my $diff = `mtn --root=$config{mtnrootdir} au content_diff -r $oldrev -r $rev $file`; # was just $out;
250                 if ($diff) {
251                         # Commit a revision with just this file changed off
252                         # the old revision.
253                         #
254                         # first get the contents
255                         debug("File changed: forming branch");
256                         my $newfile=readfile("$config{srcdir}/$file");
257                         
258                         # then get the old content ID from the diff
259                         if ($diff !~ m/^---\s$file\s+($sha1_pattern)$/m) {
260                                 error("Unable to find previous file ID for $file");
261                         }
262                         my $oldFileID = $1;
264                         # get the branch we're working in
265                         ($out, $err) = $automator->call("get_option", "branch");
266                         chomp $out;
267                         error("Illegal branch name in monotone workspace") if ($out !~ m/^([-\@\w\.]+)$/);
268                         my $branch = $1;
270                         # then put the new content into the DB (and record the new content ID)
271                         my $newRevID = commit_file_to_new_rev($automator, $file, $oldFileID, $newfile, $oldrev, $branch, $author, $message);
273                         $automator->close();
275                         # if we made it to here then the file has been committed... revert the local copy
276                         if (system("mtn", "--root=$config{mtnrootdir}", "revert", $file) != 0) {
277                                 debug("Unable to revert $file after merge on conflicted commit!");
278                         }
279                         debug("Divergence created! Attempting auto-merge.");
281                         check_mergerc();
283                         # see if it will merge cleanly
284                         $ENV{MTN_MERGE}="fail";
285                         my $mergeResult = mtn_merge($newRevID, $rev, $branch, $author);
286                         $ENV{MTN_MERGE}="";
288                         # push any changes so far
289                         if (defined($config{mtnsync}) && $config{mtnsync}) {
290                                 if (system("mtn", "--root=$config{mtnrootdir}", "push", "--quiet", "--ticker=none", "--key", $config{mtnkey}) != 0) {
291                                         debug("monotone push failed");
292                                 }
293                         }
294                         
295                         if (defined($mergeResult)) {
296                                 # everything is merged - bring outselves up to date
297                                 if (system("mtn", "--root=$config{mtnrootdir}",
298                                            "update", "-r", $mergeResult) != 0) {
299                                         debug("Unable to update to rev $mergeResult after merge on conflicted commit!");
300                                 }
301                         }
302                         else {
303                                 debug("Auto-merge failed.  Using diff-merge to add conflict markers.");
304                                 
305                                 $ENV{MTN_MERGE}="diffutils_force";
306                                 $mergeResult = mtn_merge($newRevID, $rev, $branch, $author);
307                                 $ENV{MTN_MERGE}="";
308                                 
309                                 if (!defined($mergeResult)) {
310                                         debug("Unable to insert conflict markers!");
311                                         error("Your commit succeeded. Unfortunately, someone else committed something to the same ".
312                                                 "part of the wiki at the same time. Both versions are stored in the monotone repository, ".
313                                                 "but at present the different versions cannot be reconciled through the web interface. ".
314                                                 "Please use the non-web interface to resolve the conflicts.");
315                                 }
316                                 
317                                 # suspend this revision because it has
318                                 # conflict markers...
319                                 if (system("mtn", "--root=$config{mtnrootdir}",
320                                            "update", "-r", $mergeResult) != 0) {
321                                         debug("Unable to update to rev $mergeResult after conflict-enhanced merge on conflicted commit!");
322                                 }
323                                 
324                                 # return "conflict enhanced" file to the user
325                                 # for cleanup note, this relies on the fact
326                                 # that ikiwiki seems to call rcs_prepedit()
327                                 # again after we return
328                                 return readfile("$config{srcdir}/$file");
329                         }
330                         return undef;
331                 }
332                 $automator->close();
333         }
335         # If we reached here then the file we're looking at hasn't changed
336         # since $oldrev. Commit it.
338         if (system("mtn", "--root=$config{mtnrootdir}", "commit", "--quiet",
339                    "--author", $author, "--key", $config{mtnkey}, "-m",
340                    possibly_foolish_untaint($message), $file) != 0) {
341                 debug("Traditional commit failed! Returning data as conflict.");
342                 my $conflict=readfile("$config{srcdir}/$file");
343                 if (system("mtn", "--root=$config{mtnrootdir}", "revert",
344                            "--quiet", $file) != 0) {
345                         debug("monotone revert failed");
346                 }
347                 return $conflict;
348         }
349         if (defined($config{mtnsync}) && $config{mtnsync}) {
350                 if (system("mtn", "--root=$config{mtnrootdir}", "sync",
351                            "--quiet", "--ticker=none", "--key",
352                            $config{mtnkey}) != 0) {
353                         debug("monotone sync failed");
354                 }
355         }
357         return undef # success
358 } #}}}
360 sub rcs_add ($) { #{{{
361         my $file=shift;
363         check_config();
365         if (system("mtn", "--root=$config{mtnrootdir}", "add", "--quiet",
366                    "$config{srcdir}/$file") != 0) {
367                 error("Monotone add failed");
368         }
369 } #}}}
371 sub rcs_recentchanges ($) { #{{{
372         my $num=shift;
373         my @ret;
375         check_config();
377         # use log --brief to get a list of revs, as this
378         # gives the results in a nice order
379         # (otherwise we'd have to do our own date sorting)
381         my @revs;
383         my $child = open(MTNLOG, "-|");
384         if (! $child) {
385                 exec("mtn", "log", "--root=$config{mtnrootdir}", "--no-graph",
386                      "--brief") || error("mtn log failed to run");
387         }
389         while (($num >= 0) and (my $line = <MTNLOG>)) {
390                 if ($line =~ m/^($sha1_pattern)/) {
391                         push @revs, $1;
392                         $num -= 1;
393                 }
394         }
395         close MTNLOG || debug("mtn log exited $?");
397         my $automator = Monotone->new();
398         $automator->open(undef, $config{mtnrootdir});
400         while (@revs != 0) {
401                 my $rev = shift @revs;
402                 # first go through and figure out the messages, etc
404                 my $certs = [read_certs($automator, $rev)];
405                 
406                 my $user;
407                 my $when;
408                 my $committype;
409                 my (@pages, @message);
410                 
411                 foreach my $cert (@$certs) {
412                         if ($cert->{signature} eq "ok" &&
413                             $cert->{trust} eq "trusted") {
414                                 if ($cert->{name} eq "author") {
415                                         $user = $cert->{value};
416                                         # detect the source of the commit
417                                         # from the changelog
418                                         if ($cert->{key} eq $config{mtnkey}) {
419                                                 $committype = "web";
420                                         } else {
421                                                 $committype = "monotone";
422                                         }
423                                 } elsif ($cert->{name} eq "date") {
424                                         $when = time - str2time($cert->{value}, 'UTC');
425                                 } elsif ($cert->{name} eq "changelog") {
426                                         my $messageText = $cert->{value};
427                                         # split the changelog into multiple
428                                         # lines
429                                         foreach my $msgline (split(/\n/, $messageText)) {
430                                                 push @message, { line => $msgline };
431                                         }
432                                 }
433                         }
434                 }
435                 
436                 my @changed_files = get_changed_files($automator, $rev);
437                 my $file;
438                 
439                 foreach $file (@changed_files) {
440                         push @pages, {
441                                 page => pagename($file),
442                         } if length $file;
443                 }
444                 
445                 push @ret, {
446                         rev => $rev,
447                         user => $user,
448                         committype => $committype,
449                         when => $when,
450                         message => [@message],
451                         pages => [@pages],
452                 } if @pages;
453         }
455         $automator->close();
457         return @ret;
458 } #}}}
460 sub rcs_notify () { #{{{
461         debug("The monotone rcs_notify function is currently untested. Use at own risk!");
462         
463         if (! exists $ENV{REV}) {
464                 error(gettext("REV is not set, not running from mtn post-commit hook, cannot send notifications"));
465         }
466         if ($ENV{REV} !~ m/($sha1_pattern)/) { # sha1 is untainted now
467                 error(gettext("REV is not a valid revision identifier, cannot send notifications"));
468         }
469         my $rev = $1;
470         
471         check_config();
473         my $automator = Monotone->new();
474         $automator->open(undef, $config{mtnrootdir});
476         my $certs = [read_certs($automator, $rev)];
477         my $user;
478         my $message;
479         my $when;
481         foreach my $cert (@$certs) {
482                 if ($cert->{signature} eq "ok" && $cert->{trust} eq "trusted") {
483                         if ($cert->{name} eq "author") {
484                                 $user = $cert->{value};
485                         } elsif ($cert->{name} eq "date") {
486                                 $when = $cert->{value};
487                         } elsif ($cert->{name} eq "changelog") {
488                                 $message = $cert->{value};
489                         }
490                 }
491         }
492                 
493         my @changed_pages = get_changed_files($automator, $rev);
494         
495         $automator->close();
496         
497         require IkiWiki::UserInfo;
498         send_commit_mails(
499                 sub {
500                         return $message;
501                 },
502                 sub {
503                         `mtn --root=$config{mtnrootdir} au content_diff -r $rev`;
504                 },
505                 $user, @changed_pages);
506 } #}}}
508 sub rcs_getctime ($) { #{{{
509         my $file=shift;
511         check_config();
513         my $child = open(MTNLOG, "-|");
514         if (! $child) {
515                 exec("mtn", "log", "--root=$config{mtnrootdir}", "--no-graph",
516                      "--brief", $file) || error("mtn log $file failed to run");
517         }
519         my $firstRev;
520         while (<MTNLOG>) {
521                 if (/^($sha1_pattern)/) {
522                         $firstRev=$1;
523                 }
524         }
525         close MTNLOG || debug("mtn log $file exited $?");
527         if (! defined $firstRev) {
528                 debug "failed to parse mtn log for $file";
529                 return 0;
530         }
532         my $automator = Monotone->new();
533         $automator->open(undef, $config{mtnrootdir});
535         my $certs = [read_certs($automator, $firstRev)];
537         $automator->close();
539         my $date;
541         foreach my $cert (@$certs) {
542                 if ($cert->{signature} eq "ok" && $cert->{trust} eq "trusted") {
543                         if ($cert->{name} eq "date") {
544                                 $date = $cert->{value};
545                         }
546                 }
547         }
549         if (! defined $date) {
550                 debug "failed to find date cert for revision $firstRev when looking for creation time of $file";
551                 return 0;
552         }
554         $date=str2time($date, 'UTC');
555         debug("found ctime ".localtime($date)." for $file");
556         return $date;
557 } #}}}
561 # default mergerc content
562 __DATA__
563         function local_execute_redirected(stdin, stdout, stderr, path, ...)
564            local pid
565            local ret = -1
566            io.flush();
567            pid = spawn_redirected(stdin, stdout, stderr, path, unpack(arg))
568            if (pid ~= -1) then ret, pid = wait(pid) end
569            return ret
570         end
571         if (not execute_redirected) then -- use standard function if available
572            execute_redirected = local_execute_redirected
573         end
574         if (not mergers.fail) then -- use standard merger if available
575            mergers.fail = {
576               cmd = function (tbl) return false end,
577               available = function () return true end,
578               wanted = function () return true end
579            }
580         end
581         mergers.diffutils_force = {
582            cmd = function (tbl)
583               local ret = execute_redirected(
584                   "",
585                   tbl.outfile,
586                   "",
587                   "diff3",
588                   "--merge",
589                   "--show-overlap",
590                   "--label", string.format("[Yours]",     tbl.left_path ),
591                   "--label", string.format("[Original]",  tbl.anc_path  ),
592                   "--label", string.format("[Theirs]",    tbl.right_path),
593                   tbl.lfile,
594                   tbl.afile,
595                   tbl.rfile
596               )
597               if (ret > 1) then
598                  io.write(gettext("Error running GNU diffutils 3-way difference tool 'diff3'"))
599                  return false
600               end
601               return tbl.outfile
602            end,
603            available =
604               function ()
605                   return program_exists_in_path("diff3");
606               end,
607            wanted =
608               function ()
609                    return true
610               end
611         }
612 EOF