#!/usr/bin/perl -w # (c) 2018, Lars Kurth # # Add maintainers to patches generated with git format-patch # # Usage: perl scripts/add_maintainers.pl [OPTIONS] -patchdir # # Prerequisites: Execute # git format-patch ... -o ... # # ./scripts/get_maintainer.pl is present in the tree # # Licensed under the terms of the GNU GPL License version 2 use strict; use Getopt::Long qw(:config no_auto_abbrev); use File::Basename; use List::MoreUtils qw(uniq); use IO::Handle; sub getmaintainers ($$$); sub gettagsfrompatch ($$$;$); sub normalize ($$); sub insert ($$$$); sub hastag ($$); # Tool Variables my $tool = $0; my $get_maintainer = $tool; $get_maintainer =~ s/add_maintainers/get_maintainer/; my $usage = < --reroll-count | -v Choose patch files for specific version. This results into the following filters on 0: default - *.patch >1: v*.patch --patchcc (header|commit|comment|none) | -p (header|commit|comment|none) Insert CC lines into *.patch files in the specified location. When `none` is specified, the *.patch files are not changed. See LOCATIONS for a definition of the various locations. The default is `header`. --covercc (header|end|none) | -c (header|end|none) Insert CC lines into cover letter in the specified location. See When `none` is specified, the cover letter is not changed. LOCATIONS for a definition of the various locations. The default is `header`. --tagscc In addition to the output of get_maintainer.pl, include email addresses from commit tags (e.g., Reviewed-by, Tested-by, ...) in the list of CC lines to insert. These extra lines will be inserted as specified by the --patchcc and --covercc options. When used with `--patchcc commit`, this will duplicate e-mail addresses in the commit message. --tags | -t As above, but the insert location is special-cased: e-mail addresses will always be inserted into the `header` of patches and the cover letter. --get-maintainers= Run instead of $get_maintainer. (Passing `true` for suppresses the usual computation of CCs, from files touched by patches and MAINTAINERS.) --arg | -a ... Arguments passed on to get_maintainer.pl This option can be used multiple times, e.g. -a -a ... --verbose Show more output --help | -h Show this help information LOCATIONS: ---------- *.patch and cover letters files consist of several sections relevant to processing:
: This is the email header containing email related information It ends with the Subject: line : This is the email body that ends up in the commit message. It ends with ---. CC lines added here will be checked into the git tree on commit. Only applicable to normal patch files. : This is the 'comment for reviewers' section, after the --- but before the diff actually starts. CCs added here are processed by git send-email, but are not checked into the git tree on commit. Only applicable to normal patch files. : The part of a cover letter just before `-- ` (which normally begins a diffstat). Only applicable to cover letters. DEFAULT BEHAVIOUR: ------------------ * get_maintainer is called on each patch to find email addresses of maintainers/reviewers for that patch * All of the above addresses are added to the CC mail headers of each patch * All of the above addresses are added to the CC mail headers of the cover letter WORKFLOW: --------- This script is intended to be used as part of the following workflow Step 1: git format-patch ... -o ... Step 2: ./scripts/add_maintainers.pl -d This overwrites *.patch files in but makes a backup Step 3: git send-email -to xen-devel\@lists.xenproject.org /*.patch EOT # Constants and functions related to LOCATIONS # Constants for -p|--patchcc and -c|--covercc option processing my @plocations= ("header", "commit", "comment", "none"); my @clocations= ("header", "end", "none"); # Hash is used to determine which mode value maps onto which search string my %inssearch = ( "header" => "Date:", # Insert before Date: "commit" => "Signed-off-by:", # Insert before Signed-off-by: "comment" => "---", # Insert after --- "end" => "-- ", # Insert before '-- ' ); # Hash is used to determine whether for a given mode we insert CCs after # the search string or before my %insafter = ( "header" => 0, "commit" => 0, "comment" => 1, "end" => 0, ); # The following subroutines take a areference to arrays of # - @header: contains CCs from *-by: tags and TOs from mailing lists # - @cc: contains all other CC's # It will then apply the corect locations on the input file sub applylocation_header ($$$) { my ($file, $rheader, $rcc) = @_; my $insert = join("\n", uniq (@$rheader, @$rcc)); insert($file , $insert, $inssearch{header}, $insafter{header}); } sub applymixedlocation ($$$$) { my ($file, $rheader, $rcc, $mode) = @_; my $header = join("\n", @$rheader); my $cc = join("\n", @$rcc); # Insert snippets into files insert($file , $cc, $inssearch{$mode}, $insafter{$mode}); # The header insert($file , $header, $inssearch{header}, $insafter{header}); } sub applylocation_commit($$$) { my ($file, $rheader, $rcc) = @_; applymixedlocation($file, $rheader, $rcc, "commit"); } # Use a different name to make sure perl doesn't throw a syntax error sub applylocation_comment ($$$) { my ($file, $rheader, $rcc) = @_; applymixedlocation($file, $rheader, $rcc, "comment"); } sub applylocation_end ($$$) { my ($file, $rheader, $rcc) = @_; applymixedlocation($file, $rheader, $rcc, "end"); } sub applylocation_none ($$$) { return; } # Hash for location functions my %applylocation = ( "header" => \&applylocation_header, "commit" => \&applylocation_commit, "comment" => \&applylocation_comment, "end" => \&applylocation_end, "none" => \&applylocation_none, ); # Arguments / Options my $help = 0; my $patch_dir = 0; my @get_maintainer_args = (); my $verbose = 0; my $rerollcount = 0; my $tags = 0; my $tagscc = 0; my $plocation = "header"; my $clocation = "header"; # Constants # Keep these as constants, in case we want to make these configurable # in future my $CC = "Cc:"; # Note: git-send-mail requires Cc: my $TO = "To:"; my $cover_letter = "0000-cover-letter.patch"; my $patch_ext = ".patch"; my $maintainers = "MAINTAINERS"; if (!GetOptions( 'd|patchdir=s' => \$patch_dir, 'v|reroll-count=i' => \$rerollcount, 'p|patchcc=s' => \$plocation, 'c|covercc=s' => \$clocation, 't|tags' => \$tags, 'tagscc' => \$tagscc, 'a|arg=s' => \@get_maintainer_args, 'get-maintainers=s' => \$get_maintainer, 'verbose' => \$verbose, 'h|help' => \$help, )) { die "$tool: invalid argument - use --help if necessary\n"; } if ($help) { print $usage; exit 0; } if (!$patch_dir) { die "$tool: Directory -d|--patchdir not specified\n"; } if (! -e $patch_dir) { die "$tool: Directory $patch_dir does not exist\n"; } # Calculate the $patch_prefix my $patch_prefix = ""; if ($rerollcount == 0) { # If the user didn't specify -v and we are here, then # - either the directory is empty # - or it contains some version of a patch # In this case we search for the first patch and # work out the version $!=0; my @coverletters = glob($patch_dir.'/*'.$patch_ext); if (!$! && scalar @coverletters) { if ($coverletters[0] =~ /\/v([0-9]+)-\Q$cover_letter\E/) { $rerollcount = $1; } } } if ($rerollcount > 0) { $patch_prefix = "v".$rerollcount."-"; } if ( ! grep $_ eq $plocation, @plocations) { die "$tool: Invalid -p|--patchcc value\n"; } if ( ! grep $_ eq $clocation, @clocations) { die "$tool: Invalid -c|--covercc value\n"; } # Get the list of patches my $has_cover_letter = 0; my $cover_letter_file; my $pattern = $patch_dir.'/'.$patch_prefix.'[0-9][0-9][0-9][0-9]*'.$patch_ext; $!=0; my @patches = glob($pattern); if ($!) { die "$tool: Directory $patch_dir contains no patches\n"; } if (!scalar @patches) { die "$tool: Directory $patch_dir contains no matching patches.\n". "Please try --reroll-count | -v \n"; } # Do the actual processing my $file; my @combined_header; my @combined_cc; foreach my $file (@patches) { if ($file =~ /\/\Q$patch_prefix$cover_letter\E/) { $has_cover_letter = 1; $cover_letter_file = $file; } else { my @header; # To: lists returned by get_maintainers.pl my @headerpatch;# To: entries in *.patch # # Also includes CC's from tags as we do not want # entries in the body such as # CC: lars.kurth@citrix.com # ... # Tested-by: lars.kurth@citrix.com my @cc; # Cc: maintainers returned by get_maintainers.pl my @ccpatch; # Cc: entries in *.patch my @extrapatch; # Cc: for AB, RB, RAB in *.patch print "Processing: ".basename($file)."\n"; # Read tags from output of get_maintainers.pl # Lists go into @header and everything else into @cc getmaintainers($file, \@header, \@cc); # Read all lines with CC & TO from the patch file (these will # likely come from the commit message). Also read tags. gettagsfrompatch($file, \@headerpatch, \@ccpatch, \@extrapatch); # With -t|--tags only add @extrapatch to @header and @combined_header # With --tagscc treat tags as CC that came from the *.patch file if ($tags && !$tagscc) { # Copy these always onto the TO related arrays push @header, @extrapatch; push @combined_header, @extrapatch; } elsif ($tagscc) { # Treat these as if they came from CC's push @ccpatch, @extrapatch; push @combined_cc, @extrapatch; } # In this section we normalize the lists. We remove entries # that are already in the patch, from @cc and @to my @header_only = normalize(\@header, \@headerpatch); my @cc_only = normalize(\@cc, \@ccpatch); # Apply the location $applylocation{$plocation}($file, \@header_only, \@cc_only); } } # Deal with the cover letter if ($has_cover_letter) { my @headerpatch; # Entries inserted at the header my @ccpatch; # Cc: entries in *.patch print "Processing: ".basename($cover_letter_file)."\n"; # Read all lines with CC & TO from the patch file such that subsequent # calls don't lead to duplication gettagsfrompatch($cover_letter_file, \@headerpatch, \@ccpatch); # In this section we normalize the lists. We remove entries # that are already in the patch, from @cc and @to my @header_only = normalize(\@combined_header, \@headerpatch); my @cc_only = normalize(\@combined_cc, \@ccpatch); # Apply the location $applylocation{$clocation}($cover_letter_file, \@header_only, \@cc_only); print "\nDon't forget to add the subject and message to ". $cover_letter_file."\n"; } print "Then perform:\n". "git send-email -to xen-devel\@lists.xenproject.org ". $patch_dir.'/'.$patch_prefix."*.patch"."\n"; exit 0; my $getmailinglists_done = 0; my @mailinglists = (); sub getmailinglists () { # Read mailing list from MAINTAINERS file and copy # a list of e-mail addresses to @mailinglists if (!$getmailinglists_done) { if (-e $maintainers) { my $fh; my $line; open($fh, "<", $maintainers) or die $!; while (my $line = <$fh>) { chomp $line; if ($line =~ /^L:[[:blank:]]+/m) { push @mailinglists, $'; } } $fh->error and die $!; close $fh or die $!; } else { print "Warning: file '$maintainers' does not exist\n"; print "Warning: Mailing lists will be treated as CC's\n"; } # Don't try again, even if the MAINTAINERS file does not exist $getmailinglists_done = 1; # Remove any duplicates @mailinglists = uniq @mailinglists; } } sub ismailinglist ($) { my ($check) = @_; # Get the mailing list information getmailinglists(); # Do the check if ( grep { $_ eq $check} @mailinglists) { return 1; } return 0; } sub getmaintainers ($$$) { my ($file, $rto, $rcc) = @_; my $fh; open($fh, "-|", $get_maintainer, @get_maintainer_args, $file) or die "Failed to open '$get_maintainer'\n"; while(my $line = <$fh>) { chomp $line; # Keep lists and CC's separately as we dont want them in # the commit message under a Cc: line if (ismailinglist($line)) { push @$rto, $TO." ".$line; push @combined_header, $TO." ".$line; } else { push @$rcc, $CC." ".$line; push @combined_cc, $CC." ".$line; } } $fh->error and die $!; close $fh or die $!; } sub gettagsfrompatch ($$$;$) { my ($file, $rto, $rcc, $rextra) = @_; my $fh; open($fh, "<", $file) or die "Failed to open '$file'\n"; while(my $line = <$fh>) { chomp $line; my $nline; if (hastag($line, $TO)) { push @$rto, $line; push @combined_header, $line; } if (hastag($line, $CC)) { push @$rcc, $line; push @combined_cc, $line; } # If there is an $rextra, then get various tags and add # email addresses to the CC list if ($rextra && $line =~ /^[-0-9a-z]+-by:[[:blank:]]+/mi) { push @$rextra, $CC." ".$'; } } $fh->error and die $!; close $fh or die $!; } sub hastag ($$) { my ($line, $tag) = @_; if ($line =~ m{^\Q$tag\E}i) { return 1; } return 0; } sub normalize ($$) { my ($ra, $rb) = @_; # This function is used to normalize lists of tags or CC / TO lists # It returns a list of the unique elements # in @$ra, excluding any which are in @$rb. # Comparisons are case-insensitive. my @aonly = (); my %seen; my $item; foreach $item (@$rb) { $seen{lc($item)} = 1; } foreach $item (@$ra) { unless ($seen{lc($item)}++) { # it's not in %seen, so add to @aonly push @aonly, $item; } } return @aonly; } sub readfile ($) { my ($file) = @_; my $fh; my $content; open($fh, "<", $file) or die "Could not open file '$file' $!"; $content = do { local $/; <$fh> }; $fh->error and die $!; close $fh or die $!; return $content; } sub writefile ($$) { my ($content, $file) = @_; my $fh; open($fh, ">", "$file.tmp") or die "Could not open file '$file.tmp' $!"; print $fh $content or die $!; close $fh or die $!; rename "$file.tmp", $file or die "Could not rename '$file' into place $!"; } sub insert ($$$$) { my ($file, $insert, $delimiter, $insafter) = @_; my $content; if ($insert eq "") { # Nothing to insert return; } # Read file $content = readfile($file) or die $!; # Split the string and generate new content if ($content =~ /^\Q$delimiter\E/mi) { if ($insafter) { writefile($`.$delimiter."\n".$insert."\n".$', $file); if ($verbose) { print "\nInserted into ".basename($file).' after "'. $delimiter."'"."\n-----\n".$insert."\n-----\n"; } } else { writefile($`.$insert."\n".$delimiter.$', $file); if ($verbose) { print "\nInserted into ".basename($file).' before "'. $delimiter."'"."\n-----\n".$insert."\n-----\n"; } } } else { print "Error: Didn't find '$delimiter' in '$file'\n"; } }