diff options
Diffstat (limited to 'src/lib/Gitolite/Hooks')
-rw-r--r-- | src/lib/Gitolite/Hooks/PostUpdate.pm | 75 | ||||
-rw-r--r-- | src/lib/Gitolite/Hooks/Update.pm | 172 |
2 files changed, 247 insertions, 0 deletions
diff --git a/src/lib/Gitolite/Hooks/PostUpdate.pm b/src/lib/Gitolite/Hooks/PostUpdate.pm new file mode 100644 index 0000000..a76d1d9 --- /dev/null +++ b/src/lib/Gitolite/Hooks/PostUpdate.pm @@ -0,0 +1,75 @@ +package Gitolite::Hooks::PostUpdate; + +# everything to do with the post-update hook +# ---------------------------------------------------------------------- + +@EXPORT = qw( + post_update + post_update_hook +); + +use Exporter 'import'; + +use Gitolite::Rc; +use Gitolite::Common; + +use strict; +use warnings; + +# ---------------------------------------------------------------------- + +sub post_update { + trace( 3, 'post-up', @ARGV ); + exit 0 unless grep( m(^refs/heads/master$), @ARGV ); + # this is the *real* post_update hook for gitolite + + tsh_try("git ls-tree --name-only master"); + _die "no files/dirs called 'hooks' or 'logs' are allowed" if tsh_text() =~ /^(hooks|logs)$/m; + + my $hooks_changed = 0; + { + local $ENV{GIT_WORK_TREE} = $rc{GL_ADMIN_BASE}; + + tsh_try("git diff --name-only master"); + $hooks_changed++ if tsh_text() =~ m(/hooks/common/); + # the leading slash ensure that this hooks/common directory is below + # some top level directory, not *at* the top. That's LOCAL_CODE, and + # it's actual name could be anything but it doesn't matter to us. + + tsh_try("git checkout -f --quiet master"); + } + _system("gitolite compile"); + _system("gitolite setup --hooks-only") if $hooks_changed; + _system("gitolite trigger POST_COMPILE"); + + exit 0; +} + +{ + my $text = ''; + + sub post_update_hook { + if ( not $text ) { + local $/ = undef; + $text = <DATA>; + } + return $text; + } +} + +1; + +__DATA__ +#!/usr/bin/perl + +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Hooks::PostUpdate; + +# gitolite post-update hook (only for the admin repo) +# ---------------------------------------------------------------------- + +post_update(); # is not expected to return +exit 1; # so if it does, something is wrong diff --git a/src/lib/Gitolite/Hooks/Update.pm b/src/lib/Gitolite/Hooks/Update.pm new file mode 100644 index 0000000..2bc43a8 --- /dev/null +++ b/src/lib/Gitolite/Hooks/Update.pm @@ -0,0 +1,172 @@ +package Gitolite::Hooks::Update; + +# everything to do with the update hook +# ---------------------------------------------------------------------- + +@EXPORT = qw( + update + update_hook +); + +use Exporter 'import'; + +use Gitolite::Rc; +use Gitolite::Common; +use Gitolite::Conf::Load; + +use strict; +use warnings; + +$|++; + +# ---------------------------------------------------------------------- + +sub update { + # this is the *real* update hook for gitolite + + bypass() if $ENV{GL_BYPASS_ACCESS_CHECKS}; + + my ( $ref, $oldsha, $newsha, $oldtree, $newtree, $aa ) = args(@ARGV); + + trace( 2, $ENV{GL_REPO}, $ENV{GL_USER}, $aa, @ARGV ); + + my $ret = access( $ENV{GL_REPO}, $ENV{GL_USER}, $aa, $ref ); + trigger( 'ACCESS_2', $ENV{GL_REPO}, $ENV{GL_USER}, $aa, $ref, $ret, $oldsha, $newsha ); + _die $ret if $ret =~ /DENIED/; + + check_vrefs( $ref, $oldsha, $newsha, $oldtree, $newtree, $aa ); + + gl_log( 'update', $ENV{GL_REPO}, $ENV{GL_USER}, $aa, @ARGV, $ret ); + exit 0; +} + +sub bypass { + require Cwd; + Cwd->import; + gl_log( 'update', getcwd(), '(' . ( $ENV{USER} || '?' ) . ')', 'bypass', @ARGV ); + exit 0; +} + +sub check_vrefs { + my ( $ref, $oldsha, $newsha, $oldtree, $newtree, $aa ) = @_; + my $name_seen = 0; + my $n_vrefs = 0; + for my $vref ( vrefs( $ENV{GL_REPO}, $ENV{GL_USER} ) ) { + $n_vrefs++; + if ( $vref =~ m(^VREF/NAME/) ) { + # this one is special; we process it right here, and only once + next if $name_seen++; + + for my $ref ( map { chomp; s(^)(VREF/NAME/); $_; } `git diff --name-only $oldtree $newtree` ) { + check_vref( $aa, $ref ); + } + } else { + my ( $dummy, $pgm, @args ) = split '/', $vref; + $pgm = _which( "VREF/$pgm", 'x' ); + $pgm or _die "'$vref': helper program missing or unexecutable"; + + open( my $fh, "-|", $pgm, @_, $vref, @args ) or _die "'$vref': can't spawn helper program: $!"; + while (<$fh>) { + # print non-vref lines and skip processing (for example, + # normal STDOUT by a normal update hook) + unless (m(^VREF/)) { + print; + next; + } + my ( $ref, $deny_message ) = split( ' ', $_, 2 ); + check_vref( $aa, $ref, $deny_message ); + } + close($fh) or _die $! + ? "Error closing sort pipe: $!" + : "$vref: helper program exit status $?"; + } + } + return $n_vrefs; +} + +sub check_vref { + my ( $aa, $ref, $deny_message ) = @_; + + my $ret = access( $ENV{GL_REPO}, $ENV{GL_USER}, $aa, $ref ); + trace( 2, "access($ENV{GL_REPO}, $ENV{GL_USER}, $aa, $ref)", "-> $ret" ); + if ( $ret =~ /by fallthru/ ) { + trace( 3, "remember, fallthru is success here!" ); + return; + } + trigger( 'ACCESS_2', $ENV{GL_REPO}, $ENV{GL_USER}, $aa, $ref, $ret ); + _die "$ret" . ( $deny_message ? "\n$deny_message" : '' ) if $ret =~ /DENIED/; +} + +{ + my $text = ''; + + sub update_hook { + if ( not $text ) { + local $/ = undef; + $text = <DATA>; + } + return $text; + } +} + +# ---------------------------------------------------------------------- + +sub args { + my ( $ref, $oldsha, $newsha ) = @_; + my ( $oldtree, $newtree, $aa ); + + # this is special to git -- the hash of an empty tree + my $empty = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; + $oldtree = $oldsha eq '0' x 40 ? $empty : $oldsha; + $newtree = $newsha eq '0' x 40 ? $empty : $newsha; + + my $merge_base = '0' x 40; + # for branch create or delete, merge_base stays at '0'x40 + chomp( $merge_base = `git merge-base $oldsha $newsha` ) + unless $oldsha eq '0' x 40 + or $newsha eq '0' x 40; + + $aa = 'W'; + # tag rewrite + $aa = '+' if $ref =~ m(refs/tags/) and $oldsha ne ( '0' x 40 ); + # non-ff push to ref (including ref delete) + $aa = '+' if $oldsha ne $merge_base; + + $aa = 'D' if ( option( $ENV{GL_REPO}, 'DELETE_IS_D' ) ) and $newsha eq '0' x 40; + $aa = 'C' if ( option( $ENV{GL_REPO}, 'CREATE_IS_C' ) ) and $oldsha eq '0' x 40; + + # and now "M" commits. All the other accesses (W, +, C, D) were mutually + # exclusive in some sense. Sure a W could be a C or a + could be a D but + # that's by design. A merge commit, however, could still be any of the + # others (except a "D"). + + # so we have to *append* 'M' to $aa (if the repo has MERGE_CHECK in + # effect and this push contains a merge inside) + + if ( option( $ENV{GL_REPO}, 'MERGE_CHECK' ) ) { + if ( $oldsha eq '0' x 40 or $newsha eq '0' x 40 ) { + _warn "ref create/delete ignored for purposes of merge-check\n"; + } else { + $aa .= 'M' if `git rev-list -n 1 --merges $oldsha..$newsha` =~ /./; + } + } + + return ( $ref, $oldsha, $newsha, $oldtree, $newtree, $aa ); +} + +1; + +__DATA__ +#!/usr/bin/perl + +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Hooks::Update; + +# gitolite update hook +# ---------------------------------------------------------------------- + +update(); # is not expected to return +exit 1; # so if it does, something is wrong |