File Coverage

git-autofixup
Criterion Covered Total %
statement 284 346 82.0
branch 99 156 63.4
condition 27 36 75.0
subroutine 28 31 90.3
pod n/a
total 438 569 76.9


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             package main;
3 1     1   6355 use 5.008004;
  1         8  
4 1     1   11 use strict;
  1         6  
  1         44  
5 1     1   5 use warnings FATAL => 'all';
  1         6  
  1         82  
6              
7 1     1   13 use Carp qw(croak);
  1         2  
  1         106  
8 1     1   569 use Pod::Usage;
  1         51952  
  1         266  
9 1     1   964 use Getopt::Long qw(:config bundling);
  1         10497  
  1         4  
10 1     1   173 use File::Temp;
  1         2  
  1         162  
11 1     1   8 use File::Spec ();
  1         5  
  1         4174  
12              
13             our $VERSION = 0.003000; # X.YYYZZZ
14              
15             my $VERBOSE;
16             my @GIT_OPTIONS;
17              
18             # Strictness levels.
19             my ($CONTEXT, $ADJACENT, $SURROUNDED) = (0..10);
20              
21             my $usage =<<'END';
22             usage: git-autofixup []
23              
24             -h show usage
25             --help show manpage
26             --version show version
27             -v, --verbose increase verbosity (use up to 2 times)
28             -c N, --context N set number of diff context lines (default 3)
29             -e, --exit-code use more detailed exit codes (see --help)
30             -s N, --strict N set strictness (default 0)
31             Assign a hunk to fixup a topic branch commit if:
32              
33             0: either only one topic branch commit is blamed in the hunk context or
34             blocks of added lines are adjacent to exactly one topic branch commit.
35             Removing upstream lines is allowed for this level.
36             1: blocks of added lines are adjacent to exactly one topic branch commit
37             2: blocks of added lines are surrounded by exactly one topic branch commit
38              
39             Regardless of strictness level, removed lines are correlated with the
40             commit they're blamed on, and all the blocks of changed lines in a hunk
41             must be correlated with the same topic branch commit in order to be
42             assigned to it. See the --help for more details.
43             -g ARG, --gitopt ARG
44             Specify option for git. Can be used multiple times.
45             END
46              
47             # Parse hunks out of `git diff` output. Return an array of hunk hashrefs.
48             sub parse_hunks {
49 40     40   362 my $fh = shift;
50 40         239 my ($file_a, $file_b);
51 40         0 my @hunks;
52 40         0 my $line;
53 40         84950 while ($line = <$fh>) {
54 233 100       3237 if ($line =~ /^--- (.*)/) {
    100          
    100          
55 45         1033 $file_a = $1;
56             } elsif ($line =~ /^\+\+\+ (.*)/) {
57 45         412 $file_b = $1;
58             } elsif ($line =~ /^@@ -(\d+)(?:,(\d+))? \+\d+(?:,\d+)? @@/) {
59 47         188 my $header = $line;
60              
61 47         306 for ($file_a, $file_b) {
62 94         1105 s#^[abiwco]/##;
63             }
64              
65 47 100       212 next if $file_a ne $file_b; # Ignore creations and deletions.
66              
67 44         359 my $lines = [];
68 44         123 while (1) {
69 158         611 $line = <$fh>;
70 158 100 100     1299 if (!defined($line) || $line =~ /^[^ +\\-]/) {
71 44         137 last;
72             }
73 114         220 push @{$lines}, $line;
  114         705  
74             }
75              
76 44 100       2002 push(@hunks, {
77             file => $file_a,
78             start => $1,
79             count => defined($2) ? $2 : 1,
80             header => $header,
81             lines => $lines,
82             });
83             # The next line after a hunk could be a header for the next commit
84             # or hunk.
85 44 100       426 redo if defined $line;
86             }
87             }
88 40         431 return @hunks;
89             }
90              
91             sub git_cmd {
92 181     181   349361 return ('git', @GIT_OPTIONS, @_);
93             }
94              
95             sub summary_for_commits {
96 40     40   139 my $rev = shift;
97 40         79 my %commits;
98 40         176538 for (qx(git log --no-merges --format=%H:%s $rev..)) {
99 50         724 chomp;
100 50         1161 my ($sha, $msg) = split ':', $_, 2;
101 50         828 $commits{$sha} = $msg;
102             }
103 40         960 return \%commits;
104             }
105              
106             # Return targets of fixup!/squash! commits.
107             sub sha_aliases {
108 40     40   128 my $summary_for = shift;
109 40         155 my %aliases;
110 40         101 my @targets = keys(%{$summary_for});
  40         557  
111 40         443 for my $sha (@targets) {
112 50         197 my $summary = $summary_for->{$sha};
113 50 100       610 next if $summary !~ /^(?:fixup|squash)! (.*)/;
114 3         49 my $prefix = $1;
115 3 50       33 if ($prefix =~ /^(?:(?:fixup|squash)! ){2}/) {
116 0         0 die "fixup commits for fixup commits aren't supported: $sha";
117             }
118 3         39 my @matches = grep {startswith($summary_for->{$_}, $prefix)} @targets;
  6         61  
119 3 50       60 if (@matches > 1) {
    50          
    50          
120 0         0 die "ambiguous fixup commit target: multiple commit summaries start with: $prefix\n";
121             } elsif (@matches == 0) {
122 0         0 die "no fixup target in topic branch: $sha\n";
123             } elsif (@matches == 1) {
124 3         20 $aliases{$sha} = $matches[0];
125             }
126             }
127 40         189 return \%aliases;
128             }
129              
130             sub fixup_sha {
131 44     44   110 my $args = shift;
132 44         100 my $hunk = $args->{hunk};
133 44         74 my $blame = $args->{blame};
134 44         63 my $summary_for = $args->{summary_for};
135 44         73 my $strict = $args->{strict};
136 44 50       85 if (grep {!defined} ($hunk, $blame, $summary_for, $strict)) {
  176         340  
137 0         0 croak 'missing argument';
138             }
139              
140 44         62 my @targets;
141 44 100       215 if ($args->{strict} == $CONTEXT) {
142 25         90 @targets = fixup_targets_from_all_context($args);
143 25         55 my @topic_targets = grep {defined $summary_for->{$_}} @targets;
  29         83  
144 25 100       86 if (@topic_targets > 1) {
145             # The context assignment is ambiguous, but an adjacency assignment
146             # might not be.
147 3         36 @targets = fixup_targets_from_adjacent_context($args);
148             }
149             } else {
150 19         118 @targets = fixup_targets_from_adjacent_context($args);
151             }
152              
153 44         139 my $upstream_is_blamed = grep {!defined $summary_for->{$_}} @targets;
  46         133  
154 44         106 my @topic_targets = grep {defined $summary_for->{$_}} @targets;
  46         102  
155 44 100 100     367 if ($strict && $upstream_is_blamed) {
    50          
    100          
156 4 50       33 $VERBOSE && print hunk_desc($hunk), " changes lines blamed on upstream\n";
157 4         23 return;
158             } elsif (@topic_targets > 1) {
159 0 0       0 $VERBOSE && print hunk_desc($hunk), " has multiple targets\n";
160 0         0 return;
161             } elsif (@topic_targets == 0) {
162 5 50       60 $VERBOSE && print hunk_desc($hunk), " has no targets\n";
163 5         30 return;
164             }
165 35         105 return $topic_targets[0];
166             }
167              
168             sub hunk_desc {
169 0     0   0 my $hunk = shift;
170             return join " ", (
171             $hunk->{file},
172 0         0 $hunk->{header} =~ /(@@[^@]*@@)/,
173             );
174             }
175              
176             sub fixup_targets_from_all_context {
177 25     25   48 my $args = shift;
178 25         48 my ($hunk, $blame, $summary_for) = @{$args}{qw(hunk blame summary_for)};
  25         75  
179 25 50       43 croak 'missing argument' if grep {!defined} ($hunk, $blame, $summary_for);
  75         132  
180              
181 25         37 my @targets = uniq(map {$_->{sha}} values(%{$blame}));
  42         133  
  25         84  
182 25 50       165 return wantarray ? @targets : \@targets;
183             }
184              
185             sub uniq {
186 83     83   115 my %seen;
187 83         165 return grep {!$seen{$_}++} @_;
  99         530  
188             }
189              
190             sub fixup_targets_from_adjacent_context {
191 22     22   138 my $args = shift;
192 22         68 my $hunk = $args->{hunk};
193 22         48 my $blame = $args->{blame};
194 22         43 my $summary_for = $args->{summary_for};
195 22         77 my $strict = $args->{strict};
196 22 50       131 if (grep {!defined} ($hunk, $blame, $summary_for, $strict)) {
  88         181  
197 0         0 croak 'missing argument';
198             }
199              
200 22         114 my $blame_indexes = blame_indexes($hunk);
201              
202 22         98 my %blamed;
203 22         59 my $diff = $hunk->{lines};
204 22         50 for (my $di = 0; $di < @{$diff}; $di++) { # diff index
  78         169  
205 56         73 my $bi = $blame_indexes->[$di];
206 56         70 my $line = $diff->[$di];
207 56 100       94 if (startswith($line, '-')) {
    100          
208 17         32 my $sha = $blame->{$bi}{sha};
209 17         87 $blamed{$sha} = 1;
210             } elsif (startswith($line, '+')) {
211 18         29 my @lines;
212 18 100 66     251 if ($di > 0 && defined $blame->{$bi-1}) {
213 17         65 push @lines, $bi-1;
214             }
215 18 100       50 if (defined $blame->{$bi}) {
216 5         23 push @lines, $bi;
217             }
218 18         33 my @adjacent_shas = uniq(map {$_->{sha}} @{$blame}{@lines});
  22         116  
  18         160  
219 18         42 my @target_shas = grep {defined $summary_for->{$_}} @adjacent_shas;
  21         58  
220             # Note that lines at the beginning or end of a file can be
221             # "surrounded" by a single line.
222 18   100     190 my $is_surrounded = @target_shas > 0
223             && @target_shas == @adjacent_shas
224             && $target_shas[0] eq $target_shas[-1];
225 18         66 my $is_adjacent = @target_shas == 1;
226 18 100 100     121 if ($is_surrounded || ($strict < $SURROUNDED && $is_adjacent)) {
      100        
227 14         35 $blamed{$target_shas[0]} = 1;
228             }
229 18   100     125 while ($di < @$diff-1 && startswith($diff->[$di+1], '+')) {
230 4         24 $di++;
231             }
232             }
233             }
234 22         112 my @targets = keys %blamed;
235 22 50       138 return wantarray ? @targets : \@targets;
236             }
237              
238             sub startswith {
239 782     782   1572 my ($haystack, $needle) = @_;
240 782         3473 return index($haystack, $needle, 0) == 0;
241             }
242              
243             # Map lines in a hunk's diff to the corresponding `git blame HEAD` output.
244             sub blame_indexes {
245 22     22   69 my $hunk = shift;
246 22         36 my @indexes;
247 22         175 my $bi = $hunk->{start};
248 22         71 for (my $di = 0; $di < @{$hunk->{lines}}; $di++) {
  82         253  
249 60         209 push @indexes, $bi;
250 60         323 my $first = substr($hunk->{lines}[$di], 0, 1);
251 60 100 100     397 if ($first eq '-' or $first eq ' ') {
252 38         83 $bi++;
253             }
254             # Don't increment $bi for added lines.
255             }
256 22         158 return \@indexes;
257             }
258              
259             sub print_hunk_blamediff {
260 0     0   0 my $args = shift;
261 0         0 my $fh = $args->{fh};
262 0         0 my $hunk = $args->{hunk};
263 0         0 my $summary_for = $args->{summary_for};
264 0         0 my $blame = $args->{blame};
265 0         0 my $blame_indexes = $args->{blame_indexes};
266 0 0       0 if (grep {!defined} ($fh, $hunk, $summary_for, $blame, $blame_indexes)) {
  0         0  
267 0         0 croak 'missing argument';
268             }
269              
270 0         0 my $format = "%-8.8s|%4.4s|%-30.30s|%-30.30s\n";
271 0         0 for (my $i = 0; $i < @{$hunk->{lines}}; $i++) {
  0         0  
272 0         0 my $line_r = $hunk->{lines}[$i];
273 0         0 my $bi = $blame_indexes->[$i];
274 0 0       0 my $sha = defined $blame->{$bi} ? $blame->{$bi}{sha} : undef;
275              
276 0 0       0 my $display_sha = defined($sha) ? $sha : q{};
277 0         0 my $display_bi = $bi;
278 0 0       0 if (startswith($line_r, '+')) {
279 0         0 $display_sha = q{}; # For added lines.
280 0         0 $display_bi = q{};
281             }
282 0 0 0     0 if (defined($sha) && !defined($summary_for->{$sha})) {
283             # For lines from before the given upstream revision.
284 0         0 $display_sha = '^';
285             }
286              
287 0         0 my $line_l = '';
288 0 0 0     0 if (defined $blame->{$bi} && !startswith($line_r, '+')) {
289 0         0 $line_l = $blame->{$bi}{text};
290             }
291              
292 0         0 for ($line_l, $line_r) {
293             # For the table to line up, tabs need to be converted to a string of fixed width.
294 0         0 s/\t/^I/g;
295             # Remove trailing newlines and carriage returns. If more trailing
296             # whitespace is removed, that's fine.
297 0         0 $_ = rtrim($_);
298             }
299              
300 0         0 printf {$fh} $format, $display_sha, $display_bi, $line_l, $line_r;
  0         0  
301             }
302 0         0 print {$fh} "\n";
  0         0  
303 0         0 return;
304             }
305              
306             sub rtrim {
307 0     0   0 my $s = shift;
308 0         0 $s =~ s/\s+\z//;
309 0         0 return $s;
310             }
311              
312             sub blame {
313 44     44   206 my ($hunk, $alias_for) = @_;
314 44 100       232 if ($hunk->{count} == 0) {
315 1         31 return {};
316             }
317 43         886 my @cmd = git_cmd(
318             'blame', '--porcelain',
319             '-L' => "$hunk->{start},+$hunk->{count}",
320             'HEAD', '--',
321             "$hunk->{file}");
322 43         139 my %blame;
323 43         70 my ($sha, $line_num);
324 43 50       93050 open(my $fh, '-|', @cmd) or die "git blame: $!\n";
325 43         106835 while (my $line = <$fh>) {
326 672 100       2430 if ($line =~ /^([0-9a-f]{40}) \d+ (\d+)/) {
327 70         1536 ($sha, $line_num) = ($1, $2);
328             }
329 672 100       1471 if (startswith($line, "\t")) {
330 70 100       247 if (defined $alias_for->{$sha}) {
331 3         34 $sha = $alias_for->{$sha};
332             }
333 70         2320 $blame{$line_num} = {sha => $sha, text => substr($line, 1)};
334             }
335             }
336 43 50       1364 close($fh) or die "git blame: non-zero exit code";
337 43         2770 return \%blame;
338             }
339              
340             sub diff_hunks {
341 40     40   314 my $num_context_lines = shift;
342 40         1236 my @cmd = git_cmd(qw(-c diff.noprefix=false diff --no-ext-diff --ignore-submodules), "-U$num_context_lines");
343 40 100       277 if (is_index_dirty()) {
344 2         24 push @cmd, "--cached";
345             }
346 40 50       84519 open(my $fh, '-|', @cmd) or die $!;
347 40         1487 my @hunks = parse_hunks($fh, keep_lines => 1);
348 40 50       1382 close($fh) or die "git diff: non-zero exit code";
349 40 50       2135 return wantarray ? @hunks : \@hunks;
350             }
351              
352             sub commit_fixup {
353 29     29   315 my ($sha, $hunks) = @_;
354 29 50       941 open my $fh, '|-', git_cmd(qw(apply --unidiff-zero --cached -)) or die "git apply: $!\n";
355 29         1085 for my $hunk (@{$hunks}) {
  29         365  
356 35         411 print({$fh}
357             "--- a/$hunk->{file}\n",
358             "+++ b/$hunk->{file}\n",
359             $hunk->{header},
360 35         99 @{$hunk->{lines}},
  35         725  
361             );
362             }
363 29 50       67088 close $fh or die "git apply: non-zero exit code\n";
364 29 50       717 system(git_cmd('commit', "--fixup=$sha")) == 0 or die "git commit: $!\n";
365 29         2692 return;
366             }
367              
368             sub is_index_dirty {
369 40 50   40   979 open(my $fh, '-|', git_cmd(qw(status --porcelain))) or die "git status: $!\n";
370 40         1266 my $dirty;
371 40         126392 while (my $line = <$fh>) {
372 47 100       4760 if ($line =~ /^[^?! ]/) {
373 2         31 $dirty = 1;
374 2         23 last;
375             }
376             }
377 40 50       1511 close $fh or die "git status: non-zero exit code\n";
378 40         1823 return $dirty;
379             }
380              
381             sub fixup_hunks_by_sha {
382 40     40   170 my $args = shift;
383 40         108 my $hunks = $args->{hunks};
384 40         78 my $blame_for = $args->{blame_for};
385 40         100 my $summary_for = $args->{summary_for};
386 40         90 my $strict = $args->{strict};
387 40 50       377 if (grep {!defined} ($hunks, $blame_for, $summary_for, $strict)) {
  160         465  
388 0         0 croak 'missing argument';
389             }
390              
391 40         86 my %hunks_for;
392 40         61 for my $hunk (@{$hunks}) {
  40         293  
393 44         134 my $blame = $blame_for->{$hunk};
394 44         671 my $sha = fixup_sha({
395             hunk => $hunk,
396             blame => $blame,
397             summary_for => $summary_for,
398             strict => $strict,
399             });
400 44 50 66     436 if ($sha && $VERBOSE) {
401             printf "%s fixes %s %s\n",
402             hunk_desc($hunk),
403             substr($sha, 0, 8),
404 0         0 $summary_for->{$sha};
405             }
406 44 50       151 if ($VERBOSE > 1) {
407 0         0 print_hunk_blamediff({
408             fh => *STDOUT,
409             hunk => $hunk,
410             summary_for => $summary_for,
411             blame => $blame,
412             blame_indexes => blame_indexes($hunk)
413             });
414             }
415 44 100       148 next if !$sha;
416 35         58 push @{$hunks_for{$sha}}, $hunk;
  35         211  
417             }
418 40         229 return \%hunks_for;
419             }
420              
421             # Return SHAs in some consistent order.
422             #
423             # Currently they're ordered by how early their assigned hunks appear in the
424             # diff output. This assumes $hunks is in the order it was parsed from the diff.
425             # This ordering seems nice since it'd be similar to the order a human would
426             # make commits in if they were working their way down the diff.
427             sub ordered_shas {
428 40     40   74 my $hunks = shift;
429 40         66 my $sha_for = shift;
430 40         92 my @ordered = ();
431 40         50 for my $hunk (@{$hunks}) {
  40         229  
432 44 100       232 if (defined $sha_for->{$hunk}) {
433 35         200 push @ordered, $sha_for->{$hunk};
434             }
435             }
436 40         174 return uniq(@ordered);
437             }
438              
439             # Reverse the sha->hunks hashef and return a hunk->sha hashref.
440             sub sha_for_hunk_map {
441 40     40   126 my $hunks_for = shift;
442 40         58 my %sha_for;
443 40         68 for my $sha (keys %{$hunks_for}) {
  40         358  
444 29         64 for my $hunk (@{$hunks_for->{$sha}}) {
  29         95  
445 35 50       126 if (defined $sha_for{$hunk}) {
446 0         0 die "multiple SHAs for hunk"; # This should never happen.
447             }
448 35         161 $sha_for{$hunk} = $sha;
449             }
450             }
451 40         194 return \%sha_for;
452             }
453              
454             sub exit_code {
455 40     40   367 my ($hunks, $hunks_for) = @_;
456 40         124 my $hunk_count = scalar @{$hunks};
  40         240  
457              
458 40         118 my $assigned_hunk_count = 0;
459 40         73 for (values %{$hunks_for}) {
  40         375  
460 29         102 $assigned_hunk_count += @{$_};
  29         94  
461             }
462              
463 40         85 my $rc;
464 40 100       323 if ($hunk_count == 0) {
    100          
    100          
    50          
465 6         28 $rc = 3; # no hunks to assign
466             } elsif ($assigned_hunk_count == 0) {
467 7         48 $rc = 2; # hunks exist, but none assigned
468             } elsif ($assigned_hunk_count < $hunk_count) {
469 2         16 $rc = 1; # not all hunks assigned
470             } elsif ($hunk_count == $assigned_hunk_count) {
471 25         48 $rc = 0; # all hunks assigned
472             } else {
473 0         0 die "unexpected conditions when choosing exit code";
474             }
475 40         1598 return $rc;
476             }
477              
478             # Create a temporary index so we can craft commits with already-staged hunks.
479             # Return a File::Temp object so the caller has control over its lifetime.
480             sub create_temp_index {
481 40     40   4113 my $tempfile = File::Temp->new(
482             TEMPLATE => 'git-autofixup_index.XXXXXX',
483             DIR => File::Spec->tmpdir());
484 40         32641 $ENV{GIT_INDEX_FILE} = $tempfile->filename();
485             # A blank index makes it look like we're deleting everything, so read
486             # HEAD's tree into it.
487 40         248633 qx(git read-tree HEAD^{tree});
488 40 50       1395 $? == 0 or die "Can't read HEAD's tree into temp index.\n";
489 40         1328 return $tempfile;
490             }
491              
492             sub main {
493 40     40   2852294 $VERBOSE = 0;
494 40         567 my $help;
495             my $man;
496 40         0 my $show_version;
497 40         334 my $strict = $CONTEXT;
498 40         371 my $num_context_lines = 3;
499 40         268 my $dryrun;
500             my $use_detailed_exit_codes;
501              
502 40 50       1194 GetOptions(
503             'h' => \$help,
504             'help' => \$man,
505             'version' => \$show_version,
506             'verbose|v+' => \$VERBOSE,
507             'strict|s=i' => \$strict,
508             'context|c=i' => \$num_context_lines,
509             'dryrun|n' => \$dryrun,
510             'gitopt|g=s' => \@GIT_OPTIONS,
511             'exit-code' => \$use_detailed_exit_codes,
512             ) or return 1;
513 40 50       58746 if ($help) {
514 0         0 print $usage;
515 0         0 return 0;
516             }
517 40 50       348 if ($show_version) {
518 0         0 print "$VERSION\n";
519 0         0 return 0;
520             }
521 40 50       177 if ($man) {
522 0         0 pod2usage(-exitval => 0, -verbose => 2);
523             }
524              
525 40 50       162 @ARGV == 1 or die "No upstream commit given.\n";
526 40         165 my $upstream = shift @ARGV;
527 40         239131 qx(git rev-parse --verify ${upstream}^{commit});
528 40 50       1251 $? == 0 or die "Can't resolve given commit.\n";
529              
530 40 50       356 if ($num_context_lines < 0) {
531 0         0 die "invalid number of context lines: $num_context_lines\n";
532             }
533              
534 40 50 66     1000 if ($strict < 0) {
    50          
535 0         0 die "invalid strictness level: $strict\n";
536             } elsif ($strict > 0 && $num_context_lines == 0) {
537 0         0 die "strict hunk assignment requires context\n";
538             }
539              
540              
541 40         146834 my $toplevel = qx(git rev-parse --show-toplevel);
542 40         625 chomp $toplevel;
543 40 50       523 $? == 0 or die "Can't get repo toplevel dir\n";
544 40 50       825 chdir $toplevel or die $!;
545              
546 40         1431 my $hunks = diff_hunks($num_context_lines);
547 40         619 my $summary_for = summary_for_commits($upstream);
548 40         494 my $alias_for = sha_aliases($summary_for);
549 40         114 my %blame_for = map {$_ => blame($_, $alias_for)} @{$hunks};
  44         465  
  40         468  
550 40         1126 my $hunks_for = fixup_hunks_by_sha({
551             hunks => $hunks,
552             blame_for => \%blame_for,
553             summary_for => $summary_for,
554             strict => $strict,
555             });
556 40         389 my @ordered_shas = ordered_shas($hunks, sha_for_hunk_map($hunks_for));
557              
558 40 50       152 if ($dryrun) {
559 0 0       0 if ($use_detailed_exit_codes) {
560 0         0 return exit_code($hunks, $hunks_for);
561             }
562 0         0 return 0;
563             }
564              
565             # Limit the tempfile's lifetime to the execution of main().
566 40         686 local $ENV{GIT_INDEX_FILE}; # Throw away ENV changes between main() calls.
567 40         319 my $tempfile = create_temp_index();
568              
569 40         528 for my $sha (@ordered_shas) {
570 29         138 my $fixup_hunks = $hunks_for->{$sha};
571 29         356 commit_fixup($sha, $fixup_hunks);
572             }
573 40 50       295 if ($use_detailed_exit_codes) {
574 40         533 return exit_code($hunks, $hunks_for);
575             }
576 0           return 0;
577             }
578              
579             if (!caller()) {
580             exit main();
581             }
582             1;
583              
584             __END__