]> 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.
 
 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
 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
 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
 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",
          },
          "requires" : {
             "Carp" : "0",
-            "Color::ANSI::Util" : "0",
+            "Color::ANSI::Util" : "0.03",
             "Encode" : "0",
             "Exporter" : "0",
             "Getopt::Long" : "2.39",
             "IPC::Open2" : "0",
             "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",
             "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",
             "perl" : "v5.10.1",
             "strict" : "0",
             "utf8" : "0",
@@ -89,7 +90,7 @@
          "suggests" : {
             "JSON::MaybeXS" : "0",
             "Text::CSV" : "0",
          "suggests" : {
             "JSON::MaybeXS" : "0",
             "Text::CSV" : "0",
-            "Text::Table" : "0",
+            "Text::Table::Any" : "0",
             "YAML" : "0"
          }
       },
             "YAML" : "0"
          }
       },
    "provides" : {
       "App::Codeowners" : {
          "file" : "lib/App/Codeowners.pm",
    "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",
       },
       "App::Codeowners::Options" : {
          "file" : "lib/App/Codeowners/Options.pm",
-         "version" : "0.41"
+         "version" : "0.42"
       },
       "App::Codeowners::Util" : {
          "file" : "lib/App/Codeowners/Util.pm",
       },
       "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",
       },
       "File::Codeowners" : {
          "file" : "lib/File/Codeowners.pm",
-         "version" : "0.41"
+         "version" : "0.42"
       },
       "Test::File::Codeowners" : {
          "file" : "lib/Test/File/Codeowners.pm",
       },
       "Test::File::Codeowners" : {
          "file" : "lib/Test/File/Codeowners.pm",
-         "version" : "0.41"
+         "version" : "0.42"
       }
    },
    "release_status" : "stable",
       }
    },
    "release_status" : "stable",
          "web" : "https://github.com/chazmcgarvey/git-codeowners"
       }
    },
          "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"
    "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
 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
   App::Codeowners::Options:
     file: lib/App/Codeowners/Options.pm
-    version: '0.41'
+    version: '0.42'
   App::Codeowners::Util:
     file: lib/App/Codeowners/Util.pm
   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
   File::Codeowners:
     file: lib/File/Codeowners.pm
-    version: '0.41'
+    version: '0.42'
   Test::File::Codeowners:
     file: lib/Test/File/Codeowners.pm
   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'
 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'
   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'
   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'
   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
   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'
 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,
   "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,
     "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,
     "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
     "strict" => 0,
     "utf8" => 0,
     "warnings" => 0
@@ -47,7 +48,7 @@ my %WriteMakefileArgs = (
     "Test::Exit" => 0,
     "Test::More" => 0
   },
     "Test::Exit" => 0,
     "Test::More" => 0
   },
-  "VERSION" => "0.41",
+  "VERSION" => "0.42",
   "test" => {
     "TESTS" => "t/*.t"
   }
   "test" => {
     "TESTS" => "t/*.t"
   }
@@ -57,7 +58,7 @@ my %WriteMakefileArgs = (
 my %FallbackPrereqs = (
   "Capture::Tiny" => 0,
   "Carp" => 0,
 my %FallbackPrereqs = (
   "Capture::Tiny" => 0,
   "Carp" => 0,
-  "Color::ANSI::Util" => 0,
+  "Color::ANSI::Util" => "0.03",
   "Encode" => 0,
   "Exporter" => 0,
   "ExtUtils::MakeMaker" => 0,
   "Encode" => 0,
   "Exporter" => 0,
   "ExtUtils::MakeMaker" => 0,
@@ -68,6 +69,7 @@ my %FallbackPrereqs = (
   "IO::Handle" => 0,
   "IPC::Open2" => 0,
   "IPC::Open3" => 0,
   "IO::Handle" => 0,
   "IPC::Open2" => 0,
   "IPC::Open3" => 0,
+  "Module::Load" => 0,
   "Path::Tiny" => 0,
   "Pod::Usage" => 0,
   "Scalar::Util" => 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,
   "Test::Exit" => 0,
   "Test::More" => 0,
   "Text::Gitignore" => 0,
-  "Text::Table::Any" => 0,
+  "parent" => 0,
   "strict" => 0,
   "utf8" => 0,
   "warnings" => 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
 
-    version 0.41
+    version 0.42
 
 SYNOPSIS
 
         git-codeowners [--version|--help|--manual]
     
 
 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]
     
     
         git-codeowners owners [--format FORMAT] [--pattern PATTERN]
     
@@ -100,18 +102,35 @@ COMMANDS
 
  show
 
 
  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.
 
 
     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]
 
  owners
 
         git-codeowners owners [--format FORMAT] [--pattern PATTERN]
 
+    List all owners defined in the CODEOWNERS file.
+
  patterns
 
         git-codeowners patterns [--format FORMAT] [--owner OWNER]
 
  patterns
 
         git-codeowners patterns [--format FORMAT] [--owner OWNER]
 
+    List all patterns defined in the CODEOWNERS file.
+
  create
 
         git-codeowners create [REPO_DIRPATH|CODEOWNERS_FILEPATH]
  create
 
         git-codeowners create [REPO_DIRPATH|CODEOWNERS_FILEPATH]
@@ -144,7 +163,7 @@ FORMAT
 
       * FORMAT - Custom format (see below)
 
 
       * 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:
 
     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.
 
 
       * 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
 
     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.
 
 
     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
 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;
 
 
 use App::Codeowners;
 
-our $VERSION = '0.41'; # VERSION
+our $VERSION = '0.42'; # VERSION
 
 App::Codeowners->main(@ARGV);
 
 
 App::Codeowners->main(@ARGV);
 
@@ -26,13 +26,15 @@ git-codeowners - A tool for managing CODEOWNERS files
 
 =head1 VERSION
 
 
 =head1 VERSION
 
-version 0.41
+version 0.42
 
 =head1 SYNOPSIS
 
     git-codeowners [--version|--help|--manual]
 
 
 =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]
 
 
     git-codeowners owners [--format FORMAT] [--pattern PATTERN]
 
@@ -128,18 +130,33 @@ Does not yet support Zsh...
 
 =head2 show
 
 
 =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.
 
 
 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]
 
 =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]
 
 =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]
 =head2 create
 
     git-codeowners create [REPO_DIRPATH|CODEOWNERS_FILEPATH]
@@ -189,7 +206,7 @@ C<FORMAT> - Custom format (see below)
 
 =back
 
 
 =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:
 
 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
 
 
 =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
 
 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>.
 
 
 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
 =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 warnings;
 use strict;
 
+use App::Codeowners::Formatter;
 use App::Codeowners::Options;
 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;
 
 use Encode qw(encode);
 use File::Codeowners;
 use Path::Tiny;
 
-our $VERSION = '0.41'; # VERSION
+our $VERSION = '0.42'; # VERSION
 
 
 sub main {
 
 
 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);
 
         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));
         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,
             $filepath,
+            $opts->{patterns} ? $match->{pattern} : (),
             $match->{owners},
             $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 {
 }
 
 sub _command_owners {
@@ -77,12 +104,12 @@ sub _command_owners {
 
     my $results = $codeowners->owners($opts->{pattern});
 
 
     my $results = $codeowners->owners($opts->{pattern});
 
-    _format(
+    my $formatter = App::Codeowners::Formatter->new(
         format  => $opts->{format} || '%O',
         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 {
 }
 
 sub _command_patterns {
@@ -97,12 +124,32 @@ sub _command_patterns {
 
     my $results = $codeowners->patterns($opts->{owner});
 
 
     my $results = $codeowners->patterns($opts->{owner});
 
-    _format(
+    my $formatter = App::Codeowners::Formatter->new(
         format  => $opts->{format} || '%T',
         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 }
 }
 
 sub _command_create { goto &_command_update }
@@ -158,167 +205,6 @@ END
     print STDERR "Wrote $path\n";
 }
 
     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__
 1;
 
 __END__
@@ -333,7 +219,7 @@ App::Codeowners - A tool for managing CODEOWNERS files
 
 =head1 VERSION
 
 
 =head1 VERSION
 
-version 0.41
+version 0.42
 
 =head1 METHODS
 
 
 =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;
 
 use Path::Tiny;
 use Pod::Usage;
 
-our $VERSION = '0.41'; # VERSION
+our $VERSION = '0.42'; # VERSION
 
 sub early_options {
     return {
 
 sub early_options {
     return {
@@ -30,8 +30,13 @@ sub command_options {
         'patterns'  => {
             'owner=s'   => '',
         },
         'patterns'  => {
             'owner=s'   => '',
         },
+        'projects'  => {},
         'show'      => {
         'show'      => {
-            'project!'  => 1,
+            'owner=s@'      => [],
+            'pattern=s@'    => [],
+            'project=s@'    => [],
+            'patterns!'     => 0,
+            'projects!'     => undef,
         },
         'update'    => {},
     };
         },
         'update'    => {},
     };
@@ -84,7 +89,7 @@ sub new {
         exit 0;
     }
     if ($opts->{help}) {
         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);
     }
     if ($opts->{manual}) {
         pod2usage(-exitval => 0, -verbose => 2);
@@ -273,7 +278,7 @@ App::Codeowners::Options - Getopt and shell completion for App::Codeowners
 
 =head1 VERSION
 
 
 =head1 VERSION
 
-version 0.41
+version 0.42
 
 =head1 METHODS
 
 
 =head1 METHODS
 
index cb0b795d676088abcf62af9f46d09031499b3bff..762f040e0a21153c49f32ed809b4d64a2bf238aa 100644 (file)
@@ -15,12 +15,15 @@ our @EXPORT_OK = qw(
     find_nearest_codeowners
     git_ls_files
     git_toplevel
     find_nearest_codeowners
     git_ls_files
     git_toplevel
+    run_command
     run_git
     stringf
     run_git
     stringf
+    stringify
     unbackslash
     unbackslash
+    zip
 );
 
 );
 
-our $VERSION = '0.41'; # VERSION
+our $VERSION = '0.42'; # VERSION
 
 
 sub find_nearest_codeowners {
 
 
 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 ($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)');
     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 || '.';
 }
 
 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 || '.';
 
 }
 
 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 path($path);
 }
 
@@ -103,6 +107,22 @@ sub colorstrip {
     return $str;
 }
 
     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,
 # 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;
 }
 
     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__
 1;
 
 __END__
@@ -209,7 +280,7 @@ App::Codeowners::Util - Grab bag of utility subs for Codeowners modules
 
 =head1 VERSION
 
 
 =head1 VERSION
 
-version 0.41
+version 0.42
 
 =head1 DESCRIPTION
 
 
 =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);
 
 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 _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;
 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->{match_lines};
     delete $self->{owners};
     delete $self->{patterns};
+    delete $self->{projects};
 }
 
 1;
 }
 
 1;
@@ -335,7 +354,7 @@ File::Codeowners - Read and write CODEOWNERS files
 
 =head1 VERSION
 
 
 =head1 VERSION
 
-version 0.41
+version 0.42
 
 =head1 METHODS
 
 
 =head1 METHODS
 
@@ -432,6 +451,12 @@ defined in the file.
 
 Get an arrayref of all patterns defined.
 
 
 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);
 =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;
 
 use File::Codeowners;
 use Test::Builder;
 
-our $VERSION = '0.41'; # VERSION
+our $VERSION = '0.42'; # VERSION
 
 my $Test = Test::Builder->new;
 
 
 my $Test = Test::Builder->new;
 
@@ -49,11 +49,11 @@ sub codeowners_git_files_ok {
             return;
         }
 
             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);
             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
 
 
 =head1 VERSION
 
-version 0.41
+version 0.42
 
 =head1 SYNOPSIS
 
 
 =head1 SYNOPSIS
 
index 379fc9b02bc64fea6541ae51060b6a9a7cb91a46..37c2ffc8d5a5f482ee6910e755a511da20ea984b 100644 (file)
@@ -6,10 +6,17 @@ use warnings;
 
 use Test::More;
 
 
 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',
 
 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',
     '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',
                                       },
                       'requires' => {
                                       'Carp' => '0',
-                                      'Color::ANSI::Util' => '0',
+                                      'Color::ANSI::Util' => '0.03',
                                       'Encode' => '0',
                                       'Exporter' => '0',
                                       'Getopt::Long' => '2.39',
                                       'IPC::Open2' => '0',
                                       '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',
                                       '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',
                                       'perl' => 'v5.10.1',
                                       'strict' => '0',
                                       'utf8' => '0',
@@ -65,7 +66,7 @@ do { my $x = {
                       'suggests' => {
                                       'JSON::MaybeXS' => '0',
                                       'Text::CSV' => '0',
                       'suggests' => {
                                       'JSON::MaybeXS' => '0',
                                       'Text::CSV' => '0',
-                                      'Text::Table' => '0',
+                                      'Text::Table::Any' => '0',
                                       'YAML' => '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{
 use Test::More;
 
 can_ok('App::Codeowners::Util', qw{
-    find_nearest_codeowners
+    colorstrip
     find_codeowners_in_directory
     find_codeowners_in_directory
-    run_git
+    find_nearest_codeowners
     git_ls_files
     git_toplevel
     git_ls_files
     git_toplevel
+    run_command
+    run_git
+    stringf
+    stringify
+    unbackslash
+    zip
 });
 
 my $can_git = _can_git();
 });
 
 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();
 
     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)
         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 'git_toplevel' => sub {
@@ -45,7 +51,9 @@ subtest 'git_toplevel' => sub {
 };
 
 subtest 'find_nearest_codeowners' => sub {
 };
 
 subtest 'find_nearest_codeowners' => sub {
+    plan skip_all => 'Cannot run git' if !$can_git;
     my $repodir =_setup_git_repo();
     my $repodir =_setup_git_repo();
+
     $repodir->child('docs')->mkpath;
     my $filepath = _spew_codeowners($repodir->child('docs/CODEOWNERS'));
 
     $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 {
 };
 
 subtest 'find_codeowners_in_directory' => sub {
+    plan skip_all => 'Cannot run git' if !$can_git;
     my $repodir =_setup_git_repo();
     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);
     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 {
 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;
 
 }
 
 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;
 
     $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();
 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;
 
 
 # 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;
 
     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 {
     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 {
     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 $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;
     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
 
 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;;
     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 };
 
     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');
         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 {
 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 {
 }
 
 sub _setup_git_repo {
@@ -92,9 +94,9 @@ sub _setup_git_repo {
 a/  @snickers
 END
 
 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;
 }
 
     return $repodir;
 }
index 37c6b05a203f87daa20503f50d4ea3c8857a0ea2..0f00e36187ee466a353d4948809ebb86d65719fb 100644 (file)
@@ -9,6 +9,13 @@ use Test::EOL;
 my @files = (
     'bin/git-codeowners',
     'lib/App/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',
     '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',
 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',
     'lib/App/Codeowners/Options.pm',
     'lib/App/Codeowners/Util.pm',
     'lib/File/Codeowners.pm',
This page took 0.099184 seconds and 4 git commands to generate.