X-Git-Url: http://git.vanrenterghem.biz/git.ikiwiki.info.git/blobdiff_plain/bd7221ae5450e14e9641e01e0c2890a8f018ddb4..606be5f7a73895d8f987706546cc40da562aa384:/IkiWiki/Plugin/git.pm diff --git a/IkiWiki/Plugin/git.pm b/IkiWiki/Plugin/git.pm index 992c6226b..0c0e27521 100644 --- a/IkiWiki/Plugin/git.pm +++ b/IkiWiki/Plugin/git.pm @@ -5,11 +5,11 @@ use warnings; use strict; use IkiWiki; use Encode; +use URI::Escape q{uri_escape_utf8}; use open qw{:utf8 :std}; my $sha1_pattern = qr/[0-9a-fA-F]{40}/; # pattern to validate Git sha1sums my $dummy_commit_msg = 'dummy commit'; # message to skip in recent changes -my $no_chdir=0; sub import { hook(type => "checkconfig", id => "git", call => \&checkconfig); @@ -27,6 +27,8 @@ 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); } sub checkconfig () { @@ -41,11 +43,14 @@ sub checkconfig () { push @{$config{wrappers}}, { wrapper => $config{git_wrapper}, wrappermode => (defined $config{git_wrappermode} ? $config{git_wrappermode} : "06755"), + wrapper_background_command => $config{git_wrapper_background_command}, }; } if (defined $config{git_test_receive_wrapper} && - length $config{git_test_receive_wrapper}) { + length $config{git_test_receive_wrapper} && + defined $config{untrusted_committers} && + @{$config{untrusted_committers}}) { push @{$config{wrappers}}, { test_receive => 1, wrapper => $config{git_test_receive_wrapper}, @@ -78,6 +83,13 @@ sub getsetup () { safe => 0, # file rebuild => 0, }, + git_wrapper_background_command => { + type => "string", + example => "git push github", + description => "shell command for git_wrapper to run, in the background", + safe => 0, # command + rebuild => 0, + }, git_wrappermode => { type => "string", example => '06755', @@ -101,7 +113,7 @@ sub getsetup () { }, historyurl => { type => "string", - example => "http://git.example.com/gitweb.cgi?p=wiki.git;a=history;f=[[file]]", + example => "http://git.example.com/gitweb.cgi?p=wiki.git;a=history;f=[[file]];hb=HEAD", description => "gitweb url to show file history ([[file]] substituted)", safe => 1, rebuild => 1, @@ -139,11 +151,23 @@ sub genwrapper { } } +my $git_dir=undef; +my $prefix=undef; + +sub in_git_dir ($$) { + $git_dir=shift; + my @ret=shift->(); + $git_dir=undef; + $prefix=undef; + return @ret; +} + sub safe_git (&@) { - # Start a child process safely without resorting /bin/sh. - # Return command output or success state (in scalar context). + # Start a child process safely without resorting to /bin/sh. + # Returns command output (in list content) or success state + # (in scalar context), or runs the specified data handler. - my ($error_handler, @cmdline) = @_; + my ($error_handler, $data_handler, @cmdline) = @_; my $pid = open my $OUT, "-|"; @@ -152,9 +176,13 @@ sub safe_git (&@) { if (!$pid) { # In child. # Git commands want to be in wc. - if (! $no_chdir) { + if (! defined $git_dir) { chdir $config{srcdir} - or error("Cannot chdir to $config{srcdir}: $!"); + or error("cannot chdir to $config{srcdir}: $!"); + } + else { + chdir $git_dir + or error("cannot chdir to $git_dir: $!"); } exec @cmdline or error("Cannot exec '@cmdline': $!"); } @@ -171,7 +199,12 @@ sub safe_git (&@) { chomp; - push @lines, $_; + if (! defined $data_handler) { + push @lines, $_; + } + else { + last unless $data_handler->($_); + } } close $OUT; @@ -181,9 +214,9 @@ sub safe_git (&@) { return wantarray ? @lines : ($? == 0); } # Convenient wrappers. -sub run_or_die ($@) { safe_git(\&error, @_) } -sub run_or_cry ($@) { safe_git(sub { warn @_ }, @_) } -sub run_or_non ($@) { safe_git(undef, @_) } +sub run_or_die ($@) { safe_git(\&error, undef, @_) } +sub run_or_cry ($@) { safe_git(sub { warn @_ }, undef, @_) } +sub run_or_non ($@) { safe_git(undef, undef, @_) } sub merge_past ($$$) { @@ -280,8 +313,6 @@ sub merge_past ($$$) { return $conflict; } -{ -my $prefix; sub decode_git_file ($) { my $file=shift; @@ -303,7 +334,6 @@ sub decode_git_file ($) { return decode("utf8", $file); } -} sub parse_diff_tree ($) { # Parse the raw diff tree chunk and return the info hash. @@ -439,20 +469,17 @@ sub git_sha1 (;$) { # Ignore error since a non-existing file might be given. my ($sha1) = run_or_non('git', 'rev-list', '--max-count=1', 'HEAD', '--', $file); - if ($sha1) { + if (defined $sha1) { ($sha1) = $sha1 =~ m/($sha1_pattern)/; # sha1 is untainted now } - else { - debug("Empty sha1sum for '$file'."); - } - return defined $sha1 ? $sha1 : q{}; + return defined $sha1 ? $sha1 : ''; } sub rcs_update () { # Update working directory. if (length $config{gitorigin_branch}) { - run_or_cry('git', 'pull', $config{gitorigin_branch}); + run_or_cry('git', 'pull', '--prune', $config{gitorigin_branch}); } } @@ -480,16 +507,16 @@ sub rcs_commit (@) { return $conflict if defined $conflict; } - rcs_add($params{file}); - return rcs_commit_staged( - message => $params{message}, - session => $params{session}, - ); + return rcs_commit_helper(@_); } sub rcs_commit_staged (@) { # Commits all staged changes. Changes can be staged using rcs_add, # rcs_remove, and rcs_rename. + return rcs_commit_helper(@_); +} + +sub rcs_commit_helper (@) { my %params=@_; my %env=%ENV; @@ -509,6 +536,8 @@ sub rcs_commit_staged (@) { } if (defined $params{session}->param("nickname")) { $u=encode_utf8($params{session}->param("nickname")); + $u=~s/\s+/_/g; + $u=~s/[^-_0-9[:alnum:]]+//g; } if (defined $u) { $ENV{GIT_AUTHOR_EMAIL}="$u\@web"; @@ -528,10 +557,12 @@ sub rcs_commit_staged (@) { $params{message}.="."; } } - push @opts, '-q'; - # git commit returns non-zero if file has not been really changed. - # so we should ignore its exit status (hence run_or_non). - if (run_or_non('git', 'commit', @opts, '-m', $params{message})) { + if (exists $params{file}) { + push @opts, '--', $params{file}; + } + # git commit returns non-zero if nothing really changed. + # So we should ignore its exit status (hence run_or_non). + if (run_or_non('git', 'commit', '-m', $params{message}, '-q', @opts)) { if (length $config{gitorigin_branch}) { run_or_cry('git', 'push', $config{gitorigin_branch}); } @@ -584,9 +615,10 @@ sub rcs_recentchanges ($) { my @pages; foreach my $detail (@{ $ci->{'details'} }) { my $file = $detail->{'file'}; + my $efile = uri_escape_utf8($file); my $diffurl = defined $config{'diffurl'} ? $config{'diffurl'} : ""; - $diffurl =~ s/\[\[file\]\]/$file/go; + $diffurl =~ s/\[\[file\]\]/$efile/go; $diffurl =~ s/\[\[sha1_parent\]\]/$ci->{'parent'}/go; $diffurl =~ s/\[\[sha1_from\]\]/$detail->{'sha1_from'}/go; $diffurl =~ s/\[\[sha1_to\]\]/$detail->{'sha1_to'}/go; @@ -643,15 +675,19 @@ sub rcs_recentchanges ($) { return @rets; } -sub rcs_diff ($) { +sub rcs_diff ($;$) { my $rev=shift; + my $maxlines=shift; my ($sha1) = $rev =~ /^($sha1_pattern)$/; # untaint my @lines; - foreach my $line (run_or_non("git", "show", $sha1)) { - if (@lines || $line=~/^diff --git/) { - push @lines, $line."\n"; - } - } + my $addlines=sub { + my $line=shift; + return if defined $maxlines && @lines == $maxlines; + push @lines, $line."\n" + if (@lines || $line=~/^diff --git/); + return 1; + }; + safe_git(undef, $addlines, "git", "show", $sha1); if (wantarray) { return @lines; } @@ -670,7 +706,7 @@ sub findtimes ($$) { if (! keys %time_cache) { my $date; foreach my $line (run_or_die('git', 'log', - '--pretty=format:%ct', + '--pretty=format:%at', '--name-only', '--relative')) { if (! defined $date && $line =~ /^(\d+)$/) { $date=$line; @@ -706,10 +742,15 @@ sub rcs_getmtime ($) { return findtimes($file, 0); } -sub rcs_receive () { +{ +my $ret; +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 @$ret if defined $ret; + my $subdir=""; my $dir=$config{srcdir}; while (! -d "$dir/.git") { @@ -720,83 +761,140 @@ sub rcs_receive () { } } + $ret=[$subdir, $dir]; + return @$ret; +} + +} + +sub git_parse_changes { + my $reverted = shift; + my @changes = @_; + + my ($subdir, $rootdir) = 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= $reverted ? "remove" : "add"; + $mode=$detail->{'mode_to'}; + } + elsif ($detail->{'status'} =~ /^[DAM]+\d*/) { + $action= $reverted ? "add" : "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(undef, UNLINK => 1); + my $cmd = "cd $git_dir && ". + "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. - $no_chdir=1; - my @changes=git_commit_info($oldrev."..".$newrev); - $no_chdir=0; - - 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); - } + # (Also, if a subdir is involved, we don't want to chdir to + # it and only see changes in it.) + # The pre-receive hook already puts us in the right place. + in_git_dir(".", sub { + push @rets, git_parse_changes(0, git_commit_info($oldrev."..".$newrev)); + }); + } - 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"); - } - } + return reverse @rets; +} - push @rets, { - file => $file, - action => $action, - path => $path, - }; - } +sub rcs_preprevert ($) { + my $rev=shift; + my ($sha1) = $rev =~ /^($sha1_pattern)$/; # untaint + + # Examine changes from root of git repo, not from any subdir, + # in order to see all changes. + my ($subdir, $rootdir) = git_find_root(); + in_git_dir($rootdir, sub { + my @commits=git_commit_info($sha1, 1); + + if (! @commits) { + error "unknown commit"; # just in case } - } - return reverse @rets; + # git revert will fail on merge commits. Add a nice message. + if (exists $commits[0]->{parents} && + @{$commits[0]->{parents}} > 1) { + error gettext("you are not allowed to revert a merge"); + } + + git_parse_changes(1, @commits); + }); +} + +sub rcs_revert ($) { + # Try to revert the given rev; returns undef on _success_. + my $rev = shift; + my ($sha1) = $rev =~ /^($sha1_pattern)$/; # untaint + + if (run_or_non('git', 'revert', '--no-commit', $sha1)) { + return undef; + } + else { + run_or_die('git', 'reset', '--hard'); + return sprintf(gettext("Failed to revert commit %s"), $sha1); + } } 1