Merge branch 'sb/blame-color'
"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
This commit is contained in:
commit
3d24129799
@ -1251,6 +1251,33 @@ color.status.<slot>::
|
|||||||
status short-format), or
|
status short-format), or
|
||||||
`unmerged` (files which have unmerged changes).
|
`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::
|
color.transport::
|
||||||
A boolean to enable/disable color when pushes are rejected. May be
|
A boolean to enable/disable color when pushes are rejected. May be
|
||||||
set to `always`, `false` (or `never`) or `auto` (or `true`), in which
|
set to `always`, `false` (or `never`) or `auto` (or `true`), in which
|
||||||
|
123
builtin/blame.c
123
builtin/blame.c
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "cache.h"
|
#include "cache.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "color.h"
|
||||||
#include "builtin.h"
|
#include "builtin.h"
|
||||||
#include "commit.h"
|
#include "commit.h"
|
||||||
#include "diff.h"
|
#include "diff.h"
|
||||||
@ -23,6 +24,7 @@
|
|||||||
#include "dir.h"
|
#include "dir.h"
|
||||||
#include "progress.h"
|
#include "progress.h"
|
||||||
#include "blame.h"
|
#include "blame.h"
|
||||||
|
#include "string-list.h"
|
||||||
|
|
||||||
static char blame_usage[] = N_("git blame [<options>] [<rev-opts>] [<rev>] [--] <file>");
|
static char blame_usage[] = N_("git blame [<options>] [<rev-opts>] [<rev>] [--] <file>");
|
||||||
|
|
||||||
@ -46,6 +48,8 @@ static int xdl_opts;
|
|||||||
static int abbrev = -1;
|
static int abbrev = -1;
|
||||||
static int no_whole_file_rename;
|
static int no_whole_file_rename;
|
||||||
static int show_progress;
|
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 struct date_mode blame_date_mode = { DATE_ISO8601 };
|
||||||
static size_t blame_date_width;
|
static size_t blame_date_width;
|
||||||
@ -320,6 +324,8 @@ static const char *format_time(timestamp_t time, const char *tz_str,
|
|||||||
#define OUTPUT_NO_AUTHOR 0200
|
#define OUTPUT_NO_AUTHOR 0200
|
||||||
#define OUTPUT_SHOW_EMAIL 0400
|
#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)
|
static void emit_porcelain_details(struct blame_origin *suspect, int repeat)
|
||||||
{
|
{
|
||||||
@ -367,6 +373,63 @@ static void emit_porcelain(struct blame_scoreboard *sb, struct blame_entry *ent,
|
|||||||
putchar('\n');
|
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)
|
static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent, int opt)
|
||||||
{
|
{
|
||||||
int cnt;
|
int cnt;
|
||||||
@ -375,15 +438,35 @@ static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent, int
|
|||||||
struct commit_info ci;
|
struct commit_info ci;
|
||||||
char hex[GIT_MAX_HEXSZ + 1];
|
char hex[GIT_MAX_HEXSZ + 1];
|
||||||
int show_raw_time = !!(opt & OUTPUT_RAW_TIMESTAMP);
|
int show_raw_time = !!(opt & OUTPUT_RAW_TIMESTAMP);
|
||||||
|
const char *default_color = NULL, *color = NULL, *reset = NULL;
|
||||||
|
|
||||||
get_commit_info(suspect->commit, &ci, 1);
|
get_commit_info(suspect->commit, &ci, 1);
|
||||||
oid_to_hex_r(hex, &suspect->commit->object.oid);
|
oid_to_hex_r(hex, &suspect->commit->object.oid);
|
||||||
|
|
||||||
cp = blame_nth_line(sb, ent->lno);
|
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++) {
|
for (cnt = 0; cnt < ent->num_lines; cnt++) {
|
||||||
char ch;
|
char ch;
|
||||||
int length = (opt & OUTPUT_LONG_OBJECT_NAME) ? GIT_SHA1_HEXSZ : abbrev;
|
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 (suspect->commit->object.flags & UNINTERESTING) {
|
||||||
if (blank_boundary)
|
if (blank_boundary)
|
||||||
memset(hex, ' ', length);
|
memset(hex, ' ', length);
|
||||||
@ -433,6 +516,8 @@ static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent, int
|
|||||||
printf(" %*d) ",
|
printf(" %*d) ",
|
||||||
max_digits, ent->lno + 1 + cnt);
|
max_digits, ent->lno + 1 + cnt);
|
||||||
}
|
}
|
||||||
|
if (reset)
|
||||||
|
fputs(reset, stdout);
|
||||||
do {
|
do {
|
||||||
ch = *cp++;
|
ch = *cp++;
|
||||||
putchar(ch);
|
putchar(ch);
|
||||||
@ -607,6 +692,30 @@ static int git_blame_config(const char *var, const char *value, void *cb)
|
|||||||
parse_date_format(value, &blame_date_mode);
|
parse_date_format(value, &blame_date_mode);
|
||||||
return 0;
|
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)
|
if (git_diff_heuristic_config(var, value, cb) < 0)
|
||||||
return -1;
|
return -1;
|
||||||
@ -690,6 +799,8 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
|
|||||||
OPT_BIT('s', NULL, &output_option, N_("Suppress author name and timestamp (Default: off)"), OUTPUT_NO_AUTHOR),
|
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('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('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()
|
* The following two options are parsed by parse_revision_opt()
|
||||||
@ -714,6 +825,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
|
|||||||
unsigned int range_i;
|
unsigned int range_i;
|
||||||
long anchor;
|
long anchor;
|
||||||
|
|
||||||
|
setup_default_color_by_age();
|
||||||
git_config(git_blame_config, &output_option);
|
git_config(git_blame_config, &output_option);
|
||||||
init_revisions(&revs, NULL);
|
init_revisions(&revs, NULL);
|
||||||
revs.date_mode = blame_date_mode;
|
revs.date_mode = blame_date_mode;
|
||||||
@ -949,8 +1061,17 @@ parse_done:
|
|||||||
|
|
||||||
blame_coalesce(&sb);
|
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);
|
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);
|
output(&sb, output_option);
|
||||||
free((void *)sb.final_buf);
|
free((void *)sb.final_buf);
|
||||||
|
48
t/t8012-blame-colors.sh
Executable file
48
t/t8012-blame-colors.sh
Executable file
@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
test_description='colored git blame'
|
||||||
|
. ./test-lib.sh
|
||||||
|
|
||||||
|
PROG='git blame -c'
|
||||||
|
. "$TEST_DIRECTORY"/annotate-tests.sh
|
||||||
|
|
||||||
|
test_expect_success 'colored blame colors contiguous lines' '
|
||||||
|
git -c color.blame.repeatedLines=yellow blame --color-lines --abbrev=12 hello.c >actual.raw &&
|
||||||
|
git -c color.blame.repeatedLines=yellow -c blame.coloring=repeatedLines blame --abbrev=12 hello.c >actual.raw.2 &&
|
||||||
|
test_cmp actual.raw actual.raw.2 &&
|
||||||
|
test_decode_color <actual.raw >actual &&
|
||||||
|
grep "<YELLOW>" <actual >darkened &&
|
||||||
|
grep "(F" darkened > F.expect &&
|
||||||
|
grep "(H" darkened > H.expect &&
|
||||||
|
test_line_count = 2 F.expect &&
|
||||||
|
test_line_count = 3 H.expect
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'color by age consistently colors old code' '
|
||||||
|
git blame --color-by-age hello.c >actual.raw &&
|
||||||
|
git -c blame.coloring=highlightRecent blame hello.c >actual.raw.2 &&
|
||||||
|
test_cmp actual.raw actual.raw.2 &&
|
||||||
|
test_decode_color <actual.raw >actual &&
|
||||||
|
grep "<BLUE>" <actual >colored &&
|
||||||
|
test_line_count = 10 colored
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'blame color by age: new code is different' '
|
||||||
|
cat >>hello.c <<-EOF &&
|
||||||
|
void qfunc();
|
||||||
|
EOF
|
||||||
|
git add hello.c &&
|
||||||
|
GIT_AUTHOR_DATE="" git commit -m "new commit" &&
|
||||||
|
|
||||||
|
git -c color.blame.highlightRecent="yellow,1 month ago, cyan" blame --color-by-age hello.c >actual.raw &&
|
||||||
|
test_decode_color <actual.raw >actual &&
|
||||||
|
|
||||||
|
grep "<YELLOW>" <actual >colored &&
|
||||||
|
test_line_count = 10 colored &&
|
||||||
|
|
||||||
|
grep "<CYAN>" <actual >colored &&
|
||||||
|
test_line_count = 1 colored &&
|
||||||
|
grep qfunc colored
|
||||||
|
'
|
||||||
|
|
||||||
|
test_done
|
Loading…
Reference in New Issue
Block a user