]> Dogcows Code - chaz/git-codeowners/blob - lib/File/Codeowners.pm
65b0f54e28932990aa0f707f684138cbab424142
[chaz/git-codeowners] / lib / File / Codeowners.pm
1 package File::Codeowners;
2 # ABSTRACT: Read and write CODEOWNERS files
3
4 use v5.10.1; # defined-or
5 use warnings;
6 use strict;
7
8 use Encode qw(encode);
9 use Path::Tiny;
10 use Scalar::Util qw(openhandle);
11 use Text::Gitignore qw(build_gitignore_matcher);
12
13 our $VERSION = '0.44'; # VERSION
14
15 sub _croak { require Carp; Carp::croak(@_); }
16 sub _usage { _croak("Usage: @_\n") }
17
18
19 sub new {
20 my $class = shift;
21 my $self = bless {}, $class;
22 }
23
24
25 sub parse {
26 my $self = shift;
27 my $input = shift or _usage(q{$codeowners->parse($input)});
28
29 return $self->parse_from_array($input, @_) if @_;
30 return $self->parse_from_array($input) if ref($input) eq 'ARRAY';
31 return $self->parse_from_string($input) if ref($input) eq 'SCALAR';
32 return $self->parse_from_fh($input) if openhandle($input);
33 return $self->parse_from_filepath($input);
34 }
35
36
37 sub parse_from_filepath {
38 my $self = shift;
39 my $path = shift or _usage(q{$codeowners->parse_from_filepath($filepath)});
40
41 $self = bless({}, $self) if !ref($self);
42
43 return $self->parse_from_fh(path($path)->openr_utf8);
44 }
45
46
47 sub parse_from_fh {
48 my $self = shift;
49 my $fh = shift or _usage(q{$codeowners->parse_from_fh($fh)});
50
51 $self = bless({}, $self) if !ref($self);
52
53 my @lines;
54
55 my $parse_unowned;
56 my %unowned;
57 my $current_project;
58
59 while (my $line = <$fh>) {
60 my $lineno = $. - 1;
61 chomp $line;
62 if ($line eq '### UNOWNED (File::Codeowners)') {
63 $parse_unowned++;
64 last;
65 }
66 elsif ($line =~ /^\h*#(.*)/) {
67 my $comment = $1;
68 if ($comment =~ /^\h*Project:\h*(.+?)\h*$/i) {
69 $current_project = $1 || undef;
70 }
71 $lines[$lineno] = {
72 comment => $comment,
73 };
74 }
75 elsif ($line =~ /^\h*$/) {
76 # blank line
77 }
78 elsif ($line =~ /^\h*(.+?)(?<!\\)\h+(.+)/) {
79 my $pattern = $1;
80 my @owners = $2 =~ /( (?:\@+"[^"]*") | (?:\H+) )/gx;
81 $lines[$lineno] = {
82 pattern => $pattern,
83 owners => \@owners,
84 $current_project ? (project => $current_project) : (),
85 };
86 }
87 else {
88 die "Parse error on line $.: $line\n";
89 }
90 }
91
92 if ($parse_unowned) {
93 while (my $line = <$fh>) {
94 chomp $line;
95 if ($line =~ /# (.+)/) {
96 my $filepath = $1;
97 $unowned{$filepath}++;
98 }
99 }
100 }
101
102 $self->{lines} = \@lines;
103 $self->{unowned} = \%unowned;
104
105 return $self;
106 }
107
108
109 sub parse_from_array {
110 my $self = shift;
111 my $arr = shift or _usage(q{$codeowners->parse_from_array(\@lines)});
112
113 $self = bless({}, $self) if !ref($self);
114
115 $arr = [$arr, @_] if @_;
116 my $str = join("\n", @$arr);
117 return $self->parse_from_string(\$str);
118 }
119
120
121 sub parse_from_string {
122 my $self = shift;
123 my $str = shift or _usage(q{$codeowners->parse_from_string(\$string)});
124
125 $self = bless({}, $self) if !ref($self);
126
127 my $ref = ref($str) eq 'SCALAR' ? $str : \$str;
128 open(my $fh, '<:encoding(UTF-8)', $ref) or die "open failed: $!";
129
130 return $self->parse_from_fh($fh);
131 }
132
133
134 sub write_to_filepath {
135 my $self = shift;
136 my $path = shift or _usage(q{$codeowners->write_to_filepath($filepath)});
137
138 path($path)->spew_utf8([map { "$_\n" } @{$self->write_to_array('')}]);
139 }
140
141
142 sub write_to_fh {
143 my $self = shift;
144 my $fh = shift or _usage(q{$codeowners->write_to_fh($fh)});
145
146 for my $line (@{$self->write_to_array}) {
147 print $fh "$line\n";
148 }
149 }
150
151
152 sub write_to_string {
153 my $self = shift;
154
155 my $str = join("\n", @{$self->write_to_array}) . "\n";
156 return \$str;
157 }
158
159
160 sub write_to_array {
161 my $self = shift;
162 my $charset = shift // 'UTF-8';
163
164 my @format;
165
166 for my $line (@{$self->_lines}) {
167 if (my $comment = $line->{comment}) {
168 push @format, "#$comment";
169 }
170 elsif (my $pattern = $line->{pattern}) {
171 my $owners = join(' ', @{$line->{owners}});
172 push @format, "$pattern $owners";
173 }
174 else {
175 push @format, '';
176 }
177 }
178
179 my @unowned = sort keys %{$self->_unowned};
180 if (@unowned) {
181 push @format, '' if $format[-1];
182 push @format, '### UNOWNED (File::Codeowners)';
183 for my $unowned (@unowned) {
184 push @format, "# $unowned";
185 }
186 }
187
188 if ($charset) {
189 $_ = encode($charset, $_) for @format;
190 }
191 return \@format;
192 }
193
194
195 sub match {
196 my $self = shift;
197 my $filepath = shift or _usage(q{$codeowners->match($filepath)});
198
199 my $lines = $self->{match_lines} ||= [reverse grep { ($_ || {})->{pattern} } @{$self->_lines}];
200
201 for my $line (@$lines) {
202 my $matcher = $line->{matcher} ||= build_gitignore_matcher([$line->{pattern}]);
203 return { # deep copy
204 pattern => $line->{pattern},
205 owners => [@{$line->{owners} || []}],
206 $line->{project} ? (project => $line->{project}) : (),
207 } if $matcher->($filepath);
208 }
209
210 return undef; ## no critic (Subroutines::ProhibitExplicitReturn)
211 }
212
213
214 sub owners {
215 my $self = shift;
216 my $pattern = shift;
217
218 return $self->{owners} if !$pattern && $self->{owners};
219
220 my %owners;
221 for my $line (@{$self->_lines}) {
222 next if $pattern && $line->{pattern} && $pattern ne $line->{pattern};
223 $owners{$_}++ for (@{$line->{owners} || []});
224 }
225
226 my $owners = [sort keys %owners];
227 $self->{owners} = $owners if !$pattern;
228
229 return $owners;
230 }
231
232
233 sub patterns {
234 my $self = shift;
235 my $owner = shift;
236
237 return $self->{patterns} if !$owner && $self->{patterns};
238
239 my %patterns;
240 for my $line (@{$self->_lines}) {
241 next if $owner && !grep { $_ eq $owner } @{$line->{owners} || []};
242 my $pattern = $line->{pattern};
243 $patterns{$pattern}++ if $pattern;
244 }
245
246 my $patterns = [sort keys %patterns];
247 $self->{patterns} = $patterns if !$owner;
248
249 return $patterns;
250 }
251
252
253 sub projects {
254 my $self = shift;
255
256 return $self->{projects} if $self->{projects};
257
258 my %projects;
259 for my $line (@{$self->_lines}) {
260 my $project = $line->{project};
261 $projects{$project}++ if $project;
262 }
263
264 my $projects = [sort keys %projects];
265 $self->{projects} = $projects;
266
267 return $projects;
268 }
269
270
271 sub update_owners {
272 my $self = shift;
273 my $pattern = shift;
274 my $owners = shift;
275 $pattern && $owners or _usage(q{$codeowners->update_owners($pattern => \@owners)});
276
277 $owners = [$owners] if ref($owners) ne 'ARRAY';
278
279 $self->_clear;
280
281 for my $line (@{$self->_lines}) {
282 next if !$line->{pattern};
283 next if $pattern ne $line->{pattern};
284 $line->{owners} = [@$owners];
285 }
286 }
287
288
289 sub append {
290 my $self = shift;
291 $self->_clear;
292 push @{$self->_lines}, (@_ ? {@_} : undef);
293 }
294
295
296 sub prepend {
297 my $self = shift;
298 $self->_clear;
299 unshift @{$self->_lines}, (@_ ? {@_} : undef);
300 }
301
302
303 sub unowned {
304 my $self = shift;
305 [sort keys %{$self->{unowned} || {}}];
306 }
307
308
309 sub add_unowned {
310 my $self = shift;
311 $self->_unowned->{$_}++ for @_;
312 }
313
314
315 sub remove_unowned {
316 my $self = shift;
317 delete $self->_unowned->{$_} for @_;
318 }
319
320 sub is_unowned {
321 my $self = shift;
322 my $filepath = shift;
323 $self->_unowned->{$filepath};
324 }
325
326
327 sub clear_unowned {
328 my $self = shift;
329 $self->{unowned} = {};
330 }
331
332 sub _lines { shift->{lines} ||= [] }
333 sub _unowned { shift->{unowned} ||= {} }
334
335 sub _clear {
336 my $self = shift;
337 delete $self->{match_lines};
338 delete $self->{owners};
339 delete $self->{patterns};
340 delete $self->{projects};
341 }
342
343 1;
344
345 __END__
346
347 =pod
348
349 =encoding UTF-8
350
351 =head1 NAME
352
353 File::Codeowners - Read and write CODEOWNERS files
354
355 =head1 VERSION
356
357 version 0.44
358
359 =head1 METHODS
360
361 =head2 new
362
363 $codeowners = File::Codeowners->new;
364
365 Construct a new L<File::Codeowners>.
366
367 =head2 parse
368
369 $codeowners = File::Codeowners->parse('path/to/CODEOWNERS');
370 $codeowners = File::Codeowners->parse($filehandle);
371 $codeowners = File::Codeowners->parse(\@lines);
372 $codeowners = File::Codeowners->parse(\$string);
373
374 Parse a F<CODEOWNERS> file.
375
376 This is a shortcut for the C<parse_from_*> methods.
377
378 =head2 parse_from_filepath
379
380 $codeowners = File::Codeowners->parse_from_filepath('path/to/CODEOWNERS');
381
382 Parse a F<CODEOWNERS> file from the filesystem.
383
384 =head2 parse_from_fh
385
386 $codeowners = File::Codeowners->parse_from_fh($filehandle);
387
388 Parse a F<CODEOWNERS> file from an open filehandle.
389
390 =head2 parse_from_array
391
392 $codeowners = File::Codeowners->parse_from_array(\@lines);
393
394 Parse a F<CODEOWNERS> file stored as lines in an array.
395
396 =head2 parse_from_string
397
398 $codeowners = File::Codeowners->parse_from_string(\$string);
399 $codeowners = File::Codeowners->parse_from_string($string);
400
401 Parse a F<CODEOWNERS> file stored as a string. String should be UTF-8 encoded.
402
403 =head2 write_to_filepath
404
405 $codeowners->write_to_filepath($filepath);
406
407 Write the contents of the file to the filesystem atomically.
408
409 =head2 write_to_fh
410
411 $codeowners->write_to_fh($fh);
412
413 Format the file contents and write to a filehandle.
414
415 =head2 write_to_string
416
417 $scalarref = $codeowners->write_to_string;
418
419 Format the file contents and return a reference to a formatted string.
420
421 =head2 write_to_array
422
423 $lines = $codeowners->write_to_array;
424
425 Format the file contents as an arrayref of lines.
426
427 =head2 match
428
429 $owners = $codeowners->match($filepath);
430
431 Match the given filepath against the available patterns and return just the
432 owners for the matching pattern. Patterns are checked in the reverse order
433 they were defined in the file.
434
435 Returns C<undef> if no patterns match.
436
437 =head2 owners
438
439 $owners = $codeowners->owners; # get all defined owners
440 $owners = $codeowners->owners($pattern);
441
442 Get an arrayref of owners defined in the file. If a pattern argument is given,
443 only owners for the given pattern are returned (or empty arrayref if the
444 pattern does not exist). If no argument is given, simply returns all owners
445 defined in the file.
446
447 =head2 patterns
448
449 $patterns = $codeowners->patterns;
450 $patterns = $codeowners->patterns($owner);
451
452 Get an arrayref of all patterns defined.
453
454 =head2 projects
455
456 $projects = $codeowners->projects;
457
458 Get an arrayref of all projects defined.
459
460 =head2 update_owners
461
462 $codeowners->update_owners($pattern => \@new_owners);
463
464 Set a new set of owners for a given pattern. If for some reason the file has
465 multiple such patterns, they will all be updated.
466
467 Nothing happens if the file does not already have at least one such pattern.
468
469 =head2 append
470
471 $codeowners->append(comment => $str);
472 $codeowners->append(pattern => $pattern, owners => \@owners);
473 $codeowners->append(); # blank line
474
475 Append a new line.
476
477 =head2 prepend
478
479 $codeowners->prepend(comment => $str);
480 $codeowners->prepend(pattern => $pattern, owners => \@owners);
481 $codeowners->prepend(); # blank line
482
483 Prepend a new line.
484
485 =head2 unowned
486
487 $filepaths = $codeowners->unowned;
488
489 Get the list of filepaths in the "unowned" section.
490
491 This parser supports an "extension" to the F<CODEOWNERS> file format which
492 lists unowned files at the end of the file. This list can be useful to have in
493 order to figure out what files we know are unowned versus what files we don't
494 know are unowned.
495
496 =head2 add_unowned
497
498 $codeowners->add_unowned($filepath, ...);
499
500 Add one or more filepaths to the "unowned" list.
501
502 This method does not check to make sure the filepath(s) actually do not match
503 any patterns in the file, so you might want to call L</match> first.
504
505 See L</unowned> for an explanation.
506
507 =head2 remove_unowned
508
509 $codeowners->remove_unowned($filepath, ...);
510
511 Remove one or more filepaths from the "unowned" list.
512
513 Silently ignores filepaths that are already not listed.
514
515 See L</unowned> for an explanation.
516
517 =head2 clear_unowned
518
519 $codeowners->clear_unowned;
520
521 Remove all filepaths from the "unowned" list.
522
523 See L</unowned> for an explanation.
524
525 =head1 BUGS
526
527 Please report any bugs or feature requests on the bugtracker website
528 L<https://github.com/chazmcgarvey/git-codeowners/issues>
529
530 When submitting a bug or request, please include a test-file or a
531 patch to an existing test-file that illustrates the bug or desired
532 feature.
533
534 =head1 AUTHOR
535
536 Charles McGarvey <chazmcgarvey@brokenzipper.com>
537
538 =head1 COPYRIGHT AND LICENSE
539
540 This software is copyright (c) 2019 by Charles McGarvey.
541
542 This is free software; you can redistribute it and/or modify it under
543 the same terms as the Perl 5 programming language system itself.
544
545 =cut
This page took 0.064737 seconds and 3 git commands to generate.