diff options
-rw-r--r-- | IkiWiki.pm | 12 | ||||
-rw-r--r-- | IkiWiki/Plugin/git.pm | 208 | ||||
-rw-r--r-- | IkiWiki/Plugin/recentchanges.pm | 73 | ||||
-rw-r--r-- | IkiWiki/Receive.pm | 32 | ||||
-rw-r--r-- | doc/templates.mdwn | 2 | ||||
-rw-r--r-- | doc/wikiicons/revert.png | bin | 0 -> 840 bytes | |||
-rw-r--r-- | templates/change.tmpl | 2 | ||||
-rw-r--r-- | templates/revert.tmpl | 14 |
8 files changed, 260 insertions, 83 deletions
diff --git a/IkiWiki.pm b/IkiWiki.pm index 66ae86809..aa4dccac3 100644 --- a/IkiWiki.pm +++ b/IkiWiki.pm @@ -1941,6 +1941,18 @@ sub rcs_receive () { $hooks{rcs}{rcs_receive}{call}->(); } +sub rcs_preprevert (@) { + $hooks{rcs}{rcs_preprevert}{call}->(@_); +} + +sub rcs_revert (@) { + $hooks{rcs}{rcs_revert}{call}->(@_); +} + +sub rcs_showpatch (@) { + $hooks{rcs}{rcs_showpatch}{call}->(@_); +} + sub add_depends ($$;$) { my $page=shift; my $pagespec=shift; diff --git a/IkiWiki/Plugin/git.pm b/IkiWiki/Plugin/git.pm index fd57ce1e4..457975d95 100644 --- a/IkiWiki/Plugin/git.pm +++ b/IkiWiki/Plugin/git.pm @@ -27,6 +27,10 @@ sub import { hook(type => "rcs", id => "rcs_getctime", call => \&rcs_getctime); hook(type => "rcs", id => "rcs_getmtime", call => \&rcs_getmtime); hook(type => "rcs", id => "rcs_receive", call => \&rcs_receive); + hook(type => "rcs", id => "rcs_preprevert", call => \&rcs_preprevert); + hook(type => "rcs", id => "rcs_revert", call => \&rcs_revert); + hook(type => "rcs", id => "rcs_showpatch", call => \&rcs_showpatch); + hook(type => "rcs", id => "rcs_revert", call => \&rcs_revert); } sub checkconfig () { @@ -718,97 +722,155 @@ sub rcs_getmtime ($) { return findtimes($file, 0); } -sub rcs_receive () { - # The wiki may not be the only thing in the git repo. - # Determine if it is in a subdirectory by examining the srcdir, - # and its parents, looking for the .git directory. - my $subdir=""; - my $dir=$config{srcdir}; - while (! -d "$dir/.git") { - $subdir=IkiWiki::basename($dir)."/".$subdir; - $dir=IkiWiki::dirname($dir); - if (! length $dir) { - error("cannot determine root of git repo"); - } - } +{ +my $git_root; +sub git_find_root { + # The wiki may not be the only thing in the git repo. + # Determine if it is in a subdirectory by examining the srcdir, + # and its parents, looking for the .git directory. + + return $git_root if $git_root; + + my $subdir=""; + my $dir=$config{srcdir}; + while (! -d "$dir/.git") { + $subdir=IkiWiki::basename($dir)."/".$subdir; + $dir=IkiWiki::dirname($dir); + if (! length $dir) { + error("cannot determine root of git repo"); + } + } + + return $subdir; +} +} + +sub git_parse_changes { + my @changes = @_; + + my $subdir = git_find_root(); + my @rets; + foreach my $ci (@changes) { + foreach my $detail (@{ $ci->{'details'} }) { + my $file = $detail->{'file'}; + + # check that all changed files are in the + # subdir + if (length $subdir && + ! ($file =~ s/^\Q$subdir\E//)) { + error sprintf(gettext("you are not allowed to change %s"), $file); + } + + my ($action, $mode, $path); + if ($detail->{'status'} =~ /^[M]+\d*$/) { + $action="change"; + $mode=$detail->{'mode_to'}; + } + elsif ($detail->{'status'} =~ /^[AM]+\d*$/) { + $action="add"; + $mode=$detail->{'mode_to'}; + } + elsif ($detail->{'status'} =~ /^[DAM]+\d*/) { + $action="remove"; + $mode=$detail->{'mode_from'}; + } + else { + error "unknown status ".$detail->{'status'}; + } + + # test that the file mode is ok + if ($mode !~ /^100[64][64][64]$/) { + error sprintf(gettext("you cannot act on a file with mode %s"), $mode); + } + if ($action eq "change") { + if ($detail->{'mode_from'} ne $detail->{'mode_to'}) { + error gettext("you are not allowed to change file modes"); + } + } + + # extract attachment to temp file + if (($action eq 'add' || $action eq 'change') && + ! pagetype($file)) { + + eval q{use File::Temp}; + die $@ if $@; + my $fh; + ($fh, $path)=File::Temp::tempfile("XXXXXXXXXX", UNLINK => 1); + # Ensure we run this in the right place, see comments in rcs_receive. + my $cmd = ($no_chdir ? '' : "cd $config{srcdir} && ") + . "git show $detail->{sha1_to} > '$path'"; + if (system($cmd) != 0) { + error("failed writing temp file '$path'."); + } + } + + push @rets, { + file => $file, + action => $action, + path => $path, + }; + } + } + + return @rets; +} +sub rcs_receive () { my @rets; while (<>) { chomp; my ($oldrev, $newrev, $refname) = split(' ', $_, 3); - + # only allow changes to gitmaster_branch if ($refname !~ /^refs\/heads\/\Q$config{gitmaster_branch}\E$/) { error sprintf(gettext("you are not allowed to change %s"), $refname); } - + # Avoid chdir when running git here, because the changes # are in the master git repo, not the srcdir repo. - # The pre-recieve hook already puts us in the right place. + # The pre-receive hook already puts us in the right place. $no_chdir=1; - my @changes=git_commit_info($oldrev."..".$newrev); + push @rets, git_parse_changes(git_commit_info($oldrev."..".$newrev)); $no_chdir=0; + } - foreach my $ci (@changes) { - foreach my $detail (@{ $ci->{'details'} }) { - my $file = $detail->{'file'}; + return reverse @rets; +} - # check that all changed files are in the - # subdir - if (length $subdir && - ! ($file =~ s/^\Q$subdir\E//)) { - error sprintf(gettext("you are not allowed to change %s"), $file); - } +sub rcs_preprevert (@) { + # Determine what the effects are of reverting the patch with the + # ID given by 'rev'. Returns the same structure as rcs_receive. + # Note test_changes expects 'cgi' and 'session' parameters. + my %params = @_; + my $rev = $params{rev}; - my ($action, $mode, $path); - if ($detail->{'status'} =~ /^[M]+\d*$/) { - $action="change"; - $mode=$detail->{'mode_to'}; - } - elsif ($detail->{'status'} =~ /^[AM]+\d*$/) { - $action="add"; - $mode=$detail->{'mode_to'}; - } - elsif ($detail->{'status'} =~ /^[DAM]+\d*/) { - $action="remove"; - $mode=$detail->{'mode_from'}; - } - else { - error "unknown status ".$detail->{'status'}; - } - - # test that the file mode is ok - if ($mode !~ /^100[64][64][64]$/) { - error sprintf(gettext("you cannot act on a file with mode %s"), $mode); - } - if ($action eq "change") { - if ($detail->{'mode_from'} ne $detail->{'mode_to'}) { - error gettext("you are not allowed to change file modes"); - } - } - - # extract attachment to temp file - if (($action eq 'add' || $action eq 'change') && - ! pagetype($file)) { - eval q{use File::Temp}; - die $@ if $@; - my $fh; - ($fh, $path)=File::Temp::tempfile("XXXXXXXXXX", UNLINK => 1); - if (system("git show ".$detail->{sha1_to}." > '$path'") != 0) { - error("failed writing temp file"); - } - } + require IkiWiki::Receive; + IkiWiki::Receive::test_changes(%params, changes => [git_parse_changes(git_commit_info($rev, 1))]); +} - push @rets, { - file => $file, - action => $action, - path => $path, - }; - } - } - } +sub rcs_revert (@) { + # Try to revert the given patch; returns undef on _success_. + # Same parameters as rcs_commit_staged + 'rev', the patch ID to be + # reverted. + my %params = @_; + my $rev = $params{rev}; + + if(run_or_non('git', 'revert', '--no-commit', $rev)) { + debug "Committing revert for patch '$rev'."; + rcs_commit_staged(message => "This reverts commit $rev", @_); + } else { + # No idea what is actually getting reverted, so all we can do is say we failed. + run_or_die('git', 'reset', '--hard'); + return "Failed to revert patch $rev."; + } +} - return reverse @rets; +sub rcs_showpatch (@) { + # Show the patch with the given revision id. + my %params = @_; + my $rev = $params{rev}; + + return join "\n", run_or_die('git', 'show', $rev); } 1 diff --git a/IkiWiki/Plugin/recentchanges.pm b/IkiWiki/Plugin/recentchanges.pm index 758b98348..9a62cb243 100644 --- a/IkiWiki/Plugin/recentchanges.pm +++ b/IkiWiki/Plugin/recentchanges.pm @@ -13,6 +13,7 @@ sub import { hook(type => "refresh", id => "recentchanges", call => \&refresh); hook(type => "pagetemplate", id => "recentchanges", call => \&pagetemplate); hook(type => "htmlize", id => "_change", call => \&htmlize); + hook(type => "sessioncgi", id => "recentchanges", call => \&sessioncgi); # Load goto to fix up links from recentchanges IkiWiki::loadplugin("goto"); } @@ -60,6 +61,71 @@ sub refresh ($) { } } +sub confirmation_form { + my ($q, $session, $rev) = @_; + + eval q{use CGI::FormBuilder}; + error($@) if $@; + my $f = CGI::FormBuilder->new( + name => "revert", + header => 0, + charset => "utf-8", + method => 'POST', + javascript => 0, + params => $q, + action => $config{cgiurl}, + stylesheet => 1, + template => { template('revert.tmpl') }, + ); + + $f->field(name => "sid", type => "hidden", value => $session->id, + force => 1); + $f->field(name => "do", type => "hidden", value => "revert", force => 1); + + return $f, ["Revert", "Cancel"]; +} + +sub sessioncgi ($$) { + my ($q, $session) = @_; + my $do = $q->param('do'); + my $rev = $q->param('rev'); + + return unless $do eq 'revert' && $rev; + + IkiWiki::rcs_preprevert(cgi => $q, session => $session, rev => $rev); + + my ($form, $buttons) = confirmation_form($q, $session); + IkiWiki::decode_form_utf8($form); + + if($form->submitted eq 'Revert' && $form->validate) { + IkiWiki::checksessionexpiry($q, $session, $q->param('sid')); + + IkiWiki::disable_commit_hook(); + my $r = IkiWiki::rcs_revert( + session => $session, + rev => $rev); + IkiWiki::enable_commit_hook(); + + if($r) { + die "Revert '$rev' failed."; + } else { + require IkiWiki::Render; + IkiWiki::refresh(); + IkiWiki::saveindex(); + } + } else { + $form->title(sprintf(gettext("confirm reversion of %s"), $rev)); + my $patch_contents = IkiWiki::rcs_showpatch(rev => $rev); + $form->tmpl_param(patch_contents => encode_entities($patch_contents)); + $form->field(name => "rev", type => "hidden", value => $rev, force => 1); + IkiWiki::showform($form, $buttons, $session, $q); + exit 0; + } + + IkiWiki::redirect($q, urlto($config{recentchangespage}, '')); + exit 0; +} + # Enable the recentchanges link. sub pagetemplate (@) { my %params=@_; @@ -114,6 +180,13 @@ sub store ($$$) { ]; push @{$change->{pages}}, { link => '...' } if $is_excess; + if (length $config{cgiurl}) { + $change->{reverturl} = IkiWiki::cgiurl( + do => "revert", + rev => $change->{rev} + ); + } + $change->{author}=$change->{user}; my $oiduser=eval { IkiWiki::openiduser($change->{user}) }; if (defined $oiduser) { diff --git a/IkiWiki/Receive.pm b/IkiWiki/Receive.pm index e77c477a9..48228e5f0 100644 --- a/IkiWiki/Receive.pm +++ b/IkiWiki/Receive.pm @@ -48,10 +48,10 @@ EOF sub test () { exit 0 if trusted(); - + IkiWiki::lockwiki(); IkiWiki::loadindex(); - + # Dummy up a cgi environment to use when calling check_canedit # and friends. eval q{use CGI}; @@ -72,10 +72,22 @@ sub test () { regdate => time, }) || error("failed adding user"); } - - my %newfiles; - foreach my $change (IkiWiki::rcs_receive()) { + test_changes(cgi => $cgi, + session => $session, + changes => [IkiWiki::rcs_receive()] + ); + exit 0; +} + +sub test_changes { + my %params = @_; + my $cgi = $params{cgi}; + my $session = $params{session}; + my @changes = @{$params{changes}}; + + my %newfiles; + foreach my $change (@changes) { # This untaint is safe because we check file_pruned and # wiki_file_regexp. my ($file)=$change->{file}=~/$config{wiki_file_regexp}/; @@ -87,7 +99,7 @@ sub test () { my $type=pagetype($file); my $page=pagename($file) if defined $type; - + if ($change->{action} eq 'add') { $newfiles{$file}=1; } @@ -104,6 +116,10 @@ sub test () { IkiWiki::check_canedit($file, $cgi, $session); next; } + else { + use Data::Dumper; + die "fall through test_changes add: " . Data::Dumper::Dumper($change); + } } } elsif ($change->{action} eq 'remove') { @@ -125,11 +141,9 @@ sub test () { else { error "unknown action ".$change->{action}; } - + error sprintf(gettext("you are not allowed to change %s"), $file); } - - exit 0; } 1 diff --git a/doc/templates.mdwn b/doc/templates.mdwn index bfb6a439a..4fd2bf501 100644 --- a/doc/templates.mdwn +++ b/doc/templates.mdwn @@ -74,7 +74,7 @@ html out of ikiwiki and in the templates. * `editpage.tmpl`, `editconflict.tmpl`, `editcreationconflict.tmpl`, `editfailedsave.tmpl`, `editpagegone.tmpl`, `pocreatepage.tmpl`, `editcomment.tmpl` `commentmoderation.tmpl`, `renamesummary.tmpl`, - `passwordmail.tmpl`, `openid-selector.tmpl` - Parts of ikiwiki's user + `passwordmail.tmpl`, `openid-selector.tmpl`, `revert.tmpl` - Parts of ikiwiki's user interface; do not normally need to be customised. [[!meta robots="noindex, follow"]] diff --git a/doc/wikiicons/revert.png b/doc/wikiicons/revert.png Binary files differnew file mode 100644 index 000000000..9036046e9 --- /dev/null +++ b/doc/wikiicons/revert.png diff --git a/templates/change.tmpl b/templates/change.tmpl index 671b9e483..9cf40777b 100644 --- a/templates/change.tmpl +++ b/templates/change.tmpl @@ -28,6 +28,8 @@ <span class="committype"><TMPL_VAR COMMITTYPE></span> <span class="desc"><br />Date:</span> <span class="changedate"><TMPL_VAR COMMITDATE></span> +<span class="desc"><br /></span> +<span class="revert"><a href="<TMPL_VAR REVERTURL>" rel="nofollow">[[revert|wikiicons/revert.png]]</a></span> </div> <div class="changelog"> <TMPL_LOOP MESSAGE> diff --git a/templates/revert.tmpl b/templates/revert.tmpl new file mode 100644 index 000000000..03cd0838d --- /dev/null +++ b/templates/revert.tmpl @@ -0,0 +1,14 @@ +<pre> +<TMPL_VAR patch_contents> +</pre> +<TMPL_VAR FORM-START> +<div> + <TMPL_VAR FIELD-DO> + <TMPL_VAR FIELD-SID> + <TMPL_VAR FIELD-REV> +</div> +<div class="revert buttons"> + <TMPL_VAR form-submit> + <TMPL_VAR form-cancel> +</div> +<TMPL_VAR FORM-END> |