1 package App
::Codeowners
::Util
;
2 # ABSTRACT: Grab bag of utility subs for Codeowners modules
6 B<DO NOT USE> except in L<App::Codeowners> and related modules.
13 use Encode
qw(decode);
14 use Exporter
qw(import);
19 find_codeowners_in_directory
20 find_nearest_codeowners
31 our $VERSION = '9999.999'; # VERSION
33 =func find_nearest_codeowners
35 $filepath = find_nearest_codeowners
($dirpath);
37 Find the F
<CODEOWNERS
> file
in the current working directory
, or search
in the
38 parent directory recursively
until a F
<CODEOWNERS
> file
is found
.
40 Returns C
<undef> if no F
<CODEOWNERS
> is found
.
44 sub find_nearest_codeowners
{
45 my $path = path
(shift || '.')->absolute;
47 while (!$path->is_rootdir) {
48 my $filepath = find_codeowners_in_directory
($path);
49 return $filepath if $filepath;
50 $path = $path->parent;
54 =func find_codeowners_in_directory
56 $filepath = find_codeowners_in_directory
($dirpath);
58 Find the F
<CODEOWNERS
> file
in a
given directory
. No recursive searching
is done
.
60 Returns the first of
(or undef if none found
):
65 * F<.bitbucket/CODEOWNERS>
66 * F<.github/CODEOWNERS>
67 * F<.gitlab/CODEOWNERS>
71 sub find_codeowners_in_directory
{
72 my $path = path
(shift) or die;
76 [qw(docs CODEOWNERS)],
77 [qw(.bitbucket CODEOWNERS)],
78 [qw(.github CODEOWNERS)],
79 [qw(.gitlab CODEOWNERS)],
82 for my $parts (@tries) {
83 my $try = $path->child(@$parts);
84 return $try if $try->is_file;
90 $filter = pop if ref($_[-1]) eq 'CODE';
92 my ($child_in, $child_out);
94 my $pid = IPC
::Open2
::open2
($child_out, $child_in, @_);
97 binmode($child_out, ':encoding(UTF-8)');
99 my $proc = App
::Codeowners
::Util
::Process-
>new(
105 return wantarray ? ($proc, @{$proc->all}) : $proc;
109 return run_command
('git', @_);
113 my $dir = shift || '.';
114 return run_git
('-C', $dir, 'ls-files', @_, \
&_unescape_git_filepath
);
117 # Depending on git's "core.quotepath" config, non-ASCII chars may be
118 # escaped (identified by surrounding dquotes), so try to unescape.
119 sub _unescape_git_filepath
{
120 return $_ if $_ !~ /^"(.+)"$/;
121 return decode
('UTF-8', unbackslash
($1));
125 my $dir = shift || '.';
127 my ($proc, $path) = run_git
('-C', $dir, qw{rev-parse --show-toplevel});
129 return if $proc->wait != 0 || !$path;
134 my $str = shift || '';
135 $str =~ s/\e\[[\d;]*m//g;
141 return ref($item) eq 'ARRAY' ? join(',', @$item) : $item;
144 # The zip code is from List::SomeUtils (thanks DROLSKY), copied just so as not
145 # to bring in the extra dependency.
146 sub zip
(\
@\
@) { ## no critic (Subroutines::ProhibitSubroutinePrototypes)
148 $max < $#$_ && ( $max = $#$_ ) foreach @_;
155 # The stringf code is from String::Format (thanks SREZIC), with changes:
156 # - Use Unicode::GCString for better Unicode character padding,
157 # - Strip ANSI color sequences,
158 # - Prevent 'Negative repeat count does nothing' warnings
160 my ($args, $orig, $alignment, $min_width,
161 $max_width, $passme, $formchar) = @_;
163 # For unknown escapes, return the orignial
164 return $orig unless defined $args->{$formchar};
166 $alignment = '+' unless defined $alignment;
168 my $replacement = $args->{$formchar};
169 if (ref $replacement eq 'CODE') {
170 # $passme gets passed to subrefs.
173 $replacement = $replacement->($passme);
177 if (eval { require Unicode
::GCString
}) {
178 my $gcstring = Unicode
::GCString-
>new(colorstrip
($replacement));
179 $replength = $gcstring->columns;
182 $replength = length colorstrip
($replacement);
185 $min_width ||= $replength;
186 $max_width ||= $replength;
188 # length of replacement is between min and max
189 if (($replength > $min_width) && ($replength < $max_width)) {
193 # length of replacement is longer than max; truncate
194 if ($replength > $max_width) {
195 return substr($replacement, 0, $max_width);
198 my $padding = $min_width - $replength;
199 $padding = 0 if $padding < 0;
201 # length of replacement is less than min: pad
202 if ($alignment eq '-') {
203 # left align; pad in front
204 return $replacement . ' ' x
$padding;
207 # right align, pad at end
208 return ' ' x
$padding . $replacement;
212 (-)? # left-align, rather than right
213 (\d
*)? # (optional) minimum field width
214 (?:\
.(\d
*))? # (optional) maximum field width
215 (\
{.*?\
})? # (optional) stuff inside
216 (\S
) # actual format character
219 my $format = shift || return;
220 my $args = UNIVERSAL
::isa
($_[0], 'HASH') ? shift : { @_ };
221 $args->{'n'} = "\n" unless exists $args->{'n'};
222 $args->{'t'} = "\t" unless exists $args->{'t'};
223 $args->{'%'} = "%" unless exists $args->{'%'};
225 $format =~ s/$regex/_replace($args, $1, $2, $3, $4, $5, $6)/ge;
230 # The unbacklash code is from String::Escape (thanks EVO), with changes:
231 # - Handle \a, \b, \f and \v (thanks Berk Akinci)
235 # Earlier definitions are preferred to later ones, thus we output \n not \x0d
237 ( map { $_ => $_ } ( '\\', '"', '$', '@' ) ),
238 ( 'r' => "\r", 'n' => "\n", 't' => "\t" ),
239 ( map { 'x' . unpack('H2', chr($_)) => chr($_) } (0..255) ),
240 ( map { sprintf('%03o', $_) => chr($_) } (0..255) ),
241 ( 'a' => "\x07", 'b' => "\x08", 'f' => "\x0c", 'v' => "\x0b" ),
243 $str =~ s/ (\A|\G|[^\\]) \\ ( [0-7]{3} | x[\da-fA-F]{2} | . ) / $1 . $unbackslash{lc($2)} /gsxe;
248 package App
::Codeowners
::Util
::Process
;
252 return bless {@_}, $class;
257 my $line = readline($self->{fh
});
260 if (my $filter = $self->{filter
}) {
262 $line = $filter->($line);
270 chomp(my @lines = readline($self->{fh
}));
271 if (my $filter = $self->{filter
}) {
272 $_ = $filter->($_) for @lines;
279 my $pid = $self->{pid
} or return;
280 if (my $fh = $self->{fh
}) {
291 my ($self, $global_destruction) = @_;
292 return if $global_destruction;