1 package App
::Codeowners
;
2 # ABSTRACT: A tool for managing CODEOWNERS files
4 use v5
.10
.1; # defined-or
9 use App
::Codeowners
::Options
;
10 use App
::Codeowners
::Util
qw(find_codeowners_in_directory run_git git_ls_files git_toplevel stringf);
11 use Color
::ANSI
::Util
qw(ansifg ansi_reset);
12 use Encode
qw(encode);
16 our $VERSION = '0.41'; # VERSION
21 my $self = bless {}, $class;
23 my $opts = App
::Codeowners
::Options-
>new(@_);
25 my $color = $opts->{color
};
26 local $ENV{NO_COLOR
} = 1 if defined $color && !$color;
28 my $command = $opts->command;
29 my $handler = $self->can("_command_$command")
30 or die "Unknown command: $command\n";
31 $self->$handler($opts);
40 my $toplevel = git_toplevel
('.') or die "Not a git repo\n";
42 my $codeowners_path = find_codeowners_in_directory
($toplevel)
43 or die "No CODEOWNERS file in $toplevel\n";
44 my $codeowners = File
::Codeowners-
>parse_from_filepath($codeowners_path);
46 my ($cdup) = run_git
(qw{rev-parse --show-cdup});
50 my $filepaths = git_ls_files
('.', $opts->args) or die "Cannot list files\n";
51 for my $filepath (@$filepaths) {
52 my $match = $codeowners->match(path
($filepath)->relative($cdup));
56 $opts->{project
} ? $match->{project
} : (),
61 format
=> $opts->{format
} || ' * %-50F %O',
63 headers
=> [qw(File Owner), $opts->{project
} ? 'Project' : ()],
72 my $toplevel = git_toplevel
('.') or die "Not a git repo\n";
74 my $codeowners_path = find_codeowners_in_directory
($toplevel)
75 or die "No CODEOWNERS file in $toplevel\n";
76 my $codeowners = File
::Codeowners-
>parse_from_filepath($codeowners_path);
78 my $results = $codeowners->owners($opts->{pattern
});
81 format
=> $opts->{format
} || '%O',
83 headers
=> [qw(Owner)],
84 rows
=> [map { [$_] } @$results],
88 sub _command_patterns
{
92 my $toplevel = git_toplevel
('.') or die "Not a git repo\n";
94 my $codeowners_path = find_codeowners_in_directory
($toplevel)
95 or die "No CODEOWNERS file in $toplevel\n";
96 my $codeowners = File
::Codeowners-
>parse_from_filepath($codeowners_path);
98 my $results = $codeowners->patterns($opts->{owner
});
101 format
=> $opts->{format
} || '%T',
103 headers
=> [qw(Pattern)],
104 rows
=> [map { [$_] } @$results],
108 sub _command_create
{ goto &_command_update
}
109 sub _command_update
{
113 my ($filepath) = $opts->args;
115 my $path = path
($filepath || '.');
118 die "Does not exist: $path\n" if !$path->parent->exists;
122 $path = find_codeowners_in_directory
($path) || $repopath->child('CODEOWNERS');
125 my $is_new = !$path->is_file;
129 $codeowners = File
::Codeowners-
>new;
130 my $template = <<'END';
131 This file shows mappings between subdirs/files and the individuals and
132 teams who own them. You can read this file yourself or use tools to query it,
133 so you can quickly determine who to speak with or send pull requests to. ❤️
135 Simply write a gitignore pattern followed by one or more names/emails/groups.
138 *.js @harry @javascript-cabal
140 for my $line (split(/\n/, $template)) {
141 $codeowners->append(comment
=> $line);
145 $codeowners = File
::Codeowners-
>parse_from_filepath($path);
149 # if there is a repo we can try to update the list of unowned files
150 my $git_files = git_ls_files
($repopath);
152 $codeowners->clear_unowned;
153 $codeowners->add_unowned(grep { !$codeowners->match($_) } @$git_files);
157 $codeowners->write_to_filepath($path);
158 print STDERR
"Wrote $path\n";
164 my $format = $args{format
} || 'table';
165 my $fh = $args{out
} || *STDOUT
;
166 my $headers = $args{headers
} || [];
167 my $rows = $args{rows
} || [];
169 if ($format eq 'table') {
170 eval { require Text
::Table
::Any
} or die "Missing dependency: Text::Table::Any\n";
172 my $table = Text
::Table
::Any
::table
(
174 rows
=> [$headers, map { [map { _stringify
($_) } @$_] } @$rows],
175 backend
=> $ENV{PERL_TEXT_TABLE
},
177 print { $fh } encode
('UTF-8', $table);
179 elsif ($format =~ /^json(:pretty)?$/) {
181 eval { require JSON
::MaybeXS
} or die "Missing dependency: JSON::MaybeXS\n";
183 my $json = JSON
::MaybeXS-
>new(canonical
=> 1, utf8
=> 1, pretty
=> $pretty);
184 my $data = _combine_headers_rows
($headers, $rows);
185 print { $fh } $json->encode($data);
187 elsif ($format =~ /^([ct])sv$/) {
188 my $sep = $1 eq 'c' ? ',' : "\t";
189 eval { require Text
::CSV
} or die "Missing dependency: Text::CSV\n";
191 my $csv = Text
::CSV-
>new({binary
=> 1, eol
=> $/, sep
=> $sep});
192 $csv->print($fh, $headers);
193 $csv->print($fh, [map { encode
('UTF-8', _stringify
($_)) } @$_]) for @$rows;
195 elsif ($format =~ /^ya?ml$/) {
196 eval { require YAML
} or die "Missing dependency: YAML\n";
198 my $data = _combine_headers_rows
($headers, $rows);
199 print { $fh } encode
('UTF-8', YAML
::Dump
($data));
202 my $data = _combine_headers_rows
($headers, $rows);
204 # https://sashat.me/2017/01/11/list-of-20-simple-distinct-colors/
205 my @contrasting_colors = qw(
206 e6194b 3cb44b ffe119 4363d8 f58231
207 911eb4 42d4f4 f032e6 bfef45 fabebe
208 469990 e6beff 9a6324 fffac8 800000
209 aaffc3 808000 ffd8b1 000075 a9a9a9
212 # assign a color to each owner, on demand
215 my $owner_color = sub {
216 my $owner = shift or return;
217 $owner_colors{$owner} ||= do {
218 $num = ($num + 1) % scalar @contrasting_colors;
219 $contrasting_colors[$num];
224 quote
=> sub { local $_ = $_[0]; s/"/\"/s; "\"$_\"" },
227 my $create_filterer = sub {
228 my $value = shift || '';
229 my $color = shift || '';
230 my $gencolor = ref($color) eq 'CODE' ? $color : sub { $color };
233 my ($filters, $color) = _expand_filter_args
($arg);
234 if (ref($value) eq 'ARRAY') {
235 $value = join(',', map { _colored
($_, $color // $gencolor->($_)) } @$value);
238 $value = _colored
($value, $color // $gencolor->($value));
240 for my $key (@$filters) {
241 if (my $filter = $filter{$key}) {
242 $value = $filter->($value);
245 warn "Unknown filter: $key\n"
252 for my $row (@$data) {
254 F
=> $create_filterer->($row->{File
}, undef),
255 O
=> $create_filterer->($row->{Owner
}, $owner_color),
256 P
=> $create_filterer->($row->{Project
}, undef),
257 T
=> $create_filterer->($row->{Pattern
}, undef),
260 my $text = stringf
($format, %info);
261 print { $fh } encode
('UTF-8', $text), "\n";
266 sub _expand_filter_args
{
267 my $arg = shift || '';
269 my @filters = split(/,/, $arg);
272 for (my $i = 0; $i < @filters; ++$i) {
273 my $filter = $filters[$i] or next;
274 if ($filter =~ /^(?:nocolor|color:([0-9a-fA-F]{3,6}))$/) {
275 $color_override = $1 || '';
276 splice(@filters, $i, 1);
281 return (\
@filters, $color_override);
286 my $rgb = shift or return $text;
288 # ansifg honors NO_COLOR already, but ansi_reset does not.
289 return $text if $ENV{NO_COLOR
};
291 $rgb =~ s/^(.)(.)(.)$/$1$1$2$2$3$3/;
292 if ($rgb !~ m/^[0-9a-fA-F]{6}$/) {
293 warn "Color value must be in 'ffffff' or 'fff' form.\n";
297 my ($begin, $end) = (ansifg
($rgb), ansi_reset
);
298 return "${begin}${text}${end}";
301 sub _combine_headers_rows
{
307 for my $row (@$rows) {
308 push @new_rows, (my $new_row = {});
309 for (my $i = 0; $i < @$headers; ++$i) {
310 $new_row->{$headers->[$i]} = $row->[$i];
319 return ref($item) eq 'ARRAY' ? join(',', @$item) : $item;
332 App::Codeowners - A tool for managing CODEOWNERS files
342 App::Codeowners->main(@ARGV);
344 Run the script and exit; does not return.
348 Please report any bugs or feature requests on the bugtracker website
349 L<https://github.com/chazmcgarvey/git-codeowners/issues>
351 When submitting a bug or request, please include a test-file or a
352 patch to an existing test-file that illustrates the bug or desired
357 Charles McGarvey <chazmcgarvey@brokenzipper.com>
359 =head1 COPYRIGHT AND LICENSE
361 This software is copyright (c) 2019 by Charles McGarvey.
363 This is free software; you can redistribute it and/or modify it under
364 the same terms as the Perl 5 programming language system itself.