Merge branch 'lp/maint-diff-three-dash-with-graph' into maint
authorJunio C Hamano <gitster@pobox.com>
Wed, 2 May 2012 04:11:40 +0000 (21:11 -0700)
committerJunio C Hamano <gitster@pobox.com>
Wed, 2 May 2012 04:11:40 +0000 (21:11 -0700)
"log -p --graph" used with "--stat" had a few formatting error.

By Lucian Poston
* lp/maint-diff-three-dash-with-graph:
t4202: add test for "log --graph --stat -p" separator lines
log --graph: fix break in graph lines
log --graph --stat: three-dash separator should come after graph lines

1  2 
diff.c
log-tree.c
t/t4202-log.sh
diff --combined diff.c
index 0dea484ee8c8bd2f2b356c8a8eb1ef15d1be07f4,7d5b7ecf14b1e8f256e78eca687474155bc006b7..cd029b33d536a36bc5797821848d25fa98fe7d8f
--- 1/diff.c
--- 2/diff.c
+++ b/diff.c
@@@ -31,7 -31,6 +31,7 @@@ static const char *external_diff_cmd_cf
  int diff_auto_refresh_index = 1;
  static int diff_mnemonic_prefix;
  static int diff_no_prefix;
 +static int diff_stat_graph_width;
  static int diff_dirstat_permille_default = 30;
  static struct diff_options default_diff_options;
  
@@@ -157,10 -156,6 +157,10 @@@ int git_diff_ui_config(const char *var
                diff_no_prefix = git_config_bool(var, value);
                return 0;
        }
 +      if (!strcmp(var, "diff.statgraphwidth")) {
 +              diff_stat_graph_width = git_config_int(var, value);
 +              return 0;
 +      }
        if (!strcmp(var, "diff.external"))
                return git_config_string(&external_diff_cmd_cfg, var, value);
        if (!strcmp(var, "diff.wordregex"))
@@@ -182,8 -177,11 +182,8 @@@ int git_diff_basic_config(const char *v
                return 0;
        }
  
 -      switch (userdiff_config(var, value)) {
 -              case 0: break;
 -              case -1: return -1;
 -              default: return 0;
 -      }
 +      if (userdiff_config(var, value) < 0)
 +              return -1;
  
        if (!prefixcmp(var, "diff.color.") || !prefixcmp(var, "color.diff.")) {
                int slot = parse_diff_color_slot(var, 11);
@@@ -989,74 -987,10 +989,74 @@@ static void diff_words_flush(struct emi
                diff_words_show(ecbdata->diff_words);
  }
  
 +static void diff_filespec_load_driver(struct diff_filespec *one)
 +{
 +      /* Use already-loaded driver */
 +      if (one->driver)
 +              return;
 +
 +      if (S_ISREG(one->mode))
 +              one->driver = userdiff_find_by_path(one->path);
 +
 +      /* Fallback to default settings */
 +      if (!one->driver)
 +              one->driver = userdiff_find_by_name("default");
 +}
 +
 +static const char *userdiff_word_regex(struct diff_filespec *one)
 +{
 +      diff_filespec_load_driver(one);
 +      return one->driver->word_regex;
 +}
 +
 +static void init_diff_words_data(struct emit_callback *ecbdata,
 +                               struct diff_options *orig_opts,
 +                               struct diff_filespec *one,
 +                               struct diff_filespec *two)
 +{
 +      int i;
 +      struct diff_options *o = xmalloc(sizeof(struct diff_options));
 +      memcpy(o, orig_opts, sizeof(struct diff_options));
 +
 +      ecbdata->diff_words =
 +              xcalloc(1, sizeof(struct diff_words_data));
 +      ecbdata->diff_words->type = o->word_diff;
 +      ecbdata->diff_words->opt = o;
 +      if (!o->word_regex)
 +              o->word_regex = userdiff_word_regex(one);
 +      if (!o->word_regex)
 +              o->word_regex = userdiff_word_regex(two);
 +      if (!o->word_regex)
 +              o->word_regex = diff_word_regex_cfg;
 +      if (o->word_regex) {
 +              ecbdata->diff_words->word_regex = (regex_t *)
 +                      xmalloc(sizeof(regex_t));
 +              if (regcomp(ecbdata->diff_words->word_regex,
 +                          o->word_regex,
 +                          REG_EXTENDED | REG_NEWLINE))
 +                      die ("Invalid regular expression: %s",
 +                           o->word_regex);
 +      }
 +      for (i = 0; i < ARRAY_SIZE(diff_words_styles); i++) {
 +              if (o->word_diff == diff_words_styles[i].type) {
 +                      ecbdata->diff_words->style =
 +                              &diff_words_styles[i];
 +                      break;
 +              }
 +      }
 +      if (want_color(o->use_color)) {
 +              struct diff_words_style *st = ecbdata->diff_words->style;
 +              st->old.color = diff_get_color_opt(o, DIFF_FILE_OLD);
 +              st->new.color = diff_get_color_opt(o, DIFF_FILE_NEW);
 +              st->ctx.color = diff_get_color_opt(o, DIFF_PLAIN);
 +      }
 +}
 +
  static void free_diff_words_data(struct emit_callback *ecbdata)
  {
        if (ecbdata->diff_words) {
                diff_words_flush(ecbdata);
 +              free (ecbdata->diff_words->opt);
                free (ecbdata->diff_words->minus.text.ptr);
                free (ecbdata->diff_words->minus.orig);
                free (ecbdata->diff_words->plus.text.ptr);
@@@ -1179,15 -1113,6 +1179,15 @@@ static void fn_out_consume(void *priv, 
                        diff_words_append(line, len,
                                          &ecbdata->diff_words->plus);
                        return;
 +              } else if (!prefixcmp(line, "\\ ")) {
 +                      /*
 +                       * Eat the "no newline at eof" marker as if we
 +                       * saw a "+" or "-" line with nothing on it,
 +                       * and return without diff_words_flush() to
 +                       * defer processing. If this is the end of
 +                       * preimage, more "+" lines may come after it.
 +                       */
 +                      return;
                }
                diff_words_flush(ecbdata);
                if (ecbdata->diff_words->type == DIFF_WORDS_PORCELAIN) {
@@@ -1342,15 -1267,13 +1342,15 @@@ const char mime_boundary_leader[] = "--
  
  static int scale_linear(int it, int width, int max_change)
  {
 +      if (!it)
 +              return 0;
        /*
 -       * make sure that at least one '-' is printed if there were deletions,
 -       * and likewise for '+'.
 +       * make sure that at least one '-' or '+' is printed if
 +       * there is any change to this path. The easiest way is to
 +       * scale linearly as if the alloted width is one column shorter
 +       * than it is, and then add 1 to the result.
         */
 -      if (max_change < 2)
 -              return it;
 -      return ((it - 1) * (width - 1) + max_change - 1) / (max_change - 1);
 +      return 1 + (it * (width - 1) / max_change);
  }
  
  static void show_name(FILE *file,
@@@ -1390,61 -1313,12 +1390,61 @@@ static void fill_print_name(struct diff
        file->print_name = pname;
  }
  
 +int print_stat_summary(FILE *fp, int files, int insertions, int deletions)
 +{
 +      struct strbuf sb = STRBUF_INIT;
 +      int ret;
 +
 +      if (!files) {
 +              assert(insertions == 0 && deletions == 0);
 +              return fputs(_(" 0 files changed\n"), fp);
 +      }
 +
 +      strbuf_addf(&sb,
 +                  Q_(" %d file changed", " %d files changed", files),
 +                  files);
 +
 +      /*
 +       * For binary diff, the caller may want to print "x files
 +       * changed" with insertions == 0 && deletions == 0.
 +       *
 +       * Not omitting "0 insertions(+), 0 deletions(-)" in this case
 +       * is probably less confusing (i.e skip over "2 files changed
 +       * but nothing about added/removed lines? Is this a bug in Git?").
 +       */
 +      if (insertions || deletions == 0) {
 +              /*
 +               * TRANSLATORS: "+" in (+) is a line addition marker;
 +               * do not translate it.
 +               */
 +              strbuf_addf(&sb,
 +                          Q_(", %d insertion(+)", ", %d insertions(+)",
 +                             insertions),
 +                          insertions);
 +      }
 +
 +      if (deletions || insertions == 0) {
 +              /*
 +               * TRANSLATORS: "-" in (-) is a line removal marker;
 +               * do not translate it.
 +               */
 +              strbuf_addf(&sb,
 +                          Q_(", %d deletion(-)", ", %d deletions(-)",
 +                             deletions),
 +                          deletions);
 +      }
 +      strbuf_addch(&sb, '\n');
 +      ret = fputs(sb.buf, fp);
 +      strbuf_release(&sb);
 +      return ret;
 +}
 +
  static void show_stats(struct diffstat_t *data, struct diff_options *options)
  {
        int i, len, add, del, adds = 0, dels = 0;
        uintmax_t max_change = 0, max_len = 0;
        int total_files = data->nr;
 -      int width, name_width, count;
 +      int width, name_width, graph_width, number_width = 4, count;
        const char *reset, *add_c, *del_c;
        const char *line_prefix = "";
        int extra_shown = 0;
                line_prefix = msg->buf;
        }
  
 -      width = options->stat_width ? options->stat_width : 80;
 -      name_width = options->stat_name_width ? options->stat_name_width : 50;
        count = options->stat_count ? options->stat_count : data->nr;
  
 -      /* Sanity: give at least 5 columns to the graph,
 -       * but leave at least 10 columns for the name.
 -       */
 -      if (width < 25)
 -              width = 25;
 -      if (name_width < 10)
 -              name_width = 10;
 -      else if (width < name_width + 15)
 -              name_width = width - 15;
 -
 -      /* Find the longest filename and max number of changes */
        reset = diff_get_color_opt(options, DIFF_RESET);
        add_c = diff_get_color_opt(options, DIFF_FILE_NEW);
        del_c = diff_get_color_opt(options, DIFF_FILE_OLD);
  
 +      /*
 +       * Find the longest filename and max number of changes
 +       */
        for (i = 0; (i < count) && (i < data->nr); i++) {
                struct diffstat_file *file = data->files[i];
                uintmax_t change = file->added + file->deleted;
        }
        count = i; /* min(count, data->nr) */
  
 -      /* Compute the width of the graph part;
 -       * 10 is for one blank at the beginning of the line plus
 -       * " | count " between the name and the graph.
 +      /*
 +       * We have width = stat_width or term_columns() columns total.
 +       * We want a maximum of min(max_len, stat_name_width) for the name part.
 +       * We want a maximum of min(max_change, stat_graph_width) for the +- part.
 +       * We also need 1 for " " and 4 + decimal_width(max_change)
 +       * for " | NNNN " and one the empty column at the end, altogether
 +       * 6 + decimal_width(max_change).
         *
 -       * From here on, name_width is the width of the name area,
 -       * and width is the width of the graph area.
 +       * If there's not enough space, we will use the smaller of
 +       * stat_name_width (if set) and 5/8*width for the filename,
 +       * and the rest for constant elements + graph part, but no more
 +       * than stat_graph_width for the graph part.
 +       * (5/8 gives 50 for filename and 30 for the constant parts + graph
 +       * for the standard terminal size).
 +       *
 +       * In other words: stat_width limits the maximum width, and
 +       * stat_name_width fixes the maximum width of the filename,
 +       * and is also used to divide available columns if there
 +       * aren't enough.
         */
 -      name_width = (name_width < max_len) ? name_width : max_len;
 -      if (width < (name_width + 10) + max_change)
 -              width = width - (name_width + 10);
 +
 +      if (options->stat_width == -1)
 +              width = term_columns();
        else
 -              width = max_change;
 +              width = options->stat_width ? options->stat_width : 80;
 +
 +      if (options->stat_graph_width == -1)
 +              options->stat_graph_width = diff_stat_graph_width;
  
 +      /*
 +       * Guarantee 3/8*16==6 for the graph part
 +       * and 5/8*16==10 for the filename part
 +       */
 +      if (width < 16 + 6 + number_width)
 +              width = 16 + 6 + number_width;
 +
 +      /*
 +       * First assign sizes that are wanted, ignoring available width.
 +       */
 +      graph_width = (options->stat_graph_width &&
 +                     options->stat_graph_width < max_change) ?
 +              options->stat_graph_width : max_change;
 +      name_width = (options->stat_name_width > 0 &&
 +                    options->stat_name_width < max_len) ?
 +              options->stat_name_width : max_len;
 +
 +      /*
 +       * Adjust adjustable widths not to exceed maximum width
 +       */
 +      if (name_width + number_width + 6 + graph_width > width) {
 +              if (graph_width > width * 3/8 - number_width - 6)
 +                      graph_width = width * 3/8 - number_width - 6;
 +              if (options->stat_graph_width &&
 +                  graph_width > options->stat_graph_width)
 +                      graph_width = options->stat_graph_width;
 +              if (name_width > width - number_width - 6 - graph_width)
 +                      name_width = width - number_width - 6 - graph_width;
 +              else
 +                      graph_width = width - number_width - 6 - name_width;
 +      }
 +
 +      /*
 +       * From here name_width is the width of the name area,
 +       * and graph_width is the width of the graph area.
 +       * max_change is used to scale graph properly.
 +       */
        for (i = 0; i < count; i++) {
                const char *prefix = "";
                char *name = data->files[i]->print_name;
                adds += add;
                dels += del;
  
 -              if (width <= max_change) {
 -                      add = scale_linear(add, width, max_change);
 -                      del = scale_linear(del, width, max_change);
 +              if (graph_width <= max_change) {
 +                      int total = add + del;
 +
 +                      total = scale_linear(add + del, graph_width, max_change);
 +                      if (total < 2 && add && del)
 +                              /* width >= 2 due to the sanity check */
 +                              total = 2;
 +                      if (add < del) {
 +                              add = scale_linear(add, graph_width, max_change);
 +                              del = total - add;
 +                      } else {
 +                              del = scale_linear(del, graph_width, max_change);
 +                              add = total - del;
 +                      }
                }
                fprintf(options->file, "%s", line_prefix);
                show_name(options->file, prefix, name, len);
                extra_shown = 1;
        }
        fprintf(options->file, "%s", line_prefix);
 -      fprintf(options->file,
 -             " %d files changed, %d insertions(+), %d deletions(-)\n",
 -             total_files, adds, dels);
 +      print_stat_summary(options->file, total_files, adds, dels);
  }
  
  static void show_shortstats(struct diffstat_t *data, struct diff_options *options)
                                options->output_prefix_data);
                fprintf(options->file, "%s", msg->buf);
        }
 -      fprintf(options->file, " %d files changed, %d insertions(+), %d deletions(-)\n",
 -             total_files, adds, dels);
 +      print_stat_summary(options->file, total_files, adds, dels);
  }
  
  static void show_numstat(struct diffstat_t *data, struct diff_options *options)
@@@ -2125,6 -1948,20 +2125,6 @@@ static void emit_binary_diff(FILE *file
        emit_binary_diff_body(file, two, one, prefix);
  }
  
 -static void diff_filespec_load_driver(struct diff_filespec *one)
 -{
 -      /* Use already-loaded driver */
 -      if (one->driver)
 -              return;
 -
 -      if (S_ISREG(one->mode))
 -              one->driver = userdiff_find_by_path(one->path);
 -
 -      /* Fallback to default settings */
 -      if (!one->driver)
 -              one->driver = userdiff_find_by_name("default");
 -}
 -
  int diff_filespec_is_binary(struct diff_filespec *one)
  {
        if (one->is_binary == -1) {
@@@ -2150,6 -1987,12 +2150,6 @@@ static const struct userdiff_funcname *
        return one->driver->funcname.pattern ? &one->driver->funcname : NULL;
  }
  
 -static const char *userdiff_word_regex(struct diff_filespec *one)
 -{
 -      diff_filespec_load_driver(one);
 -      return one->driver->word_regex;
 -}
 -
  void diff_set_mnemonic_prefix(struct diff_options *options, const char *a, const char *b)
  {
        if (!options->a_prefix)
@@@ -2299,7 -2142,7 +2299,7 @@@ static void builtin_diff(const char *na
                struct emit_callback ecbdata;
                const struct userdiff_funcname *pe;
  
 -              if (!DIFF_XDL_TST(o, WHITESPACE_FLAGS) || must_show_header) {
 +              if (must_show_header) {
                        fprintf(o->file, "%s", header.buf);
                        strbuf_reset(&header);
                }
                xecfg.ctxlen = o->context;
                xecfg.interhunkctxlen = o->interhunkcontext;
                xecfg.flags = XDL_EMIT_FUNCNAMES;
 +              if (DIFF_OPT_TST(o, FUNCCONTEXT))
 +                      xecfg.flags |= XDL_EMIT_FUNCCONTEXT;
                if (pe)
                        xdiff_set_find_func(&xecfg, pe->pattern, pe->cflags);
                if (!diffopts)
                        xecfg.ctxlen = strtoul(diffopts + 10, NULL, 10);
                else if (!prefixcmp(diffopts, "-u"))
                        xecfg.ctxlen = strtoul(diffopts + 2, NULL, 10);
 -              if (o->word_diff) {
 -                      int i;
 -
 -                      ecbdata.diff_words =
 -                              xcalloc(1, sizeof(struct diff_words_data));
 -                      ecbdata.diff_words->type = o->word_diff;
 -                      ecbdata.diff_words->opt = o;
 -                      if (!o->word_regex)
 -                              o->word_regex = userdiff_word_regex(one);
 -                      if (!o->word_regex)
 -                              o->word_regex = userdiff_word_regex(two);
 -                      if (!o->word_regex)
 -                              o->word_regex = diff_word_regex_cfg;
 -                      if (o->word_regex) {
 -                              ecbdata.diff_words->word_regex = (regex_t *)
 -                                      xmalloc(sizeof(regex_t));
 -                              if (regcomp(ecbdata.diff_words->word_regex,
 -                                              o->word_regex,
 -                                              REG_EXTENDED | REG_NEWLINE))
 -                                      die ("Invalid regular expression: %s",
 -                                                      o->word_regex);
 -                      }
 -                      for (i = 0; i < ARRAY_SIZE(diff_words_styles); i++) {
 -                              if (o->word_diff == diff_words_styles[i].type) {
 -                                      ecbdata.diff_words->style =
 -                                              &diff_words_styles[i];
 -                                      break;
 -                              }
 -                      }
 -                      if (want_color(o->use_color)) {
 -                              struct diff_words_style *st = ecbdata.diff_words->style;
 -                              st->old.color = diff_get_color_opt(o, DIFF_FILE_OLD);
 -                              st->new.color = diff_get_color_opt(o, DIFF_FILE_NEW);
 -                              st->ctx.color = diff_get_color_opt(o, DIFF_PLAIN);
 -                      }
 -              }
 +              if (o->word_diff)
 +                      init_diff_words_data(&ecbdata, o, one, two);
                xdi_diff_outf(&mf1, &mf2, fn_out_consume, &ecbdata,
                              &xpp, &xecfg);
                if (o->word_diff)
@@@ -3146,7 -3021,6 +3146,7 @@@ void diff_setup(struct diff_options *op
        options->rename_limit = -1;
        options->dirstat_permille = diff_dirstat_permille_default;
        options->context = 3;
 +      DIFF_OPT_SET(options, RENAME_EMPTY);
  
        options->change = diff_change;
        options->add_remove = diff_addremove;
@@@ -3358,7 -3232,6 +3358,7 @@@ static int stat_opt(struct diff_option
        char *end;
        int width = options->stat_width;
        int name_width = options->stat_name_width;
 +      int graph_width = options->stat_graph_width;
        int count = options->stat_count;
        int argcount = 1;
  
                                name_width = strtoul(av[1], &end, 10);
                                argcount = 2;
                        }
 +              } else if (!prefixcmp(arg, "-graph-width")) {
 +                      arg += strlen("-graph-width");
 +                      if (*arg == '=')
 +                              graph_width = strtoul(arg + 1, &end, 10);
 +                      else if (!*arg && !av[1])
 +                              die("Option '--stat-graph-width' requires a value");
 +                      else if (!*arg) {
 +                              graph_width = strtoul(av[1], &end, 10);
 +                              argcount = 2;
 +                      }
                } else if (!prefixcmp(arg, "-count")) {
                        arg += strlen("-count");
                        if (*arg == '=')
                return 0;
        options->output_format |= DIFF_FORMAT_DIFFSTAT;
        options->stat_name_width = name_width;
 +      options->stat_graph_width = graph_width;
        options->stat_width = width;
        options->stat_count = count;
        return argcount;
@@@ -3517,10 -3379,6 +3517,10 @@@ int diff_opt_parse(struct diff_options 
        }
        else if (!strcmp(arg, "--no-renames"))
                options->detect_rename = 0;
 +      else if (!strcmp(arg, "--rename-empty"))
 +              DIFF_OPT_SET(options, RENAME_EMPTY);
 +      else if (!strcmp(arg, "--no-rename-empty"))
 +              DIFF_OPT_CLR(options, RENAME_EMPTY);
        else if (!strcmp(arg, "--relative"))
                DIFF_OPT_SET(options, RELATIVE_NAME);
        else if (!prefixcmp(arg, "--relative=")) {
        else if (opt_arg(arg, '\0', "inter-hunk-context",
                         &options->interhunkcontext))
                ;
 +      else if (!strcmp(arg, "-W"))
 +              DIFF_OPT_SET(options, FUNCCONTEXT);
 +      else if (!strcmp(arg, "--function-context"))
 +              DIFF_OPT_SET(options, FUNCCONTEXT);
 +      else if (!strcmp(arg, "--no-function-context"))
 +              DIFF_OPT_CLR(options, FUNCCONTEXT);
        else if ((argcount = parse_long_opt("output", av, &optarg))) {
                options->file = fopen(optarg, "w");
                if (!options->file)
@@@ -4414,6 -4266,12 +4414,12 @@@ void diff_flush(struct diff_options *op
  
        if (output_format & DIFF_FORMAT_PATCH) {
                if (separator) {
+                       if (options->output_prefix) {
+                               struct strbuf *msg = NULL;
+                               msg = options->output_prefix(options,
+                                       options->output_prefix_data);
+                               fwrite(msg->buf, msg->len, 1, stdout);
+                       }
                        putc(options->line_termination, options->file);
                        if (options->stat_sep) {
                                /* attach patch instead of inline */
diff --combined log-tree.c
index cea8756866e5ab86f09f3fadb0fe33e92b04b4bd,f962203ec5527b4d6cac6d05abad61134b38897c..34c49e7b3322d32ead03844bdc478a2e27836f16
@@@ -8,7 -8,6 +8,7 @@@
  #include "refs.h"
  #include "string-list.h"
  #include "color.h"
 +#include "gpg-interface.h"
  
  struct decoration name_decoration = { "object names" };
  
@@@ -120,9 -119,9 +120,9 @@@ static int add_ref_decoration(const cha
                type = DECORATION_REF_REMOTE;
        else if (!prefixcmp(refname, "refs/tags/"))
                type = DECORATION_REF_TAG;
 -      else if (!prefixcmp(refname, "refs/stash"))
 +      else if (!strcmp(refname, "refs/stash"))
                type = DECORATION_REF_STASH;
 -      else if (!prefixcmp(refname, "HEAD"))
 +      else if (!strcmp(refname, "HEAD"))
                type = DECORATION_REF_HEAD;
  
        if (!cb_data || *(int *)cb_data == DECORATE_SHORT_REFS)
@@@ -166,14 -165,6 +166,14 @@@ static void show_parents(struct commit 
        }
  }
  
 +static void show_children(struct rev_info *opt, struct commit *commit, int abbrev)
 +{
 +      struct commit_list *p = lookup_decoration(&opt->children, &commit->object);
 +      for ( ; p; p = p->next) {
 +              printf(" %s", find_unique_abbrev(p->item->object.sha1, abbrev));
 +      }
 +}
 +
  void show_decorations(struct rev_info *opt, struct commit *commit)
  {
        const char *prefix;
@@@ -404,129 -395,6 +404,129 @@@ void log_write_email_headers(struct rev
        *extra_headers_p = extra_headers;
  }
  
 +static void show_sig_lines(struct rev_info *opt, int status, const char *bol)
 +{
 +      const char *color, *reset, *eol;
 +
 +      color = diff_get_color_opt(&opt->diffopt,
 +                                 status ? DIFF_WHITESPACE : DIFF_FRAGINFO);
 +      reset = diff_get_color_opt(&opt->diffopt, DIFF_RESET);
 +      while (*bol) {
 +              eol = strchrnul(bol, '\n');
 +              printf("%s%.*s%s%s", color, (int)(eol - bol), bol, reset,
 +                     *eol ? "\n" : "");
 +              bol = (*eol) ? (eol + 1) : eol;
 +      }
 +}
 +
 +static void show_signature(struct rev_info *opt, struct commit *commit)
 +{
 +      struct strbuf payload = STRBUF_INIT;
 +      struct strbuf signature = STRBUF_INIT;
 +      struct strbuf gpg_output = STRBUF_INIT;
 +      int status;
 +
 +      if (parse_signed_commit(commit->object.sha1, &payload, &signature) <= 0)
 +              goto out;
 +
 +      status = verify_signed_buffer(payload.buf, payload.len,
 +                                    signature.buf, signature.len,
 +                                    &gpg_output);
 +      if (status && !gpg_output.len)
 +              strbuf_addstr(&gpg_output, "No signature\n");
 +
 +      show_sig_lines(opt, status, gpg_output.buf);
 +
 + out:
 +      strbuf_release(&gpg_output);
 +      strbuf_release(&payload);
 +      strbuf_release(&signature);
 +}
 +
 +static int which_parent(const unsigned char *sha1, const struct commit *commit)
 +{
 +      int nth;
 +      const struct commit_list *parent;
 +
 +      for (nth = 0, parent = commit->parents; parent; parent = parent->next) {
 +              if (!hashcmp(parent->item->object.sha1, sha1))
 +                      return nth;
 +              nth++;
 +      }
 +      return -1;
 +}
 +
 +static int is_common_merge(const struct commit *commit)
 +{
 +      return (commit->parents
 +              && commit->parents->next
 +              && !commit->parents->next->next);
 +}
 +
 +static void show_one_mergetag(struct rev_info *opt,
 +                            struct commit_extra_header *extra,
 +                            struct commit *commit)
 +{
 +      unsigned char sha1[20];
 +      struct tag *tag;
 +      struct strbuf verify_message;
 +      int status, nth;
 +      size_t payload_size, gpg_message_offset;
 +
 +      hash_sha1_file(extra->value, extra->len, typename(OBJ_TAG), sha1);
 +      tag = lookup_tag(sha1);
 +      if (!tag)
 +              return; /* error message already given */
 +
 +      strbuf_init(&verify_message, 256);
 +      if (parse_tag_buffer(tag, extra->value, extra->len))
 +              strbuf_addstr(&verify_message, "malformed mergetag\n");
 +      else if (is_common_merge(commit) &&
 +               !hashcmp(tag->tagged->sha1,
 +                        commit->parents->next->item->object.sha1))
 +              strbuf_addf(&verify_message,
 +                          "merged tag '%s'\n", tag->tag);
 +      else if ((nth = which_parent(tag->tagged->sha1, commit)) < 0)
 +              strbuf_addf(&verify_message, "tag %s names a non-parent %s\n",
 +                                  tag->tag, tag->tagged->sha1);
 +      else
 +              strbuf_addf(&verify_message,
 +                          "parent #%d, tagged '%s'\n", nth + 1, tag->tag);
 +      gpg_message_offset = verify_message.len;
 +
 +      payload_size = parse_signature(extra->value, extra->len);
 +      if ((extra->len <= payload_size) ||
 +          (verify_signed_buffer(extra->value, payload_size,
 +                                extra->value + payload_size,
 +                                extra->len - payload_size,
 +                                &verify_message) &&
 +           verify_message.len <= gpg_message_offset)) {
 +              strbuf_addstr(&verify_message, "No signature\n");
 +              status = -1;
 +      }
 +      else if (strstr(verify_message.buf + gpg_message_offset,
 +                      ": Good signature from "))
 +              status = 0;
 +      else
 +              status = -1;
 +
 +      show_sig_lines(opt, status, verify_message.buf);
 +      strbuf_release(&verify_message);
 +}
 +
 +static void show_mergetag(struct rev_info *opt, struct commit *commit)
 +{
 +      struct commit_extra_header *extra, *to_free;
 +
 +      to_free = read_commit_extra_headers(commit, NULL);
 +      for (extra = to_free; extra; extra = extra->next) {
 +              if (strcmp(extra->key, "mergetag"))
 +                      continue; /* not a merge tag */
 +              show_one_mergetag(opt, extra, commit);
 +      }
 +      free_commit_extra_headers(to_free);
 +}
 +
  void show_log(struct rev_info *opt)
  {
        struct strbuf msgbuf = STRBUF_INIT;
                fputs(find_unique_abbrev(commit->object.sha1, abbrev_commit), stdout);
                if (opt->print_parents)
                        show_parents(commit, abbrev_commit);
 +              if (opt->children.name)
 +                      show_children(opt, commit, abbrev_commit);
                show_decorations(opt, commit);
                if (opt->graph && !graph_is_commit_finished(opt->graph)) {
                        putchar('\n');
                      stdout);
                if (opt->print_parents)
                        show_parents(commit, abbrev_commit);
 +              if (opt->children.name)
 +                      show_children(opt, commit, abbrev_commit);
                if (parent)
                        printf(" (from %s)",
                               find_unique_abbrev(parent->object.sha1,
                }
        }
  
 +      if (opt->show_signature) {
 +              show_signature(opt, commit);
 +              show_mergetag(opt, commit);
 +      }
 +
        if (!commit->buffer)
                return;
  
@@@ -711,14 -570,15 +711,15 @@@ int log_tree_diff_flush(struct rev_inf
                    opt->verbose_header &&
                    opt->commit_format != CMIT_FMT_ONELINE) {
                        int pch = DIFF_FORMAT_DIFFSTAT | DIFF_FORMAT_PATCH;
-                       if ((pch & opt->diffopt.output_format) == pch)
-                               printf("---");
                        if (opt->diffopt.output_prefix) {
                                struct strbuf *msg = NULL;
                                msg = opt->diffopt.output_prefix(&opt->diffopt,
                                        opt->diffopt.output_prefix_data);
                                fwrite(msg->buf, msg->len, 1, stdout);
                        }
+                       if ((pch & opt->diffopt.output_format) == pch) {
+                               printf("---");
+                       }
                        putchar('\n');
                }
        }
  
  static int do_diff_combined(struct rev_info *opt, struct commit *commit)
  {
 -      unsigned const char *sha1 = commit->object.sha1;
 -
 -      diff_tree_combined_merge(sha1, opt->dense_combined_merges, opt);
 +      diff_tree_combined_merge(commit, opt->dense_combined_merges, opt);
        return !opt->loginfo;
  }
  
diff --combined t/t4202-log.sh
index 222f7559e92caa2a0bbd128b0bb6bac14e2e113f,19a0851e7aee6e6bcc02a72616d5f51846c87442..32cf0bd218ea081e3e1d73400943b30a946f9dd5
@@@ -346,11 -346,11 +346,11 @@@ test_expect_success 'set up more tangle
  '
  
  cat > expect <<\EOF
 -*   Merge commit 'reach'
 +*   Merge tag 'reach'
  |\
  | \
  |  \
 -*-. \   Merge commit 'octopus-a'; commit 'octopus-b'
 +*-. \   Merge tags 'octopus-a' and 'octopus-b'
  |\ \ \
  * | | | seventh
  | | * | octopus-b
@@@ -516,4 -516,294 +516,294 @@@ test_expect_success 'show added path un
        )
  '
  
+ cat >expect <<\EOF
+ *   commit COMMIT_OBJECT_NAME
+ |\  Merge: MERGE_PARENTS
+ | | Author: A U Thor <author@example.com>
+ | |
+ | |     Merge HEADS DESCRIPTION
+ | |
+ | * commit COMMIT_OBJECT_NAME
+ | | Author: A U Thor <author@example.com>
+ | |
+ | |     reach
+ | | ---
+ | |  reach.t |    1 +
+ | |  1 file changed, 1 insertion(+)
+ | |
+ | | diff --git a/reach.t b/reach.t
+ | | new file mode 100644
+ | | index 0000000..10c9591
+ | | --- /dev/null
+ | | +++ b/reach.t
+ | | @@ -0,0 +1 @@
+ | | +reach
+ | |
+ |  \
+ *-. \   commit COMMIT_OBJECT_NAME
+ |\ \ \  Merge: MERGE_PARENTS
+ | | | | Author: A U Thor <author@example.com>
+ | | | |
+ | | | |     Merge HEADS DESCRIPTION
+ | | | |
+ | | * | commit COMMIT_OBJECT_NAME
+ | | |/  Author: A U Thor <author@example.com>
+ | | |
+ | | |       octopus-b
+ | | |   ---
+ | | |    octopus-b.t |    1 +
+ | | |    1 file changed, 1 insertion(+)
+ | | |
+ | | |   diff --git a/octopus-b.t b/octopus-b.t
+ | | |   new file mode 100644
+ | | |   index 0000000..d5fcad0
+ | | |   --- /dev/null
+ | | |   +++ b/octopus-b.t
+ | | |   @@ -0,0 +1 @@
+ | | |   +octopus-b
+ | | |
+ | * | commit COMMIT_OBJECT_NAME
+ | |/  Author: A U Thor <author@example.com>
+ | |
+ | |       octopus-a
+ | |   ---
+ | |    octopus-a.t |    1 +
+ | |    1 file changed, 1 insertion(+)
+ | |
+ | |   diff --git a/octopus-a.t b/octopus-a.t
+ | |   new file mode 100644
+ | |   index 0000000..11ee015
+ | |   --- /dev/null
+ | |   +++ b/octopus-a.t
+ | |   @@ -0,0 +1 @@
+ | |   +octopus-a
+ | |
+ * | commit COMMIT_OBJECT_NAME
+ |/  Author: A U Thor <author@example.com>
+ |
+ |       seventh
+ |   ---
+ |    seventh.t |    1 +
+ |    1 file changed, 1 insertion(+)
+ |
+ |   diff --git a/seventh.t b/seventh.t
+ |   new file mode 100644
+ |   index 0000000..9744ffc
+ |   --- /dev/null
+ |   +++ b/seventh.t
+ |   @@ -0,0 +1 @@
+ |   +seventh
+ |
+ *   commit COMMIT_OBJECT_NAME
+ |\  Merge: MERGE_PARENTS
+ | | Author: A U Thor <author@example.com>
+ | |
+ | |     Merge branch 'tangle'
+ | |
+ | *   commit COMMIT_OBJECT_NAME
+ | |\  Merge: MERGE_PARENTS
+ | | | Author: A U Thor <author@example.com>
+ | | |
+ | | |     Merge branch 'side' (early part) into tangle
+ | | |
+ | * |   commit COMMIT_OBJECT_NAME
+ | |\ \  Merge: MERGE_PARENTS
+ | | | | Author: A U Thor <author@example.com>
+ | | | |
+ | | | |     Merge branch 'master' (early part) into tangle
+ | | | |
+ | * | | commit COMMIT_OBJECT_NAME
+ | | | | Author: A U Thor <author@example.com>
+ | | | |
+ | | | |     tangle-a
+ | | | | ---
+ | | | |  tangle-a |    1 +
+ | | | |  1 file changed, 1 insertion(+)
+ | | | |
+ | | | | diff --git a/tangle-a b/tangle-a
+ | | | | new file mode 100644
+ | | | | index 0000000..7898192
+ | | | | --- /dev/null
+ | | | | +++ b/tangle-a
+ | | | | @@ -0,0 +1 @@
+ | | | | +a
+ | | | |
+ * | | |   commit COMMIT_OBJECT_NAME
+ |\ \ \ \  Merge: MERGE_PARENTS
+ | | | | | Author: A U Thor <author@example.com>
+ | | | | |
+ | | | | |     Merge branch 'side'
+ | | | | |
+ | * | | | commit COMMIT_OBJECT_NAME
+ | | |_|/  Author: A U Thor <author@example.com>
+ | |/| |
+ | | | |       side-2
+ | | | |   ---
+ | | | |    2 |    1 +
+ | | | |    1 file changed, 1 insertion(+)
+ | | | |
+ | | | |   diff --git a/2 b/2
+ | | | |   new file mode 100644
+ | | | |   index 0000000..0cfbf08
+ | | | |   --- /dev/null
+ | | | |   +++ b/2
+ | | | |   @@ -0,0 +1 @@
+ | | | |   +2
+ | | | |
+ | * | | commit COMMIT_OBJECT_NAME
+ | | | | Author: A U Thor <author@example.com>
+ | | | |
+ | | | |     side-1
+ | | | | ---
+ | | | |  1 |    1 +
+ | | | |  1 file changed, 1 insertion(+)
+ | | | |
+ | | | | diff --git a/1 b/1
+ | | | | new file mode 100644
+ | | | | index 0000000..d00491f
+ | | | | --- /dev/null
+ | | | | +++ b/1
+ | | | | @@ -0,0 +1 @@
+ | | | | +1
+ | | | |
+ * | | | commit COMMIT_OBJECT_NAME
+ | | | | Author: A U Thor <author@example.com>
+ | | | |
+ | | | |     Second
+ | | | | ---
+ | | | |  one |    1 +
+ | | | |  1 file changed, 1 insertion(+)
+ | | | |
+ | | | | diff --git a/one b/one
+ | | | | new file mode 100644
+ | | | | index 0000000..9a33383
+ | | | | --- /dev/null
+ | | | | +++ b/one
+ | | | | @@ -0,0 +1 @@
+ | | | | +case
+ | | | |
+ * | | | commit COMMIT_OBJECT_NAME
+ | |_|/  Author: A U Thor <author@example.com>
+ |/| |
+ | | |       sixth
+ | | |   ---
+ | | |    a/two |    1 -
+ | | |    1 file changed, 1 deletion(-)
+ | | |
+ | | |   diff --git a/a/two b/a/two
+ | | |   deleted file mode 100644
+ | | |   index 9245af5..0000000
+ | | |   --- a/a/two
+ | | |   +++ /dev/null
+ | | |   @@ -1 +0,0 @@
+ | | |   -ni
+ | | |
+ * | | commit COMMIT_OBJECT_NAME
+ | | | Author: A U Thor <author@example.com>
+ | | |
+ | | |     fifth
+ | | | ---
+ | | |  a/two |    1 +
+ | | |  1 file changed, 1 insertion(+)
+ | | |
+ | | | diff --git a/a/two b/a/two
+ | | | new file mode 100644
+ | | | index 0000000..9245af5
+ | | | --- /dev/null
+ | | | +++ b/a/two
+ | | | @@ -0,0 +1 @@
+ | | | +ni
+ | | |
+ * | | commit COMMIT_OBJECT_NAME
+ |/ /  Author: A U Thor <author@example.com>
+ | |
+ | |       fourth
+ | |   ---
+ | |    ein |    1 +
+ | |    1 file changed, 1 insertion(+)
+ | |
+ | |   diff --git a/ein b/ein
+ | |   new file mode 100644
+ | |   index 0000000..9d7e69f
+ | |   --- /dev/null
+ | |   +++ b/ein
+ | |   @@ -0,0 +1 @@
+ | |   +ichi
+ | |
+ * | commit COMMIT_OBJECT_NAME
+ |/  Author: A U Thor <author@example.com>
+ |
+ |       third
+ |   ---
+ |    ichi |    1 +
+ |    one  |    1 -
+ |    2 files changed, 1 insertion(+), 1 deletion(-)
+ |
+ |   diff --git a/ichi b/ichi
+ |   new file mode 100644
+ |   index 0000000..9d7e69f
+ |   --- /dev/null
+ |   +++ b/ichi
+ |   @@ -0,0 +1 @@
+ |   +ichi
+ |   diff --git a/one b/one
+ |   deleted file mode 100644
+ |   index 9d7e69f..0000000
+ |   --- a/one
+ |   +++ /dev/null
+ |   @@ -1 +0,0 @@
+ |   -ichi
+ |
+ * commit COMMIT_OBJECT_NAME
+ | Author: A U Thor <author@example.com>
+ |
+ |     second
+ | ---
+ |  one |    2 +-
+ |  1 file changed, 1 insertion(+), 1 deletion(-)
+ |
+ | diff --git a/one b/one
+ | index 5626abf..9d7e69f 100644
+ | --- a/one
+ | +++ b/one
+ | @@ -1 +1 @@
+ | -one
+ | +ichi
+ |
+ * commit COMMIT_OBJECT_NAME
+   Author: A U Thor <author@example.com>
+       initial
+   ---
+    one |    1 +
+    1 file changed, 1 insertion(+)
+   diff --git a/one b/one
+   new file mode 100644
+   index 0000000..5626abf
+   --- /dev/null
+   +++ b/one
+   @@ -0,0 +1 @@
+   +one
+ EOF
+ sanitize_output () {
+       sed -e 's/ *$//' \
+           -e 's/commit [0-9a-f]*$/commit COMMIT_OBJECT_NAME/' \
+           -e 's/Merge: [ 0-9a-f]*$/Merge: MERGE_PARENTS/' \
+           -e 's/Merge tag.*/Merge HEADS DESCRIPTION/' \
+           -e 's/Merge commit.*/Merge HEADS DESCRIPTION/' \
+           -e 's/, 0 deletions(-)//' \
+           -e 's/, 0 insertions(+)//' \
+           -e 's/ 1 files changed, / 1 file changed, /' \
+           -e 's/, 1 deletions(-)/, 1 deletion(-)/' \
+           -e 's/, 1 insertions(+)/, 1 insertion(+)/'
+ }
+ test_expect_success 'log --graph with diff and stats' '
+       git log --graph --pretty=short --stat -p >actual &&
+       sanitize_output >actual.sanitized <actual &&
+       test_cmp expect actual.sanitized
+ '
  test_done