threads / patch / 64429

patch, 12 partsIncomplete lines

Subject: [PATCH 00/12] Incomplete lines

## tl;dr

73 messages between Nov 4, 2025 and Nov 23, 2025. Diffs are folded; open one to read it.

replies: 72people: 4as markdown or json

Junio C Hamano· Nov 4, 2025, 02:09 UTC · lore

One of the common kind of whitespace errors is to lack the terminating newline at the end of a file, but so far, neither "git diff" or "git apply" did anything about them.

This series introduces "incomplete-line" whitespace error class, that you can add to either the core.whitespace configuration variable, or the whitespace attribute in your .gitattributes files. The class is disabled by default.

When incomplete-line whitespace error is enabled, a patch file that has "\ No newline at the end of file" line for the postimage file is considered to introduce a whitespace error. This is true even if the corresponding preimage file ends in a different contents on the final line that is incomplete. The incomplete line marker that is given for a context line is not considered an error. The reasoning is that your preimage did have incomplete line, but you did not touch the contents on that incomplete line in your patch, so you left the line intact. It is not a new breakage you are responsible for. If the incomplete line marker follows a postimage line, on the other hand, it means that you either made a file that used to end with a complete line to end with an incomplete line, or the file ended with an incomplete line before your change, and you did not fix it even though you modified other bytes on that same last line, which you could have easily fixed while at it, and that is flagged as an error.

 * "git diff --check" notices and errors out.
 * "git diff" and friends highlight the offending "\ No newline ..."
   line.
 * "git apply --whitespace=(error|warn)" triggers an error, and "git
   apply --whitespace=fix" would correct it by appending a newline.
The organization of the series is
 * The first patch [01/12] is a clean-up we have seen earlier on the
   list already (https://lore.kernel.org/git/xmqqfrb4hyjl.fsf@gitster.g/).
 * The patches [02/12] - [08/12] are preliminary clean-up made to
   both "git diff" and "git apply" machinery.
 * The patch [09/12] shifts the bit assignment (cleaned-up in
   [01/12] without changing any values) to make room for new
   whitespace error class (which was last updated in 2007 IIRC, so
   the set of whitespace errors surprisingly haven't changed for
   quite some time).
 * The patch [10/12] teaches "git apply --whitespace=<mode>" about
   the incomplete-line error class.
 * The patch [11/12] teaches "git diff [--check]" about the
   incomplete-line error class.
 * The final patch [12/12] enables the incomplete-line error class
   for our project for C source files and shell scripts.  I didn't
   touch the cover-all * entry.
Junio C Hamano (12):
  whitespace: correct bit assignment comments
  diff: emit_line_ws_markup() if/else style fix
  diff: correct suppress_blank_empty hack
  diff: fix incorrect counting of line numbers
  diff: refactor output of incomplete line
  diff: call emit_callback ecbdata everywhere
  diff: update the way rewrite diff handles incomplete lines
  apply: revamp the parsing of incomplete lines
  whitespace: allocate a few more bits
  apply: check and fix incomplete lines
  diff: highlight and error out on incomplete lines
  attr: enable incomplete-line whitespace error for this project
 .gitattributes             |   4 +-
 apply.c                    |  70 ++++++++++++------
 diff.c                     | 148 +++++++++++++++++++++++++------------
 diff.h                     |   6 +-
 t/t4015-diff-whitespace.sh |  63 +++++++++++++++-
 t/t4124-apply-ws-rule.sh   | 112 ++++++++++++++++++++++++++++
 ws.c                       |  20 +++++
 ws.h                       |  26 ++++---
 8 files changed, 361 insertions(+), 88 deletions(-)
-- 
2.52.0-rc0
Junio C Hamano· Nov 4, 2025, 02:09 UTC · re: Junio C Hamano · lore

[PATCH 01/12] whitespace: correct bit assignment comments

A comment in diff.c claimed that bits up to 12th (counting from 0th) are whitespace rules, and 13th thru 15th are for new/old/context, but it turns out it was miscounting. Correct them, and clarify where the whitespace rule bits come from in the comment. Extend bit assignment comments to cover bits used for color-moved, which weren't described.

Also update the way these bit constants are defined to use (1 << N) notation, instead of octal constants, as it tends to make it easier to notice a breakage like this.

Sprinkle a few blank lines between logically distinct groups of CPP macro definitions to make them easier to read.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c |  7 +++++--
 diff.h |  6 +++---
 ws.h   | 25 ++++++++++++++-----------
 3 files changed, 22 insertions(+), 16 deletions(-)
Show changes to 3 files +22 −16

diff.c, diff.h, ws.h

diff --git a/diff.c b/diff.c
index a74e701806..74261b332a 100644
--- a/diff.c
+++ b/diff.c
@@ -801,16 +801,19 @@ enum diff_symbol {
 	DIFF_SYMBOL_CONTEXT_MARKER,
 	DIFF_SYMBOL_SEPARATOR
 };
+
 /*
  * Flags for content lines:
- * 0..12 are whitespace rules
- * 13-15 are WSEH_NEW | WSEH_OLD | WSEH_CONTEXT
+ * 0..11 are whitespace rules (see ws.h)
+ * 12..14 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD
  * 16 is marking if the line is blank at EOF
+ * 17..19 are used for color-moved.
  */
 #define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF	(1<<16)
 #define DIFF_SYMBOL_MOVED_LINE			(1<<17)
 #define DIFF_SYMBOL_MOVED_LINE_ALT		(1<<18)
 #define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING	(1<<19)
+
 #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)
 
 /*
diff --git a/diff.h b/diff.h
index 2fa256c3ef..cbd355cf50 100644
--- a/diff.h
+++ b/diff.h
@@ -331,9 +331,9 @@ struct diff_options {
 
 	int ita_invisible_in_index;
 /* white-space error highlighting */
-#define WSEH_NEW (1<<12)
-#define WSEH_CONTEXT (1<<13)
-#define WSEH_OLD (1<<14)
+#define WSEH_NEW        (1<<12)
+#define WSEH_CONTEXT    (1<<13)
+#define WSEH_OLD        (1<<14)
 	unsigned ws_error_highlight;
 	const char *prefix;
 	int prefix_length;
diff --git a/ws.h b/ws.h
index 5ba676c559..23708efb73 100644
--- a/ws.h
+++ b/ws.h
@@ -7,19 +7,22 @@ struct strbuf;
 /*
  * whitespace rules.
  * used by both diff and apply
- * last two digits are tab width
+ * last two octal-digits are tab width (we support only up to 63).
  */
-#define WS_BLANK_AT_EOL         0100
-#define WS_SPACE_BEFORE_TAB     0200
-#define WS_INDENT_WITH_NON_TAB  0400
-#define WS_CR_AT_EOL           01000
-#define WS_BLANK_AT_EOF        02000
-#define WS_TAB_IN_INDENT       04000
-#define WS_TRAILING_SPACE      (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
+#define WS_BLANK_AT_EOL         (1<<6)
+#define WS_SPACE_BEFORE_TAB     (1<<7)
+#define WS_INDENT_WITH_NON_TAB  (1<<8)
+#define WS_CR_AT_EOL            (1<<9)
+#define WS_BLANK_AT_EOF         (1<<10)
+#define WS_TAB_IN_INDENT        (1<<11)
+
+#define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
 #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)
-#define WS_TAB_WIDTH_MASK        077
-/* All WS_* -- when extended, adapt diff.c emit_symbol */
-#define WS_RULE_MASK           07777
+#define WS_TAB_WIDTH_MASK       ((1<<6)-1)
+
+/* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */
+#define WS_RULE_MASK            ((1<<12)-1)
+
 extern unsigned whitespace_rule_cfg;
 unsigned whitespace_rule(struct index_state *, const char *);
 unsigned parse_whitespace_rule(const char *);
-- 
2.52.0-rc0
Junio C Hamano· Nov 4, 2025, 02:09 UTC · re: Junio C Hamano · lore

[PATCH 02/12] diff: emit_line_ws_markup() if/else style fix

Apply the simple rule: if you need {} in one arm of the if/else if/else... cascade, have {} in all of them.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
Show changes to diff.c +4 −4
diff --git a/diff.c b/diff.c
index 74261b332a..9a24a0791c 100644
--- a/diff.c
+++ b/diff.c
@@ -1327,14 +1327,14 @@ static void emit_line_ws_markup(struct diff_options *o,
 			ws = NULL;
 	}
 
-	if (!ws && !set_sign)
+	if (!ws && !set_sign) {
 		emit_line_0(o, set, NULL, 0, reset, sign, line, len);
-	else if (!ws) {
+	} else if (!ws) {
 		emit_line_0(o, set_sign, set, !!set_sign, reset, sign, line, len);
-	} else if (blank_at_eof)
+	} else if (blank_at_eof) {
 		/* Blank line at EOF - paint '+' as well */
 		emit_line_0(o, ws, NULL, 0, reset, sign, line, len);
-	else {
+	} else {
 		/* Emit just the prefix, then the rest. */
 		emit_line_0(o, set_sign ? set_sign : set, NULL, !!set_sign, reset,
 			    sign, "", 0);
-- 
2.52.0-rc0
Junio C Hamano· Nov 4, 2025, 02:09 UTC · re: Junio C Hamano · lore

[PATCH 03/12] diff: correct suppress_blank_empty hack

The suppress-blank-empty feature abused the CONTEXT_INCOMPLETE symbol that was meant to be used only for "\ No newline at the end of file" code path.

The intent of the feature was to turn a context line we receive from xdiff machinery (which always uses ' ' for context lines, even an empty one) and spit it out as a truly empty line.

Perform such a conversion very locally at where a line from xdiff that begins with ' ' is handled for output; there are many checks before the control reaches such place that checks the first letter of the diff output line to see if it is a context line, and having to check for '\n' and treat it as a special case is error prone.

In order to catch similar hacks in the future, make sure the code path that is meant for "\ No newline" case checks the first byte is indeed a backslash.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 27 +++++++++++----------------
 1 file changed, 11 insertions(+), 16 deletions(-)
Show changes to diff.c +11 −16
diff --git a/diff.c b/diff.c
index 9a24a0791c..b9ef8550cc 100644
--- a/diff.c
+++ b/diff.c
@@ -1321,6 +1321,11 @@ static void emit_line_ws_markup(struct diff_options *o,
 	const char *ws = NULL;
 	int sign = o->output_indicators[sign_index];
 
+	if (diff_suppress_blank_empty &&
+	    sign_index == OUTPUT_INDICATOR_CONTEXT &&
+	    len == 1 && line[0] == '\n')
+		sign = 0;
+
 	if (o->ws_error_highlight & ws_rule) {
 		ws = diff_get_color_opt(o, DIFF_WHITESPACE);
 		if (!*ws)
@@ -1498,15 +1503,9 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
 	case DIFF_SYMBOL_WORDS:
 		context = diff_get_color_opt(o, DIFF_CONTEXT);
 		reset = diff_get_color_opt(o, DIFF_RESET);
-		/*
-		 * Skip the prefix character, if any.  With
-		 * diff_suppress_blank_empty, there may be
-		 * none.
-		 */
-		if (line[0] != '\n') {
-			line++;
-			len--;
-		}
+
+		/* Skip the prefix character */
+		line++; len--;
 		emit_line(o, context, reset, line, len);
 		break;
 	case DIFF_SYMBOL_FILEPAIR_PLUS:
@@ -2375,12 +2374,6 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 		ecbdata->label_path[0] = ecbdata->label_path[1] = NULL;
 	}
 
-	if (diff_suppress_blank_empty
-	    && len == 2 && line[0] == ' ' && line[1] == '\n') {
-		line[0] = '\n';
-		len = 1;
-	}
-
 	if (line[0] == '@') {
 		if (ecbdata->diff_words)
 			diff_words_flush(ecbdata);
@@ -2431,12 +2424,14 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 		ecbdata->lno_in_preimage++;
 		emit_context_line(ecbdata, line + 1, len - 1);
 		break;
-	default:
+	case '\\':
 		/* incomplete line at the end */
 		ecbdata->lno_in_preimage++;
 		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
 				 line, len, 0);
 		break;
+	default:
+		BUG("fn_out_consume: unknown line '%s'", line);
 	}
 	return 0;
 }
-- 
2.52.0-rc0
Junio C Hamano· Nov 4, 2025, 02:09 UTC · re: Junio C Hamano · lore

[PATCH 04/12] diff: fix incorrect counting of line numbers

The "\ No newline at the end of the file" can come after any of the "-" (deleted preimage line), " " (unchanged line), or "+" (added postimage line). Incrementing only the preimage line number upon seeing it does not make any sense.

We can keep track of what the previous line was, and increment lno_in_{pre,post}image variables properly, like this patch does. I do not think it matters, as these numbers are used only to compare them with blank_at_eof_in_{pre,post}image to issue the warning every time we see an added line, but by definition, after we see "\ No newline at the end of the file" for an added line, we will not see an added line for the file.

Keeping track of what the last line was (in other words, "is it that the file used to end in an incomplete line? The file ends in an incomplete line after the change? Both the file before and after the change ends in an incomplete line that did not change?") will be independently useful.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)
Show changes to diff.c +17 −1
diff --git a/diff.c b/diff.c
index b9ef8550cc..e73320dfb1 100644
--- a/diff.c
+++ b/diff.c
@@ -601,6 +601,7 @@ struct emit_callback {
 	int blank_at_eof_in_postimage;
 	int lno_in_preimage;
 	int lno_in_postimage;
+	int last_line_kind;
 	const char **label_path;
 	struct diff_words_data *diff_words;
 	struct diff_options *opt;
@@ -2426,13 +2427,28 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 		break;
 	case '\\':
 		/* incomplete line at the end */
-		ecbdata->lno_in_preimage++;
+		switch (ecbdata->last_line_kind) {
+		case '+':
+			ecbdata->lno_in_postimage++;
+			break;
+		case '-':
+			ecbdata->lno_in_preimage++;
+			break;
+		case ' ':
+			ecbdata->lno_in_preimage++;
+			ecbdata->lno_in_postimage++;
+			break;
+		default:
+			BUG("fn_out_consume: '\\No newline' after unknown line (%c)",
+			    ecbdata->last_line_kind);
+		}
 		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
 				 line, len, 0);
 		break;
 	default:
 		BUG("fn_out_consume: unknown line '%s'", line);
 	}
+	ecbdata->last_line_kind = line[0];
 	return 0;
 }
 
-- 
2.52.0-rc0
Phillip Wood· Nov 10, 2025, 14:54 UTC · re: Junio C Hamano · lore

Re: [PATCH 04/12] diff: fix incorrect counting of line numbers

On 04/11/2025 02:09, Junio C Hamano wrote:
Show 18 quoted lines
> The "\ No newline at the end of the file" can come after any of the
> "-" (deleted preimage line), " " (unchanged line), or "+" (added
> postimage line).  Incrementing only the preimage line number upon
> seeing it does not make any sense.
> 
> We can keep track of what the previous line was, and increment
> lno_in_{pre,post}image variables properly, like this patch does.  I
> do not think it matters, as these numbers are used only to compare
> them with blank_at_eof_in_{pre,post}image to issue the warning every
> time we see an added line, but by definition, after we see "\ No
> newline at the end of the file" for an added line, we will not see
> an added line for the file.
> 
> Keeping track of what the last line was (in other words, "is it that
> the file used to end in an incomplete line?  The file ends in an
> incomplete line after the change?  Both the file before and after
> the change ends in an incomplete line that did not change?") will be
> independently useful.

The "\ No newline at end of file" line is an annotation on the previous line in the diff so why are we incrementing any {pre,post}image line numbers here?

Thanks
Phillip
Show 47 quoted lines
> Signed-off-by: Junio C Hamano <gitster@pobox.com>
> ---
>   diff.c | 18 +++++++++++++++++-
>   1 file changed, 17 insertions(+), 1 deletion(-)
> 
> diff --git a/diff.c b/diff.c
> index b9ef8550cc..e73320dfb1 100644
> --- a/diff.c
> +++ b/diff.c
> @@ -601,6 +601,7 @@ struct emit_callback {
>   	int blank_at_eof_in_postimage;
>   	int lno_in_preimage;
>   	int lno_in_postimage;
> +	int last_line_kind;
>   	const char **label_path;
>   	struct diff_words_data *diff_words;
>   	struct diff_options *opt;
> @@ -2426,13 +2427,28 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
>   		break;
>   	case '\\':
>   		/* incomplete line at the end */
> -		ecbdata->lno_in_preimage++;
> +		switch (ecbdata->last_line_kind) {
> +		case '+':
> +			ecbdata->lno_in_postimage++;
> +			break;
> +		case '-':
> +			ecbdata->lno_in_preimage++;
> +			break;
> +		case ' ':
> +			ecbdata->lno_in_preimage++;
> +			ecbdata->lno_in_postimage++;
> +			break;
> +		default:
> +			BUG("fn_out_consume: '\\No newline' after unknown line (%c)",
> +			    ecbdata->last_line_kind);
> +		}
>   		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
>   				 line, len, 0);
>   		break;
>   	default:
>   		BUG("fn_out_consume: unknown line '%s'", line);
>   	}
> +	ecbdata->last_line_kind = line[0];
>   	return 0;
>   }
>   
Junio C Hamano· Nov 10, 2025, 18:29 UTC · re: Phillip Wood · lore

Re: [PATCH 04/12] diff: fix incorrect counting of line numbers

Phillip Wood <phillip.wood123@gmail.com> writes:
Show 23 quoted lines
> On 04/11/2025 02:09, Junio C Hamano wrote:
>> The "\ No newline at the end of the file" can come after any of the
>> "-" (deleted preimage line), " " (unchanged line), or "+" (added
>> postimage line).  Incrementing only the preimage line number upon
>> seeing it does not make any sense.
>> 
>> We can keep track of what the previous line was, and increment
>> lno_in_{pre,post}image variables properly, like this patch does.  I
>> do not think it matters, as these numbers are used only to compare
>> them with blank_at_eof_in_{pre,post}image to issue the warning every
>> time we see an added line, but by definition, after we see "\ No
>> newline at the end of the file" for an added line, we will not see
>> an added line for the file.
>> 
>> Keeping track of what the last line was (in other words, "is it that
>> the file used to end in an incomplete line?  The file ends in an
>> incomplete line after the change?  Both the file before and after
>> the change ends in an incomplete line that did not change?") will be
>> independently useful.
>
> The "\ No newline at end of file" line is an annotation on the previous 
> line in the diff so why are we incrementing any {pre,post}image line 
> numbers here?

No particular reason ;-) As I said, I do not think these numbers are used after these lines are seen. At least this change makes these unused data incremented in a more coherent way than the previous one, which unconditionally incremented the number for the preimage without even checking which side the "\ No newline" is for.

Show 51 quoted lines
> Thanks
>
> Phillip
>
>> Signed-off-by: Junio C Hamano <gitster@pobox.com>
>> ---
>>   diff.c | 18 +++++++++++++++++-
>>   1 file changed, 17 insertions(+), 1 deletion(-)
>> 
>> diff --git a/diff.c b/diff.c
>> index b9ef8550cc..e73320dfb1 100644
>> --- a/diff.c
>> +++ b/diff.c
>> @@ -601,6 +601,7 @@ struct emit_callback {
>>   	int blank_at_eof_in_postimage;
>>   	int lno_in_preimage;
>>   	int lno_in_postimage;
>> +	int last_line_kind;
>>   	const char **label_path;
>>   	struct diff_words_data *diff_words;
>>   	struct diff_options *opt;
>> @@ -2426,13 +2427,28 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
>>   		break;
>>   	case '\\':
>>   		/* incomplete line at the end */
>> -		ecbdata->lno_in_preimage++;
>> +		switch (ecbdata->last_line_kind) {
>> +		case '+':
>> +			ecbdata->lno_in_postimage++;
>> +			break;
>> +		case '-':
>> +			ecbdata->lno_in_preimage++;
>> +			break;
>> +		case ' ':
>> +			ecbdata->lno_in_preimage++;
>> +			ecbdata->lno_in_postimage++;
>> +			break;
>> +		default:
>> +			BUG("fn_out_consume: '\\No newline' after unknown line (%c)",
>> +			    ecbdata->last_line_kind);
>> +		}
>>   		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
>>   				 line, len, 0);
>>   		break;
>>   	default:
>>   		BUG("fn_out_consume: unknown line '%s'", line);
>>   	}
>> +	ecbdata->last_line_kind = line[0];
>>   	return 0;
>>   }
>>   
Phillip Wood· Nov 11, 2025, 14:26 UTC · re: Junio C Hamano · lore

Re: [PATCH 04/12] diff: fix incorrect counting of line numbers

On 10/11/2025 18:29, Junio C Hamano wrote:
Show 12 quoted lines
> Phillip Wood <phillip.wood123@gmail.com> writes:
>> On 04/11/2025 02:09, Junio C Hamano wrote:
>>
>> The "\ No newline at end of file" line is an annotation on the previous
>> line in the diff so why are we incrementing any {pre,post}image line
>> numbers here?
> 
> No particular reason ;-)  As I said, I do not think these numbers
> are used after these lines are seen.  At least this change makes
> these unused data incremented in a more coherent way than the
> previous one, which unconditionally incremented the number for the
> preimage without even checking which side the "\ No newline" is for.

It maybe coherent but it is still wrong to increment the line numbers here. To be correct we should remove the erroneous increment of lno_in_postimage.

Thanks
Phillip
Show 51 quoted lines
>> Thanks
>>
>> Phillip
>>
>>> Signed-off-by: Junio C Hamano <gitster@pobox.com>
>>> ---
>>>    diff.c | 18 +++++++++++++++++-
>>>    1 file changed, 17 insertions(+), 1 deletion(-)
>>>
>>> diff --git a/diff.c b/diff.c
>>> index b9ef8550cc..e73320dfb1 100644
>>> --- a/diff.c
>>> +++ b/diff.c
>>> @@ -601,6 +601,7 @@ struct emit_callback {
>>>    	int blank_at_eof_in_postimage;
>>>    	int lno_in_preimage;
>>>    	int lno_in_postimage;
>>> +	int last_line_kind;
>>>    	const char **label_path;
>>>    	struct diff_words_data *diff_words;
>>>    	struct diff_options *opt;
>>> @@ -2426,13 +2427,28 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
>>>    		break;
>>>    	case '\\':
>>>    		/* incomplete line at the end */
>>> -		ecbdata->lno_in_preimage++;
>>> +		switch (ecbdata->last_line_kind) {
>>> +		case '+':
>>> +			ecbdata->lno_in_postimage++;
>>> +			break;
>>> +		case '-':
>>> +			ecbdata->lno_in_preimage++;
>>> +			break;
>>> +		case ' ':
>>> +			ecbdata->lno_in_preimage++;
>>> +			ecbdata->lno_in_postimage++;
>>> +			break;
>>> +		default:
>>> +			BUG("fn_out_consume: '\\No newline' after unknown line (%c)",
>>> +			    ecbdata->last_line_kind);
>>> +		}
>>>    		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
>>>    				 line, len, 0);
>>>    		break;
>>>    	default:
>>>    		BUG("fn_out_consume: unknown line '%s'", line);
>>>    	}
>>> +	ecbdata->last_line_kind = line[0];
>>>    	return 0;
>>>    }
>>>    
Junio C Hamano· Nov 11, 2025, 14:37 UTC · re: Phillip Wood · lore

Re: [PATCH 04/12] diff: fix incorrect counting of line numbers

Phillip Wood <phillip.wood123@gmail.com> writes:
Show 17 quoted lines
> On 10/11/2025 18:29, Junio C Hamano wrote:
>> Phillip Wood <phillip.wood123@gmail.com> writes:
>>> On 04/11/2025 02:09, Junio C Hamano wrote:
>>>
>>> The "\ No newline at end of file" line is an annotation on the previous
>>> line in the diff so why are we incrementing any {pre,post}image line
>>> numbers here?
>> 
>> No particular reason ;-)  As I said, I do not think these numbers
>> are used after these lines are seen.  At least this change makes
>> these unused data incremented in a more coherent way than the
>> previous one, which unconditionally incremented the number for the
>> preimage without even checking which side the "\ No newline" is for.
>
> It maybe coherent but it is still wrong to increment the line numbers 
> here. To be correct we should remove the erroneous increment of 
> lno_in_postimage.

Ah, if your lno_in_postimage is not a typo for preimage side, then I can buy that and it would be even safer than the version under discussion. I do not think anybody has audited the code to be absolutely sure that the existing increment for preimage line number, which we think is wrong in this discussion thread, is not compensated by something else, which would make removal of the increment break it, so in the absense of such an audit (and the theme of this topic certainly is not about it), it is prudent to let this sleeping dog lie.

The primary thing this step needed to do was to introduce the last_line_kind member to the emit-callback structure and switch on its value to allow us to process "\ No newline" differently depending on what kind of line the previous line was. The "consistently increment on both sides" was a "while at it" change that does not have to be done and probably better left out.

Thanks.
Junio C Hamano· Nov 4, 2025, 02:09 UTC · re: Junio C Hamano · lore

[PATCH 05/12] diff: refactor output of incomplete line

Create a helper function that reacts to "\ No newline at the end of file" in preparation for unifying the incomplete line handling in the code path that handles xdiff output and the code path that bypasses xdiff and produces complete rewrite patch.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)
Show changes to diff.c +12 −2
diff --git a/diff.c b/diff.c
index e73320dfb1..d388d318e4 100644
--- a/diff.c
+++ b/diff.c
@@ -1379,6 +1379,10 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
 		emit_line(o, "", "", line, len);
 		break;
 	case DIFF_SYMBOL_CONTEXT_INCOMPLETE:
+		set = diff_get_color_opt(o, DIFF_CONTEXT);
+		reset = diff_get_color_opt(o, DIFF_RESET);
+		emit_line(o, set, reset, line, len);
+		break;
 	case DIFF_SYMBOL_CONTEXT_MARKER:
 		context = diff_get_color_opt(o, DIFF_CONTEXT);
 		reset = diff_get_color_opt(o, DIFF_RESET);
@@ -1668,6 +1672,13 @@ static void emit_context_line(struct emit_callback *ecbdata,
 	emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT, line, len, flags);
 }
 
+static void emit_incomplete_line(struct emit_callback *ecbdata,
+				 const char *line, int len)
+{
+	emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
+			 line, len, 0);
+}
+
 static void emit_hunk_header(struct emit_callback *ecbdata,
 			     const char *line, int len)
 {
@@ -2442,8 +2453,7 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 			BUG("fn_out_consume: '\\No newline' after unknown line (%c)",
 			    ecbdata->last_line_kind);
 		}
-		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
-				 line, len, 0);
+		emit_incomplete_line(ecbdata, line, len);
 		break;
 	default:
 		BUG("fn_out_consume: unknown line '%s'", line);
-- 
2.52.0-rc0
Junio C Hamano· Nov 4, 2025, 02:09 UTC · re: Junio C Hamano · lore

[PATCH 06/12] diff: call emit_callback ecbdata everywhere

Everybody else, except for emit_rewrite_lines(), calls the emit_callback data ecbdata. Make sure we call the same thing by the same name for consistency.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)
Show changes to diff.c +6 −6
diff --git a/diff.c b/diff.c
index d388d318e4..347cd9c6e9 100644
--- a/diff.c
+++ b/diff.c
@@ -1780,7 +1780,7 @@ static void add_line_count(struct strbuf *out, int count)
 	}
 }
 
-static void emit_rewrite_lines(struct emit_callback *ecb,
+static void emit_rewrite_lines(struct emit_callback *ecbdata,
 			       int prefix, const char *data, int size)
 {
 	const char *endp = NULL;
@@ -1791,17 +1791,17 @@ static void emit_rewrite_lines(struct emit_callback *ecb,
 		endp = memchr(data, '\n', size);
 		len = endp ? (endp - data + 1) : size;
 		if (prefix != '+') {
-			ecb->lno_in_preimage++;
-			emit_del_line(ecb, data, len);
+			ecbdata->lno_in_preimage++;
+			emit_del_line(ecbdata, data, len);
 		} else {
-			ecb->lno_in_postimage++;
-			emit_add_line(ecb, data, len);
+			ecbdata->lno_in_postimage++;
+			emit_add_line(ecbdata, data, len);
 		}
 		size -= len;
 		data += len;
 	}
 	if (!endp)
-		emit_diff_symbol(ecb->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
+		emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
 }
 
 static void emit_rewrite_diff(const char *name_a,
-- 
2.52.0-rc0
Junio C Hamano· Nov 4, 2025, 02:09 UTC · re: Junio C Hamano · lore

[PATCH 07/12] diff: update the way rewrite diff handles incomplete lines

The diff_symbol based output framework uses one DIFF_SYMBOL_* enum value per the kind of output lines of "git diff", which corresponds to one output line from the xdiff machinery used internally. Most notably, DIFF_SYMBOL_PLUS and DIFF_SYMBOL_MINUS that correspond to "+" and "-" lines are designed to always take a complete line, even if the output from xdiff machinery may produce "\ No newline at the end of file" immediately after them.

But this is not true in the rewrite-diff codepath, which completely bypasses the xdiff machinery. Since the code path feeds the bytes directly from the payload to the output routines, the output layer has to deal with an incomplete line with DIFF_SYMBOL_PLUS and DIFF_SYMBOL_MINUS, which never would see an incomplete line in the normal code paths. This lack of final newline is compensated by an ugly hack for a fabricated DIFF_SYMBOL_NO_LF_EOF token to inject an extra newline to the output to simulate output coming from the xdiff machinery.

Revamp the way the complete-rewrite code path feeds the lines to the output layer by treating the last line of the pre/post image when it is an incomplete line specially.

This lets us remove the DIFF_SYMBOL_NO_LF_EOF hack and use the usual DIFF_SYMBOL_CONTEXT_INCOMPLETE code path, which will later learn how to handle whitespace errors.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 33 +++++++++++++++++++--------------
 1 file changed, 19 insertions(+), 14 deletions(-)
Show changes to diff.c +19 −14
diff --git a/diff.c b/diff.c
index 347cd9c6e9..99298720f4 100644
--- a/diff.c
+++ b/diff.c
@@ -797,7 +797,6 @@ enum diff_symbol {
 	DIFF_SYMBOL_CONTEXT_INCOMPLETE,
 	DIFF_SYMBOL_PLUS,
 	DIFF_SYMBOL_MINUS,
-	DIFF_SYMBOL_NO_LF_EOF,
 	DIFF_SYMBOL_CONTEXT_FRAGINFO,
 	DIFF_SYMBOL_CONTEXT_MARKER,
 	DIFF_SYMBOL_SEPARATOR
@@ -1352,7 +1351,6 @@ static void emit_line_ws_markup(struct diff_options *o,
 static void emit_diff_symbol_from_struct(struct diff_options *o,
 					 struct emitted_diff_symbol *eds)
 {
-	static const char *nneof = " No newline at end of file\n";
 	const char *context, *reset, *set, *set_sign, *meta, *fraginfo;
 
 	enum diff_symbol s = eds->s;
@@ -1361,13 +1359,6 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
 	unsigned flags = eds->flags;
 
 	switch (s) {
-	case DIFF_SYMBOL_NO_LF_EOF:
-		context = diff_get_color_opt(o, DIFF_CONTEXT);
-		reset = diff_get_color_opt(o, DIFF_RESET);
-		putc('\n', o->file);
-		emit_line_0(o, context, NULL, 0, reset, '\\',
-			    nneof, strlen(nneof));
-		break;
 	case DIFF_SYMBOL_SUBMODULE_HEADER:
 	case DIFF_SYMBOL_SUBMODULE_ERROR:
 	case DIFF_SYMBOL_SUBMODULE_PIPETHROUGH:
@@ -1786,22 +1777,36 @@ static void emit_rewrite_lines(struct emit_callback *ecbdata,
 	const char *endp = NULL;
 
 	while (0 < size) {
-		int len;
+		int len, plen;
+		char *pdata = NULL;
 
 		endp = memchr(data, '\n', size);
 		len = endp ? (endp - data + 1) : size;
+		plen = len;
+
+		if (!endp) {
+			plen = len + 1;
+			pdata = xmalloc(plen + 2);
+			memcpy(pdata, data, len);
+			pdata[len] = '\n';
+			pdata[len + 1] = '\0';
+		}
 		if (prefix != '+') {
 			ecbdata->lno_in_preimage++;
-			emit_del_line(ecbdata, data, len);
+			emit_del_line(ecbdata, pdata ? pdata : data, plen);
 		} else {
 			ecbdata->lno_in_postimage++;
-			emit_add_line(ecbdata, data, len);
+			emit_add_line(ecbdata, pdata ? pdata : data, plen);
 		}
+		free(pdata);
 		size -= len;
 		data += len;
 	}
-	if (!endp)
-		emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
+	if (!endp) {
+		static const char nneof[] = "\\ No newline at end of file\n";
+		ecbdata->last_line_kind = prefix;
+		emit_incomplete_line(ecbdata, nneof, sizeof(nneof) - 1);
+	}
 }
 
 static void emit_rewrite_diff(const char *name_a,
-- 
2.52.0-rc0
Phillip Wood· Nov 10, 2025, 14:54 UTC · re: Junio C Hamano · lore

Re: [PATCH 07/12] diff: update the way rewrite diff handles incomplete lines

On 04/11/2025 02:09, Junio C Hamano wrote:
Show 8 quoted lines
> 
> Revamp the way the complete-rewrite code path feeds the lines to the
> output layer by treating the last line of the pre/post image when it
> is an incomplete line specially.
> 
> This lets us remove the DIFF_SYMBOL_NO_LF_EOF hack and use the usual
> DIFF_SYMBOL_CONTEXT_INCOMPLETE code path, which will later learn how
> to handle whitespace errors.
This is a nice cleanup
Show 19 quoted lines
> @@ -1786,22 +1777,36 @@ static void emit_rewrite_lines(struct emit_callback *ecbdata,
>   	const char *endp = NULL;
>   
>   	while (0 < size) {
> -		int len;
> +		int len, plen;
> +		char *pdata = NULL;
>   
>   		endp = memchr(data, '\n', size);
>   		len = endp ? (endp - data + 1) : size;
> +		plen = len;
> +
> +		if (!endp) {
> +			plen = len + 1;
> +			pdata = xmalloc(plen + 2);
> +			memcpy(pdata, data, len);
> +			pdata[len] = '\n';
> +			pdata[len + 1] = '\0';
> +		}
I think it would be clearer to refactor this as
  		endp = memchr(data, '\n', size);
-		len = endp ? (endp - data + 1) : size;
		if (endp) {
			len = endp - data + 1;
			plen = len;
		} else {
			len = size;
+			plen = len + 1;
+			pdata = xmalloc(plen + 2);
+			memcpy(pdata, data, len);
+			pdata[len] = '\n';
+			pdata[len + 1] = '\0';
+		}
Thanks
Phillip
Show 23 quoted lines
>   		if (prefix != '+') {
>   			ecbdata->lno_in_preimage++;
> -			emit_del_line(ecbdata, data, len);
> +			emit_del_line(ecbdata, pdata ? pdata : data, plen);
>   		} else {
>   			ecbdata->lno_in_postimage++;
> -			emit_add_line(ecbdata, data, len);
> +			emit_add_line(ecbdata, pdata ? pdata : data, plen);
>   		}
> +		free(pdata);
>   		size -= len;
>   		data += len;
>   	}
> -	if (!endp)
> -		emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
> +	if (!endp) {
> +		static const char nneof[] = "\\ No newline at end of file\n";
> +		ecbdata->last_line_kind = prefix;
> +		emit_incomplete_line(ecbdata, nneof, sizeof(nneof) - 1);
> +	}
>   }
>   
>   static void emit_rewrite_diff(const char *name_a,
Junio C Hamano· Nov 10, 2025, 18:33 UTC · re: Phillip Wood · lore

Re: [PATCH 07/12] diff: update the way rewrite diff handles incomplete lines

Phillip Wood <phillip.wood123@gmail.com> writes:
Show 27 quoted lines
>>   		endp = memchr(data, '\n', size);
>>   		len = endp ? (endp - data + 1) : size;
>> +		plen = len;
>> +
>> +		if (!endp) {
>> +			plen = len + 1;
>> +			pdata = xmalloc(plen + 2);
>> +			memcpy(pdata, data, len);
>> +			pdata[len] = '\n';
>> +			pdata[len + 1] = '\0';
>> +		}
>
> I think it would be clearer to refactor this as
>
>   		endp = memchr(data, '\n', size);
> -		len = endp ? (endp - data + 1) : size;
> 		if (endp) {
> 			len = endp - data + 1;
> 			plen = len;
> 		} else {
> 			len = size;
> +			plen = len + 1;
> +			pdata = xmalloc(plen + 2);
> +			memcpy(pdata, data, len);
> +			pdata[len] = '\n';
> +			pdata[len + 1] = '\0';
> +		}
Perhaps.  I'll try and see if I agree.
Thanks.
Junio C Hamano· Nov 4, 2025, 02:09 UTC · re: Junio C Hamano · lore

[PATCH 08/12] apply: revamp the parsing of incomplete lines

A patch file represents the incomplete line at the end of the file with two lines, one that is the usual "context" with " " as the first letter, "added" with "+" as the first letter, or "removed" with "-" as the first letter that shows the content of the line, plus an extra "\ No newline at the end of file" line that comes immediately after it.

Ever since the apply machinery was written, the "git apply" machinery parses "\ No newline at the end of file" line independently, without even knowing what line the incomplete-ness applies to, simply because it does not even remember what the previous line was.

This poses a problem if we want to check and warn on an incomplete line. Revamp the code that parses a fragment, to actually drop the '\n' at the end of the incoming patch file that terminates a line, so that check_whitespace() calls made from the code path actually sees an incomplete as incomplete.

Note that the result of this parsing is not directly used by the code path that applies the patch. apply_one_fragment() function already checks if each of the patch text it handles is followed by a line that begins with a backslash to drop the newline at the end of the current line it is looking at. In a sense, this patch harmonizes the behaviour of the parsing side to what is already done in the application side.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 apply.c | 70 ++++++++++++++++++++++++++++++++++++++++-----------------
 1 file changed, 49 insertions(+), 21 deletions(-)
Show changes to apply.c +49 −21
diff --git a/apply.c b/apply.c
index a2ceb3fb40..2b0f8bdab5 100644
--- a/apply.c
+++ b/apply.c
@@ -1670,6 +1670,35 @@ static void check_old_for_crlf(struct patch *patch, const char *line, int len)
 }
 
 
+/*
+ * Just saw a single line in a fragment.  If it is a part of this hunk
+ * that is a context " ", an added "+", or a removed "-" line, it may
+ * be followed by "\\ No newline..." to signal that the last "\n" on
+ * this line needs to be dropped.  Depending on locale settings when
+ * the patch was produced we don't know what this line would exactly
+ * say. The only thing we do know is that it begins with "\ ".
+ * Checking for 12 is just for sanity check; "\ No newline..." would
+ * be at least that long in any l10n.
+ *
+ * Return 0 if the line we saw is not followed by "\ No newline...",
+ * or length of that line.  The caller will use it to skip over the
+ * "\ No newline..." line.
+ */
+static int adjust_incomplete(const char *line, int len,
+			     unsigned long size)
+{
+	int nextlen;
+
+	if (*line != '\n' && *line != ' ' && *line != '+' && *line != '-')
+		return 0;
+	if (size - len < 12 || memcmp(line + len, "\\ ", 2))
+		return 0;
+	nextlen = linelen(line + len, size - len);
+	if (nextlen < 12)
+		return 0;
+	return nextlen;
+}
+
 /*
  * Parse a unified diff. Note that this really needs to parse each
  * fragment separately, since the only way to know the difference
@@ -1684,6 +1713,7 @@ static int parse_fragment(struct apply_state *state,
 {
 	int added, deleted;
 	int len = linelen(line, size), offset;
+	int skip_len = 0;
 	unsigned long oldlines, newlines;
 	unsigned long leading, trailing;
 
@@ -1710,6 +1740,22 @@ static int parse_fragment(struct apply_state *state,
 		len = linelen(line, size);
 		if (!len || line[len-1] != '\n')
 			return -1;
+
+		/*
+		 * For an incomplete line, skip_len counts the bytes
+		 * on "\\ No newline..." marker line that comes next
+		 * to the current line.
+		 *
+		 * Reduce "len" to drop the newline at the end of
+		 * line[], but add one to "skip_len", which will be
+		 * added back to "len" for the next iteration, to
+		 * compensate.
+		 */
+		skip_len = adjust_incomplete(line, len, size);
+		if (skip_len) {
+			len--;
+			skip_len++;
+		}
 		switch (*line) {
 		default:
 			return -1;
@@ -1745,20 +1791,10 @@ static int parse_fragment(struct apply_state *state,
 			newlines--;
 			trailing = 0;
 			break;
-
-		/*
-		 * We allow "\ No newline at end of file". Depending
-		 * on locale settings when the patch was produced we
-		 * don't know what this line looks like. The only
-		 * thing we do know is that it begins with "\ ".
-		 * Checking for 12 is just for sanity check -- any
-		 * l10n of "\ No newline..." is at least that long.
-		 */
-		case '\\':
-			if (len < 12 || memcmp(line, "\\ ", 2))
-				return -1;
-			break;
 		}
+
+		/* eat the "\\ No newline..." as well, if exists */
+		len += skip_len;
 	}
 	if (oldlines || newlines)
 		return -1;
@@ -1768,14 +1804,6 @@ static int parse_fragment(struct apply_state *state,
 	fragment->leading = leading;
 	fragment->trailing = trailing;
 
-	/*
-	 * If a fragment ends with an incomplete line, we failed to include
-	 * it in the above loop because we hit oldlines == newlines == 0
-	 * before seeing it.
-	 */
-	if (12 < size && !memcmp(line, "\\ ", 2))
-		offset += linelen(line, size);
-
 	patch->lines_added += added;
 	patch->lines_deleted += deleted;
 
-- 
2.52.0-rc0
Junio C Hamano· Nov 4, 2025, 02:09 UTC · re: Junio C Hamano · lore

[PATCH 09/12] whitespace: allocate a few more bits

Reserve a few more bits in the diff flags word to be used for future whitespace rules. No behaviour changes intended.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 16 ++++++++--------
 diff.h |  6 +++---
 ws.h   |  2 +-
 3 files changed, 12 insertions(+), 12 deletions(-)
Show changes to 3 files +12 −12

diff.c, diff.h, ws.h

diff --git a/diff.c b/diff.c
index 99298720f4..8d03146aaa 100644
--- a/diff.c
+++ b/diff.c
@@ -804,15 +804,15 @@ enum diff_symbol {
 
 /*
  * Flags for content lines:
- * 0..11 are whitespace rules (see ws.h)
- * 12..14 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD
- * 16 is marking if the line is blank at EOF
- * 17..19 are used for color-moved.
+ * 0..15 are whitespace rules (see ws.h)
+ * 16..18 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD
+ * 19 is marking if the line is blank at EOF
+ * 20..22 are used for color-moved.
  */
-#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF	(1<<16)
-#define DIFF_SYMBOL_MOVED_LINE			(1<<17)
-#define DIFF_SYMBOL_MOVED_LINE_ALT		(1<<18)
-#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING	(1<<19)
+#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF	(1<<19)
+#define DIFF_SYMBOL_MOVED_LINE			(1<<20)
+#define DIFF_SYMBOL_MOVED_LINE_ALT		(1<<21)
+#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING	(1<<22)
 
 #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)
 
diff --git a/diff.h b/diff.h
index cbd355cf50..422658407d 100644
--- a/diff.h
+++ b/diff.h
@@ -331,9 +331,9 @@ struct diff_options {
 
 	int ita_invisible_in_index;
 /* white-space error highlighting */
-#define WSEH_NEW        (1<<12)
-#define WSEH_CONTEXT    (1<<13)
-#define WSEH_OLD        (1<<14)
+#define WSEH_NEW        (1<<16)
+#define WSEH_CONTEXT    (1<<17)
+#define WSEH_OLD        (1<<18)
 	unsigned ws_error_highlight;
 	const char *prefix;
 	int prefix_length;
diff --git a/ws.h b/ws.h
index 23708efb73..c77d3b6b19 100644
--- a/ws.h
+++ b/ws.h
@@ -21,7 +21,7 @@ struct strbuf;
 #define WS_TAB_WIDTH_MASK       ((1<<6)-1)
 
 /* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */
-#define WS_RULE_MASK            ((1<<12)-1)
+#define WS_RULE_MASK            ((1<<16)-1)
 
 extern unsigned whitespace_rule_cfg;
 unsigned whitespace_rule(struct index_state *, const char *);
-- 
2.52.0-rc0
Junio C Hamano· Nov 4, 2025, 02:09 UTC · re: Junio C Hamano · lore

[PATCH 10/12] apply: check and fix incomplete lines

The final line of a file that lacks the terminating newline at its end is called an incomplete line. In general they are frowned upon for many reasons (imagine concatenating two files with "cat A B" and what happens when A ends in an incomplete line, for example), and text-oriented tools often mishandle such a line.

Introduce a new whitespace rule "incomplete-line", which is off by default for backward compatibility's sake, so that "git apply --whitespace={fix,warn,error}" can notice, warn against, and fix them.

As one of the new test shows, if you modify contents on an incomplete line in the original and leave the resulting line incomplete, it is still considered a whitespace error, the reasoning being that "you'd better fix it while at it if you are making a change on an incomplete line anyway", which may controversial.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 t/t4124-apply-ws-rule.sh | 112 +++++++++++++++++++++++++++++++++++++++
 ws.c                     |  20 +++++++
 ws.h                     |   1 +
 3 files changed, 133 insertions(+)
Show changes to 3 files +133 −0

t/t4124-apply-ws-rule.sh, ws.c, ws.h

diff --git a/t/t4124-apply-ws-rule.sh b/t/t4124-apply-ws-rule.sh
index 485c7d2d12..568805df31 100755
--- a/t/t4124-apply-ws-rule.sh
+++ b/t/t4124-apply-ws-rule.sh
@@ -556,4 +556,116 @@ test_expect_success 'whitespace check skipped for excluded paths' '
 	git apply --include=used --stat --whitespace=error <patch
 '
 
+test_expect_success 'check incomplete lines (setup)' '
+	rm -f .gitattributes &&
+	git config core.whitespace incomplete-line
+'
+
+test_expect_success 'incomplete context line (not an error)' '
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	(test_write_lines 1 2 3 0 5 && printf 6) >sample2-i &&
+	cat sample-i >target &&
+	git add target &&
+	cat sample2-i >target &&
+	git diff-files -p target >patch &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error <patch &&
+	test_cmp sample2-i target &&
+
+	cat sample2-i >target &&
+	git apply --whitespace=error -R <patch &&
+	test_cmp sample-i target
+'
+
+test_expect_success 'last line made incomplete (error)' '
+	test_write_lines 1 2 3 4 5 6 >sample &&
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	cat sample >target &&
+	git add target &&
+	cat sample-i >target &&
+	git diff-files -p target >patch &&
+
+	cat sample >target &&
+	test_must_fail git apply --whitespace=error <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error -R <patch &&
+	test_cmp sample target &&
+
+	cat sample >target &&
+	git apply --whitespace=fix <patch &&
+	test_cmp sample target
+'
+
+test_expect_success 'incomplete line removed at the end (not an error)' '
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	test_write_lines 1 2 3 4 5 6 >sample &&
+	cat sample-i >target &&
+	git add target &&
+	cat sample >target &&
+	git diff-files -p target >patch &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error <patch &&
+	test_cmp sample target &&
+
+	cat sample >target &&
+	test_must_fail git apply --whitespace=error -R <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample >target &&
+	git apply --whitespace=fix -R <patch &&
+	test_cmp sample target
+'
+
+test_expect_success 'incomplete line corrected at the end (not an error)' '
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	test_write_lines 1 2 3 4 5 7 >sample3 &&
+	cat sample-i >target &&
+	git add target &&
+	cat sample3 >target &&
+	git diff-files -p target >patch &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error <patch &&
+	test_cmp sample3 target &&
+
+	cat sample3 >target &&
+	test_must_fail git apply --whitespace=error -R <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample3 >target &&
+	git apply --whitespace=fix -R <patch &&
+	test_cmp sample target
+'
+
+test_expect_success 'incomplete line modified at the end (error)' '
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	(test_write_lines 1 2 3 4 5 && printf 7) >sample3-i &&
+	test_write_lines 1 2 3 4 5 6 >sample &&
+	test_write_lines 1 2 3 4 5 7 >sample3 &&
+	cat sample-i >target &&
+	git add target &&
+	cat sample3-i >target &&
+	git diff-files -p target >patch &&
+
+	cat sample-i >target &&
+	test_must_fail git apply --whitespace=error <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample3-i >target &&
+	test_must_fail git apply --whitespace=error -R <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample-i >target &&
+	git apply --whitespace=fix <patch &&
+	test_cmp sample3 target &&
+
+	cat sample3-i >target &&
+	git apply --whitespace=fix -R <patch &&
+	test_cmp sample target
+'
+
 test_done
diff --git a/ws.c b/ws.c
index 70acee3337..6cc2466c0c 100644
--- a/ws.c
+++ b/ws.c
@@ -26,6 +26,7 @@ static struct whitespace_rule {
 	{ "blank-at-eol", WS_BLANK_AT_EOL, 0 },
 	{ "blank-at-eof", WS_BLANK_AT_EOF, 0 },
 	{ "tab-in-indent", WS_TAB_IN_INDENT, 0, 1 },
+	{ "incomplete-line", WS_INCOMPLETE_LINE, 0, 0 },
 };
 
 unsigned parse_whitespace_rule(const char *string)
@@ -139,6 +140,11 @@ char *whitespace_error_string(unsigned ws)
 			strbuf_addstr(&err, ", ");
 		strbuf_addstr(&err, "tab in indent");
 	}
+	if (ws & WS_INCOMPLETE_LINE) {
+		if (err.len)
+			strbuf_addstr(&err, ", ");
+		strbuf_addstr(&err, "no newline at the end of file");
+	}
 	return strbuf_detach(&err, NULL);
 }
 
@@ -180,6 +186,9 @@ static unsigned ws_check_emit_1(const char *line, int len, unsigned ws_rule,
 	if (trailing_whitespace == -1)
 		trailing_whitespace = len;
 
+	if (!trailing_newline && (ws_rule & WS_INCOMPLETE_LINE))
+		result |= WS_INCOMPLETE_LINE;
+
 	/* Check indentation */
 	for (i = 0; i < trailing_whitespace; i++) {
 		if (line[i] == ' ')
@@ -291,6 +300,17 @@ void ws_fix_copy(struct strbuf *dst, const char *src, int len, unsigned ws_rule,
 	int last_space_in_indent = -1;
 	int need_fix_leading_space = 0;
 
+	/*
+	 * Remembering that we need to add '\n' at the end
+	 * is sufficient to fix an incomplete line.
+	 */
+	if (ws_rule & WS_INCOMPLETE_LINE) {
+		if (0 < len && src[len - 1] != '\n') {
+			fixed = 1;
+			add_nl_to_tail = 1;
+		}
+	}
+
 	/*
 	 * Strip trailing whitespace
 	 */
diff --git a/ws.h b/ws.h
index c77d3b6b19..06d5cb73f8 100644
--- a/ws.h
+++ b/ws.h
@@ -15,6 +15,7 @@ struct strbuf;
 #define WS_CR_AT_EOL            (1<<9)
 #define WS_BLANK_AT_EOF         (1<<10)
 #define WS_TAB_IN_INDENT        (1<<11)
+#define WS_INCOMPLETE_LINE      (1<<12)
 
 #define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
 #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)
-- 
2.52.0-rc0
Junio C Hamano· Nov 4, 2025, 02:09 UTC · re: Junio C Hamano · lore

[PATCH 11/12] 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>
---
 diff.c                     | 29 ++++++++++++++++--
 t/t4015-diff-whitespace.sh | 63 +++++++++++++++++++++++++++++++++++---
 2 files changed, 86 insertions(+), 6 deletions(-)
Show changes to 2 files +86 −6

diff.c, t/t4015-diff-whitespace.sh

diff --git a/diff.c b/diff.c
index 8d03146aaa..965b97f7f0 100644
--- a/diff.c
+++ b/diff.c
@@ -1370,7 +1370,11 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
 		emit_line(o, "", "", line, len);
 		break;
 	case DIFF_SYMBOL_CONTEXT_INCOMPLETE:
-		set = diff_get_color_opt(o, DIFF_CONTEXT);
+		if ((flags & WS_INCOMPLETE_LINE) &&
+		    (flags & o->ws_error_highlight))
+			set = diff_get_color_opt(o, DIFF_WHITESPACE);
+		else
+			set = diff_get_color_opt(o, DIFF_CONTEXT);
 		reset = diff_get_color_opt(o, DIFF_RESET);
 		emit_line(o, set, reset, line, len);
 		break;
@@ -1666,8 +1670,14 @@ static void emit_context_line(struct emit_callback *ecbdata,
 static void emit_incomplete_line(struct emit_callback *ecbdata,
 				 const char *line, int len)
 {
+	int last_line_kind = ecbdata->last_line_kind;
+	unsigned flags = (last_line_kind == '+'
+			  ? WSEH_NEW
+			  : last_line_kind == '-'
+			  ? WSEH_OLD
+			  : WSEH_CONTEXT) | ecbdata->ws_rule;
 	emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
-			 line, len, 0);
+			 line, len, flags);
 }
 
 static void emit_hunk_header(struct emit_callback *ecbdata,
@@ -3257,6 +3267,7 @@ struct checkdiff_t {
 	struct diff_options *o;
 	unsigned ws_rule;
 	unsigned status;
+	int last_line_kind;
 };
 
 static int is_conflict_marker(const char *line, int marker_size, unsigned long len)
@@ -3295,6 +3306,7 @@ static void checkdiff_consume_hunk(void *priv,
 static int checkdiff_consume(void *priv, char *line, unsigned long len)
 {
 	struct checkdiff_t *data = priv;
+	int last_line_kind;
 	int marker_size = data->conflict_marker_size;
 	const char *ws = diff_get_color(data->o->use_color, DIFF_WHITESPACE);
 	const char *reset = diff_get_color(data->o->use_color, DIFF_RESET);
@@ -3305,6 +3317,8 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)
 	assert(data->o);
 	line_prefix = diff_line_prefix(data->o);
 
+	last_line_kind = data->last_line_kind;
+	data->last_line_kind = line[0];
 	if (line[0] == '+') {
 		unsigned bad;
 		data->lineno++;
@@ -3327,6 +3341,17 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)
 			      data->o->file, set, reset, ws);
 	} else if (line[0] == ' ') {
 		data->lineno++;
+	} else if (line[0] == '\\') {
+		/* no newline at the end of the line */
+		if ((data->ws_rule & WS_INCOMPLETE_LINE) &&
+		    (last_line_kind == '+')) {
+			unsigned bad = WS_INCOMPLETE_LINE;
+			data->status |= bad;
+			err = whitespace_error_string(bad);
+			fprintf(data->o->file, "%s%s:%d: %s.\n",
+				line_prefix, data->filename, data->lineno, err);
+			free(err);
+		}
 	}
 	return 0;
 }
diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh
index 9de7f73f42..138730cbce 100755
--- a/t/t4015-diff-whitespace.sh
+++ b/t/t4015-diff-whitespace.sh
@@ -43,6 +43,49 @@ do
 	'
 done
 
+test_expect_success "incomplete line in both pre- and post-image context" '
+	(echo foo && echo baz | tr -d "\012") >x &&
+	git add x &&
+	(echo bar && echo baz | tr -d "\012") >x &&
+	git diff x &&
+	git -c core.whitespace=incomplete diff --check x &&
+	git diff -R x &&
+	git -c core.whitespace=incomplete diff -R --check x
+'
+
+test_expect_success "incomplete lines on both pre- and post-image" '
+	# The interpretation taken here is "since you are toucing
+	# the line anyway, you would better fix the incomplete line
+	# while you are at it."  but this is debatable.
+	echo foo | tr -d "\012" >x &&
+	git add x &&
+	echo bar | tr -d "\012" >x &&
+	git diff x &&
+	test_must_fail git -c core.whitespace=incomplete diff --check x &&
+	git diff -R x &&
+	test_must_fail git -c core.whitespace=incomplete diff -R --check x
+'
+
+test_expect_success "fix incomplete line in pre-image" '
+	echo foo | tr -d "\012" >x &&
+	git add x &&
+	echo bar >x &&
+	git diff x &&
+	git -c core.whitespace=incomplete diff --check x &&
+	git diff -R x &&
+	test_must_fail git -c core.whitespace=incomplete diff -R --check x
+'
+
+test_expect_success "new incomplete line in post-image" '
+	echo foo >x &&
+	git add x &&
+	echo bar | tr -d "\012" >x &&
+	git diff x &&
+	test_must_fail git -c core.whitespace=incomplete diff --check x &&
+	git diff -R x &&
+	git -c core.whitespace=incomplete diff -R --check x
+'
+
 test_expect_success "Ray Lehtiniemi's example" '
 	cat <<-\EOF >x &&
 	do {
@@ -1040,7 +1083,8 @@ test_expect_success 'ws-error-highlight test setup' '
 	{
 		echo "0. blank-at-eol " &&
 		echo "1. still-blank-at-eol " &&
-		echo "2. and a new line "
+		echo "2. and a new line " &&
+		printf "3. and more"
 	} >x &&
 	new_hash_x=$(git hash-object x) &&
 	after=$(git rev-parse --short "$new_hash_x") &&
@@ -1050,11 +1094,13 @@ test_expect_success 'ws-error-highlight test setup' '
 	<BOLD>index $before..$after 100644<RESET>
 	<BOLD>--- a/x<RESET>
 	<BOLD>+++ b/x<RESET>
-	<CYAN>@@ -1,2 +1,3 @@<RESET>
+	<CYAN>@@ -1,2 +1,4 @@<RESET>
 	 0. blank-at-eol <RESET>
 	<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>
 	<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>
 	<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>
+	<GREEN>+<RESET><GREEN>3. and more<RESET>
+	<BLUE>\ No newline at end of file<RESET>
 	EOF
 
 	cat >expect.all <<-EOF &&
@@ -1062,11 +1108,13 @@ test_expect_success 'ws-error-highlight test setup' '
 	<BOLD>index $before..$after 100644<RESET>
 	<BOLD>--- a/x<RESET>
 	<BOLD>+++ b/x<RESET>
-	<CYAN>@@ -1,2 +1,3 @@<RESET>
+	<CYAN>@@ -1,2 +1,4 @@<RESET>
 	 <RESET>0. blank-at-eol<RESET><BLUE> <RESET>
 	<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>
 	<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>
 	<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>
+	<GREEN>+<RESET><GREEN>3. and more<RESET>
+	<BLUE>\ No newline at end of file<RESET>
 	EOF
 
 	cat >expect.none <<-EOF
@@ -1074,16 +1122,19 @@ test_expect_success 'ws-error-highlight test setup' '
 	<BOLD>index $before..$after 100644<RESET>
 	<BOLD>--- a/x<RESET>
 	<BOLD>+++ b/x<RESET>
-	<CYAN>@@ -1,2 +1,3 @@<RESET>
+	<CYAN>@@ -1,2 +1,4 @@<RESET>
 	 0. blank-at-eol <RESET>
 	<RED>-1. blank-at-eol <RESET>
 	<GREEN>+1. still-blank-at-eol <RESET>
 	<GREEN>+2. and a new line <RESET>
+	<GREEN>+3. and more<RESET>
+	\ No newline at end of file<RESET>
 	EOF
 
 '
 
 test_expect_success 'test --ws-error-highlight option' '
+	git config core.whitespace blank-at-eol,incomplete-line &&
 
 	git diff --color --ws-error-highlight=default,old >current.raw &&
 	test_decode_color <current.raw >current &&
@@ -1100,6 +1151,7 @@ test_expect_success 'test --ws-error-highlight option' '
 '
 
 test_expect_success 'test diff.wsErrorHighlight config' '
+	git config core.whitespace blank-at-eol,incomplete-line &&
 
 	git -c diff.wsErrorHighlight=default,old diff --color >current.raw &&
 	test_decode_color <current.raw >current &&
@@ -1116,6 +1168,7 @@ test_expect_success 'test diff.wsErrorHighlight config' '
 '
 
 test_expect_success 'option overrides diff.wsErrorHighlight' '
+	git config core.whitespace blank-at-eol,incomplete-line &&
 
 	git -c diff.wsErrorHighlight=none \
 		diff --color --ws-error-highlight=default,old >current.raw &&
@@ -1135,6 +1188,8 @@ test_expect_success 'option overrides diff.wsErrorHighlight' '
 '
 
 test_expect_success 'detect moved code, complete file' '
+	git config core.whitespace blank-at-eol &&
+
 	git reset --hard &&
 	cat <<-\EOF >test.c &&
 	#include<stdio.h>
-- 
2.52.0-rc0
Phillip Wood· Nov 10, 2025, 14:55 UTC · re: Junio C Hamano · lore

Re: [PATCH 11/12] diff: highlight and error out on incomplete lines

On 04/11/2025 02:09, Junio C Hamano wrote:
Show 9 quoted 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.
This explains why we don't need to call emit_line_ws_markup() in this case
> Also teach "git diff --check" to notice incomplete lines as
> whitespace errors and report when incomplete-line whitespace error
> class is in effect.
Nice. The implementation looks good, I've left a few comments on the tests
Show 10 quoted lines
> diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh
> index 9de7f73f42..138730cbce 100755
> --- a/t/t4015-diff-whitespace.sh
> +++ b/t/t4015-diff-whitespace.sh
> @@ -43,6 +43,49 @@ do
>   	'
>   done
>   
> +test_expect_success "incomplete line in both pre- and post-image context" '
> +	(echo foo && echo baz | tr -d "\012") >x &&
'printf "foo\nbaz"' might be clearer and save us forking "tr"
Show 10 quoted lines
> +	git add x &&
> +	(echo bar && echo baz | tr -d "\012") >x &&
> +	git diff x &&
> +	git -c core.whitespace=incomplete diff --check x &&
> +	git diff -R x &&
> +	git -c core.whitespace=incomplete diff -R --check x
> +'
> +
> +test_expect_success "incomplete lines on both pre- and post-image" '
> +	# The interpretation taken here is "since you are toucing
s/toucing/touching/
> +	# the line anyway, you would better fix the incomplete line
> +	# while you are at it."  but this is debatable.
I think it is a reasonable default.
Show 5 quoted lines
> +	echo foo | tr -d "\012" >x &&
> +	git add x &&
> +	echo bar | tr -d "\012" >x &&
> +	git diff x &&
> +	test_must_fail git -c core.whitespace=incomplete diff --check x &&
Do we want to check the error message here?

Looking at the tests below the coverage looks good for "diff --check" and for diff.wsErrorHighlight

Thanks
Phillip
Show 113 quoted lines
> +	git diff -R x &&
> +	test_must_fail git -c core.whitespace=incomplete diff -R --check x
> +'
> +
> +test_expect_success "fix incomplete line in pre-image" '
> +	echo foo | tr -d "\012" >x &&
> +	git add x &&
> +	echo bar >x &&
> +	git diff x &&
> +	git -c core.whitespace=incomplete diff --check x &&
> +	git diff -R x &&
> +	test_must_fail git -c core.whitespace=incomplete diff -R --check x
> +'
> +
> +test_expect_success "new incomplete line in post-image" '
> +	echo foo >x &&
> +	git add x &&
> +	echo bar | tr -d "\012" >x &&
> +	git diff x &&
> +	test_must_fail git -c core.whitespace=incomplete diff --check x &&
> +	git diff -R x &&
> +	git -c core.whitespace=incomplete diff -R --check x
> +'
> +
>   test_expect_success "Ray Lehtiniemi's example" '
>   	cat <<-\EOF >x &&
>   	do {
> @@ -1040,7 +1083,8 @@ test_expect_success 'ws-error-highlight test setup' '
>   	{
>   		echo "0. blank-at-eol " &&
>   		echo "1. still-blank-at-eol " &&
> -		echo "2. and a new line "
> +		echo "2. and a new line " &&
> +		printf "3. and more"
>   	} >x &&
>   	new_hash_x=$(git hash-object x) &&
>   	after=$(git rev-parse --short "$new_hash_x") &&
> @@ -1050,11 +1094,13 @@ test_expect_success 'ws-error-highlight test setup' '
>   	<BOLD>index $before..$after 100644<RESET>
>   	<BOLD>--- a/x<RESET>
>   	<BOLD>+++ b/x<RESET>
> -	<CYAN>@@ -1,2 +1,3 @@<RESET>
> +	<CYAN>@@ -1,2 +1,4 @@<RESET>
>   	 0. blank-at-eol <RESET>
>   	<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>
>   	<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>
>   	<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>
> +	<GREEN>+<RESET><GREEN>3. and more<RESET>
> +	<BLUE>\ No newline at end of file<RESET>
>   	EOF
>   
>   	cat >expect.all <<-EOF &&
> @@ -1062,11 +1108,13 @@ test_expect_success 'ws-error-highlight test setup' '
>   	<BOLD>index $before..$after 100644<RESET>
>   	<BOLD>--- a/x<RESET>
>   	<BOLD>+++ b/x<RESET>
> -	<CYAN>@@ -1,2 +1,3 @@<RESET>
> +	<CYAN>@@ -1,2 +1,4 @@<RESET>
>   	 <RESET>0. blank-at-eol<RESET><BLUE> <RESET>
>   	<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>
>   	<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>
>   	<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>
> +	<GREEN>+<RESET><GREEN>3. and more<RESET>
> +	<BLUE>\ No newline at end of file<RESET>
>   	EOF
>   
>   	cat >expect.none <<-EOF
> @@ -1074,16 +1122,19 @@ test_expect_success 'ws-error-highlight test setup' '
>   	<BOLD>index $before..$after 100644<RESET>
>   	<BOLD>--- a/x<RESET>
>   	<BOLD>+++ b/x<RESET>
> -	<CYAN>@@ -1,2 +1,3 @@<RESET>
> +	<CYAN>@@ -1,2 +1,4 @@<RESET>
>   	 0. blank-at-eol <RESET>
>   	<RED>-1. blank-at-eol <RESET>
>   	<GREEN>+1. still-blank-at-eol <RESET>
>   	<GREEN>+2. and a new line <RESET>
> +	<GREEN>+3. and more<RESET>
> +	\ No newline at end of file<RESET>
>   	EOF
>   
>   '
>   
>   test_expect_success 'test --ws-error-highlight option' '
> +	git config core.whitespace blank-at-eol,incomplete-line &&
>   
>   	git diff --color --ws-error-highlight=default,old >current.raw &&
>   	test_decode_color <current.raw >current &&
> @@ -1100,6 +1151,7 @@ test_expect_success 'test --ws-error-highlight option' '
>   '
>   
>   test_expect_success 'test diff.wsErrorHighlight config' '
> +	git config core.whitespace blank-at-eol,incomplete-line &&
>   
>   	git -c diff.wsErrorHighlight=default,old diff --color >current.raw &&
>   	test_decode_color <current.raw >current &&
> @@ -1116,6 +1168,7 @@ test_expect_success 'test diff.wsErrorHighlight config' '
>   '
>   
>   test_expect_success 'option overrides diff.wsErrorHighlight' '
> +	git config core.whitespace blank-at-eol,incomplete-line &&
>   
>   	git -c diff.wsErrorHighlight=none \
>   		diff --color --ws-error-highlight=default,old >current.raw &&
> @@ -1135,6 +1188,8 @@ test_expect_success 'option overrides diff.wsErrorHighlight' '
>   '
>   
>   test_expect_success 'detect moved code, complete file' '
> +	git config core.whitespace blank-at-eol &&
> +
>   	git reset --hard &&
>   	cat <<-\EOF >test.c &&
>   	#include<stdio.h>
Junio C Hamano· Nov 10, 2025, 18:38 UTC · re: Phillip Wood · lore

Re: [PATCH 11/12] diff: highlight and error out on incomplete lines

Phillip Wood <phillip.wood123@gmail.com> writes:
Show 12 quoted lines
> On 04/11/2025 02:09, Junio C Hamano wrote:
>> 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.
>
> This explains why we don't need to call emit_line_ws_markup() in this case
True.

Also, even if we were to call it on the previous, problematic line without terminating newline, there is no good spot on that line to paint red to grab attention to the reader, as we are trying to highlight lack of something, not presence of unwanted things, like trailing whitespaces ;-)

>> +test_expect_success "incomplete line in both pre- and post-image context" '
>> +	(echo foo && echo baz | tr -d "\012") >x &&
>
> 'printf "foo\nbaz"' might be clearer and save us forking "tr"
Perhaps.  I find it much harder to read and uglier, though.
Show 12 quoted lines
>> +	git add x &&
>> +	(echo bar && echo baz | tr -d "\012") >x &&
>> +	git diff x &&
>> +	git -c core.whitespace=incomplete diff --check x &&
>> +	git diff -R x &&
>> +	git -c core.whitespace=incomplete diff -R --check x
>> +'
>> +
>> +test_expect_success "incomplete lines on both pre- and post-image" '
>> +	# The interpretation taken here is "since you are toucing
>
> s/toucing/touching/
Thanks.
Show 12 quoted lines
>
>> +	# the line anyway, you would better fix the incomplete line
>> +	# while you are at it."  but this is debatable.
>
> I think it is a reasonable default.
>> +	echo foo | tr -d "\012" >x &&
>> +	git add x &&
>> +	echo bar | tr -d "\012" >x &&
>> +	git diff x &&
>> +	test_must_fail git -c core.whitespace=incomplete diff --check x &&
>
> Do we want to check the error message here?
Probably an overkill, but I could try.
> Looking at the tests below the coverage looks good for "diff --check" 
> and for diff.wsErrorHighlight
D. Ben Knoble· Nov 10, 2025, 23:56 UTC · re: Junio C Hamano · lore

Re: [PATCH 11/12] diff: highlight and error out on incomplete lines

On Mon, Nov 10, 2025 at 1:38 PM Junio C Hamano <gitster@pobox.com> wrote:
Show 10 quoted lines
>
> Phillip Wood <phillip.wood123@gmail.com> writes:
>
> > On 04/11/2025 02:09, Junio C Hamano wrote:
> >> +test_expect_success "incomplete line in both pre- and post-image context" '
> >> +    (echo foo && echo baz | tr -d "\012") >x &&
> >
> > 'printf "foo\nbaz"' might be clearer and save us forking "tr"
>
> Perhaps.  I find it much harder to read and uglier, though.
Funny, I find it easier to reason about.
It saves not only the tr fork but the subshell, too.
-- 
D. Ben Knoble
Junio C Hamano· Nov 4, 2025, 02:09 UTC · re: Junio C Hamano · lore

[PATCH 12/12] attr: enable incomplete-line whitespace error for this project

Now "git diff --check" and "git apply --whitespace=warn/fix" learned incomplete line is a whitespace error, enable them for this project to prevent patches to add new incomplete lines to our sources.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 .gitattributes | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
Show changes to .gitattributes +2 −2
diff --git a/.gitattributes b/.gitattributes
index 32583149c2..0accd23848 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,6 +1,6 @@
 * whitespace=!indent,trail,space
-*.[ch] whitespace=indent,trail,space diff=cpp
-*.sh whitespace=indent,trail,space text eol=lf
+*.[ch] whitespace=indent,trail,space,incomplete diff=cpp
+*.sh whitespace=indent,trail,space,incomplete text eol=lf
 *.perl text eol=lf diff=perl
 *.pl text eof=lf diff=perl
 *.pm text eol=lf diff=perl
-- 
2.52.0-rc0
Phillip Wood· Nov 10, 2025, 14:55 UTC · re: Junio C Hamano · lore

Re: [PATCH 12/12] attr: enable incomplete-line whitespace error for this project

On 04/11/2025 02:09, Junio C Hamano wrote:
> Now "git diff --check" and "git apply --whitespace=warn/fix" learned
> incomplete line is a whitespace error, enable them for this project
> to prevent patches to add new incomplete lines to our sources.
Makes sense
> -*.[ch] whitespace=indent,trail,space diff=cpp
> -*.sh whitespace=indent,trail,space text eol=lf
> +*.[ch] whitespace=indent,trail,space,incomplete diff=cpp
> +*.sh whitespace=indent,trail,space,incomplete text eol=lf

Do we want to check for incomplete lines in our documentation files as well? This series does not update WS_DEFAULT_RULE to include WS_INCOMPLETE_LINE so we will not detect incomplete lines unless we set an attribute.

Thanks
Phillip
>   *.perl text eol=lf diff=perl
>   *.pl text eof=lf diff=perl
>   *.pm text eol=lf diff=perl
Junio C Hamano· Nov 10, 2025, 18:40 UTC · re: Phillip Wood · lore

Re: [PATCH 12/12] attr: enable incomplete-line whitespace error for this project

Phillip Wood <phillip.wood123@gmail.com> writes:
Show 18 quoted lines
> On 04/11/2025 02:09, Junio C Hamano wrote:
>> Now "git diff --check" and "git apply --whitespace=warn/fix" learned
>> incomplete line is a whitespace error, enable them for this project
>> to prevent patches to add new incomplete lines to our sources.
>
> Makes sense
>
>> -*.[ch] whitespace=indent,trail,space diff=cpp
>> -*.sh whitespace=indent,trail,space text eol=lf
>> +*.[ch] whitespace=indent,trail,space,incomplete diff=cpp
>> +*.sh whitespace=indent,trail,space,incomplete text eol=lf
>
> Do we want to check for incomplete lines in our documentation files as 
> well?
> ...
> This series does not update WS_DEFAULT_RULE to include 
> WS_INCOMPLETE_LINE so we will not detect incomplete lines unless we set 
> an attribute.

Yes, that is why I said "to our sources". The rest is left for somebody else to do, hopefully long after this series settles ;-)

Thanks.
Junio C Hamano· Nov 5, 2025, 21:30 UTC · re: Junio C Hamano · lore

[PATCH v2 00/12] Incomplete lines

One of the common kind of whitespace errors is to lack the final newline at the end of a file, but so far, neither "git diff" or "git apply" did anything about them.

This series introduces "incomplete-line" whitespace error class, that you can add to either the core.whitespace configuration variable, or the whitespace attribute in your .gitattributes files.

The class is disabled by default, so the final step enables it for our project by defining it in the .gitattributes file.

The incomplete line marker that is given for a context line is not considered an error. The reasoning is that your preimage did have incomplete line, but you did not touch the contents on that incomplete line in your patch, so you left the line intact. It is not a new breakage you are responsible for.

If the incomplete line marker follows a postimage line, on the other hand, it means that you added a new line at the end of the file that is incomplete *and* that line did not exist in the preimage. The last line of the preimage may have been incomplete already, but then you updated the contents on that line, so you could have easily fixed the incompleteness of the line while at it. Either way, you are responsible for the incompleteness of the last ine in the resulting file.

The organization of the series is as follows.
 * The first patch [01/12] is a clean-up we have seen earlier on the
   list already (https://lore.kernel.org/git/xmqqfrb4hyjl.fsf@gitster.g/).
 * The patches [02/12] - [08/12] are preliminary clean-up made to
   both "git diff" and "git apply" machinery.
 * The patch [09/12] shifts the bit assignment (cleaned-up in
   [01/12] without changing any values) to make room for new
   whitespace error class (which was last updated in 2007 IIRC, so
   the set of whitespace errors surprisingly haven't changed for
   quite some time), and defines the new "incomplete-line" class.
 * The patch [10/12] teaches "git apply --whitespace=<mode>" and
   "git apply --check" about the incomplete-line error class.
 * The patch [11/12] teaches "git diff [--check]" about the
   incomplete-line error class.
 * The final patch [12/12] enables the incomplete-line error class
   for our project for C source files and shell scripts.  I didn't
   touch the cover-all * entry.
Changes in v2:
 - rolled the definition (but not implementation) of the new
   "incomplete-line" class into step [09/12] that shifts the bit
   assignment.  The documentation of core.whitespace has also be
   updated in this step.
 - "git apply --check" miscounted line number reported for the
   incomplete line error, which has been corrected in step [10/12].
 - t4124-apply-ws-rule.sh has been extended to cover "git apply
   --check" and the diagnostic output from it in step [10/12].
Junio C Hamano (12):
  whitespace: correct bit assignment comments
  diff: emit_line_ws_markup() if/else style fix
  diff: correct suppress_blank_empty hack
  diff: fix incorrect counting of line numbers
  diff: refactor output of incomplete line
  diff: call emit_callback ecbdata everywhere
  diff: update the way rewrite diff handles incomplete lines
  apply: revamp the parsing of incomplete lines
  whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE
  apply: check and fix incomplete lines
  diff: highlight and error out on incomplete lines
  attr: enable incomplete-line whitespace error for this project
 .gitattributes                 |   4 +-
 Documentation/config/core.adoc |   2 +
 apply.c                        |  79 ++++++++++----
 diff.c                         | 148 +++++++++++++++++---------
 diff.h                         |   6 +-
 t/t4015-diff-whitespace.sh     |  63 ++++++++++-
 t/t4124-apply-ws-rule.sh       | 187 +++++++++++++++++++++++++++++++++
 ws.c                           |  20 ++++
 ws.h                           |  26 +++--
 9 files changed, 448 insertions(+), 87 deletions(-)
Range-diff against v1:
 1:  4168f28fe7 =  1:  8a493cdea5 whitespace: correct bit assignment comments
 2:  53b7a010e7 =  2:  a01d99a055 diff: emit_line_ws_markup() if/else style fix
 3:  d93dd05543 =  3:  e3ea40af19 diff: correct suppress_blank_empty hack
 4:  5f58400bd7 =  4:  e15e89d3e2 diff: fix incorrect counting of line numbers
 5:  84c4ca147f =  5:  c007b3d7a7 diff: refactor output of incomplete line
 6:  55b42a1944 =  6:  0cea57091b diff: call emit_callback ecbdata everywhere
 7:  6947838d13 =  7:  523196b440 diff: update the way rewrite diff handles incomplete lines
 8:  63c36c6f70 =  8:  e098932784 apply: revamp the parsing of incomplete lines
 9:  00b645bb4e !  9:  28538f149f whitespace: allocate a few more bits
    @@ Metadata
     Author: Junio C Hamano <gitster@pobox.com>
     
      ## Commit message ##
    -    whitespace: allocate a few more bits
    +    whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE
     
         Reserve a few more bits in the diff flags word to be used for future
    -    whitespace rules.  No behaviour changes intended.
    +    whitespace rules.  Add WS_INCOMPLETE_LINE without implementing the
    +    behaviour (yet).
     
         Signed-off-by: Junio C Hamano <gitster@pobox.com>
     
    + ## Documentation/config/core.adoc ##
    +@@ Documentation/config/core.adoc: core.whitespace::
    +   part of the line terminator, i.e. with it, `trailing-space`
    +   does not trigger if the character before such a carriage-return
    +   is not a whitespace (not enabled by default).
    ++* `incomplete-line` treats the last line of a file that is missing the
    ++  newline at the end as an error (not enabled by default).
    + * `tabwidth=<n>` tells how many character positions a tab occupies; this
    +   is relevant for `indent-with-non-tab` and when Git fixes `tab-in-indent`
    +   errors. The default tab width is 8. Allowed values are 1 to 63.
    +
      ## diff.c ##
     @@ diff.c: enum diff_symbol {
      
    @@ diff.h: struct diff_options {
      	const char *prefix;
      	int prefix_length;
     
    + ## ws.c ##
    +@@ ws.c: static struct whitespace_rule {
    + 	{ "blank-at-eol", WS_BLANK_AT_EOL, 0 },
    + 	{ "blank-at-eof", WS_BLANK_AT_EOF, 0 },
    + 	{ "tab-in-indent", WS_TAB_IN_INDENT, 0, 1 },
    ++	{ "incomplete-line", WS_INCOMPLETE_LINE, 0, 0 },
    + };
    + 
    + unsigned parse_whitespace_rule(const char *string)
    +@@ ws.c: char *whitespace_error_string(unsigned ws)
    + 			strbuf_addstr(&err, ", ");
    + 		strbuf_addstr(&err, "tab in indent");
    + 	}
    ++	if (ws & WS_INCOMPLETE_LINE) {
    ++		if (err.len)
    ++			strbuf_addstr(&err, ", ");
    ++		strbuf_addstr(&err, "no newline at the end of file");
    ++	}
    + 	return strbuf_detach(&err, NULL);
    + }
    + 
    +
      ## ws.h ##
     @@ ws.h: struct strbuf;
    + #define WS_CR_AT_EOL            (1<<9)
    + #define WS_BLANK_AT_EOF         (1<<10)
    + #define WS_TAB_IN_INDENT        (1<<11)
    ++#define WS_INCOMPLETE_LINE      (1<<12)
    + 
    + #define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
    + #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)
      #define WS_TAB_WIDTH_MASK       ((1<<6)-1)
      
      /* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */
10:  662f15d0b4 ! 10:  7369e77309 apply: check and fix incomplete lines
    @@ Commit message
         what happens when A ends in an incomplete line, for example), and
         text-oriented tools often mishandle such a line.
     
    -    Introduce a new whitespace rule "incomplete-line", which is off by
    -    default for backward compatibility's sake, so that "git apply
    +    Implement checks in "git apply" for incomplete lines, which is off
    +    by default for backward compatibility's sake, so that "git apply
         --whitespace={fix,warn,error}" can notice, warn against, and fix
         them.
     
    @@ Commit message
     
         Signed-off-by: Junio C Hamano <gitster@pobox.com>
     
    + ## apply.c ##
    +@@ apply.c: static void record_ws_error(struct apply_state *state,
    + 	    state->squelch_whitespace_errors < state->whitespace_error)
    + 		return;
    + 
    ++	/*
    ++	 * line[len] for an incomplete line points at the "\n" at the end
    ++	 * of patch input line, so "%.*s" would drop the last letter on line;
    ++	 * compensate for it.
    ++	 */
    ++	if (result & WS_INCOMPLETE_LINE)
    ++		len++;
    ++
    + 	err = whitespace_error_string(result);
    + 	if (state->apply_verbosity > verbosity_silent)
    + 		fprintf(stderr, "%s:%d: %s.\n%.*s\n",
    +@@ apply.c: static int parse_fragment(struct apply_state *state,
    + 		}
    + 
    + 		/* eat the "\\ No newline..." as well, if exists */
    +-		len += skip_len;
    ++		if (skip_len) {
    ++			len += skip_len;
    ++			state->linenr++;
    ++		}
    + 	}
    + 	if (oldlines || newlines)
    + 		return -1;
    +
      ## t/t4124-apply-ws-rule.sh ##
     @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excluded paths' '
      	git apply --include=used --stat --whitespace=error <patch
    @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl
     +	git apply --whitespace=error <patch &&
     +	test_cmp sample2-i target &&
     +
    ++	cat sample-i >target &&
    ++	git apply --whitespace=error --check <patch 2>error &&
    ++	test_cmp sample-i target &&
    ++	test_must_be_empty error &&
    ++
     +	cat sample2-i >target &&
     +	git apply --whitespace=error -R <patch &&
    -+	test_cmp sample-i target
    ++	test_cmp sample-i target &&
    ++
    ++	cat sample2-i >target &&
    ++	git apply -R --whitespace=error --check <patch 2>error &&
    ++	test_cmp sample2-i target &&
    ++	test_must_be_empty error
     +'
     +
     +test_expect_success 'last line made incomplete (error)' '
    @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl
     +	test_must_fail git apply --whitespace=error <patch 2>error &&
     +	test_grep "no newline" error &&
     +
    ++	cat sample >target &&
    ++	test_must_fail git apply --whitespace=error --check <patch 2>actual &&
    ++	test_cmp sample target &&
    ++	cat >expect <<-\EOF &&
    ++	<stdin>:10: no newline at the end of file.
    ++	6
    ++	error: 1 line adds whitespace errors.
    ++	EOF
    ++	test_cmp expect actual &&
    ++
     +	cat sample-i >target &&
     +	git apply --whitespace=error -R <patch &&
     +	test_cmp sample target &&
     +
    ++	cat sample-i >target &&
    ++	git apply --whitespace=error --check -R <patch 2>error &&
    ++	test_cmp sample-i target &&
    ++	test_must_be_empty error &&
    ++
     +	cat sample >target &&
     +	git apply --whitespace=fix <patch &&
     +	test_cmp sample target
    @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl
     +	git apply --whitespace=error <patch &&
     +	test_cmp sample target &&
     +
    ++	cat sample-i >target &&
    ++	git apply --whitespace=error --check <patch 2>error &&
    ++	test_cmp sample-i target &&
    ++	test_must_be_empty error &&
    ++
     +	cat sample >target &&
     +	test_must_fail git apply --whitespace=error -R <patch 2>error &&
     +	test_grep "no newline" error &&
     +
     +	cat sample >target &&
    ++	test_must_fail git apply --whitespace=error --check -R <patch 2>actual &&
    ++	test_cmp sample target &&
    ++	cat >expect <<-\EOF &&
    ++	<stdin>:9: no newline at the end of file.
    ++	6
    ++	error: 1 line adds whitespace errors.
    ++	EOF
    ++	test_cmp expect actual &&
    ++
    ++	cat sample >target &&
     +	git apply --whitespace=fix -R <patch &&
     +	test_cmp sample target
     +'
    @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl
     +	git apply --whitespace=error <patch &&
     +	test_cmp sample3 target &&
     +
    ++	cat sample-i >target &&
    ++	git apply --whitespace=error --check <patch 2>error &&
    ++	test_cmp sample-i target &&
    ++	test_must_be_empty error &&
    ++
     +	cat sample3 >target &&
     +	test_must_fail git apply --whitespace=error -R <patch 2>error &&
     +	test_grep "no newline" error &&
     +
     +	cat sample3 >target &&
    ++	test_must_fail git apply --whitespace=error -R --check <patch 2>actual &&
    ++	test_cmp sample3 target &&
    ++	cat >expect <<-\EOF &&
    ++	<stdin>:9: no newline at the end of file.
    ++	6
    ++	error: 1 line adds whitespace errors.
    ++	EOF
    ++	test_cmp expect actual &&
    ++
    ++	cat sample3 >target &&
     +	git apply --whitespace=fix -R <patch &&
     +	test_cmp sample target
     +'
    @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl
     +	test_must_fail git apply --whitespace=error <patch 2>error &&
     +	test_grep "no newline" error &&
     +
    ++	cat sample-i >target &&
    ++	test_must_fail git apply --whitespace=error --check <patch 2>actual &&
    ++	test_cmp sample-i target &&
    ++	cat >expect <<-\EOF &&
    ++	<stdin>:11: no newline at the end of file.
    ++	7
    ++	error: 1 line adds whitespace errors.
    ++	EOF
    ++	test_cmp expect actual &&
    ++
     +	cat sample3-i >target &&
     +	test_must_fail git apply --whitespace=error -R <patch 2>error &&
     +	test_grep "no newline" error &&
     +
    ++	cat sample3-i >target &&
    ++	test_must_fail git apply --whitespace=error --check -R <patch 2>actual &&
    ++	test_cmp sample3-i target &&
    ++	cat >expect <<-\EOF &&
    ++	<stdin>:9: no newline at the end of file.
    ++	6
    ++	error: 1 line adds whitespace errors.
    ++	EOF
    ++	test_cmp expect actual &&
    ++
     +	cat sample-i >target &&
     +	git apply --whitespace=fix <patch &&
     +	test_cmp sample3 target &&
    @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl
      test_done
     
      ## ws.c ##
    -@@ ws.c: static struct whitespace_rule {
    - 	{ "blank-at-eol", WS_BLANK_AT_EOL, 0 },
    - 	{ "blank-at-eof", WS_BLANK_AT_EOF, 0 },
    - 	{ "tab-in-indent", WS_TAB_IN_INDENT, 0, 1 },
    -+	{ "incomplete-line", WS_INCOMPLETE_LINE, 0, 0 },
    - };
    - 
    - unsigned parse_whitespace_rule(const char *string)
    -@@ ws.c: char *whitespace_error_string(unsigned ws)
    - 			strbuf_addstr(&err, ", ");
    - 		strbuf_addstr(&err, "tab in indent");
    - 	}
    -+	if (ws & WS_INCOMPLETE_LINE) {
    -+		if (err.len)
    -+			strbuf_addstr(&err, ", ");
    -+		strbuf_addstr(&err, "no newline at the end of file");
    -+	}
    - 	return strbuf_detach(&err, NULL);
    - }
    - 
     @@ ws.c: static unsigned ws_check_emit_1(const char *line, int len, unsigned ws_rule,
      	if (trailing_whitespace == -1)
      		trailing_whitespace = len;
    @@ ws.c: void ws_fix_copy(struct strbuf *dst, const char *src, int len, unsigned ws
      	/*
      	 * Strip trailing whitespace
      	 */
    -
    - ## ws.h ##
    -@@ ws.h: struct strbuf;
    - #define WS_CR_AT_EOL            (1<<9)
    - #define WS_BLANK_AT_EOF         (1<<10)
    - #define WS_TAB_IN_INDENT        (1<<11)
    -+#define WS_INCOMPLETE_LINE      (1<<12)
    - 
    - #define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
    - #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)
11:  36de2ac901 = 11:  17c2fa50a7 diff: highlight and error out on incomplete lines
12:  e82056bf55 = 12:  73af29fba7 attr: enable incomplete-line whitespace error for this project
-- 
2.52.0-rc0-105-gc08128fbb6
Junio C Hamano· Nov 5, 2025, 21:30 UTC · re: Junio C Hamano · lore

[PATCH v2 01/12] whitespace: correct bit assignment comments

A comment in diff.c claimed that bits up to 12th (counting from 0th) are whitespace rules, and 13th thru 15th are for new/old/context, but it turns out it was miscounting. Correct them, and clarify where the whitespace rule bits come from in the comment. Extend bit assignment comments to cover bits used for color-moved, which weren't described.

Also update the way these bit constants are defined to use (1 << N) notation, instead of octal constants, as it tends to make it easier to notice a breakage like this.

Sprinkle a few blank lines between logically distinct groups of CPP macro definitions to make them easier to read.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c |  7 +++++--
 diff.h |  6 +++---
 ws.h   | 25 ++++++++++++++-----------
 3 files changed, 22 insertions(+), 16 deletions(-)
Show changes to 3 files +22 −16

diff.c, diff.h, ws.h

diff --git a/diff.c b/diff.c
index a74e701806..74261b332a 100644
--- a/diff.c
+++ b/diff.c
@@ -801,16 +801,19 @@ enum diff_symbol {
 	DIFF_SYMBOL_CONTEXT_MARKER,
 	DIFF_SYMBOL_SEPARATOR
 };
+
 /*
  * Flags for content lines:
- * 0..12 are whitespace rules
- * 13-15 are WSEH_NEW | WSEH_OLD | WSEH_CONTEXT
+ * 0..11 are whitespace rules (see ws.h)
+ * 12..14 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD
  * 16 is marking if the line is blank at EOF
+ * 17..19 are used for color-moved.
  */
 #define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF	(1<<16)
 #define DIFF_SYMBOL_MOVED_LINE			(1<<17)
 #define DIFF_SYMBOL_MOVED_LINE_ALT		(1<<18)
 #define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING	(1<<19)
+
 #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)
 
 /*
diff --git a/diff.h b/diff.h
index 2fa256c3ef..cbd355cf50 100644
--- a/diff.h
+++ b/diff.h
@@ -331,9 +331,9 @@ struct diff_options {
 
 	int ita_invisible_in_index;
 /* white-space error highlighting */
-#define WSEH_NEW (1<<12)
-#define WSEH_CONTEXT (1<<13)
-#define WSEH_OLD (1<<14)
+#define WSEH_NEW        (1<<12)
+#define WSEH_CONTEXT    (1<<13)
+#define WSEH_OLD        (1<<14)
 	unsigned ws_error_highlight;
 	const char *prefix;
 	int prefix_length;
diff --git a/ws.h b/ws.h
index 5ba676c559..23708efb73 100644
--- a/ws.h
+++ b/ws.h
@@ -7,19 +7,22 @@ struct strbuf;
 /*
  * whitespace rules.
  * used by both diff and apply
- * last two digits are tab width
+ * last two octal-digits are tab width (we support only up to 63).
  */
-#define WS_BLANK_AT_EOL         0100
-#define WS_SPACE_BEFORE_TAB     0200
-#define WS_INDENT_WITH_NON_TAB  0400
-#define WS_CR_AT_EOL           01000
-#define WS_BLANK_AT_EOF        02000
-#define WS_TAB_IN_INDENT       04000
-#define WS_TRAILING_SPACE      (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
+#define WS_BLANK_AT_EOL         (1<<6)
+#define WS_SPACE_BEFORE_TAB     (1<<7)
+#define WS_INDENT_WITH_NON_TAB  (1<<8)
+#define WS_CR_AT_EOL            (1<<9)
+#define WS_BLANK_AT_EOF         (1<<10)
+#define WS_TAB_IN_INDENT        (1<<11)
+
+#define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
 #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)
-#define WS_TAB_WIDTH_MASK        077
-/* All WS_* -- when extended, adapt diff.c emit_symbol */
-#define WS_RULE_MASK           07777
+#define WS_TAB_WIDTH_MASK       ((1<<6)-1)
+
+/* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */
+#define WS_RULE_MASK            ((1<<12)-1)
+
 extern unsigned whitespace_rule_cfg;
 unsigned whitespace_rule(struct index_state *, const char *);
 unsigned parse_whitespace_rule(const char *);
-- 
2.52.0-rc0-105-gc08128fbb6
Junio C Hamano· Nov 5, 2025, 21:30 UTC · re: Junio C Hamano · lore

[PATCH v2 02/12] diff: emit_line_ws_markup() if/else style fix

Apply the simple rule: if you need {} in one arm of the if/else if/else... cascade, have {} in all of them.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
Show changes to diff.c +4 −4
diff --git a/diff.c b/diff.c
index 74261b332a..9a24a0791c 100644
--- a/diff.c
+++ b/diff.c
@@ -1327,14 +1327,14 @@ static void emit_line_ws_markup(struct diff_options *o,
 			ws = NULL;
 	}
 
-	if (!ws && !set_sign)
+	if (!ws && !set_sign) {
 		emit_line_0(o, set, NULL, 0, reset, sign, line, len);
-	else if (!ws) {
+	} else if (!ws) {
 		emit_line_0(o, set_sign, set, !!set_sign, reset, sign, line, len);
-	} else if (blank_at_eof)
+	} else if (blank_at_eof) {
 		/* Blank line at EOF - paint '+' as well */
 		emit_line_0(o, ws, NULL, 0, reset, sign, line, len);
-	else {
+	} else {
 		/* Emit just the prefix, then the rest. */
 		emit_line_0(o, set_sign ? set_sign : set, NULL, !!set_sign, reset,
 			    sign, "", 0);
-- 
2.52.0-rc0-105-gc08128fbb6
Junio C Hamano· Nov 5, 2025, 21:30 UTC · re: Junio C Hamano · lore

[PATCH v2 03/12] diff: correct suppress_blank_empty hack

The suppress-blank-empty feature abused the CONTEXT_INCOMPLETE symbol that was meant to be used only for "\ No newline at the end of file" code path.

The intent of the feature was to turn a context line we receive from xdiff machinery (which always uses ' ' for context lines, even an empty one) and spit it out as a truly empty line.

Perform such a conversion very locally at where a line from xdiff that begins with ' ' is handled for output; there are many checks before the control reaches such place that checks the first letter of the diff output line to see if it is a context line, and having to check for '\n' and treat it as a special case is error prone.

In order to catch similar hacks in the future, make sure the code path that is meant for "\ No newline" case checks the first byte is indeed a backslash.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 27 +++++++++++----------------
 1 file changed, 11 insertions(+), 16 deletions(-)
Show changes to diff.c +11 −16
diff --git a/diff.c b/diff.c
index 9a24a0791c..b9ef8550cc 100644
--- a/diff.c
+++ b/diff.c
@@ -1321,6 +1321,11 @@ static void emit_line_ws_markup(struct diff_options *o,
 	const char *ws = NULL;
 	int sign = o->output_indicators[sign_index];
 
+	if (diff_suppress_blank_empty &&
+	    sign_index == OUTPUT_INDICATOR_CONTEXT &&
+	    len == 1 && line[0] == '\n')
+		sign = 0;
+
 	if (o->ws_error_highlight & ws_rule) {
 		ws = diff_get_color_opt(o, DIFF_WHITESPACE);
 		if (!*ws)
@@ -1498,15 +1503,9 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
 	case DIFF_SYMBOL_WORDS:
 		context = diff_get_color_opt(o, DIFF_CONTEXT);
 		reset = diff_get_color_opt(o, DIFF_RESET);
-		/*
-		 * Skip the prefix character, if any.  With
-		 * diff_suppress_blank_empty, there may be
-		 * none.
-		 */
-		if (line[0] != '\n') {
-			line++;
-			len--;
-		}
+
+		/* Skip the prefix character */
+		line++; len--;
 		emit_line(o, context, reset, line, len);
 		break;
 	case DIFF_SYMBOL_FILEPAIR_PLUS:
@@ -2375,12 +2374,6 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 		ecbdata->label_path[0] = ecbdata->label_path[1] = NULL;
 	}
 
-	if (diff_suppress_blank_empty
-	    && len == 2 && line[0] == ' ' && line[1] == '\n') {
-		line[0] = '\n';
-		len = 1;
-	}
-
 	if (line[0] == '@') {
 		if (ecbdata->diff_words)
 			diff_words_flush(ecbdata);
@@ -2431,12 +2424,14 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 		ecbdata->lno_in_preimage++;
 		emit_context_line(ecbdata, line + 1, len - 1);
 		break;
-	default:
+	case '\\':
 		/* incomplete line at the end */
 		ecbdata->lno_in_preimage++;
 		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
 				 line, len, 0);
 		break;
+	default:
+		BUG("fn_out_consume: unknown line '%s'", line);
 	}
 	return 0;
 }
-- 
2.52.0-rc0-105-gc08128fbb6
Junio C Hamano· Nov 5, 2025, 21:30 UTC · re: Junio C Hamano · lore

[PATCH v2 04/12] diff: fix incorrect counting of line numbers

The "\ No newline at the end of the file" can come after any of the "-" (deleted preimage line), " " (unchanged line), or "+" (added postimage line). Incrementing only the preimage line number upon seeing it does not make any sense.

We can keep track of what the previous line was, and increment lno_in_{pre,post}image variables properly, like this patch does. I do not think it matters, as these numbers are used only to compare them with blank_at_eof_in_{pre,post}image to issue the warning every time we see an added line, but by definition, after we see "\ No newline at the end of the file" for an added line, we will not see an added line for the file.

Keeping track of what the last line was (in other words, "is it that the file used to end in an incomplete line? The file ends in an incomplete line after the change? Both the file before and after the change ends in an incomplete line that did not change?") will be independently useful.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)
Show changes to diff.c +17 −1
diff --git a/diff.c b/diff.c
index b9ef8550cc..e73320dfb1 100644
--- a/diff.c
+++ b/diff.c
@@ -601,6 +601,7 @@ struct emit_callback {
 	int blank_at_eof_in_postimage;
 	int lno_in_preimage;
 	int lno_in_postimage;
+	int last_line_kind;
 	const char **label_path;
 	struct diff_words_data *diff_words;
 	struct diff_options *opt;
@@ -2426,13 +2427,28 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 		break;
 	case '\\':
 		/* incomplete line at the end */
-		ecbdata->lno_in_preimage++;
+		switch (ecbdata->last_line_kind) {
+		case '+':
+			ecbdata->lno_in_postimage++;
+			break;
+		case '-':
+			ecbdata->lno_in_preimage++;
+			break;
+		case ' ':
+			ecbdata->lno_in_preimage++;
+			ecbdata->lno_in_postimage++;
+			break;
+		default:
+			BUG("fn_out_consume: '\\No newline' after unknown line (%c)",
+			    ecbdata->last_line_kind);
+		}
 		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
 				 line, len, 0);
 		break;
 	default:
 		BUG("fn_out_consume: unknown line '%s'", line);
 	}
+	ecbdata->last_line_kind = line[0];
 	return 0;
 }
 
-- 
2.52.0-rc0-105-gc08128fbb6
Junio C Hamano· Nov 5, 2025, 21:30 UTC · re: Junio C Hamano · lore

[PATCH v2 05/12] diff: refactor output of incomplete line

Create a helper function that reacts to "\ No newline at the end of file" in preparation for unifying the incomplete line handling in the code path that handles xdiff output and the code path that bypasses xdiff and produces complete rewrite patch.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)
Show changes to diff.c +12 −2
diff --git a/diff.c b/diff.c
index e73320dfb1..d388d318e4 100644
--- a/diff.c
+++ b/diff.c
@@ -1379,6 +1379,10 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
 		emit_line(o, "", "", line, len);
 		break;
 	case DIFF_SYMBOL_CONTEXT_INCOMPLETE:
+		set = diff_get_color_opt(o, DIFF_CONTEXT);
+		reset = diff_get_color_opt(o, DIFF_RESET);
+		emit_line(o, set, reset, line, len);
+		break;
 	case DIFF_SYMBOL_CONTEXT_MARKER:
 		context = diff_get_color_opt(o, DIFF_CONTEXT);
 		reset = diff_get_color_opt(o, DIFF_RESET);
@@ -1668,6 +1672,13 @@ static void emit_context_line(struct emit_callback *ecbdata,
 	emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT, line, len, flags);
 }
 
+static void emit_incomplete_line(struct emit_callback *ecbdata,
+				 const char *line, int len)
+{
+	emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
+			 line, len, 0);
+}
+
 static void emit_hunk_header(struct emit_callback *ecbdata,
 			     const char *line, int len)
 {
@@ -2442,8 +2453,7 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 			BUG("fn_out_consume: '\\No newline' after unknown line (%c)",
 			    ecbdata->last_line_kind);
 		}
-		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
-				 line, len, 0);
+		emit_incomplete_line(ecbdata, line, len);
 		break;
 	default:
 		BUG("fn_out_consume: unknown line '%s'", line);
-- 
2.52.0-rc0-105-gc08128fbb6
Patrick Steinhardt· Nov 10, 2025, 10:06 UTC · re: Junio C Hamano · lore

Re: [PATCH v2 05/12] diff: refactor output of incomplete line

On Wed, Nov 05, 2025 at 01:30:45PM -0800, Junio C Hamano wrote:
Show 25 quoted lines
> Create a helper function that reacts to "\ No newline at the end of
> file" in preparation for unifying the incomplete line handling in
> the code path that handles xdiff output and the code path that
> bypasses xdiff and produces complete rewrite patch.
> 
> Signed-off-by: Junio C Hamano <gitster@pobox.com>
> ---
>  diff.c | 14 ++++++++++++--
>  1 file changed, 12 insertions(+), 2 deletions(-)
> 
> diff --git a/diff.c b/diff.c
> index e73320dfb1..d388d318e4 100644
> --- a/diff.c
> +++ b/diff.c
> @@ -1379,6 +1379,10 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
>  		emit_line(o, "", "", line, len);
>  		break;
>  	case DIFF_SYMBOL_CONTEXT_INCOMPLETE:
> +		set = diff_get_color_opt(o, DIFF_CONTEXT);
> +		reset = diff_get_color_opt(o, DIFF_RESET);
> +		emit_line(o, set, reset, line, len);
> +		break;
>  	case DIFF_SYMBOL_CONTEXT_MARKER:
>  		context = diff_get_color_opt(o, DIFF_CONTEXT);
>  		reset = diff_get_color_opt(o, DIFF_RESET);

I found it a bit confusing that we use `set`/`reset` here instead of `context`/`reset` as before. It doesn't make any difference as these are local variables anyway, but it might make sense to explain why you chose to use different variables.

Honestly, this whole hunk is somewhat confusing in the first place. It doesn't seem to connect with the description in any way, as it's a no-op change and we don't even use the newly introduced function.

Patrick
Junio C Hamano· Nov 10, 2025, 17:58 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v2 05/12] diff: refactor output of incomplete line

Patrick Steinhardt <ps@pks.im> writes:
Show 13 quoted lines
>>  	case DIFF_SYMBOL_CONTEXT_INCOMPLETE:
>> +		set = diff_get_color_opt(o, DIFF_CONTEXT);
>> +		reset = diff_get_color_opt(o, DIFF_RESET);
>> +		emit_line(o, set, reset, line, len);
>> +		break;
>>  	case DIFF_SYMBOL_CONTEXT_MARKER:
>>  		context = diff_get_color_opt(o, DIFF_CONTEXT);
>>  		reset = diff_get_color_opt(o, DIFF_RESET);
>
> I found it a bit confusing that we use `set`/`reset` here instead of
> `context`/`reset` as before. It doesn't make any difference as these are
> local variables anyway, but it might make sense to explain why you chose
> to use different variables.

Yup, when DIFF_SYMBOL_* stuff was introduced to this codebase, it made the code much harder to reason about X-<, and set/reset/context are used in this switch() statement in apparently "random" ways; no case arm in this switch statement use all three, so there is no need to use these three in the first place.

> Honestly, this whole hunk is somewhat confusing in the first place. It
> doesn't seem to connect with the description in any way, as it's a no-op
> change and we don't even use the newly introduced function.

The hunk in fn_out_consume() is about reacting to "\ No newline", and it calls a new helper function created just for it in the middle hunk. The way that new helper function affects the output will be updated in later steps, but for now, this hunk simply makes sure that the two DIFF_SYMBOL_*s can be treated differently.

Perhaps it would become easier to read if this hunk is removed from this step and a later step that gives a different behaviour for CONTEXT_INCOMPLETE introduce an entirely different code afresh (instead of showing how it updates what we copy here from the context code the original is abusing)?

Junio C Hamano· Nov 5, 2025, 21:30 UTC · re: Junio C Hamano · lore

[PATCH v2 06/12] diff: call emit_callback ecbdata everywhere

Everybody else, except for emit_rewrite_lines(), calls the emit_callback data ecbdata. Make sure we call the same thing by the same name for consistency.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)
Show changes to diff.c +6 −6
diff --git a/diff.c b/diff.c
index d388d318e4..347cd9c6e9 100644
--- a/diff.c
+++ b/diff.c
@@ -1780,7 +1780,7 @@ static void add_line_count(struct strbuf *out, int count)
 	}
 }
 
-static void emit_rewrite_lines(struct emit_callback *ecb,
+static void emit_rewrite_lines(struct emit_callback *ecbdata,
 			       int prefix, const char *data, int size)
 {
 	const char *endp = NULL;
@@ -1791,17 +1791,17 @@ static void emit_rewrite_lines(struct emit_callback *ecb,
 		endp = memchr(data, '\n', size);
 		len = endp ? (endp - data + 1) : size;
 		if (prefix != '+') {
-			ecb->lno_in_preimage++;
-			emit_del_line(ecb, data, len);
+			ecbdata->lno_in_preimage++;
+			emit_del_line(ecbdata, data, len);
 		} else {
-			ecb->lno_in_postimage++;
-			emit_add_line(ecb, data, len);
+			ecbdata->lno_in_postimage++;
+			emit_add_line(ecbdata, data, len);
 		}
 		size -= len;
 		data += len;
 	}
 	if (!endp)
-		emit_diff_symbol(ecb->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
+		emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
 }
 
 static void emit_rewrite_diff(const char *name_a,
-- 
2.52.0-rc0-105-gc08128fbb6
Junio C Hamano· Nov 5, 2025, 21:30 UTC · re: Junio C Hamano · lore

[PATCH v2 07/12] diff: update the way rewrite diff handles incomplete lines

The diff_symbol based output framework uses one DIFF_SYMBOL_* enum value per the kind of output lines of "git diff", which corresponds to one output line from the xdiff machinery used internally. Most notably, DIFF_SYMBOL_PLUS and DIFF_SYMBOL_MINUS that correspond to "+" and "-" lines are designed to always take a complete line, even if the output from xdiff machinery may produce "\ No newline at the end of file" immediately after them.

But this is not true in the rewrite-diff codepath, which completely bypasses the xdiff machinery. Since the code path feeds the bytes directly from the payload to the output routines, the output layer has to deal with an incomplete line with DIFF_SYMBOL_PLUS and DIFF_SYMBOL_MINUS, which never would see an incomplete line in the normal code paths. This lack of final newline is compensated by an ugly hack for a fabricated DIFF_SYMBOL_NO_LF_EOF token to inject an extra newline to the output to simulate output coming from the xdiff machinery.

Revamp the way the complete-rewrite code path feeds the lines to the output layer by treating the last line of the pre/post image when it is an incomplete line specially.

This lets us remove the DIFF_SYMBOL_NO_LF_EOF hack and use the usual DIFF_SYMBOL_CONTEXT_INCOMPLETE code path, which will later learn how to handle whitespace errors.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 33 +++++++++++++++++++--------------
 1 file changed, 19 insertions(+), 14 deletions(-)
Show changes to diff.c +19 −14
diff --git a/diff.c b/diff.c
index 347cd9c6e9..99298720f4 100644
--- a/diff.c
+++ b/diff.c
@@ -797,7 +797,6 @@ enum diff_symbol {
 	DIFF_SYMBOL_CONTEXT_INCOMPLETE,
 	DIFF_SYMBOL_PLUS,
 	DIFF_SYMBOL_MINUS,
-	DIFF_SYMBOL_NO_LF_EOF,
 	DIFF_SYMBOL_CONTEXT_FRAGINFO,
 	DIFF_SYMBOL_CONTEXT_MARKER,
 	DIFF_SYMBOL_SEPARATOR
@@ -1352,7 +1351,6 @@ static void emit_line_ws_markup(struct diff_options *o,
 static void emit_diff_symbol_from_struct(struct diff_options *o,
 					 struct emitted_diff_symbol *eds)
 {
-	static const char *nneof = " No newline at end of file\n";
 	const char *context, *reset, *set, *set_sign, *meta, *fraginfo;
 
 	enum diff_symbol s = eds->s;
@@ -1361,13 +1359,6 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
 	unsigned flags = eds->flags;
 
 	switch (s) {
-	case DIFF_SYMBOL_NO_LF_EOF:
-		context = diff_get_color_opt(o, DIFF_CONTEXT);
-		reset = diff_get_color_opt(o, DIFF_RESET);
-		putc('\n', o->file);
-		emit_line_0(o, context, NULL, 0, reset, '\\',
-			    nneof, strlen(nneof));
-		break;
 	case DIFF_SYMBOL_SUBMODULE_HEADER:
 	case DIFF_SYMBOL_SUBMODULE_ERROR:
 	case DIFF_SYMBOL_SUBMODULE_PIPETHROUGH:
@@ -1786,22 +1777,36 @@ static void emit_rewrite_lines(struct emit_callback *ecbdata,
 	const char *endp = NULL;
 
 	while (0 < size) {
-		int len;
+		int len, plen;
+		char *pdata = NULL;
 
 		endp = memchr(data, '\n', size);
 		len = endp ? (endp - data + 1) : size;
+		plen = len;
+
+		if (!endp) {
+			plen = len + 1;
+			pdata = xmalloc(plen + 2);
+			memcpy(pdata, data, len);
+			pdata[len] = '\n';
+			pdata[len + 1] = '\0';
+		}
 		if (prefix != '+') {
 			ecbdata->lno_in_preimage++;
-			emit_del_line(ecbdata, data, len);
+			emit_del_line(ecbdata, pdata ? pdata : data, plen);
 		} else {
 			ecbdata->lno_in_postimage++;
-			emit_add_line(ecbdata, data, len);
+			emit_add_line(ecbdata, pdata ? pdata : data, plen);
 		}
+		free(pdata);
 		size -= len;
 		data += len;
 	}
-	if (!endp)
-		emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
+	if (!endp) {
+		static const char nneof[] = "\\ No newline at end of file\n";
+		ecbdata->last_line_kind = prefix;
+		emit_incomplete_line(ecbdata, nneof, sizeof(nneof) - 1);
+	}
 }
 
 static void emit_rewrite_diff(const char *name_a,
-- 
2.52.0-rc0-105-gc08128fbb6
Patrick Steinhardt· Nov 10, 2025, 10:06 UTC · re: Junio C Hamano · lore

Re: [PATCH v2 07/12] diff: update the way rewrite diff handles incomplete lines

On Wed, Nov 05, 2025 at 01:30:47PM -0800, Junio C Hamano wrote:
Show 5 quoted lines
> The diff_symbol based output framework uses one DIFF_SYMBOL_* enum
> value per the kind of output lines of "git diff", which corresponds
> to one output line from the xdiff machinery used internally.  Most
> notably, DIFF_SYMBOL_PLUS and DIFF_SYMBOL_MINUS that correspond to
> "+" and "-" lines are designed to always take a complete line, even

"complete line" as in newline-terminated? I only recognized that this is what you meant in the next paragraph, so it might be useful to clarify here already what you mean.

Show 44 quoted lines
> diff --git a/diff.c b/diff.c
> index 347cd9c6e9..99298720f4 100644
> --- a/diff.c
> +++ b/diff.c
> @@ -1786,22 +1777,36 @@ static void emit_rewrite_lines(struct emit_callback *ecbdata,
>  	const char *endp = NULL;
>  
>  	while (0 < size) {
> -		int len;
> +		int len, plen;
> +		char *pdata = NULL;
>  
>  		endp = memchr(data, '\n', size);
>  		len = endp ? (endp - data + 1) : size;
> +		plen = len;
> +
> +		if (!endp) {
> +			plen = len + 1;
> +			pdata = xmalloc(plen + 2);
> +			memcpy(pdata, data, len);
> +			pdata[len] = '\n';
> +			pdata[len + 1] = '\0';
> +		}
>  		if (prefix != '+') {
>  			ecbdata->lno_in_preimage++;
> -			emit_del_line(ecbdata, data, len);
> +			emit_del_line(ecbdata, pdata ? pdata : data, plen);
>  		} else {
>  			ecbdata->lno_in_postimage++;
> -			emit_add_line(ecbdata, data, len);
> +			emit_add_line(ecbdata, pdata ? pdata : data, plen);
>  		}
> +		free(pdata);
>  		size -= len;
>  		data += len;
>  	}
> -	if (!endp)
> -		emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
> +	if (!endp) {
> +		static const char nneof[] = "\\ No newline at end of file\n";
> +		ecbdata->last_line_kind = prefix;
> +		emit_incomplete_line(ecbdata, nneof, sizeof(nneof) - 1);
> +	}
>  }

Okay. I was wondering at first how this would get executed for both pre- and postimage if it's not part of the loop anymore. But this is mostly showing my complete ignorance for the "diff" subsystem, as we end up calling `emit_rewrite_lines()` itself once for each image.

Patrick
Junio C Hamano· Nov 10, 2025, 18:14 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v2 07/12] diff: update the way rewrite diff handles incomplete lines

Patrick Steinhardt <ps@pks.im> writes:
Show 10 quoted lines
> On Wed, Nov 05, 2025 at 01:30:47PM -0800, Junio C Hamano wrote:
>> The diff_symbol based output framework uses one DIFF_SYMBOL_* enum
>> value per the kind of output lines of "git diff", which corresponds
>> to one output line from the xdiff machinery used internally.  Most
>> notably, DIFF_SYMBOL_PLUS and DIFF_SYMBOL_MINUS that correspond to
>> "+" and "-" lines are designed to always take a complete line, even
>
> "complete line" as in newline-terminated? I only recognized that this is
> what you meant in the next paragraph, so it might be useful to clarify
> here already what you mean.

Yes, "incomplete line" is a defined term people can look up in places like POSIX.1 [*] but I do not know of an official word to refer to the opposite. Would it work if I rephrase it to say "...designed to always end in a newline character, even..."?

(https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap03.html#tag_03_172)
Show 24 quoted lines
>
>> diff --git a/diff.c b/diff.c
>> index 347cd9c6e9..99298720f4 100644
>> --- a/diff.c
>> +++ b/diff.c
>> @@ -1786,22 +1777,36 @@ static void emit_rewrite_lines(struct emit_callback *ecbdata,
>>  	const char *endp = NULL;
>>  
>>  	while (0 < size) {
>> ...
>>  	}
>> -	if (!endp)
>> -		emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
>> +	if (!endp) {
>> +		static const char nneof[] = "\\ No newline at end of file\n";
>> +		ecbdata->last_line_kind = prefix;
>> +		emit_incomplete_line(ecbdata, nneof, sizeof(nneof) - 1);
>> +	}
>>  }
>
> Okay. I was wondering at first how this would get executed for both
> pre- and postimage if it's not part of the loop anymore. But this is
> mostly showing my complete ignorance for the "diff" subsystem, as we end
> up calling `emit_rewrite_lines()` itself once for each image.

The idea is to make a "complete rewrite" patch (i.e. what "diff -B" decides that it is more confusing to express the postimage in terms of "here are remaining pieces of the preimage, many lines around here were removed from the preimage and then many new lines are inserted" than "ok, we are removing everything in the preimage and then we are replacing them with these lines to form the postimage".

This function is called twice, once to show a bunch of "-removed" lines for the preimage side, and then again to show a bunch of "+added" lines for the postimage side. The loop iterates over these lines in a single image, and at the end, the last line of the image, whether it is the preimage or the postimage, may not end in a newline, in which case we need to append "\ No newline" after it.

I just realize that emit_incomplete_line() may be a misnomer. It is not used to show the last line in the pre/postimage that was incomplete. The loop gives all lines, even the final incomplete one, as if each of them ended in a newline. What the helper function emit_incomplete_line() does is to show an additional "by the way, the previous line was an incomplete line" marker after the contents of the line gets shown.

Perhaps call it emit_incomplete_line_mark() or something, and it would make it easier to follow what is going on?

Junio C Hamano· Nov 5, 2025, 21:30 UTC · re: Junio C Hamano · lore

[PATCH v2 08/12] apply: revamp the parsing of incomplete lines

A patch file represents the incomplete line at the end of the file with two lines, one that is the usual "context" with " " as the first letter, "added" with "+" as the first letter, or "removed" with "-" as the first letter that shows the content of the line, plus an extra "\ No newline at the end of file" line that comes immediately after it.

Ever since the apply machinery was written, the "git apply" machinery parses "\ No newline at the end of file" line independently, without even knowing what line the incomplete-ness applies to, simply because it does not even remember what the previous line was.

This poses a problem if we want to check and warn on an incomplete line. Revamp the code that parses a fragment, to actually drop the '\n' at the end of the incoming patch file that terminates a line, so that check_whitespace() calls made from the code path actually sees an incomplete as incomplete.

Note that the result of this parsing is not directly used by the code path that applies the patch. apply_one_fragment() function already checks if each of the patch text it handles is followed by a line that begins with a backslash to drop the newline at the end of the current line it is looking at. In a sense, this patch harmonizes the behaviour of the parsing side to what is already done in the application side.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 apply.c | 70 ++++++++++++++++++++++++++++++++++++++++-----------------
 1 file changed, 49 insertions(+), 21 deletions(-)
Show changes to apply.c +49 −21
diff --git a/apply.c b/apply.c
index a2ceb3fb40..2b0f8bdab5 100644
--- a/apply.c
+++ b/apply.c
@@ -1670,6 +1670,35 @@ static void check_old_for_crlf(struct patch *patch, const char *line, int len)
 }
 
 
+/*
+ * Just saw a single line in a fragment.  If it is a part of this hunk
+ * that is a context " ", an added "+", or a removed "-" line, it may
+ * be followed by "\\ No newline..." to signal that the last "\n" on
+ * this line needs to be dropped.  Depending on locale settings when
+ * the patch was produced we don't know what this line would exactly
+ * say. The only thing we do know is that it begins with "\ ".
+ * Checking for 12 is just for sanity check; "\ No newline..." would
+ * be at least that long in any l10n.
+ *
+ * Return 0 if the line we saw is not followed by "\ No newline...",
+ * or length of that line.  The caller will use it to skip over the
+ * "\ No newline..." line.
+ */
+static int adjust_incomplete(const char *line, int len,
+			     unsigned long size)
+{
+	int nextlen;
+
+	if (*line != '\n' && *line != ' ' && *line != '+' && *line != '-')
+		return 0;
+	if (size - len < 12 || memcmp(line + len, "\\ ", 2))
+		return 0;
+	nextlen = linelen(line + len, size - len);
+	if (nextlen < 12)
+		return 0;
+	return nextlen;
+}
+
 /*
  * Parse a unified diff. Note that this really needs to parse each
  * fragment separately, since the only way to know the difference
@@ -1684,6 +1713,7 @@ static int parse_fragment(struct apply_state *state,
 {
 	int added, deleted;
 	int len = linelen(line, size), offset;
+	int skip_len = 0;
 	unsigned long oldlines, newlines;
 	unsigned long leading, trailing;
 
@@ -1710,6 +1740,22 @@ static int parse_fragment(struct apply_state *state,
 		len = linelen(line, size);
 		if (!len || line[len-1] != '\n')
 			return -1;
+
+		/*
+		 * For an incomplete line, skip_len counts the bytes
+		 * on "\\ No newline..." marker line that comes next
+		 * to the current line.
+		 *
+		 * Reduce "len" to drop the newline at the end of
+		 * line[], but add one to "skip_len", which will be
+		 * added back to "len" for the next iteration, to
+		 * compensate.
+		 */
+		skip_len = adjust_incomplete(line, len, size);
+		if (skip_len) {
+			len--;
+			skip_len++;
+		}
 		switch (*line) {
 		default:
 			return -1;
@@ -1745,20 +1791,10 @@ static int parse_fragment(struct apply_state *state,
 			newlines--;
 			trailing = 0;
 			break;
-
-		/*
-		 * We allow "\ No newline at end of file". Depending
-		 * on locale settings when the patch was produced we
-		 * don't know what this line looks like. The only
-		 * thing we do know is that it begins with "\ ".
-		 * Checking for 12 is just for sanity check -- any
-		 * l10n of "\ No newline..." is at least that long.
-		 */
-		case '\\':
-			if (len < 12 || memcmp(line, "\\ ", 2))
-				return -1;
-			break;
 		}
+
+		/* eat the "\\ No newline..." as well, if exists */
+		len += skip_len;
 	}
 	if (oldlines || newlines)
 		return -1;
@@ -1768,14 +1804,6 @@ static int parse_fragment(struct apply_state *state,
 	fragment->leading = leading;
 	fragment->trailing = trailing;
 
-	/*
-	 * If a fragment ends with an incomplete line, we failed to include
-	 * it in the above loop because we hit oldlines == newlines == 0
-	 * before seeing it.
-	 */
-	if (12 < size && !memcmp(line, "\\ ", 2))
-		offset += linelen(line, size);
-
 	patch->lines_added += added;
 	patch->lines_deleted += deleted;
 
-- 
2.52.0-rc0-105-gc08128fbb6
Junio C Hamano· Nov 5, 2025, 21:30 UTC · re: Junio C Hamano · lore

[PATCH v2 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE

Reserve a few more bits in the diff flags word to be used for future whitespace rules. Add WS_INCOMPLETE_LINE without implementing the behaviour (yet).

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/config/core.adoc |  2 ++
 diff.c                         | 16 ++++++++--------
 diff.h                         |  6 +++---
 ws.c                           |  6 ++++++
 ws.h                           |  3 ++-
 5 files changed, 21 insertions(+), 12 deletions(-)
Show changes to 5 files +21 −12

Documentation/config/core.adoc, diff.c, diff.h, ws.c, ws.h

diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc
index e2de270c86..682fb595fb 100644
--- a/Documentation/config/core.adoc
+++ b/Documentation/config/core.adoc
@@ -626,6 +626,8 @@ core.whitespace::
   part of the line terminator, i.e. with it, `trailing-space`
   does not trigger if the character before such a carriage-return
   is not a whitespace (not enabled by default).
+* `incomplete-line` treats the last line of a file that is missing the
+  newline at the end as an error (not enabled by default).
 * `tabwidth=<n>` tells how many character positions a tab occupies; this
   is relevant for `indent-with-non-tab` and when Git fixes `tab-in-indent`
   errors. The default tab width is 8. Allowed values are 1 to 63.
diff --git a/diff.c b/diff.c
index 99298720f4..8d03146aaa 100644
--- a/diff.c
+++ b/diff.c
@@ -804,15 +804,15 @@ enum diff_symbol {
 
 /*
  * Flags for content lines:
- * 0..11 are whitespace rules (see ws.h)
- * 12..14 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD
- * 16 is marking if the line is blank at EOF
- * 17..19 are used for color-moved.
+ * 0..15 are whitespace rules (see ws.h)
+ * 16..18 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD
+ * 19 is marking if the line is blank at EOF
+ * 20..22 are used for color-moved.
  */
-#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF	(1<<16)
-#define DIFF_SYMBOL_MOVED_LINE			(1<<17)
-#define DIFF_SYMBOL_MOVED_LINE_ALT		(1<<18)
-#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING	(1<<19)
+#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF	(1<<19)
+#define DIFF_SYMBOL_MOVED_LINE			(1<<20)
+#define DIFF_SYMBOL_MOVED_LINE_ALT		(1<<21)
+#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING	(1<<22)
 
 #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)
 
diff --git a/diff.h b/diff.h
index cbd355cf50..422658407d 100644
--- a/diff.h
+++ b/diff.h
@@ -331,9 +331,9 @@ struct diff_options {
 
 	int ita_invisible_in_index;
 /* white-space error highlighting */
-#define WSEH_NEW        (1<<12)
-#define WSEH_CONTEXT    (1<<13)
-#define WSEH_OLD        (1<<14)
+#define WSEH_NEW        (1<<16)
+#define WSEH_CONTEXT    (1<<17)
+#define WSEH_OLD        (1<<18)
 	unsigned ws_error_highlight;
 	const char *prefix;
 	int prefix_length;
diff --git a/ws.c b/ws.c
index 70acee3337..34a7b4fad2 100644
--- a/ws.c
+++ b/ws.c
@@ -26,6 +26,7 @@ static struct whitespace_rule {
 	{ "blank-at-eol", WS_BLANK_AT_EOL, 0 },
 	{ "blank-at-eof", WS_BLANK_AT_EOF, 0 },
 	{ "tab-in-indent", WS_TAB_IN_INDENT, 0, 1 },
+	{ "incomplete-line", WS_INCOMPLETE_LINE, 0, 0 },
 };
 
 unsigned parse_whitespace_rule(const char *string)
@@ -139,6 +140,11 @@ char *whitespace_error_string(unsigned ws)
 			strbuf_addstr(&err, ", ");
 		strbuf_addstr(&err, "tab in indent");
 	}
+	if (ws & WS_INCOMPLETE_LINE) {
+		if (err.len)
+			strbuf_addstr(&err, ", ");
+		strbuf_addstr(&err, "no newline at the end of file");
+	}
 	return strbuf_detach(&err, NULL);
 }
 
diff --git a/ws.h b/ws.h
index 23708efb73..06d5cb73f8 100644
--- a/ws.h
+++ b/ws.h
@@ -15,13 +15,14 @@ struct strbuf;
 #define WS_CR_AT_EOL            (1<<9)
 #define WS_BLANK_AT_EOF         (1<<10)
 #define WS_TAB_IN_INDENT        (1<<11)
+#define WS_INCOMPLETE_LINE      (1<<12)
 
 #define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
 #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)
 #define WS_TAB_WIDTH_MASK       ((1<<6)-1)
 
 /* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */
-#define WS_RULE_MASK            ((1<<12)-1)
+#define WS_RULE_MASK            ((1<<16)-1)
 
 extern unsigned whitespace_rule_cfg;
 unsigned whitespace_rule(struct index_state *, const char *);
-- 
2.52.0-rc0-105-gc08128fbb6
Junio C Hamano· Nov 5, 2025, 21:30 UTC · re: Junio C Hamano · lore

[PATCH v2 10/12] apply: check and fix incomplete lines

The final line of a file that lacks the terminating newline at its end is called an incomplete line. In general they are frowned upon for many reasons (imagine concatenating two files with "cat A B" and what happens when A ends in an incomplete line, for example), and text-oriented tools often mishandle such a line.

Implement checks in "git apply" for incomplete lines, which is off by default for backward compatibility's sake, so that "git apply --whitespace={fix,warn,error}" can notice, warn against, and fix them.

As one of the new test shows, if you modify contents on an incomplete line in the original and leave the resulting line incomplete, it is still considered a whitespace error, the reasoning being that "you'd better fix it while at it if you are making a change on an incomplete line anyway", which may controversial.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 apply.c                  |  13 ++-
 t/t4124-apply-ws-rule.sh | 187 +++++++++++++++++++++++++++++++++++++++
 ws.c                     |  14 +++
 3 files changed, 213 insertions(+), 1 deletion(-)
Show changes to 3 files +213 −1

apply.c, t/t4124-apply-ws-rule.sh, ws.c

diff --git a/apply.c b/apply.c
index 2b0f8bdab5..c9fb45247d 100644
--- a/apply.c
+++ b/apply.c
@@ -1640,6 +1640,14 @@ static void record_ws_error(struct apply_state *state,
 	    state->squelch_whitespace_errors < state->whitespace_error)
 		return;
 
+	/*
+	 * line[len] for an incomplete line points at the "\n" at the end
+	 * of patch input line, so "%.*s" would drop the last letter on line;
+	 * compensate for it.
+	 */
+	if (result & WS_INCOMPLETE_LINE)
+		len++;
+
 	err = whitespace_error_string(result);
 	if (state->apply_verbosity > verbosity_silent)
 		fprintf(stderr, "%s:%d: %s.\n%.*s\n",
@@ -1794,7 +1802,10 @@ static int parse_fragment(struct apply_state *state,
 		}
 
 		/* eat the "\\ No newline..." as well, if exists */
-		len += skip_len;
+		if (skip_len) {
+			len += skip_len;
+			state->linenr++;
+		}
 	}
 	if (oldlines || newlines)
 		return -1;
diff --git a/t/t4124-apply-ws-rule.sh b/t/t4124-apply-ws-rule.sh
index 485c7d2d12..115a0f8579 100755
--- a/t/t4124-apply-ws-rule.sh
+++ b/t/t4124-apply-ws-rule.sh
@@ -556,4 +556,191 @@ test_expect_success 'whitespace check skipped for excluded paths' '
 	git apply --include=used --stat --whitespace=error <patch
 '
 
+test_expect_success 'check incomplete lines (setup)' '
+	rm -f .gitattributes &&
+	git config core.whitespace incomplete-line
+'
+
+test_expect_success 'incomplete context line (not an error)' '
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	(test_write_lines 1 2 3 0 5 && printf 6) >sample2-i &&
+	cat sample-i >target &&
+	git add target &&
+	cat sample2-i >target &&
+	git diff-files -p target >patch &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error <patch &&
+	test_cmp sample2-i target &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error --check <patch 2>error &&
+	test_cmp sample-i target &&
+	test_must_be_empty error &&
+
+	cat sample2-i >target &&
+	git apply --whitespace=error -R <patch &&
+	test_cmp sample-i target &&
+
+	cat sample2-i >target &&
+	git apply -R --whitespace=error --check <patch 2>error &&
+	test_cmp sample2-i target &&
+	test_must_be_empty error
+'
+
+test_expect_success 'last line made incomplete (error)' '
+	test_write_lines 1 2 3 4 5 6 >sample &&
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	cat sample >target &&
+	git add target &&
+	cat sample-i >target &&
+	git diff-files -p target >patch &&
+
+	cat sample >target &&
+	test_must_fail git apply --whitespace=error <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample >target &&
+	test_must_fail git apply --whitespace=error --check <patch 2>actual &&
+	test_cmp sample target &&
+	cat >expect <<-\EOF &&
+	<stdin>:10: no newline at the end of file.
+	6
+	error: 1 line adds whitespace errors.
+	EOF
+	test_cmp expect actual &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error -R <patch &&
+	test_cmp sample target &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error --check -R <patch 2>error &&
+	test_cmp sample-i target &&
+	test_must_be_empty error &&
+
+	cat sample >target &&
+	git apply --whitespace=fix <patch &&
+	test_cmp sample target
+'
+
+test_expect_success 'incomplete line removed at the end (not an error)' '
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	test_write_lines 1 2 3 4 5 6 >sample &&
+	cat sample-i >target &&
+	git add target &&
+	cat sample >target &&
+	git diff-files -p target >patch &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error <patch &&
+	test_cmp sample target &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error --check <patch 2>error &&
+	test_cmp sample-i target &&
+	test_must_be_empty error &&
+
+	cat sample >target &&
+	test_must_fail git apply --whitespace=error -R <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample >target &&
+	test_must_fail git apply --whitespace=error --check -R <patch 2>actual &&
+	test_cmp sample target &&
+	cat >expect <<-\EOF &&
+	<stdin>:9: no newline at the end of file.
+	6
+	error: 1 line adds whitespace errors.
+	EOF
+	test_cmp expect actual &&
+
+	cat sample >target &&
+	git apply --whitespace=fix -R <patch &&
+	test_cmp sample target
+'
+
+test_expect_success 'incomplete line corrected at the end (not an error)' '
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	test_write_lines 1 2 3 4 5 7 >sample3 &&
+	cat sample-i >target &&
+	git add target &&
+	cat sample3 >target &&
+	git diff-files -p target >patch &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error <patch &&
+	test_cmp sample3 target &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error --check <patch 2>error &&
+	test_cmp sample-i target &&
+	test_must_be_empty error &&
+
+	cat sample3 >target &&
+	test_must_fail git apply --whitespace=error -R <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample3 >target &&
+	test_must_fail git apply --whitespace=error -R --check <patch 2>actual &&
+	test_cmp sample3 target &&
+	cat >expect <<-\EOF &&
+	<stdin>:9: no newline at the end of file.
+	6
+	error: 1 line adds whitespace errors.
+	EOF
+	test_cmp expect actual &&
+
+	cat sample3 >target &&
+	git apply --whitespace=fix -R <patch &&
+	test_cmp sample target
+'
+
+test_expect_success 'incomplete line modified at the end (error)' '
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	(test_write_lines 1 2 3 4 5 && printf 7) >sample3-i &&
+	test_write_lines 1 2 3 4 5 6 >sample &&
+	test_write_lines 1 2 3 4 5 7 >sample3 &&
+	cat sample-i >target &&
+	git add target &&
+	cat sample3-i >target &&
+	git diff-files -p target >patch &&
+
+	cat sample-i >target &&
+	test_must_fail git apply --whitespace=error <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample-i >target &&
+	test_must_fail git apply --whitespace=error --check <patch 2>actual &&
+	test_cmp sample-i target &&
+	cat >expect <<-\EOF &&
+	<stdin>:11: no newline at the end of file.
+	7
+	error: 1 line adds whitespace errors.
+	EOF
+	test_cmp expect actual &&
+
+	cat sample3-i >target &&
+	test_must_fail git apply --whitespace=error -R <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample3-i >target &&
+	test_must_fail git apply --whitespace=error --check -R <patch 2>actual &&
+	test_cmp sample3-i target &&
+	cat >expect <<-\EOF &&
+	<stdin>:9: no newline at the end of file.
+	6
+	error: 1 line adds whitespace errors.
+	EOF
+	test_cmp expect actual &&
+
+	cat sample-i >target &&
+	git apply --whitespace=fix <patch &&
+	test_cmp sample3 target &&
+
+	cat sample3-i >target &&
+	git apply --whitespace=fix -R <patch &&
+	test_cmp sample target
+'
+
 test_done
diff --git a/ws.c b/ws.c
index 34a7b4fad2..6cc2466c0c 100644
--- a/ws.c
+++ b/ws.c
@@ -186,6 +186,9 @@ static unsigned ws_check_emit_1(const char *line, int len, unsigned ws_rule,
 	if (trailing_whitespace == -1)
 		trailing_whitespace = len;
 
+	if (!trailing_newline && (ws_rule & WS_INCOMPLETE_LINE))
+		result |= WS_INCOMPLETE_LINE;
+
 	/* Check indentation */
 	for (i = 0; i < trailing_whitespace; i++) {
 		if (line[i] == ' ')
@@ -297,6 +300,17 @@ void ws_fix_copy(struct strbuf *dst, const char *src, int len, unsigned ws_rule,
 	int last_space_in_indent = -1;
 	int need_fix_leading_space = 0;
 
+	/*
+	 * Remembering that we need to add '\n' at the end
+	 * is sufficient to fix an incomplete line.
+	 */
+	if (ws_rule & WS_INCOMPLETE_LINE) {
+		if (0 < len && src[len - 1] != '\n') {
+			fixed = 1;
+			add_nl_to_tail = 1;
+		}
+	}
+
 	/*
 	 * Strip trailing whitespace
 	 */
-- 
2.52.0-rc0-105-gc08128fbb6
Junio C Hamano· Nov 5, 2025, 21:30 UTC · re: Junio C Hamano · lore

[PATCH v2 11/12] 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>
---
 diff.c                     | 29 ++++++++++++++++--
 t/t4015-diff-whitespace.sh | 63 +++++++++++++++++++++++++++++++++++---
 2 files changed, 86 insertions(+), 6 deletions(-)
Show changes to 2 files +86 −6

diff.c, t/t4015-diff-whitespace.sh

diff --git a/diff.c b/diff.c
index 8d03146aaa..965b97f7f0 100644
--- a/diff.c
+++ b/diff.c
@@ -1370,7 +1370,11 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
 		emit_line(o, "", "", line, len);
 		break;
 	case DIFF_SYMBOL_CONTEXT_INCOMPLETE:
-		set = diff_get_color_opt(o, DIFF_CONTEXT);
+		if ((flags & WS_INCOMPLETE_LINE) &&
+		    (flags & o->ws_error_highlight))
+			set = diff_get_color_opt(o, DIFF_WHITESPACE);
+		else
+			set = diff_get_color_opt(o, DIFF_CONTEXT);
 		reset = diff_get_color_opt(o, DIFF_RESET);
 		emit_line(o, set, reset, line, len);
 		break;
@@ -1666,8 +1670,14 @@ static void emit_context_line(struct emit_callback *ecbdata,
 static void emit_incomplete_line(struct emit_callback *ecbdata,
 				 const char *line, int len)
 {
+	int last_line_kind = ecbdata->last_line_kind;
+	unsigned flags = (last_line_kind == '+'
+			  ? WSEH_NEW
+			  : last_line_kind == '-'
+			  ? WSEH_OLD
+			  : WSEH_CONTEXT) | ecbdata->ws_rule;
 	emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
-			 line, len, 0);
+			 line, len, flags);
 }
 
 static void emit_hunk_header(struct emit_callback *ecbdata,
@@ -3257,6 +3267,7 @@ struct checkdiff_t {
 	struct diff_options *o;
 	unsigned ws_rule;
 	unsigned status;
+	int last_line_kind;
 };
 
 static int is_conflict_marker(const char *line, int marker_size, unsigned long len)
@@ -3295,6 +3306,7 @@ static void checkdiff_consume_hunk(void *priv,
 static int checkdiff_consume(void *priv, char *line, unsigned long len)
 {
 	struct checkdiff_t *data = priv;
+	int last_line_kind;
 	int marker_size = data->conflict_marker_size;
 	const char *ws = diff_get_color(data->o->use_color, DIFF_WHITESPACE);
 	const char *reset = diff_get_color(data->o->use_color, DIFF_RESET);
@@ -3305,6 +3317,8 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)
 	assert(data->o);
 	line_prefix = diff_line_prefix(data->o);
 
+	last_line_kind = data->last_line_kind;
+	data->last_line_kind = line[0];
 	if (line[0] == '+') {
 		unsigned bad;
 		data->lineno++;
@@ -3327,6 +3341,17 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)
 			      data->o->file, set, reset, ws);
 	} else if (line[0] == ' ') {
 		data->lineno++;
+	} else if (line[0] == '\\') {
+		/* no newline at the end of the line */
+		if ((data->ws_rule & WS_INCOMPLETE_LINE) &&
+		    (last_line_kind == '+')) {
+			unsigned bad = WS_INCOMPLETE_LINE;
+			data->status |= bad;
+			err = whitespace_error_string(bad);
+			fprintf(data->o->file, "%s%s:%d: %s.\n",
+				line_prefix, data->filename, data->lineno, err);
+			free(err);
+		}
 	}
 	return 0;
 }
diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh
index 9de7f73f42..138730cbce 100755
--- a/t/t4015-diff-whitespace.sh
+++ b/t/t4015-diff-whitespace.sh
@@ -43,6 +43,49 @@ do
 	'
 done
 
+test_expect_success "incomplete line in both pre- and post-image context" '
+	(echo foo && echo baz | tr -d "\012") >x &&
+	git add x &&
+	(echo bar && echo baz | tr -d "\012") >x &&
+	git diff x &&
+	git -c core.whitespace=incomplete diff --check x &&
+	git diff -R x &&
+	git -c core.whitespace=incomplete diff -R --check x
+'
+
+test_expect_success "incomplete lines on both pre- and post-image" '
+	# The interpretation taken here is "since you are toucing
+	# the line anyway, you would better fix the incomplete line
+	# while you are at it."  but this is debatable.
+	echo foo | tr -d "\012" >x &&
+	git add x &&
+	echo bar | tr -d "\012" >x &&
+	git diff x &&
+	test_must_fail git -c core.whitespace=incomplete diff --check x &&
+	git diff -R x &&
+	test_must_fail git -c core.whitespace=incomplete diff -R --check x
+'
+
+test_expect_success "fix incomplete line in pre-image" '
+	echo foo | tr -d "\012" >x &&
+	git add x &&
+	echo bar >x &&
+	git diff x &&
+	git -c core.whitespace=incomplete diff --check x &&
+	git diff -R x &&
+	test_must_fail git -c core.whitespace=incomplete diff -R --check x
+'
+
+test_expect_success "new incomplete line in post-image" '
+	echo foo >x &&
+	git add x &&
+	echo bar | tr -d "\012" >x &&
+	git diff x &&
+	test_must_fail git -c core.whitespace=incomplete diff --check x &&
+	git diff -R x &&
+	git -c core.whitespace=incomplete diff -R --check x
+'
+
 test_expect_success "Ray Lehtiniemi's example" '
 	cat <<-\EOF >x &&
 	do {
@@ -1040,7 +1083,8 @@ test_expect_success 'ws-error-highlight test setup' '
 	{
 		echo "0. blank-at-eol " &&
 		echo "1. still-blank-at-eol " &&
-		echo "2. and a new line "
+		echo "2. and a new line " &&
+		printf "3. and more"
 	} >x &&
 	new_hash_x=$(git hash-object x) &&
 	after=$(git rev-parse --short "$new_hash_x") &&
@@ -1050,11 +1094,13 @@ test_expect_success 'ws-error-highlight test setup' '
 	<BOLD>index $before..$after 100644<RESET>
 	<BOLD>--- a/x<RESET>
 	<BOLD>+++ b/x<RESET>
-	<CYAN>@@ -1,2 +1,3 @@<RESET>
+	<CYAN>@@ -1,2 +1,4 @@<RESET>
 	 0. blank-at-eol <RESET>
 	<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>
 	<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>
 	<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>
+	<GREEN>+<RESET><GREEN>3. and more<RESET>
+	<BLUE>\ No newline at end of file<RESET>
 	EOF
 
 	cat >expect.all <<-EOF &&
@@ -1062,11 +1108,13 @@ test_expect_success 'ws-error-highlight test setup' '
 	<BOLD>index $before..$after 100644<RESET>
 	<BOLD>--- a/x<RESET>
 	<BOLD>+++ b/x<RESET>
-	<CYAN>@@ -1,2 +1,3 @@<RESET>
+	<CYAN>@@ -1,2 +1,4 @@<RESET>
 	 <RESET>0. blank-at-eol<RESET><BLUE> <RESET>
 	<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>
 	<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>
 	<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>
+	<GREEN>+<RESET><GREEN>3. and more<RESET>
+	<BLUE>\ No newline at end of file<RESET>
 	EOF
 
 	cat >expect.none <<-EOF
@@ -1074,16 +1122,19 @@ test_expect_success 'ws-error-highlight test setup' '
 	<BOLD>index $before..$after 100644<RESET>
 	<BOLD>--- a/x<RESET>
 	<BOLD>+++ b/x<RESET>
-	<CYAN>@@ -1,2 +1,3 @@<RESET>
+	<CYAN>@@ -1,2 +1,4 @@<RESET>
 	 0. blank-at-eol <RESET>
 	<RED>-1. blank-at-eol <RESET>
 	<GREEN>+1. still-blank-at-eol <RESET>
 	<GREEN>+2. and a new line <RESET>
+	<GREEN>+3. and more<RESET>
+	\ No newline at end of file<RESET>
 	EOF
 
 '
 
 test_expect_success 'test --ws-error-highlight option' '
+	git config core.whitespace blank-at-eol,incomplete-line &&
 
 	git diff --color --ws-error-highlight=default,old >current.raw &&
 	test_decode_color <current.raw >current &&
@@ -1100,6 +1151,7 @@ test_expect_success 'test --ws-error-highlight option' '
 '
 
 test_expect_success 'test diff.wsErrorHighlight config' '
+	git config core.whitespace blank-at-eol,incomplete-line &&
 
 	git -c diff.wsErrorHighlight=default,old diff --color >current.raw &&
 	test_decode_color <current.raw >current &&
@@ -1116,6 +1168,7 @@ test_expect_success 'test diff.wsErrorHighlight config' '
 '
 
 test_expect_success 'option overrides diff.wsErrorHighlight' '
+	git config core.whitespace blank-at-eol,incomplete-line &&
 
 	git -c diff.wsErrorHighlight=none \
 		diff --color --ws-error-highlight=default,old >current.raw &&
@@ -1135,6 +1188,8 @@ test_expect_success 'option overrides diff.wsErrorHighlight' '
 '
 
 test_expect_success 'detect moved code, complete file' '
+	git config core.whitespace blank-at-eol &&
+
 	git reset --hard &&
 	cat <<-\EOF >test.c &&
 	#include<stdio.h>
-- 
2.52.0-rc0-105-gc08128fbb6
Junio C Hamano· Nov 5, 2025, 21:30 UTC · re: Junio C Hamano · lore

[PATCH v2 12/12] attr: enable incomplete-line whitespace error for this project

Now "git diff --check" and "git apply --whitespace=warn/fix" learned incomplete line is a whitespace error, enable them for this project to prevent patches to add new incomplete lines to our sources.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 .gitattributes | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
Show changes to .gitattributes +2 −2
diff --git a/.gitattributes b/.gitattributes
index 32583149c2..0accd23848 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,6 +1,6 @@
 * whitespace=!indent,trail,space
-*.[ch] whitespace=indent,trail,space diff=cpp
-*.sh whitespace=indent,trail,space text eol=lf
+*.[ch] whitespace=indent,trail,space,incomplete diff=cpp
+*.sh whitespace=indent,trail,space,incomplete text eol=lf
 *.perl text eol=lf diff=perl
 *.pl text eof=lf diff=perl
 *.pm text eol=lf diff=perl
-- 
2.52.0-rc0-105-gc08128fbb6
Patrick Steinhardt· Nov 10, 2025, 10:09 UTC · re: Junio C Hamano · lore

Re: [PATCH v2 00/12] Incomplete lines

On Wed, Nov 05, 2025 at 01:30:40PM -0800, Junio C Hamano wrote:
Show 25 quoted lines
> One of the common kind of whitespace errors is to lack the final
> newline at the end of a file, but so far, neither "git diff" or "git
> apply" did anything about them.
> 
> This series introduces "incomplete-line" whitespace error class,
> that you can add to either the core.whitespace configuration
> variable, or the whitespace attribute in your .gitattributes files.
> 
> The class is disabled by default, so the final step enables it for
> our project by defining it in the .gitattributes file.
> 
> The incomplete line marker that is given for a context line is not
> considered an error.  The reasoning is that your preimage did have
> incomplete line, but you did not touch the contents on that
> incomplete line in your patch, so you left the line intact.  It is
> not a new breakage you are responsible for.
> 
> If the incomplete line marker follows a postimage line, on the other
> hand, it means that you added a new line at the end of the file that
> is incomplete *and* that line did not exist in the preimage.  The
> last line of the preimage may have been incomplete already, but then
> you updated the contents on that line, so you could have easily
> fixed the incompleteness of the line while at it.  Either way, you
> are responsible for the incompleteness of the last ine in the
> resulting file.

I've read through the series and left two comments, but my review definitely doesn't count as a "qualified" review. I'm way too oblivious of what's happening in the diff subsystem to really be able to point out any mistakes. So I hope that somebody more familiar with this code will chime in.

That being said, I think that the end goal of this series is quite useful and something that I want to have :)

Thanks!
Patrick
Phillip Wood· Nov 10, 2025, 14:53 UTC · re: Junio C Hamano · lore

Re: [PATCH v2 00/12] Incomplete lines

Hi Junio
On 05/11/2025 21:30, Junio C Hamano wrote:
Show 7 quoted lines
> One of the common kind of whitespace errors is to lack the final
> newline at the end of a file, but so far, neither "git diff" or "git
> apply" did anything about them.
> 
> This series introduces "incomplete-line" whitespace error class,
> that you can add to either the core.whitespace configuration
> variable, or the whitespace attribute in your .gitattributes files.

I've read through all the diff related changes but not the changes to apply. I've left a few comments but the cleanups and the new implementation of the new feature look good to me.

Thanks
Phillip
Show 379 quoted lines
> The class is disabled by default, so the final step enables it for
> our project by defining it in the .gitattributes file.
> 
> The incomplete line marker that is given for a context line is not
> considered an error.  The reasoning is that your preimage did have
> incomplete line, but you did not touch the contents on that
> incomplete line in your patch, so you left the line intact.  It is
> not a new breakage you are responsible for.
> 
> If the incomplete line marker follows a postimage line, on the other
> hand, it means that you added a new line at the end of the file that
> is incomplete *and* that line did not exist in the preimage.  The
> last line of the preimage may have been incomplete already, but then
> you updated the contents on that line, so you could have easily
> fixed the incompleteness of the line while at it.  Either way, you
> are responsible for the incompleteness of the last ine in the
> resulting file.
> 
> The organization of the series is as follows.
> 
>   * The first patch [01/12] is a clean-up we have seen earlier on the
>     list already (https://lore.kernel.org/git/xmqqfrb4hyjl.fsf@gitster.g/).
> 
>   * The patches [02/12] - [08/12] are preliminary clean-up made to
>     both "git diff" and "git apply" machinery.
> 
>   * The patch [09/12] shifts the bit assignment (cleaned-up in
>     [01/12] without changing any values) to make room for new
>     whitespace error class (which was last updated in 2007 IIRC, so
>     the set of whitespace errors surprisingly haven't changed for
>     quite some time), and defines the new "incomplete-line" class.
> 
>   * The patch [10/12] teaches "git apply --whitespace=<mode>" and
>     "git apply --check" about the incomplete-line error class.
> 
>   * The patch [11/12] teaches "git diff [--check]" about the
>     incomplete-line error class.
> 
>   * The final patch [12/12] enables the incomplete-line error class
>     for our project for C source files and shell scripts.  I didn't
>     touch the cover-all * entry.
> 
> Changes in v2:
> 
>   - rolled the definition (but not implementation) of the new
>     "incomplete-line" class into step [09/12] that shifts the bit
>     assignment.  The documentation of core.whitespace has also be
>     updated in this step.
> 
>   - "git apply --check" miscounted line number reported for the
>     incomplete line error, which has been corrected in step [10/12].
> 
>   - t4124-apply-ws-rule.sh has been extended to cover "git apply
>     --check" and the diagnostic output from it in step [10/12].
> 
> Junio C Hamano (12):
>    whitespace: correct bit assignment comments
>    diff: emit_line_ws_markup() if/else style fix
>    diff: correct suppress_blank_empty hack
>    diff: fix incorrect counting of line numbers
>    diff: refactor output of incomplete line
>    diff: call emit_callback ecbdata everywhere
>    diff: update the way rewrite diff handles incomplete lines
>    apply: revamp the parsing of incomplete lines
>    whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE
>    apply: check and fix incomplete lines
>    diff: highlight and error out on incomplete lines
>    attr: enable incomplete-line whitespace error for this project
> 
>   .gitattributes                 |   4 +-
>   Documentation/config/core.adoc |   2 +
>   apply.c                        |  79 ++++++++++----
>   diff.c                         | 148 +++++++++++++++++---------
>   diff.h                         |   6 +-
>   t/t4015-diff-whitespace.sh     |  63 ++++++++++-
>   t/t4124-apply-ws-rule.sh       | 187 +++++++++++++++++++++++++++++++++
>   ws.c                           |  20 ++++
>   ws.h                           |  26 +++--
>   9 files changed, 448 insertions(+), 87 deletions(-)
> 
> Range-diff against v1:
>   1:  4168f28fe7 =  1:  8a493cdea5 whitespace: correct bit assignment comments
>   2:  53b7a010e7 =  2:  a01d99a055 diff: emit_line_ws_markup() if/else style fix
>   3:  d93dd05543 =  3:  e3ea40af19 diff: correct suppress_blank_empty hack
>   4:  5f58400bd7 =  4:  e15e89d3e2 diff: fix incorrect counting of line numbers
>   5:  84c4ca147f =  5:  c007b3d7a7 diff: refactor output of incomplete line
>   6:  55b42a1944 =  6:  0cea57091b diff: call emit_callback ecbdata everywhere
>   7:  6947838d13 =  7:  523196b440 diff: update the way rewrite diff handles incomplete lines
>   8:  63c36c6f70 =  8:  e098932784 apply: revamp the parsing of incomplete lines
>   9:  00b645bb4e !  9:  28538f149f whitespace: allocate a few more bits
>      @@ Metadata
>       Author: Junio C Hamano <gitster@pobox.com>
>       
>        ## Commit message ##
>      -    whitespace: allocate a few more bits
>      +    whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE
>       
>           Reserve a few more bits in the diff flags word to be used for future
>      -    whitespace rules.  No behaviour changes intended.
>      +    whitespace rules.  Add WS_INCOMPLETE_LINE without implementing the
>      +    behaviour (yet).
>       
>           Signed-off-by: Junio C Hamano <gitster@pobox.com>
>       
>      + ## Documentation/config/core.adoc ##
>      +@@ Documentation/config/core.adoc: core.whitespace::
>      +   part of the line terminator, i.e. with it, `trailing-space`
>      +   does not trigger if the character before such a carriage-return
>      +   is not a whitespace (not enabled by default).
>      ++* `incomplete-line` treats the last line of a file that is missing the
>      ++  newline at the end as an error (not enabled by default).
>      + * `tabwidth=<n>` tells how many character positions a tab occupies; this
>      +   is relevant for `indent-with-non-tab` and when Git fixes `tab-in-indent`
>      +   errors. The default tab width is 8. Allowed values are 1 to 63.
>      +
>        ## diff.c ##
>       @@ diff.c: enum diff_symbol {
>        
>      @@ diff.h: struct diff_options {
>        	const char *prefix;
>        	int prefix_length;
>       
>      + ## ws.c ##
>      +@@ ws.c: static struct whitespace_rule {
>      + 	{ "blank-at-eol", WS_BLANK_AT_EOL, 0 },
>      + 	{ "blank-at-eof", WS_BLANK_AT_EOF, 0 },
>      + 	{ "tab-in-indent", WS_TAB_IN_INDENT, 0, 1 },
>      ++	{ "incomplete-line", WS_INCOMPLETE_LINE, 0, 0 },
>      + };
>      +
>      + unsigned parse_whitespace_rule(const char *string)
>      +@@ ws.c: char *whitespace_error_string(unsigned ws)
>      + 			strbuf_addstr(&err, ", ");
>      + 		strbuf_addstr(&err, "tab in indent");
>      + 	}
>      ++	if (ws & WS_INCOMPLETE_LINE) {
>      ++		if (err.len)
>      ++			strbuf_addstr(&err, ", ");
>      ++		strbuf_addstr(&err, "no newline at the end of file");
>      ++	}
>      + 	return strbuf_detach(&err, NULL);
>      + }
>      +
>      +
>        ## ws.h ##
>       @@ ws.h: struct strbuf;
>      + #define WS_CR_AT_EOL            (1<<9)
>      + #define WS_BLANK_AT_EOF         (1<<10)
>      + #define WS_TAB_IN_INDENT        (1<<11)
>      ++#define WS_INCOMPLETE_LINE      (1<<12)
>      +
>      + #define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
>      + #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)
>        #define WS_TAB_WIDTH_MASK       ((1<<6)-1)
>        
>        /* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */
> 10:  662f15d0b4 ! 10:  7369e77309 apply: check and fix incomplete lines
>      @@ Commit message
>           what happens when A ends in an incomplete line, for example), and
>           text-oriented tools often mishandle such a line.
>       
>      -    Introduce a new whitespace rule "incomplete-line", which is off by
>      -    default for backward compatibility's sake, so that "git apply
>      +    Implement checks in "git apply" for incomplete lines, which is off
>      +    by default for backward compatibility's sake, so that "git apply
>           --whitespace={fix,warn,error}" can notice, warn against, and fix
>           them.
>       
>      @@ Commit message
>       
>           Signed-off-by: Junio C Hamano <gitster@pobox.com>
>       
>      + ## apply.c ##
>      +@@ apply.c: static void record_ws_error(struct apply_state *state,
>      + 	    state->squelch_whitespace_errors < state->whitespace_error)
>      + 		return;
>      +
>      ++	/*
>      ++	 * line[len] for an incomplete line points at the "\n" at the end
>      ++	 * of patch input line, so "%.*s" would drop the last letter on line;
>      ++	 * compensate for it.
>      ++	 */
>      ++	if (result & WS_INCOMPLETE_LINE)
>      ++		len++;
>      ++
>      + 	err = whitespace_error_string(result);
>      + 	if (state->apply_verbosity > verbosity_silent)
>      + 		fprintf(stderr, "%s:%d: %s.\n%.*s\n",
>      +@@ apply.c: static int parse_fragment(struct apply_state *state,
>      + 		}
>      +
>      + 		/* eat the "\\ No newline..." as well, if exists */
>      +-		len += skip_len;
>      ++		if (skip_len) {
>      ++			len += skip_len;
>      ++			state->linenr++;
>      ++		}
>      + 	}
>      + 	if (oldlines || newlines)
>      + 		return -1;
>      +
>        ## t/t4124-apply-ws-rule.sh ##
>       @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excluded paths' '
>        	git apply --include=used --stat --whitespace=error <patch
>      @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl
>       +	git apply --whitespace=error <patch &&
>       +	test_cmp sample2-i target &&
>       +
>      ++	cat sample-i >target &&
>      ++	git apply --whitespace=error --check <patch 2>error &&
>      ++	test_cmp sample-i target &&
>      ++	test_must_be_empty error &&
>      ++
>       +	cat sample2-i >target &&
>       +	git apply --whitespace=error -R <patch &&
>      -+	test_cmp sample-i target
>      ++	test_cmp sample-i target &&
>      ++
>      ++	cat sample2-i >target &&
>      ++	git apply -R --whitespace=error --check <patch 2>error &&
>      ++	test_cmp sample2-i target &&
>      ++	test_must_be_empty error
>       +'
>       +
>       +test_expect_success 'last line made incomplete (error)' '
>      @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl
>       +	test_must_fail git apply --whitespace=error <patch 2>error &&
>       +	test_grep "no newline" error &&
>       +
>      ++	cat sample >target &&
>      ++	test_must_fail git apply --whitespace=error --check <patch 2>actual &&
>      ++	test_cmp sample target &&
>      ++	cat >expect <<-\EOF &&
>      ++	<stdin>:10: no newline at the end of file.
>      ++	6
>      ++	error: 1 line adds whitespace errors.
>      ++	EOF
>      ++	test_cmp expect actual &&
>      ++
>       +	cat sample-i >target &&
>       +	git apply --whitespace=error -R <patch &&
>       +	test_cmp sample target &&
>       +
>      ++	cat sample-i >target &&
>      ++	git apply --whitespace=error --check -R <patch 2>error &&
>      ++	test_cmp sample-i target &&
>      ++	test_must_be_empty error &&
>      ++
>       +	cat sample >target &&
>       +	git apply --whitespace=fix <patch &&
>       +	test_cmp sample target
>      @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl
>       +	git apply --whitespace=error <patch &&
>       +	test_cmp sample target &&
>       +
>      ++	cat sample-i >target &&
>      ++	git apply --whitespace=error --check <patch 2>error &&
>      ++	test_cmp sample-i target &&
>      ++	test_must_be_empty error &&
>      ++
>       +	cat sample >target &&
>       +	test_must_fail git apply --whitespace=error -R <patch 2>error &&
>       +	test_grep "no newline" error &&
>       +
>       +	cat sample >target &&
>      ++	test_must_fail git apply --whitespace=error --check -R <patch 2>actual &&
>      ++	test_cmp sample target &&
>      ++	cat >expect <<-\EOF &&
>      ++	<stdin>:9: no newline at the end of file.
>      ++	6
>      ++	error: 1 line adds whitespace errors.
>      ++	EOF
>      ++	test_cmp expect actual &&
>      ++
>      ++	cat sample >target &&
>       +	git apply --whitespace=fix -R <patch &&
>       +	test_cmp sample target
>       +'
>      @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl
>       +	git apply --whitespace=error <patch &&
>       +	test_cmp sample3 target &&
>       +
>      ++	cat sample-i >target &&
>      ++	git apply --whitespace=error --check <patch 2>error &&
>      ++	test_cmp sample-i target &&
>      ++	test_must_be_empty error &&
>      ++
>       +	cat sample3 >target &&
>       +	test_must_fail git apply --whitespace=error -R <patch 2>error &&
>       +	test_grep "no newline" error &&
>       +
>       +	cat sample3 >target &&
>      ++	test_must_fail git apply --whitespace=error -R --check <patch 2>actual &&
>      ++	test_cmp sample3 target &&
>      ++	cat >expect <<-\EOF &&
>      ++	<stdin>:9: no newline at the end of file.
>      ++	6
>      ++	error: 1 line adds whitespace errors.
>      ++	EOF
>      ++	test_cmp expect actual &&
>      ++
>      ++	cat sample3 >target &&
>       +	git apply --whitespace=fix -R <patch &&
>       +	test_cmp sample target
>       +'
>      @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl
>       +	test_must_fail git apply --whitespace=error <patch 2>error &&
>       +	test_grep "no newline" error &&
>       +
>      ++	cat sample-i >target &&
>      ++	test_must_fail git apply --whitespace=error --check <patch 2>actual &&
>      ++	test_cmp sample-i target &&
>      ++	cat >expect <<-\EOF &&
>      ++	<stdin>:11: no newline at the end of file.
>      ++	7
>      ++	error: 1 line adds whitespace errors.
>      ++	EOF
>      ++	test_cmp expect actual &&
>      ++
>       +	cat sample3-i >target &&
>       +	test_must_fail git apply --whitespace=error -R <patch 2>error &&
>       +	test_grep "no newline" error &&
>       +
>      ++	cat sample3-i >target &&
>      ++	test_must_fail git apply --whitespace=error --check -R <patch 2>actual &&
>      ++	test_cmp sample3-i target &&
>      ++	cat >expect <<-\EOF &&
>      ++	<stdin>:9: no newline at the end of file.
>      ++	6
>      ++	error: 1 line adds whitespace errors.
>      ++	EOF
>      ++	test_cmp expect actual &&
>      ++
>       +	cat sample-i >target &&
>       +	git apply --whitespace=fix <patch &&
>       +	test_cmp sample3 target &&
>      @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl
>        test_done
>       
>        ## ws.c ##
>      -@@ ws.c: static struct whitespace_rule {
>      - 	{ "blank-at-eol", WS_BLANK_AT_EOL, 0 },
>      - 	{ "blank-at-eof", WS_BLANK_AT_EOF, 0 },
>      - 	{ "tab-in-indent", WS_TAB_IN_INDENT, 0, 1 },
>      -+	{ "incomplete-line", WS_INCOMPLETE_LINE, 0, 0 },
>      - };
>      -
>      - unsigned parse_whitespace_rule(const char *string)
>      -@@ ws.c: char *whitespace_error_string(unsigned ws)
>      - 			strbuf_addstr(&err, ", ");
>      - 		strbuf_addstr(&err, "tab in indent");
>      - 	}
>      -+	if (ws & WS_INCOMPLETE_LINE) {
>      -+		if (err.len)
>      -+			strbuf_addstr(&err, ", ");
>      -+		strbuf_addstr(&err, "no newline at the end of file");
>      -+	}
>      - 	return strbuf_detach(&err, NULL);
>      - }
>      -
>       @@ ws.c: static unsigned ws_check_emit_1(const char *line, int len, unsigned ws_rule,
>        	if (trailing_whitespace == -1)
>        		trailing_whitespace = len;
>      @@ ws.c: void ws_fix_copy(struct strbuf *dst, const char *src, int len, unsigned ws
>        	/*
>        	 * Strip trailing whitespace
>        	 */
>      -
>      - ## ws.h ##
>      -@@ ws.h: struct strbuf;
>      - #define WS_CR_AT_EOL            (1<<9)
>      - #define WS_BLANK_AT_EOF         (1<<10)
>      - #define WS_TAB_IN_INDENT        (1<<11)
>      -+#define WS_INCOMPLETE_LINE      (1<<12)
>      -
>      - #define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
>      - #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)
> 11:  36de2ac901 = 11:  17c2fa50a7 diff: highlight and error out on incomplete lines
> 12:  e82056bf55 = 12:  73af29fba7 attr: enable incomplete-line whitespace error for this project
Junio C Hamano· Nov 11, 2025, 00:04 UTC · re: Junio C Hamano · lore

[PATCH v3 00/12] Incomplete lines

One of the common kind of whitespace errors is to lack the final newline at the end of a file, but so far, neither "git diff" or "git apply" did anything about them.

This series introduces "incomplete-line" whitespace error class, that you can add to either the core.whitespace configuration variable, or the whitespace attribute in your .gitattributes files.

The class is disabled by default, so the final step enables it for our project by defining it in the .gitattributes file.

The incomplete line marker that is given for a context line is not considered an error. The reasoning is that your preimage did have incomplete line, but you did not touch the contents on that incomplete line in your patch, so you left the line intact. It is not a new breakage you are responsible for.

If the incomplete line marker follows a postimage line, on the other hand, it means that you added a new line at the end of the file that is incomplete *and* that line did not exist in the preimage. The last line of the preimage may have been incomplete already, but then you updated the contents on that line, so you could have easily fixed the incompleteness of the line while at it. Either way, you are responsible for the incompleteness of the last ine in the resulting file.

The organization of the series is as follows.
 * The first patch [01/12] is a clean-up we have seen earlier on the
   list already (https://lore.kernel.org/git/xmqqfrb4hyjl.fsf@gitster.g/).
 * The patches [02/12] - [08/12] are preliminary clean-up made to
   both "git diff" and "git apply" machinery.
 * The patch [09/12] shifts the bit assignment (cleaned-up in
   [01/12] without changing any values) to make room for new
   whitespace error class (which was last updated in 2007 IIRC, so
   the set of whitespace errors surprisingly haven't changed for
   quite some time), and defines the new "incomplete-line" class.
 * The patch [10/12] teaches "git apply --whitespace=<mode>" and
   "git apply --check" about the incomplete-line error class.
 * The patch [11/12] teaches "git diff [--check]" about the
   incomplete-line error class.
 * The final patch [12/12] enables the incomplete-line error class
   for our project for C source files and shell scripts.  I didn't
   touch the cover-all * entry.
Changes in v3:
 - The proposed log message of [PATCH 05/12] explains that it
   semi-duplicates the same code shared in two case arems in
   preparation for later changes.
 - The internal helper function to emit the "\ No newline" marker
   line is now called emit_incomplete_line_marker().
 - Two conditionals in [PATCH 07/12] both of which switched on !endp
   have been consolidated into a single if/else statement.
 - The tests in [PATCH 11/12] checks the output from "diff --check"
   now.
Changes in v2:
 - rolled the definition (but not implementation) of the new
   "incomplete-line" class into step [09/12] that shifts the bit
   assignment.  The documentation of core.whitespace has also be
   updated in this step.
 - "git apply --check" miscounted line number reported for the
   incomplete line error, which has been corrected in step [10/12].
 - t4124-apply-ws-rule.sh has been extended to cover "git apply
   --check" and the diagnostic output from it in step [10/12].
Junio C Hamano (12):
  whitespace: correct bit assignment comments
  diff: emit_line_ws_markup() if/else style fix
  diff: correct suppress_blank_empty hack
  diff: fix incorrect counting of line numbers
  diff: refactor output of incomplete line
  diff: call emit_callback ecbdata everywhere
  diff: update the way rewrite diff handles incomplete lines
  apply: revamp the parsing of incomplete lines
  whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE
  apply: check and fix incomplete lines
  diff: highlight and error out on incomplete lines
  attr: enable incomplete-line whitespace error for this project
 .gitattributes                 |   4 +-
 Documentation/config/core.adoc |   2 +
 apply.c                        |  79 ++++++++++----
 diff.c                         | 152 ++++++++++++++++++---------
 diff.h                         |   6 +-
 t/t4015-diff-whitespace.sh     |  67 +++++++++++-
 t/t4124-apply-ws-rule.sh       | 187 +++++++++++++++++++++++++++++++++
 ws.c                           |  20 ++++
 ws.h                           |  26 +++--
 9 files changed, 455 insertions(+), 88 deletions(-)
Range-diff against v2:
 1:  c045e93ce5 =  1:  c045e93ce5 whitespace: correct bit assignment comments
 2:  0d95d68fb4 =  2:  0d95d68fb4 diff: emit_line_ws_markup() if/else style fix
 3:  c331218334 =  3:  c331218334 diff: correct suppress_blank_empty hack
 4:  be1473fc5a =  4:  be1473fc5a diff: fix incorrect counting of line numbers
 5:  7bcd6efba8 !  5:  9410e4257a diff: refactor output of incomplete line
    @@ Commit message
         the code path that handles xdiff output and the code path that
         bypasses xdiff and produces complete rewrite patch.
     
    +    Currently the output from the DIFF_SYMBOL_CONTEXT_INCOMPLETE case
    +    still (ab)uses the same code as what is used for context lines, but
    +    that would change in a later step where we introduce support for
    +    incomplete line detection.
    +
         Signed-off-by: Junio C Hamano <gitster@pobox.com>
     
      ## diff.c ##
    @@ diff.c: static void emit_context_line(struct emit_callback *ecbdata,
      	emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT, line, len, flags);
      }
      
    -+static void emit_incomplete_line(struct emit_callback *ecbdata,
    -+				 const char *line, int len)
    ++static void emit_incomplete_line_marker(struct emit_callback *ecbdata,
    ++					const char *line, int len)
     +{
     +	emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
     +			 line, len, 0);
    @@ diff.c: static int fn_out_consume(void *priv, char *line, unsigned long len)
      		}
     -		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
     -				 line, len, 0);
    -+		emit_incomplete_line(ecbdata, line, len);
    ++		emit_incomplete_line_marker(ecbdata, line, len);
      		break;
      	default:
      		BUG("fn_out_consume: unknown line '%s'", line);
 6:  1a6f143377 =  6:  cdc6516009 diff: call emit_callback ecbdata everywhere
 7:  dfc810b1d6 !  7:  9acb9b6217 diff: update the way rewrite diff handles incomplete lines
    @@ diff.c: static void emit_rewrite_lines(struct emit_callback *ecbdata,
     +		char *pdata = NULL;
      
      		endp = memchr(data, '\n', size);
    - 		len = endp ? (endp - data + 1) : size;
    -+		plen = len;
    +-		len = endp ? (endp - data + 1) : size;
     +
    -+		if (!endp) {
    ++		if (endp) {
    ++			len = endp - data + 1;
    ++			plen = len;
    ++		} else {
    ++			len = size;
     +			plen = len + 1;
     +			pdata = xmalloc(plen + 2);
     +			memcpy(pdata, data, len);
    @@ diff.c: static void emit_rewrite_lines(struct emit_callback *ecbdata,
     +	if (!endp) {
     +		static const char nneof[] = "\\ No newline at end of file\n";
     +		ecbdata->last_line_kind = prefix;
    -+		emit_incomplete_line(ecbdata, nneof, sizeof(nneof) - 1);
    ++		emit_incomplete_line_marker(ecbdata, nneof, sizeof(nneof) - 1);
     +	}
      }
      
 8:  c66b547f13 =  8:  86c14ee62d apply: revamp the parsing of incomplete lines
 9:  bdc2dbbe4b =  9:  b62d4020e7 whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE
10:  806aa30511 = 10:  081c21b14e apply: check and fix incomplete lines
11:  0cfb6ab295 ! 11:  73182b19a8 diff: highlight and error out on incomplete lines
    @@ diff.c: static void emit_diff_symbol_from_struct(struct diff_options *o,
      		emit_line(o, set, reset, line, len);
      		break;
     @@ diff.c: static void emit_context_line(struct emit_callback *ecbdata,
    - static void emit_incomplete_line(struct emit_callback *ecbdata,
    - 				 const char *line, int len)
    + static void emit_incomplete_line_marker(struct emit_callback *ecbdata,
    + 					const char *line, int len)
      {
     +	int last_line_kind = ecbdata->last_line_kind;
     +	unsigned flags = (last_line_kind == '+'
    @@ t/t4015-diff-whitespace.sh: do
     +'
     +
     +test_expect_success "incomplete lines on both pre- and post-image" '
    -+	# The interpretation taken here is "since you are toucing
    ++	# The interpretation taken here is "since you are touching
     +	# the line anyway, you would better fix the incomplete line
     +	# while you are at it."  but this is debatable.
     +	echo foo | tr -d "\012" >x &&
     +	git add x &&
     +	echo bar | tr -d "\012" >x &&
     +	git diff x &&
    -+	test_must_fail git -c core.whitespace=incomplete diff --check x &&
    ++	test_must_fail git -c core.whitespace=incomplete diff --check x >error &&
    ++	test_grep "no newline at the end of file" error &&
     +	git diff -R x &&
    -+	test_must_fail git -c core.whitespace=incomplete diff -R --check x
    ++	test_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&
    ++	test_grep "no newline at the end of file" error
     +'
     +
     +test_expect_success "fix incomplete line in pre-image" '
    @@ t/t4015-diff-whitespace.sh: do
     +	git diff x &&
     +	git -c core.whitespace=incomplete diff --check x &&
     +	git diff -R x &&
    -+	test_must_fail git -c core.whitespace=incomplete diff -R --check x
    ++	test_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&
    ++	test_grep "no newline at the end of file" error
     +'
     +
     +test_expect_success "new incomplete line in post-image" '
    @@ t/t4015-diff-whitespace.sh: do
     +	git add x &&
     +	echo bar | tr -d "\012" >x &&
     +	git diff x &&
    -+	test_must_fail git -c core.whitespace=incomplete diff --check x &&
    ++	test_must_fail git -c core.whitespace=incomplete diff --check x >error &&
    ++	test_grep "no newline at the end of file" error &&
     +	git diff -R x &&
     +	git -c core.whitespace=incomplete diff -R --check x
     +'
12:  33c5ae40db = 12:  85748701b4 attr: enable incomplete-line whitespace error for this project
-- 
2.52.0-rc1-455-g30608eb744
Junio C Hamano· Nov 11, 2025, 00:04 UTC · re: Junio C Hamano · lore

[PATCH v3 01/12] whitespace: correct bit assignment comments

A comment in diff.c claimed that bits up to 12th (counting from 0th) are whitespace rules, and 13th thru 15th are for new/old/context, but it turns out it was miscounting. Correct them, and clarify where the whitespace rule bits come from in the comment. Extend bit assignment comments to cover bits used for color-moved, which weren't described.

Also update the way these bit constants are defined to use (1 << N) notation, instead of octal constants, as it tends to make it easier to notice a breakage like this.

Sprinkle a few blank lines between logically distinct groups of CPP macro definitions to make them easier to read.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c |  7 +++++--
 diff.h |  6 +++---
 ws.h   | 25 ++++++++++++++-----------
 3 files changed, 22 insertions(+), 16 deletions(-)
Show changes to 3 files +22 −16

diff.c, diff.h, ws.h

diff --git a/diff.c b/diff.c
index a74e701806..74261b332a 100644
--- a/diff.c
+++ b/diff.c
@@ -801,16 +801,19 @@ enum diff_symbol {
 	DIFF_SYMBOL_CONTEXT_MARKER,
 	DIFF_SYMBOL_SEPARATOR
 };
+
 /*
  * Flags for content lines:
- * 0..12 are whitespace rules
- * 13-15 are WSEH_NEW | WSEH_OLD | WSEH_CONTEXT
+ * 0..11 are whitespace rules (see ws.h)
+ * 12..14 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD
  * 16 is marking if the line is blank at EOF
+ * 17..19 are used for color-moved.
  */
 #define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF	(1<<16)
 #define DIFF_SYMBOL_MOVED_LINE			(1<<17)
 #define DIFF_SYMBOL_MOVED_LINE_ALT		(1<<18)
 #define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING	(1<<19)
+
 #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)
 
 /*
diff --git a/diff.h b/diff.h
index 2fa256c3ef..cbd355cf50 100644
--- a/diff.h
+++ b/diff.h
@@ -331,9 +331,9 @@ struct diff_options {
 
 	int ita_invisible_in_index;
 /* white-space error highlighting */
-#define WSEH_NEW (1<<12)
-#define WSEH_CONTEXT (1<<13)
-#define WSEH_OLD (1<<14)
+#define WSEH_NEW        (1<<12)
+#define WSEH_CONTEXT    (1<<13)
+#define WSEH_OLD        (1<<14)
 	unsigned ws_error_highlight;
 	const char *prefix;
 	int prefix_length;
diff --git a/ws.h b/ws.h
index 5ba676c559..23708efb73 100644
--- a/ws.h
+++ b/ws.h
@@ -7,19 +7,22 @@ struct strbuf;
 /*
  * whitespace rules.
  * used by both diff and apply
- * last two digits are tab width
+ * last two octal-digits are tab width (we support only up to 63).
  */
-#define WS_BLANK_AT_EOL         0100
-#define WS_SPACE_BEFORE_TAB     0200
-#define WS_INDENT_WITH_NON_TAB  0400
-#define WS_CR_AT_EOL           01000
-#define WS_BLANK_AT_EOF        02000
-#define WS_TAB_IN_INDENT       04000
-#define WS_TRAILING_SPACE      (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
+#define WS_BLANK_AT_EOL         (1<<6)
+#define WS_SPACE_BEFORE_TAB     (1<<7)
+#define WS_INDENT_WITH_NON_TAB  (1<<8)
+#define WS_CR_AT_EOL            (1<<9)
+#define WS_BLANK_AT_EOF         (1<<10)
+#define WS_TAB_IN_INDENT        (1<<11)
+
+#define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
 #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)
-#define WS_TAB_WIDTH_MASK        077
-/* All WS_* -- when extended, adapt diff.c emit_symbol */
-#define WS_RULE_MASK           07777
+#define WS_TAB_WIDTH_MASK       ((1<<6)-1)
+
+/* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */
+#define WS_RULE_MASK            ((1<<12)-1)
+
 extern unsigned whitespace_rule_cfg;
 unsigned whitespace_rule(struct index_state *, const char *);
 unsigned parse_whitespace_rule(const char *);
-- 
2.52.0-rc1-455-g30608eb744
Junio C Hamano· Nov 11, 2025, 00:04 UTC · re: Junio C Hamano · lore

[PATCH v3 02/12] diff: emit_line_ws_markup() if/else style fix

Apply the simple rule: if you need {} in one arm of the if/else if/else... cascade, have {} in all of them.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
Show changes to diff.c +4 −4
diff --git a/diff.c b/diff.c
index 74261b332a..9a24a0791c 100644
--- a/diff.c
+++ b/diff.c
@@ -1327,14 +1327,14 @@ static void emit_line_ws_markup(struct diff_options *o,
 			ws = NULL;
 	}
 
-	if (!ws && !set_sign)
+	if (!ws && !set_sign) {
 		emit_line_0(o, set, NULL, 0, reset, sign, line, len);
-	else if (!ws) {
+	} else if (!ws) {
 		emit_line_0(o, set_sign, set, !!set_sign, reset, sign, line, len);
-	} else if (blank_at_eof)
+	} else if (blank_at_eof) {
 		/* Blank line at EOF - paint '+' as well */
 		emit_line_0(o, ws, NULL, 0, reset, sign, line, len);
-	else {
+	} else {
 		/* Emit just the prefix, then the rest. */
 		emit_line_0(o, set_sign ? set_sign : set, NULL, !!set_sign, reset,
 			    sign, "", 0);
-- 
2.52.0-rc1-455-g30608eb744
Junio C Hamano· Nov 11, 2025, 00:04 UTC · re: Junio C Hamano · lore

[PATCH v3 03/12] diff: correct suppress_blank_empty hack

The suppress-blank-empty feature abused the CONTEXT_INCOMPLETE symbol that was meant to be used only for "\ No newline at the end of file" code path.

The intent of the feature was to turn a context line we receive from xdiff machinery (which always uses ' ' for context lines, even an empty one) and spit it out as a truly empty line.

Perform such a conversion very locally at where a line from xdiff that begins with ' ' is handled for output; there are many checks before the control reaches such place that checks the first letter of the diff output line to see if it is a context line, and having to check for '\n' and treat it as a special case is error prone.

In order to catch similar hacks in the future, make sure the code path that is meant for "\ No newline" case checks the first byte is indeed a backslash.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 27 +++++++++++----------------
 1 file changed, 11 insertions(+), 16 deletions(-)
Show changes to diff.c +11 −16
diff --git a/diff.c b/diff.c
index 9a24a0791c..b9ef8550cc 100644
--- a/diff.c
+++ b/diff.c
@@ -1321,6 +1321,11 @@ static void emit_line_ws_markup(struct diff_options *o,
 	const char *ws = NULL;
 	int sign = o->output_indicators[sign_index];
 
+	if (diff_suppress_blank_empty &&
+	    sign_index == OUTPUT_INDICATOR_CONTEXT &&
+	    len == 1 && line[0] == '\n')
+		sign = 0;
+
 	if (o->ws_error_highlight & ws_rule) {
 		ws = diff_get_color_opt(o, DIFF_WHITESPACE);
 		if (!*ws)
@@ -1498,15 +1503,9 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
 	case DIFF_SYMBOL_WORDS:
 		context = diff_get_color_opt(o, DIFF_CONTEXT);
 		reset = diff_get_color_opt(o, DIFF_RESET);
-		/*
-		 * Skip the prefix character, if any.  With
-		 * diff_suppress_blank_empty, there may be
-		 * none.
-		 */
-		if (line[0] != '\n') {
-			line++;
-			len--;
-		}
+
+		/* Skip the prefix character */
+		line++; len--;
 		emit_line(o, context, reset, line, len);
 		break;
 	case DIFF_SYMBOL_FILEPAIR_PLUS:
@@ -2375,12 +2374,6 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 		ecbdata->label_path[0] = ecbdata->label_path[1] = NULL;
 	}
 
-	if (diff_suppress_blank_empty
-	    && len == 2 && line[0] == ' ' && line[1] == '\n') {
-		line[0] = '\n';
-		len = 1;
-	}
-
 	if (line[0] == '@') {
 		if (ecbdata->diff_words)
 			diff_words_flush(ecbdata);
@@ -2431,12 +2424,14 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 		ecbdata->lno_in_preimage++;
 		emit_context_line(ecbdata, line + 1, len - 1);
 		break;
-	default:
+	case '\\':
 		/* incomplete line at the end */
 		ecbdata->lno_in_preimage++;
 		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
 				 line, len, 0);
 		break;
+	default:
+		BUG("fn_out_consume: unknown line '%s'", line);
 	}
 	return 0;
 }
-- 
2.52.0-rc1-455-g30608eb744
Junio C Hamano· Nov 11, 2025, 00:04 UTC · re: Junio C Hamano · lore

[PATCH v3 04/12] diff: fix incorrect counting of line numbers

The "\ No newline at the end of the file" can come after any of the "-" (deleted preimage line), " " (unchanged line), or "+" (added postimage line). Incrementing only the preimage line number upon seeing it does not make any sense.

We can keep track of what the previous line was, and increment lno_in_{pre,post}image variables properly, like this patch does. I do not think it matters, as these numbers are used only to compare them with blank_at_eof_in_{pre,post}image to issue the warning every time we see an added line, but by definition, after we see "\ No newline at the end of the file" for an added line, we will not see an added line for the file.

Keeping track of what the last line was (in other words, "is it that the file used to end in an incomplete line? The file ends in an incomplete line after the change? Both the file before and after the change ends in an incomplete line that did not change?") will be independently useful.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)
Show changes to diff.c +17 −1
diff --git a/diff.c b/diff.c
index b9ef8550cc..e73320dfb1 100644
--- a/diff.c
+++ b/diff.c
@@ -601,6 +601,7 @@ struct emit_callback {
 	int blank_at_eof_in_postimage;
 	int lno_in_preimage;
 	int lno_in_postimage;
+	int last_line_kind;
 	const char **label_path;
 	struct diff_words_data *diff_words;
 	struct diff_options *opt;
@@ -2426,13 +2427,28 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 		break;
 	case '\\':
 		/* incomplete line at the end */
-		ecbdata->lno_in_preimage++;
+		switch (ecbdata->last_line_kind) {
+		case '+':
+			ecbdata->lno_in_postimage++;
+			break;
+		case '-':
+			ecbdata->lno_in_preimage++;
+			break;
+		case ' ':
+			ecbdata->lno_in_preimage++;
+			ecbdata->lno_in_postimage++;
+			break;
+		default:
+			BUG("fn_out_consume: '\\No newline' after unknown line (%c)",
+			    ecbdata->last_line_kind);
+		}
 		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
 				 line, len, 0);
 		break;
 	default:
 		BUG("fn_out_consume: unknown line '%s'", line);
 	}
+	ecbdata->last_line_kind = line[0];
 	return 0;
 }
 
-- 
2.52.0-rc1-455-g30608eb744
Junio C Hamano· Nov 11, 2025, 00:04 UTC · re: Junio C Hamano · lore

[PATCH v3 05/12] diff: refactor output of incomplete line

Create a helper function that reacts to "\ No newline at the end of file" in preparation for unifying the incomplete line handling in the code path that handles xdiff output and the code path that bypasses xdiff and produces complete rewrite patch.

Currently the output from the DIFF_SYMBOL_CONTEXT_INCOMPLETE case still (ab)uses the same code as what is used for context lines, but that would change in a later step where we introduce support for incomplete line detection.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)
Show changes to diff.c +12 −2
diff --git a/diff.c b/diff.c
index e73320dfb1..8f1b4e6069 100644
--- a/diff.c
+++ b/diff.c
@@ -1379,6 +1379,10 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
 		emit_line(o, "", "", line, len);
 		break;
 	case DIFF_SYMBOL_CONTEXT_INCOMPLETE:
+		set = diff_get_color_opt(o, DIFF_CONTEXT);
+		reset = diff_get_color_opt(o, DIFF_RESET);
+		emit_line(o, set, reset, line, len);
+		break;
 	case DIFF_SYMBOL_CONTEXT_MARKER:
 		context = diff_get_color_opt(o, DIFF_CONTEXT);
 		reset = diff_get_color_opt(o, DIFF_RESET);
@@ -1668,6 +1672,13 @@ static void emit_context_line(struct emit_callback *ecbdata,
 	emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT, line, len, flags);
 }
 
+static void emit_incomplete_line_marker(struct emit_callback *ecbdata,
+					const char *line, int len)
+{
+	emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
+			 line, len, 0);
+}
+
 static void emit_hunk_header(struct emit_callback *ecbdata,
 			     const char *line, int len)
 {
@@ -2442,8 +2453,7 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 			BUG("fn_out_consume: '\\No newline' after unknown line (%c)",
 			    ecbdata->last_line_kind);
 		}
-		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
-				 line, len, 0);
+		emit_incomplete_line_marker(ecbdata, line, len);
 		break;
 	default:
 		BUG("fn_out_consume: unknown line '%s'", line);
-- 
2.52.0-rc1-455-g30608eb744
Junio C Hamano· Nov 11, 2025, 00:04 UTC · re: Junio C Hamano · lore

[PATCH v3 06/12] diff: call emit_callback ecbdata everywhere

Everybody else, except for emit_rewrite_lines(), calls the emit_callback data ecbdata. Make sure we call the same thing by the same name for consistency.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)
Show changes to diff.c +6 −6
diff --git a/diff.c b/diff.c
index 8f1b4e6069..67071136a4 100644
--- a/diff.c
+++ b/diff.c
@@ -1780,7 +1780,7 @@ static void add_line_count(struct strbuf *out, int count)
 	}
 }
 
-static void emit_rewrite_lines(struct emit_callback *ecb,
+static void emit_rewrite_lines(struct emit_callback *ecbdata,
 			       int prefix, const char *data, int size)
 {
 	const char *endp = NULL;
@@ -1791,17 +1791,17 @@ static void emit_rewrite_lines(struct emit_callback *ecb,
 		endp = memchr(data, '\n', size);
 		len = endp ? (endp - data + 1) : size;
 		if (prefix != '+') {
-			ecb->lno_in_preimage++;
-			emit_del_line(ecb, data, len);
+			ecbdata->lno_in_preimage++;
+			emit_del_line(ecbdata, data, len);
 		} else {
-			ecb->lno_in_postimage++;
-			emit_add_line(ecb, data, len);
+			ecbdata->lno_in_postimage++;
+			emit_add_line(ecbdata, data, len);
 		}
 		size -= len;
 		data += len;
 	}
 	if (!endp)
-		emit_diff_symbol(ecb->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
+		emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
 }
 
 static void emit_rewrite_diff(const char *name_a,
-- 
2.52.0-rc1-455-g30608eb744
Junio C Hamano· Nov 11, 2025, 00:04 UTC · re: Junio C Hamano · lore

[PATCH v3 07/12] diff: update the way rewrite diff handles incomplete lines

The diff_symbol based output framework uses one DIFF_SYMBOL_* enum value per the kind of output lines of "git diff", which corresponds to one output line from the xdiff machinery used internally. Most notably, DIFF_SYMBOL_PLUS and DIFF_SYMBOL_MINUS that correspond to "+" and "-" lines are designed to always take a complete line, even if the output from xdiff machinery may produce "\ No newline at the end of file" immediately after them.

But this is not true in the rewrite-diff codepath, which completely bypasses the xdiff machinery. Since the code path feeds the bytes directly from the payload to the output routines, the output layer has to deal with an incomplete line with DIFF_SYMBOL_PLUS and DIFF_SYMBOL_MINUS, which never would see an incomplete line in the normal code paths. This lack of final newline is compensated by an ugly hack for a fabricated DIFF_SYMBOL_NO_LF_EOF token to inject an extra newline to the output to simulate output coming from the xdiff machinery.

Revamp the way the complete-rewrite code path feeds the lines to the output layer by treating the last line of the pre/post image when it is an incomplete line specially.

This lets us remove the DIFF_SYMBOL_NO_LF_EOF hack and use the usual DIFF_SYMBOL_CONTEXT_INCOMPLETE code path, which will later learn how to handle whitespace errors.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 37 ++++++++++++++++++++++---------------
 1 file changed, 22 insertions(+), 15 deletions(-)
Show changes to diff.c +22 −15
diff --git a/diff.c b/diff.c
index 67071136a4..c3fb3015d6 100644
--- a/diff.c
+++ b/diff.c
@@ -797,7 +797,6 @@ enum diff_symbol {
 	DIFF_SYMBOL_CONTEXT_INCOMPLETE,
 	DIFF_SYMBOL_PLUS,
 	DIFF_SYMBOL_MINUS,
-	DIFF_SYMBOL_NO_LF_EOF,
 	DIFF_SYMBOL_CONTEXT_FRAGINFO,
 	DIFF_SYMBOL_CONTEXT_MARKER,
 	DIFF_SYMBOL_SEPARATOR
@@ -1352,7 +1351,6 @@ static void emit_line_ws_markup(struct diff_options *o,
 static void emit_diff_symbol_from_struct(struct diff_options *o,
 					 struct emitted_diff_symbol *eds)
 {
-	static const char *nneof = " No newline at end of file\n";
 	const char *context, *reset, *set, *set_sign, *meta, *fraginfo;
 
 	enum diff_symbol s = eds->s;
@@ -1361,13 +1359,6 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
 	unsigned flags = eds->flags;
 
 	switch (s) {
-	case DIFF_SYMBOL_NO_LF_EOF:
-		context = diff_get_color_opt(o, DIFF_CONTEXT);
-		reset = diff_get_color_opt(o, DIFF_RESET);
-		putc('\n', o->file);
-		emit_line_0(o, context, NULL, 0, reset, '\\',
-			    nneof, strlen(nneof));
-		break;
 	case DIFF_SYMBOL_SUBMODULE_HEADER:
 	case DIFF_SYMBOL_SUBMODULE_ERROR:
 	case DIFF_SYMBOL_SUBMODULE_PIPETHROUGH:
@@ -1786,22 +1777,38 @@ static void emit_rewrite_lines(struct emit_callback *ecbdata,
 	const char *endp = NULL;
 
 	while (0 < size) {
-		int len;
+		int len, plen;
+		char *pdata = NULL;
 
 		endp = memchr(data, '\n', size);
-		len = endp ? (endp - data + 1) : size;
+
+		if (endp) {
+			len = endp - data + 1;
+			plen = len;
+		} else {
+			len = size;
+			plen = len + 1;
+			pdata = xmalloc(plen + 2);
+			memcpy(pdata, data, len);
+			pdata[len] = '\n';
+			pdata[len + 1] = '\0';
+		}
 		if (prefix != '+') {
 			ecbdata->lno_in_preimage++;
-			emit_del_line(ecbdata, data, len);
+			emit_del_line(ecbdata, pdata ? pdata : data, plen);
 		} else {
 			ecbdata->lno_in_postimage++;
-			emit_add_line(ecbdata, data, len);
+			emit_add_line(ecbdata, pdata ? pdata : data, plen);
 		}
+		free(pdata);
 		size -= len;
 		data += len;
 	}
-	if (!endp)
-		emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
+	if (!endp) {
+		static const char nneof[] = "\\ No newline at end of file\n";
+		ecbdata->last_line_kind = prefix;
+		emit_incomplete_line_marker(ecbdata, nneof, sizeof(nneof) - 1);
+	}
 }
 
 static void emit_rewrite_diff(const char *name_a,
-- 
2.52.0-rc1-455-g30608eb744
Junio C Hamano· Nov 11, 2025, 00:04 UTC · re: Junio C Hamano · lore

[PATCH v3 08/12] apply: revamp the parsing of incomplete lines

A patch file represents the incomplete line at the end of the file with two lines, one that is the usual "context" with " " as the first letter, "added" with "+" as the first letter, or "removed" with "-" as the first letter that shows the content of the line, plus an extra "\ No newline at the end of file" line that comes immediately after it.

Ever since the apply machinery was written, the "git apply" machinery parses "\ No newline at the end of file" line independently, without even knowing what line the incomplete-ness applies to, simply because it does not even remember what the previous line was.

This poses a problem if we want to check and warn on an incomplete line. Revamp the code that parses a fragment, to actually drop the '\n' at the end of the incoming patch file that terminates a line, so that check_whitespace() calls made from the code path actually sees an incomplete as incomplete.

Note that the result of this parsing is not directly used by the code path that applies the patch. apply_one_fragment() function already checks if each of the patch text it handles is followed by a line that begins with a backslash to drop the newline at the end of the current line it is looking at. In a sense, this patch harmonizes the behaviour of the parsing side to what is already done in the application side.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 apply.c | 70 ++++++++++++++++++++++++++++++++++++++++-----------------
 1 file changed, 49 insertions(+), 21 deletions(-)
Show changes to apply.c +49 −21
diff --git a/apply.c b/apply.c
index a2ceb3fb40..2b0f8bdab5 100644
--- a/apply.c
+++ b/apply.c
@@ -1670,6 +1670,35 @@ static void check_old_for_crlf(struct patch *patch, const char *line, int len)
 }
 
 
+/*
+ * Just saw a single line in a fragment.  If it is a part of this hunk
+ * that is a context " ", an added "+", or a removed "-" line, it may
+ * be followed by "\\ No newline..." to signal that the last "\n" on
+ * this line needs to be dropped.  Depending on locale settings when
+ * the patch was produced we don't know what this line would exactly
+ * say. The only thing we do know is that it begins with "\ ".
+ * Checking for 12 is just for sanity check; "\ No newline..." would
+ * be at least that long in any l10n.
+ *
+ * Return 0 if the line we saw is not followed by "\ No newline...",
+ * or length of that line.  The caller will use it to skip over the
+ * "\ No newline..." line.
+ */
+static int adjust_incomplete(const char *line, int len,
+			     unsigned long size)
+{
+	int nextlen;
+
+	if (*line != '\n' && *line != ' ' && *line != '+' && *line != '-')
+		return 0;
+	if (size - len < 12 || memcmp(line + len, "\\ ", 2))
+		return 0;
+	nextlen = linelen(line + len, size - len);
+	if (nextlen < 12)
+		return 0;
+	return nextlen;
+}
+
 /*
  * Parse a unified diff. Note that this really needs to parse each
  * fragment separately, since the only way to know the difference
@@ -1684,6 +1713,7 @@ static int parse_fragment(struct apply_state *state,
 {
 	int added, deleted;
 	int len = linelen(line, size), offset;
+	int skip_len = 0;
 	unsigned long oldlines, newlines;
 	unsigned long leading, trailing;
 
@@ -1710,6 +1740,22 @@ static int parse_fragment(struct apply_state *state,
 		len = linelen(line, size);
 		if (!len || line[len-1] != '\n')
 			return -1;
+
+		/*
+		 * For an incomplete line, skip_len counts the bytes
+		 * on "\\ No newline..." marker line that comes next
+		 * to the current line.
+		 *
+		 * Reduce "len" to drop the newline at the end of
+		 * line[], but add one to "skip_len", which will be
+		 * added back to "len" for the next iteration, to
+		 * compensate.
+		 */
+		skip_len = adjust_incomplete(line, len, size);
+		if (skip_len) {
+			len--;
+			skip_len++;
+		}
 		switch (*line) {
 		default:
 			return -1;
@@ -1745,20 +1791,10 @@ static int parse_fragment(struct apply_state *state,
 			newlines--;
 			trailing = 0;
 			break;
-
-		/*
-		 * We allow "\ No newline at end of file". Depending
-		 * on locale settings when the patch was produced we
-		 * don't know what this line looks like. The only
-		 * thing we do know is that it begins with "\ ".
-		 * Checking for 12 is just for sanity check -- any
-		 * l10n of "\ No newline..." is at least that long.
-		 */
-		case '\\':
-			if (len < 12 || memcmp(line, "\\ ", 2))
-				return -1;
-			break;
 		}
+
+		/* eat the "\\ No newline..." as well, if exists */
+		len += skip_len;
 	}
 	if (oldlines || newlines)
 		return -1;
@@ -1768,14 +1804,6 @@ static int parse_fragment(struct apply_state *state,
 	fragment->leading = leading;
 	fragment->trailing = trailing;
 
-	/*
-	 * If a fragment ends with an incomplete line, we failed to include
-	 * it in the above loop because we hit oldlines == newlines == 0
-	 * before seeing it.
-	 */
-	if (12 < size && !memcmp(line, "\\ ", 2))
-		offset += linelen(line, size);
-
 	patch->lines_added += added;
 	patch->lines_deleted += deleted;
 
-- 
2.52.0-rc1-455-g30608eb744
Junio C Hamano· Nov 11, 2025, 00:04 UTC · re: Junio C Hamano · lore

[PATCH v3 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE

Reserve a few more bits in the diff flags word to be used for future whitespace rules. Add WS_INCOMPLETE_LINE without implementing the behaviour (yet).

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/config/core.adoc |  2 ++
 diff.c                         | 16 ++++++++--------
 diff.h                         |  6 +++---
 ws.c                           |  6 ++++++
 ws.h                           |  3 ++-
 5 files changed, 21 insertions(+), 12 deletions(-)
Show changes to 5 files +21 −12

Documentation/config/core.adoc, diff.c, diff.h, ws.c, ws.h

diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc
index e2de270c86..682fb595fb 100644
--- a/Documentation/config/core.adoc
+++ b/Documentation/config/core.adoc
@@ -626,6 +626,8 @@ core.whitespace::
   part of the line terminator, i.e. with it, `trailing-space`
   does not trigger if the character before such a carriage-return
   is not a whitespace (not enabled by default).
+* `incomplete-line` treats the last line of a file that is missing the
+  newline at the end as an error (not enabled by default).
 * `tabwidth=<n>` tells how many character positions a tab occupies; this
   is relevant for `indent-with-non-tab` and when Git fixes `tab-in-indent`
   errors. The default tab width is 8. Allowed values are 1 to 63.
diff --git a/diff.c b/diff.c
index c3fb3015d6..64cf1f139f 100644
--- a/diff.c
+++ b/diff.c
@@ -804,15 +804,15 @@ enum diff_symbol {
 
 /*
  * Flags for content lines:
- * 0..11 are whitespace rules (see ws.h)
- * 12..14 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD
- * 16 is marking if the line is blank at EOF
- * 17..19 are used for color-moved.
+ * 0..15 are whitespace rules (see ws.h)
+ * 16..18 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD
+ * 19 is marking if the line is blank at EOF
+ * 20..22 are used for color-moved.
  */
-#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF	(1<<16)
-#define DIFF_SYMBOL_MOVED_LINE			(1<<17)
-#define DIFF_SYMBOL_MOVED_LINE_ALT		(1<<18)
-#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING	(1<<19)
+#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF	(1<<19)
+#define DIFF_SYMBOL_MOVED_LINE			(1<<20)
+#define DIFF_SYMBOL_MOVED_LINE_ALT		(1<<21)
+#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING	(1<<22)
 
 #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)
 
diff --git a/diff.h b/diff.h
index cbd355cf50..422658407d 100644
--- a/diff.h
+++ b/diff.h
@@ -331,9 +331,9 @@ struct diff_options {
 
 	int ita_invisible_in_index;
 /* white-space error highlighting */
-#define WSEH_NEW        (1<<12)
-#define WSEH_CONTEXT    (1<<13)
-#define WSEH_OLD        (1<<14)
+#define WSEH_NEW        (1<<16)
+#define WSEH_CONTEXT    (1<<17)
+#define WSEH_OLD        (1<<18)
 	unsigned ws_error_highlight;
 	const char *prefix;
 	int prefix_length;
diff --git a/ws.c b/ws.c
index 70acee3337..34a7b4fad2 100644
--- a/ws.c
+++ b/ws.c
@@ -26,6 +26,7 @@ static struct whitespace_rule {
 	{ "blank-at-eol", WS_BLANK_AT_EOL, 0 },
 	{ "blank-at-eof", WS_BLANK_AT_EOF, 0 },
 	{ "tab-in-indent", WS_TAB_IN_INDENT, 0, 1 },
+	{ "incomplete-line", WS_INCOMPLETE_LINE, 0, 0 },
 };
 
 unsigned parse_whitespace_rule(const char *string)
@@ -139,6 +140,11 @@ char *whitespace_error_string(unsigned ws)
 			strbuf_addstr(&err, ", ");
 		strbuf_addstr(&err, "tab in indent");
 	}
+	if (ws & WS_INCOMPLETE_LINE) {
+		if (err.len)
+			strbuf_addstr(&err, ", ");
+		strbuf_addstr(&err, "no newline at the end of file");
+	}
 	return strbuf_detach(&err, NULL);
 }
 
diff --git a/ws.h b/ws.h
index 23708efb73..06d5cb73f8 100644
--- a/ws.h
+++ b/ws.h
@@ -15,13 +15,14 @@ struct strbuf;
 #define WS_CR_AT_EOL            (1<<9)
 #define WS_BLANK_AT_EOF         (1<<10)
 #define WS_TAB_IN_INDENT        (1<<11)
+#define WS_INCOMPLETE_LINE      (1<<12)
 
 #define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
 #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)
 #define WS_TAB_WIDTH_MASK       ((1<<6)-1)
 
 /* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */
-#define WS_RULE_MASK            ((1<<12)-1)
+#define WS_RULE_MASK            ((1<<16)-1)
 
 extern unsigned whitespace_rule_cfg;
 unsigned whitespace_rule(struct index_state *, const char *);
-- 
2.52.0-rc1-455-g30608eb744
Junio C Hamano· Nov 11, 2025, 00:04 UTC · re: Junio C Hamano · lore

[PATCH v3 10/12] apply: check and fix incomplete lines

The final line of a file that lacks the terminating newline at its end is called an incomplete line. In general they are frowned upon for many reasons (imagine concatenating two files with "cat A B" and what happens when A ends in an incomplete line, for example), and text-oriented tools often mishandle such a line.

Implement checks in "git apply" for incomplete lines, which is off by default for backward compatibility's sake, so that "git apply --whitespace={fix,warn,error}" can notice, warn against, and fix them.

As one of the new test shows, if you modify contents on an incomplete line in the original and leave the resulting line incomplete, it is still considered a whitespace error, the reasoning being that "you'd better fix it while at it if you are making a change on an incomplete line anyway", which may controversial.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 apply.c                  |  13 ++-
 t/t4124-apply-ws-rule.sh | 187 +++++++++++++++++++++++++++++++++++++++
 ws.c                     |  14 +++
 3 files changed, 213 insertions(+), 1 deletion(-)
Show changes to 3 files +213 −1

apply.c, t/t4124-apply-ws-rule.sh, ws.c

diff --git a/apply.c b/apply.c
index 2b0f8bdab5..c9fb45247d 100644
--- a/apply.c
+++ b/apply.c
@@ -1640,6 +1640,14 @@ static void record_ws_error(struct apply_state *state,
 	    state->squelch_whitespace_errors < state->whitespace_error)
 		return;
 
+	/*
+	 * line[len] for an incomplete line points at the "\n" at the end
+	 * of patch input line, so "%.*s" would drop the last letter on line;
+	 * compensate for it.
+	 */
+	if (result & WS_INCOMPLETE_LINE)
+		len++;
+
 	err = whitespace_error_string(result);
 	if (state->apply_verbosity > verbosity_silent)
 		fprintf(stderr, "%s:%d: %s.\n%.*s\n",
@@ -1794,7 +1802,10 @@ static int parse_fragment(struct apply_state *state,
 		}
 
 		/* eat the "\\ No newline..." as well, if exists */
-		len += skip_len;
+		if (skip_len) {
+			len += skip_len;
+			state->linenr++;
+		}
 	}
 	if (oldlines || newlines)
 		return -1;
diff --git a/t/t4124-apply-ws-rule.sh b/t/t4124-apply-ws-rule.sh
index 485c7d2d12..115a0f8579 100755
--- a/t/t4124-apply-ws-rule.sh
+++ b/t/t4124-apply-ws-rule.sh
@@ -556,4 +556,191 @@ test_expect_success 'whitespace check skipped for excluded paths' '
 	git apply --include=used --stat --whitespace=error <patch
 '
 
+test_expect_success 'check incomplete lines (setup)' '
+	rm -f .gitattributes &&
+	git config core.whitespace incomplete-line
+'
+
+test_expect_success 'incomplete context line (not an error)' '
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	(test_write_lines 1 2 3 0 5 && printf 6) >sample2-i &&
+	cat sample-i >target &&
+	git add target &&
+	cat sample2-i >target &&
+	git diff-files -p target >patch &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error <patch &&
+	test_cmp sample2-i target &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error --check <patch 2>error &&
+	test_cmp sample-i target &&
+	test_must_be_empty error &&
+
+	cat sample2-i >target &&
+	git apply --whitespace=error -R <patch &&
+	test_cmp sample-i target &&
+
+	cat sample2-i >target &&
+	git apply -R --whitespace=error --check <patch 2>error &&
+	test_cmp sample2-i target &&
+	test_must_be_empty error
+'
+
+test_expect_success 'last line made incomplete (error)' '
+	test_write_lines 1 2 3 4 5 6 >sample &&
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	cat sample >target &&
+	git add target &&
+	cat sample-i >target &&
+	git diff-files -p target >patch &&
+
+	cat sample >target &&
+	test_must_fail git apply --whitespace=error <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample >target &&
+	test_must_fail git apply --whitespace=error --check <patch 2>actual &&
+	test_cmp sample target &&
+	cat >expect <<-\EOF &&
+	<stdin>:10: no newline at the end of file.
+	6
+	error: 1 line adds whitespace errors.
+	EOF
+	test_cmp expect actual &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error -R <patch &&
+	test_cmp sample target &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error --check -R <patch 2>error &&
+	test_cmp sample-i target &&
+	test_must_be_empty error &&
+
+	cat sample >target &&
+	git apply --whitespace=fix <patch &&
+	test_cmp sample target
+'
+
+test_expect_success 'incomplete line removed at the end (not an error)' '
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	test_write_lines 1 2 3 4 5 6 >sample &&
+	cat sample-i >target &&
+	git add target &&
+	cat sample >target &&
+	git diff-files -p target >patch &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error <patch &&
+	test_cmp sample target &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error --check <patch 2>error &&
+	test_cmp sample-i target &&
+	test_must_be_empty error &&
+
+	cat sample >target &&
+	test_must_fail git apply --whitespace=error -R <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample >target &&
+	test_must_fail git apply --whitespace=error --check -R <patch 2>actual &&
+	test_cmp sample target &&
+	cat >expect <<-\EOF &&
+	<stdin>:9: no newline at the end of file.
+	6
+	error: 1 line adds whitespace errors.
+	EOF
+	test_cmp expect actual &&
+
+	cat sample >target &&
+	git apply --whitespace=fix -R <patch &&
+	test_cmp sample target
+'
+
+test_expect_success 'incomplete line corrected at the end (not an error)' '
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	test_write_lines 1 2 3 4 5 7 >sample3 &&
+	cat sample-i >target &&
+	git add target &&
+	cat sample3 >target &&
+	git diff-files -p target >patch &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error <patch &&
+	test_cmp sample3 target &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error --check <patch 2>error &&
+	test_cmp sample-i target &&
+	test_must_be_empty error &&
+
+	cat sample3 >target &&
+	test_must_fail git apply --whitespace=error -R <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample3 >target &&
+	test_must_fail git apply --whitespace=error -R --check <patch 2>actual &&
+	test_cmp sample3 target &&
+	cat >expect <<-\EOF &&
+	<stdin>:9: no newline at the end of file.
+	6
+	error: 1 line adds whitespace errors.
+	EOF
+	test_cmp expect actual &&
+
+	cat sample3 >target &&
+	git apply --whitespace=fix -R <patch &&
+	test_cmp sample target
+'
+
+test_expect_success 'incomplete line modified at the end (error)' '
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	(test_write_lines 1 2 3 4 5 && printf 7) >sample3-i &&
+	test_write_lines 1 2 3 4 5 6 >sample &&
+	test_write_lines 1 2 3 4 5 7 >sample3 &&
+	cat sample-i >target &&
+	git add target &&
+	cat sample3-i >target &&
+	git diff-files -p target >patch &&
+
+	cat sample-i >target &&
+	test_must_fail git apply --whitespace=error <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample-i >target &&
+	test_must_fail git apply --whitespace=error --check <patch 2>actual &&
+	test_cmp sample-i target &&
+	cat >expect <<-\EOF &&
+	<stdin>:11: no newline at the end of file.
+	7
+	error: 1 line adds whitespace errors.
+	EOF
+	test_cmp expect actual &&
+
+	cat sample3-i >target &&
+	test_must_fail git apply --whitespace=error -R <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample3-i >target &&
+	test_must_fail git apply --whitespace=error --check -R <patch 2>actual &&
+	test_cmp sample3-i target &&
+	cat >expect <<-\EOF &&
+	<stdin>:9: no newline at the end of file.
+	6
+	error: 1 line adds whitespace errors.
+	EOF
+	test_cmp expect actual &&
+
+	cat sample-i >target &&
+	git apply --whitespace=fix <patch &&
+	test_cmp sample3 target &&
+
+	cat sample3-i >target &&
+	git apply --whitespace=fix -R <patch &&
+	test_cmp sample target
+'
+
 test_done
diff --git a/ws.c b/ws.c
index 34a7b4fad2..6cc2466c0c 100644
--- a/ws.c
+++ b/ws.c
@@ -186,6 +186,9 @@ static unsigned ws_check_emit_1(const char *line, int len, unsigned ws_rule,
 	if (trailing_whitespace == -1)
 		trailing_whitespace = len;
 
+	if (!trailing_newline && (ws_rule & WS_INCOMPLETE_LINE))
+		result |= WS_INCOMPLETE_LINE;
+
 	/* Check indentation */
 	for (i = 0; i < trailing_whitespace; i++) {
 		if (line[i] == ' ')
@@ -297,6 +300,17 @@ void ws_fix_copy(struct strbuf *dst, const char *src, int len, unsigned ws_rule,
 	int last_space_in_indent = -1;
 	int need_fix_leading_space = 0;
 
+	/*
+	 * Remembering that we need to add '\n' at the end
+	 * is sufficient to fix an incomplete line.
+	 */
+	if (ws_rule & WS_INCOMPLETE_LINE) {
+		if (0 < len && src[len - 1] != '\n') {
+			fixed = 1;
+			add_nl_to_tail = 1;
+		}
+	}
+
 	/*
 	 * Strip trailing whitespace
 	 */
-- 
2.52.0-rc1-455-g30608eb744
Junio C Hamano· Nov 11, 2025, 00:04 UTC · re: Junio C Hamano · lore

[PATCH v3 11/12] 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>
---
 diff.c                     | 29 +++++++++++++++--
 t/t4015-diff-whitespace.sh | 67 +++++++++++++++++++++++++++++++++++---
 2 files changed, 90 insertions(+), 6 deletions(-)
Show changes to 2 files +90 −6

diff.c, t/t4015-diff-whitespace.sh

diff --git a/diff.c b/diff.c
index 64cf1f139f..e12931c5e7 100644
--- a/diff.c
+++ b/diff.c
@@ -1370,7 +1370,11 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
 		emit_line(o, "", "", line, len);
 		break;
 	case DIFF_SYMBOL_CONTEXT_INCOMPLETE:
-		set = diff_get_color_opt(o, DIFF_CONTEXT);
+		if ((flags & WS_INCOMPLETE_LINE) &&
+		    (flags & o->ws_error_highlight))
+			set = diff_get_color_opt(o, DIFF_WHITESPACE);
+		else
+			set = diff_get_color_opt(o, DIFF_CONTEXT);
 		reset = diff_get_color_opt(o, DIFF_RESET);
 		emit_line(o, set, reset, line, len);
 		break;
@@ -1666,8 +1670,14 @@ static void emit_context_line(struct emit_callback *ecbdata,
 static void emit_incomplete_line_marker(struct emit_callback *ecbdata,
 					const char *line, int len)
 {
+	int last_line_kind = ecbdata->last_line_kind;
+	unsigned flags = (last_line_kind == '+'
+			  ? WSEH_NEW
+			  : last_line_kind == '-'
+			  ? WSEH_OLD
+			  : WSEH_CONTEXT) | ecbdata->ws_rule;
 	emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
-			 line, len, 0);
+			 line, len, flags);
 }
 
 static void emit_hunk_header(struct emit_callback *ecbdata,
@@ -3259,6 +3269,7 @@ struct checkdiff_t {
 	struct diff_options *o;
 	unsigned ws_rule;
 	unsigned status;
+	int last_line_kind;
 };
 
 static int is_conflict_marker(const char *line, int marker_size, unsigned long len)
@@ -3297,6 +3308,7 @@ static void checkdiff_consume_hunk(void *priv,
 static int checkdiff_consume(void *priv, char *line, unsigned long len)
 {
 	struct checkdiff_t *data = priv;
+	int last_line_kind;
 	int marker_size = data->conflict_marker_size;
 	const char *ws = diff_get_color(data->o->use_color, DIFF_WHITESPACE);
 	const char *reset = diff_get_color(data->o->use_color, DIFF_RESET);
@@ -3307,6 +3319,8 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)
 	assert(data->o);
 	line_prefix = diff_line_prefix(data->o);
 
+	last_line_kind = data->last_line_kind;
+	data->last_line_kind = line[0];
 	if (line[0] == '+') {
 		unsigned bad;
 		data->lineno++;
@@ -3329,6 +3343,17 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)
 			      data->o->file, set, reset, ws);
 	} else if (line[0] == ' ') {
 		data->lineno++;
+	} else if (line[0] == '\\') {
+		/* no newline at the end of the line */
+		if ((data->ws_rule & WS_INCOMPLETE_LINE) &&
+		    (last_line_kind == '+')) {
+			unsigned bad = WS_INCOMPLETE_LINE;
+			data->status |= bad;
+			err = whitespace_error_string(bad);
+			fprintf(data->o->file, "%s%s:%d: %s.\n",
+				line_prefix, data->filename, data->lineno, err);
+			free(err);
+		}
 	}
 	return 0;
 }
diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh
index 9de7f73f42..3c8eb02e4f 100755
--- a/t/t4015-diff-whitespace.sh
+++ b/t/t4015-diff-whitespace.sh
@@ -43,6 +43,53 @@ do
 	'
 done
 
+test_expect_success "incomplete line in both pre- and post-image context" '
+	(echo foo && echo baz | tr -d "\012") >x &&
+	git add x &&
+	(echo bar && echo baz | tr -d "\012") >x &&
+	git diff x &&
+	git -c core.whitespace=incomplete diff --check x &&
+	git diff -R x &&
+	git -c core.whitespace=incomplete diff -R --check x
+'
+
+test_expect_success "incomplete lines on both pre- and post-image" '
+	# The interpretation taken here is "since you are touching
+	# the line anyway, you would better fix the incomplete line
+	# while you are at it."  but this is debatable.
+	echo foo | tr -d "\012" >x &&
+	git add x &&
+	echo bar | tr -d "\012" >x &&
+	git diff x &&
+	test_must_fail git -c core.whitespace=incomplete diff --check x >error &&
+	test_grep "no newline at the end of file" error &&
+	git diff -R x &&
+	test_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&
+	test_grep "no newline at the end of file" error
+'
+
+test_expect_success "fix incomplete line in pre-image" '
+	echo foo | tr -d "\012" >x &&
+	git add x &&
+	echo bar >x &&
+	git diff x &&
+	git -c core.whitespace=incomplete diff --check x &&
+	git diff -R x &&
+	test_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&
+	test_grep "no newline at the end of file" error
+'
+
+test_expect_success "new incomplete line in post-image" '
+	echo foo >x &&
+	git add x &&
+	echo bar | tr -d "\012" >x &&
+	git diff x &&
+	test_must_fail git -c core.whitespace=incomplete diff --check x >error &&
+	test_grep "no newline at the end of file" error &&
+	git diff -R x &&
+	git -c core.whitespace=incomplete diff -R --check x
+'
+
 test_expect_success "Ray Lehtiniemi's example" '
 	cat <<-\EOF >x &&
 	do {
@@ -1040,7 +1087,8 @@ test_expect_success 'ws-error-highlight test setup' '
 	{
 		echo "0. blank-at-eol " &&
 		echo "1. still-blank-at-eol " &&
-		echo "2. and a new line "
+		echo "2. and a new line " &&
+		printf "3. and more"
 	} >x &&
 	new_hash_x=$(git hash-object x) &&
 	after=$(git rev-parse --short "$new_hash_x") &&
@@ -1050,11 +1098,13 @@ test_expect_success 'ws-error-highlight test setup' '
 	<BOLD>index $before..$after 100644<RESET>
 	<BOLD>--- a/x<RESET>
 	<BOLD>+++ b/x<RESET>
-	<CYAN>@@ -1,2 +1,3 @@<RESET>
+	<CYAN>@@ -1,2 +1,4 @@<RESET>
 	 0. blank-at-eol <RESET>
 	<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>
 	<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>
 	<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>
+	<GREEN>+<RESET><GREEN>3. and more<RESET>
+	<BLUE>\ No newline at end of file<RESET>
 	EOF
 
 	cat >expect.all <<-EOF &&
@@ -1062,11 +1112,13 @@ test_expect_success 'ws-error-highlight test setup' '
 	<BOLD>index $before..$after 100644<RESET>
 	<BOLD>--- a/x<RESET>
 	<BOLD>+++ b/x<RESET>
-	<CYAN>@@ -1,2 +1,3 @@<RESET>
+	<CYAN>@@ -1,2 +1,4 @@<RESET>
 	 <RESET>0. blank-at-eol<RESET><BLUE> <RESET>
 	<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>
 	<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>
 	<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>
+	<GREEN>+<RESET><GREEN>3. and more<RESET>
+	<BLUE>\ No newline at end of file<RESET>
 	EOF
 
 	cat >expect.none <<-EOF
@@ -1074,16 +1126,19 @@ test_expect_success 'ws-error-highlight test setup' '
 	<BOLD>index $before..$after 100644<RESET>
 	<BOLD>--- a/x<RESET>
 	<BOLD>+++ b/x<RESET>
-	<CYAN>@@ -1,2 +1,3 @@<RESET>
+	<CYAN>@@ -1,2 +1,4 @@<RESET>
 	 0. blank-at-eol <RESET>
 	<RED>-1. blank-at-eol <RESET>
 	<GREEN>+1. still-blank-at-eol <RESET>
 	<GREEN>+2. and a new line <RESET>
+	<GREEN>+3. and more<RESET>
+	\ No newline at end of file<RESET>
 	EOF
 
 '
 
 test_expect_success 'test --ws-error-highlight option' '
+	git config core.whitespace blank-at-eol,incomplete-line &&
 
 	git diff --color --ws-error-highlight=default,old >current.raw &&
 	test_decode_color <current.raw >current &&
@@ -1100,6 +1155,7 @@ test_expect_success 'test --ws-error-highlight option' '
 '
 
 test_expect_success 'test diff.wsErrorHighlight config' '
+	git config core.whitespace blank-at-eol,incomplete-line &&
 
 	git -c diff.wsErrorHighlight=default,old diff --color >current.raw &&
 	test_decode_color <current.raw >current &&
@@ -1116,6 +1172,7 @@ test_expect_success 'test diff.wsErrorHighlight config' '
 '
 
 test_expect_success 'option overrides diff.wsErrorHighlight' '
+	git config core.whitespace blank-at-eol,incomplete-line &&
 
 	git -c diff.wsErrorHighlight=none \
 		diff --color --ws-error-highlight=default,old >current.raw &&
@@ -1135,6 +1192,8 @@ test_expect_success 'option overrides diff.wsErrorHighlight' '
 '
 
 test_expect_success 'detect moved code, complete file' '
+	git config core.whitespace blank-at-eol &&
+
 	git reset --hard &&
 	cat <<-\EOF >test.c &&
 	#include<stdio.h>
-- 
2.52.0-rc1-455-g30608eb744
Junio C Hamano· Nov 11, 2025, 00:04 UTC · re: Junio C Hamano · lore

[PATCH v3 12/12] attr: enable incomplete-line whitespace error for this project

Now "git diff --check" and "git apply --whitespace=warn/fix" learned incomplete line is a whitespace error, enable them for this project to prevent patches to add new incomplete lines to our sources.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 .gitattributes | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
Show changes to .gitattributes +2 −2
diff --git a/.gitattributes b/.gitattributes
index 32583149c2..0accd23848 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,6 +1,6 @@
 * whitespace=!indent,trail,space
-*.[ch] whitespace=indent,trail,space diff=cpp
-*.sh whitespace=indent,trail,space text eol=lf
+*.[ch] whitespace=indent,trail,space,incomplete diff=cpp
+*.sh whitespace=indent,trail,space,incomplete text eol=lf
 *.perl text eol=lf diff=perl
 *.pl text eof=lf diff=perl
 *.pm text eol=lf diff=perl
-- 
2.52.0-rc1-455-g30608eb744
Phillip Wood· Nov 11, 2025, 14:29 UTC · re: Junio C Hamano · lore

Re: [PATCH v3 00/12] Incomplete lines

On 11/11/2025 00:04, Junio C Hamano wrote:
Show 14 quoted lines
> Changes in v3:
> 
>   - The proposed log message of [PATCH 05/12] explains that it
>     semi-duplicates the same code shared in two case arems in
>     preparation for later changes.
> 
>   - The internal helper function to emit the "\ No newline" marker
>     line is now called emit_incomplete_line_marker().
> 
>   - Two conditionals in [PATCH 07/12] both of which switched on !endp
>     have been consolidated into a single if/else statement.
> 
>   - The tests in [PATCH 11/12] checks the output from "diff --check"
>     now.

The range-diff looks good. I'm still not keen on patch 4 but all the other diff related changes look fine to me.

Thanks
Phillip
Show 163 quoted lines
> 
> Changes in v2:
> 
>   - rolled the definition (but not implementation) of the new
>     "incomplete-line" class into step [09/12] that shifts the bit
>     assignment.  The documentation of core.whitespace has also be
>     updated in this step.
> 
>   - "git apply --check" miscounted line number reported for the
>     incomplete line error, which has been corrected in step [10/12].
> 
>   - t4124-apply-ws-rule.sh has been extended to cover "git apply
>     --check" and the diagnostic output from it in step [10/12].
> 
> Junio C Hamano (12):
>    whitespace: correct bit assignment comments
>    diff: emit_line_ws_markup() if/else style fix
>    diff: correct suppress_blank_empty hack
>    diff: fix incorrect counting of line numbers
>    diff: refactor output of incomplete line
>    diff: call emit_callback ecbdata everywhere
>    diff: update the way rewrite diff handles incomplete lines
>    apply: revamp the parsing of incomplete lines
>    whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE
>    apply: check and fix incomplete lines
>    diff: highlight and error out on incomplete lines
>    attr: enable incomplete-line whitespace error for this project
> 
>   .gitattributes                 |   4 +-
>   Documentation/config/core.adoc |   2 +
>   apply.c                        |  79 ++++++++++----
>   diff.c                         | 152 ++++++++++++++++++---------
>   diff.h                         |   6 +-
>   t/t4015-diff-whitespace.sh     |  67 +++++++++++-
>   t/t4124-apply-ws-rule.sh       | 187 +++++++++++++++++++++++++++++++++
>   ws.c                           |  20 ++++
>   ws.h                           |  26 +++--
>   9 files changed, 455 insertions(+), 88 deletions(-)
> 
> Range-diff against v2:
>   1:  c045e93ce5 =  1:  c045e93ce5 whitespace: correct bit assignment comments
>   2:  0d95d68fb4 =  2:  0d95d68fb4 diff: emit_line_ws_markup() if/else style fix
>   3:  c331218334 =  3:  c331218334 diff: correct suppress_blank_empty hack
>   4:  be1473fc5a =  4:  be1473fc5a diff: fix incorrect counting of line numbers
>   5:  7bcd6efba8 !  5:  9410e4257a diff: refactor output of incomplete line
>      @@ Commit message
>           the code path that handles xdiff output and the code path that
>           bypasses xdiff and produces complete rewrite patch.
>       
>      +    Currently the output from the DIFF_SYMBOL_CONTEXT_INCOMPLETE case
>      +    still (ab)uses the same code as what is used for context lines, but
>      +    that would change in a later step where we introduce support for
>      +    incomplete line detection.
>      +
>           Signed-off-by: Junio C Hamano <gitster@pobox.com>
>       
>        ## diff.c ##
>      @@ diff.c: static void emit_context_line(struct emit_callback *ecbdata,
>        	emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT, line, len, flags);
>        }
>        
>      -+static void emit_incomplete_line(struct emit_callback *ecbdata,
>      -+				 const char *line, int len)
>      ++static void emit_incomplete_line_marker(struct emit_callback *ecbdata,
>      ++					const char *line, int len)
>       +{
>       +	emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
>       +			 line, len, 0);
>      @@ diff.c: static int fn_out_consume(void *priv, char *line, unsigned long len)
>        		}
>       -		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
>       -				 line, len, 0);
>      -+		emit_incomplete_line(ecbdata, line, len);
>      ++		emit_incomplete_line_marker(ecbdata, line, len);
>        		break;
>        	default:
>        		BUG("fn_out_consume: unknown line '%s'", line);
>   6:  1a6f143377 =  6:  cdc6516009 diff: call emit_callback ecbdata everywhere
>   7:  dfc810b1d6 !  7:  9acb9b6217 diff: update the way rewrite diff handles incomplete lines
>      @@ diff.c: static void emit_rewrite_lines(struct emit_callback *ecbdata,
>       +		char *pdata = NULL;
>        
>        		endp = memchr(data, '\n', size);
>      - 		len = endp ? (endp - data + 1) : size;
>      -+		plen = len;
>      +-		len = endp ? (endp - data + 1) : size;
>       +
>      -+		if (!endp) {
>      ++		if (endp) {
>      ++			len = endp - data + 1;
>      ++			plen = len;
>      ++		} else {
>      ++			len = size;
>       +			plen = len + 1;
>       +			pdata = xmalloc(plen + 2);
>       +			memcpy(pdata, data, len);
>      @@ diff.c: static void emit_rewrite_lines(struct emit_callback *ecbdata,
>       +	if (!endp) {
>       +		static const char nneof[] = "\\ No newline at end of file\n";
>       +		ecbdata->last_line_kind = prefix;
>      -+		emit_incomplete_line(ecbdata, nneof, sizeof(nneof) - 1);
>      ++		emit_incomplete_line_marker(ecbdata, nneof, sizeof(nneof) - 1);
>       +	}
>        }
>        
>   8:  c66b547f13 =  8:  86c14ee62d apply: revamp the parsing of incomplete lines
>   9:  bdc2dbbe4b =  9:  b62d4020e7 whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE
> 10:  806aa30511 = 10:  081c21b14e apply: check and fix incomplete lines
> 11:  0cfb6ab295 ! 11:  73182b19a8 diff: highlight and error out on incomplete lines
>      @@ diff.c: static void emit_diff_symbol_from_struct(struct diff_options *o,
>        		emit_line(o, set, reset, line, len);
>        		break;
>       @@ diff.c: static void emit_context_line(struct emit_callback *ecbdata,
>      - static void emit_incomplete_line(struct emit_callback *ecbdata,
>      - 				 const char *line, int len)
>      + static void emit_incomplete_line_marker(struct emit_callback *ecbdata,
>      + 					const char *line, int len)
>        {
>       +	int last_line_kind = ecbdata->last_line_kind;
>       +	unsigned flags = (last_line_kind == '+'
>      @@ t/t4015-diff-whitespace.sh: do
>       +'
>       +
>       +test_expect_success "incomplete lines on both pre- and post-image" '
>      -+	# The interpretation taken here is "since you are toucing
>      ++	# The interpretation taken here is "since you are touching
>       +	# the line anyway, you would better fix the incomplete line
>       +	# while you are at it."  but this is debatable.
>       +	echo foo | tr -d "\012" >x &&
>       +	git add x &&
>       +	echo bar | tr -d "\012" >x &&
>       +	git diff x &&
>      -+	test_must_fail git -c core.whitespace=incomplete diff --check x &&
>      ++	test_must_fail git -c core.whitespace=incomplete diff --check x >error &&
>      ++	test_grep "no newline at the end of file" error &&
>       +	git diff -R x &&
>      -+	test_must_fail git -c core.whitespace=incomplete diff -R --check x
>      ++	test_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&
>      ++	test_grep "no newline at the end of file" error
>       +'
>       +
>       +test_expect_success "fix incomplete line in pre-image" '
>      @@ t/t4015-diff-whitespace.sh: do
>       +	git diff x &&
>       +	git -c core.whitespace=incomplete diff --check x &&
>       +	git diff -R x &&
>      -+	test_must_fail git -c core.whitespace=incomplete diff -R --check x
>      ++	test_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&
>      ++	test_grep "no newline at the end of file" error
>       +'
>       +
>       +test_expect_success "new incomplete line in post-image" '
>      @@ t/t4015-diff-whitespace.sh: do
>       +	git add x &&
>       +	echo bar | tr -d "\012" >x &&
>       +	git diff x &&
>      -+	test_must_fail git -c core.whitespace=incomplete diff --check x &&
>      ++	test_must_fail git -c core.whitespace=incomplete diff --check x >error &&
>      ++	test_grep "no newline at the end of file" error &&
>       +	git diff -R x &&
>       +	git -c core.whitespace=incomplete diff -R --check x
>       +'
> 12:  33c5ae40db = 12:  85748701b4 attr: enable incomplete-line whitespace error for this project
Junio C Hamano· Nov 12, 2025, 22:02 UTC · re: Junio C Hamano · lore

[PATCH v4 00/12] Incomplete lines

One of the common kind of whitespace errors is to lack the final newline at the end of a file, but so far, neither "git diff" or "git apply" did anything about them.

This series introduces "incomplete-line" whitespace error class, that you can add to either the core.whitespace configuration variable, or the whitespace attribute in your .gitattributes files.

The class is disabled by default, so the final step enables it for our project by defining it in the .gitattributes file.

The incomplete line marker that is given for a context line is not considered an error. The reasoning is that your preimage did have incomplete line, but you did not touch the contents on that incomplete line in your patch, so you left the line intact. It is not a new breakage you are responsible for.

If the incomplete line marker follows a postimage line, on the other hand, it means that you added a new line at the end of the file that is incomplete *and* that line did not exist in the preimage. The last line of the preimage may have been incomplete already, but then you updated the contents on that line, so you could have easily fixed the incompleteness of the line while at it. Either way, you are responsible for the incompleteness of the last ine in the resulting file.

The organization of the series is as follows.
 * The first patch [01/12] is a preliminary clean-up to reformat the
   definition of bit assignments for whitespace error classes.
 * The patches [02/12] - [08/12] are preliminary clean-up made to
   both "git diff" and "git apply" machinery.
 * The patch [09/12] shifts the bit assignment to make room for new
   whitespace error class (which was last updated in 2007 IIRC, so
   the set of whitespace errors surprisingly haven't changed for
   quite some time), and defines the new "incomplete-line" class.
 * The patch [10/12] teaches "git apply --whitespace=<mode>" and
   "git apply --check" about the incomplete-line error class.
 * The patch [11/12] teaches "git diff [--check]" about the
   incomplete-line error class.
 * The final patch [12/12] enables the incomplete-line error class
   for our project for C source files and shell scripts.  I didn't
   touch the cover-all * entry.
Changes in v4:
 - The preliminary clean-up for "git diff" in [04/12] no longer
   "corrects" the code that updates the line number upon seeing
   "\ No newline" marker, and leaves it for later series to clean
   it up as #leftoverbits.
 - Our house rule updates in [12/12] now forbids the documentation
   sources *.adoc to end in an incomplete line.
 - Some typofixes in log messages.
Changes in v3:
 - The proposed log message of [PATCH 05/12] explains that it
   semi-duplicates the same code shared in two case arems in
   preparation for later changes.
 - The internal helper function to emit the "\ No newline" marker
   line is now called emit_incomplete_line_marker().
 - Two conditionals in [PATCH 07/12] both of which switched on !endp
   have been consolidated into a single if/else statement.
 - The tests in [PATCH 11/12] checks the output from "diff --check"
   now.
Changes in v2:
 - rolled the definition (but not implementation) of the new
   "incomplete-line" class into step [09/12] that shifts the bit
   assignment.  The documentation of core.whitespace has also be
   updated in this step.
 - "git apply --check" miscounted line number reported for the
   incomplete line error, which has been corrected in step [10/12].
 - t4124-apply-ws-rule.sh has been extended to cover "git apply
   --check" and the diagnostic output from it in step [10/12].
Junio C Hamano (12):
  whitespace: correct bit assignment comments
  diff: emit_line_ws_markup() if/else style fix
  diff: correct suppress_blank_empty hack
  diff: keep track of the type of the last line seen
  diff: refactor output of incomplete line
  diff: call emit_callback ecbdata everywhere
  diff: update the way rewrite diff handles incomplete lines
  apply: revamp the parsing of incomplete lines
  whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE
  apply: check and fix incomplete lines
  diff: highlight and error out on incomplete lines
  attr: enable incomplete-line whitespace error for this project
 .gitattributes                 |   6 +-
 Documentation/config/core.adoc |   2 +
 apply.c                        |  79 ++++++++++----
 diff.c                         | 145 ++++++++++++++++---------
 diff.h                         |   6 +-
 t/t4015-diff-whitespace.sh     |  67 +++++++++++-
 t/t4124-apply-ws-rule.sh       | 187 +++++++++++++++++++++++++++++++++
 ws.c                           |  20 ++++
 ws.h                           |  26 +++--
 9 files changed, 450 insertions(+), 88 deletions(-)
 1:  39ce594b48 =  1:  5486212662 whitespace: correct bit assignment comments
 2:  7c53ac154b =  2:  89504329a4 diff: emit_line_ws_markup() if/else style fix
 3:  239644a8b4 =  3:  78f55aebf6 diff: correct suppress_blank_empty hack
 4:  a2c279cca0 !  4:  9f6b6ad756 diff: fix incorrect counting of line numbers
    @@ Metadata
     Author: Junio C Hamano <gitster@pobox.com>
     
      ## Commit message ##
    -    diff: fix incorrect counting of line numbers
    +    diff: keep track of the type of the last line seen
     
         The "\ No newline at the end of the file" can come after any of the
         "-" (deleted preimage line), " " (unchanged line), or "+" (added
    -    postimage line).  Incrementing only the preimage line number upon
    -    seeing it does not make any sense.
    +    postimage line).  In later steps in this series, we will start
    +    treating a change that makes a file to end in an incomplete line
    +    as a whitespace error, and we would need to know what the previous
    +    line was when we react to "\ No newline" in the diff output.  If
    +    the previous line was a context (i.e., unchanged) line, the file
    +    lacked the final newline before the change, and the change did not
    +    touch that line and left it still incomplete, so we do not want to
    +    warn in such a case.
     
    -    We can keep track of what the previous line was, and increment
    -    lno_in_{pre,post}image variables properly, like this patch does.  I
    -    do not think it matters, as these numbers are used only to compare
    -    them with blank_at_eof_in_{pre,post}image to issue the warning every
    -    time we see an added line, but by definition, after we see "\ No
    -    newline at the end of the file" for an added line, we will not see
    -    an added line for the file.
    +    Teach fn_out_consume() function to keep track of what the previous
    +    line was, and prepare an otherwise empty switch statement to let us
    +    react differently to "\ No newline" based on that.
     
    -    Keeping track of what the last line was (in other words, "is it that
    -    the file used to end in an incomplete line?  The file ends in an
    -    incomplete line after the change?  Both the file before and after
    -    the change ends in an incomplete line that did not change?") will be
    -    independently useful.
    +    Note that there is an existing curiosity (read: likely to be a bug)
    +    in the code that increments line number in the preimage file every
    +    time it sees a line with "\ No newline" on it, regardless of what
    +    the previous line was.  I left it as-is, because it does not affect
    +    the main theme of this series, and more importantly, I do not think
    +    it matters, as these numbers are used only to compare them with
    +    blank_at_eof_in_{pre,post}image to issue a warning when we see more
    +    empty line was added at the end, but by definition, after we see
    +    "\ No newline at the end of the file" for an added line, we will not
    +    see an added line for the file.
    +
    +    An independent audit to ensure that this curious increment can be
    +    safely removed would make a good #leftoverbits clean-up (we may even
    +    find some code that decrements this counter or over-increments the
    +    other quantity this counter is compared with that compensates the
    +    effect of this curious increment that hides a bug, in which case we
    +    may also need to remove them).
     
         Signed-off-by: Junio C Hamano <gitster@pobox.com>
     
    @@ diff.c: static int fn_out_consume(void *priv, char *line, unsigned long len)
      		break;
      	case '\\':
      		/* incomplete line at the end */
    --		ecbdata->lno_in_preimage++;
     +		switch (ecbdata->last_line_kind) {
     +		case '+':
    -+			ecbdata->lno_in_postimage++;
    -+			break;
     +		case '-':
    -+			ecbdata->lno_in_preimage++;
    -+			break;
     +		case ' ':
    -+			ecbdata->lno_in_preimage++;
    -+			ecbdata->lno_in_postimage++;
     +			break;
     +		default:
     +			BUG("fn_out_consume: '\\No newline' after unknown line (%c)",
     +			    ecbdata->last_line_kind);
     +		}
    + 		ecbdata->lno_in_preimage++;
      		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
      				 line, len, 0);
    - 		break;
    +@@ diff.c: static int fn_out_consume(void *priv, char *line, unsigned long len)
      	default:
      		BUG("fn_out_consume: unknown line '%s'", line);
      	}
 5:  7a91bf04be !  5:  16dda4fe13 diff: refactor output of incomplete line
    @@ Commit message
         Create a helper function that reacts to "\ No newline at the end of
         file" in preparation for unifying the incomplete line handling in
         the code path that handles xdiff output and the code path that
    -    bypasses xdiff and produces complete rewrite patch.
    +    bypasses xdiff and produces a complete-rewrite patch.
     
         Currently the output from the DIFF_SYMBOL_CONTEXT_INCOMPLETE case
         still (ab)uses the same code as what is used for context lines, but
    -    that would change in a later step where we introduce support for
    -    incomplete line detection.
    +    that would change in a later step where we introduce support to treat
    +    an incomplete line as a whitespace error.
     
         Signed-off-by: Junio C Hamano <gitster@pobox.com>
     
    @@ diff.c: static void emit_context_line(struct emit_callback *ecbdata,
      			     const char *line, int len)
      {
     @@ diff.c: static int fn_out_consume(void *priv, char *line, unsigned long len)
    - 			BUG("fn_out_consume: '\\No newline' after unknown line (%c)",
      			    ecbdata->last_line_kind);
      		}
    + 		ecbdata->lno_in_preimage++;
     -		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
     -				 line, len, 0);
     +		emit_incomplete_line_marker(ecbdata, line, len);
 6:  3f56e084f8 =  6:  dd310cc6dc diff: call emit_callback ecbdata everywhere
 7:  af5b4c94e4 =  7:  3360f2882c diff: update the way rewrite diff handles incomplete lines
 8:  e4945e8951 =  8:  ff29d34f3c apply: revamp the parsing of incomplete lines
 9:  d4fd89039a =  9:  b720c4f49d whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE
10:  075ef5d32f = 10:  5ddf90cd08 apply: check and fix incomplete lines
11:  04401decef = 11:  cf52c68ec1 diff: highlight and error out on incomplete lines
12:  068229790d ! 12:  cb86d9b45f attr: enable incomplete-line whitespace error for this project
    @@ Commit message
     
         Now "git diff --check" and "git apply --whitespace=warn/fix" learned
         incomplete line is a whitespace error, enable them for this project
    -    to prevent patches to add new incomplete lines to our sources.
    +    to prevent patches to add new incomplete lines to our source to both
    +    code and documentation files.
     
         Signed-off-by: Junio C Hamano <gitster@pobox.com>
     
    @@ .gitattributes
      *.perl text eol=lf diff=perl
      *.pl text eof=lf diff=perl
      *.pm text eol=lf diff=perl
    + *.py text eol=lf diff=python
    + *.bat text eol=crlf
    + CODE_OF_CONDUCT.md -whitespace
    +-/Documentation/**/*.adoc text eol=lf
    ++/Documentation/**/*.adoc text eol=lf whitespace=!indent,trail,space,incomplete
    + /command-list.txt text eol=lf
    + /GIT-VERSION-GEN text eol=lf
    + /mergetools/* text eol=lf
-- 
2.52.0-rc2-441-g030905368a
Junio C Hamano· Nov 12, 2025, 22:02 UTC · re: Junio C Hamano · lore

[PATCH v4 01/12] whitespace: correct bit assignment comments

A comment in diff.c claimed that bits up to 12th (counting from 0th) are whitespace rules, and 13th thru 15th are for new/old/context, but it turns out it was miscounting. Correct them, and clarify where the whitespace rule bits come from in the comment. Extend bit assignment comments to cover bits used for color-moved, which weren't described.

Also update the way these bit constants are defined to use (1 << N) notation, instead of octal constants, as it tends to make it easier to notice a breakage like this.

Sprinkle a few blank lines between logically distinct groups of CPP macro definitions to make them easier to read.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c |  7 +++++--
 diff.h |  6 +++---
 ws.h   | 25 ++++++++++++++-----------
 3 files changed, 22 insertions(+), 16 deletions(-)
Show changes to 3 files +22 −16

diff.c, diff.h, ws.h

diff --git a/diff.c b/diff.c
index a74e701806..74261b332a 100644
--- a/diff.c
+++ b/diff.c
@@ -801,16 +801,19 @@ enum diff_symbol {
 	DIFF_SYMBOL_CONTEXT_MARKER,
 	DIFF_SYMBOL_SEPARATOR
 };
+
 /*
  * Flags for content lines:
- * 0..12 are whitespace rules
- * 13-15 are WSEH_NEW | WSEH_OLD | WSEH_CONTEXT
+ * 0..11 are whitespace rules (see ws.h)
+ * 12..14 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD
  * 16 is marking if the line is blank at EOF
+ * 17..19 are used for color-moved.
  */
 #define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF	(1<<16)
 #define DIFF_SYMBOL_MOVED_LINE			(1<<17)
 #define DIFF_SYMBOL_MOVED_LINE_ALT		(1<<18)
 #define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING	(1<<19)
+
 #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)
 
 /*
diff --git a/diff.h b/diff.h
index 2fa256c3ef..cbd355cf50 100644
--- a/diff.h
+++ b/diff.h
@@ -331,9 +331,9 @@ struct diff_options {
 
 	int ita_invisible_in_index;
 /* white-space error highlighting */
-#define WSEH_NEW (1<<12)
-#define WSEH_CONTEXT (1<<13)
-#define WSEH_OLD (1<<14)
+#define WSEH_NEW        (1<<12)
+#define WSEH_CONTEXT    (1<<13)
+#define WSEH_OLD        (1<<14)
 	unsigned ws_error_highlight;
 	const char *prefix;
 	int prefix_length;
diff --git a/ws.h b/ws.h
index 5ba676c559..23708efb73 100644
--- a/ws.h
+++ b/ws.h
@@ -7,19 +7,22 @@ struct strbuf;
 /*
  * whitespace rules.
  * used by both diff and apply
- * last two digits are tab width
+ * last two octal-digits are tab width (we support only up to 63).
  */
-#define WS_BLANK_AT_EOL         0100
-#define WS_SPACE_BEFORE_TAB     0200
-#define WS_INDENT_WITH_NON_TAB  0400
-#define WS_CR_AT_EOL           01000
-#define WS_BLANK_AT_EOF        02000
-#define WS_TAB_IN_INDENT       04000
-#define WS_TRAILING_SPACE      (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
+#define WS_BLANK_AT_EOL         (1<<6)
+#define WS_SPACE_BEFORE_TAB     (1<<7)
+#define WS_INDENT_WITH_NON_TAB  (1<<8)
+#define WS_CR_AT_EOL            (1<<9)
+#define WS_BLANK_AT_EOF         (1<<10)
+#define WS_TAB_IN_INDENT        (1<<11)
+
+#define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
 #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)
-#define WS_TAB_WIDTH_MASK        077
-/* All WS_* -- when extended, adapt diff.c emit_symbol */
-#define WS_RULE_MASK           07777
+#define WS_TAB_WIDTH_MASK       ((1<<6)-1)
+
+/* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */
+#define WS_RULE_MASK            ((1<<12)-1)
+
 extern unsigned whitespace_rule_cfg;
 unsigned whitespace_rule(struct index_state *, const char *);
 unsigned parse_whitespace_rule(const char *);
-- 
2.52.0-rc2-441-g030905368a
Junio C Hamano· Nov 12, 2025, 22:02 UTC · re: Junio C Hamano · lore

[PATCH v4 02/12] diff: emit_line_ws_markup() if/else style fix

Apply the simple rule: if you need {} in one arm of the if/else if/else... cascade, have {} in all of them.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
Show changes to diff.c +4 −4
diff --git a/diff.c b/diff.c
index 74261b332a..9a24a0791c 100644
--- a/diff.c
+++ b/diff.c
@@ -1327,14 +1327,14 @@ static void emit_line_ws_markup(struct diff_options *o,
 			ws = NULL;
 	}
 
-	if (!ws && !set_sign)
+	if (!ws && !set_sign) {
 		emit_line_0(o, set, NULL, 0, reset, sign, line, len);
-	else if (!ws) {
+	} else if (!ws) {
 		emit_line_0(o, set_sign, set, !!set_sign, reset, sign, line, len);
-	} else if (blank_at_eof)
+	} else if (blank_at_eof) {
 		/* Blank line at EOF - paint '+' as well */
 		emit_line_0(o, ws, NULL, 0, reset, sign, line, len);
-	else {
+	} else {
 		/* Emit just the prefix, then the rest. */
 		emit_line_0(o, set_sign ? set_sign : set, NULL, !!set_sign, reset,
 			    sign, "", 0);
-- 
2.52.0-rc2-441-g030905368a
Junio C Hamano· Nov 12, 2025, 22:02 UTC · re: Junio C Hamano · lore

[PATCH v4 03/12] diff: correct suppress_blank_empty hack

The suppress-blank-empty feature abused the CONTEXT_INCOMPLETE symbol that was meant to be used only for "\ No newline at the end of file" code path.

The intent of the feature was to turn a context line we receive from xdiff machinery (which always uses ' ' for context lines, even an empty one) and spit it out as a truly empty line.

Perform such a conversion very locally at where a line from xdiff that begins with ' ' is handled for output; there are many checks before the control reaches such place that checks the first letter of the diff output line to see if it is a context line, and having to check for '\n' and treat it as a special case is error prone.

In order to catch similar hacks in the future, make sure the code path that is meant for "\ No newline" case checks the first byte is indeed a backslash.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 27 +++++++++++----------------
 1 file changed, 11 insertions(+), 16 deletions(-)
Show changes to diff.c +11 −16
diff --git a/diff.c b/diff.c
index 9a24a0791c..b9ef8550cc 100644
--- a/diff.c
+++ b/diff.c
@@ -1321,6 +1321,11 @@ static void emit_line_ws_markup(struct diff_options *o,
 	const char *ws = NULL;
 	int sign = o->output_indicators[sign_index];
 
+	if (diff_suppress_blank_empty &&
+	    sign_index == OUTPUT_INDICATOR_CONTEXT &&
+	    len == 1 && line[0] == '\n')
+		sign = 0;
+
 	if (o->ws_error_highlight & ws_rule) {
 		ws = diff_get_color_opt(o, DIFF_WHITESPACE);
 		if (!*ws)
@@ -1498,15 +1503,9 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
 	case DIFF_SYMBOL_WORDS:
 		context = diff_get_color_opt(o, DIFF_CONTEXT);
 		reset = diff_get_color_opt(o, DIFF_RESET);
-		/*
-		 * Skip the prefix character, if any.  With
-		 * diff_suppress_blank_empty, there may be
-		 * none.
-		 */
-		if (line[0] != '\n') {
-			line++;
-			len--;
-		}
+
+		/* Skip the prefix character */
+		line++; len--;
 		emit_line(o, context, reset, line, len);
 		break;
 	case DIFF_SYMBOL_FILEPAIR_PLUS:
@@ -2375,12 +2374,6 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 		ecbdata->label_path[0] = ecbdata->label_path[1] = NULL;
 	}
 
-	if (diff_suppress_blank_empty
-	    && len == 2 && line[0] == ' ' && line[1] == '\n') {
-		line[0] = '\n';
-		len = 1;
-	}
-
 	if (line[0] == '@') {
 		if (ecbdata->diff_words)
 			diff_words_flush(ecbdata);
@@ -2431,12 +2424,14 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 		ecbdata->lno_in_preimage++;
 		emit_context_line(ecbdata, line + 1, len - 1);
 		break;
-	default:
+	case '\\':
 		/* incomplete line at the end */
 		ecbdata->lno_in_preimage++;
 		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
 				 line, len, 0);
 		break;
+	default:
+		BUG("fn_out_consume: unknown line '%s'", line);
 	}
 	return 0;
 }
-- 
2.52.0-rc2-441-g030905368a
Junio C Hamano· Nov 12, 2025, 22:02 UTC · re: Junio C Hamano · lore

[PATCH v4 04/12] diff: keep track of the type of the last line seen

The "\ No newline at the end of the file" can come after any of the "-" (deleted preimage line), " " (unchanged line), or "+" (added postimage line). In later steps in this series, we will start treating a change that makes a file to end in an incomplete line as a whitespace error, and we would need to know what the previous line was when we react to "\ No newline" in the diff output. If the previous line was a context (i.e., unchanged) line, the file lacked the final newline before the change, and the change did not touch that line and left it still incomplete, so we do not want to warn in such a case.

Teach fn_out_consume() function to keep track of what the previous line was, and prepare an otherwise empty switch statement to let us react differently to "\ No newline" based on that.

Note that there is an existing curiosity (read: likely to be a bug) in the code that increments line number in the preimage file every time it sees a line with "\ No newline" on it, regardless of what the previous line was. I left it as-is, because it does not affect the main theme of this series, and more importantly, I do not think it matters, as these numbers are used only to compare them with blank_at_eof_in_{pre,post}image to issue a warning when we see more empty line was added at the end, but by definition, after we see "\ No newline at the end of the file" for an added line, we will not see an added line for the file.

An independent audit to ensure that this curious increment can be safely removed would make a good #leftoverbits clean-up (we may even find some code that decrements this counter or over-increments the other quantity this counter is compared with that compensates the effect of this curious increment that hides a bug, in which case we may also need to remove them).

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 11 +++++++++++
 1 file changed, 11 insertions(+)
Show changes to diff.c +11 −0
diff --git a/diff.c b/diff.c
index b9ef8550cc..ff8fc91f88 100644
--- a/diff.c
+++ b/diff.c
@@ -601,6 +601,7 @@ struct emit_callback {
 	int blank_at_eof_in_postimage;
 	int lno_in_preimage;
 	int lno_in_postimage;
+	int last_line_kind;
 	const char **label_path;
 	struct diff_words_data *diff_words;
 	struct diff_options *opt;
@@ -2426,6 +2427,15 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 		break;
 	case '\\':
 		/* incomplete line at the end */
+		switch (ecbdata->last_line_kind) {
+		case '+':
+		case '-':
+		case ' ':
+			break;
+		default:
+			BUG("fn_out_consume: '\\No newline' after unknown line (%c)",
+			    ecbdata->last_line_kind);
+		}
 		ecbdata->lno_in_preimage++;
 		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
 				 line, len, 0);
@@ -2433,6 +2443,7 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 	default:
 		BUG("fn_out_consume: unknown line '%s'", line);
 	}
+	ecbdata->last_line_kind = line[0];
 	return 0;
 }
 
-- 
2.52.0-rc2-441-g030905368a
Junio C Hamano· Nov 12, 2025, 22:02 UTC · re: Junio C Hamano · lore

[PATCH v4 05/12] diff: refactor output of incomplete line

Create a helper function that reacts to "\ No newline at the end of file" in preparation for unifying the incomplete line handling in the code path that handles xdiff output and the code path that bypasses xdiff and produces a complete-rewrite patch.

Currently the output from the DIFF_SYMBOL_CONTEXT_INCOMPLETE case still (ab)uses the same code as what is used for context lines, but that would change in a later step where we introduce support to treat an incomplete line as a whitespace error.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)
Show changes to diff.c +12 −2
diff --git a/diff.c b/diff.c
index ff8fc91f88..7ee8620429 100644
--- a/diff.c
+++ b/diff.c
@@ -1379,6 +1379,10 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
 		emit_line(o, "", "", line, len);
 		break;
 	case DIFF_SYMBOL_CONTEXT_INCOMPLETE:
+		set = diff_get_color_opt(o, DIFF_CONTEXT);
+		reset = diff_get_color_opt(o, DIFF_RESET);
+		emit_line(o, set, reset, line, len);
+		break;
 	case DIFF_SYMBOL_CONTEXT_MARKER:
 		context = diff_get_color_opt(o, DIFF_CONTEXT);
 		reset = diff_get_color_opt(o, DIFF_RESET);
@@ -1668,6 +1672,13 @@ static void emit_context_line(struct emit_callback *ecbdata,
 	emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT, line, len, flags);
 }
 
+static void emit_incomplete_line_marker(struct emit_callback *ecbdata,
+					const char *line, int len)
+{
+	emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
+			 line, len, 0);
+}
+
 static void emit_hunk_header(struct emit_callback *ecbdata,
 			     const char *line, int len)
 {
@@ -2437,8 +2448,7 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
 			    ecbdata->last_line_kind);
 		}
 		ecbdata->lno_in_preimage++;
-		emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
-				 line, len, 0);
+		emit_incomplete_line_marker(ecbdata, line, len);
 		break;
 	default:
 		BUG("fn_out_consume: unknown line '%s'", line);
-- 
2.52.0-rc2-441-g030905368a
Junio C Hamano· Nov 12, 2025, 22:02 UTC · re: Junio C Hamano · lore

[PATCH v4 06/12] diff: call emit_callback ecbdata everywhere

Everybody else, except for emit_rewrite_lines(), calls the emit_callback data ecbdata. Make sure we call the same thing by the same name for consistency.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)
Show changes to diff.c +6 −6
diff --git a/diff.c b/diff.c
index 7ee8620429..44b86544b7 100644
--- a/diff.c
+++ b/diff.c
@@ -1780,7 +1780,7 @@ static void add_line_count(struct strbuf *out, int count)
 	}
 }
 
-static void emit_rewrite_lines(struct emit_callback *ecb,
+static void emit_rewrite_lines(struct emit_callback *ecbdata,
 			       int prefix, const char *data, int size)
 {
 	const char *endp = NULL;
@@ -1791,17 +1791,17 @@ static void emit_rewrite_lines(struct emit_callback *ecb,
 		endp = memchr(data, '\n', size);
 		len = endp ? (endp - data + 1) : size;
 		if (prefix != '+') {
-			ecb->lno_in_preimage++;
-			emit_del_line(ecb, data, len);
+			ecbdata->lno_in_preimage++;
+			emit_del_line(ecbdata, data, len);
 		} else {
-			ecb->lno_in_postimage++;
-			emit_add_line(ecb, data, len);
+			ecbdata->lno_in_postimage++;
+			emit_add_line(ecbdata, data, len);
 		}
 		size -= len;
 		data += len;
 	}
 	if (!endp)
-		emit_diff_symbol(ecb->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
+		emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
 }
 
 static void emit_rewrite_diff(const char *name_a,
-- 
2.52.0-rc2-441-g030905368a
Junio C Hamano· Nov 12, 2025, 22:02 UTC · re: Junio C Hamano · lore

[PATCH v4 07/12] diff: update the way rewrite diff handles incomplete lines

The diff_symbol based output framework uses one DIFF_SYMBOL_* enum value per the kind of output lines of "git diff", which corresponds to one output line from the xdiff machinery used internally. Most notably, DIFF_SYMBOL_PLUS and DIFF_SYMBOL_MINUS that correspond to "+" and "-" lines are designed to always take a complete line, even if the output from xdiff machinery may produce "\ No newline at the end of file" immediately after them.

But this is not true in the rewrite-diff codepath, which completely bypasses the xdiff machinery. Since the code path feeds the bytes directly from the payload to the output routines, the output layer has to deal with an incomplete line with DIFF_SYMBOL_PLUS and DIFF_SYMBOL_MINUS, which never would see an incomplete line in the normal code paths. This lack of final newline is compensated by an ugly hack for a fabricated DIFF_SYMBOL_NO_LF_EOF token to inject an extra newline to the output to simulate output coming from the xdiff machinery.

Revamp the way the complete-rewrite code path feeds the lines to the output layer by treating the last line of the pre/post image when it is an incomplete line specially.

This lets us remove the DIFF_SYMBOL_NO_LF_EOF hack and use the usual DIFF_SYMBOL_CONTEXT_INCOMPLETE code path, which will later learn how to handle whitespace errors.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 diff.c | 37 ++++++++++++++++++++++---------------
 1 file changed, 22 insertions(+), 15 deletions(-)
Show changes to diff.c +22 −15
diff --git a/diff.c b/diff.c
index 44b86544b7..5c606409bb 100644
--- a/diff.c
+++ b/diff.c
@@ -797,7 +797,6 @@ enum diff_symbol {
 	DIFF_SYMBOL_CONTEXT_INCOMPLETE,
 	DIFF_SYMBOL_PLUS,
 	DIFF_SYMBOL_MINUS,
-	DIFF_SYMBOL_NO_LF_EOF,
 	DIFF_SYMBOL_CONTEXT_FRAGINFO,
 	DIFF_SYMBOL_CONTEXT_MARKER,
 	DIFF_SYMBOL_SEPARATOR
@@ -1352,7 +1351,6 @@ static void emit_line_ws_markup(struct diff_options *o,
 static void emit_diff_symbol_from_struct(struct diff_options *o,
 					 struct emitted_diff_symbol *eds)
 {
-	static const char *nneof = " No newline at end of file\n";
 	const char *context, *reset, *set, *set_sign, *meta, *fraginfo;
 
 	enum diff_symbol s = eds->s;
@@ -1361,13 +1359,6 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
 	unsigned flags = eds->flags;
 
 	switch (s) {
-	case DIFF_SYMBOL_NO_LF_EOF:
-		context = diff_get_color_opt(o, DIFF_CONTEXT);
-		reset = diff_get_color_opt(o, DIFF_RESET);
-		putc('\n', o->file);
-		emit_line_0(o, context, NULL, 0, reset, '\\',
-			    nneof, strlen(nneof));
-		break;
 	case DIFF_SYMBOL_SUBMODULE_HEADER:
 	case DIFF_SYMBOL_SUBMODULE_ERROR:
 	case DIFF_SYMBOL_SUBMODULE_PIPETHROUGH:
@@ -1786,22 +1777,38 @@ static void emit_rewrite_lines(struct emit_callback *ecbdata,
 	const char *endp = NULL;
 
 	while (0 < size) {
-		int len;
+		int len, plen;
+		char *pdata = NULL;
 
 		endp = memchr(data, '\n', size);
-		len = endp ? (endp - data + 1) : size;
+
+		if (endp) {
+			len = endp - data + 1;
+			plen = len;
+		} else {
+			len = size;
+			plen = len + 1;
+			pdata = xmalloc(plen + 2);
+			memcpy(pdata, data, len);
+			pdata[len] = '\n';
+			pdata[len + 1] = '\0';
+		}
 		if (prefix != '+') {
 			ecbdata->lno_in_preimage++;
-			emit_del_line(ecbdata, data, len);
+			emit_del_line(ecbdata, pdata ? pdata : data, plen);
 		} else {
 			ecbdata->lno_in_postimage++;
-			emit_add_line(ecbdata, data, len);
+			emit_add_line(ecbdata, pdata ? pdata : data, plen);
 		}
+		free(pdata);
 		size -= len;
 		data += len;
 	}
-	if (!endp)
-		emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
+	if (!endp) {
+		static const char nneof[] = "\\ No newline at end of file\n";
+		ecbdata->last_line_kind = prefix;
+		emit_incomplete_line_marker(ecbdata, nneof, sizeof(nneof) - 1);
+	}
 }
 
 static void emit_rewrite_diff(const char *name_a,
-- 
2.52.0-rc2-441-g030905368a
Junio C Hamano· Nov 12, 2025, 22:02 UTC · re: Junio C Hamano · lore

[PATCH v4 08/12] apply: revamp the parsing of incomplete lines

A patch file represents the incomplete line at the end of the file with two lines, one that is the usual "context" with " " as the first letter, "added" with "+" as the first letter, or "removed" with "-" as the first letter that shows the content of the line, plus an extra "\ No newline at the end of file" line that comes immediately after it.

Ever since the apply machinery was written, the "git apply" machinery parses "\ No newline at the end of file" line independently, without even knowing what line the incomplete-ness applies to, simply because it does not even remember what the previous line was.

This poses a problem if we want to check and warn on an incomplete line. Revamp the code that parses a fragment, to actually drop the '\n' at the end of the incoming patch file that terminates a line, so that check_whitespace() calls made from the code path actually sees an incomplete as incomplete.

Note that the result of this parsing is not directly used by the code path that applies the patch. apply_one_fragment() function already checks if each of the patch text it handles is followed by a line that begins with a backslash to drop the newline at the end of the current line it is looking at. In a sense, this patch harmonizes the behaviour of the parsing side to what is already done in the application side.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 apply.c | 70 ++++++++++++++++++++++++++++++++++++++++-----------------
 1 file changed, 49 insertions(+), 21 deletions(-)
Show changes to apply.c +49 −21
diff --git a/apply.c b/apply.c
index a2ceb3fb40..2b0f8bdab5 100644
--- a/apply.c
+++ b/apply.c
@@ -1670,6 +1670,35 @@ static void check_old_for_crlf(struct patch *patch, const char *line, int len)
 }
 
 
+/*
+ * Just saw a single line in a fragment.  If it is a part of this hunk
+ * that is a context " ", an added "+", or a removed "-" line, it may
+ * be followed by "\\ No newline..." to signal that the last "\n" on
+ * this line needs to be dropped.  Depending on locale settings when
+ * the patch was produced we don't know what this line would exactly
+ * say. The only thing we do know is that it begins with "\ ".
+ * Checking for 12 is just for sanity check; "\ No newline..." would
+ * be at least that long in any l10n.
+ *
+ * Return 0 if the line we saw is not followed by "\ No newline...",
+ * or length of that line.  The caller will use it to skip over the
+ * "\ No newline..." line.
+ */
+static int adjust_incomplete(const char *line, int len,
+			     unsigned long size)
+{
+	int nextlen;
+
+	if (*line != '\n' && *line != ' ' && *line != '+' && *line != '-')
+		return 0;
+	if (size - len < 12 || memcmp(line + len, "\\ ", 2))
+		return 0;
+	nextlen = linelen(line + len, size - len);
+	if (nextlen < 12)
+		return 0;
+	return nextlen;
+}
+
 /*
  * Parse a unified diff. Note that this really needs to parse each
  * fragment separately, since the only way to know the difference
@@ -1684,6 +1713,7 @@ static int parse_fragment(struct apply_state *state,
 {
 	int added, deleted;
 	int len = linelen(line, size), offset;
+	int skip_len = 0;
 	unsigned long oldlines, newlines;
 	unsigned long leading, trailing;
 
@@ -1710,6 +1740,22 @@ static int parse_fragment(struct apply_state *state,
 		len = linelen(line, size);
 		if (!len || line[len-1] != '\n')
 			return -1;
+
+		/*
+		 * For an incomplete line, skip_len counts the bytes
+		 * on "\\ No newline..." marker line that comes next
+		 * to the current line.
+		 *
+		 * Reduce "len" to drop the newline at the end of
+		 * line[], but add one to "skip_len", which will be
+		 * added back to "len" for the next iteration, to
+		 * compensate.
+		 */
+		skip_len = adjust_incomplete(line, len, size);
+		if (skip_len) {
+			len--;
+			skip_len++;
+		}
 		switch (*line) {
 		default:
 			return -1;
@@ -1745,20 +1791,10 @@ static int parse_fragment(struct apply_state *state,
 			newlines--;
 			trailing = 0;
 			break;
-
-		/*
-		 * We allow "\ No newline at end of file". Depending
-		 * on locale settings when the patch was produced we
-		 * don't know what this line looks like. The only
-		 * thing we do know is that it begins with "\ ".
-		 * Checking for 12 is just for sanity check -- any
-		 * l10n of "\ No newline..." is at least that long.
-		 */
-		case '\\':
-			if (len < 12 || memcmp(line, "\\ ", 2))
-				return -1;
-			break;
 		}
+
+		/* eat the "\\ No newline..." as well, if exists */
+		len += skip_len;
 	}
 	if (oldlines || newlines)
 		return -1;
@@ -1768,14 +1804,6 @@ static int parse_fragment(struct apply_state *state,
 	fragment->leading = leading;
 	fragment->trailing = trailing;
 
-	/*
-	 * If a fragment ends with an incomplete line, we failed to include
-	 * it in the above loop because we hit oldlines == newlines == 0
-	 * before seeing it.
-	 */
-	if (12 < size && !memcmp(line, "\\ ", 2))
-		offset += linelen(line, size);
-
 	patch->lines_added += added;
 	patch->lines_deleted += deleted;
 
-- 
2.52.0-rc2-441-g030905368a
Junio C Hamano· Nov 12, 2025, 22:02 UTC · re: Junio C Hamano · lore

[PATCH v4 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE

Reserve a few more bits in the diff flags word to be used for future whitespace rules. Add WS_INCOMPLETE_LINE without implementing the behaviour (yet).

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/config/core.adoc |  2 ++
 diff.c                         | 16 ++++++++--------
 diff.h                         |  6 +++---
 ws.c                           |  6 ++++++
 ws.h                           |  3 ++-
 5 files changed, 21 insertions(+), 12 deletions(-)
Show changes to 5 files +21 −12

Documentation/config/core.adoc, diff.c, diff.h, ws.c, ws.h

diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc
index e2de270c86..682fb595fb 100644
--- a/Documentation/config/core.adoc
+++ b/Documentation/config/core.adoc
@@ -626,6 +626,8 @@ core.whitespace::
   part of the line terminator, i.e. with it, `trailing-space`
   does not trigger if the character before such a carriage-return
   is not a whitespace (not enabled by default).
+* `incomplete-line` treats the last line of a file that is missing the
+  newline at the end as an error (not enabled by default).
 * `tabwidth=<n>` tells how many character positions a tab occupies; this
   is relevant for `indent-with-non-tab` and when Git fixes `tab-in-indent`
   errors. The default tab width is 8. Allowed values are 1 to 63.
diff --git a/diff.c b/diff.c
index 5c606409bb..1b27b15f84 100644
--- a/diff.c
+++ b/diff.c
@@ -804,15 +804,15 @@ enum diff_symbol {
 
 /*
  * Flags for content lines:
- * 0..11 are whitespace rules (see ws.h)
- * 12..14 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD
- * 16 is marking if the line is blank at EOF
- * 17..19 are used for color-moved.
+ * 0..15 are whitespace rules (see ws.h)
+ * 16..18 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD
+ * 19 is marking if the line is blank at EOF
+ * 20..22 are used for color-moved.
  */
-#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF	(1<<16)
-#define DIFF_SYMBOL_MOVED_LINE			(1<<17)
-#define DIFF_SYMBOL_MOVED_LINE_ALT		(1<<18)
-#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING	(1<<19)
+#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF	(1<<19)
+#define DIFF_SYMBOL_MOVED_LINE			(1<<20)
+#define DIFF_SYMBOL_MOVED_LINE_ALT		(1<<21)
+#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING	(1<<22)
 
 #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)
 
diff --git a/diff.h b/diff.h
index cbd355cf50..422658407d 100644
--- a/diff.h
+++ b/diff.h
@@ -331,9 +331,9 @@ struct diff_options {
 
 	int ita_invisible_in_index;
 /* white-space error highlighting */
-#define WSEH_NEW        (1<<12)
-#define WSEH_CONTEXT    (1<<13)
-#define WSEH_OLD        (1<<14)
+#define WSEH_NEW        (1<<16)
+#define WSEH_CONTEXT    (1<<17)
+#define WSEH_OLD        (1<<18)
 	unsigned ws_error_highlight;
 	const char *prefix;
 	int prefix_length;
diff --git a/ws.c b/ws.c
index 70acee3337..34a7b4fad2 100644
--- a/ws.c
+++ b/ws.c
@@ -26,6 +26,7 @@ static struct whitespace_rule {
 	{ "blank-at-eol", WS_BLANK_AT_EOL, 0 },
 	{ "blank-at-eof", WS_BLANK_AT_EOF, 0 },
 	{ "tab-in-indent", WS_TAB_IN_INDENT, 0, 1 },
+	{ "incomplete-line", WS_INCOMPLETE_LINE, 0, 0 },
 };
 
 unsigned parse_whitespace_rule(const char *string)
@@ -139,6 +140,11 @@ char *whitespace_error_string(unsigned ws)
 			strbuf_addstr(&err, ", ");
 		strbuf_addstr(&err, "tab in indent");
 	}
+	if (ws & WS_INCOMPLETE_LINE) {
+		if (err.len)
+			strbuf_addstr(&err, ", ");
+		strbuf_addstr(&err, "no newline at the end of file");
+	}
 	return strbuf_detach(&err, NULL);
 }
 
diff --git a/ws.h b/ws.h
index 23708efb73..06d5cb73f8 100644
--- a/ws.h
+++ b/ws.h
@@ -15,13 +15,14 @@ struct strbuf;
 #define WS_CR_AT_EOL            (1<<9)
 #define WS_BLANK_AT_EOF         (1<<10)
 #define WS_TAB_IN_INDENT        (1<<11)
+#define WS_INCOMPLETE_LINE      (1<<12)
 
 #define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
 #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)
 #define WS_TAB_WIDTH_MASK       ((1<<6)-1)
 
 /* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */
-#define WS_RULE_MASK            ((1<<12)-1)
+#define WS_RULE_MASK            ((1<<16)-1)
 
 extern unsigned whitespace_rule_cfg;
 unsigned whitespace_rule(struct index_state *, const char *);
-- 
2.52.0-rc2-441-g030905368a
Junio C Hamano· Nov 12, 2025, 22:02 UTC · re: Junio C Hamano · lore

[PATCH v4 10/12] apply: check and fix incomplete lines

The final line of a file that lacks the terminating newline at its end is called an incomplete line. In general they are frowned upon for many reasons (imagine concatenating two files with "cat A B" and what happens when A ends in an incomplete line, for example), and text-oriented tools often mishandle such a line.

Implement checks in "git apply" for incomplete lines, which is off by default for backward compatibility's sake, so that "git apply --whitespace={fix,warn,error}" can notice, warn against, and fix them.

As one of the new test shows, if you modify contents on an incomplete line in the original and leave the resulting line incomplete, it is still considered a whitespace error, the reasoning being that "you'd better fix it while at it if you are making a change on an incomplete line anyway", which may controversial.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 apply.c                  |  13 ++-
 t/t4124-apply-ws-rule.sh | 187 +++++++++++++++++++++++++++++++++++++++
 ws.c                     |  14 +++
 3 files changed, 213 insertions(+), 1 deletion(-)
Show changes to 3 files +213 −1

apply.c, t/t4124-apply-ws-rule.sh, ws.c

diff --git a/apply.c b/apply.c
index 2b0f8bdab5..c9fb45247d 100644
--- a/apply.c
+++ b/apply.c
@@ -1640,6 +1640,14 @@ static void record_ws_error(struct apply_state *state,
 	    state->squelch_whitespace_errors < state->whitespace_error)
 		return;
 
+	/*
+	 * line[len] for an incomplete line points at the "\n" at the end
+	 * of patch input line, so "%.*s" would drop the last letter on line;
+	 * compensate for it.
+	 */
+	if (result & WS_INCOMPLETE_LINE)
+		len++;
+
 	err = whitespace_error_string(result);
 	if (state->apply_verbosity > verbosity_silent)
 		fprintf(stderr, "%s:%d: %s.\n%.*s\n",
@@ -1794,7 +1802,10 @@ static int parse_fragment(struct apply_state *state,
 		}
 
 		/* eat the "\\ No newline..." as well, if exists */
-		len += skip_len;
+		if (skip_len) {
+			len += skip_len;
+			state->linenr++;
+		}
 	}
 	if (oldlines || newlines)
 		return -1;
diff --git a/t/t4124-apply-ws-rule.sh b/t/t4124-apply-ws-rule.sh
index 485c7d2d12..115a0f8579 100755
--- a/t/t4124-apply-ws-rule.sh
+++ b/t/t4124-apply-ws-rule.sh
@@ -556,4 +556,191 @@ test_expect_success 'whitespace check skipped for excluded paths' '
 	git apply --include=used --stat --whitespace=error <patch
 '
 
+test_expect_success 'check incomplete lines (setup)' '
+	rm -f .gitattributes &&
+	git config core.whitespace incomplete-line
+'
+
+test_expect_success 'incomplete context line (not an error)' '
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	(test_write_lines 1 2 3 0 5 && printf 6) >sample2-i &&
+	cat sample-i >target &&
+	git add target &&
+	cat sample2-i >target &&
+	git diff-files -p target >patch &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error <patch &&
+	test_cmp sample2-i target &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error --check <patch 2>error &&
+	test_cmp sample-i target &&
+	test_must_be_empty error &&
+
+	cat sample2-i >target &&
+	git apply --whitespace=error -R <patch &&
+	test_cmp sample-i target &&
+
+	cat sample2-i >target &&
+	git apply -R --whitespace=error --check <patch 2>error &&
+	test_cmp sample2-i target &&
+	test_must_be_empty error
+'
+
+test_expect_success 'last line made incomplete (error)' '
+	test_write_lines 1 2 3 4 5 6 >sample &&
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	cat sample >target &&
+	git add target &&
+	cat sample-i >target &&
+	git diff-files -p target >patch &&
+
+	cat sample >target &&
+	test_must_fail git apply --whitespace=error <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample >target &&
+	test_must_fail git apply --whitespace=error --check <patch 2>actual &&
+	test_cmp sample target &&
+	cat >expect <<-\EOF &&
+	<stdin>:10: no newline at the end of file.
+	6
+	error: 1 line adds whitespace errors.
+	EOF
+	test_cmp expect actual &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error -R <patch &&
+	test_cmp sample target &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error --check -R <patch 2>error &&
+	test_cmp sample-i target &&
+	test_must_be_empty error &&
+
+	cat sample >target &&
+	git apply --whitespace=fix <patch &&
+	test_cmp sample target
+'
+
+test_expect_success 'incomplete line removed at the end (not an error)' '
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	test_write_lines 1 2 3 4 5 6 >sample &&
+	cat sample-i >target &&
+	git add target &&
+	cat sample >target &&
+	git diff-files -p target >patch &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error <patch &&
+	test_cmp sample target &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error --check <patch 2>error &&
+	test_cmp sample-i target &&
+	test_must_be_empty error &&
+
+	cat sample >target &&
+	test_must_fail git apply --whitespace=error -R <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample >target &&
+	test_must_fail git apply --whitespace=error --check -R <patch 2>actual &&
+	test_cmp sample target &&
+	cat >expect <<-\EOF &&
+	<stdin>:9: no newline at the end of file.
+	6
+	error: 1 line adds whitespace errors.
+	EOF
+	test_cmp expect actual &&
+
+	cat sample >target &&
+	git apply --whitespace=fix -R <patch &&
+	test_cmp sample target
+'
+
+test_expect_success 'incomplete line corrected at the end (not an error)' '
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	test_write_lines 1 2 3 4 5 7 >sample3 &&
+	cat sample-i >target &&
+	git add target &&
+	cat sample3 >target &&
+	git diff-files -p target >patch &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error <patch &&
+	test_cmp sample3 target &&
+
+	cat sample-i >target &&
+	git apply --whitespace=error --check <patch 2>error &&
+	test_cmp sample-i target &&
+	test_must_be_empty error &&
+
+	cat sample3 >target &&
+	test_must_fail git apply --whitespace=error -R <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample3 >target &&
+	test_must_fail git apply --whitespace=error -R --check <patch 2>actual &&
+	test_cmp sample3 target &&
+	cat >expect <<-\EOF &&
+	<stdin>:9: no newline at the end of file.
+	6
+	error: 1 line adds whitespace errors.
+	EOF
+	test_cmp expect actual &&
+
+	cat sample3 >target &&
+	git apply --whitespace=fix -R <patch &&
+	test_cmp sample target
+'
+
+test_expect_success 'incomplete line modified at the end (error)' '
+	(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&
+	(test_write_lines 1 2 3 4 5 && printf 7) >sample3-i &&
+	test_write_lines 1 2 3 4 5 6 >sample &&
+	test_write_lines 1 2 3 4 5 7 >sample3 &&
+	cat sample-i >target &&
+	git add target &&
+	cat sample3-i >target &&
+	git diff-files -p target >patch &&
+
+	cat sample-i >target &&
+	test_must_fail git apply --whitespace=error <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample-i >target &&
+	test_must_fail git apply --whitespace=error --check <patch 2>actual &&
+	test_cmp sample-i target &&
+	cat >expect <<-\EOF &&
+	<stdin>:11: no newline at the end of file.
+	7
+	error: 1 line adds whitespace errors.
+	EOF
+	test_cmp expect actual &&
+
+	cat sample3-i >target &&
+	test_must_fail git apply --whitespace=error -R <patch 2>error &&
+	test_grep "no newline" error &&
+
+	cat sample3-i >target &&
+	test_must_fail git apply --whitespace=error --check -R <patch 2>actual &&
+	test_cmp sample3-i target &&
+	cat >expect <<-\EOF &&
+	<stdin>:9: no newline at the end of file.
+	6
+	error: 1 line adds whitespace errors.
+	EOF
+	test_cmp expect actual &&
+
+	cat sample-i >target &&
+	git apply --whitespace=fix <patch &&
+	test_cmp sample3 target &&
+
+	cat sample3-i >target &&
+	git apply --whitespace=fix -R <patch &&
+	test_cmp sample target
+'
+
 test_done
diff --git a/ws.c b/ws.c
index 34a7b4fad2..6cc2466c0c 100644
--- a/ws.c
+++ b/ws.c
@@ -186,6 +186,9 @@ static unsigned ws_check_emit_1(const char *line, int len, unsigned ws_rule,
 	if (trailing_whitespace == -1)
 		trailing_whitespace = len;
 
+	if (!trailing_newline && (ws_rule & WS_INCOMPLETE_LINE))
+		result |= WS_INCOMPLETE_LINE;
+
 	/* Check indentation */
 	for (i = 0; i < trailing_whitespace; i++) {
 		if (line[i] == ' ')
@@ -297,6 +300,17 @@ void ws_fix_copy(struct strbuf *dst, const char *src, int len, unsigned ws_rule,
 	int last_space_in_indent = -1;
 	int need_fix_leading_space = 0;
 
+	/*
+	 * Remembering that we need to add '\n' at the end
+	 * is sufficient to fix an incomplete line.
+	 */
+	if (ws_rule & WS_INCOMPLETE_LINE) {
+		if (0 < len && src[len - 1] != '\n') {
+			fixed = 1;
+			add_nl_to_tail = 1;
+		}
+	}
+
 	/*
 	 * Strip trailing whitespace
 	 */
-- 
2.52.0-rc2-441-g030905368a
Junio C Hamano· Nov 12, 2025, 22:02 UTC · re: Junio C Hamano · lore

[PATCH v4 11/12] 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>
---
 diff.c                     | 29 +++++++++++++++--
 t/t4015-diff-whitespace.sh | 67 +++++++++++++++++++++++++++++++++++---
 2 files changed, 90 insertions(+), 6 deletions(-)
Show changes to 2 files +90 −6

diff.c, t/t4015-diff-whitespace.sh

diff --git a/diff.c b/diff.c
index 1b27b15f84..7b7cd50dc2 100644
--- a/diff.c
+++ b/diff.c
@@ -1370,7 +1370,11 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
 		emit_line(o, "", "", line, len);
 		break;
 	case DIFF_SYMBOL_CONTEXT_INCOMPLETE:
-		set = diff_get_color_opt(o, DIFF_CONTEXT);
+		if ((flags & WS_INCOMPLETE_LINE) &&
+		    (flags & o->ws_error_highlight))
+			set = diff_get_color_opt(o, DIFF_WHITESPACE);
+		else
+			set = diff_get_color_opt(o, DIFF_CONTEXT);
 		reset = diff_get_color_opt(o, DIFF_RESET);
 		emit_line(o, set, reset, line, len);
 		break;
@@ -1666,8 +1670,14 @@ static void emit_context_line(struct emit_callback *ecbdata,
 static void emit_incomplete_line_marker(struct emit_callback *ecbdata,
 					const char *line, int len)
 {
+	int last_line_kind = ecbdata->last_line_kind;
+	unsigned flags = (last_line_kind == '+'
+			  ? WSEH_NEW
+			  : last_line_kind == '-'
+			  ? WSEH_OLD
+			  : WSEH_CONTEXT) | ecbdata->ws_rule;
 	emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
-			 line, len, 0);
+			 line, len, flags);
 }
 
 static void emit_hunk_header(struct emit_callback *ecbdata,
@@ -3254,6 +3264,7 @@ struct checkdiff_t {
 	struct diff_options *o;
 	unsigned ws_rule;
 	unsigned status;
+	int last_line_kind;
 };
 
 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,
 static int checkdiff_consume(void *priv, char *line, unsigned long len)
 {
 	struct checkdiff_t *data = priv;
+	int last_line_kind;
 	int marker_size = data->conflict_marker_size;
 	const char *ws = diff_get_color(data->o->use_color, DIFF_WHITESPACE);
 	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)
 	assert(data->o);
 	line_prefix = diff_line_prefix(data->o);
 
+	last_line_kind = data->last_line_kind;
+	data->last_line_kind = line[0];
 	if (line[0] == '+') {
 		unsigned bad;
 		data->lineno++;
@@ -3324,6 +3338,17 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)
 			      data->o->file, set, reset, ws);
 	} else if (line[0] == ' ') {
 		data->lineno++;
+	} else if (line[0] == '\\') {
+		/* no newline at the end of the line */
+		if ((data->ws_rule & WS_INCOMPLETE_LINE) &&
+		    (last_line_kind == '+')) {
+			unsigned bad = WS_INCOMPLETE_LINE;
+			data->status |= bad;
+			err = whitespace_error_string(bad);
+			fprintf(data->o->file, "%s%s:%d: %s.\n",
+				line_prefix, data->filename, data->lineno, err);
+			free(err);
+		}
 	}
 	return 0;
 }
diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh
index 9de7f73f42..3c8eb02e4f 100755
--- a/t/t4015-diff-whitespace.sh
+++ b/t/t4015-diff-whitespace.sh
@@ -43,6 +43,53 @@ do
 	'
 done
 
+test_expect_success "incomplete line in both pre- and post-image context" '
+	(echo foo && echo baz | tr -d "\012") >x &&
+	git add x &&
+	(echo bar && echo baz | tr -d "\012") >x &&
+	git diff x &&
+	git -c core.whitespace=incomplete diff --check x &&
+	git diff -R x &&
+	git -c core.whitespace=incomplete diff -R --check x
+'
+
+test_expect_success "incomplete lines on both pre- and post-image" '
+	# The interpretation taken here is "since you are touching
+	# the line anyway, you would better fix the incomplete line
+	# while you are at it."  but this is debatable.
+	echo foo | tr -d "\012" >x &&
+	git add x &&
+	echo bar | tr -d "\012" >x &&
+	git diff x &&
+	test_must_fail git -c core.whitespace=incomplete diff --check x >error &&
+	test_grep "no newline at the end of file" error &&
+	git diff -R x &&
+	test_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&
+	test_grep "no newline at the end of file" error
+'
+
+test_expect_success "fix incomplete line in pre-image" '
+	echo foo | tr -d "\012" >x &&
+	git add x &&
+	echo bar >x &&
+	git diff x &&
+	git -c core.whitespace=incomplete diff --check x &&
+	git diff -R x &&
+	test_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&
+	test_grep "no newline at the end of file" error
+'
+
+test_expect_success "new incomplete line in post-image" '
+	echo foo >x &&
+	git add x &&
+	echo bar | tr -d "\012" >x &&
+	git diff x &&
+	test_must_fail git -c core.whitespace=incomplete diff --check x >error &&
+	test_grep "no newline at the end of file" error &&
+	git diff -R x &&
+	git -c core.whitespace=incomplete diff -R --check x
+'
+
 test_expect_success "Ray Lehtiniemi's example" '
 	cat <<-\EOF >x &&
 	do {
@@ -1040,7 +1087,8 @@ test_expect_success 'ws-error-highlight test setup' '
 	{
 		echo "0. blank-at-eol " &&
 		echo "1. still-blank-at-eol " &&
-		echo "2. and a new line "
+		echo "2. and a new line " &&
+		printf "3. and more"
 	} >x &&
 	new_hash_x=$(git hash-object x) &&
 	after=$(git rev-parse --short "$new_hash_x") &&
@@ -1050,11 +1098,13 @@ test_expect_success 'ws-error-highlight test setup' '
 	<BOLD>index $before..$after 100644<RESET>
 	<BOLD>--- a/x<RESET>
 	<BOLD>+++ b/x<RESET>
-	<CYAN>@@ -1,2 +1,3 @@<RESET>
+	<CYAN>@@ -1,2 +1,4 @@<RESET>
 	 0. blank-at-eol <RESET>
 	<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>
 	<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>
 	<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>
+	<GREEN>+<RESET><GREEN>3. and more<RESET>
+	<BLUE>\ No newline at end of file<RESET>
 	EOF
 
 	cat >expect.all <<-EOF &&
@@ -1062,11 +1112,13 @@ test_expect_success 'ws-error-highlight test setup' '
 	<BOLD>index $before..$after 100644<RESET>
 	<BOLD>--- a/x<RESET>
 	<BOLD>+++ b/x<RESET>
-	<CYAN>@@ -1,2 +1,3 @@<RESET>
+	<CYAN>@@ -1,2 +1,4 @@<RESET>
 	 <RESET>0. blank-at-eol<RESET><BLUE> <RESET>
 	<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>
 	<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>
 	<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>
+	<GREEN>+<RESET><GREEN>3. and more<RESET>
+	<BLUE>\ No newline at end of file<RESET>
 	EOF
 
 	cat >expect.none <<-EOF
@@ -1074,16 +1126,19 @@ test_expect_success 'ws-error-highlight test setup' '
 	<BOLD>index $before..$after 100644<RESET>
 	<BOLD>--- a/x<RESET>
 	<BOLD>+++ b/x<RESET>
-	<CYAN>@@ -1,2 +1,3 @@<RESET>
+	<CYAN>@@ -1,2 +1,4 @@<RESET>
 	 0. blank-at-eol <RESET>
 	<RED>-1. blank-at-eol <RESET>
 	<GREEN>+1. still-blank-at-eol <RESET>
 	<GREEN>+2. and a new line <RESET>
+	<GREEN>+3. and more<RESET>
+	\ No newline at end of file<RESET>
 	EOF
 
 '
 
 test_expect_success 'test --ws-error-highlight option' '
+	git config core.whitespace blank-at-eol,incomplete-line &&
 
 	git diff --color --ws-error-highlight=default,old >current.raw &&
 	test_decode_color <current.raw >current &&
@@ -1100,6 +1155,7 @@ test_expect_success 'test --ws-error-highlight option' '
 '
 
 test_expect_success 'test diff.wsErrorHighlight config' '
+	git config core.whitespace blank-at-eol,incomplete-line &&
 
 	git -c diff.wsErrorHighlight=default,old diff --color >current.raw &&
 	test_decode_color <current.raw >current &&
@@ -1116,6 +1172,7 @@ test_expect_success 'test diff.wsErrorHighlight config' '
 '
 
 test_expect_success 'option overrides diff.wsErrorHighlight' '
+	git config core.whitespace blank-at-eol,incomplete-line &&
 
 	git -c diff.wsErrorHighlight=none \
 		diff --color --ws-error-highlight=default,old >current.raw &&
@@ -1135,6 +1192,8 @@ test_expect_success 'option overrides diff.wsErrorHighlight' '
 '
 
 test_expect_success 'detect moved code, complete file' '
+	git config core.whitespace blank-at-eol &&
+
 	git reset --hard &&
 	cat <<-\EOF >test.c &&
 	#include<stdio.h>
-- 
2.52.0-rc2-441-g030905368a
Junio C Hamano· Nov 12, 2025, 22:02 UTC · re: Junio C Hamano · lore

[PATCH v4 12/12] attr: enable incomplete-line whitespace error for this project

Now "git diff --check" and "git apply --whitespace=warn/fix" learned incomplete line is a whitespace error, enable them for this project to prevent patches to add new incomplete lines to our source to both code and documentation files.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 .gitattributes | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)
Show changes to .gitattributes +3 −3
diff --git a/.gitattributes b/.gitattributes
index 32583149c2..673527dd67 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,13 +1,13 @@
 * whitespace=!indent,trail,space
-*.[ch] whitespace=indent,trail,space diff=cpp
-*.sh whitespace=indent,trail,space text eol=lf
+*.[ch] whitespace=indent,trail,space,incomplete diff=cpp
+*.sh whitespace=indent,trail,space,incomplete text eol=lf
 *.perl text eol=lf diff=perl
 *.pl text eof=lf diff=perl
 *.pm text eol=lf diff=perl
 *.py text eol=lf diff=python
 *.bat text eol=crlf
 CODE_OF_CONDUCT.md -whitespace
-/Documentation/**/*.adoc text eol=lf
+/Documentation/**/*.adoc text eol=lf whitespace=!indent,trail,space,incomplete
 /command-list.txt text eol=lf
 /GIT-VERSION-GEN text eol=lf
 /mergetools/* text eol=lf
-- 
2.52.0-rc2-441-g030905368a
Phillip Wood· Nov 14, 2025, 10:24 UTC · re: Junio C Hamano · lore

Re: [PATCH v4 00/12] Incomplete lines

On 12/11/2025 22:02, Junio C Hamano wrote:
Show 6 quoted lines
> Changes in v4:
> 
>   - The preliminary clean-up for "git diff" in [04/12] no longer
>     "corrects" the code that updates the line number upon seeing
>     "\ No newline" marker, and leaves it for later series to clean
>     it up as #leftoverbits.

I agree it makes sense to leave the existing increment alone as it's not really related to this series.

Show 5 quoted lines
>   - Our house rule updates in [12/12] now forbids the documentation
>     sources *.adoc to end in an incomplete line.
> 12:  068229790d ! 12:  cb86d9b45f attr: enable incomplete-line whitespace error for this project
>      +-/Documentation/**/*.adoc text eol=lf
>      ++/Documentation/**/*.adoc text eol=lf whitespace=!indent,trail,space,incomplete

Should that be "-indent" c.f. 358e94dc705 (.gitattributes: remove misspelled no-op whitespace attribute, 2025-11-11)

Thanks
Phillip
Junio C Hamano· Nov 14, 2025, 16:25 UTC · re: Phillip Wood · lore

Re: [PATCH v4 00/12] Incomplete lines

Phillip Wood <phillip.wood123@gmail.com> writes:
Show 18 quoted lines
> On 12/11/2025 22:02, Junio C Hamano wrote:
>> Changes in v4:
>> 
>>   - The preliminary clean-up for "git diff" in [04/12] no longer
>>     "corrects" the code that updates the line number upon seeing
>>     "\ No newline" marker, and leaves it for later series to clean
>>     it up as #leftoverbits.
>
> I agree it makes sense to leave the existing increment alone as it's not 
> really related to this series.
>>   - Our house rule updates in [12/12] now forbids the documentation
>>     sources *.adoc to end in an incomplete line.
>> 12:  068229790d ! 12:  cb86d9b45f attr: enable incomplete-line whitespace error for this project
>>      +-/Documentation/**/*.adoc text eol=lf
>>      ++/Documentation/**/*.adoc text eol=lf whitespace=!indent,trail,space,incomplete
>
> Should that be "-indent" c.f. 358e94dc705 (.gitattributes: remove 
> misspelled no-op whitespace attribute, 2025-11-11)
Oops.  Thanks for spotting.
Junio C Hamano· Nov 23, 2025, 02:35 UTC · re: Junio C Hamano · lore

Re: [PATCH v4 00/12] Incomplete lines

Junio C Hamano <gitster@pobox.com> writes:
Show 22 quoted lines
> Phillip Wood <phillip.wood123@gmail.com> writes:
>
>> On 12/11/2025 22:02, Junio C Hamano wrote:
>>> Changes in v4:
>>> 
>>>   - The preliminary clean-up for "git diff" in [04/12] no longer
>>>     "corrects" the code that updates the line number upon seeing
>>>     "\ No newline" marker, and leaves it for later series to clean
>>>     it up as #leftoverbits.
>>
>> I agree it makes sense to leave the existing increment alone as it's not 
>> really related to this series.
>>>   - Our house rule updates in [12/12] now forbids the documentation
>>>     sources *.adoc to end in an incomplete line.
>>> 12:  068229790d ! 12:  cb86d9b45f attr: enable incomplete-line whitespace error for this project
>>>      +-/Documentation/**/*.adoc text eol=lf
>>>      ++/Documentation/**/*.adoc text eol=lf whitespace=!indent,trail,space,incomplete
>>
>> Should that be "-indent" c.f. 358e94dc705 (.gitattributes: remove 
>> misspelled no-op whitespace attribute, 2025-11-11)
>
> Oops.  Thanks for spotting.

This has been locally corrected. We saw no other comments on this iteration, and hopefully it is not due to lack of interest. Let me mark the topic for 'next' and merge it down.

----- >8 -----
Subject: [PATCH v3bis 12/12] attr: enable incomplete-line whitespace error for this project

Now "git diff --check" and "git apply --whitespace=warn/fix" learned incomplete line is a whitespace error, enable them for this project to prevent patches to add new incomplete lines to our source to both code and documentation files.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 .gitattributes | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)
Show changes to .gitattributes +3 −3
diff --git a/.gitattributes b/.gitattributes
index 32583149c2..a8e2950a73 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,13 +1,13 @@
 * whitespace=!indent,trail,space
-*.[ch] whitespace=indent,trail,space diff=cpp
-*.sh whitespace=indent,trail,space text eol=lf
+*.[ch] whitespace=indent,trail,space,incomplete diff=cpp
+*.sh whitespace=indent,trail,space,incomplete text eol=lf
 *.perl text eol=lf diff=perl
 *.pl text eof=lf diff=perl
 *.pm text eol=lf diff=perl
 *.py text eol=lf diff=python
 *.bat text eol=crlf
 CODE_OF_CONDUCT.md -whitespace
-/Documentation/**/*.adoc text eol=lf
+/Documentation/**/*.adoc text eol=lf whitespace=trail,space,incomplete
 /command-list.txt text eol=lf
 /GIT-VERSION-GEN text eol=lf
 /mergetools/* text eol=lf
-- 
2.52.0-168-g07aa2ddc22

← back to recent threads