File Coverage

blib/lib/File/Codeowners.pm
Criterion Covered Total %
statement 226 275 82.1
branch 95 136 69.8
condition 33 49 67.3
subroutine 33 41 80.4
pod 26 26 100.0
total 413 527 78.3


line stmt bran cond sub pod time code
1             package File::Codeowners;
2             # ABSTRACT: Read and write CODEOWNERS files
3              
4 1     1   1748 use v5.10.1; # defined-or
  1         8  
5 1     1   5 use warnings;
  1         1  
  1         28  
6 1     1   4 use strict;
  1         2  
  1         19  
7              
8 1     1   471 use Encode qw(encode);
  1         8234  
  1         56  
9 1     1   646 use Path::Tiny 0.089;
  1         12290  
  1         46  
10 1     1   5 use Scalar::Util qw(openhandle);
  1         2  
  1         64  
11 1     1   341 use Text::Gitignore qw(build_gitignore_matcher);
  1         771  
  1         2489  
12              
13             our $VERSION = '0.54'; # VERSION
14              
15             my $RE_PATTERN = qr/.+?(?
16             my $RE_OWNER = qr/(?:\@+"[^"]*")|(?:\H+)/;
17              
18 0     0   0 sub _croak { require Carp; Carp::croak(@_); }
  0         0  
19 0     0   0 sub _usage { _croak("Usage: @_\n") }
20              
21              
22             sub new {
23 0     0 1 0 my $class = shift;
24 0         0 my $self = bless {}, $class;
25             }
26              
27              
28             sub parse {
29 6     6 1 9737 my $self = shift;
30 6 50       15 my $input = shift or _usage(q{$codeowners->parse($input)});
31              
32 6 50       17 return $self->parse_from_array($input, @_) if ref($input) eq 'ARRAY';
33 6 100       14 return $self->parse_from_string($input, @_) if ref($input) eq 'SCALAR';
34 5 50       31 return $self->parse_from_fh($input, @_) if openhandle($input);
35 5         11 return $self->parse_from_filepath($input, @_);
36             }
37              
38              
39             sub parse_from_filepath {
40 6     6 1 908 my $self = shift;
41 6 50       11 my $path = shift or _usage(q{$codeowners->parse_from_filepath($filepath)});
42              
43 6 50       15 $self = bless({}, $self) if !ref($self);
44              
45 6         23 return $self->parse_from_fh(path($path)->openr_utf8, @_);
46             }
47              
48              
49             sub parse_from_fh {
50 10     10 1 1937 my $self = shift;
51 10 50       21 my $fh = shift or _usage(q{$codeowners->parse_from_fh($fh)});
52 10         18 my %opts = @_;
53              
54 10 100       20 $self = bless({}, $self) if !ref($self);
55              
56 10         32 my @lines;
57              
58             my $parse_unowned;
59 10         0 my %unowned;
60 10         0 my %aliases;
61 10         0 my $current_project;
62              
63 10         191 while (my $line = <$fh>) {
64 53         202 my $lineno = $. - 1;
65 53         67 chomp $line;
66 53 100 100     374 if ($line eq '### UNOWNED (File::Codeowners)') {
    100          
    100          
    100          
    100          
67 2         3 $parse_unowned++;
68 2         4 last;
69             }
70             elsif ($line =~ /^\h*#(.*)/) {
71 15         33 my $comment = $1;
72 15         19 my $project;
73 15 100       39 if ($comment =~ /^\h*Project:\h*(.+?)\h*$/i) {
74 2   50     7 $project = $current_project = $1 || undef;
75             }
76 15 100       75 $lines[$lineno] = {
77             comment => $comment,
78             $project ? (project => $project) : (),
79             };
80             }
81             elsif ($line =~ /^\h*$/) {
82             # blank line
83             }
84             elsif ($opts{aliases} && $line =~ /^\h*\@($RE_OWNER)\h+(.+)/) {
85 1         4 my $alias = $1;
86 1         23 my @owners = $2 =~ /($RE_OWNER)/g;
87 1         4 $aliases{$alias} = \@owners;
88 1         6 $lines[$lineno] = {
89             alias => $alias,
90             owners => \@owners,
91             };
92             }
93             elsif ($line =~ /^\h*($RE_PATTERN)\h+(.+)/) {
94 20         38 my $pattern = $1;
95 20         121 my @owners = $2 =~ /($RE_OWNER)/g;
96 20 100       137 $lines[$lineno] = {
97             pattern => $pattern,
98             owners => \@owners,
99             $current_project ? (project => $current_project) : (),
100             };
101             }
102             else {
103 1         12 die "Parse error on line $.: $line\n";
104             }
105             }
106              
107 9 100       20 if ($parse_unowned) {
108 2         6 while (my $line = <$fh>) {
109 2         2 chomp $line;
110 2 50       9 if ($line =~ /# (.+)/) {
111 2         4 my $filepath = $1;
112 2         26 $unowned{$filepath}++;
113             }
114             }
115             }
116              
117 9         26 $self->{lines} = \@lines;
118 9         16 $self->{unowned} = \%unowned;
119 9         12 $self->{aliases} = \%aliases;
120              
121 9         96 return $self;
122             }
123              
124              
125             sub parse_from_array {
126 1     1 1 2 my $self = shift;
127 1 50       3 my $arr = shift or _usage(q{$codeowners->parse_from_array(\@lines)});
128              
129 1 50       12 $self = bless({}, $self) if !ref($self);
130              
131 1         4 my $str = join("\n", @$arr);
132 1         4 return $self->parse_from_string(\$str, @_);
133             }
134              
135              
136             sub parse_from_string {
137 3     3 1 4 my $self = shift;
138 3 50       6 my $str = shift or _usage(q{$codeowners->parse_from_string(\$string)});
139              
140 3 100       7 $self = bless({}, $self) if !ref($self);
141              
142 3 50       10 my $ref = ref($str) eq 'SCALAR' ? $str : \$str;
143 1 50   1   5 open(my $fh, '<:encoding(UTF-8)', $ref) or die "open failed: $!";
  1         2  
  1         4  
  3         53  
144              
145 3         716 return $self->parse_from_fh($fh, @_);
146             }
147              
148              
149             sub write_to_filepath {
150 0     0 1 0 my $self = shift;
151 0 0       0 my $path = shift or _usage(q{$codeowners->write_to_filepath($filepath)});
152              
153 0         0 path($path)->spew_utf8([map { "$_\n" } @{$self->write_to_array}]);
  0         0  
  0         0  
154             }
155              
156              
157             sub write_to_fh {
158 0     0 1 0 my $self = shift;
159 0 0       0 my $fh = shift or _usage(q{$codeowners->write_to_fh($fh)});
160 0         0 my $charset = shift;
161              
162 0         0 for my $line (@{$self->write_to_array($charset)}) {
  0         0  
163 0         0 print $fh "$line\n";
164             }
165             }
166              
167              
168             sub write_to_string {
169 0     0 1 0 my $self = shift;
170 0         0 my $charset = shift;
171              
172 0         0 my $str = join("\n", @{$self->write_to_array($charset)}) . "\n";
  0         0  
173 0         0 return \$str;
174             }
175              
176              
177             sub write_to_array {
178 2     2 1 4 my $self = shift;
179 2         2 my $charset = shift;
180              
181 2         4 my @format;
182              
183 2         2 for my $line (@{$self->_lines}) {
  2         5  
184 8 100       20 if (my $comment = $line->{comment}) {
    100          
    50          
185 4         10 push @format, "#$comment";
186             }
187             elsif (my $pattern = $line->{pattern}) {
188 3         4 my $owners = join(' ', @{$line->{owners}});
  3         16  
189 3         9 push @format, "$pattern $owners";
190             }
191             elsif (my $alias = $line->{alias}) {
192 1         2 my $owners = join(' ', @{$line->{owners}});
  1         3  
193 1         4 push @format, "\@$alias $owners";
194             }
195             else {
196 0         0 push @format, '';
197             }
198             }
199              
200 2         3 my @unowned = sort keys %{$self->_unowned};
  2         9  
201 2 100       5 if (@unowned) {
202 1 50       3 push @format, '' if $format[-1];
203 1         3 push @format, '### UNOWNED (File::Codeowners)';
204 1         2 for my $unowned (@unowned) {
205 1         2 push @format, "# $unowned";
206             }
207             }
208              
209 2 50       5 if (defined $charset) {
210 0         0 $_ = encode($charset, $_) for @format;
211             }
212 2         11 return \@format;
213             }
214              
215              
216             sub match {
217 6     6 1 11 my $self = shift;
218 6 50       12 my $filepath = shift or _usage(q{$codeowners->match($filepath)});
219 6         10 my %opts = @_;
220              
221             my $expand = $opts{expand} ? do {
222 1         3 my $aliases = $self->aliases;
223             sub {
224 2     2   3 my $owner = shift;
225 2         4 my $alias = $aliases->{$owner};
226 2 100       6 return @$alias if $alias;
227 1         12 return $owner;
228 1         4 };
229 6 100   7   21 } : sub { shift }; # noop
  7         59  
230              
231 6   50     17 my $lines = $self->{match_lines} ||= [reverse grep { ($_ || {})->{pattern} } @{$self->_lines}];
  18   100     43  
  2         3  
232              
233 6         9 for my $line (@$lines) {
234 15   66     242 my $matcher = $line->{matcher} ||= build_gitignore_matcher([$line->{pattern}]);
235             return { # deep copy
236             pattern => $line->{pattern},
237 9 50       15 owners => [map { $expand->($_) } @{$line->{owners} || []}],
  6         171  
238 15 100       377 $line->{project} ? (project => $line->{project}) : (),
    100          
239             } if $matcher->($filepath);
240             }
241              
242 0         0 return undef; ## no critic (Subroutines::ProhibitExplicitReturn)
243             }
244              
245              
246             sub owners {
247 5     5 1 312 my $self = shift;
248 5         6 my $pattern = shift;
249              
250 5 100 100     21 return $self->{owners} if !$pattern && $self->{owners};
251              
252 4         5 my %owners;
253 4         5 for my $line (@{$self->_lines}) {
  4         7  
254 44 100 100     84 next if $pattern && $line->{pattern} && $pattern ne $line->{pattern};
      100        
255 39 100       38 $owners{$_}++ for (@{$line->{owners} || []});
  39         106  
256             }
257              
258 4         21 my $owners = [sort keys %owners];
259 4 100       11 $self->{owners} = $owners if !$pattern;
260              
261 4         16 return $owners;
262             }
263              
264              
265             sub patterns {
266 3     3 1 5 my $self = shift;
267 3         4 my $owner = shift;
268              
269 3 50 66     11 return $self->{patterns} if !$owner && $self->{patterns};
270              
271 3         5 my %patterns;
272 3         3 for my $line (@{$self->_lines}) {
  3         5  
273 32 100 100     45 next if $owner && !grep { $_ eq $owner } @{$line->{owners} || []};
  9 100       21  
  14         37  
274 20         32 my $pattern = $line->{pattern};
275 20 100       45 $patterns{$pattern}++ if $pattern;
276             }
277              
278 3         16 my $patterns = [sort keys %patterns];
279 3 100       7 $self->{patterns} = $patterns if !$owner;
280              
281 3         15 return $patterns;
282             }
283              
284              
285             sub aliases {
286 3     3 1 15 my $self = shift;
287              
288 3 50       15 return $self->{aliases} if $self->{aliases};
289              
290 0         0 my %aliases;
291 0         0 for my $line (@{$self->_lines}) {
  0         0  
292 0 0       0 next if !defined $line->{alias};
293 0         0 $aliases{$line->{alias}} = [@{$line->{owners}}];
  0         0  
294             }
295              
296 0         0 return $self->{aliases} = \%aliases;
297             }
298              
299              
300             sub projects {
301 2     2 1 8 my $self = shift;
302              
303 2 50       5 return $self->{projects} if $self->{projects};
304              
305 2         2 my %projects;
306 2         3 for my $line (@{$self->_lines}) {
  2         3  
307 28         36 my $project = $line->{project};
308 28 100       42 $projects{$project}++ if $project;
309             }
310              
311 2         7 my $projects = [sort keys %projects];
312 2         3 $self->{projects} = $projects;
313              
314 2         9 return $projects;
315             }
316              
317              
318             sub update_owners {
319 2     2 1 9 my $self = shift;
320 2         3 my $pattern = shift;
321 2         2 my $owners = shift;
322 2 50 33     11 $pattern && $owners or _usage(q{$codeowners->update_owners($pattern => \@owners)});
323              
324 2 50       6 $owners = [$owners] if ref($owners) ne 'ARRAY';
325              
326 2         4 $self->_clear;
327              
328 2         2 my $count = 0;
329              
330 2         3 for my $line (@{$self->_lines}) {
  2         4  
331 4 100       8 next if !$line->{pattern};
332 2 100       5 next if $pattern ne $line->{pattern};
333 1         4 $line->{owners} = [@$owners];
334 1         2 ++$count;
335             }
336              
337 2         4 return $count;
338             }
339              
340              
341             sub update_owners_by_project {
342 1     1 1 2 my $self = shift;
343 1         2 my $project = shift;
344 1         10 my $owners = shift;
345 1 50 33     6 $project && $owners or _usage(q{$codeowners->update_owners_by_project($project => \@owners)});
346              
347 1 50       3 $owners = [$owners] if ref($owners) ne 'ARRAY';
348              
349 1         4 $self->_clear;
350              
351 1         2 my $count = 0;
352              
353 1         2 for my $line (@{$self->_lines}) {
  1         9  
354 14 100 100     28 next if !$line->{project} || !$line->{owners};
355 1 50       3 next if $project ne $line->{project};
356 1         3 $line->{owners} = [@$owners];
357 1         2 ++$count;
358             }
359              
360 1         2 return $count;
361             }
362              
363              
364             sub rename_owner {
365 0     0 1 0 my $self = shift;
366 0         0 my $old_owner = shift;
367 0         0 my $new_owner = shift;
368 0 0 0     0 $old_owner && $new_owner or _usage(q{$codeowners->rename_owner($owner => $new_owner)});
369              
370 0         0 $self->_clear;
371              
372 0         0 my $count = 0;
373              
374 0         0 for my $line (@{$self->_lines}) {
  0         0  
375 0 0       0 next if !exists $line->{owners};
376 0         0 for (my $i = 0; $i < @{$line->{owners}}; ++$i) {
  0         0  
377 0 0       0 next if $line->{owners}[$i] ne $old_owner;
378 0         0 $line->{owners}[$i] = $new_owner;
379 0         0 ++$count;
380             }
381             }
382              
383 0         0 return $count;
384             }
385              
386              
387             sub rename_project {
388 1     1 1 3 my $self = shift;
389 1         2 my $old_project = shift;
390 1         1 my $new_project = shift;
391 1 50 33     6 $old_project && $new_project or _usage(q{$codeowners->rename_project($project => $new_project)});
392              
393 1         3 $self->_clear;
394              
395 1         2 my $count = 0;
396              
397 1         2 for my $line (@{$self->_lines}) {
  1         2  
398 14 100 66     27 next if !exists $line->{project} || $old_project ne $line->{project};
399 2         5 $line->{project} = $new_project;
400 2 100       4 $line->{comment} = " Project: $new_project" if exists $line->{comment};
401 2         3 ++$count;
402             }
403              
404 1         2 return $count;
405             }
406              
407              
408             sub append {
409 1     1 1 2 my $self = shift;
410 1         2 $self->_clear;
411 1 50       2 push @{$self->_lines}, (@_ ? {@_} : undef);
  1         1  
412             }
413              
414              
415             sub prepend {
416 1     1 1 2 my $self = shift;
417 1         3 $self->_clear;
418 1 50       2 unshift @{$self->_lines}, (@_ ? {@_} : undef);
  1         2  
419             }
420              
421              
422             sub unowned {
423 4     4 1 6 my $self = shift;
424 4 50       12 [sort keys %{$self->{unowned} || {}}];
  4         31  
425             }
426              
427              
428             sub add_unowned {
429 1     1 1 2 my $self = shift;
430 1         4 $self->_unowned->{$_}++ for @_;
431             }
432              
433              
434             sub remove_unowned {
435 1     1 1 2 my $self = shift;
436 1         3 delete $self->_unowned->{$_} for @_;
437             }
438              
439              
440              
441             sub is_unowned {
442 0     0 1 0 my $self = shift;
443 0         0 my $filepath = shift;
444 0         0 $self->_unowned->{$filepath};
445             }
446              
447              
448             sub clear_unowned {
449 1     1 1 3 my $self = shift;
450 1         13 $self->{unowned} = {};
451             }
452              
453 27   50 27   146 sub _lines { shift->{lines} ||= [] }
454 5   50 5   18 sub _unowned { shift->{unowned} ||= {} }
455              
456             sub _clear {
457 6     6   7 my $self = shift;
458 6         9 delete $self->{match_lines};
459 6         8 delete $self->{owners};
460 6         8 delete $self->{patterns};
461 6         7 delete $self->{aliases};
462 6         8 delete $self->{projects};
463             }
464              
465              
466             1;
467              
468             __END__