File Coverage

blib/lib/File/Codeowners.pm
Criterion Covered Total %
statement 205 247 83.0
branch 87 128 67.9
condition 30 46 65.2
subroutine 30 38 78.9
pod 25 25 100.0
total 377 484 77.8


line stmt bran cond sub pod time code
1             package File::Codeowners;
2             # ABSTRACT: Read and write CODEOWNERS files
3              
4 1     1   2975 use v5.10.1; # defined-or
  1         19  
5 1     1   7 use warnings;
  1         2  
  1         47  
6 1     1   7 use strict;
  1         2  
  1         31  
7              
8 1     1   728 use Encode qw(encode);
  1         11685  
  1         108  
9 1     1   824 use Path::Tiny 0.089;
  1         17647  
  1         88  
10 1     1   12 use Scalar::Util qw(openhandle);
  1         3  
  1         114  
11 1     1   641 use Text::Gitignore qw(build_gitignore_matcher);
  1         1138  
  1         2837  
12              
13             our $VERSION = '0.53'; # 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 0     0 1 0 my $class = shift;
21 0         0 my $self = bless {}, $class;
22             }
23              
24              
25             sub parse {
26 4     4 1 10028 my $self = shift;
27 4 50       15 my $input = shift or _usage(q{$codeowners->parse($input)});
28              
29 4 50       13 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       15 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         8 return $self->parse_from_filepath($input);
34             }
35              
36              
37             sub parse_from_filepath {
38 4     4 1 1392 my $self = shift;
39 4 50       13 my $path = shift or _usage(q{$codeowners->parse_from_filepath($filepath)});
40              
41 4 50       13 $self = bless({}, $self) if !ref($self);
42              
43 4         18 return $self->parse_from_fh(path($path)->openr_utf8);
44             }
45              
46              
47             sub parse_from_fh {
48 8     8 1 2173 my $self = shift;
49 8 50       25 my $fh = shift or _usage(q{$codeowners->parse_from_fh($fh)});
50              
51 8 100       18 $self = bless({}, $self) if !ref($self);
52              
53 8         28 my @lines;
54              
55             my $parse_unowned;
56 8         0 my %unowned;
57 8         0 my $current_project;
58              
59 8         190 while (my $line = <$fh>) {
60 45         163 my $lineno = $. - 1;
61 45         65 chomp $line;
62 45 100       265 if ($line eq '### UNOWNED (File::Codeowners)') {
    100          
    100          
    100          
63 2         3 $parse_unowned++;
64 2         5 last;
65             }
66             elsif ($line =~ /^\h*#(.*)/) {
67 11         38 my $comment = $1;
68 11         15 my $project;
69 11 100       44 if ($comment =~ /^\h*Project:\h*(.+?)\h*$/i) {
70 2   50     9 $project = $current_project = $1 || undef;
71             }
72 11 100       69 $lines[$lineno] = {
73             comment => $comment,
74             $project ? (project => $project) : (),
75             };
76             }
77             elsif ($line =~ /^\h*$/) {
78             # blank line
79             }
80             elsif ($line =~ /^\h*(.+?)(?
81 17         38 my $pattern = $1;
82 17         82 my @owners = $2 =~ /( (?:\@+"[^"]*") | (?:\H+) )/gx;
83 17 100       114 $lines[$lineno] = {
84             pattern => $pattern,
85             owners => \@owners,
86             $current_project ? (project => $current_project) : (),
87             };
88             }
89             else {
90 1         15 die "Parse error on line $.: $line\n";
91             }
92             }
93              
94 7 100       20 if ($parse_unowned) {
95 2         8 while (my $line = <$fh>) {
96 2         5 chomp $line;
97 2 50       11 if ($line =~ /# (.+)/) {
98 2         6 my $filepath = $1;
99 2         30 $unowned{$filepath}++;
100             }
101             }
102             }
103              
104 7         29 $self->{lines} = \@lines;
105 7         15 $self->{unowned} = \%unowned;
106              
107 7         82 return $self;
108             }
109              
110              
111             sub parse_from_array {
112 1     1 1 4 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         5 my $str = join("\n", @$arr);
119 1         5 return $self->parse_from_string(\$str);
120             }
121              
122              
123             sub parse_from_string {
124 3     3 1 5 my $self = shift;
125 3 50       9 my $str = shift or _usage(q{$codeowners->parse_from_string(\$string)});
126              
127 3 100       22 $self = bless({}, $self) if !ref($self);
128              
129 3 50       12 my $ref = ref($str) eq 'SCALAR' ? $str : \$str;
130 1 50   1   7 open(my $fh, '<:encoding(UTF-8)', $ref) or die "open failed: $!";
  1         2  
  1         5  
  3         67  
131              
132 3         1015 return $self->parse_from_fh($fh);
133             }
134              
135              
136             sub write_to_filepath {
137 0     0 1 0 my $self = shift;
138 0 0       0 my $path = shift or _usage(q{$codeowners->write_to_filepath($filepath)});
139              
140 0         0 path($path)->spew_utf8([map { "$_\n" } @{$self->write_to_array}]);
  0         0  
  0         0  
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 1     1 1 4 my $self = shift;
166 1         2 my $charset = shift;
167              
168 1         2 my @format;
169              
170 1         2 for my $line (@{$self->_lines}) {
  1         3  
171 4 100       14 if (my $comment = $line->{comment}) {
    50          
172 2         6 push @format, "#$comment";
173             }
174             elsif (my $pattern = $line->{pattern}) {
175 2         3 my $owners = join(' ', @{$line->{owners}});
  2         6  
176 2         8 push @format, "$pattern $owners";
177             }
178             else {
179 0         0 push @format, '';
180             }
181             }
182              
183 1         2 my @unowned = sort keys %{$self->_unowned};
  1         3  
184 1 50       4 if (@unowned) {
185 1 50       4 push @format, '' if $format[-1];
186 1         2 push @format, '### UNOWNED (File::Codeowners)';
187 1         3 for my $unowned (@unowned) {
188 1         3 push @format, "# $unowned";
189             }
190             }
191              
192 1 50       4 if (defined $charset) {
193 0         0 $_ = encode($charset, $_) for @format;
194             }
195 1         8 return \@format;
196             }
197              
198              
199             sub match {
200 4     4 1 8 my $self = shift;
201 4 50       11 my $filepath = shift or _usage(q{$codeowners->match($filepath)});
202              
203 4   50     27 my $lines = $self->{match_lines} ||= [reverse grep { ($_ || {})->{pattern} } @{$self->_lines}];
  14   100     34  
  1         4  
204              
205 4         10 for my $line (@$lines) {
206 13   66     318 my $matcher = $line->{matcher} ||= build_gitignore_matcher([$line->{pattern}]);
207             return { # deep copy
208             pattern => $line->{pattern},
209 4 50       190 owners => [@{$line->{owners} || []}],
210 13 100       436 $line->{project} ? (project => $line->{project}) : (),
    100          
211             } if $matcher->($filepath);
212             }
213              
214 0         0 return undef; ## no critic (Subroutines::ProhibitExplicitReturn)
215             }
216              
217              
218             sub owners {
219 5     5 1 399 my $self = shift;
220 5         10 my $pattern = shift;
221              
222 5 100 100     26 return $self->{owners} if !$pattern && $self->{owners};
223              
224 4         8 my %owners;
225 4         6 for my $line (@{$self->_lines}) {
  4         9  
226 44 100 100     111 next if $pattern && $line->{pattern} && $pattern ne $line->{pattern};
      100        
227 39 100       44 $owners{$_}++ for (@{$line->{owners} || []});
  39         147  
228             }
229              
230 4         31 my $owners = [sort keys %owners];
231 4 100       14 $self->{owners} = $owners if !$pattern;
232              
233 4         25 return $owners;
234             }
235              
236              
237             sub patterns {
238 2     2 1 9 my $self = shift;
239 2         4 my $owner = shift;
240              
241 2 50 66     10 return $self->{patterns} if !$owner && $self->{patterns};
242              
243 2         3 my %patterns;
244 2         4 for my $line (@{$self->_lines}) {
  2         5  
245 28 100 100     58 next if $owner && !grep { $_ eq $owner } @{$line->{owners} || []};
  9 100       29  
  14         65  
246 16         25 my $pattern = $line->{pattern};
247 16 100       60 $patterns{$pattern}++ if $pattern;
248             }
249              
250 2         16 my $patterns = [sort keys %patterns];
251 2 100       7 $self->{patterns} = $patterns if !$owner;
252              
253 2         16 return $patterns;
254             }
255              
256              
257             sub projects {
258 2     2 1 12 my $self = shift;
259              
260 2 50       7 return $self->{projects} if $self->{projects};
261              
262 2         3 my %projects;
263 2         4 for my $line (@{$self->_lines}) {
  2         5  
264 28         48 my $project = $line->{project};
265 28 100       55 $projects{$project}++ if $project;
266             }
267              
268 2         8 my $projects = [sort keys %projects];
269 2         4 $self->{projects} = $projects;
270              
271 2         12 return $projects;
272             }
273              
274              
275             sub update_owners {
276 2     2 1 14 my $self = shift;
277 2         4 my $pattern = shift;
278 2         4 my $owners = shift;
279 2 50 33     12 $pattern && $owners or _usage(q{$codeowners->update_owners($pattern => \@owners)});
280              
281 2 50       8 $owners = [$owners] if ref($owners) ne 'ARRAY';
282              
283 2         5 $self->_clear;
284              
285 2         3 my $count = 0;
286              
287 2         4 for my $line (@{$self->_lines}) {
  2         5  
288 4 100       10 next if !$line->{pattern};
289 2 100       7 next if $pattern ne $line->{pattern};
290 1         3 $line->{owners} = [@$owners];
291 1         3 ++$count;
292             }
293              
294 2         5 return $count;
295             }
296              
297              
298             sub update_owners_by_project {
299 1     1 1 3 my $self = shift;
300 1         19 my $project = shift;
301 1         3 my $owners = shift;
302 1 50 33     25 $project && $owners or _usage(q{$codeowners->update_owners_by_project($project => \@owners)});
303              
304 1 50       10 $owners = [$owners] if ref($owners) ne 'ARRAY';
305              
306 1         5 $self->_clear;
307              
308 1         2 my $count = 0;
309              
310 1         2 for my $line (@{$self->_lines}) {
  1         4  
311 14 100 100     36 next if !$line->{project} || !$line->{owners};
312 1 50       4 next if $project ne $line->{project};
313 1         3 $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         3 my $new_project = shift;
348 1 50 33     7 $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         4  
355 14 100 66     37 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         4 ++$count;
359             }
360              
361 1         4 return $count;
362             }
363              
364              
365             sub append {
366 1     1 1 2 my $self = shift;
367 1         4 $self->_clear;
368 1 50       2 push @{$self->_lines}, (@_ ? {@_} : undef);
  1         2  
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         2  
376             }
377              
378              
379             sub unowned {
380 4     4 1 9 my $self = shift;
381 4 50       7 [sort keys %{$self->{unowned} || {}}];
  4         47  
382             }
383              
384              
385             sub add_unowned {
386 1     1 1 3 my $self = shift;
387 1         6 $self->_unowned->{$_}++ for @_;
388             }
389              
390              
391             sub remove_unowned {
392 1     1 1 3 my $self = shift;
393 1         6 delete $self->_unowned->{$_} for @_;
394             }
395              
396              
397              
398             sub is_unowned {
399 0     0 1 0 my $self = shift;
400 0         0 my $filepath = shift;
401 0         0 $self->_unowned->{$filepath};
402             }
403              
404              
405             sub clear_unowned {
406 1     1 1 3 my $self = shift;
407 1         3 $self->{unowned} = {};
408             }
409              
410 24   50 24   205 sub _lines { shift->{lines} ||= [] }
411 4   50 4   22 sub _unowned { shift->{unowned} ||= {} }
412              
413             sub _clear {
414 6     6   13 my $self = shift;
415 6         8 delete $self->{match_lines};
416 6         10 delete $self->{owners};
417 6         8 delete $self->{patterns};
418 6         12 delete $self->{projects};
419             }
420              
421             1;
422              
423             __END__