diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt index 6cb083aae5..85a6547774 100644 --- a/Documentation/diff-options.txt +++ b/Documentation/diff-options.txt @@ -278,6 +278,16 @@ ifndef::git-format-patch[] initial indent of the line are considered whitespace errors. Exits with non-zero status if problems are found. Not compatible with --exit-code. + +--ws-error-highlight=:: + Highlight whitespace errors on lines specified by + in the color specified by `color.diff.whitespace`. + is a comma separated list of `old`, `new`, `context`. When + this option is not given, only whitespace errors in `new` + lines are highlighted. E.g. `--ws-error-highlight=new,old` + highlights whitespace errors on both deleted and added lines. + `all` can be used as a short-hand for `old,new,context`. + endif::git-format-patch[] --full-index:: diff --git a/diff.c b/diff.c index c575c452f1..34012b4b63 100644 --- a/diff.c +++ b/diff.c @@ -478,42 +478,57 @@ static int new_blank_line_at_eof(struct emit_callback *ecbdata, const char *line return ws_blank_line(line, len, ecbdata->ws_rule); } +static void emit_line_checked(const char *reset, + struct emit_callback *ecbdata, + const char *line, int len, + enum color_diff color, + unsigned ws_error_highlight, + char sign) +{ + const char *set = diff_get_color(ecbdata->color_diff, color); + const char *ws = NULL; + + if (ecbdata->opt->ws_error_highlight & ws_error_highlight) { + ws = diff_get_color(ecbdata->color_diff, DIFF_WHITESPACE); + if (!*ws) + ws = NULL; + } + + if (!ws) + emit_line_0(ecbdata->opt, set, reset, sign, line, len); + else if (sign == '+' && new_blank_line_at_eof(ecbdata, line, len)) + /* Blank line at EOF - paint '+' as well */ + emit_line_0(ecbdata->opt, ws, reset, sign, line, len); + else { + /* Emit just the prefix, then the rest. */ + emit_line_0(ecbdata->opt, set, reset, sign, "", 0); + ws_check_emit(line, len, ecbdata->ws_rule, + ecbdata->opt->file, set, reset, ws); + } +} + static void emit_add_line(const char *reset, struct emit_callback *ecbdata, const char *line, int len) { - const char *ws = diff_get_color(ecbdata->color_diff, DIFF_WHITESPACE); - const char *set = diff_get_color(ecbdata->color_diff, DIFF_FILE_NEW); - - if (!*ws) - emit_line_0(ecbdata->opt, set, reset, '+', line, len); - else if (new_blank_line_at_eof(ecbdata, line, len)) - /* Blank line at EOF - paint '+' as well */ - emit_line_0(ecbdata->opt, ws, reset, '+', line, len); - else { - /* Emit just the prefix, then the rest. */ - emit_line_0(ecbdata->opt, set, reset, '+', "", 0); - ws_check_emit(line, len, ecbdata->ws_rule, - ecbdata->opt->file, set, reset, ws); - } + emit_line_checked(reset, ecbdata, line, len, + DIFF_FILE_NEW, WSEH_NEW, '+'); } static void emit_del_line(const char *reset, struct emit_callback *ecbdata, const char *line, int len) { - const char *set = diff_get_color(ecbdata->color_diff, DIFF_FILE_OLD); - - emit_line_0(ecbdata->opt, set, reset, '-', line, len); + emit_line_checked(reset, ecbdata, line, len, + DIFF_FILE_OLD, WSEH_OLD, '-'); } static void emit_context_line(const char *reset, struct emit_callback *ecbdata, const char *line, int len) { - const char *set = diff_get_color(ecbdata->color_diff, DIFF_PLAIN); - - emit_line_0(ecbdata->opt, set, reset, ' ', line, len); + emit_line_checked(reset, ecbdata, line, len, + DIFF_PLAIN, WSEH_CONTEXT, ' '); } static void emit_hunk_header(struct emit_callback *ecbdata, @@ -3250,6 +3265,7 @@ void diff_setup(struct diff_options *options) options->rename_limit = -1; options->dirstat_permille = diff_dirstat_permille_default; options->context = diff_context_default; + options->ws_error_highlight = WSEH_NEW; DIFF_OPT_SET(options, RENAME_EMPTY); /* pathchange left =NULL by default */ @@ -3636,6 +3652,40 @@ static void enable_patch_output(int *fmt) { *fmt |= DIFF_FORMAT_PATCH; } +static int parse_one_token(const char **arg, const char *token) +{ + return skip_prefix(*arg, token, arg) && (!**arg || **arg == ','); +} + +static int parse_ws_error_highlight(struct diff_options *opt, const char *arg) +{ + const char *orig_arg = arg; + unsigned val = 0; + while (*arg) { + if (parse_one_token(&arg, "none")) + val = 0; + else if (parse_one_token(&arg, "default")) + val = WSEH_NEW; + else if (parse_one_token(&arg, "all")) + val = WSEH_NEW | WSEH_OLD | WSEH_CONTEXT; + else if (parse_one_token(&arg, "new")) + val |= WSEH_NEW; + else if (parse_one_token(&arg, "old")) + val |= WSEH_OLD; + else if (parse_one_token(&arg, "context")) + val |= WSEH_CONTEXT; + else { + error("unknown value after ws-error-highlight=%.*s", + (int)(arg - orig_arg), orig_arg); + return 0; + } + if (*arg) + arg++; + } + opt->ws_error_highlight = val; + return 1; +} + int diff_opt_parse(struct diff_options *options, const char **av, int ac) { const char *arg = av[0]; @@ -3833,6 +3883,8 @@ int diff_opt_parse(struct diff_options *options, const char **av, int ac) DIFF_OPT_SET(options, SUBMODULE_LOG); else if (skip_prefix(arg, "--submodule=", &arg)) return parse_submodule_opt(options, arg); + else if (skip_prefix(arg, "--ws-error-highlight=", &arg)) + return parse_ws_error_highlight(options, arg); /* misc options */ else if (!strcmp(arg, "-z")) diff --git a/diff.h b/diff.h index b4a624d235..90c7cd61f5 100644 --- a/diff.h +++ b/diff.h @@ -137,6 +137,11 @@ struct diff_options { int dirstat_permille; int setup; int abbrev; +/* white-space error highlighting */ +#define WSEH_NEW 1 +#define WSEH_CONTEXT 2 +#define WSEH_OLD 4 + unsigned ws_error_highlight; const char *prefix; int prefix_length; const char *stat_sep; diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh index 4da30e5a1e..2434157aa7 100755 --- a/t/t4015-diff-whitespace.sh +++ b/t/t4015-diff-whitespace.sh @@ -838,4 +838,100 @@ test_expect_success 'diff that introduces a line with only tabs' ' test_cmp expected current ' +test_expect_success 'diff that introduces and removes ws breakages' ' + git reset --hard && + { + echo "0. blank-at-eol " && + echo "1. blank-at-eol " + } >x && + git commit -a --allow-empty -m preimage && + { + echo "0. blank-at-eol " && + echo "1. still-blank-at-eol " && + echo "2. and a new line " + } >x && + + git -c color.diff=always diff | + test_decode_color >current && + + cat >expected <<-\EOF && + diff --git a/x b/x + index d0233a2..700886e 100644 + --- a/x + +++ b/x + @@ -1,2 +1,3 @@ + 0. blank-at-eol + -1. blank-at-eol + +1. still-blank-at-eol + +2. and a new line + EOF + + test_cmp expected current +' + +test_expect_success 'the same with --ws-error-highlight' ' + git reset --hard && + { + echo "0. blank-at-eol " && + echo "1. blank-at-eol " + } >x && + git commit -a --allow-empty -m preimage && + { + echo "0. blank-at-eol " && + echo "1. still-blank-at-eol " && + echo "2. and a new line " + } >x && + + git -c color.diff=always diff --ws-error-highlight=default,old | + test_decode_color >current && + + cat >expected <<-\EOF && + diff --git a/x b/x + index d0233a2..700886e 100644 + --- a/x + +++ b/x + @@ -1,2 +1,3 @@ + 0. blank-at-eol + -1. blank-at-eol + +1. still-blank-at-eol + +2. and a new line + EOF + + test_cmp expected current && + + git -c color.diff=always diff --ws-error-highlight=all | + test_decode_color >current && + + cat >expected <<-\EOF && + diff --git a/x b/x + index d0233a2..700886e 100644 + --- a/x + +++ b/x + @@ -1,2 +1,3 @@ + 0. blank-at-eol + -1. blank-at-eol + +1. still-blank-at-eol + +2. and a new line + EOF + + test_cmp expected current && + + git -c color.diff=always diff --ws-error-highlight=none | + test_decode_color >current && + + cat >expected <<-\EOF && + diff --git a/x b/x + index d0233a2..700886e 100644 + --- a/x + +++ b/x + @@ -1,2 +1,3 @@ + 0. blank-at-eol + -1. blank-at-eol + +1. still-blank-at-eol + +2. and a new line + EOF + + test_cmp expected current +' + test_done