Browse Source

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 17 years ago
parent
commit
496db64202
  1. 28
      Documentation/git-send-email.txt
  2. 244
      git-send-email.perl
  3. 8
      t/t9001-send-email.sh

28
Documentation/git-send-email.txt

@ -8,7 +8,7 @@ git-send-email - Send a collection of patches as emails @@ -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. @@ -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 @@ -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:: @@ -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
------

244
git-send-email.perl

@ -22,8 +22,12 @@ use Term::ReadLine; @@ -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; @@ -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>... @@ -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>... @@ -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; @@ -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 ($@) { @@ -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 = ( @@ -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, @@ -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, @@ -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,21 +397,50 @@ if (@alias_files and $aliasfiletype and defined $parse_alias{$aliasfiletype}) { @@ -368,21 +397,50 @@ 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) {
@ -403,6 +461,108 @@ if (@files) { @@ -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 { @@ -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) { @@ -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,8 +644,10 @@ EOT @@ -548,8 +644,10 @@ EOT
exit(0);
}

if ($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);

8
t/t9001-send-email.sh

@ -292,4 +292,12 @@ test_expect_success '--compose adds MIME for utf8 subject' ' @@ -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…
Cancel
Save