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.
This commit is contained in:
commit
496db64202
@ -8,7 +8,7 @@ git-send-email - Send a collection of patches as emails
|
||||
|
||||
SYNOPSIS
|
||||
--------
|
||||
'git send-email' [options] <file|directory> [... file|directory]
|
||||
'git send-email' [options] <file|directory|rev-list options>...
|
||||
|
||||
|
||||
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.
|
||||
|
||||
--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::
|
||||
Use $GIT_EDITOR, core.editor, $VISUAL, or $EDITOR to edit an
|
||||
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::
|
||||
Specify the sender of the emails. This will default to
|
||||
@ -183,6 +197,12 @@ Administering
|
||||
--[no-]validate::
|
||||
Perform sanity checks on patches.
|
||||
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
|
||||
@ -204,6 +224,12 @@ sendemail.aliasfiletype::
|
||||
Format of the file(s) specified in sendemail.aliasesfile. Must be
|
||||
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
|
||||
------
|
||||
|
@ -22,8 +22,12 @@ use Term::ReadLine;
|
||||
use Getopt::Long;
|
||||
use Data::Dumper;
|
||||
use Term::ANSIColor;
|
||||
use File::Temp qw/ tempdir /;
|
||||
use Error qw(:try);
|
||||
use Git;
|
||||
|
||||
Getopt::Long::Configure qw/ pass_through /;
|
||||
|
||||
package FakeTerm;
|
||||
sub new {
|
||||
my ($class, $reason) = @_;
|
||||
@ -38,7 +42,7 @@ package main;
|
||||
|
||||
sub usage {
|
||||
print <<EOT;
|
||||
git send-email [options] <file | directory>...
|
||||
git send-email [options] <file | directory | rev-list options >
|
||||
|
||||
Composing:
|
||||
--from <str> * Email From:
|
||||
@ -47,6 +51,7 @@ git send-email [options] <file | directory>...
|
||||
--bcc <str> * Email Bcc:
|
||||
--subject <str> * Email "Subject:"
|
||||
--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.
|
||||
|
||||
Sending:
|
||||
@ -73,6 +78,8 @@ git send-email [options] <file | directory>...
|
||||
--quiet * Output one line of info per email.
|
||||
--dry-run * Don't actually send the emails.
|
||||
--[no-]validate * Perform patch sanity checks. Default on.
|
||||
--[no-]format-patch * understand any non optional arguments as
|
||||
`git format-patch` ones.
|
||||
|
||||
EOT
|
||||
exit(1);
|
||||
@ -124,12 +131,10 @@ my $auth;
|
||||
sub unique_email_list(@);
|
||||
sub cleanup_compose_files();
|
||||
|
||||
# Constants (essentially)
|
||||
my $compose_filename = ".msg.$$";
|
||||
|
||||
# Variables we fill in automatically, or via prompting:
|
||||
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;
|
||||
|
||||
@ -149,6 +154,27 @@ if ($@) {
|
||||
|
||||
# Behavior modification variables
|
||||
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
|
||||
my ($thread, $chain_reply_to, $suppress_from, $signed_off_by_cc, $cc_cmd);
|
||||
@ -179,6 +205,7 @@ my %config_settings = (
|
||||
"aliasesfile" => \@alias_files,
|
||||
"suppresscc" => \@suppress_cc,
|
||||
"envelopesender" => \$envelope_sender,
|
||||
"multiedit" => \$multiedit,
|
||||
);
|
||||
|
||||
# Handle Uncouth Termination
|
||||
@ -221,6 +248,7 @@ my $rc = GetOptions("sender|from=s" => \$sender,
|
||||
"smtp-ssl" => sub { $smtp_encryption = 'ssl' },
|
||||
"smtp-encryption=s" => \$smtp_encryption,
|
||||
"identity=s" => \$identity,
|
||||
"annotate" => \$annotate,
|
||||
"compose" => \$compose,
|
||||
"quiet" => \$quiet,
|
||||
"cc-cmd=s" => \$cc_cmd,
|
||||
@ -231,6 +259,7 @@ my $rc = GetOptions("sender|from=s" => \$sender,
|
||||
"envelope-sender=s" => \$envelope_sender,
|
||||
"thread!" => \$thread,
|
||||
"validate!" => \$validate,
|
||||
"format-patch!" => \$format_patch,
|
||||
);
|
||||
|
||||
unless ($rc) {
|
||||
@ -368,23 +397,52 @@ if (@alias_files and $aliasfiletype and defined $parse_alias{$aliasfiletype}) {
|
||||
|
||||
($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
|
||||
# arguments and collect up the files that need to be processed.
|
||||
for my $f (@ARGV) {
|
||||
if (-d $f) {
|
||||
my @rev_list_opts;
|
||||
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)
|
||||
or die "Failed to opendir $f: $!";
|
||||
|
||||
push @files, grep { -f $_ } map { +$f . "/" . $_ }
|
||||
sort readdir(DH);
|
||||
closedir(DH);
|
||||
} elsif (-f $f or -p $f) {
|
||||
} elsif ((-f $f or -p $f) and !check_file_rev_conflict($f)) {
|
||||
push @files, $f;
|
||||
} 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) {
|
||||
foreach my $f (@files) {
|
||||
unless (-p $f) {
|
||||
@ -403,6 +461,108 @@ if (@files) {
|
||||
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;
|
||||
if (!defined $sender) {
|
||||
$sender = $repoauthor || $repocommitter || '';
|
||||
@ -447,17 +607,6 @@ sub expand_aliases {
|
||||
@initial_cc = expand_aliases(@initial_cc);
|
||||
@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) {
|
||||
while (1) {
|
||||
$_= $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) {
|
||||
# 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) {
|
||||
$_ = $term->readline("Send this email? (y|n) ");
|
||||
last if defined $_;
|
||||
@ -548,7 +644,9 @@ EOT
|
||||
exit(0);
|
||||
}
|
||||
|
||||
@files = ($compose_filename . ".final", @files);
|
||||
if ($compose > 0) {
|
||||
@files = ($compose_filename . ".final", @files);
|
||||
}
|
||||
}
|
||||
|
||||
# Variables we set as part of the loop over files
|
||||
|
@ -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
|
||||
'
|
||||
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user