git.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH 00/12] Incomplete lines
@ 2025-11-04  2:09 Junio C Hamano
  2025-11-04  2:09 ` [PATCH 01/12] whitespace: correct bit assignment comments Junio C Hamano
                   ` (12 more replies)
  0 siblings, 13 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-04  2:09 UTC (permalink / raw)
  To: git

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


^ permalink raw reply	[flat|nested] 73+ messages in thread

* [PATCH 01/12] whitespace: correct bit assignment comments
  2025-11-04  2:09 [PATCH 00/12] Incomplete lines Junio C Hamano
@ 2025-11-04  2:09 ` Junio C Hamano
  2025-11-04  2:09 ` [PATCH 02/12] diff: emit_line_ws_markup() if/else style fix Junio C Hamano
                   ` (11 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-04  2:09 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH 02/12] diff: emit_line_ws_markup() if/else style fix
  2025-11-04  2:09 [PATCH 00/12] Incomplete lines Junio C Hamano
  2025-11-04  2:09 ` [PATCH 01/12] whitespace: correct bit assignment comments Junio C Hamano
@ 2025-11-04  2:09 ` Junio C Hamano
  2025-11-04  2:09 ` [PATCH 03/12] diff: correct suppress_blank_empty hack Junio C Hamano
                   ` (10 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-04  2:09 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH 03/12] diff: correct suppress_blank_empty hack
  2025-11-04  2:09 [PATCH 00/12] Incomplete lines Junio C Hamano
  2025-11-04  2:09 ` [PATCH 01/12] whitespace: correct bit assignment comments Junio C Hamano
  2025-11-04  2:09 ` [PATCH 02/12] diff: emit_line_ws_markup() if/else style fix Junio C Hamano
@ 2025-11-04  2:09 ` Junio C Hamano
  2025-11-04  2:09 ` [PATCH 04/12] diff: fix incorrect counting of line numbers Junio C Hamano
                   ` (9 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-04  2:09 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH 04/12] diff: fix incorrect counting of line numbers
  2025-11-04  2:09 [PATCH 00/12] Incomplete lines Junio C Hamano
                   ` (2 preceding siblings ...)
  2025-11-04  2:09 ` [PATCH 03/12] diff: correct suppress_blank_empty hack Junio C Hamano
@ 2025-11-04  2:09 ` Junio C Hamano
  2025-11-10 14:54   ` Phillip Wood
  2025-11-04  2:09 ` [PATCH 05/12] diff: refactor output of incomplete line Junio C Hamano
                   ` (8 subsequent siblings)
  12 siblings, 1 reply; 73+ messages in thread
From: Junio C Hamano @ 2025-11-04  2:09 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH 05/12] diff: refactor output of incomplete line
  2025-11-04  2:09 [PATCH 00/12] Incomplete lines Junio C Hamano
                   ` (3 preceding siblings ...)
  2025-11-04  2:09 ` [PATCH 04/12] diff: fix incorrect counting of line numbers Junio C Hamano
@ 2025-11-04  2:09 ` Junio C Hamano
  2025-11-04  2:09 ` [PATCH 06/12] diff: call emit_callback ecbdata everywhere Junio C Hamano
                   ` (7 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-04  2:09 UTC (permalink / raw)
  To: git

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);
@@ -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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH 06/12] diff: call emit_callback ecbdata everywhere
  2025-11-04  2:09 [PATCH 00/12] Incomplete lines Junio C Hamano
                   ` (4 preceding siblings ...)
  2025-11-04  2:09 ` [PATCH 05/12] diff: refactor output of incomplete line Junio C Hamano
@ 2025-11-04  2:09 ` Junio C Hamano
  2025-11-04  2:09 ` [PATCH 07/12] diff: update the way rewrite diff handles incomplete lines Junio C Hamano
                   ` (6 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-04  2:09 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH 07/12] diff: update the way rewrite diff handles incomplete lines
  2025-11-04  2:09 [PATCH 00/12] Incomplete lines Junio C Hamano
                   ` (5 preceding siblings ...)
  2025-11-04  2:09 ` [PATCH 06/12] diff: call emit_callback ecbdata everywhere Junio C Hamano
@ 2025-11-04  2:09 ` Junio C Hamano
  2025-11-10 14:54   ` Phillip Wood
  2025-11-04  2:09 ` [PATCH 08/12] apply: revamp the parsing of " Junio C Hamano
                   ` (5 subsequent siblings)
  12 siblings, 1 reply; 73+ messages in thread
From: Junio C Hamano @ 2025-11-04  2:09 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH 08/12] apply: revamp the parsing of incomplete lines
  2025-11-04  2:09 [PATCH 00/12] Incomplete lines Junio C Hamano
                   ` (6 preceding siblings ...)
  2025-11-04  2:09 ` [PATCH 07/12] diff: update the way rewrite diff handles incomplete lines Junio C Hamano
@ 2025-11-04  2:09 ` Junio C Hamano
  2025-11-04  2:09 ` [PATCH 09/12] whitespace: allocate a few more bits Junio C Hamano
                   ` (4 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-04  2:09 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH 09/12] whitespace: allocate a few more bits
  2025-11-04  2:09 [PATCH 00/12] Incomplete lines Junio C Hamano
                   ` (7 preceding siblings ...)
  2025-11-04  2:09 ` [PATCH 08/12] apply: revamp the parsing of " Junio C Hamano
@ 2025-11-04  2:09 ` Junio C Hamano
  2025-11-04  2:09 ` [PATCH 10/12] apply: check and fix incomplete lines Junio C Hamano
                   ` (3 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-04  2:09 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH 10/12] apply: check and fix incomplete lines
  2025-11-04  2:09 [PATCH 00/12] Incomplete lines Junio C Hamano
                   ` (8 preceding siblings ...)
  2025-11-04  2:09 ` [PATCH 09/12] whitespace: allocate a few more bits Junio C Hamano
@ 2025-11-04  2:09 ` Junio C Hamano
  2025-11-04  2:09 ` [PATCH 11/12] diff: highlight and error out on " Junio C Hamano
                   ` (2 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-04  2:09 UTC (permalink / raw)
  To: git

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(+)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH 11/12] diff: highlight and error out on incomplete lines
  2025-11-04  2:09 [PATCH 00/12] Incomplete lines Junio C Hamano
                   ` (9 preceding siblings ...)
  2025-11-04  2:09 ` [PATCH 10/12] apply: check and fix incomplete lines Junio C Hamano
@ 2025-11-04  2:09 ` Junio C Hamano
  2025-11-10 14:55   ` Phillip Wood
  2025-11-04  2:09 ` [PATCH 12/12] attr: enable incomplete-line whitespace error for this project Junio C Hamano
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
  12 siblings, 1 reply; 73+ messages in thread
From: Junio C Hamano @ 2025-11-04  2:09 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH 12/12] attr: enable incomplete-line whitespace error for this project
  2025-11-04  2:09 [PATCH 00/12] Incomplete lines Junio C Hamano
                   ` (10 preceding siblings ...)
  2025-11-04  2:09 ` [PATCH 11/12] diff: highlight and error out on " Junio C Hamano
@ 2025-11-04  2:09 ` Junio C Hamano
  2025-11-10 14:55   ` Phillip Wood
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
  12 siblings, 1 reply; 73+ messages in thread
From: Junio C Hamano @ 2025-11-04  2:09 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v2 00/12] Incomplete lines
  2025-11-04  2:09 [PATCH 00/12] Incomplete lines Junio C Hamano
                   ` (11 preceding siblings ...)
  2025-11-04  2:09 ` [PATCH 12/12] attr: enable incomplete-line whitespace error for this project Junio C Hamano
@ 2025-11-05 21:30 ` Junio C Hamano
  2025-11-05 21:30   ` [PATCH v2 01/12] whitespace: correct bit assignment comments Junio C Hamano
                     ` (14 more replies)
  12 siblings, 15 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-05 21:30 UTC (permalink / raw)
  To: git

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


^ permalink raw reply	[flat|nested] 73+ messages in thread

* [PATCH v2 01/12] whitespace: correct bit assignment comments
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
@ 2025-11-05 21:30   ` Junio C Hamano
  2025-11-05 21:30   ` [PATCH v2 02/12] diff: emit_line_ws_markup() if/else style fix Junio C Hamano
                     ` (13 subsequent siblings)
  14 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-05 21:30 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v2 02/12] diff: emit_line_ws_markup() if/else style fix
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
  2025-11-05 21:30   ` [PATCH v2 01/12] whitespace: correct bit assignment comments Junio C Hamano
@ 2025-11-05 21:30   ` Junio C Hamano
  2025-11-05 21:30   ` [PATCH v2 03/12] diff: correct suppress_blank_empty hack Junio C Hamano
                     ` (12 subsequent siblings)
  14 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-05 21:30 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v2 03/12] diff: correct suppress_blank_empty hack
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
  2025-11-05 21:30   ` [PATCH v2 01/12] whitespace: correct bit assignment comments Junio C Hamano
  2025-11-05 21:30   ` [PATCH v2 02/12] diff: emit_line_ws_markup() if/else style fix Junio C Hamano
@ 2025-11-05 21:30   ` Junio C Hamano
  2025-11-05 21:30   ` [PATCH v2 04/12] diff: fix incorrect counting of line numbers Junio C Hamano
                     ` (11 subsequent siblings)
  14 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-05 21:30 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v2 04/12] diff: fix incorrect counting of line numbers
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
                     ` (2 preceding siblings ...)
  2025-11-05 21:30   ` [PATCH v2 03/12] diff: correct suppress_blank_empty hack Junio C Hamano
@ 2025-11-05 21:30   ` Junio C Hamano
  2025-11-05 21:30   ` [PATCH v2 05/12] diff: refactor output of incomplete line Junio C Hamano
                     ` (10 subsequent siblings)
  14 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-05 21:30 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v2 05/12] diff: refactor output of incomplete line
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
                     ` (3 preceding siblings ...)
  2025-11-05 21:30   ` [PATCH v2 04/12] diff: fix incorrect counting of line numbers Junio C Hamano
@ 2025-11-05 21:30   ` Junio C Hamano
  2025-11-10 10:06     ` Patrick Steinhardt
  2025-11-05 21:30   ` [PATCH v2 06/12] diff: call emit_callback ecbdata everywhere Junio C Hamano
                     ` (9 subsequent siblings)
  14 siblings, 1 reply; 73+ messages in thread
From: Junio C Hamano @ 2025-11-05 21:30 UTC (permalink / raw)
  To: git

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);
@@ -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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v2 06/12] diff: call emit_callback ecbdata everywhere
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
                     ` (4 preceding siblings ...)
  2025-11-05 21:30   ` [PATCH v2 05/12] diff: refactor output of incomplete line Junio C Hamano
@ 2025-11-05 21:30   ` Junio C Hamano
  2025-11-05 21:30   ` [PATCH v2 07/12] diff: update the way rewrite diff handles incomplete lines Junio C Hamano
                     ` (8 subsequent siblings)
  14 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-05 21:30 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v2 07/12] diff: update the way rewrite diff handles incomplete lines
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
                     ` (5 preceding siblings ...)
  2025-11-05 21:30   ` [PATCH v2 06/12] diff: call emit_callback ecbdata everywhere Junio C Hamano
@ 2025-11-05 21:30   ` Junio C Hamano
  2025-11-10 10:06     ` Patrick Steinhardt
  2025-11-05 21:30   ` [PATCH v2 08/12] apply: revamp the parsing of " Junio C Hamano
                     ` (7 subsequent siblings)
  14 siblings, 1 reply; 73+ messages in thread
From: Junio C Hamano @ 2025-11-05 21:30 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v2 08/12] apply: revamp the parsing of incomplete lines
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
                     ` (6 preceding siblings ...)
  2025-11-05 21:30   ` [PATCH v2 07/12] diff: update the way rewrite diff handles incomplete lines Junio C Hamano
@ 2025-11-05 21:30   ` Junio C Hamano
  2025-11-05 21:30   ` [PATCH v2 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE Junio C Hamano
                     ` (6 subsequent siblings)
  14 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-05 21:30 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v2 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
                     ` (7 preceding siblings ...)
  2025-11-05 21:30   ` [PATCH v2 08/12] apply: revamp the parsing of " Junio C Hamano
@ 2025-11-05 21:30   ` Junio C Hamano
  2025-11-05 21:30   ` [PATCH v2 10/12] apply: check and fix incomplete lines Junio C Hamano
                     ` (5 subsequent siblings)
  14 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-05 21:30 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v2 10/12] apply: check and fix incomplete lines
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
                     ` (8 preceding siblings ...)
  2025-11-05 21:30   ` [PATCH v2 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE Junio C Hamano
@ 2025-11-05 21:30   ` Junio C Hamano
  2025-11-05 21:30   ` [PATCH v2 11/12] diff: highlight and error out on " Junio C Hamano
                     ` (4 subsequent siblings)
  14 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-05 21:30 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v2 11/12] diff: highlight and error out on incomplete lines
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
                     ` (9 preceding siblings ...)
  2025-11-05 21:30   ` [PATCH v2 10/12] apply: check and fix incomplete lines Junio C Hamano
@ 2025-11-05 21:30   ` Junio C Hamano
  2025-11-05 21:30   ` [PATCH v2 12/12] attr: enable incomplete-line whitespace error for this project Junio C Hamano
                     ` (3 subsequent siblings)
  14 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-05 21:30 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v2 12/12] attr: enable incomplete-line whitespace error for this project
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
                     ` (10 preceding siblings ...)
  2025-11-05 21:30   ` [PATCH v2 11/12] diff: highlight and error out on " Junio C Hamano
@ 2025-11-05 21:30   ` Junio C Hamano
  2025-11-10 10:09   ` [PATCH v2 00/12] Incomplete lines Patrick Steinhardt
                     ` (2 subsequent siblings)
  14 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-05 21:30 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* Re: [PATCH v2 05/12] diff: refactor output of incomplete line
  2025-11-05 21:30   ` [PATCH v2 05/12] diff: refactor output of incomplete line Junio C Hamano
@ 2025-11-10 10:06     ` Patrick Steinhardt
  2025-11-10 17:58       ` Junio C Hamano
  0 siblings, 1 reply; 73+ messages in thread
From: Patrick Steinhardt @ 2025-11-10 10:06 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git

On Wed, Nov 05, 2025 at 01:30:45PM -0800, Junio C Hamano wrote:
> 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

^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH v2 07/12] diff: update the way rewrite diff handles incomplete lines
  2025-11-05 21:30   ` [PATCH v2 07/12] diff: update the way rewrite diff handles incomplete lines Junio C Hamano
@ 2025-11-10 10:06     ` Patrick Steinhardt
  2025-11-10 18:14       ` Junio C Hamano
  0 siblings, 1 reply; 73+ messages in thread
From: Patrick Steinhardt @ 2025-11-10 10:06 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git

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.

> 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

^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH v2 00/12] Incomplete lines
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
                     ` (11 preceding siblings ...)
  2025-11-05 21:30   ` [PATCH v2 12/12] attr: enable incomplete-line whitespace error for this project Junio C Hamano
@ 2025-11-10 10:09   ` Patrick Steinhardt
  2025-11-10 14:53   ` Phillip Wood
  2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
  14 siblings, 0 replies; 73+ messages in thread
From: Patrick Steinhardt @ 2025-11-10 10:09 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git

On Wed, Nov 05, 2025 at 01:30:40PM -0800, Junio C Hamano wrote:
> 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

^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH v2 00/12] Incomplete lines
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
                     ` (12 preceding siblings ...)
  2025-11-10 10:09   ` [PATCH v2 00/12] Incomplete lines Patrick Steinhardt
@ 2025-11-10 14:53   ` Phillip Wood
  2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
  14 siblings, 0 replies; 73+ messages in thread
From: Phillip Wood @ 2025-11-10 14:53 UTC (permalink / raw)
  To: Junio C Hamano, git

Hi Junio

On 05/11/2025 21:30, Junio C Hamano wrote:
> 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


> 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


^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH 04/12] diff: fix incorrect counting of line numbers
  2025-11-04  2:09 ` [PATCH 04/12] diff: fix incorrect counting of line numbers Junio C Hamano
@ 2025-11-10 14:54   ` Phillip Wood
  2025-11-10 18:29     ` Junio C Hamano
  0 siblings, 1 reply; 73+ messages in thread
From: Phillip Wood @ 2025-11-10 14:54 UTC (permalink / raw)
  To: Junio C Hamano, git; +Cc: Patrick Steinhardt

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?

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;
>   }
>   


^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH 07/12] diff: update the way rewrite diff handles incomplete lines
  2025-11-04  2:09 ` [PATCH 07/12] diff: update the way rewrite diff handles incomplete lines Junio C Hamano
@ 2025-11-10 14:54   ` Phillip Wood
  2025-11-10 18:33     ` Junio C Hamano
  0 siblings, 1 reply; 73+ messages in thread
From: Phillip Wood @ 2025-11-10 14:54 UTC (permalink / raw)
  To: Junio C Hamano, git; +Cc: Patrick Steinhardt

On 04/11/2025 02:09, Junio C Hamano wrote:
> 
> 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

> @@ -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
>   		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,


^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH 11/12] diff: highlight and error out on incomplete lines
  2025-11-04  2:09 ` [PATCH 11/12] diff: highlight and error out on " Junio C Hamano
@ 2025-11-10 14:55   ` Phillip Wood
  2025-11-10 18:38     ` Junio C Hamano
  0 siblings, 1 reply; 73+ messages in thread
From: Phillip Wood @ 2025-11-10 14:55 UTC (permalink / raw)
  To: Junio C Hamano, git; +Cc: Patrick Steinhardt

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

> 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
> 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"

> +	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.
> +	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

> +	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>


^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH 12/12] attr: enable incomplete-line whitespace error for this project
  2025-11-04  2:09 ` [PATCH 12/12] attr: enable incomplete-line whitespace error for this project Junio C Hamano
@ 2025-11-10 14:55   ` Phillip Wood
  2025-11-10 18:40     ` Junio C Hamano
  0 siblings, 1 reply; 73+ messages in thread
From: Phillip Wood @ 2025-11-10 14:55 UTC (permalink / raw)
  To: Junio C Hamano, git; +Cc: Patrick Steinhardt

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



^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH v2 05/12] diff: refactor output of incomplete line
  2025-11-10 10:06     ` Patrick Steinhardt
@ 2025-11-10 17:58       ` Junio C Hamano
  0 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-10 17:58 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git

Patrick Steinhardt <ps@pks.im> writes:

>>  	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)?

^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH v2 07/12] diff: update the way rewrite diff handles incomplete lines
  2025-11-10 10:06     ` Patrick Steinhardt
@ 2025-11-10 18:14       ` Junio C Hamano
  0 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-10 18:14 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git

Patrick Steinhardt <ps@pks.im> writes:

> 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)

>
>> 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?

^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH 04/12] diff: fix incorrect counting of line numbers
  2025-11-10 14:54   ` Phillip Wood
@ 2025-11-10 18:29     ` Junio C Hamano
  2025-11-11 14:26       ` Phillip Wood
  0 siblings, 1 reply; 73+ messages in thread
From: Junio C Hamano @ 2025-11-10 18:29 UTC (permalink / raw)
  To: Phillip Wood; +Cc: git, Patrick Steinhardt

Phillip Wood <phillip.wood123@gmail.com> writes:

> 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.

> 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;
>>   }
>>   

^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH 07/12] diff: update the way rewrite diff handles incomplete lines
  2025-11-10 14:54   ` Phillip Wood
@ 2025-11-10 18:33     ` Junio C Hamano
  0 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-10 18:33 UTC (permalink / raw)
  To: Phillip Wood; +Cc: git, Patrick Steinhardt

Phillip Wood <phillip.wood123@gmail.com> writes:

>>   		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.

^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH 11/12] diff: highlight and error out on incomplete lines
  2025-11-10 14:55   ` Phillip Wood
@ 2025-11-10 18:38     ` Junio C Hamano
  2025-11-10 23:56       ` D. Ben Knoble
  0 siblings, 1 reply; 73+ messages in thread
From: Junio C Hamano @ 2025-11-10 18:38 UTC (permalink / raw)
  To: Phillip Wood; +Cc: git, Patrick Steinhardt

Phillip Wood <phillip.wood123@gmail.com> writes:

> 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.

>> +	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.

>
>> +	# 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

^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH 12/12] attr: enable incomplete-line whitespace error for this project
  2025-11-10 14:55   ` Phillip Wood
@ 2025-11-10 18:40     ` Junio C Hamano
  0 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-10 18:40 UTC (permalink / raw)
  To: Phillip Wood; +Cc: git, Patrick Steinhardt

Phillip Wood <phillip.wood123@gmail.com> writes:

> 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.

^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH 11/12] diff: highlight and error out on incomplete lines
  2025-11-10 18:38     ` Junio C Hamano
@ 2025-11-10 23:56       ` D. Ben Knoble
  0 siblings, 0 replies; 73+ messages in thread
From: D. Ben Knoble @ 2025-11-10 23:56 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Phillip Wood, git, Patrick Steinhardt

On Mon, Nov 10, 2025 at 1:38 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> 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

^ permalink raw reply	[flat|nested] 73+ messages in thread

* [PATCH v3 00/12] Incomplete lines
  2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
                     ` (13 preceding siblings ...)
  2025-11-10 14:53   ` Phillip Wood
@ 2025-11-11  0:04   ` Junio C Hamano
  2025-11-11  0:04     ` [PATCH v3 01/12] whitespace: correct bit assignment comments Junio C Hamano
                       ` (13 more replies)
  14 siblings, 14 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-11  0:04 UTC (permalink / raw)
  To: git

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


^ permalink raw reply	[flat|nested] 73+ messages in thread

* [PATCH v3 01/12] whitespace: correct bit assignment comments
  2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
@ 2025-11-11  0:04     ` Junio C Hamano
  2025-11-11  0:04     ` [PATCH v3 02/12] diff: emit_line_ws_markup() if/else style fix Junio C Hamano
                       ` (12 subsequent siblings)
  13 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-11  0:04 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v3 02/12] diff: emit_line_ws_markup() if/else style fix
  2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
  2025-11-11  0:04     ` [PATCH v3 01/12] whitespace: correct bit assignment comments Junio C Hamano
@ 2025-11-11  0:04     ` Junio C Hamano
  2025-11-11  0:04     ` [PATCH v3 03/12] diff: correct suppress_blank_empty hack Junio C Hamano
                       ` (11 subsequent siblings)
  13 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-11  0:04 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v3 03/12] diff: correct suppress_blank_empty hack
  2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
  2025-11-11  0:04     ` [PATCH v3 01/12] whitespace: correct bit assignment comments Junio C Hamano
  2025-11-11  0:04     ` [PATCH v3 02/12] diff: emit_line_ws_markup() if/else style fix Junio C Hamano
@ 2025-11-11  0:04     ` Junio C Hamano
  2025-11-11  0:04     ` [PATCH v3 04/12] diff: fix incorrect counting of line numbers Junio C Hamano
                       ` (10 subsequent siblings)
  13 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-11  0:04 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v3 04/12] diff: fix incorrect counting of line numbers
  2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
                       ` (2 preceding siblings ...)
  2025-11-11  0:04     ` [PATCH v3 03/12] diff: correct suppress_blank_empty hack Junio C Hamano
@ 2025-11-11  0:04     ` Junio C Hamano
  2025-11-11  0:04     ` [PATCH v3 05/12] diff: refactor output of incomplete line Junio C Hamano
                       ` (9 subsequent siblings)
  13 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-11  0:04 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v3 05/12] diff: refactor output of incomplete line
  2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
                       ` (3 preceding siblings ...)
  2025-11-11  0:04     ` [PATCH v3 04/12] diff: fix incorrect counting of line numbers Junio C Hamano
@ 2025-11-11  0:04     ` Junio C Hamano
  2025-11-11  0:04     ` [PATCH v3 06/12] diff: call emit_callback ecbdata everywhere Junio C Hamano
                       ` (8 subsequent siblings)
  13 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-11  0:04 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v3 06/12] diff: call emit_callback ecbdata everywhere
  2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
                       ` (4 preceding siblings ...)
  2025-11-11  0:04     ` [PATCH v3 05/12] diff: refactor output of incomplete line Junio C Hamano
@ 2025-11-11  0:04     ` Junio C Hamano
  2025-11-11  0:04     ` [PATCH v3 07/12] diff: update the way rewrite diff handles incomplete lines Junio C Hamano
                       ` (7 subsequent siblings)
  13 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-11  0:04 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v3 07/12] diff: update the way rewrite diff handles incomplete lines
  2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
                       ` (5 preceding siblings ...)
  2025-11-11  0:04     ` [PATCH v3 06/12] diff: call emit_callback ecbdata everywhere Junio C Hamano
@ 2025-11-11  0:04     ` Junio C Hamano
  2025-11-11  0:04     ` [PATCH v3 08/12] apply: revamp the parsing of " Junio C Hamano
                       ` (6 subsequent siblings)
  13 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-11  0:04 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v3 08/12] apply: revamp the parsing of incomplete lines
  2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
                       ` (6 preceding siblings ...)
  2025-11-11  0:04     ` [PATCH v3 07/12] diff: update the way rewrite diff handles incomplete lines Junio C Hamano
@ 2025-11-11  0:04     ` Junio C Hamano
  2025-11-11  0:04     ` [PATCH v3 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE Junio C Hamano
                       ` (5 subsequent siblings)
  13 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-11  0:04 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v3 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE
  2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
                       ` (7 preceding siblings ...)
  2025-11-11  0:04     ` [PATCH v3 08/12] apply: revamp the parsing of " Junio C Hamano
@ 2025-11-11  0:04     ` Junio C Hamano
  2025-11-11  0:04     ` [PATCH v3 10/12] apply: check and fix incomplete lines Junio C Hamano
                       ` (4 subsequent siblings)
  13 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-11  0:04 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v3 10/12] apply: check and fix incomplete lines
  2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
                       ` (8 preceding siblings ...)
  2025-11-11  0:04     ` [PATCH v3 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE Junio C Hamano
@ 2025-11-11  0:04     ` Junio C Hamano
  2025-11-11  0:04     ` [PATCH v3 11/12] diff: highlight and error out on " Junio C Hamano
                       ` (3 subsequent siblings)
  13 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-11  0:04 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v3 11/12] diff: highlight and error out on incomplete lines
  2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
                       ` (9 preceding siblings ...)
  2025-11-11  0:04     ` [PATCH v3 10/12] apply: check and fix incomplete lines Junio C Hamano
@ 2025-11-11  0:04     ` Junio C Hamano
  2025-11-11  0:04     ` [PATCH v3 12/12] attr: enable incomplete-line whitespace error for this project Junio C Hamano
                       ` (2 subsequent siblings)
  13 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-11  0:04 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v3 12/12] attr: enable incomplete-line whitespace error for this project
  2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
                       ` (10 preceding siblings ...)
  2025-11-11  0:04     ` [PATCH v3 11/12] diff: highlight and error out on " Junio C Hamano
@ 2025-11-11  0:04     ` Junio C Hamano
  2025-11-11 14:29     ` [PATCH v3 00/12] Incomplete lines Phillip Wood
  2025-11-12 22:02     ` [PATCH v4 " Junio C Hamano
  13 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-11  0:04 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* Re: [PATCH 04/12] diff: fix incorrect counting of line numbers
  2025-11-10 18:29     ` Junio C Hamano
@ 2025-11-11 14:26       ` Phillip Wood
  2025-11-11 14:37         ` Junio C Hamano
  0 siblings, 1 reply; 73+ messages in thread
From: Phillip Wood @ 2025-11-11 14:26 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, Patrick Steinhardt

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.

Thanks

Phillip

>> 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;
>>>    }
>>>    


^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH v3 00/12] Incomplete lines
  2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
                       ` (11 preceding siblings ...)
  2025-11-11  0:04     ` [PATCH v3 12/12] attr: enable incomplete-line whitespace error for this project Junio C Hamano
@ 2025-11-11 14:29     ` Phillip Wood
  2025-11-12 22:02     ` [PATCH v4 " Junio C Hamano
  13 siblings, 0 replies; 73+ messages in thread
From: Phillip Wood @ 2025-11-11 14:29 UTC (permalink / raw)
  To: Junio C Hamano, git

On 11/11/2025 00:04, Junio C Hamano wrote:
> 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

> 
> 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


^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH 04/12] diff: fix incorrect counting of line numbers
  2025-11-11 14:26       ` Phillip Wood
@ 2025-11-11 14:37         ` Junio C Hamano
  0 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-11 14:37 UTC (permalink / raw)
  To: Phillip Wood; +Cc: git, Patrick Steinhardt

Phillip Wood <phillip.wood123@gmail.com> writes:

> 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.

^ permalink raw reply	[flat|nested] 73+ messages in thread

* [PATCH v4 00/12] Incomplete lines
  2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
                       ` (12 preceding siblings ...)
  2025-11-11 14:29     ` [PATCH v3 00/12] Incomplete lines Phillip Wood
@ 2025-11-12 22:02     ` Junio C Hamano
  2025-11-12 22:02       ` [PATCH v4 01/12] whitespace: correct bit assignment comments Junio C Hamano
                         ` (12 more replies)
  13 siblings, 13 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-12 22:02 UTC (permalink / raw)
  To: git

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


^ permalink raw reply	[flat|nested] 73+ messages in thread

* [PATCH v4 01/12] whitespace: correct bit assignment comments
  2025-11-12 22:02     ` [PATCH v4 " Junio C Hamano
@ 2025-11-12 22:02       ` Junio C Hamano
  2025-11-12 22:02       ` [PATCH v4 02/12] diff: emit_line_ws_markup() if/else style fix Junio C Hamano
                         ` (11 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-12 22:02 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v4 02/12] diff: emit_line_ws_markup() if/else style fix
  2025-11-12 22:02     ` [PATCH v4 " Junio C Hamano
  2025-11-12 22:02       ` [PATCH v4 01/12] whitespace: correct bit assignment comments Junio C Hamano
@ 2025-11-12 22:02       ` Junio C Hamano
  2025-11-12 22:02       ` [PATCH v4 03/12] diff: correct suppress_blank_empty hack Junio C Hamano
                         ` (10 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-12 22:02 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v4 03/12] diff: correct suppress_blank_empty hack
  2025-11-12 22:02     ` [PATCH v4 " Junio C Hamano
  2025-11-12 22:02       ` [PATCH v4 01/12] whitespace: correct bit assignment comments Junio C Hamano
  2025-11-12 22:02       ` [PATCH v4 02/12] diff: emit_line_ws_markup() if/else style fix Junio C Hamano
@ 2025-11-12 22:02       ` Junio C Hamano
  2025-11-12 22:02       ` [PATCH v4 04/12] diff: keep track of the type of the last line seen Junio C Hamano
                         ` (9 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-12 22:02 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v4 04/12] diff: keep track of the type of the last line seen
  2025-11-12 22:02     ` [PATCH v4 " Junio C Hamano
                         ` (2 preceding siblings ...)
  2025-11-12 22:02       ` [PATCH v4 03/12] diff: correct suppress_blank_empty hack Junio C Hamano
@ 2025-11-12 22:02       ` Junio C Hamano
  2025-11-12 22:02       ` [PATCH v4 05/12] diff: refactor output of incomplete line Junio C Hamano
                         ` (8 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-12 22:02 UTC (permalink / raw)
  To: git

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(+)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v4 05/12] diff: refactor output of incomplete line
  2025-11-12 22:02     ` [PATCH v4 " Junio C Hamano
                         ` (3 preceding siblings ...)
  2025-11-12 22:02       ` [PATCH v4 04/12] diff: keep track of the type of the last line seen Junio C Hamano
@ 2025-11-12 22:02       ` Junio C Hamano
  2025-11-12 22:02       ` [PATCH v4 06/12] diff: call emit_callback ecbdata everywhere Junio C Hamano
                         ` (7 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-12 22:02 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v4 06/12] diff: call emit_callback ecbdata everywhere
  2025-11-12 22:02     ` [PATCH v4 " Junio C Hamano
                         ` (4 preceding siblings ...)
  2025-11-12 22:02       ` [PATCH v4 05/12] diff: refactor output of incomplete line Junio C Hamano
@ 2025-11-12 22:02       ` Junio C Hamano
  2025-11-12 22:02       ` [PATCH v4 07/12] diff: update the way rewrite diff handles incomplete lines Junio C Hamano
                         ` (6 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-12 22:02 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v4 07/12] diff: update the way rewrite diff handles incomplete lines
  2025-11-12 22:02     ` [PATCH v4 " Junio C Hamano
                         ` (5 preceding siblings ...)
  2025-11-12 22:02       ` [PATCH v4 06/12] diff: call emit_callback ecbdata everywhere Junio C Hamano
@ 2025-11-12 22:02       ` Junio C Hamano
  2025-11-12 22:02       ` [PATCH v4 08/12] apply: revamp the parsing of " Junio C Hamano
                         ` (5 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-12 22:02 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v4 08/12] apply: revamp the parsing of incomplete lines
  2025-11-12 22:02     ` [PATCH v4 " Junio C Hamano
                         ` (6 preceding siblings ...)
  2025-11-12 22:02       ` [PATCH v4 07/12] diff: update the way rewrite diff handles incomplete lines Junio C Hamano
@ 2025-11-12 22:02       ` Junio C Hamano
  2025-11-12 22:02       ` [PATCH v4 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE Junio C Hamano
                         ` (4 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-12 22:02 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v4 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE
  2025-11-12 22:02     ` [PATCH v4 " Junio C Hamano
                         ` (7 preceding siblings ...)
  2025-11-12 22:02       ` [PATCH v4 08/12] apply: revamp the parsing of " Junio C Hamano
@ 2025-11-12 22:02       ` Junio C Hamano
  2025-11-12 22:02       ` [PATCH v4 10/12] apply: check and fix incomplete lines Junio C Hamano
                         ` (3 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-12 22:02 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v4 10/12] apply: check and fix incomplete lines
  2025-11-12 22:02     ` [PATCH v4 " Junio C Hamano
                         ` (8 preceding siblings ...)
  2025-11-12 22:02       ` [PATCH v4 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE Junio C Hamano
@ 2025-11-12 22:02       ` Junio C Hamano
  2025-11-12 22:02       ` [PATCH v4 11/12] diff: highlight and error out on " Junio C Hamano
                         ` (2 subsequent siblings)
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-12 22:02 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v4 11/12] diff: highlight and error out on incomplete lines
  2025-11-12 22:02     ` [PATCH v4 " Junio C Hamano
                         ` (9 preceding siblings ...)
  2025-11-12 22:02       ` [PATCH v4 10/12] apply: check and fix incomplete lines Junio C Hamano
@ 2025-11-12 22:02       ` Junio C Hamano
  2025-11-12 22:02       ` [PATCH v4 12/12] attr: enable incomplete-line whitespace error for this project Junio C Hamano
  2025-11-14 10:24       ` [PATCH v4 00/12] Incomplete lines Phillip Wood
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-12 22:02 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* [PATCH v4 12/12] attr: enable incomplete-line whitespace error for this project
  2025-11-12 22:02     ` [PATCH v4 " Junio C Hamano
                         ` (10 preceding siblings ...)
  2025-11-12 22:02       ` [PATCH v4 11/12] diff: highlight and error out on " Junio C Hamano
@ 2025-11-12 22:02       ` Junio C Hamano
  2025-11-14 10:24       ` [PATCH v4 00/12] Incomplete lines Phillip Wood
  12 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-12 22:02 UTC (permalink / raw)
  To: git

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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

* Re: [PATCH v4 00/12] Incomplete lines
  2025-11-12 22:02     ` [PATCH v4 " Junio C Hamano
                         ` (11 preceding siblings ...)
  2025-11-12 22:02       ` [PATCH v4 12/12] attr: enable incomplete-line whitespace error for this project Junio C Hamano
@ 2025-11-14 10:24       ` Phillip Wood
  2025-11-14 16:25         ` Junio C Hamano
  12 siblings, 1 reply; 73+ messages in thread
From: Phillip Wood @ 2025-11-14 10:24 UTC (permalink / raw)
  To: Junio C Hamano, git

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)

Thanks

Phillip


^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH v4 00/12] Incomplete lines
  2025-11-14 10:24       ` [PATCH v4 00/12] Incomplete lines Phillip Wood
@ 2025-11-14 16:25         ` Junio C Hamano
  2025-11-23  2:35           ` Junio C Hamano
  0 siblings, 1 reply; 73+ messages in thread
From: Junio C Hamano @ 2025-11-14 16:25 UTC (permalink / raw)
  To: Phillip Wood; +Cc: git

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.

^ permalink raw reply	[flat|nested] 73+ messages in thread

* Re: [PATCH v4 00/12] Incomplete lines
  2025-11-14 16:25         ` Junio C Hamano
@ 2025-11-23  2:35           ` Junio C Hamano
  0 siblings, 0 replies; 73+ messages in thread
From: Junio C Hamano @ 2025-11-23  2:35 UTC (permalink / raw)
  To: Phillip Wood; +Cc: git

Junio C Hamano <gitster@pobox.com> writes:

> 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(-)

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


^ permalink raw reply related	[flat|nested] 73+ messages in thread

end of thread, other threads:[~2025-11-23  2:35 UTC | newest]

Thread overview: 73+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-11-04  2:09 [PATCH 00/12] Incomplete lines Junio C Hamano
2025-11-04  2:09 ` [PATCH 01/12] whitespace: correct bit assignment comments Junio C Hamano
2025-11-04  2:09 ` [PATCH 02/12] diff: emit_line_ws_markup() if/else style fix Junio C Hamano
2025-11-04  2:09 ` [PATCH 03/12] diff: correct suppress_blank_empty hack Junio C Hamano
2025-11-04  2:09 ` [PATCH 04/12] diff: fix incorrect counting of line numbers Junio C Hamano
2025-11-10 14:54   ` Phillip Wood
2025-11-10 18:29     ` Junio C Hamano
2025-11-11 14:26       ` Phillip Wood
2025-11-11 14:37         ` Junio C Hamano
2025-11-04  2:09 ` [PATCH 05/12] diff: refactor output of incomplete line Junio C Hamano
2025-11-04  2:09 ` [PATCH 06/12] diff: call emit_callback ecbdata everywhere Junio C Hamano
2025-11-04  2:09 ` [PATCH 07/12] diff: update the way rewrite diff handles incomplete lines Junio C Hamano
2025-11-10 14:54   ` Phillip Wood
2025-11-10 18:33     ` Junio C Hamano
2025-11-04  2:09 ` [PATCH 08/12] apply: revamp the parsing of " Junio C Hamano
2025-11-04  2:09 ` [PATCH 09/12] whitespace: allocate a few more bits Junio C Hamano
2025-11-04  2:09 ` [PATCH 10/12] apply: check and fix incomplete lines Junio C Hamano
2025-11-04  2:09 ` [PATCH 11/12] diff: highlight and error out on " Junio C Hamano
2025-11-10 14:55   ` Phillip Wood
2025-11-10 18:38     ` Junio C Hamano
2025-11-10 23:56       ` D. Ben Knoble
2025-11-04  2:09 ` [PATCH 12/12] attr: enable incomplete-line whitespace error for this project Junio C Hamano
2025-11-10 14:55   ` Phillip Wood
2025-11-10 18:40     ` Junio C Hamano
2025-11-05 21:30 ` [PATCH v2 00/12] Incomplete lines Junio C Hamano
2025-11-05 21:30   ` [PATCH v2 01/12] whitespace: correct bit assignment comments Junio C Hamano
2025-11-05 21:30   ` [PATCH v2 02/12] diff: emit_line_ws_markup() if/else style fix Junio C Hamano
2025-11-05 21:30   ` [PATCH v2 03/12] diff: correct suppress_blank_empty hack Junio C Hamano
2025-11-05 21:30   ` [PATCH v2 04/12] diff: fix incorrect counting of line numbers Junio C Hamano
2025-11-05 21:30   ` [PATCH v2 05/12] diff: refactor output of incomplete line Junio C Hamano
2025-11-10 10:06     ` Patrick Steinhardt
2025-11-10 17:58       ` Junio C Hamano
2025-11-05 21:30   ` [PATCH v2 06/12] diff: call emit_callback ecbdata everywhere Junio C Hamano
2025-11-05 21:30   ` [PATCH v2 07/12] diff: update the way rewrite diff handles incomplete lines Junio C Hamano
2025-11-10 10:06     ` Patrick Steinhardt
2025-11-10 18:14       ` Junio C Hamano
2025-11-05 21:30   ` [PATCH v2 08/12] apply: revamp the parsing of " Junio C Hamano
2025-11-05 21:30   ` [PATCH v2 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE Junio C Hamano
2025-11-05 21:30   ` [PATCH v2 10/12] apply: check and fix incomplete lines Junio C Hamano
2025-11-05 21:30   ` [PATCH v2 11/12] diff: highlight and error out on " Junio C Hamano
2025-11-05 21:30   ` [PATCH v2 12/12] attr: enable incomplete-line whitespace error for this project Junio C Hamano
2025-11-10 10:09   ` [PATCH v2 00/12] Incomplete lines Patrick Steinhardt
2025-11-10 14:53   ` Phillip Wood
2025-11-11  0:04   ` [PATCH v3 " Junio C Hamano
2025-11-11  0:04     ` [PATCH v3 01/12] whitespace: correct bit assignment comments Junio C Hamano
2025-11-11  0:04     ` [PATCH v3 02/12] diff: emit_line_ws_markup() if/else style fix Junio C Hamano
2025-11-11  0:04     ` [PATCH v3 03/12] diff: correct suppress_blank_empty hack Junio C Hamano
2025-11-11  0:04     ` [PATCH v3 04/12] diff: fix incorrect counting of line numbers Junio C Hamano
2025-11-11  0:04     ` [PATCH v3 05/12] diff: refactor output of incomplete line Junio C Hamano
2025-11-11  0:04     ` [PATCH v3 06/12] diff: call emit_callback ecbdata everywhere Junio C Hamano
2025-11-11  0:04     ` [PATCH v3 07/12] diff: update the way rewrite diff handles incomplete lines Junio C Hamano
2025-11-11  0:04     ` [PATCH v3 08/12] apply: revamp the parsing of " Junio C Hamano
2025-11-11  0:04     ` [PATCH v3 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE Junio C Hamano
2025-11-11  0:04     ` [PATCH v3 10/12] apply: check and fix incomplete lines Junio C Hamano
2025-11-11  0:04     ` [PATCH v3 11/12] diff: highlight and error out on " Junio C Hamano
2025-11-11  0:04     ` [PATCH v3 12/12] attr: enable incomplete-line whitespace error for this project Junio C Hamano
2025-11-11 14:29     ` [PATCH v3 00/12] Incomplete lines Phillip Wood
2025-11-12 22:02     ` [PATCH v4 " Junio C Hamano
2025-11-12 22:02       ` [PATCH v4 01/12] whitespace: correct bit assignment comments Junio C Hamano
2025-11-12 22:02       ` [PATCH v4 02/12] diff: emit_line_ws_markup() if/else style fix Junio C Hamano
2025-11-12 22:02       ` [PATCH v4 03/12] diff: correct suppress_blank_empty hack Junio C Hamano
2025-11-12 22:02       ` [PATCH v4 04/12] diff: keep track of the type of the last line seen Junio C Hamano
2025-11-12 22:02       ` [PATCH v4 05/12] diff: refactor output of incomplete line Junio C Hamano
2025-11-12 22:02       ` [PATCH v4 06/12] diff: call emit_callback ecbdata everywhere Junio C Hamano
2025-11-12 22:02       ` [PATCH v4 07/12] diff: update the way rewrite diff handles incomplete lines Junio C Hamano
2025-11-12 22:02       ` [PATCH v4 08/12] apply: revamp the parsing of " Junio C Hamano
2025-11-12 22:02       ` [PATCH v4 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE Junio C Hamano
2025-11-12 22:02       ` [PATCH v4 10/12] apply: check and fix incomplete lines Junio C Hamano
2025-11-12 22:02       ` [PATCH v4 11/12] diff: highlight and error out on " Junio C Hamano
2025-11-12 22:02       ` [PATCH v4 12/12] attr: enable incomplete-line whitespace error for this project Junio C Hamano
2025-11-14 10:24       ` [PATCH v4 00/12] Incomplete lines Phillip Wood
2025-11-14 16:25         ` Junio C Hamano
2025-11-23  2:35           ` Junio C Hamano

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).