]> git.ipfire.org Git - thirdparty/git.git/commitdiff
diff: highlight and error out on incomplete lines
authorJunio C Hamano <gitster@pobox.com>
Wed, 5 Nov 2025 21:30:51 +0000 (13:30 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 5 Nov 2025 21:37:20 +0000 (13:37 -0800)
Teach "git diff" to highlight "\ No newline at end of file" message
as a whitespace error when incomplete-line whitespace error class is
in effect.  Thanks to the previous refactoring of complete rewrite
code path, we can do this at a single place.

Unlike whitespace errors in the payload where we need to annotate in
line, possibly using colors, the line that has whitespace problems,
we have a dedicated line already that can serve as the error
message, so paint it as a whitespace error message.

Also teach "git diff --check" to notice incomplete lines as
whitespace errors and report when incomplete-line whitespace error
class is in effect.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
diff.c
t/t4015-diff-whitespace.sh

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