Merge branch 'wc/diff'
authorJunio C Hamano <gitster@pobox.com>
Sat, 15 Dec 2007 05:42:53 +0000 (21:42 -0800)
committerJunio C Hamano <gitster@pobox.com>
Sat, 15 Dec 2007 05:42:53 +0000 (21:42 -0800)
* wc/diff:
Test interaction between diff --check and --exit-code
Use shorter error messages for whitespace problems
Add tests for "git diff --check" with core.whitespace options
Make "diff --check" output match "git apply"
Unify whitespace checking
diff --check: minor fixups
"diff --check" should affect exit status

12 files changed:
Documentation/diff-options.txt
builtin-apply.c
builtin-diff-files.c
builtin-diff-index.c
builtin-diff-tree.c
builtin-diff.c
cache.h
diff.c
diff.h
t/t4015-diff-whitespace.sh
t/t4017-diff-retval.sh
ws.c
index 5d22b7b58c5950fe70d09e0bb7384179ae30220f..9ecc1d7bc460dd723ae08618b0deab88e407114f 100644 (file)
@@ -93,7 +93,9 @@ endif::git-format-patch[]
 
 --check::
        Warn if changes introduce trailing whitespace
-       or an indent that uses a space before a tab.
+       or an indent that uses a space before a tab. Exits with
+       non-zero status if problems are found. Not compatible with
+       --exit-code.
 
 --full-index::
        Instead of the first handful characters, show full
index f2e9a332ca0e148366f49b2825ed8826576f1bad..2edd83bf40f51f52e58737ef5e7220ae70afb6e1 100644 (file)
@@ -900,56 +900,22 @@ static int find_header(char *line, unsigned long size, int *hdrsize, struct patc
 
 static void check_whitespace(const char *line, int len, unsigned ws_rule)
 {
-       const char *err = "Adds trailing whitespace";
-       int seen_space = 0;
-       int i;
-
-       /*
-        * We know len is at least two, since we have a '+' and we
-        * checked that the last character was a '\n' before calling
-        * this function.  That is, an addition of an empty line would
-        * check the '+' here.  Sneaky...
-        */
-       if ((ws_rule & WS_TRAILING_SPACE) && isspace(line[len-2]))
-               goto error;
-
-       /*
-        * Make sure that there is no space followed by a tab in
-        * indentation.
-        */
-       if (ws_rule & WS_SPACE_BEFORE_TAB) {
-               err = "Space in indent is followed by a tab";
-               for (i = 1; i < len; i++) {
-                       if (line[i] == '\t') {
-                               if (seen_space)
-                                       goto error;
-                       }
-                       else if (line[i] == ' ')
-                               seen_space = 1;
-                       else
-                               break;
-               }
-       }
-
-       /*
-        * Make sure that the indentation does not contain more than
-        * 8 spaces.
-        */
-       if ((ws_rule & WS_INDENT_WITH_NON_TAB) &&
-           (8 < len) && !strncmp("+        ", line, 9)) {
-               err = "Indent more than 8 places with spaces";
-               goto error;
-       }
-       return;
+       char *err;
+       unsigned result = check_and_emit_line(line + 1, len - 1, ws_rule,
+           NULL, NULL, NULL, NULL);
+       if (!result)
+               return;
 
- error:
        whitespace_error++;
        if (squelch_whitespace_errors &&
            squelch_whitespace_errors < whitespace_error)
                ;
-       else
-               fprintf(stderr, "%s.\n%s:%d:%.*s\n",
-                       err, patch_input_file, linenr, len-2, line+1);
+       else {
+               err = whitespace_error_string(result);
+               fprintf(stderr, "%s:%d: %s.\n%.*s\n",
+                    patch_input_file, linenr, err, len - 2, line + 1);
+               free(err);
+       }
 }
 
 /*
index 046b7e34b5d5c6306d335dddbd5e283c8166e80e..9c0411165691bb1c3a581e9927e124e55fbd7bfd 100644 (file)
@@ -31,7 +31,5 @@ int cmd_diff_files(int argc, const char **argv, const char *prefix)
        if (!rev.diffopt.output_format)
                rev.diffopt.output_format = DIFF_FORMAT_RAW;
        result = run_diff_files_cmd(&rev, argc, argv);
-       if (DIFF_OPT_TST(&rev.diffopt, EXIT_WITH_STATUS))
-               return DIFF_OPT_TST(&rev.diffopt, HAS_CHANGES) != 0;
-       return result;
+       return diff_result_code(&rev.diffopt, result);
 }
index 556c506bfa7b5c5ef739d4203c585412a2764073..0f2390a20a6a138388564cd5e0395aee0a48b59c 100644 (file)
@@ -44,7 +44,5 @@ int cmd_diff_index(int argc, const char **argv, const char *prefix)
                return -1;
        }
        result = run_diff_index(&rev, cached);
-       if (DIFF_OPT_TST(&rev.diffopt, EXIT_WITH_STATUS))
-               return DIFF_OPT_TST(&rev.diffopt, HAS_CHANGES) != 0;
-       return result;
+       return diff_result_code(&rev.diffopt, result);
 }
index 2e13716eec985e7274d6f44646c94d8224964b7c..ebc50efbd22f7fa294d66d73d7b2c3084931b4b7 100644 (file)
@@ -117,23 +117,21 @@ int cmd_diff_tree(int argc, const char **argv, const char *prefix)
                break;
        }
 
-       if (!read_stdin)
-               return DIFF_OPT_TST(&opt->diffopt, EXIT_WITH_STATUS)
-                       && DIFF_OPT_TST(&opt->diffopt, HAS_CHANGES);
+       if (read_stdin) {
+               if (opt->diffopt.detect_rename)
+                       opt->diffopt.setup |= (DIFF_SETUP_USE_SIZE_CACHE |
+                                              DIFF_SETUP_USE_CACHE);
+               while (fgets(line, sizeof(line), stdin)) {
+                       unsigned char sha1[20];
 
-       if (opt->diffopt.detect_rename)
-               opt->diffopt.setup |= (DIFF_SETUP_USE_SIZE_CACHE |
-                                      DIFF_SETUP_USE_CACHE);
-       while (fgets(line, sizeof(line), stdin)) {
-               unsigned char sha1[20];
-
-               if (get_sha1_hex(line, sha1)) {
-                       fputs(line, stdout);
-                       fflush(stdout);
+                       if (get_sha1_hex(line, sha1)) {
+                               fputs(line, stdout);
+                               fflush(stdout);
+                       }
+                       else
+                               diff_tree_stdin(line);
                }
-               else
-                       diff_tree_stdin(line);
        }
-       return DIFF_OPT_TST(&opt->diffopt, EXIT_WITH_STATUS)
-               && DIFF_OPT_TST(&opt->diffopt, HAS_CHANGES);
+
+       return diff_result_code(&opt->diffopt, 0);
 }
index 55fb84c730e4141bdb53fec6602a2bdafc99f8fd..29365a0b17348982ea806add83f3c95bea43d4f6 100644 (file)
@@ -244,7 +244,8 @@ int cmd_diff(int argc, const char **argv, const char *prefix)
        DIFF_OPT_SET(&rev.diffopt, ALLOW_EXTERNAL);
        DIFF_OPT_SET(&rev.diffopt, RECURSIVE);
 
-       /* If the user asked for our exit code then don't start a
+       /*
+        * If the user asked for our exit code then don't start a
         * pager or we would end up reporting its exit code instead.
         */
        if (!DIFF_OPT_TST(&rev.diffopt, EXIT_WITH_STATUS))
@@ -351,9 +352,7 @@ int cmd_diff(int argc, const char **argv, const char *prefix)
        else
                result = builtin_diff_combined(&rev, argc, argv,
                                             ent, ents);
-       if (DIFF_OPT_TST(&rev.diffopt, EXIT_WITH_STATUS))
-               result = DIFF_OPT_TST(&rev.diffopt, HAS_CHANGES) != 0;
-
+       result = diff_result_code(&rev.diffopt, result);
        if (1 < rev.diffopt.skip_stat_unmatch)
                refresh_index_quietly();
        return result;
diff --git a/cache.h b/cache.h
index 27d90fe543b97eac91f61bf96f170c32d735e44d..39331c28be15605f4b943cea22a7a8caa5b501c2 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -655,6 +655,10 @@ void shift_tree(const unsigned char *, const unsigned char *, unsigned char *, i
 extern unsigned whitespace_rule_cfg;
 extern unsigned whitespace_rule(const char *);
 extern unsigned parse_whitespace_rule(const char *);
+extern unsigned check_and_emit_line(const char *line, int len, unsigned ws_rule,
+    FILE *stream, const char *set,
+    const char *reset, const char *ws);
+extern char *whitespace_error_string(unsigned ws);
 
 /* ls-files */
 int pathspec_match(const char **spec, char *matched, const char *filename, int skiplen);
diff --git a/diff.c b/diff.c
index 3dd2f35f7364341bcb5f4507ea36d03925fdc938..08ec66c7945fb330d289f9e579a162e9217a16eb 100644 (file)
--- a/diff.c
+++ b/diff.c
@@ -486,88 +486,9 @@ const char *diff_get_color(int diff_use_color, enum color_diff ix)
 
 static void emit_line(const char *set, const char *reset, const char *line, int len)
 {
-       if (len > 0 && line[len-1] == '\n')
-               len--;
        fputs(set, stdout);
        fwrite(line, len, 1, stdout);
-       puts(reset);
-}
-
-static void emit_line_with_ws(int nparents,
-                             const char *set, const char *reset, const char *ws,
-                             const char *line, int len, unsigned ws_rule)
-{
-       int col0 = nparents;
-       int last_tab_in_indent = -1;
-       int last_space_in_indent = -1;
-       int i;
-       int tail = len;
-       int need_highlight_leading_space = 0;
-       /*
-        * The line is a newly added line.  Does it have funny leading
-        * whitespaces?  In indent, SP should never precede a TAB.  In
-        * addition, under "indent with non tab" rule, there should not
-        * be more than 8 consecutive spaces.
-        */
-       for (i = col0; i < len; i++) {
-               if (line[i] == '\t') {
-                       last_tab_in_indent = i;
-                       if ((ws_rule & WS_SPACE_BEFORE_TAB) &&
-                           0 <= last_space_in_indent)
-                               need_highlight_leading_space = 1;
-               }
-               else if (line[i] == ' ')
-                       last_space_in_indent = i;
-               else
-                       break;
-       }
-       if ((ws_rule & WS_INDENT_WITH_NON_TAB) &&
-           0 <= last_space_in_indent &&
-           last_tab_in_indent < 0 &&
-           8 <= (i - col0)) {
-               last_tab_in_indent = i;
-               need_highlight_leading_space = 1;
-       }
-       fputs(set, stdout);
-       fwrite(line, col0, 1, stdout);
        fputs(reset, stdout);
-       if (((i == len) || line[i] == '\n') && i != col0) {
-               /* The whole line was indent */
-               emit_line(ws, reset, line + col0, len - col0);
-               return;
-       }
-       i = col0;
-       if (need_highlight_leading_space) {
-               while (i < last_tab_in_indent) {
-                       if (line[i] == ' ') {
-                               fputs(ws, stdout);
-                               putchar(' ');
-                               fputs(reset, stdout);
-                       }
-                       else
-                               putchar(line[i]);
-                       i++;
-               }
-       }
-       tail = len - 1;
-       if (line[tail] == '\n' && i < tail)
-               tail--;
-       if (ws_rule & WS_TRAILING_SPACE) {
-               while (i < tail) {
-                       if (!isspace(line[tail]))
-                               break;
-                       tail--;
-               }
-       }
-       if ((i < tail && line[tail + 1] != '\n')) {
-               /* This has whitespace between tail+1..len */
-               fputs(set, stdout);
-               fwrite(line + i, tail - i + 1, 1, stdout);
-               fputs(reset, stdout);
-               emit_line(ws, reset, line + tail + 1, len - tail - 1);
-       }
-       else
-               emit_line(set, reset, line + i, len - i);
 }
 
 static void emit_add_line(const char *reset, struct emit_callback *ecbdata, const char *line, int len)
@@ -577,9 +498,13 @@ static void emit_add_line(const char *reset, struct emit_callback *ecbdata, cons
 
        if (!*ws)
                emit_line(set, reset, line, len);
-       else
-               emit_line_with_ws(ecbdata->nparents, set, reset, ws,
-                                 line, len, ecbdata->ws_rule);
+       else {
+               /* Emit just the prefix, then the rest. */
+               emit_line(set, reset, line, ecbdata->nparents);
+               (void)check_and_emit_line(line + ecbdata->nparents,
+                   len - ecbdata->nparents, ecbdata->ws_rule,
+                   stdout, set, reset, ws);
+       }
 }
 
 static void fn_out_consume(void *priv, char *line, unsigned long len)
@@ -1031,6 +956,7 @@ struct checkdiff_t {
        const char *filename;
        int lineno, color_diff;
        unsigned ws_rule;
+       unsigned status;
 };
 
 static void checkdiff_consume(void *priv, char *line, unsigned long len)
@@ -1039,44 +965,19 @@ static void checkdiff_consume(void *priv, char *line, unsigned long len)
        const char *ws = diff_get_color(data->color_diff, DIFF_WHITESPACE);
        const char *reset = diff_get_color(data->color_diff, DIFF_RESET);
        const char *set = diff_get_color(data->color_diff, DIFF_FILE_NEW);
+       char *err;
 
        if (line[0] == '+') {
-               int i, spaces = 0, space_before_tab = 0, white_space_at_end = 0;
-
-               /* check space before tab */
-               for (i = 1; i < len; i++) {
-                       if (line[i] == ' ')
-                               spaces++;
-                       else if (line[i] == '\t') {
-                               if (spaces) {
-                                       space_before_tab = 1;
-                                       break;
-                               }
-                       }
-                       else
-                               break;
-               }
-
-               /* check whitespace at line end */
-               if (line[len - 1] == '\n')
-                       len--;
-               if (isspace(line[len - 1]))
-                       white_space_at_end = 1;
-
-               if (space_before_tab || white_space_at_end) {
-                       printf("%s:%d: %s", data->filename, data->lineno, ws);
-                       if (space_before_tab) {
-                               printf("space before tab");
-                               if (white_space_at_end)
-                                       putchar(',');
-                       }
-                       if (white_space_at_end)
-                               printf("whitespace at end");
-                       printf(":%s ", reset);
-                       emit_line_with_ws(1, set, reset, ws, line, len,
-                                         data->ws_rule);
-               }
-
+               data->status = check_and_emit_line(line + 1, len - 1,
+                   data->ws_rule, NULL, NULL, NULL, NULL);
+               if (!data->status)
+                       return;
+               err = whitespace_error_string(data->status);
+               printf("%s:%d: %s.\n", data->filename, data->lineno, err);
+               free(err);
+               emit_line(set, reset, line, 1);
+               (void)check_and_emit_line(line + 1, len - 1, data->ws_rule,
+                   stdout, set, reset, ws);
                data->lineno++;
        } else if (line[0] == ' ')
                data->lineno++;
@@ -1491,6 +1392,8 @@ static void builtin_checkdiff(const char *name_a, const char *name_b,
  free_and_return:
        diff_free_filespec_data(one);
        diff_free_filespec_data(two);
+       if (data.status)
+               DIFF_OPT_SET(o, CHECK_FAILED);
 }
 
 struct diff_filespec *alloc_filespec(const char *path)
@@ -3171,6 +3074,20 @@ void diffcore_std(struct diff_options *options)
                DIFF_OPT_CLR(options, HAS_CHANGES);
 }
 
+int diff_result_code(struct diff_options *opt, int status)
+{
+       int result = 0;
+       if (!DIFF_OPT_TST(opt, EXIT_WITH_STATUS) &&
+           !(opt->output_format & DIFF_FORMAT_CHECKDIFF))
+               return status;
+       if (DIFF_OPT_TST(opt, EXIT_WITH_STATUS) &&
+           DIFF_OPT_TST(opt, HAS_CHANGES))
+               result |= 01;
+       if ((opt->output_format & DIFF_FORMAT_CHECKDIFF) &&
+           DIFF_OPT_TST(opt, CHECK_FAILED))
+               result |= 02;
+       return result;
+}
 
 void diff_addremove(struct diff_options *options,
                    int addremove, unsigned mode,
diff --git a/diff.h b/diff.h
index a52496a1086ccbe8ea90acd6800dd355b1d7ff68..7e8000a5d7ed7b36669abaca38111c43038092aa 100644 (file)
--- a/diff.h
+++ b/diff.h
@@ -59,6 +59,7 @@ typedef void (*diff_format_fn_t)(struct diff_queue_struct *q,
 #define DIFF_OPT_ALLOW_EXTERNAL      (1 << 13)
 #define DIFF_OPT_EXIT_WITH_STATUS    (1 << 14)
 #define DIFF_OPT_REVERSE_DIFF        (1 << 15)
+#define DIFF_OPT_CHECK_FAILED        (1 << 16)
 #define DIFF_OPT_TST(opts, flag)    ((opts)->flags & DIFF_OPT_##flag)
 #define DIFF_OPT_SET(opts, flag)    ((opts)->flags |= DIFF_OPT_##flag)
 #define DIFF_OPT_CLR(opts, flag)    ((opts)->flags &= ~DIFF_OPT_##flag)
@@ -246,4 +247,6 @@ extern int run_diff_index(struct rev_info *revs, int cached);
 extern int do_diff_cache(const unsigned char *, struct diff_options *);
 extern int diff_flush_patch_id(struct diff_options *, unsigned char *);
 
+extern int diff_result_code(struct diff_options *, int);
+
 #endif /* DIFF_H */
index 6adf9d11d03c3e9b1b7ee1ee46364f0e3a755016..9bff8f5e4bae6beb22ba31e8be03a0528e06519e 100755 (executable)
@@ -117,12 +117,197 @@ EOF
 git diff -b > out
 test_expect_success 'another test, with -b' 'git diff expect out'
 
-
 test_expect_success 'check mixed spaces and tabs in indent' '
 
        # This is indented with SP HT SP.
        echo "   foo();" > x &&
-       git diff --check | grep "space before tab"
+       git diff --check | grep "space before tab in indent"
+
+'
+
+test_expect_success 'check with no whitespace errors' '
+
+       git commit -m "snapshot" &&
+       echo "foo();" > x &&
+       git diff --check
+
+'
+
+test_expect_success 'check with trailing whitespace' '
+
+       echo "foo(); " > x &&
+       ! git diff --check
+
+'
+
+test_expect_success 'check with space before tab in indent' '
+
+       # indent has space followed by hard tab
+       echo "  foo();" > x &&
+       ! git diff --check
+
+'
+
+test_expect_success '--check and --exit-code are not exclusive' '
+
+       git checkout x &&
+       git diff --check --exit-code
+
+'
+
+test_expect_success '--check and --quiet are not exclusive' '
+
+       git diff --check --quiet
+
+'
+
+test_expect_success 'check staged with no whitespace errors' '
+
+       echo "foo();" > x &&
+       git add x &&
+       git diff --cached --check
+
+'
+
+test_expect_success 'check staged with trailing whitespace' '
+
+       echo "foo(); " > x &&
+       git add x &&
+       ! git diff --cached --check
+
+'
+
+test_expect_success 'check staged with space before tab in indent' '
+
+       # indent has space followed by hard tab
+       echo "  foo();" > x &&
+       git add x &&
+       ! git diff --cached --check
+
+'
+
+test_expect_success 'check with no whitespace errors (diff-index)' '
+
+       echo "foo();" > x &&
+       git add x &&
+       git diff-index --check HEAD
+
+'
+
+test_expect_success 'check with trailing whitespace (diff-index)' '
+
+       echo "foo(); " > x &&
+       git add x &&
+       ! git diff-index --check HEAD
+
+'
+
+test_expect_success 'check with space before tab in indent (diff-index)' '
+
+       # indent has space followed by hard tab
+       echo "  foo();" > x &&
+       git add x &&
+       ! git diff-index --check HEAD
+
+'
+
+test_expect_success 'check staged with no whitespace errors (diff-index)' '
+
+       echo "foo();" > x &&
+       git add x &&
+       git diff-index --cached --check HEAD
+
+'
+
+test_expect_success 'check staged with trailing whitespace (diff-index)' '
+
+       echo "foo(); " > x &&
+       git add x &&
+       ! git diff-index --cached --check HEAD
+
+'
+
+test_expect_success 'check staged with space before tab in indent (diff-index)' '
+
+       # indent has space followed by hard tab
+       echo "  foo();" > x &&
+       git add x &&
+       ! git diff-index --cached --check HEAD
+
+'
+
+test_expect_success 'check with no whitespace errors (diff-tree)' '
+
+       echo "foo();" > x &&
+       git commit -m "new commit" x &&
+       git diff-tree --check HEAD^ HEAD
+
+'
+
+test_expect_success 'check with trailing whitespace (diff-tree)' '
+
+       echo "foo(); " > x &&
+       git commit -m "another commit" x &&
+       ! git diff-tree --check HEAD^ HEAD
+
+'
+
+test_expect_success 'check with space before tab in indent (diff-tree)' '
+
+       # indent has space followed by hard tab
+       echo "  foo();" > x &&
+       git commit -m "yet another" x &&
+       ! git diff-tree --check HEAD^ HEAD
+
+'
+
+test_expect_success 'check trailing whitespace (trailing-space: off)' '
+
+       git config core.whitespace "-trailing-space" &&
+       echo "foo ();   " > x &&
+       git diff --check
+
+'
+
+test_expect_success 'check trailing whitespace (trailing-space: on)' '
+
+       git config core.whitespace "trailing-space" &&
+       echo "foo ();   " > x &&
+       ! git diff --check
+
+'
+
+test_expect_success 'check space before tab in indent (space-before-tab: off)' '
+
+       # indent contains space followed by HT
+       git config core.whitespace "-space-before-tab" &&
+       echo "  foo ();" > x &&
+       git diff --check
+
+'
+
+test_expect_success 'check space before tab in indent (space-before-tab: on)' '
+
+       # indent contains space followed by HT
+       git config core.whitespace "space-before-tab" &&
+       echo "  foo ();   " > x &&
+       ! git diff --check
+
+'
+
+test_expect_success 'check spaces as indentation (indent-with-non-tab: off)' '
+
+       git config core.whitespace "-indent-with-non-tab"
+       echo "                foo ();" > x &&
+       git diff --check
+
+'
+
+test_expect_success 'check spaces as indentation (indent-with-non-tab: on)' '
+
+       git config core.whitespace "indent-with-non-tab" &&
+       echo "                foo ();" > x &&
+       ! git diff --check
 
 '
 
index 68731908beaae88da6cc309fea91e84df533dfdc..dc0b7126cc996594b415058d83014a2c7d732895 100755 (executable)
@@ -76,4 +76,33 @@ test_expect_success 'git diff-index --cached HEAD' '
        }
 '
 
+test_expect_success '--check --exit-code returns 0 for no difference' '
+
+       git diff --check --exit-code
+
+'
+
+test_expect_success '--check --exit-code returns 1 for a clean difference' '
+
+       echo "good" > a &&
+       git diff --check --exit-code
+       test $? = 1
+
+'
+
+test_expect_success '--check --exit-code returns 3 for a dirty difference' '
+
+       echo "bad   " >> a &&
+       git diff --check --exit-code
+       test $? = 3
+
+'
+
+test_expect_success '--check with --no-pager returns 2 for dirty difference' '
+
+       git --no-pager diff --check
+       test $? = 2
+
+'
+
 test_done
diff --git a/ws.c b/ws.c
index 52c10caf3523b877ef7fa77f7af3c64de3055b4f..46cbdd63793aad534d8d8b640df7fd2e86c7d915 100644 (file)
--- a/ws.c
+++ b/ws.c
@@ -94,3 +94,108 @@ unsigned whitespace_rule(const char *pathname)
                return whitespace_rule_cfg;
        }
 }
+
+/* The returned string should be freed by the caller. */
+char *whitespace_error_string(unsigned ws)
+{
+       struct strbuf err;
+       strbuf_init(&err, 0);
+       if (ws & WS_TRAILING_SPACE)
+               strbuf_addstr(&err, "trailing whitespace");
+       if (ws & WS_SPACE_BEFORE_TAB) {
+               if (err.len)
+                       strbuf_addstr(&err, ", ");
+               strbuf_addstr(&err, "space before tab in indent");
+       }
+       if (ws & WS_INDENT_WITH_NON_TAB) {
+               if (err.len)
+                       strbuf_addstr(&err, ", ");
+               strbuf_addstr(&err, "indent with spaces");
+       }
+       return strbuf_detach(&err, NULL);
+}
+
+/* If stream is non-NULL, emits the line after checking. */
+unsigned check_and_emit_line(const char *line, int len, unsigned ws_rule,
+                            FILE *stream, const char *set,
+                            const char *reset, const char *ws)
+{
+       unsigned result = 0;
+       int leading_space = -1;
+       int trailing_whitespace = -1;
+       int trailing_newline = 0;
+       int i;
+
+       /* Logic is simpler if we temporarily ignore the trailing newline. */
+       if (len > 0 && line[len - 1] == '\n') {
+               trailing_newline = 1;
+               len--;
+       }
+
+       /* Check for trailing whitespace. */
+       if (ws_rule & WS_TRAILING_SPACE) {
+               for (i = len - 1; i >= 0; i--) {
+                       if (isspace(line[i])) {
+                               trailing_whitespace = i;
+                               result |= WS_TRAILING_SPACE;
+                       }
+                       else
+                               break;
+               }
+       }
+
+       /* Check for space before tab in initial indent. */
+       for (i = 0; i < len; i++) {
+               if (line[i] == '\t') {
+                       if ((ws_rule & WS_SPACE_BEFORE_TAB) &&
+                           (leading_space != -1))
+                               result |= WS_SPACE_BEFORE_TAB;
+                       break;
+               }
+               else if (line[i] == ' ')
+                       leading_space = i;
+               else
+                       break;
+       }
+
+       /* Check for indent using non-tab. */
+       if ((ws_rule & WS_INDENT_WITH_NON_TAB) && leading_space >= 8)
+               result |= WS_INDENT_WITH_NON_TAB;
+
+       if (stream) {
+               /* Highlight errors in leading whitespace. */
+               if ((result & WS_SPACE_BEFORE_TAB) ||
+                   (result & WS_INDENT_WITH_NON_TAB)) {
+                       fputs(ws, stream);
+                       fwrite(line, leading_space + 1, 1, stream);
+                       fputs(reset, stream);
+                       leading_space++;
+               }
+               else
+                       leading_space = 0;
+
+               /* Now the rest of the line starts at leading_space.
+                * The non-highlighted part ends at trailing_whitespace. */
+               if (trailing_whitespace == -1)
+                       trailing_whitespace = len;
+
+               /* Emit non-highlighted (middle) segment. */
+               if (trailing_whitespace - leading_space > 0) {
+                       fputs(set, stream);
+                       fwrite(line + leading_space,
+                           trailing_whitespace - leading_space, 1, stream);
+                       fputs(reset, stream);
+               }
+
+               /* Highlight errors in trailing whitespace. */
+               if (trailing_whitespace != len) {
+                       fputs(ws, stream);
+                       fwrite(line + trailing_whitespace,
+                           len - trailing_whitespace, 1, stream);
+                       fputs(reset, stream);
+               }
+               if (trailing_newline)
+                       fputc('\n', stream);
+       }
+       return result;
+}