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.
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
},
"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",
"suggests" : {
"JSON::MaybeXS" : "0",
"Text::CSV" : "0",
- "Text::Table" : "0",
+ "Text::Table::Any" : "0",
"YAML" : "0"
}
},
"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",
"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"
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'
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'
"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
"Test::Exit" => 0,
"Test::More" => 0
},
- "VERSION" => "0.41",
+ "VERSION" => "0.42",
"test" => {
"TESTS" => "t/*.t"
}
my %FallbackPrereqs = (
"Capture::Tiny" => 0,
"Carp" => 0,
- "Color::ANSI::Util" => 0,
+ "Color::ANSI::Util" => "0.03",
"Encode" => 0,
"Exporter" => 0,
"ExtUtils::MakeMaker" => 0,
"IO::Handle" => 0,
"IPC::Open2" => 0,
"IPC::Open3" => 0,
+ "Module::Load" => 0,
"Path::Tiny" => 0,
"Pod::Usage" => 0,
"Scalar::Util" => 0,
"Test::Exit" => 0,
"Test::More" => 0,
"Text::Gitignore" => 0,
- "Text::Table::Any" => 0,
+ "parent" => 0,
"strict" => 0,
"utf8" => 0,
"warnings" => 0
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]
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]
* 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:
* 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
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
use App::Codeowners;
-our $VERSION = '0.41'; # VERSION
+our $VERSION = '0.42'; # VERSION
App::Codeowners->main(@ARGV);
=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]
=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<CODEOWNERS> 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<CODEOWNERS> file.
+
=head2 patterns
git-codeowners patterns [--format FORMAT] [--owner OWNER]
+List all patterns defined in the F<CODEOWNERS> file.
+
=head2 create
git-codeowners create [REPO_DIRPATH|CODEOWNERS_FILEPATH]
=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:
=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<Text::Table::Tiny>, but this can be overridden using the
The list of available modules is at L<Text::Table::Any/@BACKENDS>.
+=head1 CAVEATS
+
+=over 4
+
+=item *
+
+Some commands require F<git> (at least version 1.8.5).
+
+=back
+
=head1 BUGS
Please report any bugs or feature requests on the bugtracker website
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 {
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 {
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 {
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 }
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__
=head1 VERSION
-version 0.41
+version 0.42
=head1 METHODS
--- /dev/null
+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<App::Codeowners::Formatter::CSV>
+
+=item *
+
+L<App::Codeowners::Formatter::String>
+
+=item *
+
+L<App::Codeowners::Formatter::JSON>
+
+=item *
+
+L<App::Codeowners::Formatter::TSV>
+
+=item *
+
+L<App::Codeowners::Formatter::Table>
+
+=item *
+
+L<App::Codeowners::Formatter::YAML>
+
+=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</add_result> 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</finish>.
+
+=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</stream> method.
+
+This method may print a header to the L</handle>. 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</handle>. This
+method is used by subclasses and should typically not called be called explicitly.
+
+The default implementation simply stores the L</results> so they will be available to L</finish>.
+
+=head2 finish
+
+ $formatter->finish;
+
+End formatting results. Called after all results are passed to the L</stream> method.
+
+This method may print a footer to the L</handle>. 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<App::Codeowners::Formatter>
+namespace.
+
+=head1 BUGS
+
+Please report any bugs or feature requests on the bugtracker website
+L<https://github.com/chazmcgarvey/git-codeowners/issues>
+
+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 <chazmcgarvey@brokenzipper.com>
+
+=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
--- /dev/null
+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<App::Codeowners::Formatter> that formats output using L<Text::CSV>.
+
+=head1 ATTRIBUTES
+
+=head2 text_csv
+
+Get the L<Text::CSV> instance.
+
+=head2 sep
+
+Get the value used for L<Text::CSV/sep>.
+
+=head2 quote
+
+Get the value used for L<Text::CSV/quote>.
+
+=head2 escape_char
+
+Get the value used for L<Text::CSV/escape_char>.
+
+=head1 BUGS
+
+Please report any bugs or feature requests on the bugtracker website
+L<https://github.com/chazmcgarvey/git-codeowners/issues>
+
+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 <chazmcgarvey@brokenzipper.com>
+
+=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
--- /dev/null
+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<App::Codeowners::Formatter> that formats output using L<JSON::MaybeXS>.
+
+=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<https://github.com/chazmcgarvey/git-codeowners/issues>
+
+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 <chazmcgarvey@brokenzipper.com>
+
+=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
--- /dev/null
+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<App::Codeowners::Formatter> that formats output using a printf-like string.
+
+See L<git-codeowners/"Format string">.
+
+=head1 BUGS
+
+Please report any bugs or feature requests on the bugtracker website
+L<https://github.com/chazmcgarvey/git-codeowners/issues>
+
+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 <chazmcgarvey@brokenzipper.com>
+
+=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
--- /dev/null
+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<App::Codeowners::Formatter::CSV> that formats output using L<Text::CSV>.
+
+=head1 BUGS
+
+Please report any bugs or feature requests on the bugtracker website
+L<https://github.com/chazmcgarvey/git-codeowners/issues>
+
+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 <chazmcgarvey@brokenzipper.com>
+
+=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
--- /dev/null
+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<App::Codeowners::Formatter> that formats output using L<Text::Table::Any>.
+
+=head1 BUGS
+
+Please report any bugs or feature requests on the bugtracker website
+L<https://github.com/chazmcgarvey/git-codeowners/issues>
+
+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 <chazmcgarvey@brokenzipper.com>
+
+=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
--- /dev/null
+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<App::Codeowners::Formatter> that formats output using L<YAML>.
+
+=head1 BUGS
+
+Please report any bugs or feature requests on the bugtracker website
+L<https://github.com/chazmcgarvey/git-codeowners/issues>
+
+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 <chazmcgarvey@brokenzipper.com>
+
+=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
use Path::Tiny;
use Pod::Usage;
-our $VERSION = '0.41'; # VERSION
+our $VERSION = '0.42'; # VERSION
sub early_options {
return {
'patterns' => {
'owner=s' => '',
},
+ 'projects' => {},
'show' => {
- 'project!' => 1,
+ 'owner=s@' => [],
+ 'pattern=s@' => [],
+ 'project=s@' => [],
+ 'patterns!' => 0,
+ 'projects!' => undef,
},
'update' => {},
};
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);
=head1 VERSION
-version 0.41
+version 0.42
=head1 METHODS
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 {
}
}
-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);
}
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,
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__
=head1 VERSION
-version 0.41
+version 0.42
=head1 DESCRIPTION
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") }
}
+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;
delete $self->{match_lines};
delete $self->{owners};
delete $self->{patterns};
+ delete $self->{projects};
}
1;
=head1 VERSION
-version 0.41
+version 0.42
=head1 METHODS
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);
use File::Codeowners;
use Test::Builder;
-our $VERSION = '0.41'; # VERSION
+our $VERSION = '0.42'; # VERSION
my $Test = Test::Builder->new;
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);
=head1 VERSION
-version 0.41
+version 0.42
=head1 SYNOPSIS
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',
},
'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',
'suggests' => {
'JSON::MaybeXS' => '0',
'Text::CSV' => '0',
- 'Text::Table' => '0',
+ 'Text::Table::Any' => '0',
'YAML' => '0'
}
},
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();
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 {
};
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'));
};
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);
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;
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;
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;;
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');
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 {
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;
}
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',
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',