File Coverage

blib/lib/App/Codeowners/Util.pm
Criterion Covered Total %
statement 112 127 88.1
branch 34 58 58.6
condition 15 27 55.5
subroutine 22 24 91.6
pod 2 11 18.1
total 185 247 74.9


line stmt bran cond sub pod time code
1             package App::Codeowners::Util;
2             # ABSTRACT: Grab bag of utility subs for Codeowners modules
3              
4              
5 2     2   71210 use warnings;
  2         20  
  2         60  
6 2     2   9 use strict;
  2         3  
  2         54  
7              
8 2     2   1033 use Encode qw(decode);
  2         18790  
  2         135  
9 2     2   13 use Exporter qw(import);
  2         4  
  2         49  
10 2     2   1412 use Path::Tiny;
  2         25680  
  2         3514  
11              
12             our @EXPORT_OK = qw(
13             colorstrip
14             find_codeowners_in_directory
15             find_nearest_codeowners
16             git_ls_files
17             git_toplevel
18             run_command
19             run_git
20             stringf
21             stringify
22             unbackslash
23             zip
24             );
25              
26             our $VERSION = '0.49'; # VERSION
27              
28              
29             sub find_nearest_codeowners {
30 1   50 1 1 3254 my $path = path(shift || '.')->absolute;
31              
32 1         66 while (!$path->is_rootdir) {
33 4         191 my $filepath = find_codeowners_in_directory($path);
34 4 100       79 return $filepath if $filepath;
35 3         13 $path = $path->parent;
36             }
37             }
38              
39              
40             sub find_codeowners_in_directory {
41 10 50   10 1 3391 my $path = path(shift) or die;
42              
43 10         490 my @tries = (
44             [qw(CODEOWNERS)],
45             [qw(docs CODEOWNERS)],
46             [qw(.bitbucket CODEOWNERS)],
47             [qw(.github CODEOWNERS)],
48             [qw(.gitlab CODEOWNERS)],
49             );
50              
51 10         62 for my $parts (@tries) {
52 28         478 my $try = $path->child(@$parts);
53 28 100       992 return $try if $try->is_file;
54             }
55             }
56              
57             sub run_command {
58 40     40 0 226 my $filter;
59 40 100       173 $filter = pop if ref($_[-1]) eq 'CODE';
60              
61 40 50       114 print STDERR "# @_\n" if $ENV{GIT_CODEOWNERS_DEBUG};
62              
63 40         104 my ($child_in, $child_out);
64 40         1313 require IPC::Open2;
65 40         7283 my $pid = IPC::Open2::open2($child_out, $child_in, @_);
66 40         139049 close($child_in);
67              
68 2     2   40 binmode($child_out, ':encoding(UTF-8)');
  2         10  
  2         82  
  40         1692  
69              
70 40         10626 my $proc = App::Codeowners::Util::Process->new(
71             pid => $pid,
72             fh => $child_out,
73             filter => $filter,
74             );
75              
76 40 100       667 return wantarray ? ($proc, @{$proc->all}) : $proc;
  13         102  
77             }
78              
79             sub run_git {
80 40     40 0 115943 return run_command('git', @_);
81             }
82              
83             sub git_ls_files {
84 6   50 6 0 1447 my $dir = shift || '.';
85 6         43 return run_git('-C', $dir, 'ls-files', @_, \&_unescape_git_filepath);
86             }
87              
88             # Depending on git's "core.quotepath" config, non-ASCII chars may be
89             # escaped (identified by surrounding dquotes), so try to unescape.
90             sub _unescape_git_filepath {
91 14 50   14   154 return $_ if $_ !~ /^"(.+)"$/;
92 0         0 return decode('UTF-8', unbackslash($1));
93             }
94              
95             sub git_toplevel {
96 5   50 5 0 2884 my $dir = shift || '.';
97              
98 5         27 my ($proc, $path) = run_git('-C', $dir, qw{rev-parse --show-toplevel});
99              
100 5 50 33     62 return if $proc->wait != 0 || !$path;
101 5         111 return path($path);
102             }
103              
104             sub colorstrip {
105 15   100 15 0 62 my $str = shift || '';
106 15         45 $str =~ s/\e\[[\d;]*m//g;
107 15         76 return $str;
108             }
109              
110             sub stringify {
111 0     0 0 0 my $item = shift;
112 0 0       0 return ref($item) eq 'ARRAY' ? join(',', @$item) : $item;
113             }
114              
115             # The zip code is from List::SomeUtils (thanks DROLSKY), copied just so as not
116             # to bring in the extra dependency.
117             sub zip (\@\@) { ## no critic (Subroutines::ProhibitSubroutinePrototypes)
118 9     9 0 20 my $max = -1;
119 9   66     48 $max < $#$_ && ( $max = $#$_ ) foreach @_;
120             map {
121 9         23 my $ix = $_;
  24         39  
122 24         160 map $_->[$ix], @_;
123             } 0 .. $max;
124             }
125              
126             # The stringf code is from String::Format (thanks SREZIC), with changes:
127             # - Use Unicode::GCString for better Unicode character padding,
128             # - Strip ANSI color sequences,
129             # - Prevent 'Negative repeat count does nothing' warnings
130             sub _replace {
131 15     15   89 my ($args, $orig, $alignment, $min_width,
132             $max_width, $passme, $formchar) = @_;
133              
134             # For unknown escapes, return the orignial
135 15 50       47 return $orig unless defined $args->{$formchar};
136              
137 15 50       41 $alignment = '+' unless defined $alignment;
138              
139 15         29 my $replacement = $args->{$formchar};
140 15 50       39 if (ref $replacement eq 'CODE') {
141             # $passme gets passed to subrefs.
142 15   50     75 $passme ||= "";
143 15         26 $passme =~ tr/{}//d;
144 15         40 $replacement = $replacement->($passme);
145             }
146              
147 15         32 my $replength;
148 15 50       25 if (eval { require Unicode::GCString }) {
  15         960  
149 15         16280 my $gcstring = Unicode::GCString->new(colorstrip($replacement));
150 15         285 $replength = $gcstring->columns;
151             }
152             else {
153 0         0 $replength = length colorstrip($replacement);
154             }
155              
156 15   66     78 $min_width ||= $replength;
157 15   66     49 $max_width ||= $replength;
158              
159             # length of replacement is between min and max
160 15 50 33     37 if (($replength > $min_width) && ($replength < $max_width)) {
161 0         0 return $replacement;
162             }
163              
164             # length of replacement is longer than max; truncate
165 15 50       26 if ($replength > $max_width) {
166 0         0 return substr($replacement, 0, $max_width);
167             }
168              
169 15         29 my $padding = $min_width - $replength;
170 15 50       32 $padding = 0 if $padding < 0;
171              
172             # length of replacement is less than min: pad
173 15 50       26 if ($alignment eq '-') {
174             # left align; pad in front
175 0         0 return $replacement . ' ' x $padding;
176             }
177              
178             # right align, pad at end
179 15         143 return ' ' x $padding . $replacement;
180             }
181             my $regex = qr/
182             (% # leading '%'
183             (-)? # left-align, rather than right
184             (\d*)? # (optional) minimum field width
185             (?:\.(\d*))? # (optional) maximum field width
186             (\{.*?\})? # (optional) stuff inside
187             (\S) # actual format character
188             )/x;
189             sub stringf {
190 6   50 6 0 26 my $format = shift || return;
191 6 50       51 my $args = UNIVERSAL::isa($_[0], 'HASH') ? shift : { @_ };
192 6 50       44 $args->{'n'} = "\n" unless exists $args->{'n'};
193 6 50       26 $args->{'t'} = "\t" unless exists $args->{'t'};
194 6 50       29 $args->{'%'} = "%" unless exists $args->{'%'};
195              
196 6         93 $format =~ s/$regex/_replace($args, $1, $2, $3, $4, $5, $6)/ge;
  15         61  
197              
198 6         30 return $format;
199             }
200              
201             # The unbacklash code is from String::Escape (thanks EVO), with changes:
202             # - Handle \a, \b, \f and \v (thanks Berk Akinci)
203             my %unbackslash;
204             sub unbackslash {
205 0     0 0 0 my $str = shift;
206             # Earlier definitions are preferred to later ones, thus we output \n not \x0d
207             %unbackslash = (
208 0         0 ( map { $_ => $_ } ( '\\', '"', '$', '@' ) ),
209             ( 'r' => "\r", 'n' => "\n", 't' => "\t" ),
210 0         0 ( map { 'x' . unpack('H2', chr($_)) => chr($_) } (0..255) ),
211 0 0       0 ( map { sprintf('%03o', $_) => chr($_) } (0..255) ),
  0         0  
212             ( 'a' => "\x07", 'b' => "\x08", 'f' => "\x0c", 'v' => "\x0b" ),
213             ) if !%unbackslash;
214 0         0 $str =~ s/ (\A|\G|[^\\]) \\ ( [0-7]{3} | x[\da-fA-F]{2} | . ) / $1 . $unbackslash{lc($2)} /gsxe;
  0         0  
215 0         0 return $str;
216             }
217              
218             {
219             package App::Codeowners::Util::Process;
220              
221             sub new {
222 40     40   221 my $class = shift;
223 40         570 return bless {@_}, $class;
224             }
225              
226             sub next {
227 12     12   53 my $self = shift;
228 12         1821 my $line = readline($self->{fh});
229 12 100       100 if (defined $line) {
230 9         20 chomp $line;
231 9 50       34 if (my $filter = $self->{filter}) {
232 9         28 local $_ = $line;
233 9         48 $line = $filter->($line);
234             }
235             }
236 12         91 $line;
237             }
238              
239             sub all {
240 13     13   32 my $self = shift;
241 13         9115 chomp(my @lines = readline($self->{fh}));
242 13 100       908 if (my $filter = $self->{filter}) {
243 3         49 $_ = $filter->($_) for @lines;
244             }
245 13         400 \@lines;
246             }
247              
248             sub wait {
249 76     76   162 my $self = shift;
250 76 100       992 my $pid = $self->{pid} or return;
251 40 50       125 if (my $fh = $self->{fh}) {
252 40         587 close($fh);
253 40         121 delete $self->{fh};
254             }
255 40         106534 waitpid($pid, 0);
256 40         553 my $status = $?;
257 40 50       191 print STDERR "# -> status $status\n" if $ENV{GIT_CODEOWNERS_DEBUG};
258 40         96 delete $self->{pid};
259 40         611 return $status;
260             }
261              
262             sub DESTROY {
263 40     40   638 my ($self, $global_destruction) = @_;
264 40 50       126 return if $global_destruction;
265 40         128 $self->wait;
266             }
267             }
268              
269             1;
270              
271             __END__