This is a live mirror of the Perl 5 development currently hosted at https://github.com/perl/perl5
t/porting/authors.t - make robust to -Dmksymlinks to a git checkout
[perl5.git] / Porting / updateAUTHORS.pl
1 #!/usr/bin/env perl
2 package App::Porting::updateAUTHORS;
3 use strict;
4 use warnings;
5 use Getopt::Long qw(GetOptions);
6 use Pod::Usage qw(pod2usage);
7 use Data::Dumper;
8 use Encode qw(encode_utf8 decode_utf8);
9 use lib "./";
10 use Porting::updateAUTHORS;
11 use Test::More;
12 use Text::Wrap qw(wrap);
13
14 # The style of this file is determined by:
15 #
16 # perltidy -w -ple -bbb -bbc -bbs -nolq -l=80 -noll -nola -nwls='=' \
17 #   -isbc -nolc -otr -kis -ci=4 -se -sot -sct -nsbl -pt=2 -fs  \
18 #   -fsb='#start-no-tidy' -fse='#end-no-tidy'
19
20 my @OPTSPEC= qw(
21     help|?
22     man
23     authors_file=s
24     mailmap_file=s
25     source_dir=s
26
27     validate|tap
28     verbose+
29     exclude_missing|exclude
30     exclude_contrib=s@
31     exclude_me
32     dump_opts
33
34     show_rank|rank
35     show_applied|thanks_applied|applied
36     show_stats|stats
37     show_who|who
38     show_files|files
39     show_file_changes|activity
40     show_file_chainsaw|chainsaw
41
42     as_percentage|percentage
43     as_cumulative|cumulative
44     as_list|old_style
45
46     in_reverse|reverse
47     with_rank_numbers|numbered|num
48
49     from_commit|from=s
50     to_commit|to=s
51
52     numstat
53     no_update
54
55     change_name_for_name|change_name=s%
56     change_name_for_email=s%
57     change_email_for_name=s%
58     change_email_for_email|change_email=s%
59 );
60
61 my %implies_numstat= (
62     show_files         => 1,
63     show_file_changes  => 1,
64     show_file_chainsaw => 1,
65 );
66
67 sub main {
68     local $Data::Dumper::Sortkeys= 1;
69     my %opts= (
70         authors_file    => "AUTHORS",
71         mailmap_file    => ".mailmap",
72         exclude_file    => "Porting/exclude_contrib.txt",
73         from            => "",
74         to              => "",
75         exclude_contrib => [],
76     );
77
78     ## Parse options and print usage if there is a syntax error,
79     ## or if usage was explicitly requested.
80     GetOptions(
81         \%opts,
82         map {
83             # support hyphens as well as underbars,
84             # underbars must be first. Only handles two
85             # part words right now.
86             ref $_ ? $_ : s/\b([a-z]+)_([a-z]+)\b/${1}_${2}|${1}-${2}/gr
87         } @OPTSPEC,
88     ) or pod2usage(2);
89     $opts{commit_range}= join " ", @ARGV;
90     if (!$opts{commit_range}) {
91         if ($opts{from_commit}) {
92             $opts{to_commit} ||= "HEAD";
93             $opts{$_} =~ s/\.+\z// for qw(from_commit to_commit);
94             $opts{commit_range}= "$opts{from_commit}..$opts{to_commit}";
95         }
96     }
97     pod2usage(1)             if $opts{help};
98     pod2usage(-verbose => 2) if $opts{man};
99
100     foreach my $opt (keys %opts) {
101         $opts{numstat}++   if $implies_numstat{$opt};
102         $opts{no_update}++ if $opt =~ /^show_/ or $opt eq "validate";
103     }
104
105     if (delete $opts{exclude_me}) {
106         my ($author_full)=
107             Porting::updateAUTHORS->current_author_name_email("full");
108         my ($committer_full)=
109             Porting::updateAUTHORS->current_committer_name_email("full");
110
111         push @{ $opts{exclude_contrib} }, $author_full
112             if $author_full;
113         push @{ $opts{exclude_contrib} }, $committer_full
114             if $committer_full
115             and (!$author_full
116             or $committer_full ne $author_full);
117     }
118
119     my $self= Porting::updateAUTHORS->new(%opts);
120
121     my $changed= $self->read_and_update();
122
123     if ($self->{validate}) {
124         for my $file_type (qw(authors_file mailmap_file exclude_file)) {
125             my $file= $self->{$file_type};
126             my $changes= $self->changed_file($file);
127             ok(!$changes, "Is $file_type '$file' up to date?")
128                 or diag $self->_diff_diag($file);
129         }
130         my $dupe_info= $self->dupe_info();
131         ok(!$dupe_info, "No dupes in AUTHORS")
132             or diag $dupe_info;
133
134         ok(
135             !$self->{missing_author}{$_},
136             sprintf "%s is listed in AUTHORS",
137             _clean_name($_)) for sort keys %{ $self->{missing_author} || {} };
138
139         SKIP: {
140             # What is tested in this block:
141             # - check if there uncommitted changes in the git-tree
142             # - if so: is the (configured) author a known contributor?
143
144             skip "AUTOMATED_TESTING is set" if ($ENV{AUTOMATED_TESTING});
145
146             # Test::Smoke leaves some files in the build dir which causes
147             # this code to (correctly) conclude that there are uncommitted
148             # files which then proceeds to check the author name/email.
149             #
150             # On several smokers:
151             # - there is *no* git config;
152             # - a different name/address is configured then the one listed
153             #   in AUTHORS;
154             # which causes the test to fail.
155             #
156             # Unfortunately Test::Smoke doesn't set the AUTOMATED_TESTING
157             # env-var.. Therefor check if mktest.out exist, it's one of the
158             # first files Test::Smoke creates in the build directory.
159             skip "Test::Smoke running" if (-e "./mktest.out");
160
161             my $uncommitted_files= $self->git_status_porcelain;
162             if ($uncommitted_files) {
163                 my ($author_name, $author_email)=
164                     $self->current_author_name_email();
165                 my ($committer_name, $committer_email)=
166                     $self->current_committer_name_email();
167
168                 ok($author_name && $author_email,
169                     "git knows your author name and email.");
170                 ok(
171                     $committer_name && $committer_email,
172                     "git knows your committer name and email."
173                 );
174
175                 my $author_known=
176                     $self->known_contributor($author_name, $author_email);
177                 my $committer_known=
178                     $self->known_contributor($committer_name, $committer_email);
179                 if (
180                     is(
181                         $author_known && $committer_known,
182                         1, "Uncommitted changes are by a known contributor?"
183                     ))
184                 {
185                     diag
186                         "Testing uncommtted changes! Remember to commit before you push!"
187                         if $ENV{TEST_VERBOSE};
188                 }
189                 else {
190                     diag error_advice_for_uncommitted_changes(
191                         $author_name,    $author_email,
192                         $committer_name, $committer_email,
193                         $uncommitted_files
194                     );
195                 }
196             }
197             else {
198                 # this will always pass... but it adds test output that is helpful
199                 ok(!$uncommitted_files,
200                     "git status --porcelain should be empty");
201             }
202         }
203
204         diag "\nFiles need updating! You probably just need to run\n\n",
205             "   Porting/updateAUTHORS.pl\n\n", "and commit the results."
206             if $self->changed_count;
207         done_testing();
208         return 0;
209     }
210     elsif ($self->{show_rank}) {
211         $self->report_stats("who_stats", "author");
212         return 0;
213     }
214     elsif ($self->{show_applied}) {
215         $self->report_stats("who_stats", "applied");
216         return 0;
217     }
218     elsif ($self->{show_stats}) {
219         my @fields= ("author", "applied", "committer");
220         push @fields,
221             ("num_files", "lines_added", "lines_removed", "lines_delta")
222             if $self->{numstat};
223         $self->report_stats("who_stats", @fields);
224         return 0;
225     }
226     elsif ($self->{show_files}) {
227         $self->report_stats(
228             "file_stats",  "commits", "lines_added", "lines_removed",
229             "lines_delta", "binary_change"
230         );
231         return 0;
232     }
233     elsif ($self->{show_file_changes}) {
234         $self->report_stats(
235             "file_stats", "lines_delta", "lines_added", "lines_removed",
236             "commits"
237         );
238         return 0;
239     }
240     elsif ($self->{show_file_chainsaw}) {
241         $self->{in_reverse}= !$self->{in_reverse};
242         $self->report_stats(
243             "file_stats", "lines_delta", "lines_added", "lines_removed",
244             "commits"
245         );
246         return 0;
247     }
248     elsif ($self->{show_who}) {
249         $self->print_who();
250         return 0;
251     }
252     return $changed;    # 0 means nothing changed
253 }
254
255 exit(main()) unless caller;
256
257 sub error_advice_for_uncommitted_changes {
258     my (
259         $author_name,     $author_email, $committer_name,
260         $committer_email, $uncommitted_files
261     )= @_;
262     $_ //= ""
263         for $author_name, $author_email, $committer_name, $committer_email;
264     my $extra= "";
265     my @git_env_keys=
266         map { /^GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL)\z/ ? "$_='$ENV{$_}'" : () }
267         sort keys %ENV;
268     if (@git_env_keys) {
269         $extra .= "\n" . wrap "", "",
270               "Its seems that your environment has "
271             . join(", ", @git_env_keys)
272             . " defined. This may cause this test to fail.\n\n";
273     }
274
275     my $quote= $^O =~ /Win/ ? '"' : "'";
276     my @config= map decode_utf8($_),
277         `git config --get-regexp $quote^(user|author|committer).(name|email)$quote`;
278     if (@config) {
279
280         $extra .=
281             "\nYou have configured the following relevant git config settings:\n\n"
282             . join("",
283             map { sprintf "    %-16s = %s", split /\s+/, $_, 2 } @config)
284             . "\n";
285     }
286     else {
287         $extra .=
288               "\nYou do not have any git user config set up, consider using\n\n"
289             . "    git config user.name 'Your Name'\n"
290             . "    git config user.email 'your\@email.com'\n\n";
291     }
292
293     my $props= "";
294     if (   $author_name ne $committer_name
295         or $author_email ne $committer_email)
296     {
297         $props .= <<EOF_PROPS;
298
299     Author Name     = $author_name
300     Author Email    = $author_email
301     Committer Name  = $committer_name
302     Committer Email = $committer_email
303 EOF_PROPS
304
305         $extra .= <<EOF_EXTRA;
306
307 Your committer and author details differ. You may want to review your
308 git configuration.
309
310 EOF_EXTRA
311
312     }
313     else {
314         $props .= <<EOF_PROPS;
315
316     Name = $author_name
317     Email = $author_email
318 EOF_PROPS
319     }
320
321     return encode_utf8 <<"EOF_MESAGE";
322
323 There are uncommitted changes in the working directory
324 $uncommitted_files
325 and your git credentials are new to us. We think that git thinks your
326 credentials are as follows (git may use defaults we don't guess
327 properly):
328 $props$extra
329 To resolve this you can perform one or more of these steps:
330
331     1. Remove the uncommitted changes, including untracked files that
332        show up in
333
334             git status
335
336        if you wish to REMOVE UNTRACKED FILES and DELETE ANY CHANGES
337        you can
338
339             git clean -dfx
340             git checkout -f
341
342         BE WARNED: THIS MAY LOSE DATA.
343
344     2. You are already configured in git and you just need to add
345        yourself to AUTHORS and other infra: commit the changes in the
346        working directory, including any untracked files that you plan to
347        add (the rest should be removed), and then run
348
349             Porting/updateAUTHORS.pl
350
351        to update the AUTHORS and .mailmap files automatically. Inspect
352        the changes it makes and then commit them once you are
353        satisfied. This is your option to decide who you will be known
354        as in the future!
355
356     3. You are already a contributor to the project but you are committing
357        changes on behalf of someone who is new. Run
358
359             Porting/updateAUTHORS.pl
360
361        to update the AUTHORS and .mailmap files automatically. Inspect
362        the changes it makes and then commit them once you are satisfied.
363        Make sure the conributor is ok with the decisions you make before
364        you merge.
365
366     3. You are already an author but your git config is broken or
367        different from what you expect, or you are a new author but you
368        havent configured your git details properly, in which case you
369        can use something like the following commands:
370
371             git config user.name "Some Name"
372             git config user.email "somewhere\@provider"
373
374        If you are known to the project already this is all you need to
375        do. If you are not then you should perform option 2 or 4 as well
376        afterwards.
377
378     4. You do not want to be listed in AUTHORS: commit the changes,
379        including any untracked unignored files, and then run
380
381             Porting/updateAUTHORS.pl --exclude
382
383        and commit the changes it creates. This test should pass once
384        those commits are created. Thank you for your contributions.
385 EOF_MESAGE
386 }
387 1;
388 __END__
389
390 =head1 NAME
391
392 F<Porting/updateAUTHORS.pl> - Automatically update F<AUTHORS> and F<.mailmap>
393 and F<Porting/exclude_contrib.txt> based on commit data.
394
395 =head1 SYNOPSIS
396
397 Porting/updateAUTHORS.pl [OPTIONS] [GIT_REF_RANGE]
398
399 By default scans the commit history specified (or the entire history from the
400 current commit) and then updates F<AUTHORS> and F<.mailmap> so all contributors
401 are properly listed.
402
403  Options:
404    --help               brief help message
405    --man                full documentation
406    --verbose            be verbose
407
408  Commit Range:
409    --from=GIT_REF       Select commits to use
410    --to=GIT_REF         Select commits to use, defaults to HEAD
411
412  File Locations:
413    --authors-file=FILE  override default of 'AUTHORS'
414    --mailmap-file=FILE  override default of '.mailmap'
415
416  Action Modifiers
417    --no-update          Do not update.
418    --validate           output TAP about status and change nothing
419    --exclude-missing    Add new names to the exclude file so they never
420                         appear in AUTHORS or .mailmap.
421
422  Details Changes
423     Update canonical name or email in AUTHORS and .mailmap properly.
424     --exclude-contrib       NAME_AND_EMAIL
425     --exclude-me
426     --change-name           OLD_NAME=NEW_NAME
427     --change-name-for-email OLD_ADDR=NEW_NAME
428     --change-email-for-name OLD_NAME=NEW_ADDR
429     --change-email          OLD_ADDR=NEW_EMAIL
430
431  Reports About People
432     --stats             detailed report of authors and what they did
433     --who               Sorted, wrapped list of who did what
434     --thanks-applied    report who applied stuff for others
435     --rank              report authors by number of commits created
436
437  Reports About Files
438     --files             detailed report files that were modified
439     --activity          simple report of files that grew the most
440     --chainsaw          simple report of files that shrank the most
441
442  Report Modifiers
443     --percentage        show percentages not counts
444     --cumulative        show cumulative numbers not individual
445     --reverse           show reports in reverse order
446     --numstat           show additional file based data in some reports
447                         (not needed for most reports)
448     --as-list           show reports with names with common values
449                         folded into a list like checkAUTHORS.pl used to
450     --numbered          add rank numbers to reports where they are missing
451
452 =head1 OPTIONS
453
454 =over 4
455
456 =item C<--help>
457
458 Print a brief help message and exits.
459
460 =item C<--man>
461
462 Prints the manual page and exits.
463
464 =item C<--verbose>
465
466 Be verbose about what is happening. Can be repeated more than once.
467
468 =item C<--no-update>
469
470 Do not update files on disk even if they need to be changed.
471
472 =item C<--validate>
473
474 =item C<--tap>
475
476 Instead of modifying files, test to see which would be modified and
477 output TAP test output about the validation.
478
479 =item C<--authors-file=FILE>
480
481 =item C<--authors_file=FILE>
482
483 Override the default location of the authors file, which is by default
484 the F<AUTHORS> file in the current directory.
485
486 =item C<--mailmap-file=FILE>
487
488 =item C<--mailmap_file=FILE>
489
490 Override the default location of the mailmap file, which is by default
491 the F<.mailmap> file in the current directory.
492
493 =item C<--exclude-file=FILE>
494
495 =item C<--exclude_file=FILE>
496
497 Override the default location of the exclude file, which is by default
498 the F<Porting/exclude_contrib.txt> file reachable from the current
499 directory.
500
501 =item C<--exclude-contrib=NAME_AND_EMAIL>
502
503 =item C<--exclude_contrib=NAME_AND_EMAIL>
504
505 Exclude a specific name/email combination from our contributor datasets.
506 Can be repeated multiple times on the command line to remove multiple
507 items at once. If the contributor details correspond to a canonical
508 identity of a contributor (one that is in the AUTHORS file or on the
509 left in the .mailmap file) then ALL records, including those linked to
510 that identity in .mailmap will be marked for exclusion. This is similar
511 to C<--exclude-missing> but it only affects the specifically named
512 users. Note that the format for NAME_AND_EMAIL is similar to that of the
513 .mailmap file, email addresses and C< @github > style identifiers should
514 be wrapped in angle brackets like this: C<< <@github> >>, users with no
515 email in the AUTHORS file should use C<< <unknown> >>.
516
517 For example:
518
519   Porting/updateAUTHORS.pl --exclude-contrib="Joe B <b@joe.com>"
520
521 Would remove all references to "Joe B" from F<AUTHORS> and F<.mailmap>
522 and add the required entires to F<Porting/exclude_contrib.txt> such that
523 the contributor would never be automatically added back, and would be
524 automatically removed should someone read them manually.
525
526 =item C<--exclude-missing>
527
528 =item C<--exclude_missing>
529
530 =item C<--exclude>
531
532 Normally when the tool is run it *adds* missing data only. If this
533 option is set then the reverse will happen, any author data missing will
534 be marked as intentionally missing in such a way that future "normal"
535 runs of the script ignore the author(s) that were excluded.
536
537 The exclude data is stored in F<Porting/exclude_contrib.txt> as a SHA256
538 digest (in base 64) of the user name and email being excluded so that
539 the list itself doesnt contain the contributor details in plain text.
540
541 The general idea is that if you want to remove someone from F<AUTHORS>
542 and F<.mailmap> you delete their details manually, and then run this
543 tool with the C<--exclude> option. It is probably a good idea to run it
544 first without any arguments to make sure you dont exclude something or
545 someone you did not intend to.
546
547 =item C<--stats>
548
549 Show detailed stats about committers and the work they did in a tabular
550 form. If the C<--numstat> option is provided this report will provide
551 additional data about the files a developer worked on. May be slow the
552 first time it is used as git unpacks the relevant data.
553
554 =item C<--who>
555
556 Show a list of which committers and authors contributed to the project
557 in the selected range of commits. The list will contain the name only,
558 and will sorted according to unicode collation rules. This list is
559 suitable in release notes and similar contexts.
560
561 =item C<--thanks-applied>
562
563 Show a report of which committers applied work on behalf of
564 someone else, including counts. Modified by the C<--as-list> and
565 C<--display-rank>.
566
567 =item C<--rank>
568
569 Shows a report of which commits did the most work. Modified by the
570 C<--as-list> and C<--display-rank> options.
571
572 =item C<--files>
573
574 Show detailed stats about the files that have been modified in the
575 selected range of commits. Implies C<--numstat>. May be slow the first
576 time it is used as git unpacks the relevant data.
577
578 =item C<--activity>
579
580 Show simple stats about which files had the most additions. Implies
581 C<--numstat>. May be slow the first time it is used as git unpacks the
582 relevant data.
583
584
585 =item C<--chainsaw>
586
587 Show simple stats about whcih files had the most removals. Implies
588 C<--numstat>. May be slow the first time it is used as git unpacks the
589 relevant data.
590
591 =item C<--percentage>
592
593 Show numeric data as percentages of the total, not counts.
594
595 =item C<--cumulative>
596
597 Show numeric data as cumulative counts in the reports.
598
599 =item C<--reverse>
600
601 Show the reports in reverse order to normal.
602
603 =item C<--numstat>
604
605 Gather additional data about the files that were changed, not just the
606 authors who did the changes. This option currently is only necessary for
607 the C<--stats> option, which will display additional data when this
608 option is also provided.
609
610 =item C<--as-list>
611
612 Show the reports with name data rolled up together into a list like the
613 older checkAUTHORS.pl script would have.
614
615 =item C<--numbered>
616
617 Show an additional column with the rank number of a row in the report in
618 reports that do not normally show the rank number.
619
620 =item C<--change-name OLD_NAME=NEW_NAME>
621
622 =item C<--change-name-for-email OLD_EMAIL=NEW_NAME>
623
624 =item C<--change-email OLD_EMAIL=NEW_EMAIL>
625
626 =item C<--change-email-for-name OLD_NAME=NEW_EMAIL>
627
628 Change email or name based on OLD_NAME or OLD_EMAIL.
629
630 Eg,
631
632     --change-name-for-email somebody@gmail.com="Bob Rob"
633
634 would cause the preferred name for the person with the preferred email
635 C<somebody@gmail.com> to change to "Bob Rob" in our records. If that
636 persons name was "Daniel Dude" then we might have done this as well:
637
638     --change-name "Bob Rob"="Daniel Dude"
639
640 =back
641
642 =head1 DESCRIPTION
643
644 This program will automatically manage updates to the F<AUTHORS> file
645 and F<.mailmap> file based on the data in our commits and the data in
646 the files themselves. It uses no other sources of data. Expects to be
647 run from the root directory of a git repo of perl.
648
649 In simple, execute the script and it will either die with a helpful
650 message or it will update the files as necessary, possibly not at all
651 if there is no need to do so. If the C<--validate> option is provided
652 the content will not be updated and instead the tool will act as a
653 test script validating that the F<AUTHORS> and F<.mailmap> files are
654 up to date.
655
656 By default the script operates on the *entire* history of Perl
657 development that is reachable from HEAD. This can be overriden by using
658 the C<--from> and C<--to> options, or providing a git commit range as an
659 argument after the options just like you might do with C<git log>.
660
661 The script can also be used to produce various reports and other content
662 about the commits it has analyzed.
663
664 =head2 ADDING A NEW CONTRIBUTOR
665
666 Commit your changes. Run the tool with no arguments. It will add
667 anything that is missing. Check the changes and then commit them.
668
669 =head2 CHANGING A CONTRIBUTORS CANONICAL NAME OR EMAIL
670
671 Use the C<--change-name-for-name> and related options. This will do
672 things "properly" and update all the files.
673
674 =head2 A CONTRIBUTOR WANTS TO BE FORGOTTEN
675
676 There are several ways to do this:
677
678 =over 2
679
680 =item Manual Exclusion
681
682 Manually modify F<AUTHORS> and F<.mailmap> so the user detals are
683 removed and then run this tool with the C<--exclude> option. This should
684 result in various SHA-256 digests (in base64) being added to
685 F<Porting/exclude_contrib.txt>. Commit the changes afterwards.
686
687 =item Exclude Yourself
688
689 Use the C<--exclude-me> option to the tool, review and commit the results.
690 This will use roughly the same rules that git would to figure out what your
691 name and email are.
692
693 =item Exclude Someone Else
694
695 Use the C<--exclude-contrib> option and specify their name and email.
696 For example
697
698  --exclude-contrib="Their Name <email@provider.com>"
699
700 Should exclude the person with this name from our files.
701
702 =back
703
704 Note that excluding a person by canonical details (that is the details
705 in the F<AUTHORS> file) will result in their .mailmap'ed names being
706 excluded as well. Excluding a persons secondary account details will
707 simply block that specific email from being listed, and is likely not
708 what you want to do most of the time.
709
710 =head2 AFTER RUNNING THE TOOL
711
712 Review the changes to make sure they are sane. If they are ok (and
713 they should be most of the time) commit. If they are not then update
714 the F<AUTHORS> or F<.mailmap> files as is appropriate and run the
715 tool again.
716
717 Do not panic that your email details get added to F<.mailmap>, this is
718 by design so that your chosen name and email are displayed on GitHub and
719 in casual use of C<git log> and other C<git> tooling.
720
721 =head1 RECIPES
722
723   perl Porting/updateAUTHORS.pl --who --from=v5.31.6 --to=v5.31.7
724   perl Porting/updateAUTHORS.pl --who v5.31.6..v5.31.7
725   perl Porting/updateAUTHORS.pl --rank --percentage --from=v5.31.6
726   perl Porting/updateAUTHORS.pl --thanks-applied --from=v5.31.6
727   perl Porting/updateAUTHORS.pl --tap --from=v5.31.6
728   perl Porting/updateAUTHORS.pl --files --from=v5.31.6
729   perl Porting/updateAUTHORS.pl --activity --from=v5.31.6
730   perl Porting/updateAUTHORS.pl --chainsaw v5.31.6..HEAD
731   perl Porting/updateAUTHORS.pl --change-name "Old Name"="New Name"
732   perl Porting/updateAUTHORS.pl --change-name-for-email "x@y.com"="Name"
733   perl Porting/updateAUTHORS.pl --change-email-for-name "Name"="p@q.com"
734
735 =head1 RELATED FILES
736
737 F<AUTHORS>, F<.mailmap>, F<Porting/excluded_author.txt>
738
739 =head1 TODO
740
741 More documentation and testing.
742
743 =head1 AUTHOR
744
745 Yves Orton <demerphq@gmail.com>
746
747 =head1 THANKS
748
749 Loosely based on the older F<Porting/checkAUTHORS.pl> script which this tool
750 replaced. Thanks to the contributors of that tool. See the Perl change log.
751
752 =cut