Skip to content

Commit 57c6c54

Browse files
committed
default codeowners
- added support for section default codeowners notation - bumped version to 1.1.2 closes #15
1 parent 79ebb8b commit 57c6c54

File tree

4 files changed

+120
-49
lines changed

4 files changed

+120
-49
lines changed

lib/GPH.pm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ package GPH;
33
use strict;
44
use warnings FATAL => 'all';
55

6-
our $VERSION = '1.1.1';
6+
our $VERSION = '1.1.2';
77

88
1;

lib/GPH/Gitlab.pm

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ package GPH::Gitlab;
1515
use strict;
1616
use warnings FATAL => 'all';
1717

18+
use Data::Dumper;
19+
1820
#------------------------------------------------------------------------------
1921
# Construct new class
2022
#
@@ -49,7 +51,7 @@ sub new {
4951
# Returns: reference to Gitlab object
5052
sub parseCodeowners {
5153
my ($self, %args) = @_;
52-
my ($fh, %codeowners, %excludeHash, %blacklist);
54+
my ($fh, %excludes, $default_owners);
5355

5456
open $fh, '<', $args{codeowners} or die "unable to open codeowners file: $!";
5557
my @lines = <$fh>;
@@ -58,49 +60,77 @@ sub parseCodeowners {
5860
# build excludes hash for quick lookup
5961
if (exists($args{excludes})) {
6062
foreach my $item (@{$args{excludes}}) {
61-
$excludeHash{$item} = 1;
63+
$excludes{$item} = 1;
6264
}
6365
}
6466

65-
for my $line (@lines) {
66-
# skip section line. default codeowners not yet supported
67-
next if $line =~ /[\[\]]/;
68-
# skip if line does not contain @
69-
next unless $line =~ /^.*\s\@[\w]+\/.*$/x;
70-
# replace /**/* with a trailing forward slash
71-
my $pat = quotemeta('/**/* ');
72-
$line =~ s|$pat|/ |;
67+
foreach (@lines) {
68+
next if $_ =~ /^#.*/ or $_ =~ /^[\s]?$/;
69+
my $line = $self->sanitise($_);
70+
71+
if ($line =~ /\]/) {
72+
$default_owners = ($line =~ /^[\^]?\[[^\]]+\](?:[\[0-9\]]{0,}) (.*)$/) ? $1 : undef;
73+
74+
next;
75+
}
76+
77+
my ($class_path, $owners) = split(/\s/, $line, 2);
7378

74-
my ($class_path, $owners) = split(' ', $line, 2);
79+
next if exists $excludes{$class_path};
7580

76-
# skip if path is excluded
77-
next if exists $excludeHash{$class_path};
81+
$owners = $owners || $default_owners;
7882

79-
foreach (split(' ', $owners)) {
80-
next unless /(\@[\w\-\/]{0,})$/x;
83+
next unless defined $owners;
8184

82-
if (not exists $codeowners{$1}) {
83-
$codeowners{$1} = [];
84-
$blacklist{$1} = [];
85+
foreach my $owner (split(/\s/, $owners)) {
86+
next unless $owner =~ /@/;
87+
if (not exists $self->{codeowners}{$owner}) {
88+
$self->{codeowners}{$owner} = [];
89+
$self->{blacklist}{$owner} = [];
8590
}
8691

87-
push(@{$codeowners{$1}}, $class_path);
92+
push($self->{codeowners}{$owner}, $class_path);
8893

89-
# check whether less specific path is already defined and add it to the blacklist
90-
foreach my $key (keys %codeowners) {
91-
foreach my $defined (@{$codeowners{$key}}) {
92-
if ($class_path =~ $defined and $class_path ne $defined) {
93-
push(@{$blacklist{$key}}, $class_path);
94-
}
95-
}
94+
$self->blacklist($class_path);
95+
}
96+
}
97+
98+
return ($self);
99+
}
100+
101+
#------------------------------------------------------------------------------
102+
# Check whether less specific path is already defined and add it to the blacklist
103+
#
104+
# Inputs: class_path => (string) path to check and blacklist
105+
#
106+
# Returns: reference to Gitlab object
107+
sub blacklist {
108+
my ($self, $class_path) = @_;
109+
110+
foreach my $owner (keys $self->{codeowners}) {
111+
foreach my $path (@{$self->{codeowners}{$owner}}) {
112+
if ($class_path =~ $path and $class_path ne $path) {
113+
push($self->{blacklist}{$owner}, $class_path);
96114
}
97115
}
98116
}
99117

100-
$self->{codeowners} = \%codeowners;
101-
$self->{blacklist} = \%blacklist;
118+
return ($self);
119+
}
120+
121+
#------------------------------------------------------------------------------
122+
# Replace /**/* with a trailing forward slash
123+
#
124+
# Inputs: line => (string) line to sanitise
125+
#
126+
# Returns: string
127+
sub sanitise {
128+
my ($self, $line) = @_;
129+
130+
my $pat = quotemeta('/**/* ');
131+
$line =~ s|$pat|/ |;
102132

103-
return $self;
133+
return ($line);
104134
}
105135

106136
#------------------------------------------------------------------------------

t/share/Gitlab/CODEOWNERS

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
# Lines that start with `#` are ignored.
12
[alpha]
23
/src/Command/**/* @teams/alpha
3-
/src/Service/ @teams/alpha @#!$%^
4-
.gitlab-ci.yml @teams/alpha
4+
/src/Service/ @teams/alpha [email protected]
5+
.gitlab-ci.yml @teams/alpha henkie
56

6-
[beta]
7-
/src/Command/Config/ConfigPhraseKeyCommand.php @teams/beta
8-
/src/DependencyInjection/ @teams/beta
9-
.gitlab-ci.yml @teams/beta
7+
[beta] @teams/beta
8+
/src/Command/Config/ConfigPhraseKeyCommand.php
9+
/src/DependencyInjection/
10+
.gitlab-ci.yml
11+
12+
^[gamma][3] @teams/gamma
13+
/tests/Unit/Service/

t/unit/GPH/Gitlab.t

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,26 @@ local $SIG{__WARN__} = sub {};
1616
describe "class `$CLASS`" => sub {
1717
my %config = (
1818
codeowners => CODEOWNERS_FILE,
19-
owner =>'@teams/alpha',
20-
excludes => ['.gitlab-ci.yml']
19+
owner => '@teams/alpha',
20+
excludes => [ '.gitlab-ci.yml' ]
2121
);
2222

2323
tests 'it can be instantiated' => sub {
2424
can_ok($CLASS, 'new');
2525
};
2626

2727
tests "codeowners file not found" => sub {
28-
ok(dies{$CLASS->new((codeowners => 'foo.php', owner =>'@teams/alpha'))}, 'died with codeowners not found') or note ($@);
28+
ok(dies {$CLASS->new((codeowners => 'foo.php', owner => '@teams/alpha'))}, 'died with codeowners not found') or note($@);
2929
};
3030

3131
tests "mandatory config options" => sub {
32-
ok(dies{$CLASS->new((owner =>'@teams/alpha'))}, 'died with missing codeowners option') or note ($@);
33-
ok(dies{$CLASS->new((codeowners => CODEOWNERS_FILE))}, 'died with missing owner option') or note ($@);
34-
ok(lives{$CLASS->new((owner =>'@teams/alpha', codeowners => CODEOWNERS_FILE))}, 'lived with mandatory options') or note ($@);
32+
ok(dies {$CLASS->new((owner => '@teams/alpha'))}, 'died with missing codeowners option') or note($@);
33+
ok(dies {$CLASS->new((codeowners => CODEOWNERS_FILE))}, 'died with missing owner option') or note($@);
34+
ok(lives {$CLASS->new((owner => '@teams/alpha', codeowners => CODEOWNERS_FILE))}, 'lived with mandatory options') or note($@);
3535
};
3636

3737
tests 'owner with blacklist and exclude' => sub {
38-
my ($object, $exception, $warnings, @excludes);
38+
my ($object, $exception, $warnings);
3939

4040
$exception = dies {
4141
$warnings = warns {
@@ -46,12 +46,12 @@ describe "class `$CLASS`" => sub {
4646
is($exception, undef, 'no exception thrown');
4747
is($warnings, 0, 'no warnings generated');
4848

49-
is($object->{blacklist}{'@teams/alpha'}, ['/src/Command/Config/ConfigPhraseKeyCommand.php'], 'blacklist correct');
49+
is($object->{blacklist}{'@teams/alpha'}, [ '/src/Command/Config/ConfigPhraseKeyCommand.php' ], 'blacklist correct') or diag Dumper($object);
5050
is('.gitlab-ci.yml', not_in_set(@{$object->{codeowners}{'@teams/alpha'}}), 'excluded file not defined');
5151
};
5252

5353
tests 'module methods' => sub {
54-
my ($object, $exception, $warnings, @excludes);
54+
my ($object, $exception, $warnings);
5555

5656
$exception = dies {
5757
$warnings = warns {
@@ -68,15 +68,15 @@ describe "class `$CLASS`" => sub {
6868
item '/src/Service/';
6969
end;
7070
},
71-
'GetPaths call correct'
71+
'GetPaths call correct'
7272
);
7373

7474
is($object->getBlacklistPaths(),
7575
array {
7676
item '/src/Command/Config/ConfigPhraseKeyCommand.php';
7777
end;
7878
},
79-
'GetBlacklistPaths call correct'
79+
'GetBlacklistPaths call correct'
8080
);
8181

8282
is($object->getCommaSeparatedPathList(), '/src/Command/,/src/Service/', 'GetCommaSeparatedPathList call correct');
@@ -85,12 +85,12 @@ describe "class `$CLASS`" => sub {
8585

8686
is($object->intersectCommaSeparatedPathList(@arr), '/src/Command/', 'IntersectToCommaSeparatedPathList call correct');
8787

88-
is([$object->intersect(@arr)],
88+
is([ $object->intersect(@arr) ],
8989
array {
9090
item '/src/Command/';
9191
end;
9292
},
93-
'Intersect call correct'
93+
'Intersect call correct'
9494
);
9595

9696
is($object->match('/src/Service/'), 1, 'Match call match correct');
@@ -101,5 +101,42 @@ describe "class `$CLASS`" => sub {
101101
};
102102
};
103103

104+
describe 'test codeowners syntax' => sub {
105+
my ($object, %config, $owner, $expected_paths, $exception, $warnings);
106+
107+
case 'section with default owner' => sub {
108+
$owner = '@teams/beta';
109+
$expected_paths = [ '/src/Command/Config/ConfigPhraseKeyCommand.php', '/src/DependencyInjection/' ];
110+
};
111+
112+
case 'optional section with default owner and required approvals' => sub {
113+
$owner = '@teams/gamma';
114+
$expected_paths = [ '/tests/Unit/Service/' ];
115+
};
116+
117+
case 'owner with email' => sub {
118+
$owner = '[email protected]';
119+
$expected_paths = [ '/src/Service/' ];
120+
};
121+
122+
tests 'module get paths' => sub {
123+
%config = (
124+
codeowners => CODEOWNERS_FILE,
125+
owner => $owner,
126+
excludes => [ '.gitlab-ci.yml' ]
127+
);
128+
129+
$exception = dies {
130+
$warnings = warns {
131+
$object = $CLASS->new(%config);
132+
};
133+
};
134+
135+
is($exception, undef, 'no exception thrown');
136+
is($warnings, 0, 'no warnings generated');
137+
is($object->getPaths(), $expected_paths, 'paths correct') or diag Dumper($object);
138+
};
139+
};
140+
104141
done_testing();
105142

0 commit comments

Comments
 (0)