send-email: fix regression in sendemail.identity parsing

Fix a regression in my recent 3494dfd3ee ("send-email: do defaults -> config -> getopt in that order", 2019-05-09). I missed that the $identity variable needs to be extracted from the command-line before we do the config reading, as it determines which config variable we should read first. See [1] for the report. The sendemail.identity feature was added back in 34cc60ce2b ("send-email: Add support for SSL and SMTP-AUTH", 2007-09-03), there were no tests to assert that it worked properly. So let's fix both the regression, and add some tests to assert that this is being parsed properly. While I'm at it I'm adding a --no-identity option to go with --[to|cc|bcc] variable, since the semantics are similar. It's like to/cc/bcc except that unlike those we don't support multiple identities, but we could now easily add it support for it if anyone cares. In just fixing the --identity command-line parsing bug I discovered that a narrow fix to that wouldn't do. In read_config() we had a state machine that would only set config values if they weren't set already, and thus by proxy we wouldn't e.g. set "to" based on sendemail.to if we'd seen sendemail.gmail.to before, with --identity=gmail. I'd modified some of the relevant code in 3494dfd3ee, but just reverting to that wouldn't do, since it would bring back the regression fixed in that commit. Refactor read_config() do what we actually mean here. We don't want to set a given sendemail.VAR if a sendemail.$identity.VAR previously set it. The old code was conflating this desire with the hardcoded defaults for these variables, and as discussed in 3494dfd3ee that was never going to work. Instead pass along the state of whether an identity config set something before, as distinguished from the state of the default just being false, or the default being a non-bool or true (e.g. --transferencoding). I'm still not happy with the test coverage here, e.g. there's nothing testing sendemail.smtpEncryption, but I only have so much time to fix this code. 1. https://public-inbox.org/git/5cddeb61.1c69fb81.47ed4.e648@mx.google.com/ Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Ævar Arnfjörð Bjarmason committed May 17, 2019 at 21:55 UTC 3ff15040e22676db0b369efbd8b908b8fcc9b38d
3 files changed +108 -19
Documentation/git-send-email.txt
+4
@@ -277,6 +277,10 @@ Automating
277 Clears any list of "To:", "Cc:", "Bcc:" addresses previously
278 set via config.
279
280 +--no-identity::
281 + Clears the previously read value of `sendemail.identity` set
282 + via config, if any.
283 +
284 --to-cmd=<command>::
285 Specify a command to execute once per patch file which
286 should generate patch file specific "To:" entries.
git-send-email.perl
+40 -19
@@ -174,7 +174,7 @@ my (@to,@cc,@xh,$envelope_sender,
174 $author,$sender,$smtp_authpass,$annotate,$compose,$time);
175 # Things we either get from config, *or* are overridden on the
176 # command-line.
177 -my ($no_cc, $no_to, $no_bcc);
177 +my ($no_cc, $no_to, $no_bcc, $no_identity);
178 my (@config_to, @getopt_to);
179 my (@config_cc, @getopt_cc);
180 my (@config_bcc, @getopt_bcc);
@@ -317,44 +317,53 @@ $SIG{INT} = \&signal_handler;
317
318 # Read our sendemail.* config
319 sub read_config {
320 - my ($prefix) = @_;
320 + my ($configured, $prefix) = @_;
321
322 foreach my $setting (keys %config_bool_settings) {
323 my $target = $config_bool_settings{$setting};
324 my $v = Git::config_bool(@repo, "$prefix.$setting");
325 - $$target = $v if defined $v;
325 + next unless defined $v;
326 + next if $configured->{$setting}++;
327 + $$target = $v;
328 }
329
330 foreach my $setting (keys %config_path_settings) {
331 my $target = $config_path_settings{$setting};
332 if (ref($target) eq "ARRAY") {
331 - unless (@$target) {
332 - my @values = Git::config_path(@repo, "$prefix.$setting");
333 - @$target = @values if (@values && defined $values[0]);
334 - }
333 + my @values = Git::config_path(@repo, "$prefix.$setting");
334 + next unless @values;
335 + next if $configured->{$setting}++;
336 + @$target = @values;
337 }
338 else {
339 my $v = Git::config_path(@repo, "$prefix.$setting");
338 - $$target = $v if defined $v;
340 + next unless defined $v;
341 + next if $configured->{$setting}++;
342 + $$target = $v;
343 }
344 }
345
346 foreach my $setting (keys %config_settings) {
347 my $target = $config_settings{$setting};
348 if (ref($target) eq "ARRAY") {
345 - unless (@$target) {
346 - my @values = Git::config(@repo, "$prefix.$setting");
347 - @$target = @values if (@values && defined $values[0]);
348 - }
349 + my @values = Git::config(@repo, "$prefix.$setting");
350 + next unless @values;
351 + next if $configured->{$setting}++;
352 + @$target = @values;
353 }
354 else {
355 my $v = Git::config(@repo, "$prefix.$setting");
352 - $$target = $v if defined $v;
356 + next unless defined $v;
357 + next if $configured->{$setting}++;
358 + $$target = $v;
359 }
360 }
361
362 if (!defined $smtp_encryption) {
357 - my $enc = Git::config(@repo, "$prefix.smtpencryption");
363 + my $setting = "$prefix.smtpencryption";
364 + my $enc = Git::config(@repo, $setting);
365 + return unless defined $enc;
366 + return if $configured->{$setting}++;
367 if (defined $enc) {
368 $smtp_encryption = $enc;
369 } elsif (Git::config_bool(@repo, "$prefix.smtpssl")) {
@@ -363,16 +372,29 @@ sub read_config {
372 }
373 }
374
375 +# sendemail.identity yields to --identity. We must parse this
376 +# special-case first before the rest of the config is read.
377 $identity = Git::config(@repo, "sendemail.identity");
367 -read_config("sendemail.$identity") if defined $identity;
368 -read_config("sendemail");
378 +my $rc = GetOptions(
379 + "identity=s" => \$identity,
380 + "no-identity" => \$no_identity,
381 +);
382 +usage() unless $rc;
383 +undef $identity if $no_identity;
384 +
385 +# Now we know enough to read the config
386 +{
387 + my %configured;
388 + read_config(\%configured, "sendemail.$identity") if defined $identity;
389 + read_config(\%configured, "sendemail");
390 +}
391
392 # Begin by accumulating all the variables (defined above), that we will end up
393 # needing, first, from the command line:
394
395 my $help;
374 -my $rc = GetOptions("h" => \$help,
375 - "dump-aliases" => \$dump_aliases);
396 +$rc = GetOptions("h" => \$help,
397 + "dump-aliases" => \$dump_aliases);
398 usage() unless $rc;
399 die __("--dump-aliases incompatible with other options\n")
400 if !$help and $dump_aliases and @ARGV;
@@ -401,7 +423,6 @@ $rc = GetOptions(
423 "smtp-debug:i" => \$debug_net_smtp,
424 "smtp-domain:s" => \$smtp_domain,
425 "smtp-auth=s" => \$smtp_auth,
404 - "identity=s" => \$identity,
426 "annotate!" => \$annotate,
427 "no-annotate" => sub {$annotate = 0},
428 "compose" => \$compose,
t/t9001-send-email.sh
+64
@@ -1200,6 +1200,61 @@ test_expect_success $PREREQ 'sendemail.to works' '
1200 grep "To: Somebody <somebody@ex.com>" stdout
1201 '
1202
1203 +test_expect_success $PREREQ 'setup sendemail.identity' '
1204 + git config --replace-all sendemail.to "default@example.com" &&
1205 + git config --replace-all sendemail.isp.to "isp@example.com" &&
1206 + git config --replace-all sendemail.cloud.to "cloud@example.com"
1207 +'
1208 +
1209 +test_expect_success $PREREQ 'sendemail.identity: reads the correct identity config' '
1210 + git -c sendemail.identity=cloud send-email \
1211 + --dry-run \
1212 + --from="nobody@example.com" \
1213 + $patches >stdout &&
1214 + grep "To: cloud@example.com" stdout
1215 +'
1216 +
1217 +test_expect_success $PREREQ 'sendemail.identity: identity overrides sendemail.identity' '
1218 + git -c sendemail.identity=cloud send-email \
1219 + --identity=isp \
1220 + --dry-run \
1221 + --from="nobody@example.com" \
1222 + $patches >stdout &&
1223 + grep "To: isp@example.com" stdout
1224 +'
1225 +
1226 +test_expect_success $PREREQ 'sendemail.identity: --no-identity clears previous identity' '
1227 + git -c sendemail.identity=cloud send-email \
1228 + --no-identity \
1229 + --dry-run \
1230 + --from="nobody@example.com" \
1231 + $patches >stdout &&
1232 + grep "To: default@example.com" stdout
1233 +'
1234 +
1235 +test_expect_success $PREREQ 'sendemail.identity: bool identity variable existance overrides' '
1236 + git -c sendemail.identity=cloud \
1237 + -c sendemail.xmailer=true \
1238 + -c sendemail.cloud.xmailer=false \
1239 + send-email \
1240 + --dry-run \
1241 + --from="nobody@example.com" \
1242 + $patches >stdout &&
1243 + grep "To: cloud@example.com" stdout &&
1244 + ! grep "X-Mailer" stdout
1245 +'
1246 +
1247 +test_expect_success $PREREQ 'sendemail.identity: bool variable fallback' '
1248 + git -c sendemail.identity=cloud \
1249 + -c sendemail.xmailer=false \
1250 + send-email \
1251 + --dry-run \
1252 + --from="nobody@example.com" \
1253 + $patches >stdout &&
1254 + grep "To: cloud@example.com" stdout &&
1255 + ! grep "X-Mailer" stdout
1256 +'
1257 +
1258 test_expect_success $PREREQ '--no-to overrides sendemail.to' '
1259 git send-email \
1260 --dry-run \
@@ -1757,6 +1812,15 @@ test_expect_success '--dump-aliases must be used alone' '
1812 test_must_fail git send-email --dump-aliases --to=janice@example.com -1 refs/heads/accounting
1813 '
1814
1815 +test_expect_success $PREREQ 'aliases and sendemail.identity' '
1816 + test_must_fail git \
1817 + -c sendemail.identity=cloud \
1818 + -c sendemail.aliasesfile=default-aliases \
1819 + -c sendemail.cloud.aliasesfile=cloud-aliases \
1820 + send-email -1 2>stderr &&
1821 + test_i18ngrep "cloud-aliases" stderr
1822 +'
1823 +
1824 test_sendmail_aliases () {
1825 msg="$1" && shift &&
1826 expect="$@" &&