Merge branch 'ph/send-email'

* ph/send-email:
  git send-email: ask less questions when --compose is used.
  git send-email: add --annotate option
  git send-email: interpret unknown files as revision lists
  git send-email: make the message file name more specific.
maint
Junio C Hamano 2008-11-27 19:24:00 -08:00
commit 496db64202
3 changed files with 207 additions and 75 deletions

View File

@ -8,7 +8,7 @@ git-send-email - Send a collection of patches as emails


SYNOPSIS SYNOPSIS
-------- --------
'git send-email' [options] <file|directory> [... file|directory] 'git send-email' [options] <file|directory|rev-list options>...




DESCRIPTION DESCRIPTION
@ -37,9 +37,23 @@ 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. The --cc option must be repeated for each user you want on the cc list.


--annotate::
Review each patch you're about to send in an editor. The setting
'sendemail.multiedit' defines if this will spawn one editor per patch
or one for all of them at once.

--compose:: --compose::
Use $GIT_EDITOR, core.editor, $VISUAL, or $EDITOR to edit an Use $GIT_EDITOR, core.editor, $VISUAL, or $EDITOR to edit an
introductory message for the patch series. introductory message for the patch series.
+
When compose is in used, git send-email gets less interactive will use the
values of the headers you set there. If the body of the email (what you type
after the headers and a blank line) only contains blank (or GIT: prefixed)
lines, the summary won't be sent, but git-send-email will still use the
Headers values if you don't removed them.
+
If it wasn't able to see a header in the summary it will ask you about it
interactively after quitting your editor.


--from:: --from::
Specify the sender of the emails. This will default to Specify the sender of the emails. This will default to
@ -183,6 +197,12 @@ Administering
--[no-]validate:: --[no-]validate::
Perform sanity checks on patches. Perform sanity checks on patches.
Currently, validation means the following: Currently, validation means the following:

--[no-]format-patch::
When an argument may be understood either as a reference or as a file name,
choose to understand it as a format-patch argument ('--format-patch')
or as a file name ('--no-format-patch'). By default, when such a conflict
occurs, git send-email will fail.
+ +
-- --
* Warn of patches that contain lines longer than 998 characters; this * Warn of patches that contain lines longer than 998 characters; this
@ -204,6 +224,12 @@ sendemail.aliasfiletype::
Format of the file(s) specified in sendemail.aliasesfile. Must be Format of the file(s) specified in sendemail.aliasesfile. Must be
one of 'mutt', 'mailrc', 'pine', or 'gnus'. one of 'mutt', 'mailrc', 'pine', or 'gnus'.


sendemail.multiedit::
If true (default), a single editor instance will be spawned to edit
files you have to edit (patches when '--annotate' is used, and the
summary when '--compose' is used). If false, files will be edited one
after the other, spawning a new editor each time.



Author Author
------ ------

View File

@ -22,8 +22,12 @@ use Term::ReadLine;
use Getopt::Long; use Getopt::Long;
use Data::Dumper; use Data::Dumper;
use Term::ANSIColor; use Term::ANSIColor;
use File::Temp qw/ tempdir /;
use Error qw(:try);
use Git; use Git;


Getopt::Long::Configure qw/ pass_through /;

package FakeTerm; package FakeTerm;
sub new { sub new {
my ($class, $reason) = @_; my ($class, $reason) = @_;
@ -38,7 +42,7 @@ package main;


sub usage { sub usage {
print <<EOT; print <<EOT;
git send-email [options] <file | directory>... git send-email [options] <file | directory | rev-list options >


Composing: Composing:
--from <str> * Email From: --from <str> * Email From:
@ -47,6 +51,7 @@ git send-email [options] <file | directory>...
--bcc <str> * Email Bcc: --bcc <str> * Email Bcc:
--subject <str> * Email "Subject:" --subject <str> * Email "Subject:"
--in-reply-to <str> * Email "In-Reply-To:" --in-reply-to <str> * Email "In-Reply-To:"
--annotate * Review each patch that will be sent in an editor.
--compose * Open an editor for introduction. --compose * Open an editor for introduction.


Sending: Sending:
@ -73,6 +78,8 @@ git send-email [options] <file | directory>...
--quiet * Output one line of info per email. --quiet * Output one line of info per email.
--dry-run * Don't actually send the emails. --dry-run * Don't actually send the emails.
--[no-]validate * Perform patch sanity checks. Default on. --[no-]validate * Perform patch sanity checks. Default on.
--[no-]format-patch * understand any non optional arguments as
`git format-patch` ones.


EOT EOT
exit(1); exit(1);
@ -124,12 +131,10 @@ my $auth;
sub unique_email_list(@); sub unique_email_list(@);
sub cleanup_compose_files(); sub cleanup_compose_files();


# Constants (essentially)
my $compose_filename = ".msg.$$";

# Variables we fill in automatically, or via prompting: # Variables we fill in automatically, or via prompting:
my (@to,@cc,@initial_cc,@bcclist,@xh, my (@to,@cc,@initial_cc,@bcclist,@xh,
$initial_reply_to,$initial_subject,@files,$author,$sender,$smtp_authpass,$compose,$time); $initial_reply_to,$initial_subject,@files,
$author,$sender,$smtp_authpass,$annotate,$compose,$time);


my $envelope_sender; my $envelope_sender;


@ -149,6 +154,27 @@ if ($@) {


# Behavior modification variables # Behavior modification variables
my ($quiet, $dry_run) = (0, 0); my ($quiet, $dry_run) = (0, 0);
my $format_patch;
my $compose_filename = $repo->repo_path() . "/.gitsendemail.msg.$$";

# Handle interactive edition of files.
my $multiedit;
my $editor = $ENV{GIT_EDITOR} || Git::config(@repo, "core.editor") || $ENV{VISUAL} || $ENV{EDITOR} || "vi";
sub do_edit {
if (defined($multiedit) && !$multiedit) {
map {
system('sh', '-c', $editor.' "$@"', $editor, $_);
if (($? & 127) || ($? >> 8)) {
die("the editor exited uncleanly, aborting everything");
}
} @_;
} else {
system('sh', '-c', $editor.' "$@"', $editor, @_);
if (($? & 127) || ($? >> 8)) {
die("the editor exited uncleanly, aborting everything");
}
}
}


# Variables with corresponding config settings # Variables with corresponding config settings
my ($thread, $chain_reply_to, $suppress_from, $signed_off_by_cc, $cc_cmd); my ($thread, $chain_reply_to, $suppress_from, $signed_off_by_cc, $cc_cmd);
@ -179,6 +205,7 @@ my %config_settings = (
"aliasesfile" => \@alias_files, "aliasesfile" => \@alias_files,
"suppresscc" => \@suppress_cc, "suppresscc" => \@suppress_cc,
"envelopesender" => \$envelope_sender, "envelopesender" => \$envelope_sender,
"multiedit" => \$multiedit,
); );


# Handle Uncouth Termination # Handle Uncouth Termination
@ -221,6 +248,7 @@ my $rc = GetOptions("sender|from=s" => \$sender,
"smtp-ssl" => sub { $smtp_encryption = 'ssl' }, "smtp-ssl" => sub { $smtp_encryption = 'ssl' },
"smtp-encryption=s" => \$smtp_encryption, "smtp-encryption=s" => \$smtp_encryption,
"identity=s" => \$identity, "identity=s" => \$identity,
"annotate" => \$annotate,
"compose" => \$compose, "compose" => \$compose,
"quiet" => \$quiet, "quiet" => \$quiet,
"cc-cmd=s" => \$cc_cmd, "cc-cmd=s" => \$cc_cmd,
@ -231,6 +259,7 @@ my $rc = GetOptions("sender|from=s" => \$sender,
"envelope-sender=s" => \$envelope_sender, "envelope-sender=s" => \$envelope_sender,
"thread!" => \$thread, "thread!" => \$thread,
"validate!" => \$validate, "validate!" => \$validate,
"format-patch!" => \$format_patch,
); );


unless ($rc) { unless ($rc) {
@ -368,23 +397,52 @@ if (@alias_files and $aliasfiletype and defined $parse_alias{$aliasfiletype}) {


($sender) = expand_aliases($sender) if defined $sender; ($sender) = expand_aliases($sender) if defined $sender;


# returns 1 if the conflict must be solved using it as a format-patch argument
sub check_file_rev_conflict($) {
my $f = shift;
try {
$repo->command('rev-parse', '--verify', '--quiet', $f);
if (defined($format_patch)) {
print "foo\n";
return $format_patch;
}
die(<<EOF);
File '$f' exists but it could also be the range of commits
to produce patches for. Please disambiguate by...

* Saying "./$f" if you mean a file; or
* Giving --format-patch option if you mean a range.
EOF
} catch Git::Error::Command with {
return 0;
}
}

# Now that all the defaults are set, process the rest of the command line # 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. # arguments and collect up the files that need to be processed.
for my $f (@ARGV) { my @rev_list_opts;
if (-d $f) { while (my $f = pop @ARGV) {
if ($f eq "--") {
push @rev_list_opts, "--", @ARGV;
@ARGV = ();
} elsif (-d $f and !check_file_rev_conflict($f)) {
opendir(DH,$f) opendir(DH,$f)
or die "Failed to opendir $f: $!"; or die "Failed to opendir $f: $!";


push @files, grep { -f $_ } map { +$f . "/" . $_ } push @files, grep { -f $_ } map { +$f . "/" . $_ }
sort readdir(DH); sort readdir(DH);
closedir(DH); closedir(DH);
} elsif (-f $f or -p $f) { } elsif ((-f $f or -p $f) and !check_file_rev_conflict($f)) {
push @files, $f; push @files, $f;
} else { } else {
print STDERR "Skipping $f - not found.\n"; push @rev_list_opts, $f;
} }
} }


if (@rev_list_opts) {
push @files, $repo->command('format-patch', '-o', tempdir(CLEANUP => 1), @rev_list_opts);
}

if ($validate) { if ($validate) {
foreach my $f (@files) { foreach my $f (@files) {
unless (-p $f) { unless (-p $f) {
@ -403,6 +461,108 @@ if (@files) {
usage(); usage();
} }


sub get_patch_subject($) {
my $fn = shift;
open (my $fh, '<', $fn);
while (my $line = <$fh>) {
next unless ($line =~ /^Subject: (.*)$/);
close $fh;
return "GIT: $1\n";
}
close $fh;
die "No subject line in $fn ?";
}

if ($compose) {
# Note that this does not need to be secure, but we will make a small
# effort to have it be unique
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

EOT
for my $f (@files) {
print C get_patch_subject($f);
}
close(C);

my $editor = $ENV{GIT_EDITOR} || Git::config(@repo, "core.editor") || $ENV{VISUAL} || $ENV{EDITOR} || "vi";

if ($annotate) {
do_edit($compose_filename, @files);
} else {
do_edit($compose_filename);
}

open(C2,">",$compose_filename . ".final")
or die "Failed to open $compose_filename.final : " . $!;

open(C,"<",$compose_filename)
or die "Failed to open $compose_filename : " . $!;

my $need_8bit_cte = file_has_nonascii($compose_filename);
my $in_body = 0;
my $summary_empty = 1;
while(<C>) {
next if m/^GIT: /;
if ($in_body) {
$summary_empty = 0 unless (/^\n$/);
} elsif (/^\n$/) {
$in_body = 1;
if ($need_8bit_cte) {
print C2 "MIME-Version: 1.0\n",
"Content-Type: text/plain; ",
"charset=utf-8\n",
"Content-Transfer-Encoding: 8bit\n";
}
} 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";
} elsif (/^In-Reply-To:\s*(.+)\s*$/i) {
$initial_reply_to = $1;
next;
} elsif (/^From:\s*(.+)\s*$/i) {
$sender = $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;
}
} elsif ($annotate) {
do_edit(@files);
}

my $prompting = 0; my $prompting = 0;
if (!defined $sender) { if (!defined $sender) {
$sender = $repoauthor || $repocommitter || ''; $sender = $repoauthor || $repocommitter || '';
@ -447,17 +607,6 @@ sub expand_aliases {
@initial_cc = expand_aliases(@initial_cc); @initial_cc = expand_aliases(@initial_cc);
@bcclist = expand_aliases(@bcclist); @bcclist = expand_aliases(@bcclist);


if (!defined $initial_subject && $compose) {
while (1) {
$_ = $term->readline("What subject should the initial email start with? ", $initial_subject);
last if defined $_;
print "\n";
}

$initial_subject = $_;
$prompting++;
}

if ($thread && !defined $initial_reply_to && $prompting) { if ($thread && !defined $initial_reply_to && $prompting) {
while (1) { while (1) {
$_= $term->readline("Message-ID to be used as In-Reply-To for the first email? ", $initial_reply_to); $_= $term->readline("Message-ID to be used as In-Reply-To for the first email? ", $initial_reply_to);
@ -484,59 +633,6 @@ if (!defined $smtp_server) {
} }


if ($compose) { if ($compose) {
# Note that this does not need to be secure, but we will make a small
# effort to have it be unique
open(C,">",$compose_filename)
or die "Failed to open for writing $compose_filename: $!";
print C "From $sender # This line is ignored.\n";
printf C "Subject: %s\n\n", $initial_subject;
printf C <<EOT;
GIT: Please enter your email below.
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.

EOT
close(C);

my $editor = $ENV{GIT_EDITOR} || Git::config(@repo, "core.editor") || $ENV{VISUAL} || $ENV{EDITOR} || "vi";
system('sh', '-c', $editor.' "$@"', $editor, $compose_filename);

open(C2,">",$compose_filename . ".final")
or die "Failed to open $compose_filename.final : " . $!;

open(C,"<",$compose_filename)
or die "Failed to open $compose_filename : " . $!;

my $need_8bit_cte = file_has_nonascii($compose_filename);
my $in_body = 0;
while(<C>) {
next if m/^GIT: /;
if (!$in_body && /^\n$/) {
$in_body = 1;
if ($need_8bit_cte) {
print C2 "MIME-Version: 1.0\n",
"Content-Type: text/plain; ",
"charset=utf-8\n",
"Content-Transfer-Encoding: 8bit\n";
}
}
if (!$in_body && /^MIME-Version:/i) {
$need_8bit_cte = 0;
}
if (!$in_body && /^Subject: ?(.*)/i) {
my $subject = $1;
$_ = "Subject: " .
($subject =~ /[^[:ascii:]]/ ?
quote_rfc2047($subject) :
$subject) .
"\n";
}
print C2 $_;
}
close(C);
close(C2);

while (1) { while (1) {
$_ = $term->readline("Send this email? (y|n) "); $_ = $term->readline("Send this email? (y|n) ");
last if defined $_; last if defined $_;
@ -548,8 +644,10 @@ EOT
exit(0); exit(0);
} }


if ($compose > 0) {
@files = ($compose_filename . ".final", @files); @files = ($compose_filename . ".final", @files);
} }
}


# Variables we set as part of the loop over files # Variables we set as part of the loop over files
our ($message_id, %mail, $subject, $reply_to, $references, $message); our ($message_id, %mail, $subject, $reply_to, $references, $message);

View File

@ -292,4 +292,12 @@ test_expect_success '--compose adds MIME for utf8 subject' '
grep "^Subject: =?utf-8?q?utf8-s=C3=BCbj=C3=ABct?=" msgtxt1 grep "^Subject: =?utf-8?q?utf8-s=C3=BCbj=C3=ABct?=" msgtxt1
' '


test_expect_success 'detects ambiguous reference/file conflict' '
echo master > master &&
git add master &&
git commit -m"add master" &&
test_must_fail git send-email --dry-run master 2>errors &&
grep disambiguate errors
'

test_done test_done