blame: reserve mark column only if necessary

git blame prepends commit hashes of boundary commits with "^", ignored commits with "?" and unblamable commits with "*" and reserves one column for them by extending the hash abbreviation, to avoid showing ambiguous hashes. This reserved column wastes precious screen space, which can be especially irritating when using the option -b to blank out boundary commit hashes and not ignoring any commits. Reserve it only as needed, i.e. if any of those cases are actually shown. Pointed-out-by: Laszlo Ersek <laszlo.ersek@posteo.net> Signed-off-by: René Scharfe <l.s.r@web.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

René Scharfe committed Jul 6, 2026 at 10:38 UTC c3df27f347dcefc14629c7afb178420762d45eef
3 files changed +53 -33
Documentation/git-blame.adoc
+6 -5
@@ -88,11 +88,12 @@ include::blame-options.adoc[]
88 include::diff-algorithm-option.adoc[]
89
90 `--abbrev=<n>`::
91 - Instead of using the default _7+1_ hexadecimal digits as the
92 - abbreviated object name, use _<m>+1_ digits, where _<m>_ is at
93 - least _<n>_ but ensures the commit object names are unique.
94 - Note that 1 column
95 - is used for a caret to mark the boundary commit.
91 + Instead of using the default _7_ hexadecimal digits as the
92 + abbreviated object name, use at least _<n>_ digits, but ensure
93 + the commit object names are unique.
94 + If commits marked with caret (boundary), question mark (ignored)
95 + or asterisk (unblamable) are shown, extend unmarked object names
96 + to align them.
97
98
99 THE DEFAULT FORMAT
builtin/blame.c
+45 -23
@@ -453,6 +453,36 @@ static void determine_line_heat(struct commit_info *ci, const char **dest_color)
453 *dest_color = colorfield[i].col;
454 }
455
456 +static inline int maybe_putc(int c, FILE *out)
457 +{
458 + return out ? putc(c, out) : 0;
459 +}
460 +
461 +static size_t print_marks(FILE *out, const struct blame_entry *ent, int opt)
462 +{
463 + size_t len = 0;
464 +
465 + if ((ent->suspect->commit->object.flags & UNINTERESTING) &&
466 + !blank_boundary && !(opt & OUTPUT_ANNOTATE_COMPAT)) {
467 + maybe_putc('^', out);
468 + len++;
469 + }
470 + if (mark_unblamable_lines && ent->unblamable) {
471 + maybe_putc('*', out);
472 + len++;
473 + }
474 + if (mark_ignored_lines && ent->ignored) {
475 + maybe_putc('?', out);
476 + len++;
477 + }
478 + return len;
479 +}
480 +
481 +static size_t count_marks(const struct blame_entry *ent, int opt)
482 +{
483 + return print_marks(NULL, ent, opt);
484 +}
485 +
486 static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent,
487 int opt, struct blame_entry *prev_ent)
488 {
@@ -499,23 +529,10 @@ static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent,
529 if (color)
530 fputs(color, stdout);
531
502 - if (suspect->commit->object.flags & UNINTERESTING) {
503 - if (blank_boundary) {
504 - memset(hex, ' ', strlen(hex));
505 - } else if (!(opt & OUTPUT_ANNOTATE_COMPAT)) {
506 - length--;
507 - putchar('^');
508 - }
509 - }
510 -
511 - if (mark_unblamable_lines && ent->unblamable) {
512 - length--;
513 - putchar('*');
514 - }
515 - if (mark_ignored_lines && ent->ignored) {
516 - length--;
517 - putchar('?');
518 - }
532 + if ((suspect->commit->object.flags & UNINTERESTING) &&
533 + blank_boundary)
534 + memset(hex, ' ', strlen(hex));
535 + length -= print_marks(stdout, ent, opt);
536
537 printf("%.*s", (int)(length < GIT_MAX_HEXSZ ? length : GIT_MAX_HEXSZ), hex);
538 if (opt & OUTPUT_ANNOTATE_COMPAT) {
@@ -647,11 +664,15 @@ static void find_alignment(struct blame_scoreboard *sb, int *option)
664 struct blame_entry *e;
665 int compute_auto_abbrev = (abbrev < 0);
666 int auto_abbrev = DEFAULT_ABBREV;
667 + size_t max_marks_count = 0;
668
669 for (e = sb->ent; e; e = e->next) {
670 struct blame_origin *suspect = e->suspect;
671 int num;
672 + size_t marks_count = count_marks(e, *option);
673
674 + if (max_marks_count < marks_count)
675 + max_marks_count = marks_count;
676 if (compute_auto_abbrev)
677 auto_abbrev = update_auto_abbrev(auto_abbrev, suspect);
678 if (strcmp(suspect->path, sb->path))
@@ -685,8 +706,12 @@ static void find_alignment(struct blame_scoreboard *sb, int *option)
706 max_score_digits = decimal_width(largest_score);
707
708 if (compute_auto_abbrev)
688 - /* one more abbrev length is needed for the boundary commit */
689 - abbrev = auto_abbrev + 1;
709 + abbrev = auto_abbrev;
710 + if (abbrev < (int)the_hash_algo->hexsz) {
711 + abbrev += max_marks_count;
712 + if (abbrev > (int)the_hash_algo->hexsz)
713 + abbrev = the_hash_algo->hexsz;
714 + }
715 }
716
717 static void sanity_check_on_fail(struct blame_scoreboard *sb, int baa)
@@ -1047,10 +1072,7 @@ parse_done:
1072 } else if (show_progress < 0)
1073 show_progress = isatty(2);
1074
1050 - if (0 < abbrev && abbrev < (int)the_hash_algo->hexsz)
1051 - /* one more abbrev length is needed for the boundary commit */
1052 - abbrev++;
1053 - else if (!abbrev)
1075 + if (!abbrev)
1076 abbrev = the_hash_algo->hexsz;
1077
1078 if (revs_file && read_ancestry(revs_file))
t/t8002-blame.sh
+2 -5
@@ -113,8 +113,7 @@ test_expect_success 'set up abbrev tests' '
113 '
114
115 test_expect_success 'blame --abbrev=<n> works' '
116 - # non-boundary commits get +1 for alignment
117 - check_abbrev 31 --abbrev=30 HEAD &&
116 + check_abbrev 30 --abbrev=30 HEAD &&
117 check_abbrev 30 --abbrev=30 ^HEAD
118 '
119
@@ -141,10 +140,8 @@ test_expect_success 'blame --abbrev gets truncated with boundary commit' '
140 '
141
142 test_expect_success 'blame --abbrev -b truncates the blank boundary' '
144 - # Note that `--abbrev=` always gets incremented by 1, which is why we
145 - # expect 11 leading spaces and not 10.
143 cat >expect <<-EOF &&
147 - $(printf "%11s" "") (<author@example.com> 2005-04-07 15:45:13 -0700 1) abbrev
144 + $(printf "%10s" "") (<author@example.com> 2005-04-07 15:45:13 -0700 1) abbrev
145 EOF
146 git blame -b --abbrev=10 ^HEAD -- abbrev.t >actual &&
147 test_cmp expect actual