Merge branch 'jc/whitespace-incomplete-line'

Both "git apply" and "git diff" learn a new whitespace error class,
"incomplete-line".

* jc/whitespace-incomplete-line:
  attr: enable incomplete-line whitespace error for this project
  diff: highlight and error out on incomplete lines
  apply: check and fix incomplete lines
  whitespace: allocate a few more bits and define WS_INCOMPLETE_LINE
  apply: revamp the parsing of incomplete lines
  diff: update the way rewrite diff handles incomplete lines
  diff: call emit_callback ecbdata everywhere
  diff: refactor output of incomplete line
  diff: keep track of the type of the last line seen
  diff: correct suppress_blank_empty hack
  diff: emit_line_ws_markup() if/else style fix
  whitespace: correct bit assignment comments
This commit is contained in:
Junio C Hamano
2025-11-30 18:31:40 -08:00
9 changed files with 451 additions and 89 deletions

6
.gitattributes vendored
View File

@@ -1,13 +1,13 @@
* whitespace=trail,space
*.[ch] whitespace=indent,trail,space diff=cpp
*.sh whitespace=indent,trail,space text eol=lf
*.[ch] whitespace=indent,trail,space,incomplete diff=cpp
*.sh whitespace=indent,trail,space,incomplete text eol=lf
*.perl text eol=lf diff=perl
*.pl text eof=lf diff=perl
*.pm text eol=lf diff=perl
*.py text eol=lf diff=python
*.bat text eol=crlf
CODE_OF_CONDUCT.md -whitespace
/Documentation/**/*.adoc text eol=lf
/Documentation/**/*.adoc text eol=lf whitespace=trail,space,incomplete
/command-list.txt text eol=lf
/GIT-VERSION-GEN text eol=lf
/mergetools/* text eol=lf

View File

@@ -629,6 +629,8 @@ core.whitespace::
part of the line terminator, i.e. with it, `trailing-space`
does not trigger if the character before such a carriage-return
is not a whitespace (not enabled by default).
* `incomplete-line` treats the last line of a file that is missing the
newline at the end as an error (not enabled by default).
* `tabwidth=<n>` tells how many character positions a tab occupies; this
is relevant for `indent-with-non-tab` and when Git fixes `tab-in-indent`
errors. The default tab width is 8. Allowed values are 1 to 63.

79
apply.c
View File

@@ -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",
@@ -1670,6 +1678,35 @@ static void check_old_for_crlf(struct patch *patch, const char *line, int len)
}
/*
* Just saw a single line in a fragment. If it is a part of this hunk
* that is a context " ", an added "+", or a removed "-" line, it may
* be followed by "\\ No newline..." to signal that the last "\n" on
* this line needs to be dropped. Depending on locale settings when
* the patch was produced we don't know what this line would exactly
* say. The only thing we do know is that it begins with "\ ".
* Checking for 12 is just for sanity check; "\ No newline..." would
* be at least that long in any l10n.
*
* Return 0 if the line we saw is not followed by "\ No newline...",
* or length of that line. The caller will use it to skip over the
* "\ No newline..." line.
*/
static int adjust_incomplete(const char *line, int len,
unsigned long size)
{
int nextlen;
if (*line != '\n' && *line != ' ' && *line != '+' && *line != '-')
return 0;
if (size - len < 12 || memcmp(line + len, "\\ ", 2))
return 0;
nextlen = linelen(line + len, size - len);
if (nextlen < 12)
return 0;
return nextlen;
}
/*
* Parse a unified diff. Note that this really needs to parse each
* fragment separately, since the only way to know the difference
@@ -1684,6 +1721,7 @@ static int parse_fragment(struct apply_state *state,
{
int added, deleted;
int len = linelen(line, size), offset;
int skip_len = 0;
unsigned long oldlines, newlines;
unsigned long leading, trailing;
@@ -1710,6 +1748,22 @@ static int parse_fragment(struct apply_state *state,
len = linelen(line, size);
if (!len || line[len-1] != '\n')
return -1;
/*
* For an incomplete line, skip_len counts the bytes
* on "\\ No newline..." marker line that comes next
* to the current line.
*
* Reduce "len" to drop the newline at the end of
* line[], but add one to "skip_len", which will be
* added back to "len" for the next iteration, to
* compensate.
*/
skip_len = adjust_incomplete(line, len, size);
if (skip_len) {
len--;
skip_len++;
}
switch (*line) {
default:
return -1;
@@ -1745,19 +1799,12 @@ static int parse_fragment(struct apply_state *state,
newlines--;
trailing = 0;
break;
}
/*
* We allow "\ No newline at end of file". Depending
* on locale settings when the patch was produced we
* don't know what this line looks like. The only
* thing we do know is that it begins with "\ ".
* Checking for 12 is just for sanity check -- any
* l10n of "\ No newline..." is at least that long.
*/
case '\\':
if (len < 12 || memcmp(line, "\\ ", 2))
return -1;
break;
/* eat the "\\ No newline..." as well, if exists */
if (skip_len) {
len += skip_len;
state->linenr++;
}
}
if (oldlines || newlines)
@@ -1768,14 +1815,6 @@ static int parse_fragment(struct apply_state *state,
fragment->leading = leading;
fragment->trailing = trailing;
/*
* If a fragment ends with an incomplete line, we failed to include
* it in the above loop because we hit oldlines == newlines == 0
* before seeing it.
*/
if (12 < size && !memcmp(line, "\\ ", 2))
offset += linelen(line, size);
patch->lines_added += added;
patch->lines_deleted += deleted;

147
diff.c
View File

@@ -601,6 +601,7 @@ struct emit_callback {
int blank_at_eof_in_postimage;
int lno_in_preimage;
int lno_in_postimage;
int last_line_kind;
const char **label_path;
struct diff_words_data *diff_words;
struct diff_options *opt;
@@ -796,21 +797,23 @@ enum diff_symbol {
DIFF_SYMBOL_CONTEXT_INCOMPLETE,
DIFF_SYMBOL_PLUS,
DIFF_SYMBOL_MINUS,
DIFF_SYMBOL_NO_LF_EOF,
DIFF_SYMBOL_CONTEXT_FRAGINFO,
DIFF_SYMBOL_CONTEXT_MARKER,
DIFF_SYMBOL_SEPARATOR
};
/*
* Flags for content lines:
* 0..12 are whitespace rules
* 13-15 are WSEH_NEW | WSEH_OLD | WSEH_CONTEXT
* 16 is marking if the line is blank at EOF
* 0..15 are whitespace rules (see ws.h)
* 16..18 are WSEH_NEW | WSEH_CONTEXT | WSEH_OLD
* 19 is marking if the line is blank at EOF
* 20..22 are used for color-moved.
*/
#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF (1<<16)
#define DIFF_SYMBOL_MOVED_LINE (1<<17)
#define DIFF_SYMBOL_MOVED_LINE_ALT (1<<18)
#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING (1<<19)
#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF (1<<19)
#define DIFF_SYMBOL_MOVED_LINE (1<<20)
#define DIFF_SYMBOL_MOVED_LINE_ALT (1<<21)
#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING (1<<22)
#define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)
/*
@@ -1318,20 +1321,25 @@ static void emit_line_ws_markup(struct diff_options *o,
const char *ws = NULL;
int sign = o->output_indicators[sign_index];
if (diff_suppress_blank_empty &&
sign_index == OUTPUT_INDICATOR_CONTEXT &&
len == 1 && line[0] == '\n')
sign = 0;
if (o->ws_error_highlight & ws_rule) {
ws = diff_get_color_opt(o, DIFF_WHITESPACE);
if (!*ws)
ws = NULL;
}
if (!ws && !set_sign)
if (!ws && !set_sign) {
emit_line_0(o, set, NULL, 0, reset, sign, line, len);
else if (!ws) {
} else if (!ws) {
emit_line_0(o, set_sign, set, !!set_sign, reset, sign, line, len);
} else if (blank_at_eof)
} else if (blank_at_eof) {
/* Blank line at EOF - paint '+' as well */
emit_line_0(o, ws, NULL, 0, reset, sign, line, len);
else {
} else {
/* Emit just the prefix, then the rest. */
emit_line_0(o, set_sign ? set_sign : set, NULL, !!set_sign, reset,
sign, "", 0);
@@ -1343,7 +1351,6 @@ static void emit_line_ws_markup(struct diff_options *o,
static void emit_diff_symbol_from_struct(struct diff_options *o,
struct emitted_diff_symbol *eds)
{
static const char *nneof = " No newline at end of file\n";
const char *context, *reset, *set, *set_sign, *meta, *fraginfo;
enum diff_symbol s = eds->s;
@@ -1355,13 +1362,6 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
return;
switch (s) {
case DIFF_SYMBOL_NO_LF_EOF:
context = diff_get_color_opt(o, DIFF_CONTEXT);
reset = diff_get_color_opt(o, DIFF_RESET);
putc('\n', o->file);
emit_line_0(o, context, NULL, 0, reset, '\\',
nneof, strlen(nneof));
break;
case DIFF_SYMBOL_SUBMODULE_HEADER:
case DIFF_SYMBOL_SUBMODULE_ERROR:
case DIFF_SYMBOL_SUBMODULE_PIPETHROUGH:
@@ -1373,6 +1373,14 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
emit_line(o, "", "", line, len);
break;
case DIFF_SYMBOL_CONTEXT_INCOMPLETE:
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;
case DIFF_SYMBOL_CONTEXT_MARKER:
context = diff_get_color_opt(o, DIFF_CONTEXT);
reset = diff_get_color_opt(o, DIFF_RESET);
@@ -1498,15 +1506,9 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
case DIFF_SYMBOL_WORDS:
context = diff_get_color_opt(o, DIFF_CONTEXT);
reset = diff_get_color_opt(o, DIFF_RESET);
/*
* Skip the prefix character, if any. With
* diff_suppress_blank_empty, there may be
* none.
*/
if (line[0] != '\n') {
line++;
len--;
}
/* Skip the prefix character */
line++; len--;
emit_line(o, context, reset, line, len);
break;
case DIFF_SYMBOL_FILEPAIR_PLUS:
@@ -1668,6 +1670,19 @@ static void emit_context_line(struct emit_callback *ecbdata,
emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT, line, len, flags);
}
static void emit_incomplete_line_marker(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, flags);
}
static void emit_hunk_header(struct emit_callback *ecbdata,
const char *line, int len)
{
@@ -1769,28 +1784,44 @@ static void add_line_count(struct strbuf *out, int count)
}
}
static void emit_rewrite_lines(struct emit_callback *ecb,
static void emit_rewrite_lines(struct emit_callback *ecbdata,
int prefix, const char *data, int size)
{
const char *endp = NULL;
while (0 < size) {
int len;
int len, plen;
char *pdata = NULL;
endp = memchr(data, '\n', size);
len = endp ? (endp - data + 1) : size;
if (prefix != '+') {
ecb->lno_in_preimage++;
emit_del_line(ecb, data, len);
if (endp) {
len = endp - data + 1;
plen = len;
} else {
ecb->lno_in_postimage++;
emit_add_line(ecb, data, len);
len = size;
plen = len + 1;
pdata = xmalloc(plen + 2);
memcpy(pdata, data, len);
pdata[len] = '\n';
pdata[len + 1] = '\0';
}
if (prefix != '+') {
ecbdata->lno_in_preimage++;
emit_del_line(ecbdata, pdata ? pdata : data, plen);
} else {
ecbdata->lno_in_postimage++;
emit_add_line(ecbdata, pdata ? pdata : data, plen);
}
free(pdata);
size -= len;
data += len;
}
if (!endp)
emit_diff_symbol(ecb->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
if (!endp) {
static const char nneof[] = "\\ No newline at end of file\n";
ecbdata->last_line_kind = prefix;
emit_incomplete_line_marker(ecbdata, nneof, sizeof(nneof) - 1);
}
}
static void emit_rewrite_diff(const char *name_a,
@@ -2375,12 +2406,6 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
ecbdata->label_path[0] = ecbdata->label_path[1] = NULL;
}
if (diff_suppress_blank_empty
&& len == 2 && line[0] == ' ' && line[1] == '\n') {
line[0] = '\n';
len = 1;
}
if (line[0] == '@') {
if (ecbdata->diff_words)
diff_words_flush(ecbdata);
@@ -2431,13 +2456,24 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
ecbdata->lno_in_preimage++;
emit_context_line(ecbdata, line + 1, len - 1);
break;
default:
case '\\':
/* incomplete line at the end */
switch (ecbdata->last_line_kind) {
case '+':
case '-':
case ' ':
break;
default:
BUG("fn_out_consume: '\\No newline' after unknown line (%c)",
ecbdata->last_line_kind);
}
ecbdata->lno_in_preimage++;
emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
line, len, 0);
emit_incomplete_line_marker(ecbdata, line, len);
break;
default:
BUG("fn_out_consume: unknown line '%s'", line);
}
ecbdata->last_line_kind = line[0];
return 0;
}
@@ -3231,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)
@@ -3269,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);
@@ -3279,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++;
@@ -3301,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;
}

6
diff.h
View File

@@ -331,9 +331,9 @@ struct diff_options {
int ita_invisible_in_index;
/* white-space error highlighting */
#define WSEH_NEW (1<<12)
#define WSEH_CONTEXT (1<<13)
#define WSEH_OLD (1<<14)
#define WSEH_NEW (1<<16)
#define WSEH_CONTEXT (1<<17)
#define WSEH_OLD (1<<18)
unsigned ws_error_highlight;
const char *prefix;
int prefix_length;

View File

@@ -43,6 +43,53 @@ 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 touching
# 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 >error &&
test_grep "no newline at the end of file" error &&
git diff -R x &&
test_must_fail git -c core.whitespace=incomplete diff -R --check x >error &&
test_grep "no newline at the end of file" error
'
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 >error &&
test_grep "no newline at the end of file" error
'
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 >error &&
test_grep "no newline at the end of file" error &&
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 +1087,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 +1098,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 +1112,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 +1126,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 +1155,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 +1172,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 +1192,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>

View File

@@ -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

20
ws.c
View File

@@ -26,6 +26,7 @@ static struct whitespace_rule {
{ "blank-at-eol", WS_BLANK_AT_EOL, 0 },
{ "blank-at-eof", WS_BLANK_AT_EOF, 0 },
{ "tab-in-indent", WS_TAB_IN_INDENT, 0, 1 },
{ "incomplete-line", WS_INCOMPLETE_LINE, 0, 0 },
};
unsigned parse_whitespace_rule(const char *string)
@@ -139,6 +140,11 @@ char *whitespace_error_string(unsigned ws)
strbuf_addstr(&err, ", ");
strbuf_addstr(&err, "tab in indent");
}
if (ws & WS_INCOMPLETE_LINE) {
if (err.len)
strbuf_addstr(&err, ", ");
strbuf_addstr(&err, "no newline at the end of file");
}
return strbuf_detach(&err, NULL);
}
@@ -180,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] == ' ')
@@ -291,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
*/

26
ws.h
View File

@@ -7,19 +7,23 @@ struct strbuf;
/*
* whitespace rules.
* used by both diff and apply
* last two digits are tab width
* last two octal-digits are tab width (we support only up to 63).
*/
#define WS_BLANK_AT_EOL 0100
#define WS_SPACE_BEFORE_TAB 0200
#define WS_INDENT_WITH_NON_TAB 0400
#define WS_CR_AT_EOL 01000
#define WS_BLANK_AT_EOF 02000
#define WS_TAB_IN_INDENT 04000
#define WS_TRAILING_SPACE (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
#define WS_BLANK_AT_EOL (1<<6)
#define WS_SPACE_BEFORE_TAB (1<<7)
#define WS_INDENT_WITH_NON_TAB (1<<8)
#define WS_CR_AT_EOL (1<<9)
#define WS_BLANK_AT_EOF (1<<10)
#define WS_TAB_IN_INDENT (1<<11)
#define WS_INCOMPLETE_LINE (1<<12)
#define WS_TRAILING_SPACE (WS_BLANK_AT_EOL|WS_BLANK_AT_EOF)
#define WS_DEFAULT_RULE (WS_TRAILING_SPACE|WS_SPACE_BEFORE_TAB|8)
#define WS_TAB_WIDTH_MASK 077
/* All WS_* -- when extended, adapt diff.c emit_symbol */
#define WS_RULE_MASK 07777
#define WS_TAB_WIDTH_MASK ((1<<6)-1)
/* All WS_* -- when extended, adapt constants defined after diff.c:diff_symbol */
#define WS_RULE_MASK ((1<<16)-1)
extern unsigned whitespace_rule_cfg;
unsigned whitespace_rule(struct index_state *, const char *);
unsigned parse_whitespace_rule(const char *);