Merge branch 'ob/typofixes'
authorJunio C Hamano <gitster@pobox.com>
Thu, 1 Aug 2013 18:58:32 +0000 (11:58 -0700)
committerJunio C Hamano <gitster@pobox.com>
Thu, 1 Aug 2013 19:01:01 +0000 (12:01 -0700)
* ob/typofixes:
many small typofixes

51 files changed:
.mailmap
Documentation/RelNotes/1.8.4.txt
Documentation/git-clean.txt
Documentation/git-config.txt
Documentation/git-pack-refs.txt
Documentation/git-prune.txt
Documentation/git-tag.txt
Documentation/technical/protocol-capabilities.txt
GIT-VERSION-GEN
advice.c
builtin/cat-file.c
builtin/clean.c
builtin/commit.c
builtin/rm.c
cache.h
commit-slab.h
commit.h
compat/unsetenv.c
config.mak.uname
contrib/completion/git-completion.bash
contrib/contacts/git-contacts [new file with mode: 0755]
contrib/contacts/git-contacts.txt [new file with mode: 0644]
contrib/hg-to-git/hg-to-git.py
contrib/hooks/multimail/README [new file with mode: 0644]
contrib/hooks/multimail/README.Git [new file with mode: 0644]
contrib/hooks/multimail/README.migrate-from-post-receive-email [new file with mode: 0644]
contrib/hooks/multimail/git_multimail.py [new file with mode: 0755]
contrib/hooks/multimail/migrate-mailhook-config [new file with mode: 0755]
contrib/hooks/multimail/post-receive [new file with mode: 0755]
contrib/hooks/post-receive-email
contrib/subtree/Makefile
contrib/subtree/git-subtree.sh
contrib/subtree/t/t7900-subtree.sh
daemon.c
diff.c
environment.c
git-rebase.sh
line-log.c
refs.c
sha1_file.c
sha1_name.c
streaming.c
t/perf/perf-lib.sh
t/t3032-merge-recursive-options.sh
t/t4211-line-log.sh
t/t5560-http-backend-noserver.sh
t/t7301-clean-interactive.sh
t/t7600-merge.sh
t/test-lib.sh
templates/hooks--pre-commit.sample
tree-walk.c
index 57070b50d1b2c385ed82c4402c428bc296ba01a0..57551b05d6b61262d1618283f8a11b1ca1e8b852 100644 (file)
--- a/.mailmap
+++ b/.mailmap
@@ -26,6 +26,8 @@ Bryan Larsen <bryan@larsen.st> <bryanlarsen@yahoo.com>
 Cheng Renquan <crquan@gmail.com>
 Chris Shoemaker <c.shoemaker@cox.net>
 Chris Wright <chrisw@sous-sol.org> <chrisw@osdl.org>
+Cord Seele <cowose@gmail.com> <cowose@googlemail.com>
+Christian Stimming <stimming@tuhh.de> <chs@ckiste.goetheallee>
 Csaba Henk <csaba@gluster.com> <csaba@lowlife.hu>
 Dan Johnson <computerdruid@gmail.com>
 Dana L. How <danahow@gmail.com> <how@deathvalley.cswitch.com>
@@ -74,6 +76,7 @@ Johannes Schindelin <Johannes.Schindelin@gmx.de> <johannes.schindelin@gmx.de>
 Johannes Sixt <j6t@kdbg.org> <J.Sixt@eudaptics.com>
 Johannes Sixt <j6t@kdbg.org> <j.sixt@viscovery.net>
 Johannes Sixt <j6t@kdbg.org> <johannes.sixt@telecom.at>
+John 'Warthog9' Hawley <warthog9@kernel.org> <warthog9@eaglescrag.net>
 Jon Loeliger <jdl@jdl.com> <jdl@freescale.com>
 Jon Loeliger <jdl@jdl.com> <jdl@freescale.org>
 Jon Seymour <jon.seymour@gmail.com> <jon@blackcubes.dyndns.org>
@@ -181,6 +184,7 @@ Simon Hausmann <hausmann@kde.org> <shausman@trolltech.com>
 Stefan Naewe <stefan.naewe@gmail.com> <stefan.naewe@atlas-elektronik.com>
 Stefan Naewe <stefan.naewe@gmail.com> <stefan.naewe@googlemail.com>
 Stefan Sperling <stsp@elego.de> <stsp@stsp.name>
+Štěpán Němec <stepnem@gmail.com> <stepan.nemec@gmail.com>
 Stephen Boyd <bebarino@gmail.com> <sboyd@codeaurora.org>
 Steven Drake <sdrake@xnet.co.nz> <sdrake@ihug.co.nz>
 Steven Grimm <koreth@midwinter.com> <sgrimm@sgrimm-mbp.local>
index ad58ac6dbd75d68c7ff800a5af79407c61c918eb..b4f8737c5a494efeac5d8a901cb1bad46ba8c9f1 100644 (file)
@@ -48,6 +48,8 @@ Updates since v1.8.3
 
 Foreign interfaces, subsystems and ports.
 
+ * Cygwin port has been updated for more recent Cygwin 1.7.
+
  * "git rebase -i" now honors --strategy and -X options.
 
  * Git-gui has been updated to its 0.18.0 version.
@@ -83,6 +85,12 @@ Foreign interfaces, subsystems and ports.
 
 UI, Workflows & Features
 
+ * Sample "post-receive-email" hook script got an enhanced replacement
+   "multimail" (in contrib/).
+
+ * Also in contrib/ is a new "contacts" script that runs "git blame"
+   to find out the people who may be interested in a set of changes.
+
  * "git clean" command learned an interactive mode.
 
  * The "--head" option to "git show-ref" was only to add "HEAD" to the
@@ -182,7 +190,7 @@ UI, Workflows & Features
    directly uses the 40-hex string as an object name, even if a ref
    "refs/<some hierarchy>/<name>" exists.  This disambiguation order
    is unlikely to change, but we should warn about the ambiguity just
-   like we warn when more than one refs/ hierachies share the same
+   like we warn when more than one refs/ hierarchies share the same
    name.
 
  * "git rebase" learned "--[no-]autostash" option to save local
index 75fb54339397c1bd8a38bd2bcc62eb6469645986..89979228b12080606f5becc268792c6eae8f128e 100644 (file)
@@ -111,7 +111,7 @@ select by numbers::
    '>>' like this, you can make more than one selection, concatenated
    with whitespace or comma.  Also you can say ranges.  E.g. "2-5 7,9"
    to choose 2,3,4,5,7,9 from the list.  If the second number in a
-   range is omitted, all remaining patches are taken.  E.g. "7-" to
+   range is omitted, all remaining items are selected.  E.g. "7-" to
    choose 7,8,9 from the list.  You can say '*' to choose everything.
    Also when you are satisfied with the filtered result, press ENTER
    (empty) back to the main menu.
index 34b089464627fc66c5a734adc7699fcceaff7c4f..2dbe486eb16d8edde01ece856fa1e0ad75053042 100644 (file)
@@ -96,29 +96,31 @@ OPTIONS
        names are not.
 
 --global::
-       For writing options: write to global ~/.gitconfig file rather than
-       the repository .git/config, write to $XDG_CONFIG_HOME/git/config file
-       if this file exists and the ~/.gitconfig file doesn't.
+       For writing options: write to global `~/.gitconfig` file
+       rather than the repository `.git/config`, write to
+       `$XDG_CONFIG_HOME/git/config` file if this file exists and the
+       `~/.gitconfig` file doesn't.
 +
-For reading options: read only from global ~/.gitconfig and from
-$XDG_CONFIG_HOME/git/config rather than from all available files.
+For reading options: read only from global `~/.gitconfig` and from
+`$XDG_CONFIG_HOME/git/config` rather than from all available files.
 +
 See also <<FILES>>.
 
 --system::
-       For writing options: write to system-wide $(prefix)/etc/gitconfig
-       rather than the repository .git/config.
+       For writing options: write to system-wide
+       `$(prefix)/etc/gitconfig` rather than the repository
+       `.git/config`.
 +
-For reading options: read only from system-wide $(prefix)/etc/gitconfig
+For reading options: read only from system-wide `$(prefix)/etc/gitconfig`
 rather than from all available files.
 +
 See also <<FILES>>.
 
 --local::
-       For writing options: write to the repository .git/config file.
+       For writing options: write to the repository `.git/config` file.
        This is the default behavior.
 +
-For reading options: read only from the repository .git/config rather than
+For reading options: read only from the repository `.git/config` rather than
 from all available files.
 +
 See also <<FILES>>.
@@ -218,9 +220,9 @@ $(prefix)/etc/gitconfig::
 
 $XDG_CONFIG_HOME/git/config::
        Second user-specific configuration file. If $XDG_CONFIG_HOME is not set
-       or empty, $HOME/.config/git/config will be used. Any single-valued
+       or empty, `$HOME/.config/git/config` will be used. Any single-valued
        variable set in this file will be overwritten by whatever is in
-       ~/.gitconfig.  It is a good idea not to create this file if
+       `~/.gitconfig`.  It is a good idea not to create this file if
        you sometimes use older versions of Git, as support for this
        file was added fairly recently.
 
index f131677478e91f8ab6611a6e45dda56ff48a9e2a..154081f2de269975a09c1f5899dc7d5948c54ca7 100644 (file)
@@ -33,8 +33,8 @@ Subsequent updates to branches always create new files under
 `$GIT_DIR/refs` directory hierarchy.
 
 A recommended practice to deal with a repository with too many
-refs is to pack its refs with `--all --prune` once, and
-occasionally run `git pack-refs --prune`.  Tags are by
+refs is to pack its refs with `--all` once, and
+occasionally run `git pack-refs`.  Tags are by
 definition stationary and are not expected to change.  Branch
 heads will be packed with the initial `pack-refs --all`, but
 only the currently active branch heads will become unpacked,
index 80d01b05710e250a5f1d548fca3fba54d50e0f29..bf824108c116a34972d1dacf1e9fb77db097b2c5 100644 (file)
@@ -59,7 +59,7 @@ borrows from your repository via its
 `.git/objects/info/alternates`:
 
 ------------
-$ git prune $(cd ../another && $(git rev-parse --all))
+$ git prune $(cd ../another && git rev-parse --all)
 ------------
 
 Notes
index 22894cbee6794fd738cf45aefefff0679dcb940d..c418c44d40d1494d2f87d6ed45c229a56064f0b3 100644 (file)
@@ -42,6 +42,17 @@ committer identity for the current user is used to find the
 GnuPG key for signing.         The configuration variable `gpg.program`
 is used to specify custom GnuPG binary.
 
+Tag objects (created with `-a`, `s`, or `-u`) are called "annotated"
+tags; they contain a creation date, the tagger name and e-mail, a
+tagging message, and an optional GnuPG signature. Whereas a
+"lightweight" tag is simply a name for an object (usually a commit
+object).
+
+Annotated tags are meant for release while lightweight tags are meant
+for private or temporary object labels. For this reason, some git
+commands for naming objects (like `git describe`) will ignore
+lightweight tags by default.
+
 
 OPTIONS
 -------
index b15517fa06bd782fb757e6b0836f9bceea8b7c05..fd8ffa5df38c8dd1a682ab77327654a4c2b80cd4 100644 (file)
@@ -18,11 +18,12 @@ was sent.  Server MUST NOT ignore capabilities that client requested
 and server advertised.  As a consequence of these rules, server MUST
 NOT advertise capabilities it does not understand.
 
-The 'report-status' and 'delete-refs' capabilities are sent and
+The 'report-status', 'delete-refs', and 'quiet' capabilities are sent and
 recognized by the receive-pack (push to server) process.
 
-The 'ofs-delta' capability is sent and recognized by both upload-pack
-and receive-pack protocols.
+The 'ofs-delta' and 'side-band-64k' capabilities are sent and recognized
+by both upload-pack and receive-pack protocols.  The 'agent' capability
+may optionally be sent in both protocols.
 
 All other capabilities are only recognized by the upload-pack (fetch
 from server) process.
@@ -123,6 +124,20 @@ Server can send, and client understand PACKv2 with delta referring to
 its base by position in pack rather than by an obj-id.  That is, they can
 send/read OBJ_OFS_DELTA (aka type 6) in a packfile.
 
+agent
+-----
+
+The server may optionally send a capability of the form `agent=X` to
+notify the client that the server is running version `X`. The client may
+optionally return its own agent string by responding with an `agent=Y`
+capability (but it MUST NOT do so if the server did not mention the
+agent capability). The `X` and `Y` strings may contain any printable
+ASCII characters except space (i.e., the byte range 32 < x < 127), and
+are typically of the form "package/version" (e.g., "git/1.8.3.1"). The
+agent strings are purely informative for statistics and debugging
+purposes, and MUST NOT be used to programatically assume the presence
+or absence of particular features.
+
 shallow
 -------
 
@@ -168,7 +183,7 @@ of whether or not there are tags available.
 report-status
 -------------
 
-The upload-pack process can receive a 'report-status' capability,
+The receive-pack process can receive a 'report-status' capability,
 which tells it that the client wants a report of what happened after
 a packfile upload and reference update.  If the pushing client requests
 this capability, after unpacking and updating references the server
@@ -185,3 +200,20 @@ it is capable of accepting a zero-id value as the target
 value of a reference update.  It is not sent back by the client, it
 simply informs the client that it can be sent zero-id values
 to delete references.
+
+quiet
+-----
+
+If the receive-pack server advertises the 'quiet' capability, it is
+capable of silencing human-readable progress output which otherwise may
+be shown when processing the received pack. A send-pack client should
+respond with the 'quiet' capability to suppress server-side progress
+reporting if the local progress reporting is also being suppressed
+(e.g., via `push -q`, or if stderr does not go to a tty).
+
+allow-tip-sha1-in-want
+----------------------
+
+If the upload-pack server advertises this capability, fetch-pack may
+send "want" lines with SHA-1s that exist at the server but are not
+advertised by upload-pack.
index b4d4e5045fc78173c77de6de3fb9330de49a9628..2c109111e3e2a3a76126f25ad3eadec275682c4d 100755 (executable)
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 GVF=GIT-VERSION-FILE
-DEF_VER=v1.8.3.GIT
+DEF_VER=v1.8.4-rc0
 
 LF='
 '
index 2a52098a29dd1c2192edb6fff8ca6979383e77c6..3eca9f5ffdd6e3596584ed0aa79e2634683bd496 100644 (file)
--- a/advice.c
+++ b/advice.c
@@ -35,7 +35,7 @@ static struct {
        { "implicitidentity", &advice_implicit_identity },
        { "detachedhead", &advice_detached_head },
        { "setupstreamfailure", &advice_set_upstream_failure },
-       { "object_name_warning", &advice_object_name_warning },
+       { "objectnamewarning", &advice_object_name_warning },
        { "rmhints", &advice_rm_hints },
 
        /* make this an alias for backward compatibility */
index 0e64b4159c4f8a81ed1d346631f6c3894297ceeb..163ce6c77cea19b8c209e52ea2694f98d6cb0d05 100644 (file)
@@ -150,7 +150,9 @@ static void expand_atom(struct strbuf *sb, const char *atom, int len,
                if (!data->mark_query)
                        strbuf_addstr(sb, sha1_to_hex(data->sha1));
        } else if (is_atom("objecttype", atom, len)) {
-               if (!data->mark_query)
+               if (data->mark_query)
+                       data->info.typep = &data->type;
+               else
                        strbuf_addstr(sb, typename(data->type));
        } else if (is_atom("objectsize", atom, len)) {
                if (data->mark_query)
@@ -229,8 +231,7 @@ static int batch_one_object(const char *obj_name, struct batch_options *opt,
                return 0;
        }
 
-       data->type = sha1_object_info_extended(data->sha1, &data->info);
-       if (data->type <= 0) {
+       if (sha1_object_info_extended(data->sha1, &data->info) < 0) {
                printf("%s missing\n", obj_name);
                fflush(stdout);
                return 0;
@@ -266,6 +267,15 @@ static int batch_objects(struct batch_options *opt)
        strbuf_expand(&buf, opt->format, expand_format, &data);
        data.mark_query = 0;
 
+       /*
+        * We are going to call get_sha1 on a potentially very large number of
+        * objects. In most large cases, these will be actual object sha1s. The
+        * cost to double-check that each one is not also a ref (just so we can
+        * warn) ends up dwarfing the actual cost of the object lookups
+        * themselves. We can work around it by just turning off the warning.
+        */
+       warn_on_object_refname_ambiguity = 0;
+
        while (strbuf_getline(&buf, stdin, '\n') != EOF) {
                char *p;
                int error;
index dba8387747b0fa5129b9270ce5140ba847a7a139..3c85e152e140741855590f422c599407b6d6ae55 100644 (file)
@@ -365,6 +365,56 @@ static void print_highlight_menu_stuff(struct menu_stuff *stuff, int **chosen)
        string_list_clear(&menu_list, 0);
 }
 
+static int find_unique(const char *choice, struct menu_stuff *menu_stuff)
+{
+       struct menu_item *menu_item;
+       struct string_list_item *string_list_item;
+       int i, len, found = 0;
+
+       len = strlen(choice);
+       switch (menu_stuff->type) {
+       default:
+               die("Bad type of menu_stuff when parse choice");
+       case MENU_STUFF_TYPE_MENU_ITEM:
+
+               menu_item = (struct menu_item *)menu_stuff->stuff;
+               for (i = 0; i < menu_stuff->nr; i++, menu_item++) {
+                       if (len == 1 && *choice == menu_item->hotkey) {
+                               found = i + 1;
+                               break;
+                       }
+                       if (!strncasecmp(choice, menu_item->title, len)) {
+                               if (found) {
+                                       if (len == 1) {
+                                               /* continue for hotkey matching */
+                                               found = -1;
+                                       } else {
+                                               found = 0;
+                                               break;
+                                       }
+                               } else {
+                                       found = i + 1;
+                               }
+                       }
+               }
+               break;
+       case MENU_STUFF_TYPE_STRING_LIST:
+               string_list_item = ((struct string_list *)menu_stuff->stuff)->items;
+               for (i = 0; i < menu_stuff->nr; i++, string_list_item++) {
+                       if (!strncasecmp(choice, string_list_item->string, len)) {
+                               if (found) {
+                                       found = 0;
+                                       break;
+                               }
+                               found = i + 1;
+                       }
+               }
+               break;
+       }
+       return found;
+}
+
+
 /*
  * Parse user input, and return choice(s) for menu (menu_stuff).
  *
@@ -392,8 +442,6 @@ static int parse_choice(struct menu_stuff *menu_stuff,
                        int **chosen)
 {
        struct strbuf **choice_list, **ptr;
-       struct menu_item *menu_item;
-       struct string_list_item *string_list_item;
        int nr = 0;
        int i;
 
@@ -457,32 +505,8 @@ static int parse_choice(struct menu_stuff *menu_stuff,
                        bottom = 1;
                        top = menu_stuff->nr;
                } else {
-                       switch (menu_stuff->type) {
-                       default:
-                               die("Bad type of menu_stuff when parse choice");
-                       case MENU_STUFF_TYPE_MENU_ITEM:
-                               menu_item = (struct menu_item *)menu_stuff->stuff;
-                               for (i = 0; i < menu_stuff->nr; i++, menu_item++) {
-                                       if (((*ptr)->len == 1 &&
-                                            *(*ptr)->buf == menu_item->hotkey) ||
-                                           !strcasecmp((*ptr)->buf, menu_item->title)) {
-                                               bottom = i + 1;
-                                               top = bottom;
-                                               break;
-                                       }
-                               }
-                               break;
-                       case MENU_STUFF_TYPE_STRING_LIST:
-                               string_list_item = ((struct string_list *)menu_stuff->stuff)->items;
-                               for (i = 0; i < menu_stuff->nr; i++, string_list_item++) {
-                                       if (!strcasecmp((*ptr)->buf, string_list_item->string)) {
-                                               bottom = i + 1;
-                                               top = bottom;
-                                               break;
-                                       }
-                               }
-                               break;
-                       }
+                       bottom = find_unique((*ptr)->buf, menu_stuff);
+                       top = bottom;
                }
 
                if (top <= 0 || bottom <= 0 || top > menu_stuff->nr || bottom > top ||
index 65cf2a79b71021cc456634a52bc8ba6f73260bc1..10acc53f8012f53b6a15a3d006b622bda3409b17 100644 (file)
@@ -63,8 +63,18 @@ N_("The previous cherry-pick is now empty, possibly due to conflict resolution.\
 "If you wish to commit it anyway, use:\n"
 "\n"
 "    git commit --allow-empty\n"
+"\n");
+
+static const char empty_cherry_pick_advice_single[] =
+N_("Otherwise, please use 'git reset'\n");
+
+static const char empty_cherry_pick_advice_multi[] =
+N_("If you wish to skip this commit, use:\n"
 "\n"
-"Otherwise, please use 'git reset'\n");
+"    git reset\n"
+"\n"
+"Then \"git cherry-pick --continue\" will resume cherry-picking\n"
+"the remaining commits.\n");
 
 static const char *use_message_buffer;
 static const char commit_editmsg[] = "COMMIT_EDITMSG";
@@ -107,6 +117,7 @@ static enum {
 static const char *cleanup_arg;
 
 static enum commit_whence whence;
+static int sequencer_in_use;
 static int use_editor = 1, include_status = 1;
 static int show_ignored_in_status, have_option_m;
 static const char *only_include_assumed;
@@ -141,8 +152,11 @@ static void determine_whence(struct wt_status *s)
 {
        if (file_exists(git_path("MERGE_HEAD")))
                whence = FROM_MERGE;
-       else if (file_exists(git_path("CHERRY_PICK_HEAD")))
+       else if (file_exists(git_path("CHERRY_PICK_HEAD"))) {
                whence = FROM_CHERRY_PICK;
+               if (file_exists(git_path("sequencer")))
+                       sequencer_in_use = 1;
+       }
        else
                whence = FROM_COMMIT;
        if (s)
@@ -534,7 +548,6 @@ static void determine_author_info(struct strbuf *author_ident)
                                        (lb - strlen(" ") -
                                         (a + strlen("\nauthor "))));
                email = xmemdupz(lb + strlen("<"), rb - (lb + strlen("<")));
-               date = xmemdupz(rb + strlen("> "), eol - (rb + strlen("> ")));
                len = eol - (rb + strlen("> "));
                date = xmalloc(len + 2);
                *date = '@';
@@ -811,8 +824,13 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
                run_status(stdout, index_file, prefix, 0, s);
                if (amend)
                        fputs(_(empty_amend_advice), stderr);
-               else if (whence == FROM_CHERRY_PICK)
+               else if (whence == FROM_CHERRY_PICK) {
                        fputs(_(empty_cherry_pick_advice), stderr);
+                       if (!sequencer_in_use)
+                               fputs(_(empty_cherry_pick_advice_single), stderr);
+                       else
+                               fputs(_(empty_cherry_pick_advice_multi), stderr);
+               }
                return 0;
        }
 
index 18916e022a2189a477927f0f351f63dd3b6fe695..0df0b4d942c09769ff42539f6aaf83dcc31e20fb 100644 (file)
@@ -58,6 +58,21 @@ static void print_error_files(struct string_list *files_list,
        }
 }
 
+static void error_removing_concrete_submodules(struct string_list *files, int *errs)
+{
+       print_error_files(files,
+                         Q_("the following submodule (or one of its nested "
+                            "submodules)\n"
+                            "uses a .git directory:",
+                            "the following submodules (or one of its nested "
+                            "submodules)\n"
+                            "use a .git directory:", files->nr),
+                         _("\n(use 'rm -rf' if you really want to remove "
+                           "it including all of its history)"),
+                         errs);
+       string_list_clear(files, 0);
+}
+
 static int check_submodules_use_gitfiles(void)
 {
        int i;
@@ -86,16 +101,8 @@ static int check_submodules_use_gitfiles(void)
                if (!submodule_uses_gitfile(name))
                        string_list_append(&files, name);
        }
-       print_error_files(&files,
-                         Q_("the following submodule (or one of its nested "
-                            "submodules)\n uses a .git directory:",
-                            "the following submodules (or one of its nested "
-                            "submodules)\n use a .git directory:",
-                            files.nr),
-                         _("\n(use 'rm -rf' if you really want to remove "
-                           "it including all of its history)"),
-                         &errs);
-       string_list_clear(&files, 0);
+
+       error_removing_concrete_submodules(&files, &errs);
 
        return errs;
 }
@@ -237,17 +244,9 @@ static int check_local_mod(unsigned char *head, int index_only)
                            " or -f to force removal)"),
                          &errs);
        string_list_clear(&files_cached, 0);
-       print_error_files(&files_submodule,
-                         Q_("the following submodule (or one of its nested "
-                            "submodule)\nuses a .git directory:",
-                            "the following submodules (or one of its nested "
-                            "submodule)\nuse a .git directory:",
-                            files_submodule.nr),
-                         _("\n(use 'rm -rf' if you really "
-                           "want to remove it including all "
-                           "of its history)"),
-                         &errs);
-       string_list_clear(&files_submodule, 0);
+
+       error_removing_concrete_submodules(&files_submodule, &errs);
+
        print_error_files(&files_local,
                          Q_("the following file has local modifications:",
                             "the following files have local modifications:",
diff --git a/cache.h b/cache.h
index 561b728676f8d63cbd78a15eaa4b2634704215d9..85b544f38d934fe68d1e155c86337f1400eea14d 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -577,6 +577,7 @@ extern int assume_unchanged;
 extern int prefer_symlink_refs;
 extern int log_all_ref_updates;
 extern int warn_ambiguous_refs;
+extern int warn_on_object_refname_ambiguity;
 extern int shared_repository;
 extern const char *apply_default_whitespace;
 extern const char *apply_default_ignorewhitespace;
@@ -1131,6 +1132,7 @@ extern int unpack_object_header(struct packed_git *, struct pack_window **, off_
 
 struct object_info {
        /* Request */
+       enum object_type *typep;
        unsigned long *sizep;
        unsigned long *disk_sizep;
 
index 7d481638af6878e3bdabfa145401c3b181758818..d4c8286470c1adef499ca30e9f57c7fe4b2be2da 100644 (file)
@@ -48,7 +48,7 @@ static void init_ ##slabname## _with_stride(struct slabname *s,               \
        if (!stride)                                                    \
                stride = 1;                                             \
        s->stride = stride;                                             \
-       elem_size = sizeof(struct slabname) * stride;                   \
+       elem_size = sizeof(elemtype) * stride;                          \
        s->slab_size = COMMIT_SLAB_SIZE / elem_size;                    \
        s->slab_count = 0;                                              \
        s->slab = NULL;                                                 \
@@ -72,11 +72,10 @@ static void clear_ ##slabname(struct slabname *s)                   \
 static elemtype *slabname## _at(struct slabname *s,                    \
                                const struct commit *c)                 \
 {                                                                      \
-       int nth_slab, nth_slot, ix;                                     \
+       int nth_slab, nth_slot;                                         \
                                                                        \
-       ix = c->index * s->stride;                                      \
-       nth_slab = ix / s->slab_size;                                   \
-       nth_slot = ix % s->slab_size;                                   \
+       nth_slab = c->index / s->slab_size;                             \
+       nth_slot = c->index % s->slab_size;                             \
                                                                        \
        if (s->slab_count <= nth_slab) {                                \
                int i;                                                  \
@@ -89,8 +88,8 @@ static elemtype *slabname## _at(struct slabname *s,                   \
        }                                                               \
        if (!s->slab[nth_slab])                                         \
                s->slab[nth_slab] = xcalloc(s->slab_size,               \
-                                           sizeof(**s->slab));         \
-       return &s->slab[nth_slab][nth_slot];                            \
+                                           sizeof(**s->slab) * s->stride);             \
+       return &s->slab[nth_slab][nth_slot * s->stride];                                \
 }                                                                      \
                                                                        \
 static int stat_ ##slabname## realloc
index 35cc4e266bd4afe0e00d2efb2781db2e95906936..d912a9d4ac3fe36b2e69d48b1725f01bdf289b26 100644 (file)
--- a/commit.h
+++ b/commit.h
@@ -102,8 +102,6 @@ struct pretty_print_context {
         * Fields below here are manipulated internally by pp_* functions and
         * should not be counted on by callers.
         */
-
-       /* Manipulated by the pp_* functions internally. */
        struct string_list in_body_headers;
 };
 
index 4ea18569c240900b0e889873ddc782614c7724a6..bf5fd7063bc98964382b193cc9171a734c7aa790 100644 (file)
@@ -2,6 +2,9 @@
 
 void gitunsetenv (const char *name)
 {
+#if !defined(__MINGW32__)
+     extern char **environ;
+#endif
      int src, dst;
      size_t nmln;
 
index 7ac541e9ebbe77b339846d8efafdd0b6f33fee95..b45b91075966f450e221bb373ab79a311093284d 100644 (file)
@@ -159,16 +159,17 @@ ifeq ($(uname_O),Cygwin)
                NO_SYMLINK_HEAD = YesPlease
                NO_IPV6 = YesPlease
                OLD_ICONV = UnfortunatelyYes
+               NO_THREAD_SAFE_PREAD = YesPlease
+               # There are conflicting reports about this.
+               # On some boxes NO_MMAP is needed, and not so elsewhere.
+               # Try commenting this out if you suspect MMAP is more efficient
+               NO_MMAP = YesPlease
+       else
+               NO_REGEX = UnfortunatelyYes
        endif
-       NO_THREAD_SAFE_PREAD = YesPlease
        NEEDS_LIBICONV = YesPlease
        NO_FAST_WORKING_DIRECTORY = UnfortunatelyYes
-       NO_TRUSTABLE_FILEMODE = UnfortunatelyYes
        NO_ST_BLOCKS_IN_STRUCT_STAT = YesPlease
-       # There are conflicting reports about this.
-       # On some boxes NO_MMAP is needed, and not so elsewhere.
-       # Try commenting this out if you suspect MMAP is more efficient
-       NO_MMAP = YesPlease
        X = .exe
        COMPAT_OBJS += compat/cygwin.o
        UNRELIABLE_FSTAT = UnfortunatelyYes
index cd509a5d63922e05d5c2f5239a8acdd7baab3047..32d1b45c7e9bbfc721581f97ae4ca88bc2e0b6e9 100644 (file)
@@ -2581,7 +2581,7 @@ if [[ -n ${ZSH_VERSION-} ]]; then
                                --*=*|*.) ;;
                                *) c="$c " ;;
                                esac
-                               array+=("$c")
+                               array[$#array+1]="$c"
                        done
                        compset -P '*[=:]'
                        compadd -Q -S '' -p "${2-}" -a -- array && _ret=0
diff --git a/contrib/contacts/git-contacts b/contrib/contacts/git-contacts
new file mode 100755 (executable)
index 0000000..d80f7d1
--- /dev/null
@@ -0,0 +1,188 @@
+#!/usr/bin/perl
+
+# List people who might be interested in a patch.  Useful as the argument to
+# git-send-email --cc-cmd option, and in other situations.
+#
+# Usage: git contacts <file | rev-list option> ...
+
+use strict;
+use warnings;
+use IPC::Open2;
+
+my $since = '5-years-ago';
+my $min_percent = 10;
+my $labels_rx = qr/Signed-off-by|Reviewed-by|Acked-by|Cc/i;
+my %seen;
+
+sub format_contact {
+       my ($name, $email) = @_;
+       return "$name <$email>";
+}
+
+sub parse_commit {
+       my ($commit, $data) = @_;
+       my $contacts = $commit->{contacts};
+       my $inbody = 0;
+       for (split(/^/m, $data)) {
+               if (not $inbody) {
+                       if (/^author ([^<>]+) <(\S+)> .+$/) {
+                               $contacts->{format_contact($1, $2)} = 1;
+                       } elsif (/^$/) {
+                               $inbody = 1;
+                       }
+               } elsif (/^$labels_rx:\s+([^<>]+)\s+<(\S+?)>$/o) {
+                       $contacts->{format_contact($1, $2)} = 1;
+               }
+       }
+}
+
+sub import_commits {
+       my ($commits) = @_;
+       return unless %$commits;
+       my $pid = open2 my $reader, my $writer, qw(git cat-file --batch);
+       for my $id (keys(%$commits)) {
+               print $writer "$id\n";
+               my $line = <$reader>;
+               if ($line =~ /^([0-9a-f]{40}) commit (\d+)/) {
+                       my ($cid, $len) = ($1, $2);
+                       die "expected $id but got $cid\n" unless $id eq $cid;
+                       my $data;
+                       # cat-file emits newline after data, so read len+1
+                       read $reader, $data, $len + 1;
+                       parse_commit($commits->{$id}, $data);
+               }
+       }
+       close $reader;
+       close $writer;
+       waitpid($pid, 0);
+       die "git-cat-file error: $?\n" if $?;
+}
+
+sub get_blame {
+       my ($commits, $source, $start, $len, $from) = @_;
+       $len = 1 unless defined($len);
+       return if $len == 0;
+       open my $f, '-|',
+               qw(git blame --porcelain -C), '-L', "$start,+$len",
+               '--since', $since, "$from^", '--', $source or die;
+       while (<$f>) {
+               if (/^([0-9a-f]{40}) \d+ \d+ \d+$/) {
+                       my $id = $1;
+                       $commits->{$id} = { id => $id, contacts => {} }
+                               unless $seen{$id};
+                       $seen{$id} = 1;
+               }
+       }
+       close $f;
+}
+
+sub scan_patches {
+       my ($commits, $id, $f) = @_;
+       my $source;
+       while (<$f>) {
+               if (/^From ([0-9a-f]{40}) Mon Sep 17 00:00:00 2001$/) {
+                       $id = $1;
+                       $seen{$id} = 1;
+               }
+               next unless $id;
+               if (m{^--- (?:a/(.+)|/dev/null)$}) {
+                       $source = $1;
+               } elsif (/^--- /) {
+                       die "Cannot parse hunk source: $_\n";
+               } elsif (/^@@ -(\d+)(?:,(\d+))?/ && $source) {
+                       get_blame($commits, $source, $1, $2, $id);
+               }
+       }
+}
+
+sub scan_patch_file {
+       my ($commits, $file) = @_;
+       open my $f, '<', $file or die "read failure: $file: $!\n";
+       scan_patches($commits, undef, $f);
+       close $f;
+}
+
+sub parse_rev_args {
+       my @args = @_;
+       open my $f, '-|',
+               qw(git rev-parse --revs-only --default HEAD --symbolic), @args
+               or die;
+       my @revs;
+       while (<$f>) {
+               chomp;
+               push @revs, $_;
+       }
+       close $f;
+       return @revs if scalar(@revs) != 1;
+       return "^$revs[0]", 'HEAD' unless $revs[0] =~ /^-/;
+       return $revs[0], 'HEAD';
+}
+
+sub scan_rev_args {
+       my ($commits, $args) = @_;
+       my @revs = parse_rev_args(@$args);
+       open my $f, '-|', qw(git rev-list --reverse), @revs or die;
+       while (<$f>) {
+               chomp;
+               my $id = $_;
+               $seen{$id} = 1;
+               open my $g, '-|', qw(git show -C --oneline), $id or die;
+               scan_patches($commits, $id, $g);
+               close $g;
+       }
+       close $f;
+}
+
+sub mailmap_contacts {
+       my ($contacts) = @_;
+       my %mapped;
+       my $pid = open2 my $reader, my $writer, qw(git check-mailmap --stdin);
+       for my $contact (keys(%$contacts)) {
+               print $writer "$contact\n";
+               my $canonical = <$reader>;
+               chomp $canonical;
+               $mapped{$canonical} += $contacts->{$contact};
+       }
+       close $reader;
+       close $writer;
+       waitpid($pid, 0);
+       die "git-check-mailmap error: $?\n" if $?;
+       return \%mapped;
+}
+
+if (!@ARGV) {
+       die "No input revisions or patch files\n";
+}
+
+my (@files, @rev_args);
+for (@ARGV) {
+       if (-e) {
+               push @files, $_;
+       } else {
+               push @rev_args, $_;
+       }
+}
+
+my %commits;
+for (@files) {
+       scan_patch_file(\%commits, $_);
+}
+if (@rev_args) {
+       scan_rev_args(\%commits, \@rev_args)
+}
+import_commits(\%commits);
+
+my $contacts = {};
+for my $commit (values %commits) {
+       for my $contact (keys %{$commit->{contacts}}) {
+               $contacts->{$contact}++;
+       }
+}
+$contacts = mailmap_contacts($contacts);
+
+my $ncommits = scalar(keys %commits);
+for my $contact (keys %$contacts) {
+       my $percent = $contacts->{$contact} * 100 / $ncommits;
+       next if $percent < $min_percent;
+       print "$contact\n";
+}
diff --git a/contrib/contacts/git-contacts.txt b/contrib/contacts/git-contacts.txt
new file mode 100644 (file)
index 0000000..dd914d1
--- /dev/null
@@ -0,0 +1,94 @@
+git-contacts(1)
+===============
+
+NAME
+----
+git-contacts - List people who might be interested in a set of changes
+
+
+SYNOPSIS
+--------
+[verse]
+'git contacts' (<patch>|<range>|<rev>)...
+
+
+DESCRIPTION
+-----------
+
+Given a set of changes, specified as patch files or revisions, determine people
+who might be interested in those changes.  This is done by consulting the
+history of each patch or revision hunk to find people mentioned by commits
+which touched the lines of files under consideration.
+
+Input consists of one or more patch files or revision arguments.  A revision
+argument can be a range or a single `<rev>` which is interpreted as
+`<rev>..HEAD`, thus the same revision arguments are accepted as for
+linkgit:git-format-patch[1]. Patch files and revision arguments can be combined
+in the same invocation.
+
+This command can be useful for determining the list of people with whom to
+discuss proposed changes, or for finding the list of recipients to Cc: when
+submitting a patch series via `git send-email`. For the latter case, `git
+contacts` can be used as the argument to `git send-email`'s `--cc-cmd` option.
+
+
+DISCUSSION
+----------
+
+`git blame` is invoked for each hunk in a patch file or revision.  For each
+commit mentioned by `git blame`, the commit message is consulted for people who
+authored, reviewed, signed, acknowledged, or were Cc:'d.  Once the list of
+participants is known, each person's relevance is computed by considering how
+many commits mentioned that person compared with the total number of commits
+under consideration.  The final output consists only of participants who exceed
+a minimum threshold of participation.
+
+
+OUTPUT
+------
+
+For each person of interest, a single line is output, terminated by a newline.
+If the person's name is known, ``Name $$<user@host>$$'' is printed; otherwise
+only ``$$<user@host>$$'' is printed.
+
+
+EXAMPLES
+--------
+
+* Consult patch files:
++
+------------
+$ git contacts feature/*.patch
+------------
+
+* Revision range:
++
+------------
+$ git contacts R1..R2
+------------
+
+* From a single revision to `HEAD`:
++
+------------
+$ git contacts origin
+------------
+
+* Helper for `git send-email`:
++
+------------
+$ git send-email --cc-cmd='git contacts' feature/*.patch
+------------
+
+
+LIMITATIONS
+-----------
+
+Several conditions controlling a person's significance are currently
+hard-coded, such as minimum participation level (10%), blame date-limiting (5
+years), and `-C` level for detecting moved and copied lines (a single `-C`). In
+the future, these conditions may become configurable.
+
+
+GIT
+---
+Part of the linkgit:git[1] suite
index 232625a7b72aef88ef9cc0285eb7208ce38a2138..60dec86d37efbffb557e611cde4bf2b2dbfbf478 100755 (executable)
@@ -225,7 +225,7 @@ def getgitenv(user, date):
     os.system('git ls-files -x .hg --deleted | git update-index --remove --stdin')
 
     # commit
-    os.system(getgitenv(user, date) + 'git commit --allow-empty -a -F %s' % filecomment)
+    os.system(getgitenv(user, date) + 'git commit --allow-empty --allow-empty-message -a -F %s' % filecomment)
     os.unlink(filecomment)
 
     # tag
diff --git a/contrib/hooks/multimail/README b/contrib/hooks/multimail/README
new file mode 100644 (file)
index 0000000..9904396
--- /dev/null
@@ -0,0 +1,486 @@
+                          git-multimail
+                          =============
+
+git-multimail is a tool for sending notification emails on pushes to a
+Git repository.  It includes a Python module called git_multimail.py,
+which can either be used as a hook script directly or can be imported
+as a Python module into another script.
+
+git-multimail is derived from the Git project's old
+contrib/hooks/post-receive-email, and is mostly compatible with that
+script.  See README.migrate-from-post-receive-email for details about
+the differences and for how to migrate from post-receive-email to
+git-multimail.
+
+git-multimail, like the rest of the Git project, is licensed under
+GPLv2 (see the COPYING file for details).
+
+Please note: although, as a convenience, git-multimail may be
+distributed along with the main Git project, development of
+git-multimail takes place in its own, separate project.  See section
+"Getting involved" below for more information.
+
+
+By default, for each push received by the repository, git-multimail:
+
+1. Outputs one email summarizing each reference that was changed.
+   These "reference change" (called "refchange" below) emails describe
+   the nature of the change (e.g., was the reference created, deleted,
+   fast-forwarded, etc.) and include a one-line summary of each commit
+   that was added to the reference.
+
+2. Outputs one email for each new commit that was introduced by the
+   reference change.  These "commit" emails include a list of the
+   files changed by the commit, followed by the diffs of files
+   modified by the commit.  The commit emails are threaded to the
+   corresponding reference change email via "In-Reply-To".  This style
+   (similar to the "git format-patch" style used on the Git mailing
+   list) makes it easy to scan through the emails, jump to patches
+   that need further attention, and write comments about specific
+   commits.  Commits are handled in reverse topological order (i.e.,
+   parents shown before children).  For example,
+
+   [git] branch master updated
+   + [git] 01/08: doc: fix xref link from api docs to manual pages
+   + [git] 02/08: api-credentials.txt: show the big picture first
+   + [git] 03/08: api-credentials.txt: mention credential.helper explicitly
+   + [git] 04/08: api-credentials.txt: add "see also" section
+   + [git] 05/08: t3510 (cherry-pick-sequence): add missing '&&'
+   + [git] 06/08: Merge branch 'rr/maint-t3510-cascade-fix'
+   + [git] 07/08: Merge branch 'mm/api-credentials-doc'
+   + [git] 08/08: Git 1.7.11-rc2
+
+   Each commit appears in exactly one commit email, the first time
+   that it is pushed to the repository.  If a commit is later merged
+   into another branch, then a one-line summary of the commit is
+   included in the reference change email (as usual), but no
+   additional commit email is generated.
+
+   By default, reference change emails have their "Reply-To" field set
+   to the person who pushed the change, and commit emails have their
+   "Reply-To" field set to the author of the commit.
+
+3. Output one "announce" mail for each new annotated tag, including
+   information about the tag and optionally a shortlog describing the
+   changes since the previous tag.  Such emails might be useful if you
+   use annotated tags to mark releases of your project.
+
+
+Requirements
+------------
+
+* Python 2.x, version 2.4 or later.  No non-standard Python modules
+  are required.  git-multimail does *not* currently work with Python
+  3.x.
+
+  The example scripts invoke Python using the following shebang line
+  (following PEP 394 [1]):
+
+      #! /usr/bin/env python2
+
+  If your system's Python2 interpreter is not in your PATH or is not
+  called "python2", you can change the lines accordingly.  Or you can
+  invoke the Python interpreter explicitly, for example via a tiny
+  shell script like
+
+      #! /bin/sh
+      /usr/local/bin/python /path/to/git_multimail.py "$@"
+
+* The "git" command must be in your PATH.  git-multimail is known to
+  work with Git versions back to 1.7.1.  (Earlier versions have not
+  been tested; if you do so, please report your results.)
+
+* To send emails using the default configuration, a standard sendmail
+  program must be located at '/usr/sbin/sendmail' and configured
+  correctly to send emails.  If this is not the case, see the
+  multimailhook.mailer configuration variable below for how to
+  configure git-multimail to send emails via an SMTP server.
+
+
+Invocation
+----------
+
+git_multimail.py is designed to be used as a "post-receive" hook in a
+Git repository (see githooks(5)).  Link or copy it to
+$GIT_DIR/hooks/post-receive within the repository for which email
+notifications are desired.  Usually it should be installed on the
+central repository for a project, to which all commits are eventually
+pushed.
+
+For use on pre-v1.5.1 Git servers, git_multimail.py can also work as
+an "update" hook, taking its arguments on the command line.  To use
+this script in this manner, link or copy it to $GIT_DIR/hooks/update.
+Please note that the script is not completely reliable in this mode
+[2].
+
+Alternatively, git_multimail.py can be imported as a Python module
+into your own Python post-receive script.  This method is a bit more
+work, but allows the behavior of the hook to be customized using
+arbitrary Python code.  For example, you can use a custom environment
+(perhaps inheriting from GenericEnvironment or GitoliteEnvironment) to
+
+* change how the user who did the push is determined
+
+* read users' email addresses from an LDAP server or from a database
+
+* decide which users should be notified about which commits based on
+  the contents of the commits (e.g., for users who want to be notified
+  only about changes affecting particular files or subdirectories)
+
+Or you can change how emails are sent by writing your own Mailer
+class.  The "post-receive" script in this directory demonstrates how
+to use git_multimail.py as a Python module.  (If you make interesting
+changes of this type, please consider sharing them with the
+community.)
+
+
+Configuration
+-------------
+
+By default, git-multimail mostly takes its configuration from the
+following "git config" settings:
+
+multimailhook.environment
+
+    This describes the general environment of the repository.
+    Currently supported values:
+
+    "generic" -- the username of the pusher is read from $USER and the
+        repository name is derived from the repository's path.
+
+    "gitolite" -- the username of the pusher is read from $GL_USER and
+        the repository name from $GL_REPO.
+
+    If neither of these environments is suitable for your setup, then
+    you can implement a Python class that inherits from Environment
+    and instantiate it via a script that looks like the example
+    post-receive script.
+
+    The environment value can be specified on the command line using
+    the --environment option.  If it is not specified on the command
+    line or by multimailhook.environment, then it defaults to
+    "gitolite" if the environment contains variables $GL_USER and
+    $GL_REPO; otherwise "generic".
+
+multimailhook.repoName
+
+    A short name of this Git repository, to be used in various places
+    in the notification email text.  The default is to use $GL_REPO
+    for gitolite repositories, or otherwise to derive this value from
+    the repository path name.
+
+multimailhook.mailinglist
+
+    The list of email addresses to which notification emails should be
+    sent, as RFC 2822 email addresses separated by commas.  This
+    configuration option can be multivalued.  Leave it unset or set it
+    to the empty string to not send emails by default.  The next few
+    settings can be used to configure specific address lists for
+    specific types of notification email.
+
+multimailhook.refchangeList
+
+    The list of email addresses to which summary emails about
+    reference changes should be sent, as RFC 2822 email addresses
+    separated by commas.  This configuration option can be
+    multivalued.  The default is the value in
+    multimailhook.mailinglist.  Set this value to the empty string to
+    prevent reference change emails from being sent.
+
+multimailhook.announceList
+
+    The list of email addresses to which emails about new annotated
+    tags should be sent, as RFC 2822 email addresses separated by
+    commas.  This configuration option can be multivalued.  The
+    default is the value in multimailhook.refchangelist or
+    multimailhook.mailinglist.  Set this value to the empty string to
+    prevent annotated tag announcement emails from being sent.
+
+multimailhook.commitList
+
+    The list of email addresses to which emails about individual new
+    commits should be sent, as RFC 2822 email addresses separated by
+    commas.  This configuration option can be multivalued.  The
+    default is the value in multimailhook.mailinglist.  Set this value
+    to the empty string to prevent notification emails about
+    individual commits from being sent.
+
+multimailhook.announceShortlog
+
+    If this option is set to true, then emails about changes to
+    annotated tags include a shortlog of changes since the previous
+    tag.  This can be useful if the annotated tags represent releases;
+    then the shortlog will be a kind of rough summary of what has
+    happened since the last release.  But if your tagging policy is
+    not so straightforward, then the shortlog might be confusing
+    rather than useful.  Default is false.
+
+multimailhook.refchangeShowLog
+
+    If this option is set to true, then summary emails about reference
+    changes will include a detailed log of the added commits in
+    addition to the one line summary.  The log is generated by running
+    "git log" with the options specified in multimailhook.logOpts.
+    Default is false.
+
+multimailhook.mailer
+
+    This option changes the way emails are sent.  Accepted values are:
+
+    - sendmail (the default): use the command /usr/sbin/sendmail or
+      /usr/lib/sendmail (or sendmailCommand, if configured).  This
+      mode can be further customized via the following options:
+
+       multimailhook.sendmailCommand
+
+           The command used by mailer "sendmail" to send emails.  Shell
+           quoting is allowed in the value of this setting, but remember that
+           Git requires double-quotes to be escaped; e.g.,
+
+             git config multimailhook.sendmailcommand '/usr/sbin/sendmail -t -F \"Git Repo\"'
+
+           Default is '/usr/sbin/sendmail -t' or '/usr/lib/sendmail
+           -t' (depending on which file is present and executable).
+
+       multimailhook.envelopeSender
+
+           If set then pass this value to sendmail via the -f option to set
+           the envelope sender address.
+
+    - smtp: use Python's smtplib.  This is useful when the sendmail
+      command is not available on the system.  This mode can be
+      further customized via the following options:
+
+       multimailhook.smtpServer
+
+           The name of the SMTP server to connect to.  The value can
+           also include a colon and a port number; e.g.,
+           "mail.example.com:25".  Default is 'localhost' using port
+           25.
+
+       multimailhook.envelopeSender
+
+           The sender address to be passed to the SMTP server.  If
+           unset, then the value of multimailhook.from is used.
+
+multimailhook.from
+
+    If set then use this value in the From: field of generated emails.
+    If unset, then use the repository's user configuration (user.name
+    and user.email).  If user.email is also unset, then use
+    multimailhook.envelopeSender.
+
+multimailhook.administrator
+
+    The name and/or email address of the administrator of the Git
+    repository; used in FOOTER_TEMPLATE.  Default is
+    multimailhook.envelopesender if it is set; otherwise a generic
+    string is used.
+
+multimailhook.emailPrefix
+
+    All emails have this string prepended to their subjects, to aid
+    email filtering (though filtering based on the X-Git-* email
+    headers is probably more robust).  Default is the short name of
+    the repository in square brackets; e.g., "[myrepo]".
+
+multimailhook.emailMaxLines
+
+    The maximum number of lines that should be included in the body of
+    a generated email.  If not specified, there is no limit.  Lines
+    beyond the limit are suppressed and counted, and a final line is
+    added indicating the number of suppressed lines.
+
+multimailhook.emailMaxLineLength
+
+    The maximum length of a line in the email body.  Lines longer than
+    this limit are truncated to this length with a trailing " [...]"
+    added to indicate the missing text.  The default is 500, because
+    (a) diffs with longer lines are probably from binary files, for
+    which a diff is useless, and (b) even if a text file has such long
+    lines, the diffs are probably unreadable anyway.  To disable line
+    truncation, set this option to 0.
+
+multimailhook.maxCommitEmails
+
+    The maximum number of commit emails to send for a given change.
+    When the number of patches is larger that this value, only the
+    summary refchange email is sent.  This can avoid accidental
+    mailbombing, for example on an initial push.  To disable commit
+    emails limit, set this option to 0.  The default is 500.
+
+multimailhook.emailStrictUTF8
+
+    If this boolean option is set to "true", then the main part of the
+    email body is forced to be valid UTF-8.  Any characters that are
+    not valid UTF-8 are converted to the Unicode replacement
+    character, U+FFFD.  The default is "true".
+
+multimailhook.diffOpts
+
+    Options passed to "git diff-tree" when generating the summary
+    information for ReferenceChange emails.  Default is "--stat
+    --summary --find-copies-harder".  Add -p to those options to
+    include a unified diff of changes in addition to the usual summary
+    output.  Shell quoting is allowed; see multimailhook.logOpts for
+    details.
+
+multimailhook.logOpts
+
+    Options passed to "git log" to generate additional info for
+    reference change emails (used only if refchangeShowLog is set).
+    For example, adding --graph will show the graph of revisions, -p
+    will show the complete diff, etc.  The default is empty.
+
+    Shell quoting is allowed; for example, a log format that contains
+    spaces can be specified using something like:
+
+      git config multimailhook.logopts '--pretty=format:"%h %aN <%aE>%n%s%n%n%b%n"'
+
+    If you want to set this by editing your configuration file
+    directly, remember that Git requires double-quotes to be escaped
+    (see git-config(1) for more information):
+
+      [multimailhook]
+              logopts = --pretty=format:\"%h %aN <%aE>%n%s%n%n%b%n\"
+
+multimailhook.emailDomain
+
+    Domain name appended to the username of the person doing the push
+    to convert it into an email address (via "%s@%s" % (username,
+    emaildomain)).  More complicated schemes can be implemented by
+    overriding Environment and overriding its get_pusher_email()
+    method.
+
+multimailhook.replyTo
+multimailhook.replyToCommit
+multimailhook.replyToRefchange
+
+    Addresses to use in the Reply-To: field for commit emails
+    (replyToCommit) and refchange emails (replyToRefchange).
+    multimailhook.replyTo is used as default when replyToCommit or
+    replyToRefchange is not set.  The value for these variables can be
+    either:
+
+    - An email address, which will be used directly.
+
+    - The value "pusher", in which case the pusher's address (if
+      available) will be used.  This is the default for refchange
+      emails.
+
+    - The value "author" (meaningful only for replyToCommit), in which
+      case the commit author's address will be used.  This is the
+      default for commit emails.
+
+    - The value "none", in which case the Reply-To: field will be
+      omitted.
+
+
+Email filtering aids
+--------------------
+
+All emails include extra headers to enable fine tuned filtering and
+give information for debugging.  All emails include the headers
+"X-Git-Repo", "X-Git-Refname", and "X-Git-Reftype".  ReferenceChange
+emails also include headers "X-Git-Oldrev" and "X-Git-Newrev";
+Revision emails also include header "X-Git-Rev".
+
+
+Customizing email contents
+--------------------------
+
+git-multimail mostly generates emails by expanding templates.  The
+templates can be customized.  To avoid the need to edit
+git_multimail.py directly, the preferred way to change the templates
+is to write a separate Python script that imports git_multimail.py as
+a module, then replaces the templates in place.  See the provided
+post-receive script for an example of how this is done.
+
+
+Customizing git-multimail for your environment
+----------------------------------------------
+
+git-multimail is mostly customized via an "environment" that describes
+the local environment in which Git is running.  Two types of
+environment are built in:
+
+* GenericEnvironment: a stand-alone Git repository.
+
+* GitoliteEnvironment: a Git repository that is managed by gitolite
+  [3].  For such repositories, the identity of the pusher is read from
+  environment variable $GL_USER, and the name of the repository is
+  read from $GL_REPO (if it is not overridden by
+  multimailhook.reponame).
+
+By default, git-multimail assumes GitoliteEnvironment if $GL_USER and
+$GL_REPO are set, and otherwise assumes GenericEnvironment.
+Alternatively, you can choose one of these two environments explicitly
+by setting a "multimailhook.environment" config setting (which can
+have the value "generic" or "gitolite") or by passing an --environment
+option to the script.
+
+If you need to customize the script in ways that are not supported by
+the existing environments, you can define your own environment class
+class using arbitrary Python code.  To do so, you need to import
+git_multimail.py as a Python module, as demonstrated by the example
+post-receive script.  Then implement your environment class; it should
+usually inherit from one of the existing Environment classes and
+possibly one or more of the EnvironmentMixin classes.  Then set the
+"environment" variable to an instance of your own environment class
+and pass it to run_as_post_receive_hook().
+
+The standard environment classes, GenericEnvironment and
+GitoliteEnvironment, are in fact themselves put together out of a
+number of mixin classes, each of which handles one aspect of the
+customization.  For the finest control over your configuration, you
+can specify exactly which mixin classes your own environment class
+should inherit from, and override individual methods (or even add your
+own mixin classes) to implement entirely new behaviors.  If you
+implement any mixins that might be useful to other people, please
+consider sharing them with the community!
+
+
+Getting involved
+----------------
+
+git-multimail is an open-source project, built by volunteers.  We
+would welcome your help!
+
+The current maintainer is Michael Haggerty <mhagger@alum.mit.edu>.
+
+General discussion of git-multimail takes place on the main Git
+mailing list,
+
+    git@vger.kernel.org
+
+Please CC emails regarding git-multimail to me so that I don't
+overlook them.
+
+The git-multimail project itself is currently hosted on GitHub:
+
+    https://github.com/mhagger/git-multimail
+
+We use the GitHub issue tracker to keep track of bugs and feature
+requests, and GitHub pull requests to exchange patches (though, if you
+prefer, you can send patches via the Git mailing list with cc to me).
+
+Please note that although a copy of git-multimail will probably be
+distributed in the "contrib" section of the main Git project,
+development takes place in the separate git-multimail repository on
+GitHub!  (Whenever enough changes to git-multimail have accumulated, a
+new code-drop of git-multimail will be submitted for inclusion in the
+Git project.)
+
+
+Footnotes
+---------
+
+[1] http://www.python.org/dev/peps/pep-0394/
+
+[2] Because of the way information is passed to update hooks, the
+    script's method of determining whether a commit has already been
+    seen does not work when it is used as an "update" script.  In
+    particular, no notification email will be generated for a new
+    commit that is added to multiple references in the same push.
+
+[3] https://github.com/sitaramc/gitolite
diff --git a/contrib/hooks/multimail/README.Git b/contrib/hooks/multimail/README.Git
new file mode 100644 (file)
index 0000000..9c2e66a
--- /dev/null
@@ -0,0 +1,15 @@
+This copy of git-multimail is distributed as part of the "contrib"
+section of the Git project as a convenience to Git users.
+git-multimail is developed as an independent project at the following
+website:
+
+    https://github.com/mhagger/git-multimail
+
+The version in this directory was obtained from the upstream project
+on 2013-07-14 and consists of the "git-multimail" subdirectory from
+revision
+
+    1a5cb09c698a74d15a715a86b09ead5f56bf4b06
+
+Please see the README file in this directory for information about how
+to report bugs or contribute to git-multimail.
diff --git a/contrib/hooks/multimail/README.migrate-from-post-receive-email b/contrib/hooks/multimail/README.migrate-from-post-receive-email
new file mode 100644 (file)
index 0000000..1e6a976
--- /dev/null
@@ -0,0 +1,145 @@
+git-multimail is close to, but not exactly, a plug-in replacement for
+the old Git project script contrib/hooks/post-receive-email.  This
+document describes the differences and explains how to configure
+git-multimail to get behavior closest to that of post-receive-email.
+
+If you are in a hurry
+=====================
+
+A script called migrate-mailhook-config is included with
+git-multimail.  If you run this script within a Git repository that is
+configured to use post-receive-email, it will convert the
+configuration settings into the approximate equivalent settings for
+git-multimail.  For more information, run
+
+    migrate-mailhook-config --help
+
+
+Configuration differences
+=========================
+
+* The names of the config options for git-multimail are in namespace
+  "multimailhook.*" instead of "hooks.*".  (Editorial comment:
+  post-receive-email should never have used such a generic top-level
+  namespace.)
+
+* In emails about new annotated tags, post-receive-email includes a
+  shortlog of all changes since the previous annotated tag.  To get
+  this behavior with git-multimail, you need to set
+  multimailhook.announceshortlog to true:
+
+      git config multimailhook.announceshortlog true
+
+* multimailhook.commitlist -- This is a new configuration variable.
+  Recipients listed here will receive a separate email for each new
+  commit.  However, if this variable is *not* set, it defaults to the
+  value of multimailhook.mailinglist.  Therefore, if you *don't* want
+  the members of multimailhook.mailinglist to receive one email per
+  commit, then set this value to the empty string:
+
+      git config multimailhook.commitlist ''
+
+* multimailhook.emailprefix -- If this value is not set, then the
+  subjects of generated emails are prefixed with the short name of the
+  repository enclosed in square brackets; e.g., "[myrepo]".
+  post-receive-email defaults to prefix "[SCM]" if this option is not
+  set.  So if you were using the old default and want to retain it
+  (for example, to avoid having to change your email filters), set
+  this variable explicitly to the old value:
+
+      git config multimailhook.emailprefix "[SCM]"
+
+* The "multimailhook.showrev" configuration option is not supported.
+  Its main use is obsoleted by the one-email-per-commit feature of
+  git-multimail.
+
+
+Other differences
+=================
+
+This section describes other differences in the behavior of
+git-multimail vs. post-receive-email.  For full details, please refer
+to the main README file:
+
+* One email per commit.  For each reference change, the script first
+  outputs one email summarizing the reference change (including
+  one-line summaries of the new commits), then it outputs a separate
+  email for each new commit that was introduced, including patches.
+  These one-email-per-commit emails go to the addresses listed in
+  multimailhook.commitlist.  post-receive-email sends only one email
+  for each *reference* that is changed, no matter how many commits
+  were added to the reference.
+
+* Better algorithm for detecting new commits.  post-receive-email
+  processes one reference change at a time, which causes it to fail to
+  describe new commits that were included in multiple branches.  For
+  example, if a single push adds the "*" commits in the diagram below,
+  then post-receive-email would never include the details of the two
+  commits that are common to "master" and "branch" in its
+  notifications.
+
+      o---o---o---*---*---*    <-- master
+                       \
+                        *---*  <-- branch
+
+  git-multimail analyzes all reference modifications to determine
+  which commits were not present before the change, therefore avoiding
+  that error.
+
+* In reference change emails, git-multimail tells which commits have
+  been added to the reference vs. are entirely new to the repository,
+  and which commits that have been omitted from the reference
+  vs. entirely discarded from the repository.
+
+* The environment in which Git is running can be configured via an
+  "Environment" abstraction.
+
+* Built-in support for Gitolite-managed repositories.
+
+* Instead of using full SHA1 object names in emails, git-multimail
+  mostly uses abbreviated SHA1s, plus one-line log message summaries
+  where appropriate.
+
+* In the schematic diagrams that explain non-fast-forward commits,
+  git-multimail shows the names of the branches involved.
+
+* The emails generated by git-multimail include the name of the Git
+  repository that was modified; this is convenient for recipients who
+  are monitoring multiple repositories.
+
+* git-multimail allows the email "From" addresses to be configured.
+
+* The recipients lists (multimailhook.mailinglist,
+  multimailhook.refchangelist, multimailhook.announcelist, and
+  multimailhook.commitlist) can be comma-separated values and/or
+  multivalued settings in the config file; e.g.,
+
+      [multimailhook]
+              mailinglist = mr.brown@example.com, mr.black@example.com
+              announcelist = Him <him@example.com>
+              announcelist = Jim <jim@example.com>
+              announcelist = pop@example.com
+
+  This might make it easier to maintain short recipients lists without
+  requiring full-fledged mailing list software.
+
+* By default, git-multimail sets email "Reply-To" headers to reply to
+  the pusher (for reference updates) and to the author (for commit
+  notifications).  By default, the pusher's email address is
+  constructed by appending "multimailhook.emaildomain" to the pusher's
+  username.
+
+* The generated emails contain a configurable footer.  By default, it
+  lists the name of the administrator who should be contacted to
+  unsubscribe from notification emails.
+
+* New option multimailhook.emailmaxlinelength to limit the length of
+  lines in the main part of the email body.  The default limit is 500
+  characters.
+
+* New option multimailhook.emailstrictutf8 to ensure that the main
+  part of the email body is valid UTF-8.  Invalid characters are
+  turned into the Unicode replacement character, U+FFFD.  By default
+  this option is turned on.
+
+* Written in Python.  Easier to add new features.
diff --git a/contrib/hooks/multimail/git_multimail.py b/contrib/hooks/multimail/git_multimail.py
new file mode 100755 (executable)
index 0000000..81c6a51
--- /dev/null
@@ -0,0 +1,2393 @@
+#! /usr/bin/env python2
+
+# Copyright (c) 2012,2013 Michael Haggerty
+# Derived from contrib/hooks/post-receive-email, which is
+# Copyright (c) 2007 Andy Parkins
+# and also includes contributions by other authors.
+#
+# This file is part of git-multimail.
+#
+# git-multimail is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version
+# 2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see
+# <http://www.gnu.org/licenses/>.
+
+"""Generate notification emails for pushes to a git repository.
+
+This hook sends emails describing changes introduced by pushes to a
+git repository.  For each reference that was changed, it emits one
+ReferenceChange email summarizing how the reference was changed,
+followed by one Revision email for each new commit that was introduced
+by the reference change.
+
+Each commit is announced in exactly one Revision email.  If the same
+commit is merged into another branch in the same or a later push, then
+the ReferenceChange email will list the commit's SHA1 and its one-line
+summary, but no new Revision email will be generated.
+
+This script is designed to be used as a "post-receive" hook in a git
+repository (see githooks(5)).  It can also be used as an "update"
+script, but this usage is not completely reliable and is deprecated.
+
+To help with debugging, this script accepts a --stdout option, which
+causes the emails to be written to standard output rather than sent
+using sendmail.
+
+See the accompanying README file for the complete documentation.
+
+"""
+
+import sys
+import os
+import re
+import bisect
+import subprocess
+import shlex
+import optparse
+import smtplib
+
+try:
+    from email.utils import make_msgid
+    from email.utils import getaddresses
+    from email.utils import formataddr
+    from email.header import Header
+except ImportError:
+    # Prior to Python 2.5, the email module used different names:
+    from email.Utils import make_msgid
+    from email.Utils import getaddresses
+    from email.Utils import formataddr
+    from email.Header import Header
+
+
+DEBUG = False
+
+ZEROS = '0' * 40
+LOGBEGIN = '- Log -----------------------------------------------------------------\n'
+LOGEND = '-----------------------------------------------------------------------\n'
+
+
+# It is assumed in many places that the encoding is uniformly UTF-8,
+# so changing these constants is unsupported.  But define them here
+# anyway, to make it easier to find (at least most of) the places
+# where the encoding is important.
+(ENCODING, CHARSET) = ('UTF-8', 'utf-8')
+
+
+REF_CREATED_SUBJECT_TEMPLATE = (
+    '%(emailprefix)s%(refname_type)s %(short_refname)s created'
+    ' (now %(newrev_short)s)'
+    )
+REF_UPDATED_SUBJECT_TEMPLATE = (
+    '%(emailprefix)s%(refname_type)s %(short_refname)s updated'
+    ' (%(oldrev_short)s -> %(newrev_short)s)'
+    )
+REF_DELETED_SUBJECT_TEMPLATE = (
+    '%(emailprefix)s%(refname_type)s %(short_refname)s deleted'
+    ' (was %(oldrev_short)s)'
+    )
+
+REFCHANGE_HEADER_TEMPLATE = """\
+To: %(recipients)s
+Subject: %(subject)s
+MIME-Version: 1.0
+Content-Type: text/plain; charset=%(charset)s
+Content-Transfer-Encoding: 8bit
+Message-ID: %(msgid)s
+From: %(fromaddr)s
+Reply-To: %(reply_to)s
+X-Git-Repo: %(repo_shortname)s
+X-Git-Refname: %(refname)s
+X-Git-Reftype: %(refname_type)s
+X-Git-Oldrev: %(oldrev)s
+X-Git-Newrev: %(newrev)s
+Auto-Submitted: auto-generated
+"""
+
+REFCHANGE_INTRO_TEMPLATE = """\
+This is an automated email from the git hooks/post-receive script.
+
+%(pusher)s pushed a change to %(refname_type)s %(short_refname)s
+in repository %(repo_shortname)s.
+
+"""
+
+
+FOOTER_TEMPLATE = """\
+
+-- \n\
+To stop receiving notification emails like this one, please contact
+%(administrator)s.
+"""
+
+
+REWIND_ONLY_TEMPLATE = """\
+This update removed existing revisions from the reference, leaving the
+reference pointing at a previous point in the repository history.
+
+ * -- * -- N   %(refname)s (%(newrev_short)s)
+            \\
+             O -- O -- O   (%(oldrev_short)s)
+
+Any revisions marked "omits" are not gone; other references still
+refer to them.  Any revisions marked "discards" are gone forever.
+"""
+
+
+NON_FF_TEMPLATE = """\
+This update added new revisions after undoing existing revisions.
+That is to say, some revisions that were in the old version of the
+%(refname_type)s are not in the new version.  This situation occurs
+when a user --force pushes a change and generates a repository
+containing something like this:
+
+ * -- * -- B -- O -- O -- O   (%(oldrev_short)s)
+            \\
+             N -- N -- N   %(refname)s (%(newrev_short)s)
+
+You should already have received notification emails for all of the O
+revisions, and so the following emails describe only the N revisions
+from the common base, B.
+
+Any revisions marked "omits" are not gone; other references still
+refer to them.  Any revisions marked "discards" are gone forever.
+"""
+
+
+NO_NEW_REVISIONS_TEMPLATE = """\
+No new revisions were added by this update.
+"""
+
+
+DISCARDED_REVISIONS_TEMPLATE = """\
+This change permanently discards the following revisions:
+"""
+
+
+NO_DISCARDED_REVISIONS_TEMPLATE = """\
+The revisions that were on this %(refname_type)s are still contained in
+other references; therefore, this change does not discard any commits
+from the repository.
+"""
+
+
+NEW_REVISIONS_TEMPLATE = """\
+The %(tot)s revisions listed above as "new" are entirely new to this
+repository and will be described in separate emails.  The revisions
+listed as "adds" were already present in the repository and have only
+been added to this reference.
+
+"""
+
+
+TAG_CREATED_TEMPLATE = """\
+        at  %(newrev_short)-9s (%(newrev_type)s)
+"""
+
+
+TAG_UPDATED_TEMPLATE = """\
+*** WARNING: tag %(short_refname)s was modified! ***
+
+      from  %(oldrev_short)-9s (%(oldrev_type)s)
+        to  %(newrev_short)-9s (%(newrev_type)s)
+"""
+
+
+TAG_DELETED_TEMPLATE = """\
+*** WARNING: tag %(short_refname)s was deleted! ***
+
+"""
+
+
+# The template used in summary tables.  It looks best if this uses the
+# same alignment as TAG_CREATED_TEMPLATE and TAG_UPDATED_TEMPLATE.
+BRIEF_SUMMARY_TEMPLATE = """\
+%(action)10s  %(rev_short)-9s %(text)s
+"""
+
+
+NON_COMMIT_UPDATE_TEMPLATE = """\
+This is an unusual reference change because the reference did not
+refer to a commit either before or after the change.  We do not know
+how to provide full information about this reference change.
+"""
+
+
+REVISION_HEADER_TEMPLATE = """\
+To: %(recipients)s
+Subject: %(emailprefix)s%(num)02d/%(tot)02d: %(oneline)s
+MIME-Version: 1.0
+Content-Type: text/plain; charset=%(charset)s
+Content-Transfer-Encoding: 8bit
+From: %(fromaddr)s
+Reply-To: %(reply_to)s
+In-Reply-To: %(reply_to_msgid)s
+References: %(reply_to_msgid)s
+X-Git-Repo: %(repo_shortname)s
+X-Git-Refname: %(refname)s
+X-Git-Reftype: %(refname_type)s
+X-Git-Rev: %(rev)s
+Auto-Submitted: auto-generated
+"""
+
+REVISION_INTRO_TEMPLATE = """\
+This is an automated email from the git hooks/post-receive script.
+
+%(pusher)s pushed a commit to %(refname_type)s %(short_refname)s
+in repository %(repo_shortname)s.
+
+"""
+
+
+REVISION_FOOTER_TEMPLATE = FOOTER_TEMPLATE
+
+
+class CommandError(Exception):
+    def __init__(self, cmd, retcode):
+        self.cmd = cmd
+        self.retcode = retcode
+        Exception.__init__(
+            self,
+            'Command "%s" failed with retcode %s' % (' '.join(cmd), retcode,)
+            )
+
+
+class ConfigurationException(Exception):
+    pass
+
+
+def read_git_output(args, input=None, keepends=False, **kw):
+    """Read the output of a Git command."""
+
+    return read_output(
+        ['git', '-c', 'i18n.logoutputencoding=%s' % (ENCODING,)] + args,
+        input=input, keepends=keepends, **kw
+        )
+
+
+def read_output(cmd, input=None, keepends=False, **kw):
+    if input:
+        stdin = subprocess.PIPE
+    else:
+        stdin = None
+    p = subprocess.Popen(
+        cmd, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kw
+        )
+    (out, err) = p.communicate(input)
+    retcode = p.wait()
+    if retcode:
+        raise CommandError(cmd, retcode)
+    if not keepends:
+        out = out.rstrip('\n\r')
+    return out
+
+
+def read_git_lines(args, keepends=False, **kw):
+    """Return the lines output by Git command.
+
+    Return as single lines, with newlines stripped off."""
+
+    return read_git_output(args, keepends=True, **kw).splitlines(keepends)
+
+
+class Config(object):
+    def __init__(self, section, git_config=None):
+        """Represent a section of the git configuration.
+
+        If git_config is specified, it is passed to "git config" in
+        the GIT_CONFIG environment variable, meaning that "git config"
+        will read the specified path rather than the Git default
+        config paths."""
+
+        self.section = section
+        if git_config:
+            self.env = os.environ.copy()
+            self.env['GIT_CONFIG'] = git_config
+        else:
+            self.env = None
+
+    @staticmethod
+    def _split(s):
+        """Split NUL-terminated values."""
+
+        words = s.split('\0')
+        assert words[-1] == ''
+        return words[:-1]
+
+    def get(self, name, default=None):
+        try:
+            values = self._split(read_git_output(
+                    ['config', '--get', '--null', '%s.%s' % (self.section, name)],
+                    env=self.env, keepends=True,
+                    ))
+            assert len(values) == 1
+            return values[0]
+        except CommandError:
+            return default
+
+    def get_bool(self, name, default=None):
+        try:
+            value = read_git_output(
+                ['config', '--get', '--bool', '%s.%s' % (self.section, name)],
+                env=self.env,
+                )
+        except CommandError:
+            return default
+        return value == 'true'
+
+    def get_all(self, name, default=None):
+        """Read a (possibly multivalued) setting from the configuration.
+
+        Return the result as a list of values, or default if the name
+        is unset."""
+
+        try:
+            return self._split(read_git_output(
+                ['config', '--get-all', '--null', '%s.%s' % (self.section, name)],
+                env=self.env, keepends=True,
+                ))
+        except CommandError, e:
+            if e.retcode == 1:
+                # "the section or key is invalid"; i.e., there is no
+                # value for the specified key.
+                return default
+            else:
+                raise
+
+    def get_recipients(self, name, default=None):
+        """Read a recipients list from the configuration.
+
+        Return the result as a comma-separated list of email
+        addresses, or default if the option is unset.  If the setting
+        has multiple values, concatenate them with comma separators."""
+
+        lines = self.get_all(name, default=None)
+        if lines is None:
+            return default
+        return ', '.join(line.strip() for line in lines)
+
+    def set(self, name, value):
+        read_git_output(
+            ['config', '%s.%s' % (self.section, name), value],
+            env=self.env,
+            )
+
+    def add(self, name, value):
+        read_git_output(
+            ['config', '--add', '%s.%s' % (self.section, name), value],
+            env=self.env,
+            )
+
+    def has_key(self, name):
+        return self.get_all(name, default=None) is not None
+
+    def unset_all(self, name):
+        try:
+            read_git_output(
+                ['config', '--unset-all', '%s.%s' % (self.section, name)],
+                env=self.env,
+                )
+        except CommandError, e:
+            if e.retcode == 5:
+                # The name doesn't exist, which is what we wanted anyway...
+                pass
+            else:
+                raise
+
+    def set_recipients(self, name, value):
+        self.unset_all(name)
+        for pair in getaddresses([value]):
+            self.add(name, formataddr(pair))
+
+
+def generate_summaries(*log_args):
+    """Generate a brief summary for each revision requested.
+
+    log_args are strings that will be passed directly to "git log" as
+    revision selectors.  Iterate over (sha1_short, subject) for each
+    commit specified by log_args (subject is the first line of the
+    commit message as a string without EOLs)."""
+
+    cmd = [
+        'log', '--abbrev', '--format=%h %s',
+        ] + list(log_args) + ['--']
+    for line in read_git_lines(cmd):
+        yield tuple(line.split(' ', 1))
+
+
+def limit_lines(lines, max_lines):
+    for (index, line) in enumerate(lines):
+        if index < max_lines:
+            yield line
+
+    if index >= max_lines:
+        yield '... %d lines suppressed ...\n' % (index + 1 - max_lines,)
+
+
+def limit_linelength(lines, max_linelength):
+    for line in lines:
+        # Don't forget that lines always include a trailing newline.
+        if len(line) > max_linelength + 1:
+            line = line[:max_linelength - 7] + ' [...]\n'
+        yield line
+
+
+class CommitSet(object):
+    """A (constant) set of object names.
+
+    The set should be initialized with full SHA1 object names.  The
+    __contains__() method returns True iff its argument is an
+    abbreviation of any the names in the set."""
+
+    def __init__(self, names):
+        self._names = sorted(names)
+
+    def __len__(self):
+        return len(self._names)
+
+    def __contains__(self, sha1_abbrev):
+        """Return True iff this set contains sha1_abbrev (which might be abbreviated)."""
+
+        i = bisect.bisect_left(self._names, sha1_abbrev)
+        return i < len(self) and self._names[i].startswith(sha1_abbrev)
+
+
+class GitObject(object):
+    def __init__(self, sha1, type=None):
+        if sha1 == ZEROS:
+            self.sha1 = self.type = self.commit_sha1 = None
+        else:
+            self.sha1 = sha1
+            self.type = type or read_git_output(['cat-file', '-t', self.sha1])
+
+            if self.type == 'commit':
+                self.commit_sha1 = self.sha1
+            elif self.type == 'tag':
+                try:
+                    self.commit_sha1 = read_git_output(
+                        ['rev-parse', '--verify', '%s^0' % (self.sha1,)]
+                        )
+                except CommandError:
+                    # Cannot deref tag to determine commit_sha1
+                    self.commit_sha1 = None
+            else:
+                self.commit_sha1 = None
+
+        self.short = read_git_output(['rev-parse', '--short', sha1])
+
+    def get_summary(self):
+        """Return (sha1_short, subject) for this commit."""
+
+        if not self.sha1:
+            raise ValueError('Empty commit has no summary')
+
+        return iter(generate_summaries('--no-walk', self.sha1)).next()
+
+    def __eq__(self, other):
+        return isinstance(other, GitObject) and self.sha1 == other.sha1
+
+    def __hash__(self):
+        return hash(self.sha1)
+
+    def __nonzero__(self):
+        return bool(self.sha1)
+
+    def __str__(self):
+        return self.sha1 or ZEROS
+
+
+class Change(object):
+    """A Change that has been made to the Git repository.
+
+    Abstract class from which both Revisions and ReferenceChanges are
+    derived.  A Change knows how to generate a notification email
+    describing itself."""
+
+    def __init__(self, environment):
+        self.environment = environment
+        self._values = None
+
+    def _compute_values(self):
+        """Return a dictionary {keyword : expansion} for this Change.
+
+        Derived classes overload this method to add more entries to
+        the return value.  This method is used internally by
+        get_values().  The return value should always be a new
+        dictionary."""
+
+        return self.environment.get_values()
+
+    def get_values(self, **extra_values):
+        """Return a dictionary {keyword : expansion} for this Change.
+
+        Return a dictionary mapping keywords to the values that they
+        should be expanded to for this Change (used when interpolating
+        template strings).  If any keyword arguments are supplied, add
+        those to the return value as well.  The return value is always
+        a new dictionary."""
+
+        if self._values is None:
+            self._values = self._compute_values()
+
+        values = self._values.copy()
+        if extra_values:
+            values.update(extra_values)
+        return values
+
+    def expand(self, template, **extra_values):
+        """Expand template.
+
+        Expand the template (which should be a string) using string
+        interpolation of the values for this Change.  If any keyword
+        arguments are provided, also include those in the keywords
+        available for interpolation."""
+
+        return template % self.get_values(**extra_values)
+
+    def expand_lines(self, template, **extra_values):
+        """Break template into lines and expand each line."""
+
+        values = self.get_values(**extra_values)
+        for line in template.splitlines(True):
+            yield line % values
+
+    def expand_header_lines(self, template, **extra_values):
+        """Break template into lines and expand each line as an RFC 2822 header.
+
+        Encode values and split up lines that are too long.  Silently
+        skip lines that contain references to unknown variables."""
+
+        values = self.get_values(**extra_values)
+        for line in template.splitlines():
+            (name, value) = line.split(':', 1)
+
+            try:
+                value = value % values
+            except KeyError, e:
+                if DEBUG:
+                    sys.stderr.write(
+                        'Warning: unknown variable %r in the following line; line skipped:\n'
+                        '    %s\n'
+                        % (e.args[0], line,)
+                        )
+            else:
+                try:
+                    h = Header(value, header_name=name)
+                except UnicodeDecodeError:
+                    h = Header(value, header_name=name, charset=CHARSET, errors='replace')
+                for splitline in ('%s: %s\n' % (name, h.encode(),)).splitlines(True):
+                    yield splitline
+
+    def generate_email_header(self):
+        """Generate the RFC 2822 email headers for this Change, a line at a time.
+
+        The output should not include the trailing blank line."""
+
+        raise NotImplementedError()
+
+    def generate_email_intro(self):
+        """Generate the email intro for this Change, a line at a time.
+
+        The output will be used as the standard boilerplate at the top
+        of the email body."""
+
+        raise NotImplementedError()
+
+    def generate_email_body(self):
+        """Generate the main part of the email body, a line at a time.
+
+        The text in the body might be truncated after a specified
+        number of lines (see multimailhook.emailmaxlines)."""
+
+        raise NotImplementedError()
+
+    def generate_email_footer(self):
+        """Generate the footer of the email, a line at a time.
+
+        The footer is always included, irrespective of
+        multimailhook.emailmaxlines."""
+
+        raise NotImplementedError()
+
+    def generate_email(self, push, body_filter=None):
+        """Generate an email describing this change.
+
+        Iterate over the lines (including the header lines) of an
+        email describing this change.  If body_filter is not None,
+        then use it to filter the lines that are intended for the
+        email body."""
+
+        for line in self.generate_email_header():
+            yield line
+        yield '\n'
+        for line in self.generate_email_intro():
+            yield line
+
+        body = self.generate_email_body(push)
+        if body_filter is not None:
+            body = body_filter(body)
+        for line in body:
+            yield line
+
+        for line in self.generate_email_footer():
+            yield line
+
+
+class Revision(Change):
+    """A Change consisting of a single git commit."""
+
+    def __init__(self, reference_change, rev, num, tot):
+        Change.__init__(self, reference_change.environment)
+        self.reference_change = reference_change
+        self.rev = rev
+        self.change_type = self.reference_change.change_type
+        self.refname = self.reference_change.refname
+        self.num = num
+        self.tot = tot
+        self.author = read_git_output(['log', '--no-walk', '--format=%aN <%aE>', self.rev.sha1])
+        self.recipients = self.environment.get_revision_recipients(self)
+
+    def _compute_values(self):
+        values = Change._compute_values(self)
+
+        oneline = read_git_output(
+            ['log', '--format=%s', '--no-walk', self.rev.sha1]
+            )
+
+        values['rev'] = self.rev.sha1
+        values['rev_short'] = self.rev.short
+        values['change_type'] = self.change_type
+        values['refname'] = self.refname
+        values['short_refname'] = self.reference_change.short_refname
+        values['refname_type'] = self.reference_change.refname_type
+        values['reply_to_msgid'] = self.reference_change.msgid
+        values['num'] = self.num
+        values['tot'] = self.tot
+        values['recipients'] = self.recipients
+        values['oneline'] = oneline
+        values['author'] = self.author
+
+        reply_to = self.environment.get_reply_to_commit(self)
+        if reply_to:
+            values['reply_to'] = reply_to
+
+        return values
+
+    def generate_email_header(self):
+        for line in self.expand_header_lines(REVISION_HEADER_TEMPLATE):
+            yield line
+
+    def generate_email_intro(self):
+        for line in self.expand_lines(REVISION_INTRO_TEMPLATE):
+            yield line
+
+    def generate_email_body(self, push):
+        """Show this revision."""
+
+        return read_git_lines(
+            [
+                'log', '-C',
+                 '--stat', '-p', '--cc',
+                '-1', self.rev.sha1,
+                ],
+            keepends=True,
+            )
+
+    def generate_email_footer(self):
+        return self.expand_lines(REVISION_FOOTER_TEMPLATE)
+
+
+class ReferenceChange(Change):
+    """A Change to a Git reference.
+
+    An abstract class representing a create, update, or delete of a
+    Git reference.  Derived classes handle specific types of reference
+    (e.g., tags vs. branches).  These classes generate the main
+    reference change email summarizing the reference change and
+    whether it caused any any commits to be added or removed.
+
+    ReferenceChange objects are usually created using the static
+    create() method, which has the logic to decide which derived class
+    to instantiate."""
+
+    REF_RE = re.compile(r'^refs\/(?P<area>[^\/]+)\/(?P<shortname>.*)$')
+
+    @staticmethod
+    def create(environment, oldrev, newrev, refname):
+        """Return a ReferenceChange object representing the change.
+
+        Return an object that represents the type of change that is being
+        made. oldrev and newrev should be SHA1s or ZEROS."""
+
+        old = GitObject(oldrev)
+        new = GitObject(newrev)
+        rev = new or old
+
+        # The revision type tells us what type the commit is, combined with
+        # the location of the ref we can decide between
+        #  - working branch
+        #  - tracking branch
+        #  - unannotated tag
+        #  - annotated tag
+        m = ReferenceChange.REF_RE.match(refname)
+        if m:
+            area = m.group('area')
+            short_refname = m.group('shortname')
+        else:
+            area = ''
+            short_refname = refname
+
+        if rev.type == 'tag':
+            # Annotated tag:
+            klass = AnnotatedTagChange
+        elif rev.type == 'commit':
+            if area == 'tags':
+                # Non-annotated tag:
+                klass = NonAnnotatedTagChange
+            elif area == 'heads':
+                # Branch:
+                klass = BranchChange
+            elif area == 'remotes':
+                # Tracking branch:
+                sys.stderr.write(
+                    '*** Push-update of tracking branch %r\n'
+                    '***  - incomplete email generated.\n'
+                     % (refname,)
+                    )
+                klass = OtherReferenceChange
+            else:
+                # Some other reference namespace:
+                sys.stderr.write(
+                    '*** Push-update of strange reference %r\n'
+                    '***  - incomplete email generated.\n'
+                     % (refname,)
+                    )
+                klass = OtherReferenceChange
+        else:
+            # Anything else (is there anything else?)
+            sys.stderr.write(
+                '*** Unknown type of update to %r (%s)\n'
+                '***  - incomplete email generated.\n'
+                 % (refname, rev.type,)
+                )
+            klass = OtherReferenceChange
+
+        return klass(
+            environment,
+            refname=refname, short_refname=short_refname,
+            old=old, new=new, rev=rev,
+            )
+
+    def __init__(self, environment, refname, short_refname, old, new, rev):
+        Change.__init__(self, environment)
+        self.change_type = {
+            (False, True) : 'create',
+            (True, True) : 'update',
+            (True, False) : 'delete',
+            }[bool(old), bool(new)]
+        self.refname = refname
+        self.short_refname = short_refname
+        self.old = old
+        self.new = new
+        self.rev = rev
+        self.msgid = make_msgid()
+        self.diffopts = environment.diffopts
+        self.logopts = environment.logopts
+        self.showlog = environment.refchange_showlog
+
+    def _compute_values(self):
+        values = Change._compute_values(self)
+
+        values['change_type'] = self.change_type
+        values['refname_type'] = self.refname_type
+        values['refname'] = self.refname
+        values['short_refname'] = self.short_refname
+        values['msgid'] = self.msgid
+        values['recipients'] = self.recipients
+        values['oldrev'] = str(self.old)
+        values['oldrev_short'] = self.old.short
+        values['newrev'] = str(self.new)
+        values['newrev_short'] = self.new.short
+
+        if self.old:
+            values['oldrev_type'] = self.old.type
+        if self.new:
+            values['newrev_type'] = self.new.type
+
+        reply_to = self.environment.get_reply_to_refchange(self)
+        if reply_to:
+            values['reply_to'] = reply_to
+
+        return values
+
+    def get_subject(self):
+        template = {
+            'create' : REF_CREATED_SUBJECT_TEMPLATE,
+            'update' : REF_UPDATED_SUBJECT_TEMPLATE,
+            'delete' : REF_DELETED_SUBJECT_TEMPLATE,
+            }[self.change_type]
+        return self.expand(template)
+
+    def generate_email_header(self):
+        for line in self.expand_header_lines(
+            REFCHANGE_HEADER_TEMPLATE, subject=self.get_subject(),
+            ):
+            yield line
+
+    def generate_email_intro(self):
+        for line in self.expand_lines(REFCHANGE_INTRO_TEMPLATE):
+            yield line
+
+    def generate_email_body(self, push):
+        """Call the appropriate body-generation routine.
+
+        Call one of generate_create_summary() /
+        generate_update_summary() / generate_delete_summary()."""
+
+        change_summary = {
+            'create' : self.generate_create_summary,
+            'delete' : self.generate_delete_summary,
+            'update' : self.generate_update_summary,
+            }[self.change_type](push)
+        for line in change_summary:
+            yield line
+
+        for line in self.generate_revision_change_summary(push):
+            yield line
+
+    def generate_email_footer(self):
+        return self.expand_lines(FOOTER_TEMPLATE)
+
+    def generate_revision_change_log(self, new_commits_list):
+        if self.showlog:
+            yield '\n'
+            yield 'Detailed log of new commits:\n\n'
+            for line in read_git_lines(
+                    ['log', '--no-walk']
+                    + self.logopts
+                    + new_commits_list
+                    + ['--'],
+                    keepends=True,
+                ):
+                yield line
+
+    def generate_revision_change_summary(self, push):
+        """Generate a summary of the revisions added/removed by this change."""
+
+        if self.new.commit_sha1 and not self.old.commit_sha1:
+            # A new reference was created.  List the new revisions
+            # brought by the new reference (i.e., those revisions that
+            # were not in the repository before this reference
+            # change).
+            sha1s = list(push.get_new_commits(self))
+            sha1s.reverse()
+            tot = len(sha1s)
+            new_revisions = [
+                Revision(self, GitObject(sha1), num=i+1, tot=tot)
+                for (i, sha1) in enumerate(sha1s)
+                ]
+
+            if new_revisions:
+                yield self.expand('This %(refname_type)s includes the following new commits:\n')
+                yield '\n'
+                for r in new_revisions:
+                    (sha1, subject) = r.rev.get_summary()
+                    yield r.expand(
+                        BRIEF_SUMMARY_TEMPLATE, action='new', text=subject,
+                        )
+                yield '\n'
+                for line in self.expand_lines(NEW_REVISIONS_TEMPLATE, tot=tot):
+                    yield line
+                for line in self.generate_revision_change_log([r.rev.sha1 for r in new_revisions]):
+                    yield line
+            else:
+                for line in self.expand_lines(NO_NEW_REVISIONS_TEMPLATE):
+                    yield line
+
+        elif self.new.commit_sha1 and self.old.commit_sha1:
+            # A reference was changed to point at a different commit.
+            # List the revisions that were removed and/or added *from
+            # that reference* by this reference change, along with a
+            # diff between the trees for its old and new values.
+
+            # List of the revisions that were added to the branch by
+            # this update.  Note this list can include revisions that
+            # have already had notification emails; we want such
+            # revisions in the summary even though we will not send
+            # new notification emails for them.
+            adds = list(generate_summaries(
+                    '--topo-order', '--reverse', '%s..%s'
+                    % (self.old.commit_sha1, self.new.commit_sha1,)
+                    ))
+
+            # List of the revisions that were removed from the branch
+            # by this update.  This will be empty except for
+            # non-fast-forward updates.
+            discards = list(generate_summaries(
+                    '%s..%s' % (self.new.commit_sha1, self.old.commit_sha1,)
+                    ))
+
+            if adds:
+                new_commits_list = push.get_new_commits(self)
+            else:
+                new_commits_list = []
+            new_commits = CommitSet(new_commits_list)
+
+            if discards:
+                discarded_commits = CommitSet(push.get_discarded_commits(self))
+            else:
+                discarded_commits = CommitSet([])
+
+            if discards and adds:
+                for (sha1, subject) in discards:
+                    if sha1 in discarded_commits:
+                        action = 'discards'
+                    else:
+                        action = 'omits'
+                    yield self.expand(
+                        BRIEF_SUMMARY_TEMPLATE, action=action,
+                        rev_short=sha1, text=subject,
+                        )
+                for (sha1, subject) in adds:
+                    if sha1 in new_commits:
+                        action = 'new'
+                    else:
+                        action = 'adds'
+                    yield self.expand(
+                        BRIEF_SUMMARY_TEMPLATE, action=action,
+                        rev_short=sha1, text=subject,
+                        )
+                yield '\n'
+                for line in self.expand_lines(NON_FF_TEMPLATE):
+                    yield line
+
+            elif discards:
+                for (sha1, subject) in discards:
+                    if sha1 in discarded_commits:
+                        action = 'discards'
+                    else:
+                        action = 'omits'
+                    yield self.expand(
+                        BRIEF_SUMMARY_TEMPLATE, action=action,
+                        rev_short=sha1, text=subject,
+                        )
+                yield '\n'
+                for line in self.expand_lines(REWIND_ONLY_TEMPLATE):
+                    yield line
+
+            elif adds:
+                (sha1, subject) = self.old.get_summary()
+                yield self.expand(
+                    BRIEF_SUMMARY_TEMPLATE, action='from',
+                    rev_short=sha1, text=subject,
+                    )
+                for (sha1, subject) in adds:
+                    if sha1 in new_commits:
+                        action = 'new'
+                    else:
+                        action = 'adds'
+                    yield self.expand(
+                        BRIEF_SUMMARY_TEMPLATE, action=action,
+                        rev_short=sha1, text=subject,
+                        )
+
+            yield '\n'
+
+            if new_commits:
+                for line in self.expand_lines(NEW_REVISIONS_TEMPLATE, tot=len(new_commits)):
+                    yield line
+                for line in self.generate_revision_change_log(new_commits_list):
+                    yield line
+            else:
+                for line in self.expand_lines(NO_NEW_REVISIONS_TEMPLATE):
+                    yield line
+
+            # The diffstat is shown from the old revision to the new
+            # revision.  This is to show the truth of what happened in
+            # this change.  There's no point showing the stat from the
+            # base to the new revision because the base is effectively a
+            # random revision at this point - the user will be interested
+            # in what this revision changed - including the undoing of
+            # previous revisions in the case of non-fast-forward updates.
+            yield '\n'
+            yield 'Summary of changes:\n'
+            for line in read_git_lines(
+                ['diff-tree']
+                + self.diffopts
+                + ['%s..%s' % (self.old.commit_sha1, self.new.commit_sha1,)],
+                keepends=True,
+                ):
+                yield line
+
+        elif self.old.commit_sha1 and not self.new.commit_sha1:
+            # A reference was deleted.  List the revisions that were
+            # removed from the repository by this reference change.
+
+            sha1s = list(push.get_discarded_commits(self))
+            tot = len(sha1s)
+            discarded_revisions = [
+                Revision(self, GitObject(sha1), num=i+1, tot=tot)
+                for (i, sha1) in enumerate(sha1s)
+                ]
+
+            if discarded_revisions:
+                for line in self.expand_lines(DISCARDED_REVISIONS_TEMPLATE):
+                    yield line
+                yield '\n'
+                for r in discarded_revisions:
+                    (sha1, subject) = r.rev.get_summary()
+                    yield r.expand(
+                        BRIEF_SUMMARY_TEMPLATE, action='discards', text=subject,
+                        )
+            else:
+                for line in self.expand_lines(NO_DISCARDED_REVISIONS_TEMPLATE):
+                    yield line
+
+        elif not self.old.commit_sha1 and not self.new.commit_sha1:
+            for line in self.expand_lines(NON_COMMIT_UPDATE_TEMPLATE):
+                yield line
+
+    def generate_create_summary(self, push):
+        """Called for the creation of a reference."""
+
+        # This is a new reference and so oldrev is not valid
+        (sha1, subject) = self.new.get_summary()
+        yield self.expand(
+            BRIEF_SUMMARY_TEMPLATE, action='at',
+            rev_short=sha1, text=subject,
+            )
+        yield '\n'
+
+    def generate_update_summary(self, push):
+        """Called for the change of a pre-existing branch."""
+
+        return iter([])
+
+    def generate_delete_summary(self, push):
+        """Called for the deletion of any type of reference."""
+
+        (sha1, subject) = self.old.get_summary()
+        yield self.expand(
+            BRIEF_SUMMARY_TEMPLATE, action='was',
+            rev_short=sha1, text=subject,
+            )
+        yield '\n'
+
+
+class BranchChange(ReferenceChange):
+    refname_type = 'branch'
+
+    def __init__(self, environment, refname, short_refname, old, new, rev):
+        ReferenceChange.__init__(
+            self, environment,
+            refname=refname, short_refname=short_refname,
+            old=old, new=new, rev=rev,
+            )
+        self.recipients = environment.get_refchange_recipients(self)
+
+
+class AnnotatedTagChange(ReferenceChange):
+    refname_type = 'annotated tag'
+
+    def __init__(self, environment, refname, short_refname, old, new, rev):
+        ReferenceChange.__init__(
+            self, environment,
+            refname=refname, short_refname=short_refname,
+            old=old, new=new, rev=rev,
+            )
+        self.recipients = environment.get_announce_recipients(self)
+        self.show_shortlog = environment.announce_show_shortlog
+
+    ANNOTATED_TAG_FORMAT = (
+        '%(*objectname)\n'
+        '%(*objecttype)\n'
+        '%(taggername)\n'
+        '%(taggerdate)'
+        )
+
+    def describe_tag(self, push):
+        """Describe the new value of an annotated tag."""
+
+        # Use git for-each-ref to pull out the individual fields from
+        # the tag
+        [tagobject, tagtype, tagger, tagged] = read_git_lines(
+            ['for-each-ref', '--format=%s' % (self.ANNOTATED_TAG_FORMAT,), self.refname],
+            )
+
+        yield self.expand(
+            BRIEF_SUMMARY_TEMPLATE, action='tagging',
+            rev_short=tagobject, text='(%s)' % (tagtype,),
+            )
+        if tagtype == 'commit':
+            # If the tagged object is a commit, then we assume this is a
+            # release, and so we calculate which tag this tag is
+            # replacing
+            try:
+                prevtag = read_git_output(['describe', '--abbrev=0', '%s^' % (self.new,)])
+            except CommandError:
+                prevtag = None
+            if prevtag:
+                yield '  replaces  %s\n' % (prevtag,)
+        else:
+            prevtag = None
+            yield '    length  %s bytes\n' % (read_git_output(['cat-file', '-s', tagobject]),)
+
+        yield ' tagged by  %s\n' % (tagger,)
+        yield '        on  %s\n' % (tagged,)
+        yield '\n'
+
+        # Show the content of the tag message; this might contain a
+        # change log or release notes so is worth displaying.
+        yield LOGBEGIN
+        contents = list(read_git_lines(['cat-file', 'tag', self.new.sha1], keepends=True))
+        contents = contents[contents.index('\n') + 1:]
+        if contents and contents[-1][-1:] != '\n':
+            contents.append('\n')
+        for line in contents:
+            yield line
+
+        if self.show_shortlog and tagtype == 'commit':
+            # Only commit tags make sense to have rev-list operations
+            # performed on them
+            yield '\n'
+            if prevtag:
+                # Show changes since the previous release
+                revlist = read_git_output(
+                    ['rev-list', '--pretty=short', '%s..%s' % (prevtag, self.new,)],
+                    keepends=True,
+                    )
+            else:
+                # No previous tag, show all the changes since time
+                # began
+                revlist = read_git_output(
+                    ['rev-list', '--pretty=short', '%s' % (self.new,)],
+                    keepends=True,
+                    )
+            for line in read_git_lines(['shortlog'], input=revlist, keepends=True):
+                yield line
+
+        yield LOGEND
+        yield '\n'
+
+    def generate_create_summary(self, push):
+        """Called for the creation of an annotated tag."""
+
+        for line in self.expand_lines(TAG_CREATED_TEMPLATE):
+            yield line
+
+        for line in self.describe_tag(push):
+            yield line
+
+    def generate_update_summary(self, push):
+        """Called for the update of an annotated tag.
+
+        This is probably a rare event and may not even be allowed."""
+
+        for line in self.expand_lines(TAG_UPDATED_TEMPLATE):
+            yield line
+
+        for line in self.describe_tag(push):
+            yield line
+
+    def generate_delete_summary(self, push):
+        """Called when a non-annotated reference is updated."""
+
+        for line in self.expand_lines(TAG_DELETED_TEMPLATE):
+            yield line
+
+        yield self.expand('   tag was  %(oldrev_short)s\n')
+        yield '\n'
+
+
+class NonAnnotatedTagChange(ReferenceChange):
+    refname_type = 'tag'
+
+    def __init__(self, environment, refname, short_refname, old, new, rev):
+        ReferenceChange.__init__(
+            self, environment,
+            refname=refname, short_refname=short_refname,
+            old=old, new=new, rev=rev,
+            )
+        self.recipients = environment.get_refchange_recipients(self)
+
+    def generate_create_summary(self, push):
+        """Called for the creation of an annotated tag."""
+
+        for line in self.expand_lines(TAG_CREATED_TEMPLATE):
+            yield line
+
+    def generate_update_summary(self, push):
+        """Called when a non-annotated reference is updated."""
+
+        for line in self.expand_lines(TAG_UPDATED_TEMPLATE):
+            yield line
+
+    def generate_delete_summary(self, push):
+        """Called when a non-annotated reference is updated."""
+
+        for line in self.expand_lines(TAG_DELETED_TEMPLATE):
+            yield line
+
+        for line in ReferenceChange.generate_delete_summary(self, push):
+            yield line
+
+
+class OtherReferenceChange(ReferenceChange):
+    refname_type = 'reference'
+
+    def __init__(self, environment, refname, short_refname, old, new, rev):
+        # We use the full refname as short_refname, because otherwise
+        # the full name of the reference would not be obvious from the
+        # text of the email.
+        ReferenceChange.__init__(
+            self, environment,
+            refname=refname, short_refname=refname,
+            old=old, new=new, rev=rev,
+            )
+        self.recipients = environment.get_refchange_recipients(self)
+
+
+class Mailer(object):
+    """An object that can send emails."""
+
+    def send(self, lines, to_addrs):
+        """Send an email consisting of lines.
+
+        lines must be an iterable over the lines constituting the
+        header and body of the email.  to_addrs is a list of recipient
+        addresses (can be needed even if lines already contains a
+        "To:" field).  It can be either a string (comma-separated list
+        of email addresses) or a Python list of individual email
+        addresses.
+
+        """
+
+        raise NotImplementedError()
+
+
+class SendMailer(Mailer):
+    """Send emails using 'sendmail -t'."""
+
+    SENDMAIL_CANDIDATES = [
+        '/usr/sbin/sendmail',
+        '/usr/lib/sendmail',
+        ]
+
+    @staticmethod
+    def find_sendmail():
+        for path in SendMailer.SENDMAIL_CANDIDATES:
+            if os.access(path, os.X_OK):
+                return path
+        else:
+            raise ConfigurationException(
+                'No sendmail executable found.  '
+                'Try setting multimailhook.sendmailCommand.'
+                )
+
+    def __init__(self, command=None, envelopesender=None):
+        """Construct a SendMailer instance.
+
+        command should be the command and arguments used to invoke
+        sendmail, as a list of strings.  If an envelopesender is
+        provided, it will also be passed to the command, via '-f
+        envelopesender'."""
+
+        if command:
+            self.command = command[:]
+        else:
+            self.command = [self.find_sendmail(), '-t']
+
+        if envelopesender:
+            self.command.extend(['-f', envelopesender])
+
+    def send(self, lines, to_addrs):
+        try:
+            p = subprocess.Popen(self.command, stdin=subprocess.PIPE)
+        except OSError, e:
+            sys.stderr.write(
+                '*** Cannot execute command: %s\n' % ' '.join(self.command)
+                + '*** %s\n' % str(e)
+                + '*** Try setting multimailhook.mailer to "smtp"\n'
+                '*** to send emails without using the sendmail command.\n'
+                )
+            sys.exit(1)
+        try:
+            p.stdin.writelines(lines)
+        except:
+            sys.stderr.write(
+                '*** Error while generating commit email\n'
+                '***  - mail sending aborted.\n'
+                )
+            p.terminate()
+            raise
+        else:
+            p.stdin.close()
+            retcode = p.wait()
+            if retcode:
+                raise CommandError(self.command, retcode)
+
+
+class SMTPMailer(Mailer):
+    """Send emails using Python's smtplib."""
+
+    def __init__(self, envelopesender, smtpserver):
+        if not envelopesender:
+            sys.stderr.write(
+                'fatal: git_multimail: cannot use SMTPMailer without a sender address.\n'
+                'please set either multimailhook.envelopeSender or user.email\n'
+                )
+            sys.exit(1)
+        self.envelopesender = envelopesender
+        self.smtpserver = smtpserver
+        try:
+            self.smtp = smtplib.SMTP(self.smtpserver)
+        except Exception, e:
+            sys.stderr.write('*** Error establishing SMTP connection to %s***\n' % self.smtpserver)
+            sys.stderr.write('*** %s\n' % str(e))
+            sys.exit(1)
+
+    def __del__(self):
+        self.smtp.quit()
+
+    def send(self, lines, to_addrs):
+        try:
+            msg = ''.join(lines)
+            # turn comma-separated list into Python list if needed.
+            if isinstance(to_addrs, basestring):
+                to_addrs = [email for (name, email) in getaddresses([to_addrs])]
+            self.smtp.sendmail(self.envelopesender, to_addrs, msg)
+        except Exception, e:
+            sys.stderr.write('*** Error sending email***\n')
+            sys.stderr.write('*** %s\n' % str(e))
+            self.smtp.quit()
+            sys.exit(1)
+
+
+class OutputMailer(Mailer):
+    """Write emails to an output stream, bracketed by lines of '=' characters.
+
+    This is intended for debugging purposes."""
+
+    SEPARATOR = '=' * 75 + '\n'
+
+    def __init__(self, f):
+        self.f = f
+
+    def send(self, lines, to_addrs):
+        self.f.write(self.SEPARATOR)
+        self.f.writelines(lines)
+        self.f.write(self.SEPARATOR)
+
+
+def get_git_dir():
+    """Determine GIT_DIR.
+
+    Determine GIT_DIR either from the GIT_DIR environment variable or
+    from the working directory, using Git's usual rules."""
+
+    try:
+        return read_git_output(['rev-parse', '--git-dir'])
+    except CommandError:
+        sys.stderr.write('fatal: git_multimail: not in a git directory\n')
+        sys.exit(1)
+
+
+class Environment(object):
+    """Describes the environment in which the push is occurring.
+
+    An Environment object encapsulates information about the local
+    environment.  For example, it knows how to determine:
+
+    * the name of the repository to which the push occurred
+
+    * what user did the push
+
+    * what users want to be informed about various types of changes.
+
+    An Environment object is expected to have the following methods:
+
+        get_repo_shortname()
+
+            Return a short name for the repository, for display
+            purposes.
+
+        get_repo_path()
+
+            Return the absolute path to the Git repository.
+
+        get_emailprefix()
+
+            Return a string that will be prefixed to every email's
+            subject.
+
+        get_pusher()
+
+            Return the username of the person who pushed the changes.
+            This value is used in the email body to indicate who
+            pushed the change.
+
+        get_pusher_email() (may return None)
+
+            Return the email address of the person who pushed the
+            changes.  The value should be a single RFC 2822 email
+            address as a string; e.g., "Joe User <user@example.com>"
+            if available, otherwise "user@example.com".  If set, the
+            value is used as the Reply-To address for refchange
+            emails.  If it is impossible to determine the pusher's
+            email, this attribute should be set to None (in which case
+            no Reply-To header will be output).
+
+        get_sender()
+
+            Return the address to be used as the 'From' email address
+            in the email envelope.
+
+        get_fromaddr()
+
+            Return the 'From' email address used in the email 'From:'
+            headers.  (May be a full RFC 2822 email address like 'Joe
+            User <user@example.com>'.)
+
+        get_administrator()
+
+            Return the name and/or email of the repository
+            administrator.  This value is used in the footer as the
+            person to whom requests to be removed from the
+            notification list should be sent.  Ideally, it should
+            include a valid email address.
+
+        get_reply_to_refchange()
+        get_reply_to_commit()
+
+            Return the address to use in the email "Reply-To" header,
+            as a string.  These can be an RFC 2822 email address, or
+            None to omit the "Reply-To" header.
+            get_reply_to_refchange() is used for refchange emails;
+            get_reply_to_commit() is used for individual commit
+            emails.
+
+    They should also define the following attributes:
+
+        announce_show_shortlog (bool)
+
+            True iff announce emails should include a shortlog.
+
+        refchange_showlog (bool)
+
+            True iff refchanges emails should include a detailed log.
+
+        diffopts (list of strings)
+
+            The options that should be passed to 'git diff' for the
+            summary email.  The value should be a list of strings
+            representing words to be passed to the command.
+
+        logopts (list of strings)
+
+            Analogous to diffopts, but contains options passed to
+            'git log' when generating the detailed log for a set of
+            commits (see refchange_showlog)
+
+    """
+
+    REPO_NAME_RE = re.compile(r'^(?P<name>.+?)(?:\.git)$')
+
+    def __init__(self, osenv=None):
+        self.osenv = osenv or os.environ
+        self.announce_show_shortlog = False
+        self.maxcommitemails = 500
+        self.diffopts = ['--stat', '--summary', '--find-copies-harder']
+        self.logopts = []
+        self.refchange_showlog = False
+
+        self.COMPUTED_KEYS = [
+            'administrator',
+            'charset',
+            'emailprefix',
+            'fromaddr',
+            'pusher',
+            'pusher_email',
+            'repo_path',
+            'repo_shortname',
+            'sender',
+            ]
+
+        self._values = None
+
+    def get_repo_shortname(self):
+        """Use the last part of the repo path, with ".git" stripped off if present."""
+
+        basename = os.path.basename(os.path.abspath(self.get_repo_path()))
+        m = self.REPO_NAME_RE.match(basename)
+        if m:
+            return m.group('name')
+        else:
+            return basename
+
+    def get_pusher(self):
+        raise NotImplementedError()
+
+    def get_pusher_email(self):
+        return None
+
+    def get_administrator(self):
+        return 'the administrator of this repository'
+
+    def get_emailprefix(self):
+        return ''
+
+    def get_repo_path(self):
+        if read_git_output(['rev-parse', '--is-bare-repository']) == 'true':
+            path = get_git_dir()
+        else:
+            path = read_git_output(['rev-parse', '--show-toplevel'])
+        return os.path.abspath(path)
+
+    def get_charset(self):
+        return CHARSET
+
+    def get_values(self):
+        """Return a dictionary {keyword : expansion} for this Environment.
+
+        This method is called by Change._compute_values().  The keys
+        in the returned dictionary are available to be used in any of
+        the templates.  The dictionary is created by calling
+        self.get_NAME() for each of the attributes named in
+        COMPUTED_KEYS and recording those that do not return None.
+        The return value is always a new dictionary."""
+
+        if self._values is None:
+            values = {}
+
+            for key in self.COMPUTED_KEYS:
+                value = getattr(self, 'get_%s' % (key,))()
+                if value is not None:
+                    values[key] = value
+
+            self._values = values
+
+        return self._values.copy()
+
+    def get_refchange_recipients(self, refchange):
+        """Return the recipients for notifications about refchange.
+
+        Return the list of email addresses to which notifications
+        about the specified ReferenceChange should be sent."""
+
+        raise NotImplementedError()
+
+    def get_announce_recipients(self, annotated_tag_change):
+        """Return the recipients for notifications about annotated_tag_change.
+
+        Return the list of email addresses to which notifications
+        about the specified AnnotatedTagChange should be sent."""
+
+        raise NotImplementedError()
+
+    def get_reply_to_refchange(self, refchange):
+        return self.get_pusher_email()
+
+    def get_revision_recipients(self, revision):
+        """Return the recipients for messages about revision.
+
+        Return the list of email addresses to which notifications
+        about the specified Revision should be sent.  This method
+        could be overridden, for example, to take into account the
+        contents of the revision when deciding whom to notify about
+        it.  For example, there could be a scheme for users to express
+        interest in particular files or subdirectories, and only
+        receive notification emails for revisions that affecting those
+        files."""
+
+        raise NotImplementedError()
+
+    def get_reply_to_commit(self, revision):
+        return revision.author
+
+    def filter_body(self, lines):
+        """Filter the lines intended for an email body.
+
+        lines is an iterable over the lines that would go into the
+        email body.  Filter it (e.g., limit the number of lines, the
+        line length, character set, etc.), returning another iterable.
+        See FilterLinesEnvironmentMixin and MaxlinesEnvironmentMixin
+        for classes implementing this functionality."""
+
+        return lines
+
+
+class ConfigEnvironmentMixin(Environment):
+    """A mixin that sets self.config to its constructor's config argument.
+
+    This class's constructor consumes the "config" argument.
+
+    Mixins that need to inspect the config should inherit from this
+    class (1) to make sure that "config" is still in the constructor
+    arguments with its own constructor runs and/or (2) to be sure that
+    self.config is set after construction."""
+
+    def __init__(self, config, **kw):
+        super(ConfigEnvironmentMixin, self).__init__(**kw)
+        self.config = config
+
+
+class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
+    """An Environment that reads most of its information from "git config"."""
+
+    def __init__(self, config, **kw):
+        super(ConfigOptionsEnvironmentMixin, self).__init__(
+            config=config, **kw
+            )
+
+        self.announce_show_shortlog = config.get_bool(
+            'announceshortlog', default=self.announce_show_shortlog
+            )
+
+        self.refchange_showlog = config.get_bool(
+            'refchangeshowlog', default=self.refchange_showlog
+            )
+
+        maxcommitemails = config.get('maxcommitemails')
+        if maxcommitemails is not None:
+            try:
+                self.maxcommitemails = int(maxcommitemails)
+            except ValueError:
+                sys.stderr.write(
+                    '*** Malformed value for multimailhook.maxCommitEmails: %s\n' % maxcommitemails
+                    + '*** Expected a number.  Ignoring.\n'
+                    )
+
+        diffopts = config.get('diffopts')
+        if diffopts is not None:
+            self.diffopts = shlex.split(diffopts)
+
+        logopts = config.get('logopts')
+        if logopts is not None:
+            self.logopts = shlex.split(logopts)
+
+        reply_to = config.get('replyTo')
+        self.__reply_to_refchange = config.get('replyToRefchange', default=reply_to)
+        if (
+            self.__reply_to_refchange is not None
+            and self.__reply_to_refchange.lower() == 'author'
+            ):
+            raise ConfigurationException(
+                '"author" is not an allowed setting for replyToRefchange'
+                )
+        self.__reply_to_commit = config.get('replyToCommit', default=reply_to)
+
+    def get_administrator(self):
+        return (
+            self.config.get('administrator')
+            or self.get_sender()
+            or super(ConfigOptionsEnvironmentMixin, self).get_administrator()
+            )
+
+    def get_repo_shortname(self):
+        return (
+            self.config.get('reponame')
+            or super(ConfigOptionsEnvironmentMixin, self).get_repo_shortname()
+            )
+
+    def get_emailprefix(self):
+        emailprefix = self.config.get('emailprefix')
+        if emailprefix and emailprefix.strip():
+            return emailprefix.strip() + ' '
+        else:
+            return '[%s] ' % (self.get_repo_shortname(),)
+
+    def get_sender(self):
+        return self.config.get('envelopesender')
+
+    def get_fromaddr(self):
+        fromaddr = self.config.get('from')
+        if fromaddr:
+            return fromaddr
+        else:
+            config = Config('user')
+            fromname = config.get('name', default='')
+            fromemail = config.get('email', default='')
+            if fromemail:
+                return formataddr([fromname, fromemail])
+            else:
+                return self.get_sender()
+
+    def get_reply_to_refchange(self, refchange):
+        if self.__reply_to_refchange is None:
+            return super(ConfigOptionsEnvironmentMixin, self).get_reply_to_refchange(refchange)
+        elif self.__reply_to_refchange.lower() == 'pusher':
+            return self.get_pusher_email()
+        elif self.__reply_to_refchange.lower() == 'none':
+            return None
+        else:
+            return self.__reply_to_refchange
+
+    def get_reply_to_commit(self, revision):
+        if self.__reply_to_commit is None:
+            return super(ConfigOptionsEnvironmentMixin, self).get_reply_to_commit(revision)
+        elif self.__reply_to_commit.lower() == 'author':
+            return revision.get_author()
+        elif self.__reply_to_commit.lower() == 'pusher':
+            return self.get_pusher_email()
+        elif self.__reply_to_commit.lower() == 'none':
+            return None
+        else:
+            return self.__reply_to_commit
+
+
+class FilterLinesEnvironmentMixin(Environment):
+    """Handle encoding and maximum line length of body lines.
+
+        emailmaxlinelength (int or None)
+
+            The maximum length of any single line in the email body.
+            Longer lines are truncated at that length with ' [...]'
+            appended.
+
+        strict_utf8 (bool)
+
+            If this field is set to True, then the email body text is
+            expected to be UTF-8.  Any invalid characters are
+            converted to U+FFFD, the Unicode replacement character
+            (encoded as UTF-8, of course).
+
+    """
+
+    def __init__(self, strict_utf8=True, emailmaxlinelength=500, **kw):
+        super(FilterLinesEnvironmentMixin, self).__init__(**kw)
+        self.__strict_utf8 = strict_utf8
+        self.__emailmaxlinelength = emailmaxlinelength
+
+    def filter_body(self, lines):
+        lines = super(FilterLinesEnvironmentMixin, self).filter_body(lines)
+        if self.__strict_utf8:
+            lines = (line.decode(ENCODING, 'replace') for line in lines)
+            # Limit the line length in Unicode-space to avoid
+            # splitting characters:
+            if self.__emailmaxlinelength:
+                lines = limit_linelength(lines, self.__emailmaxlinelength)
+            lines = (line.encode(ENCODING, 'replace') for line in lines)
+        elif self.__emailmaxlinelength:
+            lines = limit_linelength(lines, self.__emailmaxlinelength)
+
+        return lines
+
+
+class ConfigFilterLinesEnvironmentMixin(
+    ConfigEnvironmentMixin,
+    FilterLinesEnvironmentMixin,
+    ):
+    """Handle encoding and maximum line length based on config."""
+
+    def __init__(self, config, **kw):
+        strict_utf8 = config.get_bool('emailstrictutf8', default=None)
+        if strict_utf8 is not None:
+            kw['strict_utf8'] = strict_utf8
+
+        emailmaxlinelength = config.get('emailmaxlinelength')
+        if emailmaxlinelength is not None:
+            kw['emailmaxlinelength'] = int(emailmaxlinelength)
+
+        super(ConfigFilterLinesEnvironmentMixin, self).__init__(
+            config=config, **kw
+            )
+
+
+class MaxlinesEnvironmentMixin(Environment):
+    """Limit the email body to a specified number of lines."""
+
+    def __init__(self, emailmaxlines, **kw):
+        super(MaxlinesEnvironmentMixin, self).__init__(**kw)
+        self.__emailmaxlines = emailmaxlines
+
+    def filter_body(self, lines):
+        lines = super(MaxlinesEnvironmentMixin, self).filter_body(lines)
+        if self.__emailmaxlines:
+            lines = limit_lines(lines, self.__emailmaxlines)
+        return lines
+
+
+class ConfigMaxlinesEnvironmentMixin(
+    ConfigEnvironmentMixin,
+    MaxlinesEnvironmentMixin,
+    ):
+    """Limit the email body to the number of lines specified in config."""
+
+    def __init__(self, config, **kw):
+        emailmaxlines = int(config.get('emailmaxlines', default='0'))
+        super(ConfigMaxlinesEnvironmentMixin, self).__init__(
+            config=config,
+            emailmaxlines=emailmaxlines,
+            **kw
+            )
+
+
+class PusherDomainEnvironmentMixin(ConfigEnvironmentMixin):
+    """Deduce pusher_email from pusher by appending an emaildomain."""
+
+    def __init__(self, **kw):
+        super(PusherDomainEnvironmentMixin, self).__init__(**kw)
+        self.__emaildomain = self.config.get('emaildomain')
+
+    def get_pusher_email(self):
+        if self.__emaildomain:
+            # Derive the pusher's full email address in the default way:
+            return '%s@%s' % (self.get_pusher(), self.__emaildomain)
+        else:
+            return super(PusherDomainEnvironmentMixin, self).get_pusher_email()
+
+
+class StaticRecipientsEnvironmentMixin(Environment):
+    """Set recipients statically based on constructor parameters."""
+
+    def __init__(
+        self,
+        refchange_recipients, announce_recipients, revision_recipients,
+        **kw
+        ):
+        super(StaticRecipientsEnvironmentMixin, self).__init__(**kw)
+
+        # The recipients for various types of notification emails, as
+        # RFC 2822 email addresses separated by commas (or the empty
+        # string if no recipients are configured).  Although there is
+        # a mechanism to choose the recipient lists based on on the
+        # actual *contents* of the change being reported, we only
+        # choose based on the *type* of the change.  Therefore we can
+        # compute them once and for all:
+        self.__refchange_recipients = refchange_recipients
+        self.__announce_recipients = announce_recipients
+        self.__revision_recipients = revision_recipients
+
+    def get_refchange_recipients(self, refchange):
+        return self.__refchange_recipients
+
+    def get_announce_recipients(self, annotated_tag_change):
+        return self.__announce_recipients
+
+    def get_revision_recipients(self, revision):
+        return self.__revision_recipients
+
+
+class ConfigRecipientsEnvironmentMixin(
+    ConfigEnvironmentMixin,
+    StaticRecipientsEnvironmentMixin
+    ):
+    """Determine recipients statically based on config."""
+
+    def __init__(self, config, **kw):
+        super(ConfigRecipientsEnvironmentMixin, self).__init__(
+            config=config,
+            refchange_recipients=self._get_recipients(
+                config, 'refchangelist', 'mailinglist',
+                ),
+            announce_recipients=self._get_recipients(
+                config, 'announcelist', 'refchangelist', 'mailinglist',
+                ),
+            revision_recipients=self._get_recipients(
+                config, 'commitlist', 'mailinglist',
+                ),
+            **kw
+            )
+
+    def _get_recipients(self, config, *names):
+        """Return the recipients for a particular type of message.
+
+        Return the list of email addresses to which a particular type
+        of notification email should be sent, by looking at the config
+        value for "multimailhook.$name" for each of names.  Use the
+        value from the first name that is configured.  The return
+        value is a (possibly empty) string containing RFC 2822 email
+        addresses separated by commas.  If no configuration could be
+        found, raise a ConfigurationException."""
+
+        for name in names:
+            retval = config.get_recipients(name)
+            if retval is not None:
+                return retval
+        if len(names) == 1:
+            hint = 'Please set "%s.%s"' % (config.section, name)
+        else:
+            hint = (
+                'Please set one of the following:\n    "%s"'
+                % ('"\n    "'.join('%s.%s' % (config.section, name) for name in names))
+                )
+
+        raise ConfigurationException(
+            'The list of recipients for %s is not configured.\n%s' % (names[0], hint)
+            )
+
+
+class ProjectdescEnvironmentMixin(Environment):
+    """Make a "projectdesc" value available for templates.
+
+    By default, it is set to the first line of $GIT_DIR/description
+    (if that file is present and appears to be set meaningfully)."""
+
+    def __init__(self, **kw):
+        super(ProjectdescEnvironmentMixin, self).__init__(**kw)
+        self.COMPUTED_KEYS += ['projectdesc']
+
+    def get_projectdesc(self):
+        """Return a one-line descripition of the project."""
+
+        git_dir = get_git_dir()
+        try:
+            projectdesc = open(os.path.join(git_dir, 'description')).readline().strip()
+            if projectdesc and not projectdesc.startswith('Unnamed repository'):
+                return projectdesc
+        except IOError:
+            pass
+
+        return 'UNNAMED PROJECT'
+
+
+class GenericEnvironmentMixin(Environment):
+    def get_pusher(self):
+        return self.osenv.get('USER', 'unknown user')
+
+
+class GenericEnvironment(
+    ProjectdescEnvironmentMixin,
+    ConfigMaxlinesEnvironmentMixin,
+    ConfigFilterLinesEnvironmentMixin,
+    ConfigRecipientsEnvironmentMixin,
+    PusherDomainEnvironmentMixin,
+    ConfigOptionsEnvironmentMixin,
+    GenericEnvironmentMixin,
+    Environment,
+    ):
+    pass
+
+
+class GitoliteEnvironmentMixin(Environment):
+    def get_repo_shortname(self):
+        # The gitolite environment variable $GL_REPO is a pretty good
+        # repo_shortname (though it's probably not as good as a value
+        # the user might have explicitly put in his config).
+        return (
+            self.osenv.get('GL_REPO', None)
+            or super(GitoliteEnvironmentMixin, self).get_repo_shortname()
+            )
+
+    def get_pusher(self):
+        return self.osenv.get('GL_USER', 'unknown user')
+
+
+class GitoliteEnvironment(
+    ProjectdescEnvironmentMixin,
+    ConfigMaxlinesEnvironmentMixin,
+    ConfigFilterLinesEnvironmentMixin,
+    ConfigRecipientsEnvironmentMixin,
+    PusherDomainEnvironmentMixin,
+    ConfigOptionsEnvironmentMixin,
+    GitoliteEnvironmentMixin,
+    Environment,
+    ):
+    pass
+
+
+class Push(object):
+    """Represent an entire push (i.e., a group of ReferenceChanges).
+
+    It is easy to figure out what commits were added to a *branch* by
+    a Reference change:
+
+        git rev-list change.old..change.new
+
+    or removed from a *branch*:
+
+        git rev-list change.new..change.old
+
+    But it is not quite so trivial to determine which entirely new
+    commits were added to the *repository* by a push and which old
+    commits were discarded by a push.  A big part of the job of this
+    class is to figure out these things, and to make sure that new
+    commits are only detailed once even if they were added to multiple
+    references.
+
+    The first step is to determine the "other" references--those
+    unaffected by the current push.  They are computed by
+    Push._compute_other_ref_sha1s() by listing all references then
+    removing any affected by this push.
+
+    The commits contained in the repository before this push were
+
+        git rev-list other1 other2 other3 ... change1.old change2.old ...
+
+    Where "changeN.old" is the old value of one of the references
+    affected by this push.
+
+    The commits contained in the repository after this push are
+
+        git rev-list other1 other2 other3 ... change1.new change2.new ...
+
+    The commits added by this push are the difference between these
+    two sets, which can be written
+
+        git rev-list \
+            ^other1 ^other2 ... \
+            ^change1.old ^change2.old ... \
+            change1.new change2.new ...
+
+    The commits removed by this push can be computed by
+
+        git rev-list \
+            ^other1 ^other2 ... \
+            ^change1.new ^change2.new ... \
+            change1.old change2.old ...
+
+    The last point is that it is possible that other pushes are
+    occurring simultaneously to this one, so reference values can
+    change at any time.  It is impossible to eliminate all race
+    conditions, but we reduce the window of time during which problems
+    can occur by translating reference names to SHA1s as soon as
+    possible and working with SHA1s thereafter (because SHA1s are
+    immutable)."""
+
+    # A map {(changeclass, changetype) : integer} specifying the order
+    # that reference changes will be processed if multiple reference
+    # changes are included in a single push.  The order is significant
+    # mostly because new commit notifications are threaded together
+    # with the first reference change that includes the commit.  The
+    # following order thus causes commits to be grouped with branch
+    # changes (as opposed to tag changes) if possible.
+    SORT_ORDER = dict(
+        (value, i) for (i, value) in enumerate([
+            (BranchChange, 'update'),
+            (BranchChange, 'create'),
+            (AnnotatedTagChange, 'update'),
+            (AnnotatedTagChange, 'create'),
+            (NonAnnotatedTagChange, 'update'),
+            (NonAnnotatedTagChange, 'create'),
+            (BranchChange, 'delete'),
+            (AnnotatedTagChange, 'delete'),
+            (NonAnnotatedTagChange, 'delete'),
+            (OtherReferenceChange, 'update'),
+            (OtherReferenceChange, 'create'),
+            (OtherReferenceChange, 'delete'),
+            ])
+        )
+
+    def __init__(self, changes):
+        self.changes = sorted(changes, key=self._sort_key)
+
+        # The SHA-1s of commits referred to by references unaffected
+        # by this push:
+        other_ref_sha1s = self._compute_other_ref_sha1s()
+
+        self._old_rev_exclusion_spec = self._compute_rev_exclusion_spec(
+            other_ref_sha1s.union(
+                change.old.sha1
+                for change in self.changes
+                if change.old.type in ['commit', 'tag']
+                )
+            )
+        self._new_rev_exclusion_spec = self._compute_rev_exclusion_spec(
+            other_ref_sha1s.union(
+                change.new.sha1
+                for change in self.changes
+                if change.new.type in ['commit', 'tag']
+                )
+            )
+
+    @classmethod
+    def _sort_key(klass, change):
+        return (klass.SORT_ORDER[change.__class__, change.change_type], change.refname,)
+
+    def _compute_other_ref_sha1s(self):
+        """Return the GitObjects referred to by references unaffected by this push."""
+
+        # The refnames being changed by this push:
+        updated_refs = set(
+            change.refname
+            for change in self.changes
+            )
+
+        # The SHA-1s of commits referred to by all references in this
+        # repository *except* updated_refs:
+        sha1s = set()
+        fmt = (
+            '%(objectname) %(objecttype) %(refname)\n'
+            '%(*objectname) %(*objecttype) %(refname)'
+            )
+        for line in read_git_lines(['for-each-ref', '--format=%s' % (fmt,)]):
+            (sha1, type, name) = line.split(' ', 2)
+            if sha1 and type == 'commit' and name not in updated_refs:
+                sha1s.add(sha1)
+
+        return sha1s
+
+    def _compute_rev_exclusion_spec(self, sha1s):
+        """Return an exclusion specification for 'git rev-list'.
+
+        git_objects is an iterable over GitObject instances.  Return a
+        string that can be passed to the standard input of 'git
+        rev-list --stdin' to exclude all of the commits referred to by
+        git_objects."""
+
+        return ''.join(
+            ['^%s\n' % (sha1,) for sha1 in sorted(sha1s)]
+            )
+
+    def get_new_commits(self, reference_change=None):
+        """Return a list of commits added by this push.
+
+        Return a list of the object names of commits that were added
+        by the part of this push represented by reference_change.  If
+        reference_change is None, then return a list of *all* commits
+        added by this push."""
+
+        if not reference_change:
+            new_revs = sorted(
+                change.new.sha1
+                for change in self.changes
+                if change.new
+                )
+        elif not reference_change.new.commit_sha1:
+            return []
+        else:
+            new_revs = [reference_change.new.commit_sha1]
+
+        cmd = ['rev-list', '--stdin'] + new_revs
+        return read_git_lines(cmd, input=self._old_rev_exclusion_spec)
+
+    def get_discarded_commits(self, reference_change):
+        """Return a list of commits discarded by this push.
+
+        Return a list of the object names of commits that were
+        entirely discarded from the repository by the part of this
+        push represented by reference_change."""
+
+        if not reference_change.old.commit_sha1:
+            return []
+        else:
+            old_revs = [reference_change.old.commit_sha1]
+
+        cmd = ['rev-list', '--stdin'] + old_revs
+        return read_git_lines(cmd, input=self._new_rev_exclusion_spec)
+
+    def send_emails(self, mailer, body_filter=None):
+        """Use send all of the notification emails needed for this push.
+
+        Use send all of the notification emails (including reference
+        change emails and commit emails) needed for this push.  Send
+        the emails using mailer.  If body_filter is not None, then use
+        it to filter the lines that are intended for the email
+        body."""
+
+        # The sha1s of commits that were introduced by this push.
+        # They will be removed from this set as they are processed, to
+        # guarantee that one (and only one) email is generated for
+        # each new commit.
+        unhandled_sha1s = set(self.get_new_commits())
+        for change in self.changes:
+            # Check if we've got anyone to send to
+            if not change.recipients:
+                sys.stderr.write(
+                    '*** no recipients configured so no email will be sent\n'
+                    '*** for %r update %s->%s\n'
+                    % (change.refname, change.old.sha1, change.new.sha1,)
+                    )
+            else:
+                sys.stderr.write('Sending notification emails to: %s\n' % (change.recipients,))
+                mailer.send(change.generate_email(self, body_filter), change.recipients)
+
+            sha1s = []
+            for sha1 in reversed(list(self.get_new_commits(change))):
+                if sha1 in unhandled_sha1s:
+                    sha1s.append(sha1)
+                    unhandled_sha1s.remove(sha1)
+
+            max_emails = change.environment.maxcommitemails
+            if max_emails and len(sha1s) > max_emails:
+                sys.stderr.write(
+                    '*** Too many new commits (%d), not sending commit emails.\n' % len(sha1s)
+                    + '*** Try setting multimailhook.maxCommitEmails to a greater value\n'
+                    + '*** Currently, multimailhook.maxCommitEmails=%d\n' % max_emails
+                    )
+                return
+
+            for (num, sha1) in enumerate(sha1s):
+                rev = Revision(change, GitObject(sha1), num=num+1, tot=len(sha1s))
+                if rev.recipients:
+                    mailer.send(rev.generate_email(self, body_filter), rev.recipients)
+
+        # Consistency check:
+        if unhandled_sha1s:
+            sys.stderr.write(
+                'ERROR: No emails were sent for the following new commits:\n'
+                '    %s\n'
+                % ('\n    '.join(sorted(unhandled_sha1s)),)
+                )
+
+
+def run_as_post_receive_hook(environment, mailer):
+    changes = []
+    for line in sys.stdin:
+        (oldrev, newrev, refname) = line.strip().split(' ', 2)
+        changes.append(
+            ReferenceChange.create(environment, oldrev, newrev, refname)
+            )
+    push = Push(changes)
+    push.send_emails(mailer, body_filter=environment.filter_body)
+
+
+def run_as_update_hook(environment, mailer, refname, oldrev, newrev):
+    changes = [
+        ReferenceChange.create(
+            environment,
+            read_git_output(['rev-parse', '--verify', oldrev]),
+            read_git_output(['rev-parse', '--verify', newrev]),
+            refname,
+            ),
+        ]
+    push = Push(changes)
+    push.send_emails(mailer, body_filter=environment.filter_body)
+
+
+def choose_mailer(config, environment):
+    mailer = config.get('mailer', default='sendmail')
+
+    if mailer == 'smtp':
+        smtpserver = config.get('smtpserver', default='localhost')
+        mailer = SMTPMailer(
+            envelopesender=(environment.get_sender() or environment.get_fromaddr()),
+            smtpserver=smtpserver,
+            )
+    elif mailer == 'sendmail':
+        command = config.get('sendmailcommand')
+        if command:
+            command = shlex.split(command)
+        mailer = SendMailer(command=command, envelopesender=environment.get_sender())
+    else:
+        sys.stderr.write(
+            'fatal: multimailhook.mailer is set to an incorrect value: "%s"\n' % mailer
+            + 'please use one of "smtp" or "sendmail".\n'
+            )
+        sys.exit(1)
+    return mailer
+
+
+KNOWN_ENVIRONMENTS = {
+    'generic' : GenericEnvironmentMixin,
+    'gitolite' : GitoliteEnvironmentMixin,
+    }
+
+
+def choose_environment(config, osenv=None, env=None, recipients=None):
+    if not osenv:
+        osenv = os.environ
+
+    environment_mixins = [
+        ProjectdescEnvironmentMixin,
+        ConfigMaxlinesEnvironmentMixin,
+        ConfigFilterLinesEnvironmentMixin,
+        PusherDomainEnvironmentMixin,
+        ConfigOptionsEnvironmentMixin,
+        ]
+    environment_kw = {
+        'osenv' : osenv,
+        'config' : config,
+        }
+
+    if not env:
+        env = config.get('environment')
+
+    if not env:
+        if 'GL_USER' in osenv and 'GL_REPO' in osenv:
+            env = 'gitolite'
+        else:
+            env = 'generic'
+
+    environment_mixins.append(KNOWN_ENVIRONMENTS[env])
+
+    if recipients:
+        environment_mixins.insert(0, StaticRecipientsEnvironmentMixin)
+        environment_kw['refchange_recipients'] = recipients
+        environment_kw['announce_recipients'] = recipients
+        environment_kw['revision_recipients'] = recipients
+    else:
+        environment_mixins.insert(0, ConfigRecipientsEnvironmentMixin)
+
+    environment_klass = type(
+        'EffectiveEnvironment',
+        tuple(environment_mixins) + (Environment,),
+        {},
+        )
+    return environment_klass(**environment_kw)
+
+
+def main(args):
+    parser = optparse.OptionParser(
+        description=__doc__,
+        usage='%prog [OPTIONS]\n   or: %prog [OPTIONS] REFNAME OLDREV NEWREV',
+        )
+
+    parser.add_option(
+        '--environment', '--env', action='store', type='choice',
+        choices=['generic', 'gitolite'], default=None,
+        help=(
+            'Choose type of environment is in use.  Default is taken from '
+            'multimailhook.environment if set; otherwise "generic".'
+            ),
+        )
+    parser.add_option(
+        '--stdout', action='store_true', default=False,
+        help='Output emails to stdout rather than sending them.',
+        )
+    parser.add_option(
+        '--recipients', action='store', default=None,
+        help='Set list of email recipients for all types of emails.',
+        )
+    parser.add_option(
+        '--show-env', action='store_true', default=False,
+        help=(
+            'Write to stderr the values determined for the environment '
+            '(intended for debugging purposes).'
+            ),
+        )
+
+    (options, args) = parser.parse_args(args)
+
+    config = Config('multimailhook')
+
+    try:
+        environment = choose_environment(
+            config, osenv=os.environ,
+            env=options.environment,
+            recipients=options.recipients,
+            )
+
+        if options.show_env:
+            sys.stderr.write('Environment values:\n')
+            for (k,v) in sorted(environment.get_values().items()):
+                sys.stderr.write('    %s : %r\n' % (k,v))
+            sys.stderr.write('\n')
+
+        if options.stdout:
+            mailer = OutputMailer(sys.stdout)
+        else:
+            mailer = choose_mailer(config, environment)
+
+        # Dual mode: if arguments were specified on the command line, run
+        # like an update hook; otherwise, run as a post-receive hook.
+        if args:
+            if len(args) != 3:
+                parser.error('Need zero or three non-option arguments')
+            (refname, oldrev, newrev) = args
+            run_as_update_hook(environment, mailer, refname, oldrev, newrev)
+        else:
+            run_as_post_receive_hook(environment, mailer)
+    except ConfigurationException, e:
+        sys.exit(str(e))
+
+
+if __name__ == '__main__':
+    main(sys.argv[1:])
diff --git a/contrib/hooks/multimail/migrate-mailhook-config b/contrib/hooks/multimail/migrate-mailhook-config
new file mode 100755 (executable)
index 0000000..04eeaac
--- /dev/null
@@ -0,0 +1,269 @@
+#! /usr/bin/env python2
+
+"""Migrate a post-receive-email configuration to be usable with git_multimail.py.
+
+See README.migrate-from-post-receive-email for more information.
+
+"""
+
+import sys
+import optparse
+
+from git_multimail import CommandError
+from git_multimail import Config
+from git_multimail import read_output
+
+
+OLD_NAMES = [
+    'mailinglist',
+    'announcelist',
+    'envelopesender',
+    'emailprefix',
+    'showrev',
+    'emailmaxlines',
+    'diffopts',
+    ]
+
+NEW_NAMES = [
+    'environment',
+    'reponame',
+    'mailinglist',
+    'refchangelist',
+    'commitlist',
+    'announcelist',
+    'announceshortlog',
+    'envelopesender',
+    'administrator',
+    'emailprefix',
+    'emailmaxlines',
+    'diffopts',
+    'emaildomain',
+    ]
+
+
+INFO = """\
+
+SUCCESS!
+
+Your post-receive-email configuration has been converted to
+git-multimail format.  Please see README and
+README.migrate-from-post-receive-email to learn about other
+git-multimail configuration possibilities.
+
+For example, git-multimail has the following new options with no
+equivalent in post-receive-email.  You might want to read about them
+to see if they would be useful in your situation:
+
+"""
+
+
+def _check_old_config_exists(old):
+    """Check that at least one old configuration value is set."""
+
+    for name in OLD_NAMES:
+        if old.has_key(name):
+            return True
+
+    return False
+
+
+def _check_new_config_clear(new):
+    """Check that none of the new configuration names are set."""
+
+    retval = True
+    for name in NEW_NAMES:
+        if new.has_key(name):
+            if retval:
+                sys.stderr.write('INFO: The following configuration values already exist:\n\n')
+            sys.stderr.write('    "%s.%s"\n' % (new.section, name))
+            retval = False
+
+    return retval
+
+
+def erase_values(config, names):
+    for name in names:
+        if config.has_key(name):
+            try:
+                sys.stderr.write('...unsetting "%s.%s"\n' % (config.section, name))
+                config.unset_all(name)
+            except CommandError:
+                sys.stderr.write(
+                    '\nWARNING: could not unset "%s.%s".  '
+                    'Perhaps it is not set at the --local level?\n\n'
+                    % (config.section, name)
+                    )
+
+
+def is_section_empty(section, local):
+    """Return True iff the specified configuration section is empty.
+
+    Iff local is True, use the --local option when invoking 'git
+    config'."""
+
+    if local:
+        local_option = ['--local']
+    else:
+        local_option = []
+
+    try:
+        read_output(
+            ['git', 'config']
+            + local_option
+            + ['--get-regexp', '^%s\.' % (section,)]
+            )
+    except CommandError, e:
+        if e.retcode == 1:
+            # This means that no settings were found.
+            return True
+        else:
+            raise
+    else:
+        return False
+
+
+def remove_section_if_empty(section):
+    """If the specified configuration section is empty, delete it."""
+
+    try:
+        empty = is_section_empty(section, local=True)
+    except CommandError:
+        # Older versions of git do not support the --local option, so
+        # if the first attempt fails, try without --local.
+        try:
+            empty = is_section_empty(section, local=False)
+        except CommandError:
+            sys.stderr.write(
+                '\nINFO: If configuration section "%s.*" is empty, you might want '
+                'to delete it.\n\n'
+                % (section,)
+                )
+            return
+
+    if empty:
+        sys.stderr.write('...removing section "%s.*"\n' % (section,))
+        read_output(['git', 'config', '--remove-section', section])
+    else:
+        sys.stderr.write(
+            '\nINFO: Configuration section "%s.*" still has contents.  '
+            'It will not be deleted.\n\n'
+            % (section,)
+            )
+
+
+def migrate_config(strict=False, retain=False, overwrite=False):
+    old = Config('hooks')
+    new = Config('multimailhook')
+    if not _check_old_config_exists(old):
+        sys.exit(
+            'Your repository has no post-receive-email configuration.  '
+            'Nothing to do.'
+            )
+    if not _check_new_config_clear(new):
+        if overwrite:
+            sys.stderr.write('\nWARNING: Erasing the above values...\n\n')
+            erase_values(new, NEW_NAMES)
+        else:
+            sys.exit(
+                '\nERROR: Refusing to overwrite existing values.  Use the --overwrite\n'
+                'option to continue anyway.'
+                )
+
+    name = 'showrev'
+    if old.has_key(name):
+        msg = 'git-multimail does not support "%s.%s"' % (old.section, name,)
+        if strict:
+            sys.exit(
+                'ERROR: %s.\n'
+                'Please unset that value then try again, or run without --strict.'
+                % (msg,)
+                )
+        else:
+            sys.stderr.write('\nWARNING: %s (ignoring).\n\n' % (msg,))
+
+    for name in ['mailinglist', 'announcelist']:
+        if old.has_key(name):
+            sys.stderr.write(
+                '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name)
+                )
+            new.set_recipients(name, old.get_recipients(name))
+
+    if strict:
+        sys.stderr.write(
+            '...setting "%s.commitlist" to the empty string\n' % (new.section,)
+            )
+        new.set_recipients('commitlist', '')
+        sys.stderr.write(
+            '...setting "%s.announceshortlog" to "true"\n' % (new.section,)
+            )
+        new.set('announceshortlog', 'true')
+
+    for name in ['envelopesender', 'emailmaxlines', 'diffopts']:
+        if old.has_key(name):
+            sys.stderr.write(
+                '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name)
+                )
+            new.set(name, old.get(name))
+
+    name = 'emailprefix'
+    if old.has_key(name):
+        sys.stderr.write(
+            '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name)
+            )
+        new.set(name, old.get(name))
+    elif strict:
+        sys.stderr.write(
+            '...setting "%s.%s" to "[SCM]" to preserve old subject lines\n'
+            % (new.section, name)
+            )
+        new.set(name, '[SCM]')
+
+    if not retain:
+        erase_values(old, OLD_NAMES)
+        remove_section_if_empty(old.section)
+
+    sys.stderr.write(INFO)
+    for name in NEW_NAMES:
+        if name not in OLD_NAMES:
+            sys.stderr.write('    "%s.%s"\n' % (new.section, name,))
+    sys.stderr.write('\n')
+
+
+def main(args):
+    parser = optparse.OptionParser(
+        description=__doc__,
+        usage='%prog [OPTIONS]',
+        )
+
+    parser.add_option(
+        '--strict', action='store_true', default=False,
+        help=(
+            'Slavishly configure git-multimail as closely as possible to '
+            'the post-receive-email configuration.  Default is to turn '
+            'on some new features that have no equivalent in post-receive-email.'
+            ),
+        )
+    parser.add_option(
+        '--retain', action='store_true', default=False,
+        help=(
+            'Retain the post-receive-email configuration values.  '
+            'Default is to delete them after the new values are set.'
+            ),
+        )
+    parser.add_option(
+        '--overwrite', action='store_true', default=False,
+        help=(
+            'Overwrite any existing git-multimail configuration settings.  '
+            'Default is to abort if such settings already exist.'
+            ),
+        )
+
+    (options, args) = parser.parse_args(args)
+
+    if args:
+        parser.error('Unexpected arguments: %s' % (' '.join(args),))
+
+    migrate_config(strict=options.strict, retain=options.retain, overwrite=options.overwrite)
+
+
+main(sys.argv[1:])
diff --git a/contrib/hooks/multimail/post-receive b/contrib/hooks/multimail/post-receive
new file mode 100755 (executable)
index 0000000..93ebb43
--- /dev/null
@@ -0,0 +1,90 @@
+#! /usr/bin/env python2
+
+"""Example post-receive hook based on git-multimail.
+
+This script is a simple example of a post-receive hook implemented
+using git_multimail.py as a Python module.  It is intended to be
+customized before use; see the comments in the script to help you get
+started.
+
+It is possible to use git_multimail.py itself as a post-receive or
+update hook, configured via git config settings and/or command-line
+parameters.  But for more flexibility, it can also be imported as a
+Python module by a custom post-receive script as done here.  The
+latter has the following advantages:
+
+* The tool's behavior can be customized using arbitrary Python code,
+  without having to edit git_multimail.py.
+
+* Configuration settings can be read from other sources; for example,
+  user names and email addresses could be read from LDAP or from a
+  database.  Or the settings can even be hardcoded in the importing
+  Python script, if this is preferred.
+
+This script is a very basic example of how to use git_multimail.py as
+a module.  The comments below explain some of the points at which the
+script's behavior could be changed or customized.
+
+"""
+
+import sys
+import os
+
+# If necessary, add the path to the directory containing
+# git_multimail.py to the Python path as follows.  (This is not
+# necessary if git_multimail.py is in the same directory as this
+# script):
+
+#LIBDIR = 'path/to/directory/containing/module'
+#sys.path.insert(0, LIBDIR)
+
+import git_multimail
+
+
+# It is possible to modify the output templates here; e.g.:
+
+#git_multimail.FOOTER_TEMPLATE = """\
+#
+#-- \n\
+#This email was generated by the wonderful git-multimail tool.
+#"""
+
+
+# Specify which "git config" section contains the configuration for
+# git-multimail:
+config = git_multimail.Config('multimailhook')
+
+
+# Select the type of environment:
+environment = git_multimail.GenericEnvironment(config=config)
+#environment = git_multimail.GitoliteEnvironment(config=config)
+
+
+# Choose the method of sending emails based on the git config:
+mailer = git_multimail.choose_mailer(config, environment)
+
+# Alternatively, you may hardcode the mailer using code like one of
+# the following:
+
+# Use "/usr/sbin/sendmail -t" to send emails.  The envelopesender
+# argument is optional:
+#mailer = git_multimail.SendMailer(
+#    command=['/usr/sbin/sendmail', '-t'],
+#    envelopesender='git-repo@example.com',
+#    )
+
+# Use Python's smtplib to send emails.  Both arguments are required.
+#mailer = git_multimail.SMTPMailer(
+#    envelopesender='git-repo@example.com',
+#    # The smtpserver argument can also include a port number; e.g.,
+#    #     smtpserver='mail.example.com:25'
+#    smtpserver='mail.example.com',
+#    )
+
+# OutputMailer is intended only for testing; it writes the emails to
+# the specified file stream.
+#mailer = git_multimail.OutputMailer(sys.stdout)
+
+
+# Read changes from stdin and send notification emails:
+git_multimail.run_as_post_receive_hook(environment, mailer)
index 0e5b72d7f19185c3b6b36150085d475c7ef15db4..153115029df0764a3f42812a9a2bd8992360c4bd 100755 (executable)
@@ -2,10 +2,19 @@
 #
 # Copyright (c) 2007 Andy Parkins
 #
-# An example hook script to mail out commit update information.  This hook
-# sends emails listing new revisions to the repository introduced by the
-# change being reported.  The rule is that (for branch updates) each commit
-# will appear on one email and one email only.
+# An example hook script to mail out commit update information.
+#
+# NOTE: This script is no longer under active development.  There
+# is another script, git-multimail, which is more capable and
+# configurable and is largely backwards-compatible with this script;
+# please see "contrib/hooks/multimail/".  For instructions on how to
+# migrate from post-receive-email to git-multimail, please see
+# "README.migrate-from-post-receive-email" in that directory.
+#
+# This hook sends emails listing new revisions to the repository
+# introduced by the change being reported.  The rule is that (for
+# branch updates) each commit will appear on one email and one email
+# only.
 #
 # This hook is stored in the contrib/hooks directory.  Your distribution
 # will have put this somewhere standard.  You should make this script
index b50750565ff0499dcbdf2f2b9802f109a830d797..435b2dea293b01daaad83e6905148be2256c02df 100644 (file)
@@ -30,6 +30,7 @@ $(GIT_SUBTREE): $(GIT_SUBTREE_SH)
 doc: $(GIT_SUBTREE_DOC)
 
 install: $(GIT_SUBTREE)
+       $(INSTALL) -d -m 755 $(DESTDIR)$(libexecdir)
        $(INSTALL) -m 755 $(GIT_SUBTREE) $(DESTDIR)$(libexecdir)
 
 install-doc: install-man
index 51ae932e5edc54db32fca93e8f6f8aca1bea32f4..7d7af03274ee0759c9e915a6ea7b52ecce65cf55 100755 (executable)
@@ -311,7 +311,7 @@ copy_commit()
                        GIT_COMMITTER_NAME \
                        GIT_COMMITTER_EMAIL \
                        GIT_COMMITTER_DATE
-               (echo -n "$annotate"; cat ) |
+               (printf "%s" "$annotate"; cat ) |
                git commit-tree "$2" $3  # reads the rest of stdin
        ) || die "Can't copy commit $1"
 }
index b0f8536fca58d910f85334727b89925651bfa8ae..66ce4b07c2dc2d2d56dc260883f86d8672d2ac69 100755 (executable)
@@ -182,9 +182,9 @@ test_expect_success 'merge new subproj history into subdir' '
 test_expect_success 'Check that prefix argument is required for split' '
         echo "You must provide the --prefix option." > expected &&
         test_must_fail git subtree split > actual 2>&1 &&
-        test_debug "echo -n expected: " &&
+       test_debug "printf '"'"'expected: '"'"'" &&
         test_debug "cat expected" &&
-        test_debug "echo -n actual: " &&
+       test_debug "printf '"'"'actual: '"'"'" &&
         test_debug "cat actual" &&
         test_cmp expected actual &&
         rm -f expected actual
@@ -193,9 +193,9 @@ test_expect_success 'Check that prefix argument is required for split' '
 test_expect_success 'Check that the <prefix> exists for a split' '
         echo "'"'"'non-existent-directory'"'"'" does not exist\; use "'"'"'git subtree add'"'"'" > expected &&
         test_must_fail git subtree split --prefix=non-existent-directory > actual 2>&1 &&
-        test_debug "echo -n expected: " &&
+       test_debug "printf '"'"'expected: '"'"'" &&
         test_debug "cat expected" &&
-        test_debug "echo -n actual: " &&
+       test_debug "printf '"'"'actual: '"'"'" &&
         test_debug "cat actual" &&
         test_cmp expected actual
 #        rm -f expected actual
index 973ec38fafd6e679eeebc7c013163fbefe57ec05..34916c5e105812e36d659dfeeb8341e003e746bd 100644 (file)
--- a/daemon.c
+++ b/daemon.c
@@ -760,7 +760,7 @@ static void handle(int incoming, struct sockaddr *addr, socklen_t addrlen)
                snprintf(portbuf, sizeof(portbuf), "REMOTE_PORT=%d",
                    ntohs(sin_addr->sin_port));
 #ifndef NO_IPV6
-       } else if (addr && addr->sa_family == AF_INET6) {
+       } else if (addr->sa_family == AF_INET6) {
                struct sockaddr_in6 *sin6_addr = (void *) addr;
 
                char *buf = addrbuf + 12;
diff --git a/diff.c b/diff.c
index 76c6d8df5a720fbc941896464be36b9bec4e6074..266112ca6104450497fb9be504c2404cced595f0 100644 (file)
--- a/diff.c
+++ b/diff.c
@@ -1683,9 +1683,7 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
                del = deleted;
 
                if (graph_width <= max_change) {
-                       int total = add + del;
-
-                       total = scale_linear(add + del, graph_width, max_change);
+                       int total = scale_linear(add + del, graph_width, max_change);
                        if (total < 2 && add && del)
                                /* width >= 2 due to the sanity check */
                                total = 2;
index 0cb67b22cf5a8754daf429b3dbe45ef3e663650b..5398c36dd4dc2c1d7ded3f92c217a99a240a0a23 100644 (file)
@@ -22,6 +22,7 @@ int prefer_symlink_refs;
 int is_bare_repository_cfg = -1; /* unspecified */
 int log_all_ref_updates = -1; /* unspecified */
 int warn_ambiguous_refs = 1;
+int warn_on_object_refname_ambiguity = 1;
 int repository_format_version;
 const char *git_commit_encoding;
 const char *git_log_output_encoding;
index 0039ecfb407960444c31d48767b65cf773a3243a..8d7659a22c253b9f9535aee38e3bf0ce380cef75 100755 (executable)
@@ -159,7 +159,7 @@ finish_rebase () {
                        die "$(eval_gettext "Cannot store \$stash_sha1")"
                        gettext 'Applying autostash resulted in conflicts.
 Your changes are safe in the stash.
-You can run "git stash pop" or "git stash drop" it at any time.
+You can run "git stash pop" or "git stash drop" at any time.
 '
                fi
        fi
index 8cc29a0000f8456331457c0d247c242f3113517e..c2d01dccc2a12a767c442de58ed78df130699d74 100644 (file)
@@ -110,12 +110,14 @@ static void range_set_check_invariants(struct range_set *rs)
 static void sort_and_merge_range_set(struct range_set *rs)
 {
        int i;
-       int o = 1; /* output cursor */
+       int o = 0; /* output cursor */
 
        qsort(rs->ranges, rs->nr, sizeof(struct range), range_cmp);
 
-       for (i = 1; i < rs->nr; i++) {
-               if (rs->ranges[i].start <= rs->ranges[o-1].end) {
+       for (i = 0; i < rs->nr; i++) {
+               if (rs->ranges[i].start == rs->ranges[i].end)
+                       continue;
+               if (o > 0 && rs->ranges[i].start <= rs->ranges[o-1].end) {
                        if (rs->ranges[o-1].end < rs->ranges[i].end)
                                rs->ranges[o-1].end = rs->ranges[i].end;
                } else {
@@ -297,6 +299,7 @@ static void line_log_data_insert(struct line_log_data **list,
        p = xcalloc(1, sizeof(struct line_log_data));
        p->path = path;
        range_set_append(&p->ranges, begin, end);
+       sort_and_merge_range_set(&p->ranges);
        if (ip) {
                p->next = ip->next;
                ip->next = p;
diff --git a/refs.c b/refs.c
index 43022066499ffaf828ac31e82b77f53539bc232d..7b08a37153457d335ecf676a73252aa5bdbee4fb 100644 (file)
--- a/refs.c
+++ b/refs.c
@@ -634,7 +634,9 @@ struct ref_entry_cb {
 static int do_one_ref(struct ref_entry *entry, void *cb_data)
 {
        struct ref_entry_cb *data = cb_data;
+       struct ref_entry *old_current_ref;
        int retval;
+
        if (prefixcmp(entry->name, data->base))
                return 0;
 
@@ -642,10 +644,12 @@ static int do_one_ref(struct ref_entry *entry, void *cb_data)
              !ref_resolves_to_object(entry))
                return 0;
 
+       /* Store the old value, in case this is a recursive call: */
+       old_current_ref = current_ref;
        current_ref = entry;
        retval = data->fn(entry->name + data->trim, entry->u.value.sha1,
                          entry->flag, data->cb_data);
-       current_ref = NULL;
+       current_ref = old_current_ref;
        return retval;
 }
 
@@ -2174,11 +2178,14 @@ int lock_packed_refs(int flags)
 {
        struct packed_ref_cache *packed_ref_cache;
 
-       /* Discard the old cache because it might be invalid: */
-       clear_packed_ref_cache(&ref_cache);
        if (hold_lock_file_for_update(&packlock, git_path("packed-refs"), flags) < 0)
                return -1;
-       /* Read the current packed-refs while holding the lock: */
+       /*
+        * Get the current packed-refs while holding the lock.  If the
+        * packed-refs file has been modified since we last read it,
+        * this will automatically invalidate the cache and re-read
+        * the packed-refs file.
+        */
        packed_ref_cache = get_packed_ref_cache(&ref_cache);
        packed_ref_cache->lock = &packlock;
        /* Increment the reference count to prevent it from being freed: */
index 4c2365f48f7ce361e009d5f4d9323184f6782d34..8e27db1bd2b49f28b235fcd7e18a0dda43a1f045 100644 (file)
@@ -1306,6 +1306,26 @@ static int git_open_noatime(const char *name)
        }
 }
 
+static int stat_sha1_file(const unsigned char *sha1, struct stat *st)
+{
+       char *name = sha1_file_name(sha1);
+       struct alternate_object_database *alt;
+
+       if (!lstat(name, st))
+               return 0;
+
+       prepare_alt_odb();
+       errno = ENOENT;
+       for (alt = alt_odb_list; alt; alt = alt->next) {
+               name = alt->name;
+               fill_sha1_path(name, sha1);
+               if (!lstat(alt->base, st))
+                       return 0;
+       }
+
+       return -1;
+}
+
 static int open_sha1_file(const unsigned char *sha1)
 {
        int fd;
@@ -1693,52 +1713,21 @@ static int retry_bad_packed_offset(struct packed_git *p, off_t obj_offset)
        return type;
 }
 
-
 #define POI_STACK_PREALLOC 64
 
-static int packed_object_info(struct packed_git *p, off_t obj_offset,
-                             unsigned long *sizep, int *rtype,
-                             unsigned long *disk_sizep)
+static enum object_type packed_to_object_type(struct packed_git *p,
+                                             off_t obj_offset,
+                                             enum object_type type,
+                                             struct pack_window **w_curs,
+                                             off_t curpos)
 {
-       struct pack_window *w_curs = NULL;
-       unsigned long size;
-       off_t curpos = obj_offset;
-       enum object_type type;
        off_t small_poi_stack[POI_STACK_PREALLOC];
        off_t *poi_stack = small_poi_stack;
        int poi_stack_nr = 0, poi_stack_alloc = POI_STACK_PREALLOC;
 
-       type = unpack_object_header(p, &w_curs, &curpos, &size);
-
-       if (rtype)
-               *rtype = type; /* representation type */
-
-       if (sizep) {
-               if (type == OBJ_OFS_DELTA || type == OBJ_REF_DELTA) {
-                       off_t tmp_pos = curpos;
-                       off_t base_offset = get_delta_base(p, &w_curs, &tmp_pos,
-                                                          type, obj_offset);
-                       if (!base_offset) {
-                               type = OBJ_BAD;
-                               goto out;
-                       }
-                       *sizep = get_size_from_delta(p, &w_curs, tmp_pos);
-                       if (*sizep == 0) {
-                               type = OBJ_BAD;
-                               goto out;
-                       }
-               } else {
-                       *sizep = size;
-               }
-       }
-
-       if (disk_sizep) {
-               struct revindex_entry *revidx = find_pack_revindex(p, obj_offset);
-               *disk_sizep = revidx[1].offset - obj_offset;
-       }
-
        while (type == OBJ_OFS_DELTA || type == OBJ_REF_DELTA) {
                off_t base_offset;
+               unsigned long size;
                /* Push the object we're going to leave behind */
                if (poi_stack_nr >= poi_stack_alloc && poi_stack == small_poi_stack) {
                        poi_stack_alloc = alloc_nr(poi_stack_nr);
@@ -1749,11 +1738,11 @@ static int packed_object_info(struct packed_git *p, off_t obj_offset,
                }
                poi_stack[poi_stack_nr++] = obj_offset;
                /* If parsing the base offset fails, just unwind */
-               base_offset = get_delta_base(p, &w_curs, &curpos, type, obj_offset);
+               base_offset = get_delta_base(p, w_curs, &curpos, type, obj_offset);
                if (!base_offset)
                        goto unwind;
                curpos = obj_offset = base_offset;
-               type = unpack_object_header(p, &w_curs, &curpos, &size);
+               type = unpack_object_header(p, w_curs, &curpos, &size);
                if (type <= OBJ_NONE) {
                        /* If getting the base itself fails, we first
                         * retry the base, otherwise unwind */
@@ -1780,7 +1769,6 @@ static int packed_object_info(struct packed_git *p, off_t obj_offset,
 out:
        if (poi_stack != small_poi_stack)
                free(poi_stack);
-       unuse_pack(&w_curs);
        return type;
 
 unwind:
@@ -1794,6 +1782,57 @@ static int packed_object_info(struct packed_git *p, off_t obj_offset,
        goto out;
 }
 
+static int packed_object_info(struct packed_git *p, off_t obj_offset,
+                             struct object_info *oi)
+{
+       struct pack_window *w_curs = NULL;
+       unsigned long size;
+       off_t curpos = obj_offset;
+       enum object_type type;
+
+       /*
+        * We always get the representation type, but only convert it to
+        * a "real" type later if the caller is interested.
+        */
+       type = unpack_object_header(p, &w_curs, &curpos, &size);
+
+       if (oi->sizep) {
+               if (type == OBJ_OFS_DELTA || type == OBJ_REF_DELTA) {
+                       off_t tmp_pos = curpos;
+                       off_t base_offset = get_delta_base(p, &w_curs, &tmp_pos,
+                                                          type, obj_offset);
+                       if (!base_offset) {
+                               type = OBJ_BAD;
+                               goto out;
+                       }
+                       *oi->sizep = get_size_from_delta(p, &w_curs, tmp_pos);
+                       if (*oi->sizep == 0) {
+                               type = OBJ_BAD;
+                               goto out;
+                       }
+               } else {
+                       *oi->sizep = size;
+               }
+       }
+
+       if (oi->disk_sizep) {
+               struct revindex_entry *revidx = find_pack_revindex(p, obj_offset);
+               *oi->disk_sizep = revidx[1].offset - obj_offset;
+       }
+
+       if (oi->typep) {
+               *oi->typep = packed_to_object_type(p, obj_offset, type, &w_curs, curpos);
+               if (*oi->typep < 0) {
+                       type = OBJ_BAD;
+                       goto out;
+               }
+       }
+
+out:
+       unuse_pack(&w_curs);
+       return type;
+}
+
 static void *unpack_compressed_entry(struct packed_git *p,
                                    struct pack_window **w_curs,
                                    off_t curpos,
@@ -2363,8 +2402,8 @@ struct packed_git *find_sha1_pack(const unsigned char *sha1,
 
 }
 
-static int sha1_loose_object_info(const unsigned char *sha1, unsigned long *sizep,
-                                 unsigned long *disk_sizep)
+static int sha1_loose_object_info(const unsigned char *sha1,
+                                 struct object_info *oi)
 {
        int status;
        unsigned long mapsize, size;
@@ -2372,21 +2411,37 @@ static int sha1_loose_object_info(const unsigned char *sha1, unsigned long *size
        git_zstream stream;
        char hdr[32];
 
+       /*
+        * If we don't care about type or size, then we don't
+        * need to look inside the object at all.
+        */
+       if (!oi->typep && !oi->sizep) {
+               if (oi->disk_sizep) {
+                       struct stat st;
+                       if (stat_sha1_file(sha1, &st) < 0)
+                               return -1;
+                       *oi->disk_sizep = st.st_size;
+               }
+               return 0;
+       }
+
        map = map_sha1_file(sha1, &mapsize);
        if (!map)
                return -1;
-       if (disk_sizep)
-               *disk_sizep = mapsize;
+       if (oi->disk_sizep)
+               *oi->disk_sizep = mapsize;
        if (unpack_sha1_header(&stream, map, mapsize, hdr, sizeof(hdr)) < 0)
                status = error("unable to unpack %s header",
                               sha1_to_hex(sha1));
        else if ((status = parse_sha1_header(hdr, &size)) < 0)
                status = error("unable to parse %s header", sha1_to_hex(sha1));
-       else if (sizep)
-               *sizep = size;
+       else if (oi->sizep)
+               *oi->sizep = size;
        git_inflate_end(&stream);
        munmap(map, mapsize);
-       return status;
+       if (oi->typep)
+               *oi->typep = status;
+       return 0;
 }
 
 /* returns enum object_type or negative */
@@ -2394,37 +2449,37 @@ int sha1_object_info_extended(const unsigned char *sha1, struct object_info *oi)
 {
        struct cached_object *co;
        struct pack_entry e;
-       int status, rtype;
+       int rtype;
 
        co = find_cached_object(sha1);
        if (co) {
+               if (oi->typep)
+                       *(oi->typep) = co->type;
                if (oi->sizep)
                        *(oi->sizep) = co->size;
                if (oi->disk_sizep)
                        *(oi->disk_sizep) = 0;
                oi->whence = OI_CACHED;
-               return co->type;
+               return 0;
        }
 
        if (!find_pack_entry(sha1, &e)) {
                /* Most likely it's a loose object. */
-               status = sha1_loose_object_info(sha1, oi->sizep, oi->disk_sizep);
-               if (status >= 0) {
+               if (!sha1_loose_object_info(sha1, oi)) {
                        oi->whence = OI_LOOSE;
-                       return status;
+                       return 0;
                }
 
                /* Not a loose object; someone else may have just packed it. */
                reprepare_packed_git();
                if (!find_pack_entry(sha1, &e))
-                       return status;
+                       return -1;
        }
 
-       status = packed_object_info(e.p, e.offset, oi->sizep, &rtype,
-                                   oi->disk_sizep);
-       if (status < 0) {
+       rtype = packed_object_info(e.p, e.offset, oi);
+       if (rtype < 0) {
                mark_bad_packed_object(e.p, sha1);
-               status = sha1_object_info_extended(sha1, oi);
+               return sha1_object_info_extended(sha1, oi);
        } else if (in_delta_base_cache(e.p, e.offset)) {
                oi->whence = OI_DBCACHED;
        } else {
@@ -2435,15 +2490,19 @@ int sha1_object_info_extended(const unsigned char *sha1, struct object_info *oi)
                                         rtype == OBJ_OFS_DELTA);
        }
 
-       return status;
+       return 0;
 }
 
 int sha1_object_info(const unsigned char *sha1, unsigned long *sizep)
 {
-       struct object_info oi = {0};
+       enum object_type type;
+       struct object_info oi = {NULL};
 
+       oi.typep = &type;
        oi.sizep = sizep;
-       return sha1_object_info_extended(sha1, &oi);
+       if (sha1_object_info_extended(sha1, &oi) < 0)
+               return -1;
+       return type;
 }
 
 static void *read_packed_sha1(const unsigned char *sha1,
index 6f7d4d13d7393968b7c82a7fc51505296b5b67d3..852dd951dc2d93660727de23ea9467503288e25b 100644 (file)
@@ -445,20 +445,22 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
        "\n"
        "where \"$br\" is somehow empty and a 40-hex ref is created. Please\n"
        "examine these refs and maybe delete them. Turn this message off by\n"
-       "running \"git config advice.object_name_warning false\"");
+       "running \"git config advice.objectNameWarning false\"");
        unsigned char tmp_sha1[20];
        char *real_ref = NULL;
        int refs_found = 0;
        int at, reflog_len, nth_prior = 0;
 
        if (len == 40 && !get_sha1_hex(str, sha1)) {
-               refs_found = dwim_ref(str, len, tmp_sha1, &real_ref);
-               if (refs_found > 0 && warn_ambiguous_refs) {
-                       warning(warn_msg, len, str);
-                       if (advice_object_name_warning)
-                               fprintf(stderr, "%s\n", _(object_name_msg));
+               if (warn_on_object_refname_ambiguity) {
+                       refs_found = dwim_ref(str, len, tmp_sha1, &real_ref);
+                       if (refs_found > 0 && warn_ambiguous_refs) {
+                               warning(warn_msg, len, str);
+                               if (advice_object_name_warning)
+                                       fprintf(stderr, "%s\n", _(object_name_msg));
+                       }
+                       free(real_ref);
                }
-               free(real_ref);
                return 0;
        }
 
index cac282f06b3751d8f316653daa79026c9633629c..debe904523252ae4fd2e18784641575c3f828dd2 100644 (file)
@@ -111,11 +111,11 @@ static enum input_source istream_source(const unsigned char *sha1,
        unsigned long size;
        int status;
 
+       oi->typep = type;
        oi->sizep = &size;
        status = sha1_object_info_extended(sha1, oi);
        if (status < 0)
                return stream_error;
-       *type = status;
 
        switch (oi->whence) {
        case OI_LOOSE:
@@ -135,7 +135,7 @@ struct git_istream *open_istream(const unsigned char *sha1,
                                 struct stream_filter *filter)
 {
        struct git_istream *st;
-       struct object_info oi = {0};
+       struct object_info oi = {NULL};
        const unsigned char *real = lookup_replace_object(sha1);
        enum input_source src = istream_source(real, type, &oi);
 
@@ -149,7 +149,7 @@ struct git_istream *open_istream(const unsigned char *sha1,
                        return NULL;
                }
        }
-       if (st && filter) {
+       if (filter) {
                /* Add "&& !is_null_stream_filter(filter)" for performance */
                struct git_istream *nst = attach_stream_filter(st, filter);
                if (!nst)
index c61d5351e1ff0b1b4431eb28553d0d882ebe7451..f4eecaa17110c2974cfdedab90db4604b6923443 100644 (file)
@@ -161,7 +161,7 @@ test_perf () {
                echo "$test_count" >>"$perf_results_dir"/$base.subtests
                echo "$1" >"$perf_results_dir"/$base.$test_count.descr
                if test -z "$verbose"; then
-                       echo -n "perf $test_count - $1:"
+                       printf "%s" "perf $test_count - $1:"
                else
                        echo "perf $test_count - $1:"
                fi
@@ -170,7 +170,7 @@ test_perf () {
                        if test_run_perf_ "$2"
                        then
                                if test -z "$verbose"; then
-                                       echo -n " $i"
+                                       printf " %s" "$i"
                                else
                                        echo "* timing run $i/$GIT_PERF_REPEAT_COUNT:"
                                fi
index 2b17311cb0870ea210d9b5cbe167363d13641d67..5fd7bbb65244bdf6470508b6bc79aef58e79e86d 100755 (executable)
@@ -14,7 +14,7 @@ test_description='merge-recursive options
 . ./test-lib.sh
 
 test_have_prereq SED_STRIPS_CR && SED_OPTIONS=-b
-test_have_prereq MINGW && export GREP_OPTIONS=-U
+test_have_prereq GREP_STRIPS_CR && export GREP_OPTIONS=-U
 
 test_expect_success 'setup' '
        conflict_hunks () {
index 7776f93e3dfe1b6f822d5ee6f206bed5ba38c75c..7665d6785c1a91ed0171e8cac61174ebee9b9a37 100755 (executable)
@@ -64,4 +64,17 @@ test_bad_opts "-L 1,1000:b.c" "has only.*lines"
 test_bad_opts "-L :b.c" "argument.*not of the form"
 test_bad_opts "-L :foo:b.c" "no match"
 
+# There is a separate bug when an empty -L range is the first -L encountered,
+# thus to demonstrate this particular bug, the empty -L range must follow a
+# non-empty -L range.
+test_expect_success '-L {empty-range} (any -L)' '
+       n=$(expr $(wc -l <b.c) + 1) &&
+       git log -L1,1:b.c -L$n:b.c
+'
+
+test_expect_success '-L {empty-range} (first -L)' '
+       n=$(expr $(wc -l <b.c) + 1) &&
+       git log -L$n:b.c
+'
+
 test_done
index ef98d95e00d7d25ee408b311339cf4f10c44a5ad..9be9ae3436c929921bc0a2cd2a458e4e4500c154 100755 (executable)
@@ -5,7 +5,7 @@ test_description='test git-http-backend-noserver'
 
 HTTPD_DOCUMENT_ROOT_PATH="$TRASH_DIRECTORY"
 
-test_have_prereq MINGW && export GREP_OPTIONS=-U
+test_have_prereq GREP_STRIPS_CR && export GREP_OPTIONS=-U
 
 run_backend() {
        echo "$2" |
index 4e6055d06af2042e6bc6c1467d58680668c12881..3ae394e934d972d29286ecd25596eedd45bee902 100755 (executable)
@@ -17,7 +17,7 @@ test_expect_success 'setup' '
 
 '
 
-test_expect_success 'git clean -i (clean)' '
+test_expect_success 'git clean -i (c: clean hotkey)' '
 
        mkdir -p build docs &&
        touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \
@@ -38,12 +38,33 @@ test_expect_success 'git clean -i (clean)' '
 
 '
 
+test_expect_success 'git clean -i (cl: clean prefix)' '
+
+       mkdir -p build docs &&
+       touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \
+       docs/manual.txt obj.o build/lib.so &&
+       echo cl | git clean -i &&
+       test -f Makefile &&
+       test -f README &&
+       test -f src/part1.c &&
+       test -f src/part2.c &&
+       test ! -f a.out &&
+       test -f docs/manual.txt &&
+       test ! -f src/part3.c &&
+       test ! -f src/part3.h &&
+       test ! -f src/part4.c &&
+       test ! -f src/part4.h &&
+       test -f obj.o &&
+       test -f build/lib.so
+
+'
+
 test_expect_success 'git clean -i (quit)' '
 
        mkdir -p build docs &&
        touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \
        docs/manual.txt obj.o build/lib.so &&
-       echo q | git clean -i &&
+       echo quit | git clean -i &&
        test -f Makefile &&
        test -f README &&
        test -f src/part1.c &&
@@ -256,6 +277,21 @@ test_expect_success 'git clean -id (select - number 3)' '
 
 '
 
+test_expect_success 'git clean -id (select - filenames)' '
+
+       mkdir -p build docs &&
+       touch a.out foo.txt bar.txt baz.txt &&
+       (echo s; echo a.out fo ba bar; echo; echo c) | \
+       git clean -id &&
+       test -f Makefile &&
+       test ! -f a.out &&
+       test ! -f foo.txt &&
+       test ! -f bar.txt &&
+       test -f baz.txt &&
+       rm baz.txt
+
+'
+
 test_expect_success 'git clean -id (select - range)' '
 
        mkdir -p build docs &&
index 3ff5fb853c941586e3e5ed69a74a628dfe5bb84a..10aa028017833479c43bbb551741dfd36d9741aa 100755 (executable)
@@ -502,7 +502,7 @@ test_expect_success 'option --ff-only overwrites --no-ff' '
        test_must_fail git merge --no-ff --ff-only c2
 '
 
-test_expect_success 'option --ff-only overwrites merge.ff=only config' '
+test_expect_success 'option --no-ff overrides merge.ff=only config' '
        git reset --hard c0 &&
        test_config merge.ff only &&
        git merge --no-ff c1
index b490283182ce29f91519fd390f2a9639efa8c09c..1aa27bdbbf39bd937a6bb66a7eafeda16bb76246 100644 (file)
@@ -825,6 +825,7 @@ case $(uname -s) in
        test_set_prereq MINGW
        test_set_prereq NOT_CYGWIN
        test_set_prereq SED_STRIPS_CR
+       test_set_prereq GREP_STRIPS_CR
        ;;
 *CYGWIN*)
        test_set_prereq POSIXPERM
@@ -832,6 +833,7 @@ case $(uname -s) in
        test_set_prereq NOT_MINGW
        test_set_prereq CYGWIN
        test_set_prereq SED_STRIPS_CR
+       test_set_prereq GREP_STRIPS_CR
        ;;
 *)
        test_set_prereq POSIXPERM
index 18c48297652174ffae65b877dd131711a5746181..586e3bf94da1a2d69a061fe98aa1604f9223280b 100755 (executable)
@@ -15,13 +15,13 @@ else
        against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
 fi
 
-# If you want to allow non-ascii filenames set this variable to true.
+# If you want to allow non-ASCII filenames set this variable to true.
 allownonascii=$(git config hooks.allownonascii)
 
 # Redirect output to stderr.
 exec 1>&2
 
-# Cross platform projects tend to avoid non-ascii filenames; prevent
+# Cross platform projects tend to avoid non-ASCII filenames; prevent
 # them from being added to the repository. We exploit the fact that the
 # printable range starts at the space character and ends with tilde.
 if [ "$allownonascii" != "true" ] &&
@@ -31,18 +31,17 @@ if [ "$allownonascii" != "true" ] &&
        test $(git diff --cached --name-only --diff-filter=A -z $against |
          LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
 then
-       echo "Error: Attempt to add a non-ascii file name."
-       echo
-       echo "This can cause problems if you want to work"
-       echo "with people on other platforms."
-       echo
-       echo "To be portable it is advisable to rename the file ..."
-       echo
-       echo "If you know what you are doing you can disable this"
-       echo "check using:"
-       echo
-       echo "  git config hooks.allownonascii true"
-       echo
+       cat <<\EOF
+Error: Attempt to add a non-ASCII file name.
+
+This can cause problems if you want to work with people on other platforms.
+
+To be portable it is advisable to rename the file.
+
+If you know what you are doing you can disable this check using:
+
+  git config hooks.allownonascii true
+EOF
        exit 1
 fi
 
index 6e30ef9d048c62c11a92aa5b0ee6df2d227776e6..c626135234f2fbf2d8711e32a020d2e228a307b2 100644 (file)
@@ -323,7 +323,6 @@ static inline int prune_traversal(struct name_entry *e,
 
 int traverse_trees(int n, struct tree_desc *t, struct traverse_info *info)
 {
-       int ret = 0;
        int error = 0;
        struct name_entry *entry = xmalloc(n*sizeof(*entry));
        int i;
@@ -341,6 +340,7 @@ int traverse_trees(int n, struct tree_desc *t, struct traverse_info *info)
                strbuf_setlen(&base, info->pathlen);
        }
        for (;;) {
+               int trees_used;
                unsigned long mask, dirmask;
                const char *first = NULL;
                int first_len = 0;
@@ -404,15 +404,14 @@ int traverse_trees(int n, struct tree_desc *t, struct traverse_info *info)
                if (interesting < 0)
                        break;
                if (interesting) {
-                       ret = info->fn(n, mask, dirmask, entry, info);
-                       if (ret < 0) {
-                               error = ret;
+                       trees_used = info->fn(n, mask, dirmask, entry, info);
+                       if (trees_used < 0) {
+                               error = trees_used;
                                if (!info->show_all_errors)
                                        break;
                        }
-                       mask &= ret;
+                       mask &= trees_used;
                }
-               ret = 0;
                for (i = 0; i < n; i++)
                        if (mask & (1ul << i))
                                update_extended_entry(tx + i, entry + i);