diff mbox series

[bug#63802,2/3] client: Add git-send-email-headers subcommand.

Message ID 20230608171453.14788-2-arunisaac@systemreboot.net
State New
Headers show
Series [bug#63802,1/3] client: Separate serialize-email-address into a function. | expand

Commit Message

Arun Isaac June 8, 2023, 5:14 p.m. UTC
* mumi/client.scm: Import (rnrs exceptions).
(git-send-email-headers): New public function.
(split-cc): New function.
* scripts/mumi.in: Add git-send-email-headers subcommand.
* tests/client.scm (split-cc): New variable.
("split Cc field"): New test.
---
 mumi/client.scm  | 52 +++++++++++++++++++++++++++++++++++++++++++++++-
 scripts/mumi.in  |  2 ++
 tests/client.scm |  8 ++++++++
 3 files changed, 61 insertions(+), 1 deletion(-)

Comments

Maxim Cournoyer July 16, 2023, 3:39 a.m. UTC | #1
Hello,

Arun Isaac <arunisaac@systemreboot.net> writes:

> * mumi/client.scm: Import (rnrs exceptions).
> (git-send-email-headers): New public function.
> (split-cc): New function.

nitpick: I guess these should be called 'procedures', which seems to be
the preferred term in Scheme (ironically enough for a functional
language?  eh!).  CC'ing Ludovic in case they have some thoughts on the
matter, as I think they were the one originally teaching me that.

If that was just me, intuitively I'd use 'procedure' for something
involving side effects while 'function' as something taking an input and
always returning the same output, without side effects (pure function),
but that doesn't seem to be the prevalent style in the Scheme community.

> * scripts/mumi.in: Add git-send-email-headers subcommand.
> * tests/client.scm (split-cc): New variable.
> ("split Cc field"): New test.
> ---
>  mumi/client.scm  | 52 +++++++++++++++++++++++++++++++++++++++++++++++-
>  scripts/mumi.in  |  2 ++
>  tests/client.scm |  8 ++++++++
>  3 files changed, 61 insertions(+), 1 deletion(-)
>
> diff --git a/mumi/client.scm b/mumi/client.scm
> index c30429d..b89e608 100644
> --- a/mumi/client.scm
> +++ b/mumi/client.scm
> @@ -17,6 +17,7 @@
>  ;;; along with mumi.  If not, see <http://www.gnu.org/licenses/>.
>  
>  (define-module (mumi client)
> +  #:use-module (rnrs exceptions)
>    #:use-module (rnrs io ports)
>    #:use-module (srfi srfi-1)
>    #:use-module (srfi srfi-19)
> @@ -38,7 +39,9 @@
>              print-current-issue
>              set-current-issue!
>              clear-current-issue!
> -            send-email))
> +            send-email
> +            git-send-email-headers
> +            compose))

I think you've exported 'compose' erroneously here.

>  (define (git-top-level)
>    "Return the top-level directory of the current git repository."
> @@ -229,6 +232,13 @@ arguments."
>                       name)
>                   " <" address ">"))
>  
> +(define (split-cc cc)
> +  "Split CC into a list of email addresses."
> +  (map (lambda (address)
> +         (serialize-email-address (assq-ref address 'name)
> +                                  (assq-ref address 'address)))
> +       (assq-ref (parse-email-headers (string-append "Cc: " cc "\n"))
> +                 'cc)))
>  
>  (define* (git-send-email to patches #:optional (options '()))
>    "Send PATCHES using git send-email to the TO address with
> @@ -311,3 +321,43 @@ ISSUE-NUMBER."
>                           "@"
>                           (client-config 'debbugs-host))
>            other-patches)))))
> +
> +(define (git-send-email-headers patch)
> +  "Print send-email headers for PATCH."
> +  (let* (;; Compute headers if configured in git config.
> +         (header-command
> +          (guard (ex (#t #f))
> +            (call-with-input-pipe* (list "git" "config" "sendemail.headerCmd")
> +              get-line)))

Ain't this guard equivalent to '(false-if-exception
(call-with-input-pipe* ...))' ? I find the later more readable if yes,
but: does call-with-input-pipe* raise an exception when git is available
but 'sendemail.headerCmd' not set, thus exiting with status 1?  I wasn't
able to find its documentation in the Guile Reference manual.  Otherwise
you'd get header-command set to the empty string, which seems like it'd
be a problem...

> +         (headers
> +          (if header-command
> +              (call-with-input-pipe (string-append header-command " " patch)

                  ^ ... here.  Also, why the mixed use of
                  'call-with-input-pipe*' and 'call-with-input-pipe'?  I'd
                  stick with the former.
                  
> +                get-string-all)
> +              ""))
> +         (external-x-debbugs-cc
> +          (cond
> +           ((assq-ref (parse-email-headers (string-append headers "\n"))
> +                       'x-debbugs-cc)
> +            => split-cc)
> +           (else '())))
> +         ;; Fetch Cc addresses for current issue.
> +         (x-debbugs-cc
> +          (cond
> +           ((assq-ref (reply-email-headers (current-issue-number))
> +                       'cc)
> +            => split-cc)
> +           (else '()))))
> +    ;; Print X-Debbugs-Cc header.
> +    (display "X-Debbugs-Cc: ")
> +    (display (string-join (delete-duplicates
> +                           (append x-debbugs-cc external-x-debbugs-cc))
> +                          ", "))
> +    (newline)
> +    ;; Print headers other than X-Debbugs-Cc.
> +    ;; TODO: RFC5322 headers are not restricted to a single
> +    ;; line. "Folded" multi-line headers are allowed. Support them.
> +    (for-each (lambda (line)
> +                (unless (string-prefix-ci? "X-Debbugs-Cc:" line)
> +                  (display line)
> +                  (newline)))
> +              (string-split headers #\newline))))
> diff --git a/scripts/mumi.in b/scripts/mumi.in
> index 2295328..8fb7cd4 100644
> --- a/scripts/mumi.in
> +++ b/scripts/mumi.in
> @@ -163,6 +163,8 @@
>     (client:clear-current-issue!))
>    (("send-email" . patches)
>     (client:send-email patches))
> +  (("git-send-email-headers" patch)
> +   (client:git-send-email-headers patch))
>    (("mailer" . rest)
>     (let* ((opts (parse-options rest))
>            (sender (assoc-ref opts 'sender))
> diff --git a/tests/client.scm b/tests/client.scm
> index 2b2c1be..ced573b 100644
> --- a/tests/client.scm
> +++ b/tests/client.scm
> @@ -68,6 +68,9 @@ called with."
>  (define serialize-email-address
>    (@@ (mumi client) serialize-email-address))
>  
> +(define split-cc
> +  (@@ (mumi client) split-cc))
> +
>  (test-begin "client")
>  
>  (test-equal "serialize email address"
> @@ -78,6 +81,11 @@ called with."
>    "\"Bar, Foo\" <foobar@example.com>"
>    (serialize-email-address "Bar, Foo" "foobar@example.com"))
>  
> +(test-equal "split Cc field"
> +  (list "Foo <foo@example.com>"
> +        "\"Bar, Foo\" <foobar@example.com>")
> +  (split-cc "Foo <foo@example.com>, \"Bar, Foo\" <foobar@example.com>"))
> +
>  (test-equal "send patches to new issue"
>    '(("git" "send-email" "--to=foo@patches.com" "foo.patch")
>      ("git" "send-email" "--to=12345@example.com" "bar.patch" "foobar.patch"))

The rest LGTM.
Arun Isaac July 17, 2023, 10:14 p.m. UTC | #2
Hi Maxim,

> nitpick: I guess these should be called 'procedures', which seems to be
> the preferred term in Scheme (ironically enough for a functional
> language?  eh!).  CC'ing Ludovic in case they have some thoughts on the
> matter, as I think they were the one originally teaching me that.
>
> If that was just me, intuitively I'd use 'procedure' for something
> involving side effects while 'function' as something taking an input and
> always returning the same output, without side effects (pure function),
> but that doesn't seem to be the prevalent style in the Scheme
> community.

Exactly, that was my intuition too. Maybe, we should break with
tradition! :-) In any case, I have changed all instances of "function"
to "procedure".

>> +            git-send-email-headers
>> +            compose))
>
> I think you've exported 'compose' erroneously here.

Good catch! compose is part of a new "mumi compose" feature I am working
on. I had accidentally committed it. I have removed it from this commit.

Now that you mention it, maybe I should call it compose-email so as to
not conflict with compose from guile core.

>> +(define (git-send-email-headers patch)
>> +  "Print send-email headers for PATCH."
>> +  (let* (;; Compute headers if configured in git config.
>> +         (header-command
>> +          (guard (ex (#t #f))
>> +            (call-with-input-pipe* (list "git" "config" "sendemail.headerCmd")
>> +              get-line)))
>
> Ain't this guard equivalent to '(false-if-exception
> (call-with-input-pipe* ...))' ? I find the later more readable if yes,

Good point! I was not aware of false-if-exception. I have changed to
using it now.

> but: does call-with-input-pipe* raise an exception when git is available
> but 'sendemail.headerCmd' not set, thus exiting with status 1?  I wasn't
> able to find its documentation in the Guile Reference manual.

call-with-input-pipe* and call-with-input-pipe are both defined in
mumi/client.scm. They are not part of guile. The only difference between
them is whether they accept the command as a string or as a list of
arguments---thus, they parallel open-pipe and open-pipe*.

> Otherwise you'd get header-command set to the empty string, which
> seems like it'd be a problem...

call-with-input-pipe* does raise an exception when git is available but
sendemail.headerCmd is not set. I checked. So, this is not a problem.

>> +         (headers
>> +          (if header-command
>> +              (call-with-input-pipe (string-append header-command " " patch)
>
>                   ^ ... here.  Also, why the mixed use of
>                   'call-with-input-pipe*' and 'call-with-input-pipe'?  I'd
>                   stick with the former.

sendemail.headerCmd is only available to us as a string, and not as a
list of arguments. It is quite non-trivial to correctly split the string
back into a list of arguments. That would require correct handling of
quotes like the shell does. So, we use call-with-input-pipe to handle
this case.

But everywhere else (such as when invoking "git config
sendemail.headerCmd"), we prefer to pass commands as a list of
arguments. So, we need call-with-input-pipe*.

I understand it's a bit confusing to have two very similar
functions. But, the only possible compromise is to use
call-with-input-pipe everywhere. Should I make that compromise? WDYT?

Thanks for the review!

Regards,
Arun
Maxim Cournoyer July 18, 2023, 3:32 p.m. UTC | #3
Hello,

Arun Isaac <arunisaac@systemreboot.net> writes:


[...]

>>> +            git-send-email-headers
>>> +            compose))
>>
>> I think you've exported 'compose' erroneously here.
>
> Good catch! compose is part of a new "mumi compose" feature I am working
> on. I had accidentally committed it. I have removed it from this commit.
>
> Now that you mention it, maybe I should call it compose-email so as to
> not conflict with compose from guile core.

Good idea!  Shadowing builtins should be avoided; the warnings are
annoying and require the use of #:hide on imports (and the code more
confusing to read).

[...]

>> but: does call-with-input-pipe* raise an exception when git is available
>> but 'sendemail.headerCmd' not set, thus exiting with status 1?  I wasn't
>> able to find its documentation in the Guile Reference manual.
>
> call-with-input-pipe* and call-with-input-pipe are both defined in
> mumi/client.scm. They are not part of guile. The only difference between
> them is whether they accept the command as a string or as a list of
> arguments---thus, they parallel open-pipe and open-pipe*.
>
>> Otherwise you'd get header-command set to the empty string, which
>> seems like it'd be a problem...
>
> call-with-input-pipe* does raise an exception when git is available but
> sendemail.headerCmd is not set. I checked. So, this is not a problem.

Good, thanks for checking.

>>> +         (headers
>>> +          (if header-command
>>> +              (call-with-input-pipe (string-append header-command " " patch)
>>
>>                   ^ ... here.  Also, why the mixed use of
>>                   'call-with-input-pipe*' and 'call-with-input-pipe'?  I'd
>>                   stick with the former.
>
> sendemail.headerCmd is only available to us as a string, and not as a
> list of arguments. It is quite non-trivial to correctly split the string
> back into a list of arguments. That would require correct handling of
> quotes like the shell does. So, we use call-with-input-pipe to handle
> this case.

Ah, I see.  It's reasonable then to use it as is.

> But everywhere else (such as when invoking "git config
> sendemail.headerCmd"), we prefer to pass commands as a list of
> arguments. So, we need call-with-input-pipe*.
>
> I understand it's a bit confusing to have two very similar
> functions. But, the only possible compromise is to use
> call-with-input-pipe everywhere. Should I make that compromise? WDYT?

No, just the explanation here (and a possible comment in the source
mirroring it) is enough!

LGTM.
Arun Isaac July 19, 2023, 4:49 p.m. UTC | #4
> Good idea!  Shadowing builtins should be avoided; the warnings are
> annoying and require the use of #:hide on imports (and the code more
> confusing to read).

I definitely agree. I was engrossed in writing mumi-compose, and quite
forgot about the builtin compose.

> No, just the explanation here (and a possible comment in the source
> mirroring it) is enough!

Done, and pushed!

Thanks!
diff mbox series

Patch

diff --git a/mumi/client.scm b/mumi/client.scm
index c30429d..b89e608 100644
--- a/mumi/client.scm
+++ b/mumi/client.scm
@@ -17,6 +17,7 @@ 
 ;;; along with mumi.  If not, see <http://www.gnu.org/licenses/>.
 
 (define-module (mumi client)
+  #:use-module (rnrs exceptions)
   #:use-module (rnrs io ports)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-19)
@@ -38,7 +39,9 @@ 
             print-current-issue
             set-current-issue!
             clear-current-issue!
-            send-email))
+            send-email
+            git-send-email-headers
+            compose))
 
 (define (git-top-level)
   "Return the top-level directory of the current git repository."
@@ -229,6 +232,13 @@  arguments."
                      name)
                  " <" address ">"))
 
+(define (split-cc cc)
+  "Split CC into a list of email addresses."
+  (map (lambda (address)
+         (serialize-email-address (assq-ref address 'name)
+                                  (assq-ref address 'address)))
+       (assq-ref (parse-email-headers (string-append "Cc: " cc "\n"))
+                 'cc)))
 
 (define* (git-send-email to patches #:optional (options '()))
   "Send PATCHES using git send-email to the TO address with
@@ -311,3 +321,43 @@  ISSUE-NUMBER."
                          "@"
                          (client-config 'debbugs-host))
           other-patches)))))
+
+(define (git-send-email-headers patch)
+  "Print send-email headers for PATCH."
+  (let* (;; Compute headers if configured in git config.
+         (header-command
+          (guard (ex (#t #f))
+            (call-with-input-pipe* (list "git" "config" "sendemail.headerCmd")
+              get-line)))
+         (headers
+          (if header-command
+              (call-with-input-pipe (string-append header-command " " patch)
+                get-string-all)
+              ""))
+         (external-x-debbugs-cc
+          (cond
+           ((assq-ref (parse-email-headers (string-append headers "\n"))
+                       'x-debbugs-cc)
+            => split-cc)
+           (else '())))
+         ;; Fetch Cc addresses for current issue.
+         (x-debbugs-cc
+          (cond
+           ((assq-ref (reply-email-headers (current-issue-number))
+                       'cc)
+            => split-cc)
+           (else '()))))
+    ;; Print X-Debbugs-Cc header.
+    (display "X-Debbugs-Cc: ")
+    (display (string-join (delete-duplicates
+                           (append x-debbugs-cc external-x-debbugs-cc))
+                          ", "))
+    (newline)
+    ;; Print headers other than X-Debbugs-Cc.
+    ;; TODO: RFC5322 headers are not restricted to a single
+    ;; line. "Folded" multi-line headers are allowed. Support them.
+    (for-each (lambda (line)
+                (unless (string-prefix-ci? "X-Debbugs-Cc:" line)
+                  (display line)
+                  (newline)))
+              (string-split headers #\newline))))
diff --git a/scripts/mumi.in b/scripts/mumi.in
index 2295328..8fb7cd4 100644
--- a/scripts/mumi.in
+++ b/scripts/mumi.in
@@ -163,6 +163,8 @@ 
    (client:clear-current-issue!))
   (("send-email" . patches)
    (client:send-email patches))
+  (("git-send-email-headers" patch)
+   (client:git-send-email-headers patch))
   (("mailer" . rest)
    (let* ((opts (parse-options rest))
           (sender (assoc-ref opts 'sender))
diff --git a/tests/client.scm b/tests/client.scm
index 2b2c1be..ced573b 100644
--- a/tests/client.scm
+++ b/tests/client.scm
@@ -68,6 +68,9 @@  called with."
 (define serialize-email-address
   (@@ (mumi client) serialize-email-address))
 
+(define split-cc
+  (@@ (mumi client) split-cc))
+
 (test-begin "client")
 
 (test-equal "serialize email address"
@@ -78,6 +81,11 @@  called with."
   "\"Bar, Foo\" <foobar@example.com>"
   (serialize-email-address "Bar, Foo" "foobar@example.com"))
 
+(test-equal "split Cc field"
+  (list "Foo <foo@example.com>"
+        "\"Bar, Foo\" <foobar@example.com>")
+  (split-cc "Foo <foo@example.com>, \"Bar, Foo\" <foobar@example.com>"))
+
 (test-equal "send patches to new issue"
   '(("git" "send-email" "--to=foo@patches.com" "foo.patch")
     ("git" "send-email" "--to=12345@example.com" "bar.patch" "foobar.patch"))