From: Charles McGarvey Date: Wed, 13 Nov 2019 04:53:15 +0000 (-0700) Subject: Version 0.42 X-Git-Url: https://git.dogcows.com/gitweb?a=commitdiff_plain;h=3a345750fddc777385eacb334ea011f9327b774e;p=chaz%2Fgit-codeowners Version 0.42 --- diff --git a/Changes b/Changes index 91ff0ab..9b62078 100644 --- a/Changes +++ b/Changes @@ -1,4 +1,10 @@ Revision history for App-Codeowners. +0.42 2019-11-12 21:52:12-07:00 MST7MDT + * Add "projects" command to list defined projects. + * Add flags to filter matches with the "show" command. + * Remove unused Text::Table suggested dependency. + * Fix tests to skip if not git 1.8.5+ (thanks CPAN testers). + 0.41 2019-11-09 17:45:16-07:00 MST7MDT - * First public release + * First public release. diff --git a/MANIFEST b/MANIFEST index f072e92..8d701ef 100644 --- a/MANIFEST +++ b/MANIFEST @@ -9,6 +9,13 @@ README bin/git-codeowners eg/test.t lib/App/Codeowners.pm +lib/App/Codeowners/Formatter.pm +lib/App/Codeowners/Formatter/CSV.pm +lib/App/Codeowners/Formatter/JSON.pm +lib/App/Codeowners/Formatter/String.pm +lib/App/Codeowners/Formatter/TSV.pm +lib/App/Codeowners/Formatter/Table.pm +lib/App/Codeowners/Formatter/YAML.pm lib/App/Codeowners/Options.pm lib/App/Codeowners/Util.pm lib/File/Codeowners.pm diff --git a/META.json b/META.json index 773b23b..cf36b2d 100644 --- a/META.json +++ b/META.json @@ -70,17 +70,18 @@ }, "requires" : { "Carp" : "0", - "Color::ANSI::Util" : "0", + "Color::ANSI::Util" : "0.03", "Encode" : "0", "Exporter" : "0", "Getopt::Long" : "2.39", "IPC::Open2" : "0", + "Module::Load" : "0", "Path::Tiny" : "0", "Pod::Usage" : "0", "Scalar::Util" : "0", "Test::Builder" : "0", "Text::Gitignore" : "0", - "Text::Table::Any" : "0", + "parent" : "0", "perl" : "v5.10.1", "strict" : "0", "utf8" : "0", @@ -89,7 +90,7 @@ "suggests" : { "JSON::MaybeXS" : "0", "Text::CSV" : "0", - "Text::Table" : "0", + "Text::Table::Any" : "0", "YAML" : "0" } }, @@ -113,23 +114,55 @@ "provides" : { "App::Codeowners" : { "file" : "lib/App/Codeowners.pm", - "version" : "0.41" + "version" : "0.42" + }, + "App::Codeowners::Formatter" : { + "file" : "lib/App/Codeowners/Formatter.pm", + "version" : "0.42" + }, + "App::Codeowners::Formatter::CSV" : { + "file" : "lib/App/Codeowners/Formatter/CSV.pm", + "version" : "0.42" + }, + "App::Codeowners::Formatter::JSON" : { + "file" : "lib/App/Codeowners/Formatter/JSON.pm", + "version" : "0.42" + }, + "App::Codeowners::Formatter::String" : { + "file" : "lib/App/Codeowners/Formatter/String.pm", + "version" : "0.42" + }, + "App::Codeowners::Formatter::TSV" : { + "file" : "lib/App/Codeowners/Formatter/TSV.pm", + "version" : "0.42" + }, + "App::Codeowners::Formatter::Table" : { + "file" : "lib/App/Codeowners/Formatter/Table.pm", + "version" : "0.42" + }, + "App::Codeowners::Formatter::YAML" : { + "file" : "lib/App/Codeowners/Formatter/YAML.pm", + "version" : "0.42" }, "App::Codeowners::Options" : { "file" : "lib/App/Codeowners/Options.pm", - "version" : "0.41" + "version" : "0.42" }, "App::Codeowners::Util" : { "file" : "lib/App/Codeowners/Util.pm", - "version" : "0.41" + "version" : "0.42" + }, + "App::Codeowners::Util::Process" : { + "file" : "lib/App/Codeowners/Util.pm", + "version" : "0.42" }, "File::Codeowners" : { "file" : "lib/File/Codeowners.pm", - "version" : "0.41" + "version" : "0.42" }, "Test::File::Codeowners" : { "file" : "lib/Test/File/Codeowners.pm", - "version" : "0.41" + "version" : "0.42" } }, "release_status" : "stable", @@ -144,7 +177,7 @@ "web" : "https://github.com/chazmcgarvey/git-codeowners" } }, - "version" : "0.41", + "version" : "0.42", "x_authority" : "cpan:CCM", "x_generated_by_perl" : "v5.28.0", "x_serialization_backend" : "Cpanel::JSON::XS version 4.15" diff --git a/META.yml b/META.yml index 78c7ee4..838d588 100644 --- a/META.yml +++ b/META.yml @@ -31,35 +31,60 @@ no_index: provides: App::Codeowners: file: lib/App/Codeowners.pm - version: '0.41' + version: '0.42' + App::Codeowners::Formatter: + file: lib/App/Codeowners/Formatter.pm + version: '0.42' + App::Codeowners::Formatter::CSV: + file: lib/App/Codeowners/Formatter/CSV.pm + version: '0.42' + App::Codeowners::Formatter::JSON: + file: lib/App/Codeowners/Formatter/JSON.pm + version: '0.42' + App::Codeowners::Formatter::String: + file: lib/App/Codeowners/Formatter/String.pm + version: '0.42' + App::Codeowners::Formatter::TSV: + file: lib/App/Codeowners/Formatter/TSV.pm + version: '0.42' + App::Codeowners::Formatter::Table: + file: lib/App/Codeowners/Formatter/Table.pm + version: '0.42' + App::Codeowners::Formatter::YAML: + file: lib/App/Codeowners/Formatter/YAML.pm + version: '0.42' App::Codeowners::Options: file: lib/App/Codeowners/Options.pm - version: '0.41' + version: '0.42' App::Codeowners::Util: file: lib/App/Codeowners/Util.pm - version: '0.41' + version: '0.42' + App::Codeowners::Util::Process: + file: lib/App/Codeowners/Util.pm + version: '0.42' File::Codeowners: file: lib/File/Codeowners.pm - version: '0.41' + version: '0.42' Test::File::Codeowners: file: lib/Test/File/Codeowners.pm - version: '0.41' + version: '0.42' recommends: Term::Detect::Software: '0' Unicode::GCString: '0' requires: Carp: '0' - Color::ANSI::Util: '0' + Color::ANSI::Util: '0.03' Encode: '0' Exporter: '0' Getopt::Long: '2.39' IPC::Open2: '0' + Module::Load: '0' Path::Tiny: '0' Pod::Usage: '0' Scalar::Util: '0' Test::Builder: '0' Text::Gitignore: '0' - Text::Table::Any: '0' + parent: '0' perl: v5.10.1 strict: '0' utf8: '0' @@ -68,7 +93,7 @@ resources: bugtracker: https://github.com/chazmcgarvey/git-codeowners/issues homepage: https://github.com/chazmcgarvey/git-codeowners repository: https://github.com/chazmcgarvey/git-codeowners.git -version: '0.41' +version: '0.42' x_authority: cpan:CCM x_generated_by_perl: v5.28.0 x_serialization_backend: 'YAML::Tiny version 1.73' diff --git a/Makefile.PL b/Makefile.PL index c4ebbbe..db4c282 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -21,17 +21,18 @@ my %WriteMakefileArgs = ( "NAME" => "App::Codeowners", "PREREQ_PM" => { "Carp" => 0, - "Color::ANSI::Util" => 0, + "Color::ANSI::Util" => "0.03", "Encode" => 0, "Exporter" => 0, "Getopt::Long" => "2.39", "IPC::Open2" => 0, + "Module::Load" => 0, "Path::Tiny" => 0, "Pod::Usage" => 0, "Scalar::Util" => 0, "Test::Builder" => 0, "Text::Gitignore" => 0, - "Text::Table::Any" => 0, + "parent" => 0, "strict" => 0, "utf8" => 0, "warnings" => 0 @@ -47,7 +48,7 @@ my %WriteMakefileArgs = ( "Test::Exit" => 0, "Test::More" => 0 }, - "VERSION" => "0.41", + "VERSION" => "0.42", "test" => { "TESTS" => "t/*.t" } @@ -57,7 +58,7 @@ my %WriteMakefileArgs = ( my %FallbackPrereqs = ( "Capture::Tiny" => 0, "Carp" => 0, - "Color::ANSI::Util" => 0, + "Color::ANSI::Util" => "0.03", "Encode" => 0, "Exporter" => 0, "ExtUtils::MakeMaker" => 0, @@ -68,6 +69,7 @@ my %FallbackPrereqs = ( "IO::Handle" => 0, "IPC::Open2" => 0, "IPC::Open3" => 0, + "Module::Load" => 0, "Path::Tiny" => 0, "Pod::Usage" => 0, "Scalar::Util" => 0, @@ -75,7 +77,7 @@ my %FallbackPrereqs = ( "Test::Exit" => 0, "Test::More" => 0, "Text::Gitignore" => 0, - "Text::Table::Any" => 0, + "parent" => 0, "strict" => 0, "utf8" => 0, "warnings" => 0 diff --git a/README b/README index a5e5712..98bbc5b 100644 --- a/README +++ b/README @@ -4,13 +4,15 @@ NAME VERSION - version 0.41 + version 0.42 SYNOPSIS git-codeowners [--version|--help|--manual] - git-codeowners [show] [--format FORMAT] [--[no-]project] [PATH...] + git-codeowners [show] [--format FORMAT] [--owner OWNER]... + [--pattern PATTERN]... [--[no-]patterns] + [--project PROJECT]... [--[no-]projects] [PATH...] git-codeowners owners [--format FORMAT] [--pattern PATTERN] @@ -100,18 +102,35 @@ COMMANDS show - git-codeowners [show] [--format FORMAT] [--[no-]project] [PATH...] + git-codeowners [show] [--format FORMAT] [--owner OWNER]... + [--pattern PATTERN]... [--[no-]patterns] + [--project PROJECT]... [--[no-]projects] [PATH...] Show owners of one or more files in a repo. + If --owner, --project, --pattern are set, only show files with matching + criteria. These can be repeated. + + Use --patterns to also show the matching pattern associated with each + file. + + By default the output might show associated projects if the CODEOWNERS + file defines them. You can control this by explicitly using --projects + or --no-projects to always show or always hide defined projects, + respectively. + owners git-codeowners owners [--format FORMAT] [--pattern PATTERN] + List all owners defined in the CODEOWNERS file. + patterns git-codeowners patterns [--format FORMAT] [--owner OWNER] + List all patterns defined in the CODEOWNERS file. + create git-codeowners create [REPO_DIRPATH|CODEOWNERS_FILEPATH] @@ -144,7 +163,7 @@ FORMAT * FORMAT - Custom format (see below) - Custom + Format string You can specify a custom format using printf-like format sequences. These are the items that can be substituted: @@ -177,7 +196,7 @@ FORMAT * nocolor - Do not colorize replacement string. - Table + Format table Table formatting can be done by one of several different modules, each with its own features and bugs. The default module is @@ -188,6 +207,10 @@ FORMAT The list of available modules is at "@BACKENDS" in Text::Table::Any. +CAVEATS + + * Some commands require git (at least version 1.8.5). + BUGS Please report any bugs or feature requests on the bugtracker website diff --git a/bin/git-codeowners b/bin/git-codeowners index a0bc3fe..2c680eb 100755 --- a/bin/git-codeowners +++ b/bin/git-codeowners @@ -10,7 +10,7 @@ use strict; use App::Codeowners; -our $VERSION = '0.41'; # VERSION +our $VERSION = '0.42'; # VERSION App::Codeowners->main(@ARGV); @@ -26,13 +26,15 @@ git-codeowners - A tool for managing CODEOWNERS files =head1 VERSION -version 0.41 +version 0.42 =head1 SYNOPSIS git-codeowners [--version|--help|--manual] - git-codeowners [show] [--format FORMAT] [--[no-]project] [PATH...] + git-codeowners [show] [--format FORMAT] [--owner OWNER]... + [--pattern PATTERN]... [--[no-]patterns] + [--project PROJECT]... [--[no-]projects] [PATH...] git-codeowners owners [--format FORMAT] [--pattern PATTERN] @@ -128,18 +130,33 @@ Does not yet support Zsh... =head2 show - git-codeowners [show] [--format FORMAT] [--[no-]project] [PATH...] + git-codeowners [show] [--format FORMAT] [--owner OWNER]... + [--pattern PATTERN]... [--[no-]patterns] + [--project PROJECT]... [--[no-]projects] [PATH...] Show owners of one or more files in a repo. +If C<--owner>, C<--project>, C<--pattern> are set, only show files with matching +criteria. These can be repeated. + +Use C<--patterns> to also show the matching pattern associated with each file. + +By default the output might show associated projects if the C file +defines them. You can control this by explicitly using C<--projects> or +C<--no-projects> to always show or always hide defined projects, respectively. + =head2 owners git-codeowners owners [--format FORMAT] [--pattern PATTERN] +List all owners defined in the F file. + =head2 patterns git-codeowners patterns [--format FORMAT] [--owner OWNER] +List all patterns defined in the F file. + =head2 create git-codeowners create [REPO_DIRPATH|CODEOWNERS_FILEPATH] @@ -189,7 +206,7 @@ C - Custom format (see below) =back -=head2 Custom +=head2 Format string You can specify a custom format using printf-like format sequences. These are the items that can be substituted: @@ -250,7 +267,7 @@ C - Do not colorize replacement string. =back -=head2 Table +=head2 Format table Table formatting can be done by one of several different modules, each with its own features and bugs. The default module is L, but this can be overridden using the @@ -260,6 +277,16 @@ C environment variable if desired, like this: The list of available modules is at L. +=head1 CAVEATS + +=over 4 + +=item * + +Some commands require F (at least version 1.8.5). + +=back + =head1 BUGS Please report any bugs or feature requests on the bugtracker website diff --git a/lib/App/Codeowners.pm b/lib/App/Codeowners.pm index 626fc82..e70a109 100644 --- a/lib/App/Codeowners.pm +++ b/lib/App/Codeowners.pm @@ -6,14 +6,15 @@ 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 stringf); -use Color::ANSI::Util qw(ansifg ansi_reset); +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.41'; # VERSION +our $VERSION = '0.42'; # VERSION sub main { @@ -43,26 +44,52 @@ sub _command_show { or die "No CODEOWNERS file in $toplevel\n"; my $codeowners = File::Codeowners->parse_from_filepath($codeowners_path); - my ($cdup) = run_git(qw{rev-parse --show-cdup}); + my ($proc, $cdup) = run_git(qw{rev-parse --show-cdup}); + $proc->wait and exit 1; - my @results; + my $show_projects = $opts->{projects} // scalar @{$codeowners->projects}; - my $filepaths = git_ls_files('.', $opts->args) or die "Cannot list files\n"; - for my $filepath (@$filepaths) { + 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)); - push @results, [ + 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}, - $opts->{project} ? $match->{project} : (), - ]; + $show_projects ? $match->{project} : (), + ]); } - - _format( - format => $opts->{format} || ' * %-50F %O', - out => *STDOUT, - headers => [qw(File Owner), $opts->{project} ? 'Project' : ()], - rows => \@results, - ); + $proc->wait and exit 1; } sub _command_owners { @@ -77,12 +104,12 @@ sub _command_owners { my $results = $codeowners->owners($opts->{pattern}); - _format( + my $formatter = App::Codeowners::Formatter->new( format => $opts->{format} || '%O', - out => *STDOUT, - headers => [qw(Owner)], - rows => [map { [$_] } @$results], + handle => *STDOUT, + columns => [qw(Owner)], ); + $formatter->add_result(map { [$_] } @$results); } sub _command_patterns { @@ -97,12 +124,32 @@ sub _command_patterns { my $results = $codeowners->patterns($opts->{owner}); - _format( + my $formatter = App::Codeowners::Formatter->new( format => $opts->{format} || '%T', - out => *STDOUT, - headers => [qw(Pattern)], - rows => [map { [$_] } @$results], + 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 } @@ -158,167 +205,6 @@ END print STDERR "Wrote $path\n"; } -sub _format { - my %args = @_; - - my $format = $args{format} || 'table'; - my $fh = $args{out} || *STDOUT; - my $headers = $args{headers} || []; - my $rows = $args{rows} || []; - - if ($format eq 'table') { - eval { require Text::Table::Any } or die "Missing dependency: Text::Table::Any\n"; - - my $table = Text::Table::Any::table( - header_row => 1, - rows => [$headers, map { [map { _stringify($_) } @$_] } @$rows], - backend => $ENV{PERL_TEXT_TABLE}, - ); - print { $fh } encode('UTF-8', $table); - } - elsif ($format =~ /^json(:pretty)?$/) { - my $pretty = !!$1; - eval { require JSON::MaybeXS } or die "Missing dependency: JSON::MaybeXS\n"; - - my $json = JSON::MaybeXS->new(canonical => 1, utf8 => 1, pretty => $pretty); - my $data = _combine_headers_rows($headers, $rows); - print { $fh } $json->encode($data); - } - elsif ($format =~ /^([ct])sv$/) { - my $sep = $1 eq 'c' ? ',' : "\t"; - eval { require Text::CSV } or die "Missing dependency: Text::CSV\n"; - - my $csv = Text::CSV->new({binary => 1, eol => $/, sep => $sep}); - $csv->print($fh, $headers); - $csv->print($fh, [map { encode('UTF-8', _stringify($_)) } @$_]) for @$rows; - } - elsif ($format =~ /^ya?ml$/) { - eval { require YAML } or die "Missing dependency: YAML\n"; - - my $data = _combine_headers_rows($headers, $rows); - print { $fh } encode('UTF-8', YAML::Dump($data)); - } - else { - my $data = _combine_headers_rows($headers, $rows); - - # https://sashat.me/2017/01/11/list-of-20-simple-distinct-colors/ - my @contrasting_colors = qw( - e6194b 3cb44b ffe119 4363d8 f58231 - 911eb4 42d4f4 f032e6 bfef45 fabebe - 469990 e6beff 9a6324 fffac8 800000 - aaffc3 808000 ffd8b1 000075 a9a9a9 - ); - - # assign a color to each owner, on demand - my %owner_colors; - my $num = -1; - my $owner_color = sub { - my $owner = shift or return; - $owner_colors{$owner} ||= do { - $num = ($num + 1) % scalar @contrasting_colors; - $contrasting_colors[$num]; - }; - }; - - my %filter = ( - quote => sub { local $_ = $_[0]; s/"/\"/s; "\"$_\"" }, - ); - - my $create_filterer = sub { - my $value = shift || ''; - my $color = shift || ''; - my $gencolor = ref($color) eq 'CODE' ? $color : sub { $color }; - return sub { - my $arg = shift; - my ($filters, $color) = _expand_filter_args($arg); - if (ref($value) eq 'ARRAY') { - $value = join(',', map { _colored($_, $color // $gencolor->($_)) } @$value); - } - else { - $value = _colored($value, $color // $gencolor->($value)); - } - for my $key (@$filters) { - if (my $filter = $filter{$key}) { - $value = $filter->($value); - } - else { - warn "Unknown filter: $key\n" - } - } - $value || ''; - }; - }; - - for my $row (@$data) { - my %info = ( - F => $create_filterer->($row->{File}, undef), - O => $create_filterer->($row->{Owner}, $owner_color), - P => $create_filterer->($row->{Project}, undef), - T => $create_filterer->($row->{Pattern}, undef), - ); - - my $text = stringf($format, %info); - print { $fh } encode('UTF-8', $text), "\n"; - } - } -} - -sub _expand_filter_args { - my $arg = shift || ''; - - my @filters = split(/,/, $arg); - my $color_override; - - for (my $i = 0; $i < @filters; ++$i) { - my $filter = $filters[$i] or next; - if ($filter =~ /^(?:nocolor|color:([0-9a-fA-F]{3,6}))$/) { - $color_override = $1 || ''; - splice(@filters, $i, 1); - redo; - } - } - - return (\@filters, $color_override); -} - -sub _colored { - my $text = shift; - my $rgb = shift or return $text; - - # ansifg honors NO_COLOR already, but ansi_reset does not. - return $text if $ENV{NO_COLOR}; - - $rgb =~ s/^(.)(.)(.)$/$1$1$2$2$3$3/; - if ($rgb !~ m/^[0-9a-fA-F]{6}$/) { - warn "Color value must be in 'ffffff' or 'fff' form.\n"; - return $text; - } - - my ($begin, $end) = (ansifg($rgb), ansi_reset); - return "${begin}${text}${end}"; -} - -sub _combine_headers_rows { - my $headers = shift; - my $rows = shift; - - my @new_rows; - - for my $row (@$rows) { - push @new_rows, (my $new_row = {}); - for (my $i = 0; $i < @$headers; ++$i) { - $new_row->{$headers->[$i]} = $row->[$i]; - } - } - - return \@new_rows; -} - -sub _stringify { - my $item = shift; - return ref($item) eq 'ARRAY' ? join(',', @$item) : $item; -} - 1; __END__ @@ -333,7 +219,7 @@ App::Codeowners - A tool for managing CODEOWNERS files =head1 VERSION -version 0.41 +version 0.42 =head1 METHODS diff --git a/lib/App/Codeowners/Formatter.pm b/lib/App/Codeowners/Formatter.pm new file mode 100644 index 0000000..095a6ef --- /dev/null +++ b/lib/App/Codeowners/Formatter.pm @@ -0,0 +1,260 @@ +package App::Codeowners::Formatter; +# ABSTRACT: Base class for formatting codeowners output + + +use warnings; +use strict; + +our $VERSION = '0.42'; # VERSION + +use Module::Load; + + +sub new { + my $class = shift; + my $args = {@_ == 1 && ref $_[0] eq 'HASH' ? %{$_[0]} : @_}; + + $args->{results} = []; + + # see if we can find a better class to bless into + ($class, my $format) = $class->_best_formatter($args->{format}) if $args->{format}; + $args->{format} = $format; + + my $self = bless $args, $class; + + $self->start; + + return $self; +} + +### _best_formatter +# Find a formatter that can handle the format requested. +sub _best_formatter { + my $class = shift; + my $type = shift || ''; + + return ($class, $type) if $class ne __PACKAGE__; + + my ($name, $format) = $type =~ /^([A-Za-z]+)(?::(.*))?$/; + if (!$name) { + $name = ''; + $format = ''; + } + + $name = lc($name); + $name =~ s/:.*//; + + my @formatters = $class->formatters; + + # default to the string formatter since it has no dependencies + my $package = __PACKAGE__.'::String'; + + # look for a formatter whose name matches the format + for my $formatter (@formatters) { + my $module = lc($formatter); + $module =~ s/.*:://; + + if ($module eq $name) { + $package = $formatter; + $type = $format; + last; + } + } + + load $package; + return ($package, $type); +} + + +sub DESTROY { + my $self = shift; + my $global_destruction = shift; + + return if $global_destruction; + + my $results = $self->{results}; + $self->finish($results) if $results; + delete $self->{results}; +} + + +sub handle { shift->{handle} } +sub format { shift->{format} || '' } +sub columns { shift->{columns} || [] } +sub results { shift->{results} } + + +sub add_result { + my $self = shift; + $self->stream($_) for @_; +} + + +sub start {} +sub stream { push @{$_[0]->results}, $_[1] } +sub finish {} + + +sub formatters { + return qw( + App::Codeowners::Formatter::CSV + App::Codeowners::Formatter::JSON + App::Codeowners::Formatter::String + App::Codeowners::Formatter::TSV + App::Codeowners::Formatter::Table + App::Codeowners::Formatter::YAML + ); +} + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +App::Codeowners::Formatter - Base class for formatting codeowners output + +=head1 VERSION + +version 0.42 + +=head1 SYNOPSIS + + my $formatter = App::Codeowners::Formatter->new(handle => *STDOUT); + $formatter->add_result($_) for @results; + +=head1 DESCRIPTION + +This is a base class for formatters. A formatter is a class that takes data records, stringifies +them, and prints them to an IO handle. + +This class is mostly abstract, though it is also usable as a null formatter where results are simply +discarded if it is instantiated directly. These other formatters do more interesting things: + +=over 4 + +=item * + +L + +=item * + +L + +=item * + +L + +=item * + +L + +=item * + +L + +=item * + +L + +=back + +=head1 ATTRIBUTES + +=head2 handle + +Get the IO handle associated with a formatter. + +=head2 format + +Get the format string, which may be used to customize the formatting. + +=head2 columns + +Get an arrayref of column headings. + +=head2 results + +Get an arrayref of all the results that have been provided to the formatter using L but +have not yet been formatted. + +=head1 METHODS + +=head2 new + + $formatter = App::Codeowners::Formatter->new; + $formatter = App::Codeowners::Formatter->new(%attributes); + +Construct a new formatter. + +=head2 DESTROY + +Destructor calls L. + +=head2 add_result + + $formatter->add_result($result); + +Provide an additional lint result to be formatted. + +=head2 start + + $formatter->start; + +Begin formatting results. Called before any results are passed to the L method. + +This method may print a header to the L. This method is used by subclasses and should +typically not be called explicitly. + +=head2 stream + + $formatter->stream(\@result, ...); + +Format one result. + +This method is expected to print a string representation of the result to the L. This +method is used by subclasses and should typically not called be called explicitly. + +The default implementation simply stores the L so they will be available to L. + +=head2 finish + + $formatter->finish; + +End formatting results. Called after all results are passed to the L method. + +This method may print a footer to the L. This method is used by subclasses and should +typically not be called explicitly. + +=head2 formatters + + @formatters = App::Codeowners::Formatter->formatters; + +Get a list of package names of potential formatters within the C +namespace. + +=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 diff --git a/lib/App/Codeowners/Formatter/CSV.pm b/lib/App/Codeowners/Formatter/CSV.pm new file mode 100644 index 0000000..a60dc94 --- /dev/null +++ b/lib/App/Codeowners/Formatter/CSV.pm @@ -0,0 +1,110 @@ +package App::Codeowners::Formatter::CSV; +# ABSTRACT: Format codeowners output as comma-separated values + + +use warnings; +use strict; + +our $VERSION = '0.42'; # VERSION + +use parent 'App::Codeowners::Formatter'; + +use App::Codeowners::Util qw(stringify); +use Encode qw(encode); + +sub start { + my $self = shift; + + $self->text_csv->print($self->handle, $self->columns); +} + +sub stream { + my $self = shift; + my $result = shift; + + $self->text_csv->print($self->handle, [map { encode('UTF-8', stringify($_)) } @$result]); +} + + +sub text_csv { + my $self = shift; + + $self->{text_csv} ||= do { + eval { require Text::CSV } or die "Missing dependency: Text::CSV\n"; + + my %options; + $options{escape_char} = $self->escape_char if $self->escape_char; + $options{quote} = $self->quote if $self->quote; + $options{sep} = $self->sep if $self->sep; + if ($options{sep} && $options{sep} eq ($options{quote} || '"')) { + die "Invalid separator value for CSV format.\n"; + } + + Text::CSV->new({binary => 1, eol => $/, %options}); + } or die "Failed to construct Text::CSV object"; +} + + +sub sep { $_[0]->{sep} || $_[0]->format } +sub quote { $_[0]->{quote} } +sub escape_char { $_[0]->{escape_char} } + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +App::Codeowners::Formatter::CSV - Format codeowners output as comma-separated values + +=head1 VERSION + +version 0.42 + +=head1 DESCRIPTION + +This is a L that formats output using L. + +=head1 ATTRIBUTES + +=head2 text_csv + +Get the L instance. + +=head2 sep + +Get the value used for L. + +=head2 quote + +Get the value used for L. + +=head2 escape_char + +Get the value used for L. + +=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 diff --git a/lib/App/Codeowners/Formatter/JSON.pm b/lib/App/Codeowners/Formatter/JSON.pm new file mode 100644 index 0000000..2ead10c --- /dev/null +++ b/lib/App/Codeowners/Formatter/JSON.pm @@ -0,0 +1,77 @@ +package App::Codeowners::Formatter::JSON; +# ABSTRACT: Format codeowners output as JSON + + +use warnings; +use strict; + +our $VERSION = '0.42'; # VERSION + +use parent 'App::Codeowners::Formatter'; + +use App::Codeowners::Util qw(zip); + + +sub finish { + my $self = shift; + my $results = shift; + + eval { require JSON::MaybeXS } or die "Missing dependency: JSON::MaybeXS\n"; + + my %options; + $options{pretty} = 1 if lc($self->format) eq 'pretty'; + + my $json = JSON::MaybeXS->new(canonical => 1, utf8 => 1, %options); + + my $columns = $self->columns; + $results = [map { +{zip @$columns, @$_} } @$results]; + print { $self->handle } $json->encode($results); +} + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +App::Codeowners::Formatter::JSON - Format codeowners output as JSON + +=head1 VERSION + +version 0.42 + +=head1 DESCRIPTION + +This is a L that formats output using L. + +=head1 ATTRIBUTES + +=head2 format + +If unset (default), the output will be compact. If "pretty", the output will look nicer to humans. + +=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 diff --git a/lib/App/Codeowners/Formatter/String.pm b/lib/App/Codeowners/Formatter/String.pm new file mode 100644 index 0000000..3342331 --- /dev/null +++ b/lib/App/Codeowners/Formatter/String.pm @@ -0,0 +1,167 @@ +package App::Codeowners::Formatter::String; +# ABSTRACT: Format codeowners output using printf-like strings + + +use warnings; +use strict; + +our $VERSION = '0.42'; # VERSION + +use parent 'App::Codeowners::Formatter'; + +use App::Codeowners::Util qw(stringf zip); +use Color::ANSI::Util 0.03 qw(ansifg); +use Encode qw(encode); + +sub stream { + my $self = shift; + my $result = shift; + + $result = {zip @{$self->columns}, @$result}; + + my %info = ( + F => $self->_create_filterer->($result->{File}, undef), + O => $self->_create_filterer->($result->{Owner}, $self->_owner_colorgen), + P => $self->_create_filterer->($result->{Project}, undef), + T => $self->_create_filterer->($result->{Pattern}, undef), + ); + + my $text = stringf($self->format, %info); + print { $self->handle } encode('UTF-8', $text), "\n"; +} + +sub _expand_filter_args { + my $arg = shift || ''; + + my @filters = split(/,/, $arg); + my $color_override; + + for (my $i = 0; $i < @filters; ++$i) { + my $filter = $filters[$i] or next; + if ($filter =~ /^(?:nocolor|color:([0-9a-fA-F]{3,6}))$/) { + $color_override = $1 || ''; + splice(@filters, $i, 1); + redo; + } + } + + return (\@filters, $color_override); +} + +sub _ansi_reset { "\033[0m" } + +sub _colored { + my $text = shift; + my $rgb = shift or return $text; + + return $text if $ENV{NO_COLOR}; + + $rgb =~ s/^(.)(.)(.)$/$1$1$2$2$3$3/; + if ($rgb !~ m/^[0-9a-fA-F]{6}$/) { + warn "Color value must be in 'ffffff' or 'fff' form.\n"; + return $text; + } + + my ($begin, $end) = (ansifg($rgb), _ansi_reset); + return "${begin}${text}${end}"; +} + +sub _create_filterer { + my $self = shift; + + my %filter = ( + quote => sub { local $_ = $_[0]; s/"/\"/s; "\"$_\"" }, + ); + + return sub { + my $value = shift || ''; + my $color = shift || ''; + my $gencolor = ref($color) eq 'CODE' ? $color : sub { $color }; + return sub { + my $arg = shift; + my ($filters, $color) = _expand_filter_args($arg); + if (ref($value) eq 'ARRAY') { + $value = join(',', map { _colored($_, $color // $gencolor->($_)) } @$value); + } + else { + $value = _colored($value, $color // $gencolor->($value)); + } + for my $key (@$filters) { + if (my $filter = $filter{$key}) { + $value = $filter->($value); + } + else { + warn "Unknown filter: $key\n" + } + } + $value || ''; + }; + }; +} + +sub _owner_colorgen { + my $self = shift; + + # https://sashat.me/2017/01/11/list-of-20-simple-distinct-colors/ + my @contrasting_colors = qw( + e6194b 3cb44b ffe119 4363d8 f58231 + 911eb4 42d4f4 f032e6 bfef45 fabebe + 469990 e6beff 9a6324 fffac8 800000 + aaffc3 808000 ffd8b1 000075 a9a9a9 + ); + + # assign a color to each owner, on demand + my %owner_colors; + my $num = -1; + $self->{owner_color} ||= sub { + my $owner = shift or return; + $owner_colors{$owner} ||= do { + $num = ($num + 1) % scalar @contrasting_colors; + $contrasting_colors[$num]; + }; + }; +} + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +App::Codeowners::Formatter::String - Format codeowners output using printf-like strings + +=head1 VERSION + +version 0.42 + +=head1 DESCRIPTION + +This is a L that formats output using a printf-like string. + +See L. + +=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 diff --git a/lib/App/Codeowners/Formatter/TSV.pm b/lib/App/Codeowners/Formatter/TSV.pm new file mode 100644 index 0000000..b2f22bd --- /dev/null +++ b/lib/App/Codeowners/Formatter/TSV.pm @@ -0,0 +1,54 @@ +package App::Codeowners::Formatter::TSV; +# ABSTRACT: Format codeowners output as tab-separated values + + +use warnings; +use strict; + +our $VERSION = '0.42'; # VERSION + +use parent 'App::Codeowners::Formatter::CSV'; + +sub sep { "\t" } + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +App::Codeowners::Formatter::TSV - Format codeowners output as tab-separated values + +=head1 VERSION + +version 0.42 + +=head1 DESCRIPTION + +This is a L that formats output using L. + +=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 diff --git a/lib/App/Codeowners/Formatter/Table.pm b/lib/App/Codeowners/Formatter/Table.pm new file mode 100644 index 0000000..df9bf39 --- /dev/null +++ b/lib/App/Codeowners/Formatter/Table.pm @@ -0,0 +1,69 @@ +package App::Codeowners::Formatter::Table; +# ABSTRACT: Format codeowners output as a table + + +use warnings; +use strict; + +our $VERSION = '0.42'; # VERSION + +use parent 'App::Codeowners::Formatter'; + +use App::Codeowners::Util qw(stringify); +use Encode qw(encode); + +sub finish { + my $self = shift; + my $results = shift; + + eval { require Text::Table::Any } or die "Missing dependency: Text::Table::Any\n"; + + my $table = Text::Table::Any::table( + header_row => 1, + rows => [$self->columns, map { [map { stringify($_) } @$_] } @$results], + backend => $ENV{PERL_TEXT_TABLE}, + ); + print { $self->handle } encode('UTF-8', $table); +} + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +App::Codeowners::Formatter::Table - Format codeowners output as a table + +=head1 VERSION + +version 0.42 + +=head1 DESCRIPTION + +This is a L that formats output using L. + +=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 diff --git a/lib/App/Codeowners/Formatter/YAML.pm b/lib/App/Codeowners/Formatter/YAML.pm new file mode 100644 index 0000000..00b4d1e --- /dev/null +++ b/lib/App/Codeowners/Formatter/YAML.pm @@ -0,0 +1,65 @@ +package App::Codeowners::Formatter::YAML; +# ABSTRACT: Format codeowners output as YAML + + +use warnings; +use strict; + +our $VERSION = '0.42'; # VERSION + +use parent 'App::Codeowners::Formatter'; + +use App::Codeowners::Util qw(zip); + +sub finish { + my $self = shift; + my $results = shift; + + eval { require YAML } or die "Missing dependency: YAML\n"; + + my $columns = $self->columns; + $results = [map { +{zip @$columns, @$_} } @$results]; + print { $self->handle } YAML::Dump($results); +} + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +App::Codeowners::Formatter::YAML - Format codeowners output as YAML + +=head1 VERSION + +version 0.42 + +=head1 DESCRIPTION + +This is a L that formats output using L. + +=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 diff --git a/lib/App/Codeowners/Options.pm b/lib/App/Codeowners/Options.pm index 950f0b8..1bd3c0f 100644 --- a/lib/App/Codeowners/Options.pm +++ b/lib/App/Codeowners/Options.pm @@ -8,7 +8,7 @@ use Getopt::Long 2.39 (); use Path::Tiny; use Pod::Usage; -our $VERSION = '0.41'; # VERSION +our $VERSION = '0.42'; # VERSION sub early_options { return { @@ -30,8 +30,13 @@ sub command_options { 'patterns' => { 'owner=s' => '', }, + 'projects' => {}, 'show' => { - 'project!' => 1, + 'owner=s@' => [], + 'pattern=s@' => [], + 'project=s@' => [], + 'patterns!' => 0, + 'projects!' => undef, }, 'update' => {}, }; @@ -84,7 +89,7 @@ sub new { exit 0; } if ($opts->{help}) { - pod2usage(-exitval => 0, -verbose => 99, -sections => [qw(NAME SYNOPSIS OPTIONS)]); + pod2usage(-exitval => 0, -verbose => 99, -sections => [qw(NAME SYNOPSIS OPTIONS COMMANDS)]); } if ($opts->{manual}) { pod2usage(-exitval => 0, -verbose => 2); @@ -273,7 +278,7 @@ App::Codeowners::Options - Getopt and shell completion for App::Codeowners =head1 VERSION -version 0.41 +version 0.42 =head1 METHODS diff --git a/lib/App/Codeowners/Util.pm b/lib/App/Codeowners/Util.pm index cb0b795..762f040 100644 --- a/lib/App/Codeowners/Util.pm +++ b/lib/App/Codeowners/Util.pm @@ -15,12 +15,15 @@ our @EXPORT_OK = qw( find_nearest_codeowners git_ls_files git_toplevel + run_command run_git stringf + stringify unbackslash + zip ); -our $VERSION = '0.41'; # VERSION +our $VERSION = '0.42'; # VERSION sub find_nearest_codeowners { @@ -51,49 +54,50 @@ sub find_codeowners_in_directory { } } -sub run_git { - my @cmd = ('git', @_); +sub run_command { + my $filter; + $filter = pop if ref($_[-1]) eq 'CODE'; - require IPC::Open2; + print STDERR "# @_\n" if $ENV{GIT_CODEOWNERS_DEBUG}; my ($child_in, $child_out); - my $pid = IPC::Open2::open2($child_out, $child_in, @cmd); + require IPC::Open2; + my $pid = IPC::Open2::open2($child_out, $child_in, @_); close($child_in); binmode($child_out, ':encoding(UTF-8)'); - chomp(my @lines = <$child_out>); - waitpid($pid, 0); - return if $? != 0; + my $proc = App::Codeowners::Util::Process->new( + pid => $pid, + fh => $child_out, + filter => $filter, + ); - return @lines; + return wantarray ? ($proc, @{$proc->all}) : $proc; +} + +sub run_git { + return run_command('git', @_); } sub git_ls_files { my $dir = shift || '.'; + return run_git('-C', $dir, 'ls-files', @_, \&_unescape_git_filepath); +} - my @files = run_git('-C', $dir, qw{ls-files}, @_); - - return undef if !@files; ## no critic (Subroutines::ProhibitExplicitReturn) - - # Depending on git's "core.quotepath" config, non-ASCII chars may be - # escaped (identified by surrounding dquotes), so try to unescape. - for my $file (@files) { - next if $file !~ /^"(.+)"$/; - $file = $1; - $file = unbackslash($file); - $file = decode('UTF-8', $file); - } - - return \@files; +# Depending on git's "core.quotepath" config, non-ASCII chars may be +# escaped (identified by surrounding dquotes), so try to unescape. +sub _unescape_git_filepath { + return $_ if $_ !~ /^"(.+)"$/; + return decode('UTF-8', unbackslash($1)); } sub git_toplevel { my $dir = shift || '.'; - my ($path) = run_git('-C', $dir, qw{rev-parse --show-toplevel}); + my ($proc, $path) = run_git('-C', $dir, qw{rev-parse --show-toplevel}); - return if !$path; + return if $proc->wait != 0 || !$path; return path($path); } @@ -103,6 +107,22 @@ sub colorstrip { return $str; } +sub stringify { + my $item = shift; + return ref($item) eq 'ARRAY' ? join(',', @$item) : $item; +} + +# The zip code is from List::SomeUtils (thanks DROLSKY), copied just so as not +# to bring in the extra dependency. +sub zip (\@\@) { ## no critic (Subroutines::ProhibitSubroutinePrototypes) + my $max = -1; + $max < $#$_ && ( $max = $#$_ ) foreach @_; + map { + my $ix = $_; + map $_->[$ix], @_; + } 0 .. $max; +} + # The stringf code is from String::Format (thanks SREZIC), with changes: # - Use Unicode::GCString for better Unicode character padding, # - Strip ANSI color sequences, @@ -195,6 +215,57 @@ sub unbackslash { return $str; } +{ + package App::Codeowners::Util::Process; + + sub new { + my $class = shift; + return bless {@_}, $class; + } + + sub next { + my $self = shift; + my $line = readline($self->{fh}); + if (defined $line) { + chomp $line; + if (my $filter = $self->{filter}) { + local $_ = $line; + $line = $filter->($line); + } + } + $line; + } + + sub all { + my $self = shift; + chomp(my @lines = readline($self->{fh})); + if (my $filter = $self->{filter}) { + $_ = $filter->($_) for @lines; + } + \@lines; + } + + sub wait { + my $self = shift; + my $pid = $self->{pid} or return; + if (my $fh = $self->{fh}) { + close($fh); + delete $self->{fh}; + } + waitpid($pid, 0); + my $status = $?; + print STDERR "# -> status $status\n" if $ENV{GIT_CODEOWNERS_DEBUG}; + delete $self->{pid}; + return $status; + } + + sub DESTROY { + my ($self, $global_destruction) = @_; + return if $global_destruction; + $self->wait; + } +} + 1; __END__ @@ -209,7 +280,7 @@ App::Codeowners::Util - Grab bag of utility subs for Codeowners modules =head1 VERSION -version 0.41 +version 0.42 =head1 DESCRIPTION diff --git a/lib/File/Codeowners.pm b/lib/File/Codeowners.pm index f987561..e7b23de 100644 --- a/lib/File/Codeowners.pm +++ b/lib/File/Codeowners.pm @@ -10,7 +10,7 @@ use Path::Tiny; use Scalar::Util qw(openhandle); use Text::Gitignore qw(build_gitignore_matcher); -our $VERSION = '0.41'; # VERSION +our $VERSION = '0.42'; # VERSION sub _croak { require Carp; Carp::croak(@_); } sub _usage { _croak("Usage: @_\n") } @@ -250,6 +250,24 @@ sub patterns { } +sub projects { + my $self = shift; + + return $self->{projects} if $self->{projects}; + + my %projects; + for my $line (@{$self->_lines}) { + my $project = $line->{project}; + $projects{$project}++ if $project; + } + + my $projects = [sort keys %projects]; + $self->{projects} = $projects; + + return $projects; +} + + sub update_owners { my $self = shift; my $pattern = shift; @@ -319,6 +337,7 @@ sub _clear { delete $self->{match_lines}; delete $self->{owners}; delete $self->{patterns}; + delete $self->{projects}; } 1; @@ -335,7 +354,7 @@ File::Codeowners - Read and write CODEOWNERS files =head1 VERSION -version 0.41 +version 0.42 =head1 METHODS @@ -432,6 +451,12 @@ defined in the file. Get an arrayref of all patterns defined. +=head2 projects + + $projects = $codeowners->projects; + +Get an arrayref of all projects defined. + =head2 update_owners $codeowners->update_owners($pattern => \@new_owners); diff --git a/lib/Test/File/Codeowners.pm b/lib/Test/File/Codeowners.pm index 44a384f..a166e71 100644 --- a/lib/Test/File/Codeowners.pm +++ b/lib/Test/File/Codeowners.pm @@ -10,7 +10,7 @@ use Encode qw(encode); use File::Codeowners; use Test::Builder; -our $VERSION = '0.41'; # VERSION +our $VERSION = '0.42'; # VERSION my $Test = Test::Builder->new; @@ -49,11 +49,11 @@ sub codeowners_git_files_ok { return; } - my $files = git_ls_files(git_toplevel()); + my ($proc, @files) = git_ls_files(git_toplevel()); - $Test->plan(@$files ? (tests => scalar @$files) : (skip_all => 'git ls-files failed')); + $Test->plan($proc->wait == 0 ? (tests => scalar @files) : (skip_all => 'git ls-files failed')); - for my $filepath (@$files) { + for my $filepath (@files) { my $msg = encode('UTF-8', "Check file: $filepath"); my $match = $codeowners->match($filepath); @@ -88,7 +88,7 @@ Test::File::Codeowners - Write tests for CODEOWNERS files =head1 VERSION -version 0.41 +version 0.42 =head1 SYNOPSIS diff --git a/t/00-compile.t b/t/00-compile.t index 379fc9b..37c2ffc 100644 --- a/t/00-compile.t +++ b/t/00-compile.t @@ -6,10 +6,17 @@ use warnings; use Test::More; -plan tests => 6 + ($ENV{AUTHOR_TESTING} ? 1 : 0); +plan tests => 13 + ($ENV{AUTHOR_TESTING} ? 1 : 0); my @module_files = ( 'App/Codeowners.pm', + 'App/Codeowners/Formatter.pm', + 'App/Codeowners/Formatter/CSV.pm', + 'App/Codeowners/Formatter/JSON.pm', + 'App/Codeowners/Formatter/String.pm', + 'App/Codeowners/Formatter/TSV.pm', + 'App/Codeowners/Formatter/Table.pm', + 'App/Codeowners/Formatter/YAML.pm', 'App/Codeowners/Options.pm', 'App/Codeowners/Util.pm', 'File/Codeowners.pm', diff --git a/t/00-report-prereqs.dd b/t/00-report-prereqs.dd index 2f32b18..0c517b6 100644 --- a/t/00-report-prereqs.dd +++ b/t/00-report-prereqs.dd @@ -46,17 +46,18 @@ do { my $x = { }, 'requires' => { 'Carp' => '0', - 'Color::ANSI::Util' => '0', + 'Color::ANSI::Util' => '0.03', 'Encode' => '0', 'Exporter' => '0', 'Getopt::Long' => '2.39', 'IPC::Open2' => '0', + 'Module::Load' => '0', 'Path::Tiny' => '0', 'Pod::Usage' => '0', 'Scalar::Util' => '0', 'Test::Builder' => '0', 'Text::Gitignore' => '0', - 'Text::Table::Any' => '0', + 'parent' => '0', 'perl' => 'v5.10.1', 'strict' => '0', 'utf8' => '0', @@ -65,7 +66,7 @@ do { my $x = { 'suggests' => { 'JSON::MaybeXS' => '0', 'Text::CSV' => '0', - 'Text::Table' => '0', + 'Text::Table::Any' => '0', 'YAML' => '0' } }, diff --git a/t/app-codeowners-util.t b/t/app-codeowners-util.t index 93fdce4..2edbcc7 100644 --- a/t/app-codeowners-util.t +++ b/t/app-codeowners-util.t @@ -8,11 +8,17 @@ use Path::Tiny qw(path tempdir); use Test::More; can_ok('App::Codeowners::Util', qw{ - find_nearest_codeowners + colorstrip find_codeowners_in_directory - run_git + find_nearest_codeowners git_ls_files git_toplevel + run_command + run_git + stringf + stringify + unbackslash + zip }); my $can_git = _can_git(); @@ -21,16 +27,16 @@ subtest 'git_ls_files' => sub { plan skip_all => 'Cannot run git' if !$can_git; my $repodir =_setup_git_repo(); - my $r = App::Codeowners::Util::git_ls_files($repodir); - is($r, undef, 'git ls-files returns undef when no repo files') or diag explain $r; + my (undef, @r) = App::Codeowners::Util::git_ls_files($repodir); + is_deeply(\@r, [], 'git ls-files returns [] when no repo files') or diag explain \@r; - run_git('-C', $repodir, qw{add .}); - run_git('-C', $repodir, qw{commit -m}, 'initial commit'); + run_git('-C', $repodir, qw{add .})->wait; + run_git('-C', $repodir, qw{commit -m}, 'initial commit')->wait; - $r = App::Codeowners::Util::git_ls_files($repodir); - is_deeply($r, [ + (undef, @r) = App::Codeowners::Util::git_ls_files($repodir); + is_deeply(\@r, [ qw(a/b/c/bar.txt foo.txt) - ], 'git ls-files returns correct repo files') or diag explain $r; + ], 'git ls-files returns correct repo files') or diag explain \@r; }; subtest 'git_toplevel' => sub { @@ -45,7 +51,9 @@ subtest 'git_toplevel' => sub { }; subtest 'find_nearest_codeowners' => sub { + plan skip_all => 'Cannot run git' if !$can_git; my $repodir =_setup_git_repo(); + $repodir->child('docs')->mkpath; my $filepath = _spew_codeowners($repodir->child('docs/CODEOWNERS')); @@ -54,9 +62,10 @@ subtest 'find_nearest_codeowners' => sub { }; subtest 'find_codeowners_in_directory' => sub { + plan skip_all => 'Cannot run git' if !$can_git; my $repodir =_setup_git_repo(); - $repodir->child('docs')->mkpath; + $repodir->child('docs')->mkpath; my $filepath = _spew_codeowners($repodir->child('docs/CODEOWNERS')); my $r = App::Codeowners::Util::find_codeowners_in_directory($repodir); @@ -71,14 +80,16 @@ done_testing; exit; sub _can_git { - my ($version) = run_git('--version'); - return $version; + my (undef, $version) = eval { run_git('--version') }; + note $@ if $@; + note "Found: $version" if $version; + return $version && $version ge 'git version 1.8.5'; # for -C flag } sub _setup_git_repo { my $repodir = tempdir; - run_git('-C', $repodir, 'init'); + run_git('-C', $repodir, 'init')->wait; $repodir->child('foo.txt')->touchpath; $repodir->child('a/b/c/bar.txt')->touchpath; diff --git a/t/app-codeowners.t b/t/app-codeowners.t index 5d37841..28309d3 100644 --- a/t/app-codeowners.t +++ b/t/app-codeowners.t @@ -13,40 +13,40 @@ use Path::Tiny qw(path tempdir); use Test::More; my $can_git = _can_git(); -plan skip_all => 'Cannot run git' if !$can_git; # Set progname so that pod2usage knows how to find the script after we chdir $0 = path($Bin)->parent->child('bin/git-codeowners')->absolute; $ENV{NO_COLOR} = 1; -subtest 'basic options' => sub { - my $repodir = _setup_git_repo(); - my $chdir = pushd($repodir); +sub run(&) { ## no critic (Subroutines::ProhibitSubroutinePrototypes) + my $code = shift; + capture { exit_code { $code->() } }; +} - my ($stdout, $stderr, $exit) = capture { exit_code { App::Codeowners->main('--help') } }; +subtest 'basic options' => sub { + my ($stdout, $stderr, $exit) = run { App::Codeowners->main('--help') }; is($exit, 0, 'exited 0 when --help'); like($stdout, qr/Usage:/, 'correct --help output') or diag $stdout; - ($stdout, $stderr, $exit) = capture { exit_code { App::Codeowners->main('--version') } }; + ($stdout, $stderr, $exit) = run { App::Codeowners->main('--version') }; is($exit, 0, 'exited 0 when --version'); like($stdout, qr/git-codeowners [\d.]+\n/, 'correct --version output') or diag $stdout; }; subtest 'bad options' => sub { - my $repodir = _setup_git_repo(); - my $chdir = pushd($repodir); - - my ($stdout, $stderr, $exit) = capture { exit_code { App::Codeowners->main(qw{show --not-an-option}) } }; + my ($stdout, $stderr, $exit) = run { App::Codeowners->main(qw{show --not-an-option}) }; is($exit, 2, 'exited with error on bad option'); like($stderr, qr/Unknown option: not-an-option/, 'correct error message') or diag $stderr; }; subtest 'show' => sub { + plan skip_all => 'Cannot run git' if !$can_git; + my $repodir = _setup_git_repo(); my $chdir = pushd($repodir); - my ($stdout, $stderr, $exit) = capture { exit_code { App::Codeowners->main(qw{-f %F;%O show}) } }; + my ($stdout, $stderr, $exit) = run { App::Codeowners->main(qw{-f %F;%O show}) }; is($exit, 0, 'exited without error'); is($stdout, <<'END', 'correct output'); CODEOWNERS; @@ -54,7 +54,7 @@ a/b/c/bar.txt;@snickers foo.txt;@twix END - ($stdout, $stderr, $exit) = capture { exit_code { App::Codeowners->main(qw{-f %F;%O;%P show}) } }; + ($stdout, $stderr, $exit) = run { App::Codeowners->main(qw{-f %F;%O;%P show}) }; is($exit, 0, 'exited without error'); is($stdout, <<'END', 'correct output'); CODEOWNERS;; @@ -65,7 +65,7 @@ END subtest 'format json' => sub { plan skip_all => 'No JSON::MaybeXS' if !eval { require JSON::MaybeXS }; - ($stdout, $stderr, $exit) = capture { exit_code { App::Codeowners->main(qw{-f json show --no-project}) } }; + ($stdout, $stderr, $exit) = run { App::Codeowners->main(qw{-f json show --no-projects}) }; is($exit, 0, 'exited without error'); my $expect = '[{"File":"CODEOWNERS","Owner":null},{"File":"a/b/c/bar.txt","Owner":["@snickers"]},{"File":"foo.txt","Owner":["@twix"]}]'; is($stdout, $expect, 'correct output with json format'); @@ -76,8 +76,10 @@ done_testing; exit; sub _can_git { - my ($version) = run_git('--version'); - return $version; + my (undef, $version) = eval { run_git('--version') }; + note $@ if $@; + note "Found: $version" if $version; + return $version && $version ge 'git version 1.8.5'; # for -C flag } sub _setup_git_repo { @@ -92,9 +94,9 @@ sub _setup_git_repo { a/ @snickers END - run_git('-C', $repodir, qw{init}); - run_git('-C', $repodir, qw{add .}); - run_git('-C', $repodir, qw{commit -m}, 'initial commit'); + run_git('-C', $repodir, qw{init})->wait; + run_git('-C', $repodir, qw{add .})->wait; + run_git('-C', $repodir, qw{commit -m}, 'initial commit')->wait; return $repodir; } diff --git a/xt/author/eol.t b/xt/author/eol.t index 37c6b05..0f00e36 100644 --- a/xt/author/eol.t +++ b/xt/author/eol.t @@ -9,6 +9,13 @@ use Test::EOL; my @files = ( 'bin/git-codeowners', 'lib/App/Codeowners.pm', + 'lib/App/Codeowners/Formatter.pm', + 'lib/App/Codeowners/Formatter/CSV.pm', + 'lib/App/Codeowners/Formatter/JSON.pm', + 'lib/App/Codeowners/Formatter/String.pm', + 'lib/App/Codeowners/Formatter/TSV.pm', + 'lib/App/Codeowners/Formatter/Table.pm', + 'lib/App/Codeowners/Formatter/YAML.pm', 'lib/App/Codeowners/Options.pm', 'lib/App/Codeowners/Util.pm', 'lib/File/Codeowners.pm', diff --git a/xt/author/no-tabs.t b/xt/author/no-tabs.t index 1d28f3b..0872e0d 100644 --- a/xt/author/no-tabs.t +++ b/xt/author/no-tabs.t @@ -9,6 +9,13 @@ use Test::NoTabs; my @files = ( 'bin/git-codeowners', 'lib/App/Codeowners.pm', + 'lib/App/Codeowners/Formatter.pm', + 'lib/App/Codeowners/Formatter/CSV.pm', + 'lib/App/Codeowners/Formatter/JSON.pm', + 'lib/App/Codeowners/Formatter/String.pm', + 'lib/App/Codeowners/Formatter/TSV.pm', + 'lib/App/Codeowners/Formatter/Table.pm', + 'lib/App/Codeowners/Formatter/YAML.pm', 'lib/App/Codeowners/Options.pm', 'lib/App/Codeowners/Util.pm', 'lib/File/Codeowners.pm',