git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH RFC 10/10] send-email: --compose takes optional argument to existing file

From
Michael Witten <mfwitten@gmail.com>
Date
Apr 11, 2009, 19:08 UTC
Message-ID
<1239476908-25944-10-git-send-email-mfwitten@gmail.com>
In-Reply-To
<1239476908-25944-9-git-send-email-mfwitten@gmail.com>

Now, a user may specify an existing (in-progress) file to use as the introductory/summary email.

The file is opened for any additional editing as usual, but it is not deleted upon normal termination.

There are also a number of fixes to how the internals and temporaries are handled.

Signed-off-by: Michael Witten <mfwitten@gmail.com>
---
 Documentation/git-send-email.txt |   39 ++++++--
 git-send-email.perl              |  190 ++++++++++++++++++++++++--------------
 2 files changed, 151 insertions(+), 78 deletions(-)
diff --git a/Documentation/git-send-email.txt b/Documentation/git-send-email.txt
index 07c831e..a2af3e5 100644
--- a/Documentation/git-send-email.txt
+++ b/Documentation/git-send-email.txt
@@ -60,17 +60,38 @@ The --bcc option must be repeated for each user you want on the bcc list.
 +
 The --cc option must be repeated for each user you want on the cc list.
 
---compose::
-	Use $GIT_EDITOR, core.editor, $VISUAL, or $EDITOR to edit an
-	introductory message for the patch series.
+--compose[=<path>]::
+	Use `$GIT_EDITOR`, 'core.editor', `$VISUAL`, or `$EDITOR`, or
+	`vi` to edit an introductory message for the patch series. A
+	path for the intermediate composition may be given. If the path
+	doesn't exist, a file with default contents is created at the
+	path and opened for editing. If the path does exist, then the
+	file at that path is opened for editing as-is. If no path is
+	specified, a new temporary file is created with some default
+	contents. Upon the successful completion of send-email, all
+	temporary files are automatically unlinked (deleted). However,
+	if send-email is terminated by a trappable signal, then this
+	temporary file is not unlinked, and the user is informed of its
+	path.
 +
-When '--compose' is used, git send-email will use the From, Subject, and
-In-Reply-To headers specified in the message. If the body of the message
-(what you type after the headers and a blank line) only contains blank
-(or GIT: prefixed) lines the summary won't be sent, but From, Subject,
-and In-Reply-To headers will be used unless they are removed.
+The user actually composes what will become a valid email message;
+therefore, the message must have the following form (as described in
+`RFC 822`):
 +
-Missing From or In-Reply-To headers will be prompted for.
+	<headers>
+	<blank line>
+	<body>
++
+In particular, `<headers>` must contain the "`Subject`" header. Once the
+user saves the message and quits the editor, this intermediate message is
+transformed into the final email message by removing all lines that begin
+with "`GIT:`". If the `<body>` of the final version is empty, then the
+email is not sent, and send-email proceeds as if '--compose' had never been
+used.
++
+The "`From`", "`Subject`", and "`In-Reply-To`" headers are taken directly
+from the message; missing "`From`" or "`In-Reply-To`" headers will be
+prompted for.
 
 --from=<address>::
 	Specify the sender of the emails.  This will default to
diff --git a/git-send-email.perl b/git-send-email.perl
index e771720..877732f 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -63,10 +63,9 @@ git send-email [options] <file | directory | rev-list options >
     --bcc               <address>  * Email Bcc:
     --subject            <string>  * Email "Subject:"
     --in-reply-to    <message-id>  * Email "In-Reply-To:"; include '<' and '>'.
+    --compose             [<path>] * Open an editor for introduction.
     --annotate                     * Review each patch that will be sent in
                                      an editor.
-    --compose                      * Open an editor for introduction.
-
   Sending:
     --envelope-sender   <address>  * Email envelope sender.
     --smtp-server          <host>  * Outgoing SMTP server to use.
@@ -173,7 +172,7 @@ if ($@) {
 # Behavior modification variables
 my ($quiet, $dry_run) = (0, 0);
 my $format_patch;
-my $compose_filename;
+my ($compose_filename, $compose_final_filename);
 
 # Handle interactive edition of files.
 my $multiedit;
@@ -239,16 +238,15 @@ sub signal_handler {
 	system "stty echo";
 
 	# tmp files from --compose
-	if (defined $compose_filename) {
-		if (-e $compose_filename) {
-			print "'$compose_filename' contains an intermediate version of the email you were composing.\n";
-		}
-		if (-e ($compose_filename . ".final")) {
-			print "'$compose_filename.final' contains the composed email.\n"
-		}
+	if (defined $compose_filename and -f $compose_filename) {
+		print "'$compose_filename' contains an intermediate version of the email you were composing.\n";
+	}
+
+	if (defined $compose_final_filename) {
+		unlink $compose_final_filename if defined $compose_final_filename;
 	}
 
-	exit;
+	exit 1;
 };
 
 $SIG{TERM} = \&signal_handler;
@@ -273,8 +271,8 @@ my $rc = GetOptions(
 	"smtp-ssl" => sub { $smtp_encryption = 'ssl' },
 	"smtp-encryption=s" => \$smtp_encryption,
 	"identity=s" => \$identity,
+	"compose:s" => \$compose,
 	"annotate" => \$annotate,
-	"compose" => \$compose,
 	"quiet" => \$quiet,
 	"cc-cmd=s" => \$cc_cmd,
 	"suppress-from!" => \$suppress_from,
@@ -596,36 +594,69 @@ sub get_patch_subject($) {
 	die "'Subject:' line expected in '$patch'";
 }
 
-if ($compose) {
-	# Note that this does not need to be secure, but we will make a small
-	# effort to have it be unique
-	$compose_filename = ($repo ?
-		tempfile(".gitsendemail.msg.XXXXXX", DIR => $repo->repo_path()) :
-		tempfile(".gitsendemail.msg.XXXXXX", DIR => "."))[1];
-	open(C,">",$compose_filename)
-		or die "Failed to open for writing $compose_filename: $!";
-
-
-	my $tpl_sender = $sender || $repoauthor || $repocommitter || '';
-	my $tpl_subject = $initial_subject || '';
-	my $tpl_reply_to = $initial_reply_to || '';
-
-	print C <<EOT;
-From $tpl_sender # This line is ignored.
-GIT: Lines beginning in "GIT:" will be removed.
-GIT: Consider including an overall diffstat or table of contents
-GIT: for the patch you are writing.
-GIT:
-GIT: Clear the body content if you don't wish to send a summary.
-From: $tpl_sender
-Subject: $tpl_subject
-In-Reply-To: $tpl_reply_to
+if (defined $compose) {{
 
-EOT
-	for my $f (@files) {
-		print C "GIT: ", get_patch_subject($f), "\n";
+	# Some parameters for creating temporary files:
+
+	my $template = ".gitsendemail.msg.XXXXXX";
+	my @dir = (DIR => ($repo ? $repo->repo_path() : "."));
+	my @suffix = (SUFFIX => ".final");
+
+	# Figure out the file that the user should be editing:
+
+	my $compose_file;
+	my $needs_default_contents;
+
+	if ($compose eq '') { # if no path was given
+		$needs_default_contents = 1;
+		($compose_file, $compose_filename) = tempfile($template, @dir)
+			or die "--compose: Could not create temporary file for the user to edit: $!\n";
+	} else {
+		$compose_filename = $compose;
+		$needs_default_contents = not -f $compose_filename;
 	}
-	close(C);
+
+	# Fill in default contents if necessary:
+
+	if ($needs_default_contents) {
+
+		open $compose_file, ">", $compose_filename;
+
+		# For convenience:
+
+		local *STDOUT = $compose_file;
+
+		# Help the user out with some instruction and initial headers:
+
+		my $from = $sender || $repoauthor || $repocommitter || '';
+		my $subject = $initial_subject || '';
+		my $reply_to = $initial_reply_to || '';
+
+		print "From $from # This line is ignored.\n";
+		print "GIT:\n";
+		print "GIT: Lines beginning in 'GIT:' will be removed.\n";
+		print "GIT:\n";
+		print "GIT: Consider including an overall diffstat\n";
+		print "GIT: (git diff --stat) or table of contents\n";
+		print "GIT: (as provide below).\n";
+		print "GIT:\n";
+		print "GIT: Clear the body content if you decide not\n";
+		print "GIT: to send this message.\n";
+		print "GIT:\n";
+		print "GIT: Here are the <headers>:\n";
+		print "From: $from\n";
+		print "Subject: $subject\n";
+		print "In-Reply-To: $reply_to\n";
+		print "\n";
+		print "GIT: This is the first line of the <body>:\n";
+		print "\n";
+
+		for my $f (@files) {
+			print "GIT: ", get_patch_subject($f), "\n";
+		}
+	}
+
+	# Do the editing:
 
 	my $editor = $ENV{GIT_EDITOR} || Git::config(@repo, "core.editor") || $ENV{VISUAL} || $ENV{EDITOR} || "vi";
 
@@ -635,23 +666,40 @@ EOT
 		do_edit($compose_filename);
 	}
 
-	open(C2,">",$compose_filename . ".final")
-		or die "Failed to open $compose_filename.final : " . $!;
+	# Now transform the user-edited introduction into something
+	# suitable for sending via email; the user's editor may have
+	# unlinked the original file and replaced it with an entirely
+	# new one. If this be the case, then it wouldn't do just to seek
+	# to the beginning and start reading, because then only the
+	# original content would be retrieved. Consequently, the file
+	# must be reopened to be safe (note, the original filehandle is
+	# closed automatically):
+
+	unless (-f $compose_filename) {
+		print "--compose: File '$compose_filename' doesn't exist; not sending.\n";
+		last;
+	}
+
+	open $compose_file, "<", $compose_filename
+		or die "--compose: Failed to open '$compose_filename' for reading: $!";
 
-	open(C,"<",$compose_filename)
-		or die "Failed to open $compose_filename : " . $!;
+	# Create the final version:
 
+	(my $compose_final_file, $compose_final_filename) = tempfile($template, @dir, @suffix)
+		or die "--compose: Could not create temporary file for final version: $!\n";
+
+	my ($subject, $reply_to, $from);
 	my $need_8bit_cte = file_has_nonascii($compose_filename);
 	my $in_body = 0;
 	my $summary_empty = 1;
-	while(<C>) {
+	while(<$compose_file>) {
 		next if m/^GIT:/;
 		if ($in_body) {
 			$summary_empty = 0 unless (/^\n$/);
 		} elsif (/^\n$/) {
 			$in_body = 1;
 			if ($need_8bit_cte) {
-				print C2
+				print $compose_final_file
 					"MIME-Version: 1.0\n",
 					"Content-Type: text/plain; ",
 					"charset=utf-8\n",
@@ -660,33 +708,31 @@ EOT
 		} elsif (/^MIME-Version:/i) {
 			$need_8bit_cte = 0;
 		} elsif (/^Subject:\s*(.+)\s*$/i) {
-			$initial_subject = $1;
-			my $subject = $initial_subject;
-			$_ = "Subject: " .
-				($subject =~ /[^[:ascii:]]/ ?
-				quote_rfc2047($subject) :
-				$subject) .
-				"\n";
+			$subject = $1;
+			next;
 		} elsif (/^In-Reply-To:\s*(.+)\s*$/i) {
-			$initial_reply_to = $1;
+			$reply_to = $1;
 			next;
 		} elsif (/^From:\s*(.+)\s*$/i) {
-			$sender = $1;
+			$from = $1;
 			next;
 		} elsif (/^(?:To|Cc|Bcc):/i) {
 			print "To/Cc/Bcc fields are not interpreted yet, they have been ignored\n";
 			next;
 		}
-		print C2 $_;
-	}
-	close(C);
-	close(C2);
 
-	if ($summary_empty) {
-		print "Summary email is empty, skipping it\n";
-		$compose = -1;
+		print $compose_final_file $_;
 	}
-} elsif ($annotate) {
+
+	print("Summary email is empty, skipping it.\n"), last if ($summary_empty);
+
+	$initial_subject = $subject;
+	$initial_reply_to = $reply_to;
+	$sender = $from;
+
+	unshift(@files, $compose_final_filename);
+
+}} elsif ($annotate) {
 	do_edit(@files);
 }
 
@@ -776,10 +822,6 @@ if (defined $smtp_server and $smtp_server ne '') {
 	$smtp_server ||= '127.0.0.1';
 }
 
-if ($compose && $compose > 0) {
-	@files = ($compose_filename . ".final", @files);
-}
-
 # Variables we set as part of the loop over files
 our ($message_id, %mail, $subject, $reply_to, $references, $message,
 	$needs_confirm, $message_num, $ask_default);
@@ -924,9 +966,12 @@ sub send_message
 	my $sanitized_sender = sanitize_address($sender);
 	make_message_id() unless defined($message_id);
 
+	my $has_non_ascii = ($subject =~ /[^[:ascii:]]/);
+	my $sanitized_subject = $has_non_ascii ? quote_rfc2047($subject) : $subject;
+
 	my $header = "From: $sanitized_sender
 To: $to${ccline}
-Subject: $subject
+Subject: $sanitized_subject
 Date: $date
 Message-Id: $message_id
 X-Mailer: git-send-email $gitversion
@@ -1286,7 +1331,14 @@ for (my $index = 0; $index < @files; $index++) {
 cleanup_compose_files();
 
 sub cleanup_compose_files() {
-	unlink($compose_filename, $compose_filename . ".final") if $compose;
+
+	if (defined $compose_final_filename) {
+		unlink $compose_final_filename;
+	}
+
+	if (defined $compose_filename and not $compose) {
+		unlink $compose_filename;
+	}
 }
 
 $smtp->quit if $smtp;
-- 
1.6.2.2.479.g2aec
Previous: Michael WittenNext: Sverre Rabbelier
Message 10 of 23 in “Docs: send-email: Put options back into alphabetical order”
  1. 01/10 Docs: send-email: Put options back into alphabetical orderMichael Witten, Apr 11, 2009
  2. 02/10 Docs: send-email: Remove superfluous information in CONFIGURATIONMichael Witten, Apr 11, 2009
  3. 03/10 send-email: Cleanup the usage text and docs a bitMichael Witten, Apr 11, 2009
  4. 04/10 send-email: --smtp-server-port should take an integerMichael Witten, Apr 11, 2009
  5. 05/10 send-email: Handle "GIT:" rather than "GIT: " during --composeMichael Witten, Apr 11, 2009
  6. 06/10 send-email: References: should only reference what is actually sentMichael Witten, Apr 11, 2009
  7. 07/10 send-email: Remove horrible mix of tabs and spacesMichael Witten, Apr 11, 2009
  8. 08/10 send-email: Add --sleep for email throttlingMichael Witten, Apr 11, 2009
  9. 09/10 send-email: Minor cleanup of $smtp_server usage and send_message()Michael Witten, Apr 11, 2009
  10. 10/10 send-email: --compose takes optional argument to existing fileMichael Witten, Apr 11, 2009
  11. Sverre RabbelierApr 11, 2009
  12. Michael WittenApr 11, 2009
  13. Stephen BoydApr 11, 2009
  14. Michael WittenApr 11, 2009
  15. Jay SoffianApr 12, 2009
  16. Michael WittenApr 12, 2009
  17. Sverre RabbelierApr 11, 2009
  18. Michael WittenApr 11, 2009
  19. Miles BaderApr 13, 2009
  20. Michael WittenApr 13, 2009
  21. Stephen BoydApr 11, 2009
  22. Michael WittenApr 11, 2009
  23. Stephen BoydApr 11, 2009

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.