mirror of
https://github.com/wahyd4/gitolite.git
synced 2026-08-09 04:55:55 +10:00
(vrs) virtual refs and scoring
Note: filenames of actual VR scripts on disk start with "gl-"
compile
- insert scores into %repos, if provided
- recognise VR_[A-Z0-9_]+/ like NAME/
update and gitolite.pm
- move die()s up the call chain
- (set up for using other forms of virtref later)
- collect virtual refs and call special routines for each one found
- (virtrefs can have arguments)
- (get rid of NAME_LIMITS; you don't need it anymore)
- add score computation code
Other stuff:
- basic support for tracing; undocumented for now
- remove "in-core" NAME; use new VR_NAME by default
Implementation notes:
- the interface to check_access is no written in stone (since
v1.5.8+1; 5b9bf70: "allow access checks from ADC or hook"), and we
don't want to change that (would cause problems if people missed the
change warning!)
So the new version of check_access is called 'check_score', and
check_access is not just a compatibility layer.
The new check_score shall stay undocumented for now; we will decide
later if we should "out" it.
This commit is contained in:
@@ -41,8 +41,9 @@ my $ret = check_access($repo, 'refs/heads/foo', 'W', 1);
|
||||
|
||||
# the return value may look like this:
|
||||
# refs/.*
|
||||
# or perhaps this, if you were denied
|
||||
# DENIED by fallthru
|
||||
# On failure, it will have the reason, and it will contain the word DENIED
|
||||
# somewhere, for example:
|
||||
# W refs/heads/master foo u1 DENIED by fallthru
|
||||
|
||||
# NOTE: do NOT pass "R" as the 3rd argument. It will seem to work because
|
||||
# you're merely testing the permissions in this code, but an *actual* "git
|
||||
|
||||
+65
-24
@@ -28,6 +28,7 @@ use gitolite qw(:DEFAULT %repos);
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
my ($perm, $creator, $wild) = repo_rights($ENV{GL_REPO});
|
||||
trace("update: repo_rights perm is $perm");
|
||||
my $reported_repo = $ENV{GL_REPO} . ( $wild ? " ($wild)" : "" );
|
||||
|
||||
# arguments are as supplied to an update hook by git; man githooks
|
||||
@@ -69,40 +70,80 @@ if ( $repos{ $ENV{GL_REPO} }{MERGE_CHECK} or $repos{'@all'}{MERGE_CHECK} ) {
|
||||
}
|
||||
|
||||
my @allowed_refs;
|
||||
# @all repos: see comments in similar code in check_access
|
||||
# @all repos: see comments in similar code in check_score
|
||||
push @allowed_refs, @ { $repos{$ENV{GL_REPO}}{$ENV{GL_USER}} || [] };
|
||||
push @allowed_refs, @ { $repos{'@all'} {$ENV{GL_USER}} || [] };
|
||||
push @allowed_refs, @ { $repos{$ENV{GL_REPO}}{'@all'} || [] };
|
||||
push @allowed_refs, @ { $repos{'@all'} {'@all'} || [] };
|
||||
|
||||
# prepare the list of refs to be checked
|
||||
my $minscore = 10;
|
||||
|
||||
# previously, we just checked $ref -- the ref being updated, which is passed
|
||||
# to us by git (see man githooks). Now we also have to treat each NAME being
|
||||
# updated as a potential "ref" and check that, if NAME-based restrictions have
|
||||
# been specified
|
||||
# check the first ref -- this is a real (git) ref, like refs/heads/master
|
||||
my ($score, $txt) = check_ref(\@allowed_refs, $ENV{GL_REPO}, $ref, $att_acc);
|
||||
trace("update: score $score trying $att_acc on ref $ref");
|
||||
my $log_refex = $txt; # save this for printing in the log record
|
||||
my $denymsg = $txt if $score < 10;
|
||||
|
||||
my @refs = ($ref); # the first ref to check is the real one
|
||||
# because making it work screws up efficiency like no tomorrow...
|
||||
if (exists $repos{$ENV{GL_REPO}}{NAME_LIMITS}) {
|
||||
# this is special to git -- the hash of an empty tree
|
||||
my $empty='4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
||||
# well they're not really "trees" but $empty is indeed the empty tree so
|
||||
# we can just pretend $oldsha/$newsha are also trees, and anyway 'git
|
||||
# diff' only wants trees
|
||||
my $oldtree = $oldsha eq '0' x 40 ? $empty : $oldsha;
|
||||
my $newtree = $newsha eq '0' x 40 ? $empty : $newsha;
|
||||
push @refs, map { chomp; s/^/NAME\//; $_; } `git diff --name-only $oldtree $newtree`;
|
||||
# collect virtual ref types, if any
|
||||
my %virtreftypes;
|
||||
for (map { $_->[1] } @allowed_refs) {
|
||||
$virtreftypes{$1}++ if m(^VR_([\w-]+)/);
|
||||
}
|
||||
trace("update: virt refs applicable: " . join(",", sort keys %virtreftypes)) if %virtreftypes;
|
||||
|
||||
# here's how scoring works:
|
||||
# (1) computing minscore: minimum score is 10 * number of ref types
|
||||
# [ref types: there's exactly one "real" ref (refs/heads/master, etc), and
|
||||
# any number of "virt"ual refs. For each ref type applicable to this
|
||||
# repo+user, add 10 to the minimum score]
|
||||
# (2) computing total score: each rule specified a score (default 10 for
|
||||
# accept rules and 0 for deny rules. The score for a reftype is the
|
||||
# minimum of the rules in that reftype. The total score is the sum of the
|
||||
# scores of each reftype. [example: 1 real ref, 20 NAME/ refs (20 files
|
||||
# were changed), is 2 reftypes so minscore is 20. Actual score is whatever
|
||||
# the real ref returns + minimum(whatever the NAME/ refs return).
|
||||
|
||||
# find virtual refs
|
||||
for my $vrt (sort keys %virtreftypes) {
|
||||
my $vscore;
|
||||
my $match_sub;
|
||||
|
||||
my @refs = get_virt_refs($vrt, $ref, $oldsha, $newsha);
|
||||
# the first *may* be one of the following words, to indicate what sort of
|
||||
# matching the virtref is expecting
|
||||
if ( ($refs[0] || '') =~ m(^VR_$vrt/(regex|lesser|greater|IP)$) ) {
|
||||
# "regex" is the default; there's no sub for this
|
||||
$match_sub = \&match_lesser if ($1 eq 'lesser');
|
||||
$match_sub = \&match_greater if ($1 eq 'greater');
|
||||
$match_sub = \&match_IP if ($1 eq 'IP');
|
||||
shift @refs;
|
||||
}
|
||||
|
||||
# if there were no virtrefs to start with (for example, an empty commit),
|
||||
# then bumping minscore up causes problems, so bail out
|
||||
next unless @refs;
|
||||
# each reftype specified increases the min score by 10
|
||||
$minscore += 10;
|
||||
|
||||
# allow only refs that match the $vrt, for efficiency
|
||||
my @allowed_vrefs = grep { $_->[1] =~ m(^VR_$vrt/) } @allowed_refs;
|
||||
for my $r (@refs) {
|
||||
my ($rscore, $txt) = check_ref(\@allowed_vrefs, $ENV{GL_REPO}, $r, $att_acc, $match_sub);
|
||||
# the score for each vref type is the minimum of the scores for the
|
||||
# refs of that type
|
||||
unless (defined($vscore) and $vscore < $rscore) {
|
||||
$vscore = $rscore;
|
||||
$denymsg ||= $txt if $rscore < 10;
|
||||
}
|
||||
}
|
||||
$score += $vscore || 0;
|
||||
trace("update: score $score after adding vscore $vscore using " . scalar(@refs) . " refs from $vrt");
|
||||
}
|
||||
|
||||
# we potentially have many "refs" to check. The one we print in the log is
|
||||
# the *first* one (which is a *real* ref, like refs/heads/master), while all
|
||||
# the rest (if they exist) are like NAME/something. So we do the first one
|
||||
# separately to capture it, then run the rest (if any)
|
||||
my $log_refex = check_ref(\@allowed_refs, $ENV{GL_REPO}, (shift @refs), $att_acc);
|
||||
check_ref (\@allowed_refs, $ENV{GL_REPO}, $_ , $att_acc) for @refs;
|
||||
die (($denymsg || "access DENIED. Score $score < $minscore") . "\n") if $score < $minscore;
|
||||
|
||||
# if we returned at all, all the checks succeeded. Check secondary hooks now
|
||||
# if we came here, all the checks succeeded. Check secondary hooks now.
|
||||
# Secondary hooks do not participate in scoring, by the way!
|
||||
$UPDATE_CHAINS_TO ||= 'hooks/update.secondary';
|
||||
-x $UPDATE_CHAINS_TO and system ( $UPDATE_CHAINS_TO, @ARGV ) and die "$UPDATE_CHAINS_TO died\n";
|
||||
|
||||
|
||||
+108
-19
@@ -5,12 +5,15 @@ use Exporter 'import';
|
||||
@EXPORT = qw(
|
||||
can_read
|
||||
check_access
|
||||
check_score
|
||||
check_ref
|
||||
check_repo_write_enabled
|
||||
cli_repo_rights
|
||||
cli_grouplist
|
||||
dbg
|
||||
dos2unix
|
||||
trace
|
||||
get_virt_refs
|
||||
list_phy_repos
|
||||
ln_sf
|
||||
log_it
|
||||
@@ -34,6 +37,10 @@ use Exporter 'import';
|
||||
mirror_mode
|
||||
mirror_listslaves
|
||||
mirror_redirectOK
|
||||
|
||||
match_lesser
|
||||
match_greater
|
||||
match_IP
|
||||
);
|
||||
@EXPORT_OK = qw(
|
||||
%repos
|
||||
@@ -162,6 +169,17 @@ sub dos2unix {
|
||||
return @_;
|
||||
}
|
||||
|
||||
sub trace {
|
||||
return unless $ENV{GL_TRACE};
|
||||
our $starttime;
|
||||
unless (defined $starttime) {
|
||||
require Time::HiRes; Time::HiRes->import ( qw(gettimeofday tv_interval) );
|
||||
$starttime = [gettimeofday()];
|
||||
}
|
||||
print STDERR "## ", sprintf("%08.6f", tv_interval($starttime)), " gl-trace: ", @_, "\n";
|
||||
$starttime = [gettimeofday()];
|
||||
}
|
||||
|
||||
sub log_it {
|
||||
my ($ip, $logmsg);
|
||||
open my $log_fh, ">>", $ENV{GL_LOG} or die "open log failed: $!\n";
|
||||
@@ -218,9 +236,9 @@ sub check_ref {
|
||||
# permission must also match the action (W/+, or C/D if used) being
|
||||
# attempted. If none of them match, the access is denied.
|
||||
|
||||
# NOTE: the function DIES when access is denied, unless arg 5 is true
|
||||
# the return value is either (10, some-string) or (0, some-string)
|
||||
|
||||
my ($allowed_refs, $repo, $ref, $perm, $dry_run) = @_;
|
||||
my ($allowed_refs, $repo, $ref, $perm, $match_sub) = @_;
|
||||
|
||||
# sanity check the ref
|
||||
die "invalid characters in ref or filename: $ref\n" unless $ref =~ $GL_REF_OR_FILENAME_PATT;
|
||||
@@ -229,20 +247,23 @@ sub check_ref {
|
||||
for my $ar (@allowed_refs) {
|
||||
my $refex = $ar->[1];
|
||||
# refex? sure -- a regex to match a ref against :)
|
||||
next unless $ref =~ /^$refex/ or $ref eq 'joker';
|
||||
# joker matches any refex; it will only ever be sent internally
|
||||
return "$perm $ref $repo $ENV{GL_USER} DENIED by $refex" if $ar->[2] eq '-' and $dry_run;
|
||||
die "$perm $ref $repo $ENV{GL_USER} DENIED by $refex\n" if $ar->[2] eq '-';
|
||||
if ($match_sub) {
|
||||
next unless &$match_sub($ref, $refex);
|
||||
} else {
|
||||
# match sub not passed; use the normal default
|
||||
next unless $ref =~ /^$refex/ or $ref eq 'joker';
|
||||
# joker matches any refex; it will only ever be sent internally
|
||||
}
|
||||
return ($ar->[3] || 0, "$perm $ref $repo $ENV{GL_USER} DENIED by $refex") if $ar->[2] eq '-';
|
||||
|
||||
# $ar->[2] can be RW\+?(C|D|CD|DC)?M?. $perm can be W, +, C or
|
||||
# D, or any of these followed by "M".
|
||||
( my $permq = $perm ) =~ s/\+/\\+/;
|
||||
$permq =~ s/M/.*M/;
|
||||
# as far as *this* ref is concerned we're ok
|
||||
return $refex if ($ar->[2] =~ /$permq/);
|
||||
return ($ar->[3] || 10, $refex) if ($ar->[2] =~ /$permq/);
|
||||
}
|
||||
return "$perm $ref $repo $ENV{GL_USER} DENIED by fallthru" if $dry_run;
|
||||
die "$perm $ref $repo $ENV{GL_USER} DENIED by fallthru\n";
|
||||
return (0, "$perm $ref $repo $ENV{GL_USER} DENIED by fallthru");
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
@@ -724,7 +745,6 @@ sub parse_acl
|
||||
my $dr = $repo; $dr = '@all' if $r eq '@all';
|
||||
$repos{$dr}{DELETE_IS_D} = 1 if $repos{$r}{DELETE_IS_D};
|
||||
$repos{$dr}{CREATE_IS_C} = 1 if $repos{$r}{CREATE_IS_C};
|
||||
$repos{$dr}{NAME_LIMITS} = 1 if $repos{$r}{NAME_LIMITS};
|
||||
$repos{$dr}{MERGE_CHECK} = 1 if $repos{$r}{MERGE_CHECK};
|
||||
# this needs to copy the key-value pairs from RHS to LHS, not just
|
||||
# assign RHS to LHS! However, we want to roll in '@all' configs also
|
||||
@@ -983,12 +1003,26 @@ sub get_memberships {
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# generic check access routine
|
||||
# build allowed refs and call check_ref for each of them
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# compat layer for documented (and likely used in ADCs and such) check_access
|
||||
# when used with 4 arguments; see contrib/adc/get-rights-and-owner.in-perl
|
||||
sub check_access
|
||||
{
|
||||
my ($repo, $ref, $aa, $dry_run) = @_;
|
||||
if ($_[3]) {
|
||||
my ($score, $txt) = check_score(@_[0..2]);
|
||||
return $txt; # this is all we return, per that doc
|
||||
}
|
||||
# otherwise the new check_score is the same as the old check_access
|
||||
return check_score(@_);
|
||||
}
|
||||
|
||||
# what used to be 'check_access' is now 'check_score' all round; see comments
|
||||
# on previous sub for more
|
||||
sub check_score
|
||||
{
|
||||
my ($repo, $ref, $aa) = @_;
|
||||
# aa = attempted access
|
||||
|
||||
my ($perm, $creator, $wild);
|
||||
@@ -999,15 +1033,16 @@ sub check_access
|
||||
return ($perm, $creator);
|
||||
}
|
||||
|
||||
# level 1 check -- just one reponame as arg, gets you (perm, creator)
|
||||
($perm, $creator, $wild) = repo_rights($repo) unless $ref eq 'joker';
|
||||
# calling it when ref eq joker is infinitely recursive! check_access
|
||||
# will only be called with ref eq joker only when repo_rights has
|
||||
# already been called and %repos populated already. (See comments
|
||||
# elsewhere for what 'joker' is and why it is called that).
|
||||
|
||||
# until I do some major refactoring (which will bloat the update hook a
|
||||
# bit, sadly), this code duplicates stuff in the current update hook.
|
||||
# level 2 check -- repo, ref, aa as args, gets you (score, text)
|
||||
|
||||
# this code duplicates stuff in the current update hook.
|
||||
my @allowed_refs;
|
||||
# user+repo specific perms override everything else, so they come first.
|
||||
# Then perms given to specific user for @all repos, and finally perms
|
||||
@@ -1017,11 +1052,7 @@ sub check_access
|
||||
push @allowed_refs, @ { $repos{$repo}{'@all'} || [] };
|
||||
push @allowed_refs, @ { $repos{'@all'}{'@all'} || [] };
|
||||
|
||||
if ($dry_run) {
|
||||
return check_ref(\@allowed_refs, $repo, $ref, $aa, $dry_run);
|
||||
} else {
|
||||
check_ref(\@allowed_refs, $repo, $ref, $aa);
|
||||
}
|
||||
return check_ref(\@allowed_refs, $repo, $ref, $aa);
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
@@ -1141,6 +1172,35 @@ sub setup_authkeys
|
||||
system("rm $ENV{HOME}/.ssh/new_authkeys");
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# V I R T U A L R E F S
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
sub get_virt_refs {
|
||||
my ($vrt, $ref, $oldsha, $newsha) = @_;
|
||||
my @refs;
|
||||
|
||||
# this is special to git -- the hash of an empty tree
|
||||
my $empty='4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
||||
# well they're not really "trees" but $empty is indeed the empty tree so
|
||||
# we can just pretend $oldsha/$newsha are also trees, and anyway 'git
|
||||
# diff' only wants trees
|
||||
my $oldtree = $oldsha eq '0' x 40 ? $empty : $oldsha;
|
||||
my $newtree = $newsha eq '0' x 40 ? $empty : $newsha;
|
||||
|
||||
my ($vrtpgm, @args) = split(/-/, $vrt);
|
||||
if ( -x "$ENV{GL_BINDIR}/gl-VR_$vrtpgm" ) {
|
||||
$vrtpgm = "$ENV{GL_BINDIR}/gl-VR_$vrtpgm $ref $oldsha $newsha $oldtree $newtree";
|
||||
$vrtpgm .= " " . join(" ", @args) if @args;
|
||||
chomp(@refs = `$vrtpgm`);
|
||||
s/^/VR_$vrt\// for @refs;
|
||||
} else {
|
||||
die "virtual ref $vrt failed; you need a program called gl-VR_$vrtpgm among the gitolite scripts\n";
|
||||
}
|
||||
|
||||
return @refs;
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# S P E C I A L C O M M A N D S
|
||||
# ----------------------------------------------------------------------------
|
||||
@@ -1279,6 +1339,35 @@ sub mirror_redirectOK {
|
||||
# deciding the name of the program (yet another rc var?)
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# VRS HELPERS
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
sub match_lesser
|
||||
{
|
||||
my ($ref, $refex) = @_;
|
||||
for ($ref, $refex) {
|
||||
s(^VR_[-\w]+/(\d+)$)($1) or die "malformed $_ in match_lesser\n";
|
||||
}
|
||||
return $ref < $refex;
|
||||
}
|
||||
|
||||
sub match_greater
|
||||
{
|
||||
my ($ref, $refex) = @_;
|
||||
for ($ref, $refex) {
|
||||
s(^VR_[-\w]+/(\d+)$)($1) or die "malformed $_ in match_greater\n";
|
||||
}
|
||||
return $ref > $refex;
|
||||
}
|
||||
|
||||
sub match_IP
|
||||
{
|
||||
my ($ref, $refex) = @_;
|
||||
die "match_IP is not yet implemented\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# per perl rules, this should be the last line in such a file:
|
||||
1;
|
||||
|
||||
+6
-1
@@ -121,12 +121,15 @@ if ($ENV{SSH_ORIGINAL_COMMAND} =~ $CUSTOM_COMMANDS) {
|
||||
# single quotes):
|
||||
# git-receive-pack 'reponame.git'
|
||||
|
||||
my ($verb, $repo) = ($ENV{SSH_ORIGINAL_COMMAND} =~ /^\s*(git\s+\S+|\S+)\s+'\/?(.*?)(?:\.git)?'/);
|
||||
my ($verb, $repo) = ($ENV{SSH_ORIGINAL_COMMAND} =~ /^\s*(git\s+\S+|\S+)\s+'\/?(.*?)(?:\.git|\.git-trace)?'/);
|
||||
unless ( $verb and ( $verb eq 'git-init' or $verb =~ $R_COMMANDS or $verb =~ $W_COMMANDS ) and $repo and $repo =~ $REPONAME_PATT ) {
|
||||
special_cmd ($shell_allowed);
|
||||
exit 0;
|
||||
}
|
||||
|
||||
# set trace mode, if called for
|
||||
$ENV{GL_TRACE} = $ENV{SSH_ORIGINAL_COMMAND} =~ /\.git-trace'$/;
|
||||
|
||||
# some final sanity checks
|
||||
die "$repo ends with a slash; I don't like that\n" if $repo =~ /\/$/;
|
||||
die "$repo has two consecutive periods; I don't like that\n" if $repo =~ /\.\./;
|
||||
@@ -156,8 +159,10 @@ if ( $aa eq 'W' and mirror_mode($repo) =~ /^slave of (\S+)/ ) {
|
||||
my ($perm, $creator, $wild);
|
||||
if ( $GL_ALL_READ_ALL and $verb =~ $R_COMMANDS and -d "$REPO_BASE/$repo.git") {
|
||||
$perm = 'R';
|
||||
trace("auth: GL_ALL_READ_ALL set, verb is $verb, granting 'R'");
|
||||
} else {
|
||||
($perm, $creator, $wild) = repo_rights($repo);
|
||||
trace("auth: repo_rights perm is $perm");
|
||||
}
|
||||
# it was missing, and you have create perms, so create it
|
||||
new_wild_repo($repo, $user) if ($perm =~ /C/);
|
||||
|
||||
+12
-8
@@ -167,17 +167,21 @@ sub parse_conf_line
|
||||
{
|
||||
my $perms = $1;
|
||||
my @refs; @refs = split( ' ', $2 ) if $2;
|
||||
@refs = expand_list ( @refs );
|
||||
my @users = split ' ', $3;
|
||||
my $score = 0;
|
||||
$score = shift @refs if @refs and $refs[0] =~ /^-?\d+$/;
|
||||
@refs = expand_list ( @refs );
|
||||
die "$ABRT \$GL_WILDREPOS is not set, you cant use 'C' in config\n" if $perms eq 'C' and not $GL_WILDREPOS;
|
||||
|
||||
# if no ref is given, this PERM applies to all refs
|
||||
@refs = qw(refs/.*) unless @refs;
|
||||
# deprecation warning
|
||||
map { print STDERR "WARNING: old syntax 'PATH/' found; please use new syntax 'NAME/'\n" if s(^PATH/)(NAME/) } @refs;
|
||||
# fully qualify refs that dont start with "refs/" or "NAME/";
|
||||
# prefix them with "refs/heads/"
|
||||
@refs = map { m(^(refs|NAME)/) or s(^)(refs/heads/); $_ } @refs;
|
||||
map { print STDERR "WARNING: really old syntax 'PATH/' found; please use new syntax 'VR_NAME/'\n" if s(^PATH/)(VR_NAME/) } @refs;
|
||||
# (NAME is grandfathered in as VR_NAME)
|
||||
s(^NAME/)(VR_NAME/) for @refs;
|
||||
# now fully qualify refs that dont start with "refs/" or the VR
|
||||
# pattern by prefixing them with "refs/heads/"
|
||||
@refs = map { m(^(refs|VR_(?:[\w-]+))/) or s(^)(refs/heads/); $_ } @refs;
|
||||
@refs = map { s(/USER/)(/\$gl_user/); $_ } @refs;
|
||||
|
||||
# expand the user list, unless it is just "@all"
|
||||
@@ -234,10 +238,10 @@ sub parse_conf_line
|
||||
# to avoid doing it for the large majority of repos
|
||||
# that do *not* use NAME limits. Setting a flag that
|
||||
# can be checked right away will help us do that
|
||||
$repos{$repo}{NAME_LIMITS} = 1 if $ref =~ /^NAME\//;
|
||||
my $p_user = $user; $p_user =~ s/creator$/creator - wild/;
|
||||
push @{ $repos{$repo}{$p_user} }, [ $rule_seq++, $ref, $perms ]
|
||||
unless $rurp_seen{$repo}{$p_user}{$ref}{$perms}++;
|
||||
push @{ $repos{$repo}{$p_user} },
|
||||
( $score ? [ $rule_seq++, $ref, $perms, $score ] : [ $rule_seq++, $ref, $perms ] )
|
||||
unless $rurp_seen{$repo}{$p_user}{$ref}{$perms}++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user