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>