Skip to content

Commit 25d5f52

Browse files
stefanbellergitster
authored andcommitted
builtin/blame: highlight recently changed lines
Choose a different color for dates and imitate a 'temperature cool down' depending upon age. Originally I had planned to have the temperature cool down dependent on the age of the project or file for example, as that might scale better, but that can be added on top of this commit, e.g. instead of giving a date, you could imagine giving a percentage that would be the linearly interpolated between now and the beginning of the file. Similarly to the previous patch, this offers the command line option '--color-by-age' to enable this mode and the config option 'color.blame.highlightrecent' to select colors. A later patch will offer a config option to select the default mode. Signed-off-by: Stefan Beller <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent cdc2d5f commit 25d5f52

File tree

3 files changed

+118
-4
lines changed

3 files changed

+118
-4
lines changed

Documentation/config.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,23 @@ color.blame.repeatedLines::
12231223
is repeated meta information per line (such as commit id,
12241224
author name, date and timezone). Defaults to cyan.
12251225

1226+
color.blame.highlightRecent::
1227+
This can be used to color the metadata of a blame line depending
1228+
on age of the line.
1229+
+
1230+
This setting should be set to a comma-separated list of color and date settings,
1231+
starting and ending with a color, the dates should be set from oldest to newest.
1232+
The metadata will be colored given the colors if the the line was introduced
1233+
before the given timestamp, overwriting older timestamped colors.
1234+
+
1235+
Instead of an absolute timestamp relative timestamps work as well, e.g.
1236+
2.weeks.ago is valid to address anything older than 2 weeks.
1237+
+
1238+
It defaults to 'blue,12 month ago,white,1 month ago,red', which colors
1239+
everything older than one year blue, recent changes between one month and
1240+
one year old are kept white, and lines introduced within the last month are
1241+
colored red.
1242+
12261243
color.ui::
12271244
This variable determines the default value for variables such
12281245
as `color.diff` and `color.grep` that control the use of color

builtin/blame.c

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "dir.h"
2525
#include "progress.h"
2626
#include "blame.h"
27+
#include "string-list.h"
2728

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

@@ -323,6 +324,7 @@ static const char *format_time(timestamp_t time, const char *tz_str,
323324
#define OUTPUT_SHOW_EMAIL 0400
324325
#define OUTPUT_LINE_PORCELAIN 01000
325326
#define OUTPUT_COLOR_LINE 02000
327+
#define OUTPUT_SHOW_AGE_WITH_COLOR 04000
326328

327329
static void emit_porcelain_details(struct blame_origin *suspect, int repeat)
328330
{
@@ -370,6 +372,63 @@ static void emit_porcelain(struct blame_scoreboard *sb, struct blame_entry *ent,
370372
putchar('\n');
371373
}
372374

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

383442
get_commit_info(suspect->commit, &ci, 1);
384443
oid_to_hex_r(hex, &suspect->commit->object.oid);
385444

386445
cp = blame_nth_line(sb, ent->lno);
446+
447+
if (opt & OUTPUT_SHOW_AGE_WITH_COLOR) {
448+
determine_line_heat(ent, &default_color);
449+
color = default_color;
450+
reset = GIT_COLOR_RESET;
451+
}
452+
387453
for (cnt = 0; cnt < ent->num_lines; cnt++) {
388454
char ch;
389455
int length = (opt & OUTPUT_LONG_OBJECT_NAME) ? GIT_SHA1_HEXSZ : abbrev;
@@ -393,8 +459,8 @@ static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent, int
393459
color = repeated_meta_color;
394460
reset = GIT_COLOR_RESET;
395461
} else {
396-
color = NULL;
397-
reset = NULL;
462+
color = default_color ? default_color : NULL;
463+
reset = default_color ? GIT_COLOR_RESET : NULL;
398464
}
399465
}
400466
if (color)
@@ -631,6 +697,10 @@ static int git_blame_config(const char *var, const char *value, void *cb)
631697
value);
632698
return 0;
633699
}
700+
if (!strcmp(var, "color.blame.highlightrecent")) {
701+
parse_color_fields(value);
702+
return 0;
703+
}
634704

635705
if (git_diff_heuristic_config(var, value, cb) < 0)
636706
return -1;
@@ -706,6 +776,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
706776
OPT_BIT('e', "show-email", &output_option, N_("Show author email instead of name (Default: off)"), OUTPUT_SHOW_EMAIL),
707777
OPT_BIT('w', NULL, &xdl_opts, N_("Ignore whitespace differences"), XDF_IGNORE_WHITESPACE),
708778
OPT_BIT(0, "color-lines", &output_option, N_("color redundant metadata from previous line differently"), OUTPUT_COLOR_LINE),
779+
OPT_BIT(0, "color-by-age", &output_option, N_("color lines by age"), OUTPUT_SHOW_AGE_WITH_COLOR),
709780

710781
/*
711782
* The following two options are parsed by parse_revision_opt()
@@ -730,6 +801,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
730801
unsigned int range_i;
731802
long anchor;
732803

804+
setup_default_color_by_age();
733805
git_config(git_blame_config, &output_option);
734806
init_revisions(&revs, NULL);
735807
revs.date_mode = blame_date_mode;
@@ -972,7 +1044,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
9721044
strcpy(repeated_meta_color, GIT_COLOR_CYAN);
9731045
}
9741046
if (output_option & OUTPUT_ANNOTATE_COMPAT)
975-
output_option &= ~OUTPUT_COLOR_LINE;
1047+
output_option &= ~(OUTPUT_COLOR_LINE | OUTPUT_SHOW_AGE_WITH_COLOR);
9761048

9771049
output(&sb, output_option);
9781050
free((void *)sb.final_buf);

t/t8012-blame-colors.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,29 @@ test_expect_success 'colored blame colors contiguous lines' '
1616
test_line_count = 3 H.expect
1717
'
1818

19+
test_expect_success 'color by age consistently colors old code' '
20+
git blame --color-by-age hello.c >actual.raw &&
21+
test_decode_color <actual.raw >actual &&
22+
grep "<BLUE>" <actual >colored &&
23+
test_line_count = 10 colored
24+
'
25+
26+
test_expect_success 'blame color by age: new code is different' '
27+
cat >>hello.c <<-EOF &&
28+
void qfunc();
29+
EOF
30+
git add hello.c &&
31+
GIT_AUTHOR_DATE="" git commit -m "new commit" &&
32+
33+
git -c color.blame.highlightRecent="yellow,1 month ago, cyan" blame --color-by-age hello.c >actual.raw &&
34+
test_decode_color <actual.raw >actual &&
35+
36+
grep "<YELLOW>" <actual >colored &&
37+
test_line_count = 10 colored &&
38+
39+
grep "<CYAN>" <actual >colored &&
40+
test_line_count = 1 colored &&
41+
grep qfunc colored
42+
'
43+
1944
test_done

0 commit comments

Comments
 (0)