diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 14:17:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 14:17:27 +0000 |
commit | aae1a14ea756102251351d96e2567b4986d30e2b (patch) | |
tree | a1af617672e26aee4c1031a3aa83e8ff08f6a0a5 /src/commands | |
parent | Initial commit. (diff) | |
download | gitolite3-aae1a14ea756102251351d96e2567b4986d30e2b.tar.xz gitolite3-aae1a14ea756102251351d96e2567b4986d30e2b.zip |
Adding upstream version 3.6.12.upstream/3.6.12upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
33 files changed, 2924 insertions, 0 deletions
diff --git a/src/commands/1plus1 b/src/commands/1plus1 new file mode 100755 index 0000000..1d94006 --- /dev/null +++ b/src/commands/1plus1 @@ -0,0 +1,41 @@ +#!/usr/bin/perl +use strict; +use warnings; + +# import LOCK_* +use Fcntl qw(:flock); + +my $lockbase = shift; # suggested: $GL_REPO_BASE/$GL_REPO.git/.gl-mirror-push-lock.$COPY_NAME +my @cmd_plus_args = @ARGV; # the actual 'gitolite mirror ...' command +@ARGV = (); + +# ---------------------------------------------------------------------- + +open( my $fhrun, ">", "$lockbase.run" ) or die "open '$lockbase.run' failed: $!"; +if ( flock( $fhrun, LOCK_EX | LOCK_NB ) ) { + # got run lock; you're good to go + + system(@cmd_plus_args); + + flock( $fhrun, LOCK_UN ); + exit 0; +} + +# "run" lock failed; someone is already running the command + +open( my $fhqueue, ">", "$lockbase.queue" ) or die "open '$lockbase.queue' failed: $!"; +if ( flock( $fhqueue, LOCK_EX | LOCK_NB ) ) { + # got queue lock, now block waiting for "run" lock + flock( $fhrun, LOCK_EX ); + # got run lock, so take yourself out of "queue" state, then run + flock( $fhqueue, LOCK_UN ); + + system(@cmd_plus_args); + + flock( $fhrun, LOCK_UN ); + exit 0; +} + +# "queue" lock also failed; someone is running AND someone is queued; we can go home +say STDERR "INFO: nothing to do/queue; '$lockbase' already running and 1 in queue"; +exit 0; diff --git a/src/commands/D b/src/commands/D new file mode 100755 index 0000000..016a365 --- /dev/null +++ b/src/commands/D @@ -0,0 +1,131 @@ +#!/bin/sh + +# ---------------------------------------------------------------------- +# ADMINISTRATOR NOTES: +# ---------------------------------------------------------------------- + +# - set TRASH_CAN in the rc if you don't like the default. It should be +# relative to GL_REPO_BASE or an absolute value. It should also be on the +# same filesystem as GL_REPO_BASE, otherwise the 'mv' will take too long. + +# - you could set TRASH_SUFFIX also but I recomend you leave it as it is + +# - run a cron job to delete old repos based on age (the TRASH_SUFFIX has a +# timestamp); your choice how/how often you do that + +# - you can completely disable the 'rm' command by setting an rc variable +# called D_DISABLE_RM to "1". +# ---------------------------------------------------------------------- + +# ---------------------------------------------------------------------- +# Usage: ssh git@host D <subcommand> <argument> +# +# The whimsically named "D" command deletes repos ("D" is a counterpart to the +# "C" permission which lets you create repos. Which also means that, just +# like "C", it only works for wild repos). +# +# There are two kinds of deletions: 'rm' removes a repo completely, while +# 'trash' moves it to a trashcan which can be recovered later (upto a time +# limit that your admin will tell you). +# +# The 'rm', 'lock', and 'unlock' subcommands: +# Initially, all repos are "locked" against 'rm'. The correct sequence is +# ssh git@host D unlock repo +# ssh git@host D rm repo +# Since the initial condition is always locked, the "lock" command is +# rarely used but it is there if you want it. +# +# The 'trash', 'list-trash', and 'restore' subcommands: +# You can 'trash' a repo, which moves it to a special place: +# ssh git@host D trash repo +# You can then 'list-trash' +# ssh git@host D list-trash +# which prints something like +# repo/2012-04-11_05:58:51 +# allowing you to restore by saying +# ssh git@host D restore repo/2012-04-11_05:58:51 + +die() { echo "$@" >&2; exit 1; } +usage() { perl -lne 'print substr($_, 2) if /^# Usage/../^$/' < $0; exit 1; } +[ -z "$1" ] && usage +[ "$1" = "-h" ] && usage +[ "$1" != "list-trash" ] && [ -z "$2" ] && usage +[ -z "$GL_USER" ] && die GL_USER not set + +# ---------------------------------------------------------------------- +cmd=$1 +repo=$2 +# ---------------------------------------------------------------------- +RB=`gitolite query-rc GL_REPO_BASE`; cd $RB +TRASH_CAN=`gitolite query-rc TRASH_CAN`; tcan=Trash; TRASH_CAN=${TRASH_CAN:-$tcan} +TRASH_SUFFIX=`gitolite query-rc TRASH_SUFFIX`; tsuf=`date +%Y-%m-%d_%H:%M:%S`; TRASH_SUFFIX=${TRASH_SUFFIX:-$tsuf} +# ---------------------------------------------------------------------- + +owner_or_die() { + gitolite owns "$repo" || die You are not authorised +} + +# ---------------------------------------------------------------------- + +if [ "$cmd" = "rm" ] +then + + gitolite query-rc -q D_DISABLE_RM && die "sorry, 'unlock' and 'rm' are disabled" + + owner_or_die + [ -f $repo.git/gl-rm-ok ] || die "'$repo' is locked!" + rm -rf $repo.git + echo "'$repo' is now gone!" + +elif [ "$cmd" = "lock" ] +then + + owner_or_die + rm -f $repo.git/gl-rm-ok + echo "'$repo' is now locked" + +elif [ "$cmd" = "unlock" ] +then + + gitolite query-rc -q D_DISABLE_RM && die "sorry, 'unlock' and 'rm' are disabled" + + owner_or_die + touch $repo.git/gl-rm-ok + echo "'$repo' is now unlocked" + +elif [ "$cmd" = "trash" ] +then + + owner_or_die + mkdir -p $TRASH_CAN/$repo 2>/dev/null || die "failed creating directory in trashcan" + [ -d $TRASH_CAN/$repo/$TRASH_SUFFIX ] && die "try again in a few seconds..." + mv $repo.git $TRASH_CAN/$repo/$TRASH_SUFFIX + echo "'$repo' moved to trashcan" + +elif [ "$cmd" = "list-trash" ] +then + + cd $TRASH_CAN 2>/dev/null || exit 0 + find . -name gl-creator | sort | while read t + do + owner= + owner=`cat "$t"` + [ "$owner" = "$GL_USER" ] && dirname $t + done | cut -c3- + +elif [ "$cmd" = "restore" ] +then + + owner= + owner=`cat $TRASH_CAN/$repo/gl-creator 2>/dev/null` + [ "$owner" = "$GL_USER" ] || die "'$repo' is not yours!" + + cd $TRASH_CAN + realrepo=`dirname $repo` + [ -d $RB/$realrepo.git ] && die "'$realrepo' already exists" + mv $repo $RB/$realrepo.git + echo "'$repo' restored to '$realrepo'" + +else + die "unknown subcommand '$cmd'" +fi diff --git a/src/commands/access b/src/commands/access new file mode 100755 index 0000000..7d4a5b9 --- /dev/null +++ b/src/commands/access @@ -0,0 +1,173 @@ +#!/usr/bin/perl -s +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; +use Gitolite::Common; +use Gitolite::Conf::Load; + +our ( $q, $s, $h ); # quiet, show, help + +=for usage +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). For '-s', see below. + + - repo: mandatory + - user: mandatory + - perm: defauts to '+'. Valid values: R, W, +, C, D, M + - ref: defauts to 'any'. See notes below + +Notes: + - 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. + + - you can also test the ability to create wild repos if you set GL_USER to + the username and use ^C as the permission to check for. +=cut + +usage() if not @ARGV >= 2 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 =~ $REF_OR_FILENAME_PATT ); + +my $ret = ''; + +if ( $repo ne '%' and $user ne '%' ) { + # single repo, single user; no STDIN + $ret = access( $repo, $user, adjust_aa($repo, $aa), $ref ); + + show($ret) if $s; + + # adjust for fallthru in VREFs + $ret =~ s/DENIED by fallthru/allowed by fallthru/ if $ref =~ m(^VREF/); + + if ( $ret =~ /DENIED/ ) { + print "$ret\n" unless $q; + exit 1; + } + + print "$ret\n" unless $q; + exit 0; +} + +$repo = '' if $repo eq '%'; +$user = '' if $user eq '%'; + +_die "'-q' and '-s' meaningless in pipe mode" if $q or $s; +@ARGV = (); +while (<>) { + my @in = split; + my $r = $repo || shift @in; + my $u = $user || shift @in; + $ret = access( $r, $u, adjust_aa($r, $aa), $ref ); + print "$r\t$u\t$ret\n"; +} + +sub adjust_aa { + my ($repo, $aa) = @_; + $aa = 'W' if $aa eq 'C' and not option($repo, 'CREATE_IS_C'); + $aa = '+' if $aa eq 'D' and not option($repo, 'DELETE_IS_D'); + $aa = 'W' if $aa eq 'M' and not option($repo, 'MERGE_CHECK'); + return $aa; +} + +sub show { + my $ret = shift; + die "repo already exists; ^C won't work\n" if $ret =~ /DENIED by existence/; + + my $in = $rc{RULE_TRACE} or die "this should not happen! $ret"; + + 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 => fallthru; access denied for normal refs, allowed for VREFs + +"; + + 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/compile-template-data b/src/commands/compile-template-data new file mode 100755 index 0000000..e4ef86e --- /dev/null +++ b/src/commands/compile-template-data @@ -0,0 +1,101 @@ +#!/usr/bin/perl +use strict; +use warnings; + +# read template data to produce gl-perms and gl-repo-groups files in each +# $repo dir. Create the repo if needed, using the wild repos create logic +# (with a "creator" of "gitolite-admin"!), though they're not really wild +# repos. + +# see rule-templates.html in the gitolite documentation site. + +# pure text manipulation (and very little of that!), no git or gitolite +# functions, no access checks, no possibility of a performance drama (or at +# least not a *complex* performance drama) + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; +use Gitolite::Common; +use Gitolite::Conf::Load; +use Gitolite::Conf::Store; + +my $rb = $rc{GL_REPO_BASE}; + +@ARGV = `find $rc{GL_ADMIN_BASE}/conf -type f -name "*.conf" | sort`; chomp(@ARGV); +# we don't see the files in the exact same order that gitolite compile sees +# them, but we don't need to, for the data we are interested in (as long as +# you don't break up one repo's data across multiple files!) + +# XXX We also potentially see more; a conf file may be in the directory, but +# not pulled in via an 'include' or 'subconf', so it doesn't exist as far as +# 'gitolite compile' is concerned, but here we *do* pull it in. + +my $repos = ''; +my $perms = ''; +my $list = ''; # list of templates to apply +my $lip = ''; # line in progress +while (<>) { + chomp; + next unless /^=begin template-data$/ .. /^=end$/ and not /^=(begin|end)/; + + next unless /\S/; + next if /^\s*#/; + + s/\t/ /g; # all the same to us + + # handle continuation lines (backslash as last character) + if (/\\$/) { + s/\\$//; + $lip .= $_; + next; + } + $_ = $lip . $_; + $lip = ''; + + _warn("bad line: $_"), next if m([^ \w.\@/=-]); # silently ignore lines that have characters we don't need + if (/^\s*repo\s+(\S.*)=\s*(\S.*)$/) { + flush($repos, $list, $perms); + $repos = $1; + $perms = ''; + $list = $2; + + } elsif (/^\s*(\S+)\s*=\s*(\S.*)$/) { + $perms .= "$1 = $2\n"; + } else { + # probably a blank line or a comment line. If not, well *shrug* + } +} +flush($repos, $list, $perms); + +sub flush { + my ($r, $l, $p) = @_; + return unless $r and $l and $p; + $l =~ s/\s+/ /g; + + my @r = split ' ', $r; + while (@r) { + my $r1 = shift @r; + if ($r1 =~ m(^@)) { + my @g = @{ Gitolite::Conf::Load::list_members($r1) }; + _warn "undefined group '$r1'" unless @g; + unshift @r, @g; + next; + } + + flush_1($r1, $l, $p); + } +} +sub flush_1 { + my ($repo, $list, $perms) = @_; + + # beware of wild characters! + return unless $repo =~ $REPONAME_PATT; + + if (not -d "$rb/$repo.git") { + new_wild_repo( $repo, 'gitolite-admin', 'template-data' ); + } + + _print("$rb/$repo.git/gl-repo-groups", $list); + + _print("$rb/$repo.git/gl-perms", $perms); +} diff --git a/src/commands/config b/src/commands/config new file mode 100755 index 0000000..214158b --- /dev/null +++ b/src/commands/config @@ -0,0 +1,110 @@ +#!/usr/bin/perl +use 5.10.0; + +# ---- WARNING ---- + +# If your site makes a distinction between "right to push the admin repo" and +# "right to run arbitrary commands on the server" (i.e., if not all of your +# "admins" have shell access to the server), this is a security risk. If that +# is the case, DO NOT ENABLE THIS COMMAND. + +# ---------------------------------------------------------------------- +# gitolite command to allow "git config" on repos (with some restrictions) + +# (Not to be confused with the 'git-config' command, which is used only in +# server-side scripts, not remotely.) + +# setup: +# 1. Enable the command by adding it to the COMMANDS section in the ENABLE +# list in the rc file. (Have you read the warning above?) +# +# 2. Specify configs allowed to be changed by the user. This is a space +# separated regex list. For example: + +# repo ... +# ... (various rules) ... +# option user-configs = hook\..* foo.bar[0-9].* + +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Easy; +use Gitolite::Common; + +# ---------------------------------------------------------------------- +# usage + +=for usage +Usage: ssh git@host config <repo> [git config options] + +Runs "git config" in the repo. Only the following 3 syntaxes are supported +(see 'man git-config'): + + --add name value + --get-all name + --unset-all name + --list + +Your administrator should tell you what keys are allowed for the "name". +=cut + +# ---------------------------------------------------------------------- +# arg checks + +my %nargs = qw( + --add 3 + --get-all 2 + --unset-all 2 + --list 1 + ); + +usage() if not @ARGV or $ARGV[0] eq '-h'; + +my $repo = shift; + +my $op = shift; +usage() unless $op and exists $nargs{$op}; + +# ---------------------------------------------------------------------- +# authorisation checks + +die "sorry, you are not authorised\n" unless + owns($repo) + or + ( ( $op eq '--get-all' or $op eq '--list' ) + ? can_read($repo) + : ( can_write($repo) and option( $repo, 'writer-is-owner' ) ) + ); + +# ---------------------------------------------------------------------- +# key validity checks + +unless ($op eq '--list') { + my $key = shift; + + my $val = ''; + $val = join(" ", @ARGV) if @ARGV; + # values with spaces embedded get flattened by sshd when it passes + # SSH_ORIGINAL_COMMAND to gitolite. In this specific instance, we will + # pretend we know what the user meant, and join up the last 1+ args into + # one space-separated arg. + + my $user_configs = option( $repo, 'user-configs' ); + # this is a space separated list of allowed config keys + my @validkeys = split( ' ', ( $user_configs || '' ) ); + my @matched = grep { $key =~ /^$_$/i } @validkeys; + _die "config '$key' not allowed\n" if ( @matched < 1 ); + + @ARGV = ($key); + push @ARGV, $val if $val; +} + +# ---------------------------------------------------------------------- +# go! + +unshift @ARGV, $op; +usage() unless @ARGV == $nargs{$op}; + +_chdir("$rc{GL_REPO_BASE}/$repo.git"); +_system( "git", "config", @ARGV ); diff --git a/src/commands/create b/src/commands/create new file mode 100755 index 0000000..8565e68 --- /dev/null +++ b/src/commands/create @@ -0,0 +1,29 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; +use Gitolite::Common; +use Gitolite::Conf::Load; +use Gitolite::Conf::Store; + +=for usage +create -- create a wild repo. + +Usage: + ssh git@host create <repo> +=cut + +usage() if @ARGV != 1 or $ARGV[0] eq '-h'; + +$ENV{GL_USER} or _die "GL_USER not set"; + +my $repo = shift; +_die "invalid repo '$repo'" unless $repo =~ $REPONAME_PATT; + +my $ret = access( $repo, $ENV{GL_USER}, '^C', 'any' ); +_die "repo already exists or you are not authorised to create it" if $ret =~ /DENIED/; + +new_wild_repo( $repo, $ENV{GL_USER}, 'create' ); +gl_log( 'create', $repo, $ENV{GL_USER}, 'create' ); diff --git a/src/commands/creator b/src/commands/creator new file mode 100755 index 0000000..702df73 --- /dev/null +++ b/src/commands/creator @@ -0,0 +1,40 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; +use Gitolite::Common; +use Gitolite::Conf::Load; + +=for usage +Usage: gitolite creator [-n] <reponame> [<username>] + +Print the creator name for the repo. A '-n' suppresses the newline. + +When an optional username is supplied, it checks if the user is the creator of +the repo and returns an exit code (shell truth, 0 for success) instead of +printing anything, which makes it possible to do this in shell: + + if gitolite creator someRepo someUser + then + ... +=cut + +usage() if not @ARGV or $ARGV[0] eq '-h'; +my $nl = "\n"; +if ( $ARGV[0] eq '-n' ) { + $nl = ''; + shift; +} +my $repo = shift; +my $user = shift || ''; + +my $creator = ''; +$creator = creator($repo) if not repo_missing($repo); +if ($user) { + exit 0 if $creator eq $user; + exit 1; +} +return ( $creator eq $user ) if $user; +print "$creator$nl"; diff --git a/src/commands/desc b/src/commands/desc new file mode 100755 index 0000000..4a4bf20 --- /dev/null +++ b/src/commands/desc @@ -0,0 +1,49 @@ +#!/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 new file mode 100755 index 0000000..49994fc --- /dev/null +++ b/src/commands/fork @@ -0,0 +1,57 @@ +#!/bin/sh + +# Usage: ssh git@host fork <repo1> <repo2> +# +# Forks repo1 to repo2. You must have read permissions on repo1, and create +# ("C") permissions for repo2, which of course must not exist. +# +# A fork is functionally the same as cloning repo1 to a client and pushing it +# to a new repo2. It's just a little more efficient, not just in network +# traffic but because it uses git clone's "-l" option to share the object +# store also, so it is likely to be almost instantaneous, regardless of how +# big the repo actually is. + +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 + +# ---------------------------------------------------------------------- +from=$1; shift +to=$1; shift +[ -z "$to" ] && usage + +gitolite access -q "$from" $GL_USER R any || die "'$from' does not exist or you are not allowed to read it" +gitolite access -q "$to" $GL_USER ^C any || die "'$to' already exists or you are not allowed to create it" + +# ---------------------------------------------------------------------- +# IMPORTANT NOTE: checking whether someone can create a repo is done as above. +# However, make sure that the env var GL_USER is set, and that too to the same +# value as arg-2 of the access command), otherwise it won't work. + +# Ideally, you'll leave such code to me. There's a reason ^C is not listed in +# the help message for 'gitolite access'. +# ---------------------------------------------------------------------- + +# clone $from to $to +git clone --bare -l $GL_REPO_BASE/$from.git $GL_REPO_BASE/$to.git +[ $? -ne 0 ] && exit 1 + +echo "$from forked to $to" >&2 + +# fix up creator, default role permissions (gl-perms), and hooks +cd $GL_REPO_BASE/$to.git +echo $GL_USER > gl-creator + +gitolite query-rc -q LOCAL_CODE && ln -sf `gitolite query-rc LOCAL_CODE`/hooks/common/* hooks +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..572aba6 --- /dev/null +++ b/src/commands/git-annex-shell @@ -0,0 +1,71 @@ +#!/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 '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 new file mode 100755 index 0000000..94211de --- /dev/null +++ b/src/commands/git-config @@ -0,0 +1,97 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use Getopt::Long; +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; +use Gitolite::Common; +use Gitolite::Conf::Load; + +=for usage +Usage: gitolite git-config [-n] [-q] [-r] <repo> <key|pattern> + +Print git config keys and values for the given repo. The key is either a full +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 + +Notes: + +1. When the key is treated as a pattern, prints: + + reponame<tab>key<tab>value<newline> + + Otherwise the output is just the value. + +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, $ev ) = (0) x 5; +GetOptions( + 'n' => \$nonl, + 'q' => \$quiet, + 'r' => \$regex, + 'h' => \$help, + 'ev' => \$ev, +) or usage(); + +my ( $repo, $key ) = @ARGV; +usage() unless $key; + +my $ret = ''; + +if ( $repo ne '%' and $key ne '%' ) { + # single repo, single key; no STDIN + $key = "^\Q$key\E\$" unless $regex; + + $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; + + # unlike access, there's nothing to print if we don't find any matching keys + exit 1 unless %$ret; + + if ($regex) { + map { print "$repo\t$_\t" . $ret->{$_} . "\n" } sort keys %$ret unless $quiet; + } else { + map { print $ret->{$_} . ( $nonl ? "" : "\n" ) } sort keys %$ret unless $quiet; + } + exit 0; +} + +$repo = '' if $repo eq '%'; +$key = '' if $key eq '%'; + +_die "'-q' doesn't go with using a pipe" if $quiet; +@ARGV = (); +while (<>) { + my @in = split; + my $r = $repo || shift @in; + my $k = $key || shift @in; + $k = "^\Q$k\E\$" unless $regex; + $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 new file mode 100755 index 0000000..cf54084 --- /dev/null +++ b/src/commands/help @@ -0,0 +1,43 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; +use Gitolite::Common; + +=for usage +Usage: ssh git@host help # via ssh + gitolite help # directly on server command line + +Prints a list of custom commands available at this gitolite installation. + +Each command has its own help, accessed by passing it '-h' again. +=cut + +usage() if @ARGV; + +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} || '' ) ); +for ( sort keys %list ) { + print "\t$list{$_}" if $ENV{D}; + print "\t$_\n" if not $user or $rc{COMMANDS}{$_}; +} + +print "\n"; +print "$rc{SITE_INFO}\n" if $rc{SITE_INFO}; + +exit 0; + +# ------------------------------------------------------------------------------ +sub list_x { + my $d = shift; + return unless $d; + return unless -d "$d/commands"; + _chdir "$d/commands"; + return map { $_ => $d } grep { -x $_ } map { chomp; s(^./)(); $_ } `find . -type f -o -type l|sort`; +} diff --git a/src/commands/htpasswd b/src/commands/htpasswd new file mode 100755 index 0000000..bbfacc7 --- /dev/null +++ b/src/commands/htpasswd @@ -0,0 +1,44 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; +use Gitolite::Common; + +=for usage +Usage: ssh git@host htpasswd + +Sets your htpasswd, assuming your admin has enabled it. + +(Admins: You need to add HTPASSWD_FILE to the rc file, pointing to an +existing, writable, but possibly an initially empty, file, as well as adding +'htpasswd' to the ENABLE list). +=cut + +# usage and sanity checks +usage() if @ARGV and $ARGV[0] eq '-h'; +$ENV{GL_USER} or _die "GL_USER not set"; +my $htpasswd_file = $rc{HTPASSWD_FILE} || ''; +die "htpasswd not enabled\n" unless $htpasswd_file; +die "$htpasswd_file doesn't exist or is not writable\n" unless -w $htpasswd_file; + +# prompt +$|++; +print <<EOFhtp; +Please type in your new htpasswd at the prompt. You only have to type it once. + +NOTE THAT THE PASSWORD WILL BE ECHOED, so please make sure no one is +shoulder-surfing, and make sure you clear your screen as well as scrollback +history after you're done (or close your terminal instance). + +EOFhtp +print "new htpasswd: "; + +# get the password and run htpasswd +my $password = <>; +$password =~ s/[\n\r]*$//; +die "empty passwords are not allowed\n" unless $password; +my $res = system( "htpasswd", "-mb", $htpasswd_file, $ENV{GL_USER}, $password ); +die "htpasswd command seems to have failed with return code: $res.\n" if $res; diff --git a/src/commands/info b/src/commands/info new file mode 100755 index 0000000..b88e288 --- /dev/null +++ b/src/commands/info @@ -0,0 +1,144 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use Getopt::Long; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; +use Gitolite::Common; +use Gitolite::Conf::Load; + +=for args +Usage: gitolite info [-lc] [-ld] [-json] [<repo name pattern>] + +List all existing repos you can access, as well as repo name patterns (see +"wild repos") you have any kind of access to. + + '-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 + '-p' limits output to physical repos only (no wild repo regexes!) + +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 are globals +my ( $lc, $ld, $json, $p, $patt ) = args(); +my %out; # holds info to be json'd + +$ENV{GL_USER} or _die "GL_USER not set"; +if ($json) { + greeting(\%out); +} else { + print greeting(); +} + +print_patterns() unless $p; # repos he can create for himself +print_phy_repos(); # repos already created + +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, $json, $p, $patt ) = ( '', '', '', '' ); + my $help = ''; + + GetOptions( + 'lc' => \$lc, + 'ld' => \$ld, + 'json' => \$json, + 'p' => \$p, + 'h' => \$help, + ) or usage(); + + usage() if @ARGV > 1 or $help; + $patt = shift @ARGV || '.'; + + require JSON if $json; + + return ( $lc, $ld, $json, $p, $patt ); +} + +sub print_patterns { + my ( $repos, @aa ); + + my $lm = \&Gitolite::Conf::Load::list_members; + + # find repo patterns only, call them with ^C flag included + @$repos = grep { !/$REPONAME_PATT/ } map { /^@/ ? @{ $lm->($_) } : $_ } @{ lister_dispatch('list-repos')->() }; + @aa = qw(R W ^C); + listem( $repos, '', '', @aa ); + # but squelch the 'lc' and 'ld' flags for these +} + +sub print_phy_repos { + my ( $repos, @aa ); + + # now get the actual repos and get R or W only + _chdir( $rc{GL_REPO_BASE} ); + $repos = list_phy_repos(1); + @aa = qw(R W); + listem( $repos, $lc, $ld, @aa ); +} + +sub listem { + my ( $repos, $lc, $ld, @aa ) = @_; + my @list; + my $mlr = 0; # max length of reponame + my $mlc = 0; # ...and creator + for my $repo (@$repos) { + next unless $repo =~ /$patt/; + my $creator = ''; + my $desc = ''; + my $perm = ''; + $creator = creator($repo) if $lc; + + if ($ld) { + # use config value first, else 'description' file as second choice + my $k = 'gitweb.description'; + my $d = "$ENV{GL_REPO_BASE}/$repo.git/description"; + $desc = git_config( $repo, $k )->{$k} || ''; + if ( !$desc and -r $d ) { + $desc = slurp($d); + chomp($desc); + } + } + + for my $aa (@aa) { + my $ret = access( $repo, $ENV{GL_USER}, $aa, 'any' ); + $perm .= ( $ret =~ /DENIED/ ? " " : " $aa" ); + } + $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); + } else { + $mlr = length($repo) if ( $lc or $ld ) and $mlr < length($repo); + $mlc = length($creator) if $lc and $ld and $mlc < length($creator); + push @list, [ $perm, $repo, $creator, $desc ]; + } + } + return if $json; + + my $fmt = "%s\t%-${mlr}s\t%-${mlc}s\t%s\n"; + map { s/\t\t/\t/; s/\s*$/\n/; print } map { sprintf $fmt, @$_ } @list; +} + +sub _hash { + my $in = shift; + my %out = map { $_ => 1 } ( $in =~ /(\S)/g ); + return \%out; +} diff --git a/src/commands/list-dangling-repos b/src/commands/list-dangling-repos new file mode 100755 index 0000000..60a3592 --- /dev/null +++ b/src/commands/list-dangling-repos @@ -0,0 +1,55 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Common; +use Gitolite::Conf::Load; + +=for usage +Usage: gitolite list-dangling-repos + +List all existing repos that no one can access remotely any more. They could +be normal repos that were taken out of "repo" statements in the conf file, or +wildcard repos whose matching "wild" pattern was taken out or changed so it no +longer matches. + +I would advise caution if you use this as a basis for deleting repos from the +file system. A bug in this program could cause you to lose important data! +=cut + +usage() if @ARGV and $ARGV[0] eq '-h'; + +# get the two lists we need. %repos is the list of repos in "repo" statements +# in the conf file. %phy_repos is the list of actual repos on disk. Our job +# is to cull %phy_repos of all keys that have a matching key in %repos, where +# "matching" means "string equal" or "regex match". +my %repos = map { chomp; $_ => 1 } `gitolite list-repos`; +for my $r ( grep /^@/, keys %repos ) { + map { chomp; $repos{$_} = 1; } `gitolite list-members $r`; +} +my %phy_repos = map { chomp; $_ => 1 } `gitolite list-phy-repos`; + +# Remove exact matches. But for repo names like "gtk+", you could have +# collapsed this into the next step (the regex match). +for my $pr ( keys %phy_repos ) { + next unless exists $repos{$pr}; + delete $repos{$pr}; + delete $phy_repos{$pr}; +} + +# Remove regex matches. +for my $pr ( keys %phy_repos ) { + my $matched = 0; + my $pr2 = Gitolite::Conf::Load::generic_name($pr); + for my $r ( keys %repos ) { + if ( $pr =~ /^$r$/ or $pr2 =~ /^$r$/ ) { + $matched = 1; + next; + } + } + delete $phy_repos{$pr} if $matched; +} + +# what's left in %phy_repos are dangling repos. +print join( "\n", sort keys %phy_repos ), "\n"; diff --git a/src/commands/lock b/src/commands/lock new file mode 100755 index 0000000..70c2190 --- /dev/null +++ b/src/commands/lock @@ -0,0 +1,143 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use Getopt::Long; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; +use Gitolite::Common; +use Gitolite::Conf::Load; + +# gitolite command to lock and unlock (binary) files and deal with locks. + +=for usage +Usage: ssh git@host lock -l <repo> <file> # lock a file + ssh git@host lock -u <repo> <file> # unlock a file + ssh git@host lock --break <repo> <file> # break someone else's lock + ssh git@host lock -ls <repo> # list locked files for repo + +See doc/locking.mkd for other details. +=cut + +usage() if not @ARGV or $ARGV[0] eq '-h'; +$ENV{GL_USER} or _die "GL_USER not set"; + +my $op = ''; +$op = 'lock' if $ARGV[0] eq '-l'; +$op = 'unlock' if $ARGV[0] eq '-u'; +$op = 'break' if $ARGV[0] eq '--break'; +$op = 'list' if $ARGV[0] eq '-ls'; +usage() if not $op; +shift; + +my $repo = shift; +_die "You are not authorised" if access( $repo, $ENV{GL_USER}, 'W', 'any' ) =~ /DENIED/; +_die "You are not authorised" if $op eq 'break' and access( $repo, $ENV{GL_USER}, '+', 'any' ) =~ /DENIED/; + +my $file = shift || ''; +usage() if $op ne 'list' and not $file; + +_chdir( $ENV{GL_REPO_BASE} ); +_chdir("$repo.git"); + +_die "aborting, file '$file' not found in any branch" if $file and not object_exists($file); + +my $ff = "gl-locks"; + +if ( $op eq 'lock' ) { + f_lock( $repo, $file ); +} elsif ( $op eq 'unlock' ) { + f_unlock( $repo, $file ); +} elsif ( $op eq 'break' ) { + f_break( $repo, $file ); +} elsif ( $op eq 'list' ) { + f_list($repo); +} + +# ---------------------------------------------------------------------- +# For a given path, return 1 if object exists in any branch, 0 if not. +# This is to prevent locking invalid objects. + +sub object_exists { + my $file = shift; + + my @branches = `git for-each-ref refs/heads '--format=%(refname)'`; + foreach my $b (@branches) { + chomp($b); + system("git cat-file -e $b:$file 2>/dev/null") or return 1; + # note that with system(), the return value is "shell truth", so + # you check for success with "or", not "and" + } + return 0; # report object not found +} + +# ---------------------------------------------------------------------- +# everything below assumes we have already chdir'd to "$repo.git". Also, $ff +# is used as a global. + +sub f_lock { + my ( $repo, $file ) = @_; + + my %locks = get_locks(); + _die "'$file' locked by '$locks{$file}{USER}' since " . localtime( $locks{$file}{TIME} ) if $locks{$file}{USER}; + $locks{$file}{USER} = $ENV{GL_USER}; + $locks{$file}{TIME} = time; + put_locks(%locks); +} + +sub f_unlock { + my ( $repo, $file ) = @_; + + my %locks = get_locks(); + _die "'$file' not locked by '$ENV{GL_USER}'" if ( $locks{$file}{USER} || '' ) ne $ENV{GL_USER}; + delete $locks{$file}; + put_locks(%locks); +} + +sub f_break { + my ( $repo, $file ) = @_; + + my %locks = get_locks(); + _die "'$file' was not locked" unless $locks{$file}; + push @{ $locks{BREAKS} }, time . " $ENV{GL_USER} $locks{$file}{USER} $locks{$file}{TIME} $file"; + delete $locks{$file}; + put_locks(%locks); +} + +sub f_list { + my $repo = shift; + + my %locks = get_locks(); + print "\n# locks held:\n\n"; + map { print "$locks{$_}{USER}\t$_\t(" . scalar( localtime( $locks{$_}{TIME} ) ) . ")\n" } grep { $_ ne 'BREAKS' } sort keys %locks; + print "\n# locks broken:\n\n"; + for my $b ( @{ $locks{BREAKS} } ) { + my ( $when, $who, $whose, $how_old, $what ) = split ' ', $b; + print "$who\t$what\t(" . scalar( localtime($when) ) . ")\t(locked by $whose at " . scalar( localtime($how_old) ) . ")\n"; + } +} + +sub get_locks { + if ( -f $ff ) { + our %locks; + + my $t = slurp($ff); + eval $t; + _die "do '$ff' failed with '$@', contact your administrator" if $@; + + return %locks; + } + return (); +} + +sub put_locks { + my %locks = @_; + + use Data::Dumper; + $Data::Dumper::Indent = 1; + $Data::Dumper::Sortkeys = 1; + + my $dumped_data = Data::Dumper->Dump( [ \%locks ], [qw(*locks)] ); + _print( $ff, $dumped_data ); +} diff --git a/src/commands/mirror b/src/commands/mirror new file mode 100755 index 0000000..b22ec2a --- /dev/null +++ b/src/commands/mirror @@ -0,0 +1,186 @@ +#!/usr/bin/perl +use strict; +use warnings; + +my $tid; + +BEGIN { + $tid = $ENV{GL_TID} || 0; + delete $ENV{GL_TID}; +} + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; +use Gitolite::Common; +use Gitolite::Conf::Load; + +=for usage +Usage 1: gitolite mirror push <copy> <repo> + gitolite mirror status <copy> <repo> + gitolite mirror status all <repo> + gitolite mirror status all all +Usage 2: ssh git@master-server mirror push <copy> <repo> + ssh git@master-server mirror status <copy> <repo> + +Forces a push of one repo to one copy. + +Usage 1 is directly on the master server. Nothing is checked; if the copy +accepts it, the push happens, even if the copy is not in any copies +option. This is how you do delayed or lagged pushes to servers that do not +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 copy is in one of the copies options +before doing the push. + +MIRROR STATUS: The usage examples above show what can be done. The 'status +all <repo>' usage checks the status of all the copies defined for the given +repo. The 'status all all' usage is special, in that it only prints a list of +repos that have *some* error, instead of dumping all the error info itself. + +SERVER LIST: 'gitolite mirror list master <reponame>' and 'gitolite mirror +list copies <reponame>' will show you the name of the master server, and list +the copy 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'; + +_die "HOSTNAME not set" if not $rc{HOSTNAME}; + +my ( $cmd, $host, $repo ) = @ARGV; +$host = 'copies' if $host eq 'slaves'; +$repo =~ s/\.git$//; +usage() if not $repo; + +if ( $cmd eq 'push' ) { + valid_copy( $host, $repo ) if exists $ENV{GL_USER}; + # will die if host not in copies for repo + + trace( 1, "TID=$tid host=$host repo=$repo", "gitolite mirror push started" ); + _chdir( $rc{GL_REPO_BASE} ); + _chdir("$repo.git"); + + if ( -f "gl-creator" ) { + # try to propagate the wild repo, including creator name and gl-perms + my $creator = `cat gl-creator`; chomp($creator); + trace( 1, `cat gl-perms 2>/dev/null | ssh $host CREATOR=$creator perms -c \\'$repo\\' 2>/dev/null` ); + } + + 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; + gl_log( 'mirror', $_ ); + } else { + trace( 1, "mirror: $_" ); + } + } + # save the mirror push status for this copy 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-copy-$host.status", $glss); + } else { + unlink "gl-copy-$host.status"; + } + + exit $errors; +} elsif ($cmd eq 'status') { + if (not exists $ENV{GL_USER} and $repo eq 'all') { + # this means 'gitolite mirror status all all'; in this case we only + # return a list of repos that *have* status files (indicating some + # problem). It's upto you what you do with that list. This is not + # allowed to be run remotely; far too wide ranging, sorry. + _chdir( $rc{GL_REPO_BASE} ); + my $phy_repos = list_phy_repos(1); + for my $repo ( @{$phy_repos} ) { + my @x = glob("$rc{GL_REPO_BASE}/$repo.git/gl-copy-*.status"); + print "$repo\n" if @x; + } + exit 0; + } + + valid_copy( $host, $repo ) if exists $ENV{GL_USER}; + # will die if host not in copies for repo + + _chdir( $rc{GL_REPO_BASE} ); + _chdir("$repo.git"); + + $host = '*' if $host eq 'all'; + map { print_status($repo, $_) } sort glob("gl-copy-$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_copy { + my ( $host, $repo ) = @_; + _die "invalid repo '$repo'" unless $repo =~ $REPONAME_PATT; + + my %list = repo_copies($repo); + _die "'$host' not a valid copy for '$repo'" unless $list{$host}; +} + +sub repo_copies { + my $repo = shift; + + my $ref = git_config( $repo, "^gitolite-options\\.mirror\\.copies.*" ); + my %list = map { $_ => 1 } map { split } values %$ref; + + 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 $repo = shift; + my $file = shift; + return unless -f $file; + my $copy = $1 if $file =~ /^gl-copy-(.+)\.status$/; + print "----------\n"; + print "WARNING: previous mirror push of repo '$repo' to host '$copy' failed, status is:\n"; + print slurp($file); + print "----------\n"; +} + +# ---------------------------------------------------------------------- +# server side commands. Very little error checking. +# gitolite mirror list master <repo> +# gitolite mirror list copies <repo> + +sub server_side_commands { + if ( $cmd eq 'list' ) { + if ( $host eq 'master' ) { + say repo_master($repo); + } elsif ( $host eq 'copies' ) { + my %list = repo_copies($repo); + say join( " ", sort keys %list ); + } else { + _die "gitolite mirror list master|copies <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/newbranch b/src/commands/newbranch new file mode 100755 index 0000000..6dff545 --- /dev/null +++ b/src/commands/newbranch @@ -0,0 +1,41 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Easy; + +=for usage +Usage: ssh git@host newbranch <repo name> <new branch name> <based-on ref name> + +Create a new branch and set it to existing branch or tag. You should have +write access to that branch. + +NOTE: runs "git branch arg-2 arg-3" in repo given by arg-1, which means you +should NOT prefix arguments with "refs/heads/" or "refs/tags/". + +---- + +This is for people who have restrictions on what files they can "touch". When +you fork a branch and change a file, even if you changed only the files you're +allowed to, gitolite thinks you changed *all* the files in the repo because +the "old SHA" is basically empty. + +This helps get around that by first creating the new branch, so that you can +then push to it. + +To enable this command, add it to the rc file as a 'command'. + +TODO: handle deletes also (less commonly encountered and left as an "exercise +for the reader" for now!) +=cut + +usage() if not @ARGV or @ARGV < 3 or $ARGV[0] eq '-h'; + +my $repo = shift; +my $newbr = shift; +my $oldref = shift; + +_die "you are not authorized" unless can_write($repo, "W", "refs/heads/$newbr"); + +Gitolite::Common::_system("git", "branch", $newbr, $oldref); diff --git a/src/commands/option b/src/commands/option new file mode 100755 index 0000000..de49aab --- /dev/null +++ b/src/commands/option @@ -0,0 +1,127 @@ +#!/usr/bin/perl + +# ---------------------------------------------------------------------- +# gitolite command to allow repo "owners" to set "options" on repos + +# This command can be run by a user to set "options" for any repo that she +# owns. +# +# However, gitolite does *not* have the concept of an incremental "compile", +# and options are only designed to be specified in the gitolite.conf file +# (which a user should not be able to even see!). Therefore, we allow one +# specific file (conf/options.conf) to be manipulated by a remote user in a +# *controlled* fashion, and this file is "include"d in the main gitolite.conf +# file. + +# WARNINGS: +# 1. Runs "gitolite compile" at the end. On really huge systems (where the +# sum total of the conf files is in the order of tens of thousands of +# lines) this may take a second or two :) +# 2. Since "options.conf" is not part of the admin repo, you may need to +# back it up separately, just like you currently back up gl-creator and +# gl-perms files from individual repos. +# 3. "options.conf" is formatted very strictly because it's not meant to be +# human edited. If you edit it directly on the server, be careful. + +# Relevant gitolite doc links: +# "wild" repos and "owners" +# http://gitolite.com/gitolite/wild.html +# http://gitolite.com/gitolite/wild.html#specifying-owners +# http://gitolite.com/gitolite/wild.html#appendix-1-owner-and-creator +# gitolite "options" +# http://gitolite.com/gitolite/options.html +# the "include" statement +# http://gitolite.com/gitolite/conf.html#include + +# setup: +# 1. Enable the command by adding it to the ENABLE list in the rc file. +# +# 2. Make sure your gitolite.conf has this line at the end: +# +# include "options.conf" +# +# then add/commit/push. +# +# Do NOT add a file called "options.conf" to your gitolite-admin repo! +# This means every time you compile (push the admin repo) you will get a +# warning about the missing file. +# +# You can either "touch ~/.gitolite/conf/options.conf" on the server, or +# take *any* wild repo and add *any* option to create it. +# +# 3. Specify options allowed to be changed by the user. For example: +# +# repo foo/..* +# C = blah blah +# ...other rules... +# option user-options = hook\..* foo bar[0-9].* +# +# Users can then set any of these options, but no others. + +# ---------------------------------------------------------------------- + +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Easy; +use Gitolite::Common; + +# ---------------------------------------------------------------------- +# usage and arg checks + +=for usage +Usage: ssh git@host option <repo> add <key> <val> + ssh git@host option <repo> del <key> + ssh git@host option <repo> list + +Add, delete, or list options for wild repos. Keys must match one of the +allowed patterns; your system administrator will tell you what they are. + +Doesn't check things like adding a key that already exists (simply overwrites +without warning), deleting a key that doesn't, etc. +=cut + +usage() if not @ARGV or $ARGV[0] eq '-h'; + +my $OPTIONS = "$ENV{HOME}/.gitolite/conf/options.conf"; + +my $repo = shift; +die "sorry, you are not authorised\n" unless owns($repo); + +my $op = shift; usage() unless $op =~ /^(add|del|list)$/; +my $key = shift; usage() if not $key and $op ne 'list'; +my $val = shift; usage() if not $val and $op eq 'add'; + +_print( $OPTIONS, "" ) unless -f $OPTIONS; # avoid error on first run +my $options = slurp($OPTIONS); + +# ---------------------------------------------------------------------- +# get 'list' out of the way first +if ( $op eq 'list' ) { + print "$1\t$2\n" while $options =~ /^repo $repo\n option (\S+) = (.*)/mg; + exit 0; +} + +# ---------------------------------------------------------------------- +# that leaves 'add' or 'del' + +# NOTE: sanity check on characters in key and val not needed; +# REMOTE_COMMAND_PATT is more restrictive than UNSAFE_PATT anyway! + +# check if the key is allowed +my $user_options = option( $repo, 'user-options' ); +# this is a space separated list of allowed option keys +my @validkeys = split( ' ', ( $user_options || '' ) ); +my @matched = grep { $key =~ /^$_$/i } @validkeys; +_die "option '$key' not allowed\n" if ( @matched < 1 ); + +# delete anyway +$options =~ s/^repo $repo\n option $key = .*\n//m; +# then re-add if needed +$options .= "repo $repo\n option $key = $val\n" if $op eq 'add'; + +# ---------------------------------------------------------------------- +# save and compile +_print( $OPTIONS, $options ); +system("gitolite compile"); diff --git a/src/commands/owns b/src/commands/owns new file mode 100755 index 0000000..d1d8757 --- /dev/null +++ b/src/commands/owns @@ -0,0 +1,22 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Easy; + +=for usage +Usage: gitolite owns <reponame> + +Checks if $GL_USER is an owner of the repo and returns an exit code (shell +truth, 0 for success), which makes it possible to do this in shell: + + if gitolite owns someRepo + then + ... +=cut + +usage() if not @ARGV or $ARGV[0] eq '-h'; +my $repo = shift; + +exit not owns($repo); diff --git a/src/commands/perms b/src/commands/perms new file mode 100755 index 0000000..be7be69 --- /dev/null +++ b/src/commands/perms @@ -0,0 +1,193 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; +use Gitolite::Common; +use Gitolite::Easy; + +=for usage +perms -- list or set permissions for user-created ("wild") repo. + +Usage summary: + ssh git@host perms <repo> -l + # list current permissions on repo + ssh git@host perms <repo> -lr + # list available roles and their access rights + + ssh git@host perms <repo> + <rolename> <username> + # change permissions: add a user to a role + ssh git@host perms <repo> - <rolename> <username> + # change permissions: remove a user from a role + +Examples: + ssh git@host perms my/repo + READERS alice + ssh git@host perms my/repo + WRITERS bob + +---- +There is also a batch mode useful for scripting and bulk loading; see the +source code of the perms command for details. +=cut + +# BATCH MODE: DO NOT combine this with the +/- mode above. This mode also +# creates the repo if it does not already exist (assuming $GL_USER has +# permissions to create it). +# +# Example: +# cat copy-of-backed-up-gl-perms | ssh git@host perms -c <repo> + +usage() if not @ARGV or $ARGV[0] eq '-h' or @ARGV < 2; + +$ENV{GL_USER} or _die "GL_USER not set"; + +my $generic_error = "repo does not exist, or you are not authorised"; + +if ( $ARGV[1] eq '-l' ) { + getperms($ARGV[0]); # doesn't return +} + +# auto-create the repo if -c passed and repo doesn't exist +if ( $ARGV[0] eq '-c' ) { + shift; + my $repo = $ARGV[0] or usage(); + _die "invalid repo '$repo'" unless $repo =~ $REPONAME_PATT; + + if ( not -d "$rc{GL_REPO_BASE}/$repo.git" ) { + unless ($ENV{GL_BYPASS_CREATOR_CHECK}) { + my $ret = Gitolite::Conf::Load::access( $repo, $ENV{GL_USER}, '^C', 'any' ); + _die $generic_error if $ret =~ /DENIED/; + } + + require Gitolite::Conf::Store; + Gitolite::Conf::Store->import; + new_wild_repo( $repo, $ENV{GL_USER}, 'perms-c' ); + gl_log( 'create', $repo, $ENV{GL_USER}, 'perms-c' ); + } +} + +my $repo = shift; + +if ( @ARGV and $ARGV[0] eq '-lr' ) { + list_roles(); + exit 0; +} else { + 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' ); + +# ---------------------------------------------------------------------- + +sub getperms { + my $repo = shift; + _die $generic_error if not owns($repo); + my $pf = "$rc{GL_REPO_BASE}/$repo.git/gl-perms"; + + print slurp($pf) if -f $pf; + + exit 0; +} + +sub setperms { + _die $generic_error if not owns($repo); + my $pf = "$rc{GL_REPO_BASE}/$repo.git/gl-perms"; + + if ( not @_ ) { + # legacy mode; pipe data in + print STDERR "'batch' mode started, waiting for input (run with '-h' for details).\n"; + print STDERR "Please enter 'cancel' to abort if you did not intend to do this.\n"; + @ARGV = (); + my @a; + while (<>) { + _die "CANCELLED" if /^\s*cancel\s*$/i; + invalid_role($1) if /(\S+)/ and not $rc{ROLES}{$1}; + push @a, $_; + } + + _print( $pf, @a ); + return; + } + + _die "Invalid syntax. Please re-run with '-h' for detailed usage" if @_ != 3; + my ( $op, $role, $user ) = @_; + _die "Invalid syntax. Please re-run with '-h' for detailed usage" if $op ne '+' and $op ne '-'; + _die "Invalid user '$user'" if not $user =~ $USERNAME_PATT; + + my $text = ''; + my @text = slurp($pf) if -f $pf; + + my $present = grep { $_ eq "$role $user\n" } @text; + + if ( $op eq '-' ) { + if ( not $present ) { + _warn "'$role $user' was not present in file"; + } else { + @text = grep { $_ ne "$role $user\n" } @text; + _print( $pf, @text ); + } + } else { + invalid_role($role) unless grep { $_->[3] eq $role } load_roles(); + if ($present) { + _warn "'$role $user' already present in file"; + } else { + push @text, "$role $user\n"; + @text = sort @text; + _print( $pf, @text ); + } + } +} + +my @rules; + +sub load_roles { + return @rules if @rules; + + require Gitolite::Conf::Load; + Gitolite::Conf::Load::load($repo); + + my %repos = %Gitolite::Conf::Load::repos; + my @repo_memberships = Gitolite::Conf::Load::memberships('repo', $repo); + + for my $rp (@repo_memberships) { + my $hr = $repos{$rp}; + for my $r ( keys %$hr ) { + next unless $r =~ s/^@//; + next unless $rc{ROLES}{$r}; + map { $_->[3] = $r } @{ $hr->{"\@$r"} }; + push @rules, @{ $hr->{"\@$r"} }; + } + } + return @rules; +} + +sub invalid_role { + my $role = shift; + + print STDERR "Invalid role '$role'; valid roles for this repo:\n"; + open(STDOUT, '>&', \*STDERR); # make list_roles print to STDERR + list_roles(); + exit 1; +} + +sub list_roles { + + my @rules = sort { $a->[0] <=> $b->[0] } load_roles(); + + for (@rules) { + $_->[2] =~ s(^refs/heads/)(); + $_->[2] = '--any--' if $_->[2] eq 'refs/.*'; + } + + my $max = 0; + map { $max = $_ if $_ > $max } map { length($_->[2]) } @rules; + printf("\t%s\t%*s\t \t%s\n", "perm", -$max, "ref", "role"); + printf("\t%s\t%*s\t \t%s\n", "----", -$max, "---", "----"); + printf("\t%s\t%*s\t=\t%s\n", $_->[1], -$max, $_->[2], $_->[3]) for @rules; +} diff --git a/src/commands/print-default-rc b/src/commands/print-default-rc new file mode 100755 index 0000000..79b88c1 --- /dev/null +++ b/src/commands/print-default-rc @@ -0,0 +1,8 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; + +print glrc('default-text'); diff --git a/src/commands/push b/src/commands/push new file mode 100755 index 0000000..f97f730 --- /dev/null +++ b/src/commands/push @@ -0,0 +1,5 @@ +#!/bin/sh + +export GL_BYPASS_ACCESS_CHECKS=1 + +git push "$@" 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/rsync b/src/commands/rsync new file mode 100755 index 0000000..c7b25d1 --- /dev/null +++ b/src/commands/rsync @@ -0,0 +1,143 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Easy; + +=for admins + +BUNDLE SUPPORT + + (1) For each repo in gitolite.conf for which you want bundle support (or + '@all', if you wish), add the following line: + + option bundle = 1 + + Or you can say: + + option bundle.ttl = <number> + + A bundle file that is more than <number> seconds old (default value + 86400, i.e., 1 day) is recreated on the next bundle request. Increase + this if your repo is not terribly active. + + Note: a bundle file is also deleted and recreated if it contains a ref + that was then either deleted or rewound in the repo. This is checked + on every invocation. + + (2) Add 'rsync' to the ENABLE list in the rc file + +=cut + +=for usage +rsync helper for gitolite + +BUNDLE SUPPORT + + Admins: see src/commands/rsync for setup instructions + + Users: + rsync git@host:repo.bundle . + # downloads a file called "<basename of repo>.bundle"; repeat as + # needed till the whole thing is downloaded + git clone repo.bundle repo + cd repo + git remote set-url origin git@host:repo + git fetch origin # and maybe git pull, etc. to freshen the clone + + NOTE on options to the rsync command: you are only allowed to use the + "-v", "-n", "-q", and "-P" options. + +=cut + +usage() if not @ARGV or $ARGV[0] eq '-h'; + +# rsync driver program. Several things can be done later, but for now it +# drives just the 'bundle' transfer. + +if ( $ENV{SSH_ORIGINAL_COMMAND} =~ /^rsync --server --sender (?:-[vn]*(?:e\d*\.\w*)? )?\. (\S+)\.bundle$/ ) { + + my $repo = $1; + $repo =~ s/\.git$//; + + # all errors have the same message to avoid leaking info + can_read($repo) or _die "you are not authorised"; + my %config = config( $repo, "gitolite-options.bundle" ) or _die "you are not authorised"; + + my $ttl = $config{'gitolite-options.bundle.ttl'} || 86400; # in seconds (default 1 day) + + my $bundle = bundle_create( $repo, $ttl ); + + $ENV{SSH_ORIGINAL_COMMAND} =~ s( \S+\.bundle)( $bundle); + trace( 1, "rsync bundle", $ENV{SSH_ORIGINAL_COMMAND} ); + Gitolite::Common::_system( split ' ', $ENV{SSH_ORIGINAL_COMMAND} ); + exit 0; +} + +_warn "Sorry, you are only allowed to use the '-v', '-n', '-q', and '-P' options."; +usage(); + +# ---------------------------------------------------------------------- +# helpers +# ---------------------------------------------------------------------- + +sub bundle_create { + my ( $repo, $ttl ) = @_; + my $bundle = "$repo.bundle"; + $bundle =~ s(.*/)(); + my $recreate = 0; + + my ( %b, %r ); + if ( -f $bundle ) { + %b = map { chomp; reverse split; } `git ls-remote --heads --tags $bundle`; + %r = map { chomp; reverse split; } `git ls-remote --heads --tags .`; + + for my $ref ( sort keys %b ) { + + my $mtime = ( stat $bundle )[9]; + if ( time() - $mtime > $ttl ) { + trace( 1, "bundle too old" ); + $recreate++; + last; + } + + if ( not $r{$ref} ) { + trace( 1, "ref '$ref' deleted in repo" ); + $recreate++; + last; + } + + if ( $r{$ref} eq $b{$ref} ) { + # same on both sides; ignore + delete $r{$ref}; + delete $b{$ref}; + next; + } + + `git rev-list --count --left-right $b{$ref}...$r{$ref}` =~ /^(\d+)\s+(\d+)$/ or _die "git too old"; + if ($1) { + trace( 1, "ref '$ref' rewound in repo" ); + $recreate++; + last; + } + + } + + } else { + trace( 1, "no bundle found" ); + $recreate++; + } + + return $bundle if not $recreate; + + trace( 1, "creating bundle for '$repo'" ); + -f $bundle and ( unlink $bundle or die "a horrible death" ); + system("git bundle create $bundle --branches --tags >&2"); + + return $bundle; +} + +sub trace { + Gitolite::Common::trace(@_); +} diff --git a/src/commands/sshkeys-lint b/src/commands/sshkeys-lint new file mode 100755 index 0000000..3f07b13 --- /dev/null +++ b/src/commands/sshkeys-lint @@ -0,0 +1,176 @@ +#!/usr/bin/perl +use strict; +use warnings; + +# complete rewrite of the sshkeys-lint program. Usage has changed, see +# usage() function or run without arguments. +use lib $ENV{GL_LIBDIR}; +use Gitolite::Common; + +use Getopt::Long; +my $admin = 0; +my $quiet = 0; +my $help = 0; +GetOptions( 'admin|a=s' => \$admin, 'quiet|q' => \$quiet, 'help|h' => \$help ); + +use Data::Dumper; +$Data::Dumper::Deepcopy = 1; +$|++; + +my $in_gl_section = 0; +my $warnings = 0; +my $KEYTYPE_REGEX = qr/\b(?:ssh-(?:rsa|dss|ed25519)|ecdsa-sha2-nistp(?:256|384|521))\b/; + +sub msg { + my $warning = shift; + return if $quiet and not $warning; + $warnings++ if $warning; + print "sshkeys-lint: " . ( $warning ? "WARNING: " : "" ) . $_ for @_; +} + +usage() if $help; + +our @pubkeyfiles = @ARGV; @ARGV = (); +my $kd = "$ENV{HOME}/.gitolite/keydir"; +if ( not @pubkeyfiles ) { + chomp( @pubkeyfiles = `find $kd -type f -name "*.pub" | sort` ); +} + +if ( -t STDIN ) { + @ARGV = ("$ENV{HOME}/.ssh/authorized_keys"); +} + +# ------------------------------------------------------------------------ + +my @authkeys; +my %seen_fprints; +my %pkf_by_fp; +msg 0, "==== checking authkeys file:\n"; +fill_authkeys(); # uses up STDIN + +if ($admin) { + my $fp = fprint("$admin.pub"); + my $fpu = ( $fp && $seen_fprints{$fp}{user} || 'no access' ); + # dbg("fpu = $fpu, admin=$admin"); + #<<< + die "\t\t*** FATAL ***\n" . + "$admin.pub maps to $fpu, not $admin.\n" . + "You will not be able to access gitolite with this key.\n" . + "Look for the 'ssh troubleshooting' link in http://gitolite.com/gitolite/ssh.html.\n" + if $fpu ne "user $admin"; + #>>> +} + +msg 0, "==== checking pubkeys:\n" if @pubkeyfiles; +for my $pkf (@pubkeyfiles) { + # get the short name for the pubkey file + ( my $pkfsn = $pkf ) =~ s(^$kd/)(); + + my $fp = fprint($pkf); + next unless $fp; + msg 1, "$pkfsn appears to be a COPY of $pkf_by_fp{$fp}\n" if $pkf_by_fp{$fp}; + $pkf_by_fp{$fp} ||= $pkfsn; + my $fpu = ( $seen_fprints{$fp}{user} || 'no access' ); + msg 0, "$pkfsn maps to $fpu\n"; +} + +if ($warnings) { + print "\n$warnings warnings found\n"; +} + +exit $warnings; + +# ------------------------------------------------------------------------ +sub fill_authkeys { + while (<>) { + my $seq = $.; + next if ak_comment($_); # also sets/clears $in_gl_section global + my $fp = fprint($_); + my $user = user($_); + + check( $seq, $fp, $user ); + + $authkeys[$seq]{fprint} = $fp; + $authkeys[$seq]{ustatus} = $user; + } +} + +sub check { + my ( $seq, $fp, $user ) = @_; + + msg 1, "line $seq, $user key found *outside* gitolite section!\n" + if $user =~ /^user / and not $in_gl_section; + + msg 1, "line $seq, $user key found *inside* gitolite section!\n" + if $user !~ /^user / and $in_gl_section; + + if ( $seen_fprints{$fp} ) { + #<<< + msg 1, "authkeys line $seq ($user) will be ignored by sshd; " . + "same key found on line " . + $seen_fprints{$fp}{seq} . " (" . + $seen_fprints{$fp}{user} . ")\n"; + return; + #>>> + } + + $seen_fprints{$fp}{seq} = $seq; + $seen_fprints{$fp}{user} = $user; +} + +sub user { + my $user = ''; + $user ||= "user $1" if /^command=.*gitolite-shell (.*?)"/; + $user ||= "unknown command" if /^command/; + $user ||= "shell access" if /$KEYTYPE_REGEX/; + + return $user; +} + +sub ak_comment { + local $_ = shift; + $in_gl_section = 1 if /^# gitolite start/; + $in_gl_section = 0 if /^# gitolite end/; + die "gitosis? what's that?\n" if /^#.*gitosis/; + return /^\s*(#|$)/; +} + +sub fprint { + local $_ = shift; + my ($fp, $output); + if ( /$KEYTYPE_REGEX/ ) { + # an actual key was passed. ssh-keygen CAN correctly handle options on + # the front of the key, so don't bother to strip them at all. + ($fp, $output) = ssh_fingerprint_line($_); + } else { + # a filename was passed + ($fp, $output) = ssh_fingerprint_file($_); + # include the line of input as well, as it won't always be included by the ssh-keygen command + warn "Bad line: $_\n" unless $fp; + } + # sshkeys-lint should only be run by a trusted admin, so we can give the output here. + warn "$output\n" unless $fp; + return $fp; +} + +# ------------------------------------------------------------------------ +=for usage + +Usage: gitolite sshkeys-lint [-q] [optional list of pubkey filenames] + (optionally, STDIN can be a pipe or redirected from a file; see below) + +Look for potential problems in ssh keys. + +sshkeys-lint expects: + - the contents of an authorized_keys file via STDIN, otherwise it uses + \$HOME/.ssh/authorized_keys + - one or more pubkey filenames as arguments, otherwise it uses all the keys + found (recursively) in \$HOME/.gitolite/keydir + +The '-q' option will print only warnings instead of all mappings. + +Note that this runs ssh-keygen -l for each line in the authkeys file and each +pubkey in the argument list, so be wary of running it on something huge. This +is meant for troubleshooting. + +=cut diff --git a/src/commands/sskm b/src/commands/sskm new file mode 100755 index 0000000..eb51f69 --- /dev/null +++ b/src/commands/sskm @@ -0,0 +1,281 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; +use Gitolite::Common; + +=for usage +Usage for this command is not that simple. Please read the full documentation +in doc/sskm.mkd or online at http://gitolite.com/gitolite/sskm.html. +=cut + +usage() if @ARGV and $ARGV[0] eq '-h'; + +my $rb = $rc{GL_REPO_BASE}; +my $ab = $rc{GL_ADMIN_BASE}; +# get to the keydir +_chdir("$ab/keydir"); + +# save arguments for later +my $operation = shift || 'list'; +my $keyid = shift || ''; +# keyid must fit a very specific pattern +$keyid and $keyid !~ /^@[-0-9a-z_]+$/i and die "invalid keyid $keyid\n"; + +# get the actual userid and keytype +my $gl_user = $ENV{GL_USER}; +my $keytype = ''; +$keytype = $1 if $gl_user =~ s/^zzz-marked-for-(...)-//; +print STDERR "hello $gl_user, you are currently using " + . ( + $keytype + ? "a key in the 'marked for $keytype' state\n" + : "a normal (\"active\") key\n" + ); + +# ---- +# first collect the keys + +my ( @pubkeys, @marked_for_add, @marked_for_del ); +# get the list of pubkey files for this user, including pubkeys marked for +# add/delete + +for my $pubkey (`find . -type f -name "*.pub" | sort`) { + chomp($pubkey); + $pubkey =~ s(^./)(); # artifact of the find command + + my $user = $pubkey; + $user =~ s(.*/)(); # foo/bar/baz.pub -> baz.pub + $user =~ s/(\@[^.]+)?\.pub$//; # baz.pub, baz@home.pub -> baz + + next unless $user eq $gl_user or $user =~ /^zzz-marked-for-...-$gl_user/; + + if ( $user =~ m(^zzz-marked-for-add-) ) { + push @marked_for_add, $pubkey; + } elsif ( $user =~ m(^zzz-marked-for-del-) ) { + push @marked_for_del, $pubkey; + } else { + push @pubkeys, $pubkey; + } +} + +# ---- +# list mode; just do it and exit +sub print_keylist { + my ( $message, @list ) = @_; + return unless @list; + print "== $message ==\n"; + my $count = 1; + for (@list) { + my $fp = fingerprint($_); + s/zzz-marked(\/|-for-...-)//g; + print $count++ . ": $fp : $_\n"; + } +} +if ( $operation eq 'list' ) { + print "you have the following keys:\n"; + print_keylist( "active keys", @pubkeys ); + print_keylist( "keys marked for addition/replacement", @marked_for_add ); + print_keylist( "keys marked for deletion", @marked_for_del ); + print "\n\n"; + exit; +} + +# ---- +# please see docs for details on how a user interacts with this + +if ( $keytype eq '' ) { + # user logging in with a normal key + die "valid operations: add, del, undo-add, confirm-del\n" unless $operation =~ /^(add|del|confirm-del|undo-add)$/; + if ( $operation eq 'add' ) { + print STDERR "please supply the new key on STDIN. (I recommend you + don't try to do this interactively, but use a pipe)\n"; + kf_add( $gl_user, $keyid, safe_stdin() ); + } elsif ( $operation eq 'del' ) { + kf_del( $gl_user, $keyid ); + } elsif ( $operation eq 'confirm-del' ) { + die "you dont have any keys marked for deletion\n" unless @marked_for_del; + kf_confirm_del( $gl_user, $keyid ); + } elsif ( $operation eq 'undo-add' ) { + die "you dont have any keys marked for addition\n" unless @marked_for_add; + kf_undo_add( $gl_user, $keyid ); + } +} elsif ( $keytype eq 'del' ) { + # user is using a key that was marked for deletion. The only possible use + # for this is that she changed her mind for some reason (maybe she marked + # the wrong key for deletion) or is not able to get her client-side sshd + # to stop using this key + die "valid operations: undo-del\n" unless $operation eq 'undo-del'; + + # reinstate the key + kf_undo_del( $gl_user, $keyid ); +} elsif ( $keytype eq 'add' ) { + die "valid operations: confirm-add\n" unless $operation eq 'confirm-add'; + # user is trying to validate a key that has been previously marked for + # addition. This isn't interactive, but it *could* be... if someone asked + kf_confirm_add( $gl_user, $keyid ); +} + +exit; + +# ---- + +# make a temp clone and switch to it +our $TEMPDIR; +BEGIN { $TEMPDIR = `mktemp -d -t tmp.XXXXXXXXXX`; } +END { `/bin/rm -rf $TEMPDIR`; } + +sub cd_temp_clone { + chomp($TEMPDIR); + hushed_git( "clone", "$rb/gitolite-admin.git", "$TEMPDIR" ); + chdir($TEMPDIR); + my $hostname = `hostname`; chomp($hostname); + hushed_git( "config", "--get", "user.email" ) and hushed_git( "config", "user.email", $ENV{USER} . "@" . $hostname ); + hushed_git( "config", "--get", "user.name" ) and hushed_git( "config", "user.name", "$ENV{USER} on $hostname" ); +} + +sub fingerprint { + my ($fp, $output) = ssh_fingerprint_file(shift); + # Do not print the output of $output to an untrusted destination. + die "does not seem to be a valid pubkey\n" unless $fp; + return $fp; +} + +sub safe_stdin { + # read one line from STDIN + my $data; + my $ret = read STDIN, $data, 4096; + # current pubkeys are approx 400 bytes so we go a little overboard + die "could not read pubkey data" . ( defined($ret) ? "" : ": $!" ) . "\n" unless $ret; + die "pubkey data seems to have more than one line\n" if $data =~ /\n./; + return $data; +} + +sub hushed_git { + local (*STDOUT) = \*STDOUT; + local (*STDERR) = \*STDERR; + open( STDOUT, ">", "/dev/null" ); + open( STDERR, ">", "/dev/null" ); + system( "git", @_ ); +} + +sub highlander { + # there can be only one + my ( $keyid, $die_if_empty, @a ) = @_; + # too many? + if ( @a > 1 ) { + print STDERR " +more than one key satisfies this condition, and I can't deal with that! +The keys are: + +"; + print STDERR "\t" . join( "\n\t", @a ), "\n\n"; + exit 1; + } + # too few? + die "no keys with " . ( $keyid || "empty" ) . " keyid found\n" if $die_if_empty and not @a; + + return @a; +} + +sub kf_add { + my ( $gl_user, $keyid, $keymaterial ) = @_; + + # add a new "marked for addition" key for $gl_user. + cd_temp_clone(); + chdir("keydir"); + + mkdir("zzz-marked"); + _print( "zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub", $keymaterial ); + hushed_git( "add", "." ) and die "git add failed\n"; + my $fp = fingerprint("zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub"); + hushed_git( "commit", "-m", "sskm: add $gl_user$keyid ($fp)" ) and die "git commit failed\n"; + system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; +} + +sub kf_confirm_add { + my ( $gl_user, $keyid ) = @_; + # find entries in both @pubkeys and @marked_for_add whose basename matches $gl_user$keyid + my @pk = highlander( $keyid, 0, grep { m(^(.*/)?$gl_user$keyid.pub$) } @pubkeys ); + my @mfa = highlander( $keyid, 1, grep { m(^zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub$) } @marked_for_add ); + + cd_temp_clone(); + chdir("keydir"); + + my $fp = fingerprint( $mfa[0] ); + if ( $pk[0] ) { + hushed_git( "mv", "-f", $mfa[0], $pk[0] ); + hushed_git( "commit", "-m", "sskm: confirm-add (replace) $pk[0] ($fp)" ) and die "git commit failed\n"; + } else { + hushed_git( "mv", "-f", $mfa[0], "$gl_user$keyid.pub" ); + hushed_git( "commit", "-m", "sskm: confirm-add $gl_user$keyid ($fp)" ) and die "git commit failed\n"; + } + system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; +} + +sub kf_undo_add { + # XXX some code at start is shared with kf_confirm_add + my ( $gl_user, $keyid ) = @_; + my @mfa = highlander( $keyid, 1, grep { m(^zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub$) } @marked_for_add ); + + cd_temp_clone(); + chdir("keydir"); + + my $fp = fingerprint( $mfa[0] ); + hushed_git( "rm", $mfa[0] ); + hushed_git( "commit", "-m", "sskm: undo-add $gl_user$keyid ($fp)" ) and die "git commit failed\n"; + system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; +} + +sub kf_del { + my ( $gl_user, $keyid ) = @_; + + cd_temp_clone(); + chdir("keydir"); + + mkdir("zzz-marked"); + my @pk = highlander( $keyid, 1, grep { m(^(.*/)?$gl_user$keyid.pub$) } @pubkeys ); + + my $fp = fingerprint( $pk[0] ); + hushed_git( "mv", $pk[0], "zzz-marked/zzz-marked-for-del-$gl_user$keyid.pub" ) and die "git mv failed\n"; + hushed_git( "commit", "-m", "sskm: del $pk[0] ($fp)" ) and die "git commit failed\n"; + system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; +} + +sub kf_confirm_del { + my ( $gl_user, $keyid ) = @_; + my @mfd = highlander( $keyid, 1, grep { m(^zzz-marked/zzz-marked-for-del-$gl_user$keyid.pub$) } @marked_for_del ); + + cd_temp_clone(); + chdir("keydir"); + + my $fp = fingerprint( $mfd[0] ); + hushed_git( "rm", $mfd[0] ); + hushed_git( "commit", "-m", "sskm: confirm-del $gl_user$keyid ($fp)" ) and die "git commit failed\n"; + system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; +} + +sub kf_undo_del { + my ( $gl_user, $keyid ) = @_; + + my @mfd = highlander( $keyid, 1, grep { m(^zzz-marked/zzz-marked-for-del-$gl_user$keyid.pub$) } @marked_for_del ); + + print STDERR " +You're undeleting a key that is currently marked for deletion. + Hit ENTER to undelete this key + Hit Ctrl-C to cancel the undelete +Please see documentation for caveats on the undelete process as well as how to +actually delete it. +"; + <>; # yeay... always wanted to do that -- throw away user input! + + cd_temp_clone(); + chdir("keydir"); + + my $fp = fingerprint( $mfd[0] ); + hushed_git( "mv", "-f", $mfd[0], "$gl_user$keyid.pub" ); + hushed_git( "commit", "-m", "sskm: undo-del $gl_user$keyid ($fp)" ) and die "git commit failed\n"; + system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; +} diff --git a/src/commands/sudo b/src/commands/sudo new file mode 100755 index 0000000..eeb0083 --- /dev/null +++ b/src/commands/sudo @@ -0,0 +1,24 @@ +#!/bin/sh + +# Usage: ssh git@host sudo <user> <command> <arguments> +# +# Let super-user run commands as any other user. "Super-user" is defined as +# "have write access to the gitolite-admin repo". + +die() { echo "$@" >&2; exit 1; } +usage() { perl -lne 'print substr($_, 2) if /^# Usage/../^$/' < $0; exit 1; } +[ -z "$2" ] && usage +[ "$1" = "-h" ] && usage +[ -z "$GL_USER" ] && die GL_USER not set + +gitolite access -q gitolite-admin $GL_USER W any || die "You are not authorised" + +user="$1"; shift +cmd="$1"; shift + +# switch user +GL_USER="$user" + +# figure out if the command is allowed from a remote user +gitolite query-rc -q COMMANDS $cmd || die "Command '$cmd' not allowed" +gitolite $cmd "$@" diff --git a/src/commands/svnserve b/src/commands/svnserve new file mode 100755 index 0000000..6e68acf --- /dev/null +++ b/src/commands/svnserve @@ -0,0 +1,17 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Rc; +my $svnserve = $rc{SVNSERVE} || ''; +$svnserve ||= "/usr/bin/svnserve -r /var/svn/ -t --tunnel-user=%u"; + +my $cmd = $ENV{SSH_ORIGINAL_COMMAND}; + +die "expecting 'svnserve -t', got '$cmd'\n" unless $cmd eq 'svnserve -t'; + +$svnserve =~ s/%u/$ENV{GL_USER}/g; +exec $svnserve; +die "svnserve exec failed\n"; diff --git a/src/commands/symbolic-ref b/src/commands/symbolic-ref new file mode 100755 index 0000000..b65c792 --- /dev/null +++ b/src/commands/symbolic-ref @@ -0,0 +1,31 @@ +#!/bin/sh + +# Usage: ssh git@host symbolic-ref <repo> <arguments to git-symbolic-ref> +# +# allow 'git symbolic-ref' over a gitolite connection + +# Security: remember all arguments to commands must match a very conservative +# pattern. Once that is assured, the symbolic-ref command has no security +# related side-effects, so we don't check arguments at all. + +# Note: because of the restriction on allowed characters in arguments, you +# can't supply an arbitrary string to the '-m' option. The simplest +# work-around is-to-just-use-join-up-words-like-this if you feel the need to +# supply a "reason" string. In any case this is useless by default; you'd +# have to have core.logAllRefUpdates set for it to have any meaning. + +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 +repo=${repo%.git} +gitolite access -q "$repo" $GL_USER W any || die You are not authorised + +# change head +cd $GL_REPO_BASE/$repo.git + +git symbolic-ref "$@" diff --git a/src/commands/who-pushed b/src/commands/who-pushed new file mode 100755 index 0000000..e59a750 --- /dev/null +++ b/src/commands/who-pushed @@ -0,0 +1,172 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Easy; + +usage() if not @ARGV; +usage($ARGV[1]) if $ARGV[1] and $ARGV[1] =~ /^[\w-]+$/ and $ARGV[0] eq '-h'; + +( my $logdir = $ENV{GL_LOGFILE} ) =~ s(/[^/]+$)(); + +# deal with migrate +my %gl_log_lines_buffer; +my $countr = 0; +my $countl = 0; +migrate(@ARGV) if $ARGV[0] eq '--migrate'; # won't return; exits right there + +# tip search? +my $tip_search = 0; +if ($ARGV[0] eq '--tip') { + shift; + $tip_search = 1; +} + +# the normal who-pushed +usage() if @ARGV < 2 or $ARGV[0] eq '-h'; +usage() if $ARGV[1] !~ /^[0-9a-f]+$/i; + +my $repo = shift; +my $sha = shift; $sha =~ tr/A-F/a-f/; + +$ENV{GL_USER} and ( can_read($repo) or die "no read permissions on '$repo'" ); + +# ---------------------------------------------------------------------- + +my $repodir = "$ENV{GL_REPO_BASE}/$repo.git"; +chdir $repodir or die "repo '$repo' missing"; + +my @logfiles = reverse glob("$logdir/*"); +@logfiles = ( "$repodir/gl-log" ) if -f "$repodir/gl-log"; + +for my $logfile ( @logfiles ) { + @ARGV = ($logfile); + for my $line ( reverse grep { m(\tupdate\t($repo|$repodir)\t) } <> ) { + chomp($line); + my @fields = split /\t/, $line; + my ( $ts, $pid, $who, $ref, $d_old, $new ) = @fields[ 0, 1, 4, 6, 7, 8 ]; + + # d_old is what you display + my $old = $d_old; + $old = "" if $d_old eq ( "0" x 40 ); + $old = "$old.." if $old; + + if ($tip_search) { + print "$ts $pid $who $ref $d_old $new\n" if $new =~ /^$sha/; + } else { + system("git rev-list $old$new 2>/dev/null | grep ^$sha >/dev/null && echo '$ts $pid $who $ref $d_old $new'"); + } + } +} + +# ---------------------------------------------------------------------- +# migration + +sub migrate { + chdir $ENV{GL_REPO_BASE}; + my @repos = `gitolite list-phy-repos`; chomp @repos; + + my $count = scalar( grep { -f "$_.git/gl-log" } @repos ); + if ( $count and ( $_[1] || '' ) ne '--force' ) { + say2 "$count repo(s) already have gl-log files. To confirm overwriting, please re-run as:"; + say2 "\tgitolite who-pushed --migrate --force"; + say2 "see help ('-h', '-h logfiles', or '-h migrate') for details."; + exit 1; + } + + foreach my $r (@repos) { + _print("$r.git/gl-log", ''); + } + + my %repo_exists = map { $_ => 1 } @repos; + @ARGV = sort ( glob("$logdir/*") ); + while (<>) { + say2 "processed '$ARGV'" if eof(ARGV); + next unless /\tupdate\t/; + my @f = split /\t/; + my $repo = $f[3]; + if ($repo =~ m(^/)) { + $repo =~ s/^$ENV{GL_REPO_BASE}\///; + $repo =~ s/\.git$//; + } + + gen_gl_log($repo, $_) if $repo_exists{$repo}; + } + flush_gl_log(); + + exit 0; +} +sub gen_gl_log { + my ($repo, $l) = @_; + + $countr++ unless $gl_log_lines_buffer{$repo}; # new repo, not yet seen + $countl++; + $gl_log_lines_buffer{$repo} .= $l; + + # once we have buffered log lines for about 100 repos, or about 10,000 log + # lines, we flush them + flush_gl_log() if $countr >= 100 or $countl >= 10_000; +} +sub flush_gl_log { + while (my ($r, $l) = each %gl_log_lines_buffer) { + open my $fh, ">>", "$r.git/gl-log" or _die "open flush_gl_log failed: $!"; + print $fh $l; + close $fh; + } + %gl_log_lines_buffer = (); + say2 "flushed $countl lines to $countr repos..."; + $countr = $countl = 0; +} + +__END__ + +=for usage +usage: ssh git@host who-pushed [--tip] <repo> <SHA> + +Determine who pushed the given commit. The first few hex digits of the SHA +should suffice. If the '--tip' option is supplied, it'll only look for the +SHA among "tip" commits (i.e., search the "new SHA"s, without running the +expensive 'git rev-parse' for each push). + +Each line of the output contains the following fields: timestamp, a +transaction ID, username, refname, and the old and new SHAs for the ref. + +Note on the "transaction ID" field: if looking at the log file doesn't help +you figure out what its purpose is, please just ignore it. + +TO SEE ADDITIONAL HELP, run with options "-h logfiles" or "-h migrate". +=cut + +=for logfiles +There are 2 places that gitolite logs to, based on the value give to the +LOG_DEST rc variable. By default, log files go to ~/.gitolite/logs, but you +can choose to send them to syslog instead (in which case 'who-pushed' will not +work), or to both syslog and the normal log files. + +In addition, gitolite can also be told to log just the "update" records to a +special "gl-log" file in the bare repo directory. This makes 'who-pushed' +**much** faster (thanks to milki for the problem *and* the simple solution). + +'who-pushed' will look for that special file first and use only that if it is +found. Otherwise it will look in the normal gitolite log files, which will of +course be much slower. +=cut + +=for migrate +If you installed gitolite before v3.6.4, and you wish to use the new, more +efficient logging that helps who-pushed run faster, you should first update +the rc file (see http://gitolite.com/gitolite/rc.html for notes on that) to +specify a suitable value for LOG_DEST. + +After that you should probably do a one-time generation of the repo-specific +'gl-log' files from the normal log files. This can only be done from the +server command line, even if the 'who-pushed' command has been enabled for +remote access. + +To do this, just run 'gitolite who-pushed --migrate'. If some of your repos +already had gl-log files, it will warn you, and tell you how to override. +You're only supposed to to use this *once* after upgrading to v3.6.4 and +setting LOG_DEST in the rc file anyway. +=cut + diff --git a/src/commands/writable b/src/commands/writable new file mode 100755 index 0000000..3e97f0b --- /dev/null +++ b/src/commands/writable @@ -0,0 +1,63 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use lib $ENV{GL_LIBDIR}; +use Gitolite::Easy; + +=for usage +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 +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' and $ARGV[1] ne 'status'; + +my $repo = shift; +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) or is_admin() or ( can_write($repo) and $op eq 'status' ) ); +} + +my $msg = join( " ", @ARGV ); +# try STDIN only if no msg found in args *and* it's an 'off' command +if ( not $msg and $op eq 'off' ) { + say2 "...please type the message to be shown to users:"; + $msg = join( "", <> ); +} + +my $sf = ".gitolite.down"; +my $rb = $ENV{GL_REPO_BASE}; + +if ( $repo eq '@all' ) { + target( $ENV{HOME} ); +} else { + target("$rb/$repo.git"); + target( $ENV{HOME} ) if $op eq 'status'; +} + +exit 0; + +sub target { + my $repodir = shift; + if ( $op eq 'status' ) { + exit 1 if -e "$repodir/$sf"; + } elsif ( $op eq 'on' ) { + unlink "$repodir/$sf"; + } elsif ( $op eq 'off' ) { + _print( "$repodir/$sf", $msg ); + } +} |