{"thread":{"id":"64429","subject":"[PATCH 00/12] Incomplete lines","startedAt":"2025-11-04T02:09:31Z","lastAt":"2025-11-23T02:35:19Z","messageCount":73,"participants":["Junio C Hamano","Patrick Steinhardt","Phillip Wood","D. Ben Knoble"],"isPatch":true,"patchVersion":1,"patchTotal":12},"messages":[{"id":"530158","messageId":"20251104020928.582199-1-gitster@pobox.com","threadId":"64429","inReplyTo":null,"subject":"[PATCH 00/12] Incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-04T02:09:16Z","receivedAt":"2025-11-04T02:09:31Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"One of the common kind of whitespace errors is to lack the\nterminating newline at the end of a file, but so far, neither \"git\ndiff\" or \"git apply\" did anything about them.\n\nThis series introduces \"incomplete-line\" whitespace error class,\nthat you can add to either the core.whitespace configuration\nvariable, or the whitespace attribute in your .gitattributes files.\nThe class is disabled by default.\n\nWhen incomplete-line whitespace error is enabled, a patch file that\nhas \"\\ No newline at the end of file\" line for the postimage file is\nconsidered to introduce a whitespace error.  This is true even if\nthe corresponding preimage file ends in a different contents on the\nfinal line that is incomplete.  The incomplete line marker that is\ngiven for a context line is not considered an error.  The reasoning\nis that your preimage did have incomplete line, but you did not\ntouch the contents on that incomplete line in your patch, so you\nleft the line intact.  It is not a new breakage you are responsible\nfor.  If the incomplete line marker follows a postimage line, on the\nother hand, it means that you either made a file that used to end\nwith a complete line to end with an incomplete line, or the file\nended with an incomplete line before your change, and you did not\nfix it even though you modified other bytes on that same last line,\nwhich you could have easily fixed while at it, and that is flagged\nas an error.\n\n * \"git diff --check\" notices and errors out.\n\n * \"git diff\" and friends highlight the offending \"\\ No newline ...\"\n   line.\n\n * \"git apply --whitespace=(error|warn)\" triggers an error, and \"git\n   apply --whitespace=fix\" would correct it by appending a newline.\n\nThe organization of the series is\n\n * The first patch [01/12] is a clean-up we have seen earlier on the\n   list already (https://lore.kernel.org/git/xmqqfrb4hyjl.fsf@gitster.g/).\n\n * The patches [02/12] - [08/12] are preliminary clean-up made to\n   both \"git diff\" and \"git apply\" machinery.\n\n * The patch [09/12] shifts the bit assignment (cleaned-up in\n   [01/12] without changing any values) to make room for new\n   whitespace error class (which was last updated in 2007 IIRC, so\n   the set of whitespace errors surprisingly haven't changed for\n   quite some time).\n\n * The patch [10/12] teaches \"git apply --whitespace=<mode>\" about\n   the incomplete-line error class.\n\n * The patch [11/12] teaches \"git diff [--check]\" about the\n   incomplete-line error class.\n\n * The final patch [12/12] enables the incomplete-line error class\n   for our project for C source files and shell scripts.  I didn't\n   touch the cover-all * entry.\n\nJunio C Hamano (12):\n  whitespace: correct bit assignment comments\n  diff: emit_line_ws_markup() if/else style fix\n  diff: correct suppress_blank_empty hack\n  diff: fix incorrect counting of line numbers\n  diff: refactor output of incomplete line\n  diff: call emit_callback ecbdata everywhere\n  diff: update the way rewrite diff handles incomplete lines\n  apply: revamp the parsing of incomplete lines\n  whitespace: allocate a few more bits\n  apply: check and fix incomplete lines\n  diff: highlight and error out on incomplete lines\n  attr: enable incomplete-line whitespace error for this project\n\n .gitattributes             |   4 +-\n apply.c                    |  70 ++++++++++++------\n diff.c                     | 148 +++++++++++++++++++++++++------------\n diff.h                     |   6 +-\n t/t4015-diff-whitespace.sh |  63 +++++++++++++++-\n t/t4124-apply-ws-rule.sh   | 112 ++++++++++++++++++++++++++++\n ws.c                       |  20 +++++\n ws.h                       |  26 ++++---\n 8 files changed, 361 insertions(+), 88 deletions(-)\n\n-- \n2.52.0-rc0\n\n"},{"id":"530159","messageId":"20251104020928.582199-2-gitster@pobox.com","threadId":"64429","inReplyTo":"20251104020928.582199-1-gitster@pobox.com","subject":"[PATCH 01/12] whitespace: correct bit assignment comments","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-04T02:09:17Z","receivedAt":"2025-11-04T02:09:33Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"A comment in diff.c claimed that bits up to 12th (counting from 0th)\nare whitespace rules, and 13th thru 15th are for new/old/context,\nbut it turns out it was miscounting.  Correct them, and clarify\nwhere the whitespace rule bits come from in the comment.  Extend bit\nassignment comments to cover bits used for color-moved, which\nweren't described.\n\nAlso update the way these bit constants are defined to use (1 << N)\nnotation, instead of octal constants, as it tends to make it easier\nto notice a breakage like this.\n\nSprinkle a few blank lines between logically distinct groups of CPP\nmacro definitions to make them easier to read.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c |  7 +++++--\n diff.h |  6 +++---\n ws.h   | 25 ++++++++++++++-----------\n 3 files changed, 22 insertions(+), 16 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex a74e701806..74261b332a 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -801,16 +801,19 @@ enum diff_symbol {\n \tDIFF_SYMBOL_CONTEXT_MARKER,\n \tDIFF_SYMBOL_SEPARATOR\n };\n+\n /*\n  * Flags for content lines:\n- * 0..12 are whitespace rules\n- * 13-15 are WSEH_NEW | WSEH_OLD | WSEH_CONTEXT\n+ * 0..11 are whitespace rules (see ws.h)\n+ * 12..14 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD\n  * 16 is marking if the line is blank at EOF\n+ * 17..19 are used for color-moved.\n  */\n #define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF\t(1<<16)\n #define DIFF_SYMBOL_MOVED_LINE\t\t\t(1<<17)\n #define DIFF_SYMBOL_MOVED_LINE_ALT\t\t(1<<18)\n #define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING\t(1<<19)\n+\n #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)\n \n /*\ndiff --git a/diff.h b/diff.h\nindex 2fa256c3ef..cbd355cf50 100644\n--- a/diff.h\n+++ b/diff.h\n@@ -331,9 +331,9 @@ struct diff_options {\n \n \tint ita_invisible_in_index;\n /* white-space error highlighting */\n-#define WSEH_NEW (1<<12)\n-#define WSEH_CONTEXT (1<<13)\n-#define WSEH_OLD (1<<14)\n+#define WSEH_NEW        (1<<12)\n+#define WSEH_CONTEXT    (1<<13)\n+#define WSEH_OLD        (1<<14)\n \tunsigned ws_error_highlight;\n \tconst char *prefix;\n \tint prefix_length;\ndiff --git a/ws.h b/ws.h\nindex 5ba676c559..23708efb73 100644\n--- a/ws.h\n+++ b/ws.h\n@@ -7,19 +7,22 @@ struct strbuf;\n /*\n  * whitespace rules.\n  * used by both diff and apply\n- * last two digits are tab width\n+ * last two octal-digits are tab width (we support only up to 63).\n  */\n-#define WS_BLANK_AT_EOL         0100\n-#define WS_SPACE_BEFORE_TAB     0200\n-#define WS_INDENT_WITH_NON_TAB  0400\n-#define WS_CR_AT_EOL           01000\n-#define WS_BLANK_AT_EOF        02000\n-#define WS_TAB_IN_INDENT       04000\n-#define WS_TRAILING_SPACE      (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)\n+#define WS_BLANK_AT_EOL         (1<<6)\n+#define WS_SPACE_BEFORE_TAB     (1<<7)\n+#define WS_INDENT_WITH_NON_TAB  (1<<8)\n+#define WS_CR_AT_EOL            (1<<9)\n+#define WS_BLANK_AT_EOF         (1<<10)\n+#define WS_TAB_IN_INDENT        (1<<11)\n+\n+#define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)\n #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)\n-#define WS_TAB_WIDTH_MASK        077\n-/* All WS_* -- when extended, adapt diff.c emit_symbol */\n-#define WS_RULE_MASK           07777\n+#define WS_TAB_WIDTH_MASK       ((1<<6)-1)\n+\n+/* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */\n+#define WS_RULE_MASK            ((1<<12)-1)\n+\n extern unsigned whitespace_rule_cfg;\n unsigned whitespace_rule(struct index_state *, const char *);\n unsigned parse_whitespace_rule(const char *);\n-- \n2.52.0-rc0\n\n"},{"id":"530160","messageId":"20251104020928.582199-3-gitster@pobox.com","threadId":"64429","inReplyTo":"20251104020928.582199-1-gitster@pobox.com","subject":"[PATCH 02/12] diff: emit_line_ws_markup() if/else style fix","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-04T02:09:18Z","receivedAt":"2025-11-04T02:09:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Apply the simple rule: if you need {} in one arm of the if/else\nif/else... cascade, have {} in all of them.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 74261b332a..9a24a0791c 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1327,14 +1327,14 @@ static void emit_line_ws_markup(struct diff_options *o,\n \t\t\tws = NULL;\n \t}\n \n-\tif (!ws && !set_sign)\n+\tif (!ws && !set_sign) {\n \t\temit_line_0(o, set, NULL, 0, reset, sign, line, len);\n-\telse if (!ws) {\n+\t} else if (!ws) {\n \t\temit_line_0(o, set_sign, set, !!set_sign, reset, sign, line, len);\n-\t} else if (blank_at_eof)\n+\t} else if (blank_at_eof) {\n \t\t/* Blank line at EOF - paint '+' as well */\n \t\temit_line_0(o, ws, NULL, 0, reset, sign, line, len);\n-\telse {\n+\t} else {\n \t\t/* Emit just the prefix, then the rest. */\n \t\temit_line_0(o, set_sign ? set_sign : set, NULL, !!set_sign, reset,\n \t\t\t    sign, \"\", 0);\n-- \n2.52.0-rc0\n\n"},{"id":"530161","messageId":"20251104020928.582199-4-gitster@pobox.com","threadId":"64429","inReplyTo":"20251104020928.582199-1-gitster@pobox.com","subject":"[PATCH 03/12] diff: correct suppress_blank_empty hack","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-04T02:09:19Z","receivedAt":"2025-11-04T02:09:37Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The suppress-blank-empty feature abused the CONTEXT_INCOMPLETE\nsymbol that was meant to be used only for \"\\ No newline at the end\nof file\" code path.\n\nThe intent of the feature was to turn a context line we receive from\nxdiff machinery (which always uses ' ' for context lines, even an\nempty one) and spit it out as a truly empty line.\n\nPerform such a conversion very locally at where a line from xdiff\nthat begins with ' ' is handled for output; there are many checks\nbefore the control reaches such place that checks the first letter\nof the diff output line to see if it is a context line, and having\nto check for '\\n' and treat it as a special case is error prone.\n\nIn order to catch similar hacks in the future, make sure the code\npath that is meant for \"\\ No newline\" case checks the first byte is\nindeed a backslash.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 27 +++++++++++----------------\n 1 file changed, 11 insertions(+), 16 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 9a24a0791c..b9ef8550cc 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1321,6 +1321,11 @@ static void emit_line_ws_markup(struct diff_options *o,\n \tconst char *ws = NULL;\n \tint sign = o->output_indicators[sign_index];\n \n+\tif (diff_suppress_blank_empty &&\n+\t    sign_index == OUTPUT_INDICATOR_CONTEXT &&\n+\t    len == 1 && line[0] == '\\n')\n+\t\tsign = 0;\n+\n \tif (o->ws_error_highlight & ws_rule) {\n \t\tws = diff_get_color_opt(o, DIFF_WHITESPACE);\n \t\tif (!*ws)\n@@ -1498,15 +1503,9 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n \tcase DIFF_SYMBOL_WORDS:\n \t\tcontext = diff_get_color_opt(o, DIFF_CONTEXT);\n \t\treset = diff_get_color_opt(o, DIFF_RESET);\n-\t\t/*\n-\t\t * Skip the prefix character, if any.  With\n-\t\t * diff_suppress_blank_empty, there may be\n-\t\t * none.\n-\t\t */\n-\t\tif (line[0] != '\\n') {\n-\t\t\tline++;\n-\t\t\tlen--;\n-\t\t}\n+\n+\t\t/* Skip the prefix character */\n+\t\tline++; len--;\n \t\temit_line(o, context, reset, line, len);\n \t\tbreak;\n \tcase DIFF_SYMBOL_FILEPAIR_PLUS:\n@@ -2375,12 +2374,6 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \t\tecbdata->label_path[0] = ecbdata->label_path[1] = NULL;\n \t}\n \n-\tif (diff_suppress_blank_empty\n-\t    && len == 2 && line[0] == ' ' && line[1] == '\\n') {\n-\t\tline[0] = '\\n';\n-\t\tlen = 1;\n-\t}\n-\n \tif (line[0] == '@') {\n \t\tif (ecbdata->diff_words)\n \t\t\tdiff_words_flush(ecbdata);\n@@ -2431,12 +2424,14 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \t\tecbdata->lno_in_preimage++;\n \t\temit_context_line(ecbdata, line + 1, len - 1);\n \t\tbreak;\n-\tdefault:\n+\tcase '\\\\':\n \t\t/* incomplete line at the end */\n \t\tecbdata->lno_in_preimage++;\n \t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n \t\t\t\t line, len, 0);\n \t\tbreak;\n+\tdefault:\n+\t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n \t}\n \treturn 0;\n }\n-- \n2.52.0-rc0\n\n"},{"id":"530162","messageId":"20251104020928.582199-5-gitster@pobox.com","threadId":"64429","inReplyTo":"20251104020928.582199-1-gitster@pobox.com","subject":"[PATCH 04/12] diff: fix incorrect counting of line numbers","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-04T02:09:20Z","receivedAt":"2025-11-04T02:09:38Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The \"\\ No newline at the end of the file\" can come after any of the\n\"-\" (deleted preimage line), \" \" (unchanged line), or \"+\" (added\npostimage line).  Incrementing only the preimage line number upon\nseeing it does not make any sense.\n\nWe can keep track of what the previous line was, and increment\nlno_in_{pre,post}image variables properly, like this patch does.  I\ndo not think it matters, as these numbers are used only to compare\nthem with blank_at_eof_in_{pre,post}image to issue the warning every\ntime we see an added line, but by definition, after we see \"\\ No\nnewline at the end of the file\" for an added line, we will not see\nan added line for the file.\n\nKeeping track of what the last line was (in other words, \"is it that\nthe file used to end in an incomplete line?  The file ends in an\nincomplete line after the change?  Both the file before and after\nthe change ends in an incomplete line that did not change?\") will be\nindependently useful.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 18 +++++++++++++++++-\n 1 file changed, 17 insertions(+), 1 deletion(-)\n\ndiff --git a/diff.c b/diff.c\nindex b9ef8550cc..e73320dfb1 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -601,6 +601,7 @@ struct emit_callback {\n \tint blank_at_eof_in_postimage;\n \tint lno_in_preimage;\n \tint lno_in_postimage;\n+\tint last_line_kind;\n \tconst char **label_path;\n \tstruct diff_words_data *diff_words;\n \tstruct diff_options *opt;\n@@ -2426,13 +2427,28 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \t\tbreak;\n \tcase '\\\\':\n \t\t/* incomplete line at the end */\n-\t\tecbdata->lno_in_preimage++;\n+\t\tswitch (ecbdata->last_line_kind) {\n+\t\tcase '+':\n+\t\t\tecbdata->lno_in_postimage++;\n+\t\t\tbreak;\n+\t\tcase '-':\n+\t\t\tecbdata->lno_in_preimage++;\n+\t\t\tbreak;\n+\t\tcase ' ':\n+\t\t\tecbdata->lno_in_preimage++;\n+\t\t\tecbdata->lno_in_postimage++;\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tBUG(\"fn_out_consume: '\\\\No newline' after unknown line (%c)\",\n+\t\t\t    ecbdata->last_line_kind);\n+\t\t}\n \t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n \t\t\t\t line, len, 0);\n \t\tbreak;\n \tdefault:\n \t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n \t}\n+\tecbdata->last_line_kind = line[0];\n \treturn 0;\n }\n \n-- \n2.52.0-rc0\n\n"},{"id":"530163","messageId":"20251104020928.582199-6-gitster@pobox.com","threadId":"64429","inReplyTo":"20251104020928.582199-1-gitster@pobox.com","subject":"[PATCH 05/12] diff: refactor output of incomplete line","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-04T02:09:21Z","receivedAt":"2025-11-04T02:09:40Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Create a helper function that reacts to \"\\ No newline at the end of\nfile\" in preparation for unifying the incomplete line handling in\nthe code path that handles xdiff output and the code path that\nbypasses xdiff and produces complete rewrite patch.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 14 ++++++++++++--\n 1 file changed, 12 insertions(+), 2 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex e73320dfb1..d388d318e4 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1379,6 +1379,10 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n \t\temit_line(o, \"\", \"\", line, len);\n \t\tbreak;\n \tcase DIFF_SYMBOL_CONTEXT_INCOMPLETE:\n+\t\tset = diff_get_color_opt(o, DIFF_CONTEXT);\n+\t\treset = diff_get_color_opt(o, DIFF_RESET);\n+\t\temit_line(o, set, reset, line, len);\n+\t\tbreak;\n \tcase DIFF_SYMBOL_CONTEXT_MARKER:\n \t\tcontext = diff_get_color_opt(o, DIFF_CONTEXT);\n \t\treset = diff_get_color_opt(o, DIFF_RESET);\n@@ -1668,6 +1672,13 @@ static void emit_context_line(struct emit_callback *ecbdata,\n \temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT, line, len, flags);\n }\n \n+static void emit_incomplete_line(struct emit_callback *ecbdata,\n+\t\t\t\t const char *line, int len)\n+{\n+\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n+\t\t\t line, len, 0);\n+}\n+\n static void emit_hunk_header(struct emit_callback *ecbdata,\n \t\t\t     const char *line, int len)\n {\n@@ -2442,8 +2453,7 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \t\t\tBUG(\"fn_out_consume: '\\\\No newline' after unknown line (%c)\",\n \t\t\t    ecbdata->last_line_kind);\n \t\t}\n-\t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n-\t\t\t\t line, len, 0);\n+\t\temit_incomplete_line(ecbdata, line, len);\n \t\tbreak;\n \tdefault:\n \t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n-- \n2.52.0-rc0\n\n"},{"id":"530164","messageId":"20251104020928.582199-7-gitster@pobox.com","threadId":"64429","inReplyTo":"20251104020928.582199-1-gitster@pobox.com","subject":"[PATCH 06/12] diff: call emit_callback ecbdata everywhere","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-04T02:09:22Z","receivedAt":"2025-11-04T02:09:42Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Everybody else, except for emit_rewrite_lines(), calls the\nemit_callback data ecbdata.  Make sure we call the same thing by\nthe same name for consistency.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 12 ++++++------\n 1 file changed, 6 insertions(+), 6 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex d388d318e4..347cd9c6e9 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1780,7 +1780,7 @@ static void add_line_count(struct strbuf *out, int count)\n \t}\n }\n \n-static void emit_rewrite_lines(struct emit_callback *ecb,\n+static void emit_rewrite_lines(struct emit_callback *ecbdata,\n \t\t\t       int prefix, const char *data, int size)\n {\n \tconst char *endp = NULL;\n@@ -1791,17 +1791,17 @@ static void emit_rewrite_lines(struct emit_callback *ecb,\n \t\tendp = memchr(data, '\\n', size);\n \t\tlen = endp ? (endp - data + 1) : size;\n \t\tif (prefix != '+') {\n-\t\t\tecb->lno_in_preimage++;\n-\t\t\temit_del_line(ecb, data, len);\n+\t\t\tecbdata->lno_in_preimage++;\n+\t\t\temit_del_line(ecbdata, data, len);\n \t\t} else {\n-\t\t\tecb->lno_in_postimage++;\n-\t\t\temit_add_line(ecb, data, len);\n+\t\t\tecbdata->lno_in_postimage++;\n+\t\t\temit_add_line(ecbdata, data, len);\n \t\t}\n \t\tsize -= len;\n \t\tdata += len;\n \t}\n \tif (!endp)\n-\t\temit_diff_symbol(ecb->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);\n+\t\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);\n }\n \n static void emit_rewrite_diff(const char *name_a,\n-- \n2.52.0-rc0\n\n"},{"id":"530165","messageId":"20251104020928.582199-8-gitster@pobox.com","threadId":"64429","inReplyTo":"20251104020928.582199-1-gitster@pobox.com","subject":"[PATCH 07/12] diff: update the way rewrite diff handles incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-04T02:09:23Z","receivedAt":"2025-11-04T02:09:43Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The diff_symbol based output framework uses one DIFF_SYMBOL_* enum\nvalue per the kind of output lines of \"git diff\", which corresponds\nto one output line from the xdiff machinery used internally.  Most\nnotably, DIFF_SYMBOL_PLUS and DIFF_SYMBOL_MINUS that correspond to\n\"+\" and \"-\" lines are designed to always take a complete line, even\nif the output from xdiff machinery may produce \"\\ No newline at the\nend of file\" immediately after them.\n\nBut this is not true in the rewrite-diff codepath, which completely\nbypasses the xdiff machinery.  Since the code path feeds the bytes\ndirectly from the payload to the output routines, the output layer\nhas to deal with an incomplete line with DIFF_SYMBOL_PLUS and\nDIFF_SYMBOL_MINUS, which never would see an incomplete line in the\nnormal code paths.  This lack of final newline is compensated by an\nugly hack for a fabricated DIFF_SYMBOL_NO_LF_EOF token to inject an\nextra newline to the output to simulate output coming from the xdiff\nmachinery.\n\nRevamp the way the complete-rewrite code path feeds the lines to the\noutput layer by treating the last line of the pre/post image when it\nis an incomplete line specially.\n\nThis lets us remove the DIFF_SYMBOL_NO_LF_EOF hack and use the usual\nDIFF_SYMBOL_CONTEXT_INCOMPLETE code path, which will later learn how\nto handle whitespace errors.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 33 +++++++++++++++++++--------------\n 1 file changed, 19 insertions(+), 14 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 347cd9c6e9..99298720f4 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -797,7 +797,6 @@ enum diff_symbol {\n \tDIFF_SYMBOL_CONTEXT_INCOMPLETE,\n \tDIFF_SYMBOL_PLUS,\n \tDIFF_SYMBOL_MINUS,\n-\tDIFF_SYMBOL_NO_LF_EOF,\n \tDIFF_SYMBOL_CONTEXT_FRAGINFO,\n \tDIFF_SYMBOL_CONTEXT_MARKER,\n \tDIFF_SYMBOL_SEPARATOR\n@@ -1352,7 +1351,6 @@ static void emit_line_ws_markup(struct diff_options *o,\n static void emit_diff_symbol_from_struct(struct diff_options *o,\n \t\t\t\t\t struct emitted_diff_symbol *eds)\n {\n-\tstatic const char *nneof = \" No newline at end of file\\n\";\n \tconst char *context, *reset, *set, *set_sign, *meta, *fraginfo;\n \n \tenum diff_symbol s = eds->s;\n@@ -1361,13 +1359,6 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n \tunsigned flags = eds->flags;\n \n \tswitch (s) {\n-\tcase DIFF_SYMBOL_NO_LF_EOF:\n-\t\tcontext = diff_get_color_opt(o, DIFF_CONTEXT);\n-\t\treset = diff_get_color_opt(o, DIFF_RESET);\n-\t\tputc('\\n', o->file);\n-\t\temit_line_0(o, context, NULL, 0, reset, '\\\\',\n-\t\t\t    nneof, strlen(nneof));\n-\t\tbreak;\n \tcase DIFF_SYMBOL_SUBMODULE_HEADER:\n \tcase DIFF_SYMBOL_SUBMODULE_ERROR:\n \tcase DIFF_SYMBOL_SUBMODULE_PIPETHROUGH:\n@@ -1786,22 +1777,36 @@ static void emit_rewrite_lines(struct emit_callback *ecbdata,\n \tconst char *endp = NULL;\n \n \twhile (0 < size) {\n-\t\tint len;\n+\t\tint len, plen;\n+\t\tchar *pdata = NULL;\n \n \t\tendp = memchr(data, '\\n', size);\n \t\tlen = endp ? (endp - data + 1) : size;\n+\t\tplen = len;\n+\n+\t\tif (!endp) {\n+\t\t\tplen = len + 1;\n+\t\t\tpdata = xmalloc(plen + 2);\n+\t\t\tmemcpy(pdata, data, len);\n+\t\t\tpdata[len] = '\\n';\n+\t\t\tpdata[len + 1] = '\\0';\n+\t\t}\n \t\tif (prefix != '+') {\n \t\t\tecbdata->lno_in_preimage++;\n-\t\t\temit_del_line(ecbdata, data, len);\n+\t\t\temit_del_line(ecbdata, pdata ? pdata : data, plen);\n \t\t} else {\n \t\t\tecbdata->lno_in_postimage++;\n-\t\t\temit_add_line(ecbdata, data, len);\n+\t\t\temit_add_line(ecbdata, pdata ? pdata : data, plen);\n \t\t}\n+\t\tfree(pdata);\n \t\tsize -= len;\n \t\tdata += len;\n \t}\n-\tif (!endp)\n-\t\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);\n+\tif (!endp) {\n+\t\tstatic const char nneof[] = \"\\\\ No newline at end of file\\n\";\n+\t\tecbdata->last_line_kind = prefix;\n+\t\temit_incomplete_line(ecbdata, nneof, sizeof(nneof) - 1);\n+\t}\n }\n \n static void emit_rewrite_diff(const char *name_a,\n-- \n2.52.0-rc0\n\n"},{"id":"530166","messageId":"20251104020928.582199-9-gitster@pobox.com","threadId":"64429","inReplyTo":"20251104020928.582199-1-gitster@pobox.com","subject":"[PATCH 08/12] apply: revamp the parsing of incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-04T02:09:24Z","receivedAt":"2025-11-04T02:09:45Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"A patch file represents the incomplete line at the end of the file\nwith two lines, one that is the usual \"context\" with \" \" as the\nfirst letter, \"added\" with \"+\" as the first letter, or \"removed\"\nwith \"-\" as the first letter that shows the content of the line,\nplus an extra \"\\ No newline at the end of file\" line that comes\nimmediately after it.\n\nEver since the apply machinery was written, the \"git apply\"\nmachinery parses \"\\ No newline at the end of file\" line\nindependently, without even knowing what line the incomplete-ness\napplies to, simply because it does not even remember what the\nprevious line was.\n\nThis poses a problem if we want to check and warn on an incomplete\nline.  Revamp the code that parses a fragment, to actually drop the\n'\\n' at the end of the incoming patch file that terminates a line,\nso that check_whitespace() calls made from the code path actually\nsees an incomplete as incomplete.\n\nNote that the result of this parsing is not directly used by the\ncode path that applies the patch.  apply_one_fragment() function\nalready checks if each of the patch text it handles is followed by a\nline that begins with a backslash to drop the newline at the end of\nthe current line it is looking at.  In a sense, this patch harmonizes\nthe behaviour of the parsing side to what is already done in the\napplication side.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n apply.c | 70 ++++++++++++++++++++++++++++++++++++++++-----------------\n 1 file changed, 49 insertions(+), 21 deletions(-)\n\ndiff --git a/apply.c b/apply.c\nindex a2ceb3fb40..2b0f8bdab5 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -1670,6 +1670,35 @@ static void check_old_for_crlf(struct patch *patch, const char *line, int len)\n }\n \n \n+/*\n+ * Just saw a single line in a fragment.  If it is a part of this hunk\n+ * that is a context \" \", an added \"+\", or a removed \"-\" line, it may\n+ * be followed by \"\\\\ No newline...\" to signal that the last \"\\n\" on\n+ * this line needs to be dropped.  Depending on locale settings when\n+ * the patch was produced we don't know what this line would exactly\n+ * say. The only thing we do know is that it begins with \"\\ \".\n+ * Checking for 12 is just for sanity check; \"\\ No newline...\" would\n+ * be at least that long in any l10n.\n+ *\n+ * Return 0 if the line we saw is not followed by \"\\ No newline...\",\n+ * or length of that line.  The caller will use it to skip over the\n+ * \"\\ No newline...\" line.\n+ */\n+static int adjust_incomplete(const char *line, int len,\n+\t\t\t     unsigned long size)\n+{\n+\tint nextlen;\n+\n+\tif (*line != '\\n' && *line != ' ' && *line != '+' && *line != '-')\n+\t\treturn 0;\n+\tif (size - len < 12 || memcmp(line + len, \"\\\\ \", 2))\n+\t\treturn 0;\n+\tnextlen = linelen(line + len, size - len);\n+\tif (nextlen < 12)\n+\t\treturn 0;\n+\treturn nextlen;\n+}\n+\n /*\n  * Parse a unified diff. Note that this really needs to parse each\n  * fragment separately, since the only way to know the difference\n@@ -1684,6 +1713,7 @@ static int parse_fragment(struct apply_state *state,\n {\n \tint added, deleted;\n \tint len = linelen(line, size), offset;\n+\tint skip_len = 0;\n \tunsigned long oldlines, newlines;\n \tunsigned long leading, trailing;\n \n@@ -1710,6 +1740,22 @@ static int parse_fragment(struct apply_state *state,\n \t\tlen = linelen(line, size);\n \t\tif (!len || line[len-1] != '\\n')\n \t\t\treturn -1;\n+\n+\t\t/*\n+\t\t * For an incomplete line, skip_len counts the bytes\n+\t\t * on \"\\\\ No newline...\" marker line that comes next\n+\t\t * to the current line.\n+\t\t *\n+\t\t * Reduce \"len\" to drop the newline at the end of\n+\t\t * line[], but add one to \"skip_len\", which will be\n+\t\t * added back to \"len\" for the next iteration, to\n+\t\t * compensate.\n+\t\t */\n+\t\tskip_len = adjust_incomplete(line, len, size);\n+\t\tif (skip_len) {\n+\t\t\tlen--;\n+\t\t\tskip_len++;\n+\t\t}\n \t\tswitch (*line) {\n \t\tdefault:\n \t\t\treturn -1;\n@@ -1745,20 +1791,10 @@ static int parse_fragment(struct apply_state *state,\n \t\t\tnewlines--;\n \t\t\ttrailing = 0;\n \t\t\tbreak;\n-\n-\t\t/*\n-\t\t * We allow \"\\ No newline at end of file\". Depending\n-\t\t * on locale settings when the patch was produced we\n-\t\t * don't know what this line looks like. The only\n-\t\t * thing we do know is that it begins with \"\\ \".\n-\t\t * Checking for 12 is just for sanity check -- any\n-\t\t * l10n of \"\\ No newline...\" is at least that long.\n-\t\t */\n-\t\tcase '\\\\':\n-\t\t\tif (len < 12 || memcmp(line, \"\\\\ \", 2))\n-\t\t\t\treturn -1;\n-\t\t\tbreak;\n \t\t}\n+\n+\t\t/* eat the \"\\\\ No newline...\" as well, if exists */\n+\t\tlen += skip_len;\n \t}\n \tif (oldlines || newlines)\n \t\treturn -1;\n@@ -1768,14 +1804,6 @@ static int parse_fragment(struct apply_state *state,\n \tfragment->leading = leading;\n \tfragment->trailing = trailing;\n \n-\t/*\n-\t * If a fragment ends with an incomplete line, we failed to include\n-\t * it in the above loop because we hit oldlines == newlines == 0\n-\t * before seeing it.\n-\t */\n-\tif (12 < size && !memcmp(line, \"\\\\ \", 2))\n-\t\toffset += linelen(line, size);\n-\n \tpatch->lines_added += added;\n \tpatch->lines_deleted += deleted;\n \n-- \n2.52.0-rc0\n\n"},{"id":"530167","messageId":"20251104020928.582199-10-gitster@pobox.com","threadId":"64429","inReplyTo":"20251104020928.582199-1-gitster@pobox.com","subject":"[PATCH 09/12] whitespace: allocate a few more bits","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-04T02:09:25Z","receivedAt":"2025-11-04T02:09:47Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Reserve a few more bits in the diff flags word to be used for future\nwhitespace rules.  No behaviour changes intended.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 16 ++++++++--------\n diff.h |  6 +++---\n ws.h   |  2 +-\n 3 files changed, 12 insertions(+), 12 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 99298720f4..8d03146aaa 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -804,15 +804,15 @@ enum diff_symbol {\n \n /*\n  * Flags for content lines:\n- * 0..11 are whitespace rules (see ws.h)\n- * 12..14 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD\n- * 16 is marking if the line is blank at EOF\n- * 17..19 are used for color-moved.\n+ * 0..15 are whitespace rules (see ws.h)\n+ * 16..18 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD\n+ * 19 is marking if the line is blank at EOF\n+ * 20..22 are used for color-moved.\n  */\n-#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF\t(1<<16)\n-#define DIFF_SYMBOL_MOVED_LINE\t\t\t(1<<17)\n-#define DIFF_SYMBOL_MOVED_LINE_ALT\t\t(1<<18)\n-#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING\t(1<<19)\n+#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF\t(1<<19)\n+#define DIFF_SYMBOL_MOVED_LINE\t\t\t(1<<20)\n+#define DIFF_SYMBOL_MOVED_LINE_ALT\t\t(1<<21)\n+#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING\t(1<<22)\n \n #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)\n \ndiff --git a/diff.h b/diff.h\nindex cbd355cf50..422658407d 100644\n--- a/diff.h\n+++ b/diff.h\n@@ -331,9 +331,9 @@ struct diff_options {\n \n \tint ita_invisible_in_index;\n /* white-space error highlighting */\n-#define WSEH_NEW        (1<<12)\n-#define WSEH_CONTEXT    (1<<13)\n-#define WSEH_OLD        (1<<14)\n+#define WSEH_NEW        (1<<16)\n+#define WSEH_CONTEXT    (1<<17)\n+#define WSEH_OLD        (1<<18)\n \tunsigned ws_error_highlight;\n \tconst char *prefix;\n \tint prefix_length;\ndiff --git a/ws.h b/ws.h\nindex 23708efb73..c77d3b6b19 100644\n--- a/ws.h\n+++ b/ws.h\n@@ -21,7 +21,7 @@ struct strbuf;\n #define WS_TAB_WIDTH_MASK       ((1<<6)-1)\n \n /* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */\n-#define WS_RULE_MASK            ((1<<12)-1)\n+#define WS_RULE_MASK            ((1<<16)-1)\n \n extern unsigned whitespace_rule_cfg;\n unsigned whitespace_rule(struct index_state *, const char *);\n-- \n2.52.0-rc0\n\n"},{"id":"530168","messageId":"20251104020928.582199-11-gitster@pobox.com","threadId":"64429","inReplyTo":"20251104020928.582199-1-gitster@pobox.com","subject":"[PATCH 10/12] apply: check and fix incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-04T02:09:26Z","receivedAt":"2025-11-04T02:09:49Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The final line of a file that lacks the terminating newline at its\nend is called an incomplete line.  In general they are frowned upon\nfor many reasons (imagine concatenating two files with \"cat A B\" and\nwhat happens when A ends in an incomplete line, for example), and\ntext-oriented tools often mishandle such a line.\n\nIntroduce a new whitespace rule \"incomplete-line\", which is off by\ndefault for backward compatibility's sake, so that \"git apply\n--whitespace={fix,warn,error}\" can notice, warn against, and fix\nthem.\n\nAs one of the new test shows, if you modify contents on an\nincomplete line in the original and leave the resulting line\nincomplete, it is still considered a whitespace error, the reasoning\nbeing that \"you'd better fix it while at it if you are making a\nchange on an incomplete line anyway\", which may controversial.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n t/t4124-apply-ws-rule.sh | 112 +++++++++++++++++++++++++++++++++++++++\n ws.c                     |  20 +++++++\n ws.h                     |   1 +\n 3 files changed, 133 insertions(+)\n\ndiff --git a/t/t4124-apply-ws-rule.sh b/t/t4124-apply-ws-rule.sh\nindex 485c7d2d12..568805df31 100755\n--- a/t/t4124-apply-ws-rule.sh\n+++ b/t/t4124-apply-ws-rule.sh\n@@ -556,4 +556,116 @@ test_expect_success 'whitespace check skipped for excluded paths' '\n \tgit apply --include=used --stat --whitespace=error <patch\n '\n \n+test_expect_success 'check incomplete lines (setup)' '\n+\trm -f .gitattributes &&\n+\tgit config core.whitespace incomplete-line\n+'\n+\n+test_expect_success 'incomplete context line (not an error)' '\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\t(test_write_lines 1 2 3 0 5 && printf 6) >sample2-i &&\n+\tcat sample-i >target &&\n+\tgit add target &&\n+\tcat sample2-i >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error <patch &&\n+\ttest_cmp sample2-i target &&\n+\n+\tcat sample2-i >target &&\n+\tgit apply --whitespace=error -R <patch &&\n+\ttest_cmp sample-i target\n+'\n+\n+test_expect_success 'last line made incomplete (error)' '\n+\ttest_write_lines 1 2 3 4 5 6 >sample &&\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\tcat sample >target &&\n+\tgit add target &&\n+\tcat sample-i >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample >target &&\n+\ttest_must_fail git apply --whitespace=error <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error -R <patch &&\n+\ttest_cmp sample target &&\n+\n+\tcat sample >target &&\n+\tgit apply --whitespace=fix <patch &&\n+\ttest_cmp sample target\n+'\n+\n+test_expect_success 'incomplete line removed at the end (not an error)' '\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\ttest_write_lines 1 2 3 4 5 6 >sample &&\n+\tcat sample-i >target &&\n+\tgit add target &&\n+\tcat sample >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error <patch &&\n+\ttest_cmp sample target &&\n+\n+\tcat sample >target &&\n+\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample >target &&\n+\tgit apply --whitespace=fix -R <patch &&\n+\ttest_cmp sample target\n+'\n+\n+test_expect_success 'incomplete line corrected at the end (not an error)' '\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\ttest_write_lines 1 2 3 4 5 7 >sample3 &&\n+\tcat sample-i >target &&\n+\tgit add target &&\n+\tcat sample3 >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error <patch &&\n+\ttest_cmp sample3 target &&\n+\n+\tcat sample3 >target &&\n+\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample3 >target &&\n+\tgit apply --whitespace=fix -R <patch &&\n+\ttest_cmp sample target\n+'\n+\n+test_expect_success 'incomplete line modified at the end (error)' '\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\t(test_write_lines 1 2 3 4 5 && printf 7) >sample3-i &&\n+\ttest_write_lines 1 2 3 4 5 6 >sample &&\n+\ttest_write_lines 1 2 3 4 5 7 >sample3 &&\n+\tcat sample-i >target &&\n+\tgit add target &&\n+\tcat sample3-i >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample-i >target &&\n+\ttest_must_fail git apply --whitespace=error <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample3-i >target &&\n+\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=fix <patch &&\n+\ttest_cmp sample3 target &&\n+\n+\tcat sample3-i >target &&\n+\tgit apply --whitespace=fix -R <patch &&\n+\ttest_cmp sample target\n+'\n+\n test_done\ndiff --git a/ws.c b/ws.c\nindex 70acee3337..6cc2466c0c 100644\n--- a/ws.c\n+++ b/ws.c\n@@ -26,6 +26,7 @@ static struct whitespace_rule {\n \t{ \"blank-at-eol\", WS_BLANK_AT_EOL, 0 },\n \t{ \"blank-at-eof\", WS_BLANK_AT_EOF, 0 },\n \t{ \"tab-in-indent\", WS_TAB_IN_INDENT, 0, 1 },\n+\t{ \"incomplete-line\", WS_INCOMPLETE_LINE, 0, 0 },\n };\n \n unsigned parse_whitespace_rule(const char *string)\n@@ -139,6 +140,11 @@ char *whitespace_error_string(unsigned ws)\n \t\t\tstrbuf_addstr(&err, \", \");\n \t\tstrbuf_addstr(&err, \"tab in indent\");\n \t}\n+\tif (ws & WS_INCOMPLETE_LINE) {\n+\t\tif (err.len)\n+\t\t\tstrbuf_addstr(&err, \", \");\n+\t\tstrbuf_addstr(&err, \"no newline at the end of file\");\n+\t}\n \treturn strbuf_detach(&err, NULL);\n }\n \n@@ -180,6 +186,9 @@ static unsigned ws_check_emit_1(const char *line, int len, unsigned ws_rule,\n \tif (trailing_whitespace == -1)\n \t\ttrailing_whitespace = len;\n \n+\tif (!trailing_newline && (ws_rule & WS_INCOMPLETE_LINE))\n+\t\tresult |= WS_INCOMPLETE_LINE;\n+\n \t/* Check indentation */\n \tfor (i = 0; i < trailing_whitespace; i++) {\n \t\tif (line[i] == ' ')\n@@ -291,6 +300,17 @@ void ws_fix_copy(struct strbuf *dst, const char *src, int len, unsigned ws_rule,\n \tint last_space_in_indent = -1;\n \tint need_fix_leading_space = 0;\n \n+\t/*\n+\t * Remembering that we need to add '\\n' at the end\n+\t * is sufficient to fix an incomplete line.\n+\t */\n+\tif (ws_rule & WS_INCOMPLETE_LINE) {\n+\t\tif (0 < len && src[len - 1] != '\\n') {\n+\t\t\tfixed = 1;\n+\t\t\tadd_nl_to_tail = 1;\n+\t\t}\n+\t}\n+\n \t/*\n \t * Strip trailing whitespace\n \t */\ndiff --git a/ws.h b/ws.h\nindex c77d3b6b19..06d5cb73f8 100644\n--- a/ws.h\n+++ b/ws.h\n@@ -15,6 +15,7 @@ struct strbuf;\n #define WS_CR_AT_EOL            (1<<9)\n #define WS_BLANK_AT_EOF         (1<<10)\n #define WS_TAB_IN_INDENT        (1<<11)\n+#define WS_INCOMPLETE_LINE      (1<<12)\n \n #define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)\n #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)\n-- \n2.52.0-rc0\n\n"},{"id":"530169","messageId":"20251104020928.582199-12-gitster@pobox.com","threadId":"64429","inReplyTo":"20251104020928.582199-1-gitster@pobox.com","subject":"[PATCH 11/12] diff: highlight and error out on incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-04T02:09:27Z","receivedAt":"2025-11-04T02:09:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Teach \"git diff\" to highlight \"\\ No newline at end of file\" message\nas a whitespace error when incomplete-line whitespace error class is\nin effect.  Thanks to the previous refactoring of complete rewrite\ncode path, we can do this at a single place.\n\nUnlike whitespace errors in the payload where we need to annotate in\nline, possibly using colors, the line that has whitespace problems,\nwe have a dedicated line already that can serve as the error\nmessage, so paint it as a whitespace error message.\n\nAlso teach \"git diff --check\" to notice incomplete lines as\nwhitespace errors and report when incomplete-line whitespace error\nclass is in effect.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c                     | 29 ++++++++++++++++--\n t/t4015-diff-whitespace.sh | 63 +++++++++++++++++++++++++++++++++++---\n 2 files changed, 86 insertions(+), 6 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 8d03146aaa..965b97f7f0 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1370,7 +1370,11 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n \t\temit_line(o, \"\", \"\", line, len);\n \t\tbreak;\n \tcase DIFF_SYMBOL_CONTEXT_INCOMPLETE:\n-\t\tset = diff_get_color_opt(o, DIFF_CONTEXT);\n+\t\tif ((flags & WS_INCOMPLETE_LINE) &&\n+\t\t    (flags & o->ws_error_highlight))\n+\t\t\tset = diff_get_color_opt(o, DIFF_WHITESPACE);\n+\t\telse\n+\t\t\tset = diff_get_color_opt(o, DIFF_CONTEXT);\n \t\treset = diff_get_color_opt(o, DIFF_RESET);\n \t\temit_line(o, set, reset, line, len);\n \t\tbreak;\n@@ -1666,8 +1670,14 @@ static void emit_context_line(struct emit_callback *ecbdata,\n static void emit_incomplete_line(struct emit_callback *ecbdata,\n \t\t\t\t const char *line, int len)\n {\n+\tint last_line_kind = ecbdata->last_line_kind;\n+\tunsigned flags = (last_line_kind == '+'\n+\t\t\t  ? WSEH_NEW\n+\t\t\t  : last_line_kind == '-'\n+\t\t\t  ? WSEH_OLD\n+\t\t\t  : WSEH_CONTEXT) | ecbdata->ws_rule;\n \temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n-\t\t\t line, len, 0);\n+\t\t\t line, len, flags);\n }\n \n static void emit_hunk_header(struct emit_callback *ecbdata,\n@@ -3257,6 +3267,7 @@ struct checkdiff_t {\n \tstruct diff_options *o;\n \tunsigned ws_rule;\n \tunsigned status;\n+\tint last_line_kind;\n };\n \n static int is_conflict_marker(const char *line, int marker_size, unsigned long len)\n@@ -3295,6 +3306,7 @@ static void checkdiff_consume_hunk(void *priv,\n static int checkdiff_consume(void *priv, char *line, unsigned long len)\n {\n \tstruct checkdiff_t *data = priv;\n+\tint last_line_kind;\n \tint marker_size = data->conflict_marker_size;\n \tconst char *ws = diff_get_color(data->o->use_color, DIFF_WHITESPACE);\n \tconst char *reset = diff_get_color(data->o->use_color, DIFF_RESET);\n@@ -3305,6 +3317,8 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)\n \tassert(data->o);\n \tline_prefix = diff_line_prefix(data->o);\n \n+\tlast_line_kind = data->last_line_kind;\n+\tdata->last_line_kind = line[0];\n \tif (line[0] == '+') {\n \t\tunsigned bad;\n \t\tdata->lineno++;\n@@ -3327,6 +3341,17 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)\n \t\t\t      data->o->file, set, reset, ws);\n \t} else if (line[0] == ' ') {\n \t\tdata->lineno++;\n+\t} else if (line[0] == '\\\\') {\n+\t\t/* no newline at the end of the line */\n+\t\tif ((data->ws_rule & WS_INCOMPLETE_LINE) &&\n+\t\t    (last_line_kind == '+')) {\n+\t\t\tunsigned bad = WS_INCOMPLETE_LINE;\n+\t\t\tdata->status |= bad;\n+\t\t\terr = whitespace_error_string(bad);\n+\t\t\tfprintf(data->o->file, \"%s%s:%d: %s.\\n\",\n+\t\t\t\tline_prefix, data->filename, data->lineno, err);\n+\t\t\tfree(err);\n+\t\t}\n \t}\n \treturn 0;\n }\ndiff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh\nindex 9de7f73f42..138730cbce 100755\n--- a/t/t4015-diff-whitespace.sh\n+++ b/t/t4015-diff-whitespace.sh\n@@ -43,6 +43,49 @@ do\n \t'\n done\n \n+test_expect_success \"incomplete line in both pre- and post-image context\" '\n+\t(echo foo && echo baz | tr -d \"\\012\") >x &&\n+\tgit add x &&\n+\t(echo bar && echo baz | tr -d \"\\012\") >x &&\n+\tgit diff x &&\n+\tgit -c core.whitespace=incomplete diff --check x &&\n+\tgit diff -R x &&\n+\tgit -c core.whitespace=incomplete diff -R --check x\n+'\n+\n+test_expect_success \"incomplete lines on both pre- and post-image\" '\n+\t# The interpretation taken here is \"since you are toucing\n+\t# the line anyway, you would better fix the incomplete line\n+\t# while you are at it.\"  but this is debatable.\n+\techo foo | tr -d \"\\012\" >x &&\n+\tgit add x &&\n+\techo bar | tr -d \"\\012\" >x &&\n+\tgit diff x &&\n+\ttest_must_fail git -c core.whitespace=incomplete diff --check x &&\n+\tgit diff -R x &&\n+\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x\n+'\n+\n+test_expect_success \"fix incomplete line in pre-image\" '\n+\techo foo | tr -d \"\\012\" >x &&\n+\tgit add x &&\n+\techo bar >x &&\n+\tgit diff x &&\n+\tgit -c core.whitespace=incomplete diff --check x &&\n+\tgit diff -R x &&\n+\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x\n+'\n+\n+test_expect_success \"new incomplete line in post-image\" '\n+\techo foo >x &&\n+\tgit add x &&\n+\techo bar | tr -d \"\\012\" >x &&\n+\tgit diff x &&\n+\ttest_must_fail git -c core.whitespace=incomplete diff --check x &&\n+\tgit diff -R x &&\n+\tgit -c core.whitespace=incomplete diff -R --check x\n+'\n+\n test_expect_success \"Ray Lehtiniemi's example\" '\n \tcat <<-\\EOF >x &&\n \tdo {\n@@ -1040,7 +1083,8 @@ test_expect_success 'ws-error-highlight test setup' '\n \t{\n \t\techo \"0. blank-at-eol \" &&\n \t\techo \"1. still-blank-at-eol \" &&\n-\t\techo \"2. and a new line \"\n+\t\techo \"2. and a new line \" &&\n+\t\tprintf \"3. and more\"\n \t} >x &&\n \tnew_hash_x=$(git hash-object x) &&\n \tafter=$(git rev-parse --short \"$new_hash_x\") &&\n@@ -1050,11 +1094,13 @@ test_expect_success 'ws-error-highlight test setup' '\n \t<BOLD>index $before..$after 100644<RESET>\n \t<BOLD>--- a/x<RESET>\n \t<BOLD>+++ b/x<RESET>\n-\t<CYAN>@@ -1,2 +1,3 @@<RESET>\n+\t<CYAN>@@ -1,2 +1,4 @@<RESET>\n \t 0. blank-at-eol <RESET>\n \t<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>\n \t<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>\n \t<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>\n+\t<GREEN>+<RESET><GREEN>3. and more<RESET>\n+\t<BLUE>\\ No newline at end of file<RESET>\n \tEOF\n \n \tcat >expect.all <<-EOF &&\n@@ -1062,11 +1108,13 @@ test_expect_success 'ws-error-highlight test setup' '\n \t<BOLD>index $before..$after 100644<RESET>\n \t<BOLD>--- a/x<RESET>\n \t<BOLD>+++ b/x<RESET>\n-\t<CYAN>@@ -1,2 +1,3 @@<RESET>\n+\t<CYAN>@@ -1,2 +1,4 @@<RESET>\n \t <RESET>0. blank-at-eol<RESET><BLUE> <RESET>\n \t<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>\n \t<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>\n \t<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>\n+\t<GREEN>+<RESET><GREEN>3. and more<RESET>\n+\t<BLUE>\\ No newline at end of file<RESET>\n \tEOF\n \n \tcat >expect.none <<-EOF\n@@ -1074,16 +1122,19 @@ test_expect_success 'ws-error-highlight test setup' '\n \t<BOLD>index $before..$after 100644<RESET>\n \t<BOLD>--- a/x<RESET>\n \t<BOLD>+++ b/x<RESET>\n-\t<CYAN>@@ -1,2 +1,3 @@<RESET>\n+\t<CYAN>@@ -1,2 +1,4 @@<RESET>\n \t 0. blank-at-eol <RESET>\n \t<RED>-1. blank-at-eol <RESET>\n \t<GREEN>+1. still-blank-at-eol <RESET>\n \t<GREEN>+2. and a new line <RESET>\n+\t<GREEN>+3. and more<RESET>\n+\t\\ No newline at end of file<RESET>\n \tEOF\n \n '\n \n test_expect_success 'test --ws-error-highlight option' '\n+\tgit config core.whitespace blank-at-eol,incomplete-line &&\n \n \tgit diff --color --ws-error-highlight=default,old >current.raw &&\n \ttest_decode_color <current.raw >current &&\n@@ -1100,6 +1151,7 @@ test_expect_success 'test --ws-error-highlight option' '\n '\n \n test_expect_success 'test diff.wsErrorHighlight config' '\n+\tgit config core.whitespace blank-at-eol,incomplete-line &&\n \n \tgit -c diff.wsErrorHighlight=default,old diff --color >current.raw &&\n \ttest_decode_color <current.raw >current &&\n@@ -1116,6 +1168,7 @@ test_expect_success 'test diff.wsErrorHighlight config' '\n '\n \n test_expect_success 'option overrides diff.wsErrorHighlight' '\n+\tgit config core.whitespace blank-at-eol,incomplete-line &&\n \n \tgit -c diff.wsErrorHighlight=none \\\n \t\tdiff --color --ws-error-highlight=default,old >current.raw &&\n@@ -1135,6 +1188,8 @@ test_expect_success 'option overrides diff.wsErrorHighlight' '\n '\n \n test_expect_success 'detect moved code, complete file' '\n+\tgit config core.whitespace blank-at-eol &&\n+\n \tgit reset --hard &&\n \tcat <<-\\EOF >test.c &&\n \t#include<stdio.h>\n-- \n2.52.0-rc0\n\n"},{"id":"530170","messageId":"20251104020928.582199-13-gitster@pobox.com","threadId":"64429","inReplyTo":"20251104020928.582199-1-gitster@pobox.com","subject":"[PATCH 12/12] attr: enable incomplete-line whitespace error for this project","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-04T02:09:28Z","receivedAt":"2025-11-04T02:09:52Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Now \"git diff --check\" and \"git apply --whitespace=warn/fix\" learned\nincomplete line is a whitespace error, enable them for this project\nto prevent patches to add new incomplete lines to our sources.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n .gitattributes | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/.gitattributes b/.gitattributes\nindex 32583149c2..0accd23848 100644\n--- a/.gitattributes\n+++ b/.gitattributes\n@@ -1,6 +1,6 @@\n * whitespace=!indent,trail,space\n-*.[ch] whitespace=indent,trail,space diff=cpp\n-*.sh whitespace=indent,trail,space text eol=lf\n+*.[ch] whitespace=indent,trail,space,incomplete diff=cpp\n+*.sh whitespace=indent,trail,space,incomplete text eol=lf\n *.perl text eol=lf diff=perl\n *.pl text eof=lf diff=perl\n *.pm text eol=lf diff=perl\n-- \n2.52.0-rc0\n\n"},{"id":"530269","messageId":"20251105213052.1499224-1-gitster@pobox.com","threadId":"64429","inReplyTo":"20251104020928.582199-1-gitster@pobox.com","subject":"[PATCH v2 00/12] Incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-05T21:30:40Z","receivedAt":"2025-11-05T21:30:54Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"One of the common kind of whitespace errors is to lack the final\nnewline at the end of a file, but so far, neither \"git diff\" or \"git\napply\" did anything about them.\n\nThis series introduces \"incomplete-line\" whitespace error class,\nthat you can add to either the core.whitespace configuration\nvariable, or the whitespace attribute in your .gitattributes files.\n\nThe class is disabled by default, so the final step enables it for\nour project by defining it in the .gitattributes file.\n\nThe incomplete line marker that is given for a context line is not\nconsidered an error.  The reasoning is that your preimage did have\nincomplete line, but you did not touch the contents on that\nincomplete line in your patch, so you left the line intact.  It is\nnot a new breakage you are responsible for.\n\nIf the incomplete line marker follows a postimage line, on the other\nhand, it means that you added a new line at the end of the file that\nis incomplete *and* that line did not exist in the preimage.  The\nlast line of the preimage may have been incomplete already, but then\nyou updated the contents on that line, so you could have easily\nfixed the incompleteness of the line while at it.  Either way, you\nare responsible for the incompleteness of the last ine in the\nresulting file.\n\nThe organization of the series is as follows.\n\n * The first patch [01/12] is a clean-up we have seen earlier on the\n   list already (https://lore.kernel.org/git/xmqqfrb4hyjl.fsf@gitster.g/).\n\n * The patches [02/12] - [08/12] are preliminary clean-up made to\n   both \"git diff\" and \"git apply\" machinery.\n\n * The patch [09/12] shifts the bit assignment (cleaned-up in\n   [01/12] without changing any values) to make room for new\n   whitespace error class (which was last updated in 2007 IIRC, so\n   the set of whitespace errors surprisingly haven't changed for\n   quite some time), and defines the new \"incomplete-line\" class.\n\n * The patch [10/12] teaches \"git apply --whitespace=<mode>\" and\n   \"git apply --check\" about the incomplete-line error class.\n\n * The patch [11/12] teaches \"git diff [--check]\" about the\n   incomplete-line error class.\n\n * The final patch [12/12] enables the incomplete-line error class\n   for our project for C source files and shell scripts.  I didn't\n   touch the cover-all * entry.\n\nChanges in v2:\n\n - rolled the definition (but not implementation) of the new\n   \"incomplete-line\" class into step [09/12] that shifts the bit\n   assignment.  The documentation of core.whitespace has also be\n   updated in this step.\n\n - \"git apply --check\" miscounted line number reported for the\n   incomplete line error, which has been corrected in step [10/12].\n\n - t4124-apply-ws-rule.sh has been extended to cover \"git apply\n   --check\" and the diagnostic output from it in step [10/12].\n\nJunio C Hamano (12):\n  whitespace: correct bit assignment comments\n  diff: emit_line_ws_markup() if/else style fix\n  diff: correct suppress_blank_empty hack\n  diff: fix incorrect counting of line numbers\n  diff: refactor output of incomplete line\n  diff: call emit_callback ecbdata everywhere\n  diff: update the way rewrite diff handles incomplete lines\n  apply: revamp the parsing of incomplete lines\n  whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE\n  apply: check and fix incomplete lines\n  diff: highlight and error out on incomplete lines\n  attr: enable incomplete-line whitespace error for this project\n\n .gitattributes                 |   4 +-\n Documentation/config/core.adoc |   2 +\n apply.c                        |  79 ++++++++++----\n diff.c                         | 148 +++++++++++++++++---------\n diff.h                         |   6 +-\n t/t4015-diff-whitespace.sh     |  63 ++++++++++-\n t/t4124-apply-ws-rule.sh       | 187 +++++++++++++++++++++++++++++++++\n ws.c                           |  20 ++++\n ws.h                           |  26 +++--\n 9 files changed, 448 insertions(+), 87 deletions(-)\n\nRange-diff against v1:\n 1:  4168f28fe7 =  1:  8a493cdea5 whitespace: correct bit assignment comments\n 2:  53b7a010e7 =  2:  a01d99a055 diff: emit_line_ws_markup() if/else style fix\n 3:  d93dd05543 =  3:  e3ea40af19 diff: correct suppress_blank_empty hack\n 4:  5f58400bd7 =  4:  e15e89d3e2 diff: fix incorrect counting of line numbers\n 5:  84c4ca147f =  5:  c007b3d7a7 diff: refactor output of incomplete line\n 6:  55b42a1944 =  6:  0cea57091b diff: call emit_callback ecbdata everywhere\n 7:  6947838d13 =  7:  523196b440 diff: update the way rewrite diff handles incomplete lines\n 8:  63c36c6f70 =  8:  e098932784 apply: revamp the parsing of incomplete lines\n 9:  00b645bb4e !  9:  28538f149f whitespace: allocate a few more bits\n    @@ Metadata\n     Author: Junio C Hamano <gitster@pobox.com>\n     \n      ## Commit message ##\n    -    whitespace: allocate a few more bits\n    +    whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE\n     \n         Reserve a few more bits in the diff flags word to be used for future\n    -    whitespace rules.  No behaviour changes intended.\n    +    whitespace rules.  Add WS_INCOMPLETE_LINE without implementing the\n    +    behaviour (yet).\n     \n         Signed-off-by: Junio C Hamano <gitster@pobox.com>\n     \n    + ## Documentation/config/core.adoc ##\n    +@@ Documentation/config/core.adoc: core.whitespace::\n    +   part of the line terminator, i.e. with it, `trailing-space`\n    +   does not trigger if the character before such a carriage-return\n    +   is not a whitespace (not enabled by default).\n    ++* `incomplete-line` treats the last line of a file that is missing the\n    ++  newline at the end as an error (not enabled by default).\n    + * `tabwidth=<n>` tells how many character positions a tab occupies; this\n    +   is relevant for `indent-with-non-tab` and when Git fixes `tab-in-indent`\n    +   errors. The default tab width is 8. Allowed values are 1 to 63.\n    +\n      ## diff.c ##\n     @@ diff.c: enum diff_symbol {\n      \n    @@ diff.h: struct diff_options {\n      \tconst char *prefix;\n      \tint prefix_length;\n     \n    + ## ws.c ##\n    +@@ ws.c: static struct whitespace_rule {\n    + \t{ \"blank-at-eol\", WS_BLANK_AT_EOL, 0 },\n    + \t{ \"blank-at-eof\", WS_BLANK_AT_EOF, 0 },\n    + \t{ \"tab-in-indent\", WS_TAB_IN_INDENT, 0, 1 },\n    ++\t{ \"incomplete-line\", WS_INCOMPLETE_LINE, 0, 0 },\n    + };\n    + \n    + unsigned parse_whitespace_rule(const char *string)\n    +@@ ws.c: char *whitespace_error_string(unsigned ws)\n    + \t\t\tstrbuf_addstr(&err, \", \");\n    + \t\tstrbuf_addstr(&err, \"tab in indent\");\n    + \t}\n    ++\tif (ws & WS_INCOMPLETE_LINE) {\n    ++\t\tif (err.len)\n    ++\t\t\tstrbuf_addstr(&err, \", \");\n    ++\t\tstrbuf_addstr(&err, \"no newline at the end of file\");\n    ++\t}\n    + \treturn strbuf_detach(&err, NULL);\n    + }\n    + \n    +\n      ## ws.h ##\n     @@ ws.h: struct strbuf;\n    + #define WS_CR_AT_EOL            (1<<9)\n    + #define WS_BLANK_AT_EOF         (1<<10)\n    + #define WS_TAB_IN_INDENT        (1<<11)\n    ++#define WS_INCOMPLETE_LINE      (1<<12)\n    + \n    + #define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)\n    + #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)\n      #define WS_TAB_WIDTH_MASK       ((1<<6)-1)\n      \n      /* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */\n10:  662f15d0b4 ! 10:  7369e77309 apply: check and fix incomplete lines\n    @@ Commit message\n         what happens when A ends in an incomplete line, for example), and\n         text-oriented tools often mishandle such a line.\n     \n    -    Introduce a new whitespace rule \"incomplete-line\", which is off by\n    -    default for backward compatibility's sake, so that \"git apply\n    +    Implement checks in \"git apply\" for incomplete lines, which is off\n    +    by default for backward compatibility's sake, so that \"git apply\n         --whitespace={fix,warn,error}\" can notice, warn against, and fix\n         them.\n     \n    @@ Commit message\n     \n         Signed-off-by: Junio C Hamano <gitster@pobox.com>\n     \n    + ## apply.c ##\n    +@@ apply.c: static void record_ws_error(struct apply_state *state,\n    + \t    state->squelch_whitespace_errors < state->whitespace_error)\n    + \t\treturn;\n    + \n    ++\t/*\n    ++\t * line[len] for an incomplete line points at the \"\\n\" at the end\n    ++\t * of patch input line, so \"%.*s\" would drop the last letter on line;\n    ++\t * compensate for it.\n    ++\t */\n    ++\tif (result & WS_INCOMPLETE_LINE)\n    ++\t\tlen++;\n    ++\n    + \terr = whitespace_error_string(result);\n    + \tif (state->apply_verbosity > verbosity_silent)\n    + \t\tfprintf(stderr, \"%s:%d: %s.\\n%.*s\\n\",\n    +@@ apply.c: static int parse_fragment(struct apply_state *state,\n    + \t\t}\n    + \n    + \t\t/* eat the \"\\\\ No newline...\" as well, if exists */\n    +-\t\tlen += skip_len;\n    ++\t\tif (skip_len) {\n    ++\t\t\tlen += skip_len;\n    ++\t\t\tstate->linenr++;\n    ++\t\t}\n    + \t}\n    + \tif (oldlines || newlines)\n    + \t\treturn -1;\n    +\n      ## t/t4124-apply-ws-rule.sh ##\n     @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excluded paths' '\n      \tgit apply --include=used --stat --whitespace=error <patch\n    @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl\n     +\tgit apply --whitespace=error <patch &&\n     +\ttest_cmp sample2-i target &&\n     +\n    ++\tcat sample-i >target &&\n    ++\tgit apply --whitespace=error --check <patch 2>error &&\n    ++\ttest_cmp sample-i target &&\n    ++\ttest_must_be_empty error &&\n    ++\n     +\tcat sample2-i >target &&\n     +\tgit apply --whitespace=error -R <patch &&\n    -+\ttest_cmp sample-i target\n    ++\ttest_cmp sample-i target &&\n    ++\n    ++\tcat sample2-i >target &&\n    ++\tgit apply -R --whitespace=error --check <patch 2>error &&\n    ++\ttest_cmp sample2-i target &&\n    ++\ttest_must_be_empty error\n     +'\n     +\n     +test_expect_success 'last line made incomplete (error)' '\n    @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl\n     +\ttest_must_fail git apply --whitespace=error <patch 2>error &&\n     +\ttest_grep \"no newline\" error &&\n     +\n    ++\tcat sample >target &&\n    ++\ttest_must_fail git apply --whitespace=error --check <patch 2>actual &&\n    ++\ttest_cmp sample target &&\n    ++\tcat >expect <<-\\EOF &&\n    ++\t<stdin>:10: no newline at the end of file.\n    ++\t6\n    ++\terror: 1 line adds whitespace errors.\n    ++\tEOF\n    ++\ttest_cmp expect actual &&\n    ++\n     +\tcat sample-i >target &&\n     +\tgit apply --whitespace=error -R <patch &&\n     +\ttest_cmp sample target &&\n     +\n    ++\tcat sample-i >target &&\n    ++\tgit apply --whitespace=error --check -R <patch 2>error &&\n    ++\ttest_cmp sample-i target &&\n    ++\ttest_must_be_empty error &&\n    ++\n     +\tcat sample >target &&\n     +\tgit apply --whitespace=fix <patch &&\n     +\ttest_cmp sample target\n    @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl\n     +\tgit apply --whitespace=error <patch &&\n     +\ttest_cmp sample target &&\n     +\n    ++\tcat sample-i >target &&\n    ++\tgit apply --whitespace=error --check <patch 2>error &&\n    ++\ttest_cmp sample-i target &&\n    ++\ttest_must_be_empty error &&\n    ++\n     +\tcat sample >target &&\n     +\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n     +\ttest_grep \"no newline\" error &&\n     +\n     +\tcat sample >target &&\n    ++\ttest_must_fail git apply --whitespace=error --check -R <patch 2>actual &&\n    ++\ttest_cmp sample target &&\n    ++\tcat >expect <<-\\EOF &&\n    ++\t<stdin>:9: no newline at the end of file.\n    ++\t6\n    ++\terror: 1 line adds whitespace errors.\n    ++\tEOF\n    ++\ttest_cmp expect actual &&\n    ++\n    ++\tcat sample >target &&\n     +\tgit apply --whitespace=fix -R <patch &&\n     +\ttest_cmp sample target\n     +'\n    @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl\n     +\tgit apply --whitespace=error <patch &&\n     +\ttest_cmp sample3 target &&\n     +\n    ++\tcat sample-i >target &&\n    ++\tgit apply --whitespace=error --check <patch 2>error &&\n    ++\ttest_cmp sample-i target &&\n    ++\ttest_must_be_empty error &&\n    ++\n     +\tcat sample3 >target &&\n     +\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n     +\ttest_grep \"no newline\" error &&\n     +\n     +\tcat sample3 >target &&\n    ++\ttest_must_fail git apply --whitespace=error -R --check <patch 2>actual &&\n    ++\ttest_cmp sample3 target &&\n    ++\tcat >expect <<-\\EOF &&\n    ++\t<stdin>:9: no newline at the end of file.\n    ++\t6\n    ++\terror: 1 line adds whitespace errors.\n    ++\tEOF\n    ++\ttest_cmp expect actual &&\n    ++\n    ++\tcat sample3 >target &&\n     +\tgit apply --whitespace=fix -R <patch &&\n     +\ttest_cmp sample target\n     +'\n    @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl\n     +\ttest_must_fail git apply --whitespace=error <patch 2>error &&\n     +\ttest_grep \"no newline\" error &&\n     +\n    ++\tcat sample-i >target &&\n    ++\ttest_must_fail git apply --whitespace=error --check <patch 2>actual &&\n    ++\ttest_cmp sample-i target &&\n    ++\tcat >expect <<-\\EOF &&\n    ++\t<stdin>:11: no newline at the end of file.\n    ++\t7\n    ++\terror: 1 line adds whitespace errors.\n    ++\tEOF\n    ++\ttest_cmp expect actual &&\n    ++\n     +\tcat sample3-i >target &&\n     +\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n     +\ttest_grep \"no newline\" error &&\n     +\n    ++\tcat sample3-i >target &&\n    ++\ttest_must_fail git apply --whitespace=error --check -R <patch 2>actual &&\n    ++\ttest_cmp sample3-i target &&\n    ++\tcat >expect <<-\\EOF &&\n    ++\t<stdin>:9: no newline at the end of file.\n    ++\t6\n    ++\terror: 1 line adds whitespace errors.\n    ++\tEOF\n    ++\ttest_cmp expect actual &&\n    ++\n     +\tcat sample-i >target &&\n     +\tgit apply --whitespace=fix <patch &&\n     +\ttest_cmp sample3 target &&\n    @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl\n      test_done\n     \n      ## ws.c ##\n    -@@ ws.c: static struct whitespace_rule {\n    - \t{ \"blank-at-eol\", WS_BLANK_AT_EOL, 0 },\n    - \t{ \"blank-at-eof\", WS_BLANK_AT_EOF, 0 },\n    - \t{ \"tab-in-indent\", WS_TAB_IN_INDENT, 0, 1 },\n    -+\t{ \"incomplete-line\", WS_INCOMPLETE_LINE, 0, 0 },\n    - };\n    - \n    - unsigned parse_whitespace_rule(const char *string)\n    -@@ ws.c: char *whitespace_error_string(unsigned ws)\n    - \t\t\tstrbuf_addstr(&err, \", \");\n    - \t\tstrbuf_addstr(&err, \"tab in indent\");\n    - \t}\n    -+\tif (ws & WS_INCOMPLETE_LINE) {\n    -+\t\tif (err.len)\n    -+\t\t\tstrbuf_addstr(&err, \", \");\n    -+\t\tstrbuf_addstr(&err, \"no newline at the end of file\");\n    -+\t}\n    - \treturn strbuf_detach(&err, NULL);\n    - }\n    - \n     @@ ws.c: static unsigned ws_check_emit_1(const char *line, int len, unsigned ws_rule,\n      \tif (trailing_whitespace == -1)\n      \t\ttrailing_whitespace = len;\n    @@ ws.c: void ws_fix_copy(struct strbuf *dst, const char *src, int len, unsigned ws\n      \t/*\n      \t * Strip trailing whitespace\n      \t */\n    -\n    - ## ws.h ##\n    -@@ ws.h: struct strbuf;\n    - #define WS_CR_AT_EOL            (1<<9)\n    - #define WS_BLANK_AT_EOF         (1<<10)\n    - #define WS_TAB_IN_INDENT        (1<<11)\n    -+#define WS_INCOMPLETE_LINE      (1<<12)\n    - \n    - #define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)\n    - #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)\n11:  36de2ac901 = 11:  17c2fa50a7 diff: highlight and error out on incomplete lines\n12:  e82056bf55 = 12:  73af29fba7 attr: enable incomplete-line whitespace error for this project\n-- \n2.52.0-rc0-105-gc08128fbb6\n\n"},{"id":"530270","messageId":"20251105213052.1499224-2-gitster@pobox.com","threadId":"64429","inReplyTo":"20251105213052.1499224-1-gitster@pobox.com","subject":"[PATCH v2 01/12] whitespace: correct bit assignment comments","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-05T21:30:41Z","receivedAt":"2025-11-05T21:30:56Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"A comment in diff.c claimed that bits up to 12th (counting from 0th)\nare whitespace rules, and 13th thru 15th are for new/old/context,\nbut it turns out it was miscounting.  Correct them, and clarify\nwhere the whitespace rule bits come from in the comment.  Extend bit\nassignment comments to cover bits used for color-moved, which\nweren't described.\n\nAlso update the way these bit constants are defined to use (1 << N)\nnotation, instead of octal constants, as it tends to make it easier\nto notice a breakage like this.\n\nSprinkle a few blank lines between logically distinct groups of CPP\nmacro definitions to make them easier to read.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c |  7 +++++--\n diff.h |  6 +++---\n ws.h   | 25 ++++++++++++++-----------\n 3 files changed, 22 insertions(+), 16 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex a74e701806..74261b332a 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -801,16 +801,19 @@ enum diff_symbol {\n \tDIFF_SYMBOL_CONTEXT_MARKER,\n \tDIFF_SYMBOL_SEPARATOR\n };\n+\n /*\n  * Flags for content lines:\n- * 0..12 are whitespace rules\n- * 13-15 are WSEH_NEW | WSEH_OLD | WSEH_CONTEXT\n+ * 0..11 are whitespace rules (see ws.h)\n+ * 12..14 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD\n  * 16 is marking if the line is blank at EOF\n+ * 17..19 are used for color-moved.\n  */\n #define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF\t(1<<16)\n #define DIFF_SYMBOL_MOVED_LINE\t\t\t(1<<17)\n #define DIFF_SYMBOL_MOVED_LINE_ALT\t\t(1<<18)\n #define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING\t(1<<19)\n+\n #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)\n \n /*\ndiff --git a/diff.h b/diff.h\nindex 2fa256c3ef..cbd355cf50 100644\n--- a/diff.h\n+++ b/diff.h\n@@ -331,9 +331,9 @@ struct diff_options {\n \n \tint ita_invisible_in_index;\n /* white-space error highlighting */\n-#define WSEH_NEW (1<<12)\n-#define WSEH_CONTEXT (1<<13)\n-#define WSEH_OLD (1<<14)\n+#define WSEH_NEW        (1<<12)\n+#define WSEH_CONTEXT    (1<<13)\n+#define WSEH_OLD        (1<<14)\n \tunsigned ws_error_highlight;\n \tconst char *prefix;\n \tint prefix_length;\ndiff --git a/ws.h b/ws.h\nindex 5ba676c559..23708efb73 100644\n--- a/ws.h\n+++ b/ws.h\n@@ -7,19 +7,22 @@ struct strbuf;\n /*\n  * whitespace rules.\n  * used by both diff and apply\n- * last two digits are tab width\n+ * last two octal-digits are tab width (we support only up to 63).\n  */\n-#define WS_BLANK_AT_EOL         0100\n-#define WS_SPACE_BEFORE_TAB     0200\n-#define WS_INDENT_WITH_NON_TAB  0400\n-#define WS_CR_AT_EOL           01000\n-#define WS_BLANK_AT_EOF        02000\n-#define WS_TAB_IN_INDENT       04000\n-#define WS_TRAILING_SPACE      (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)\n+#define WS_BLANK_AT_EOL         (1<<6)\n+#define WS_SPACE_BEFORE_TAB     (1<<7)\n+#define WS_INDENT_WITH_NON_TAB  (1<<8)\n+#define WS_CR_AT_EOL            (1<<9)\n+#define WS_BLANK_AT_EOF         (1<<10)\n+#define WS_TAB_IN_INDENT        (1<<11)\n+\n+#define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)\n #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)\n-#define WS_TAB_WIDTH_MASK        077\n-/* All WS_* -- when extended, adapt diff.c emit_symbol */\n-#define WS_RULE_MASK           07777\n+#define WS_TAB_WIDTH_MASK       ((1<<6)-1)\n+\n+/* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */\n+#define WS_RULE_MASK            ((1<<12)-1)\n+\n extern unsigned whitespace_rule_cfg;\n unsigned whitespace_rule(struct index_state *, const char *);\n unsigned parse_whitespace_rule(const char *);\n-- \n2.52.0-rc0-105-gc08128fbb6\n\n"},{"id":"530271","messageId":"20251105213052.1499224-3-gitster@pobox.com","threadId":"64429","inReplyTo":"20251105213052.1499224-1-gitster@pobox.com","subject":"[PATCH v2 02/12] diff: emit_line_ws_markup() if/else style fix","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-05T21:30:42Z","receivedAt":"2025-11-05T21:30:58Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Apply the simple rule: if you need {} in one arm of the if/else\nif/else... cascade, have {} in all of them.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 74261b332a..9a24a0791c 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1327,14 +1327,14 @@ static void emit_line_ws_markup(struct diff_options *o,\n \t\t\tws = NULL;\n \t}\n \n-\tif (!ws && !set_sign)\n+\tif (!ws && !set_sign) {\n \t\temit_line_0(o, set, NULL, 0, reset, sign, line, len);\n-\telse if (!ws) {\n+\t} else if (!ws) {\n \t\temit_line_0(o, set_sign, set, !!set_sign, reset, sign, line, len);\n-\t} else if (blank_at_eof)\n+\t} else if (blank_at_eof) {\n \t\t/* Blank line at EOF - paint '+' as well */\n \t\temit_line_0(o, ws, NULL, 0, reset, sign, line, len);\n-\telse {\n+\t} else {\n \t\t/* Emit just the prefix, then the rest. */\n \t\temit_line_0(o, set_sign ? set_sign : set, NULL, !!set_sign, reset,\n \t\t\t    sign, \"\", 0);\n-- \n2.52.0-rc0-105-gc08128fbb6\n\n"},{"id":"530272","messageId":"20251105213052.1499224-4-gitster@pobox.com","threadId":"64429","inReplyTo":"20251105213052.1499224-1-gitster@pobox.com","subject":"[PATCH v2 03/12] diff: correct suppress_blank_empty hack","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-05T21:30:43Z","receivedAt":"2025-11-05T21:31:00Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The suppress-blank-empty feature abused the CONTEXT_INCOMPLETE\nsymbol that was meant to be used only for \"\\ No newline at the end\nof file\" code path.\n\nThe intent of the feature was to turn a context line we receive from\nxdiff machinery (which always uses ' ' for context lines, even an\nempty one) and spit it out as a truly empty line.\n\nPerform such a conversion very locally at where a line from xdiff\nthat begins with ' ' is handled for output; there are many checks\nbefore the control reaches such place that checks the first letter\nof the diff output line to see if it is a context line, and having\nto check for '\\n' and treat it as a special case is error prone.\n\nIn order to catch similar hacks in the future, make sure the code\npath that is meant for \"\\ No newline\" case checks the first byte is\nindeed a backslash.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 27 +++++++++++----------------\n 1 file changed, 11 insertions(+), 16 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 9a24a0791c..b9ef8550cc 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1321,6 +1321,11 @@ static void emit_line_ws_markup(struct diff_options *o,\n \tconst char *ws = NULL;\n \tint sign = o->output_indicators[sign_index];\n \n+\tif (diff_suppress_blank_empty &&\n+\t    sign_index == OUTPUT_INDICATOR_CONTEXT &&\n+\t    len == 1 && line[0] == '\\n')\n+\t\tsign = 0;\n+\n \tif (o->ws_error_highlight & ws_rule) {\n \t\tws = diff_get_color_opt(o, DIFF_WHITESPACE);\n \t\tif (!*ws)\n@@ -1498,15 +1503,9 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n \tcase DIFF_SYMBOL_WORDS:\n \t\tcontext = diff_get_color_opt(o, DIFF_CONTEXT);\n \t\treset = diff_get_color_opt(o, DIFF_RESET);\n-\t\t/*\n-\t\t * Skip the prefix character, if any.  With\n-\t\t * diff_suppress_blank_empty, there may be\n-\t\t * none.\n-\t\t */\n-\t\tif (line[0] != '\\n') {\n-\t\t\tline++;\n-\t\t\tlen--;\n-\t\t}\n+\n+\t\t/* Skip the prefix character */\n+\t\tline++; len--;\n \t\temit_line(o, context, reset, line, len);\n \t\tbreak;\n \tcase DIFF_SYMBOL_FILEPAIR_PLUS:\n@@ -2375,12 +2374,6 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \t\tecbdata->label_path[0] = ecbdata->label_path[1] = NULL;\n \t}\n \n-\tif (diff_suppress_blank_empty\n-\t    && len == 2 && line[0] == ' ' && line[1] == '\\n') {\n-\t\tline[0] = '\\n';\n-\t\tlen = 1;\n-\t}\n-\n \tif (line[0] == '@') {\n \t\tif (ecbdata->diff_words)\n \t\t\tdiff_words_flush(ecbdata);\n@@ -2431,12 +2424,14 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \t\tecbdata->lno_in_preimage++;\n \t\temit_context_line(ecbdata, line + 1, len - 1);\n \t\tbreak;\n-\tdefault:\n+\tcase '\\\\':\n \t\t/* incomplete line at the end */\n \t\tecbdata->lno_in_preimage++;\n \t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n \t\t\t\t line, len, 0);\n \t\tbreak;\n+\tdefault:\n+\t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n \t}\n \treturn 0;\n }\n-- \n2.52.0-rc0-105-gc08128fbb6\n\n"},{"id":"530273","messageId":"20251105213052.1499224-5-gitster@pobox.com","threadId":"64429","inReplyTo":"20251105213052.1499224-1-gitster@pobox.com","subject":"[PATCH v2 04/12] diff: fix incorrect counting of line numbers","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-05T21:30:44Z","receivedAt":"2025-11-05T21:31:02Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The \"\\ No newline at the end of the file\" can come after any of the\n\"-\" (deleted preimage line), \" \" (unchanged line), or \"+\" (added\npostimage line).  Incrementing only the preimage line number upon\nseeing it does not make any sense.\n\nWe can keep track of what the previous line was, and increment\nlno_in_{pre,post}image variables properly, like this patch does.  I\ndo not think it matters, as these numbers are used only to compare\nthem with blank_at_eof_in_{pre,post}image to issue the warning every\ntime we see an added line, but by definition, after we see \"\\ No\nnewline at the end of the file\" for an added line, we will not see\nan added line for the file.\n\nKeeping track of what the last line was (in other words, \"is it that\nthe file used to end in an incomplete line?  The file ends in an\nincomplete line after the change?  Both the file before and after\nthe change ends in an incomplete line that did not change?\") will be\nindependently useful.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 18 +++++++++++++++++-\n 1 file changed, 17 insertions(+), 1 deletion(-)\n\ndiff --git a/diff.c b/diff.c\nindex b9ef8550cc..e73320dfb1 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -601,6 +601,7 @@ struct emit_callback {\n \tint blank_at_eof_in_postimage;\n \tint lno_in_preimage;\n \tint lno_in_postimage;\n+\tint last_line_kind;\n \tconst char **label_path;\n \tstruct diff_words_data *diff_words;\n \tstruct diff_options *opt;\n@@ -2426,13 +2427,28 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \t\tbreak;\n \tcase '\\\\':\n \t\t/* incomplete line at the end */\n-\t\tecbdata->lno_in_preimage++;\n+\t\tswitch (ecbdata->last_line_kind) {\n+\t\tcase '+':\n+\t\t\tecbdata->lno_in_postimage++;\n+\t\t\tbreak;\n+\t\tcase '-':\n+\t\t\tecbdata->lno_in_preimage++;\n+\t\t\tbreak;\n+\t\tcase ' ':\n+\t\t\tecbdata->lno_in_preimage++;\n+\t\t\tecbdata->lno_in_postimage++;\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tBUG(\"fn_out_consume: '\\\\No newline' after unknown line (%c)\",\n+\t\t\t    ecbdata->last_line_kind);\n+\t\t}\n \t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n \t\t\t\t line, len, 0);\n \t\tbreak;\n \tdefault:\n \t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n \t}\n+\tecbdata->last_line_kind = line[0];\n \treturn 0;\n }\n \n-- \n2.52.0-rc0-105-gc08128fbb6\n\n"},{"id":"530274","messageId":"20251105213052.1499224-6-gitster@pobox.com","threadId":"64429","inReplyTo":"20251105213052.1499224-1-gitster@pobox.com","subject":"[PATCH v2 05/12] diff: refactor output of incomplete line","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-05T21:30:45Z","receivedAt":"2025-11-05T21:31:03Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Create a helper function that reacts to \"\\ No newline at the end of\nfile\" in preparation for unifying the incomplete line handling in\nthe code path that handles xdiff output and the code path that\nbypasses xdiff and produces complete rewrite patch.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 14 ++++++++++++--\n 1 file changed, 12 insertions(+), 2 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex e73320dfb1..d388d318e4 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1379,6 +1379,10 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n \t\temit_line(o, \"\", \"\", line, len);\n \t\tbreak;\n \tcase DIFF_SYMBOL_CONTEXT_INCOMPLETE:\n+\t\tset = diff_get_color_opt(o, DIFF_CONTEXT);\n+\t\treset = diff_get_color_opt(o, DIFF_RESET);\n+\t\temit_line(o, set, reset, line, len);\n+\t\tbreak;\n \tcase DIFF_SYMBOL_CONTEXT_MARKER:\n \t\tcontext = diff_get_color_opt(o, DIFF_CONTEXT);\n \t\treset = diff_get_color_opt(o, DIFF_RESET);\n@@ -1668,6 +1672,13 @@ static void emit_context_line(struct emit_callback *ecbdata,\n \temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT, line, len, flags);\n }\n \n+static void emit_incomplete_line(struct emit_callback *ecbdata,\n+\t\t\t\t const char *line, int len)\n+{\n+\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n+\t\t\t line, len, 0);\n+}\n+\n static void emit_hunk_header(struct emit_callback *ecbdata,\n \t\t\t     const char *line, int len)\n {\n@@ -2442,8 +2453,7 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \t\t\tBUG(\"fn_out_consume: '\\\\No newline' after unknown line (%c)\",\n \t\t\t    ecbdata->last_line_kind);\n \t\t}\n-\t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n-\t\t\t\t line, len, 0);\n+\t\temit_incomplete_line(ecbdata, line, len);\n \t\tbreak;\n \tdefault:\n \t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n-- \n2.52.0-rc0-105-gc08128fbb6\n\n"},{"id":"530275","messageId":"20251105213052.1499224-7-gitster@pobox.com","threadId":"64429","inReplyTo":"20251105213052.1499224-1-gitster@pobox.com","subject":"[PATCH v2 06/12] diff: call emit_callback ecbdata everywhere","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-05T21:30:46Z","receivedAt":"2025-11-05T21:31:05Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Everybody else, except for emit_rewrite_lines(), calls the\nemit_callback data ecbdata.  Make sure we call the same thing by\nthe same name for consistency.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 12 ++++++------\n 1 file changed, 6 insertions(+), 6 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex d388d318e4..347cd9c6e9 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1780,7 +1780,7 @@ static void add_line_count(struct strbuf *out, int count)\n \t}\n }\n \n-static void emit_rewrite_lines(struct emit_callback *ecb,\n+static void emit_rewrite_lines(struct emit_callback *ecbdata,\n \t\t\t       int prefix, const char *data, int size)\n {\n \tconst char *endp = NULL;\n@@ -1791,17 +1791,17 @@ static void emit_rewrite_lines(struct emit_callback *ecb,\n \t\tendp = memchr(data, '\\n', size);\n \t\tlen = endp ? (endp - data + 1) : size;\n \t\tif (prefix != '+') {\n-\t\t\tecb->lno_in_preimage++;\n-\t\t\temit_del_line(ecb, data, len);\n+\t\t\tecbdata->lno_in_preimage++;\n+\t\t\temit_del_line(ecbdata, data, len);\n \t\t} else {\n-\t\t\tecb->lno_in_postimage++;\n-\t\t\temit_add_line(ecb, data, len);\n+\t\t\tecbdata->lno_in_postimage++;\n+\t\t\temit_add_line(ecbdata, data, len);\n \t\t}\n \t\tsize -= len;\n \t\tdata += len;\n \t}\n \tif (!endp)\n-\t\temit_diff_symbol(ecb->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);\n+\t\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);\n }\n \n static void emit_rewrite_diff(const char *name_a,\n-- \n2.52.0-rc0-105-gc08128fbb6\n\n"},{"id":"530276","messageId":"20251105213052.1499224-8-gitster@pobox.com","threadId":"64429","inReplyTo":"20251105213052.1499224-1-gitster@pobox.com","subject":"[PATCH v2 07/12] diff: update the way rewrite diff handles incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-05T21:30:47Z","receivedAt":"2025-11-05T21:31:07Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The diff_symbol based output framework uses one DIFF_SYMBOL_* enum\nvalue per the kind of output lines of \"git diff\", which corresponds\nto one output line from the xdiff machinery used internally.  Most\nnotably, DIFF_SYMBOL_PLUS and DIFF_SYMBOL_MINUS that correspond to\n\"+\" and \"-\" lines are designed to always take a complete line, even\nif the output from xdiff machinery may produce \"\\ No newline at the\nend of file\" immediately after them.\n\nBut this is not true in the rewrite-diff codepath, which completely\nbypasses the xdiff machinery.  Since the code path feeds the bytes\ndirectly from the payload to the output routines, the output layer\nhas to deal with an incomplete line with DIFF_SYMBOL_PLUS and\nDIFF_SYMBOL_MINUS, which never would see an incomplete line in the\nnormal code paths.  This lack of final newline is compensated by an\nugly hack for a fabricated DIFF_SYMBOL_NO_LF_EOF token to inject an\nextra newline to the output to simulate output coming from the xdiff\nmachinery.\n\nRevamp the way the complete-rewrite code path feeds the lines to the\noutput layer by treating the last line of the pre/post image when it\nis an incomplete line specially.\n\nThis lets us remove the DIFF_SYMBOL_NO_LF_EOF hack and use the usual\nDIFF_SYMBOL_CONTEXT_INCOMPLETE code path, which will later learn how\nto handle whitespace errors.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 33 +++++++++++++++++++--------------\n 1 file changed, 19 insertions(+), 14 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 347cd9c6e9..99298720f4 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -797,7 +797,6 @@ enum diff_symbol {\n \tDIFF_SYMBOL_CONTEXT_INCOMPLETE,\n \tDIFF_SYMBOL_PLUS,\n \tDIFF_SYMBOL_MINUS,\n-\tDIFF_SYMBOL_NO_LF_EOF,\n \tDIFF_SYMBOL_CONTEXT_FRAGINFO,\n \tDIFF_SYMBOL_CONTEXT_MARKER,\n \tDIFF_SYMBOL_SEPARATOR\n@@ -1352,7 +1351,6 @@ static void emit_line_ws_markup(struct diff_options *o,\n static void emit_diff_symbol_from_struct(struct diff_options *o,\n \t\t\t\t\t struct emitted_diff_symbol *eds)\n {\n-\tstatic const char *nneof = \" No newline at end of file\\n\";\n \tconst char *context, *reset, *set, *set_sign, *meta, *fraginfo;\n \n \tenum diff_symbol s = eds->s;\n@@ -1361,13 +1359,6 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n \tunsigned flags = eds->flags;\n \n \tswitch (s) {\n-\tcase DIFF_SYMBOL_NO_LF_EOF:\n-\t\tcontext = diff_get_color_opt(o, DIFF_CONTEXT);\n-\t\treset = diff_get_color_opt(o, DIFF_RESET);\n-\t\tputc('\\n', o->file);\n-\t\temit_line_0(o, context, NULL, 0, reset, '\\\\',\n-\t\t\t    nneof, strlen(nneof));\n-\t\tbreak;\n \tcase DIFF_SYMBOL_SUBMODULE_HEADER:\n \tcase DIFF_SYMBOL_SUBMODULE_ERROR:\n \tcase DIFF_SYMBOL_SUBMODULE_PIPETHROUGH:\n@@ -1786,22 +1777,36 @@ static void emit_rewrite_lines(struct emit_callback *ecbdata,\n \tconst char *endp = NULL;\n \n \twhile (0 < size) {\n-\t\tint len;\n+\t\tint len, plen;\n+\t\tchar *pdata = NULL;\n \n \t\tendp = memchr(data, '\\n', size);\n \t\tlen = endp ? (endp - data + 1) : size;\n+\t\tplen = len;\n+\n+\t\tif (!endp) {\n+\t\t\tplen = len + 1;\n+\t\t\tpdata = xmalloc(plen + 2);\n+\t\t\tmemcpy(pdata, data, len);\n+\t\t\tpdata[len] = '\\n';\n+\t\t\tpdata[len + 1] = '\\0';\n+\t\t}\n \t\tif (prefix != '+') {\n \t\t\tecbdata->lno_in_preimage++;\n-\t\t\temit_del_line(ecbdata, data, len);\n+\t\t\temit_del_line(ecbdata, pdata ? pdata : data, plen);\n \t\t} else {\n \t\t\tecbdata->lno_in_postimage++;\n-\t\t\temit_add_line(ecbdata, data, len);\n+\t\t\temit_add_line(ecbdata, pdata ? pdata : data, plen);\n \t\t}\n+\t\tfree(pdata);\n \t\tsize -= len;\n \t\tdata += len;\n \t}\n-\tif (!endp)\n-\t\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);\n+\tif (!endp) {\n+\t\tstatic const char nneof[] = \"\\\\ No newline at end of file\\n\";\n+\t\tecbdata->last_line_kind = prefix;\n+\t\temit_incomplete_line(ecbdata, nneof, sizeof(nneof) - 1);\n+\t}\n }\n \n static void emit_rewrite_diff(const char *name_a,\n-- \n2.52.0-rc0-105-gc08128fbb6\n\n"},{"id":"530277","messageId":"20251105213052.1499224-9-gitster@pobox.com","threadId":"64429","inReplyTo":"20251105213052.1499224-1-gitster@pobox.com","subject":"[PATCH v2 08/12] apply: revamp the parsing of incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-05T21:30:48Z","receivedAt":"2025-11-05T21:31:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"A patch file represents the incomplete line at the end of the file\nwith two lines, one that is the usual \"context\" with \" \" as the\nfirst letter, \"added\" with \"+\" as the first letter, or \"removed\"\nwith \"-\" as the first letter that shows the content of the line,\nplus an extra \"\\ No newline at the end of file\" line that comes\nimmediately after it.\n\nEver since the apply machinery was written, the \"git apply\"\nmachinery parses \"\\ No newline at the end of file\" line\nindependently, without even knowing what line the incomplete-ness\napplies to, simply because it does not even remember what the\nprevious line was.\n\nThis poses a problem if we want to check and warn on an incomplete\nline.  Revamp the code that parses a fragment, to actually drop the\n'\\n' at the end of the incoming patch file that terminates a line,\nso that check_whitespace() calls made from the code path actually\nsees an incomplete as incomplete.\n\nNote that the result of this parsing is not directly used by the\ncode path that applies the patch.  apply_one_fragment() function\nalready checks if each of the patch text it handles is followed by a\nline that begins with a backslash to drop the newline at the end of\nthe current line it is looking at.  In a sense, this patch harmonizes\nthe behaviour of the parsing side to what is already done in the\napplication side.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n apply.c | 70 ++++++++++++++++++++++++++++++++++++++++-----------------\n 1 file changed, 49 insertions(+), 21 deletions(-)\n\ndiff --git a/apply.c b/apply.c\nindex a2ceb3fb40..2b0f8bdab5 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -1670,6 +1670,35 @@ static void check_old_for_crlf(struct patch *patch, const char *line, int len)\n }\n \n \n+/*\n+ * Just saw a single line in a fragment.  If it is a part of this hunk\n+ * that is a context \" \", an added \"+\", or a removed \"-\" line, it may\n+ * be followed by \"\\\\ No newline...\" to signal that the last \"\\n\" on\n+ * this line needs to be dropped.  Depending on locale settings when\n+ * the patch was produced we don't know what this line would exactly\n+ * say. The only thing we do know is that it begins with \"\\ \".\n+ * Checking for 12 is just for sanity check; \"\\ No newline...\" would\n+ * be at least that long in any l10n.\n+ *\n+ * Return 0 if the line we saw is not followed by \"\\ No newline...\",\n+ * or length of that line.  The caller will use it to skip over the\n+ * \"\\ No newline...\" line.\n+ */\n+static int adjust_incomplete(const char *line, int len,\n+\t\t\t     unsigned long size)\n+{\n+\tint nextlen;\n+\n+\tif (*line != '\\n' && *line != ' ' && *line != '+' && *line != '-')\n+\t\treturn 0;\n+\tif (size - len < 12 || memcmp(line + len, \"\\\\ \", 2))\n+\t\treturn 0;\n+\tnextlen = linelen(line + len, size - len);\n+\tif (nextlen < 12)\n+\t\treturn 0;\n+\treturn nextlen;\n+}\n+\n /*\n  * Parse a unified diff. Note that this really needs to parse each\n  * fragment separately, since the only way to know the difference\n@@ -1684,6 +1713,7 @@ static int parse_fragment(struct apply_state *state,\n {\n \tint added, deleted;\n \tint len = linelen(line, size), offset;\n+\tint skip_len = 0;\n \tunsigned long oldlines, newlines;\n \tunsigned long leading, trailing;\n \n@@ -1710,6 +1740,22 @@ static int parse_fragment(struct apply_state *state,\n \t\tlen = linelen(line, size);\n \t\tif (!len || line[len-1] != '\\n')\n \t\t\treturn -1;\n+\n+\t\t/*\n+\t\t * For an incomplete line, skip_len counts the bytes\n+\t\t * on \"\\\\ No newline...\" marker line that comes next\n+\t\t * to the current line.\n+\t\t *\n+\t\t * Reduce \"len\" to drop the newline at the end of\n+\t\t * line[], but add one to \"skip_len\", which will be\n+\t\t * added back to \"len\" for the next iteration, to\n+\t\t * compensate.\n+\t\t */\n+\t\tskip_len = adjust_incomplete(line, len, size);\n+\t\tif (skip_len) {\n+\t\t\tlen--;\n+\t\t\tskip_len++;\n+\t\t}\n \t\tswitch (*line) {\n \t\tdefault:\n \t\t\treturn -1;\n@@ -1745,20 +1791,10 @@ static int parse_fragment(struct apply_state *state,\n \t\t\tnewlines--;\n \t\t\ttrailing = 0;\n \t\t\tbreak;\n-\n-\t\t/*\n-\t\t * We allow \"\\ No newline at end of file\". Depending\n-\t\t * on locale settings when the patch was produced we\n-\t\t * don't know what this line looks like. The only\n-\t\t * thing we do know is that it begins with \"\\ \".\n-\t\t * Checking for 12 is just for sanity check -- any\n-\t\t * l10n of \"\\ No newline...\" is at least that long.\n-\t\t */\n-\t\tcase '\\\\':\n-\t\t\tif (len < 12 || memcmp(line, \"\\\\ \", 2))\n-\t\t\t\treturn -1;\n-\t\t\tbreak;\n \t\t}\n+\n+\t\t/* eat the \"\\\\ No newline...\" as well, if exists */\n+\t\tlen += skip_len;\n \t}\n \tif (oldlines || newlines)\n \t\treturn -1;\n@@ -1768,14 +1804,6 @@ static int parse_fragment(struct apply_state *state,\n \tfragment->leading = leading;\n \tfragment->trailing = trailing;\n \n-\t/*\n-\t * If a fragment ends with an incomplete line, we failed to include\n-\t * it in the above loop because we hit oldlines == newlines == 0\n-\t * before seeing it.\n-\t */\n-\tif (12 < size && !memcmp(line, \"\\\\ \", 2))\n-\t\toffset += linelen(line, size);\n-\n \tpatch->lines_added += added;\n \tpatch->lines_deleted += deleted;\n \n-- \n2.52.0-rc0-105-gc08128fbb6\n\n"},{"id":"530278","messageId":"20251105213052.1499224-10-gitster@pobox.com","threadId":"64429","inReplyTo":"20251105213052.1499224-1-gitster@pobox.com","subject":"[PATCH v2 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-05T21:30:49Z","receivedAt":"2025-11-05T21:31:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Reserve a few more bits in the diff flags word to be used for future\nwhitespace rules.  Add WS_INCOMPLETE_LINE without implementing the\nbehaviour (yet).\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/config/core.adoc |  2 ++\n diff.c                         | 16 ++++++++--------\n diff.h                         |  6 +++---\n ws.c                           |  6 ++++++\n ws.h                           |  3 ++-\n 5 files changed, 21 insertions(+), 12 deletions(-)\n\ndiff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc\nindex e2de270c86..682fb595fb 100644\n--- a/Documentation/config/core.adoc\n+++ b/Documentation/config/core.adoc\n@@ -626,6 +626,8 @@ core.whitespace::\n   part of the line terminator, i.e. with it, `trailing-space`\n   does not trigger if the character before such a carriage-return\n   is not a whitespace (not enabled by default).\n+* `incomplete-line` treats the last line of a file that is missing the\n+  newline at the end as an error (not enabled by default).\n * `tabwidth=<n>` tells how many character positions a tab occupies; this\n   is relevant for `indent-with-non-tab` and when Git fixes `tab-in-indent`\n   errors. The default tab width is 8. Allowed values are 1 to 63.\ndiff --git a/diff.c b/diff.c\nindex 99298720f4..8d03146aaa 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -804,15 +804,15 @@ enum diff_symbol {\n \n /*\n  * Flags for content lines:\n- * 0..11 are whitespace rules (see ws.h)\n- * 12..14 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD\n- * 16 is marking if the line is blank at EOF\n- * 17..19 are used for color-moved.\n+ * 0..15 are whitespace rules (see ws.h)\n+ * 16..18 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD\n+ * 19 is marking if the line is blank at EOF\n+ * 20..22 are used for color-moved.\n  */\n-#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF\t(1<<16)\n-#define DIFF_SYMBOL_MOVED_LINE\t\t\t(1<<17)\n-#define DIFF_SYMBOL_MOVED_LINE_ALT\t\t(1<<18)\n-#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING\t(1<<19)\n+#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF\t(1<<19)\n+#define DIFF_SYMBOL_MOVED_LINE\t\t\t(1<<20)\n+#define DIFF_SYMBOL_MOVED_LINE_ALT\t\t(1<<21)\n+#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING\t(1<<22)\n \n #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)\n \ndiff --git a/diff.h b/diff.h\nindex cbd355cf50..422658407d 100644\n--- a/diff.h\n+++ b/diff.h\n@@ -331,9 +331,9 @@ struct diff_options {\n \n \tint ita_invisible_in_index;\n /* white-space error highlighting */\n-#define WSEH_NEW        (1<<12)\n-#define WSEH_CONTEXT    (1<<13)\n-#define WSEH_OLD        (1<<14)\n+#define WSEH_NEW        (1<<16)\n+#define WSEH_CONTEXT    (1<<17)\n+#define WSEH_OLD        (1<<18)\n \tunsigned ws_error_highlight;\n \tconst char *prefix;\n \tint prefix_length;\ndiff --git a/ws.c b/ws.c\nindex 70acee3337..34a7b4fad2 100644\n--- a/ws.c\n+++ b/ws.c\n@@ -26,6 +26,7 @@ static struct whitespace_rule {\n \t{ \"blank-at-eol\", WS_BLANK_AT_EOL, 0 },\n \t{ \"blank-at-eof\", WS_BLANK_AT_EOF, 0 },\n \t{ \"tab-in-indent\", WS_TAB_IN_INDENT, 0, 1 },\n+\t{ \"incomplete-line\", WS_INCOMPLETE_LINE, 0, 0 },\n };\n \n unsigned parse_whitespace_rule(const char *string)\n@@ -139,6 +140,11 @@ char *whitespace_error_string(unsigned ws)\n \t\t\tstrbuf_addstr(&err, \", \");\n \t\tstrbuf_addstr(&err, \"tab in indent\");\n \t}\n+\tif (ws & WS_INCOMPLETE_LINE) {\n+\t\tif (err.len)\n+\t\t\tstrbuf_addstr(&err, \", \");\n+\t\tstrbuf_addstr(&err, \"no newline at the end of file\");\n+\t}\n \treturn strbuf_detach(&err, NULL);\n }\n \ndiff --git a/ws.h b/ws.h\nindex 23708efb73..06d5cb73f8 100644\n--- a/ws.h\n+++ b/ws.h\n@@ -15,13 +15,14 @@ struct strbuf;\n #define WS_CR_AT_EOL            (1<<9)\n #define WS_BLANK_AT_EOF         (1<<10)\n #define WS_TAB_IN_INDENT        (1<<11)\n+#define WS_INCOMPLETE_LINE      (1<<12)\n \n #define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)\n #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)\n #define WS_TAB_WIDTH_MASK       ((1<<6)-1)\n \n /* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */\n-#define WS_RULE_MASK            ((1<<12)-1)\n+#define WS_RULE_MASK            ((1<<16)-1)\n \n extern unsigned whitespace_rule_cfg;\n unsigned whitespace_rule(struct index_state *, const char *);\n-- \n2.52.0-rc0-105-gc08128fbb6\n\n"},{"id":"530279","messageId":"20251105213052.1499224-11-gitster@pobox.com","threadId":"64429","inReplyTo":"20251105213052.1499224-1-gitster@pobox.com","subject":"[PATCH v2 10/12] apply: check and fix incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-05T21:30:50Z","receivedAt":"2025-11-05T21:31:12Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The final line of a file that lacks the terminating newline at its\nend is called an incomplete line.  In general they are frowned upon\nfor many reasons (imagine concatenating two files with \"cat A B\" and\nwhat happens when A ends in an incomplete line, for example), and\ntext-oriented tools often mishandle such a line.\n\nImplement checks in \"git apply\" for incomplete lines, which is off\nby default for backward compatibility's sake, so that \"git apply\n--whitespace={fix,warn,error}\" can notice, warn against, and fix\nthem.\n\nAs one of the new test shows, if you modify contents on an\nincomplete line in the original and leave the resulting line\nincomplete, it is still considered a whitespace error, the reasoning\nbeing that \"you'd better fix it while at it if you are making a\nchange on an incomplete line anyway\", which may controversial.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n apply.c                  |  13 ++-\n t/t4124-apply-ws-rule.sh | 187 +++++++++++++++++++++++++++++++++++++++\n ws.c                     |  14 +++\n 3 files changed, 213 insertions(+), 1 deletion(-)\n\ndiff --git a/apply.c b/apply.c\nindex 2b0f8bdab5..c9fb45247d 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -1640,6 +1640,14 @@ static void record_ws_error(struct apply_state *state,\n \t    state->squelch_whitespace_errors < state->whitespace_error)\n \t\treturn;\n \n+\t/*\n+\t * line[len] for an incomplete line points at the \"\\n\" at the end\n+\t * of patch input line, so \"%.*s\" would drop the last letter on line;\n+\t * compensate for it.\n+\t */\n+\tif (result & WS_INCOMPLETE_LINE)\n+\t\tlen++;\n+\n \terr = whitespace_error_string(result);\n \tif (state->apply_verbosity > verbosity_silent)\n \t\tfprintf(stderr, \"%s:%d: %s.\\n%.*s\\n\",\n@@ -1794,7 +1802,10 @@ static int parse_fragment(struct apply_state *state,\n \t\t}\n \n \t\t/* eat the \"\\\\ No newline...\" as well, if exists */\n-\t\tlen += skip_len;\n+\t\tif (skip_len) {\n+\t\t\tlen += skip_len;\n+\t\t\tstate->linenr++;\n+\t\t}\n \t}\n \tif (oldlines || newlines)\n \t\treturn -1;\ndiff --git a/t/t4124-apply-ws-rule.sh b/t/t4124-apply-ws-rule.sh\nindex 485c7d2d12..115a0f8579 100755\n--- a/t/t4124-apply-ws-rule.sh\n+++ b/t/t4124-apply-ws-rule.sh\n@@ -556,4 +556,191 @@ test_expect_success 'whitespace check skipped for excluded paths' '\n \tgit apply --include=used --stat --whitespace=error <patch\n '\n \n+test_expect_success 'check incomplete lines (setup)' '\n+\trm -f .gitattributes &&\n+\tgit config core.whitespace incomplete-line\n+'\n+\n+test_expect_success 'incomplete context line (not an error)' '\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\t(test_write_lines 1 2 3 0 5 && printf 6) >sample2-i &&\n+\tcat sample-i >target &&\n+\tgit add target &&\n+\tcat sample2-i >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error <patch &&\n+\ttest_cmp sample2-i target &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error --check <patch 2>error &&\n+\ttest_cmp sample-i target &&\n+\ttest_must_be_empty error &&\n+\n+\tcat sample2-i >target &&\n+\tgit apply --whitespace=error -R <patch &&\n+\ttest_cmp sample-i target &&\n+\n+\tcat sample2-i >target &&\n+\tgit apply -R --whitespace=error --check <patch 2>error &&\n+\ttest_cmp sample2-i target &&\n+\ttest_must_be_empty error\n+'\n+\n+test_expect_success 'last line made incomplete (error)' '\n+\ttest_write_lines 1 2 3 4 5 6 >sample &&\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\tcat sample >target &&\n+\tgit add target &&\n+\tcat sample-i >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample >target &&\n+\ttest_must_fail git apply --whitespace=error <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample >target &&\n+\ttest_must_fail git apply --whitespace=error --check <patch 2>actual &&\n+\ttest_cmp sample target &&\n+\tcat >expect <<-\\EOF &&\n+\t<stdin>:10: no newline at the end of file.\n+\t6\n+\terror: 1 line adds whitespace errors.\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error -R <patch &&\n+\ttest_cmp sample target &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error --check -R <patch 2>error &&\n+\ttest_cmp sample-i target &&\n+\ttest_must_be_empty error &&\n+\n+\tcat sample >target &&\n+\tgit apply --whitespace=fix <patch &&\n+\ttest_cmp sample target\n+'\n+\n+test_expect_success 'incomplete line removed at the end (not an error)' '\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\ttest_write_lines 1 2 3 4 5 6 >sample &&\n+\tcat sample-i >target &&\n+\tgit add target &&\n+\tcat sample >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error <patch &&\n+\ttest_cmp sample target &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error --check <patch 2>error &&\n+\ttest_cmp sample-i target &&\n+\ttest_must_be_empty error &&\n+\n+\tcat sample >target &&\n+\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample >target &&\n+\ttest_must_fail git apply --whitespace=error --check -R <patch 2>actual &&\n+\ttest_cmp sample target &&\n+\tcat >expect <<-\\EOF &&\n+\t<stdin>:9: no newline at the end of file.\n+\t6\n+\terror: 1 line adds whitespace errors.\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tcat sample >target &&\n+\tgit apply --whitespace=fix -R <patch &&\n+\ttest_cmp sample target\n+'\n+\n+test_expect_success 'incomplete line corrected at the end (not an error)' '\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\ttest_write_lines 1 2 3 4 5 7 >sample3 &&\n+\tcat sample-i >target &&\n+\tgit add target &&\n+\tcat sample3 >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error <patch &&\n+\ttest_cmp sample3 target &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error --check <patch 2>error &&\n+\ttest_cmp sample-i target &&\n+\ttest_must_be_empty error &&\n+\n+\tcat sample3 >target &&\n+\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample3 >target &&\n+\ttest_must_fail git apply --whitespace=error -R --check <patch 2>actual &&\n+\ttest_cmp sample3 target &&\n+\tcat >expect <<-\\EOF &&\n+\t<stdin>:9: no newline at the end of file.\n+\t6\n+\terror: 1 line adds whitespace errors.\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tcat sample3 >target &&\n+\tgit apply --whitespace=fix -R <patch &&\n+\ttest_cmp sample target\n+'\n+\n+test_expect_success 'incomplete line modified at the end (error)' '\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\t(test_write_lines 1 2 3 4 5 && printf 7) >sample3-i &&\n+\ttest_write_lines 1 2 3 4 5 6 >sample &&\n+\ttest_write_lines 1 2 3 4 5 7 >sample3 &&\n+\tcat sample-i >target &&\n+\tgit add target &&\n+\tcat sample3-i >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample-i >target &&\n+\ttest_must_fail git apply --whitespace=error <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample-i >target &&\n+\ttest_must_fail git apply --whitespace=error --check <patch 2>actual &&\n+\ttest_cmp sample-i target &&\n+\tcat >expect <<-\\EOF &&\n+\t<stdin>:11: no newline at the end of file.\n+\t7\n+\terror: 1 line adds whitespace errors.\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tcat sample3-i >target &&\n+\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample3-i >target &&\n+\ttest_must_fail git apply --whitespace=error --check -R <patch 2>actual &&\n+\ttest_cmp sample3-i target &&\n+\tcat >expect <<-\\EOF &&\n+\t<stdin>:9: no newline at the end of file.\n+\t6\n+\terror: 1 line adds whitespace errors.\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=fix <patch &&\n+\ttest_cmp sample3 target &&\n+\n+\tcat sample3-i >target &&\n+\tgit apply --whitespace=fix -R <patch &&\n+\ttest_cmp sample target\n+'\n+\n test_done\ndiff --git a/ws.c b/ws.c\nindex 34a7b4fad2..6cc2466c0c 100644\n--- a/ws.c\n+++ b/ws.c\n@@ -186,6 +186,9 @@ static unsigned ws_check_emit_1(const char *line, int len, unsigned ws_rule,\n \tif (trailing_whitespace == -1)\n \t\ttrailing_whitespace = len;\n \n+\tif (!trailing_newline && (ws_rule & WS_INCOMPLETE_LINE))\n+\t\tresult |= WS_INCOMPLETE_LINE;\n+\n \t/* Check indentation */\n \tfor (i = 0; i < trailing_whitespace; i++) {\n \t\tif (line[i] == ' ')\n@@ -297,6 +300,17 @@ void ws_fix_copy(struct strbuf *dst, const char *src, int len, unsigned ws_rule,\n \tint last_space_in_indent = -1;\n \tint need_fix_leading_space = 0;\n \n+\t/*\n+\t * Remembering that we need to add '\\n' at the end\n+\t * is sufficient to fix an incomplete line.\n+\t */\n+\tif (ws_rule & WS_INCOMPLETE_LINE) {\n+\t\tif (0 < len && src[len - 1] != '\\n') {\n+\t\t\tfixed = 1;\n+\t\t\tadd_nl_to_tail = 1;\n+\t\t}\n+\t}\n+\n \t/*\n \t * Strip trailing whitespace\n \t */\n-- \n2.52.0-rc0-105-gc08128fbb6\n\n"},{"id":"530280","messageId":"20251105213052.1499224-12-gitster@pobox.com","threadId":"64429","inReplyTo":"20251105213052.1499224-1-gitster@pobox.com","subject":"[PATCH v2 11/12] diff: highlight and error out on incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-05T21:30:51Z","receivedAt":"2025-11-05T21:31:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Teach \"git diff\" to highlight \"\\ No newline at end of file\" message\nas a whitespace error when incomplete-line whitespace error class is\nin effect.  Thanks to the previous refactoring of complete rewrite\ncode path, we can do this at a single place.\n\nUnlike whitespace errors in the payload where we need to annotate in\nline, possibly using colors, the line that has whitespace problems,\nwe have a dedicated line already that can serve as the error\nmessage, so paint it as a whitespace error message.\n\nAlso teach \"git diff --check\" to notice incomplete lines as\nwhitespace errors and report when incomplete-line whitespace error\nclass is in effect.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c                     | 29 ++++++++++++++++--\n t/t4015-diff-whitespace.sh | 63 +++++++++++++++++++++++++++++++++++---\n 2 files changed, 86 insertions(+), 6 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 8d03146aaa..965b97f7f0 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1370,7 +1370,11 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n \t\temit_line(o, \"\", \"\", line, len);\n \t\tbreak;\n \tcase DIFF_SYMBOL_CONTEXT_INCOMPLETE:\n-\t\tset = diff_get_color_opt(o, DIFF_CONTEXT);\n+\t\tif ((flags & WS_INCOMPLETE_LINE) &&\n+\t\t    (flags & o->ws_error_highlight))\n+\t\t\tset = diff_get_color_opt(o, DIFF_WHITESPACE);\n+\t\telse\n+\t\t\tset = diff_get_color_opt(o, DIFF_CONTEXT);\n \t\treset = diff_get_color_opt(o, DIFF_RESET);\n \t\temit_line(o, set, reset, line, len);\n \t\tbreak;\n@@ -1666,8 +1670,14 @@ static void emit_context_line(struct emit_callback *ecbdata,\n static void emit_incomplete_line(struct emit_callback *ecbdata,\n \t\t\t\t const char *line, int len)\n {\n+\tint last_line_kind = ecbdata->last_line_kind;\n+\tunsigned flags = (last_line_kind == '+'\n+\t\t\t  ? WSEH_NEW\n+\t\t\t  : last_line_kind == '-'\n+\t\t\t  ? WSEH_OLD\n+\t\t\t  : WSEH_CONTEXT) | ecbdata->ws_rule;\n \temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n-\t\t\t line, len, 0);\n+\t\t\t line, len, flags);\n }\n \n static void emit_hunk_header(struct emit_callback *ecbdata,\n@@ -3257,6 +3267,7 @@ struct checkdiff_t {\n \tstruct diff_options *o;\n \tunsigned ws_rule;\n \tunsigned status;\n+\tint last_line_kind;\n };\n \n static int is_conflict_marker(const char *line, int marker_size, unsigned long len)\n@@ -3295,6 +3306,7 @@ static void checkdiff_consume_hunk(void *priv,\n static int checkdiff_consume(void *priv, char *line, unsigned long len)\n {\n \tstruct checkdiff_t *data = priv;\n+\tint last_line_kind;\n \tint marker_size = data->conflict_marker_size;\n \tconst char *ws = diff_get_color(data->o->use_color, DIFF_WHITESPACE);\n \tconst char *reset = diff_get_color(data->o->use_color, DIFF_RESET);\n@@ -3305,6 +3317,8 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)\n \tassert(data->o);\n \tline_prefix = diff_line_prefix(data->o);\n \n+\tlast_line_kind = data->last_line_kind;\n+\tdata->last_line_kind = line[0];\n \tif (line[0] == '+') {\n \t\tunsigned bad;\n \t\tdata->lineno++;\n@@ -3327,6 +3341,17 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)\n \t\t\t      data->o->file, set, reset, ws);\n \t} else if (line[0] == ' ') {\n \t\tdata->lineno++;\n+\t} else if (line[0] == '\\\\') {\n+\t\t/* no newline at the end of the line */\n+\t\tif ((data->ws_rule & WS_INCOMPLETE_LINE) &&\n+\t\t    (last_line_kind == '+')) {\n+\t\t\tunsigned bad = WS_INCOMPLETE_LINE;\n+\t\t\tdata->status |= bad;\n+\t\t\terr = whitespace_error_string(bad);\n+\t\t\tfprintf(data->o->file, \"%s%s:%d: %s.\\n\",\n+\t\t\t\tline_prefix, data->filename, data->lineno, err);\n+\t\t\tfree(err);\n+\t\t}\n \t}\n \treturn 0;\n }\ndiff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh\nindex 9de7f73f42..138730cbce 100755\n--- a/t/t4015-diff-whitespace.sh\n+++ b/t/t4015-diff-whitespace.sh\n@@ -43,6 +43,49 @@ do\n \t'\n done\n \n+test_expect_success \"incomplete line in both pre- and post-image context\" '\n+\t(echo foo && echo baz | tr -d \"\\012\") >x &&\n+\tgit add x &&\n+\t(echo bar && echo baz | tr -d \"\\012\") >x &&\n+\tgit diff x &&\n+\tgit -c core.whitespace=incomplete diff --check x &&\n+\tgit diff -R x &&\n+\tgit -c core.whitespace=incomplete diff -R --check x\n+'\n+\n+test_expect_success \"incomplete lines on both pre- and post-image\" '\n+\t# The interpretation taken here is \"since you are toucing\n+\t# the line anyway, you would better fix the incomplete line\n+\t# while you are at it.\"  but this is debatable.\n+\techo foo | tr -d \"\\012\" >x &&\n+\tgit add x &&\n+\techo bar | tr -d \"\\012\" >x &&\n+\tgit diff x &&\n+\ttest_must_fail git -c core.whitespace=incomplete diff --check x &&\n+\tgit diff -R x &&\n+\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x\n+'\n+\n+test_expect_success \"fix incomplete line in pre-image\" '\n+\techo foo | tr -d \"\\012\" >x &&\n+\tgit add x &&\n+\techo bar >x &&\n+\tgit diff x &&\n+\tgit -c core.whitespace=incomplete diff --check x &&\n+\tgit diff -R x &&\n+\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x\n+'\n+\n+test_expect_success \"new incomplete line in post-image\" '\n+\techo foo >x &&\n+\tgit add x &&\n+\techo bar | tr -d \"\\012\" >x &&\n+\tgit diff x &&\n+\ttest_must_fail git -c core.whitespace=incomplete diff --check x &&\n+\tgit diff -R x &&\n+\tgit -c core.whitespace=incomplete diff -R --check x\n+'\n+\n test_expect_success \"Ray Lehtiniemi's example\" '\n \tcat <<-\\EOF >x &&\n \tdo {\n@@ -1040,7 +1083,8 @@ test_expect_success 'ws-error-highlight test setup' '\n \t{\n \t\techo \"0. blank-at-eol \" &&\n \t\techo \"1. still-blank-at-eol \" &&\n-\t\techo \"2. and a new line \"\n+\t\techo \"2. and a new line \" &&\n+\t\tprintf \"3. and more\"\n \t} >x &&\n \tnew_hash_x=$(git hash-object x) &&\n \tafter=$(git rev-parse --short \"$new_hash_x\") &&\n@@ -1050,11 +1094,13 @@ test_expect_success 'ws-error-highlight test setup' '\n \t<BOLD>index $before..$after 100644<RESET>\n \t<BOLD>--- a/x<RESET>\n \t<BOLD>+++ b/x<RESET>\n-\t<CYAN>@@ -1,2 +1,3 @@<RESET>\n+\t<CYAN>@@ -1,2 +1,4 @@<RESET>\n \t 0. blank-at-eol <RESET>\n \t<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>\n \t<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>\n \t<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>\n+\t<GREEN>+<RESET><GREEN>3. and more<RESET>\n+\t<BLUE>\\ No newline at end of file<RESET>\n \tEOF\n \n \tcat >expect.all <<-EOF &&\n@@ -1062,11 +1108,13 @@ test_expect_success 'ws-error-highlight test setup' '\n \t<BOLD>index $before..$after 100644<RESET>\n \t<BOLD>--- a/x<RESET>\n \t<BOLD>+++ b/x<RESET>\n-\t<CYAN>@@ -1,2 +1,3 @@<RESET>\n+\t<CYAN>@@ -1,2 +1,4 @@<RESET>\n \t <RESET>0. blank-at-eol<RESET><BLUE> <RESET>\n \t<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>\n \t<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>\n \t<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>\n+\t<GREEN>+<RESET><GREEN>3. and more<RESET>\n+\t<BLUE>\\ No newline at end of file<RESET>\n \tEOF\n \n \tcat >expect.none <<-EOF\n@@ -1074,16 +1122,19 @@ test_expect_success 'ws-error-highlight test setup' '\n \t<BOLD>index $before..$after 100644<RESET>\n \t<BOLD>--- a/x<RESET>\n \t<BOLD>+++ b/x<RESET>\n-\t<CYAN>@@ -1,2 +1,3 @@<RESET>\n+\t<CYAN>@@ -1,2 +1,4 @@<RESET>\n \t 0. blank-at-eol <RESET>\n \t<RED>-1. blank-at-eol <RESET>\n \t<GREEN>+1. still-blank-at-eol <RESET>\n \t<GREEN>+2. and a new line <RESET>\n+\t<GREEN>+3. and more<RESET>\n+\t\\ No newline at end of file<RESET>\n \tEOF\n \n '\n \n test_expect_success 'test --ws-error-highlight option' '\n+\tgit config core.whitespace blank-at-eol,incomplete-line &&\n \n \tgit diff --color --ws-error-highlight=default,old >current.raw &&\n \ttest_decode_color <current.raw >current &&\n@@ -1100,6 +1151,7 @@ test_expect_success 'test --ws-error-highlight option' '\n '\n \n test_expect_success 'test diff.wsErrorHighlight config' '\n+\tgit config core.whitespace blank-at-eol,incomplete-line &&\n \n \tgit -c diff.wsErrorHighlight=default,old diff --color >current.raw &&\n \ttest_decode_color <current.raw >current &&\n@@ -1116,6 +1168,7 @@ test_expect_success 'test diff.wsErrorHighlight config' '\n '\n \n test_expect_success 'option overrides diff.wsErrorHighlight' '\n+\tgit config core.whitespace blank-at-eol,incomplete-line &&\n \n \tgit -c diff.wsErrorHighlight=none \\\n \t\tdiff --color --ws-error-highlight=default,old >current.raw &&\n@@ -1135,6 +1188,8 @@ test_expect_success 'option overrides diff.wsErrorHighlight' '\n '\n \n test_expect_success 'detect moved code, complete file' '\n+\tgit config core.whitespace blank-at-eol &&\n+\n \tgit reset --hard &&\n \tcat <<-\\EOF >test.c &&\n \t#include<stdio.h>\n-- \n2.52.0-rc0-105-gc08128fbb6\n\n"},{"id":"530281","messageId":"20251105213052.1499224-13-gitster@pobox.com","threadId":"64429","inReplyTo":"20251105213052.1499224-1-gitster@pobox.com","subject":"[PATCH v2 12/12] attr: enable incomplete-line whitespace error for this project","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-05T21:30:52Z","receivedAt":"2025-11-05T21:31:15Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Now \"git diff --check\" and \"git apply --whitespace=warn/fix\" learned\nincomplete line is a whitespace error, enable them for this project\nto prevent patches to add new incomplete lines to our sources.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n .gitattributes | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/.gitattributes b/.gitattributes\nindex 32583149c2..0accd23848 100644\n--- a/.gitattributes\n+++ b/.gitattributes\n@@ -1,6 +1,6 @@\n * whitespace=!indent,trail,space\n-*.[ch] whitespace=indent,trail,space diff=cpp\n-*.sh whitespace=indent,trail,space text eol=lf\n+*.[ch] whitespace=indent,trail,space,incomplete diff=cpp\n+*.sh whitespace=indent,trail,space,incomplete text eol=lf\n *.perl text eol=lf diff=perl\n *.pl text eof=lf diff=perl\n *.pm text eol=lf diff=perl\n-- \n2.52.0-rc0-105-gc08128fbb6\n\n"},{"id":"530436","messageId":"aRG5KeoZg1Q1y8DE@pks.im","threadId":"64429","inReplyTo":"20251105213052.1499224-6-gitster@pobox.com","subject":"Re: [PATCH v2 05/12] diff: refactor output of incomplete line","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-11-10T10:06:33Z","receivedAt":"2025-11-10T10:06:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Nov 05, 2025 at 01:30:45PM -0800, Junio C Hamano wrote:\n> Create a helper function that reacts to \"\\ No newline at the end of\n> file\" in preparation for unifying the incomplete line handling in\n> the code path that handles xdiff output and the code path that\n> bypasses xdiff and produces complete rewrite patch.\n> \n> Signed-off-by: Junio C Hamano <gitster@pobox.com>\n> ---\n>  diff.c | 14 ++++++++++++--\n>  1 file changed, 12 insertions(+), 2 deletions(-)\n> \n> diff --git a/diff.c b/diff.c\n> index e73320dfb1..d388d318e4 100644\n> --- a/diff.c\n> +++ b/diff.c\n> @@ -1379,6 +1379,10 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n>  \t\temit_line(o, \"\", \"\", line, len);\n>  \t\tbreak;\n>  \tcase DIFF_SYMBOL_CONTEXT_INCOMPLETE:\n> +\t\tset = diff_get_color_opt(o, DIFF_CONTEXT);\n> +\t\treset = diff_get_color_opt(o, DIFF_RESET);\n> +\t\temit_line(o, set, reset, line, len);\n> +\t\tbreak;\n>  \tcase DIFF_SYMBOL_CONTEXT_MARKER:\n>  \t\tcontext = diff_get_color_opt(o, DIFF_CONTEXT);\n>  \t\treset = diff_get_color_opt(o, DIFF_RESET);\n\nI found it a bit confusing that we use `set`/`reset` here instead of\n`context`/`reset` as before. It doesn't make any difference as these are\nlocal variables anyway, but it might make sense to explain why you chose\nto use different variables.\n\nHonestly, this whole hunk is somewhat confusing in the first place. It\ndoesn't seem to connect with the description in any way, as it's a no-op\nchange and we don't even use the newly introduced function.\n\nPatrick\n"},{"id":"530437","messageId":"aRG5NSvzeie_J3x9@pks.im","threadId":"64429","inReplyTo":"20251105213052.1499224-8-gitster@pobox.com","subject":"Re: [PATCH v2 07/12] diff: update the way rewrite diff handles incomplete lines","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-11-10T10:06:45Z","receivedAt":"2025-11-10T10:06:51Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Nov 05, 2025 at 01:30:47PM -0800, Junio C Hamano wrote:\n> The diff_symbol based output framework uses one DIFF_SYMBOL_* enum\n> value per the kind of output lines of \"git diff\", which corresponds\n> to one output line from the xdiff machinery used internally.  Most\n> notably, DIFF_SYMBOL_PLUS and DIFF_SYMBOL_MINUS that correspond to\n> \"+\" and \"-\" lines are designed to always take a complete line, even\n\n\"complete line\" as in newline-terminated? I only recognized that this is\nwhat you meant in the next paragraph, so it might be useful to clarify\nhere already what you mean.\n\n> diff --git a/diff.c b/diff.c\n> index 347cd9c6e9..99298720f4 100644\n> --- a/diff.c\n> +++ b/diff.c\n> @@ -1786,22 +1777,36 @@ static void emit_rewrite_lines(struct emit_callback *ecbdata,\n>  \tconst char *endp = NULL;\n>  \n>  \twhile (0 < size) {\n> -\t\tint len;\n> +\t\tint len, plen;\n> +\t\tchar *pdata = NULL;\n>  \n>  \t\tendp = memchr(data, '\\n', size);\n>  \t\tlen = endp ? (endp - data + 1) : size;\n> +\t\tplen = len;\n> +\n> +\t\tif (!endp) {\n> +\t\t\tplen = len + 1;\n> +\t\t\tpdata = xmalloc(plen + 2);\n> +\t\t\tmemcpy(pdata, data, len);\n> +\t\t\tpdata[len] = '\\n';\n> +\t\t\tpdata[len + 1] = '\\0';\n> +\t\t}\n>  \t\tif (prefix != '+') {\n>  \t\t\tecbdata->lno_in_preimage++;\n> -\t\t\temit_del_line(ecbdata, data, len);\n> +\t\t\temit_del_line(ecbdata, pdata ? pdata : data, plen);\n>  \t\t} else {\n>  \t\t\tecbdata->lno_in_postimage++;\n> -\t\t\temit_add_line(ecbdata, data, len);\n> +\t\t\temit_add_line(ecbdata, pdata ? pdata : data, plen);\n>  \t\t}\n> +\t\tfree(pdata);\n>  \t\tsize -= len;\n>  \t\tdata += len;\n>  \t}\n> -\tif (!endp)\n> -\t\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);\n> +\tif (!endp) {\n> +\t\tstatic const char nneof[] = \"\\\\ No newline at end of file\\n\";\n> +\t\tecbdata->last_line_kind = prefix;\n> +\t\temit_incomplete_line(ecbdata, nneof, sizeof(nneof) - 1);\n> +\t}\n>  }\n\nOkay. I was wondering at first how this would get executed for both\npre- and postimage if it's not part of the loop anymore. But this is\nmostly showing my complete ignorance for the \"diff\" subsystem, as we end\nup calling `emit_rewrite_lines()` itself once for each image.\n\nPatrick\n"},{"id":"530438","messageId":"aRG57pZ7nDfk78du@pks.im","threadId":"64429","inReplyTo":"20251105213052.1499224-1-gitster@pobox.com","subject":"Re: [PATCH v2 00/12] Incomplete lines","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-11-10T10:09:50Z","receivedAt":"2025-11-10T10:09:56Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Nov 05, 2025 at 01:30:40PM -0800, Junio C Hamano wrote:\n> One of the common kind of whitespace errors is to lack the final\n> newline at the end of a file, but so far, neither \"git diff\" or \"git\n> apply\" did anything about them.\n> \n> This series introduces \"incomplete-line\" whitespace error class,\n> that you can add to either the core.whitespace configuration\n> variable, or the whitespace attribute in your .gitattributes files.\n> \n> The class is disabled by default, so the final step enables it for\n> our project by defining it in the .gitattributes file.\n> \n> The incomplete line marker that is given for a context line is not\n> considered an error.  The reasoning is that your preimage did have\n> incomplete line, but you did not touch the contents on that\n> incomplete line in your patch, so you left the line intact.  It is\n> not a new breakage you are responsible for.\n> \n> If the incomplete line marker follows a postimage line, on the other\n> hand, it means that you added a new line at the end of the file that\n> is incomplete *and* that line did not exist in the preimage.  The\n> last line of the preimage may have been incomplete already, but then\n> you updated the contents on that line, so you could have easily\n> fixed the incompleteness of the line while at it.  Either way, you\n> are responsible for the incompleteness of the last ine in the\n> resulting file.\n\nI've read through the series and left two comments, but my review\ndefinitely doesn't count as a \"qualified\" review. I'm way too oblivious\nof what's happening in the diff subsystem to really be able to point out\nany mistakes. So I hope that somebody more familiar with this code will\nchime in.\n\nThat being said, I think that the end goal of this series is quite\nuseful and something that I want to have :)\n\nThanks!\n\nPatrick\n"},{"id":"530443","messageId":"d2bdce87-fcd9-4f2f-a46d-b6af58af6e7e@gmail.com","threadId":"64429","inReplyTo":"20251105213052.1499224-1-gitster@pobox.com","subject":"Re: [PATCH v2 00/12] Incomplete lines","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-10T14:53:40Z","receivedAt":"2025-11-10T14:53:44Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Junio\n\nOn 05/11/2025 21:30, Junio C Hamano wrote:\n> One of the common kind of whitespace errors is to lack the final\n> newline at the end of a file, but so far, neither \"git diff\" or \"git\n> apply\" did anything about them.\n> \n> This series introduces \"incomplete-line\" whitespace error class,\n> that you can add to either the core.whitespace configuration\n> variable, or the whitespace attribute in your .gitattributes files.\n\nI've read through all the diff related changes but not the changes to \napply. I've left a few comments but the cleanups and the new \nimplementation of the new feature look good to me.\n\nThanks\n\nPhillip\n\n\n> The class is disabled by default, so the final step enables it for\n> our project by defining it in the .gitattributes file.\n> \n> The incomplete line marker that is given for a context line is not\n> considered an error.  The reasoning is that your preimage did have\n> incomplete line, but you did not touch the contents on that\n> incomplete line in your patch, so you left the line intact.  It is\n> not a new breakage you are responsible for.\n> \n> If the incomplete line marker follows a postimage line, on the other\n> hand, it means that you added a new line at the end of the file that\n> is incomplete *and* that line did not exist in the preimage.  The\n> last line of the preimage may have been incomplete already, but then\n> you updated the contents on that line, so you could have easily\n> fixed the incompleteness of the line while at it.  Either way, you\n> are responsible for the incompleteness of the last ine in the\n> resulting file.\n> \n> The organization of the series is as follows.\n> \n>   * The first patch [01/12] is a clean-up we have seen earlier on the\n>     list already (https://lore.kernel.org/git/xmqqfrb4hyjl.fsf@gitster.g/).\n> \n>   * The patches [02/12] - [08/12] are preliminary clean-up made to\n>     both \"git diff\" and \"git apply\" machinery.\n> \n>   * The patch [09/12] shifts the bit assignment (cleaned-up in\n>     [01/12] without changing any values) to make room for new\n>     whitespace error class (which was last updated in 2007 IIRC, so\n>     the set of whitespace errors surprisingly haven't changed for\n>     quite some time), and defines the new \"incomplete-line\" class.\n> \n>   * The patch [10/12] teaches \"git apply --whitespace=<mode>\" and\n>     \"git apply --check\" about the incomplete-line error class.\n> \n>   * The patch [11/12] teaches \"git diff [--check]\" about the\n>     incomplete-line error class.\n> \n>   * The final patch [12/12] enables the incomplete-line error class\n>     for our project for C source files and shell scripts.  I didn't\n>     touch the cover-all * entry.\n> \n> Changes in v2:\n> \n>   - rolled the definition (but not implementation) of the new\n>     \"incomplete-line\" class into step [09/12] that shifts the bit\n>     assignment.  The documentation of core.whitespace has also be\n>     updated in this step.\n> \n>   - \"git apply --check\" miscounted line number reported for the\n>     incomplete line error, which has been corrected in step [10/12].\n> \n>   - t4124-apply-ws-rule.sh has been extended to cover \"git apply\n>     --check\" and the diagnostic output from it in step [10/12].\n> \n> Junio C Hamano (12):\n>    whitespace: correct bit assignment comments\n>    diff: emit_line_ws_markup() if/else style fix\n>    diff: correct suppress_blank_empty hack\n>    diff: fix incorrect counting of line numbers\n>    diff: refactor output of incomplete line\n>    diff: call emit_callback ecbdata everywhere\n>    diff: update the way rewrite diff handles incomplete lines\n>    apply: revamp the parsing of incomplete lines\n>    whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE\n>    apply: check and fix incomplete lines\n>    diff: highlight and error out on incomplete lines\n>    attr: enable incomplete-line whitespace error for this project\n> \n>   .gitattributes                 |   4 +-\n>   Documentation/config/core.adoc |   2 +\n>   apply.c                        |  79 ++++++++++----\n>   diff.c                         | 148 +++++++++++++++++---------\n>   diff.h                         |   6 +-\n>   t/t4015-diff-whitespace.sh     |  63 ++++++++++-\n>   t/t4124-apply-ws-rule.sh       | 187 +++++++++++++++++++++++++++++++++\n>   ws.c                           |  20 ++++\n>   ws.h                           |  26 +++--\n>   9 files changed, 448 insertions(+), 87 deletions(-)\n> \n> Range-diff against v1:\n>   1:  4168f28fe7 =  1:  8a493cdea5 whitespace: correct bit assignment comments\n>   2:  53b7a010e7 =  2:  a01d99a055 diff: emit_line_ws_markup() if/else style fix\n>   3:  d93dd05543 =  3:  e3ea40af19 diff: correct suppress_blank_empty hack\n>   4:  5f58400bd7 =  4:  e15e89d3e2 diff: fix incorrect counting of line numbers\n>   5:  84c4ca147f =  5:  c007b3d7a7 diff: refactor output of incomplete line\n>   6:  55b42a1944 =  6:  0cea57091b diff: call emit_callback ecbdata everywhere\n>   7:  6947838d13 =  7:  523196b440 diff: update the way rewrite diff handles incomplete lines\n>   8:  63c36c6f70 =  8:  e098932784 apply: revamp the parsing of incomplete lines\n>   9:  00b645bb4e !  9:  28538f149f whitespace: allocate a few more bits\n>      @@ Metadata\n>       Author: Junio C Hamano <gitster@pobox.com>\n>       \n>        ## Commit message ##\n>      -    whitespace: allocate a few more bits\n>      +    whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE\n>       \n>           Reserve a few more bits in the diff flags word to be used for future\n>      -    whitespace rules.  No behaviour changes intended.\n>      +    whitespace rules.  Add WS_INCOMPLETE_LINE without implementing the\n>      +    behaviour (yet).\n>       \n>           Signed-off-by: Junio C Hamano <gitster@pobox.com>\n>       \n>      + ## Documentation/config/core.adoc ##\n>      +@@ Documentation/config/core.adoc: core.whitespace::\n>      +   part of the line terminator, i.e. with it, `trailing-space`\n>      +   does not trigger if the character before such a carriage-return\n>      +   is not a whitespace (not enabled by default).\n>      ++* `incomplete-line` treats the last line of a file that is missing the\n>      ++  newline at the end as an error (not enabled by default).\n>      + * `tabwidth=<n>` tells how many character positions a tab occupies; this\n>      +   is relevant for `indent-with-non-tab` and when Git fixes `tab-in-indent`\n>      +   errors. The default tab width is 8. Allowed values are 1 to 63.\n>      +\n>        ## diff.c ##\n>       @@ diff.c: enum diff_symbol {\n>        \n>      @@ diff.h: struct diff_options {\n>        \tconst char *prefix;\n>        \tint prefix_length;\n>       \n>      + ## ws.c ##\n>      +@@ ws.c: static struct whitespace_rule {\n>      + \t{ \"blank-at-eol\", WS_BLANK_AT_EOL, 0 },\n>      + \t{ \"blank-at-eof\", WS_BLANK_AT_EOF, 0 },\n>      + \t{ \"tab-in-indent\", WS_TAB_IN_INDENT, 0, 1 },\n>      ++\t{ \"incomplete-line\", WS_INCOMPLETE_LINE, 0, 0 },\n>      + };\n>      +\n>      + unsigned parse_whitespace_rule(const char *string)\n>      +@@ ws.c: char *whitespace_error_string(unsigned ws)\n>      + \t\t\tstrbuf_addstr(&err, \", \");\n>      + \t\tstrbuf_addstr(&err, \"tab in indent\");\n>      + \t}\n>      ++\tif (ws & WS_INCOMPLETE_LINE) {\n>      ++\t\tif (err.len)\n>      ++\t\t\tstrbuf_addstr(&err, \", \");\n>      ++\t\tstrbuf_addstr(&err, \"no newline at the end of file\");\n>      ++\t}\n>      + \treturn strbuf_detach(&err, NULL);\n>      + }\n>      +\n>      +\n>        ## ws.h ##\n>       @@ ws.h: struct strbuf;\n>      + #define WS_CR_AT_EOL            (1<<9)\n>      + #define WS_BLANK_AT_EOF         (1<<10)\n>      + #define WS_TAB_IN_INDENT        (1<<11)\n>      ++#define WS_INCOMPLETE_LINE      (1<<12)\n>      +\n>      + #define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)\n>      + #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)\n>        #define WS_TAB_WIDTH_MASK       ((1<<6)-1)\n>        \n>        /* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */\n> 10:  662f15d0b4 ! 10:  7369e77309 apply: check and fix incomplete lines\n>      @@ Commit message\n>           what happens when A ends in an incomplete line, for example), and\n>           text-oriented tools often mishandle such a line.\n>       \n>      -    Introduce a new whitespace rule \"incomplete-line\", which is off by\n>      -    default for backward compatibility's sake, so that \"git apply\n>      +    Implement checks in \"git apply\" for incomplete lines, which is off\n>      +    by default for backward compatibility's sake, so that \"git apply\n>           --whitespace={fix,warn,error}\" can notice, warn against, and fix\n>           them.\n>       \n>      @@ Commit message\n>       \n>           Signed-off-by: Junio C Hamano <gitster@pobox.com>\n>       \n>      + ## apply.c ##\n>      +@@ apply.c: static void record_ws_error(struct apply_state *state,\n>      + \t    state->squelch_whitespace_errors < state->whitespace_error)\n>      + \t\treturn;\n>      +\n>      ++\t/*\n>      ++\t * line[len] for an incomplete line points at the \"\\n\" at the end\n>      ++\t * of patch input line, so \"%.*s\" would drop the last letter on line;\n>      ++\t * compensate for it.\n>      ++\t */\n>      ++\tif (result & WS_INCOMPLETE_LINE)\n>      ++\t\tlen++;\n>      ++\n>      + \terr = whitespace_error_string(result);\n>      + \tif (state->apply_verbosity > verbosity_silent)\n>      + \t\tfprintf(stderr, \"%s:%d: %s.\\n%.*s\\n\",\n>      +@@ apply.c: static int parse_fragment(struct apply_state *state,\n>      + \t\t}\n>      +\n>      + \t\t/* eat the \"\\\\ No newline...\" as well, if exists */\n>      +-\t\tlen += skip_len;\n>      ++\t\tif (skip_len) {\n>      ++\t\t\tlen += skip_len;\n>      ++\t\t\tstate->linenr++;\n>      ++\t\t}\n>      + \t}\n>      + \tif (oldlines || newlines)\n>      + \t\treturn -1;\n>      +\n>        ## t/t4124-apply-ws-rule.sh ##\n>       @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excluded paths' '\n>        \tgit apply --include=used --stat --whitespace=error <patch\n>      @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl\n>       +\tgit apply --whitespace=error <patch &&\n>       +\ttest_cmp sample2-i target &&\n>       +\n>      ++\tcat sample-i >target &&\n>      ++\tgit apply --whitespace=error --check <patch 2>error &&\n>      ++\ttest_cmp sample-i target &&\n>      ++\ttest_must_be_empty error &&\n>      ++\n>       +\tcat sample2-i >target &&\n>       +\tgit apply --whitespace=error -R <patch &&\n>      -+\ttest_cmp sample-i target\n>      ++\ttest_cmp sample-i target &&\n>      ++\n>      ++\tcat sample2-i >target &&\n>      ++\tgit apply -R --whitespace=error --check <patch 2>error &&\n>      ++\ttest_cmp sample2-i target &&\n>      ++\ttest_must_be_empty error\n>       +'\n>       +\n>       +test_expect_success 'last line made incomplete (error)' '\n>      @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl\n>       +\ttest_must_fail git apply --whitespace=error <patch 2>error &&\n>       +\ttest_grep \"no newline\" error &&\n>       +\n>      ++\tcat sample >target &&\n>      ++\ttest_must_fail git apply --whitespace=error --check <patch 2>actual &&\n>      ++\ttest_cmp sample target &&\n>      ++\tcat >expect <<-\\EOF &&\n>      ++\t<stdin>:10: no newline at the end of file.\n>      ++\t6\n>      ++\terror: 1 line adds whitespace errors.\n>      ++\tEOF\n>      ++\ttest_cmp expect actual &&\n>      ++\n>       +\tcat sample-i >target &&\n>       +\tgit apply --whitespace=error -R <patch &&\n>       +\ttest_cmp sample target &&\n>       +\n>      ++\tcat sample-i >target &&\n>      ++\tgit apply --whitespace=error --check -R <patch 2>error &&\n>      ++\ttest_cmp sample-i target &&\n>      ++\ttest_must_be_empty error &&\n>      ++\n>       +\tcat sample >target &&\n>       +\tgit apply --whitespace=fix <patch &&\n>       +\ttest_cmp sample target\n>      @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl\n>       +\tgit apply --whitespace=error <patch &&\n>       +\ttest_cmp sample target &&\n>       +\n>      ++\tcat sample-i >target &&\n>      ++\tgit apply --whitespace=error --check <patch 2>error &&\n>      ++\ttest_cmp sample-i target &&\n>      ++\ttest_must_be_empty error &&\n>      ++\n>       +\tcat sample >target &&\n>       +\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n>       +\ttest_grep \"no newline\" error &&\n>       +\n>       +\tcat sample >target &&\n>      ++\ttest_must_fail git apply --whitespace=error --check -R <patch 2>actual &&\n>      ++\ttest_cmp sample target &&\n>      ++\tcat >expect <<-\\EOF &&\n>      ++\t<stdin>:9: no newline at the end of file.\n>      ++\t6\n>      ++\terror: 1 line adds whitespace errors.\n>      ++\tEOF\n>      ++\ttest_cmp expect actual &&\n>      ++\n>      ++\tcat sample >target &&\n>       +\tgit apply --whitespace=fix -R <patch &&\n>       +\ttest_cmp sample target\n>       +'\n>      @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl\n>       +\tgit apply --whitespace=error <patch &&\n>       +\ttest_cmp sample3 target &&\n>       +\n>      ++\tcat sample-i >target &&\n>      ++\tgit apply --whitespace=error --check <patch 2>error &&\n>      ++\ttest_cmp sample-i target &&\n>      ++\ttest_must_be_empty error &&\n>      ++\n>       +\tcat sample3 >target &&\n>       +\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n>       +\ttest_grep \"no newline\" error &&\n>       +\n>       +\tcat sample3 >target &&\n>      ++\ttest_must_fail git apply --whitespace=error -R --check <patch 2>actual &&\n>      ++\ttest_cmp sample3 target &&\n>      ++\tcat >expect <<-\\EOF &&\n>      ++\t<stdin>:9: no newline at the end of file.\n>      ++\t6\n>      ++\terror: 1 line adds whitespace errors.\n>      ++\tEOF\n>      ++\ttest_cmp expect actual &&\n>      ++\n>      ++\tcat sample3 >target &&\n>       +\tgit apply --whitespace=fix -R <patch &&\n>       +\ttest_cmp sample target\n>       +'\n>      @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl\n>       +\ttest_must_fail git apply --whitespace=error <patch 2>error &&\n>       +\ttest_grep \"no newline\" error &&\n>       +\n>      ++\tcat sample-i >target &&\n>      ++\ttest_must_fail git apply --whitespace=error --check <patch 2>actual &&\n>      ++\ttest_cmp sample-i target &&\n>      ++\tcat >expect <<-\\EOF &&\n>      ++\t<stdin>:11: no newline at the end of file.\n>      ++\t7\n>      ++\terror: 1 line adds whitespace errors.\n>      ++\tEOF\n>      ++\ttest_cmp expect actual &&\n>      ++\n>       +\tcat sample3-i >target &&\n>       +\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n>       +\ttest_grep \"no newline\" error &&\n>       +\n>      ++\tcat sample3-i >target &&\n>      ++\ttest_must_fail git apply --whitespace=error --check -R <patch 2>actual &&\n>      ++\ttest_cmp sample3-i target &&\n>      ++\tcat >expect <<-\\EOF &&\n>      ++\t<stdin>:9: no newline at the end of file.\n>      ++\t6\n>      ++\terror: 1 line adds whitespace errors.\n>      ++\tEOF\n>      ++\ttest_cmp expect actual &&\n>      ++\n>       +\tcat sample-i >target &&\n>       +\tgit apply --whitespace=fix <patch &&\n>       +\ttest_cmp sample3 target &&\n>      @@ t/t4124-apply-ws-rule.sh: test_expect_success 'whitespace check skipped for excl\n>        test_done\n>       \n>        ## ws.c ##\n>      -@@ ws.c: static struct whitespace_rule {\n>      - \t{ \"blank-at-eol\", WS_BLANK_AT_EOL, 0 },\n>      - \t{ \"blank-at-eof\", WS_BLANK_AT_EOF, 0 },\n>      - \t{ \"tab-in-indent\", WS_TAB_IN_INDENT, 0, 1 },\n>      -+\t{ \"incomplete-line\", WS_INCOMPLETE_LINE, 0, 0 },\n>      - };\n>      -\n>      - unsigned parse_whitespace_rule(const char *string)\n>      -@@ ws.c: char *whitespace_error_string(unsigned ws)\n>      - \t\t\tstrbuf_addstr(&err, \", \");\n>      - \t\tstrbuf_addstr(&err, \"tab in indent\");\n>      - \t}\n>      -+\tif (ws & WS_INCOMPLETE_LINE) {\n>      -+\t\tif (err.len)\n>      -+\t\t\tstrbuf_addstr(&err, \", \");\n>      -+\t\tstrbuf_addstr(&err, \"no newline at the end of file\");\n>      -+\t}\n>      - \treturn strbuf_detach(&err, NULL);\n>      - }\n>      -\n>       @@ ws.c: static unsigned ws_check_emit_1(const char *line, int len, unsigned ws_rule,\n>        \tif (trailing_whitespace == -1)\n>        \t\ttrailing_whitespace = len;\n>      @@ ws.c: void ws_fix_copy(struct strbuf *dst, const char *src, int len, unsigned ws\n>        \t/*\n>        \t * Strip trailing whitespace\n>        \t */\n>      -\n>      - ## ws.h ##\n>      -@@ ws.h: struct strbuf;\n>      - #define WS_CR_AT_EOL            (1<<9)\n>      - #define WS_BLANK_AT_EOF         (1<<10)\n>      - #define WS_TAB_IN_INDENT        (1<<11)\n>      -+#define WS_INCOMPLETE_LINE      (1<<12)\n>      -\n>      - #define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)\n>      - #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)\n> 11:  36de2ac901 = 11:  17c2fa50a7 diff: highlight and error out on incomplete lines\n> 12:  e82056bf55 = 12:  73af29fba7 attr: enable incomplete-line whitespace error for this project\n\n"},{"id":"530444","messageId":"c41f3c65-d7ef-4e73-a1e0-03540df0b212@gmail.com","threadId":"64429","inReplyTo":"20251104020928.582199-5-gitster@pobox.com","subject":"Re: [PATCH 04/12] diff: fix incorrect counting of line numbers","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-10T14:54:23Z","receivedAt":"2025-11-10T14:54:26Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 04/11/2025 02:09, Junio C Hamano wrote:\n> The \"\\ No newline at the end of the file\" can come after any of the\n> \"-\" (deleted preimage line), \" \" (unchanged line), or \"+\" (added\n> postimage line).  Incrementing only the preimage line number upon\n> seeing it does not make any sense.\n> \n> We can keep track of what the previous line was, and increment\n> lno_in_{pre,post}image variables properly, like this patch does.  I\n> do not think it matters, as these numbers are used only to compare\n> them with blank_at_eof_in_{pre,post}image to issue the warning every\n> time we see an added line, but by definition, after we see \"\\ No\n> newline at the end of the file\" for an added line, we will not see\n> an added line for the file.\n> \n> Keeping track of what the last line was (in other words, \"is it that\n> the file used to end in an incomplete line?  The file ends in an\n> incomplete line after the change?  Both the file before and after\n> the change ends in an incomplete line that did not change?\") will be\n> independently useful.\n\nThe \"\\ No newline at end of file\" line is an annotation on the previous \nline in the diff so why are we incrementing any {pre,post}image line \nnumbers here?\n\nThanks\n\nPhillip\n\n> Signed-off-by: Junio C Hamano <gitster@pobox.com>\n> ---\n>   diff.c | 18 +++++++++++++++++-\n>   1 file changed, 17 insertions(+), 1 deletion(-)\n> \n> diff --git a/diff.c b/diff.c\n> index b9ef8550cc..e73320dfb1 100644\n> --- a/diff.c\n> +++ b/diff.c\n> @@ -601,6 +601,7 @@ struct emit_callback {\n>   \tint blank_at_eof_in_postimage;\n>   \tint lno_in_preimage;\n>   \tint lno_in_postimage;\n> +\tint last_line_kind;\n>   \tconst char **label_path;\n>   \tstruct diff_words_data *diff_words;\n>   \tstruct diff_options *opt;\n> @@ -2426,13 +2427,28 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n>   \t\tbreak;\n>   \tcase '\\\\':\n>   \t\t/* incomplete line at the end */\n> -\t\tecbdata->lno_in_preimage++;\n> +\t\tswitch (ecbdata->last_line_kind) {\n> +\t\tcase '+':\n> +\t\t\tecbdata->lno_in_postimage++;\n> +\t\t\tbreak;\n> +\t\tcase '-':\n> +\t\t\tecbdata->lno_in_preimage++;\n> +\t\t\tbreak;\n> +\t\tcase ' ':\n> +\t\t\tecbdata->lno_in_preimage++;\n> +\t\t\tecbdata->lno_in_postimage++;\n> +\t\t\tbreak;\n> +\t\tdefault:\n> +\t\t\tBUG(\"fn_out_consume: '\\\\No newline' after unknown line (%c)\",\n> +\t\t\t    ecbdata->last_line_kind);\n> +\t\t}\n>   \t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n>   \t\t\t\t line, len, 0);\n>   \t\tbreak;\n>   \tdefault:\n>   \t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n>   \t}\n> +\tecbdata->last_line_kind = line[0];\n>   \treturn 0;\n>   }\n>   \n\n"},{"id":"530445","messageId":"0b0181af-e63d-4e5a-9e3c-822f866df88d@gmail.com","threadId":"64429","inReplyTo":"20251104020928.582199-8-gitster@pobox.com","subject":"Re: [PATCH 07/12] diff: update the way rewrite diff handles incomplete lines","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-10T14:54:45Z","receivedAt":"2025-11-10T14:54:49Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 04/11/2025 02:09, Junio C Hamano wrote:\n> \n> Revamp the way the complete-rewrite code path feeds the lines to the\n> output layer by treating the last line of the pre/post image when it\n> is an incomplete line specially.\n> \n> This lets us remove the DIFF_SYMBOL_NO_LF_EOF hack and use the usual\n> DIFF_SYMBOL_CONTEXT_INCOMPLETE code path, which will later learn how\n> to handle whitespace errors.\n\nThis is a nice cleanup\n\n> @@ -1786,22 +1777,36 @@ static void emit_rewrite_lines(struct emit_callback *ecbdata,\n>   \tconst char *endp = NULL;\n>   \n>   \twhile (0 < size) {\n> -\t\tint len;\n> +\t\tint len, plen;\n> +\t\tchar *pdata = NULL;\n>   \n>   \t\tendp = memchr(data, '\\n', size);\n>   \t\tlen = endp ? (endp - data + 1) : size;\n> +\t\tplen = len;\n> +\n> +\t\tif (!endp) {\n> +\t\t\tplen = len + 1;\n> +\t\t\tpdata = xmalloc(plen + 2);\n> +\t\t\tmemcpy(pdata, data, len);\n> +\t\t\tpdata[len] = '\\n';\n> +\t\t\tpdata[len + 1] = '\\0';\n> +\t\t}\n\nI think it would be clearer to refactor this as\n\n  \t\tendp = memchr(data, '\\n', size);\n-\t\tlen = endp ? (endp - data + 1) : size;\n\t\tif (endp) {\n\t\t\tlen = endp - data + 1;\n\t\t\tplen = len;\n\t\t} else {\n\t\t\tlen = size;\n+\t\t\tplen = len + 1;\n+\t\t\tpdata = xmalloc(plen + 2);\n+\t\t\tmemcpy(pdata, data, len);\n+\t\t\tpdata[len] = '\\n';\n+\t\t\tpdata[len + 1] = '\\0';\n+\t\t}\n\nThanks\n\nPhillip\n>   \t\tif (prefix != '+') {\n>   \t\t\tecbdata->lno_in_preimage++;\n> -\t\t\temit_del_line(ecbdata, data, len);\n> +\t\t\temit_del_line(ecbdata, pdata ? pdata : data, plen);\n>   \t\t} else {\n>   \t\t\tecbdata->lno_in_postimage++;\n> -\t\t\temit_add_line(ecbdata, data, len);\n> +\t\t\temit_add_line(ecbdata, pdata ? pdata : data, plen);\n>   \t\t}\n> +\t\tfree(pdata);\n>   \t\tsize -= len;\n>   \t\tdata += len;\n>   \t}\n> -\tif (!endp)\n> -\t\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);\n> +\tif (!endp) {\n> +\t\tstatic const char nneof[] = \"\\\\ No newline at end of file\\n\";\n> +\t\tecbdata->last_line_kind = prefix;\n> +\t\temit_incomplete_line(ecbdata, nneof, sizeof(nneof) - 1);\n> +\t}\n>   }\n>   \n>   static void emit_rewrite_diff(const char *name_a,\n\n"},{"id":"530446","messageId":"7aa91693-bece-4fa6-ab14-f914d6fd49bd@gmail.com","threadId":"64429","inReplyTo":"20251104020928.582199-12-gitster@pobox.com","subject":"Re: [PATCH 11/12] diff: highlight and error out on incomplete lines","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-10T14:55:11Z","receivedAt":"2025-11-10T14:55:14Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 04/11/2025 02:09, Junio C Hamano wrote:\n> Teach \"git diff\" to highlight \"\\ No newline at end of file\" message\n> as a whitespace error when incomplete-line whitespace error class is\n> in effect.  Thanks to the previous refactoring of complete rewrite\n> code path, we can do this at a single place.\n> \n> Unlike whitespace errors in the payload where we need to annotate in\n> line, possibly using colors, the line that has whitespace problems,\n> we have a dedicated line already that can serve as the error\n> message, so paint it as a whitespace error message.\n\nThis explains why we don't need to call emit_line_ws_markup() in this case\n\n> Also teach \"git diff --check\" to notice incomplete lines as\n> whitespace errors and report when incomplete-line whitespace error\n> class is in effect.\n\nNice. The implementation looks good, I've left a few comments on the tests\n> diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh\n> index 9de7f73f42..138730cbce 100755\n> --- a/t/t4015-diff-whitespace.sh\n> +++ b/t/t4015-diff-whitespace.sh\n> @@ -43,6 +43,49 @@ do\n>   \t'\n>   done\n>   \n> +test_expect_success \"incomplete line in both pre- and post-image context\" '\n> +\t(echo foo && echo baz | tr -d \"\\012\") >x &&\n\n'printf \"foo\\nbaz\"' might be clearer and save us forking \"tr\"\n\n> +\tgit add x &&\n> +\t(echo bar && echo baz | tr -d \"\\012\") >x &&\n> +\tgit diff x &&\n> +\tgit -c core.whitespace=incomplete diff --check x &&\n> +\tgit diff -R x &&\n> +\tgit -c core.whitespace=incomplete diff -R --check x\n> +'\n> +\n> +test_expect_success \"incomplete lines on both pre- and post-image\" '\n> +\t# The interpretation taken here is \"since you are toucing\n\ns/toucing/touching/\n\n> +\t# the line anyway, you would better fix the incomplete line\n> +\t# while you are at it.\"  but this is debatable.\n\nI think it is a reasonable default.\n> +\techo foo | tr -d \"\\012\" >x &&\n> +\tgit add x &&\n> +\techo bar | tr -d \"\\012\" >x &&\n> +\tgit diff x &&\n> +\ttest_must_fail git -c core.whitespace=incomplete diff --check x &&\n\nDo we want to check the error message here?\n\nLooking at the tests below the coverage looks good for \"diff --check\" \nand for diff.wsErrorHighlight\n\nThanks\n\nPhillip\n\n> +\tgit diff -R x &&\n> +\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x\n> +'\n> +\n> +test_expect_success \"fix incomplete line in pre-image\" '\n> +\techo foo | tr -d \"\\012\" >x &&\n> +\tgit add x &&\n> +\techo bar >x &&\n> +\tgit diff x &&\n> +\tgit -c core.whitespace=incomplete diff --check x &&\n> +\tgit diff -R x &&\n> +\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x\n> +'\n> +\n> +test_expect_success \"new incomplete line in post-image\" '\n> +\techo foo >x &&\n> +\tgit add x &&\n> +\techo bar | tr -d \"\\012\" >x &&\n> +\tgit diff x &&\n> +\ttest_must_fail git -c core.whitespace=incomplete diff --check x &&\n> +\tgit diff -R x &&\n> +\tgit -c core.whitespace=incomplete diff -R --check x\n> +'\n> +\n>   test_expect_success \"Ray Lehtiniemi's example\" '\n>   \tcat <<-\\EOF >x &&\n>   \tdo {\n> @@ -1040,7 +1083,8 @@ test_expect_success 'ws-error-highlight test setup' '\n>   \t{\n>   \t\techo \"0. blank-at-eol \" &&\n>   \t\techo \"1. still-blank-at-eol \" &&\n> -\t\techo \"2. and a new line \"\n> +\t\techo \"2. and a new line \" &&\n> +\t\tprintf \"3. and more\"\n>   \t} >x &&\n>   \tnew_hash_x=$(git hash-object x) &&\n>   \tafter=$(git rev-parse --short \"$new_hash_x\") &&\n> @@ -1050,11 +1094,13 @@ test_expect_success 'ws-error-highlight test setup' '\n>   \t<BOLD>index $before..$after 100644<RESET>\n>   \t<BOLD>--- a/x<RESET>\n>   \t<BOLD>+++ b/x<RESET>\n> -\t<CYAN>@@ -1,2 +1,3 @@<RESET>\n> +\t<CYAN>@@ -1,2 +1,4 @@<RESET>\n>   \t 0. blank-at-eol <RESET>\n>   \t<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>\n>   \t<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>\n>   \t<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>\n> +\t<GREEN>+<RESET><GREEN>3. and more<RESET>\n> +\t<BLUE>\\ No newline at end of file<RESET>\n>   \tEOF\n>   \n>   \tcat >expect.all <<-EOF &&\n> @@ -1062,11 +1108,13 @@ test_expect_success 'ws-error-highlight test setup' '\n>   \t<BOLD>index $before..$after 100644<RESET>\n>   \t<BOLD>--- a/x<RESET>\n>   \t<BOLD>+++ b/x<RESET>\n> -\t<CYAN>@@ -1,2 +1,3 @@<RESET>\n> +\t<CYAN>@@ -1,2 +1,4 @@<RESET>\n>   \t <RESET>0. blank-at-eol<RESET><BLUE> <RESET>\n>   \t<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>\n>   \t<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>\n>   \t<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>\n> +\t<GREEN>+<RESET><GREEN>3. and more<RESET>\n> +\t<BLUE>\\ No newline at end of file<RESET>\n>   \tEOF\n>   \n>   \tcat >expect.none <<-EOF\n> @@ -1074,16 +1122,19 @@ test_expect_success 'ws-error-highlight test setup' '\n>   \t<BOLD>index $before..$after 100644<RESET>\n>   \t<BOLD>--- a/x<RESET>\n>   \t<BOLD>+++ b/x<RESET>\n> -\t<CYAN>@@ -1,2 +1,3 @@<RESET>\n> +\t<CYAN>@@ -1,2 +1,4 @@<RESET>\n>   \t 0. blank-at-eol <RESET>\n>   \t<RED>-1. blank-at-eol <RESET>\n>   \t<GREEN>+1. still-blank-at-eol <RESET>\n>   \t<GREEN>+2. and a new line <RESET>\n> +\t<GREEN>+3. and more<RESET>\n> +\t\\ No newline at end of file<RESET>\n>   \tEOF\n>   \n>   '\n>   \n>   test_expect_success 'test --ws-error-highlight option' '\n> +\tgit config core.whitespace blank-at-eol,incomplete-line &&\n>   \n>   \tgit diff --color --ws-error-highlight=default,old >current.raw &&\n>   \ttest_decode_color <current.raw >current &&\n> @@ -1100,6 +1151,7 @@ test_expect_success 'test --ws-error-highlight option' '\n>   '\n>   \n>   test_expect_success 'test diff.wsErrorHighlight config' '\n> +\tgit config core.whitespace blank-at-eol,incomplete-line &&\n>   \n>   \tgit -c diff.wsErrorHighlight=default,old diff --color >current.raw &&\n>   \ttest_decode_color <current.raw >current &&\n> @@ -1116,6 +1168,7 @@ test_expect_success 'test diff.wsErrorHighlight config' '\n>   '\n>   \n>   test_expect_success 'option overrides diff.wsErrorHighlight' '\n> +\tgit config core.whitespace blank-at-eol,incomplete-line &&\n>   \n>   \tgit -c diff.wsErrorHighlight=none \\\n>   \t\tdiff --color --ws-error-highlight=default,old >current.raw &&\n> @@ -1135,6 +1188,8 @@ test_expect_success 'option overrides diff.wsErrorHighlight' '\n>   '\n>   \n>   test_expect_success 'detect moved code, complete file' '\n> +\tgit config core.whitespace blank-at-eol &&\n> +\n>   \tgit reset --hard &&\n>   \tcat <<-\\EOF >test.c &&\n>   \t#include<stdio.h>\n\n"},{"id":"530447","messageId":"e14d2d1f-f147-49dc-897d-87f0fbeaa275@gmail.com","threadId":"64429","inReplyTo":"20251104020928.582199-13-gitster@pobox.com","subject":"Re: [PATCH 12/12] attr: enable incomplete-line whitespace error for this project","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-10T14:55:32Z","receivedAt":"2025-11-10T14:55:36Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 04/11/2025 02:09, Junio C Hamano wrote:\n> Now \"git diff --check\" and \"git apply --whitespace=warn/fix\" learned\n> incomplete line is a whitespace error, enable them for this project\n> to prevent patches to add new incomplete lines to our sources.\n\nMakes sense\n\n> -*.[ch] whitespace=indent,trail,space diff=cpp\n> -*.sh whitespace=indent,trail,space text eol=lf\n> +*.[ch] whitespace=indent,trail,space,incomplete diff=cpp\n> +*.sh whitespace=indent,trail,space,incomplete text eol=lf\n\nDo we want to check for incomplete lines in our documentation files as \nwell? This series does not update WS_DEFAULT_RULE to include \nWS_INCOMPLETE_LINE so we will not detect incomplete lines unless we set \nan attribute.\n\nThanks\n\nPhillip\n\n>   *.perl text eol=lf diff=perl\n>   *.pl text eof=lf diff=perl\n>   *.pm text eol=lf diff=perl\n\n\n"},{"id":"530457","messageId":"xmqqtsz122i8.fsf@gitster.g","threadId":"64429","inReplyTo":"aRG5KeoZg1Q1y8DE@pks.im","subject":"Re: [PATCH v2 05/12] diff: refactor output of incomplete line","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-10T17:58:39Z","receivedAt":"2025-11-10T17:58:41Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n>>  \tcase DIFF_SYMBOL_CONTEXT_INCOMPLETE:\n>> +\t\tset = diff_get_color_opt(o, DIFF_CONTEXT);\n>> +\t\treset = diff_get_color_opt(o, DIFF_RESET);\n>> +\t\temit_line(o, set, reset, line, len);\n>> +\t\tbreak;\n>>  \tcase DIFF_SYMBOL_CONTEXT_MARKER:\n>>  \t\tcontext = diff_get_color_opt(o, DIFF_CONTEXT);\n>>  \t\treset = diff_get_color_opt(o, DIFF_RESET);\n>\n> I found it a bit confusing that we use `set`/`reset` here instead of\n> `context`/`reset` as before. It doesn't make any difference as these are\n> local variables anyway, but it might make sense to explain why you chose\n> to use different variables.\n\nYup, when DIFF_SYMBOL_* stuff was introduced to this codebase, it\nmade the code much harder to reason about X-<, and set/reset/context\nare used in this switch() statement in apparently \"random\" ways; no\ncase arm in this switch statement use all three, so there is no need\nto use these three in the first place.\n\n> Honestly, this whole hunk is somewhat confusing in the first place. It\n> doesn't seem to connect with the description in any way, as it's a no-op\n> change and we don't even use the newly introduced function.\n\nThe hunk in fn_out_consume() is about reacting to \"\\ No newline\",\nand it calls a new helper function created just for it in the middle\nhunk.  The way that new helper function affects the output will be\nupdated in later steps, but for now, this hunk simply makes sure\nthat the two DIFF_SYMBOL_*s can be treated differently.\n\nPerhaps it would become easier to read if this hunk is removed from\nthis step and a later step that gives a different behaviour for\nCONTEXT_INCOMPLETE introduce an entirely different code afresh\n(instead of showing how it updates what we copy here from the\ncontext code the original is abusing)?\n"},{"id":"530459","messageId":"xmqqo6p921sj.fsf@gitster.g","threadId":"64429","inReplyTo":"aRG5NSvzeie_J3x9@pks.im","subject":"Re: [PATCH v2 07/12] diff: update the way rewrite diff handles incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-10T18:14:04Z","receivedAt":"2025-11-10T18:14:07Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Wed, Nov 05, 2025 at 01:30:47PM -0800, Junio C Hamano wrote:\n>> The diff_symbol based output framework uses one DIFF_SYMBOL_* enum\n>> value per the kind of output lines of \"git diff\", which corresponds\n>> to one output line from the xdiff machinery used internally.  Most\n>> notably, DIFF_SYMBOL_PLUS and DIFF_SYMBOL_MINUS that correspond to\n>> \"+\" and \"-\" lines are designed to always take a complete line, even\n>\n> \"complete line\" as in newline-terminated? I only recognized that this is\n> what you meant in the next paragraph, so it might be useful to clarify\n> here already what you mean.\n\nYes, \"incomplete line\" is a defined term people can look up in\nplaces like POSIX.1 [*] but I do not know of an official word to\nrefer to the opposite.  Would it work if I rephrase it to say\n\"...designed to always end in a newline character, even...\"?\n\n(https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap03.html#tag_03_172)\n\n>\n>> diff --git a/diff.c b/diff.c\n>> index 347cd9c6e9..99298720f4 100644\n>> --- a/diff.c\n>> +++ b/diff.c\n>> @@ -1786,22 +1777,36 @@ static void emit_rewrite_lines(struct emit_callback *ecbdata,\n>>  \tconst char *endp = NULL;\n>>  \n>>  \twhile (0 < size) {\n>> ...\n>>  \t}\n>> -\tif (!endp)\n>> -\t\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);\n>> +\tif (!endp) {\n>> +\t\tstatic const char nneof[] = \"\\\\ No newline at end of file\\n\";\n>> +\t\tecbdata->last_line_kind = prefix;\n>> +\t\temit_incomplete_line(ecbdata, nneof, sizeof(nneof) - 1);\n>> +\t}\n>>  }\n>\n> Okay. I was wondering at first how this would get executed for both\n> pre- and postimage if it's not part of the loop anymore. But this is\n> mostly showing my complete ignorance for the \"diff\" subsystem, as we end\n> up calling `emit_rewrite_lines()` itself once for each image.\n\nThe idea is to make a \"complete rewrite\" patch (i.e. what \"diff -B\"\ndecides that it is more confusing to express the postimage in terms\nof \"here are remaining pieces of the preimage, many lines around here\nwere removed from the preimage and then many new lines are inserted\"\nthan \"ok, we are removing everything in the preimage and then we are\nreplacing them with these lines to form the postimage\".\n\nThis function is called twice, once to show a bunch of \"-removed\"\nlines for the preimage side, and then again to show a bunch of\n\"+added\" lines for the postimage side.  The loop iterates over these\nlines in a single image, and at the end, the last line of the image,\nwhether it is the preimage or the postimage, may not end in a newline,\nin which case we need to append \"\\ No newline\" after it.\n\nI just realize that emit_incomplete_line() may be a misnomer.  It is\nnot used to show the last line in the pre/postimage that was\nincomplete.  The loop gives all lines, even the final incomplete\none, as if each of them ended in a newline.  What the helper\nfunction emit_incomplete_line() does is to show an additional \"by\nthe way, the previous line was an incomplete line\" marker after the\ncontents of the line gets shown.\n\nPerhaps call it emit_incomplete_line_mark() or something, and it\nwould make it easier to follow what is going on?\n"},{"id":"530461","messageId":"xmqqjyzx213a.fsf@gitster.g","threadId":"64429","inReplyTo":"c41f3c65-d7ef-4e73-a1e0-03540df0b212@gmail.com","subject":"Re: [PATCH 04/12] diff: fix incorrect counting of line numbers","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-10T18:29:13Z","receivedAt":"2025-11-10T18:29:16Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> On 04/11/2025 02:09, Junio C Hamano wrote:\n>> The \"\\ No newline at the end of the file\" can come after any of the\n>> \"-\" (deleted preimage line), \" \" (unchanged line), or \"+\" (added\n>> postimage line).  Incrementing only the preimage line number upon\n>> seeing it does not make any sense.\n>> \n>> We can keep track of what the previous line was, and increment\n>> lno_in_{pre,post}image variables properly, like this patch does.  I\n>> do not think it matters, as these numbers are used only to compare\n>> them with blank_at_eof_in_{pre,post}image to issue the warning every\n>> time we see an added line, but by definition, after we see \"\\ No\n>> newline at the end of the file\" for an added line, we will not see\n>> an added line for the file.\n>> \n>> Keeping track of what the last line was (in other words, \"is it that\n>> the file used to end in an incomplete line?  The file ends in an\n>> incomplete line after the change?  Both the file before and after\n>> the change ends in an incomplete line that did not change?\") will be\n>> independently useful.\n>\n> The \"\\ No newline at end of file\" line is an annotation on the previous \n> line in the diff so why are we incrementing any {pre,post}image line \n> numbers here?\n\nNo particular reason ;-)  As I said, I do not think these numbers\nare used after these lines are seen.  At least this change makes\nthese unused data incremented in a more coherent way than the\nprevious one, which unconditionally incremented the number for the\npreimage without even checking which side the \"\\ No newline\" is for.\n\n> Thanks\n>\n> Phillip\n>\n>> Signed-off-by: Junio C Hamano <gitster@pobox.com>\n>> ---\n>>   diff.c | 18 +++++++++++++++++-\n>>   1 file changed, 17 insertions(+), 1 deletion(-)\n>> \n>> diff --git a/diff.c b/diff.c\n>> index b9ef8550cc..e73320dfb1 100644\n>> --- a/diff.c\n>> +++ b/diff.c\n>> @@ -601,6 +601,7 @@ struct emit_callback {\n>>   \tint blank_at_eof_in_postimage;\n>>   \tint lno_in_preimage;\n>>   \tint lno_in_postimage;\n>> +\tint last_line_kind;\n>>   \tconst char **label_path;\n>>   \tstruct diff_words_data *diff_words;\n>>   \tstruct diff_options *opt;\n>> @@ -2426,13 +2427,28 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n>>   \t\tbreak;\n>>   \tcase '\\\\':\n>>   \t\t/* incomplete line at the end */\n>> -\t\tecbdata->lno_in_preimage++;\n>> +\t\tswitch (ecbdata->last_line_kind) {\n>> +\t\tcase '+':\n>> +\t\t\tecbdata->lno_in_postimage++;\n>> +\t\t\tbreak;\n>> +\t\tcase '-':\n>> +\t\t\tecbdata->lno_in_preimage++;\n>> +\t\t\tbreak;\n>> +\t\tcase ' ':\n>> +\t\t\tecbdata->lno_in_preimage++;\n>> +\t\t\tecbdata->lno_in_postimage++;\n>> +\t\t\tbreak;\n>> +\t\tdefault:\n>> +\t\t\tBUG(\"fn_out_consume: '\\\\No newline' after unknown line (%c)\",\n>> +\t\t\t    ecbdata->last_line_kind);\n>> +\t\t}\n>>   \t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n>>   \t\t\t\t line, len, 0);\n>>   \t\tbreak;\n>>   \tdefault:\n>>   \t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n>>   \t}\n>> +\tecbdata->last_line_kind = line[0];\n>>   \treturn 0;\n>>   }\n>>   \n"},{"id":"530462","messageId":"xmqqfral20wl.fsf@gitster.g","threadId":"64429","inReplyTo":"0b0181af-e63d-4e5a-9e3c-822f866df88d@gmail.com","subject":"Re: [PATCH 07/12] diff: update the way rewrite diff handles incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-10T18:33:14Z","receivedAt":"2025-11-10T18:33:17Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n>>   \t\tendp = memchr(data, '\\n', size);\n>>   \t\tlen = endp ? (endp - data + 1) : size;\n>> +\t\tplen = len;\n>> +\n>> +\t\tif (!endp) {\n>> +\t\t\tplen = len + 1;\n>> +\t\t\tpdata = xmalloc(plen + 2);\n>> +\t\t\tmemcpy(pdata, data, len);\n>> +\t\t\tpdata[len] = '\\n';\n>> +\t\t\tpdata[len + 1] = '\\0';\n>> +\t\t}\n>\n> I think it would be clearer to refactor this as\n>\n>   \t\tendp = memchr(data, '\\n', size);\n> -\t\tlen = endp ? (endp - data + 1) : size;\n> \t\tif (endp) {\n> \t\t\tlen = endp - data + 1;\n> \t\t\tplen = len;\n> \t\t} else {\n> \t\t\tlen = size;\n> +\t\t\tplen = len + 1;\n> +\t\t\tpdata = xmalloc(plen + 2);\n> +\t\t\tmemcpy(pdata, data, len);\n> +\t\t\tpdata[len] = '\\n';\n> +\t\t\tpdata[len + 1] = '\\0';\n> +\t\t}\n\nPerhaps.  I'll try and see if I agree.\n\nThanks.\n"},{"id":"530463","messageId":"xmqq8qgd20no.fsf@gitster.g","threadId":"64429","inReplyTo":"7aa91693-bece-4fa6-ab14-f914d6fd49bd@gmail.com","subject":"Re: [PATCH 11/12] diff: highlight and error out on incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-10T18:38:35Z","receivedAt":"2025-11-10T18:38:37Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> On 04/11/2025 02:09, Junio C Hamano wrote:\n>> Teach \"git diff\" to highlight \"\\ No newline at end of file\" message\n>> as a whitespace error when incomplete-line whitespace error class is\n>> in effect.  Thanks to the previous refactoring of complete rewrite\n>> code path, we can do this at a single place.\n>> \n>> Unlike whitespace errors in the payload where we need to annotate in\n>> line, possibly using colors, the line that has whitespace problems,\n>> we have a dedicated line already that can serve as the error\n>> message, so paint it as a whitespace error message.\n>\n> This explains why we don't need to call emit_line_ws_markup() in this case\n\nTrue.\n\nAlso, even if we were to call it on the previous, problematic line\nwithout terminating newline, there is no good spot on that line to\npaint red to grab attention to the reader, as we are trying to\nhighlight lack of something, not presence of unwanted things, like\ntrailing whitespaces ;-)\n\n>> +test_expect_success \"incomplete line in both pre- and post-image context\" '\n>> +\t(echo foo && echo baz | tr -d \"\\012\") >x &&\n>\n> 'printf \"foo\\nbaz\"' might be clearer and save us forking \"tr\"\n\nPerhaps.  I find it much harder to read and uglier, though.\n\n>> +\tgit add x &&\n>> +\t(echo bar && echo baz | tr -d \"\\012\") >x &&\n>> +\tgit diff x &&\n>> +\tgit -c core.whitespace=incomplete diff --check x &&\n>> +\tgit diff -R x &&\n>> +\tgit -c core.whitespace=incomplete diff -R --check x\n>> +'\n>> +\n>> +test_expect_success \"incomplete lines on both pre- and post-image\" '\n>> +\t# The interpretation taken here is \"since you are toucing\n>\n> s/toucing/touching/\n\nThanks.\n\n>\n>> +\t# the line anyway, you would better fix the incomplete line\n>> +\t# while you are at it.\"  but this is debatable.\n>\n> I think it is a reasonable default.\n>> +\techo foo | tr -d \"\\012\" >x &&\n>> +\tgit add x &&\n>> +\techo bar | tr -d \"\\012\" >x &&\n>> +\tgit diff x &&\n>> +\ttest_must_fail git -c core.whitespace=incomplete diff --check x &&\n>\n> Do we want to check the error message here?\n\nProbably an overkill, but I could try.\n\n> Looking at the tests below the coverage looks good for \"diff --check\" \n> and for diff.wsErrorHighlight\n"},{"id":"530464","messageId":"xmqq4ir120kd.fsf@gitster.g","threadId":"64429","inReplyTo":"e14d2d1f-f147-49dc-897d-87f0fbeaa275@gmail.com","subject":"Re: [PATCH 12/12] attr: enable incomplete-line whitespace error for this project","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-10T18:40:34Z","receivedAt":"2025-11-10T18:40:37Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> On 04/11/2025 02:09, Junio C Hamano wrote:\n>> Now \"git diff --check\" and \"git apply --whitespace=warn/fix\" learned\n>> incomplete line is a whitespace error, enable them for this project\n>> to prevent patches to add new incomplete lines to our sources.\n>\n> Makes sense\n>\n>> -*.[ch] whitespace=indent,trail,space diff=cpp\n>> -*.sh whitespace=indent,trail,space text eol=lf\n>> +*.[ch] whitespace=indent,trail,space,incomplete diff=cpp\n>> +*.sh whitespace=indent,trail,space,incomplete text eol=lf\n>\n> Do we want to check for incomplete lines in our documentation files as \n> well?\n> ...\n> This series does not update WS_DEFAULT_RULE to include \n> WS_INCOMPLETE_LINE so we will not detect incomplete lines unless we set \n> an attribute.\n\nYes, that is why I said \"to our sources\".  The rest is left for\nsomebody else to do, hopefully long after this series settles ;-)\n\nThanks.\n"},{"id":"530479","messageId":"CALnO6CBFmzhDA98N+AvezqSwaDmBsYvq4R4+1iko4c1RkYcwfw@mail.gmail.com","threadId":"64429","inReplyTo":"xmqq8qgd20no.fsf@gitster.g","subject":"Re: [PATCH 11/12] diff: highlight and error out on incomplete lines","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-11-10T23:56:53Z","receivedAt":"2025-11-10T23:57:05Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Mon, Nov 10, 2025 at 1:38 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n>\n> > On 04/11/2025 02:09, Junio C Hamano wrote:\n> >> +test_expect_success \"incomplete line in both pre- and post-image context\" '\n> >> +    (echo foo && echo baz | tr -d \"\\012\") >x &&\n> >\n> > 'printf \"foo\\nbaz\"' might be clearer and save us forking \"tr\"\n>\n> Perhaps.  I find it much harder to read and uglier, though.\n\nFunny, I find it easier to reason about.\n\nIt saves not only the tr fork but the subshell, too.\n\n-- \nD. Ben Knoble\n"},{"id":"530481","messageId":"20251111000451.2243195-1-gitster@pobox.com","threadId":"64429","inReplyTo":"20251105213052.1499224-1-gitster@pobox.com","subject":"[PATCH v3 00/12] Incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-11T00:04:39Z","receivedAt":"2025-11-11T00:04:54Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"One of the common kind of whitespace errors is to lack the final\nnewline at the end of a file, but so far, neither \"git diff\" or \"git\napply\" did anything about them.\n\nThis series introduces \"incomplete-line\" whitespace error class,\nthat you can add to either the core.whitespace configuration\nvariable, or the whitespace attribute in your .gitattributes files.\n\nThe class is disabled by default, so the final step enables it for\nour project by defining it in the .gitattributes file.\n\nThe incomplete line marker that is given for a context line is not\nconsidered an error.  The reasoning is that your preimage did have\nincomplete line, but you did not touch the contents on that\nincomplete line in your patch, so you left the line intact.  It is\nnot a new breakage you are responsible for.\n\nIf the incomplete line marker follows a postimage line, on the other\nhand, it means that you added a new line at the end of the file that\nis incomplete *and* that line did not exist in the preimage.  The\nlast line of the preimage may have been incomplete already, but then\nyou updated the contents on that line, so you could have easily\nfixed the incompleteness of the line while at it.  Either way, you\nare responsible for the incompleteness of the last ine in the\nresulting file.\n\nThe organization of the series is as follows.\n\n * The first patch [01/12] is a clean-up we have seen earlier on the\n   list already (https://lore.kernel.org/git/xmqqfrb4hyjl.fsf@gitster.g/).\n\n * The patches [02/12] - [08/12] are preliminary clean-up made to\n   both \"git diff\" and \"git apply\" machinery.\n\n * The patch [09/12] shifts the bit assignment (cleaned-up in\n   [01/12] without changing any values) to make room for new\n   whitespace error class (which was last updated in 2007 IIRC, so\n   the set of whitespace errors surprisingly haven't changed for\n   quite some time), and defines the new \"incomplete-line\" class.\n\n * The patch [10/12] teaches \"git apply --whitespace=<mode>\" and\n   \"git apply --check\" about the incomplete-line error class.\n\n * The patch [11/12] teaches \"git diff [--check]\" about the\n   incomplete-line error class.\n\n * The final patch [12/12] enables the incomplete-line error class\n   for our project for C source files and shell scripts.  I didn't\n   touch the cover-all * entry.\n\n\nChanges in v3:\n\n - The proposed log message of [PATCH 05/12] explains that it\n   semi-duplicates the same code shared in two case arems in\n   preparation for later changes.\n\n - The internal helper function to emit the \"\\ No newline\" marker\n   line is now called emit_incomplete_line_marker().\n\n - Two conditionals in [PATCH 07/12] both of which switched on !endp\n   have been consolidated into a single if/else statement.\n\n - The tests in [PATCH 11/12] checks the output from \"diff --check\"\n   now.\n\n\nChanges in v2:\n\n - rolled the definition (but not implementation) of the new\n   \"incomplete-line\" class into step [09/12] that shifts the bit\n   assignment.  The documentation of core.whitespace has also be\n   updated in this step.\n\n - \"git apply --check\" miscounted line number reported for the\n   incomplete line error, which has been corrected in step [10/12].\n\n - t4124-apply-ws-rule.sh has been extended to cover \"git apply\n   --check\" and the diagnostic output from it in step [10/12].\n\nJunio C Hamano (12):\n  whitespace: correct bit assignment comments\n  diff: emit_line_ws_markup() if/else style fix\n  diff: correct suppress_blank_empty hack\n  diff: fix incorrect counting of line numbers\n  diff: refactor output of incomplete line\n  diff: call emit_callback ecbdata everywhere\n  diff: update the way rewrite diff handles incomplete lines\n  apply: revamp the parsing of incomplete lines\n  whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE\n  apply: check and fix incomplete lines\n  diff: highlight and error out on incomplete lines\n  attr: enable incomplete-line whitespace error for this project\n\n .gitattributes                 |   4 +-\n Documentation/config/core.adoc |   2 +\n apply.c                        |  79 ++++++++++----\n diff.c                         | 152 ++++++++++++++++++---------\n diff.h                         |   6 +-\n t/t4015-diff-whitespace.sh     |  67 +++++++++++-\n t/t4124-apply-ws-rule.sh       | 187 +++++++++++++++++++++++++++++++++\n ws.c                           |  20 ++++\n ws.h                           |  26 +++--\n 9 files changed, 455 insertions(+), 88 deletions(-)\n\nRange-diff against v2:\n 1:  c045e93ce5 =  1:  c045e93ce5 whitespace: correct bit assignment comments\n 2:  0d95d68fb4 =  2:  0d95d68fb4 diff: emit_line_ws_markup() if/else style fix\n 3:  c331218334 =  3:  c331218334 diff: correct suppress_blank_empty hack\n 4:  be1473fc5a =  4:  be1473fc5a diff: fix incorrect counting of line numbers\n 5:  7bcd6efba8 !  5:  9410e4257a diff: refactor output of incomplete line\n    @@ Commit message\n         the code path that handles xdiff output and the code path that\n         bypasses xdiff and produces complete rewrite patch.\n     \n    +    Currently the output from the DIFF_SYMBOL_CONTEXT_INCOMPLETE case\n    +    still (ab)uses the same code as what is used for context lines, but\n    +    that would change in a later step where we introduce support for\n    +    incomplete line detection.\n    +\n         Signed-off-by: Junio C Hamano <gitster@pobox.com>\n     \n      ## diff.c ##\n    @@ diff.c: static void emit_context_line(struct emit_callback *ecbdata,\n      \temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT, line, len, flags);\n      }\n      \n    -+static void emit_incomplete_line(struct emit_callback *ecbdata,\n    -+\t\t\t\t const char *line, int len)\n    ++static void emit_incomplete_line_marker(struct emit_callback *ecbdata,\n    ++\t\t\t\t\tconst char *line, int len)\n     +{\n     +\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n     +\t\t\t line, len, 0);\n    @@ diff.c: static int fn_out_consume(void *priv, char *line, unsigned long len)\n      \t\t}\n     -\t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n     -\t\t\t\t line, len, 0);\n    -+\t\temit_incomplete_line(ecbdata, line, len);\n    ++\t\temit_incomplete_line_marker(ecbdata, line, len);\n      \t\tbreak;\n      \tdefault:\n      \t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n 6:  1a6f143377 =  6:  cdc6516009 diff: call emit_callback ecbdata everywhere\n 7:  dfc810b1d6 !  7:  9acb9b6217 diff: update the way rewrite diff handles incomplete lines\n    @@ diff.c: static void emit_rewrite_lines(struct emit_callback *ecbdata,\n     +\t\tchar *pdata = NULL;\n      \n      \t\tendp = memchr(data, '\\n', size);\n    - \t\tlen = endp ? (endp - data + 1) : size;\n    -+\t\tplen = len;\n    +-\t\tlen = endp ? (endp - data + 1) : size;\n     +\n    -+\t\tif (!endp) {\n    ++\t\tif (endp) {\n    ++\t\t\tlen = endp - data + 1;\n    ++\t\t\tplen = len;\n    ++\t\t} else {\n    ++\t\t\tlen = size;\n     +\t\t\tplen = len + 1;\n     +\t\t\tpdata = xmalloc(plen + 2);\n     +\t\t\tmemcpy(pdata, data, len);\n    @@ diff.c: static void emit_rewrite_lines(struct emit_callback *ecbdata,\n     +\tif (!endp) {\n     +\t\tstatic const char nneof[] = \"\\\\ No newline at end of file\\n\";\n     +\t\tecbdata->last_line_kind = prefix;\n    -+\t\temit_incomplete_line(ecbdata, nneof, sizeof(nneof) - 1);\n    ++\t\temit_incomplete_line_marker(ecbdata, nneof, sizeof(nneof) - 1);\n     +\t}\n      }\n      \n 8:  c66b547f13 =  8:  86c14ee62d apply: revamp the parsing of incomplete lines\n 9:  bdc2dbbe4b =  9:  b62d4020e7 whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE\n10:  806aa30511 = 10:  081c21b14e apply: check and fix incomplete lines\n11:  0cfb6ab295 ! 11:  73182b19a8 diff: highlight and error out on incomplete lines\n    @@ diff.c: static void emit_diff_symbol_from_struct(struct diff_options *o,\n      \t\temit_line(o, set, reset, line, len);\n      \t\tbreak;\n     @@ diff.c: static void emit_context_line(struct emit_callback *ecbdata,\n    - static void emit_incomplete_line(struct emit_callback *ecbdata,\n    - \t\t\t\t const char *line, int len)\n    + static void emit_incomplete_line_marker(struct emit_callback *ecbdata,\n    + \t\t\t\t\tconst char *line, int len)\n      {\n     +\tint last_line_kind = ecbdata->last_line_kind;\n     +\tunsigned flags = (last_line_kind == '+'\n    @@ t/t4015-diff-whitespace.sh: do\n     +'\n     +\n     +test_expect_success \"incomplete lines on both pre- and post-image\" '\n    -+\t# The interpretation taken here is \"since you are toucing\n    ++\t# The interpretation taken here is \"since you are touching\n     +\t# the line anyway, you would better fix the incomplete line\n     +\t# while you are at it.\"  but this is debatable.\n     +\techo foo | tr -d \"\\012\" >x &&\n     +\tgit add x &&\n     +\techo bar | tr -d \"\\012\" >x &&\n     +\tgit diff x &&\n    -+\ttest_must_fail git -c core.whitespace=incomplete diff --check x &&\n    ++\ttest_must_fail git -c core.whitespace=incomplete diff --check x >error &&\n    ++\ttest_grep \"no newline at the end of file\" error &&\n     +\tgit diff -R x &&\n    -+\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x\n    ++\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&\n    ++\ttest_grep \"no newline at the end of file\" error\n     +'\n     +\n     +test_expect_success \"fix incomplete line in pre-image\" '\n    @@ t/t4015-diff-whitespace.sh: do\n     +\tgit diff x &&\n     +\tgit -c core.whitespace=incomplete diff --check x &&\n     +\tgit diff -R x &&\n    -+\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x\n    ++\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&\n    ++\ttest_grep \"no newline at the end of file\" error\n     +'\n     +\n     +test_expect_success \"new incomplete line in post-image\" '\n    @@ t/t4015-diff-whitespace.sh: do\n     +\tgit add x &&\n     +\techo bar | tr -d \"\\012\" >x &&\n     +\tgit diff x &&\n    -+\ttest_must_fail git -c core.whitespace=incomplete diff --check x &&\n    ++\ttest_must_fail git -c core.whitespace=incomplete diff --check x >error &&\n    ++\ttest_grep \"no newline at the end of file\" error &&\n     +\tgit diff -R x &&\n     +\tgit -c core.whitespace=incomplete diff -R --check x\n     +'\n12:  33c5ae40db = 12:  85748701b4 attr: enable incomplete-line whitespace error for this project\n-- \n2.52.0-rc1-455-g30608eb744\n\n"},{"id":"530482","messageId":"20251111000451.2243195-2-gitster@pobox.com","threadId":"64429","inReplyTo":"20251111000451.2243195-1-gitster@pobox.com","subject":"[PATCH v3 01/12] whitespace: correct bit assignment comments","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-11T00:04:40Z","receivedAt":"2025-11-11T00:04:56Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"A comment in diff.c claimed that bits up to 12th (counting from 0th)\nare whitespace rules, and 13th thru 15th are for new/old/context,\nbut it turns out it was miscounting.  Correct them, and clarify\nwhere the whitespace rule bits come from in the comment.  Extend bit\nassignment comments to cover bits used for color-moved, which\nweren't described.\n\nAlso update the way these bit constants are defined to use (1 << N)\nnotation, instead of octal constants, as it tends to make it easier\nto notice a breakage like this.\n\nSprinkle a few blank lines between logically distinct groups of CPP\nmacro definitions to make them easier to read.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c |  7 +++++--\n diff.h |  6 +++---\n ws.h   | 25 ++++++++++++++-----------\n 3 files changed, 22 insertions(+), 16 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex a74e701806..74261b332a 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -801,16 +801,19 @@ enum diff_symbol {\n \tDIFF_SYMBOL_CONTEXT_MARKER,\n \tDIFF_SYMBOL_SEPARATOR\n };\n+\n /*\n  * Flags for content lines:\n- * 0..12 are whitespace rules\n- * 13-15 are WSEH_NEW | WSEH_OLD | WSEH_CONTEXT\n+ * 0..11 are whitespace rules (see ws.h)\n+ * 12..14 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD\n  * 16 is marking if the line is blank at EOF\n+ * 17..19 are used for color-moved.\n  */\n #define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF\t(1<<16)\n #define DIFF_SYMBOL_MOVED_LINE\t\t\t(1<<17)\n #define DIFF_SYMBOL_MOVED_LINE_ALT\t\t(1<<18)\n #define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING\t(1<<19)\n+\n #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)\n \n /*\ndiff --git a/diff.h b/diff.h\nindex 2fa256c3ef..cbd355cf50 100644\n--- a/diff.h\n+++ b/diff.h\n@@ -331,9 +331,9 @@ struct diff_options {\n \n \tint ita_invisible_in_index;\n /* white-space error highlighting */\n-#define WSEH_NEW (1<<12)\n-#define WSEH_CONTEXT (1<<13)\n-#define WSEH_OLD (1<<14)\n+#define WSEH_NEW        (1<<12)\n+#define WSEH_CONTEXT    (1<<13)\n+#define WSEH_OLD        (1<<14)\n \tunsigned ws_error_highlight;\n \tconst char *prefix;\n \tint prefix_length;\ndiff --git a/ws.h b/ws.h\nindex 5ba676c559..23708efb73 100644\n--- a/ws.h\n+++ b/ws.h\n@@ -7,19 +7,22 @@ struct strbuf;\n /*\n  * whitespace rules.\n  * used by both diff and apply\n- * last two digits are tab width\n+ * last two octal-digits are tab width (we support only up to 63).\n  */\n-#define WS_BLANK_AT_EOL         0100\n-#define WS_SPACE_BEFORE_TAB     0200\n-#define WS_INDENT_WITH_NON_TAB  0400\n-#define WS_CR_AT_EOL           01000\n-#define WS_BLANK_AT_EOF        02000\n-#define WS_TAB_IN_INDENT       04000\n-#define WS_TRAILING_SPACE      (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)\n+#define WS_BLANK_AT_EOL         (1<<6)\n+#define WS_SPACE_BEFORE_TAB     (1<<7)\n+#define WS_INDENT_WITH_NON_TAB  (1<<8)\n+#define WS_CR_AT_EOL            (1<<9)\n+#define WS_BLANK_AT_EOF         (1<<10)\n+#define WS_TAB_IN_INDENT        (1<<11)\n+\n+#define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)\n #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)\n-#define WS_TAB_WIDTH_MASK        077\n-/* All WS_* -- when extended, adapt diff.c emit_symbol */\n-#define WS_RULE_MASK           07777\n+#define WS_TAB_WIDTH_MASK       ((1<<6)-1)\n+\n+/* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */\n+#define WS_RULE_MASK            ((1<<12)-1)\n+\n extern unsigned whitespace_rule_cfg;\n unsigned whitespace_rule(struct index_state *, const char *);\n unsigned parse_whitespace_rule(const char *);\n-- \n2.52.0-rc1-455-g30608eb744\n\n"},{"id":"530483","messageId":"20251111000451.2243195-3-gitster@pobox.com","threadId":"64429","inReplyTo":"20251111000451.2243195-1-gitster@pobox.com","subject":"[PATCH v3 02/12] diff: emit_line_ws_markup() if/else style fix","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-11T00:04:41Z","receivedAt":"2025-11-11T00:04:58Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Apply the simple rule: if you need {} in one arm of the if/else\nif/else... cascade, have {} in all of them.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 74261b332a..9a24a0791c 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1327,14 +1327,14 @@ static void emit_line_ws_markup(struct diff_options *o,\n \t\t\tws = NULL;\n \t}\n \n-\tif (!ws && !set_sign)\n+\tif (!ws && !set_sign) {\n \t\temit_line_0(o, set, NULL, 0, reset, sign, line, len);\n-\telse if (!ws) {\n+\t} else if (!ws) {\n \t\temit_line_0(o, set_sign, set, !!set_sign, reset, sign, line, len);\n-\t} else if (blank_at_eof)\n+\t} else if (blank_at_eof) {\n \t\t/* Blank line at EOF - paint '+' as well */\n \t\temit_line_0(o, ws, NULL, 0, reset, sign, line, len);\n-\telse {\n+\t} else {\n \t\t/* Emit just the prefix, then the rest. */\n \t\temit_line_0(o, set_sign ? set_sign : set, NULL, !!set_sign, reset,\n \t\t\t    sign, \"\", 0);\n-- \n2.52.0-rc1-455-g30608eb744\n\n"},{"id":"530484","messageId":"20251111000451.2243195-4-gitster@pobox.com","threadId":"64429","inReplyTo":"20251111000451.2243195-1-gitster@pobox.com","subject":"[PATCH v3 03/12] diff: correct suppress_blank_empty hack","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-11T00:04:42Z","receivedAt":"2025-11-11T00:05:00Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The suppress-blank-empty feature abused the CONTEXT_INCOMPLETE\nsymbol that was meant to be used only for \"\\ No newline at the end\nof file\" code path.\n\nThe intent of the feature was to turn a context line we receive from\nxdiff machinery (which always uses ' ' for context lines, even an\nempty one) and spit it out as a truly empty line.\n\nPerform such a conversion very locally at where a line from xdiff\nthat begins with ' ' is handled for output; there are many checks\nbefore the control reaches such place that checks the first letter\nof the diff output line to see if it is a context line, and having\nto check for '\\n' and treat it as a special case is error prone.\n\nIn order to catch similar hacks in the future, make sure the code\npath that is meant for \"\\ No newline\" case checks the first byte is\nindeed a backslash.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 27 +++++++++++----------------\n 1 file changed, 11 insertions(+), 16 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 9a24a0791c..b9ef8550cc 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1321,6 +1321,11 @@ static void emit_line_ws_markup(struct diff_options *o,\n \tconst char *ws = NULL;\n \tint sign = o->output_indicators[sign_index];\n \n+\tif (diff_suppress_blank_empty &&\n+\t    sign_index == OUTPUT_INDICATOR_CONTEXT &&\n+\t    len == 1 && line[0] == '\\n')\n+\t\tsign = 0;\n+\n \tif (o->ws_error_highlight & ws_rule) {\n \t\tws = diff_get_color_opt(o, DIFF_WHITESPACE);\n \t\tif (!*ws)\n@@ -1498,15 +1503,9 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n \tcase DIFF_SYMBOL_WORDS:\n \t\tcontext = diff_get_color_opt(o, DIFF_CONTEXT);\n \t\treset = diff_get_color_opt(o, DIFF_RESET);\n-\t\t/*\n-\t\t * Skip the prefix character, if any.  With\n-\t\t * diff_suppress_blank_empty, there may be\n-\t\t * none.\n-\t\t */\n-\t\tif (line[0] != '\\n') {\n-\t\t\tline++;\n-\t\t\tlen--;\n-\t\t}\n+\n+\t\t/* Skip the prefix character */\n+\t\tline++; len--;\n \t\temit_line(o, context, reset, line, len);\n \t\tbreak;\n \tcase DIFF_SYMBOL_FILEPAIR_PLUS:\n@@ -2375,12 +2374,6 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \t\tecbdata->label_path[0] = ecbdata->label_path[1] = NULL;\n \t}\n \n-\tif (diff_suppress_blank_empty\n-\t    && len == 2 && line[0] == ' ' && line[1] == '\\n') {\n-\t\tline[0] = '\\n';\n-\t\tlen = 1;\n-\t}\n-\n \tif (line[0] == '@') {\n \t\tif (ecbdata->diff_words)\n \t\t\tdiff_words_flush(ecbdata);\n@@ -2431,12 +2424,14 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \t\tecbdata->lno_in_preimage++;\n \t\temit_context_line(ecbdata, line + 1, len - 1);\n \t\tbreak;\n-\tdefault:\n+\tcase '\\\\':\n \t\t/* incomplete line at the end */\n \t\tecbdata->lno_in_preimage++;\n \t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n \t\t\t\t line, len, 0);\n \t\tbreak;\n+\tdefault:\n+\t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n \t}\n \treturn 0;\n }\n-- \n2.52.0-rc1-455-g30608eb744\n\n"},{"id":"530485","messageId":"20251111000451.2243195-5-gitster@pobox.com","threadId":"64429","inReplyTo":"20251111000451.2243195-1-gitster@pobox.com","subject":"[PATCH v3 04/12] diff: fix incorrect counting of line numbers","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-11T00:04:43Z","receivedAt":"2025-11-11T00:05:01Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The \"\\ No newline at the end of the file\" can come after any of the\n\"-\" (deleted preimage line), \" \" (unchanged line), or \"+\" (added\npostimage line).  Incrementing only the preimage line number upon\nseeing it does not make any sense.\n\nWe can keep track of what the previous line was, and increment\nlno_in_{pre,post}image variables properly, like this patch does.  I\ndo not think it matters, as these numbers are used only to compare\nthem with blank_at_eof_in_{pre,post}image to issue the warning every\ntime we see an added line, but by definition, after we see \"\\ No\nnewline at the end of the file\" for an added line, we will not see\nan added line for the file.\n\nKeeping track of what the last line was (in other words, \"is it that\nthe file used to end in an incomplete line?  The file ends in an\nincomplete line after the change?  Both the file before and after\nthe change ends in an incomplete line that did not change?\") will be\nindependently useful.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 18 +++++++++++++++++-\n 1 file changed, 17 insertions(+), 1 deletion(-)\n\ndiff --git a/diff.c b/diff.c\nindex b9ef8550cc..e73320dfb1 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -601,6 +601,7 @@ struct emit_callback {\n \tint blank_at_eof_in_postimage;\n \tint lno_in_preimage;\n \tint lno_in_postimage;\n+\tint last_line_kind;\n \tconst char **label_path;\n \tstruct diff_words_data *diff_words;\n \tstruct diff_options *opt;\n@@ -2426,13 +2427,28 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \t\tbreak;\n \tcase '\\\\':\n \t\t/* incomplete line at the end */\n-\t\tecbdata->lno_in_preimage++;\n+\t\tswitch (ecbdata->last_line_kind) {\n+\t\tcase '+':\n+\t\t\tecbdata->lno_in_postimage++;\n+\t\t\tbreak;\n+\t\tcase '-':\n+\t\t\tecbdata->lno_in_preimage++;\n+\t\t\tbreak;\n+\t\tcase ' ':\n+\t\t\tecbdata->lno_in_preimage++;\n+\t\t\tecbdata->lno_in_postimage++;\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tBUG(\"fn_out_consume: '\\\\No newline' after unknown line (%c)\",\n+\t\t\t    ecbdata->last_line_kind);\n+\t\t}\n \t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n \t\t\t\t line, len, 0);\n \t\tbreak;\n \tdefault:\n \t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n \t}\n+\tecbdata->last_line_kind = line[0];\n \treturn 0;\n }\n \n-- \n2.52.0-rc1-455-g30608eb744\n\n"},{"id":"530486","messageId":"20251111000451.2243195-6-gitster@pobox.com","threadId":"64429","inReplyTo":"20251111000451.2243195-1-gitster@pobox.com","subject":"[PATCH v3 05/12] diff: refactor output of incomplete line","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-11T00:04:44Z","receivedAt":"2025-11-11T00:05:04Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Create a helper function that reacts to \"\\ No newline at the end of\nfile\" in preparation for unifying the incomplete line handling in\nthe code path that handles xdiff output and the code path that\nbypasses xdiff and produces complete rewrite patch.\n\nCurrently the output from the DIFF_SYMBOL_CONTEXT_INCOMPLETE case\nstill (ab)uses the same code as what is used for context lines, but\nthat would change in a later step where we introduce support for\nincomplete line detection.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 14 ++++++++++++--\n 1 file changed, 12 insertions(+), 2 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex e73320dfb1..8f1b4e6069 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1379,6 +1379,10 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n \t\temit_line(o, \"\", \"\", line, len);\n \t\tbreak;\n \tcase DIFF_SYMBOL_CONTEXT_INCOMPLETE:\n+\t\tset = diff_get_color_opt(o, DIFF_CONTEXT);\n+\t\treset = diff_get_color_opt(o, DIFF_RESET);\n+\t\temit_line(o, set, reset, line, len);\n+\t\tbreak;\n \tcase DIFF_SYMBOL_CONTEXT_MARKER:\n \t\tcontext = diff_get_color_opt(o, DIFF_CONTEXT);\n \t\treset = diff_get_color_opt(o, DIFF_RESET);\n@@ -1668,6 +1672,13 @@ static void emit_context_line(struct emit_callback *ecbdata,\n \temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT, line, len, flags);\n }\n \n+static void emit_incomplete_line_marker(struct emit_callback *ecbdata,\n+\t\t\t\t\tconst char *line, int len)\n+{\n+\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n+\t\t\t line, len, 0);\n+}\n+\n static void emit_hunk_header(struct emit_callback *ecbdata,\n \t\t\t     const char *line, int len)\n {\n@@ -2442,8 +2453,7 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \t\t\tBUG(\"fn_out_consume: '\\\\No newline' after unknown line (%c)\",\n \t\t\t    ecbdata->last_line_kind);\n \t\t}\n-\t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n-\t\t\t\t line, len, 0);\n+\t\temit_incomplete_line_marker(ecbdata, line, len);\n \t\tbreak;\n \tdefault:\n \t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n-- \n2.52.0-rc1-455-g30608eb744\n\n"},{"id":"530487","messageId":"20251111000451.2243195-7-gitster@pobox.com","threadId":"64429","inReplyTo":"20251111000451.2243195-1-gitster@pobox.com","subject":"[PATCH v3 06/12] diff: call emit_callback ecbdata everywhere","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-11T00:04:45Z","receivedAt":"2025-11-11T00:05:05Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Everybody else, except for emit_rewrite_lines(), calls the\nemit_callback data ecbdata.  Make sure we call the same thing by\nthe same name for consistency.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 12 ++++++------\n 1 file changed, 6 insertions(+), 6 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 8f1b4e6069..67071136a4 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1780,7 +1780,7 @@ static void add_line_count(struct strbuf *out, int count)\n \t}\n }\n \n-static void emit_rewrite_lines(struct emit_callback *ecb,\n+static void emit_rewrite_lines(struct emit_callback *ecbdata,\n \t\t\t       int prefix, const char *data, int size)\n {\n \tconst char *endp = NULL;\n@@ -1791,17 +1791,17 @@ static void emit_rewrite_lines(struct emit_callback *ecb,\n \t\tendp = memchr(data, '\\n', size);\n \t\tlen = endp ? (endp - data + 1) : size;\n \t\tif (prefix != '+') {\n-\t\t\tecb->lno_in_preimage++;\n-\t\t\temit_del_line(ecb, data, len);\n+\t\t\tecbdata->lno_in_preimage++;\n+\t\t\temit_del_line(ecbdata, data, len);\n \t\t} else {\n-\t\t\tecb->lno_in_postimage++;\n-\t\t\temit_add_line(ecb, data, len);\n+\t\t\tecbdata->lno_in_postimage++;\n+\t\t\temit_add_line(ecbdata, data, len);\n \t\t}\n \t\tsize -= len;\n \t\tdata += len;\n \t}\n \tif (!endp)\n-\t\temit_diff_symbol(ecb->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);\n+\t\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);\n }\n \n static void emit_rewrite_diff(const char *name_a,\n-- \n2.52.0-rc1-455-g30608eb744\n\n"},{"id":"530488","messageId":"20251111000451.2243195-8-gitster@pobox.com","threadId":"64429","inReplyTo":"20251111000451.2243195-1-gitster@pobox.com","subject":"[PATCH v3 07/12] diff: update the way rewrite diff handles incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-11T00:04:46Z","receivedAt":"2025-11-11T00:05:07Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The diff_symbol based output framework uses one DIFF_SYMBOL_* enum\nvalue per the kind of output lines of \"git diff\", which corresponds\nto one output line from the xdiff machinery used internally.  Most\nnotably, DIFF_SYMBOL_PLUS and DIFF_SYMBOL_MINUS that correspond to\n\"+\" and \"-\" lines are designed to always take a complete line, even\nif the output from xdiff machinery may produce \"\\ No newline at the\nend of file\" immediately after them.\n\nBut this is not true in the rewrite-diff codepath, which completely\nbypasses the xdiff machinery.  Since the code path feeds the bytes\ndirectly from the payload to the output routines, the output layer\nhas to deal with an incomplete line with DIFF_SYMBOL_PLUS and\nDIFF_SYMBOL_MINUS, which never would see an incomplete line in the\nnormal code paths.  This lack of final newline is compensated by an\nugly hack for a fabricated DIFF_SYMBOL_NO_LF_EOF token to inject an\nextra newline to the output to simulate output coming from the xdiff\nmachinery.\n\nRevamp the way the complete-rewrite code path feeds the lines to the\noutput layer by treating the last line of the pre/post image when it\nis an incomplete line specially.\n\nThis lets us remove the DIFF_SYMBOL_NO_LF_EOF hack and use the usual\nDIFF_SYMBOL_CONTEXT_INCOMPLETE code path, which will later learn how\nto handle whitespace errors.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 37 ++++++++++++++++++++++---------------\n 1 file changed, 22 insertions(+), 15 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 67071136a4..c3fb3015d6 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -797,7 +797,6 @@ enum diff_symbol {\n \tDIFF_SYMBOL_CONTEXT_INCOMPLETE,\n \tDIFF_SYMBOL_PLUS,\n \tDIFF_SYMBOL_MINUS,\n-\tDIFF_SYMBOL_NO_LF_EOF,\n \tDIFF_SYMBOL_CONTEXT_FRAGINFO,\n \tDIFF_SYMBOL_CONTEXT_MARKER,\n \tDIFF_SYMBOL_SEPARATOR\n@@ -1352,7 +1351,6 @@ static void emit_line_ws_markup(struct diff_options *o,\n static void emit_diff_symbol_from_struct(struct diff_options *o,\n \t\t\t\t\t struct emitted_diff_symbol *eds)\n {\n-\tstatic const char *nneof = \" No newline at end of file\\n\";\n \tconst char *context, *reset, *set, *set_sign, *meta, *fraginfo;\n \n \tenum diff_symbol s = eds->s;\n@@ -1361,13 +1359,6 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n \tunsigned flags = eds->flags;\n \n \tswitch (s) {\n-\tcase DIFF_SYMBOL_NO_LF_EOF:\n-\t\tcontext = diff_get_color_opt(o, DIFF_CONTEXT);\n-\t\treset = diff_get_color_opt(o, DIFF_RESET);\n-\t\tputc('\\n', o->file);\n-\t\temit_line_0(o, context, NULL, 0, reset, '\\\\',\n-\t\t\t    nneof, strlen(nneof));\n-\t\tbreak;\n \tcase DIFF_SYMBOL_SUBMODULE_HEADER:\n \tcase DIFF_SYMBOL_SUBMODULE_ERROR:\n \tcase DIFF_SYMBOL_SUBMODULE_PIPETHROUGH:\n@@ -1786,22 +1777,38 @@ static void emit_rewrite_lines(struct emit_callback *ecbdata,\n \tconst char *endp = NULL;\n \n \twhile (0 < size) {\n-\t\tint len;\n+\t\tint len, plen;\n+\t\tchar *pdata = NULL;\n \n \t\tendp = memchr(data, '\\n', size);\n-\t\tlen = endp ? (endp - data + 1) : size;\n+\n+\t\tif (endp) {\n+\t\t\tlen = endp - data + 1;\n+\t\t\tplen = len;\n+\t\t} else {\n+\t\t\tlen = size;\n+\t\t\tplen = len + 1;\n+\t\t\tpdata = xmalloc(plen + 2);\n+\t\t\tmemcpy(pdata, data, len);\n+\t\t\tpdata[len] = '\\n';\n+\t\t\tpdata[len + 1] = '\\0';\n+\t\t}\n \t\tif (prefix != '+') {\n \t\t\tecbdata->lno_in_preimage++;\n-\t\t\temit_del_line(ecbdata, data, len);\n+\t\t\temit_del_line(ecbdata, pdata ? pdata : data, plen);\n \t\t} else {\n \t\t\tecbdata->lno_in_postimage++;\n-\t\t\temit_add_line(ecbdata, data, len);\n+\t\t\temit_add_line(ecbdata, pdata ? pdata : data, plen);\n \t\t}\n+\t\tfree(pdata);\n \t\tsize -= len;\n \t\tdata += len;\n \t}\n-\tif (!endp)\n-\t\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);\n+\tif (!endp) {\n+\t\tstatic const char nneof[] = \"\\\\ No newline at end of file\\n\";\n+\t\tecbdata->last_line_kind = prefix;\n+\t\temit_incomplete_line_marker(ecbdata, nneof, sizeof(nneof) - 1);\n+\t}\n }\n \n static void emit_rewrite_diff(const char *name_a,\n-- \n2.52.0-rc1-455-g30608eb744\n\n"},{"id":"530490","messageId":"20251111000451.2243195-9-gitster@pobox.com","threadId":"64429","inReplyTo":"20251111000451.2243195-1-gitster@pobox.com","subject":"[PATCH v3 08/12] apply: revamp the parsing of incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-11T00:04:47Z","receivedAt":"2025-11-11T00:05:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"A patch file represents the incomplete line at the end of the file\nwith two lines, one that is the usual \"context\" with \" \" as the\nfirst letter, \"added\" with \"+\" as the first letter, or \"removed\"\nwith \"-\" as the first letter that shows the content of the line,\nplus an extra \"\\ No newline at the end of file\" line that comes\nimmediately after it.\n\nEver since the apply machinery was written, the \"git apply\"\nmachinery parses \"\\ No newline at the end of file\" line\nindependently, without even knowing what line the incomplete-ness\napplies to, simply because it does not even remember what the\nprevious line was.\n\nThis poses a problem if we want to check and warn on an incomplete\nline.  Revamp the code that parses a fragment, to actually drop the\n'\\n' at the end of the incoming patch file that terminates a line,\nso that check_whitespace() calls made from the code path actually\nsees an incomplete as incomplete.\n\nNote that the result of this parsing is not directly used by the\ncode path that applies the patch.  apply_one_fragment() function\nalready checks if each of the patch text it handles is followed by a\nline that begins with a backslash to drop the newline at the end of\nthe current line it is looking at.  In a sense, this patch harmonizes\nthe behaviour of the parsing side to what is already done in the\napplication side.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n apply.c | 70 ++++++++++++++++++++++++++++++++++++++++-----------------\n 1 file changed, 49 insertions(+), 21 deletions(-)\n\ndiff --git a/apply.c b/apply.c\nindex a2ceb3fb40..2b0f8bdab5 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -1670,6 +1670,35 @@ static void check_old_for_crlf(struct patch *patch, const char *line, int len)\n }\n \n \n+/*\n+ * Just saw a single line in a fragment.  If it is a part of this hunk\n+ * that is a context \" \", an added \"+\", or a removed \"-\" line, it may\n+ * be followed by \"\\\\ No newline...\" to signal that the last \"\\n\" on\n+ * this line needs to be dropped.  Depending on locale settings when\n+ * the patch was produced we don't know what this line would exactly\n+ * say. The only thing we do know is that it begins with \"\\ \".\n+ * Checking for 12 is just for sanity check; \"\\ No newline...\" would\n+ * be at least that long in any l10n.\n+ *\n+ * Return 0 if the line we saw is not followed by \"\\ No newline...\",\n+ * or length of that line.  The caller will use it to skip over the\n+ * \"\\ No newline...\" line.\n+ */\n+static int adjust_incomplete(const char *line, int len,\n+\t\t\t     unsigned long size)\n+{\n+\tint nextlen;\n+\n+\tif (*line != '\\n' && *line != ' ' && *line != '+' && *line != '-')\n+\t\treturn 0;\n+\tif (size - len < 12 || memcmp(line + len, \"\\\\ \", 2))\n+\t\treturn 0;\n+\tnextlen = linelen(line + len, size - len);\n+\tif (nextlen < 12)\n+\t\treturn 0;\n+\treturn nextlen;\n+}\n+\n /*\n  * Parse a unified diff. Note that this really needs to parse each\n  * fragment separately, since the only way to know the difference\n@@ -1684,6 +1713,7 @@ static int parse_fragment(struct apply_state *state,\n {\n \tint added, deleted;\n \tint len = linelen(line, size), offset;\n+\tint skip_len = 0;\n \tunsigned long oldlines, newlines;\n \tunsigned long leading, trailing;\n \n@@ -1710,6 +1740,22 @@ static int parse_fragment(struct apply_state *state,\n \t\tlen = linelen(line, size);\n \t\tif (!len || line[len-1] != '\\n')\n \t\t\treturn -1;\n+\n+\t\t/*\n+\t\t * For an incomplete line, skip_len counts the bytes\n+\t\t * on \"\\\\ No newline...\" marker line that comes next\n+\t\t * to the current line.\n+\t\t *\n+\t\t * Reduce \"len\" to drop the newline at the end of\n+\t\t * line[], but add one to \"skip_len\", which will be\n+\t\t * added back to \"len\" for the next iteration, to\n+\t\t * compensate.\n+\t\t */\n+\t\tskip_len = adjust_incomplete(line, len, size);\n+\t\tif (skip_len) {\n+\t\t\tlen--;\n+\t\t\tskip_len++;\n+\t\t}\n \t\tswitch (*line) {\n \t\tdefault:\n \t\t\treturn -1;\n@@ -1745,20 +1791,10 @@ static int parse_fragment(struct apply_state *state,\n \t\t\tnewlines--;\n \t\t\ttrailing = 0;\n \t\t\tbreak;\n-\n-\t\t/*\n-\t\t * We allow \"\\ No newline at end of file\". Depending\n-\t\t * on locale settings when the patch was produced we\n-\t\t * don't know what this line looks like. The only\n-\t\t * thing we do know is that it begins with \"\\ \".\n-\t\t * Checking for 12 is just for sanity check -- any\n-\t\t * l10n of \"\\ No newline...\" is at least that long.\n-\t\t */\n-\t\tcase '\\\\':\n-\t\t\tif (len < 12 || memcmp(line, \"\\\\ \", 2))\n-\t\t\t\treturn -1;\n-\t\t\tbreak;\n \t\t}\n+\n+\t\t/* eat the \"\\\\ No newline...\" as well, if exists */\n+\t\tlen += skip_len;\n \t}\n \tif (oldlines || newlines)\n \t\treturn -1;\n@@ -1768,14 +1804,6 @@ static int parse_fragment(struct apply_state *state,\n \tfragment->leading = leading;\n \tfragment->trailing = trailing;\n \n-\t/*\n-\t * If a fragment ends with an incomplete line, we failed to include\n-\t * it in the above loop because we hit oldlines == newlines == 0\n-\t * before seeing it.\n-\t */\n-\tif (12 < size && !memcmp(line, \"\\\\ \", 2))\n-\t\toffset += linelen(line, size);\n-\n \tpatch->lines_added += added;\n \tpatch->lines_deleted += deleted;\n \n-- \n2.52.0-rc1-455-g30608eb744\n\n"},{"id":"530489","messageId":"20251111000451.2243195-10-gitster@pobox.com","threadId":"64429","inReplyTo":"20251111000451.2243195-1-gitster@pobox.com","subject":"[PATCH v3 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-11T00:04:48Z","receivedAt":"2025-11-11T00:05:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Reserve a few more bits in the diff flags word to be used for future\nwhitespace rules.  Add WS_INCOMPLETE_LINE without implementing the\nbehaviour (yet).\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/config/core.adoc |  2 ++\n diff.c                         | 16 ++++++++--------\n diff.h                         |  6 +++---\n ws.c                           |  6 ++++++\n ws.h                           |  3 ++-\n 5 files changed, 21 insertions(+), 12 deletions(-)\n\ndiff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc\nindex e2de270c86..682fb595fb 100644\n--- a/Documentation/config/core.adoc\n+++ b/Documentation/config/core.adoc\n@@ -626,6 +626,8 @@ core.whitespace::\n   part of the line terminator, i.e. with it, `trailing-space`\n   does not trigger if the character before such a carriage-return\n   is not a whitespace (not enabled by default).\n+* `incomplete-line` treats the last line of a file that is missing the\n+  newline at the end as an error (not enabled by default).\n * `tabwidth=<n>` tells how many character positions a tab occupies; this\n   is relevant for `indent-with-non-tab` and when Git fixes `tab-in-indent`\n   errors. The default tab width is 8. Allowed values are 1 to 63.\ndiff --git a/diff.c b/diff.c\nindex c3fb3015d6..64cf1f139f 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -804,15 +804,15 @@ enum diff_symbol {\n \n /*\n  * Flags for content lines:\n- * 0..11 are whitespace rules (see ws.h)\n- * 12..14 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD\n- * 16 is marking if the line is blank at EOF\n- * 17..19 are used for color-moved.\n+ * 0..15 are whitespace rules (see ws.h)\n+ * 16..18 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD\n+ * 19 is marking if the line is blank at EOF\n+ * 20..22 are used for color-moved.\n  */\n-#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF\t(1<<16)\n-#define DIFF_SYMBOL_MOVED_LINE\t\t\t(1<<17)\n-#define DIFF_SYMBOL_MOVED_LINE_ALT\t\t(1<<18)\n-#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING\t(1<<19)\n+#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF\t(1<<19)\n+#define DIFF_SYMBOL_MOVED_LINE\t\t\t(1<<20)\n+#define DIFF_SYMBOL_MOVED_LINE_ALT\t\t(1<<21)\n+#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING\t(1<<22)\n \n #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)\n \ndiff --git a/diff.h b/diff.h\nindex cbd355cf50..422658407d 100644\n--- a/diff.h\n+++ b/diff.h\n@@ -331,9 +331,9 @@ struct diff_options {\n \n \tint ita_invisible_in_index;\n /* white-space error highlighting */\n-#define WSEH_NEW        (1<<12)\n-#define WSEH_CONTEXT    (1<<13)\n-#define WSEH_OLD        (1<<14)\n+#define WSEH_NEW        (1<<16)\n+#define WSEH_CONTEXT    (1<<17)\n+#define WSEH_OLD        (1<<18)\n \tunsigned ws_error_highlight;\n \tconst char *prefix;\n \tint prefix_length;\ndiff --git a/ws.c b/ws.c\nindex 70acee3337..34a7b4fad2 100644\n--- a/ws.c\n+++ b/ws.c\n@@ -26,6 +26,7 @@ static struct whitespace_rule {\n \t{ \"blank-at-eol\", WS_BLANK_AT_EOL, 0 },\n \t{ \"blank-at-eof\", WS_BLANK_AT_EOF, 0 },\n \t{ \"tab-in-indent\", WS_TAB_IN_INDENT, 0, 1 },\n+\t{ \"incomplete-line\", WS_INCOMPLETE_LINE, 0, 0 },\n };\n \n unsigned parse_whitespace_rule(const char *string)\n@@ -139,6 +140,11 @@ char *whitespace_error_string(unsigned ws)\n \t\t\tstrbuf_addstr(&err, \", \");\n \t\tstrbuf_addstr(&err, \"tab in indent\");\n \t}\n+\tif (ws & WS_INCOMPLETE_LINE) {\n+\t\tif (err.len)\n+\t\t\tstrbuf_addstr(&err, \", \");\n+\t\tstrbuf_addstr(&err, \"no newline at the end of file\");\n+\t}\n \treturn strbuf_detach(&err, NULL);\n }\n \ndiff --git a/ws.h b/ws.h\nindex 23708efb73..06d5cb73f8 100644\n--- a/ws.h\n+++ b/ws.h\n@@ -15,13 +15,14 @@ struct strbuf;\n #define WS_CR_AT_EOL            (1<<9)\n #define WS_BLANK_AT_EOF         (1<<10)\n #define WS_TAB_IN_INDENT        (1<<11)\n+#define WS_INCOMPLETE_LINE      (1<<12)\n \n #define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)\n #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)\n #define WS_TAB_WIDTH_MASK       ((1<<6)-1)\n \n /* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */\n-#define WS_RULE_MASK            ((1<<12)-1)\n+#define WS_RULE_MASK            ((1<<16)-1)\n \n extern unsigned whitespace_rule_cfg;\n unsigned whitespace_rule(struct index_state *, const char *);\n-- \n2.52.0-rc1-455-g30608eb744\n\n"},{"id":"530491","messageId":"20251111000451.2243195-11-gitster@pobox.com","threadId":"64429","inReplyTo":"20251111000451.2243195-1-gitster@pobox.com","subject":"[PATCH v3 10/12] apply: check and fix incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-11T00:04:49Z","receivedAt":"2025-11-11T00:05:12Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The final line of a file that lacks the terminating newline at its\nend is called an incomplete line.  In general they are frowned upon\nfor many reasons (imagine concatenating two files with \"cat A B\" and\nwhat happens when A ends in an incomplete line, for example), and\ntext-oriented tools often mishandle such a line.\n\nImplement checks in \"git apply\" for incomplete lines, which is off\nby default for backward compatibility's sake, so that \"git apply\n--whitespace={fix,warn,error}\" can notice, warn against, and fix\nthem.\n\nAs one of the new test shows, if you modify contents on an\nincomplete line in the original and leave the resulting line\nincomplete, it is still considered a whitespace error, the reasoning\nbeing that \"you'd better fix it while at it if you are making a\nchange on an incomplete line anyway\", which may controversial.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n apply.c                  |  13 ++-\n t/t4124-apply-ws-rule.sh | 187 +++++++++++++++++++++++++++++++++++++++\n ws.c                     |  14 +++\n 3 files changed, 213 insertions(+), 1 deletion(-)\n\ndiff --git a/apply.c b/apply.c\nindex 2b0f8bdab5..c9fb45247d 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -1640,6 +1640,14 @@ static void record_ws_error(struct apply_state *state,\n \t    state->squelch_whitespace_errors < state->whitespace_error)\n \t\treturn;\n \n+\t/*\n+\t * line[len] for an incomplete line points at the \"\\n\" at the end\n+\t * of patch input line, so \"%.*s\" would drop the last letter on line;\n+\t * compensate for it.\n+\t */\n+\tif (result & WS_INCOMPLETE_LINE)\n+\t\tlen++;\n+\n \terr = whitespace_error_string(result);\n \tif (state->apply_verbosity > verbosity_silent)\n \t\tfprintf(stderr, \"%s:%d: %s.\\n%.*s\\n\",\n@@ -1794,7 +1802,10 @@ static int parse_fragment(struct apply_state *state,\n \t\t}\n \n \t\t/* eat the \"\\\\ No newline...\" as well, if exists */\n-\t\tlen += skip_len;\n+\t\tif (skip_len) {\n+\t\t\tlen += skip_len;\n+\t\t\tstate->linenr++;\n+\t\t}\n \t}\n \tif (oldlines || newlines)\n \t\treturn -1;\ndiff --git a/t/t4124-apply-ws-rule.sh b/t/t4124-apply-ws-rule.sh\nindex 485c7d2d12..115a0f8579 100755\n--- a/t/t4124-apply-ws-rule.sh\n+++ b/t/t4124-apply-ws-rule.sh\n@@ -556,4 +556,191 @@ test_expect_success 'whitespace check skipped for excluded paths' '\n \tgit apply --include=used --stat --whitespace=error <patch\n '\n \n+test_expect_success 'check incomplete lines (setup)' '\n+\trm -f .gitattributes &&\n+\tgit config core.whitespace incomplete-line\n+'\n+\n+test_expect_success 'incomplete context line (not an error)' '\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\t(test_write_lines 1 2 3 0 5 && printf 6) >sample2-i &&\n+\tcat sample-i >target &&\n+\tgit add target &&\n+\tcat sample2-i >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error <patch &&\n+\ttest_cmp sample2-i target &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error --check <patch 2>error &&\n+\ttest_cmp sample-i target &&\n+\ttest_must_be_empty error &&\n+\n+\tcat sample2-i >target &&\n+\tgit apply --whitespace=error -R <patch &&\n+\ttest_cmp sample-i target &&\n+\n+\tcat sample2-i >target &&\n+\tgit apply -R --whitespace=error --check <patch 2>error &&\n+\ttest_cmp sample2-i target &&\n+\ttest_must_be_empty error\n+'\n+\n+test_expect_success 'last line made incomplete (error)' '\n+\ttest_write_lines 1 2 3 4 5 6 >sample &&\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\tcat sample >target &&\n+\tgit add target &&\n+\tcat sample-i >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample >target &&\n+\ttest_must_fail git apply --whitespace=error <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample >target &&\n+\ttest_must_fail git apply --whitespace=error --check <patch 2>actual &&\n+\ttest_cmp sample target &&\n+\tcat >expect <<-\\EOF &&\n+\t<stdin>:10: no newline at the end of file.\n+\t6\n+\terror: 1 line adds whitespace errors.\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error -R <patch &&\n+\ttest_cmp sample target &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error --check -R <patch 2>error &&\n+\ttest_cmp sample-i target &&\n+\ttest_must_be_empty error &&\n+\n+\tcat sample >target &&\n+\tgit apply --whitespace=fix <patch &&\n+\ttest_cmp sample target\n+'\n+\n+test_expect_success 'incomplete line removed at the end (not an error)' '\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\ttest_write_lines 1 2 3 4 5 6 >sample &&\n+\tcat sample-i >target &&\n+\tgit add target &&\n+\tcat sample >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error <patch &&\n+\ttest_cmp sample target &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error --check <patch 2>error &&\n+\ttest_cmp sample-i target &&\n+\ttest_must_be_empty error &&\n+\n+\tcat sample >target &&\n+\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample >target &&\n+\ttest_must_fail git apply --whitespace=error --check -R <patch 2>actual &&\n+\ttest_cmp sample target &&\n+\tcat >expect <<-\\EOF &&\n+\t<stdin>:9: no newline at the end of file.\n+\t6\n+\terror: 1 line adds whitespace errors.\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tcat sample >target &&\n+\tgit apply --whitespace=fix -R <patch &&\n+\ttest_cmp sample target\n+'\n+\n+test_expect_success 'incomplete line corrected at the end (not an error)' '\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\ttest_write_lines 1 2 3 4 5 7 >sample3 &&\n+\tcat sample-i >target &&\n+\tgit add target &&\n+\tcat sample3 >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error <patch &&\n+\ttest_cmp sample3 target &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error --check <patch 2>error &&\n+\ttest_cmp sample-i target &&\n+\ttest_must_be_empty error &&\n+\n+\tcat sample3 >target &&\n+\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample3 >target &&\n+\ttest_must_fail git apply --whitespace=error -R --check <patch 2>actual &&\n+\ttest_cmp sample3 target &&\n+\tcat >expect <<-\\EOF &&\n+\t<stdin>:9: no newline at the end of file.\n+\t6\n+\terror: 1 line adds whitespace errors.\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tcat sample3 >target &&\n+\tgit apply --whitespace=fix -R <patch &&\n+\ttest_cmp sample target\n+'\n+\n+test_expect_success 'incomplete line modified at the end (error)' '\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\t(test_write_lines 1 2 3 4 5 && printf 7) >sample3-i &&\n+\ttest_write_lines 1 2 3 4 5 6 >sample &&\n+\ttest_write_lines 1 2 3 4 5 7 >sample3 &&\n+\tcat sample-i >target &&\n+\tgit add target &&\n+\tcat sample3-i >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample-i >target &&\n+\ttest_must_fail git apply --whitespace=error <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample-i >target &&\n+\ttest_must_fail git apply --whitespace=error --check <patch 2>actual &&\n+\ttest_cmp sample-i target &&\n+\tcat >expect <<-\\EOF &&\n+\t<stdin>:11: no newline at the end of file.\n+\t7\n+\terror: 1 line adds whitespace errors.\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tcat sample3-i >target &&\n+\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample3-i >target &&\n+\ttest_must_fail git apply --whitespace=error --check -R <patch 2>actual &&\n+\ttest_cmp sample3-i target &&\n+\tcat >expect <<-\\EOF &&\n+\t<stdin>:9: no newline at the end of file.\n+\t6\n+\terror: 1 line adds whitespace errors.\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=fix <patch &&\n+\ttest_cmp sample3 target &&\n+\n+\tcat sample3-i >target &&\n+\tgit apply --whitespace=fix -R <patch &&\n+\ttest_cmp sample target\n+'\n+\n test_done\ndiff --git a/ws.c b/ws.c\nindex 34a7b4fad2..6cc2466c0c 100644\n--- a/ws.c\n+++ b/ws.c\n@@ -186,6 +186,9 @@ static unsigned ws_check_emit_1(const char *line, int len, unsigned ws_rule,\n \tif (trailing_whitespace == -1)\n \t\ttrailing_whitespace = len;\n \n+\tif (!trailing_newline && (ws_rule & WS_INCOMPLETE_LINE))\n+\t\tresult |= WS_INCOMPLETE_LINE;\n+\n \t/* Check indentation */\n \tfor (i = 0; i < trailing_whitespace; i++) {\n \t\tif (line[i] == ' ')\n@@ -297,6 +300,17 @@ void ws_fix_copy(struct strbuf *dst, const char *src, int len, unsigned ws_rule,\n \tint last_space_in_indent = -1;\n \tint need_fix_leading_space = 0;\n \n+\t/*\n+\t * Remembering that we need to add '\\n' at the end\n+\t * is sufficient to fix an incomplete line.\n+\t */\n+\tif (ws_rule & WS_INCOMPLETE_LINE) {\n+\t\tif (0 < len && src[len - 1] != '\\n') {\n+\t\t\tfixed = 1;\n+\t\t\tadd_nl_to_tail = 1;\n+\t\t}\n+\t}\n+\n \t/*\n \t * Strip trailing whitespace\n \t */\n-- \n2.52.0-rc1-455-g30608eb744\n\n"},{"id":"530492","messageId":"20251111000451.2243195-12-gitster@pobox.com","threadId":"64429","inReplyTo":"20251111000451.2243195-1-gitster@pobox.com","subject":"[PATCH v3 11/12] diff: highlight and error out on incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-11T00:04:50Z","receivedAt":"2025-11-11T00:05:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Teach \"git diff\" to highlight \"\\ No newline at end of file\" message\nas a whitespace error when incomplete-line whitespace error class is\nin effect.  Thanks to the previous refactoring of complete rewrite\ncode path, we can do this at a single place.\n\nUnlike whitespace errors in the payload where we need to annotate in\nline, possibly using colors, the line that has whitespace problems,\nwe have a dedicated line already that can serve as the error\nmessage, so paint it as a whitespace error message.\n\nAlso teach \"git diff --check\" to notice incomplete lines as\nwhitespace errors and report when incomplete-line whitespace error\nclass is in effect.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c                     | 29 +++++++++++++++--\n t/t4015-diff-whitespace.sh | 67 +++++++++++++++++++++++++++++++++++---\n 2 files changed, 90 insertions(+), 6 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 64cf1f139f..e12931c5e7 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1370,7 +1370,11 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n \t\temit_line(o, \"\", \"\", line, len);\n \t\tbreak;\n \tcase DIFF_SYMBOL_CONTEXT_INCOMPLETE:\n-\t\tset = diff_get_color_opt(o, DIFF_CONTEXT);\n+\t\tif ((flags & WS_INCOMPLETE_LINE) &&\n+\t\t    (flags & o->ws_error_highlight))\n+\t\t\tset = diff_get_color_opt(o, DIFF_WHITESPACE);\n+\t\telse\n+\t\t\tset = diff_get_color_opt(o, DIFF_CONTEXT);\n \t\treset = diff_get_color_opt(o, DIFF_RESET);\n \t\temit_line(o, set, reset, line, len);\n \t\tbreak;\n@@ -1666,8 +1670,14 @@ static void emit_context_line(struct emit_callback *ecbdata,\n static void emit_incomplete_line_marker(struct emit_callback *ecbdata,\n \t\t\t\t\tconst char *line, int len)\n {\n+\tint last_line_kind = ecbdata->last_line_kind;\n+\tunsigned flags = (last_line_kind == '+'\n+\t\t\t  ? WSEH_NEW\n+\t\t\t  : last_line_kind == '-'\n+\t\t\t  ? WSEH_OLD\n+\t\t\t  : WSEH_CONTEXT) | ecbdata->ws_rule;\n \temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n-\t\t\t line, len, 0);\n+\t\t\t line, len, flags);\n }\n \n static void emit_hunk_header(struct emit_callback *ecbdata,\n@@ -3259,6 +3269,7 @@ struct checkdiff_t {\n \tstruct diff_options *o;\n \tunsigned ws_rule;\n \tunsigned status;\n+\tint last_line_kind;\n };\n \n static int is_conflict_marker(const char *line, int marker_size, unsigned long len)\n@@ -3297,6 +3308,7 @@ static void checkdiff_consume_hunk(void *priv,\n static int checkdiff_consume(void *priv, char *line, unsigned long len)\n {\n \tstruct checkdiff_t *data = priv;\n+\tint last_line_kind;\n \tint marker_size = data->conflict_marker_size;\n \tconst char *ws = diff_get_color(data->o->use_color, DIFF_WHITESPACE);\n \tconst char *reset = diff_get_color(data->o->use_color, DIFF_RESET);\n@@ -3307,6 +3319,8 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)\n \tassert(data->o);\n \tline_prefix = diff_line_prefix(data->o);\n \n+\tlast_line_kind = data->last_line_kind;\n+\tdata->last_line_kind = line[0];\n \tif (line[0] == '+') {\n \t\tunsigned bad;\n \t\tdata->lineno++;\n@@ -3329,6 +3343,17 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)\n \t\t\t      data->o->file, set, reset, ws);\n \t} else if (line[0] == ' ') {\n \t\tdata->lineno++;\n+\t} else if (line[0] == '\\\\') {\n+\t\t/* no newline at the end of the line */\n+\t\tif ((data->ws_rule & WS_INCOMPLETE_LINE) &&\n+\t\t    (last_line_kind == '+')) {\n+\t\t\tunsigned bad = WS_INCOMPLETE_LINE;\n+\t\t\tdata->status |= bad;\n+\t\t\terr = whitespace_error_string(bad);\n+\t\t\tfprintf(data->o->file, \"%s%s:%d: %s.\\n\",\n+\t\t\t\tline_prefix, data->filename, data->lineno, err);\n+\t\t\tfree(err);\n+\t\t}\n \t}\n \treturn 0;\n }\ndiff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh\nindex 9de7f73f42..3c8eb02e4f 100755\n--- a/t/t4015-diff-whitespace.sh\n+++ b/t/t4015-diff-whitespace.sh\n@@ -43,6 +43,53 @@ do\n \t'\n done\n \n+test_expect_success \"incomplete line in both pre- and post-image context\" '\n+\t(echo foo && echo baz | tr -d \"\\012\") >x &&\n+\tgit add x &&\n+\t(echo bar && echo baz | tr -d \"\\012\") >x &&\n+\tgit diff x &&\n+\tgit -c core.whitespace=incomplete diff --check x &&\n+\tgit diff -R x &&\n+\tgit -c core.whitespace=incomplete diff -R --check x\n+'\n+\n+test_expect_success \"incomplete lines on both pre- and post-image\" '\n+\t# The interpretation taken here is \"since you are touching\n+\t# the line anyway, you would better fix the incomplete line\n+\t# while you are at it.\"  but this is debatable.\n+\techo foo | tr -d \"\\012\" >x &&\n+\tgit add x &&\n+\techo bar | tr -d \"\\012\" >x &&\n+\tgit diff x &&\n+\ttest_must_fail git -c core.whitespace=incomplete diff --check x >error &&\n+\ttest_grep \"no newline at the end of file\" error &&\n+\tgit diff -R x &&\n+\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&\n+\ttest_grep \"no newline at the end of file\" error\n+'\n+\n+test_expect_success \"fix incomplete line in pre-image\" '\n+\techo foo | tr -d \"\\012\" >x &&\n+\tgit add x &&\n+\techo bar >x &&\n+\tgit diff x &&\n+\tgit -c core.whitespace=incomplete diff --check x &&\n+\tgit diff -R x &&\n+\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&\n+\ttest_grep \"no newline at the end of file\" error\n+'\n+\n+test_expect_success \"new incomplete line in post-image\" '\n+\techo foo >x &&\n+\tgit add x &&\n+\techo bar | tr -d \"\\012\" >x &&\n+\tgit diff x &&\n+\ttest_must_fail git -c core.whitespace=incomplete diff --check x >error &&\n+\ttest_grep \"no newline at the end of file\" error &&\n+\tgit diff -R x &&\n+\tgit -c core.whitespace=incomplete diff -R --check x\n+'\n+\n test_expect_success \"Ray Lehtiniemi's example\" '\n \tcat <<-\\EOF >x &&\n \tdo {\n@@ -1040,7 +1087,8 @@ test_expect_success 'ws-error-highlight test setup' '\n \t{\n \t\techo \"0. blank-at-eol \" &&\n \t\techo \"1. still-blank-at-eol \" &&\n-\t\techo \"2. and a new line \"\n+\t\techo \"2. and a new line \" &&\n+\t\tprintf \"3. and more\"\n \t} >x &&\n \tnew_hash_x=$(git hash-object x) &&\n \tafter=$(git rev-parse --short \"$new_hash_x\") &&\n@@ -1050,11 +1098,13 @@ test_expect_success 'ws-error-highlight test setup' '\n \t<BOLD>index $before..$after 100644<RESET>\n \t<BOLD>--- a/x<RESET>\n \t<BOLD>+++ b/x<RESET>\n-\t<CYAN>@@ -1,2 +1,3 @@<RESET>\n+\t<CYAN>@@ -1,2 +1,4 @@<RESET>\n \t 0. blank-at-eol <RESET>\n \t<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>\n \t<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>\n \t<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>\n+\t<GREEN>+<RESET><GREEN>3. and more<RESET>\n+\t<BLUE>\\ No newline at end of file<RESET>\n \tEOF\n \n \tcat >expect.all <<-EOF &&\n@@ -1062,11 +1112,13 @@ test_expect_success 'ws-error-highlight test setup' '\n \t<BOLD>index $before..$after 100644<RESET>\n \t<BOLD>--- a/x<RESET>\n \t<BOLD>+++ b/x<RESET>\n-\t<CYAN>@@ -1,2 +1,3 @@<RESET>\n+\t<CYAN>@@ -1,2 +1,4 @@<RESET>\n \t <RESET>0. blank-at-eol<RESET><BLUE> <RESET>\n \t<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>\n \t<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>\n \t<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>\n+\t<GREEN>+<RESET><GREEN>3. and more<RESET>\n+\t<BLUE>\\ No newline at end of file<RESET>\n \tEOF\n \n \tcat >expect.none <<-EOF\n@@ -1074,16 +1126,19 @@ test_expect_success 'ws-error-highlight test setup' '\n \t<BOLD>index $before..$after 100644<RESET>\n \t<BOLD>--- a/x<RESET>\n \t<BOLD>+++ b/x<RESET>\n-\t<CYAN>@@ -1,2 +1,3 @@<RESET>\n+\t<CYAN>@@ -1,2 +1,4 @@<RESET>\n \t 0. blank-at-eol <RESET>\n \t<RED>-1. blank-at-eol <RESET>\n \t<GREEN>+1. still-blank-at-eol <RESET>\n \t<GREEN>+2. and a new line <RESET>\n+\t<GREEN>+3. and more<RESET>\n+\t\\ No newline at end of file<RESET>\n \tEOF\n \n '\n \n test_expect_success 'test --ws-error-highlight option' '\n+\tgit config core.whitespace blank-at-eol,incomplete-line &&\n \n \tgit diff --color --ws-error-highlight=default,old >current.raw &&\n \ttest_decode_color <current.raw >current &&\n@@ -1100,6 +1155,7 @@ test_expect_success 'test --ws-error-highlight option' '\n '\n \n test_expect_success 'test diff.wsErrorHighlight config' '\n+\tgit config core.whitespace blank-at-eol,incomplete-line &&\n \n \tgit -c diff.wsErrorHighlight=default,old diff --color >current.raw &&\n \ttest_decode_color <current.raw >current &&\n@@ -1116,6 +1172,7 @@ test_expect_success 'test diff.wsErrorHighlight config' '\n '\n \n test_expect_success 'option overrides diff.wsErrorHighlight' '\n+\tgit config core.whitespace blank-at-eol,incomplete-line &&\n \n \tgit -c diff.wsErrorHighlight=none \\\n \t\tdiff --color --ws-error-highlight=default,old >current.raw &&\n@@ -1135,6 +1192,8 @@ test_expect_success 'option overrides diff.wsErrorHighlight' '\n '\n \n test_expect_success 'detect moved code, complete file' '\n+\tgit config core.whitespace blank-at-eol &&\n+\n \tgit reset --hard &&\n \tcat <<-\\EOF >test.c &&\n \t#include<stdio.h>\n-- \n2.52.0-rc1-455-g30608eb744\n\n"},{"id":"530493","messageId":"20251111000451.2243195-13-gitster@pobox.com","threadId":"64429","inReplyTo":"20251111000451.2243195-1-gitster@pobox.com","subject":"[PATCH v3 12/12] attr: enable incomplete-line whitespace error for this project","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-11T00:04:51Z","receivedAt":"2025-11-11T00:05:16Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Now \"git diff --check\" and \"git apply --whitespace=warn/fix\" learned\nincomplete line is a whitespace error, enable them for this project\nto prevent patches to add new incomplete lines to our sources.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n .gitattributes | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/.gitattributes b/.gitattributes\nindex 32583149c2..0accd23848 100644\n--- a/.gitattributes\n+++ b/.gitattributes\n@@ -1,6 +1,6 @@\n * whitespace=!indent,trail,space\n-*.[ch] whitespace=indent,trail,space diff=cpp\n-*.sh whitespace=indent,trail,space text eol=lf\n+*.[ch] whitespace=indent,trail,space,incomplete diff=cpp\n+*.sh whitespace=indent,trail,space,incomplete text eol=lf\n *.perl text eol=lf diff=perl\n *.pl text eof=lf diff=perl\n *.pm text eol=lf diff=perl\n-- \n2.52.0-rc1-455-g30608eb744\n\n"},{"id":"530513","messageId":"4506b9c3-f4ae-488c-988c-e12b2d95195f@gmail.com","threadId":"64429","inReplyTo":"xmqqjyzx213a.fsf@gitster.g","subject":"Re: [PATCH 04/12] diff: fix incorrect counting of line numbers","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-11T14:26:01Z","receivedAt":"2025-11-11T14:26:04Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 10/11/2025 18:29, Junio C Hamano wrote:\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n>> On 04/11/2025 02:09, Junio C Hamano wrote:\n>>\n>> The \"\\ No newline at end of file\" line is an annotation on the previous\n>> line in the diff so why are we incrementing any {pre,post}image line\n>> numbers here?\n> \n> No particular reason ;-)  As I said, I do not think these numbers\n> are used after these lines are seen.  At least this change makes\n> these unused data incremented in a more coherent way than the\n> previous one, which unconditionally incremented the number for the\n> preimage without even checking which side the \"\\ No newline\" is for.\n\nIt maybe coherent but it is still wrong to increment the line numbers \nhere. To be correct we should remove the erroneous increment of \nlno_in_postimage.\n\nThanks\n\nPhillip\n\n>> Thanks\n>>\n>> Phillip\n>>\n>>> Signed-off-by: Junio C Hamano <gitster@pobox.com>\n>>> ---\n>>>    diff.c | 18 +++++++++++++++++-\n>>>    1 file changed, 17 insertions(+), 1 deletion(-)\n>>>\n>>> diff --git a/diff.c b/diff.c\n>>> index b9ef8550cc..e73320dfb1 100644\n>>> --- a/diff.c\n>>> +++ b/diff.c\n>>> @@ -601,6 +601,7 @@ struct emit_callback {\n>>>    \tint blank_at_eof_in_postimage;\n>>>    \tint lno_in_preimage;\n>>>    \tint lno_in_postimage;\n>>> +\tint last_line_kind;\n>>>    \tconst char **label_path;\n>>>    \tstruct diff_words_data *diff_words;\n>>>    \tstruct diff_options *opt;\n>>> @@ -2426,13 +2427,28 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n>>>    \t\tbreak;\n>>>    \tcase '\\\\':\n>>>    \t\t/* incomplete line at the end */\n>>> -\t\tecbdata->lno_in_preimage++;\n>>> +\t\tswitch (ecbdata->last_line_kind) {\n>>> +\t\tcase '+':\n>>> +\t\t\tecbdata->lno_in_postimage++;\n>>> +\t\t\tbreak;\n>>> +\t\tcase '-':\n>>> +\t\t\tecbdata->lno_in_preimage++;\n>>> +\t\t\tbreak;\n>>> +\t\tcase ' ':\n>>> +\t\t\tecbdata->lno_in_preimage++;\n>>> +\t\t\tecbdata->lno_in_postimage++;\n>>> +\t\t\tbreak;\n>>> +\t\tdefault:\n>>> +\t\t\tBUG(\"fn_out_consume: '\\\\No newline' after unknown line (%c)\",\n>>> +\t\t\t    ecbdata->last_line_kind);\n>>> +\t\t}\n>>>    \t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n>>>    \t\t\t\t line, len, 0);\n>>>    \t\tbreak;\n>>>    \tdefault:\n>>>    \t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n>>>    \t}\n>>> +\tecbdata->last_line_kind = line[0];\n>>>    \treturn 0;\n>>>    }\n>>>    \n\n"},{"id":"530514","messageId":"bc2b7b52-a8f9-473c-a6fe-2c0d3fa089ea@gmail.com","threadId":"64429","inReplyTo":"20251111000451.2243195-1-gitster@pobox.com","subject":"Re: [PATCH v3 00/12] Incomplete lines","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-11T14:29:15Z","receivedAt":"2025-11-11T14:29:19Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 11/11/2025 00:04, Junio C Hamano wrote:\n> Changes in v3:\n> \n>   - The proposed log message of [PATCH 05/12] explains that it\n>     semi-duplicates the same code shared in two case arems in\n>     preparation for later changes.\n> \n>   - The internal helper function to emit the \"\\ No newline\" marker\n>     line is now called emit_incomplete_line_marker().\n> \n>   - Two conditionals in [PATCH 07/12] both of which switched on !endp\n>     have been consolidated into a single if/else statement.\n> \n>   - The tests in [PATCH 11/12] checks the output from \"diff --check\"\n>     now.\n\nThe range-diff looks good. I'm still not keen on patch 4 but all the \nother diff related changes look fine to me.\n\nThanks\n\nPhillip\n\n> \n> Changes in v2:\n> \n>   - rolled the definition (but not implementation) of the new\n>     \"incomplete-line\" class into step [09/12] that shifts the bit\n>     assignment.  The documentation of core.whitespace has also be\n>     updated in this step.\n> \n>   - \"git apply --check\" miscounted line number reported for the\n>     incomplete line error, which has been corrected in step [10/12].\n> \n>   - t4124-apply-ws-rule.sh has been extended to cover \"git apply\n>     --check\" and the diagnostic output from it in step [10/12].\n> \n> Junio C Hamano (12):\n>    whitespace: correct bit assignment comments\n>    diff: emit_line_ws_markup() if/else style fix\n>    diff: correct suppress_blank_empty hack\n>    diff: fix incorrect counting of line numbers\n>    diff: refactor output of incomplete line\n>    diff: call emit_callback ecbdata everywhere\n>    diff: update the way rewrite diff handles incomplete lines\n>    apply: revamp the parsing of incomplete lines\n>    whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE\n>    apply: check and fix incomplete lines\n>    diff: highlight and error out on incomplete lines\n>    attr: enable incomplete-line whitespace error for this project\n> \n>   .gitattributes                 |   4 +-\n>   Documentation/config/core.adoc |   2 +\n>   apply.c                        |  79 ++++++++++----\n>   diff.c                         | 152 ++++++++++++++++++---------\n>   diff.h                         |   6 +-\n>   t/t4015-diff-whitespace.sh     |  67 +++++++++++-\n>   t/t4124-apply-ws-rule.sh       | 187 +++++++++++++++++++++++++++++++++\n>   ws.c                           |  20 ++++\n>   ws.h                           |  26 +++--\n>   9 files changed, 455 insertions(+), 88 deletions(-)\n> \n> Range-diff against v2:\n>   1:  c045e93ce5 =  1:  c045e93ce5 whitespace: correct bit assignment comments\n>   2:  0d95d68fb4 =  2:  0d95d68fb4 diff: emit_line_ws_markup() if/else style fix\n>   3:  c331218334 =  3:  c331218334 diff: correct suppress_blank_empty hack\n>   4:  be1473fc5a =  4:  be1473fc5a diff: fix incorrect counting of line numbers\n>   5:  7bcd6efba8 !  5:  9410e4257a diff: refactor output of incomplete line\n>      @@ Commit message\n>           the code path that handles xdiff output and the code path that\n>           bypasses xdiff and produces complete rewrite patch.\n>       \n>      +    Currently the output from the DIFF_SYMBOL_CONTEXT_INCOMPLETE case\n>      +    still (ab)uses the same code as what is used for context lines, but\n>      +    that would change in a later step where we introduce support for\n>      +    incomplete line detection.\n>      +\n>           Signed-off-by: Junio C Hamano <gitster@pobox.com>\n>       \n>        ## diff.c ##\n>      @@ diff.c: static void emit_context_line(struct emit_callback *ecbdata,\n>        \temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT, line, len, flags);\n>        }\n>        \n>      -+static void emit_incomplete_line(struct emit_callback *ecbdata,\n>      -+\t\t\t\t const char *line, int len)\n>      ++static void emit_incomplete_line_marker(struct emit_callback *ecbdata,\n>      ++\t\t\t\t\tconst char *line, int len)\n>       +{\n>       +\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n>       +\t\t\t line, len, 0);\n>      @@ diff.c: static int fn_out_consume(void *priv, char *line, unsigned long len)\n>        \t\t}\n>       -\t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n>       -\t\t\t\t line, len, 0);\n>      -+\t\temit_incomplete_line(ecbdata, line, len);\n>      ++\t\temit_incomplete_line_marker(ecbdata, line, len);\n>        \t\tbreak;\n>        \tdefault:\n>        \t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n>   6:  1a6f143377 =  6:  cdc6516009 diff: call emit_callback ecbdata everywhere\n>   7:  dfc810b1d6 !  7:  9acb9b6217 diff: update the way rewrite diff handles incomplete lines\n>      @@ diff.c: static void emit_rewrite_lines(struct emit_callback *ecbdata,\n>       +\t\tchar *pdata = NULL;\n>        \n>        \t\tendp = memchr(data, '\\n', size);\n>      - \t\tlen = endp ? (endp - data + 1) : size;\n>      -+\t\tplen = len;\n>      +-\t\tlen = endp ? (endp - data + 1) : size;\n>       +\n>      -+\t\tif (!endp) {\n>      ++\t\tif (endp) {\n>      ++\t\t\tlen = endp - data + 1;\n>      ++\t\t\tplen = len;\n>      ++\t\t} else {\n>      ++\t\t\tlen = size;\n>       +\t\t\tplen = len + 1;\n>       +\t\t\tpdata = xmalloc(plen + 2);\n>       +\t\t\tmemcpy(pdata, data, len);\n>      @@ diff.c: static void emit_rewrite_lines(struct emit_callback *ecbdata,\n>       +\tif (!endp) {\n>       +\t\tstatic const char nneof[] = \"\\\\ No newline at end of file\\n\";\n>       +\t\tecbdata->last_line_kind = prefix;\n>      -+\t\temit_incomplete_line(ecbdata, nneof, sizeof(nneof) - 1);\n>      ++\t\temit_incomplete_line_marker(ecbdata, nneof, sizeof(nneof) - 1);\n>       +\t}\n>        }\n>        \n>   8:  c66b547f13 =  8:  86c14ee62d apply: revamp the parsing of incomplete lines\n>   9:  bdc2dbbe4b =  9:  b62d4020e7 whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE\n> 10:  806aa30511 = 10:  081c21b14e apply: check and fix incomplete lines\n> 11:  0cfb6ab295 ! 11:  73182b19a8 diff: highlight and error out on incomplete lines\n>      @@ diff.c: static void emit_diff_symbol_from_struct(struct diff_options *o,\n>        \t\temit_line(o, set, reset, line, len);\n>        \t\tbreak;\n>       @@ diff.c: static void emit_context_line(struct emit_callback *ecbdata,\n>      - static void emit_incomplete_line(struct emit_callback *ecbdata,\n>      - \t\t\t\t const char *line, int len)\n>      + static void emit_incomplete_line_marker(struct emit_callback *ecbdata,\n>      + \t\t\t\t\tconst char *line, int len)\n>        {\n>       +\tint last_line_kind = ecbdata->last_line_kind;\n>       +\tunsigned flags = (last_line_kind == '+'\n>      @@ t/t4015-diff-whitespace.sh: do\n>       +'\n>       +\n>       +test_expect_success \"incomplete lines on both pre- and post-image\" '\n>      -+\t# The interpretation taken here is \"since you are toucing\n>      ++\t# The interpretation taken here is \"since you are touching\n>       +\t# the line anyway, you would better fix the incomplete line\n>       +\t# while you are at it.\"  but this is debatable.\n>       +\techo foo | tr -d \"\\012\" >x &&\n>       +\tgit add x &&\n>       +\techo bar | tr -d \"\\012\" >x &&\n>       +\tgit diff x &&\n>      -+\ttest_must_fail git -c core.whitespace=incomplete diff --check x &&\n>      ++\ttest_must_fail git -c core.whitespace=incomplete diff --check x >error &&\n>      ++\ttest_grep \"no newline at the end of file\" error &&\n>       +\tgit diff -R x &&\n>      -+\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x\n>      ++\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&\n>      ++\ttest_grep \"no newline at the end of file\" error\n>       +'\n>       +\n>       +test_expect_success \"fix incomplete line in pre-image\" '\n>      @@ t/t4015-diff-whitespace.sh: do\n>       +\tgit diff x &&\n>       +\tgit -c core.whitespace=incomplete diff --check x &&\n>       +\tgit diff -R x &&\n>      -+\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x\n>      ++\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&\n>      ++\ttest_grep \"no newline at the end of file\" error\n>       +'\n>       +\n>       +test_expect_success \"new incomplete line in post-image\" '\n>      @@ t/t4015-diff-whitespace.sh: do\n>       +\tgit add x &&\n>       +\techo bar | tr -d \"\\012\" >x &&\n>       +\tgit diff x &&\n>      -+\ttest_must_fail git -c core.whitespace=incomplete diff --check x &&\n>      ++\ttest_must_fail git -c core.whitespace=incomplete diff --check x >error &&\n>      ++\ttest_grep \"no newline at the end of file\" error &&\n>       +\tgit diff -R x &&\n>       +\tgit -c core.whitespace=incomplete diff -R --check x\n>       +'\n> 12:  33c5ae40db = 12:  85748701b4 attr: enable incomplete-line whitespace error for this project\n\n"},{"id":"530515","messageId":"xmqqbjl8y6rw.fsf@gitster.g","threadId":"64429","inReplyTo":"4506b9c3-f4ae-488c-988c-e12b2d95195f@gmail.com","subject":"Re: [PATCH 04/12] diff: fix incorrect counting of line numbers","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-11T14:37:39Z","receivedAt":"2025-11-11T14:37:42Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> On 10/11/2025 18:29, Junio C Hamano wrote:\n>> Phillip Wood <phillip.wood123@gmail.com> writes:\n>>> On 04/11/2025 02:09, Junio C Hamano wrote:\n>>>\n>>> The \"\\ No newline at end of file\" line is an annotation on the previous\n>>> line in the diff so why are we incrementing any {pre,post}image line\n>>> numbers here?\n>> \n>> No particular reason ;-)  As I said, I do not think these numbers\n>> are used after these lines are seen.  At least this change makes\n>> these unused data incremented in a more coherent way than the\n>> previous one, which unconditionally incremented the number for the\n>> preimage without even checking which side the \"\\ No newline\" is for.\n>\n> It maybe coherent but it is still wrong to increment the line numbers \n> here. To be correct we should remove the erroneous increment of \n> lno_in_postimage.\n\nAh, if your lno_in_postimage is not a typo for preimage side, then I\ncan buy that and it would be even safer than the version under\ndiscussion.  I do not think anybody has audited the code to be\nabsolutely sure that the existing increment for preimage line\nnumber, which we think is wrong in this discussion thread, is not\ncompensated by something else, which would make removal of the\nincrement break it, so in the absense of such an audit (and the\ntheme of this topic certainly is not about it), it is prudent to let\nthis sleeping dog lie.\n\nThe primary thing this step needed to do was to introduce the\nlast_line_kind member to the emit-callback structure and switch on\nits value to allow us to process \"\\ No newline\" differently\ndepending on what kind of line the previous line was.  The\n\"consistently increment on both sides\" was a \"while at it\" change\nthat does not have to be done and probably better left out.\n\nThanks.\n"},{"id":"530617","messageId":"20251112220258.1009253-1-gitster@pobox.com","threadId":"64429","inReplyTo":"20251111000451.2243195-1-gitster@pobox.com","subject":"[PATCH v4 00/12] Incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-12T22:02:46Z","receivedAt":"2025-11-12T22:03:01Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"One of the common kind of whitespace errors is to lack the final\nnewline at the end of a file, but so far, neither \"git diff\" or \"git\napply\" did anything about them.\n\nThis series introduces \"incomplete-line\" whitespace error class,\nthat you can add to either the core.whitespace configuration\nvariable, or the whitespace attribute in your .gitattributes files.\n\nThe class is disabled by default, so the final step enables it for\nour project by defining it in the .gitattributes file.\n\nThe incomplete line marker that is given for a context line is not\nconsidered an error.  The reasoning is that your preimage did have\nincomplete line, but you did not touch the contents on that\nincomplete line in your patch, so you left the line intact.  It is\nnot a new breakage you are responsible for.\n\nIf the incomplete line marker follows a postimage line, on the other\nhand, it means that you added a new line at the end of the file that\nis incomplete *and* that line did not exist in the preimage.  The\nlast line of the preimage may have been incomplete already, but then\nyou updated the contents on that line, so you could have easily\nfixed the incompleteness of the line while at it.  Either way, you\nare responsible for the incompleteness of the last ine in the\nresulting file.\n\nThe organization of the series is as follows.\n\n * The first patch [01/12] is a preliminary clean-up to reformat the\n   definition of bit assignments for whitespace error classes.\n\n * The patches [02/12] - [08/12] are preliminary clean-up made to\n   both \"git diff\" and \"git apply\" machinery.\n\n * The patch [09/12] shifts the bit assignment to make room for new\n   whitespace error class (which was last updated in 2007 IIRC, so\n   the set of whitespace errors surprisingly haven't changed for\n   quite some time), and defines the new \"incomplete-line\" class.\n\n * The patch [10/12] teaches \"git apply --whitespace=<mode>\" and\n   \"git apply --check\" about the incomplete-line error class.\n\n * The patch [11/12] teaches \"git diff [--check]\" about the\n   incomplete-line error class.\n\n * The final patch [12/12] enables the incomplete-line error class\n   for our project for C source files and shell scripts.  I didn't\n   touch the cover-all * entry.\n\n\nChanges in v4:\n\n - The preliminary clean-up for \"git diff\" in [04/12] no longer\n   \"corrects\" the code that updates the line number upon seeing\n   \"\\ No newline\" marker, and leaves it for later series to clean\n   it up as #leftoverbits.\n\n - Our house rule updates in [12/12] now forbids the documentation\n   sources *.adoc to end in an incomplete line.\n\n - Some typofixes in log messages.\n\nChanges in v3:\n\n - The proposed log message of [PATCH 05/12] explains that it\n   semi-duplicates the same code shared in two case arems in\n   preparation for later changes.\n\n - The internal helper function to emit the \"\\ No newline\" marker\n   line is now called emit_incomplete_line_marker().\n\n - Two conditionals in [PATCH 07/12] both of which switched on !endp\n   have been consolidated into a single if/else statement.\n\n - The tests in [PATCH 11/12] checks the output from \"diff --check\"\n   now.\n\n\nChanges in v2:\n\n - rolled the definition (but not implementation) of the new\n   \"incomplete-line\" class into step [09/12] that shifts the bit\n   assignment.  The documentation of core.whitespace has also be\n   updated in this step.\n\n - \"git apply --check\" miscounted line number reported for the\n   incomplete line error, which has been corrected in step [10/12].\n\n - t4124-apply-ws-rule.sh has been extended to cover \"git apply\n   --check\" and the diagnostic output from it in step [10/12].\n\nJunio C Hamano (12):\n  whitespace: correct bit assignment comments\n  diff: emit_line_ws_markup() if/else style fix\n  diff: correct suppress_blank_empty hack\n  diff: keep track of the type of the last line seen\n  diff: refactor output of incomplete line\n  diff: call emit_callback ecbdata everywhere\n  diff: update the way rewrite diff handles incomplete lines\n  apply: revamp the parsing of incomplete lines\n  whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE\n  apply: check and fix incomplete lines\n  diff: highlight and error out on incomplete lines\n  attr: enable incomplete-line whitespace error for this project\n\n .gitattributes                 |   6 +-\n Documentation/config/core.adoc |   2 +\n apply.c                        |  79 ++++++++++----\n diff.c                         | 145 ++++++++++++++++---------\n diff.h                         |   6 +-\n t/t4015-diff-whitespace.sh     |  67 +++++++++++-\n t/t4124-apply-ws-rule.sh       | 187 +++++++++++++++++++++++++++++++++\n ws.c                           |  20 ++++\n ws.h                           |  26 +++--\n 9 files changed, 450 insertions(+), 88 deletions(-)\n\n 1:  39ce594b48 =  1:  5486212662 whitespace: correct bit assignment comments\n 2:  7c53ac154b =  2:  89504329a4 diff: emit_line_ws_markup() if/else style fix\n 3:  239644a8b4 =  3:  78f55aebf6 diff: correct suppress_blank_empty hack\n 4:  a2c279cca0 !  4:  9f6b6ad756 diff: fix incorrect counting of line numbers\n    @@ Metadata\n     Author: Junio C Hamano <gitster@pobox.com>\n     \n      ## Commit message ##\n    -    diff: fix incorrect counting of line numbers\n    +    diff: keep track of the type of the last line seen\n     \n         The \"\\ No newline at the end of the file\" can come after any of the\n         \"-\" (deleted preimage line), \" \" (unchanged line), or \"+\" (added\n    -    postimage line).  Incrementing only the preimage line number upon\n    -    seeing it does not make any sense.\n    +    postimage line).  In later steps in this series, we will start\n    +    treating a change that makes a file to end in an incomplete line\n    +    as a whitespace error, and we would need to know what the previous\n    +    line was when we react to \"\\ No newline\" in the diff output.  If\n    +    the previous line was a context (i.e., unchanged) line, the file\n    +    lacked the final newline before the change, and the change did not\n    +    touch that line and left it still incomplete, so we do not want to\n    +    warn in such a case.\n     \n    -    We can keep track of what the previous line was, and increment\n    -    lno_in_{pre,post}image variables properly, like this patch does.  I\n    -    do not think it matters, as these numbers are used only to compare\n    -    them with blank_at_eof_in_{pre,post}image to issue the warning every\n    -    time we see an added line, but by definition, after we see \"\\ No\n    -    newline at the end of the file\" for an added line, we will not see\n    -    an added line for the file.\n    +    Teach fn_out_consume() function to keep track of what the previous\n    +    line was, and prepare an otherwise empty switch statement to let us\n    +    react differently to \"\\ No newline\" based on that.\n     \n    -    Keeping track of what the last line was (in other words, \"is it that\n    -    the file used to end in an incomplete line?  The file ends in an\n    -    incomplete line after the change?  Both the file before and after\n    -    the change ends in an incomplete line that did not change?\") will be\n    -    independently useful.\n    +    Note that there is an existing curiosity (read: likely to be a bug)\n    +    in the code that increments line number in the preimage file every\n    +    time it sees a line with \"\\ No newline\" on it, regardless of what\n    +    the previous line was.  I left it as-is, because it does not affect\n    +    the main theme of this series, and more importantly, I do not think\n    +    it matters, as these numbers are used only to compare them with\n    +    blank_at_eof_in_{pre,post}image to issue a warning when we see more\n    +    empty line was added at the end, but by definition, after we see\n    +    \"\\ No newline at the end of the file\" for an added line, we will not\n    +    see an added line for the file.\n    +\n    +    An independent audit to ensure that this curious increment can be\n    +    safely removed would make a good #leftoverbits clean-up (we may even\n    +    find some code that decrements this counter or over-increments the\n    +    other quantity this counter is compared with that compensates the\n    +    effect of this curious increment that hides a bug, in which case we\n    +    may also need to remove them).\n     \n         Signed-off-by: Junio C Hamano <gitster@pobox.com>\n     \n    @@ diff.c: static int fn_out_consume(void *priv, char *line, unsigned long len)\n      \t\tbreak;\n      \tcase '\\\\':\n      \t\t/* incomplete line at the end */\n    --\t\tecbdata->lno_in_preimage++;\n     +\t\tswitch (ecbdata->last_line_kind) {\n     +\t\tcase '+':\n    -+\t\t\tecbdata->lno_in_postimage++;\n    -+\t\t\tbreak;\n     +\t\tcase '-':\n    -+\t\t\tecbdata->lno_in_preimage++;\n    -+\t\t\tbreak;\n     +\t\tcase ' ':\n    -+\t\t\tecbdata->lno_in_preimage++;\n    -+\t\t\tecbdata->lno_in_postimage++;\n     +\t\t\tbreak;\n     +\t\tdefault:\n     +\t\t\tBUG(\"fn_out_consume: '\\\\No newline' after unknown line (%c)\",\n     +\t\t\t    ecbdata->last_line_kind);\n     +\t\t}\n    + \t\tecbdata->lno_in_preimage++;\n      \t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n      \t\t\t\t line, len, 0);\n    - \t\tbreak;\n    +@@ diff.c: static int fn_out_consume(void *priv, char *line, unsigned long len)\n      \tdefault:\n      \t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n      \t}\n 5:  7a91bf04be !  5:  16dda4fe13 diff: refactor output of incomplete line\n    @@ Commit message\n         Create a helper function that reacts to \"\\ No newline at the end of\n         file\" in preparation for unifying the incomplete line handling in\n         the code path that handles xdiff output and the code path that\n    -    bypasses xdiff and produces complete rewrite patch.\n    +    bypasses xdiff and produces a complete-rewrite patch.\n     \n         Currently the output from the DIFF_SYMBOL_CONTEXT_INCOMPLETE case\n         still (ab)uses the same code as what is used for context lines, but\n    -    that would change in a later step where we introduce support for\n    -    incomplete line detection.\n    +    that would change in a later step where we introduce support to treat\n    +    an incomplete line as a whitespace error.\n     \n         Signed-off-by: Junio C Hamano <gitster@pobox.com>\n     \n    @@ diff.c: static void emit_context_line(struct emit_callback *ecbdata,\n      \t\t\t     const char *line, int len)\n      {\n     @@ diff.c: static int fn_out_consume(void *priv, char *line, unsigned long len)\n    - \t\t\tBUG(\"fn_out_consume: '\\\\No newline' after unknown line (%c)\",\n      \t\t\t    ecbdata->last_line_kind);\n      \t\t}\n    + \t\tecbdata->lno_in_preimage++;\n     -\t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n     -\t\t\t\t line, len, 0);\n     +\t\temit_incomplete_line_marker(ecbdata, line, len);\n 6:  3f56e084f8 =  6:  dd310cc6dc diff: call emit_callback ecbdata everywhere\n 7:  af5b4c94e4 =  7:  3360f2882c diff: update the way rewrite diff handles incomplete lines\n 8:  e4945e8951 =  8:  ff29d34f3c apply: revamp the parsing of incomplete lines\n 9:  d4fd89039a =  9:  b720c4f49d whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE\n10:  075ef5d32f = 10:  5ddf90cd08 apply: check and fix incomplete lines\n11:  04401decef = 11:  cf52c68ec1 diff: highlight and error out on incomplete lines\n12:  068229790d ! 12:  cb86d9b45f attr: enable incomplete-line whitespace error for this project\n    @@ Commit message\n     \n         Now \"git diff --check\" and \"git apply --whitespace=warn/fix\" learned\n         incomplete line is a whitespace error, enable them for this project\n    -    to prevent patches to add new incomplete lines to our sources.\n    +    to prevent patches to add new incomplete lines to our source to both\n    +    code and documentation files.\n     \n         Signed-off-by: Junio C Hamano <gitster@pobox.com>\n     \n    @@ .gitattributes\n      *.perl text eol=lf diff=perl\n      *.pl text eof=lf diff=perl\n      *.pm text eol=lf diff=perl\n    + *.py text eol=lf diff=python\n    + *.bat text eol=crlf\n    + CODE_OF_CONDUCT.md -whitespace\n    +-/Documentation/**/*.adoc text eol=lf\n    ++/Documentation/**/*.adoc text eol=lf whitespace=!indent,trail,space,incomplete\n    + /command-list.txt text eol=lf\n    + /GIT-VERSION-GEN text eol=lf\n    + /mergetools/* text eol=lf\n\n\n-- \n2.52.0-rc2-441-g030905368a\n\n"},{"id":"530618","messageId":"20251112220258.1009253-2-gitster@pobox.com","threadId":"64429","inReplyTo":"20251112220258.1009253-1-gitster@pobox.com","subject":"[PATCH v4 01/12] whitespace: correct bit assignment comments","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-12T22:02:47Z","receivedAt":"2025-11-12T22:03:02Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"A comment in diff.c claimed that bits up to 12th (counting from 0th)\nare whitespace rules, and 13th thru 15th are for new/old/context,\nbut it turns out it was miscounting.  Correct them, and clarify\nwhere the whitespace rule bits come from in the comment.  Extend bit\nassignment comments to cover bits used for color-moved, which\nweren't described.\n\nAlso update the way these bit constants are defined to use (1 << N)\nnotation, instead of octal constants, as it tends to make it easier\nto notice a breakage like this.\n\nSprinkle a few blank lines between logically distinct groups of CPP\nmacro definitions to make them easier to read.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c |  7 +++++--\n diff.h |  6 +++---\n ws.h   | 25 ++++++++++++++-----------\n 3 files changed, 22 insertions(+), 16 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex a74e701806..74261b332a 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -801,16 +801,19 @@ enum diff_symbol {\n \tDIFF_SYMBOL_CONTEXT_MARKER,\n \tDIFF_SYMBOL_SEPARATOR\n };\n+\n /*\n  * Flags for content lines:\n- * 0..12 are whitespace rules\n- * 13-15 are WSEH_NEW | WSEH_OLD | WSEH_CONTEXT\n+ * 0..11 are whitespace rules (see ws.h)\n+ * 12..14 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD\n  * 16 is marking if the line is blank at EOF\n+ * 17..19 are used for color-moved.\n  */\n #define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF\t(1<<16)\n #define DIFF_SYMBOL_MOVED_LINE\t\t\t(1<<17)\n #define DIFF_SYMBOL_MOVED_LINE_ALT\t\t(1<<18)\n #define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING\t(1<<19)\n+\n #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)\n \n /*\ndiff --git a/diff.h b/diff.h\nindex 2fa256c3ef..cbd355cf50 100644\n--- a/diff.h\n+++ b/diff.h\n@@ -331,9 +331,9 @@ struct diff_options {\n \n \tint ita_invisible_in_index;\n /* white-space error highlighting */\n-#define WSEH_NEW (1<<12)\n-#define WSEH_CONTEXT (1<<13)\n-#define WSEH_OLD (1<<14)\n+#define WSEH_NEW        (1<<12)\n+#define WSEH_CONTEXT    (1<<13)\n+#define WSEH_OLD        (1<<14)\n \tunsigned ws_error_highlight;\n \tconst char *prefix;\n \tint prefix_length;\ndiff --git a/ws.h b/ws.h\nindex 5ba676c559..23708efb73 100644\n--- a/ws.h\n+++ b/ws.h\n@@ -7,19 +7,22 @@ struct strbuf;\n /*\n  * whitespace rules.\n  * used by both diff and apply\n- * last two digits are tab width\n+ * last two octal-digits are tab width (we support only up to 63).\n  */\n-#define WS_BLANK_AT_EOL         0100\n-#define WS_SPACE_BEFORE_TAB     0200\n-#define WS_INDENT_WITH_NON_TAB  0400\n-#define WS_CR_AT_EOL           01000\n-#define WS_BLANK_AT_EOF        02000\n-#define WS_TAB_IN_INDENT       04000\n-#define WS_TRAILING_SPACE      (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)\n+#define WS_BLANK_AT_EOL         (1<<6)\n+#define WS_SPACE_BEFORE_TAB     (1<<7)\n+#define WS_INDENT_WITH_NON_TAB  (1<<8)\n+#define WS_CR_AT_EOL            (1<<9)\n+#define WS_BLANK_AT_EOF         (1<<10)\n+#define WS_TAB_IN_INDENT        (1<<11)\n+\n+#define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)\n #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)\n-#define WS_TAB_WIDTH_MASK        077\n-/* All WS_* -- when extended, adapt diff.c emit_symbol */\n-#define WS_RULE_MASK           07777\n+#define WS_TAB_WIDTH_MASK       ((1<<6)-1)\n+\n+/* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */\n+#define WS_RULE_MASK            ((1<<12)-1)\n+\n extern unsigned whitespace_rule_cfg;\n unsigned whitespace_rule(struct index_state *, const char *);\n unsigned parse_whitespace_rule(const char *);\n-- \n2.52.0-rc2-441-g030905368a\n\n"},{"id":"530619","messageId":"20251112220258.1009253-3-gitster@pobox.com","threadId":"64429","inReplyTo":"20251112220258.1009253-1-gitster@pobox.com","subject":"[PATCH v4 02/12] diff: emit_line_ws_markup() if/else style fix","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-12T22:02:48Z","receivedAt":"2025-11-12T22:03:04Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Apply the simple rule: if you need {} in one arm of the if/else\nif/else... cascade, have {} in all of them.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 74261b332a..9a24a0791c 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1327,14 +1327,14 @@ static void emit_line_ws_markup(struct diff_options *o,\n \t\t\tws = NULL;\n \t}\n \n-\tif (!ws && !set_sign)\n+\tif (!ws && !set_sign) {\n \t\temit_line_0(o, set, NULL, 0, reset, sign, line, len);\n-\telse if (!ws) {\n+\t} else if (!ws) {\n \t\temit_line_0(o, set_sign, set, !!set_sign, reset, sign, line, len);\n-\t} else if (blank_at_eof)\n+\t} else if (blank_at_eof) {\n \t\t/* Blank line at EOF - paint '+' as well */\n \t\temit_line_0(o, ws, NULL, 0, reset, sign, line, len);\n-\telse {\n+\t} else {\n \t\t/* Emit just the prefix, then the rest. */\n \t\temit_line_0(o, set_sign ? set_sign : set, NULL, !!set_sign, reset,\n \t\t\t    sign, \"\", 0);\n-- \n2.52.0-rc2-441-g030905368a\n\n"},{"id":"530620","messageId":"20251112220258.1009253-4-gitster@pobox.com","threadId":"64429","inReplyTo":"20251112220258.1009253-1-gitster@pobox.com","subject":"[PATCH v4 03/12] diff: correct suppress_blank_empty hack","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-12T22:02:49Z","receivedAt":"2025-11-12T22:03:06Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The suppress-blank-empty feature abused the CONTEXT_INCOMPLETE\nsymbol that was meant to be used only for \"\\ No newline at the end\nof file\" code path.\n\nThe intent of the feature was to turn a context line we receive from\nxdiff machinery (which always uses ' ' for context lines, even an\nempty one) and spit it out as a truly empty line.\n\nPerform such a conversion very locally at where a line from xdiff\nthat begins with ' ' is handled for output; there are many checks\nbefore the control reaches such place that checks the first letter\nof the diff output line to see if it is a context line, and having\nto check for '\\n' and treat it as a special case is error prone.\n\nIn order to catch similar hacks in the future, make sure the code\npath that is meant for \"\\ No newline\" case checks the first byte is\nindeed a backslash.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 27 +++++++++++----------------\n 1 file changed, 11 insertions(+), 16 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 9a24a0791c..b9ef8550cc 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1321,6 +1321,11 @@ static void emit_line_ws_markup(struct diff_options *o,\n \tconst char *ws = NULL;\n \tint sign = o->output_indicators[sign_index];\n \n+\tif (diff_suppress_blank_empty &&\n+\t    sign_index == OUTPUT_INDICATOR_CONTEXT &&\n+\t    len == 1 && line[0] == '\\n')\n+\t\tsign = 0;\n+\n \tif (o->ws_error_highlight & ws_rule) {\n \t\tws = diff_get_color_opt(o, DIFF_WHITESPACE);\n \t\tif (!*ws)\n@@ -1498,15 +1503,9 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n \tcase DIFF_SYMBOL_WORDS:\n \t\tcontext = diff_get_color_opt(o, DIFF_CONTEXT);\n \t\treset = diff_get_color_opt(o, DIFF_RESET);\n-\t\t/*\n-\t\t * Skip the prefix character, if any.  With\n-\t\t * diff_suppress_blank_empty, there may be\n-\t\t * none.\n-\t\t */\n-\t\tif (line[0] != '\\n') {\n-\t\t\tline++;\n-\t\t\tlen--;\n-\t\t}\n+\n+\t\t/* Skip the prefix character */\n+\t\tline++; len--;\n \t\temit_line(o, context, reset, line, len);\n \t\tbreak;\n \tcase DIFF_SYMBOL_FILEPAIR_PLUS:\n@@ -2375,12 +2374,6 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \t\tecbdata->label_path[0] = ecbdata->label_path[1] = NULL;\n \t}\n \n-\tif (diff_suppress_blank_empty\n-\t    && len == 2 && line[0] == ' ' && line[1] == '\\n') {\n-\t\tline[0] = '\\n';\n-\t\tlen = 1;\n-\t}\n-\n \tif (line[0] == '@') {\n \t\tif (ecbdata->diff_words)\n \t\t\tdiff_words_flush(ecbdata);\n@@ -2431,12 +2424,14 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \t\tecbdata->lno_in_preimage++;\n \t\temit_context_line(ecbdata, line + 1, len - 1);\n \t\tbreak;\n-\tdefault:\n+\tcase '\\\\':\n \t\t/* incomplete line at the end */\n \t\tecbdata->lno_in_preimage++;\n \t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n \t\t\t\t line, len, 0);\n \t\tbreak;\n+\tdefault:\n+\t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n \t}\n \treturn 0;\n }\n-- \n2.52.0-rc2-441-g030905368a\n\n"},{"id":"530621","messageId":"20251112220258.1009253-5-gitster@pobox.com","threadId":"64429","inReplyTo":"20251112220258.1009253-1-gitster@pobox.com","subject":"[PATCH v4 04/12] diff: keep track of the type of the last line seen","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-12T22:02:50Z","receivedAt":"2025-11-12T22:03:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The \"\\ No newline at the end of the file\" can come after any of the\n\"-\" (deleted preimage line), \" \" (unchanged line), or \"+\" (added\npostimage line).  In later steps in this series, we will start\ntreating a change that makes a file to end in an incomplete line\nas a whitespace error, and we would need to know what the previous\nline was when we react to \"\\ No newline\" in the diff output.  If\nthe previous line was a context (i.e., unchanged) line, the file\nlacked the final newline before the change, and the change did not\ntouch that line and left it still incomplete, so we do not want to\nwarn in such a case.\n\nTeach fn_out_consume() function to keep track of what the previous\nline was, and prepare an otherwise empty switch statement to let us\nreact differently to \"\\ No newline\" based on that.\n\nNote that there is an existing curiosity (read: likely to be a bug)\nin the code that increments line number in the preimage file every\ntime it sees a line with \"\\ No newline\" on it, regardless of what\nthe previous line was.  I left it as-is, because it does not affect\nthe main theme of this series, and more importantly, I do not think\nit matters, as these numbers are used only to compare them with\nblank_at_eof_in_{pre,post}image to issue a warning when we see more\nempty line was added at the end, but by definition, after we see\n\"\\ No newline at the end of the file\" for an added line, we will not\nsee an added line for the file.\n\nAn independent audit to ensure that this curious increment can be\nsafely removed would make a good #leftoverbits clean-up (we may even\nfind some code that decrements this counter or over-increments the\nother quantity this counter is compared with that compensates the\neffect of this curious increment that hides a bug, in which case we\nmay also need to remove them).\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 11 +++++++++++\n 1 file changed, 11 insertions(+)\n\ndiff --git a/diff.c b/diff.c\nindex b9ef8550cc..ff8fc91f88 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -601,6 +601,7 @@ struct emit_callback {\n \tint blank_at_eof_in_postimage;\n \tint lno_in_preimage;\n \tint lno_in_postimage;\n+\tint last_line_kind;\n \tconst char **label_path;\n \tstruct diff_words_data *diff_words;\n \tstruct diff_options *opt;\n@@ -2426,6 +2427,15 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \t\tbreak;\n \tcase '\\\\':\n \t\t/* incomplete line at the end */\n+\t\tswitch (ecbdata->last_line_kind) {\n+\t\tcase '+':\n+\t\tcase '-':\n+\t\tcase ' ':\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tBUG(\"fn_out_consume: '\\\\No newline' after unknown line (%c)\",\n+\t\t\t    ecbdata->last_line_kind);\n+\t\t}\n \t\tecbdata->lno_in_preimage++;\n \t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n \t\t\t\t line, len, 0);\n@@ -2433,6 +2443,7 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \tdefault:\n \t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n \t}\n+\tecbdata->last_line_kind = line[0];\n \treturn 0;\n }\n \n-- \n2.52.0-rc2-441-g030905368a\n\n"},{"id":"530622","messageId":"20251112220258.1009253-6-gitster@pobox.com","threadId":"64429","inReplyTo":"20251112220258.1009253-1-gitster@pobox.com","subject":"[PATCH v4 05/12] diff: refactor output of incomplete line","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-12T22:02:51Z","receivedAt":"2025-11-12T22:03:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Create a helper function that reacts to \"\\ No newline at the end of\nfile\" in preparation for unifying the incomplete line handling in\nthe code path that handles xdiff output and the code path that\nbypasses xdiff and produces a complete-rewrite patch.\n\nCurrently the output from the DIFF_SYMBOL_CONTEXT_INCOMPLETE case\nstill (ab)uses the same code as what is used for context lines, but\nthat would change in a later step where we introduce support to treat\nan incomplete line as a whitespace error.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 14 ++++++++++++--\n 1 file changed, 12 insertions(+), 2 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex ff8fc91f88..7ee8620429 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1379,6 +1379,10 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n \t\temit_line(o, \"\", \"\", line, len);\n \t\tbreak;\n \tcase DIFF_SYMBOL_CONTEXT_INCOMPLETE:\n+\t\tset = diff_get_color_opt(o, DIFF_CONTEXT);\n+\t\treset = diff_get_color_opt(o, DIFF_RESET);\n+\t\temit_line(o, set, reset, line, len);\n+\t\tbreak;\n \tcase DIFF_SYMBOL_CONTEXT_MARKER:\n \t\tcontext = diff_get_color_opt(o, DIFF_CONTEXT);\n \t\treset = diff_get_color_opt(o, DIFF_RESET);\n@@ -1668,6 +1672,13 @@ static void emit_context_line(struct emit_callback *ecbdata,\n \temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT, line, len, flags);\n }\n \n+static void emit_incomplete_line_marker(struct emit_callback *ecbdata,\n+\t\t\t\t\tconst char *line, int len)\n+{\n+\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n+\t\t\t line, len, 0);\n+}\n+\n static void emit_hunk_header(struct emit_callback *ecbdata,\n \t\t\t     const char *line, int len)\n {\n@@ -2437,8 +2448,7 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)\n \t\t\t    ecbdata->last_line_kind);\n \t\t}\n \t\tecbdata->lno_in_preimage++;\n-\t\temit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n-\t\t\t\t line, len, 0);\n+\t\temit_incomplete_line_marker(ecbdata, line, len);\n \t\tbreak;\n \tdefault:\n \t\tBUG(\"fn_out_consume: unknown line '%s'\", line);\n-- \n2.52.0-rc2-441-g030905368a\n\n"},{"id":"530623","messageId":"20251112220258.1009253-7-gitster@pobox.com","threadId":"64429","inReplyTo":"20251112220258.1009253-1-gitster@pobox.com","subject":"[PATCH v4 06/12] diff: call emit_callback ecbdata everywhere","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-12T22:02:52Z","receivedAt":"2025-11-12T22:03:11Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Everybody else, except for emit_rewrite_lines(), calls the\nemit_callback data ecbdata.  Make sure we call the same thing by\nthe same name for consistency.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 12 ++++++------\n 1 file changed, 6 insertions(+), 6 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 7ee8620429..44b86544b7 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1780,7 +1780,7 @@ static void add_line_count(struct strbuf *out, int count)\n \t}\n }\n \n-static void emit_rewrite_lines(struct emit_callback *ecb,\n+static void emit_rewrite_lines(struct emit_callback *ecbdata,\n \t\t\t       int prefix, const char *data, int size)\n {\n \tconst char *endp = NULL;\n@@ -1791,17 +1791,17 @@ static void emit_rewrite_lines(struct emit_callback *ecb,\n \t\tendp = memchr(data, '\\n', size);\n \t\tlen = endp ? (endp - data + 1) : size;\n \t\tif (prefix != '+') {\n-\t\t\tecb->lno_in_preimage++;\n-\t\t\temit_del_line(ecb, data, len);\n+\t\t\tecbdata->lno_in_preimage++;\n+\t\t\temit_del_line(ecbdata, data, len);\n \t\t} else {\n-\t\t\tecb->lno_in_postimage++;\n-\t\t\temit_add_line(ecb, data, len);\n+\t\t\tecbdata->lno_in_postimage++;\n+\t\t\temit_add_line(ecbdata, data, len);\n \t\t}\n \t\tsize -= len;\n \t\tdata += len;\n \t}\n \tif (!endp)\n-\t\temit_diff_symbol(ecb->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);\n+\t\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);\n }\n \n static void emit_rewrite_diff(const char *name_a,\n-- \n2.52.0-rc2-441-g030905368a\n\n"},{"id":"530624","messageId":"20251112220258.1009253-8-gitster@pobox.com","threadId":"64429","inReplyTo":"20251112220258.1009253-1-gitster@pobox.com","subject":"[PATCH v4 07/12] diff: update the way rewrite diff handles incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-12T22:02:53Z","receivedAt":"2025-11-12T22:03:13Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The diff_symbol based output framework uses one DIFF_SYMBOL_* enum\nvalue per the kind of output lines of \"git diff\", which corresponds\nto one output line from the xdiff machinery used internally.  Most\nnotably, DIFF_SYMBOL_PLUS and DIFF_SYMBOL_MINUS that correspond to\n\"+\" and \"-\" lines are designed to always take a complete line, even\nif the output from xdiff machinery may produce \"\\ No newline at the\nend of file\" immediately after them.\n\nBut this is not true in the rewrite-diff codepath, which completely\nbypasses the xdiff machinery.  Since the code path feeds the bytes\ndirectly from the payload to the output routines, the output layer\nhas to deal with an incomplete line with DIFF_SYMBOL_PLUS and\nDIFF_SYMBOL_MINUS, which never would see an incomplete line in the\nnormal code paths.  This lack of final newline is compensated by an\nugly hack for a fabricated DIFF_SYMBOL_NO_LF_EOF token to inject an\nextra newline to the output to simulate output coming from the xdiff\nmachinery.\n\nRevamp the way the complete-rewrite code path feeds the lines to the\noutput layer by treating the last line of the pre/post image when it\nis an incomplete line specially.\n\nThis lets us remove the DIFF_SYMBOL_NO_LF_EOF hack and use the usual\nDIFF_SYMBOL_CONTEXT_INCOMPLETE code path, which will later learn how\nto handle whitespace errors.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c | 37 ++++++++++++++++++++++---------------\n 1 file changed, 22 insertions(+), 15 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 44b86544b7..5c606409bb 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -797,7 +797,6 @@ enum diff_symbol {\n \tDIFF_SYMBOL_CONTEXT_INCOMPLETE,\n \tDIFF_SYMBOL_PLUS,\n \tDIFF_SYMBOL_MINUS,\n-\tDIFF_SYMBOL_NO_LF_EOF,\n \tDIFF_SYMBOL_CONTEXT_FRAGINFO,\n \tDIFF_SYMBOL_CONTEXT_MARKER,\n \tDIFF_SYMBOL_SEPARATOR\n@@ -1352,7 +1351,6 @@ static void emit_line_ws_markup(struct diff_options *o,\n static void emit_diff_symbol_from_struct(struct diff_options *o,\n \t\t\t\t\t struct emitted_diff_symbol *eds)\n {\n-\tstatic const char *nneof = \" No newline at end of file\\n\";\n \tconst char *context, *reset, *set, *set_sign, *meta, *fraginfo;\n \n \tenum diff_symbol s = eds->s;\n@@ -1361,13 +1359,6 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n \tunsigned flags = eds->flags;\n \n \tswitch (s) {\n-\tcase DIFF_SYMBOL_NO_LF_EOF:\n-\t\tcontext = diff_get_color_opt(o, DIFF_CONTEXT);\n-\t\treset = diff_get_color_opt(o, DIFF_RESET);\n-\t\tputc('\\n', o->file);\n-\t\temit_line_0(o, context, NULL, 0, reset, '\\\\',\n-\t\t\t    nneof, strlen(nneof));\n-\t\tbreak;\n \tcase DIFF_SYMBOL_SUBMODULE_HEADER:\n \tcase DIFF_SYMBOL_SUBMODULE_ERROR:\n \tcase DIFF_SYMBOL_SUBMODULE_PIPETHROUGH:\n@@ -1786,22 +1777,38 @@ static void emit_rewrite_lines(struct emit_callback *ecbdata,\n \tconst char *endp = NULL;\n \n \twhile (0 < size) {\n-\t\tint len;\n+\t\tint len, plen;\n+\t\tchar *pdata = NULL;\n \n \t\tendp = memchr(data, '\\n', size);\n-\t\tlen = endp ? (endp - data + 1) : size;\n+\n+\t\tif (endp) {\n+\t\t\tlen = endp - data + 1;\n+\t\t\tplen = len;\n+\t\t} else {\n+\t\t\tlen = size;\n+\t\t\tplen = len + 1;\n+\t\t\tpdata = xmalloc(plen + 2);\n+\t\t\tmemcpy(pdata, data, len);\n+\t\t\tpdata[len] = '\\n';\n+\t\t\tpdata[len + 1] = '\\0';\n+\t\t}\n \t\tif (prefix != '+') {\n \t\t\tecbdata->lno_in_preimage++;\n-\t\t\temit_del_line(ecbdata, data, len);\n+\t\t\temit_del_line(ecbdata, pdata ? pdata : data, plen);\n \t\t} else {\n \t\t\tecbdata->lno_in_postimage++;\n-\t\t\temit_add_line(ecbdata, data, len);\n+\t\t\temit_add_line(ecbdata, pdata ? pdata : data, plen);\n \t\t}\n+\t\tfree(pdata);\n \t\tsize -= len;\n \t\tdata += len;\n \t}\n-\tif (!endp)\n-\t\temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);\n+\tif (!endp) {\n+\t\tstatic const char nneof[] = \"\\\\ No newline at end of file\\n\";\n+\t\tecbdata->last_line_kind = prefix;\n+\t\temit_incomplete_line_marker(ecbdata, nneof, sizeof(nneof) - 1);\n+\t}\n }\n \n static void emit_rewrite_diff(const char *name_a,\n-- \n2.52.0-rc2-441-g030905368a\n\n"},{"id":"530626","messageId":"20251112220258.1009253-9-gitster@pobox.com","threadId":"64429","inReplyTo":"20251112220258.1009253-1-gitster@pobox.com","subject":"[PATCH v4 08/12] apply: revamp the parsing of incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-12T22:02:54Z","receivedAt":"2025-11-12T22:03:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"A patch file represents the incomplete line at the end of the file\nwith two lines, one that is the usual \"context\" with \" \" as the\nfirst letter, \"added\" with \"+\" as the first letter, or \"removed\"\nwith \"-\" as the first letter that shows the content of the line,\nplus an extra \"\\ No newline at the end of file\" line that comes\nimmediately after it.\n\nEver since the apply machinery was written, the \"git apply\"\nmachinery parses \"\\ No newline at the end of file\" line\nindependently, without even knowing what line the incomplete-ness\napplies to, simply because it does not even remember what the\nprevious line was.\n\nThis poses a problem if we want to check and warn on an incomplete\nline.  Revamp the code that parses a fragment, to actually drop the\n'\\n' at the end of the incoming patch file that terminates a line,\nso that check_whitespace() calls made from the code path actually\nsees an incomplete as incomplete.\n\nNote that the result of this parsing is not directly used by the\ncode path that applies the patch.  apply_one_fragment() function\nalready checks if each of the patch text it handles is followed by a\nline that begins with a backslash to drop the newline at the end of\nthe current line it is looking at.  In a sense, this patch harmonizes\nthe behaviour of the parsing side to what is already done in the\napplication side.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n apply.c | 70 ++++++++++++++++++++++++++++++++++++++++-----------------\n 1 file changed, 49 insertions(+), 21 deletions(-)\n\ndiff --git a/apply.c b/apply.c\nindex a2ceb3fb40..2b0f8bdab5 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -1670,6 +1670,35 @@ static void check_old_for_crlf(struct patch *patch, const char *line, int len)\n }\n \n \n+/*\n+ * Just saw a single line in a fragment.  If it is a part of this hunk\n+ * that is a context \" \", an added \"+\", or a removed \"-\" line, it may\n+ * be followed by \"\\\\ No newline...\" to signal that the last \"\\n\" on\n+ * this line needs to be dropped.  Depending on locale settings when\n+ * the patch was produced we don't know what this line would exactly\n+ * say. The only thing we do know is that it begins with \"\\ \".\n+ * Checking for 12 is just for sanity check; \"\\ No newline...\" would\n+ * be at least that long in any l10n.\n+ *\n+ * Return 0 if the line we saw is not followed by \"\\ No newline...\",\n+ * or length of that line.  The caller will use it to skip over the\n+ * \"\\ No newline...\" line.\n+ */\n+static int adjust_incomplete(const char *line, int len,\n+\t\t\t     unsigned long size)\n+{\n+\tint nextlen;\n+\n+\tif (*line != '\\n' && *line != ' ' && *line != '+' && *line != '-')\n+\t\treturn 0;\n+\tif (size - len < 12 || memcmp(line + len, \"\\\\ \", 2))\n+\t\treturn 0;\n+\tnextlen = linelen(line + len, size - len);\n+\tif (nextlen < 12)\n+\t\treturn 0;\n+\treturn nextlen;\n+}\n+\n /*\n  * Parse a unified diff. Note that this really needs to parse each\n  * fragment separately, since the only way to know the difference\n@@ -1684,6 +1713,7 @@ static int parse_fragment(struct apply_state *state,\n {\n \tint added, deleted;\n \tint len = linelen(line, size), offset;\n+\tint skip_len = 0;\n \tunsigned long oldlines, newlines;\n \tunsigned long leading, trailing;\n \n@@ -1710,6 +1740,22 @@ static int parse_fragment(struct apply_state *state,\n \t\tlen = linelen(line, size);\n \t\tif (!len || line[len-1] != '\\n')\n \t\t\treturn -1;\n+\n+\t\t/*\n+\t\t * For an incomplete line, skip_len counts the bytes\n+\t\t * on \"\\\\ No newline...\" marker line that comes next\n+\t\t * to the current line.\n+\t\t *\n+\t\t * Reduce \"len\" to drop the newline at the end of\n+\t\t * line[], but add one to \"skip_len\", which will be\n+\t\t * added back to \"len\" for the next iteration, to\n+\t\t * compensate.\n+\t\t */\n+\t\tskip_len = adjust_incomplete(line, len, size);\n+\t\tif (skip_len) {\n+\t\t\tlen--;\n+\t\t\tskip_len++;\n+\t\t}\n \t\tswitch (*line) {\n \t\tdefault:\n \t\t\treturn -1;\n@@ -1745,20 +1791,10 @@ static int parse_fragment(struct apply_state *state,\n \t\t\tnewlines--;\n \t\t\ttrailing = 0;\n \t\t\tbreak;\n-\n-\t\t/*\n-\t\t * We allow \"\\ No newline at end of file\". Depending\n-\t\t * on locale settings when the patch was produced we\n-\t\t * don't know what this line looks like. The only\n-\t\t * thing we do know is that it begins with \"\\ \".\n-\t\t * Checking for 12 is just for sanity check -- any\n-\t\t * l10n of \"\\ No newline...\" is at least that long.\n-\t\t */\n-\t\tcase '\\\\':\n-\t\t\tif (len < 12 || memcmp(line, \"\\\\ \", 2))\n-\t\t\t\treturn -1;\n-\t\t\tbreak;\n \t\t}\n+\n+\t\t/* eat the \"\\\\ No newline...\" as well, if exists */\n+\t\tlen += skip_len;\n \t}\n \tif (oldlines || newlines)\n \t\treturn -1;\n@@ -1768,14 +1804,6 @@ static int parse_fragment(struct apply_state *state,\n \tfragment->leading = leading;\n \tfragment->trailing = trailing;\n \n-\t/*\n-\t * If a fragment ends with an incomplete line, we failed to include\n-\t * it in the above loop because we hit oldlines == newlines == 0\n-\t * before seeing it.\n-\t */\n-\tif (12 < size && !memcmp(line, \"\\\\ \", 2))\n-\t\toffset += linelen(line, size);\n-\n \tpatch->lines_added += added;\n \tpatch->lines_deleted += deleted;\n \n-- \n2.52.0-rc2-441-g030905368a\n\n"},{"id":"530625","messageId":"20251112220258.1009253-10-gitster@pobox.com","threadId":"64429","inReplyTo":"20251112220258.1009253-1-gitster@pobox.com","subject":"[PATCH v4 09/12] whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-12T22:02:55Z","receivedAt":"2025-11-12T22:03:16Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Reserve a few more bits in the diff flags word to be used for future\nwhitespace rules.  Add WS_INCOMPLETE_LINE without implementing the\nbehaviour (yet).\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/config/core.adoc |  2 ++\n diff.c                         | 16 ++++++++--------\n diff.h                         |  6 +++---\n ws.c                           |  6 ++++++\n ws.h                           |  3 ++-\n 5 files changed, 21 insertions(+), 12 deletions(-)\n\ndiff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc\nindex e2de270c86..682fb595fb 100644\n--- a/Documentation/config/core.adoc\n+++ b/Documentation/config/core.adoc\n@@ -626,6 +626,8 @@ core.whitespace::\n   part of the line terminator, i.e. with it, `trailing-space`\n   does not trigger if the character before such a carriage-return\n   is not a whitespace (not enabled by default).\n+* `incomplete-line` treats the last line of a file that is missing the\n+  newline at the end as an error (not enabled by default).\n * `tabwidth=<n>` tells how many character positions a tab occupies; this\n   is relevant for `indent-with-non-tab` and when Git fixes `tab-in-indent`\n   errors. The default tab width is 8. Allowed values are 1 to 63.\ndiff --git a/diff.c b/diff.c\nindex 5c606409bb..1b27b15f84 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -804,15 +804,15 @@ enum diff_symbol {\n \n /*\n  * Flags for content lines:\n- * 0..11 are whitespace rules (see ws.h)\n- * 12..14 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD\n- * 16 is marking if the line is blank at EOF\n- * 17..19 are used for color-moved.\n+ * 0..15 are whitespace rules (see ws.h)\n+ * 16..18 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD\n+ * 19 is marking if the line is blank at EOF\n+ * 20..22 are used for color-moved.\n  */\n-#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF\t(1<<16)\n-#define DIFF_SYMBOL_MOVED_LINE\t\t\t(1<<17)\n-#define DIFF_SYMBOL_MOVED_LINE_ALT\t\t(1<<18)\n-#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING\t(1<<19)\n+#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF\t(1<<19)\n+#define DIFF_SYMBOL_MOVED_LINE\t\t\t(1<<20)\n+#define DIFF_SYMBOL_MOVED_LINE_ALT\t\t(1<<21)\n+#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING\t(1<<22)\n \n #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)\n \ndiff --git a/diff.h b/diff.h\nindex cbd355cf50..422658407d 100644\n--- a/diff.h\n+++ b/diff.h\n@@ -331,9 +331,9 @@ struct diff_options {\n \n \tint ita_invisible_in_index;\n /* white-space error highlighting */\n-#define WSEH_NEW        (1<<12)\n-#define WSEH_CONTEXT    (1<<13)\n-#define WSEH_OLD        (1<<14)\n+#define WSEH_NEW        (1<<16)\n+#define WSEH_CONTEXT    (1<<17)\n+#define WSEH_OLD        (1<<18)\n \tunsigned ws_error_highlight;\n \tconst char *prefix;\n \tint prefix_length;\ndiff --git a/ws.c b/ws.c\nindex 70acee3337..34a7b4fad2 100644\n--- a/ws.c\n+++ b/ws.c\n@@ -26,6 +26,7 @@ static struct whitespace_rule {\n \t{ \"blank-at-eol\", WS_BLANK_AT_EOL, 0 },\n \t{ \"blank-at-eof\", WS_BLANK_AT_EOF, 0 },\n \t{ \"tab-in-indent\", WS_TAB_IN_INDENT, 0, 1 },\n+\t{ \"incomplete-line\", WS_INCOMPLETE_LINE, 0, 0 },\n };\n \n unsigned parse_whitespace_rule(const char *string)\n@@ -139,6 +140,11 @@ char *whitespace_error_string(unsigned ws)\n \t\t\tstrbuf_addstr(&err, \", \");\n \t\tstrbuf_addstr(&err, \"tab in indent\");\n \t}\n+\tif (ws & WS_INCOMPLETE_LINE) {\n+\t\tif (err.len)\n+\t\t\tstrbuf_addstr(&err, \", \");\n+\t\tstrbuf_addstr(&err, \"no newline at the end of file\");\n+\t}\n \treturn strbuf_detach(&err, NULL);\n }\n \ndiff --git a/ws.h b/ws.h\nindex 23708efb73..06d5cb73f8 100644\n--- a/ws.h\n+++ b/ws.h\n@@ -15,13 +15,14 @@ struct strbuf;\n #define WS_CR_AT_EOL            (1<<9)\n #define WS_BLANK_AT_EOF         (1<<10)\n #define WS_TAB_IN_INDENT        (1<<11)\n+#define WS_INCOMPLETE_LINE      (1<<12)\n \n #define WS_TRAILING_SPACE       (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)\n #define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)\n #define WS_TAB_WIDTH_MASK       ((1<<6)-1)\n \n /* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */\n-#define WS_RULE_MASK            ((1<<12)-1)\n+#define WS_RULE_MASK            ((1<<16)-1)\n \n extern unsigned whitespace_rule_cfg;\n unsigned whitespace_rule(struct index_state *, const char *);\n-- \n2.52.0-rc2-441-g030905368a\n\n"},{"id":"530627","messageId":"20251112220258.1009253-11-gitster@pobox.com","threadId":"64429","inReplyTo":"20251112220258.1009253-1-gitster@pobox.com","subject":"[PATCH v4 10/12] apply: check and fix incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-12T22:02:56Z","receivedAt":"2025-11-12T22:03:18Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The final line of a file that lacks the terminating newline at its\nend is called an incomplete line.  In general they are frowned upon\nfor many reasons (imagine concatenating two files with \"cat A B\" and\nwhat happens when A ends in an incomplete line, for example), and\ntext-oriented tools often mishandle such a line.\n\nImplement checks in \"git apply\" for incomplete lines, which is off\nby default for backward compatibility's sake, so that \"git apply\n--whitespace={fix,warn,error}\" can notice, warn against, and fix\nthem.\n\nAs one of the new test shows, if you modify contents on an\nincomplete line in the original and leave the resulting line\nincomplete, it is still considered a whitespace error, the reasoning\nbeing that \"you'd better fix it while at it if you are making a\nchange on an incomplete line anyway\", which may controversial.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n apply.c                  |  13 ++-\n t/t4124-apply-ws-rule.sh | 187 +++++++++++++++++++++++++++++++++++++++\n ws.c                     |  14 +++\n 3 files changed, 213 insertions(+), 1 deletion(-)\n\ndiff --git a/apply.c b/apply.c\nindex 2b0f8bdab5..c9fb45247d 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -1640,6 +1640,14 @@ static void record_ws_error(struct apply_state *state,\n \t    state->squelch_whitespace_errors < state->whitespace_error)\n \t\treturn;\n \n+\t/*\n+\t * line[len] for an incomplete line points at the \"\\n\" at the end\n+\t * of patch input line, so \"%.*s\" would drop the last letter on line;\n+\t * compensate for it.\n+\t */\n+\tif (result & WS_INCOMPLETE_LINE)\n+\t\tlen++;\n+\n \terr = whitespace_error_string(result);\n \tif (state->apply_verbosity > verbosity_silent)\n \t\tfprintf(stderr, \"%s:%d: %s.\\n%.*s\\n\",\n@@ -1794,7 +1802,10 @@ static int parse_fragment(struct apply_state *state,\n \t\t}\n \n \t\t/* eat the \"\\\\ No newline...\" as well, if exists */\n-\t\tlen += skip_len;\n+\t\tif (skip_len) {\n+\t\t\tlen += skip_len;\n+\t\t\tstate->linenr++;\n+\t\t}\n \t}\n \tif (oldlines || newlines)\n \t\treturn -1;\ndiff --git a/t/t4124-apply-ws-rule.sh b/t/t4124-apply-ws-rule.sh\nindex 485c7d2d12..115a0f8579 100755\n--- a/t/t4124-apply-ws-rule.sh\n+++ b/t/t4124-apply-ws-rule.sh\n@@ -556,4 +556,191 @@ test_expect_success 'whitespace check skipped for excluded paths' '\n \tgit apply --include=used --stat --whitespace=error <patch\n '\n \n+test_expect_success 'check incomplete lines (setup)' '\n+\trm -f .gitattributes &&\n+\tgit config core.whitespace incomplete-line\n+'\n+\n+test_expect_success 'incomplete context line (not an error)' '\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\t(test_write_lines 1 2 3 0 5 && printf 6) >sample2-i &&\n+\tcat sample-i >target &&\n+\tgit add target &&\n+\tcat sample2-i >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error <patch &&\n+\ttest_cmp sample2-i target &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error --check <patch 2>error &&\n+\ttest_cmp sample-i target &&\n+\ttest_must_be_empty error &&\n+\n+\tcat sample2-i >target &&\n+\tgit apply --whitespace=error -R <patch &&\n+\ttest_cmp sample-i target &&\n+\n+\tcat sample2-i >target &&\n+\tgit apply -R --whitespace=error --check <patch 2>error &&\n+\ttest_cmp sample2-i target &&\n+\ttest_must_be_empty error\n+'\n+\n+test_expect_success 'last line made incomplete (error)' '\n+\ttest_write_lines 1 2 3 4 5 6 >sample &&\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\tcat sample >target &&\n+\tgit add target &&\n+\tcat sample-i >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample >target &&\n+\ttest_must_fail git apply --whitespace=error <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample >target &&\n+\ttest_must_fail git apply --whitespace=error --check <patch 2>actual &&\n+\ttest_cmp sample target &&\n+\tcat >expect <<-\\EOF &&\n+\t<stdin>:10: no newline at the end of file.\n+\t6\n+\terror: 1 line adds whitespace errors.\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error -R <patch &&\n+\ttest_cmp sample target &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error --check -R <patch 2>error &&\n+\ttest_cmp sample-i target &&\n+\ttest_must_be_empty error &&\n+\n+\tcat sample >target &&\n+\tgit apply --whitespace=fix <patch &&\n+\ttest_cmp sample target\n+'\n+\n+test_expect_success 'incomplete line removed at the end (not an error)' '\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\ttest_write_lines 1 2 3 4 5 6 >sample &&\n+\tcat sample-i >target &&\n+\tgit add target &&\n+\tcat sample >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error <patch &&\n+\ttest_cmp sample target &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error --check <patch 2>error &&\n+\ttest_cmp sample-i target &&\n+\ttest_must_be_empty error &&\n+\n+\tcat sample >target &&\n+\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample >target &&\n+\ttest_must_fail git apply --whitespace=error --check -R <patch 2>actual &&\n+\ttest_cmp sample target &&\n+\tcat >expect <<-\\EOF &&\n+\t<stdin>:9: no newline at the end of file.\n+\t6\n+\terror: 1 line adds whitespace errors.\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tcat sample >target &&\n+\tgit apply --whitespace=fix -R <patch &&\n+\ttest_cmp sample target\n+'\n+\n+test_expect_success 'incomplete line corrected at the end (not an error)' '\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\ttest_write_lines 1 2 3 4 5 7 >sample3 &&\n+\tcat sample-i >target &&\n+\tgit add target &&\n+\tcat sample3 >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error <patch &&\n+\ttest_cmp sample3 target &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=error --check <patch 2>error &&\n+\ttest_cmp sample-i target &&\n+\ttest_must_be_empty error &&\n+\n+\tcat sample3 >target &&\n+\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample3 >target &&\n+\ttest_must_fail git apply --whitespace=error -R --check <patch 2>actual &&\n+\ttest_cmp sample3 target &&\n+\tcat >expect <<-\\EOF &&\n+\t<stdin>:9: no newline at the end of file.\n+\t6\n+\terror: 1 line adds whitespace errors.\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tcat sample3 >target &&\n+\tgit apply --whitespace=fix -R <patch &&\n+\ttest_cmp sample target\n+'\n+\n+test_expect_success 'incomplete line modified at the end (error)' '\n+\t(test_write_lines 1 2 3 4 5 && printf 6) >sample-i &&\n+\t(test_write_lines 1 2 3 4 5 && printf 7) >sample3-i &&\n+\ttest_write_lines 1 2 3 4 5 6 >sample &&\n+\ttest_write_lines 1 2 3 4 5 7 >sample3 &&\n+\tcat sample-i >target &&\n+\tgit add target &&\n+\tcat sample3-i >target &&\n+\tgit diff-files -p target >patch &&\n+\n+\tcat sample-i >target &&\n+\ttest_must_fail git apply --whitespace=error <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample-i >target &&\n+\ttest_must_fail git apply --whitespace=error --check <patch 2>actual &&\n+\ttest_cmp sample-i target &&\n+\tcat >expect <<-\\EOF &&\n+\t<stdin>:11: no newline at the end of file.\n+\t7\n+\terror: 1 line adds whitespace errors.\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tcat sample3-i >target &&\n+\ttest_must_fail git apply --whitespace=error -R <patch 2>error &&\n+\ttest_grep \"no newline\" error &&\n+\n+\tcat sample3-i >target &&\n+\ttest_must_fail git apply --whitespace=error --check -R <patch 2>actual &&\n+\ttest_cmp sample3-i target &&\n+\tcat >expect <<-\\EOF &&\n+\t<stdin>:9: no newline at the end of file.\n+\t6\n+\terror: 1 line adds whitespace errors.\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tcat sample-i >target &&\n+\tgit apply --whitespace=fix <patch &&\n+\ttest_cmp sample3 target &&\n+\n+\tcat sample3-i >target &&\n+\tgit apply --whitespace=fix -R <patch &&\n+\ttest_cmp sample target\n+'\n+\n test_done\ndiff --git a/ws.c b/ws.c\nindex 34a7b4fad2..6cc2466c0c 100644\n--- a/ws.c\n+++ b/ws.c\n@@ -186,6 +186,9 @@ static unsigned ws_check_emit_1(const char *line, int len, unsigned ws_rule,\n \tif (trailing_whitespace == -1)\n \t\ttrailing_whitespace = len;\n \n+\tif (!trailing_newline && (ws_rule & WS_INCOMPLETE_LINE))\n+\t\tresult |= WS_INCOMPLETE_LINE;\n+\n \t/* Check indentation */\n \tfor (i = 0; i < trailing_whitespace; i++) {\n \t\tif (line[i] == ' ')\n@@ -297,6 +300,17 @@ void ws_fix_copy(struct strbuf *dst, const char *src, int len, unsigned ws_rule,\n \tint last_space_in_indent = -1;\n \tint need_fix_leading_space = 0;\n \n+\t/*\n+\t * Remembering that we need to add '\\n' at the end\n+\t * is sufficient to fix an incomplete line.\n+\t */\n+\tif (ws_rule & WS_INCOMPLETE_LINE) {\n+\t\tif (0 < len && src[len - 1] != '\\n') {\n+\t\t\tfixed = 1;\n+\t\t\tadd_nl_to_tail = 1;\n+\t\t}\n+\t}\n+\n \t/*\n \t * Strip trailing whitespace\n \t */\n-- \n2.52.0-rc2-441-g030905368a\n\n"},{"id":"530628","messageId":"20251112220258.1009253-12-gitster@pobox.com","threadId":"64429","inReplyTo":"20251112220258.1009253-1-gitster@pobox.com","subject":"[PATCH v4 11/12] diff: highlight and error out on incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-12T22:02:57Z","receivedAt":"2025-11-12T22:03:20Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Teach \"git diff\" to highlight \"\\ No newline at end of file\" message\nas a whitespace error when incomplete-line whitespace error class is\nin effect.  Thanks to the previous refactoring of complete rewrite\ncode path, we can do this at a single place.\n\nUnlike whitespace errors in the payload where we need to annotate in\nline, possibly using colors, the line that has whitespace problems,\nwe have a dedicated line already that can serve as the error\nmessage, so paint it as a whitespace error message.\n\nAlso teach \"git diff --check\" to notice incomplete lines as\nwhitespace errors and report when incomplete-line whitespace error\nclass is in effect.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c                     | 29 +++++++++++++++--\n t/t4015-diff-whitespace.sh | 67 +++++++++++++++++++++++++++++++++++---\n 2 files changed, 90 insertions(+), 6 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 1b27b15f84..7b7cd50dc2 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -1370,7 +1370,11 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,\n \t\temit_line(o, \"\", \"\", line, len);\n \t\tbreak;\n \tcase DIFF_SYMBOL_CONTEXT_INCOMPLETE:\n-\t\tset = diff_get_color_opt(o, DIFF_CONTEXT);\n+\t\tif ((flags & WS_INCOMPLETE_LINE) &&\n+\t\t    (flags & o->ws_error_highlight))\n+\t\t\tset = diff_get_color_opt(o, DIFF_WHITESPACE);\n+\t\telse\n+\t\t\tset = diff_get_color_opt(o, DIFF_CONTEXT);\n \t\treset = diff_get_color_opt(o, DIFF_RESET);\n \t\temit_line(o, set, reset, line, len);\n \t\tbreak;\n@@ -1666,8 +1670,14 @@ static void emit_context_line(struct emit_callback *ecbdata,\n static void emit_incomplete_line_marker(struct emit_callback *ecbdata,\n \t\t\t\t\tconst char *line, int len)\n {\n+\tint last_line_kind = ecbdata->last_line_kind;\n+\tunsigned flags = (last_line_kind == '+'\n+\t\t\t  ? WSEH_NEW\n+\t\t\t  : last_line_kind == '-'\n+\t\t\t  ? WSEH_OLD\n+\t\t\t  : WSEH_CONTEXT) | ecbdata->ws_rule;\n \temit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT_INCOMPLETE,\n-\t\t\t line, len, 0);\n+\t\t\t line, len, flags);\n }\n \n static void emit_hunk_header(struct emit_callback *ecbdata,\n@@ -3254,6 +3264,7 @@ struct checkdiff_t {\n \tstruct diff_options *o;\n \tunsigned ws_rule;\n \tunsigned status;\n+\tint last_line_kind;\n };\n \n static int is_conflict_marker(const char *line, int marker_size, unsigned long len)\n@@ -3292,6 +3303,7 @@ static void checkdiff_consume_hunk(void *priv,\n static int checkdiff_consume(void *priv, char *line, unsigned long len)\n {\n \tstruct checkdiff_t *data = priv;\n+\tint last_line_kind;\n \tint marker_size = data->conflict_marker_size;\n \tconst char *ws = diff_get_color(data->o->use_color, DIFF_WHITESPACE);\n \tconst char *reset = diff_get_color(data->o->use_color, DIFF_RESET);\n@@ -3302,6 +3314,8 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)\n \tassert(data->o);\n \tline_prefix = diff_line_prefix(data->o);\n \n+\tlast_line_kind = data->last_line_kind;\n+\tdata->last_line_kind = line[0];\n \tif (line[0] == '+') {\n \t\tunsigned bad;\n \t\tdata->lineno++;\n@@ -3324,6 +3338,17 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)\n \t\t\t      data->o->file, set, reset, ws);\n \t} else if (line[0] == ' ') {\n \t\tdata->lineno++;\n+\t} else if (line[0] == '\\\\') {\n+\t\t/* no newline at the end of the line */\n+\t\tif ((data->ws_rule & WS_INCOMPLETE_LINE) &&\n+\t\t    (last_line_kind == '+')) {\n+\t\t\tunsigned bad = WS_INCOMPLETE_LINE;\n+\t\t\tdata->status |= bad;\n+\t\t\terr = whitespace_error_string(bad);\n+\t\t\tfprintf(data->o->file, \"%s%s:%d: %s.\\n\",\n+\t\t\t\tline_prefix, data->filename, data->lineno, err);\n+\t\t\tfree(err);\n+\t\t}\n \t}\n \treturn 0;\n }\ndiff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh\nindex 9de7f73f42..3c8eb02e4f 100755\n--- a/t/t4015-diff-whitespace.sh\n+++ b/t/t4015-diff-whitespace.sh\n@@ -43,6 +43,53 @@ do\n \t'\n done\n \n+test_expect_success \"incomplete line in both pre- and post-image context\" '\n+\t(echo foo && echo baz | tr -d \"\\012\") >x &&\n+\tgit add x &&\n+\t(echo bar && echo baz | tr -d \"\\012\") >x &&\n+\tgit diff x &&\n+\tgit -c core.whitespace=incomplete diff --check x &&\n+\tgit diff -R x &&\n+\tgit -c core.whitespace=incomplete diff -R --check x\n+'\n+\n+test_expect_success \"incomplete lines on both pre- and post-image\" '\n+\t# The interpretation taken here is \"since you are touching\n+\t# the line anyway, you would better fix the incomplete line\n+\t# while you are at it.\"  but this is debatable.\n+\techo foo | tr -d \"\\012\" >x &&\n+\tgit add x &&\n+\techo bar | tr -d \"\\012\" >x &&\n+\tgit diff x &&\n+\ttest_must_fail git -c core.whitespace=incomplete diff --check x >error &&\n+\ttest_grep \"no newline at the end of file\" error &&\n+\tgit diff -R x &&\n+\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&\n+\ttest_grep \"no newline at the end of file\" error\n+'\n+\n+test_expect_success \"fix incomplete line in pre-image\" '\n+\techo foo | tr -d \"\\012\" >x &&\n+\tgit add x &&\n+\techo bar >x &&\n+\tgit diff x &&\n+\tgit -c core.whitespace=incomplete diff --check x &&\n+\tgit diff -R x &&\n+\ttest_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&\n+\ttest_grep \"no newline at the end of file\" error\n+'\n+\n+test_expect_success \"new incomplete line in post-image\" '\n+\techo foo >x &&\n+\tgit add x &&\n+\techo bar | tr -d \"\\012\" >x &&\n+\tgit diff x &&\n+\ttest_must_fail git -c core.whitespace=incomplete diff --check x >error &&\n+\ttest_grep \"no newline at the end of file\" error &&\n+\tgit diff -R x &&\n+\tgit -c core.whitespace=incomplete diff -R --check x\n+'\n+\n test_expect_success \"Ray Lehtiniemi's example\" '\n \tcat <<-\\EOF >x &&\n \tdo {\n@@ -1040,7 +1087,8 @@ test_expect_success 'ws-error-highlight test setup' '\n \t{\n \t\techo \"0. blank-at-eol \" &&\n \t\techo \"1. still-blank-at-eol \" &&\n-\t\techo \"2. and a new line \"\n+\t\techo \"2. and a new line \" &&\n+\t\tprintf \"3. and more\"\n \t} >x &&\n \tnew_hash_x=$(git hash-object x) &&\n \tafter=$(git rev-parse --short \"$new_hash_x\") &&\n@@ -1050,11 +1098,13 @@ test_expect_success 'ws-error-highlight test setup' '\n \t<BOLD>index $before..$after 100644<RESET>\n \t<BOLD>--- a/x<RESET>\n \t<BOLD>+++ b/x<RESET>\n-\t<CYAN>@@ -1,2 +1,3 @@<RESET>\n+\t<CYAN>@@ -1,2 +1,4 @@<RESET>\n \t 0. blank-at-eol <RESET>\n \t<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>\n \t<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>\n \t<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>\n+\t<GREEN>+<RESET><GREEN>3. and more<RESET>\n+\t<BLUE>\\ No newline at end of file<RESET>\n \tEOF\n \n \tcat >expect.all <<-EOF &&\n@@ -1062,11 +1112,13 @@ test_expect_success 'ws-error-highlight test setup' '\n \t<BOLD>index $before..$after 100644<RESET>\n \t<BOLD>--- a/x<RESET>\n \t<BOLD>+++ b/x<RESET>\n-\t<CYAN>@@ -1,2 +1,3 @@<RESET>\n+\t<CYAN>@@ -1,2 +1,4 @@<RESET>\n \t <RESET>0. blank-at-eol<RESET><BLUE> <RESET>\n \t<RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET>\n \t<GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET>\n \t<GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET>\n+\t<GREEN>+<RESET><GREEN>3. and more<RESET>\n+\t<BLUE>\\ No newline at end of file<RESET>\n \tEOF\n \n \tcat >expect.none <<-EOF\n@@ -1074,16 +1126,19 @@ test_expect_success 'ws-error-highlight test setup' '\n \t<BOLD>index $before..$after 100644<RESET>\n \t<BOLD>--- a/x<RESET>\n \t<BOLD>+++ b/x<RESET>\n-\t<CYAN>@@ -1,2 +1,3 @@<RESET>\n+\t<CYAN>@@ -1,2 +1,4 @@<RESET>\n \t 0. blank-at-eol <RESET>\n \t<RED>-1. blank-at-eol <RESET>\n \t<GREEN>+1. still-blank-at-eol <RESET>\n \t<GREEN>+2. and a new line <RESET>\n+\t<GREEN>+3. and more<RESET>\n+\t\\ No newline at end of file<RESET>\n \tEOF\n \n '\n \n test_expect_success 'test --ws-error-highlight option' '\n+\tgit config core.whitespace blank-at-eol,incomplete-line &&\n \n \tgit diff --color --ws-error-highlight=default,old >current.raw &&\n \ttest_decode_color <current.raw >current &&\n@@ -1100,6 +1155,7 @@ test_expect_success 'test --ws-error-highlight option' '\n '\n \n test_expect_success 'test diff.wsErrorHighlight config' '\n+\tgit config core.whitespace blank-at-eol,incomplete-line &&\n \n \tgit -c diff.wsErrorHighlight=default,old diff --color >current.raw &&\n \ttest_decode_color <current.raw >current &&\n@@ -1116,6 +1172,7 @@ test_expect_success 'test diff.wsErrorHighlight config' '\n '\n \n test_expect_success 'option overrides diff.wsErrorHighlight' '\n+\tgit config core.whitespace blank-at-eol,incomplete-line &&\n \n \tgit -c diff.wsErrorHighlight=none \\\n \t\tdiff --color --ws-error-highlight=default,old >current.raw &&\n@@ -1135,6 +1192,8 @@ test_expect_success 'option overrides diff.wsErrorHighlight' '\n '\n \n test_expect_success 'detect moved code, complete file' '\n+\tgit config core.whitespace blank-at-eol &&\n+\n \tgit reset --hard &&\n \tcat <<-\\EOF >test.c &&\n \t#include<stdio.h>\n-- \n2.52.0-rc2-441-g030905368a\n\n"},{"id":"530629","messageId":"20251112220258.1009253-13-gitster@pobox.com","threadId":"64429","inReplyTo":"20251112220258.1009253-1-gitster@pobox.com","subject":"[PATCH v4 12/12] attr: enable incomplete-line whitespace error for this project","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-12T22:02:58Z","receivedAt":"2025-11-12T22:03:21Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Now \"git diff --check\" and \"git apply --whitespace=warn/fix\" learned\nincomplete line is a whitespace error, enable them for this project\nto prevent patches to add new incomplete lines to our source to both\ncode and documentation files.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n .gitattributes | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/.gitattributes b/.gitattributes\nindex 32583149c2..673527dd67 100644\n--- a/.gitattributes\n+++ b/.gitattributes\n@@ -1,13 +1,13 @@\n * whitespace=!indent,trail,space\n-*.[ch] whitespace=indent,trail,space diff=cpp\n-*.sh whitespace=indent,trail,space text eol=lf\n+*.[ch] whitespace=indent,trail,space,incomplete diff=cpp\n+*.sh whitespace=indent,trail,space,incomplete text eol=lf\n *.perl text eol=lf diff=perl\n *.pl text eof=lf diff=perl\n *.pm text eol=lf diff=perl\n *.py text eol=lf diff=python\n *.bat text eol=crlf\n CODE_OF_CONDUCT.md -whitespace\n-/Documentation/**/*.adoc text eol=lf\n+/Documentation/**/*.adoc text eol=lf whitespace=!indent,trail,space,incomplete\n /command-list.txt text eol=lf\n /GIT-VERSION-GEN text eol=lf\n /mergetools/* text eol=lf\n-- \n2.52.0-rc2-441-g030905368a\n\n"},{"id":"530688","messageId":"dfdbfb71-2d3f-450e-bbcf-95ecbead3292@gmail.com","threadId":"64429","inReplyTo":"20251112220258.1009253-1-gitster@pobox.com","subject":"Re: [PATCH v4 00/12] Incomplete lines","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-14T10:24:21Z","receivedAt":"2025-11-14T10:24:25Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 12/11/2025 22:02, Junio C Hamano wrote:\n> Changes in v4:\n> \n>   - The preliminary clean-up for \"git diff\" in [04/12] no longer\n>     \"corrects\" the code that updates the line number upon seeing\n>     \"\\ No newline\" marker, and leaves it for later series to clean\n>     it up as #leftoverbits.\n\nI agree it makes sense to leave the existing increment alone as it's not \nreally related to this series.\n>   - Our house rule updates in [12/12] now forbids the documentation\n>     sources *.adoc to end in an incomplete line.\n> 12:  068229790d ! 12:  cb86d9b45f attr: enable incomplete-line whitespace error for this project\n>      +-/Documentation/**/*.adoc text eol=lf\n>      ++/Documentation/**/*.adoc text eol=lf whitespace=!indent,trail,space,incomplete\n\nShould that be \"-indent\" c.f. 358e94dc705 (.gitattributes: remove \nmisspelled no-op whitespace attribute, 2025-11-11)\n\nThanks\n\nPhillip\n\n"},{"id":"530692","messageId":"xmqqikfck2e2.fsf@gitster.g","threadId":"64429","inReplyTo":"dfdbfb71-2d3f-450e-bbcf-95ecbead3292@gmail.com","subject":"Re: [PATCH v4 00/12] Incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-14T16:25:09Z","receivedAt":"2025-11-14T16:25:12Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> On 12/11/2025 22:02, Junio C Hamano wrote:\n>> Changes in v4:\n>> \n>>   - The preliminary clean-up for \"git diff\" in [04/12] no longer\n>>     \"corrects\" the code that updates the line number upon seeing\n>>     \"\\ No newline\" marker, and leaves it for later series to clean\n>>     it up as #leftoverbits.\n>\n> I agree it makes sense to leave the existing increment alone as it's not \n> really related to this series.\n>>   - Our house rule updates in [12/12] now forbids the documentation\n>>     sources *.adoc to end in an incomplete line.\n>> 12:  068229790d ! 12:  cb86d9b45f attr: enable incomplete-line whitespace error for this project\n>>      +-/Documentation/**/*.adoc text eol=lf\n>>      ++/Documentation/**/*.adoc text eol=lf whitespace=!indent,trail,space,incomplete\n>\n> Should that be \"-indent\" c.f. 358e94dc705 (.gitattributes: remove \n> misspelled no-op whitespace attribute, 2025-11-11)\n\nOops.  Thanks for spotting.\n"},{"id":"531167","messageId":"xmqqzf8d1noa.fsf@gitster.g","threadId":"64429","inReplyTo":"xmqqikfck2e2.fsf@gitster.g","subject":"Re: [PATCH v4 00/12] Incomplete lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-23T02:35:17Z","receivedAt":"2025-11-23T02:35:19Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n>\n>> On 12/11/2025 22:02, Junio C Hamano wrote:\n>>> Changes in v4:\n>>> \n>>>   - The preliminary clean-up for \"git diff\" in [04/12] no longer\n>>>     \"corrects\" the code that updates the line number upon seeing\n>>>     \"\\ No newline\" marker, and leaves it for later series to clean\n>>>     it up as #leftoverbits.\n>>\n>> I agree it makes sense to leave the existing increment alone as it's not \n>> really related to this series.\n>>>   - Our house rule updates in [12/12] now forbids the documentation\n>>>     sources *.adoc to end in an incomplete line.\n>>> 12:  068229790d ! 12:  cb86d9b45f attr: enable incomplete-line whitespace error for this project\n>>>      +-/Documentation/**/*.adoc text eol=lf\n>>>      ++/Documentation/**/*.adoc text eol=lf whitespace=!indent,trail,space,incomplete\n>>\n>> Should that be \"-indent\" c.f. 358e94dc705 (.gitattributes: remove \n>> misspelled no-op whitespace attribute, 2025-11-11)\n>\n> Oops.  Thanks for spotting.\n\nThis has been locally corrected.  We saw no other comments on this\niteration, and hopefully it is not due to lack of interest.  Let me\nmark the topic for 'next' and merge it down.\n\n----- >8 -----\nSubject: [PATCH v3bis 12/12] attr: enable incomplete-line whitespace error for this project\n\nNow \"git diff --check\" and \"git apply --whitespace=warn/fix\" learned\nincomplete line is a whitespace error, enable them for this project\nto prevent patches to add new incomplete lines to our source to both\ncode and documentation files.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n .gitattributes | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/.gitattributes b/.gitattributes\nindex 32583149c2..a8e2950a73 100644\n--- a/.gitattributes\n+++ b/.gitattributes\n@@ -1,13 +1,13 @@\n * whitespace=!indent,trail,space\n-*.[ch] whitespace=indent,trail,space diff=cpp\n-*.sh whitespace=indent,trail,space text eol=lf\n+*.[ch] whitespace=indent,trail,space,incomplete diff=cpp\n+*.sh whitespace=indent,trail,space,incomplete text eol=lf\n *.perl text eol=lf diff=perl\n *.pl text eof=lf diff=perl\n *.pm text eol=lf diff=perl\n *.py text eol=lf diff=python\n *.bat text eol=crlf\n CODE_OF_CONDUCT.md -whitespace\n-/Documentation/**/*.adoc text eol=lf\n+/Documentation/**/*.adoc text eol=lf whitespace=trail,space,incomplete\n /command-list.txt text eol=lf\n /GIT-VERSION-GEN text eol=lf\n /mergetools/* text eol=lf\n-- \n2.52.0-168-g07aa2ddc22\n\n"}]}