]> Dogcows Code - chaz/git-codeowners/commitdiff
Version 0.42
authorCharles McGarvey <chazmcgarvey@brokenzipper.com>
Wed, 13 Nov 2019 04:53:15 +0000 (21:53 -0700)
committerCharles McGarvey <chazmcgarvey@brokenzipper.com>
Wed, 13 Nov 2019 04:53:15 +0000 (21:53 -0700)
25 files changed:
Changes
MANIFEST
META.json
META.yml
Makefile.PL
README
bin/git-codeowners
lib/App/Codeowners.pm
lib/App/Codeowners/Formatter.pm [new file with mode: 0644]
lib/App/Codeowners/Formatter/CSV.pm [new file with mode: 0644]
lib/App/Codeowners/Formatter/JSON.pm [new file with mode: 0644]
lib/App/Codeowners/Formatter/String.pm [new file with mode: 0644]
lib/App/Codeowners/Formatter/TSV.pm [new file with mode: 0644]
lib/App/Codeowners/Formatter/Table.pm [new file with mode: 0644]
lib/App/Codeowners/Formatter/YAML.pm [new file with mode: 0644]
lib/App/Codeowners/Options.pm
lib/App/Codeowners/Util.pm
lib/File/Codeowners.pm
lib/Test/File/Codeowners.pm
t/00-compile.t
t/00-report-prereqs.dd
t/app-codeowners-util.t
t/app-codeowners.t
xt/author/eol.t
xt/author/no-tabs.t

diff --git a/Changes b/Changes
index 91ff0ab89b4932ff64b5b251c87125099ca91528..9b62078085faaa4479e9bde90132dd8c21a2311e 100644 (file)
--- 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.
index f072e924840cfc090fb6559873c52b0a9d63fd2f..8d701ef2081eb900a338c21b67b0981217bb9f5e 100644 (file)
--- 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
index 773b23b1a291dc15b3d13001f6113b2d35c6f941..cf36b2ded0e83799923f5ee517c07e63512d081b 100644 (file)
--- a/META.json
+++ b/META.json
          },
          "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"
          }
       },
    "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"
index 78c7ee4f761d536f3138b492ff83cf460ce36533..838d5888c8b168e444c031c3efa9792635130352 100644 (file)
--- 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'
index c4ebbbe2e456d7e2d229f5fc3cfb0fa1a0af5016..db4c282b449cadba8a9363eb80d3bc17b3b7cdc2 100644 (file)
@@ -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 a5e5712b2790ec9ac9530476265e44cfb468ec85..98bbc5b1692973b64e2f6f65a76fc27138632b22 100644 (file)
--- 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
index a0bc3feb986aae9be7371b4e18599e6d854946d3..2c680eb0ff91183dfb1358f1cb368da94ded5a2f 100755 (executable)
@@ -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<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]
@@ -189,7 +206,7 @@ C<FORMAT> - 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<nocolor> - 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<Text::Table::Tiny>, but this can be overridden using the
@@ -260,6 +277,16 @@ C<PERL_TEXT_TABLE> environment variable if desired, like this:
 
 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
index 626fc82b20a75520522c527db01ba601d86d6359..e70a109cf57ba499517201d4f949663407c555b8 100644 (file)
@@ -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 (file)
index 0000000..095a6ef
--- /dev/null
@@ -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<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
diff --git a/lib/App/Codeowners/Formatter/CSV.pm b/lib/App/Codeowners/Formatter/CSV.pm
new file mode 100644 (file)
index 0000000..a60dc94
--- /dev/null
@@ -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<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
diff --git a/lib/App/Codeowners/Formatter/JSON.pm b/lib/App/Codeowners/Formatter/JSON.pm
new file mode 100644 (file)
index 0000000..2ead10c
--- /dev/null
@@ -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<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
diff --git a/lib/App/Codeowners/Formatter/String.pm b/lib/App/Codeowners/Formatter/String.pm
new file mode 100644 (file)
index 0000000..3342331
--- /dev/null
@@ -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<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
diff --git a/lib/App/Codeowners/Formatter/TSV.pm b/lib/App/Codeowners/Formatter/TSV.pm
new file mode 100644 (file)
index 0000000..b2f22bd
--- /dev/null
@@ -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<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
diff --git a/lib/App/Codeowners/Formatter/Table.pm b/lib/App/Codeowners/Formatter/Table.pm
new file mode 100644 (file)
index 0000000..df9bf39
--- /dev/null
@@ -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<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
diff --git a/lib/App/Codeowners/Formatter/YAML.pm b/lib/App/Codeowners/Formatter/YAML.pm
new file mode 100644 (file)
index 0000000..00b4d1e
--- /dev/null
@@ -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<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
index 950f0b89a1635dfa53d0387432fed9ffecd7ec5e..1bd3c0f533e4e8d05e29412fdf72d2fb5a13f177 100644 (file)
@@ -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
 
index cb0b795d676088abcf62af9f46d09031499b3bff..762f040e0a21153c49f32ed809b4d64a2bf238aa 100644 (file)
@@ -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
 
index f987561554bfdbe464169a490c2964ec1aa759ec..e7b23deebded4c7e6bbe5a8e98f0624db98d837f 100644 (file)
@@ -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);
index 44a384f9fb777505f20fd30e573b14fb014abb7f..a166e71f21f7be9f58059f50aa80e978b9b73a04 100644 (file)
@@ -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
 
index 379fc9b02bc64fea6541ae51060b6a9a7cb91a46..37c2ffc8d5a5f482ee6910e755a511da20ea984b 100644 (file)
@@ -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',
index 2f32b18e2edc107b7989136aa8a767ef57729436..0c517b6074943fb486fae2ffc54d526132fd5ac4 100644 (file)
@@ -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'
                                     }
                     },
index 93fdce4cefce62897e471b098b376b2caf39c667..2edbcc79122b0f855a341688ff37bc3b4856aaf8 100644 (file)
@@ -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;
index 5d378416919d4064818aef56cc04c6e9c182817e..28309d3861ca0b802cf938133128494302850377 100644 (file)
@@ -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;
 }
index 37c6b05a203f87daa20503f50d4ea3c8857a0ea2..0f00e36187ee466a353d4948809ebb86d65719fb 100644 (file)
@@ -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',
index 1d28f3bf588e2b6437de2768ca5c01a96046c57d..0872e0db8d11263db96a6a3bcc8b69619cecb1bd 100644 (file)
@@ -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',
This page took 0.07932 seconds and 4 git commands to generate.