Code

Merge branch 'ph/send-email'
authorJunio C Hamano <gitster@pobox.com>
Fri, 28 Nov 2008 03:24:00 +0000 (19:24 -0800)
committerJunio C Hamano <gitster@pobox.com>
Fri, 28 Nov 2008 03:24:00 +0000 (19:24 -0800)
* 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.

Documentation/git-send-email.txt
git-send-email.perl
t/t9001-send-email.sh

index 82f505686e998d36b87bfe2c1a29031449fbdbc6..acf8bf41d6d68ff991f552b5010628cd26a0ac6d 100644 (file)
@@ -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
 ------
index 007e2c6ee12db18152884c5ac9aa26eff224e36e..7508f8ff242854462d0b5b505621dd840b551641 100755 (executable)
@@ -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
index 561ae7d0a6ddf9531a8770e54beb43a38fe2aab1..617e97d963dc4a9c794fbb148ac4022b2d4fff65 100755 (executable)
@@ -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