File Coverage

blib/lib/Doit/Git.pm
Criterion Covered Total %
statement 316 359 88.0
branch 192 246 78.0
condition 24 33 72.7
subroutine 27 27 100.0
pod 10 12 83.3
total 569 677 84.0


line stmt bran cond sub pod time code
1             # -*- perl -*-
2              
3             #
4             # Author: Slaven Rezic
5             #
6             # Copyright (C) 2017,2018,2019,2022,2024 Slaven Rezic. All rights reserved.
7             # This package is free software; you can redistribute it and/or
8             # modify it under the same terms as Perl itself.
9             #
10             # Mail: slaven@rezic.de
11             # WWW: http://www.rezic.de/eserte/
12             #
13              
14             package Doit::Git; # Convention: all commands here should be prefixed with 'git_'
15              
16 2     2   10 use strict;
  2         3  
  2         57  
17 2     2   5 use warnings;
  2         3  
  2         118  
18             our $VERSION = '0.030';
19              
20 2     2   10 use Doit::Log;
  2         4  
  2         112  
21 2     2   8 use Doit::Util qw(in_directory);
  2         3  
  2         7598  
22              
23             sub _pipe_open (@);
24              
25 2     2 0 15 sub new { bless {}, shift }
26 2     2 0 9 sub functions { qw(git_repo_update git_short_status git_root git_get_commit_hash git_get_commit_files git_get_changed_files git_is_shallow git_current_branch git_config git_get_default_branch) }
27              
28             sub git_repo_update {
29 42     42 1 568 my($self, %opts) = @_;
30 42         139 my $repository = delete $opts{repository};
31 42 100       113 my @repository_aliases = @{ delete $opts{repository_aliases} || [] };
  42         626  
32 42         108 my $directory = delete $opts{directory};
33 42   100     247 my $origin = delete $opts{origin} || 'origin';
34 42         106 my $branch = delete $opts{branch};
35 42         74 my $allow_remote_url_change = delete $opts{allow_remote_url_change};
36 42         64 my $clone_opts = delete $opts{clone_opts};
37 42   100     161 my $refresh = delete $opts{refresh} || 'always';
38 42 100       532 if ($refresh !~ m{^(always|never)$}) { error "refresh may be 'always' or 'never'" }
  1         8  
39 41         65 my $quiet = delete $opts{quiet};
40 41 100       230 error "Unhandled options: " . join(" ", %opts) if %opts;
41              
42 40         173 my $has_changes = 0;
43 40         225 my $do_clone;
44 40 100       1576 if (!-e $directory) {
45 13         120 $do_clone = 1;
46             } else {
47 27 100       353 if (!-d $directory) {
48 1         4 error "'$directory' exists, but is not a directory\n";
49             }
50 26 100       374 if (!-d "$directory/.git") {
51 2 100       12 if (_is_dir_empty($directory)) {
52 1         7 $do_clone = 1;
53             } else {
54 1         7 error "No .git directory found in non-empty '$directory', refusing to clone...\n";
55             }
56             }
57             }
58 38 100       190 if (!$do_clone) {
59             in_directory {
60 24     24   50 my $actual_repository = eval { $self->info_qx({quiet=>1}, qw(git config --get), "remote.$origin.url") };
  24         311  
61 24 100       244 if (!defined $actual_repository) {
62             # Remote does not exist yet --- create it.
63 1         22 $self->system(qw(git remote add), $origin, $repository);
64             } else {
65 23         65 chomp $actual_repository;
66 23 100 100     243 if ($actual_repository ne $repository && !grep { $_ eq $actual_repository } @repository_aliases) {
  2         21  
67 3         45 my @change_cmd = ('git', 'remote', 'set-url', $origin, $repository);
68 3 100       18 if ($allow_remote_url_change) {
69 1         42 info "Need to change remote URL for $origin";
70 1         191 $self->system(@change_cmd);
71             } else {
72 2 100       52 error
73             "In $directory: remote $origin does not point to $repository" . (@repository_aliases ? " (or any of the following aliases: @repository_aliases)" : "") . ", but to $actual_repository\n" .
74             "Please run manually\n" .
75             " cd $directory\n" .
76             " @change_cmd\n" .
77             "or specify allow_remote_url_change=>1\n";
78             }
79             }
80             }
81              
82 22         147 my $switch_later;
83 22 100       140 if (defined $branch) { # maybe branch switching necessary?
84 11 100       120 if ($branch =~ m{^refs/remotes/(.*)}) { # extract branch with remote
85 1         27 $branch = $1;
86             }
87 11         189 my $current_branch = $self->git_current_branch;
88 11 100 66     230 if (!defined $current_branch || $current_branch ne $branch) {
89 7 100       25 if (eval { $self->system({show_cwd=>1,quiet=>$quiet}, qw(git checkout), $branch); 1 }) {
  7         183  
  4         267  
90 4         77 $has_changes = 1;
91             } else {
92             # Cannot switch now to the branch. Maybe a
93             # git-fetch has to be done first, as the
94             # branch is not yet in the clone --- try
95             # later.
96 3         18 $switch_later = 1;
97             }
98             }
99 11         41 my %info;
100 11         189 $self->git_current_branch(info_ref => \%info);
101 11 100       886 if ($info{detached}) {
102 5         33 $switch_later = 1; # because a "git pull" wouldn't update a detached branch
103             }
104             }
105              
106 22 100       125 if ($refresh eq 'always') {
107 21         484 $self->system({show_cwd=>1,quiet=>$quiet}, qw(git fetch), $origin);
108 21         1970 my $status = $self->git_short_status(untracked_files => 'no');
109 21 100       283 if ($status =~ m{>$}) {
110             # may actually fail if diverged (status=<>)
111             # or untracked/changed files would get overwritten
112 5         210 $self->system({show_cwd=>1,quiet=>$quiet}, qw(git pull), $origin); # XXX actually would be more efficient to do a merge or rebase, but need to figure out how git does it exactly...
113 4         274 $has_changes = 1;
114             } # else: ahead, diverged, or something else
115             }
116              
117 21 100       588 if ($switch_later) {
118 8         36 my($commit_before, $branch_before);
119 8 100       41 if (!$has_changes) {
120 5         133 $commit_before = $self->git_get_commit_hash;
121 5         91 $branch_before = $self->git_current_branch;
122             }
123 8 100       45 if (!eval { $self->system({show_cwd=>1,quiet=>$quiet}, qw(git checkout), $branch) }) {
  8         322  
124             # Possible reason for the failure: $branch exists
125             # as a remote branch in multiple remotes. Try
126             # again by explicitly specifying the remote.
127             # --track exists since approx git 1.5.1
128 1         39 $self->system({show_cwd=>1,quiet=>$quiet}, qw(git checkout -b), $branch, qw(--track), "$origin/$branch");
129             }
130 8 100 100     400 if ($commit_before
      100        
131             && ( $self->git_get_commit_hash ne $commit_before
132             || $self->git_current_branch ne $branch_before
133             )
134             ) {
135 4         128 $has_changes = 1;
136             }
137             }
138 24         438 } $directory;
139             } else {
140 14         130 my @cmd = (qw(git clone --origin), $origin);
141 14 100       112 if (defined $branch) {
142 2 100       71 if ($branch =~ m{^refs/remotes/[^/]+/(.*)}) { # extract branch without remote
143 1         5 $branch = $1;
144             }
145 2         14 push @cmd, "--branch", $branch;
146             }
147 14 100       71 if ($clone_opts) {
148 1         9 push @cmd, @$clone_opts;
149             }
150 14         39 push @cmd, $repository, $directory;
151 14         163 $self->system(@cmd);
152 14         953 $has_changes = 1;
153             }
154 35         2794 $has_changes;
155             }
156              
157             sub git_short_status {
158 59     59 1 398 my($self, %opts) = @_;
159 59         270 my $directory = delete $opts{directory};
160 59         143 my $untracked_files = delete $opts{untracked_files};
161 59 100       980 if (!defined $untracked_files) {
    100          
162 21         79 $untracked_files = 'normal';
163             } elsif ($untracked_files !~ m{^(normal|no)$}) {
164 1         4 error "only values 'normal' or 'no' supported for untracked_files";
165             }
166 58 100       298 error "Unhandled options: " . join(" ", %opts) if %opts;
167              
168             in_directory {
169 57     57   750 local $ENV{LC_ALL} = 'C';
170              
171 57         101 my $untracked_marker = '';
172             {
173 57         394 my @cmd = ("git", "status", "--untracked-files=$untracked_files", "--porcelain");
174 57 50       203 my $fh = _pipe_open(@cmd)
175             or error "Can't run '@cmd': $!";
176 57         198 my $has_untracked;
177             my $has_uncommitted;
178 57         213582 while (<$fh>) {
179 13 100       224 if (m{^\?\?}) {
180 6         37 $has_untracked++;
181             } else {
182 7         38 $has_uncommitted++;
183             }
184             # Shortcut, exit as early as possible
185 13 100       1112 if ($has_uncommitted) {
186 8 100       619 if ($has_untracked) {
    100          
187 1         217 return '<<*';
188             } elsif ($untracked_files eq 'no') {
189 3         798 return '<<';
190             } # else we have to check further, for possible untracked files
191             }
192             }
193 53 100       361 if ($has_uncommitted) {
    100          
194 3         560 return '<<';
195             } elsif ($has_untracked) {
196 5         25 $untracked_marker = '*'; # will be combined later
197 5         252 last;
198             }
199 45 100       2947 close $fh
200             or error "Error while running '@cmd': $!";
201             }
202              
203             {
204 57         126 my @cmd = ("git", "status", "--untracked-files=no");
  48         149  
  48         551  
205 48 50       424 my $fh = _pipe_open(@cmd)
206             or error "Can't run '@cmd': $!";
207 48         118 my $l;
208 48         168167 $l = <$fh>;
209 48         324 $l = <$fh>;
210 48 100       8935 if ($l =~ m{^(# )?Your branch is ahead}) {
    100          
    100          
211 4         2086 return '<'.$untracked_marker;
212             } elsif ($l =~ m{^(# )?Your branch is behind}) {
213 9         3236 return $untracked_marker.'>';
214             } elsif ($l =~ m{^(# )?Your branch and .* have diverged}) {
215 4         1711 return '<'.$untracked_marker.'>';
216             }
217             }
218              
219 31 50       931 if (-f ".git/svn/.metadata") {
220             # simple-minded heuristics, works only with svn standard branch
221             # layout
222 0         0 my $root_dir = $self->git_root;
223 0 0       0 if (open my $fh_remote, "$root_dir/.git/refs/remotes/trunk") {
224 0 0       0 if (open my $fh_local, "$root_dir/.git/refs/heads/master") {
225 0         0 chomp(my $sha1_remote = <$fh_remote>);
226 0         0 chomp(my $sha1_local = <$fh_local>);
227 0 0       0 if ($sha1_remote ne $sha1_local) {
228 0         0 my $remote_is_newer;
229 0 0       0 if (my $log_fh = _pipe_open('git', 'log', '--pretty=format:%H', 'master..remotes/trunk')) {
230 0 0       0 if (scalar <$log_fh>) {
231 0         0 $remote_is_newer = 1;
232             }
233             }
234 0         0 my $local_is_newer;
235 0 0       0 if (my $log_fh = _pipe_open('git', 'log', '--pretty=format:%H', 'remotes/trunk..master')) {
236 0 0       0 if (scalar <$log_fh>) {
237 0         0 $local_is_newer = 1;
238             }
239             }
240 0 0 0     0 if ($remote_is_newer && $local_is_newer) {
    0          
    0          
241 0         0 return '<'.$untracked_marker.'>';
242             } elsif ($remote_is_newer) {
243 0         0 return $untracked_marker.'>';
244             } elsif ($local_is_newer) {
245 0         0 return '<'.$untracked_marker;
246             } else {
247 0         0 return '?'; # Should never happen
248             }
249             }
250             }
251             }
252             }
253              
254 31         3607 return $untracked_marker;
255              
256 57         1465 } $directory;
257             }
258              
259             sub git_root {
260 65     65 1 300 my($self, %opts) = @_;
261 65         131 my $directory = delete $opts{directory};
262 65 100       207 error "Unhandled options: " . join(" ", %opts) if %opts;
263              
264             in_directory {
265 64     64   698 chomp(my $dir = $self->info_qx({quiet=>1}, 'git', 'rev-parse', '--show-toplevel'));
266 57         3471 $dir;
267 64         629 } $directory;
268             }
269              
270             sub git_get_commit_hash {
271 20     20 1 123 my($self, %opts) = @_;
272 20         50 my $directory = delete $opts{directory};
273 20         45 my $commit = delete $opts{commit};
274 20 100       76 error "Unhandled options: " . join(" ", %opts) if %opts;
275              
276             in_directory {
277 19 100   19   379 chomp(my $commit = $self->info_qx({quiet=>1}, 'git', 'log', '-1', '--format=%H', (defined $commit ? $commit : ())));
278 17         971 $commit;
279 19         364 } $directory;
280             }
281              
282             sub git_get_commit_files {
283 8     8 1 43 my($self, %opts) = @_;
284 8         22 my $directory = delete $opts{directory};
285 8 100       61 my $commit = delete $opts{commit}; if (!defined $commit) { $commit = 'HEAD' }
  8         37  
  6         13  
286 8 100       29 error "Unhandled options: " . join(" ", %opts) if %opts;
287              
288 7         11 my @files;
289             in_directory {
290 6     6   25 my @cmd = ('git', 'show', $commit, '--pretty=format:', '--name-only');
291 6 50       30 my $fh = _pipe_open(@cmd)
292             or error "Error running @cmd: $!";
293 6         11886 my $first = <$fh>;
294 6 100 66     142 if (defined $first && $first ne "\n") { # first line is empty for older git versions (e.g. 1.7.x)
295 3         15 chomp $first;
296 3         31 push @files, $first;
297             }
298 6         433 while(<$fh>) {
299 1         15 chomp;
300 1         293 push @files, $_;
301             }
302 6 100       477 close $fh
303             or error "Error while running @cmd: $!";
304 7         104 } $directory;
305 3         181 @files;
306             }
307              
308             sub git_get_changed_files {
309 11     11 1 44 my($self, %opts) = @_;
310 11         37 my $directory = delete $opts{directory};
311 11         20 my $ignore_untracked = delete $opts{ignore_untracked};
312 11 100       44 error "Unhandled options: " . join(" ", %opts) if %opts;
313              
314 10         36 my @files;
315             in_directory {
316 10     10   41 my @cmd = qw(git status --porcelain);
317 10 50       40 my $fh = _pipe_open(@cmd)
318             or error "Error running @cmd: $!";
319 10         26832 while(<$fh>) {
320 7         46 chomp;
321 7 100 66     352 next if $ignore_untracked && m{^\?\?};
322 6         70 s{^...}{};
323 6         746 push @files, $_;
324             }
325 10 100       762 close $fh
326             or error "Error while running @cmd: $!";
327 10         152 } $directory;
328 8         422 @files;
329             }
330              
331             sub git_is_shallow {
332 5     5 1 22 my($self, %opts) = @_;
333 5         14 my $directory = delete $opts{directory};
334 5 100       25 error "Unhandled options: " . join(" ", %opts) if %opts;
335              
336 4         50 my $git_root = $self->git_root(directory => $directory);
337 2 100       116 -f "$git_root/.git/shallow" ? 1 : 0;
338             }
339              
340             sub git_current_branch {
341 53     53 1 396 my($self, %opts) = @_;
342 53         245 my $directory = delete $opts{directory};
343 53         213 my $info_ref = delete $opts{info_ref};
344 53 100       269 error "Unhandled options: " . join(" ", %opts) if %opts;
345              
346             in_directory {
347 52     52   354 my $git_root = $self->git_root;
348 50         159 my $fh;
349             my $this_head;
350 50 50       3141 if (open $fh, "<", "$git_root/.git/HEAD") {
351 50         695 chomp($this_head = <$fh>);
352 50 100       877 if ($this_head =~ m{refs/heads/(\S+)}) {
353 32         1830 return $1;
354             }
355             }
356              
357             # fallback to git-status
358 18         193 $ENV{LC_ALL} = 'C';
359 18 50       168 if ($fh = _pipe_open(qw(git status))) {
360 18         60276 chomp($_ = <$fh>);
361 18 50       290 if (/^On branch (.*)/) {
362 0 0       0 if ($info_ref) {
363 0         0 $info_ref->{fallback} = 'git-status';
364             }
365 0         0 return $1;
366             }
367 18 50       398 if (/^.* detached at (.*)/) {
368 18 100       97 if ($info_ref) {
369 10         103 $info_ref->{detached} = 1;
370 10         65 $info_ref->{fallback} = 'git-status';
371             }
372 18         4431 return $1;
373             }
374 0 0       0 if (/^\Q# Not currently on any branch./) {
375             # Probably old git (~ 1.5 ... 1.7)
376 0 0       0 if (my $fh2 = _pipe_open(qw(git show-ref))) {
377 0         0 while(<$fh2>) {
378 0         0 chomp;
379 0 0       0 if (my($sha1, $ref) = $_ =~ m{^(\S+)\s+refs/remotes/(.*)$}) {
380 0 0       0 if ($sha1 eq $this_head) {
381 0 0       0 if ($info_ref) {
382 0         0 $info_ref->{detached} = 1;
383 0         0 $info_ref->{fallback} = 'git-show-ref';
384             }
385 0         0 return $ref;
386             }
387             }
388             }
389 0 0       0 close $fh2
390             or warning "Problem while running 'git show-ref': $!";
391             } else {
392 0         0 warning "Error running 'git show-ref': $!";
393             }
394             }
395             }
396              
397 0         0 undef;
398 52         1055 } $directory;
399             }
400              
401             sub git_config {
402 51     51 1 470 my($self, %opts) = @_;
403 51         157 my $directory = delete $opts{directory};
404 51         104 my $key = delete $opts{key};
405 51         112 my $all = delete $opts{all};
406 51         91 my $add = delete $opts{add};
407 51         107 my $val = delete $opts{val};
408 51         168 my $unset = delete $opts{unset};
409 51 100       231 error "Unhandled options: " . join(" ", %opts) if %opts;
410 50 100 100     357 if ($all && defined $val) {
411 1         15 error "Cannot handle 'all' together with 'val'";
412             }
413 49 100       146 if ($add) {
414 6 100       21 if ($unset) {
415 1         9 error "'add' cannot be used together with 'unset'";
416             }
417 5 100       20 if (!defined $val) {
418 1         11 error "'add' must be used together with 'val'";
419             }
420 4 100       13 if (ref $val eq 'ARRAY') {
421 1         45 error "'add' only implemented for single-value 'val'";
422             }
423             }
424 46 100       249 if (ref $val eq 'ARRAY') {
425 7 100       35 if (@$val == 0) { # if array is empty, then just fallback to --unset-all
426 2         7 $unset = 1;
427 2         7 $all = 1;
428             }
429             }
430              
431             in_directory {
432 46     46   86 my $ret = eval { $self->info_qx({quiet=>1}, qw(git config --null --get-all), $key) };
  46         478  
433 46 100       554 my @old_vals = defined $ret ? split(/\0/, $ret) : ();
434 46 100       278 if ($unset) {
435 10 100       96 if ($@) {
436 3 50       11 if ($@->{exitcode} == 1) {
437             # already non-existent (or even invalid)
438 3         218 0;
439             } else {
440 0         0 error "git config $key failed with exitcode $@->{exitcode}";
441             }
442             } else {
443 7 100       39 if ($all) {
444 2 50       21 if (@old_vals) {
445 2         46 $self->system(qw(git config --unset-all), $key);
446 2         291 return 1;
447             } else {
448             # may not be reached, as getting values above probably exited with exitcode=1
449 0         0 return 0;
450             }
451             } else {
452 5         14 my $do_unset = 0;
453 5 100       26 if (defined $val) {
    50          
454 3         17 for my $i (0 .. $#old_vals) {
455 6 100       29 if ($val eq $old_vals[$i]) {
456 2         10 $do_unset = 1;
457 2         11 last;
458             }
459             }
460             } elsif (@old_vals) {
461 2         4 $do_unset = 1;
462             } else {
463             # may not be reached, as getting values above probably exited with exitcode=1
464 0         0 $do_unset = 0;
465             }
466 5 100       21 if ($do_unset) {
467 4         7 eval {
468 4 100       56 $self->system(qw(git config --unset --null), $key, (defined $val ? quotemeta($val) : ()));
469             };
470 4 100       53 if ($@) {
471 1 50       17 if ($@->{exitcode} == 5) {
472 1 50       13 if (@old_vals <= 1) {
473             # "you try to unset an option which does not exist" -> this is accepted
474 0         0 return 0;
475             } else {
476 1         18 error "Multiple values when using 'unset', please specify 'all => 1' if wanted";
477             }
478             } else {
479 0         0 error $@;
480             }
481             }
482 3         323 return 1;
483             } else {
484 1         94 return 0;
485             }
486             }
487             }
488             } else {
489 36 100       187 if (!defined $val) {
490 21 100       57 if ($all) {
491 7         622 @old_vals;
492             } else {
493 14         1319 $old_vals[-1];
494             }
495             } else {
496 15 100       67 if (ref $val eq 'ARRAY') {
497 5         27 my $do_set = @old_vals != @$val;
498 5 100       28 if (!$do_set) {
499 2         16 for my $i (0 .. $#old_vals) {
500 3 100       22 if ($old_vals[$i] ne $val->[$i]) {
501 1         9 $do_set = 1;
502 1         6 last;
503             }
504             }
505             }
506 5 100       23 if ($do_set) {
507 4         47 $self->system(qw(git config --null --replace-all), $key, $val->[0]);
508 4         204 for my $i (1..$#$val) {
509 4         63 $self->system(qw(git config --null --add), $key, $val->[$i]);
510             }
511 4         499 return 1;
512             } else {
513 1         129 return 0;
514             }
515             } else {
516 10         34 my $do_set = 1;
517 10         54 for my $i (0 .. $#old_vals) {
518 9 100       52 if ($val eq $old_vals[$i]) {
519 3         17 $do_set = 0;
520 3         13 last;
521             }
522             }
523 10 100       40 if ($do_set) {
524 7 100       124 $self->system(qw(git config --null), ($add ? '--add' : ()), $key, $val);
525 5         904 return 1;
526             } else {
527 3         297 return 0;
528             }
529             }
530             }
531             }
532 46         538 } $directory;
533             }
534              
535             sub git_get_default_branch {
536 5     5 1 52 my($self, %opts) = @_;
537 5         12 my $directory = delete $opts{directory};
538 5   50     48 my $origin = delete $opts{origin} || 'origin';
539 5         12 my $method = delete $opts{method};
540 5 50       20 error "Unhandled options: " . join(' ', %opts) if %opts;
541              
542 5 100       37 my @methods = (
    100          
543             ref $method eq 'ARRAY' ? @$method :
544             defined $method ? $method :
545             ()
546             );
547 5 100       14 if (!@methods) { @methods = 'remote' }
  1         8  
548              
549 5         11 my @error_msgs;
550             my $res;
551              
552             in_directory {
553 5     5   15 TRY_METHODS: while (@methods) {
554 5         13 my $method = shift @methods;
555 5 100       16 if ($method eq 'remote') {
    100          
556             # from https://stackoverflow.com/questions/28666357/git-how-to-get-default-branch#comment126528129_50056710
557 2         22 chomp(my $info_res = $self->info_qx({quiet=>1}, qw(env LC_ALL=C git remote show), $origin));
558 2 50       81 if ($info_res =~ /^\s*HEAD branch:\s+(.*)/m) {
559 2         30 $res = $1;
560 2         100 last TRY_METHODS;
561             } else {
562 0         0 push @error_msgs, "method $method: Can't get default branch; git-remote output is:\n$res";
563             }
564             } elsif ($method eq 'symbolic-ref') {
565 2         4 my $parent_ref = 'refs/remotes/' . $origin;
566 2         5 chomp(my $info_res = eval { $self->info_qx({quiet=>1}, qw(git symbolic-ref), "$parent_ref/HEAD") });
  2         15  
567 2 50 33     45 if (defined $info_res && $info_res ne '') {
568 2         20 $res = substr($info_res, length($parent_ref)+1);
569 2         118 last TRY_METHODS;
570             } else {
571 0         0 push @error_msgs, "method $method: Can't get default branch ($@)";
572             }
573             } else {
574 1         25 error "Unhandled git_get_default_branch method '$method'";
575             }
576             }
577 5         84 } $directory;
578              
579 4 50       141 if (@error_msgs) {
580 0         0 error join("\n", @error_msgs);
581             }
582              
583 4         147 $res;
584             }
585              
586              
587             # From https://stackoverflow.com/a/4495524/2332415
588             sub _is_dir_empty {
589 2     2   11 my ($dir) = @_;
590              
591 2 50       97 opendir my $h, $dir
592             or error "Cannot open directory: '$dir': $!";
593              
594 2         41 while (defined (my $entry = readdir $h)) {
595 5 100       91 return unless $entry =~ /^[.][.]?\z/;
596             }
597              
598 1         20 return 1;
599             }
600              
601             sub _pipe_open (@) {
602 139     139   651 my(@cmd) = @_;
603 139         252 my $fh;
604 139         241 if (Doit::IS_WIN && $] < 5.022) {
605             open $fh, '-|', Doit::Win32Util::win32_quote_list(@cmd)
606             or return undef;
607             } else {
608 139 50       550568 open $fh, '-|', @cmd
609             or return undef;
610             }
611 139         5981 return $fh;
612             }
613              
614             1;
615              
616             __END__