]> sipb.mit.edu Git - ikiwiki.git/blobdiff - ikiwiki
web commit by joey: conflict test
[ikiwiki.git] / ikiwiki
diff --git a/ikiwiki b/ikiwiki
index 1c16b67258d1cc9fb776df620d12b6cd53095695..78c706c31bea31afabd8479039d76122b0beeead 100755 (executable)
--- a/ikiwiki
+++ b/ikiwiki
@@ -10,7 +10,8 @@ use Getopt::Long;
 
 my (%links, %oldlinks, %oldpagemtime, %renderedfiles, %pagesources);
 
-my %config=( #{{{
+# Holds global config settings, also used by some modules.
+our %config=( #{{{
        wiki_file_prune_regexp => qr{((^|/).svn/|\.\.|^\.|\/\.|\.html?$)},
        wiki_link_regexp => qr/\[\[([^\s]+)\]\]/,
        wiki_file_regexp => qr/(^[-A-Za-z0-9_.:\/+]+$)/,
@@ -165,14 +166,17 @@ sub writefile ($$) { #{{{
        close OUT;
 } #}}}
 
-sub findlinks ($) { #{{{
+sub findlinks ($$) { #{{{
        my $content=shift;
+       my $page=shift;
 
        my @links;
        while ($content =~ /(?<!\\)$config{wiki_link_regexp}/g) {
                push @links, lc($1);
        }
-       return @links;
+       # Discussion links are a special case since they're not in the text
+       # of the page, but on its template.
+       return @links, "$page/discussion";
 } #}}}
 
 sub bestlink ($$) { #{{{
@@ -243,10 +247,10 @@ sub htmllink { #{{{
 
 sub linkify ($$) { #{{{
        my $content=shift;
-       my $file=shift;
+       my $page=shift;
 
        $content =~ s{(\\?)$config{wiki_link_regexp}}{
-               $1 ? "[[$2]]" : htmllink(pagename($file), $2)
+               $1 ? "[[$2]]" : htmllink($page, $2)
        }eg;
        
        return $content;
@@ -368,7 +372,7 @@ sub check_overwrite ($$) { #{{{
                        ", before, so not rendering from $src");
        }
 } #}}}
-               
+
 sub render ($) { #{{{
        my $file=shift;
        
@@ -377,9 +381,9 @@ sub render ($) { #{{{
        if ($type ne 'unknown') {
                my $page=pagename($file);
                
-               $links{$page}=[findlinks($content)];
+               $links{$page}=[findlinks($content, $page)];
                
-               $content=linkify($content, $file);
+               $content=linkify($content, $page);
                $content=htmlize($type, $content);
                $content=finalize($content, $page);
                
@@ -397,6 +401,29 @@ sub render ($) { #{{{
        }
 } #}}}
 
+sub lockwiki () { #{{{
+       # Take an exclusive lock on the wiki to prevent multiple concurrent
+       # run issues. The lock will be dropped on program exit.
+       if (! -d "$config{srcdir}/.ikiwiki") {
+               mkdir("$config{srcdir}/.ikiwiki");
+       }
+       open(WIKILOCK, ">$config{srcdir}/.ikiwiki/lockfile") || error ("cannot write to lockfile: $!");
+       if (! flock(WIKILOCK, 2 | 4)) {
+               debug("wiki seems to be locked, waiting for lock");
+               my $wait=600; # arbitrary, but don't hang forever to 
+                             # prevent process pileup
+               for (1..600) {
+                       return if flock(WIKILOCK, 2 | 4);
+                       sleep 1;
+               }
+               error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)");
+       }
+} #}}}
+
+sub unlockwiki () { #{{{
+       close WIKILOCK;
+} #}}}
+
 sub loadindex () { #{{{
        open (IN, "$config{srcdir}/.ikiwiki/index") || return;
        while (<IN>) {
@@ -434,19 +461,59 @@ sub rcs_update () { #{{{
        }
 } #}}}
 
-sub rcs_commit ($) { #{{{
+sub rcs_prepedit ($) { #{{{
+       # Prepares to edit a file under revision control. Returns a token
+       # that must be passed into rcs_commit when the file is ready
+       # for committing.
+       # The file is relative to the srcdir.
+       my $file=shift;
+       
+       if (-d "$config{srcdir}/.svn") {
+               # For subversion, return the revision of the file when
+               # editing begins.
+               my $rev=svn_info("Revision", "$config{srcdir}/$file");
+               return defined $rev ? $rev : "";
+       }
+} #}}}
+
+sub rcs_commit ($$$) { #{{{
+       # Tries to commit the page; returns undef on _success_ and
+       # a version of the page with the rcs's conflict markers on failure.
+       # The file is relative to the srcdir.
+       my $file=shift;
        my $message=shift;
+       my $rcstoken=shift;
 
        if (-d "$config{srcdir}/.svn") {
+               # Check to see if the page has been changed by someone
+               # else since rcs_prepedit was called.
+               my $oldrev=int($rcstoken);
+               my $rev=svn_info("Revision", "$config{srcdir}/$file");
+               if ($rev != $oldrev) {
+                       # Merge their changes into the file that we've
+                       # changed.
+                       if (system("svn", "merge", "--quiet", "-r$oldrev:$rev",
+                                  "$config{srcdir}/$file") != 0) {
+                               warn("svn merge -r$oldrev:$rev failed\n");
+                       }
+               }
+
                if (system("svn", "commit", "--quiet", "-m",
                           possibly_foolish_untaint($message),
-                          $config{srcdir}) != 0) {
+                          "$config{srcdir}/$file") != 0) {
                        warn("svn commit failed\n");
+                       my $conflict=readfile("$config{srcdir}/$file");
+                       if (system("svn", "revert", "--quiet", "$config{srcdir}/$file") != 0) {
+                               warn("svn revert failed\n");
+                       }
+                       return $conflict;
                }
        }
+       return undef # success
 } #}}}
 
 sub rcs_add ($) { #{{{
+       # filename is relative to the root of the srcdir
        my $file=shift;
 
        if (-d "$config{srcdir}/.svn") {
@@ -462,16 +529,25 @@ sub rcs_add ($) { #{{{
        }
 } #}}}
 
+sub svn_info ($$) { #{{{
+       my $file=shift;
+       my $field=shift;
+
+       my $info=`LANG=C svn info $file`;
+       my ($ret)=$info=~/^$field: (.*)$/m;
+       return $ret;
+} #}}}
+
 sub rcs_recentchanges ($) { #{{{
        my $num=shift;
        my @ret;
        
+       eval q{use CGI 'escapeHTML'};
        eval q{use Date::Parse};
        eval q{use Time::Duration};
        
        if (-d "$config{srcdir}/.svn") {
-               my $info=`LANG=C svn info $config{srcdir}`;
-               my ($svn_url)=$info=~/^URL: (.*)$/m;
+               my $svn_url=svn_info("URL", $config{srcdir});
 
                # FIXME: currently assumes that the wiki is somewhere
                # under trunk in svn, doesn't support other layouts.
@@ -481,7 +557,7 @@ sub rcs_recentchanges ($) { #{{{
                my $infoline=qr/^r(\d+)\s+\|\s+([^\s]+)\s+\|\s+(\d+-\d+-\d+\s+\d+:\d+:\d+\s+[-+]?\d+).*/;
                my $state='start';
                my ($rev, $user, $when, @pages, @message);
-               foreach (`LANG=C svn log -v '$svn_url'`) {
+               foreach (`LANG=C svn log --limit $num -v '$svn_url'`) {
                        chomp;
                        if ($state eq 'start' && /$div/) {
                                $state='header';
@@ -521,7 +597,7 @@ sub rcs_recentchanges ($) { #{{{
                                @pages=@message=();
                        }
                        elsif ($state eq 'body') {
-                               push @message, {line => $_},
+                               push @message, {line => escapeHTML($_)},
                        }
                }
        }
@@ -540,10 +616,9 @@ sub prune ($) { #{{{
 } #}}}
 
 sub refresh () { #{{{
-       # Find existing pages.
+       # find existing pages
        my %exists;
        my @files;
-       
        eval q{use File::Find};
        find({
                no_chdir => 1,
@@ -553,7 +628,7 @@ sub refresh () { #{{{
                                $File::Find::prune=1;
                                use warnings "all";
                        }
-                       elsif (! -d $_) {
+                       elsif (! -d $_ && ! -l $_) {
                                my ($f)=/$config{wiki_file_regexp}/; # untaint
                                if (! defined $f) {
                                        warn("skipping bad filename $_\n");
@@ -584,7 +659,7 @@ sub refresh () { #{{{
        foreach my $page (keys %oldpagemtime) {
                if (! $exists{$page}) {
                        debug("removing old page $page");
-                       push @del, $renderedfiles{$page};
+                       push @del, $pagesources{$page};
                        prune($config{destdir}."/".$renderedfiles{$page});
                        delete $renderedfiles{$page};
                        $oldpagemtime{$page}=0;
@@ -636,8 +711,7 @@ FILE:               foreach my $file (@files) {
                foreach my $file (keys %rendered, @del) {
                        my $page=pagename($file);
                        if (exists $links{$page}) {
-                               foreach my $link (@{$links{$page}}) {
-                                       $link=bestlink($page, $link);
+                               foreach my $link (map { bestlink($page, $_) } @{$links{$page}}) {
                                        if (length $link &&
                                            ! exists $oldlinks{$page} ||
                                            ! grep { $_ eq $link } @{$oldlinks{$page}}) {
@@ -646,8 +720,7 @@ FILE:               foreach my $file (@files) {
                                }
                        }
                        if (exists $oldlinks{$page}) {
-                               foreach my $link (@{$oldlinks{$page}}) {
-                                       $link=bestlink($page, $link);
+                               foreach my $link (map { bestlink($page, $_) } @{$oldlinks{$page}}) {
                                        if (length $link &&
                                            ! exists $links{$page} ||
                                            ! grep { $_ eq $link } @{$links{$page}}) {
@@ -978,7 +1051,7 @@ sub cgi_editpage ($$) { #{{{
 
        eval q{use CGI::FormBuilder};
        my $form = CGI::FormBuilder->new(
-               fields => [qw(do from page content comments)],
+               fields => [qw(do rcsinfo from page content comments)],
                header => 1,
                method => 'POST',
                validate => {
@@ -991,6 +1064,7 @@ sub cgi_editpage ($$) { #{{{
                table => 0,
                template => "$config{templatedir}/editpage.tmpl"
        );
+       my @buttons=("Save Page", "Preview", "Cancel");
        
        my ($page)=$form->param('page')=~/$config{wiki_file_regexp}/;
        if (! defined $page || ! length $page || $page ne $q->param('page') ||
@@ -998,6 +1072,13 @@ sub cgi_editpage ($$) { #{{{
                error("bad page name");
        }
        $page=lc($page);
+       
+       my $file=$page.$config{default_pageext};
+       my $newfile=1;
+       if (exists $pagesources{lc($page)}) {
+               $file=$pagesources{lc($page)};
+               $newfile=0;
+       }
 
        $form->field(name => "do", type => 'hidden');
        $form->field(name => "from", type => 'hidden');
@@ -1005,12 +1086,27 @@ sub cgi_editpage ($$) { #{{{
        $form->field(name => "comments", type => "text", size => 80);
        $form->field(name => "content", type => "textarea", rows => 20,
                cols => 80);
+       if (! $form->submitted) {
+               $form->field(name => "rcsinfo", type => 'hidden',
+                       value => rcs_prepedit($file), force => 1);
+       }
        
        if ($form->submitted eq "Cancel") {
                print $q->redirect("$config{url}/".htmlpage($page));
                return;
        }
-       if (! $form->submitted || ! $form->validate) {
+       elsif ($form->submitted eq "Preview") {
+               $form->tmpl_param("page_preview",
+                       htmlize($config{default_pageext},
+                               linkify($form->field('content'), $page)));
+       }
+       else {
+               $form->tmpl_param("page_preview", "");
+       }
+       $form->tmpl_param("page_conflict", "");
+       
+       if (! $form->submitted || $form->submitted eq "Preview" || 
+           ! $form->validate) {
                if ($form->field("do") eq "create") {
                        if (exists $pagesources{lc($page)}) {
                                # hmm, someone else made the page in the
@@ -1048,31 +1144,26 @@ sub cgi_editpage ($$) { #{{{
                        $form->title("creating $page");
                }
                elsif ($form->field("do") eq "edit") {
-                       my $content="";
-                       if (exists $pagesources{lc($page)}) {
-                               $content=readfile("$config{srcdir}/$pagesources{lc($page)}");
-                               $content=~s/\n/\r\n/g;
+                       if (! length $form->field('content')) {
+                               my $content="";
+                               if (exists $pagesources{lc($page)}) {
+                                               $content=readfile("$config{srcdir}/$pagesources{lc($page)}");
+                                       $content=~s/\n/\r\n/g;
+                               }
+                               $form->field(name => "content", value => $content,
+                                       force => 1);
                        }
                        $form->tmpl_param("page_select", 0);
-                       $form->field(name => "content", value => $content,
-                               force => 1);
                        $form->field(name => "page", type => 'hidden');
                        $form->title("editing $page");
                }
                
                $form->tmpl_param("can_commit", $config{svn});
                $form->tmpl_param("indexlink", indexlink());
-               print $form->render(submit => ["Save Page", "Cancel"]);
+               print $form->render(submit => \@buttons);
        }
        else {
                # save page
-               my $file=$page.$config{default_pageext};
-               my $newfile=1;
-               if (exists $pagesources{lc($page)}) {
-                       $file=$pagesources{lc($page)};
-                       $newfile=0;
-               }
-               
                my $content=$form->field('content');
                $content=~s/\r\n/\n/g;
                $content=~s/\r/\n/g;
@@ -1094,9 +1185,20 @@ sub cgi_editpage ($$) { #{{{
                        if ($newfile) {
                                rcs_add($file);
                        }
+                       # prevent deadlock with post-commit hook
+                       unlockwiki();
                        # presumably the commit will trigger an update
                        # of the wiki
-                       rcs_commit($message);
+                       my $conflict=rcs_commit($file, $message,
+                               $form->field("rcsinfo"));
+               
+                       if (defined $conflict) {
+                               $form->tmpl_param("page_conflict", 1);
+                               $form->field("content", $conflict);
+                               $form->field("do", "edit)");
+                               print $form->render(submit => \@buttons);
+                               return;
+                       }
                }
                else {
                        loadindex();
@@ -1164,25 +1266,13 @@ sub setup () { # {{{
        ($code)=$code=~/(.*)/s;
        close IN;
 
-       my (%setup);
        eval $code;
        error($@) if $@;
-       
-       gen_wrapper(%config, %setup, %{$setup{cgiwrapper}}) if $setup{cgiwrapper};
-       gen_wrapper(%config, %setup, %{$setup{svnwrapper}}) if $setup{svnwrapper};
-       
-       print "$setup{wikiname} setup complete, now forcing a rebuild\n";
-       foreach my $c (keys %setup) {
-               $config{$c}=possibly_foolish_untaint($setup{$c})
-                       if defined $setup{$c} && ! ref $setup{$c};
-       }
-       $config{rebuild}=1;
-       refresh();
-       saveindex();
        exit;
 } #}}}
 
 # main {{{
+lockwiki();
 setup() if $config{setup};
 if ($config{wrapper}) {
        gen_wrapper(%config);