Merge branch 'zj/diff-stat-dyncol'
authorJunio C Hamano <gitster@pobox.com>
Tue, 6 Mar 2012 22:53:06 +0000 (14:53 -0800)
committerJunio C Hamano <gitster@pobox.com>
Tue, 6 Mar 2012 22:53:06 +0000 (14:53 -0800)
By Zbigniew Jędrzejewski-Szmek (8) and Junio C Hamano (1)
* zj/diff-stat-dyncol:
: This breaks tests. Perhaps it is not worth using the decimal-width stuff
: for this series, at least initially.
diff --stat: add config option to limit graph width
diff --stat: enable limiting of the graph part
diff --stat: add a test for output with COLUMNS=40
diff --stat: use a maximum of 5/8 for the filename part
merge --stat: use the full terminal width
log --stat: use the full terminal width
show --stat: use the full terminal width
diff --stat: use the full terminal width
diff --stat: tests for long filenames and big change counts

1  2 
Documentation/diff-options.txt
builtin/merge.c
contrib/completion/git-completion.bash
diff.c
diff.h
index ba7cd13483194399762b0f48e218ddb4011dfcf1,87f0a5fb8f1b74332d0a9ce6579bc5d8d0f00ef3..7d4566f829e860758328309c377608a1083b0298
@@@ -52,17 -52,20 +52,23 @@@ endif::git-format-patch[
  --patience::
        Generate a diff using the "patience diff" algorithm.
  
 +--histogram::
 +      Generate a diff using the "histogram diff" algorithm.
 +
  --stat[=<width>[,<name-width>[,<count>]]]::
-       Generate a diffstat.  You can override the default
-       output width for 80-column terminal by `--stat=<width>`.
-       The width of the filename part can be controlled by
-       giving another width to it separated by a comma.
+       Generate a diffstat. By default, as much space as necessary
+       will be used for the filename part, and the rest for the graph
+       part. Maximum width defaults to terminal width, or 80 columns
+       if not connected to a terminal, and can be overriden by
+       `<width>`. The width of the filename part can be limited by
+       giving another width `<name-width>` after a comma. The width
+       of the graph part can be limited by using
+       `--stat-graph-width=<width>` (affects all commands generating
+       a stat graph) or by setting `diff.statGraphWidth=<width>`
+       (does not affect `git format-patch`).
        By giving a third parameter `<count>`, you can limit the
-       output to the first `<count>` lines, followed by
-       `...` if there are more.
+       output to the first `<count>` lines, followed by `...` if
+       there are more.
  +
  These parameters can also be set individually with `--stat-width=<width>`,
  `--stat-name-width=<name-width>` and `--stat-count=<count>`.
diff --combined builtin/merge.c
index d3e1e8dc9e478aaea7ef4da855fb7c2e10397644,34a5034a7667455728c7833513a43ef989d01538..cb8f14910b93edb7f1bfa6a4ee82f6a3d9fd7e40
@@@ -48,7 -48,7 +48,7 @@@ static const char * const builtin_merge
  
  static int show_diffstat = 1, shortlog_len = -1, squash;
  static int option_commit = 1, allow_fast_forward = 1;
 -static int fast_forward_only, option_edit;
 +static int fast_forward_only, option_edit = -1;
  static int allow_trivial = 1, have_message;
  static int overwrite_ignore = 1;
  static struct strbuf merge_msg = STRBUF_INIT;
@@@ -193,7 -193,7 +193,7 @@@ static struct option builtin_merge_opti
                "create a single commit instead of doing a merge"),
        OPT_BOOLEAN(0, "commit", &option_commit,
                "perform a commit if the merge succeeds (default)"),
 -      OPT_BOOLEAN('e', "edit", &option_edit,
 +      OPT_BOOL('e', "edit", &option_edit,
                "edit message before committing"),
        OPT_BOOLEAN(0, "ff", &allow_fast_forward,
                "allow fast-forward (default)"),
@@@ -399,6 -399,8 +399,8 @@@ static void finish(struct commit *head_
        if (new_head && show_diffstat) {
                struct diff_options opts;
                diff_setup(&opts);
+               opts.stat_width = -1; /* use full terminal width */
+               opts.stat_graph_width = -1; /* respect statGraphWidth config */
                opts.output_format |=
                        DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT;
                opts.detect_rename = DIFF_DETECT_RENAME;
@@@ -885,30 -887,20 +887,30 @@@ static void abort_commit(const char *er
        exit(1);
  }
  
 +static const char merge_editor_comment[] =
 +N_("Please enter a commit message to explain why this merge is necessary,\n"
 +   "especially if it merges an updated upstream into a topic branch.\n"
 +   "\n"
 +   "Lines starting with '#' will be ignored, and an empty message aborts\n"
 +   "the commit.\n");
 +
  static void prepare_to_commit(void)
  {
        struct strbuf msg = STRBUF_INIT;
 +      const char *comment = _(merge_editor_comment);
        strbuf_addbuf(&msg, &merge_msg);
        strbuf_addch(&msg, '\n');
 +      if (0 < option_edit)
 +              strbuf_add_lines(&msg, "# ", comment, strlen(comment));
        write_merge_msg(&msg);
        run_hook(get_index_file(), "prepare-commit-msg",
                 git_path("MERGE_MSG"), "merge", NULL, NULL);
 -      if (option_edit) {
 +      if (0 < option_edit) {
                if (launch_editor(git_path("MERGE_MSG"), NULL, NULL))
                        abort_commit(NULL);
        }
        read_merge_msg(&msg);
 -      stripspace(&msg, option_edit);
 +      stripspace(&msg, 0 < option_edit);
        if (!msg.len)
                abort_commit(_("Empty commit message."));
        strbuf_release(&merge_msg);
@@@ -1109,33 -1101,6 +1111,33 @@@ static void write_merge_state(void
        close(fd);
  }
  
 +static int default_edit_option(void)
 +{
 +      static const char name[] = "GIT_MERGE_AUTOEDIT";
 +      const char *e = getenv(name);
 +      struct stat st_stdin, st_stdout;
 +
 +      if (have_message)
 +              /* an explicit -m msg without --[no-]edit */
 +              return 0;
 +
 +      if (e) {
 +              int v = git_config_maybe_bool(name, e);
 +              if (v < 0)
 +                      die("Bad value '%s' in environment '%s'", e, name);
 +              return v;
 +      }
 +
 +      /* Use editor if stdin and stdout are the same and is a tty */
 +      return (!fstat(0, &st_stdin) &&
 +              !fstat(1, &st_stdout) &&
 +              isatty(0) && isatty(1) &&
 +              st_stdin.st_dev == st_stdout.st_dev &&
 +              st_stdin.st_ino == st_stdout.st_ino &&
 +              st_stdin.st_mode == st_stdout.st_mode);
 +}
 +
 +
  int cmd_merge(int argc, const char **argv, const char *prefix)
  {
        unsigned char result_tree[20];
                    merge_remote_util(commit) &&
                    merge_remote_util(commit)->obj &&
                    merge_remote_util(commit)->obj->type == OBJ_TAG) {
 -                      option_edit = 1;
 +                      if (option_edit < 0)
 +                              option_edit = 1;
                        allow_fast_forward = 0;
                }
        }
  
 +      if (option_edit < 0)
 +              option_edit = default_edit_option();
 +
        if (!use_strategies) {
                if (!remoteheads->next)
                        add_strategies(pull_twohead, DEFAULT_TWOHEAD);
index 33f0e4dd696c73ffca2b9d2c3955661f1cdd00cd,bacf40365b2eb0cfc4ba693b7dc4eb37382a1a80..fba076dde27440eaa90fd4fcfdfa55764410dd11
  #       per-repository basis by setting the bash.showUpstream config
  #       variable.
  #
 -#
 -# To submit patches:
 -#
 -#    *) Read Documentation/SubmittingPatches
 -#    *) Send all patches to the current maintainer:
 -#
 -#       "Shawn O. Pearce" <spearce@spearce.org>
 -#
 -#    *) Always CC the Git mailing list:
 -#
 -#       git@vger.kernel.org
 -#
  
  if [[ -n ${ZSH_VERSION-} ]]; then
        autoload -U +X bashcompinit && bashcompinit
@@@ -137,7 -149,7 +137,7 @@@ __git_ps1_show_upstream (
                        svn_upstream=${svn_upstream[ ${#svn_upstream[@]} - 2 ]}
                        svn_upstream=${svn_upstream%@*}
                        local n_stop="${#svn_remote[@]}"
 -                      for ((n=1; n <= n_stop; ++n)); do
 +                      for ((n=1; n <= n_stop; n++)); do
                                svn_upstream=${svn_upstream#${svn_remote[$n]}}
                        done
  
                        for commit in $commits
                        do
                                case "$commit" in
 -                              "<"*) let ++behind
 -                                      ;;
 -                              *)    let ++ahead
 -                                      ;;
 +                              "<"*) ((behind++)) ;;
 +                              *)    ((ahead++))  ;;
                                esac
                        done
                        count="$behind  $ahead"
@@@ -284,13 -298,13 +284,13 @@@ __git_ps1 (
                                fi
                        fi
                        if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ]; then
 -                              git rev-parse --verify refs/stash >/dev/null 2>&1 && s="$"
 +                              git rev-parse --verify refs/stash >/dev/null 2>&1 && s="$"
                        fi
  
                        if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ]; then
 -                         if [ -n "$(git ls-files --others --exclude-standard)" ]; then
 -                            u="%"
 -                         fi
 +                              if [ -n "$(git ls-files --others --exclude-standard)" ]; then
 +                                      u="%"
 +                              fi
                        fi
  
                        if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then
                fi
  
                local f="$w$i$s$u"
 -              printf "${1:- (%s)}" "$c${b##refs/heads/}${f:+ $f}$r$p"
 +              printf -- "${1:- (%s)}" "$c${b##refs/heads/}${f:+ $f}$r$p"
        fi
  }
  
@@@ -481,8 -495,11 +481,8 @@@ f
  # 4: A suffix to be appended to each possible completion word (optional).
  __gitcomp ()
  {
 -      local cur_="$cur"
 +      local cur_="${3-$cur}"
  
 -      if [ $# -gt 2 ]; then
 -              cur_="$3"
 -      fi
        case "$cur_" in
        --*=)
                COMPREPLY=()
  #    appended.
  __gitcomp_nl ()
  {
 -      local s=$'\n' IFS=' '$'\t'$'\n'
 -      local cur_="$cur" suffix=" "
 -
 -      if [ $# -gt 2 ]; then
 -              cur_="$3"
 -              if [ $# -gt 3 ]; then
 -                      suffix="$4"
 -              fi
 -      fi
 -
 -      IFS=$s
 -      COMPREPLY=($(compgen -P "${2-}" -S "$suffix" -W "$1" -- "$cur_"))
 +      local IFS=$'\n'
 +      COMPREPLY=($(compgen -P "${2-}" -S "${4- }" -W "$1" -- "${3-$cur}"))
  }
  
  __git_heads ()
@@@ -616,8 -643,13 +616,8 @@@ __git_refs_remotes (
  
  __git_remotes ()
  {
 -      local i ngoff IFS=$'\n' d="$(__gitdir)"
 -      __git_shopt -q nullglob || ngoff=1
 -      __git_shopt -s nullglob
 -      for i in "$d/remotes"/*; do
 -              echo ${i#$d/remotes/}
 -      done
 -      [ "$ngoff" ] && __git_shopt -u nullglob
 +      local i IFS=$'\n' d="$(__gitdir)"
 +      test -d "$d/remotes" && ls -1 "$d/remotes"
        for i in $(git --git-dir="$d" config --get-regexp 'remote\..*\.url' 2>/dev/null); do
                i="${i#remote.}"
                echo "${i/.url*/}"
@@@ -644,8 -676,7 +644,8 @@@ __git_merge_strategies
  # is needed.
  __git_compute_merge_strategies ()
  {
 -      : ${__git_merge_strategies:=$(__git_list_merge_strategies)}
 +      test -n "$__git_merge_strategies" ||
 +      __git_merge_strategies=$(__git_list_merge_strategies)
  }
  
  __git_complete_revlist_file ()
@@@ -724,9 -755,6 +724,9 @@@ __git_complete_remote_or_refspec (
  {
        local cur_="$cur" cmd="${words[1]}"
        local i c=2 remote="" pfx="" lhs=1 no_complete_refspec=0
 +      if [ "$cmd" = "remote" ]; then
 +              ((c++))
 +      fi
        while [ $c -lt $cword ]; do
                i="${words[c]}"
                case "$i" in
                -*) ;;
                *) remote="$i"; break ;;
                esac
 -              c=$((++c))
 +              ((c++))
        done
        if [ -z "$remote" ]; then
                __gitcomp_nl "$(__git_remotes)"
                        __gitcomp_nl "$(__git_refs)" "$pfx" "$cur_"
                fi
                ;;
 -      pull)
 +      pull|remote)
                if [ $lhs = 1 ]; then
                        __gitcomp_nl "$(__git_refs "$remote")" "$pfx" "$cur_"
                else
@@@ -826,8 -854,7 +826,8 @@@ __git_list_all_commands (
  __git_all_commands=
  __git_compute_all_commands ()
  {
 -      : ${__git_all_commands:=$(__git_list_all_commands)}
 +      test -n "$__git_all_commands" ||
 +      __git_all_commands=$(__git_list_all_commands)
  }
  
  __git_list_porcelain_commands ()
@@@ -920,8 -947,7 +920,8 @@@ __git_porcelain_commands
  __git_compute_porcelain_commands ()
  {
        __git_compute_all_commands
 -      : ${__git_porcelain_commands:=$(__git_list_porcelain_commands)}
 +      test -n "$__git_porcelain_commands" ||
 +      __git_porcelain_commands=$(__git_list_porcelain_commands)
  }
  
  __git_pretty_aliases ()
@@@ -984,7 -1010,7 +984,7 @@@ __git_find_on_cmdline (
                                return
                        fi
                done
 -              c=$((++c))
 +              ((c++))
        done
  }
  
@@@ -995,7 -1021,7 +995,7 @@@ __git_has_doubledash (
                if [ "--" = "${words[c]}" ]; then
                        return 0
                fi
 -              c=$((++c))
 +              ((c++))
        done
        return 1
  }
@@@ -1118,7 -1144,7 +1118,7 @@@ _git_branch (
                -d|-m)  only_local_ref="y" ;;
                -r)     has_r="y" ;;
                esac
 -              c=$((++c))
 +              ((c++))
        done
  
        case "$cur" in
                __gitcomp "
                        --color --no-color --verbose --abbrev= --no-abbrev
                        --track --no-track --contains --merged --no-merged
 -                      --set-upstream
 +                      --set-upstream --edit-description --list
                        "
                ;;
        *)
@@@ -2092,6 -2118,7 +2092,7 @@@ _git_config (
                core.whitespace
                core.worktree
                diff.autorefreshindex
+               diff.statGraphWidth
                diff.external
                diff.ignoreSubmodules
                diff.mnemonicprefix
  
  _git_remote ()
  {
 -      local subcommands="add rename rm show prune update set-head"
 +      local subcommands="add rename rm set-head set-branches set-url show prune update"
        local subcommand="$(__git_find_on_cmdline "$subcommands")"
        if [ -z "$subcommand" ]; then
                __gitcomp "$subcommands"
        fi
  
        case "$subcommand" in
 -      rename|rm|show|prune)
 +      rename|rm|set-url|show|prune)
                __gitcomp_nl "$(__git_remotes)"
                ;;
 +      set-head|set-branches)
 +              __git_complete_remote_or_refspec
 +              ;;
        update)
                local i c='' IFS=$'\n'
                for i in $(git --git-dir="$(__gitdir)" config --get-regexp "remotes\..*" 2>/dev/null); do
@@@ -2504,7 -2528,7 +2505,7 @@@ _git_svn (
                        __gitcomp "
                                --merge --strategy= --verbose --dry-run
                                --fetch-all --no-rebase --commit-url
 -                              --revision $cmt_opts $fc_opts
 +                              --revision --interactive $cmt_opts $fc_opts
                                "
                        ;;
                set-tree,--*)
@@@ -2572,7 -2596,7 +2573,7 @@@ _git_tag (
                        f=1
                        ;;
                esac
 -              c=$((++c))
 +              ((c++))
        done
  
        case "$prev" in
@@@ -2625,7 -2649,7 +2626,7 @@@ _git (
                --help) command="help"; break ;;
                *) command="$i"; break ;;
                esac
 -              c=$((++c))
 +              ((c++))
        done
  
        if [ -z "$command" ]; then
@@@ -2710,3 -2734,33 +2711,3 @@@ if [ Cygwin = "$(uname -o 2>/dev/null)
  complete -o bashdefault -o default -o nospace -F _git git.exe 2>/dev/null \
        || complete -o default -o nospace -F _git git.exe
  fi
 -
 -if [[ -n ${ZSH_VERSION-} ]]; then
 -      __git_shopt () {
 -              local option
 -              if [ $# -ne 2 ]; then
 -                      echo "USAGE: $0 (-q|-s|-u) <option>" >&2
 -                      return 1
 -              fi
 -              case "$2" in
 -              nullglob)
 -                      option="$2"
 -                      ;;
 -              *)
 -                      echo "$0: invalid option: $2" >&2
 -                      return 1
 -              esac
 -              case "$1" in
 -              -q)     setopt | grep -q "$option" ;;
 -              -u)     unsetopt "$option" ;;
 -              -s)     setopt "$option" ;;
 -              *)
 -                      echo "$0: invalid flag: $1" >&2
 -                      return 1
 -              esac
 -      }
 -else
 -      __git_shopt () {
 -              shopt "$@"
 -      }
 -fi
diff --combined diff.c
index a1c06b554b80e433dcb735c8e0adf186b6252ae5,4525cdadd805e018cfe158ea3e867ac882795a4f..f47bffa2a3641cbf69b2e6d3f0f6c93b05f2c716
--- 1/diff.c
--- 2/diff.c
+++ b/diff.c
@@@ -31,6 -31,7 +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;
  
@@@ -156,6 -157,10 +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"))
@@@ -177,8 -182,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);
@@@ -1321,61 -1329,12 +1326,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).
+        *
+        * 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).
         *
-        * From here on, name_width is the width of the name area,
-        * and width is the width of the graph area.
+        * 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) {
+               if (graph_width <= max_change) {
                        int total = add + del;
  
-                       total = scale_linear(add + del, width, max_change);
+                       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, width, max_change);
+                               add = scale_linear(add, graph_width, max_change);
                                del = total - add;
                        } else {
-                               del = scale_linear(del, width, max_change);
+                               del = scale_linear(del, graph_width, max_change);
                                add = total - del;
                        }
                }
                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)
@@@ -3299,6 -3304,7 +3347,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;
diff --combined diff.h
index 7af5f1e2a7af3fde64ac945f8b2a9ed88829895a,2021a1dfe6b47a12a215dbb92ba0cee5aedbd8ff..e6a782c220865d764eb26cfbce471cad88b910ba
--- 1/diff.h
--- 2/diff.h
+++ b/diff.h
@@@ -129,6 -129,7 +129,7 @@@ struct diff_options 
  
        int stat_width;
        int stat_name_width;
+       int stat_graph_width;
        int stat_count;
        const char *word_regex;
        enum diff_words_type word_diff;
@@@ -324,7 -325,4 +325,7 @@@ extern struct userdiff_driver *get_text
  
  extern int parse_rename_score(const char **cp_p);
  
 +extern int print_stat_summary(FILE *fp, int files,
 +                            int insertions, int deletions);
 +
  #endif /* DIFF_H */