diff options
author | Christian Ruppert <idl0r@gentoo.org> | 2014-08-24 21:36:41 +0200 |
---|---|---|
committer | Christian Ruppert <idl0r@gentoo.org> | 2014-08-24 21:36:41 +0200 |
commit | 80fef662b843af89d1c7bad32ea2e79163cf5c34 (patch) | |
tree | 31d530fefcb7c75f0846fa9c2d77f5bd4939a4d4 | |
parent | Use Bash (diff) | |
parent | v3.6.1 (diff) | |
download | gitolite-gentoo-3.6.1.tar.gz gitolite-gentoo-3.6.1.tar.bz2 gitolite-gentoo-3.6.1.zip |
Merge commit '3455375e69ffd357adf96566f79e91407af8b532'gitolite-gentoo-3.6.1
Conflicts:
src/lib/Gitolite/Rc.pm
48 files changed, 2006 insertions, 187 deletions
@@ -1,3 +1,56 @@ +2014-06-22 v3.6.1 experimental rc format convertor for "<= 3.3" users who + have already upgraded the *code* to ">= v3.4". Program is + in contrib/utils. + + giving shell access to a few users got a lot easier (see + comments in the rc file). + + allow logging to syslog as well (see comments in the rc + file) + + new 'motd' command + + redis caching redone and now in core; see + http://gitolite.com/gitolite/cache.html + +2014-05-09 v3.6 (cool stuff) the access command can now help you debug + your rules / understand how a specific access decision was + arrived at. + + mirroring: since mirroring is asynchronous (by default + anyway), when a 'git push --mirror' fails, you may not + know it unless you look in the log file on the server. + Now gitolite captures the info and -- if the word 'fatal' + appears anywhere within it, it saves the entire output and + prints it to STDERR for anyone who reads or writes the + repo on the *master* server, until the error condition + clears up. + + mirroring: allow 'nosync' slaves -- no attempt to + automatically push to these slaves will be made. Instead, + you have to manually (or via cron, etc) trigger pushes. + + (backward compat breakage) the old v2 syntax for + specifying gitweb owner and description is no longer + supported. + + macros now allow strings as arguments (thanks to Jason + Donenfeld for the idea/problem). + + the 'info' command can print in JSON format if asked to. + + repo-specific hooks: now you can specify more than one, + and gitolite runs all of them in sequence. + + new trigger 'expand-deny-messages' to show more details + when access is denied. + + git-annex support is finally in master, yaaay! + + new 'readme' command, modelled after 'desc'. Apparently + gitweb can use a README.html file in the *bare* repo + directory -- who knew! + 2013-10-14 v3.5.3 catch undefined groupnames (when possible) mirroring: async push to slaves diff --git a/contrib/utils/ldap_groups.sh b/contrib/utils/ldap_groups.sh new file mode 100755 index 0000000..0192565 --- /dev/null +++ b/contrib/utils/ldap_groups.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# author: damien.nozay@gmail.com + +# Given a username, +# Provides a space-separated list of groups that the user is a member of. +# +# see http://gitolite.com/gitolite/auth.html#ldap +# GROUPLIST_PGM => /path/to/ldap_groups.sh + +ldap_groups() { + username=$1; + # this relies on openldap / pam_ldap to be configured properly on your + # system. my system allows anonymous search. + echo $( + ldapsearch -x -LLL "(&(objectClass=posixGroup)(memberUid=${username}))" cn \ + | grep "^cn" \ + | cut -d' ' -f2 + ); +} + +ldap_groups $@ diff --git a/contrib/utils/rc-format-v3.4 b/contrib/utils/rc-format-v3.4 new file mode 100755 index 0000000..1a11737 --- /dev/null +++ b/contrib/utils/rc-format-v3.4 @@ -0,0 +1,212 @@ +#!/usr/bin/perl + +# help with rc file format change at v3.4 -- help upgrade v3 rc files from +# v3.3 and below to the new v3.4 and above format + +# once you upgrade gitolite past 3.4, you may want to use the new rc file +# format, because it's really much nicer (just to recap: the old format will +# still work, in fact internally the new format gets converted to the old +# format before actually being used. However, the new format makes it much +# easier to enable and disable features). + +# PLEASE SEE WARNINGS BELOW + +# this program helps you upgrade your rc file. + +# STEPS +# cd gitolite-source-repo-clone +# contrib/utils/upgrade-rc33 /path/to/old.gitolite.rc > new.gitolite.rc + +# WARNINGS +# make sure you also READ ALL ERROR/WARNING MESSAGES GENERATED +# make sure you EXAMINE THE FILE AND CHECK THAT EVERYTHING LOOKS GOOD before using it +# be especially careful about +# variables which contains single/double quotes or other special characters +# variables that stretch across multiple lines +# features which take arguments (like 'renice') +# new features you've enabled which don't exist in the default rc + +# ---------------------------------------------------------------------- + +use strict; +use warnings; +use 5.10.0; +use Cwd; +use Data::Dumper; +$Data::Dumper::Terse = 1; +$Data::Dumper::Indent = 1; +$Data::Dumper::Sortkeys = 1; + +BEGIN { + $ENV{HOME} = getcwd; + $ENV{HOME} .= "/.home.rcupgrade.$$"; + mkdir $ENV{HOME} or die "mkdir '$ENV{HOME}': $!\n"; +} + +END { + system("rm -rf ./.home.rcupgrade.$$"); +} + +use lib "./src/lib"; +use Gitolite::Rc; +{ + no warnings 'redefine'; + sub Gitolite::Common::gl_log { } +} + +# ---------------------------------------------------------------------- + +# everything happens inside a fresh v3.6.1+ gitolite clone; no other +# directories are used. + +# the old rc file to be migrated is somewhere *else* and is supplied as a +# command line argument. + +# ---------------------------------------------------------------------- + +my $oldrc = shift or die "need old rc filename as arg-1\n"; + +{ + + package rcup; + do $oldrc; +} + +my %oldrc; +{ + no warnings 'once'; + %oldrc = %rcup::RC; +} + +delete $rcup::{RC}; +{ + my @extra = sort keys %rcup::; + warn "**** WARNING ****\nyou have variables declared outside the %RC hash; you must handle them manually\n" if @extra; +} + +# this is the new rc text being built up +my $newrc = glrc('default-text'); + +# ---------------------------------------------------------------------- + +# default disable all features in newrc +map { disable( $_, 'sq' ) } (qw(help desc info perms writable ssh-authkeys git-config daemon gitweb)); +# map { disable($_, '') } (qw(GIT_CONFIG_KEYS)); + +set_s('HOSTNAME'); +set_s( 'UMASK', 'num' ); +set_s( 'GIT_CONFIG_KEYS', 'sq' ); +set_s( 'LOG_EXTRA', 'num' ); +set_s( 'DISPLAY_CPU_TIME', 'num' ); +set_s( 'CPU_TIME_WARN_LIMIT', 'num' ); +set_s('SITE_INFO'); + +set_s('LOCAL_CODE'); + +if ( $oldrc{WRITER_CAN_UPDATE_DESC} ) { + die "tell Sitaram he changed the default rc too much" unless $newrc =~ /rc variables used by various features$/m; + $newrc =~ s/(rc variables used by various features\n)/$1\n # backward compat\n WRITER_CAN_UPDATE_DESC => 1,\n/; + + delete $oldrc{WRITER_CAN_UPDATE_DESC}; +} + +if ( $oldrc{ROLES} ) { + my $t = ''; + for my $r ( sort keys %{ $oldrc{ROLES} } ) { + $t .= ( " " x 8 ) . $r . ( " " x ( 28 - length($r) ) ) . "=> 1,\n"; + } + $newrc =~ s/(ROLES *=> *\{\n).*?\n( *\},)/$1$t$2/s; + + delete $oldrc{ROLES}; +} + +if ( $oldrc{DEFAULT_ROLE_PERMS} ) { + warn "DEFAULT_ROLE_PERMS has been replaced by per repo option\nsee http://gitolite.com/gitolite/wild.html\n"; + delete $oldrc{DEFAULT_ROLE_PERMS}; +} + +# the following is a bit like the reverse of what the new Rc.pm does... + +for my $l ( split /\n/, $Gitolite::Rc::non_core ) { + next if $l =~ /^ *#/ or $l !~ /\S/; + + my ( $name, $where, $module ) = split ' ', $l; + $module = $name if $module eq '.'; + ( $module = $name ) .= "::" . lc($where) if $module eq '::'; + + # if you find $module as an element of $where, enable $name + enable($name) if miw( $module, $where ); +} + +# now deal with commands +if ( $oldrc{COMMANDS} ) { + for my $c ( sort keys %{ $oldrc{COMMANDS} } ) { + if ( $oldrc{COMMANDS}{$c} == 1 ) { + enable($c); + # we don't handle anything else right (and so far only git-annex + # is affected, as far as I remember) + + delete $oldrc{COMMANDS}{$c}; + } + } +} + +print $newrc; + +for my $w (qw(INPUT POST_COMPILE PRE_CREATE ACCESS_1 POST_GIT PRE_GIT ACCESS_2 POST_CREATE SYNTACTIC_SUGAR)) { + delete $oldrc{$w} unless scalar( @{ $oldrc{$w} } ); +} +delete $oldrc{COMMANDS} unless scalar keys %{ $oldrc{COMMANDS} }; + +exit 0 unless %oldrc; + +warn "the following parts of the old rc were NOT converted:\n"; +print STDERR Dumper \%oldrc; + +# ---------------------------------------------------------------------- + +# set scalars that the new file defaults to "commented out" +sub set_s { + my ( $key, $type ) = @_; + $type ||= ''; + return unless exists $oldrc{$key}; + + # special treatment for UMASK + $oldrc{$key} = substr( "00" . sprintf( "%o", $oldrc{$key} ), -4 ) if ( $key eq 'UMASK' ); + + $newrc =~ s/# $key /$key /; # uncomment if needed + if ( $type eq 'num' ) { + $newrc =~ s/$key ( *=> *).*/$key $1$oldrc{$key},/; + } elsif ( $type eq 'sq' ) { + $newrc =~ s/$key ( *=> *).*/$key $1'$oldrc{$key}',/; + } else { + $newrc =~ s/$key ( *=> *).*/$key $1"$oldrc{$key}",/; + } + + delete $oldrc{$key}; +} + +sub disable { + my ( $key, $type ) = @_; + if ( $type eq 'sq' ) { + $newrc =~ s/^( *)'$key'/$1# '$key'/m; + } else { + $newrc =~ s/^( *)$key\b/$1# $key/m; + } +} + +sub enable { + my $key = shift; + $newrc =~ s/^( *)# *'$key'/$1'$key'/m; + return if $newrc =~ /^ *'$key'/m; + $newrc =~ s/(add new commands here.*\n)/$1 '$key',\n/; +} + +sub miw { + my ( $m, $w ) = @_; + return 0 unless $oldrc{$w}; + my @in = @{ $oldrc{$w} }; + my @out = grep { !/^$m$/ } @{ $oldrc{$w} }; + $oldrc{$w} = \@out; + return not scalar(@in) == scalar(@out); +} diff --git a/src/commands/D b/src/commands/D index 1a8c2b5..016a365 100755 --- a/src/commands/D +++ b/src/commands/D @@ -62,7 +62,7 @@ TRASH_SUFFIX=`gitolite query-rc TRASH_SUFFIX`; tsuf=`date +%Y-%m-%d_%H:%M:%S`; # ---------------------------------------------------------------------- owner_or_die() { - gitolite creator "$repo" $GL_USER || die You are not authorised + gitolite owns "$repo" || die You are not authorised } # ---------------------------------------------------------------------- diff --git a/src/commands/access b/src/commands/access index 7254bc6..e8b9446 100755 --- a/src/commands/access +++ b/src/commands/access @@ -1,4 +1,4 @@ -#!/usr/bin/perl +#!/usr/bin/perl -s use strict; use warnings; @@ -7,12 +7,14 @@ use Gitolite::Rc; use Gitolite::Common; use Gitolite::Conf::Load; +our ( $q, $s, $h ); # quiet, show, help + =for usage -Usage: gitolite access [-q] <repo> <user> <perm> <ref> +Usage: gitolite access [-q|-s] <repo> <user> <perm> <ref> Print access rights for arguments given. The string printed has the word DENIED in it if access was denied. With '-q', returns only an exit code -(shell truth, not perl truth -- 0 is success). +(shell truth, not perl truth -- 0 is success). For '-s', see below. - repo: mandatory - user: mandatory @@ -20,23 +22,31 @@ DENIED in it if access was denied. With '-q', returns only an exit code - ref: defauts to 'any'. See notes below Notes: - - ref: Any fully qualified ref ('refs/heads/master', not 'master') is fine. - The 'any' ref is special -- it ignores deny rules (see docs for what this - means and exceptions). - -Batch mode: see src/triggers/post-compile/update-git-daemon-access-list for a -good example that shows how to test several repos in one invocation. This is -orders of magnitude faster than running the command multiple times; you'll -notice if you have more than a hundred or so repos. + - ref: something like 'master', or 'refs/tags/v1.0', or even a VREF if you + know what they look like. + + The 'any' ref is special -- it ignores deny rules, thus simulating + gitolite's behaviour during the pre-git access check (see 'deny-rules' + section in rules.html for details). + + - batch mode: see src/triggers/post-compile/update-git-daemon-access-list + for a good example that shows how to test several repos in one invocation. + This is orders of magnitude faster than running the command multiple + times; you'll notice if you have more than a hundred or so repos. + + - '-s' shows the rules (conf file name, line number, and rule) that were + considered and how they fared. =cut -usage() if not @ARGV or $ARGV[0] eq '-h'; -my $quiet = 0; -if ( $ARGV[0] eq '-q' ) { $quiet = 1; shift @ARGV; } +usage() if not @ARGV or $h; my ( $repo, $user, $aa, $ref ) = @ARGV; +# default access is '+' $aa ||= '+'; +# default ref is 'any' $ref ||= 'any'; +# fq the ref if needed +$ref =~ s(^)(refs/heads/) if $ref and $ref ne 'any' and $ref !~ m(^(refs|VREF)/); _die "invalid perm" if not( $aa and $aa =~ /^(R|W|\+|C|D|M|\^C)$/ ); _die "invalid ref name" if not( $ref and $ref =~ $REPONAME_PATT ); @@ -46,19 +56,21 @@ if ( $repo ne '%' and $user ne '%' ) { # single repo, single user; no STDIN $ret = access( $repo, $user, $aa, $ref ); + show() if $s; + if ( $ret =~ /DENIED/ ) { - print "$ret\n" unless $quiet; + print "$ret\n" unless $q; exit 1; } - print "$ret\n" unless $quiet; + print "$ret\n" unless $q; exit 0; } $repo = '' if $repo eq '%'; $user = '' if $user eq '%'; -_die "'-q' doesn't go with using a pipe" if $quiet; +_die "'-q' and '-s' meaningless in pipe mode" if $q or $s; @ARGV = (); while (<>) { my @in = split; @@ -67,3 +79,78 @@ while (<>) { $ret = access( $r, $u, $aa, $ref ); print "$r\t$u\t$ret\n"; } + +sub show { + my $in = $rc{RULE_TRACE} or die "this should not happen!"; + + print STDERR "legend:"; + print STDERR " + d => skipped deny rule due to ref unknown or 'any', + r => skipped due to refex not matching, + p => skipped due to perm (W, +, etc) not matching, + D => explicitly denied, + A => explicitly allowed, + F => denied due to fallthru (no rules matched) + +"; + + my %rule_info = read_ri($in); # get rule info data for all traced rules + # this means conf filename, line number, and content of the line + + # the rule-trace info is a set of pairs of a number plus a string. Only + # the last character in a string is valid (and has meanings shown above). + # At the end there may be a final 'f' + my @in = split ' ', $in; + while (@in) { + $in = shift @in; + if ( $in =~ /^\d+$/ ) { + my $res = shift @in or die "this should not happen either!"; + my $m = chop($res); + printf " %s %20s:%-6s %s\n", $m, + $rule_info{$in}{fn}, + $rule_info{$in}{ln}, + $rule_info{$in}{cl}; + } elsif ( $in eq 'F' ) { + printf " %s %20s\n", $in, "(fallthru)"; + } else { + die "and finally, this also should not happen!"; + } + } + print "\n"; +} + +sub read_ri { + my %rules = map { $_ => 1 } $_[0] =~ /(\d+)/g; + # contains a series of rule numbers, each of which we must search in + # $GL_ADMIN_BASE/.gitolite/conf/rule_info + + my %rule_info; + for ( slurp( $ENV{GL_ADMIN_BASE} . "/conf/rule_info" ) ) { + my ( $r, $f, $l ) = split ' ', $_; + next unless $rules{$r}; + $rule_info{$r}{fn} = $f; + $rule_info{$r}{ln} = $l; + $rule_info{$r}{cl} = conf_lines( $f, $l ); + + # a wee bit of optimisation, in case the rule_info file is huge and + # what we want is up near the beginning + delete $rules{$r}; + last unless %rules; + } + return %rule_info; +} + +{ + my %conf_lines; + + sub conf_lines { + my ( $file, $line ) = @_; + $line--; + + unless ( $conf_lines{$file} ) { + $conf_lines{$file} = [ slurp( $ENV{GL_ADMIN_BASE} . "/conf/$file" ) ]; + chomp( @{ $conf_lines{$file} } ); + } + return $conf_lines{$file}[$line]; + } +} diff --git a/src/commands/desc b/src/commands/desc index 4fa3060..4a4bf20 100755 --- a/src/commands/desc +++ b/src/commands/desc @@ -1,43 +1,49 @@ -#!/bin/sh - -# Usage: ssh git@host desc <repo> -# ssh git@host desc <repo> <description string> -# -# Show or set description for user-created ("wild") repo. - -die() { echo "$@" >&2; exit 1; } -usage() { perl -lne 'print substr($_, 2) if /^# Usage/../^$/' < $0; exit 1; } -[ -z "$1" ] && usage -[ "$1" = "-h" ] && usage -[ -z "$GL_USER" ] && die GL_USER not set - -# ---------------------------------------------------------------------- -repo=$1; shift - -# this shell script takes arguments that are completely under the user's -# control, so make sure you quote those suckers! - -# kernel.org needs 'desc' to be available to people who have "RW" or above, -# not just the "creator". In fact they need it for non-wild repos so there -# *is* no creator. -if gitolite query-rc -q WRITER_CAN_UPDATE_DESC -then - gitolite access -q "$repo" $GL_USER W any || die You are not authorised -else - gitolite creator "$repo" $GL_USER || die You are not authorised -fi - -# if it passes, $repo is a valid repo name so it is known to contain only sane -# characters. This is because 'gitolite creator' return true only if there -# *is* a repo of that name and it has a gl-creator file that contains the same -# text as $GL_USER. - -descfile=`gitolite query-rc GL_REPO_BASE`/"$repo".git/description - -if [ -z "$1" ] -then - [ -r "$descfile" ] && cat "$descfile" - exit 0 -fi - -echo "$*" > "$descfile" +#!/usr/bin/perl +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Easy; + +=for usage +Usage: ssh git@host desc <repo> + ssh git@host desc <repo> <description string> + +Show or set description for repo. You need to have write access to the repo +and the 'writer-is-owner' option must be set for the repo, or it must be a +user-created ('wild') repo and you must be the owner. +=cut + +usage() if not @ARGV or @ARGV < 1 or $ARGV[0] eq '-h'; + +my $repo = shift; +my $text = join( " ", @ARGV ); +my $file = 'description'; + +#<<< +_die "you are not authorized" unless + ( not $text and can_read($repo) ) or + ( $text and owns($repo) ) or + ( $text and can_write($repo) and ( $rc{WRITER_CAN_UPDATE_DESC} or option( $repo, 'writer-is-owner' ) ) ); +#>>> + +$text + ? textfile( file => $file, repo => $repo, text => $text ) + : print textfile( file => $file, repo => $repo ); + +__END__ + +kernel.org needs 'desc' to be available to people who have "RW" or above, not +just the "creator". In fact they need it for non-wild repos so there *is* no +creator. To accommodate this, we created the WRITER_CAN_UPDATE_DESC rc +variable. + +However, that has turned out to be a bit of a blunt instrument for people with +different types of wild repos -- they don't want to apply this to all of them. +It seems easier to do this as an option, so you may have it for one set of +"repo ..." and not have it for others. And if you want it for the whole +system you'd just put it under "repo @all". + +The new 'writer-is-owner' option is meant to cover desc, readme, and any other +repo-specific text file, so it's also a blunt instrument, though in a +different dimension :-) diff --git a/src/commands/fork b/src/commands/fork index 1795e03..49994fc 100755 --- a/src/commands/fork +++ b/src/commands/fork @@ -50,5 +50,8 @@ ln -sf `gitolite query-rc GL_ADMIN_BASE`/hooks/common/* hooks # record where you came from echo "$from" > gl-forked-from +# cache control, if rc says caching is on +gitolite query-rc -q CACHE && perl -I$GL_LIBDIR -MGitolite::Cache -e "cache_control('flush', '$to')"; + # trigger post_create gitolite trigger POST_CREATE $to $GL_USER fork diff --git a/src/commands/git-annex-shell b/src/commands/git-annex-shell new file mode 100755 index 0000000..a9b29d5 --- /dev/null +++ b/src/commands/git-annex-shell @@ -0,0 +1,70 @@ +#!/usr/bin/perl + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Easy; + +# This command requires unrestricted arguments, so add it to the ENABLE list +# like this: +# 'git-annex-shell ua', + +# This requires git-annex version 20111016 or newer. Older versions won't +# be secure. + +use strict; +use warnings; + +# ignore @ARGV and look at the original unmodified command +my $cmd = $ENV{SSH_ORIGINAL_COMMAND}; + +# Expect commands like: +# git-annex-shell 'configlist' '/~/repo' +# git-annex-shell 'sendkey' '/~/repo' 'key' +# The parameters are always single quoted, and the repo path is always +# the second parameter. +# Further parameters are not validated here (see below). +die "bad git-annex-shell command: $cmd" + unless $cmd =~ m#^(git-annex-shell '\w+' ')/\~/([0-9a-zA-Z][0-9a-zA-Z._\@/+-]*)('( .*|))$#; +my $start = $1; +my $repo = $2; +my $end = $3; +$repo =~ s/\.git$//; +die "I dont like some of the characters in $repo\n" unless $repo =~ $Gitolite::Rc::REPONAME_PATT; +die "I dont like absolute paths in $cmd\n" if $repo =~ /^\//; +die "I dont like '..' paths in $cmd\n" if $repo =~ /\.\./; + +# Modify $cmd, fixing up the path to the repo to include GL_REPO_BASE. +my $newcmd = "$start$rc{GL_REPO_BASE}/$repo$end"; + +# Rather than keeping track of which git-annex-shell commands +# require write access and which are readonly, we tell it +# when readonly access is needed. +if ( can_write($repo) ) { +} elsif ( can_read($repo) ) { + $ENV{GIT_ANNEX_SHELL_READONLY} = 1; +} else { + die "$repo $ENV{GL_USER} DENIED\n"; +} +# Further limit git-annex-shell to safe commands (avoid it passing +# unknown commands on to git-shell) +$ENV{GIT_ANNEX_SHELL_LIMITED} = 1; + +# Note that $newcmd does *not* get evaluated by the unix shell. +# Instead it is passed as a single parameter to git-annex-shell for +# it to parse and handle the command. This is why we do not need to +# fully validate $cmd above. +Gitolite::Common::gl_log( $ENV{SSH_ORIGINAL_COMMAND} ); +exec "git-annex-shell", "-c", $newcmd; + +__END__ + +INSTRUCTIONS... (NEED TO BE VALIDATED BY SOMEONE WHO KNOWS GIT-ANNEX WELL). + +based on http://git-annex.branchable.com/tips/using_gitolite_with_git-annex/ +ONLY VARIATIONS FROM THAT PAGE ARE WRITTEN HERE. + +setup + + * in the ENABLE list in the rc file, add an entry like this: + 'git-annex-shell ua', + +That should be it; everything else should be as in that page. diff --git a/src/commands/git-config b/src/commands/git-config index d996575..94211de 100755 --- a/src/commands/git-config +++ b/src/commands/git-config @@ -17,32 +17,43 @@ key, or, if '-r' is supplied, a regex that is applied to all available keys. -q exit code only (shell truth; 0 is success) -n suppress trailing newline when used as key (not pattern) -r treat key as regex pattern (unanchored) + -ev print keys with empty values also (see below) Examples: gitolite git-config repo gitweb.owner gitolite git-config -q repo gitweb.owner gitolite git-config -r repo gitweb -When the key is treated as a pattern, prints: +Notes: - reponame<tab>key<tab>value<newline> +1. When the key is treated as a pattern, prints: -Otherwise the output is just the value. + reponame<tab>key<tab>value<newline> -Finally, see the advanced use section of 'gitolite access -h' -- you can do -something similar here also: + Otherwise the output is just the value. - gitolite list-phy-repos | gitolite git-config -r % gitweb\\. | cut -f1 > ~/projects.list +2. By default, keys with empty values (specified as "" in the conf file) are + treated as non-existant. Using '-ev' will print those keys also. Note + that this only makes sense when the key is treated as a pattern, where + such keys are printed as: + + reponame<tab>key<tab><newline> + +3. Finally, see the advanced use section of 'gitolite access -h' -- you can + do something similar here also: + + gitolite list-phy-repos | gitolite git-config -r % gitweb\\. | cut -f1 > ~/projects.list =cut usage() if not @ARGV; -my ( $help, $nonl, $quiet, $regex ) = (0) x 4; +my ( $help, $nonl, $quiet, $regex, $ev ) = (0) x 5; GetOptions( - 'n' => \$nonl, - 'q' => \$quiet, - 'r' => \$regex, - 'h' => \$help, + 'n' => \$nonl, + 'q' => \$quiet, + 'r' => \$regex, + 'h' => \$help, + 'ev' => \$ev, ) or usage(); my ( $repo, $key ) = @ARGV; @@ -54,7 +65,7 @@ if ( $repo ne '%' and $key ne '%' ) { # single repo, single key; no STDIN $key = "^\Q$key\E\$" unless $regex; - $ret = git_config( $repo, $key ); + $ret = git_config( $repo, $key, $ev ); # if the key is not a regex, it should match at most one item _die "found more than one entry for '$key'" if not $regex and scalar( keys %$ret ) > 1; @@ -80,7 +91,7 @@ while (<>) { my $r = $repo || shift @in; my $k = $key || shift @in; $k = "^\Q$k\E\$" unless $regex; - $ret = git_config( $r, $k ); + $ret = git_config( $r, $k, $ev ); next unless %$ret; map { print "$r\t$_\t" . $ret->{$_} . "\n" } sort keys %$ret; } diff --git a/src/commands/help b/src/commands/help index 3ab60d8..cf54084 100755 --- a/src/commands/help +++ b/src/commands/help @@ -17,9 +17,9 @@ Each command has its own help, accessed by passing it '-h' again. usage() if @ARGV; -my $user = $ENV{GL_USER} || ''; -print "hello" . ( $user ? " $user" : "" ) . ", this is gitolite3 " . version() . " on git " . substr( `git --version`, 12 ) . "\n"; +print greeting(); +my $user = $ENV{GL_USER} || ''; print "list of " . ( $user ? "remote" : "gitolite" ) . " commands available:\n\n"; my %list = ( list_x( $ENV{GL_BINDIR} ), list_x( $rc{LOCAL_CODE} || '' ) ); diff --git a/src/commands/info b/src/commands/info index b2bc3fc..267e4f6 100755 --- a/src/commands/info +++ b/src/commands/info @@ -10,51 +10,61 @@ use Gitolite::Common; use Gitolite::Conf::Load; =for args -Usage: gitolite info [-lc] [-ld] [<repo name pattern>] +Usage: gitolite info [-lc] [-ld] [-json] [<repo name pattern>] List all existing repos you can access, as well as repo name patterns you can create repos from (if any). '-lc' lists creators as an additional field at the end. '-ld' lists description as an additional field at the end. + '-json' produce JSON output instead of normal output The optional pattern is an unanchored regex that will limit the repos searched, in both cases. It might speed up things a little if you have more than a few thousand repos. =cut -# these two are globals -my ( $lc, $ld, $patt ) = args(); +# these are globals +my ( $lc, $ld, $json, $patt ) = args(); +my %out; # holds info to be json'd -print_version(); +$ENV{GL_USER} or _die "GL_USER not set"; +if ($json) { + greeting(\%out); +} else { + print greeting(); +} print_patterns(); # repos he can create for himself print_phy_repos(); # repos already created -print "\n$rc{SITE_INFO}\n" if $rc{SITE_INFO}; + +if ( $rc{SITE_INFO} ) { + $json + ? $out{SITE_INFO} = $rc{SITE_INFO} + : print "\n$rc{SITE_INFO}\n"; +} + +print JSON::to_json( \%out, { utf8 => 1, pretty => 1 } ) if $json; # ---------------------------------------------------------------------- sub args { - my ( $lc, $ld, $patt ) = ( '', '', '' ); + my ( $lc, $ld, $json, $patt ) = ( '', '', '', '' ); my $help = ''; GetOptions( - 'lc' => \$lc, - 'ld' => \$ld, - 'h' => \$help, + 'lc' => \$lc, + 'ld' => \$ld, + 'json' => \$json, + 'h' => \$help, ) or usage(); usage() if @ARGV > 1 or $help; $patt = shift @ARGV || '.'; - return ( $lc, $ld, $patt ); -} + require JSON if $json; -sub print_version { - chomp( my $hn = `hostname -s 2>/dev/null || hostname` ); - my $gv = substr( `git --version`, 12 ); - $ENV{GL_USER} or _die "GL_USER not set"; - print "hello $ENV{GL_USER}, this is " . ( $ENV{USER} || "httpd" ) . "\@$hn running gitolite3 " . version() . " on git $gv\n"; + return ( $lc, $ld, $json, $patt ); } sub print_patterns { @@ -100,6 +110,15 @@ sub listem { } $perm =~ s/\^//; next unless $perm =~ /\S/; + + if ($json) { + $out{repos}{$repo}{creator} = $creator if $lc; + $out{repos}{$repo}{description} = $desc if $ld; + $out{repos}{$repo}{perms} = _hash($perm); + + next; + } + print "$perm\t$repo"; print "\t$creator" if $lc; print "\t$desc" if $ld; @@ -107,3 +126,8 @@ sub listem { } } +sub _hash { + my $in = shift; + my %out = map { $_ => 1 } ( $in =~ /(\S)/g ); + return \%out; +} diff --git a/src/commands/mirror b/src/commands/mirror index 205145c..96b985d 100755 --- a/src/commands/mirror +++ b/src/commands/mirror @@ -28,6 +28,17 @@ need real-time updates or have bandwidth/connectivity issues. Usage 2 can be initiated by *any* user who has *any* gitolite access to the master server, but it checks that the slave is in one of the slaves options before doing the push. + +MIRROR STATUS: To find the status of the last mirror push to any slave, run +the same command except with 'status' instead of 'push'. With usage 1, you +can use the special name "all" to get the status of all slaves for the given +repo. (Admins wishing to find the status of all slaves for ALL repos will +have to script it using the output of "gitolite list-phy-repos".) + +SERVER LIST: 'gitolite mirror list master <reponame>' and 'gitolite mirror +list slaves <reponame>' will show you the name of the master server, and list +the slave servers, for the repo. They only work on the server command line +(any server), but not remotely (from a normal user). =cut usage() if not @ARGV or $ARGV[0] eq '-h'; @@ -52,9 +63,11 @@ if ( $cmd eq 'push' ) { } my $errors = 0; + my $glss = ''; for (`git push --mirror $host:$repo 2>&1`) { $errors = 1 if $?; print STDERR "$_" if -t STDERR or exists $ENV{GL_USER}; + $glss .= $_; chomp; if (/FATAL/) { $errors = 1; @@ -63,15 +76,91 @@ if ( $cmd eq 'push' ) { trace( 1, "mirror: $_" ); } } + # save the mirror push status for this slave if the word 'fatal' is found, + # else remove the status file. We don't store "success" output messages; + # you can always get those from the log files if you really need them. + if ( $glss =~ /fatal/i ) { + my $glss_prefix = Gitolite::Common::gen_ts() . "\t$ENV{GL_TID}\t"; + $glss =~ s/^/$glss_prefix/gm; + _print("gl-slave-$host.status", $glss); + } else { + unlink "gl-slave-$host.status"; + } + exit $errors; +} elsif ($cmd eq 'status') { + valid_slave( $host, $repo ) if exists $ENV{GL_USER}; + # will die if host not in slaves for repo + + _chdir( $rc{GL_REPO_BASE} ); + _chdir("$repo.git"); + + $host = '*' if $host eq 'all'; + map { print_status($_) } sort glob("gl-slave-$host.status"); +} else { + # strictly speaking, we could allow some of the possible commands remotely + # also, at least for admins. However, these commands are mainly intended + # for server-side scripting so I don't care. + usage() if $ENV{GL_USER}; + + server_side_commands(@ARGV); } +# ---------------------------------------------------------------------- + sub valid_slave { my ( $host, $repo ) = @_; _die "invalid repo '$repo'" unless $repo =~ $REPONAME_PATT; + my %list = repo_slaves($repo); + _die "'$host' not a valid slave for '$repo'" unless $list{$host}; +} + +sub repo_slaves { + my $repo = shift; + my $ref = git_config( $repo, "^gitolite-options\\.mirror\\.slaves.*" ); my %list = map { $_ => 1 } map { split } values %$ref; - _die "'$host' not a valid slave for '$repo'" unless $list{$host}; + return %list; +} + +sub repo_master { + my $repo = shift; + + my $ref = git_config( $repo, "^gitolite-options\\.mirror\\.master\$" ); + my @list = map { split } values %$ref; + _die "'$repo' seems to have more than one master" if @list > 1; + + return $list[0] || ''; +} + +sub print_status { + my $file = shift; + return unless -f $file; + my $slave = $1 if $file =~ /^gl-slave-(.+)\.status$/; + print "----------\n"; + print "WARNING: previous mirror push to host '$slave' failed, status is:\n"; + print slurp($file); + print "----------\n"; +} + +# ---------------------------------------------------------------------- +# server side commands. Very little error checking. +# gitolite mirror list master <repo> +# gitolite mirror list slaves <repo> + +sub server_side_commands { + if ( $cmd eq 'list' ) { + if ( $host eq 'master' ) { + say repo_master($repo); + } elsif ( $host eq 'slaves' ) { + my %list = repo_slaves($repo); + say join( " ", sort keys %list ); + } else { + _die "gitolite mirror list master|slaves <reponame>"; + } + } else { + _die "invalid command"; + } } diff --git a/src/commands/motd b/src/commands/motd new file mode 100755 index 0000000..b56e99e --- /dev/null +++ b/src/commands/motd @@ -0,0 +1,53 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Easy; + +=for usage +Usage: ssh git@host motd <repo> rm + cat <filename> | ssh git@host motd <repo> set + +Remove or set the motd file for repo or the whole system. + +For a repo: you need to have write access to the repo and the +'writer-is-owner' option must be set for the repo, or it must be a +user-created ('wild') repo and you must be the owner. + +For the whole system: you need to be an admin (have write access to the +gitolite-admin repo). Use @all in place of the repo name. + +PLEASE NOTE that if you're using http mode, the motd will only appear for +gitolite commands, not for normal git operations. This in turn means that +only the system wide motd can be seen; repo level motd's never show up. +=cut + +usage() if not @ARGV or @ARGV < 1 or $ARGV[0] eq '-h'; + +my $repo = shift; +my $op = shift || ''; +usage() if $op ne 'rm' and $op ne 'set'; +my $file = "gl-motd"; + +#<<< +_die "you are not authorized" unless + ( $repo eq '@all' and is_admin() ) or + ( $repo ne '@all' and owns($repo) ) or + ( $repo ne '@all' and can_write($repo) and option( $repo, 'writer-is-owner' ) ); +#>>> + +my @out = + $repo eq '@all' + ? ( dir => $rc{GL_ADMIN_BASE} ) + : ( repo => $repo ); + +if ( $op eq 'rm' ) { + $repo eq '@all' + ? unlink "$rc{GL_ADMIN_BASE}/$file" + : unlink "$rc{GL_REPO_BASE}/$repo.git/$file"; +} elsif ( $op eq 'set' ) { + textfile( file => $file, @out, prompt => '' ); +} else { + print textfile( file => $file, @out, ); +} diff --git a/src/commands/perms b/src/commands/perms index f61057d..c9b8946 100755 --- a/src/commands/perms +++ b/src/commands/perms @@ -64,6 +64,13 @@ if ( $ARGV[0] eq '-c' ) { my $repo = shift; setperms(@ARGV); + +# cache control +if ($rc{CACHE}) { + require Gitolite::Cache; + Gitolite::Cache::cache_control('flush', $repo); +} + _system( "gitolite", "trigger", "POST_CREATE", $repo, $ENV{GL_USER}, 'perms' ); # ---------------------------------------------------------------------- diff --git a/src/commands/readme b/src/commands/readme new file mode 100755 index 0000000..cd9632f --- /dev/null +++ b/src/commands/readme @@ -0,0 +1,54 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Easy; + +# README.html files work similar to "description" files. For further +# information see +# https://www.kernel.org/pub/software/scm/git/docs/gitweb.html +# under "Per-repository gitweb configuration". + +=for usage +Usage: ssh git@host readme <repo> + ssh git@host readme <repo> rm + cat <filename> | ssh git@host readme <repo> set + +Show, remove or set the README.html file for repo. + +You need to have write access to the repo and the 'writer-is-owner' option +must be set for the repo, or it must be a user-created ('wild') repo and you +must be the owner. +=cut + +usage() if not @ARGV or @ARGV < 1 or $ARGV[0] eq '-h'; + +my $repo = shift; +my $op = shift || ''; +usage() if $op and $op ne 'rm' and $op ne 'set'; +my $file = 'README.html'; + +#<<< +_die "you are not authorized" unless + ( not $op and can_read($repo) ) or + ( $op and owns($repo) ) or + ( $op and can_write($repo) and option( $repo, 'writer-is-owner' ) ); +#>>> + +if ( $op eq 'rm' ) { + unlink "$rc{GL_REPO_BASE}/$repo.git/$file"; +} elsif ( $op eq 'set' ) { + textfile( file => $file, repo => $repo, prompt => '' ); +} else { + print textfile( file => $file, repo => $repo ); +} + +__END__ + +The WRITER_CAN_UPDATE_README option is gone now; it applies to all the repos +in the system. Much better to add 'option writer-is-owner = 1' to repos or +repo groups that you want this to apply to. + +This option is meant to cover desc, readme, and any other repo-specific text +file, so it's also a blunt instrument, though in a different dimension :-) diff --git a/src/commands/writable b/src/commands/writable index 828f569..e84020e 100755 --- a/src/commands/writable +++ b/src/commands/writable @@ -6,12 +6,14 @@ use lib $ENV{GL_LIBDIR}; use Gitolite::Easy; =for usage -Usage: gitolite writable <reponame>|@all on|off +Usage: gitolite writable <reponame>|@all on|off|status Disable/re-enable pushes to all repos or named repo. Useful to run non-git-aware backups and so on. 'on' enables, 'off' disables, writes (pushes) to the named repo or all repos. +'status' returns the current status as shell truth (i.e., exit code 0 for +writable, 1 for not writable). With 'off', any subsequent text is taken to be the message to be shown to users when their pushes get rejected. If it is not supplied, it will take it @@ -19,20 +21,20 @@ from STDIN; this allows longer messages. =cut usage() if not @ARGV or @ARGV < 2 or $ARGV[0] eq '-h'; -usage() if $ARGV[1] ne 'on' and $ARGV[1] ne 'off'; +usage() if $ARGV[1] ne 'on' and $ARGV[1] ne 'off' and $ARGV[1] ne 'status'; my $repo = shift; -my $on = ( shift eq 'on' ); +my $op = shift; # on|off|status if ( $repo eq '@all' ) { _die "you are not authorized" if $ENV{GL_USER} and not is_admin(); } else { - _die "you are not authorized" if $ENV{GL_USER} and not owns($repo); + _die "you are not authorized" if $ENV{GL_USER} and not( owns($repo) or is_admin() ); } my $msg = join( " ", @ARGV ); # try STDIN only if no msg found in args *and* it's an 'off' command -if ( not $msg and not $on ) { +if ( not $msg and $op eq 'off' ) { say2 "...please type the message to be shown to users:"; $msg = join( "", <> ); } @@ -48,9 +50,12 @@ if ( $repo eq '@all' ) { sub target { my $repodir = shift; - if ($on) { + if ( $op eq 'status' ) { + exit 1 if -e "$repodir/$sf"; + exit 0; + } elsif ( $op eq 'on' ) { unlink "$repodir/$sf"; - } else { + } elsif ( $op eq 'off' ) { _print( "$repodir/$sf", $msg ); } } diff --git a/src/gitolite-shell b/src/gitolite-shell index 250a1f0..d1b2ff5 100755 --- a/src/gitolite-shell +++ b/src/gitolite-shell @@ -157,10 +157,11 @@ sub parse_soc { # after this we should not return; caller expects us to handle it all here # and exit out - _die "suspicious characters loitering about '$soc'" if $soc !~ $REMOTE_COMMAND_PATT; my @words = split ' ', $soc; if ( $rc{COMMANDS}{ $words[0] } ) { + _die "suspicious characters loitering about '$soc'" + if $rc{COMMANDS}{ $words[0] } ne 'ua' and $soc !~ $REMOTE_COMMAND_PATT; trace( 2, "gitolite command", $soc ); _system( "gitolite", @words ); exit 0; @@ -222,6 +223,9 @@ sub http_simulate_ssh_connection { $ENV{SSH_ORIGINAL_COMMAND} = $verb; $ENV{SSH_ORIGINAL_COMMAND} .= " $args" if $args; http_print_headers(); # in preparation for the eventual output! + + # we also need to pipe STDERR out via STDOUT, else the user doesn't see those messages! + open(STDERR, ">&STDOUT") or _die "Can't dup STDOUT: $!"; } $ENV{SSH_CONNECTION} = "$ENV{REMOTE_ADDR} $ENV{REMOTE_PORT} $ENV{SERVER_ADDR} $ENV{SERVER_PORT}"; } diff --git a/src/lib/Gitolite/Cache.pm b/src/lib/Gitolite/Cache.pm new file mode 100644 index 0000000..351a13e --- /dev/null +++ b/src/lib/Gitolite/Cache.pm @@ -0,0 +1,161 @@ +package Gitolite::Cache; + +# cache stuff using an external database (redis) +# ---------------------------------------------------------------------- + +@EXPORT = qw( + cache_control + cache_wrap +); + +use Exporter 'import'; + +use Gitolite::Common; +use Gitolite::Rc; +use Storable qw(freeze thaw); +use Redis; + +my $redis; + +my $redis_sock = "$ENV{HOME}/.redis-gitolite.sock"; +if ( -S $redis_sock ) { + _connect_redis(); +} else { + _start_redis(); + _connect_redis(); + + # this redis db is a transient, caching only, db, so let's not + # accidentally use any stale data when if we're just starting up + cache_control('stop'); + cache_control('start'); +} + +# ---------------------------------------------------------------------- + +my %wrapped; +my $ttl = ( $rc{CACHE_TTL} || ( $rc{GROUPLIST_PGM} ? 900 : 90000 ) ); + +sub cache_control { + my $op = shift; + if ( $op eq 'stop' ) { + $redis->flushall(); + } elsif ( $op eq 'start' ) { + $redis->set( 'cache-up', 1 ); + } elsif ( $op eq 'flush' ) { + flush_repo(@_); + } +} + +sub cache_wrap { + my $sub = shift; + my $tname = $sub; # this is what will show up in the trace output + trace( 3, "wrapping '$sub'" ); + $sub = ( caller 1 )[0] . "::" . $sub if $sub !~ /::/; + return if $wrapped{$sub}++; # in case somehow it gets called twice for the same sub! + + # collect names of wrapped subs into a redis 'set' + $redis->sadd( "SUBWAY", $sub ); # subway? yeah well they wrap subs don't they? + + my $cref = eval '\&' . $sub; + my %opt = @_; + # rest of the options come in as a hash. 'list' says this functions + # returns a list. 'ttl' is a number to override the default ttl for + # the cached value. + + no strict 'refs'; + no warnings 'redefine'; + *{$sub} = sub { # the wrapper function + my $key = join( ", ", @_ ); + trace( 2, "$tname.args", @_ ); + + if ( cache_up() and defined( my $val = $redis->get("$sub: $key") ) ) { + # cache is up and we got a hit, return value from cache + if ( $opt{list} ) { + trace( 2, "$tname.getl", @{ thaw($val) } ); + return @{ thaw($val) }; + } else { + trace( 2, "$tname.get", $val ); + return $val; + } + } else { + # cache is down or we got a miss, compute + my ( $r, @r ); + if ( $opt{list} ) { + @r = $cref->(@_); # provide list context + trace( 2, "$tname.setl", @r ); + } else { + $r = $cref->(@_); # provide scalar context + trace( 2, "$tname.set", $r ); + } + + # store computed value in cache if cache is up + if ( cache_up() ) { + $redis->set( "$sub: $key", ( $opt{list} ? freeze( \@r ) : $r ) ); + $redis->expire( "$sub: $key", $opt{ttl} || $ttl ); + trace( 2, "$tname.ttl", ( $opt{ttl} || $ttl ) ); + } + + return @r if $opt{list}; + return $r; + } + }; + trace( 3, "wrapped '$sub'" ); +} + +sub cache_up { + return $redis->exists('cache-up'); +} + +sub flush_repo { + my $repo = shift; + + my @wrapped = $redis->smembers("SUBWAY"); + for my $func (@wrapped) { + # if we wrap any more functions, make sure they're functions where the + # first argument is 'repo' + my @keys = $redis->keys("$func: $repo, *"); + $redis->del( @keys ) if @keys; + } +} + +# ---------------------------------------------------------------------- + +sub _start_redis { + my $conf = join( "", <DATA> ); + $conf =~ s/%HOME/$ENV{HOME}/g; + + open( REDIS, "|-", "/usr/sbin/redis-server", "-" ) or die "start redis server failed: $!"; + print REDIS $conf; + close REDIS; + + # give it a little time to come up + select( undef, undef, undef, 0.2 ); +} + +sub _connect_redis { + $redis = Redis->new( sock => $redis_sock, encoding => undef ) or die "redis new failed: $!"; + $redis->ping or die "redis ping failed: $!"; +} + +1; + +__DATA__ +# resources +maxmemory 50MB +port 0 +unixsocket %HOME/.redis-gitolite.sock +unixsocketperm 700 +timeout 0 +databases 1 + +# daemon +daemonize yes +pidfile %HOME/.redis-gitolite.pid +dbfilename %HOME/.redis-gitolite.rdb +dir %HOME + +# feedback +loglevel notice +logfile %HOME/.redis-gitolite.log + +# we don't save diff --git a/src/lib/Gitolite/Common.pm b/src/lib/Gitolite/Common.pm index 00642cb..b32d0d0 100644 --- a/src/lib/Gitolite/Common.pm +++ b/src/lib/Gitolite/Common.pm @@ -71,9 +71,12 @@ sub dd { } { - use Time::HiRes; my %start_times; + eval "require Time::HiRes"; + # we just ignore any errors from this; nothing needs to be done as long as + # no code *calls* either of the next two functions. + sub t_start { my $name = shift || 'default'; $start_times{$name} = [ Time::HiRes::gettimeofday() ]; @@ -121,8 +124,9 @@ sub usage { } sub _mkdir { - # it's not an error if the directory exists, but it is an error if it - # doesn't exist and we can't create it + # It's not an error if the directory exists, but it is an error if it + # doesn't exist and we can't create it. This includes not guaranteeing + # dead symlinks or if mkpath traversal is blocked by a file. my $dir = shift; my $perm = shift; # optional return if -d $dir; @@ -202,6 +206,7 @@ sub sort_u { sub cleanup_conf_line { my $line = shift; + return $line if $line =~ /^# \S+ \d+$/; # kill comments, but take care of "#" inside *simple* strings $line =~ s/^((".*?"|[^#"])*)#.*/$1/; @@ -260,6 +265,9 @@ sub gen_lfn { return $template; } +my $log_dest; +my $syslog_opened = 0; +END { closelog() if $syslog_opened; } sub gl_log { # the log filename and the timestamp come from the environment. If we get # called even before they are set, we have no choice but to dump to STDERR @@ -272,6 +280,28 @@ sub gl_log { my $ts = gen_ts(); my $tid = $ENV{GL_TID} ||= $$; + # syslog + $log_dest = $Gitolite::Rc::rc{LOG_DEST} || '' if not defined $log_dest; + if ($log_dest =~ /syslog/) { # log_dest *includes* syslog + if ($syslog_opened == 0) { + require Sys::Syslog; + Sys::Syslog->import(qw(:standard)); + + openlog("gitolite" . ( $ENV{GL_TID} ? "[$ENV{GL_TID}]" : "" ), "pid", "local0"); + $syslog_opened = 1; + } + + # gl_log is called either directly, or, if the rc variable LOG_EXTRA + # is set, from trace(1, ...). The latter use is considered additional + # info for troubleshooting. Trace prefixes a tab to the arguments + # before calling gl_log, to visually set off such lines in the log + # file. Although syslog eats up that leading tab, we use it to decide + # the priority/level of the syslog message. + syslog( ( $msg =~ /^\t/ ? 'debug' : 'info' ), "%s", $msg); + + return if $log_dest eq 'syslog'; # log_dest *equals* syslog + } + my $fh; logger_plus_stderr( "errors found before logging could be setup", "$msg" ) if not $ENV{GL_LOGFILE}; open my $lfh, ">>", $ENV{GL_LOGFILE} diff --git a/src/lib/Gitolite/Conf.pm b/src/lib/Gitolite/Conf.pm index 21c060c..ce7adca 100644 --- a/src/lib/Gitolite/Conf.pm +++ b/src/lib/Gitolite/Conf.pm @@ -10,7 +10,6 @@ package Gitolite::Conf; ); use Exporter 'import'; -use Getopt::Long; use Gitolite::Rc; use Gitolite::Common; @@ -33,8 +32,21 @@ sub compile { # the order matters; new repos should be created first, to give store a # place to put the individual gl-conf files new_repos(); + + # cache control + if ($rc{CACHE}) { + require Gitolite::Cache; + Gitolite::Cache->import(qw(cache_control)); + + cache_control('stop'); + } + store(); + if ($rc{CACHE}) { + cache_control('start'); + } + for my $repo ( @{ $rc{NEW_REPOS_CREATED} } ) { trigger( 'POST_CREATE', $repo ); } @@ -44,7 +56,9 @@ sub parse { my $lines = shift; trace( 3, scalar(@$lines) . " lines incoming" ); + my ( $fname, $lnum ); for my $line (@$lines) { + ( $fname, $lnum ) = ( $1, $2 ), next if $line =~ /^# (\S+) (\d+)$/; # user or repo groups if ( $line =~ /^(@\S+) = (.*)/ ) { add_to_group( $1, split( ' ', $2 ) ); @@ -57,7 +71,7 @@ sub parse { for my $ref (@refs) { for my $user (@users) { - add_rule( $perm, $ref, $user ); + add_rule( $perm, $ref, $user, $fname, $lnum ); } } } elsif ( $line =~ /^config (.+) = ?(.*)/ ) { @@ -68,6 +82,9 @@ sub parse { my @matched = grep { $key =~ /^$_$/i } @validkeys; _die "git config '$key' not allowed\ncheck GIT_CONFIG_KEYS in the rc file" if ( @matched < 1 ); _die "bad config value '$value'" if $value =~ $UNSAFE_PATT; + while ( my ( $mk, $mv ) = each %{ $rc{SAFE_CONFIG} } ) { + $value =~ s/%$mk/$mv/g; + } add_config( 1, $key, $value ); } elsif ( $line =~ /^subconf (\S+)$/ ) { trace( 3, $line ); diff --git a/src/lib/Gitolite/Conf/Explode.pm b/src/lib/Gitolite/Conf/Explode.pm index 8f934fa..cf89620 100644 --- a/src/lib/Gitolite/Conf/Explode.pm +++ b/src/lib/Gitolite/Conf/Explode.pm @@ -43,6 +43,7 @@ sub explode { incsub( $1, $2, $3, $subconf, $out ); } else { # normal line, send it to the callback function + push @{$out}, "# $file $."; push @{$out}, $line; } } diff --git a/src/lib/Gitolite/Conf/Load.pm b/src/lib/Gitolite/Conf/Load.pm index 960af84..31966d5 100644 --- a/src/lib/Gitolite/Conf/Load.pm +++ b/src/lib/Gitolite/Conf/Load.pm @@ -102,17 +102,24 @@ sub access { } trace( 3, scalar(@rules) . " rules found" ); + + $rc{RULE_TRACE} = ''; for my $r (@rules) { + $rc{RULE_TRACE} .= " " . $r->[0] . " "; + my $perm = $r->[1]; my $refex = $r->[2]; $refex =~ s(/USER/)(/$user/); trace( 3, "perm=$perm, refex=$refex" ); + $rc{RULE_TRACE} .= "d"; # skip 'deny' rules if the ref is not (yet) known next if $perm eq '-' and $ref eq 'any' and not $deny_rules; + $rc{RULE_TRACE} .= "r"; # rule matches if ref matches or ref is any (see gitolite-shell) next unless $ref =~ /^$refex/ or $ref eq 'any'; + $rc{RULE_TRACE} .= "D"; trace( 2, "DENIED by $refex" ) if $perm eq '-'; return "$aa $safe_ref $repo $user DENIED by $refex" if $perm eq '-'; @@ -120,13 +127,26 @@ sub access { # any of these followed by "M". ( my $aaq = $aa ) =~ s/\+/\\+/; $aaq =~ s/M/.*M/; + + $rc{RULE_TRACE} .= "A"; + # as far as *this* ref is concerned we're ok return $refex if ( $perm =~ /$aaq/ ); + + $rc{RULE_TRACE} .= "p"; } + $rc{RULE_TRACE} .= " F"; + trace( 2, "DENIED by fallthru" ); return "$aa $safe_ref $repo $user DENIED by fallthru"; } +# cache control +if ($rc{CACHE}) { + require Gitolite::Cache; + Gitolite::Cache::cache_wrap('Gitolite::Conf::Load::access'); +} + sub git_config { my ( $repo, $key, $empty_values_OK ) = @_; $key ||= '.'; diff --git a/src/lib/Gitolite/Conf/Store.pm b/src/lib/Gitolite/Conf/Store.pm index 69484d3..1d566eb 100644 --- a/src/lib/Gitolite/Conf/Store.pm +++ b/src/lib/Gitolite/Conf/Store.pm @@ -112,7 +112,9 @@ sub parse_users { } sub add_rule { - my ( $perm, $ref, $user ) = @_; + my ( $perm, $ref, $user, $fname, $lnum ) = @_; + _warn "doesn't make sense to supply a ref ('$ref') for 'R' rule" + if $perm eq 'R' and $ref ne 'refs/.*'; _warn "possible undeclared group '$user'" if $user =~ /^@/ and not $groups{$user} @@ -122,6 +124,7 @@ sub add_rule { _die "bad user '$user'" unless $user =~ $USERNAME_PATT; $nextseq++; + store_rule_info( $nextseq, $fname, $lnum ); for my $repo (@repolist) { push @{ $repos{$repo}{$user} }, [ $nextseq, $perm, $ref ]; } @@ -251,6 +254,8 @@ sub parse_done { for my $ig ( sort keys %ignored ) { _warn "subconf '$ig' attempting to set access for " . join( ", ", sort keys %{ $ignored{$ig} } ); } + + close_rule_info(); } # ---------------------------------------------------------------------- @@ -386,5 +391,19 @@ sub inside_out { # %groups = ( 'bb' => [ '@bb', '@aa' ], 'cc' => [ '@bb', '@aa' ], 'dd' => [ '@bb' ]); } +{ + my $ri_fh = ''; + + sub store_rule_info { + $ri_fh = _open( ">", $rc{GL_ADMIN_BASE} . "/conf/rule_info" ) unless $ri_fh; + # $nextseq, $fname, $lnum + print $ri_fh join( "\t", @_ ) . "\n"; + } + + sub close_rule_info { + close $ri_fh or die "close rule_info file failed: $!"; + } +} + 1; diff --git a/src/lib/Gitolite/Conf/Sugar.pm b/src/lib/Gitolite/Conf/Sugar.pm index fdfbac9..986494b 100644 --- a/src/lib/Gitolite/Conf/Sugar.pm +++ b/src/lib/Gitolite/Conf/Sugar.pm @@ -124,12 +124,6 @@ sub owner_desc { # category = "whatever..." # -> config gitweb.category = whatever... - # older formats: - # repo = "some long description" - # repo "owner name" = "some long description" - # -> config gitweb.owner = owner name - # -> config gitweb.description = some long description - for my $line (@$lines) { if ( $line =~ /^desc = (\S.*)/ ) { push @ret, "config gitweb.description = $1"; @@ -137,11 +131,6 @@ sub owner_desc { push @ret, "config gitweb.owner = $1"; } elsif ( $line =~ /^category = (\S.*)/ ) { push @ret, "config gitweb.category = $1"; - } elsif ( $line =~ /^(\S+)(?: "(.*?)")? = "(.*)"$/ ) { - my ( $repo, $owner, $desc ) = ( $1, $2, $3 ); - push @ret, "repo $repo"; - push @ret, "config gitweb.description = $desc"; - push @ret, "config gitweb.owner = $owner" if $owner; } else { push @ret, $line; } diff --git a/src/lib/Gitolite/Easy.pm b/src/lib/Gitolite/Easy.pm index 308e94d..c7bd8cc 100644 --- a/src/lib/Gitolite/Easy.pm +++ b/src/lib/Gitolite/Easy.pm @@ -43,6 +43,8 @@ package Gitolite::Easy; config + textfile + %rc say say2 @@ -50,6 +52,8 @@ package Gitolite::Easy; _warn _print usage + + option ); #>>> use Exporter 'import'; @@ -182,6 +186,60 @@ sub config { # ---------------------------------------------------------------------- +# maintain a textfile; see comments in code for details, and calls in various +# other programs (like 'motd', 'desc', and 'readme') for how to call +sub textfile { + my %h = @_; + my $repodir; + + # target file + _die "need file" unless $h{file}; + _die "'$h{file}' contains a '/'" if $h{file} =~ m(/); + _sanity($h{file}); + + # target file's location. This can come from one of two places: dir + # (which comes from our code, so does not need to be sanitised), or repo, + # which may come from the user + _die "need exactly one of repo or dir" unless $h{repo} xor $h{dir}; + _die "'$h{dir}' does not exist" if $h{dir} and not -d $h{dir}; + if ($h{repo}) { + _sanity($h{repo}); + $h{dir} = "$rc{GL_REPO_BASE}/$h{repo}.git"; + _die "repo '$h{repo}' does not exist" if not -d $h{dir}; + + my $umask = option( $h{repo}, 'umask' ); + # note: using option() moves us to ADMIN_BASE, but we don't care here + umask oct($umask) if $umask; + } + + # final full file name + my $f = "$h{dir}/$h{file}"; + + # operation + _die "can't have both prompt and text" if defined $h{prompt} and defined $h{text}; + if (defined $h{prompt}) { + print STDERR $h{prompt}; + my $t = join( "", <> ); + _print($f, $t); + } elsif (defined $h{text}) { + _print($f, $h{text}); + } else { + return slurp($f) if -f $f; + } + + return ''; +} + +# ---------------------------------------------------------------------- + +sub _sanity { + my $name = shift; + _die "'$name' contains bad characters" if $name !~ $REPONAME_PATT; + _die "'$name' ends with a '/'" if $name =~ m(/$); + _die "'$name' contains '..'" if $name =~ m(\.\.); +} + + sub valid_user { _die "GL_USER not set" unless exists $ENV{GL_USER}; $user = $ENV{GL_USER}; diff --git a/src/lib/Gitolite/Hooks/Update.pm b/src/lib/Gitolite/Hooks/Update.pm index f6b3f1b..32cd6e0 100644 --- a/src/lib/Gitolite/Hooks/Update.pm +++ b/src/lib/Gitolite/Hooks/Update.pm @@ -87,10 +87,12 @@ sub check_vref { 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/ and $ret !~ /by fallthru/; - trace( 3, "remember, fallthru is success here!" ) if $ret =~ /by fallthru/; + _die "$ret" . ( $deny_message ? "\n$deny_message" : '' ) if $ret =~ /DENIED/; } { diff --git a/src/lib/Gitolite/Rc.pm b/src/lib/Gitolite/Rc.pm index 29dd4e0..03f42fd 100644 --- a/src/lib/Gitolite/Rc.pm +++ b/src/lib/Gitolite/Rc.pm @@ -8,6 +8,7 @@ package Gitolite::Rc; glrc query_rc version + greeting trigger _which @@ -20,7 +21,6 @@ package Gitolite::Rc; ); use Exporter 'import'; -use Getopt::Long; use Gitolite::Common; @@ -185,6 +185,9 @@ sub non_core_expand { push @{ $rc{$where} }, $module; } + + # finally, add in commands that were declared in the non-core list + map { /^(\S+)/; $rc{COMMANDS}{$1} = 1 } @{ $rc{COMMAND} }; } # exported functions @@ -270,6 +273,26 @@ sub version { return $version; } +sub greeting { + my $json = shift; + + chomp( my $hn = `hostname -s 2>/dev/null || hostname` ); + my $gv = substr( `git --version`, 12 ); + my $gl_user = $ENV{GL_USER} || ''; + $gl_user = " $gl_user" if $gl_user; + + if ($json) { + $json->{GL_USER} = $ENV{GL_USER}; + $json->{USER} = ( $ENV{USER} || "httpd" ) . "\@$hn"; + $json->{gitolite_version} = version(); + chomp( $json->{git_version} = $gv ); # this thing has a newline at the end + return; + } + + # normal output + return "hello$gl_user, this is " . ( $ENV{USER} || "httpd" ) . "\@$hn running gitolite3 " . version() . " on git $gv\n"; +} + sub trigger { my $rc_section = shift; @@ -366,7 +389,8 @@ Explore: sub args { my $help = 0; - GetOptions( + require Getopt::Long; + Getopt::Long::GetOptions( 'all|a' => \$all, 'nonl|n' => \$nonl, 'quiet|q' => \$quiet, @@ -392,6 +416,8 @@ BEGIN { renice PRE_GIT . + Kindergarten INPUT :: + CpuTime INPUT :: CpuTime POST_GIT :: @@ -399,12 +425,19 @@ BEGIN { Alias INPUT :: + Motd INPUT :: + Motd PRE_GIT :: + Motd COMMAND motd + Mirroring INPUT :: Mirroring PRE_GIT :: Mirroring POST_GIT :: refex-expr ACCESS_2 RefexExpr::access_2 + expand-deny-messages ACCESS_1 . + expand-deny-messages ACCESS_2 . + RepoUmask PRE_GIT :: RepoUmask POST_CREATE :: @@ -469,6 +502,12 @@ __DATA__ # comment out if you don't need all the extra detail in the logfile LOG_EXTRA => 1, + # syslog options + # 1. leave this section as is for normal gitolite logging + # 2. uncomment this line to log only to syslog: + # LOG_DEST => 'syslog', + # 3. uncomment this line to log to syslog and the normal gitolite log: + # LOG_DEST => 'syslog,normal', # roles. add more roles (like MANAGER, TESTER, ...) here. # WARNING: if you make changes to this hash, you MUST run 'gitolite @@ -478,6 +517,9 @@ __DATA__ WRITERS => 1, }, + # enable caching (currently only Redis). PLEASE RTFM BEFORE USING!!! + # CACHE => 'Redis', + # Define which metadata variables shall be exported to the gitolite # environment. # Those variables can be used in hooks, e.g. for cia.vc @@ -494,9 +536,6 @@ __DATA__ # the 'info' command prints this as additional info, if it is set # SITE_INFO => 'Please see http://blahblah/gitolite for more help', - # the 'desc' command uses this - # WRITER_CAN_UPDATE_DESC => 1, - # the CpuTime feature uses these # display user, system, and elapsed times to user after each git operation # DISPLAY_CPU_TIME => 1, @@ -506,8 +545,8 @@ __DATA__ # the Mirroring feature needs this # HOSTNAME => "foo", - # if you enabled 'Shell', you need this - # SHELL_USERS_LIST => "$ENV{HOME}/.gitolite.shell-users", + # TTL for redis cache; PLEASE SEE DOCUMENTATION BEFORE UNCOMMENTING! + # CACHE_TTL => 600, # ------------------------------------------------------------------ @@ -540,6 +579,7 @@ __DATA__ # 'create', # 'fork', # 'mirror', + # 'readme', # 'sskm', # 'D', @@ -570,12 +610,20 @@ __DATA__ # access a repo by another (possibly legacy) name # 'Alias', - # give some users direct shell access - # 'Shell', + # give some users direct shell access. See documentation in + # sts.html for details on the following two choices. + # "Shell $ENV{HOME}/.gitolite.shell-users", + # 'Shell alice bob', # set default roles from lines like 'option default.roles-1 = ...', etc. # 'set-default-roles', + # show more detailed messages on deny + # 'expand-deny-messages', + + # show a message of the day + # 'Motd', + # system admin stuff # enable mirroring (don't forget to set the HOSTNAME too!) @@ -615,6 +663,10 @@ __DATA__ # allow simple line-oriented macros # 'macros', + # Kindergarten mode + + # disallow various things that sensible people shouldn't be doing anyway + # 'Kindergarten', ], ); diff --git a/src/lib/Gitolite/Setup.pm b/src/lib/Gitolite/Setup.pm index 06b2409..43de5d9 100644 --- a/src/lib/Gitolite/Setup.pm +++ b/src/lib/Gitolite/Setup.pm @@ -39,7 +39,6 @@ Subsequent runs: ); use Exporter 'import'; -use Getopt::Long; use Gitolite::Rc; use Gitolite::Common; @@ -73,7 +72,8 @@ sub args { my $help = 0; my $argv = join( " ", @ARGV ); - GetOptions( + require Getopt::Long; + Getopt::Long::GetOptions( 'admin|a=s' => \$admin, 'pubkey|pk=s' => \$pubkey, 'hooks-only|ho' => \$h_only, diff --git a/src/lib/Gitolite/Test.pm b/src/lib/Gitolite/Test.pm index 32beb43..291eace 100644 --- a/src/lib/Gitolite/Test.pm +++ b/src/lib/Gitolite/Test.pm @@ -52,7 +52,7 @@ try " DEF AP_2 = AP_1; git add conf ; ok; git commit -m %1; ok; /master.* %1/ DEF ADMIN_PUSH = AP_2 %1; glt push admin origin; ok; gsh; /master -> master/ - DEF CS_1 = pwd; //tmp/tsh_tempdir.*gitolite-admin/; git remote -v; ok; /file://gitolite-admin/ + DEF CS_1 = pwd; //tmp/tsh_tempdir.*gitolite-admin/; git remote -v; ok; /file:///gitolite-admin/ DEF CHECK_SETUP = CS_1; git log; ok; /fa7564c1b903ea3dce49314753f25b34b9e0cea0/ DEF CLONE = glt clone %1 file:///%2 @@ -71,7 +71,7 @@ try " # clone admin repo cd tsh_tempdir - glt clone admin --progress file://gitolite-admin + glt clone admin --progress file:///gitolite-admin cd gitolite-admin " or die "could not setup the test environment; errors:\n\n" . text() . "\n\n"; diff --git a/src/lib/Gitolite/Triggers/Kindergarten.pm b/src/lib/Gitolite/Triggers/Kindergarten.pm new file mode 100755 index 0000000..6274c3d --- /dev/null +++ b/src/lib/Gitolite/Triggers/Kindergarten.pm @@ -0,0 +1,99 @@ +package Gitolite::Triggers::Kindergarten; + +# http://www.great-quotes.com/quote/424177 +# "Doctor, it hurts when I do this." +# "Then don't do that!" + +# Prevent various things that sensible people shouldn't be doing anyway. List +# of things it prevents is at the end of the program. + +# If you were forced to enable this module because someone is *constantly* +# doing things that need to be caught, consider getting rid of that person. +# Because, really, who knows what *else* he/she is doing that can't be caught +# with some clever bit of code? + +use Gitolite::Rc; +use Gitolite::Common; + +use strict; +use warnings; + +my %active; +sub active { + # in rc, you either see just 'Kindergarten' to activate all features, or + # 'Kindergarten U0 CREATOR' (i.e., a space sep list of features after the + # word Kindergarten) to activate only those named features. + + # no features specifically activated; implies all of them are active + return 1 if not %active; + # else check if this specific feature is active + return 1 if $active{ +shift }; + + return 0; +} + +my ( $verb, $repo, $cmd, $args ); +sub input { + # get the features to be activated, if supplied + while ( $_[0] ne 'INPUT' ) { + $active{ +shift } = 1; + } + + # generally fill up variables you might use later + my $git_commands = "git-upload-pack|git-receive-pack|git-upload-archive"; + if ( $ENV{SSH_ORIGINAL_COMMAND} =~ /($git_commands) '\/?(\S+)'$/ ) { + $verb = $1; + $repo = $2; + } elsif ( $ENV{SSH_ORIGINAL_COMMAND} =~ /^(\S+) (.*)$/ ) { + $cmd = $1; + $args = $2; + } + + prevent_CREATOR($repo) if active('CREATOR') and $verb; + prevent_0(@ARGV) if active('U0') and @ARGV; +} + +sub prevent_CREATOR { + my $repo = shift; + _die "'CREATOR' not allowed as part of reponame" if $repo =~ /\bCREATOR\b/; +} + +sub prevent_0 { + my $user = shift; + _die "'0' is not a valid username" if $user eq '0'; +} + +1; + +__END__ + +CREATOR + + prevent literal 'CREATOR' from being part of a repo name + + a quirk deep inside gitolite would let this config + + repo foo/CREATOR/..* + C = ... + + allow the creation of repos like "foo/CREATOR/bar", i.e., the word CREATOR is + literally used. + + I consider this a totally pathological situation to check for. The worst that + can happen is someone ends up cluttering the server with useless repos. + + One solution could be to prevent this only for wild repos, but I can't be + bothered to fine tune this, so this module prevents even normal repos from + having the literal CREATOR in them. + + See https://groups.google.com/forum/#!topic/gitolite/cS34Vxix0Us for more. + +U0 + + prevent a user from being called literal '0' + + Ideally we should prevent keydir/0.pub (or variants) from being created, + but for "Then don't do that" purposes it's enough to prevent the user from + logging in. + + See https://groups.google.com/forum/#!topic/gitolite/F1IBenuSTZo for more. diff --git a/src/lib/Gitolite/Triggers/Mirroring.pm b/src/lib/Gitolite/Triggers/Mirroring.pm index 014408d..c88fc92 100644 --- a/src/lib/Gitolite/Triggers/Mirroring.pm +++ b/src/lib/Gitolite/Triggers/Mirroring.pm @@ -71,6 +71,9 @@ sub pre_git { # now you know the repo, get its mirroring details details($repo); + # print mirror status if at least one slave status file is present + print_status( $repo ) if not $rc{HUSH_MIRROR_STATUS} and $mode ne 'local' and glob("$rc{GL_REPO_BASE}/$repo.git/gl-slave-*.status"); + # we don't deal with any reads. Note that for pre-git this check must # happen *after* getting details, to give mode() a chance to die on "known # unknown" repos (repos that are in the config, but mirror settings @@ -189,8 +192,17 @@ sub post_git { } sub slaves { - my $ref = git_config( +shift, "^gitolite-options\\.mirror\\.slaves.*" ); - my %out = map { $_ => 1 } map { split } values %$ref; + my $repo = shift; + + my $ref = git_config( $repo, "^gitolite-options\\.mirror\\.slaves.*" ); + my %out = map { $_ => 'async' } map { split } values %$ref; + + $ref = git_config( $repo, "^gitolite-options\\.mirror\\.slaves\\.sync.*" ); + map { $out{$_} = 'sync' } map { split } values %$ref; + + $ref = git_config( $repo, "^gitolite-options\\.mirror\\.slaves\\.nosync.*" ); + map { $out{$_} = 'nosync' } map { split } values %$ref; + return %out; } @@ -222,10 +234,20 @@ sub push_to_slaves { delete $ENV{GL_USER}; # why? see src/commands/mirror for my $s ( sort keys %slaves ) { - system("gitolite mirror push $s $repo </dev/null >/dev/null 2>&1 &"); + system("gitolite mirror push $s $repo </dev/null >/dev/null 2>&1 &") if $slaves{$s} eq 'async'; + system("gitolite mirror push $s $repo </dev/null >/dev/null 2>&1") if $slaves{$s} eq 'sync'; + _warn "manual mirror push pending for '$s'" if $slaves{$s} eq 'nosync'; } $ENV{GL_USER} = $u; } +sub print_status { + my $repo = shift; + my $u = $ENV{GL_USER}; + delete $ENV{GL_USER}; + system("gitolite mirror status all $repo >&2"); + $ENV{GL_USER} = $u; +} + 1; diff --git a/src/lib/Gitolite/Triggers/Motd.pm b/src/lib/Gitolite/Triggers/Motd.pm new file mode 100644 index 0000000..6de80a2 --- /dev/null +++ b/src/lib/Gitolite/Triggers/Motd.pm @@ -0,0 +1,29 @@ +package Gitolite::Triggers::Motd; + +use Gitolite::Rc; +use Gitolite::Common; + +use strict; +use warnings; + +# print a message of the day to STDERR +# ---------------------------------------------------------------------- + +my $file = "gl-motd"; + +sub input { + # at present, we print it for every single interaction with gitolite. We + # may want to change that later; if we do, get code from Kindergarten.pm + # to get the gitcmd+repo or cmd+args so you can filter on them + + my $f = "$rc{GL_ADMIN_BASE}/$file"; + print STDERR slurp($f) if -f $f; +} + +sub pre_git { + my $repo = $_[1]; + my $f = "$rc{GL_REPO_BASE}/$repo.git/$file"; + print STDERR slurp($f) if -f $f; +} + +1; diff --git a/src/syntactic-sugar/continuation-lines b/src/syntactic-sugar/continuation-lines index 3c28f20..d63475f 100644 --- a/src/syntactic-sugar/continuation-lines +++ b/src/syntactic-sugar/continuation-lines @@ -21,6 +21,8 @@ sub sugar_script { my @out = (); my $keep = ''; for my $l (@$lines) { + # skip RULE_INFO lines if in continuation mode + next if $keep and $l =~ /^ *#/; if ( $l =~ s/\\$// ) { $keep .= $l; } else { diff --git a/src/syntactic-sugar/macros b/src/syntactic-sugar/macros index a0ddf90..a3493a4 100644 --- a/src/syntactic-sugar/macros +++ b/src/syntactic-sugar/macros @@ -25,7 +25,14 @@ sub sugar_script { sub expand { my $l = shift; - my ( $word, @arg ) = split ' ', $l; + my ( $word, @arg ); + + eval "require Text::ParseWords"; + if ($@) { + ( $word, @arg ) = split ' ', $l; + } else { + ( $word, @arg ) = Text::ParseWords::shellwords($l); + } my $v = $macro{$word}; $v =~ s/%(\d+)/$arg[$1-1] or die "macro '$word' needs $1 arguments at '$l'\n"/gem; return $v; diff --git a/src/triggers/expand-deny-messages b/src/triggers/expand-deny-messages new file mode 100755 index 0000000..78d138d --- /dev/null +++ b/src/triggers/expand-deny-messages @@ -0,0 +1,133 @@ +#!/usr/bin/perl +use strict; +use warnings; + +# program name: expand-deny-messages + +# DOCUMENTATION IS AT THE BOTTOM OF THIS FILE; PLEASE READ + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; + +my %attempted_access = ( + # see triggers.html + 'ACCESS_1' => { + 'R' => 'Repo read', + 'W' => 'Repo write', + }, + 'ACCESS_2' => { + 'W' => "Fast forward push", + '+' => "Rewind push branch or overwrite tag", + 'C' => "Create ref", + 'D' => "Delete ref", + } +); + +# env var to disable is set? +exit 0 if $ENV{GL_OPTION_EDM_DISABLE}; + +# argument 1 +my $a12 = shift; # ACCESS_1 or ACCESS_2 +exit 0 if $a12 !~ /^ACCESS_[12]$/; # shouldn't happen; error in rc file? + +# the rest of the arguments +my ( $repo, $user, $aa, $ref, $msg, $oldsha, $newsha ) = @ARGV; + +# we're only interested in deny messages +exit 0 if $msg !~ /DENIED/; + +print STDERR "\nFATAL -- ACCESS DENIED\n"; + +_info( "Repo", $repo ); +_info( "User", $user ); +_info( "Stage", ( $a12 eq 'ACCESS_1' ? "Before git was called" : "From git's update hook" ) ); +_info( "Ref", _ref($ref) ) if $a12 eq 'ACCESS_2'; +_info( "Operation", _op( $a12, $aa, $oldsha, $newsha ) ); + +print STDERR "\n"; +print STDERR "$ENV{GL_OPTION_EDM_EXTRA_INFO}\n\n" if $ENV{GL_OPTION_EDM_EXTRA_INFO}; + +# ------------------------------------------------------------------------ + +sub _ref { + my $r = shift; + return "VREF '$r'" if $r =~ s(^VREF/)(); + return "Branch '$r'" if $r =~ s(^refs/heads/)(); + return "Tag '$r'" if $r =~ s(^refs/tags/)(); + return "Non-standard ref '$r'"; +} + +sub _info { + printf STDERR "%-14s %-60s\n", @_; +} + +sub _op { + my ( $a12, $aa, $oldsha, $newsha ) = @_; + + # first remove the M part and save the text for later addition if needed + my $merge = ( $aa =~ s/M// ? " with merge commit" : "" ); + + # next, the attempted access is modified to reflect the actual operation being + # attempted. NOTE: this no longer necessarily reflects what the gitolite log + # file stores; it's more granular and truly distinguishes a branch create from + # an ff push, etc. Could help when user typos a branch name I suppose + $aa = 'C' if $oldsha and $oldsha eq '0' x 40; + $aa = 'D' if $newsha and $newsha eq '0' x 40; + + # then we map it, add merge text if any + my $op = $attempted_access{$a12}{$aa} || "Unknown operation '$aa'"; + $op .= $merge; + + return $op; +} + +__END__ + +ENABLING THE FEATURE +-------------------- + +To enable this feature, uncomment the line in the rc file if your gitolite was +installed recently enough. Otherwise you will need to add these lines to the +end of your rc file, just before the "%RC" block ends: + + ACCESS_1 => [ + 'expand-deny-messages', + ], + + ACCESS_2 => [ + 'expand-deny-messages', + ], + +Please don't miss the trailing commas! + +DISABLING IT FOR SPECIFIC REPOS +------------------------------- + +Once it is enabled at the rc file level, if you wish to disable it for +specific repositories just add a line like this to those repos: + + option ENV.EDM_DISABLE = 1 + +Or you can also disable it for all repos, then enable it for some: + + repo @all + option ENV.EDM_DISABLE = 1 + + # ... then later ... + + repo foo bar @baz + option ENV.EDM_DISABLE = 0 + +(options.html[1] and pages linked from it will explain how that works). + +[1]: http://gitolite.com/gitolite/options.html + +# SUPPLYING EXTRA INFORMATION +# --------------------------- + +You can also supply some extra information to be printed, by adding a line +like this to each repository in the gitolite.conf file: + + option ENV.EDM_EXTRA_INFO = "please contact alice@example.com" + +You could of course add it under a "repo @all" section if you like. diff --git a/src/triggers/post-compile/ssh-authkeys b/src/triggers/post-compile/ssh-authkeys index d99a86c..0ccb2e8 100755 --- a/src/triggers/post-compile/ssh-authkeys +++ b/src/triggers/post-compile/ssh-authkeys @@ -78,8 +78,9 @@ sub sanity { _die "'$glshell' found but not readable; this should NOT happen..." if not -r $glshell; _die "'$glshell' found but not executable; this should NOT happen..." if not -x $glshell; - _warn "$akdir missing; creating a new one" if not -d $akdir; - _warn "$akfile missing; creating a new one" if not -f $akfile; + my $n = " (this is normal on a brand new install)"; + _warn "$akdir missing; creating a new one\n$n" if not -d $akdir; + _warn "$akfile missing; creating a new one\n$n" if not -f $akfile; _mkdir( $akdir, 0700 ) if not -d $akdir; if ( not -f $akfile ) { diff --git a/src/triggers/post-compile/ssh-authkeys-shell-users b/src/triggers/post-compile/ssh-authkeys-shell-users index 176e450..2dd6643 100755 --- a/src/triggers/post-compile/ssh-authkeys-shell-users +++ b/src/triggers/post-compile/ssh-authkeys-shell-users @@ -2,8 +2,6 @@ use strict; use warnings; -use File::Temp qw(tempfile); - use lib $ENV{GL_LIBDIR}; use Gitolite::Rc; use Gitolite::Common; @@ -11,8 +9,6 @@ use Gitolite::Common; $|++; my $akfile = "$ENV{HOME}/.ssh/authorized_keys"; -my $sufile = $rc{SHELL_USERS_LIST} or exit 0; --r $sufile or _die "'$sufile' not readable"; # ---------------------------------------------------------------------- @@ -24,8 +20,32 @@ for my $su ( shell_users() ) { _print( $akfile, $aktext ); +# two methods to specify list of shell-capable users. (1) list of usernames +# as arguments to 'Shell' in rc file, (2) list of usernames in a plain text +# file whose name is the first argument to 'Shell' in the rc file. Or both! sub shell_users { - my @ret = grep { not /^#/ } slurp($sufile); - chomp(@ret); + my ($sufile, @ret); + + # backward compat for 3.6 and below. This code will be removed in 3.7. + # Also, the variable is ignored if you end up using the new variant (i.e., + # put a file name on the 'Shell' line itself). + $sufile = $rc{SHELL_USERS_LIST} if $rc{SHELL_USERS_LIST} and -r $rc{SHELL_USERS_LIST}; + + $sufile = shift @ARGV if @ARGV and -r $ARGV[0]; + + if ($sufile) { + @ret = grep { not /^#/ } slurp($sufile); + chomp(@ret); + } + + for my $u (@ARGV) { + # arguments placed in the rc file appear before the trigger name + last if $u eq 'POST_COMPILE'; + + push @ret, $u; + # no sanity checking, since the rc file can only be created by someone + # who already has shell access + } + _die "'Shell': enabled but no usernames supplied" unless @ret; return @ret; } diff --git a/src/triggers/post-compile/ssh-authkeys-split b/src/triggers/post-compile/ssh-authkeys-split index a1c5049..3e12e0e 100755 --- a/src/triggers/post-compile/ssh-authkeys-split +++ b/src/triggers/post-compile/ssh-authkeys-split @@ -15,9 +15,18 @@ # # - assumes no "@" sign in basenames of any multi-key files (single line file # may still have them) + # - assumes you don't have a subdir in keydir called "__split_keys__" + # - God help you if you try to throw in a putty key in there. +# - RUNNING "GITOLITE SETUP" WILL LOSE ALL THESE KEYS. So if you ever do +# that, you will then need to make a dummy push to the admin repo to add +# them back. If all your **admin** keys were in split keys, then you lost +# remote access. If that happens, log on to the server using "su - git" or +# such, then use the methods described in the "bypassing gitolite" section +# in "emergencies.html" instead of a remote push. + # SUPPORT # ------- # diff --git a/src/triggers/post-compile/update-git-configs b/src/triggers/post-compile/update-git-configs index 106791a..a58a85d 100755 --- a/src/triggers/post-compile/update-git-configs +++ b/src/triggers/post-compile/update-git-configs @@ -49,9 +49,6 @@ sub fixup_config { while ( my ( $key, $value ) = each( %{$gc} ) ) { next if $key =~ /^gitolite-options\./; if ( $value ne "" ) { - while ( my ( $mk, $mv ) = each %{ $rc{SAFE_CONFIG} } ) { - $value =~ s/%$mk/$mv/g; - } system( "git", "config", "--file", "$RB/$pr.git/config", $key, $value ); } else { system( "git", "config", "--file", "$RB/$pr.git/config", "--unset-all", $key ); diff --git a/src/triggers/post-compile/update-gitweb-access-list b/src/triggers/post-compile/update-gitweb-access-list index d7c023c..937226b 100755 --- a/src/triggers/post-compile/update-gitweb-access-list +++ b/src/triggers/post-compile/update-gitweb-access-list @@ -23,7 +23,7 @@ plf=`gitolite query-rc GITWEB_PROJECTS_LIST` # since mktemp does not honor umask, we just use it to generate a temp # filename (note: 'mktemp -u' on some systems, this gets close enough) tmpfile=`mktemp $plf.tmp_XXXXXXXX` -unlink $tmpfile; +rm -f $tmpfile; ( gitolite list-phy-repos | gitolite access % gitweb R any | grep -v DENIED diff --git a/src/triggers/repo-specific-hooks b/src/triggers/repo-specific-hooks index 79e87e7..efa9c6f 100755 --- a/src/triggers/repo-specific-hooks +++ b/src/triggers/repo-specific-hooks @@ -4,24 +4,30 @@ use warnings; # setup repo-specific hooks -# code is too long, but if you take out all the error/safety/sanity checks, -# it's basically just creating a symlink in <repo.git>/hooks pointing to some -# file inside $rc{LOCAL_CODE}/hooks/repo-specific - use lib $ENV{GL_LIBDIR}; use Gitolite::Rc; use Gitolite::Common; _die "repo-specific-hooks: LOCAL_CODE not defined in rc" unless $rc{LOCAL_CODE}; -_die "repo-specific-hooks: '$rc{LOCAL_CODE}' does not exist or is not a directory" unless -d $rc{LOCAL_CODE}; +_die "repo-specific-hooks: '$rc{LOCAL_CODE}/hooks/repo-specific' does not exist or is not a directory" unless -d "$rc{LOCAL_CODE}/hooks/repo-specific"; _chdir( $ENV{GL_REPO_BASE} ); -@ARGV = ("gitolite list-phy-repos | gitolite git-config -r % gitolite-options\\.hook\\. |"); +@ARGV = ("gitolite list-phy-repos | gitolite git-config -ev -r % gitolite-options\\.hook\\. |"); + +my $driver = "$rc{LOCAL_CODE}/hooks/multi-hook-driver"; +# Hook Driver +{ + local $/ = undef; + my $hook_text = <DATA>; + _print( $driver, $hook_text ); + chmod 0755, $driver; +} while (<>) { chomp; - my ( $repo, $hook, $code ) = split /\t/, $_; + my ( $repo, $hook, $codes ) = split /\t/, $_; + $codes ||= ''; # we don't allow fiddling with the admin repo if ( $repo eq 'gitolite-admin' ) { @@ -37,18 +43,53 @@ while (<>) { next; } - if ( $code =~ m(^/|\.\.) ) { - _warn "repo-specific-hooks: double dot or leading slash not allowed in '$code'"; - next; - } + my @codes = split /\s+/, $codes; - my $src = $rc{LOCAL_CODE} . "/hooks/repo-specific/$code"; my $dst = "$repo.git/hooks/$hook"; - unless ( -x $src ) { - _warn "repo-specific-hooks: '$src' doesn't exist or is not executable"; - next; + unlink( glob("$dst.*") ); + + my $counter = "h00"; + foreach my $code (@codes) { + if ( $code =~ m(^/|\.\.) ) { + _warn "repo-specific-hooks: double dot or leading slash not allowed in '$code'"; + next; + } + + my $src = $rc{LOCAL_CODE} . "/hooks/repo-specific/$code"; + my $dst = "$repo.git/hooks/$hook.$counter-$code"; + unless ( -x $src ) { + _warn "repo-specific-hooks: '$src' doesn't exist or is not executable"; + next; + } + unlink $dst; + symlink $src, $dst or _warn "could not symlink '$src' to '$dst'"; + $counter++; + + # no sanity checks for multiple overwrites of the same hook } + unlink $dst; - symlink $src, $dst or _warn "could not symlink '$src' to '$dst'"; - # no sanity checks for multiple overwrites of the same hook + symlink $driver, $dst or die "could not symlink '$driver' to '$dst'"; } + +__DATA__ +#/bin/sh + +# Determine what input the hook needs +# post-update takes args, pre/post-receive take stdin +type=args +stdin='' +[ $0 != hooks/post-update ] && { + type=stdin + stdin=`cat` +} + +for h in $0.*; do + [ -x $h ] || continue + if [ $type = args ] + then + $h $@ + else + echo "$stdin" | $h + fi +done diff --git a/src/triggers/set-default-roles b/src/triggers/set-default-roles index a8a2b4d..18ac28b 100755 --- a/src/triggers/set-default-roles +++ b/src/triggers/set-default-roles @@ -13,3 +13,8 @@ die() { echo "$@" >&2; exit 1; } cd $GL_REPO_BASE/$2.git || die "could not cd to $GL_REPO_BASE/$2.git" gitolite git-config -r $2 gitolite-options.default.roles | sort | cut -f3 | perl -pe 's/(\s)CREATOR(\s|$)/$1$ENV{GL_USER}$2/' > gl-perms + +# cache control, if rc says caching is on +gitolite query-rc -q CACHE && perl -I$GL_LIBDIR -MGitolite::Cache -e "cache_control('flush', '$2')"; + +exit 0 diff --git a/t/0-me-first.t b/t/0-me-first.t index 47784ae..12668f6 100755 --- a/t/0-me-first.t +++ b/t/0-me-first.t @@ -31,18 +31,18 @@ try " # basic clone cd .. - glt clone u1 file://aa u1aa; ok; /Cloning into 'u1aa'.../ + glt clone u1 file:///aa u1aa; ok; /Cloning into 'u1aa'.../ /warning: You appear to have cloned an empty repository/ [ -d u1aa ]; ok # basic clone deny - glt clone u4 file://aa u4aa; !ok; /R any aa u4 DENIED by fallthru/ + glt clone u4 file:///aa u4aa; !ok; /R any aa u4 DENIED by fallthru/ [ -d u4aa ]; !ok # basic push cd u1aa; ok tc z-507; ok; /master .root-commit. 7cf7624. z-507/ - glt push u1 origin HEAD; ok; /To file://aa/ + glt push u1 origin HEAD; ok; /To file:///aa/ /\\[new branch\\] *HEAD -> master/ # basic rewind @@ -61,7 +61,7 @@ try " # basic rewind deny cd .. - glt clone u2 file://aa u2aa; ok; /Cloning into 'u2aa'.../ + glt clone u2 file:///aa u2aa; ok; /Cloning into 'u2aa'.../ cd u2aa; ok tc g-776 g-777 g-778; ok; /master 9cbc181. g-778/ glt push u2 origin HEAD; ok; /284951d..9cbc181 HEAD -> master/ diff --git a/t/branch-perms.t b/t/branch-perms.t index 64b2fcb..e59baea 100755 --- a/t/branch-perms.t +++ b/t/branch-perms.t @@ -31,11 +31,11 @@ try "ADMIN_PUSH set1; !/FATAL/" or die text(); try " cd ..; ok - glt clone u1 file://aa; ok + glt clone u1 file:///aa; ok cd aa; ok tc l-995 l-996 l-997 l-998 l-999 l-1000 l-1001 l-1002 l-1003; ok; /master a788db9. l-1003/ - glt push u1 origin HEAD; ok; /To file://aa/ + glt push u1 origin HEAD; ok; /To file:///aa/ /\\* \\[new branch\\] HEAD -> master/ git branch dev; ok @@ -49,27 +49,27 @@ try " # u2 rewind master fail git reset --hard HEAD^; ok; /HEAD is now at 65d5f4a l-1002/ tc s-361; ok; /master b331651. s-361/ - glt push u2 file://aa +master; !ok; reject + glt push u2 file:///aa +master; !ok; reject /\\+ refs/heads/master aa u2 DENIED by fallthru/ # u3 rewind master succeed git reset --hard HEAD^; ok tc m-508; ok - glt push u3 file://aa +master; ok; /\\+ .* master -> master \\(forced update\\)/ + glt push u3 file:///aa +master; ok; /\\+ .* master -> master \\(forced update\\)/ # u4 push master succeed tc f-526; ok; - glt push u4 file://aa master; ok; /master -> master/ + glt push u4 file:///aa master; ok; /master -> master/ # u4 rewind master fail git reset --hard HEAD^; ok; - glt push u4 file://aa +master; !ok; /\\+ refs/heads/master aa u4 DENIED by fallthru/ + glt push u4 file:///aa +master; !ok; /\\+ refs/heads/master aa u4 DENIED by fallthru/ # u3 and u4 / dev foo -- all 4 fail - glt push u3 file://aa dev; !ok; /W refs/heads/dev aa u3 DENIED by fallthru/ - glt push u4 file://aa dev; !ok; /W refs/heads/dev aa u4 DENIED by fallthru/ - glt push u3 file://aa foo; !ok; /W refs/heads/foo aa u3 DENIED by fallthru/ - glt push u4 file://aa foo; !ok; /W refs/heads/foo aa u4 DENIED by fallthru/ + glt push u3 file:///aa dev; !ok; /W refs/heads/dev aa u3 DENIED by fallthru/ + glt push u4 file:///aa dev; !ok; /W refs/heads/dev aa u4 DENIED by fallthru/ + glt push u3 file:///aa foo; !ok; /W refs/heads/foo aa u3 DENIED by fallthru/ + glt push u4 file:///aa foo; !ok; /W refs/heads/foo aa u4 DENIED by fallthru/ # clean up for next set glt push u1 -f origin master dev foo @@ -77,14 +77,14 @@ try " # u5 push master fail tc l-417; ok - glt push u5 file://aa master; !ok; /W refs/heads/master aa u5 DENIED by refs/heads/master/ + glt push u5 file:///aa master; !ok; /W refs/heads/master aa u5 DENIED by refs/heads/master/ # u5 rewind dev succeed - glt push u5 file://aa +dev^:dev + glt push u5 file:///aa +dev^:dev ok; /\\+ .* dev\\^ -> dev \\(forced update\\)/ # u5 rewind foo fail - glt push u5 file://aa +foo^:foo + glt push u5 file:///aa +foo^:foo !ok; /\\+ refs/heads/foo aa u5 DENIED by fallthru/ # u5 tries to push foo; succeeds @@ -92,7 +92,7 @@ try " # u5 push foo succeed tc e-530; ok; - glt push u5 file://aa foo; ok; /foo -> foo/ + glt push u5 file:///aa foo; ok; /foo -> foo/ # u1 delete branch dev succeed glt push u1 origin :dev; ok; / - \\[deleted\\] *dev/ @@ -117,6 +117,6 @@ try " glt push u1 origin :dev; !ok; /D refs/heads/dev aa u1 DENIED by fallthru/ # u4 delete branch dev succeed - glt push u4 file://aa :dev; ok; / - \\[deleted\\] *dev/ + glt push u4 file:///aa :dev; ok; / - \\[deleted\\] *dev/ "; diff --git a/t/info-json.t b/t/info-json.t new file mode 100755 index 0000000..a78b79f --- /dev/null +++ b/t/info-json.t @@ -0,0 +1,183 @@ +#!/usr/bin/perl +use strict; +use warnings; + +# this is hardcoded; change it if needed +use lib "src/lib"; +use Gitolite::Test; +use JSON; + +# the info command +# ---------------------------------------------------------------------- + +try 'plan 162'; + +try "## info"; + +confreset;confadd ' + @t1 = t1 + repo @t1 + RW = u1 + R = u2 + repo t2 + RW = u2 + R = u1 + repo t3 + RW = u3 + R = u4 + + repo foo/..* + C = u1 + RW = CREATOR u3 +'; + +try "ADMIN_PUSH info; !/FATAL/" or die text(); +try " + /Initialized.*empty.*t1.git/ + /Initialized.*empty.*t2.git/ + /Initialized.*empty.*t3.git/ +"; + +my $href; # semi-global (or at least file scoped lexical!) + +# testing for info -json is a bit unusual. The actual tests are done within +# this test script itself, and we send Tsh just enough for it to decide if +# it's 'ok' or 'not ok' and print that. + +try "glt info u1 -json; ok"; +$href = from_json(text()); +try "## u1 test_gs"; +test_gs('u1'); +try "## u1"; +perm('foo/..*', 'r w C'); +perm('testing', 'R W c'); +perm('t1', 'R W c'); +perm('t2', 'R w c'); +perm('t3', 'r w c'); + +try "## u2"; +try "glt info u2 -json; ok"; +$href = from_json(text()); +perm('foo/..*', 'r w c'); +perm('testing', 'R W c'); +perm('t1', 'R w c'); +perm('t2', 'R W c'); +perm('t3', 'r w c'); + +try "## u3"; +try "glt info u3 -json; ok"; +$href = from_json(text()); +perm('foo/..*', 'R W c'); +perm('testing', 'R W c'); +perm('t1', 'r w c'); +perm('t2', 'r w c'); +perm('t3', 'R W c'); + +try "## u4"; +try "glt info u4 -json; ok"; +$href = from_json(text()); +perm('foo/..*', 'r w c'); +perm('testing', 'R W c'); +perm('t1', 'r w c'); +perm('t2', 'r w c'); +perm('t3', 'R w c'); + +try "## u5"; +try "glt info u5 -json; ok"; +$href = from_json(text()); +perm('foo/..*', 'r w c'); +perm('testing', 'R W c'); +perm('t1', 'r w c'); +perm('t2', 'r w c'); +perm('t3', 'r w c'); + +try "## u6"; +try "glt info u6 -json; ok"; +$href = from_json(text()); +perm('foo/..*', 'r w c'); +perm('testing', 'R W c'); +perm('t1', 'r w c'); +perm('t2', 'r w c'); +perm('t3', 'r w c'); + +try "## ls-remote foo/one"; +try "glt ls-remote u1 file:///foo/one; ok"; + +try "## u1"; +try "glt info u1 -json; ok; !/creator..:/"; +$href = from_json(text()); +perm('foo/..*', 'r w C'); +perm('foo/one', 'R W c'); +test_creator('foo/one', 'u1', 'undef'); + +try "## u2"; +try "glt info u2 -json; ok; !/creator..:/"; +$href = from_json(text()); +perm('foo/..*', 'r w c'); +perm('foo/one', 'r w c'); +test_creator('foo/one', 'u1', 'undef'); + +try "## u3"; +try "glt info u3 -json; ok; !/creator..:/"; +$href = from_json(text()); +perm('foo/..*', 'R W c'); +perm('foo/one', 'R W c'); +test_creator('foo/one', 'u1', 'undef'); + +try("## with -lc now"); + +try "## u1"; +try "glt info u1 -lc -json; ok"; +$href = from_json(text()); +perm('foo/..*', 'r w C'); +perm('foo/one', 'R W c'); +test_creator('foo/one', 'u1', 1); + +try "## u2"; +try "glt info u2 -lc -json; ok"; +$href = from_json(text()); +perm('foo/..*', 'r w c'); +perm('foo/one', 'r w c'); +test_creator('foo/one', 'u1', 'undef'); + +try "## u3"; +try "glt info u3 -lc -json; ok"; +$href = from_json(text()); +perm('foo/..*', 'R W c'); +perm('foo/one', 'R W c'); +test_creator('foo/one', 'u1', 1); + +# ---------------------------------------------------------------------- + +# test perms given repo and expected perms. (lowercase r/w/c means NOT +# expected, uppercase means expected) +sub perm { + my ($repo, $aa) = @_; + for my $aa1 (split ' ', $aa) { + my $exp = 1; + if ($aa1 =~ /[a-z]/) { + $exp = 'undef'; # we can't use 0, though I'd like to + $aa1 = uc($aa1); + } + my $perm = $href->{repos}{$repo}{perms}{$aa1} || 'undef'; + try 'perl $_ = "' . $perm . '"; /' . $exp . '/'; + } +} + +# test versions in greeting string +sub test_gs { + my $glu = shift; + my $res = ( $href->{GL_USER} eq $glu ? 1 : 'undef' ); + try 'perl $_ = "' . $res . '"; /1/'; + $res = ( $href->{gitolite_version} =~ /^v3.[5-9]/ ? 1 : 'undef' ); + try 'perl $_ = "' . $res . '"; /1/'; + $res = ( $href->{git_version} =~ /^1.[6-9]/ ? 1 : 'undef' ); + try 'perl $_ = "' . $res . '"; /1/'; +} + +# test creator +sub test_creator { + my ($r, $c, $exp) = @_; + my $res = ( ($href->{repos}{$r}{creator} || '') eq $c ? 1 : 'undef' ); + try 'perl $_ = "' . $res . '"; /' . $exp . '/'; +} diff --git a/t/mirror-test-rc b/t/mirror-test-rc index d06fbc0..1d76783 100644 --- a/t/mirror-test-rc +++ b/t/mirror-test-rc @@ -37,6 +37,8 @@ # uncomment (and change) this if you wish # DEFAULT_ROLE_PERMS => 'READERS @all', + # CACHE => 'Redis', + # ------------------------------------------------------------------ # rc variables used by various features diff --git a/t/perm-default-roles.t b/t/perm-default-roles.t index 8535609..1a56ff8 100755 --- a/t/perm-default-roles.t +++ b/t/perm-default-roles.t @@ -9,7 +9,7 @@ use Gitolite::Test; # permissions using role names # ---------------------------------------------------------------------- -try "plan 27"; +try "plan 33"; try "DEF POK = !/DENIED/; !/failed to push/"; my $rb = `gitolite query-rc -n GL_REPO_BASE`; @@ -107,10 +107,20 @@ try " cd .. +gitolite access foo/u1/u1r3 u4 W + !ok + !/refs/../ + /W any foo/u1/u1r3 u4 DENIED by fallthru/ + # make foo/u1/u1r3 glt clone u1 file:///foo/u1/u1r3 /Initialized empty Git repository in .*/foo/u1/u1r3.git// +gitolite access foo/u1/u1r3 u4 W + ok + /refs/../ + !/W any foo/u1/u1r3 u4 DENIED by fallthru/ + # make bar/u3/u3r3 glt clone u3 file:///bar/u3/u3r3 /Initialized empty Git repository in .*/bar/u3/u3r3.git// diff --git a/t/repo-specific-hooks.t b/t/repo-specific-hooks.t new file mode 100755 index 0000000..88976ca --- /dev/null +++ b/t/repo-specific-hooks.t @@ -0,0 +1,210 @@ +#!/usr/bin/perl +use strict; +use warnings; + +# this is hardcoded; change it if needed +use lib "src/lib"; +use Gitolite::Test; + +# test script for partial copy feature +# ---------------------------------------------------------------------- + +try "plan 117"; +my $h = $ENV{HOME}; +my $rb = `gitolite query-rc -n GL_REPO_BASE`; + +try 'cd tsh_tempdir; mkdir -p local/hooks/repo-specific'; + +foreach my $h (qw/first second/) { + put "local/hooks/repo-specific/$h", "#!/bin/sh +echo \$0 +if [ \$# -ne 0 ]; then + echo \$0 has args: \$@ +else + echo \$0 has stdin: `cat` +fi +"; +} +try 'chmod +x local/hooks/repo-specific/*'; + +try 'pwd'; +my $tempdir = join("\n", sort (lines())); +try 'cd gitolite-admin'; + +try "# Enable LOCAL_CODE and repo-specific-hooks + cat $h/.gitolite.rc + perl s/# 'repo-specific-hooks'/'repo-specific-hooks'/ + perl s%# LOCAL_CODE%LOCAL_CODE => '$tempdir/local', #% + put $h/.gitolite.rc +"; + +confreset;confadd ' + repo foo + RW+ = @all + + repo bar + RW+ = @all + + repo baz + RW+ = @all +'; + +try "ADMIN_PUSH repo-specific-hooks-0; !/FATAL/" or die text(); + +try " + /Init.*empty.*foo\\.git/ + /Init.*empty.*bar\\.git/ + /Init.*empty.*baz\\.git/ +"; + +my $failing_hook = "#!/bin/sh +exit 1 +"; + +# Place a existing hooks in repos +put "$rb/foo.git/hooks/post-recieve", $failing_hook; +put "$rb/bar.git/hooks/pre-recieve", $failing_hook; +put "$rb/baz.git/hooks/post-update", $failing_hook; + +try "# Verify hooks + ls -l $rb/foo.git/hooks/*; ok; !/post-receive -. .*local/hooks/multi-hook-driver/ + ls -l $rb/bar.git/hooks/*; ok; !/pre-receive -. .*local/hooks/multi-hook-driver/ + ls -l $rb/baz.git/hooks/*; ok; !/post-update -. .*local/hooks/multi-hook-driver/ +"; + +confreset;confadd ' + repo foo + RW+ = @all + option hook.post-receive = first + + repo bar + RW+ = @all + option hook.pre-receive = first second + + repo baz + RW+ = @all + option hook.post-receive = first + option hook.post-update = first second +'; + + +try "ADMIN_PUSH repo-specific-hooks-1; !/FATAL/" or die text(); + +try "# Verify hooks + ls -l $rb/foo.git/hooks/*; ok; /post-receive.h00-first/ + !/post-receive.h01/ + /post-receive -. .*local/hooks/multi-hook-driver/ + ls -l $rb/bar.git/hooks/*; ok; /pre-receive.h00-first/ + /pre-receive.h01-second/ + /pre-receive -. .*local/hooks/multi-hook-driver/ + ls -l $rb/baz.git/hooks/*; ok; /post-receive.h00-first/ + /post-update.h00-first/ + /post-update.h01-second/ + /post-update -. .*local/hooks/multi-hook-driver/ +"; + +try " + cd .. + + # Single hook still works + [ -d foo ]; !ok; + CLONE admin foo; ok; /empty/; /cloned/ + cd foo + tc a1; ok; /ee47f8b/ + PUSH admin master; ok; /new.*master -. master/ + /hooks/post-receive.h00-first/ + !/post-receive.*has args:/ + /post-receive.h00-first has stdin: 0000000000000000000000000000000000000000 ee47f8b6be2160ad1a3f69c97a0cb3d488e6657e refs/heads/master/ + + cd .. + + # Multiple hooks fired + [ -d bar ]; !ok; + CLONE admin bar; ok; /empty/; /cloned/ + cd bar + tc a2; ok; /cfc8561/ + PUSH admin master; ok; /new.*master -. master/ + /hooks/pre-receive.h00-first/ + !/hooks/pre-recieve.*has args:/ + /hooks/pre-receive.h00-first has stdin: 0000000000000000000000000000000000000000 cfc8561c7827a8b94df6c5dad156383d4cb210f5 refs/heads/master/ + /hooks/pre-receive.h01-second/ + !/hooks/pre-receive.h01.*has args:/ + /hooks/pre-receive.h01-second has stdin: 0000000000000000000000000000000000000000 cfc8561c7827a8b94df6c5dad156383d4cb210f5 refs/heads/master/ + + cd .. + + # Post-update has stdin instead of arguments + [ -d baz ]; !ok; + CLONE admin baz; ok; /empty/; /cloned/ + cd baz + tc a3; ok; /2863617/ + PUSH admin master; ok; /new.*master -. master/ + /hooks/post-receive.h00-first/ + !/hooks/post-receive.h00.*has args:/ + /hooks/post-receive.h00-first has stdin: 0000000000000000000000000000000000000000 28636171ae703f42fb17c312c6b6a078ed07a2cd refs/heads/master/ + /hooks/post-update.h00-first/ + /hooks/post-update.h00-first has args: refs/heads/master/ + !/hooks/post-update.h00.*has stdin:/ + /hooks/post-update.h01-second/ + /hooks/post-update.h01-second has args: refs/heads/master/ + !/hooks/post-update.h01.*has stdin:/ +"; + +# Verify hooks are removed properly + +confreset;confadd ' + repo foo + RW+ = @all + option hook.post-receive = + + repo bar + RW+ = @all + option hook.pre-receive = second + + repo baz + RW+ = @all + option hook.post-receive = + option hook.post-update = second +'; + +try "ADMIN_PUSH repo-specific-hooks-02; !/FATAL/" or die text(); + +try " + ls $rb/foo.git/hooks/*; ok; !/post-receive/ + ls $rb/bar.git/hooks/*; ok; !/pre-receive.*first/ + /pre-receive.h00-second/ + ls $rb/baz.git/hooks/*; ok; !/post-receive/ + !/post-update.*first/ + /post-update.h00-second/ +"; + +try " + cd .. + + # Foo has no hooks + cd foo + tc b1; ok; /7ef69de/ + PUSH admin master; ok; /master -. master/ + !/hooks/post-receive/ + + cd .. + + # Bar only has the second hook + cd bar + tc b2; ok; /cc7808f/ + PUSH admin master; ok; /master -. master/ + /hooks/pre-receive.h00-second/ + !/hooks/pre-receive.*has args:/ + /hooks/pre-receive.h00-second has stdin: 0000000000000000000000000000000000000000 cc7808f77c7c7d705f82dc54dc3152146175768f refs/heads/master/ + + cd .. + + # Baz has no post-receive and keeps the second hook for post-update + cd baz + tc b3; ok; /8d20101/ + PUSH admin master; ok; /master -. master/ + !/hooks/post-receive.*/ + /hooks/post-update.h00-second/ + /hooks/post-update.h00-second has args: refs/heads/master/ + !/hooks/post-update.*has stdin/ +"; |