Skip to content

Commit 3d24129

Browse files
committed
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
2 parents 2a98a87 + 0dc95a4 commit 3d24129

File tree

3 files changed

+200
-4
lines changed

3 files changed

+200
-4
lines changed

Documentation/config.txt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,6 +1251,33 @@ color.status.<slot>::
12511251
status short-format), or
12521252
`unmerged` (files which have unmerged changes).
12531253

1254+
color.blame.repeatedLines::
1255+
Use the customized color for the part of git-blame output that
1256+
is repeated meta information per line (such as commit id,
1257+
author name, date and timezone). Defaults to cyan.
1258+
1259+
color.blame.highlightRecent::
1260+
This can be used to color the metadata of a blame line depending
1261+
on age of the line.
1262+
+
1263+
This setting should be set to a comma-separated list of color and date settings,
1264+
starting and ending with a color, the dates should be set from oldest to newest.
1265+
The metadata will be colored given the colors if the the line was introduced
1266+
before the given timestamp, overwriting older timestamped colors.
1267+
+
1268+
Instead of an absolute timestamp relative timestamps work as well, e.g.
1269+
2.weeks.ago is valid to address anything older than 2 weeks.
1270+
+
1271+
It defaults to 'blue,12 month ago,white,1 month ago,red', which colors
1272+
everything older than one year blue, recent changes between one month and
1273+
one year old are kept white, and lines introduced within the last month are
1274+
colored red.
1275+
1276+
blame.coloring::
1277+
This determines the coloring scheme to be applied to blame
1278+
output. It can be 'repeatedLines', 'highlightRecent',
1279+
or 'none' which is the default.
1280+
12541281
color.transport::
12551282
A boolean to enable/disable color when pushes are rejected. May be
12561283
set to `always`, `false` (or `never`) or `auto` (or `true`), in which

builtin/blame.c

Lines changed: 125 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include "cache.h"
99
#include "config.h"
10+
#include "color.h"
1011
#include "builtin.h"
1112
#include "commit.h"
1213
#include "diff.h"
@@ -23,6 +24,7 @@
2324
#include "dir.h"
2425
#include "progress.h"
2526
#include "blame.h"
27+
#include "string-list.h"
2628

2729
static char blame_usage[] = N_("git blame [<options>] [<rev-opts>] [<rev>] [--] <file>");
2830

@@ -46,6 +48,8 @@ static int xdl_opts;
4648
static int abbrev = -1;
4749
static int no_whole_file_rename;
4850
static int show_progress;
51+
static char repeated_meta_color[COLOR_MAXLEN];
52+
static int coloring_mode;
4953

5054
static struct date_mode blame_date_mode = { DATE_ISO8601 };
5155
static size_t blame_date_width;
@@ -316,10 +320,12 @@ static const char *format_time(timestamp_t time, const char *tz_str,
316320
#define OUTPUT_PORCELAIN 010
317321
#define OUTPUT_SHOW_NAME 020
318322
#define OUTPUT_SHOW_NUMBER 040
319-
#define OUTPUT_SHOW_SCORE 0100
320-
#define OUTPUT_NO_AUTHOR 0200
323+
#define OUTPUT_SHOW_SCORE 0100
324+
#define OUTPUT_NO_AUTHOR 0200
321325
#define OUTPUT_SHOW_EMAIL 0400
322-
#define OUTPUT_LINE_PORCELAIN 01000
326+
#define OUTPUT_LINE_PORCELAIN 01000
327+
#define OUTPUT_COLOR_LINE 02000
328+
#define OUTPUT_SHOW_AGE_WITH_COLOR 04000
323329

324330
static void emit_porcelain_details(struct blame_origin *suspect, int repeat)
325331
{
@@ -367,6 +373,63 @@ static void emit_porcelain(struct blame_scoreboard *sb, struct blame_entry *ent,
367373
putchar('\n');
368374
}
369375

376+
static struct color_field {
377+
timestamp_t hop;
378+
char col[COLOR_MAXLEN];
379+
} *colorfield;
380+
static int colorfield_nr, colorfield_alloc;
381+
382+
static void parse_color_fields(const char *s)
383+
{
384+
struct string_list l = STRING_LIST_INIT_DUP;
385+
struct string_list_item *item;
386+
enum { EXPECT_DATE, EXPECT_COLOR } next = EXPECT_COLOR;
387+
388+
colorfield_nr = 0;
389+
390+
/* Ideally this would be stripped and split at the same time? */
391+
string_list_split(&l, s, ',', -1);
392+
ALLOC_GROW(colorfield, colorfield_nr + 1, colorfield_alloc);
393+
394+
for_each_string_list_item(item, &l) {
395+
switch (next) {
396+
case EXPECT_DATE:
397+
colorfield[colorfield_nr].hop = approxidate(item->string);
398+
next = EXPECT_COLOR;
399+
colorfield_nr++;
400+
ALLOC_GROW(colorfield, colorfield_nr + 1, colorfield_alloc);
401+
break;
402+
case EXPECT_COLOR:
403+
if (color_parse(item->string, colorfield[colorfield_nr].col))
404+
die(_("expecting a color: %s"), item->string);
405+
next = EXPECT_DATE;
406+
break;
407+
}
408+
}
409+
410+
if (next == EXPECT_COLOR)
411+
die (_("must end with a color"));
412+
413+
colorfield[colorfield_nr].hop = TIME_MAX;
414+
}
415+
416+
static void setup_default_color_by_age(void)
417+
{
418+
parse_color_fields("blue,12 month ago,white,1 month ago,red");
419+
}
420+
421+
static void determine_line_heat(struct blame_entry *ent, const char **dest_color)
422+
{
423+
int i = 0;
424+
struct commit_info ci;
425+
get_commit_info(ent->suspect->commit, &ci, 1);
426+
427+
while (i < colorfield_nr && ci.author_time > colorfield[i].hop)
428+
i++;
429+
430+
*dest_color = colorfield[i].col;
431+
}
432+
370433
static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent, int opt)
371434
{
372435
int cnt;
@@ -375,15 +438,35 @@ static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent, int
375438
struct commit_info ci;
376439
char hex[GIT_MAX_HEXSZ + 1];
377440
int show_raw_time = !!(opt & OUTPUT_RAW_TIMESTAMP);
441+
const char *default_color = NULL, *color = NULL, *reset = NULL;
378442

379443
get_commit_info(suspect->commit, &ci, 1);
380444
oid_to_hex_r(hex, &suspect->commit->object.oid);
381445

382446
cp = blame_nth_line(sb, ent->lno);
447+
448+
if (opt & OUTPUT_SHOW_AGE_WITH_COLOR) {
449+
determine_line_heat(ent, &default_color);
450+
color = default_color;
451+
reset = GIT_COLOR_RESET;
452+
}
453+
383454
for (cnt = 0; cnt < ent->num_lines; cnt++) {
384455
char ch;
385456
int length = (opt & OUTPUT_LONG_OBJECT_NAME) ? GIT_SHA1_HEXSZ : abbrev;
386457

458+
if (opt & OUTPUT_COLOR_LINE) {
459+
if (cnt > 0) {
460+
color = repeated_meta_color;
461+
reset = GIT_COLOR_RESET;
462+
} else {
463+
color = default_color ? default_color : NULL;
464+
reset = default_color ? GIT_COLOR_RESET : NULL;
465+
}
466+
}
467+
if (color)
468+
fputs(color, stdout);
469+
387470
if (suspect->commit->object.flags & UNINTERESTING) {
388471
if (blank_boundary)
389472
memset(hex, ' ', length);
@@ -433,6 +516,8 @@ static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent, int
433516
printf(" %*d) ",
434517
max_digits, ent->lno + 1 + cnt);
435518
}
519+
if (reset)
520+
fputs(reset, stdout);
436521
do {
437522
ch = *cp++;
438523
putchar(ch);
@@ -607,6 +692,30 @@ static int git_blame_config(const char *var, const char *value, void *cb)
607692
parse_date_format(value, &blame_date_mode);
608693
return 0;
609694
}
695+
if (!strcmp(var, "color.blame.repeatedlines")) {
696+
if (color_parse_mem(value, strlen(value), repeated_meta_color))
697+
warning(_("invalid color '%s' in color.blame.repeatedLines"),
698+
value);
699+
return 0;
700+
}
701+
if (!strcmp(var, "color.blame.highlightrecent")) {
702+
parse_color_fields(value);
703+
return 0;
704+
}
705+
706+
if (!strcmp(var, "blame.coloring")) {
707+
if (!strcmp(value, "repeatedLines")) {
708+
coloring_mode |= OUTPUT_COLOR_LINE;
709+
} else if (!strcmp(value, "highlightRecent")) {
710+
coloring_mode |= OUTPUT_SHOW_AGE_WITH_COLOR;
711+
} else if (!strcmp(value, "none")) {
712+
coloring_mode &= ~(OUTPUT_COLOR_LINE |
713+
OUTPUT_SHOW_AGE_WITH_COLOR);
714+
} else {
715+
warning(_("invalid value for blame.coloring"));
716+
return 0;
717+
}
718+
}
610719

611720
if (git_diff_heuristic_config(var, value, cb) < 0)
612721
return -1;
@@ -690,6 +799,8 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
690799
OPT_BIT('s', NULL, &output_option, N_("Suppress author name and timestamp (Default: off)"), OUTPUT_NO_AUTHOR),
691800
OPT_BIT('e', "show-email", &output_option, N_("Show author email instead of name (Default: off)"), OUTPUT_SHOW_EMAIL),
692801
OPT_BIT('w', NULL, &xdl_opts, N_("Ignore whitespace differences"), XDF_IGNORE_WHITESPACE),
802+
OPT_BIT(0, "color-lines", &output_option, N_("color redundant metadata from previous line differently"), OUTPUT_COLOR_LINE),
803+
OPT_BIT(0, "color-by-age", &output_option, N_("color lines by age"), OUTPUT_SHOW_AGE_WITH_COLOR),
693804

694805
/*
695806
* 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)
714825
unsigned int range_i;
715826
long anchor;
716827

828+
setup_default_color_by_age();
717829
git_config(git_blame_config, &output_option);
718830
init_revisions(&revs, NULL);
719831
revs.date_mode = blame_date_mode;
@@ -949,8 +1061,17 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
9491061

9501062
blame_coalesce(&sb);
9511063

952-
if (!(output_option & OUTPUT_PORCELAIN))
1064+
if (!(output_option & (OUTPUT_COLOR_LINE | OUTPUT_SHOW_AGE_WITH_COLOR)))
1065+
output_option |= coloring_mode;
1066+
1067+
if (!(output_option & OUTPUT_PORCELAIN)) {
9531068
find_alignment(&sb, &output_option);
1069+
if (!*repeated_meta_color &&
1070+
(output_option & OUTPUT_COLOR_LINE))
1071+
strcpy(repeated_meta_color, GIT_COLOR_CYAN);
1072+
}
1073+
if (output_option & OUTPUT_ANNOTATE_COMPAT)
1074+
output_option &= ~(OUTPUT_COLOR_LINE | OUTPUT_SHOW_AGE_WITH_COLOR);
9541075

9551076
output(&sb, output_option);
9561077
free((void *)sb.final_buf);

t/t8012-blame-colors.sh

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/bin/sh
2+
3+
test_description='colored git blame'
4+
. ./test-lib.sh
5+
6+
PROG='git blame -c'
7+
. "$TEST_DIRECTORY"/annotate-tests.sh
8+
9+
test_expect_success 'colored blame colors contiguous lines' '
10+
git -c color.blame.repeatedLines=yellow blame --color-lines --abbrev=12 hello.c >actual.raw &&
11+
git -c color.blame.repeatedLines=yellow -c blame.coloring=repeatedLines blame --abbrev=12 hello.c >actual.raw.2 &&
12+
test_cmp actual.raw actual.raw.2 &&
13+
test_decode_color <actual.raw >actual &&
14+
grep "<YELLOW>" <actual >darkened &&
15+
grep "(F" darkened > F.expect &&
16+
grep "(H" darkened > H.expect &&
17+
test_line_count = 2 F.expect &&
18+
test_line_count = 3 H.expect
19+
'
20+
21+
test_expect_success 'color by age consistently colors old code' '
22+
git blame --color-by-age hello.c >actual.raw &&
23+
git -c blame.coloring=highlightRecent blame hello.c >actual.raw.2 &&
24+
test_cmp actual.raw actual.raw.2 &&
25+
test_decode_color <actual.raw >actual &&
26+
grep "<BLUE>" <actual >colored &&
27+
test_line_count = 10 colored
28+
'
29+
30+
test_expect_success 'blame color by age: new code is different' '
31+
cat >>hello.c <<-EOF &&
32+
void qfunc();
33+
EOF
34+
git add hello.c &&
35+
GIT_AUTHOR_DATE="" git commit -m "new commit" &&
36+
37+
git -c color.blame.highlightRecent="yellow,1 month ago, cyan" blame --color-by-age hello.c >actual.raw &&
38+
test_decode_color <actual.raw >actual &&
39+
40+
grep "<YELLOW>" <actual >colored &&
41+
test_line_count = 10 colored &&
42+
43+
grep "<CYAN>" <actual >colored &&
44+
test_line_count = 1 colored &&
45+
grep qfunc colored
46+
'
47+
48+
test_done

0 commit comments

Comments
 (0)