{"thread":{"id":"65893","subject":"git-blame vs. abbrev","startedAt":"2026-06-30T11:15:25Z","lastAt":"2026-07-06T20:33:28Z","messageCount":9,"participants":["Laszlo Ersek","Junio C Hamano","René Scharfe"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"546751","messageId":"b500479b-14c1-4fbb-a672-1d2cd4852601@posteo.net","threadId":"65893","inReplyTo":null,"subject":"git-blame vs. abbrev","fromName":"Laszlo Ersek","fromEmail":"laszlo.ersek@posteo.net","sentAt":"2026-06-30T11:15:17Z","receivedAt":"2026-06-30T11:15:25Z","isPatch":false,"body":"Hi,\n\nwhen git-blame is passed the \"-b\" option (\"Show blank SHA-1 for boundary \ncommits\"), shouldn't git-blame *stop* reserving a commit hash nibble for \nthe caret that otherwise marks boundary commits?\n\nMore directly, I find it inconvenient that git-blame shows commit hashes \nthat are one nibble longer (13) than my \"core.abbrev\" (12) setting; that \nmakes cutting and pasting commit hashes from the git-blame output into a \ngit-rebase TODO list cumbersome. I briefly hoped that by setting \n\"blame.blankBoundary\", I could get around that, but it doesn't seem to \nwork (I tried with Git 2.55). I now have an alias that passes \n\"--abbrev=11\" explicitly, as a last resort, to git-blame. (Even a \npotential \"blame.abbrev\" would be superior, but such a permanent setting \ndoesn't seem to exist.)\n\nThanks,\nLaszlo Ersek\n"},{"id":"546807","messageId":"xmqqy0fvreps.fsf@gitster.g","threadId":"65893","inReplyTo":"b500479b-14c1-4fbb-a672-1d2cd4852601@posteo.net","subject":"Re: git-blame vs. abbrev","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-30T19:49:19Z","receivedAt":"2026-06-30T19:49:21Z","isPatch":false,"body":"Laszlo Ersek <laszlo.ersek@posteo.net> writes:\n\n> Hi,\n>\n> when git-blame is passed the \"-b\" option (\"Show blank SHA-1 for boundary \n> commits\"), shouldn't git-blame *stop* reserving a commit hash nibble for \n> the caret that otherwise marks boundary commits?\n>\n> More directly, I find it inconvenient that git-blame shows commit hashes \n> that are one nibble longer (13) than my \"core.abbrev\" (12) setting; that \n> makes cutting and pasting commit hashes from the git-blame output into a \n> git-rebase TODO list cumbersome.\n\nI never knew that the parser in rebase did not want to see a longer\nabbreviation; shouldn't it take 16 hexadecimal abbreviation from the\nresult of letting the user edit the list, even if it initially gave\n12 hexadecimal abbreviation, as long as these extra 4 hexdigits do\nnot break the commit object name?  That is a more serious usability\nbug that needs to be fixed, if it is the case, I would think.\n\nFWIW, even if your core.abbrev says you want 12, if two objects\nshare the same 12 hexdigits as the prefix, you do end up getting 13\nor more, so a parser that insists on exact 12 hexdigits sounds like\na bug.\n\nJust for the sake of aesthetics, I agree that when we are not\nshowing the boundary mark, it would make sense not to reserve one\ncolumn that we know we will never use.  But unless there is a\nmistaken parser that insists on 12 hexdigits when 13 hexdigits you\ngive uniquely identify the same object, I suspect you wouldn't even\nnotice that the hexadecimal digits you see on the screen have one\ndigit longer than usual ;-).\n"},{"id":"546810","messageId":"7df1312b-c5d0-462a-a03f-9f07e7338de1@posteo.net","threadId":"65893","inReplyTo":"xmqqy0fvreps.fsf@gitster.g","subject":"Re: git-blame vs. abbrev","fromName":"Laszlo Ersek","fromEmail":"laszlo.ersek@posteo.net","sentAt":"2026-06-30T20:32:15Z","receivedAt":"2026-06-30T20:32:17Z","isPatch":false,"body":"On 6/30/26 21:49, Junio C Hamano wrote:\n> Laszlo Ersek <laszlo.ersek@posteo.net> writes:\n>\n>> Hi,\n>>\n>> when git-blame is passed the \"-b\" option (\"Show blank SHA-1 for\n>> boundary commits\"), shouldn't git-blame *stop* reserving a commit\n>> hash nibble for the caret that otherwise marks boundary commits?\n>>\n>> More directly, I find it inconvenient that git-blame shows commit\n>> hashes that are one nibble longer (13) than my \"core.abbrev\" (12)\n>> setting; that makes cutting and pasting commit hashes from the\n>> git-blame output into a git-rebase TODO list cumbersome.\n>\n> I never knew that the parser in rebase did not want to see a longer\n> abbreviation; shouldn't it take 16 hexadecimal abbreviation from the\n> result of letting the user edit the list, even if it initially gave\n> 12 hexadecimal abbreviation, as long as these extra 4 hexdigits do\n> not break the commit object name?  That is a more serious usability\n> bug that needs to be fixed, if it is the case, I would think.\n\nUgh, I'm very sorry; I failed to describe my problem precisely. When I\nwrote \"cutting and pasting commit hashes from the git-blame output into\na git-rebase TODO list\", I actually meant pasting the commit hash from\ngit-blame into the *search box* of the editor that keeps the git-rebase\nTODO list open.\n\nBasically I want to fix up a line of code in a patch in a longer patch\nset, but don't know off-hand which patch in the set introduces that line\nof code. So git-blame gives me a commit hash, and subsequently, I run\ngit-rebase, would like to jump to the TODO line with that commit hash,\nand change the action from \"pick\" to \"edit\". And this *lookup* is what\nfails, because the hash from git-blame is 13 nibbles long, but the\nhashes in the git-rebase TODO list are 12 nibbles long. I always have to\nremember to remove the last nibble in the search box; otherwise, there\nis no match.\n\n> FWIW, even if your core.abbrev says you want 12, if two objects share\n> the same 12 hexdigits as the prefix, you do end up getting 13 or more,\n\nIndeed, but that does not matter in practice (to me anyway); a 12-nibble\nprefix length suffices for very large projects, and if ever there were a\ncollision, I'd just increase the length permanently to 13 nibbles. The\nspecific length is not relevant; agreement between git-blame's output\nand everything else dealing with commit hashes in git is what I'd like\nvery much.\n\n> so a parser that insists on exact 12 hexdigits sounds like a bug.\n>\nApologies again, that (implied) bug existed only between my chair and\nkeyboard. :)\n\n> Just for the sake of aesthetics, I agree that when we are not showing\n> the boundary mark, it would make sense not to reserve one column that\n> we know we will never use.  But unless there is a mistaken parser that\n> insists on 12 hexdigits when 13 hexdigits you give uniquely identify\n> the same object, I suspect you wouldn't even notice that the\n> hexadecimal digits you see on the screen have one digit longer than\n> usual ;-).\n\nThat's it precisely! :) I do *not* notice that the hash from git-blame\nhas one more hexadecimal character than the central abbrev setting; so I\njust go ahead and blindly cut n' paste it, using the mouse, from the\nterminal, to the editor search box that's displaying the git-rebase TODO\nlist. And then I don't understand why the lookup fails (until I realize\nthat I have forgotten, yet again, to strip the last nibble from the\ncommit hash coming from git-blame).\n\nOnce again, sorry about misstating the problem / use case!\n\nLaszlo\n"},{"id":"546813","messageId":"xmqqcxx7raan.fsf@gitster.g","threadId":"65893","inReplyTo":"7df1312b-c5d0-462a-a03f-9f07e7338de1@posteo.net","subject":"Re: git-blame vs. abbrev","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-30T21:24:48Z","receivedAt":"2026-06-30T21:24:50Z","isPatch":false,"body":"Laszlo Ersek <laszlo.ersek@posteo.net> writes:\n\n>> ... the same object, I suspect you wouldn't even notice that the\n>> hexadecimal digits you see on the screen have one digit longer than\n>> usual ;-).\n>\n> That's it precisely! :) I do *not* notice that the hash from git-blame\n> has one more hexadecimal character than the central abbrev setting; so I\n> just go ahead and blindly cut n' paste it, using the mouse, from the\n> terminal, to the editor search box ...\n\nWhich means that there does not need any configuration variable to\ntrigger the new behaviour to protect your workflow from breaking, I\nthink, and instead we can just go ahead with the \"-b then no extra\ncolumn for '^'\" change, right? ;-)\n"},{"id":"546814","messageId":"f9761389-8c31-4928-babf-8218e9393376@web.de","threadId":"65893","inReplyTo":"xmqqy0fvreps.fsf@gitster.g","subject":"Re: git-blame vs. abbrev","fromName":"René Scharfe","fromEmail":"l.s.r@web.de","sentAt":"2026-06-30T21:38:43Z","receivedAt":"2026-06-30T21:38:45Z","isPatch":false,"body":"On 6/30/26 9:49 PM, Junio C Hamano wrote:\n> Laszlo Ersek <laszlo.ersek@posteo.net> writes:\n> \n>> Hi,\n>>\n>> when git-blame is passed the \"-b\" option (\"Show blank SHA-1 for boundary \n>> commits\"), shouldn't git-blame *stop* reserving a commit hash nibble for \n>> the caret that otherwise marks boundary commits?\n>>\n>> More directly, I find it inconvenient that git-blame shows commit hashes \n>> that are one nibble longer (13) than my \"core.abbrev\" (12) setting; \n> \n> Just for the sake of aesthetics, I agree that when we are not\n> showing the boundary mark, it would make sense not to reserve one\n> column that we know we will never use.\nHere's a patch to reserve a column only if marks are actually shown.\n\nI strongly suspect that any line can only have a single mark, but I\ndidn't bother checking and proving whether that's actually true; the\nnew code should be able to handle multiple marks just fine.\n\nMisses tests.\n\nRené\n\n\n---\n builtin/blame.c | 61 ++++++++++++++++++++++++++++++++++---------------\n 1 file changed, 42 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/blame.c b/builtin/blame.c\nindex ffbd3ce5c5..0e747a43f2 100644\n--- a/builtin/blame.c\n+++ b/builtin/blame.c\n@@ -453,6 +453,39 @@ static void determine_line_heat(struct commit_info *ci, const char **dest_color)\n \t*dest_color = colorfield[i].col;\n }\n \n+static size_t handle_marks(const struct blame_entry *ent, int opt, bool print)\n+{\n+\tsize_t len = 0;\n+\n+\tif ((ent->suspect->commit->object.flags & UNINTERESTING) &&\n+\t    !blank_boundary && !(opt & OUTPUT_ANNOTATE_COMPAT)) {\n+\t\tif (print)\n+\t\t\tputchar('^');\n+\t\tlen++;\n+\t}\n+\tif (mark_unblamable_lines && ent->unblamable) {\n+\t\tif (print)\n+\t\t\tputchar('*');\n+\t\tlen++;\n+\t}\n+\tif (mark_ignored_lines && ent->ignored) {\n+\t\tif (print)\n+\t\t\tputchar('?');\n+\t\tlen++;\n+\t}\n+\treturn len;\n+}\n+\n+static size_t print_marks(const struct blame_entry *ent, int opt)\n+{\n+\treturn handle_marks(ent, opt, true);\n+}\n+\n+static size_t count_marks(const struct blame_entry *ent, int opt)\n+{\n+\treturn handle_marks(ent, opt, false);\n+}\n+\n static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent,\n \t\t       int opt, struct blame_entry *prev_ent)\n {\n@@ -499,23 +532,10 @@ static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent,\n \t\tif (color)\n \t\t\tfputs(color, stdout);\n \n-\t\tif (suspect->commit->object.flags & UNINTERESTING) {\n-\t\t\tif (blank_boundary) {\n-\t\t\t\tmemset(hex, ' ', strlen(hex));\n-\t\t\t} else if (!(opt & OUTPUT_ANNOTATE_COMPAT)) {\n-\t\t\t\tlength--;\n-\t\t\t\tputchar('^');\n-\t\t\t}\n-\t\t}\n-\n-\t\tif (mark_unblamable_lines && ent->unblamable) {\n-\t\t\tlength--;\n-\t\t\tputchar('*');\n-\t\t}\n-\t\tif (mark_ignored_lines && ent->ignored) {\n-\t\t\tlength--;\n-\t\t\tputchar('?');\n-\t\t}\n+\t\tif ((suspect->commit->object.flags & UNINTERESTING) &&\n+\t\t    blank_boundary)\n+\t\t\tmemset(hex, ' ', strlen(hex));\n+\t\tlength -= print_marks(ent, opt);\n \n \t\tprintf(\"%.*s\", (int)(length < GIT_MAX_HEXSZ ? length : GIT_MAX_HEXSZ), hex);\n \t\tif (opt & OUTPUT_ANNOTATE_COMPAT) {\n@@ -647,11 +667,15 @@ static void find_alignment(struct blame_scoreboard *sb, int *option)\n \tstruct blame_entry *e;\n \tint compute_auto_abbrev = (abbrev < 0);\n \tint auto_abbrev = DEFAULT_ABBREV;\n+\tsize_t max_marks_count = 0;\n \n \tfor (e = sb->ent; e; e = e->next) {\n \t\tstruct blame_origin *suspect = e->suspect;\n \t\tint num;\n+\t\tsize_t marks_count = count_marks(e, *option);\n \n+\t\tif (max_marks_count < marks_count)\n+\t\t\tmax_marks_count = marks_count;\n \t\tif (compute_auto_abbrev)\n \t\t\tauto_abbrev = update_auto_abbrev(auto_abbrev, suspect);\n \t\tif (strcmp(suspect->path, sb->path))\n@@ -685,8 +709,7 @@ static void find_alignment(struct blame_scoreboard *sb, int *option)\n \tmax_score_digits = decimal_width(largest_score);\n \n \tif (compute_auto_abbrev)\n-\t\t/* one more abbrev length is needed for the boundary commit */\n-\t\tabbrev = auto_abbrev + 1;\n+\t\tabbrev = auto_abbrev + max_marks_count;\n }\n \n static void sanity_check_on_fail(struct blame_scoreboard *sb, int baa)\n-- \n2.54.0\n\n"},{"id":"546859","messageId":"6d4b0156-4de9-4f67-b864-cf6314a1e772@posteo.net","threadId":"65893","inReplyTo":"xmqqcxx7raan.fsf@gitster.g","subject":"Re: git-blame vs. abbrev","fromName":"Laszlo Ersek","fromEmail":"laszlo.ersek@posteo.net","sentAt":"2026-07-01T08:43:54Z","receivedAt":"2026-07-01T08:43:57Z","isPatch":false,"body":"On 6/30/26 23:24, Junio C Hamano wrote:\n> Laszlo Ersek <laszlo.ersek@posteo.net> writes:\n> \n>>> ... the same object, I suspect you wouldn't even notice that the\n>>> hexadecimal digits you see on the screen have one digit longer than\n>>> usual ;-).\n>>\n>> That's it precisely! :) I do *not* notice that the hash from git-blame\n>> has one more hexadecimal character than the central abbrev setting; so I\n>> just go ahead and blindly cut n' paste it, using the mouse, from the\n>> terminal, to the editor search box ...\n> \n> Which means that there does not need any configuration variable to\n> trigger the new behaviour to protect your workflow from breaking, I\n> think, and instead we can just go ahead with the \"-b then no extra\n> column for '^'\" change, right? ;-)\n\nYes, certainly!\n\nThank you!\nLaszlo\n"},{"id":"546860","messageId":"7146f6a0-89aa-41bb-b187-4fc456b0c7da@posteo.net","threadId":"65893","inReplyTo":"f9761389-8c31-4928-babf-8218e9393376@web.de","subject":"Re: git-blame vs. abbrev","fromName":"Laszlo Ersek","fromEmail":"laszlo.ersek@posteo.net","sentAt":"2026-07-01T08:45:33Z","receivedAt":"2026-07-01T08:45:36Z","isPatch":false,"body":"On 6/30/26 23:38, René Scharfe wrote:\n> On 6/30/26 9:49 PM, Junio C Hamano wrote:\n>> Laszlo Ersek <laszlo.ersek@posteo.net> writes:\n>>\n>>> Hi,\n>>>\n>>> when git-blame is passed the \"-b\" option (\"Show blank SHA-1 for boundary\n>>> commits\"), shouldn't git-blame *stop* reserving a commit hash nibble for\n>>> the caret that otherwise marks boundary commits?\n>>>\n>>> More directly, I find it inconvenient that git-blame shows commit hashes\n>>> that are one nibble longer (13) than my \"core.abbrev\" (12) setting;\n>>\n>> Just for the sake of aesthetics, I agree that when we are not\n>> showing the boundary mark, it would make sense not to reserve one\n>> column that we know we will never use.\n> Here's a patch to reserve a column only if marks are actually shown.\n> \n> I strongly suspect that any line can only have a single mark, but I\n> didn't bother checking and proving whether that's actually true; the\n> new code should be able to handle multiple marks just fine.\n> \n> Misses tests.\n> \n> René\n> \n> \n> ---\n>   builtin/blame.c | 61 ++++++++++++++++++++++++++++++++++---------------\n>   1 file changed, 42 insertions(+), 19 deletions(-)\n> \n> diff --git a/builtin/blame.c b/builtin/blame.c\n> index ffbd3ce5c5..0e747a43f2 100644\n> --- a/builtin/blame.c\n> +++ b/builtin/blame.c\n> @@ -453,6 +453,39 @@ static void determine_line_heat(struct commit_info *ci, const char **dest_color)\n>   \t*dest_color = colorfield[i].col;\n>   }\n>   \n> +static size_t handle_marks(const struct blame_entry *ent, int opt, bool print)\n> +{\n> +\tsize_t len = 0;\n> +\n> +\tif ((ent->suspect->commit->object.flags & UNINTERESTING) &&\n> +\t    !blank_boundary && !(opt & OUTPUT_ANNOTATE_COMPAT)) {\n> +\t\tif (print)\n> +\t\t\tputchar('^');\n> +\t\tlen++;\n> +\t}\n> +\tif (mark_unblamable_lines && ent->unblamable) {\n> +\t\tif (print)\n> +\t\t\tputchar('*');\n> +\t\tlen++;\n> +\t}\n> +\tif (mark_ignored_lines && ent->ignored) {\n> +\t\tif (print)\n> +\t\t\tputchar('?');\n> +\t\tlen++;\n> +\t}\n> +\treturn len;\n> +}\n> +\n> +static size_t print_marks(const struct blame_entry *ent, int opt)\n> +{\n> +\treturn handle_marks(ent, opt, true);\n> +}\n> +\n> +static size_t count_marks(const struct blame_entry *ent, int opt)\n> +{\n> +\treturn handle_marks(ent, opt, false);\n> +}\n> +\n>   static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent,\n>   \t\t       int opt, struct blame_entry *prev_ent)\n>   {\n> @@ -499,23 +532,10 @@ static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent,\n>   \t\tif (color)\n>   \t\t\tfputs(color, stdout);\n>   \n> -\t\tif (suspect->commit->object.flags & UNINTERESTING) {\n> -\t\t\tif (blank_boundary) {\n> -\t\t\t\tmemset(hex, ' ', strlen(hex));\n> -\t\t\t} else if (!(opt & OUTPUT_ANNOTATE_COMPAT)) {\n> -\t\t\t\tlength--;\n> -\t\t\t\tputchar('^');\n> -\t\t\t}\n> -\t\t}\n> -\n> -\t\tif (mark_unblamable_lines && ent->unblamable) {\n> -\t\t\tlength--;\n> -\t\t\tputchar('*');\n> -\t\t}\n> -\t\tif (mark_ignored_lines && ent->ignored) {\n> -\t\t\tlength--;\n> -\t\t\tputchar('?');\n> -\t\t}\n> +\t\tif ((suspect->commit->object.flags & UNINTERESTING) &&\n> +\t\t    blank_boundary)\n> +\t\t\tmemset(hex, ' ', strlen(hex));\n> +\t\tlength -= print_marks(ent, opt);\n>   \n>   \t\tprintf(\"%.*s\", (int)(length < GIT_MAX_HEXSZ ? length : GIT_MAX_HEXSZ), hex);\n>   \t\tif (opt & OUTPUT_ANNOTATE_COMPAT) {\n> @@ -647,11 +667,15 @@ static void find_alignment(struct blame_scoreboard *sb, int *option)\n>   \tstruct blame_entry *e;\n>   \tint compute_auto_abbrev = (abbrev < 0);\n>   \tint auto_abbrev = DEFAULT_ABBREV;\n> +\tsize_t max_marks_count = 0;\n>   \n>   \tfor (e = sb->ent; e; e = e->next) {\n>   \t\tstruct blame_origin *suspect = e->suspect;\n>   \t\tint num;\n> +\t\tsize_t marks_count = count_marks(e, *option);\n>   \n> +\t\tif (max_marks_count < marks_count)\n> +\t\t\tmax_marks_count = marks_count;\n>   \t\tif (compute_auto_abbrev)\n>   \t\t\tauto_abbrev = update_auto_abbrev(auto_abbrev, suspect);\n>   \t\tif (strcmp(suspect->path, sb->path))\n> @@ -685,8 +709,7 @@ static void find_alignment(struct blame_scoreboard *sb, int *option)\n>   \tmax_score_digits = decimal_width(largest_score);\n>   \n>   \tif (compute_auto_abbrev)\n> -\t\t/* one more abbrev length is needed for the boundary commit */\n> -\t\tabbrev = auto_abbrev + 1;\n> +\t\tabbrev = auto_abbrev + max_marks_count;\n>   }\n>   \n>   static void sanity_check_on_fail(struct blame_scoreboard *sb, int baa)\n\nThis way, I didn't even need to pass \"-b\". :)\n\nI didn't try to test the patch exhaustively, but it definitely does what \nI'm after.\n\nTested-by: Laszlo Ersek <laszlo.ersek@posteo.net>\n\nThank you!\nLaszlo\n"},{"id":"547201","messageId":"92991b5e-0667-4315-89d5-1514a5499297@web.de","threadId":"65893","inReplyTo":"b500479b-14c1-4fbb-a672-1d2cd4852601@posteo.net","subject":"[PATCH] blame: reserve mark column only if necessary","fromName":"René Scharfe","fromEmail":"l.s.r@web.de","sentAt":"2026-07-06T08:38:25Z","receivedAt":"2026-07-06T08:38:37Z","isPatch":true,"body":"git blame prepends commit hashes of boundary commits with \"^\", ignored\ncommits with \"?\" and unblamable commits with \"*\" and reserves one column\nfor them by extending the hash abbreviation, to avoid showing ambiguous\nhashes.\n\nThis reserved column wastes precious screen space, which can be\nespecially irritating when using the option -b to blank out boundary\ncommit hashes and not ignoring any commits.  Reserve it only as needed,\ni.e. if any of those cases are actually shown.\n\nPointed-out-by: Laszlo Ersek <laszlo.ersek@posteo.net>\nSigned-off-by: René Scharfe <l.s.r@web.de>\n---\n Documentation/git-blame.adoc | 11 +++---\n builtin/blame.c              | 68 ++++++++++++++++++++++++------------\n t/t8002-blame.sh             |  7 ++--\n 3 files changed, 53 insertions(+), 33 deletions(-)\n\ndiff --git a/Documentation/git-blame.adoc b/Documentation/git-blame.adoc\nindex 8808009e87e..2b74e455997 100644\n--- a/Documentation/git-blame.adoc\n+++ b/Documentation/git-blame.adoc\n@@ -88,11 +88,12 @@ include::blame-options.adoc[]\n include::diff-algorithm-option.adoc[]\n \n `--abbrev=<n>`::\n-\tInstead of using the default _7+1_ hexadecimal digits as the\n-\tabbreviated object name, use _<m>+1_ digits, where _<m>_ is at\n-\tleast _<n>_ but ensures the commit object names are unique.\n-\tNote that 1 column\n-\tis used for a caret to mark the boundary commit.\n+\tInstead of using the default _7_ hexadecimal digits as the\n+\tabbreviated object name, use at least _<n>_ digits, but ensure\n+\tthe commit object names are unique.\n+\tIf commits marked with caret (boundary), question mark (ignored)\n+\tor asterisk (unblamable) are shown, extend unmarked object names\n+\tto align them.\n \n \n THE DEFAULT FORMAT\ndiff --git a/builtin/blame.c b/builtin/blame.c\nindex ffbd3ce5c5a..5ae39d0458a 100644\n--- a/builtin/blame.c\n+++ b/builtin/blame.c\n@@ -453,6 +453,36 @@ static void determine_line_heat(struct commit_info *ci, const char **dest_color)\n \t*dest_color = colorfield[i].col;\n }\n \n+static inline int maybe_putc(int c, FILE *out)\n+{\n+\treturn out ? putc(c, out) : 0;\n+}\n+\n+static size_t print_marks(FILE *out, const struct blame_entry *ent, int opt)\n+{\n+\tsize_t len = 0;\n+\n+\tif ((ent->suspect->commit->object.flags & UNINTERESTING) &&\n+\t    !blank_boundary && !(opt & OUTPUT_ANNOTATE_COMPAT)) {\n+\t\tmaybe_putc('^', out);\n+\t\tlen++;\n+\t}\n+\tif (mark_unblamable_lines && ent->unblamable) {\n+\t\tmaybe_putc('*', out);\n+\t\tlen++;\n+\t}\n+\tif (mark_ignored_lines && ent->ignored) {\n+\t\tmaybe_putc('?', out);\n+\t\tlen++;\n+\t}\n+\treturn len;\n+}\n+\n+static size_t count_marks(const struct blame_entry *ent, int opt)\n+{\n+\treturn print_marks(NULL, ent, opt);\n+}\n+\n static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent,\n \t\t       int opt, struct blame_entry *prev_ent)\n {\n@@ -499,23 +529,10 @@ static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent,\n \t\tif (color)\n \t\t\tfputs(color, stdout);\n \n-\t\tif (suspect->commit->object.flags & UNINTERESTING) {\n-\t\t\tif (blank_boundary) {\n-\t\t\t\tmemset(hex, ' ', strlen(hex));\n-\t\t\t} else if (!(opt & OUTPUT_ANNOTATE_COMPAT)) {\n-\t\t\t\tlength--;\n-\t\t\t\tputchar('^');\n-\t\t\t}\n-\t\t}\n-\n-\t\tif (mark_unblamable_lines && ent->unblamable) {\n-\t\t\tlength--;\n-\t\t\tputchar('*');\n-\t\t}\n-\t\tif (mark_ignored_lines && ent->ignored) {\n-\t\t\tlength--;\n-\t\t\tputchar('?');\n-\t\t}\n+\t\tif ((suspect->commit->object.flags & UNINTERESTING) &&\n+\t\t    blank_boundary)\n+\t\t\tmemset(hex, ' ', strlen(hex));\n+\t\tlength -= print_marks(stdout, ent, opt);\n \n \t\tprintf(\"%.*s\", (int)(length < GIT_MAX_HEXSZ ? length : GIT_MAX_HEXSZ), hex);\n \t\tif (opt & OUTPUT_ANNOTATE_COMPAT) {\n@@ -647,11 +664,15 @@ static void find_alignment(struct blame_scoreboard *sb, int *option)\n \tstruct blame_entry *e;\n \tint compute_auto_abbrev = (abbrev < 0);\n \tint auto_abbrev = DEFAULT_ABBREV;\n+\tsize_t max_marks_count = 0;\n \n \tfor (e = sb->ent; e; e = e->next) {\n \t\tstruct blame_origin *suspect = e->suspect;\n \t\tint num;\n+\t\tsize_t marks_count = count_marks(e, *option);\n \n+\t\tif (max_marks_count < marks_count)\n+\t\t\tmax_marks_count = marks_count;\n \t\tif (compute_auto_abbrev)\n \t\t\tauto_abbrev = update_auto_abbrev(auto_abbrev, suspect);\n \t\tif (strcmp(suspect->path, sb->path))\n@@ -685,8 +706,12 @@ static void find_alignment(struct blame_scoreboard *sb, int *option)\n \tmax_score_digits = decimal_width(largest_score);\n \n \tif (compute_auto_abbrev)\n-\t\t/* one more abbrev length is needed for the boundary commit */\n-\t\tabbrev = auto_abbrev + 1;\n+\t\tabbrev = auto_abbrev;\n+\tif (abbrev < (int)the_hash_algo->hexsz) {\n+\t\tabbrev += max_marks_count;\n+\t\tif (abbrev > (int)the_hash_algo->hexsz)\n+\t\t\tabbrev = the_hash_algo->hexsz;\n+\t}\n }\n \n static void sanity_check_on_fail(struct blame_scoreboard *sb, int baa)\n@@ -1047,10 +1072,7 @@ int cmd_blame(int argc,\n \t} else if (show_progress < 0)\n \t\tshow_progress = isatty(2);\n \n-\tif (0 < abbrev && abbrev < (int)the_hash_algo->hexsz)\n-\t\t/* one more abbrev length is needed for the boundary commit */\n-\t\tabbrev++;\n-\telse if (!abbrev)\n+\tif (!abbrev)\n \t\tabbrev = the_hash_algo->hexsz;\n \n \tif (revs_file && read_ancestry(revs_file))\ndiff --git a/t/t8002-blame.sh b/t/t8002-blame.sh\nindex 7822947f028..bf04b8273ef 100755\n--- a/t/t8002-blame.sh\n+++ b/t/t8002-blame.sh\n@@ -113,8 +113,7 @@ test_expect_success 'set up abbrev tests' '\n '\n \n test_expect_success 'blame --abbrev=<n> works' '\n-\t# non-boundary commits get +1 for alignment\n-\tcheck_abbrev 31 --abbrev=30 HEAD &&\n+\tcheck_abbrev 30 --abbrev=30 HEAD &&\n \tcheck_abbrev 30 --abbrev=30 ^HEAD\n '\n \n@@ -141,10 +140,8 @@ test_expect_success 'blame --abbrev gets truncated with boundary commit' '\n '\n \n test_expect_success 'blame --abbrev -b truncates the blank boundary' '\n-\t# Note that `--abbrev=` always gets incremented by 1, which is why we\n-\t# expect 11 leading spaces and not 10.\n \tcat >expect <<-EOF &&\n-\t$(printf \"%11s\" \"\") (<author@example.com> 2005-04-07 15:45:13 -0700 1) abbrev\n+\t$(printf \"%10s\" \"\") (<author@example.com> 2005-04-07 15:45:13 -0700 1) abbrev\n \tEOF\n \tgit blame -b --abbrev=10 ^HEAD -- abbrev.t >actual &&\n \ttest_cmp expect actual\n-- \n2.55.0\n"},{"id":"547267","messageId":"xmqqzf0397u1.fsf@gitster.g","threadId":"65893","inReplyTo":"92991b5e-0667-4315-89d5-1514a5499297@web.de","subject":"Re: [PATCH] blame: reserve mark column only if necessary","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-06T20:33:26Z","receivedAt":"2026-07-06T20:33:28Z","isPatch":true,"body":"René Scharfe <l.s.r@web.de> writes:\n\n>  `--abbrev=<n>`::\n> +\tInstead of using the default _7_ hexadecimal digits as the\n> +\tabbreviated object name, use at least _<n>_ digits, but ensure\n> +\tthe commit object names are unique.\n> +\tIf commits marked with caret (boundary), question mark (ignored)\n> +\tor asterisk (unblamable) are shown, extend unmarked object names\n> +\tto align them.\n\nOK.\n\n> +static inline int maybe_putc(int c, FILE *out)\n> +{\n> +\treturn out ? putc(c, out) : 0;\n> +}\n> +\n> +static size_t print_marks(FILE *out, const struct blame_entry *ent, int opt)\n> +{\n> +\tsize_t len = 0;\n> +\n> +\tif ((ent->suspect->commit->object.flags & UNINTERESTING) &&\n> +\t    !blank_boundary && !(opt & OUTPUT_ANNOTATE_COMPAT)) {\n> +\t\tmaybe_putc('^', out);\n> +\t\tlen++;\n> +\t}\n> +\tif (mark_unblamable_lines && ent->unblamable) {\n> +\t\tmaybe_putc('*', out);\n> +\t\tlen++;\n> +\t}\n> +\tif (mark_ignored_lines && ent->ignored) {\n> +\t\tmaybe_putc('?', out);\n> +\t\tlen++;\n> +\t}\n> +\treturn len;\n> +}\n\nQuite straight-forward.\n\n> +static size_t count_marks(const struct blame_entry *ent, int opt)\n> +{\n> +\treturn print_marks(NULL, ent, opt);\n> +}\n\nOK.\n\n> @@ -499,23 +529,10 @@ static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent,\n>  \t\tif (color)\n>  \t\t\tfputs(color, stdout);\n>  \n> -\t\tif (suspect->commit->object.flags & UNINTERESTING) {\n> -\t\t\tif (blank_boundary) {\n> -\t\t\t\tmemset(hex, ' ', strlen(hex));\n> -\t\t\t} else if (!(opt & OUTPUT_ANNOTATE_COMPAT)) {\n> -\t\t\t\tlength--;\n> -\t\t\t\tputchar('^');\n> -\t\t\t}\n> -\t\t}\n> -\n> -\t\tif (mark_unblamable_lines && ent->unblamable) {\n> -\t\t\tlength--;\n> -\t\t\tputchar('*');\n> -\t\t}\n> -\t\tif (mark_ignored_lines && ent->ignored) {\n> -\t\t\tlength--;\n> -\t\t\tputchar('?');\n> -\t\t}\n> +\t\tif ((suspect->commit->object.flags & UNINTERESTING) &&\n> +\t\t    blank_boundary)\n> +\t\t\tmemset(hex, ' ', strlen(hex));\n> +\t\tlength -= print_marks(stdout, ent, opt);\n>  \n>  \t\tprintf(\"%.*s\", (int)(length < GIT_MAX_HEXSZ ? length : GIT_MAX_HEXSZ), hex);\n>  \t\tif (opt & OUTPUT_ANNOTATE_COMPAT) {\n> @@ -647,11 +664,15 @@ static void find_alignment(struct blame_scoreboard *sb, int *option)\n>  \tstruct blame_entry *e;\n>  \tint compute_auto_abbrev = (abbrev < 0);\n>  \tint auto_abbrev = DEFAULT_ABBREV;\n> +\tsize_t max_marks_count = 0;\n>  \n>  \tfor (e = sb->ent; e; e = e->next) {\n>  \t\tstruct blame_origin *suspect = e->suspect;\n>  \t\tint num;\n> +\t\tsize_t marks_count = count_marks(e, *option);\n>  \n> +\t\tif (max_marks_count < marks_count)\n> +\t\t\tmax_marks_count = marks_count;\n>  \t\tif (compute_auto_abbrev)\n>  \t\t\tauto_abbrev = update_auto_abbrev(auto_abbrev, suspect);\n>  \t\tif (strcmp(suspect->path, sb->path))\n> @@ -685,8 +706,12 @@ static void find_alignment(struct blame_scoreboard *sb, int *option)\n>  \tmax_score_digits = decimal_width(largest_score);\n>  \n>  \tif (compute_auto_abbrev)\n> -\t\t/* one more abbrev length is needed for the boundary commit */\n> -\t\tabbrev = auto_abbrev + 1;\n> +\t\tabbrev = auto_abbrev;\n> +\tif (abbrev < (int)the_hash_algo->hexsz) {\n> +\t\tabbrev += max_marks_count;\n> +\t\tif (abbrev > (int)the_hash_algo->hexsz)\n> +\t\t\tabbrev = the_hash_algo->hexsz;\n> +\t}\n>  }\n>  \n>  static void sanity_check_on_fail(struct blame_scoreboard *sb, int baa)\n> @@ -1047,10 +1072,7 @@ int cmd_blame(int argc,\n>  \t} else if (show_progress < 0)\n>  \t\tshow_progress = isatty(2);\n>  \n> -\tif (0 < abbrev && abbrev < (int)the_hash_algo->hexsz)\n> -\t\t/* one more abbrev length is needed for the boundary commit */\n> -\t\tabbrev++;\n> -\telse if (!abbrev)\n> +\tif (!abbrev)\n>  \t\tabbrev = the_hash_algo->hexsz;\n\nOK.\n\n\n> diff --git a/t/t8002-blame.sh b/t/t8002-blame.sh\n> index 7822947f028..bf04b8273ef 100755\n> --- a/t/t8002-blame.sh\n> +++ b/t/t8002-blame.sh\n> @@ -113,8 +113,7 @@ test_expect_success 'set up abbrev tests' '\n>  '\n>  \n>  test_expect_success 'blame --abbrev=<n> works' '\n> -\t# non-boundary commits get +1 for alignment\n> -\tcheck_abbrev 31 --abbrev=30 HEAD &&\n> +\tcheck_abbrev 30 --abbrev=30 HEAD &&\n>  \tcheck_abbrev 30 --abbrev=30 ^HEAD\n>  '\n>  \n> @@ -141,10 +140,8 @@ test_expect_success 'blame --abbrev gets truncated with boundary commit' '\n>  '\n>  \n>  test_expect_success 'blame --abbrev -b truncates the blank boundary' '\n> -\t# Note that `--abbrev=` always gets incremented by 1, which is why we\n> -\t# expect 11 leading spaces and not 10.\n>  \tcat >expect <<-EOF &&\n> -\t$(printf \"%11s\" \"\") (<author@example.com> 2005-04-07 15:45:13 -0700 1) abbrev\n> +\t$(printf \"%10s\" \"\") (<author@example.com> 2005-04-07 15:45:13 -0700 1) abbrev\n>  \tEOF\n\nOK.\n\n>  \tgit blame -b --abbrev=10 ^HEAD -- abbrev.t >actual &&\n>  \ttest_cmp expect actual\n"}]}