File Coverage

blib/lib/File/Codeowners.pm
Criterion Covered Total %
statement 214 247 86.6
branch 89 128 69.5
condition 31 46 67.3
subroutine 32 38 84.2
pod 24 25 96.0
total 390 484 80.5


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