Merge branch 'sb/blame-color'
authorJunio C Hamano <gitster@pobox.com>
Wed, 30 May 2018 05:04:09 +0000 (14:04 +0900)
committerJunio C Hamano <gitster@pobox.com>
Wed, 30 May 2018 05:04:09 +0000 (14:04 +0900)
"git blame" learns to unhighlight uninteresting metadata from the
originating commit on lines that are the same as the previous one,
and also paint lines in different colors depending on the age of
the commit.

* sb/blame-color:
builtin/blame: add new coloring scheme config
builtin/blame: highlight recently changed lines
builtin/blame: dim uninteresting metadata lines

1  2 
Documentation/config.txt
builtin/blame.c
diff --combined Documentation/config.txt
index 460c14f0476f0c1c2c85fbbb37b00875c38f3548,4eb030ed43736959a111d2aa9dc6401a64a75837..7d8383433ce9995c1950705fb9b345eff1bd652d
@@@ -530,12 -530,6 +530,12 @@@ core.autocrlf:
        This variable can be set to 'input',
        in which case no output conversion is performed.
  
 +core.checkRoundtripEncoding::
 +      A comma and/or whitespace separated list of encodings that Git
 +      performs UTF-8 round trip checks on if they are used in an
 +      `working-tree-encoding` attribute (see linkgit:gitattributes[5]).
 +      The default value is `SHIFT-JIS`.
 +
  core.symlinks::
        If false, symbolic links are checked out as small plain files that
        contain the link text. linkgit:git-update-index[1] and
@@@ -904,10 -898,6 +904,10 @@@ core.notesRef:
  This setting defaults to "refs/notes/commits", and it can be overridden by
  the `GIT_NOTES_REF` environment variable.  See linkgit:git-notes[1].
  
 +core.commitGraph::
 +      Enable git commit graph feature. Allows reading from the
 +      commit-graph file.
 +
  core.sparseCheckout::
        Enable "sparse checkout" feature. See section "Sparse checkout" in
        linkgit:git-read-tree[1] for more information.
@@@ -1068,10 -1058,6 +1068,10 @@@ branch.<name>.rebase:
        "git pull" is run. See "pull.rebase" for doing this in a non
        branch-specific manner.
  +
 +When `merges`, pass the `--rebase-merges` option to 'git rebase'
 +so that the local merge commits are included in the rebase (see
 +linkgit:git-rebase[1] for details).
 ++
  When preserve, also pass `--preserve-merges` along to 'git rebase'
  so that locally committed merge commits will not be flattened
  by running 'git pull'.
@@@ -1102,16 -1088,6 +1102,16 @@@ clean.requireForce:
        A boolean to make git-clean do nothing unless given -f,
        -i or -n.   Defaults to true.
  
 +color.advice::
 +      A boolean to enable/disable color in hints (e.g. when a push
 +      failed, see `advice.*` for a list).  May be set to `always`,
 +      `false` (or `never`) or `auto` (or `true`), in which case colors
 +      are used only when the error output goes to a terminal. If
 +      unset, then the value of `color.ui` is used (`auto` by default).
 +
 +color.advice.hint::
 +      Use customized color for hints.
 +
  color.branch::
        A boolean to enable/disable color in the output of
        linkgit:git-branch[1]. May be set to `always`,
@@@ -1214,15 -1190,6 +1214,15 @@@ color.pager:
        A boolean to enable/disable colored output when the pager is in
        use (default is true).
  
 +color.push::
 +      A boolean to enable/disable color in push errors. May be set to
 +      `always`, `false` (or `never`) or `auto` (or `true`), in which
 +      case colors are used only when the error output goes to a terminal.
 +      If unset, then the value of `color.ui` is used (`auto` by default).
 +
 +color.push.error::
 +      Use customized color for push errors.
 +
  color.showBranch::
        A boolean to enable/disable color in the output of
        linkgit:git-show-branch[1]. May be set to `always`,
@@@ -1251,15 -1218,33 +1251,42 @@@ color.status.<slot>:
        status short-format), or
        `unmerged` (files which have unmerged changes).
  
+ color.blame.repeatedLines::
+       Use the customized color for the part of git-blame output that
+       is repeated meta information per line (such as commit id,
+       author name, date and timezone). Defaults to cyan.
+ color.blame.highlightRecent::
+       This can be used to color the metadata of a blame line depending
+       on age of the line.
+ +
+ This setting should be set to a comma-separated list of color and date settings,
+ starting and ending with a color, the dates should be set from oldest to newest.
+ The metadata will be colored given the colors if the the line was introduced
+ before the given timestamp, overwriting older timestamped colors.
+ +
+ Instead of an absolute timestamp relative timestamps work as well, e.g.
+ 2.weeks.ago is valid to address anything older than 2 weeks.
+ +
+ It defaults to 'blue,12 month ago,white,1 month ago,red', which colors
+ everything older than one year blue, recent changes between one month and
+ one year old are kept white, and lines introduced within the last month are
+ colored red.
+ blame.coloring::
+       This determines the coloring scheme to be applied to blame
+       output. It can be 'repeatedLines', 'highlightRecent',
+       or 'none' which is the default.
 +color.transport::
 +      A boolean to enable/disable color when pushes are rejected. May be
 +      set to `always`, `false` (or `never`) or `auto` (or `true`), in which
 +      case colors are used only when the error output goes to a terminal.
 +      If unset, then the value of `color.ui` is used (`auto` by default).
 +
 +color.transport.rejected::
 +      Use customized color when a push was rejected.
 +
  color.ui::
        This variable determines the default value for variables such
        as `color.diff` and `color.grep` that control the use of color
@@@ -1440,16 -1425,7 +1467,16 @@@ fetch.unpackLimit:
  
  fetch.prune::
        If true, fetch will automatically behave as if the `--prune`
 -      option was given on the command line.  See also `remote.<name>.prune`.
 +      option was given on the command line.  See also `remote.<name>.prune`
 +      and the PRUNING section of linkgit:git-fetch[1].
 +
 +fetch.pruneTags::
 +      If true, fetch will automatically behave as if the
 +      `refs/tags/*:refs/tags/*` refspec was provided when pruning,
 +      if not set already. This allows for setting both this option
 +      and `fetch.prune` to maintain a 1=1 mapping to upstream
 +      refs. See also `remote.<name>.pruneTags` and the PRUNING
 +      section of linkgit:git-fetch[1].
  
  fetch.output::
        Control how ref update status is printed. Valid values are
@@@ -1600,18 -1576,6 +1627,18 @@@ gc.autoDetach:
        Make `git gc --auto` return immediately and run in background
        if the system supports it. Default is true.
  
 +gc.bigPackThreshold::
 +      If non-zero, all packs larger than this limit are kept when
 +      `git gc` is run. This is very similar to `--keep-base-pack`
 +      except that all packs that meet the threshold are kept, not
 +      just the base pack. Defaults to zero. Common unit suffixes of
 +      'k', 'm', or 'g' are supported.
 ++
 +Note that if the number of kept packs is more than gc.autoPackLimit,
 +this configuration variable is ignored, all packs except the base pack
 +will be repacked. After this the number of packs should go below
 +gc.autoPackLimit and gc.bigPackThreshold should be respected again.
 +
  gc.logExpiry::
        If the file gc.log exists, then `git gc --auto` won't run
        unless that file is more than 'gc.logExpiry' old.  Default is
@@@ -2011,7 -1975,6 +2038,7 @@@ http.sslVersion:
        - tlsv1.0
        - tlsv1.1
        - tlsv1.2
 +      - tlsv1.3
  
  +
  Can be overridden by the `GIT_SSL_VERSION` environment variable.
@@@ -2476,7 -2439,6 +2503,7 @@@ pack.window:
  pack.depth::
        The maximum delta depth used by linkgit:git-pack-objects[1] when no
        maximum depth is given on the command line. Defaults to 50.
 +      Maximum value is 4095.
  
  pack.windowMemory::
        The maximum size of memory that is consumed by each thread
@@@ -2513,8 -2475,7 +2540,8 @@@ pack.deltaCacheLimit:
        The maximum size of a delta, that is cached in
        linkgit:git-pack-objects[1]. This cache is used to speed up the
        writing object phase by not having to recompute the final delta
 -      result once the best match for all objects is found. Defaults to 1000.
 +      result once the best match for all objects is found.
 +      Defaults to 1000. Maximum value is 65535.
  
  pack.threads::
        Specifies the number of threads to spawn when searching for best
@@@ -2673,10 -2634,6 +2700,10 @@@ pull.rebase:
        pull" is run. See "branch.<name>.rebase" for setting this on a
        per-branch basis.
  +
 +When `merges`, pass the `--rebase-merges` option to 'git rebase'
 +so that the local merge commits are included in the rebase (see
 +linkgit:git-rebase[1] for details).
 ++
  When preserve, also pass `--preserve-merges` along to 'git rebase'
  so that locally committed merge commits will not be flattened
  by running 'git pull'.
@@@ -3015,15 -2972,6 +3042,15 @@@ remote.<name>.prune:
        remote (as if the `--prune` option was given on the command line).
        Overrides `fetch.prune` settings, if any.
  
 +remote.<name>.pruneTags::
 +      When set to true, fetching from this remote by default will also
 +      remove any local tags that no longer exist on the remote if pruning
 +      is activated in general via `remote.<name>.prune`, `fetch.prune` or
 +      `--prune`. Overrides `fetch.pruneTags` settings, if any.
 ++
 +See also `remote.<name>.prune` and the PRUNING section of
 +linkgit:git-fetch[1].
 +
  remotes.<group>::
        The list of remotes which are fetched by "git remote update
        <group>".  See linkgit:git-remote[1].
@@@ -3179,18 -3127,6 +3206,18 @@@ status.displayCommentPrefix:
        behavior of linkgit:git-status[1] in Git 1.8.4 and previous.
        Defaults to false.
  
 +status.renameLimit::
 +      The number of files to consider when performing rename detection
 +      in linkgit:git-status[1] and linkgit:git-commit[1]. Defaults to
 +      the value of diff.renameLimit.
 +
 +status.renames::
 +      Whether and how Git detects renames in linkgit:git-status[1] and
 +      linkgit:git-commit[1] .  If set to "false", rename detection is
 +      disabled. If set to "true", basic rename detection is enabled.
 +      If set to "copies" or "copy", Git will detect copies, as well.
 +      Defaults to the value of diff.renames.
 +
  status.showStash::
        If set to true, linkgit:git-status[1] will display the number of
        entries currently stashed away.
@@@ -3301,8 -3237,7 +3328,8 @@@ submodule.active:
  
  submodule.recurse::
        Specifies if commands recurse into submodules by default. This
 -      applies to all commands that have a `--recurse-submodules` option.
 +      applies to all commands that have a `--recurse-submodules` option,
 +      except `clone`.
        Defaults to false.
  
  submodule.fetchJobs::
@@@ -3435,10 -3370,6 +3462,10 @@@ uploadpack.packObjectsHook:
        was run. I.e., `upload-pack` will feed input intended for
        `pack-objects` to the hook, and expects a completed packfile on
        stdout.
 +
 +uploadpack.allowFilter::
 +      If this option is set, `upload-pack` will support partial
 +      clone and partial fetch object filtering.
  +
  Note that this configuration variable is ignored if it is seen in the
  repository-level config (this is a safety measure against fetching from
diff --combined builtin/blame.c
index bfdf7cc1325826d6b7a41520b52a07f2fa1cb042,f95aac746872af820a6c69596207620b80e5793c..4202584f97922f39f8631e23b01981ed5687b793
@@@ -7,6 -7,7 +7,7 @@@
  
  #include "cache.h"
  #include "config.h"
+ #include "color.h"
  #include "builtin.h"
  #include "commit.h"
  #include "diff.h"
@@@ -23,6 -24,7 +24,7 @@@
  #include "dir.h"
  #include "progress.h"
  #include "blame.h"
+ #include "string-list.h"
  
  static char blame_usage[] = N_("git blame [<options>] [<rev-opts>] [<rev>] [--] <file>");
  
@@@ -46,6 -48,8 +48,8 @@@ static int xdl_opts
  static int abbrev = -1;
  static int no_whole_file_rename;
  static int show_progress;
+ static char repeated_meta_color[COLOR_MAXLEN];
+ static int coloring_mode;
  
  static struct date_mode blame_date_mode = { DATE_ISO8601 };
  static size_t blame_date_width;
@@@ -316,10 -320,12 +320,12 @@@ static const char *format_time(timestam
  #define OUTPUT_PORCELAIN      010
  #define OUTPUT_SHOW_NAME      020
  #define OUTPUT_SHOW_NUMBER    040
- #define OUTPUT_SHOW_SCORE      0100
- #define OUTPUT_NO_AUTHOR       0200
+ #define OUTPUT_SHOW_SCORE     0100
+ #define OUTPUT_NO_AUTHOR      0200
  #define OUTPUT_SHOW_EMAIL     0400
- #define OUTPUT_LINE_PORCELAIN 01000
+ #define OUTPUT_LINE_PORCELAIN 01000
+ #define OUTPUT_COLOR_LINE     02000
+ #define OUTPUT_SHOW_AGE_WITH_COLOR    04000
  
  static void emit_porcelain_details(struct blame_origin *suspect, int repeat)
  {
@@@ -367,6 -373,63 +373,63 @@@ static void emit_porcelain(struct blame
                putchar('\n');
  }
  
+ static struct color_field {
+       timestamp_t hop;
+       char col[COLOR_MAXLEN];
+ } *colorfield;
+ static int colorfield_nr, colorfield_alloc;
+ static void parse_color_fields(const char *s)
+ {
+       struct string_list l = STRING_LIST_INIT_DUP;
+       struct string_list_item *item;
+       enum { EXPECT_DATE, EXPECT_COLOR } next = EXPECT_COLOR;
+       colorfield_nr = 0;
+       /* Ideally this would be stripped and split at the same time? */
+       string_list_split(&l, s, ',', -1);
+       ALLOC_GROW(colorfield, colorfield_nr + 1, colorfield_alloc);
+       for_each_string_list_item(item, &l) {
+               switch (next) {
+               case EXPECT_DATE:
+                       colorfield[colorfield_nr].hop = approxidate(item->string);
+                       next = EXPECT_COLOR;
+                       colorfield_nr++;
+                       ALLOC_GROW(colorfield, colorfield_nr + 1, colorfield_alloc);
+                       break;
+               case EXPECT_COLOR:
+                       if (color_parse(item->string, colorfield[colorfield_nr].col))
+                               die(_("expecting a color: %s"), item->string);
+                       next = EXPECT_DATE;
+                       break;
+               }
+       }
+       if (next == EXPECT_COLOR)
+               die (_("must end with a color"));
+       colorfield[colorfield_nr].hop = TIME_MAX;
+ }
+ static void setup_default_color_by_age(void)
+ {
+       parse_color_fields("blue,12 month ago,white,1 month ago,red");
+ }
+ static void determine_line_heat(struct blame_entry *ent, const char **dest_color)
+ {
+       int i = 0;
+       struct commit_info ci;
+       get_commit_info(ent->suspect->commit, &ci, 1);
+       while (i < colorfield_nr && ci.author_time > colorfield[i].hop)
+               i++;
+       *dest_color = colorfield[i].col;
+ }
  static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent, int opt)
  {
        int cnt;
        struct commit_info ci;
        char hex[GIT_MAX_HEXSZ + 1];
        int show_raw_time = !!(opt & OUTPUT_RAW_TIMESTAMP);
+       const char *default_color = NULL, *color = NULL, *reset = NULL;
  
        get_commit_info(suspect->commit, &ci, 1);
        oid_to_hex_r(hex, &suspect->commit->object.oid);
  
        cp = blame_nth_line(sb, ent->lno);
+       if (opt & OUTPUT_SHOW_AGE_WITH_COLOR) {
+               determine_line_heat(ent, &default_color);
+               color = default_color;
+               reset = GIT_COLOR_RESET;
+       }
        for (cnt = 0; cnt < ent->num_lines; cnt++) {
                char ch;
                int length = (opt & OUTPUT_LONG_OBJECT_NAME) ? GIT_SHA1_HEXSZ : abbrev;
  
+               if (opt & OUTPUT_COLOR_LINE) {
+                       if (cnt > 0) {
+                               color = repeated_meta_color;
+                               reset = GIT_COLOR_RESET;
+                       } else  {
+                               color = default_color ? default_color : NULL;
+                               reset = default_color ? GIT_COLOR_RESET : NULL;
+                       }
+               }
+               if (color)
+                       fputs(color, stdout);
                if (suspect->commit->object.flags & UNINTERESTING) {
                        if (blank_boundary)
                                memset(hex, ' ', length);
                        printf(" %*d) ",
                               max_digits, ent->lno + 1 + cnt);
                }
+               if (reset)
+                       fputs(reset, stdout);
                do {
                        ch = *cp++;
                        putchar(ch);
@@@ -499,7 -584,7 +584,7 @@@ static int read_ancestry(const char *gr
  
  static int update_auto_abbrev(int auto_abbrev, struct blame_origin *suspect)
  {
 -      const char *uniq = find_unique_abbrev(suspect->commit->object.oid.hash,
 +      const char *uniq = find_unique_abbrev(&suspect->commit->object.oid,
                                              auto_abbrev);
        int len = strlen(uniq);
        if (auto_abbrev < len)
@@@ -607,6 -692,30 +692,30 @@@ static int git_blame_config(const char 
                parse_date_format(value, &blame_date_mode);
                return 0;
        }
+       if (!strcmp(var, "color.blame.repeatedlines")) {
+               if (color_parse_mem(value, strlen(value), repeated_meta_color))
+                       warning(_("invalid color '%s' in color.blame.repeatedLines"),
+                               value);
+               return 0;
+       }
+       if (!strcmp(var, "color.blame.highlightrecent")) {
+               parse_color_fields(value);
+               return 0;
+       }
+       if (!strcmp(var, "blame.coloring")) {
+               if (!strcmp(value, "repeatedLines")) {
+                       coloring_mode |= OUTPUT_COLOR_LINE;
+               } else if (!strcmp(value, "highlightRecent")) {
+                       coloring_mode |= OUTPUT_SHOW_AGE_WITH_COLOR;
+               } else if (!strcmp(value, "none")) {
+                       coloring_mode &= ~(OUTPUT_COLOR_LINE |
+                                           OUTPUT_SHOW_AGE_WITH_COLOR);
+               } else {
+                       warning(_("invalid value for blame.coloring"));
+                       return 0;
+               }
+       }
  
        if (git_diff_heuristic_config(var, value, cb) < 0)
                return -1;
@@@ -649,15 -758,6 +758,15 @@@ static int blame_move_callback(const st
        return 0;
  }
  
 +static int is_a_rev(const char *name)
 +{
 +      struct object_id oid;
 +
 +      if (get_oid(name, &oid))
 +              return 0;
 +      return OBJ_NONE < oid_object_info(the_repository, &oid, NULL);
 +}
 +
  int cmd_blame(int argc, const char **argv, const char *prefix)
  {
        struct rev_info revs;
                OPT_BIT('s', NULL, &output_option, N_("Suppress author name and timestamp (Default: off)"), OUTPUT_NO_AUTHOR),
                OPT_BIT('e', "show-email", &output_option, N_("Show author email instead of name (Default: off)"), OUTPUT_SHOW_EMAIL),
                OPT_BIT('w', NULL, &xdl_opts, N_("Ignore whitespace differences"), XDF_IGNORE_WHITESPACE),
+               OPT_BIT(0, "color-lines", &output_option, N_("color redundant metadata from previous line differently"), OUTPUT_COLOR_LINE),
+               OPT_BIT(0, "color-by-age", &output_option, N_("color lines by age"), OUTPUT_SHOW_AGE_WITH_COLOR),
  
                /*
                 * The following two options are parsed by parse_revision_opt()
        unsigned int range_i;
        long anchor;
  
+       setup_default_color_by_age();
        git_config(git_blame_config, &output_option);
        init_revisions(&revs, NULL);
        revs.date_mode = blame_date_mode;
        for (;;) {
                switch (parse_options_step(&ctx, options, blame_opt_usage)) {
                case PARSE_OPT_HELP:
 +              case PARSE_OPT_ERROR:
                        exit(129);
                case PARSE_OPT_DONE:
                        if (ctx.argv[0])
@@@ -855,15 -957,16 +967,15 @@@ parse_done
        } else {
                if (argc < 2)
                        usage_with_options(blame_opt_usage, options);
 -              path = add_prefix(prefix, argv[argc - 1]);
 -              if (argc == 3 && !file_exists(path)) { /* (2b) */
 +              if (argc == 3 && is_a_rev(argv[argc - 1])) { /* (2b) */
                        path = add_prefix(prefix, argv[1]);
                        argv[1] = argv[2];
 +              } else {        /* (2a) */
 +                      if (argc == 2 && is_a_rev(argv[1]) && !get_git_work_tree())
 +                              die("missing <path> to blame");
 +                      path = add_prefix(prefix, argv[argc - 1]);
                }
                argv[argc - 1] = "--";
 -
 -              setup_work_tree();
 -              if (!file_exists(path))
 -                      die_errno("cannot stat path '%s'", path);
        }
  
        revs.disable_stdin = 1;
  
        blame_coalesce(&sb);
  
-       if (!(output_option & OUTPUT_PORCELAIN))
+       if (!(output_option & (OUTPUT_COLOR_LINE | OUTPUT_SHOW_AGE_WITH_COLOR)))
+               output_option |= coloring_mode;
+       if (!(output_option & OUTPUT_PORCELAIN)) {
                find_alignment(&sb, &output_option);
+               if (!*repeated_meta_color &&
+                   (output_option & OUTPUT_COLOR_LINE))
+                       strcpy(repeated_meta_color, GIT_COLOR_CYAN);
+       }
+       if (output_option & OUTPUT_ANNOTATE_COMPAT)
+               output_option &= ~(OUTPUT_COLOR_LINE | OUTPUT_SHOW_AGE_WITH_COLOR);
  
        output(&sb, output_option);
        free((void *)sb.final_buf);