blame: print unblamable and ignored commits in porcelain mode

The 'git-blame(1)' command allows users to ignore specific revisions via the '--ignore-rev <rev>' and '--ignore-revs-file <file>' flags. These flags are often combined with the 'blame.markIgnoredLines' and 'blame.markUnblamableLines' config options. These config options prefix ignored and unblamable lines with a '?' and '*', respectively. However, this option was never extended to the porcelain mode of 'git-blame(1)'. Since the documentation does not indicate this exclusion, it is a bug. Fix this by printing 'ignored' and 'unblamable' respectively for the options when using the porcelain modes. Helped-by: Patrick Steinhardt <ps@pks.im> Helped-by: Toon Claes <toon@iotcl.com> Helped-by: Phillip Wood <phillip.wood123@gmail.com> Signed-off-by: Karthik Nayak <karthik.188@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Karthik Nayak committed Apr 3, 2025 at 18:03 UTC 4d253071ddd44ee5fad88702431603d43215b3af
4 files changed +60 -5
Documentation/blame-options.adoc
+2 -1
@@ -125,7 +125,8 @@ take effect.
125 another commit will be marked with a `?` in the blame output. If the
126 `blame.markUnblamableLines` config option is set, then those lines touched
127 by an ignored commit that we could not attribute to another revision are
128 - marked with a '*'.
128 + marked with a '*'. In the porcelain modes, we print 'ignored' and
129 + 'unblamable' on a newline respectively.
130
131 --ignore-revs-file <file>::
132 Ignore revisions listed in `file`, which must be in the same format as an
Documentation/git-blame.adoc
+5 -4
@@ -135,10 +135,11 @@ header elements later.
135 The porcelain format generally suppresses commit information that has
136 already been seen. For example, two lines that are blamed to the same
137 commit will both be shown, but the details for that commit will be shown
138 -only once. This is more efficient, but may require more state be kept by
139 -the reader. The `--line-porcelain` option can be used to output full
140 -commit information for each line, allowing simpler (but less efficient)
141 -usage like:
138 +only once. Information which is specific to individual lines will not be
139 +grouped together, like revs to be marked 'ignored' or 'unblamable'. This
140 +is more efficient, but may require more state be kept by the reader. The
141 +`--line-porcelain` option can be used to output full commit information
142 +for each line, allowing simpler (but less efficient) usage like:
143
144 # count the number of lines attributed to each author
145 git blame --line-porcelain file |
builtin/blame.c
+15
@@ -351,6 +351,19 @@ static void emit_porcelain_details(struct blame_origin *suspect, int repeat)
351 write_filename_info(suspect);
352 }
353
354 +/*
355 + * Information which needs to be printed per-line goes here. Any
356 + * information which can be clubbed on a commit/file level, should
357 + * be printed via 'emit_one_suspect_detail()'.
358 + */
359 +static void emit_porcelain_per_line_details(struct blame_entry *ent)
360 +{
361 + if (mark_unblamable_lines && ent->unblamable)
362 + puts("unblamable");
363 + if (mark_ignored_lines && ent->ignored)
364 + puts("ignored");
365 +}
366 +
367 static void emit_porcelain(struct blame_scoreboard *sb, struct blame_entry *ent,
368 int opt)
369 {
@@ -367,6 +380,7 @@ static void emit_porcelain(struct blame_scoreboard *sb, struct blame_entry *ent,
380 ent->lno + 1,
381 ent->num_lines);
382 emit_porcelain_details(suspect, repeat);
383 + emit_porcelain_per_line_details(ent);
384
385 cp = blame_nth_line(sb, ent->lno);
386 for (cnt = 0; cnt < ent->num_lines; cnt++) {
@@ -377,6 +391,7 @@ static void emit_porcelain(struct blame_scoreboard *sb, struct blame_entry *ent,
391 ent->lno + 1 + cnt);
392 if (repeat)
393 emit_porcelain_details(suspect, 1);
394 + emit_porcelain_per_line_details(ent);
395 }
396 putchar('\t');
397 do {
t/t8013-blame-ignore-revs.sh
+38
@@ -158,6 +158,25 @@ test_expect_success mark_unblamable_lines '
158 test_cmp expect actual
159 '
160
161 +for opt in --porcelain --line-porcelain
162 +do
163 + test_expect_success "mark_unblamable_lines with $opt" "
164 + sha=$(git rev-parse Y) &&
165 +
166 + git -c blame.markUnblamableLines=false blame $opt --ignore-rev Y file >raw &&
167 + cat > sedscript <<- 'EOF' &&
168 + /^ y3/i\\
169 + unblamable
170 + /^ y4/i\\
171 + unblamable
172 + EOF
173 + sed -f sedscript raw >expect &&
174 +
175 + git -c blame.markUnblamableLines=true blame $opt --ignore-rev Y file >actual &&
176 + test_cmp expect actual
177 + "
178 +done
179 +
180 # Commit Z will touch the first two lines. Y touched all four.
181 # A--B--X--Y--Z
182 # The blame output when ignoring Z should be:
@@ -191,6 +210,25 @@ test_expect_success mark_ignored_lines '
210 ! test_cmp expect actual
211 '
212
213 +for opt in --porcelain --line-porcelain
214 +do
215 + test_expect_success "mark_ignored_lines with $opt" "
216 + sha=$(git rev-parse Y) &&
217 +
218 + git -c blame.markIgnoredLines=false blame $opt --ignore-rev Z file >raw &&
219 + cat > sedscript <<- 'EOF' &&
220 + /^ line-one-Z/i\\
221 + ignored
222 + /^ line-two-Z/i\\
223 + ignored
224 + EOF
225 + sed -f sedscript raw >expect &&
226 +
227 + git -c blame.markIgnoredLines=true blame $opt --ignore-rev Z file >actual &&
228 + test_cmp expect actual
229 + "
230 +done
231 +
232 # For ignored revs that added 'unblamable' lines and more recent commits changed
233 # the blamable lines, mark the unblamable lines with a
234 # '*'