send-email: add mailmap support via sendemail.mailmap and --mailmap

In some cases, a user may be generating a patch for an old commit which now has an out-of-date author or other identity. For example, consider a team member who contributes to an internal fork of an upstream project, but leaves before this change is submitted upstream. In this case, the team members company address may no longer be valid, and will thus bounce when sending email. This can be manually avoided by editing the generated patch files, or by carefully using --suppress-<cc|to> options. This requires a lot of manual intervention and is easy to forget. Git has support for mapping old email addresses and names to a canonical name and address via the .mailmap file (and its associated mailmap.file, mailmap.blob, and log.mailmap options). Teach git send-email to enable mailmap support for all addresses. This ensures that addresses point to the canonical real name and email address. Add the sendemail.mailmap configuration option and its associated --mailmap (and --use-mailmap for compatibility with git log) options. For now, the default behavior is to disable the mailmap in order to avoid any surprises or breaking any existing setups. These options support per-identity configuration via the sendemail.identity configuration blocks. This enables identity-specific configuration in cases where users may not want to enable support. In addition, support send-email specific mailmap data via sendemail.mailmap.file, sendemail.mailmap.blob and their identity-specific variants. The intention of these options is to enable mapping addresses which are no longer valid to a current project or team maintainer. Such mappings may change the actual person being referred to, and may not make sense in a traditional mailmap file which is intended for updating canonical name and address for the same individual. Signed-off-by: Jacob Keller <jacob.keller@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jacob Keller committed Aug 27, 2024 at 14:27 UTC 241499aba00741b966019112f0ffed75cb5386a4
2 files changed +142
git-send-email.perl
+20
@@ -46,6 +46,8 @@ git send-email --dump-aliases
46 --compose-encoding <str> * Encoding to assume for introduction.
47 --8bit-encoding <str> * Encoding to assume 8bit mails if undeclared
48 --transfer-encoding <str> * Transfer encoding to use (quoted-printable, 8bit, base64)
49 + --[no-]mailmap * Use mailmap file to map all email addresses to canonical
50 + real names and email addresses.
51
52 Sending:
53 --envelope-sender <str> * Email envelope sender.
@@ -272,12 +274,14 @@ my (@suppress_cc);
274 my ($auto_8bit_encoding);
275 my ($compose_encoding);
276 my ($sendmail_cmd);
277 +my ($mailmap_file, $mailmap_blob);
278 # Variables with corresponding config settings & hardcoded defaults
279 my ($debug_net_smtp) = 0; # Net::SMTP, see send_message()
280 my $thread = 1;
281 my $chain_reply_to = 0;
282 my $use_xmailer = 1;
283 my $validate = 1;
284 +my $mailmap = 0;
285 my $target_xfer_encoding = 'auto';
286 my $forbid_sendmail_variables = 1;
287
@@ -294,6 +298,7 @@ my %config_bool_settings = (
298 "annotate" => \$annotate,
299 "xmailer" => \$use_xmailer,
300 "forbidsendmailvariables" => \$forbid_sendmail_variables,
301 + "mailmap" => \$mailmap,
302 );
303
304 my %config_settings = (
@@ -327,6 +332,8 @@ my %config_settings = (
332 my %config_path_settings = (
333 "aliasesfile" => \@alias_files,
334 "smtpsslcertpath" => \$smtp_ssl_cert_path,
335 + "mailmap.file" => \$mailmap_file,
336 + "mailmap.blob" => \$mailmap_blob,
337 );
338
339 # Handle Uncouth Termination
@@ -524,6 +531,8 @@ my %options = (
531 "thread!" => \$thread,
532 "validate!" => \$validate,
533 "transfer-encoding=s" => \$target_xfer_encoding,
534 + "mailmap!" => \$mailmap,
535 + "use-mailmap!" => \$mailmap,
536 "format-patch!" => \$format_patch,
537 "8bit-encoding=s" => \$auto_8bit_encoding,
538 "compose-encoding=s" => \$compose_encoding,
@@ -1085,6 +1094,16 @@ if ($compose && $compose > 0) {
1094 our ($message_id, %mail, $subject, $in_reply_to, $references, $message,
1095 $needs_confirm, $message_num, $ask_default);
1096
1097 +sub mailmap_address_list {
1098 + return @_ unless @_ and $mailmap;
1099 + my @options = ();
1100 + push(@options, "--mailmap-file=$mailmap_file") if $mailmap_file;
1101 + push(@options, "--mailmap-blob=$mailmap_blob") if $mailmap_blob;
1102 + my @addr_list = Git::command('check-mailmap', @options, @_);
1103 + s/^<(.*)>$/$1/ for @addr_list;
1104 + return @addr_list;
1105 +}
1106 +
1107 sub extract_valid_address {
1108 my $address = shift;
1109 my $local_part_regexp = qr/[^<>"\s@]+/;
@@ -1294,6 +1313,7 @@ sub process_address_list {
1313 @addr_list = expand_aliases(@addr_list);
1314 @addr_list = sanitize_address_list(@addr_list);
1315 @addr_list = validate_address_list(@addr_list);
1316 + @addr_list = mailmap_address_list(@addr_list);
1317 return @addr_list;
1318 }
1319
t/t9001-send-email.sh
+122
@@ -2379,6 +2379,128 @@ test_expect_success $PREREQ 'leading and trailing whitespaces are removed' '
2379 test_cmp expected-list actual-list
2380 '
2381
2382 +test_expect_success $PREREQ 'mailmap support with --to' '
2383 + clean_fake_sendmail &&
2384 + test_config mailmap.file "mailmap.test" &&
2385 + cat >mailmap.test <<-EOF &&
2386 + Some Body <someone@example.com> <someone@example.org>
2387 + EOF
2388 + git format-patch --stdout -1 >a.patch &&
2389 + git send-email \
2390 + --from="Example <nobody@example.com>" \
2391 + --smtp-server="$(pwd)/fake.sendmail" \
2392 + --to=someone@example.org \
2393 + --mailmap \
2394 + a.patch \
2395 + 2>errors >out &&
2396 + grep "^!someone@example\.com!$" commandline1
2397 +'
2398 +
2399 +test_expect_success $PREREQ 'sendemail.mailmap configuration' '
2400 + clean_fake_sendmail &&
2401 + test_config mailmap.file "mailmap.test" &&
2402 + test_config sendemail.mailmap "true" &&
2403 + cat >mailmap.test <<-EOF &&
2404 + Some Body <someone@example.com> <someone@example.org>
2405 + EOF
2406 + git format-patch --stdout -1 >a.patch &&
2407 + git send-email \
2408 + --from="Example <nobody@example.com>" \
2409 + --smtp-server="$(pwd)/fake.sendmail" \
2410 + --to=someone@example.org \
2411 + a.patch \
2412 + 2>errors >out &&
2413 + grep "^!someone@example\.com!$" commandline1
2414 +'
2415 +
2416 +test_expect_success $PREREQ 'sendemail.mailmap.file configuration' '
2417 + clean_fake_sendmail &&
2418 + test_config sendemail.mailmap.file "mailmap.test" &&
2419 + test_config sendemail.mailmap "true" &&
2420 + cat >mailmap.test <<-EOF &&
2421 + Some Body <someone@example.com> <someone@example.org>
2422 + EOF
2423 + git format-patch --stdout -1 >a.patch &&
2424 + git send-email \
2425 + --from="Example <nobody@example.com>" \
2426 + --smtp-server="$(pwd)/fake.sendmail" \
2427 + --to=someone@example.org \
2428 + a.patch \
2429 + 2>errors >out &&
2430 + grep "^!someone@example\.com!$" commandline1
2431 +'
2432 +
2433 +test_expect_success $PREREQ 'sendemail.mailmap identity overrides configuration' '
2434 + clean_fake_sendmail &&
2435 + test_config sendemail.cloud.mailmap.file "mailmap.test" &&
2436 + test_config sendemail.mailmap "false" &&
2437 + test_config sendemail.cloud.mailmap "true" &&
2438 + cat >mailmap.test <<-EOF &&
2439 + Some Body <someone@example.com> <someone@example.org>
2440 + EOF
2441 + git format-patch --stdout -1 >a.patch &&
2442 + git send-email \
2443 + --from="Example <nobody@example.com>" \
2444 + --smtp-server="$(pwd)/fake.sendmail" \
2445 + --identity=cloud \
2446 + --to=someone@example.org \
2447 + a.patch \
2448 + 2>errors >out &&
2449 + grep "^!someone@example\.com!$" commandline1
2450 +'
2451 +
2452 +test_expect_success $PREREQ '--no-mailmap overrides configuration' '
2453 + clean_fake_sendmail &&
2454 + test_config sendemail.cloud.mailmap.file "mailmap.test" &&
2455 + test_config sendemail.mailmap "false" &&
2456 + test_config sendemail.cloud.mailmap "true" &&
2457 + cat >mailmap.test <<-EOF &&
2458 + Some Body <someone@example.com> <someone@example.org>
2459 + EOF
2460 + git format-patch --stdout -1 >a.patch &&
2461 + git send-email \
2462 + --from="Example <nobody@example.com>" \
2463 + --smtp-server="$(pwd)/fake.sendmail" \
2464 + --identity=cloud \
2465 + --to=someone@example.org \
2466 + --no-mailmap \
2467 + a.patch \
2468 + 2>errors >out &&
2469 + grep "^!someone@example\.org!$" commandline1
2470 +'
2471 +
2472 +test_expect_success $PREREQ 'mailmap support in To header' '
2473 + clean_fake_sendmail &&
2474 + test_config mailmap.file "mailmap.test" &&
2475 + cat >mailmap.test <<-EOF &&
2476 + <someone@example.com> <someone@example.org>
2477 + EOF
2478 + git format-patch --stdout -1 --to=someone@example.org >a.patch &&
2479 + git send-email \
2480 + --from="Example <nobody@example.com>" \
2481 + --smtp-server="$(pwd)/fake.sendmail" \
2482 + --mailmap \
2483 + a.patch \
2484 + 2>errors >out &&
2485 + grep "^!someone@example\.com!$" commandline1
2486 +'
2487 +
2488 +test_expect_success $PREREQ 'mailmap support in Cc header' '
2489 + clean_fake_sendmail &&
2490 + test_config mailmap.file "mailmap.test" &&
2491 + cat >mailmap.test <<-EOF &&
2492 + <someone@example.com> <someone@example.org>
2493 + EOF
2494 + git format-patch --stdout -1 --cc=someone@example.org >a.patch &&
2495 + git send-email \
2496 + --from="Example <nobody@example.com>" \
2497 + --smtp-server="$(pwd)/fake.sendmail" \
2498 + --mailmap \
2499 + a.patch \
2500 + 2>errors >out &&
2501 + grep "^!someone@example\.com!$" commandline1
2502 +'
2503 +
2504 test_expect_success $PREREQ 'test using command name with --sendmail-cmd' '
2505 clean_fake_sendmail &&
2506 PATH="$PWD:$PATH" \