]> git.ipfire.org Git - thirdparty/git.git/commitdiff
apply: check and fix incomplete lines
authorJunio C Hamano <gitster@pobox.com>
Wed, 5 Nov 2025 21:30:50 +0000 (13:30 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 5 Nov 2025 21:37:20 +0000 (13:37 -0800)
The final line of a file that lacks the terminating newline at its
end is called an incomplete line.  In general they are frowned upon
for many reasons (imagine concatenating two files with "cat A B" and
what happens when A ends in an incomplete line, for example), and
text-oriented tools often mishandle such a line.

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

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

Signed-off-by: Junio C Hamano <gitster@pobox.com>
apply.c
t/t4124-apply-ws-rule.sh
ws.c

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