diff mbox series

[3/3] send-email: teach git send-email option to translate aliases

Message ID 20240808-jk-translate-alias-send-email-v1-3-10a03b3d6b06@gmail.com (mailing list archive)
State Superseded
Headers show
Series send-email: teach git send-email mode to translate aliases | expand

Commit Message

Jacob Keller Aug. 8, 2024, 9:10 p.m. UTC
From: Jacob Keller <jacob.keller@gmail.com>

Add a new "--translate-aliases" option to git send-email which allows
other programs to convert email aliases according to the configured
alias file. This is intended to allow b4 send the ability to use the
same aliases as git send-email.

There is one tricky part of handling the new option, since
--translate-aliases wants to consume the rest of @ARGV. Currently, the
pass_through option is set for perl's Getopt::Long::Configure, which
causes unknown options to get passed through to other option parsers.

This is required in order to handle passing format-patch options, and is
tricky to work around. --dump-aliases handles this by testing @ARGV
before calling the full option parser. We can't do this with
--translate-aliases because it wants to consume the arguments.

Instead, skip calling GetOptions a second time of --translate-aliases is
set. This has the effect that known options will instead be translated
as aliases instead of producing a warning, but this seems like the best
trade off of the available options.

Signed-off-by: Jacob Keller <jacob.keller@gmail.com>
---
 Documentation/git-send-email.txt |  7 ++++
 git-send-email.perl              | 17 +++++++-
 t/t9001-send-email.sh            | 89 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 112 insertions(+), 1 deletion(-)

Comments

Junio C Hamano Aug. 8, 2024, 9:57 p.m. UTC | #1
Jacob Keller <jacob.e.keller@intel.com> writes:

> From: Jacob Keller <jacob.keller@gmail.com>
>
> Add a new "--translate-aliases" option to git send-email which allows

If you add a thing, it by definition is new, isn't it?

> Instead, skip calling GetOptions a second time of --translate-aliases is

"of"???  "if" perhaps?

> set. This has the effect that known options will instead be translated
> as aliases instead of producing a warning, but this seems like the best
> trade off of the available options.

Hmph, I do not quite get why you need such a hack (to be honest, I
do not quite understand why dump-aliases needs a similar hack,
either, even though I do understand why identity thing needs a
special caseing).

After GetOptions() returns, usuall we process everything remaining
on the command line as files that contain messages, right?  But
before that happens (i.e. anywhere before the while () loop that
processes elements in @ARGV), you can check if your new option was
given, and if so you can map contents of @ARGV using %aliases and
exit, and you are done, no?  Bonus point if you make sure that no
other options were given, but perhaps there are some strange folks
who want to use "--to=fu" as an e-mail alias, so instead of
complaining that "--translate" does not mix with any other options
when "git send-email --translate --to=fu" was run, giving alias
translation for "--to=fu" may be a better behaviour for those users
anyway ;-).
Jacob Keller Aug. 9, 2024, 7:12 p.m. UTC | #2
On 8/8/2024 2:57 PM, Junio C Hamano wrote:
> Jacob Keller <jacob.e.keller@intel.com> writes:
> 
>> From: Jacob Keller <jacob.keller@gmail.com>
>>
>> Add a new "--translate-aliases" option to git send-email which allows
> 
> If you add a thing, it by definition is new, isn't it?
> 

Sure.

>> Instead, skip calling GetOptions a second time of --translate-aliases is
> 
> "of"???  "if" perhaps?
>

Yep, a typo.


>> set. This has the effect that known options will instead be translated
>> as aliases instead of producing a warning, but this seems like the best
>> trade off of the available options.
> 
> Hmph, I do not quite get why you need such a hack (to be honest, I
> do not quite understand why dump-aliases needs a similar hack,
> either, even though I do understand why identity thing needs a
> special caseing).
> 


The problem is that we have pass_through set in the
GetOpt::Long::Configure. This is necessary because we want to pass the
options we don't understand into git format-patch.

But --dump-aliases and --translate-aliases both do not want to work with
any other option. If we put these into the main get options, then the
options will be interpreted normally and we then have to specially check
every single option to make sure nothing else was provided.

Dump aliases handled this by checking @ARGV and immediately bailing if
there were anything remaining after the call to parsing its inner
options. This works but does not work for --translate-aliases because it
needs to treat all the remaining arguments as aliases.

> After GetOptions() returns, usuall we process everything remaining
> on the command line as files that contain messages, right?  But
> before that happens (i.e. anywhere before the while () loop that
> processes elements in @ARGV), you can check if your new option was
> given, and if so you can map contents of @ARGV using %aliases and
> exit, and you are done, no?  Bonus point if you make sure that no
> other options were given, but perhaps there are some strange folks
> who want to use "--to=fu" as an e-mail alias, so instead of
> complaining that "--translate" does not mix with any other options
> when "git send-email --translate --to=fu" was run, giving alias
> translation for "--to=fu" may be a better behaviour for those users
> anyway ;-).
> 

I don't think thats right. We want both --dump-aliases and
--translate-aliases to reject "--to" ideally.

I didn't come up with the best way to handle that yet, as currently:

 $ git send-email --translate-aliases --to unknown-alias
 --to
 unknown-alias

I did try turning off option pass_through for the first round of get
options, but that also seems to not work properly. I can investigate
that route and see if I can get it to behave properly. I think we could
drop some special casing.

Essentially, we want to avoid parsing any other options entirely,
because they aren't compatible with --dump-aliases or --translate-aliases.
Junio C Hamano Aug. 9, 2024, 7:27 p.m. UTC | #3
Jacob Keller <jacob.e.keller@intel.com> writes:

> Dump aliases handled this by checking @ARGV and immediately bailing if
> there were anything remaining after the call to parsing its inner
> options. This works but does not work for --translate-aliases because it
> needs to treat all the remaining arguments as aliases.

When GetOptions returns, shouldn't these "remaining arguments" be
visible in @ARGV and you can iterate over them yourself, perhaps?
Jacob Keller Aug. 9, 2024, 7:51 p.m. UTC | #4
On 8/9/2024 12:27 PM, Junio C Hamano wrote:
> Jacob Keller <jacob.e.keller@intel.com> writes:
> 
>> Dump aliases handled this by checking @ARGV and immediately bailing if
>> there were anything remaining after the call to parsing its inner
>> options. This works but does not work for --translate-aliases because it
>> needs to treat all the remaining arguments as aliases.
> 
> When GetOptions returns, shouldn't these "remaining arguments" be
> visible in @ARGV and you can iterate over them yourself, perhaps?

Yes, that is essentially what we do by skipping the call to the
GetOptions for the main set of send-email options :)

We still need to go farther down the program in order to call the
functions which parse the alias file and setup the alias map.

I guess we could re-arrange the code so that the alias bits are handled
before the options?

We also probably want to reject other option-like arguments which
ideally we would have GetOpt::Long handle for us... I think if we
disable pass_through initially in the identity/information options and
then re-enable it when we call the second GetOptions that might work?
That could get tricky...

We could try to implement scanning for options ourselves, but I wouldn't
want to break things like "--" to make it treat potential option-looking
fields as aliases...

Or we could completely change the overall behavior to do something else
like take the aliases on standard input instead of the arguments?
Junio C Hamano Aug. 9, 2024, 9:05 p.m. UTC | #5
Jacob Keller <jacob.e.keller@intel.com> writes:

> We could try to implement scanning for options ourselves, but I wouldn't
> want to break things like "--" to make it treat potential option-looking
> fields as aliases...

The appoach --dump-aliases takes is already broken with respect to
options that take or do not take an argument, if you really want to
scan, understand, and skip irrelevant options anyway, no?  The
separate, trimmed down %dump_aliases_options map cannot help you to
tell from the command line "git cmd --translate --foo bar" if
skipping just "--foo" gives you the alias-to-be-expanded "bar", or
"--foo" takes an argument that is "bar" and there is no alias left.

So I do not quite know how involved you want to go, but naïvely, I
would have thought something along the lines of illustration below
is sufficient.

 git-send-email.perl | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git c/git-send-email.perl w/git-send-email.perl
index 72044e5ef3..a7239698f8 100755
--- c/git-send-email.perl
+++ w/git-send-email.perl
@@ -747,6 +747,20 @@ sub is_format_patch_arg {
 	}
 }
 
+# Now, if the user who wants --translate knows that the command will not
+# send any mails and the remainder of the command line are aliases to be
+# expanded, the user wouldn't have given useless other options to trigger
+# any of the executable code before this point (like $suppress_cc{} handling)
+# that would be wasted.  So we just see if --translate is given and deal
+# with it here.
+
+if ($translate_alias) {
+	for (@ARGV) {
+		print "$_ => $aliases{$_}\n";
+	}
+	exit(0);
+}
+
 # Now that all the defaults are set, process the rest of the command line
 # arguments and collect up the files that need to be processed.
 my @rev_list_opts;
Jacob Keller Aug. 10, 2024, 12:29 a.m. UTC | #6
On 8/9/2024 2:05 PM, Junio C Hamano wrote:
> Jacob Keller <jacob.e.keller@intel.com> writes:
> 
>> We could try to implement scanning for options ourselves, but I wouldn't
>> want to break things like "--" to make it treat potential option-looking
>> fields as aliases...
> 
> The appoach --dump-aliases takes is already broken with respect to
> options that take or do not take an argument, if you really want to
> scan, understand, and skip irrelevant options anyway, no?  The
> separate, trimmed down %dump_aliases_options map cannot help you to
> tell from the command line "git cmd --translate --foo bar" if
> skipping just "--foo" gives you the alias-to-be-expanded "bar", or
> "--foo" takes an argument that is "bar" and there is no alias left.
> 

The approach --dump-aliases takes is that *any* other command line
argument besides --dump-aliases will cause git send-email to exit with
an error. They are not interpreted.

The approach I had --translate-aliases take is that all other command
line arguments are treated as aliases, regardless of whether they are
possibly options, and no other options are parsed at all.

It is intended that --dump-aliases, --translate-aliases, and the regular
mode of operation are mutually exclusive.

I am leaning towards possibly making --translate-aliases take the
aliases to translate on stdin instead of using arguments at all. This
would make it function more like --dump-aliases w.r.t. command line
arguments. This is probably a lot simpler. I think I will implement it
this way for v2.

Alternatively, we could extract the logic for handling the aliases into
a separate script with its own git command, but I think it currently
relies heavily on some perl code that would be hard to translate to C.
diff mbox series

Patch

diff --git a/Documentation/git-send-email.txt b/Documentation/git-send-email.txt
index c5d664f4519b..6964c9914a9c 100644
--- a/Documentation/git-send-email.txt
+++ b/Documentation/git-send-email.txt
@@ -12,6 +12,7 @@  SYNOPSIS
 'git send-email' [<options>] (<file>|<directory>)...
 'git send-email' [<options>] <format-patch-options>
 'git send-email' --dump-aliases
+'git send-email' --translate-aliases (<alias>)...
 
 
 DESCRIPTION
@@ -475,6 +476,12 @@  Information
 	that this only includes the alias name and not its expanded email addresses.
 	See 'sendemail.aliasesFile' for more information about aliases.
 
+--translate-aliases::
+	Instead of the normal operation, interpret all command line
+	arguments as shorthand alias names using the configured alias
+	file(s). Output each translated email address, one per line, in the
+	order the aliases appear. See 'sendemail.aliasFile' for more
+	information about aliases.
 
 CONFIGURATION
 -------------
diff --git a/git-send-email.perl b/git-send-email.perl
index 72044e5ef3a8..2ae6cc0d7a36 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -31,6 +31,7 @@  sub usage {
 git send-email [<options>] <file|directory>
 git send-email [<options>] <format-patch options>
 git send-email --dump-aliases
+git send-email --translate-aliases <alias>
 
   Composing:
     --from                  <str>  * Email From:
@@ -99,6 +100,11 @@  sub usage {
 
   Information:
     --dump-aliases                 * Dump configured aliases and exit.
+    --translate-aliases            * Interpret all other command line arguments
+                                     as email aliases. Translate them
+                                     according to the configured alias file,
+                                     outputing each address one per line, then
+                                     exit.
 
 EOT
 	exit(1);
@@ -212,6 +218,7 @@  sub format_2822_time {
 my $compose_filename;
 my $force = 0;
 my $dump_aliases = 0;
+my $translate_aliases = 0;
 
 # Variables to prevent short format-patch options from being captured
 # as abbreviated send-email options
@@ -476,11 +483,14 @@  sub config_regexp {
 my %dump_aliases_options = (
 	"h" => \$help,
 	"dump-aliases" => \$dump_aliases,
+	"translate-aliases" => \$translate_aliases,
 );
 $rc = GetOptions(%dump_aliases_options);
 usage() unless $rc;
 die __("--dump-aliases incompatible with other options\n")
     if !$help and $dump_aliases and @ARGV;
+die __("--dump-aliases and --translate-aliases are mutually exclusive\n")
+    if !$help and $dump_aliases and $translate_aliases;
 my %options = (
 		    "sender|from=s" => \$sender,
 		    "in-reply-to=s" => \$initial_in_reply_to,
@@ -534,7 +544,7 @@  sub config_regexp {
 		    "git-completion-helper" => \$git_completion_helper,
 		    "v=s" => \$reroll_count,
 );
-$rc = GetOptions(%options);
+($rc = GetOptions(%options)) unless $translate_aliases;
 
 # Munge any "either config or getopt, not both" variables
 my @initial_to = @getopt_to ? @getopt_to : ($no_to ? () : @config_to);
@@ -724,6 +734,11 @@  sub parse_sendmail_aliases {
     exit(0);
 }
 
+if ($translate_aliases) {
+	print "$_\n" for (process_address_list(@ARGV));
+	exit(0);
+}
+
 # is_format_patch_arg($f) returns 0 if $f names a patch, or 1 if
 # $f is a revision list specification to be passed to format-patch.
 sub is_format_patch_arg {
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index c96d6955b9f2..78c451918145 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -2120,6 +2120,95 @@  test_expect_success '--dump-aliases must be used alone' '
 	test_must_fail git send-email --dump-aliases --to=janice@example.com -1 refs/heads/accounting
 '
 
+test_translate_aliases () {
+	msg="$1" && shift &&
+	filetype="$1" && shift &&
+	aliases="$1" && shift &&
+	printf '%s\n' "$@" >expect &&
+	cat >.tmp-email-aliases &&
+
+	test_expect_success $PREREQ "$msg" '
+		clean_fake_sendmail && rm -fr outdir &&
+		git config --replace-all sendemail.aliasesfile \
+			"$(pwd)/.tmp-email-aliases" &&
+		git config sendemail.aliasfiletype "$filetype" &&
+		git send-email --translate-aliases $aliases 2>errors >actual &&
+		test_cmp expect actual
+	'
+}
+
+test_translate_aliases '--translate-aliases sendmail format' \
+	'sendmail' \
+	'alice bcgrp' \
+	'Alice W Land <awol@example.com>' \
+	'Robert Bobbyton <bob@example.com>' \
+	'chloe@example.com' \
+	'Other <o@example.com>' <<-\EOF
+	alice: Alice W Land <awol@example.com>
+	bob: Robert Bobbyton <bob@example.com>
+	chloe: chloe@example.com
+	abgroup: alice, bob
+	bcgrp: bob, chloe, Other <o@example.com>
+	EOF
+
+test_translate_aliases '--translate-aliases mutt format' \
+	'mutt' \
+	'donald bob' \
+	'Donald C Carlton <donc@example.com>' \
+	'Robert Bobbyton <bob@example.com>' <<-\EOF
+	alias alice Alice W Land <awol@example.com>
+	alias donald Donald C Carlton <donc@example.com>
+	alias bob Robert Bobbyton <bob@example.com>
+	alias chloe chloe@example.com
+	EOF
+
+test_translate_aliases '--translate-aliases mailrc format' \
+	'mailrc' \
+	'chloe eve alice' \
+	'chloe@example.com' \
+	'Eve <eve@example.com>' \
+	'Alice W Land <awol@example.com>' <<-\EOF
+	alias alice   "Alice W Land <awol@example.com>"
+	alias eve     "Eve <eve@example.com>"
+	alias bob     "Robert Bobbyton <bob@example.com>"
+	alias chloe   chloe@example.com
+	EOF
+
+test_translate_aliases '--translate-aliases pine format' \
+	'pine' \
+	'eve bob bcgrp' \
+	'eve@example.com' \
+	'bob@example.com' \
+	'bob@example.com' \
+	'chloe@example.com' \
+	'Other <o@example.com>' <<-\EOF
+	alice	Alice W Land	awol@example.com		Friend
+	eve	Eve	eve@example.com
+	bob	Robert Bobbyton	bob@example.com
+	chloe		chloe@example.com
+	bcgrp		(bob, chloe, Other <o@example.com>)
+	EOF
+
+test_translate_aliases '--translate-aliases gnus format' \
+	'gnus' \
+	'alice chloe eve' \
+	'awol@example.com' \
+	'chloe@example.com' \
+	'eve@example.com' <<-\EOF
+	(define-mail-alias "alice" "awol@example.com")
+	(define-mail-alias "eve" "eve@example.com")
+	(define-mail-alias "bob" "bob@example.com")
+	(define-mail-alias "chloe" "chloe@example.com")
+	EOF
+
+test_expect_success '--translate-aliases passes unknown aliases through' '
+	cat >expect <<-\EOF &&
+	Other <o@example.com>
+	EOF
+	git send-email --translate-aliases "Other <o@example.com>" >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success $PREREQ 'aliases and sendemail.identity' '
 	test_must_fail git \
 		-c sendemail.identity=cloud \