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   20 use strict;
  2         4  
  2         92  
17 2     2   10 use warnings;
  2         5  
  2         163  
18             our $VERSION = '0.030';
19              
20 2     2   32 use Doit::Log;
  2         6  
  2         185  
21 2     2   13 use Doit::Util qw(in_directory);
  2         4  
  2         14518  
22              
23             sub _pipe_open (@);
24              
25 2     2 0 24 sub new { bless {}, shift }
26 2     2 0 13 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 730 my($self, %opts) = @_;
30 42         229 my $repository = delete $opts{repository};
31 42 100       120 my @repository_aliases = @{ delete $opts{repository_aliases} || [] };
  42         645  
32 42         124 my $directory = delete $opts{directory};
33 42   100     291 my $origin = delete $opts{origin} || 'origin';
34 42         92 my $branch = delete $opts{branch};
35 42         108 my $allow_remote_url_change = delete $opts{allow_remote_url_change};
36 42         90 my $clone_opts = delete $opts{clone_opts};
37 42   100     251 my $refresh = delete $opts{refresh} || 'always';
38 42 100       728 if ($refresh !~ m{^(always|never)$}) { error "refresh may be 'always' or 'never'" }
  1         5  
39 41         146 my $quiet = delete $opts{quiet};
40 41 100       267 error "Unhandled options: " . join(" ", %opts) if %opts;
41              
42 40         380 my $has_changes = 0;
43 40         274 my $do_clone;
44 40 100       1718 if (!-e $directory) {
45 13         101 $do_clone = 1;
46             } else {
47 27 100       439 if (!-d $directory) {
48 1         8 error "'$directory' exists, but is not a directory\n";
49             }
50 26 100       512 if (!-d "$directory/.git") {
51 2 100       26 if (_is_dir_empty($directory)) {
52 1         9 $do_clone = 1;
53             } else {
54 1         14 error "No .git directory found in non-empty '$directory', refusing to clone...\n";
55             }
56             }
57             }
58 38 100       329 if (!$do_clone) {
59             in_directory {
60 24     24   64 my $actual_repository = eval { $self->info_qx({quiet=>1}, qw(git config --get), "remote.$origin.url") };
  24         371  
61 24 100       255 if (!defined $actual_repository) {
62             # Remote does not exist yet --- create it.
63 1         33 $self->system(qw(git remote add), $origin, $repository);
64             } else {
65 23         133 chomp $actual_repository;
66 23 100 100     368 if ($actual_repository ne $repository && !grep { $_ eq $actual_repository } @repository_aliases) {
  2         31  
67 3         52 my @change_cmd = ('git', 'remote', 'set-url', $origin, $repository);
68 3 100       27 if ($allow_remote_url_change) {
69 1         30 info "Need to change remote URL for $origin";
70 1         130 $self->system(@change_cmd);
71             } else {
72 2 100       91 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         143 my $switch_later;
83 22 100       122 if (defined $branch) { # maybe branch switching necessary?
84 11 100       118 if ($branch =~ m{^refs/remotes/(.*)}) { # extract branch with remote
85 1         27 $branch = $1;
86             }
87 11         208 my $current_branch = $self->git_current_branch;
88 11 100 66     258 if (!defined $current_branch || $current_branch ne $branch) {
89 7 100       38 if (eval { $self->system({show_cwd=>1,quiet=>$quiet}, qw(git checkout), $branch); 1 }) {
  7         301  
  4         285  
90 4         27 $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         30 $switch_later = 1;
97             }
98             }
99 11         43 my %info;
100 11         192 $self->git_current_branch(info_ref => \%info);
101 11 100       1489 if ($info{detached}) {
102 5         42 $switch_later = 1; # because a "git pull" wouldn't update a detached branch
103             }
104             }
105              
106 22 100       157 if ($refresh eq 'always') {
107 21         683 $self->system({show_cwd=>1,quiet=>$quiet}, qw(git fetch), $origin);
108 21         2255 my $status = $self->git_short_status(untracked_files => 'no');
109 21 100       551 if ($status =~ m{>$}) {
110             # may actually fail if diverged (status=<>)
111             # or untracked/changed files would get overwritten
112 5         241 $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         255 $has_changes = 1;
114             } # else: ahead, diverged, or something else
115             }
116              
117 21 100       738 if ($switch_later) {
118 8         60 my($commit_before, $branch_before);
119 8 100       46 if (!$has_changes) {
120 5         112 $commit_before = $self->git_get_commit_hash;
121 5         128 $branch_before = $self->git_current_branch;
122             }
123 8 100       43 if (!eval { $self->system({show_cwd=>1,quiet=>$quiet}, qw(git checkout), $branch) }) {
  8         447  
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         43 $self->system({show_cwd=>1,quiet=>$quiet}, qw(git checkout -b), $branch, qw(--track), "$origin/$branch");
129             }
130 8 100 100     503 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         162 $has_changes = 1;
136             }
137             }
138 24         1552 } $directory;
139             } else {
140 14         207 my @cmd = (qw(git clone --origin), $origin);
141 14 100       134 if (defined $branch) {
142 2 100       24 if ($branch =~ m{^refs/remotes/[^/]+/(.*)}) { # extract branch without remote
143 1         8 $branch = $1;
144             }
145 2         13 push @cmd, "--branch", $branch;
146             }
147 14 100       59 if ($clone_opts) {
148 1         12 push @cmd, @$clone_opts;
149             }
150 14         49 push @cmd, $repository, $directory;
151 14         170 $self->system(@cmd);
152 14         1073 $has_changes = 1;
153             }
154 35         3185 $has_changes;
155             }
156              
157             sub git_short_status {
158 59     59 1 479 my($self, %opts) = @_;
159 59         202 my $directory = delete $opts{directory};
160 59         203 my $untracked_files = delete $opts{untracked_files};
161 59 100       1104 if (!defined $untracked_files) {
    100          
162 21         103 $untracked_files = 'normal';
163             } elsif ($untracked_files !~ m{^(normal|no)$}) {
164 1         5 error "only values 'normal' or 'no' supported for untracked_files";
165             }
166 58 100       371 error "Unhandled options: " . join(" ", %opts) if %opts;
167              
168             in_directory {
169 57     57   1037 local $ENV{LC_ALL} = 'C';
170              
171 57         159 my $untracked_marker = '';
172             {
173 57         493 my @cmd = ("git", "status", "--untracked-files=$untracked_files", "--porcelain");
174 57 50       357 my $fh = _pipe_open(@cmd)
175             or error "Can't run '@cmd': $!";
176 57         287 my $has_untracked;
177             my $has_uncommitted;
178 57         272063 while (<$fh>) {
179 13 100       255 if (m{^\?\?}) {
180 6         34 $has_untracked++;
181             } else {
182 7         48 $has_uncommitted++;
183             }
184             # Shortcut, exit as early as possible
185 13 100       1235 if ($has_uncommitted) {
186 8 100       1258 if ($has_untracked) {
    100          
187 1         355 return '<<*';
188             } elsif ($untracked_files eq 'no') {
189 3         1606 return '<<';
190             } # else we have to check further, for possible untracked files
191             }
192             }
193 53 100       527 if ($has_uncommitted) {
    100          
194 3         829 return '<<';
195             } elsif ($has_untracked) {
196 5         82 $untracked_marker = '*'; # will be combined later
197 5         491 last;
198             }
199 45 100       3803 close $fh
200             or error "Error while running '@cmd': $!";
201             }
202              
203             {
204 57         121 my @cmd = ("git", "status", "--untracked-files=no");
  48         212  
  48         610  
205 48 50       285 my $fh = _pipe_open(@cmd)
206             or error "Can't run '@cmd': $!";
207 48         169 my $l;
208 48         224048 $l = <$fh>;
209 48         395 $l = <$fh>;
210 48 100       31030 if ($l =~ m{^(# )?Your branch is ahead}) {
    100          
    100          
211 4         2838 return '<'.$untracked_marker;
212             } elsif ($l =~ m{^(# )?Your branch is behind}) {
213 9         7744 return $untracked_marker.'>';
214             } elsif ($l =~ m{^(# )?Your branch and .* have diverged}) {
215 4         2218 return '<'.$untracked_marker.'>';
216             }
217             }
218              
219 31 50       978 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         4371 return $untracked_marker;
255              
256 57         1909 } $directory;
257             }
258              
259             sub git_root {
260 65     65 1 393 my($self, %opts) = @_;
261 65         222 my $directory = delete $opts{directory};
262 65 100       328 error "Unhandled options: " . join(" ", %opts) if %opts;
263              
264             in_directory {
265 64     64   969 chomp(my $dir = $self->info_qx({quiet=>1}, 'git', 'rev-parse', '--show-toplevel'));
266 57         4217 $dir;
267 64         924 } $directory;
268             }
269              
270             sub git_get_commit_hash {
271 20     20 1 141 my($self, %opts) = @_;
272 20         79 my $directory = delete $opts{directory};
273 20         57 my $commit = delete $opts{commit};
274 20 100       150 error "Unhandled options: " . join(" ", %opts) if %opts;
275              
276             in_directory {
277 19 100   19   438 chomp(my $commit = $self->info_qx({quiet=>1}, 'git', 'log', '-1', '--format=%H', (defined $commit ? $commit : ())));
278 17         1225 $commit;
279 19         360 } $directory;
280             }
281              
282             sub git_get_commit_files {
283 8     8 1 49 my($self, %opts) = @_;
284 8         78 my $directory = delete $opts{directory};
285 8 100       28 my $commit = delete $opts{commit}; if (!defined $commit) { $commit = 'HEAD' }
  8         38  
  6         26  
286 8 100       58 error "Unhandled options: " . join(" ", %opts) if %opts;
287              
288 7         20 my @files;
289             in_directory {
290 6     6   51 my @cmd = ('git', 'show', $commit, '--pretty=format:', '--name-only');
291 6 50       52 my $fh = _pipe_open(@cmd)
292             or error "Error running @cmd: $!";
293 6         10871 my $first = <$fh>;
294 6 100 66     199 if (defined $first && $first ne "\n") { # first line is empty for older git versions (e.g. 1.7.x)
295 3         25 chomp $first;
296 3         33 push @files, $first;
297             }
298 6         1129 while(<$fh>) {
299 1         10 chomp;
300 1         176 push @files, $_;
301             }
302 6 100       481 close $fh
303             or error "Error while running @cmd: $!";
304 7         150 } $directory;
305 3         230 @files;
306             }
307              
308             sub git_get_changed_files {
309 11     11 1 58 my($self, %opts) = @_;
310 11         59 my $directory = delete $opts{directory};
311 11         36 my $ignore_untracked = delete $opts{ignore_untracked};
312 11 100       66 error "Unhandled options: " . join(" ", %opts) if %opts;
313              
314 10         31 my @files;
315             in_directory {
316 10     10   51 my @cmd = qw(git status --porcelain);
317 10 50       58 my $fh = _pipe_open(@cmd)
318             or error "Error running @cmd: $!";
319 10         49999 while(<$fh>) {
320 7         64 chomp;
321 7 100 66     490 next if $ignore_untracked && m{^\?\?};
322 6         123 s{^...}{};
323 6         1467 push @files, $_;
324             }
325 10 100       2836 close $fh
326             or error "Error while running @cmd: $!";
327 10         205 } $directory;
328 8         630 @files;
329             }
330              
331             sub git_is_shallow {
332 5     5 1 36 my($self, %opts) = @_;
333 5         19 my $directory = delete $opts{directory};
334 5 100       37 error "Unhandled options: " . join(" ", %opts) if %opts;
335              
336 4         52 my $git_root = $self->git_root(directory => $directory);
337 2 100       180 -f "$git_root/.git/shallow" ? 1 : 0;
338             }
339              
340             sub git_current_branch {
341 53     53 1 391 my($self, %opts) = @_;
342 53         296 my $directory = delete $opts{directory};
343 53         147 my $info_ref = delete $opts{info_ref};
344 53 100       333 error "Unhandled options: " . join(" ", %opts) if %opts;
345              
346             in_directory {
347 52     52   522 my $git_root = $self->git_root;
348 50         205 my $fh;
349             my $this_head;
350 50 50       3816 if (open $fh, "<", "$git_root/.git/HEAD") {
351 50         910 chomp($this_head = <$fh>);
352 50 100       1275 if ($this_head =~ m{refs/heads/(\S+)}) {
353 32         3158 return $1;
354             }
355             }
356              
357             # fallback to git-status
358 18         227 $ENV{LC_ALL} = 'C';
359 18 50       252 if ($fh = _pipe_open(qw(git status))) {
360 18         69978 chomp($_ = <$fh>);
361 18 50       305 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       104 if ($info_ref) {
369 10         155 $info_ref->{detached} = 1;
370 10         72 $info_ref->{fallback} = 'git-status';
371             }
372 18         7777 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         1676 } $directory;
399             }
400              
401             sub git_config {
402 51     51 1 579 my($self, %opts) = @_;
403 51         293 my $directory = delete $opts{directory};
404 51         150 my $key = delete $opts{key};
405 51         233 my $all = delete $opts{all};
406 51         121 my $add = delete $opts{add};
407 51         141 my $val = delete $opts{val};
408 51         130 my $unset = delete $opts{unset};
409 51 100       281 error "Unhandled options: " . join(" ", %opts) if %opts;
410 50 100 100     358 if ($all && defined $val) {
411 1         26 error "Cannot handle 'all' together with 'val'";
412             }
413 49 100       568 if ($add) {
414 6 100       31 if ($unset) {
415 1         23 error "'add' cannot be used together with 'unset'";
416             }
417 5 100       25 if (!defined $val) {
418 1         13 error "'add' must be used together with 'val'";
419             }
420 4 100       6781 if (ref $val eq 'ARRAY') {
421 1         22 error "'add' only implemented for single-value 'val'";
422             }
423             }
424 46 100       209 if (ref $val eq 'ARRAY') {
425 7 100       66 if (@$val == 0) { # if array is empty, then just fallback to --unset-all
426 2         14 $unset = 1;
427 2         11 $all = 1;
428             }
429             }
430              
431             in_directory {
432 46     46   161 my $ret = eval { $self->info_qx({quiet=>1}, qw(git config --null --get-all), $key) };
  46         765  
433 46 100       712 my @old_vals = defined $ret ? split(/\0/, $ret) : ();
434 46 100       301 if ($unset) {
435 10 100       112 if ($@) {
436 3 50       13 if ($@->{exitcode} == 1) {
437             # already non-existent (or even invalid)
438 3         416 0;
439             } else {
440 0         0 error "git config $key failed with exitcode $@->{exitcode}";
441             }
442             } else {
443 7 100       61 if ($all) {
444 2 50       29 if (@old_vals) {
445 2         57 $self->system(qw(git config --unset-all), $key);
446 2         368 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         32 my $do_unset = 0;
453 5 100       34 if (defined $val) {
    50          
454 3         29 for my $i (0 .. $#old_vals) {
455 6 100       38 if ($val eq $old_vals[$i]) {
456 2         10 $do_unset = 1;
457 2         11 last;
458             }
459             }
460             } elsif (@old_vals) {
461 2         12 $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       24 if ($do_unset) {
467 4         47 eval {
468 4 100       110 $self->system(qw(git config --unset --null), $key, (defined $val ? quotemeta($val) : ()));
469             };
470 4 100       62 if ($@) {
471 1 50       34 if ($@->{exitcode} == 5) {
472 1 50       11 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         23 error "Multiple values when using 'unset', please specify 'all => 1' if wanted";
477             }
478             } else {
479 0         0 error $@;
480             }
481             }
482 3         465 return 1;
483             } else {
484 1         137 return 0;
485             }
486             }
487             }
488             } else {
489 36 100       186 if (!defined $val) {
490 21 100       87 if ($all) {
491 7         1032 @old_vals;
492             } else {
493 14         1583 $old_vals[-1];
494             }
495             } else {
496 15 100       81 if (ref $val eq 'ARRAY') {
497 5         34 my $do_set = @old_vals != @$val;
498 5 100       46 if (!$do_set) {
499 2         22 for my $i (0 .. $#old_vals) {
500 3 100       31 if ($old_vals[$i] ne $val->[$i]) {
501 1         13 $do_set = 1;
502 1         11 last;
503             }
504             }
505             }
506 5 100       24 if ($do_set) {
507 4         140 $self->system(qw(git config --null --replace-all), $key, $val->[0]);
508 4         244 for my $i (1..$#$val) {
509 4         70 $self->system(qw(git config --null --add), $key, $val->[$i]);
510             }
511 4         643 return 1;
512             } else {
513 1         108 return 0;
514             }
515             } else {
516 10         28 my $do_set = 1;
517 10         58 for my $i (0 .. $#old_vals) {
518 9 100       96 if ($val eq $old_vals[$i]) {
519 3         18 $do_set = 0;
520 3         12 last;
521             }
522             }
523 10 100       31 if ($do_set) {
524 7 100       133 $self->system(qw(git config --null), ($add ? '--add' : ()), $key, $val);
525 5         956 return 1;
526             } else {
527 3         401 return 0;
528             }
529             }
530             }
531             }
532 46         823 } $directory;
533             }
534              
535             sub git_get_default_branch {
536 5     5 1 79 my($self, %opts) = @_;
537 5         26 my $directory = delete $opts{directory};
538 5   50     104 my $origin = delete $opts{origin} || 'origin';
539 5         16 my $method = delete $opts{method};
540 5 50       30 error "Unhandled options: " . join(' ', %opts) if %opts;
541              
542 5 100       48 my @methods = (
    100          
543             ref $method eq 'ARRAY' ? @$method :
544             defined $method ? $method :
545             ()
546             );
547 5 100       27 if (!@methods) { @methods = 'remote' }
  1         8  
548              
549 5         14 my @error_msgs;
550             my $res;
551              
552             in_directory {
553 5     5   31 TRY_METHODS: while (@methods) {
554 5         20 my $method = shift @methods;
555 5 100       29 if ($method eq 'remote') {
    100          
556             # from https://stackoverflow.com/questions/28666357/git-how-to-get-default-branch#comment126528129_50056710
557 2         33 chomp(my $info_res = $self->info_qx({quiet=>1}, qw(env LC_ALL=C git remote show), $origin));
558 2 50       68 if ($info_res =~ /^\s*HEAD branch:\s+(.*)/m) {
559 2         34 $res = $1;
560 2         125 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         7 my $parent_ref = 'refs/remotes/' . $origin;
566 2         7 chomp(my $info_res = eval { $self->info_qx({quiet=>1}, qw(git symbolic-ref), "$parent_ref/HEAD") });
  2         25  
567 2 50 33     72 if (defined $info_res && $info_res ne '') {
568 2         19 $res = substr($info_res, length($parent_ref)+1);
569 2         103 last TRY_METHODS;
570             } else {
571 0         0 push @error_msgs, "method $method: Can't get default branch ($@)";
572             }
573             } else {
574 1         26 error "Unhandled git_get_default_branch method '$method'";
575             }
576             }
577 5         87 } $directory;
578              
579 4 50       133 if (@error_msgs) {
580 0         0 error join("\n", @error_msgs);
581             }
582              
583 4         174 $res;
584             }
585              
586              
587             # From https://stackoverflow.com/a/4495524/2332415
588             sub _is_dir_empty {
589 2     2   18 my ($dir) = @_;
590              
591 2 50       130 opendir my $h, $dir
592             or error "Cannot open directory: '$dir': $!";
593              
594 2         62 while (defined (my $entry = readdir $h)) {
595 5 100       80 return unless $entry =~ /^[.][.]?\z/;
596             }
597              
598 1         33 return 1;
599             }
600              
601             sub _pipe_open (@) {
602 139     139   661 my(@cmd) = @_;
603 139         328 my $fh;
604 139         313 if (Doit::IS_WIN && $] < 5.022) {
605             open $fh, '-|', Doit::Win32Util::win32_quote_list(@cmd)
606             or return undef;
607             } else {
608 139 50       900401 open $fh, '-|', @cmd
609             or return undef;
610             }
611 139         9384 return $fh;
612             }
613              
614             1;
615              
616             __END__