diff: highlight and error out on incomplete lines

Teach "git diff" to highlight "\ No newline at end of file" message as a whitespace error when incomplete-line whitespace error class is in effect. Thanks to the previous refactoring of complete rewrite code path, we can do this at a single place. Unlike whitespace errors in the payload where we need to annotate in line, possibly using colors, the line that has whitespace problems, we have a dedicated line already that can serve as the error message, so paint it as a whitespace error message. Also teach "git diff --check" to notice incomplete lines as whitespace errors and report when incomplete-line whitespace error class is in effect. Signed-off-by: Junio C Hamano <gitster@pobox.com>

Junio C Hamano committed Nov 12, 2025 at 14:02 UTC ab2693cb52a40f597d3cc2b6938626d14655f7c0
2 files changed +90 -6
diff.c
+27 -2
@@ -1370,7 +1370,11 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
1370 emit_line(o, "", "", line, len);
1371 break;
1372 case DIFF_SYMBOL_CONTEXT_INCOMPLETE:
1373 - set = diff_get_color_opt(o, DIFF_CONTEXT);
1373 + if ((flags & WS_INCOMPLETE_LINE) &&
1374 + (flags & o->ws_error_highlight))
1375 + set = diff_get_color_opt(o, DIFF_WHITESPACE);
1376 + else
1377 + set = diff_get_color_opt(o, DIFF_CONTEXT);
1378 reset = diff_get_color_opt(o, DIFF_RESET);
1379 emit_line(o, set, reset, line, len);
1380 break;
@@ -1666,8 +1670,14 @@ static void emit_context_line(struct emit_callback *ecbdata,
1670 static void emit_incomplete_line_marker(struct emit_callback *ecbdata,
1671 const char *line, int len)
1672 {
1673 + int last_line_kind = ecbdata->last_line_kind;
1674 + unsigned flags = (last_line_kind == '+'
1675 + ? WSEH_NEW
1676 + : last_line_kind == '-'
1677 + ? WSEH_OLD
1678 + : WSEH_CONTEXT) | ecbdata->ws_rule;
1679 emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
1670 - line, len, 0);
1680 + line, len, flags);
1681 }
1682
1683 static void emit_hunk_header(struct emit_callback *ecbdata,
@@ -3254,6 +3264,7 @@ struct checkdiff_t {
3264 struct diff_options *o;
3265 unsigned ws_rule;
3266 unsigned status;
3267 + int last_line_kind;
3268 };
3269
3270 static int is_conflict_marker(const char *line, int marker_size, unsigned long len)
@@ -3292,6 +3303,7 @@ static void checkdiff_consume_hunk(void *priv,
3303 static int checkdiff_consume(void *priv, char *line, unsigned long len)
3304 {
3305 struct checkdiff_t *data = priv;
3306 + int last_line_kind;
3307 int marker_size = data->conflict_marker_size;
3308 const char *ws = diff_get_color(data->o->use_color, DIFF_WHITESPACE);
3309 const char *reset = diff_get_color(data->o->use_color, DIFF_RESET);
@@ -3302,6 +3314,8 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)
3314 assert(data->o);
3315 line_prefix = diff_line_prefix(data->o);
3316
3317 + last_line_kind = data->last_line_kind;
3318 + data->last_line_kind = line[0];
3319 if (line[0] == '+') {
3320 unsigned bad;
3321 data->lineno++;
@@ -3324,6 +3338,17 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)
3338 data->o->file, set, reset, ws);
3339 } else if (line[0] == ' ') {
3340 data->lineno++;
3341 + } else if (line[0] == '\\') {
3342 + /* no newline at the end of the line */
3343 + if ((data->ws_rule & WS_INCOMPLETE_LINE) &&
3344 + (last_line_kind == '+')) {
3345 + unsigned bad = WS_INCOMPLETE_LINE;
3346 + data->status |= bad;
3347 + err = whitespace_error_string(bad);
3348 + fprintf(data->o->file, "%s%s:%d: %s.\n",
3349 + line_prefix, data->filename, data->lineno, err);
3350 + free(err);
3351 + }
3352 }
3353 return 0;
3354 }
t/t4015-diff-whitespace.sh
+63 -4
@@ -43,6 +43,53 @@ do
43 '
44 done
45
46 +test_expect_success "incomplete line in both pre- and post-image context" '
47 + (echo foo && echo baz | tr -d "\012") >x &&
48 + git add x &&
49 + (echo bar && echo baz | tr -d "\012") >x &&
50 + git diff x &&
51 + git -c core.whitespace=incomplete diff --check x &&
52 + git diff -R x &&
53 + git -c core.whitespace=incomplete diff -R --check x
54 +'
55 +
56 +test_expect_success "incomplete lines on both pre- and post-image" '
57 + # The interpretation taken here is "since you are touching
58 + # the line anyway, you would better fix the incomplete line
59 + # while you are at it." but this is debatable.
60 + echo foo | tr -d "\012" >x &&
61 + git add x &&
62 + echo bar | tr -d "\012" >x &&
63 + git diff x &&
64 + test_must_fail git -c core.whitespace=incomplete diff --check x >error &&
65 + test_grep "no newline at the end of file" error &&
66 + git diff -R x &&
67 + test_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&
68 + test_grep "no newline at the end of file" error
69 +'
70 +
71 +test_expect_success "fix incomplete line in pre-image" '
72 + echo foo | tr -d "\012" >x &&
73 + git add x &&
74 + echo bar >x &&
75 + git diff x &&
76 + git -c core.whitespace=incomplete diff --check x &&
77 + git diff -R x &&
78 + test_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&
79 + test_grep "no newline at the end of file" error
80 +'
81 +
82 +test_expect_success "new incomplete line in post-image" '
83 + echo foo >x &&
84 + git add x &&
85 + echo bar | tr -d "\012" >x &&
86 + git diff x &&
87 + test_must_fail git -c core.whitespace=incomplete diff --check x >error &&
88 + test_grep "no newline at the end of file" error &&
89 + git diff -R x &&
90 + git -c core.whitespace=incomplete diff -R --check x
91 +'
92 +
93 test_expect_success "Ray Lehtiniemi's example" '
94 cat <<-\EOF >x &&
95 do {
@@ -1040,7 +1087,8 @@ test_expect_success 'ws-error-highlight test setup' '
1087 {
1088 echo "0. blank-at-eol " &&
1089 echo "1. still-blank-at-eol " &&
1043 - echo "2. and a new line "
1090 + echo "2. and a new line " &&
1091 + printf "3. and more"
1092 } >x &&
1093 new_hash_x=$(git hash-object x) &&
1094 after=$(git rev-parse --short "$new_hash_x") &&
@@ -1050,11 +1098,13 @@ test_expect_success 'ws-error-highlight test setup' '
1098 <BOLD>index $before..$after 100644<RESET>
1099 <BOLD>--- a/x<RESET>
1100 <BOLD>+++ b/x<RESET>
1053 - <CYAN>@@ -1,2 +1,3 @@<RESET>
1101 + <CYAN>@@ -1,2 +1,4 @@<RESET>
1102 0. blank-at-eol <RESET>
1103 <RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>
1104 <GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>
1105 <GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>
1106 + <GREEN>+<RESET><GREEN>3. and more<RESET>
1107 + <BLUE>\ No newline at end of file<RESET>
1108 EOF
1109
1110 cat >expect.all <<-EOF &&
@@ -1062,11 +1112,13 @@ test_expect_success 'ws-error-highlight test setup' '
1112 <BOLD>index $before..$after 100644<RESET>
1113 <BOLD>--- a/x<RESET>
1114 <BOLD>+++ b/x<RESET>
1065 - <CYAN>@@ -1,2 +1,3 @@<RESET>
1115 + <CYAN>@@ -1,2 +1,4 @@<RESET>
1116 <RESET>0. blank-at-eol<RESET><BLUE> <RESET>
1117 <RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>
1118 <GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>
1119 <GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>
1120 + <GREEN>+<RESET><GREEN>3. and more<RESET>
1121 + <BLUE>\ No newline at end of file<RESET>
1122 EOF
1123
1124 cat >expect.none <<-EOF
@@ -1074,16 +1126,19 @@ test_expect_success 'ws-error-highlight test setup' '
1126 <BOLD>index $before..$after 100644<RESET>
1127 <BOLD>--- a/x<RESET>
1128 <BOLD>+++ b/x<RESET>
1077 - <CYAN>@@ -1,2 +1,3 @@<RESET>
1129 + <CYAN>@@ -1,2 +1,4 @@<RESET>
1130 0. blank-at-eol <RESET>
1131 <RED>-1. blank-at-eol <RESET>
1132 <GREEN>+1. still-blank-at-eol <RESET>
1133 <GREEN>+2. and a new line <RESET>
1134 + <GREEN>+3. and more<RESET>
1135 + \ No newline at end of file<RESET>
1136 EOF
1137
1138 '
1139
1140 test_expect_success 'test --ws-error-highlight option' '
1141 + git config core.whitespace blank-at-eol,incomplete-line &&
1142
1143 git diff --color --ws-error-highlight=default,old >current.raw &&
1144 test_decode_color <current.raw >current &&
@@ -1100,6 +1155,7 @@ test_expect_success 'test --ws-error-highlight option' '
1155 '
1156
1157 test_expect_success 'test diff.wsErrorHighlight config' '
1158 + git config core.whitespace blank-at-eol,incomplete-line &&
1159
1160 git -c diff.wsErrorHighlight=default,old diff --color >current.raw &&
1161 test_decode_color <current.raw >current &&
@@ -1116,6 +1172,7 @@ test_expect_success 'test diff.wsErrorHighlight config' '
1172 '
1173
1174 test_expect_success 'option overrides diff.wsErrorHighlight' '
1175 + git config core.whitespace blank-at-eol,incomplete-line &&
1176
1177 git -c diff.wsErrorHighlight=none \
1178 diff --color --ws-error-highlight=default,old >current.raw &&
@@ -1135,6 +1192,8 @@ test_expect_success 'option overrides diff.wsErrorHighlight' '
1192 '
1193
1194 test_expect_success 'detect moved code, complete file' '
1195 + git config core.whitespace blank-at-eol &&
1196 +
1197 git reset --hard &&
1198 cat <<-\EOF >test.c &&
1199 #include<stdio.h>