1#!/usr/bin/perl -w
2# (c) 2018, Lars Kurth <lars.kurth@citrix.com>
3#
4# Add maintainers to patches generated with git format-patch
5#
6# Usage: perl scripts/add_maintainers.pl [OPTIONS] --patchdir <patchdir>
7#
8# Prerequisites: Execute
9#                git format-patch ... -o <patchdir> ...
10#
11#                ./scripts/get_maintainer.pl is present in the tree
12#
13# Licensed under the terms of the GNU GPL License version 2
14
15use strict;
16
17use Getopt::Long qw(:config no_auto_abbrev);
18use File::Basename;
19use List::MoreUtils qw(uniq);
20use IO::Handle;
21
22sub getmaintainers ($$$);
23sub gettagsfrompatch ($$$;$);
24sub normalize ($$);
25sub insert ($$$$);
26sub hastag ($$);
27
28# Tool Variables
29my $tool = $0;
30my $get_maintainer = $tool;
31$get_maintainer =~ s/add_maintainers/get_maintainer/;
32my $usage = <<EOT;
33OPTIONS:
34--------
35USAGE: $tool [options] (--patchdir | -d | -o) <patchdir>
36
37  --reroll-count <n> | -v <n>
38    Choose patch files for specific version. This results into the
39    following filters on <patchdir>
40    0: default - *.patch
41    >1: v<n>*.patch
42
43  --patchcc (header|commit|comment|none) | -p (header|commit|comment|none)
44
45    Insert CC lines into *.patch files in the specified location.
46    When `none` is specified, the *.patch files are not changed.
47    See LOCATIONS for a definition of the various locations.
48
49    The default is `header`.
50
51  --covercc (header|end|none) | -c (header|end|none)
52
53    Insert CC lines into cover letter in the specified location. See
54    When `none` is specified, the cover letter is not changed.
55    LOCATIONS for a definition of the various locations.
56
57    The default is `header`.
58
59  --tagscc
60
61    In addition to the output of get_maintainer.pl, include email
62    addresses from commit tags (e.g., Reviewed-by, Tested-by, ...) in
63    the list of CC lines to insert.
64
65    These extra lines will be inserted as specified by the --patchcc
66    and --covercc options. When used with `--patchcc commit`,
67    this will duplicate e-mail addresses in the commit message.
68
69  --tags | -t
70
71    As above, but the insert location is special-cased: e-mail addresses
72    will always be inserted into the `header` of patches and the cover letter.
73
74  --get-maintainers=<program>
75
76    Run <program> instead of $get_maintainer.
77    (Passing `true` for <program> suppresses the usual computation
78    of CCs, from files touched by patches and MAINTAINERS.)
79
80  --arg <argument> | -a <argument> ...
81    Arguments passed on to get_maintainer.pl
82    This option can be used multiple times, e.g. -a <a1> -a <a2> ...
83
84  --verbose
85    Show more output
86
87  --help | -h
88    Show this help information
89
90LOCATIONS:
91----------
92
93  *.patch and cover letters files consist of several sections relevant
94   to processing:
95
96  <header>:  This is the email header containing email related information
97             It ends with the Subject: line
98
99  <commit>: This is the email body that ends up in the commit message.
100             It ends with ---.  CC lines added here will be checked
101             into the git tree on commit.  Only applicable to normal
102             patch files.
103
104  <comment>: This is the 'comment for reviewers' section, after the
105             --- but before the diff actually starts. CCs added here
106             are processed by git send-email, but are not checked into
107             the git tree on commit.  Only applicable to normal patch
108             files.
109
110  <end>:     The part of a cover letter just before `-- ` (which normally
111             begins a diffstat).  Only applicable to cover letters.
112
113  DEFAULT BEHAVIOUR:
114  ------------------
115  * get_maintainer is called on each patch to find email addresses
116    of maintainers/reviewers for that patch
117  * All of the above addresses are added to the CC mail headers
118    of each patch
119  * All of the above addresses are added to the CC mail headers
120    of the cover letter
121
122WORKFLOW:
123---------
124  This script is intended to be used as part of the following workflow
125
126  Step 1: git format-patch ... -o <patchdir> ...
127  Step 2: ./scripts/add_maintainers.pl -d <patchdir>
128          This overwrites *.patch files in <patchdir> but makes a backup
129  Step 3: git send-email --to xen-devel\@lists.xenproject.org <patchdir>/*.patch
130EOT
131
132# Constants and functions related to LOCATIONS
133
134# Constants for -p|--patchcc and -c|--covercc option processing
135my @plocations= ("header", "commit", "comment", "none");
136my @clocations= ("header", "end", "none");
137
138# Hash is used to determine which mode value maps onto which search string
139my %inssearch = (
140    "header"  => "Date:",          # Insert before Date:
141    "commit"  => "Signed-off-by:", # Insert before Signed-off-by:
142    "comment" => "---",            # Insert after ---
143    "end"     => "-- ",            # Insert before '-- '
144);
145
146# Hash is used to determine whether for a given mode we insert CCs after
147# the search string or before
148my %insafter = (
149    "header"  => 0,
150    "commit"  => 0,
151    "comment" => 1,
152    "end"     => 0,
153);
154
155# The following subroutines take a areference to arrays of
156# - @header: contains CCs from *-by: tags and TOs from mailing lists
157# - @cc:  contains all other CC's
158# It will then apply the corect locations on the input file
159
160sub applylocation_header ($$$) {
161    my ($file, $rheader, $rcc) = @_;
162    my $insert = join("\n", uniq (@$rheader, @$rcc));
163    insert($file , $insert, $inssearch{header}, $insafter{header});
164}
165
166sub applymixedlocation ($$$$) {
167    my ($file, $rheader, $rcc, $mode) = @_;
168    my $header = join("\n", @$rheader);
169    my $cc  = join("\n", @$rcc);
170    # Insert snippets into files
171    insert($file , $cc, $inssearch{$mode}, $insafter{$mode});
172    # The header
173    insert($file , $header, $inssearch{header}, $insafter{header});
174}
175
176sub applylocation_commit($$$) {
177    my ($file, $rheader, $rcc) = @_;
178    applymixedlocation($file, $rheader, $rcc, "commit");
179}
180
181# Use a different name to make sure perl doesn't throw a syntax error
182sub applylocation_comment ($$$) {
183    my ($file, $rheader, $rcc) = @_;
184    applymixedlocation($file, $rheader, $rcc, "comment");
185}
186
187sub applylocation_end ($$$) {
188    my ($file, $rheader, $rcc) = @_;
189    applymixedlocation($file, $rheader, $rcc, "end");
190}
191
192sub applylocation_none ($$$) {
193    return;
194}
195
196# Hash for location functions
197my %applylocation = (
198    "header"  => \&applylocation_header,
199    "commit"  => \&applylocation_commit,
200    "comment" => \&applylocation_comment,
201    "end"     => \&applylocation_end,
202    "none"    => \&applylocation_none,
203);
204
205# Arguments / Options
206my $help = 0;
207my $patch_dir = 0;
208my @get_maintainer_args = ();
209my $verbose = 0;
210my $rerollcount = 0;
211my $tags = 0;
212my $tagscc = 0;
213my $plocation = "header";
214my $clocation = "header";
215
216# Constants
217# Keep these as constants, in case we want to make these configurable
218# in future
219my $CC                  = "Cc:"; # Note: git-send-mail requires Cc:
220my $TO                  = "To:";
221my $cover_letter        = "0000-cover-letter.patch";
222my $patch_ext           = ".patch";
223my $maintainers         = "MAINTAINERS";
224
225if (!GetOptions(
226                'd|o|patchdir=s'   => \$patch_dir,
227                'v|reroll-count=i' => \$rerollcount,
228                'p|patchcc=s'      => \$plocation,
229                'c|covercc=s'      => \$clocation,
230                't|tags'           => \$tags,
231                'tagscc'           => \$tagscc,
232                'a|arg=s'          => \@get_maintainer_args,
233                'get-maintainers=s' => \$get_maintainer,
234                'verbose'          => \$verbose,
235                'h|help'           => \$help,
236                )) {
237    die "$tool: invalid argument - use --help if necessary\n";
238}
239
240if ($help) {
241    print $usage;
242    exit 0;
243}
244
245if (!$patch_dir) {
246    die "$tool: Directory -d|--patchdir not specified\n";
247}
248
249if (! -e $patch_dir) {
250    die "$tool: Directory $patch_dir does not exist\n";
251}
252
253# Calculate the $patch_prefix
254my $patch_prefix = "";
255if ($rerollcount == 0) {
256    # If the user didn't specify -v and we are here, then
257    # - either the directory is empty
258    # - or it contains some version of a patch
259    # In this case we search for the first patch and
260    # work out the version
261    $!=0;
262    my @coverletters = glob($patch_dir.'/*'.$patch_ext);
263    if (!$! && scalar @coverletters) {
264        if ($coverletters[0] =~ /\/v([0-9]+)-\Q$cover_letter\E/) {
265            $rerollcount = $1;
266        }
267    }
268}
269if ($rerollcount > 0) {
270    $patch_prefix = "v".$rerollcount."-";
271}
272
273if ( ! grep $_ eq $plocation, @plocations) {
274    die "$tool: Invalid -p|--patchcc value\n";
275}
276if ( ! grep $_ eq $clocation, @clocations) {
277    die "$tool: Invalid -c|--covercc value\n";
278}
279
280# Get the list of patches
281my $has_cover_letter = 0;
282my $cover_letter_file;
283my $pattern = $patch_dir.'/'.$patch_prefix.'[0-9][0-9][0-9][0-9]*'.$patch_ext;
284
285$!=0;
286my @patches = glob($pattern);
287if ($!) {
288    die "$tool: Directory $patch_dir contains no patches\n";
289}
290if (!scalar @patches) {
291    die "$tool: Directory $patch_dir contains no matching patches.\n".
292         "Please try --reroll-count <n> | -v <n>\n";
293}
294
295# Do the actual processing
296my $file;
297my @combined_header;
298my @combined_cc;
299
300foreach my $file (@patches) {
301    if ($file =~ /\/\Q$patch_prefix$cover_letter\E/) {
302        $has_cover_letter = 1;
303        $cover_letter_file = $file;
304    } else {
305        my @header;     # To: lists returned by get_maintainers.pl
306        my @headerpatch;# To: entries in *.patch
307                        #
308                        # Also includes CC's from tags as we do not want
309                        # entries in the body such as
310                        # CC: lars.kurth@citrix.com
311                        # ...
312                        # Tested-by: lars.kurth@citrix.com
313
314        my @cc;         # Cc: maintainers returned by get_maintainers.pl
315        my @ccpatch;    # Cc: entries in *.patch
316        my @extrapatch; # Cc: for AB, RB, RAB in *.patch
317
318        print "Processing: ".basename($file)."\n";
319
320        # Read tags from output of get_maintainers.pl
321        # Lists go into @header and everything else into @cc
322        getmaintainers($file, \@header, \@cc);
323
324        # Read all lines with CC & TO from the patch file (these will
325        # likely come from the commit message). Also read tags.
326        gettagsfrompatch($file, \@headerpatch, \@ccpatch, \@extrapatch);
327
328        # With -t|--tags only add @extrapatch to @header and @combined_header
329        # With --tagscc treat tags as CC that came from the *.patch file
330        if ($tags && !$tagscc) {
331            # Copy these always onto the TO related arrays
332            push @header, @extrapatch;
333            push @combined_header, @extrapatch;
334        } elsif ($tagscc) {
335            # Treat these as if they came from CC's
336            push @ccpatch, @extrapatch;
337            push @combined_cc, @extrapatch;
338        }
339
340        # In this section we normalize the lists. We remove entries
341        # that are already in the patch, from @cc and @to
342        my @header_only = normalize(\@header, \@headerpatch);
343        my @cc_only  = normalize(\@cc, \@ccpatch);
344
345        # Apply the location
346        $applylocation{$plocation}($file, \@header_only, \@cc_only);
347    }
348}
349
350# Deal with the cover letter
351if ($has_cover_letter) {
352    my @headerpatch;   # Entries inserted at the header
353    my @ccpatch;    # Cc: entries in *.patch
354
355    print "Processing: ".basename($cover_letter_file)."\n";
356
357    # Read all lines with CC & TO from the patch file such that subsequent
358    # calls don't lead to duplication
359    gettagsfrompatch($cover_letter_file, \@headerpatch, \@ccpatch);
360
361    # In this section we normalize the lists. We remove entries
362    # that are already in the patch, from @cc and @to
363    my @header_only = normalize(\@combined_header, \@headerpatch);
364    my @cc_only  = normalize(\@combined_cc, \@ccpatch);
365
366    # Apply the location
367    $applylocation{$clocation}($cover_letter_file, \@header_only, \@cc_only);
368
369    print "\nDon't forget to add the subject and message to ".
370          $cover_letter_file."\n";
371}
372
373print "Then perform:\n".
374      "git send-email --to xen-devel\@lists.xenproject.org ".
375      $patch_dir.'/'.$patch_prefix."*.patch"."\n";
376
377exit 0;
378
379my $getmailinglists_done = 0;
380my @mailinglists = ();
381
382sub getmailinglists () {
383   # Read mailing list from MAINTAINERS file and copy
384   # a list of e-mail addresses to @mailinglists
385    if (!$getmailinglists_done) {
386        if (-e $maintainers) {
387            my $fh;
388            my $line;
389            open($fh, "<", $maintainers) or die $!;
390            while (my $line = <$fh>) {
391                chomp $line;
392                if ($line =~ /^L:[[:blank:]]+/m) {
393                   push @mailinglists, $';
394                }
395            }
396            $fh->error and die $!;
397            close $fh or die $!;
398        } else {
399            print "Warning: file '$maintainers' does not exist\n";
400            print "Warning: Mailing lists will be treated as CC's\n";
401        }
402    # Don't try again, even if the MAINTAINERS file does not exist
403    $getmailinglists_done = 1;
404    # Remove any duplicates
405    @mailinglists = uniq @mailinglists;
406    }
407}
408
409sub ismailinglist ($) {
410    my ($check) = @_;
411    # Get the mailing list information
412    getmailinglists();
413    # Do the check
414    if ( grep { $_ eq $check} @mailinglists) {
415        return 1;
416    }
417    return 0;
418}
419
420sub getmaintainers ($$$) {
421    my ($file, $rto, $rcc) = @_;
422    my $fh;
423    open($fh, "-|", $get_maintainer, @get_maintainer_args, $file)
424        or die "Failed to open '$get_maintainer'\n";
425    while(my $line = <$fh>) {
426        chomp $line;
427        # Keep lists and CC's separately as we dont want them in
428        # the commit message under a Cc: line
429        if (ismailinglist($line)) {
430            push @$rto, $TO." ".$line;
431            push @combined_header, $TO." ".$line;
432        } else {
433            push @$rcc, $CC." ".$line;
434            push @combined_cc, $CC." ".$line;
435        }
436    }
437    $fh->error and die $!;
438    close $fh or die $!;
439}
440
441sub gettagsfrompatch ($$$;$) {
442    my ($file, $rto, $rcc, $rextra) = @_;
443    my $fh;
444
445    open($fh, "<", $file)
446        or die "Failed to open '$file'\n";
447    while(my $line = <$fh>) {
448        chomp $line;
449        my $nline;
450
451        if (hastag($line, $TO)) {
452            push @$rto, $line;
453            push @combined_header, $line;
454        }
455        if (hastag($line, $CC)) {
456            push @$rcc, $line;
457            push @combined_cc, $line;
458        }
459        # If there is an $rextra, then get various tags and add
460        # email addresses to the CC list
461        if ($rextra && $line =~ /^[-0-9a-z]+-by:[[:blank:]]+/mi) {
462            push @$rextra, $CC." ".$';
463        }
464    }
465    $fh->error and die $!;
466    close $fh or die $!;
467}
468
469sub hastag ($$) {
470    my ($line, $tag) = @_;
471    if ($line =~ m{^\Q$tag\E}i) {
472        return 1;
473    }
474    return 0;
475}
476
477sub normalize ($$) {
478    my ($ra, $rb) = @_;
479    # This function is used to normalize lists of tags or CC / TO lists
480    # It returns a list of the unique elements
481    # in @$ra, excluding any which are in @$rb.
482    # Comparisons are case-insensitive.
483    my @aonly = ();
484    my %seen;
485    my $item;
486
487    foreach $item (@$rb) {
488        $seen{lc($item)} = 1;
489    }
490    foreach $item (@$ra) {
491        unless ($seen{lc($item)}++) {
492            # it's not in %seen, so add to @aonly
493            push @aonly, $item;
494        }
495    }
496
497    return @aonly;
498}
499
500sub readfile ($) {
501    my ($file) = @_;
502    my $fh;
503    my $content;
504    open($fh, "<", $file)
505         or die "Could not open file '$file' $!";
506    $content = do { local $/; <$fh> };
507    $fh->error and die $!;
508    close $fh or die $!;
509
510    return $content;
511}
512
513sub writefile ($$) {
514    my ($content, $file) = @_;
515    my $fh;
516    open($fh, ">", "$file.tmp")
517         or die "Could not open file '$file.tmp' $!";
518    print $fh $content or die $!;
519    close $fh or die $!;
520    rename "$file.tmp", $file or die "Could not rename '$file' into place $!";
521}
522
523sub insert ($$$$) {
524    my ($file, $insert, $delimiter, $insafter) = @_;
525    my $content;
526
527    if ($insert eq "") {
528        # Nothing to insert
529        return;
530    }
531    # Read file
532    $content = readfile($file) or die $!;
533
534    # Split the string and generate new content
535    if ($content =~ /^\Q$delimiter\E/mi) {
536        if ($insafter) {
537            writefile($`.$delimiter."\n".$insert."\n".$', $file);
538
539            if ($verbose) {
540                print "\nInserted into ".basename($file).' after "'.
541                      $delimiter."'"."\n-----\n".$insert."\n-----\n";
542            }
543        } else {
544            writefile($`.$insert."\n".$delimiter.$', $file);
545
546            if ($verbose) {
547                print "\nInserted into ".basename($file).' before "'.
548                      $delimiter."'"."\n-----\n".$insert."\n-----\n";
549            }
550        }
551
552    } else {
553       print "Error: Didn't find '$delimiter' in '$file'\n";
554    }
555}
556