package App::Codeowners; # ABSTRACT: A tool for managing CODEOWNERS files use v5.10.1; # defined-or use utf8; use warnings; use strict; use App::Codeowners::Formatter; use App::Codeowners::Options; use App::Codeowners::Util qw(find_codeowners_in_directory run_git git_ls_files git_toplevel); use Color::ANSI::Util 0.03 qw(ansifg); use Encode qw(encode); use File::Codeowners; use Path::Tiny; our $VERSION = '0.48'; # VERSION sub main { my $class = shift; my $self = bless {}, $class; my $opts = App::Codeowners::Options->new(@_); my $color = $opts->{color}; local $ENV{NO_COLOR} = 1 if defined $color && !$color; my $command = $opts->command; my $handler = $self->can("_command_$command") or die "Unknown command: $command\n"; $self->$handler($opts); exit 0; } sub _command_show { my $self = shift; my $opts = shift; my $toplevel = git_toplevel('.') or die "Not a git repo\n"; my $codeowners_path = find_codeowners_in_directory($toplevel) or die "No CODEOWNERS file in $toplevel\n"; my $codeowners = File::Codeowners->parse_from_filepath($codeowners_path); my ($proc, $cdup) = run_git(qw{rev-parse --show-cdup}); $proc->wait and exit 1; my $show_projects = $opts->{projects} // scalar @{$codeowners->projects}; my $formatter = App::Codeowners::Formatter->new( format => $opts->{format} || ' * %-50F %O', handle => *STDOUT, columns => [ 'File', $opts->{patterns} ? 'Pattern' : (), 'Owner', $show_projects ? 'Project' : (), ], ); my %filter_owners = map { $_ => 1 } @{$opts->{owner}}; my %filter_projects = map { $_ => 1 } @{$opts->{project}}; my %filter_patterns = map { $_ => 1 } @{$opts->{pattern}}; $proc = git_ls_files('.', $opts->args); while (my $filepath = $proc->next) { my $match = $codeowners->match(path($filepath)->relative($cdup)); if (%filter_owners) { for my $owner (@{$match->{owners}}) { goto ADD_RESULT if $filter_owners{$owner}; } next; } if (%filter_patterns) { goto ADD_RESULT if $filter_patterns{$match->{pattern} || ''}; next; } if (%filter_projects) { goto ADD_RESULT if $filter_projects{$match->{project} || ''}; next; } ADD_RESULT: $formatter->add_result([ $filepath, $opts->{patterns} ? $match->{pattern} : (), $match->{owners}, $show_projects ? $match->{project} : (), ]); } $proc->wait and exit 1; } sub _command_owners { my $self = shift; my $opts = shift; my $toplevel = git_toplevel('.') or die "Not a git repo\n"; my $codeowners_path = find_codeowners_in_directory($toplevel) or die "No CODEOWNERS file in $toplevel\n"; my $codeowners = File::Codeowners->parse_from_filepath($codeowners_path); my $results = $codeowners->owners($opts->{pattern}); my $formatter = App::Codeowners::Formatter->new( format => $opts->{format} || '%O', handle => *STDOUT, columns => [qw(Owner)], ); $formatter->add_result(map { [$_] } @$results); } sub _command_patterns { my $self = shift; my $opts = shift; my $toplevel = git_toplevel('.') or die "Not a git repo\n"; my $codeowners_path = find_codeowners_in_directory($toplevel) or die "No CODEOWNERS file in $toplevel\n"; my $codeowners = File::Codeowners->parse_from_filepath($codeowners_path); my $results = $codeowners->patterns($opts->{owner}); my $formatter = App::Codeowners::Formatter->new( format => $opts->{format} || '%T', handle => *STDOUT, columns => [qw(Pattern)], ); $formatter->add_result(map { [$_] } @$results); } sub _command_projects { my $self = shift; my $opts = shift; my $toplevel = git_toplevel('.') or die "Not a git repo\n"; my $codeowners_path = find_codeowners_in_directory($toplevel) or die "No CODEOWNERS file in $toplevel\n"; my $codeowners = File::Codeowners->parse_from_filepath($codeowners_path); my $results = $codeowners->projects; my $formatter = App::Codeowners::Formatter->new( format => $opts->{format} || '%P', handle => *STDOUT, columns => [qw(Project)], ); $formatter->add_result(map { [$_] } @$results); } sub _command_create { goto &_command_update } sub _command_update { my $self = shift; my $opts = shift; my ($filepath) = $opts->args; my $path = path($filepath || '.'); my $repopath; die "Does not exist: $path\n" if !$path->parent->exists; if ($path->is_dir) { $repopath = $path; $path = find_codeowners_in_directory($path) || $repopath->child('CODEOWNERS'); } my $is_new = !$path->is_file; my $codeowners; if ($is_new) { $codeowners = File::Codeowners->new; my $template = <<'END'; This file shows mappings between subdirs/files and the individuals and teams who own them. You can read this file yourself or use tools to query it, so you can quickly determine who to speak with or send pull requests to. Simply write a gitignore pattern followed by one or more names/emails/groups. Examples: /project_a/** @team1 *.js @harry @javascript-cabal END for my $line (split(/\n/, $template)) { $codeowners->append(comment => $line); } } else { $codeowners = File::Codeowners->parse_from_filepath($path); } if ($repopath) { # if there is a repo we can try to update the list of unowned files my ($proc, @filepaths) = git_ls_files($repopath); $proc->wait and exit 1; $codeowners->clear_unowned; $codeowners->add_unowned(grep { !$codeowners->match($_) } @filepaths); } $codeowners->write_to_filepath($path); print STDERR "Wrote $path\n"; } 1; __END__ =pod =encoding UTF-8 =head1 NAME App::Codeowners - A tool for managing CODEOWNERS files =head1 VERSION version 0.48 =head1 DESCRIPTION This is the implementation of the F command. See L for documentation. =head1 METHODS =head2 main App::Codeowners->main(@ARGV); Run the script and exit; does not return. =head1 BUGS Please report any bugs or feature requests on the bugtracker website L When submitting a bug or request, please include a test-file or a patch to an existing test-file that illustrates the bug or desired feature. =head1 AUTHOR Charles McGarvey =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2019 by Charles McGarvey. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut