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