]> Dogcows Code - chaz/git-codeowners/blob - lib/File/Codeowners.pm
f987561554bfdbe464169a490c2964ec1aa759ec
[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.41'; # 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 update_owners {
254 my $self = shift;
255 my $pattern = shift;
256 my $owners = shift;
257 $pattern && $owners or _usage(q{$codeowners->update_owners($pattern => \@owners)});
258
259 $owners = [$owners] if ref($owners) ne 'ARRAY';
260
261 $self->_clear;
262
263 for my $line (@{$self->_lines}) {
264 next if !$line->{pattern};
265 next if $pattern ne $line->{pattern};
266 $line->{owners} = [@$owners];
267 }
268 }
269
270
271 sub append {
272 my $self = shift;
273 $self->_clear;
274 push @{$self->_lines}, (@_ ? {@_} : undef);
275 }
276
277
278 sub prepend {
279 my $self = shift;
280 $self->_clear;
281 unshift @{$self->_lines}, (@_ ? {@_} : undef);
282 }
283
284
285 sub unowned {
286 my $self = shift;
287 [sort keys %{$self->{unowned} || {}}];
288 }
289
290
291 sub add_unowned {
292 my $self = shift;
293 $self->_unowned->{$_}++ for @_;
294 }
295
296
297 sub remove_unowned {
298 my $self = shift;
299 delete $self->_unowned->{$_} for @_;
300 }
301
302 sub is_unowned {
303 my $self = shift;
304 my $filepath = shift;
305 $self->_unowned->{$filepath};
306 }
307
308
309 sub clear_unowned {
310 my $self = shift;
311 $self->{unowned} = {};
312 }
313
314 sub _lines { shift->{lines} ||= [] }
315 sub _unowned { shift->{unowned} ||= {} }
316
317 sub _clear {
318 my $self = shift;
319 delete $self->{match_lines};
320 delete $self->{owners};
321 delete $self->{patterns};
322 }
323
324 1;
325
326 __END__
327
328 =pod
329
330 =encoding UTF-8
331
332 =head1 NAME
333
334 File::Codeowners - Read and write CODEOWNERS files
335
336 =head1 VERSION
337
338 version 0.41
339
340 =head1 METHODS
341
342 =head2 new
343
344 $codeowners = File::Codeowners->new;
345
346 Construct a new L<File::Codeowners>.
347
348 =head2 parse
349
350 $codeowners = File::Codeowners->parse('path/to/CODEOWNERS');
351 $codeowners = File::Codeowners->parse($filehandle);
352 $codeowners = File::Codeowners->parse(\@lines);
353 $codeowners = File::Codeowners->parse(\$string);
354
355 Parse a F<CODEOWNERS> file.
356
357 This is a shortcut for the C<parse_from_*> methods.
358
359 =head2 parse_from_filepath
360
361 $codeowners = File::Codeowners->parse_from_filepath('path/to/CODEOWNERS');
362
363 Parse a F<CODEOWNERS> file from the filesystem.
364
365 =head2 parse_from_fh
366
367 $codeowners = File::Codeowners->parse_from_fh($filehandle);
368
369 Parse a F<CODEOWNERS> file from an open filehandle.
370
371 =head2 parse_from_array
372
373 $codeowners = File::Codeowners->parse_from_array(\@lines);
374
375 Parse a F<CODEOWNERS> file stored as lines in an array.
376
377 =head2 parse_from_string
378
379 $codeowners = File::Codeowners->parse_from_string(\$string);
380 $codeowners = File::Codeowners->parse_from_string($string);
381
382 Parse a F<CODEOWNERS> file stored as a string. String should be UTF-8 encoded.
383
384 =head2 write_to_filepath
385
386 $codeowners->write_to_filepath($filepath);
387
388 Write the contents of the file to the filesystem atomically.
389
390 =head2 write_to_fh
391
392 $codeowners->write_to_fh($fh);
393
394 Format the file contents and write to a filehandle.
395
396 =head2 write_to_string
397
398 $scalarref = $codeowners->write_to_string;
399
400 Format the file contents and return a reference to a formatted string.
401
402 =head2 write_to_array
403
404 $lines = $codeowners->write_to_array;
405
406 Format the file contents as an arrayref of lines.
407
408 =head2 match
409
410 $owners = $codeowners->match($filepath);
411
412 Match the given filepath against the available patterns and return just the
413 owners for the matching pattern. Patterns are checked in the reverse order
414 they were defined in the file.
415
416 Returns C<undef> if no patterns match.
417
418 =head2 owners
419
420 $owners = $codeowners->owners; # get all defined owners
421 $owners = $codeowners->owners($pattern);
422
423 Get an arrayref of owners defined in the file. If a pattern argument is given,
424 only owners for the given pattern are returned (or empty arrayref if the
425 pattern does not exist). If no argument is given, simply returns all owners
426 defined in the file.
427
428 =head2 patterns
429
430 $patterns = $codeowners->patterns;
431 $patterns = $codeowners->patterns($owner);
432
433 Get an arrayref of all patterns defined.
434
435 =head2 update_owners
436
437 $codeowners->update_owners($pattern => \@new_owners);
438
439 Set a new set of owners for a given pattern. If for some reason the file has
440 multiple such patterns, they will all be updated.
441
442 Nothing happens if the file does not already have at least one such pattern.
443
444 =head2 append
445
446 $codeowners->append(comment => $str);
447 $codeowners->append(pattern => $pattern, owners => \@owners);
448 $codeowners->append(); # blank line
449
450 Append a new line.
451
452 =head2 prepend
453
454 $codeowners->prepend(comment => $str);
455 $codeowners->prepend(pattern => $pattern, owners => \@owners);
456 $codeowners->prepend(); # blank line
457
458 Prepend a new line.
459
460 =head2 unowned
461
462 $filepaths = $codeowners->unowned;
463
464 Get the list of filepaths in the "unowned" section.
465
466 This parser supports an "extension" to the F<CODEOWNERS> file format which
467 lists unowned files at the end of the file. This list can be useful to have in
468 order to figure out what files we know are unowned versus what files we don't
469 know are unowned.
470
471 =head2 add_unowned
472
473 $codeowners->add_unowned($filepath, ...);
474
475 Add one or more filepaths to the "unowned" list.
476
477 This method does not check to make sure the filepath(s) actually do not match
478 any patterns in the file, so you might want to call L</match> first.
479
480 See L</unowned> for an explanation.
481
482 =head2 remove_unowned
483
484 $codeowners->remove_unowned($filepath, ...);
485
486 Remove one or more filepaths from the "unowned" list.
487
488 Silently ignores filepaths that are already not listed.
489
490 See L</unowned> for an explanation.
491
492 =head2 clear_unowned
493
494 $codeowners->clear_unowned;
495
496 Remove all filepaths from the "unowned" list.
497
498 See L</unowned> for an explanation.
499
500 =head1 BUGS
501
502 Please report any bugs or feature requests on the bugtracker website
503 L<https://github.com/chazmcgarvey/git-codeowners/issues>
504
505 When submitting a bug or request, please include a test-file or a
506 patch to an existing test-file that illustrates the bug or desired
507 feature.
508
509 =head1 AUTHOR
510
511 Charles McGarvey <chazmcgarvey@brokenzipper.com>
512
513 =head1 COPYRIGHT AND LICENSE
514
515 This software is copyright (c) 2019 by Charles McGarvey.
516
517 This is free software; you can redistribute it and/or modify it under
518 the same terms as the Perl 5 programming language system itself.
519
520 =cut
This page took 0.064893 seconds and 3 git commands to generate.