Merge branch 'jk/warn-author-committer-after-commit'
authorJunio C Hamano <gitster@pobox.com>
Wed, 20 Jan 2010 22:40:12 +0000 (14:40 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 20 Jan 2010 22:40:12 +0000 (14:40 -0800)
* jk/warn-author-committer-after-commit:
user_ident_sufficiently_given(): refactor the logic to be usable from elsewhere
commit.c::print_summary: do not release the format string too early
commit: allow suppression of implicit identity advice
commit: show interesting ident information in summary
strbuf: add strbuf_addbuf_percentquote
strbuf_expand: convert "%%" to "%"

Conflicts:
builtin-commit.c
ident.c

1  2 
Documentation/config.txt
builtin-commit.c
daemon.c
strbuf.c
strbuf.h
t/t7501-commit.sh
diff --combined Documentation/config.txt
index 2d6775c135b466ce8a95db3fd7c22c04151f8d42,6f70cc953ebb52962d8106a7b8972f074894e164..d4332140a01635feab079e394f02d8c927cf37c5
@@@ -130,6 -130,10 +130,10 @@@ advice.*:
                Advice shown when linkgit:git-merge[1] refuses to
                merge to avoid overwritting local changes.
                Default: true.
+       implicitIdentity::
+               Advice on how to set your identity configuration when
+               your information is guessed from the system username and
+               domain name. Default: true.
  --
  
  core.fileMode::
@@@ -297,24 -301,17 +301,24 @@@ false), while all other repositories ar
  = true).
  
  core.worktree::
 -      Set the path to the working tree.  The value will not be
 -      used in combination with repositories found automatically in
 -      a .git directory (i.e. $GIT_DIR is not set).
 +      Set the path to the root of the work tree.
        This can be overridden by the GIT_WORK_TREE environment
        variable and the '--work-tree' command line option. It can be
 -      a absolute path or relative path to the directory specified by
 -      --git-dir or GIT_DIR.
 -      Note: If --git-dir or GIT_DIR are specified but none of
 +      an absolute path or a relative path to the .git directory,
 +      either specified by --git-dir or GIT_DIR, or automatically
 +      discovered.
 +      If --git-dir or GIT_DIR are specified but none of
        --work-tree, GIT_WORK_TREE and core.worktree is specified,
 -      the current working directory is regarded as the top directory
 -      of your working tree.
 +      the current working directory is regarded as the root of the
 +      work tree.
 ++
 +Note that this variable is honored even when set in a configuration
 +file in a ".git" subdirectory of a directory, and its value differs
 +from the latter directory (e.g. "/path/to/.git/config" has
 +core.worktree set to "/different/path"), which is most likely a
 +misconfiguration.  Running git commands in "/path/to" directory will
 +still use "/different/path" as the root of the work tree and can cause
 +great confusion to the users.
  
  core.logAllRefUpdates::
        Enable the reflog. Updates to a ref <ref> is logged to the file
@@@ -502,10 -499,6 +506,10 @@@ notes should be printed
  This setting defaults to "refs/notes/commits", and can be overridden by
  the `GIT_NOTES_REF` environment variable.
  
 +core.sparseCheckout::
 +      Enable "sparse checkout" feature. See section "Sparse checkout" in
 +      linkgit:git-read-tree[1] for more information.
 +
  add.ignore-errors::
        Tells 'git-add' to continue adding files when some files cannot be
        added due to indexing errors. Equivalent to the '--ignore-errors'
@@@ -541,7 -534,7 +545,7 @@@ apply.whitespace:
        as the '--whitespace' option. See linkgit:git-apply[1].
  
  branch.autosetupmerge::
 -      Tells 'git-branch' and 'git-checkout' to setup new branches
 +      Tells 'git-branch' and 'git-checkout' to set up new branches
        so that linkgit:git-pull[1] will appropriately merge from the
        starting point branch. Note that even if this option is not set,
        this behavior can be chosen per-branch using the `--track`
@@@ -716,11 -709,6 +720,11 @@@ color.ui:
        terminal. When more specific variables of color.* are set, they always
        take precedence over this setting. Defaults to false.
  
 +commit.status
 +      A boolean to enable/disable inclusion of status information in the
 +      commit message template when using an editor to prepare the commit
 +      message.  Defaults to true.
 +
  commit.template::
        Specify a file to use as the template for new commit messages.
        "{tilde}/" is expanded to the value of `$HOME` and "{tilde}user/" to the
@@@ -734,7 -722,7 +738,7 @@@ diff.autorefreshindex:
        contents in the work tree match the contents in the
        index.  This option defaults to true.  Note that this
        affects only 'git-diff' Porcelain, and not lower level
 -      'diff' commands, such as 'git-diff-files'.
 +      'diff' commands such as 'git-diff-files'.
  
  diff.external::
        If this config variable is set, diff generation is not
@@@ -850,8 -838,8 +854,8 @@@ format.pretty:
  
  format.thread::
        The default threading style for 'git-format-patch'.  Can be
 -      either a boolean value, `shallow` or `deep`.  `shallow`
 -      threading makes every mail a reply to the head of the series,
 +      a boolean value, or `shallow` or `deep`.  `shallow` threading
 +      makes every mail a reply to the head of the series,
        where the head is chosen from the cover letter, the
        `\--in-reply-to`, and the first patch mail, in this order.
        `deep` threading makes every mail a reply to the previous one.
@@@ -884,12 -872,15 +888,12 @@@ gc.autopacklimit:
        default value is 50.  Setting this to 0 disables it.
  
  gc.packrefs::
 -      'git-gc' does not run `git pack-refs` in a bare repository by
 -      default so that older dumb-transport clients can still fetch
 -      from the repository.  Setting this to `true` lets 'git-gc'
 -      to run `git pack-refs`.  Setting this to `false` tells
 -      'git-gc' never to run `git pack-refs`. The default setting is
 -      `notbare`. Enable it only when you know you do not have to
 -      support such clients.  The default setting will change to `true`
 -      at some stage, and setting this to `false` will continue to
 -      prevent `git pack-refs` from being run from 'git-gc'.
 +      Running `git pack-refs` in a repository renders it
 +      unclonable by Git versions prior to 1.5.1.2 over dumb
 +      transports such as HTTP.  This variable determines whether
 +      'git gc' runs `git pack-refs`. This can be set to "nobare"
 +      to enable it within all non-bare repos or it can be set to a
 +      boolean value.  The default is `true`.
  
  gc.pruneexpire::
        When 'git-gc' is run, it will call 'prune --expire 2.weeks.ago'.
@@@ -1145,12 -1136,6 +1149,12 @@@ http.maxRequests:
        How many HTTP requests to launch in parallel. Can be overridden
        by the 'GIT_HTTP_MAX_REQUESTS' environment variable. Default is 5.
  
 +http.minSessions::
 +      The number of curl sessions (counted across slots) to be kept across
 +      requests. They will not be ended with curl_easy_cleanup() until
 +      http_cleanup() is invoked. If USE_CURL_MULTI is not defined, this
 +      value will be capped at 1. Defaults to 1.
 +
  http.postBuffer::
        Maximum size in bytes of the buffer used by smart HTTP
        transports when POSTing data to the remote system.
@@@ -1480,10 -1465,6 +1484,10 @@@ remote.<name>.tagopt:
        Setting this value to \--no-tags disables automatic tag following when
        fetching from remote <name>
  
 +remote.<name>.vcs::
 +      Setting this to a value <vcs> will cause git to interact with
 +      the remote with the git-remote-<vcs> helper.
 +
  remotes.<group>::
        The list of remotes which are fetched by "git remote update
        <group>".  See linkgit:git-remote[1].
diff --combined builtin-commit.c
index 9b9dbaa5bf3a42c079d67902f1d8a125d0dacbba,29dc3df786831465e30dcbcec536de9e7367423b..42f11c30ca9e099cbed957107c25200efcc6ecd5
@@@ -24,7 -24,6 +24,7 @@@
  #include "string-list.h"
  #include "rerere.h"
  #include "unpack-trees.h"
 +#include "quote.h"
  
  static const char * const builtin_commit_usage[] = {
        "git commit [options] [--] <filepattern>...",
@@@ -36,7 -35,20 +36,20 @@@ static const char * const builtin_statu
        NULL
  };
  
 -static unsigned char head_sha1[20], merge_head_sha1[20];
+ static const char implicit_ident_advice[] =
+ "Your name and email address were configured automatically based\n"
+ "on your username and hostname. Please check that they are accurate.\n"
+ "You can suppress this message by setting them explicitly:\n"
+ "\n"
+ "    git config --global user.name Your Name\n"
+ "    git config --global user.email you@example.com\n"
+ "\n"
+ "If the identity used for this commit is wrong, you can fix it with:\n"
+ "\n"
+ "    git commit --amend --author='Your Name <you@example.com>'\n";
 +static unsigned char head_sha1[20];
  static char *use_message_buffer;
  static const char commit_editmsg[] = "COMMIT_EDITMSG";
  static struct lock_file index_lock; /* real index */
@@@ -53,7 -65,7 +66,7 @@@ static char *edit_message, *use_message
  static char *author_name, *author_email, *author_date;
  static int all, edit_flag, also, interactive, only, amend, signoff;
  static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
 -static char *untracked_files_arg;
 +static char *untracked_files_arg, *force_date;
  /*
   * The default commit message cleanup mode will remove the lines
   * beginning with # (shell comments) and leading and trailing
@@@ -68,17 -80,10 +81,17 @@@ static enum 
  } cleanup_mode;
  static char *cleanup_arg;
  
 -static int use_editor = 1, initial_commit, in_merge;
 +static int use_editor = 1, initial_commit, in_merge, include_status = 1;
  static const char *only_include_assumed;
  static struct strbuf message;
  
 +static int null_termination;
 +static enum {
 +      STATUS_FORMAT_LONG,
 +      STATUS_FORMAT_SHORT,
 +      STATUS_FORMAT_PORCELAIN,
 +} status_format = STATUS_FORMAT_LONG;
 +
  static int opt_parse_m(const struct option *opt, const char *arg, int unset)
  {
        struct strbuf *buf = opt->value;
  static struct option builtin_commit_options[] = {
        OPT__QUIET(&quiet),
        OPT__VERBOSE(&verbose),
 -      OPT_GROUP("Commit message options"),
  
 +      OPT_GROUP("Commit message options"),
        OPT_FILENAME('F', "file", &logfile, "read log from file"),
        OPT_STRING(0, "author", &force_author, "AUTHOR", "override author for commit"),
 +      OPT_STRING(0, "date", &force_date, "DATE", "override date for commit"),
        OPT_CALLBACK('m', "message", &message, "MESSAGE", "specify commit message", opt_parse_m),
        OPT_STRING('c', "reedit-message", &edit_message, "COMMIT", "reuse and edit message from specified commit"),
        OPT_STRING('C', "reuse-message", &use_message, "COMMIT", "reuse message from specified commit"),
        OPT_BOOLEAN('s', "signoff", &signoff, "add Signed-off-by:"),
        OPT_FILENAME('t', "template", &template_file, "use specified template file"),
        OPT_BOOLEAN('e', "edit", &edit_flag, "force edit of commit"),
 +      OPT_STRING(0, "cleanup", &cleanup_arg, "default", "how to strip spaces and #comments from message"),
 +      OPT_BOOLEAN(0, "status", &include_status, "include status in commit message template"),
 +      /* end commit message options */
  
        OPT_GROUP("Commit contents options"),
        OPT_BOOLEAN('a', "all", &all, "commit all changed files"),
        OPT_BOOLEAN('o', "only", &only, "commit only specified files"),
        OPT_BOOLEAN('n', "no-verify", &no_verify, "bypass pre-commit hook"),
        OPT_BOOLEAN(0, "dry-run", &dry_run, "show what would be committed"),
 +      OPT_SET_INT(0, "short", &status_format, "show status concisely",
 +                  STATUS_FORMAT_SHORT),
 +      OPT_SET_INT(0, "porcelain", &status_format,
 +                  "show porcelain output format", STATUS_FORMAT_PORCELAIN),
 +      OPT_BOOLEAN('z', "null", &null_termination,
 +                  "terminate entries with NUL"),
        OPT_BOOLEAN(0, "amend", &amend, "amend previous commit"),
        { OPTION_STRING, 'u', "untracked-files", &untracked_files_arg, "mode", "show untracked files, optional modes: all, normal, no. (Default: all)", PARSE_OPT_OPTARG, NULL, (intptr_t)"all" },
        OPT_BOOLEAN(0, "allow-empty", &allow_empty, "ok to record an empty change"),
 -      OPT_STRING(0, "cleanup", &cleanup_arg, "default", "how to strip spaces and #comments from message"),
 +      /* end commit contents options */
  
        OPT_END()
  };
@@@ -184,15 -179,11 +197,15 @@@ static int list_paths(struct string_lis
  
        for (i = 0; i < active_nr; i++) {
                struct cache_entry *ce = active_cache[i];
 +              struct string_list_item *item;
 +
                if (ce->ce_flags & CE_UPDATE)
                        continue;
                if (!match_pathspec(pattern, ce->name, ce_namelen(ce), 0, m))
                        continue;
 -              string_list_insert(ce->name, list);
 +              item = string_list_insert(ce->name, list);
 +              if (ce_skip_worktree(ce))
 +                      item->util = item; /* better a valid pointer than a fake one */
        }
  
        return report_path_error(m, pattern, prefix ? strlen(prefix) : 0);
@@@ -205,10 -196,6 +218,10 @@@ static void add_remove_files(struct str
                struct stat st;
                struct string_list_item *p = &(list->items[i]);
  
 +              /* p->util is skip-worktree */
 +              if (p->util)
 +                      continue;
 +
                if (!lstat(p->string, &st)) {
                        if (add_to_cache(p->string, &st, 0))
                                die("updating files failed");
@@@ -332,7 -319,7 +345,7 @@@ static char *prepare_index(int argc, co
         */
        commit_style = COMMIT_PARTIAL;
  
 -      if (file_exists(git_path("MERGE_HEAD")))
 +      if (in_merge)
                die("cannot do a partial commit during a merge.");
  
        memset(&partial, 0, sizeof(partial));
  static int run_status(FILE *fp, const char *index_file, const char *prefix, int nowarn,
                      struct wt_status *s)
  {
 +      unsigned char sha1[20];
 +
        if (s->relative_paths)
                s->prefix = prefix;
  
        s->index_file = index_file;
        s->fp = fp;
        s->nowarn = nowarn;
 +      s->is_initial = get_sha1(s->reference, sha1) ? 1 : 0;
 +
 +      wt_status_collect(s);
  
 -      wt_status_print(s);
 +      switch (status_format) {
 +      case STATUS_FORMAT_SHORT:
 +              wt_shortstatus_print(s, null_termination);
 +              break;
 +      case STATUS_FORMAT_PORCELAIN:
 +              wt_porcelain_print(s, null_termination);
 +              break;
 +      case STATUS_FORMAT_LONG:
 +              wt_status_print(s);
 +              break;
 +      }
  
        return s->commitable;
  }
@@@ -451,9 -423,6 +464,9 @@@ static void determine_author_info(void
                email = xstrndup(lb + 2, rb - (lb + 2));
        }
  
 +      if (force_date)
 +              date = force_date;
 +
        author_name = name;
        author_email = email;
        author_date = date;
@@@ -591,7 -560,7 +604,7 @@@ static int prepare_to_commit(const cha
  
        /* This checks if committer ident is explicitly given */
        git_committer_info(0);
 -      if (use_editor) {
 +      if (use_editor && include_status) {
                char *author_ident;
                const char *committer_ident;
  
@@@ -779,21 -748,6 +792,21 @@@ static const char *find_author_by_nickn
        die("No existing author found with '%s'", name);
  }
  
 +
 +static void handle_untracked_files_arg(struct wt_status *s)
 +{
 +      if (!untracked_files_arg)
 +              ; /* default already initialized */
 +      else if (!strcmp(untracked_files_arg, "no"))
 +              s->show_untracked_files = SHOW_NO_UNTRACKED_FILES;
 +      else if (!strcmp(untracked_files_arg, "normal"))
 +              s->show_untracked_files = SHOW_NORMAL_UNTRACKED_FILES;
 +      else if (!strcmp(untracked_files_arg, "all"))
 +              s->show_untracked_files = SHOW_ALL_UNTRACKED_FILES;
 +      else
 +              die("Invalid untracked files mode '%s'", untracked_files_arg);
 +}
 +
  static int parse_and_validate_options(int argc, const char *argv[],
                                      const char * const usage[],
                                      const char *prefix,
        if (get_sha1("HEAD", head_sha1))
                initial_commit = 1;
  
 -      if (!get_sha1("MERGE_HEAD", merge_head_sha1))
 -              in_merge = 1;
 -
        /* Sanity check options */
        if (amend && initial_commit)
                die("You have nothing to amend.");
        else
                die("Invalid cleanup mode %s", cleanup_arg);
  
 -      if (!untracked_files_arg)
 -              ; /* default already initialized */
 -      else if (!strcmp(untracked_files_arg, "no"))
 -              s->show_untracked_files = SHOW_NO_UNTRACKED_FILES;
 -      else if (!strcmp(untracked_files_arg, "normal"))
 -              s->show_untracked_files = SHOW_NORMAL_UNTRACKED_FILES;
 -      else if (!strcmp(untracked_files_arg, "all"))
 -              s->show_untracked_files = SHOW_ALL_UNTRACKED_FILES;
 -      else
 -              die("Invalid untracked files mode '%s'", untracked_files_arg);
 +      handle_untracked_files_arg(s);
  
        if (all && argc > 0)
                die("Paths with -a does not make sense.");
        else if (interactive && argc > 0)
                die("Paths with --interactive does not make sense.");
  
 +      if (null_termination && status_format == STATUS_FORMAT_LONG)
 +              status_format = STATUS_FORMAT_PORCELAIN;
 +      if (status_format != STATUS_FORMAT_LONG)
 +              dry_run = 1;
 +
        return argc;
  }
  
@@@ -992,72 -953,29 +1005,75 @@@ static int git_status_config(const cha
  int cmd_status(int argc, const char **argv, const char *prefix)
  {
        struct wt_status s;
 +      unsigned char sha1[20];
 +      static struct option builtin_status_options[] = {
 +              OPT__VERBOSE(&verbose),
 +              OPT_SET_INT('s', "short", &status_format,
 +                          "show status concisely", STATUS_FORMAT_SHORT),
 +              OPT_SET_INT(0, "porcelain", &status_format,
 +                          "show porcelain output format",
 +                          STATUS_FORMAT_PORCELAIN),
 +              OPT_BOOLEAN('z', "null", &null_termination,
 +                          "terminate entries with NUL"),
 +              { OPTION_STRING, 'u', "untracked-files", &untracked_files_arg,
 +                "mode",
 +                "show untracked files, optional modes: all, normal, no. (Default: all)",
 +                PARSE_OPT_OPTARG, NULL, (intptr_t)"all" },
 +              OPT_END(),
 +      };
 +
 +      if (null_termination && status_format == STATUS_FORMAT_LONG)
 +              status_format = STATUS_FORMAT_PORCELAIN;
  
        wt_status_prepare(&s);
        git_config(git_status_config, &s);
 +      in_merge = file_exists(git_path("MERGE_HEAD"));
 +      argc = parse_options(argc, argv, prefix,
 +                           builtin_status_options,
 +                           builtin_status_usage, 0);
 +      handle_untracked_files_arg(&s);
 +
 +      if (*argv)
 +              s.pathspec = get_pathspec(prefix, argv);
 +
 +      read_cache();
 +      refresh_cache(REFRESH_QUIET|REFRESH_UNMERGED);
 +      s.is_initial = get_sha1(s.reference, sha1) ? 1 : 0;
 +      s.in_merge = in_merge;
 +      wt_status_collect(&s);
 +
 +      if (s.relative_paths)
 +              s.prefix = prefix;
        if (s.use_color == -1)
                s.use_color = git_use_color_default;
        if (diff_use_color_default == -1)
                diff_use_color_default = git_use_color_default;
  
 -      argc = parse_and_validate_options(argc, argv, builtin_status_usage,
 -                                        prefix, &s);
 -      return dry_run_commit(argc, argv, prefix, &s);
 +      switch (status_format) {
 +      case STATUS_FORMAT_SHORT:
 +              wt_shortstatus_print(&s, null_termination);
 +              break;
 +      case STATUS_FORMAT_PORCELAIN:
 +              wt_porcelain_print(&s, null_termination);
 +              break;
 +      case STATUS_FORMAT_LONG:
 +              s.verbose = verbose;
 +              wt_status_print(&s);
 +              break;
 +      }
 +      return 0;
  }
  
  static void print_summary(const char *prefix, const unsigned char *sha1)
  {
        struct rev_info rev;
        struct commit *commit;
-       static const char *format = "format:%h] %s";
+       struct strbuf format = STRBUF_INIT;
        unsigned char junk_sha1[20];
        const char *head = resolve_ref("HEAD", junk_sha1, 0, NULL);
+       struct pretty_print_context pctx = {0};
+       struct strbuf author_ident = STRBUF_INIT;
+       struct strbuf committer_ident = STRBUF_INIT;
  
        commit = lookup_commit(sha1);
        if (!commit)
        if (!commit || parse_commit(commit))
                die("could not parse newly created commit");
  
+       strbuf_addstr(&format, "format:%h] %s");
+       format_commit_message(commit, "%an <%ae>", &author_ident, &pctx);
+       format_commit_message(commit, "%cn <%ce>", &committer_ident, &pctx);
+       if (strbuf_cmp(&author_ident, &committer_ident)) {
+               strbuf_addstr(&format, "\n Author: ");
+               strbuf_addbuf_percentquote(&format, &author_ident);
+       }
+       if (!user_ident_sufficiently_given()) {
+               strbuf_addstr(&format, "\n Committer: ");
+               strbuf_addbuf_percentquote(&format, &committer_ident);
+               if (advice_implicit_identity) {
+                       strbuf_addch(&format, '\n');
+                       strbuf_addstr(&format, implicit_ident_advice);
+               }
+       }
+       strbuf_release(&author_ident);
+       strbuf_release(&committer_ident);
        init_revisions(&rev, prefix);
        setup_revisions(0, NULL, &rev, NULL);
  
  
        rev.verbose_header = 1;
        rev.show_root_diff = 1;
-       get_commit_format(format, &rev);
+       get_commit_format(format.buf, &rev);
        rev.always_show_header = 0;
        rev.diffopt.detect_rename = 1;
        rev.diffopt.rename_limit = 100;
                struct pretty_print_context ctx = {0};
                struct strbuf buf = STRBUF_INIT;
                ctx.date_mode = DATE_NORMAL;
-               format_commit_message(commit, format + 7, &buf, &ctx);
+               format_commit_message(commit, format.buf + 7, &buf, &ctx);
                printf("%s\n", buf.buf);
                strbuf_release(&buf);
        }
+       strbuf_release(&format);
  }
  
  static int git_commit_config(const char *k, const char *v, void *cb)
  
        if (!strcmp(k, "commit.template"))
                return git_config_pathname(&template_file, k, v);
 +      if (!strcmp(k, "commit.status")) {
 +              include_status = git_config_bool(k, v);
 +              return 0;
 +      }
  
        return git_status_config(k, v, s);
  }
@@@ -1128,11 -1062,10 +1164,11 @@@ int cmd_commit(int argc, const char **a
  
        wt_status_prepare(&s);
        git_config(git_commit_config, &s);
 +      in_merge = file_exists(git_path("MERGE_HEAD"));
 +      s.in_merge = in_merge;
  
        if (s.use_color == -1)
                s.use_color = git_use_color_default;
 -
        argc = parse_and_validate_options(argc, argv, builtin_commit_usage,
                                          prefix, &s);
        if (dry_run) {
                     "new_index file. Check that disk is not full or quota is\n"
                     "not exceeded, and then \"git reset HEAD\" to recover.");
  
 -      rerere();
 +      rerere(0);
        run_hook(get_index_file(), "post-commit", NULL);
        if (!quiet)
                print_summary(prefix, commit_sha1);
diff --combined daemon.c
index 918e560b4e1d4b78bd2d3223db61e915c4a3ccbb,51d9d6b8acfeb445ea9854e9ce8dcff23050d20e..360635eb1c14608b44568ce39d969eb8822e688d
+++ b/daemon.c
@@@ -147,7 -147,6 +147,6 @@@ static char *path_ok(char *directory
                        { "IP", ip_address },
                        { "P", tcp_port },
                        { "D", directory },
-                       { "%", "%" },
                        { NULL }
                };
  
@@@ -562,24 -561,6 +561,24 @@@ static int execute(struct sockaddr *add
        return -1;
  }
  
 +static int addrcmp(const struct sockaddr_storage *s1,
 +    const struct sockaddr_storage *s2)
 +{
 +      if (s1->ss_family != s2->ss_family)
 +              return s1->ss_family - s2->ss_family;
 +      if (s1->ss_family == AF_INET)
 +              return memcmp(&((struct sockaddr_in *)s1)->sin_addr,
 +                  &((struct sockaddr_in *)s2)->sin_addr,
 +                  sizeof(struct in_addr));
 +#ifndef NO_IPV6
 +      if (s1->ss_family == AF_INET6)
 +              return memcmp(&((struct sockaddr_in6 *)s1)->sin6_addr,
 +                  &((struct sockaddr_in6 *)s2)->sin6_addr,
 +                  sizeof(struct in6_addr));
 +#endif
 +      return 0;
 +}
 +
  static int max_connections = 32;
  
  static unsigned int live_children;
@@@ -594,12 -575,17 +593,12 @@@ static void add_child(pid_t pid, struc
  {
        struct child *newborn, **cradle;
  
 -      /*
 -       * This must be xcalloc() -- we'll compare the whole sockaddr_storage
 -       * but individual address may be shorter.
 -       */
        newborn = xcalloc(1, sizeof(*newborn));
        live_children++;
        newborn->pid = pid;
        memcpy(&newborn->address, addr, addrlen);
        for (cradle = &firstborn; *cradle; cradle = &(*cradle)->next)
 -              if (!memcmp(&(*cradle)->address, &newborn->address,
 -                          sizeof(newborn->address)))
 +              if (!addrcmp(&(*cradle)->address, &newborn->address))
                        break;
        newborn->next = *cradle;
        *cradle = newborn;
@@@ -632,7 -618,8 +631,7 @@@ static void kill_some_child(void
                return;
  
        for (; (next = blanket->next); blanket = next)
 -              if (!memcmp(&blanket->address, &next->address,
 -                          sizeof(next->address))) {
 +              if (!addrcmp(&blanket->address, &next->address)) {
                        kill(blanket->pid, SIGTERM);
                        break;
                }
diff --combined strbuf.c
index 3fa81b33656a5d2d4887b949fd103920173d37a7,af9130e52d877bc49748dd5e2db9c5912ab08837..67448b7b07aa9d07b0c8cc250e3e0fad64c8fb5e
+++ b/strbuf.c
@@@ -91,6 -91,13 +91,6 @@@ void strbuf_ltrim(struct strbuf *sb
        sb->buf[sb->len] = '\0';
  }
  
 -void strbuf_tolower(struct strbuf *sb)
 -{
 -      int i;
 -      for (i = 0; i < sb->len; i++)
 -              sb->buf[i] = tolower(sb->buf[i]);
 -}
 -
  struct strbuf **strbuf_split(const struct strbuf *sb, int delim)
  {
        int alloc = 2, pos = 0;
@@@ -220,6 -227,12 +220,12 @@@ void strbuf_expand(struct strbuf *sb, c
                        break;
                format = percent + 1;
  
+               if (*format == '%') {
+                       strbuf_addch(sb, '%');
+                       format++;
+                       continue;
+               }
                consumed = fn(sb, format, context);
                if (consumed)
                        format += consumed;
@@@ -244,6 -257,17 +250,17 @@@ size_t strbuf_expand_dict_cb(struct str
        return 0;
  }
  
+ void strbuf_addbuf_percentquote(struct strbuf *dst, const struct strbuf *src)
+ {
+       int i, len = src->len;
+       for (i = 0; i < len; i++) {
+               if (src->buf[i] == '%')
+                       strbuf_addch(dst, '%');
+               strbuf_addch(dst, src->buf[i]);
+       }
+ }
  size_t strbuf_fread(struct strbuf *sb, size_t size, FILE *f)
  {
        size_t res;
diff --combined strbuf.h
index b37f06a34b75a14dccf99a034b683f702cb4724e,84ac9424b5446aa82ac79045ad020f9ecfc0f75f..8ce2a2e54b0180ffb8dc573953872ff00dc419b5
+++ b/strbuf.h
@@@ -81,6 -81,7 +81,6 @@@ extern void strbuf_trim(struct strbuf *
  extern void strbuf_rtrim(struct strbuf *);
  extern void strbuf_ltrim(struct strbuf *);
  extern int strbuf_cmp(const struct strbuf *, const struct strbuf *);
 -extern void strbuf_tolower(struct strbuf *);
  
  extern struct strbuf **strbuf_split(const struct strbuf *, int delim);
  extern void strbuf_list_free(struct strbuf **);
@@@ -115,6 -116,7 +115,7 @@@ struct strbuf_expand_dict_entry 
        const char *value;
  };
  extern size_t strbuf_expand_dict_cb(struct strbuf *sb, const char *placeholder, void *context);
+ extern void strbuf_addbuf_percentquote(struct strbuf *dst, const struct strbuf *src);
  
  __attribute__((format (printf,2,3)))
  extern void strbuf_addf(struct strbuf *sb, const char *fmt, ...);
diff --combined t/t7501-commit.sh
index a5297012410271f64398124da6e4c7dd8eec3154,0166d35e51fa60f7fb6e8f5f1fa94a103108b896..7940901d47fd457cda77ee333aa40145433be4d4
@@@ -117,7 -117,11 +117,11 @@@ test_expect_success 
  test_expect_success \
        "overriding author from command line" \
        "echo 'gak' >file && \
-        git commit -m 'author' --author 'Rubber Duck <rduck@convoy.org>' -a"
+        git commit -m 'author' --author 'Rubber Duck <rduck@convoy.org>' -a >output 2>&1"
+ test_expect_success \
+       "commit --author output mentions author" \
+       "grep Rubber.Duck output"
  
  test_expect_success PERL \
        "interactive add" \
@@@ -211,21 -215,6 +215,21 @@@ test_expect_success 'amend commit to fi
  
  '
  
 +test_expect_success 'amend commit to fix date' '
 +
 +      test_tick &&
 +      newtick=$GIT_AUTHOR_DATE &&
 +      git reset --hard &&
 +      git cat-file -p HEAD |
 +      sed -e "s/author.*/author $author $newtick/" \
 +              -e "s/^\(committer.*> \).*$/\1$GIT_COMMITTER_DATE/" > \
 +              expected &&
 +      git commit --amend --date="$newtick" &&
 +      git cat-file -p HEAD > current &&
 +      test_cmp expected current
 +
 +'
 +
  test_expect_success 'sign off (1)' '
  
        echo 1 >positive &&