aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2025-11-30 18:31:40 -0800
committerJunio C Hamano <gitster@pobox.com>2025-11-30 18:31:40 -0800
commit3b212a83feea0962d437a3d5883cdd635403fe3c (patch)
tree446e71b654752ef15a60d8a57869f322c75cc9af
parentffd9bb1bc7ffc635ff8687b79c23a6a0a3551185 (diff)
parent51358a1ede7f4b6b50e4e5a86558af5204691fe0 (diff)
downloadgit-3b212a83feea0962d437a3d5883cdd635403fe3c.tar.gz
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
-rw-r--r--.gitattributes6
-rw-r--r--Documentation/config/core.adoc2
-rw-r--r--apply.c79
-rw-r--r--diff.c145
-rw-r--r--diff.h6
-rwxr-xr-xt/t4015-diff-whitespace.sh67
-rwxr-xr-xt/t4124-apply-ws-rule.sh187
-rw-r--r--ws.c20
-rw-r--r--ws.h26
9 files changed, 450 insertions, 88 deletions
diff --git a/.gitattributes b/.gitattributes
index 6e66623b12..700743c3f5 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -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
diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc
index 11efad189e..01202da7cd 100644
--- a/Documentation/config/core.adoc
+++ b/Documentation/config/core.adoc
@@ -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.
diff --git a/apply.c b/apply.c
index a2ceb3fb40..c9fb45247d 100644
--- 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",
@@ -1671,6 +1679,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
* between a "---" that is part of a patch, and a "---" that starts
@@ -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;
diff --git a/diff.c b/diff.c
index 915317025f..f66dd7ff6b 100644
--- a/diff.c
+++ b/diff.c
@@ -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 (endp) {
+ len = endp - data + 1;
+ plen = len;
+ } else {
+ len = size;
+ plen = len + 1;
+ pdata = xmalloc(plen + 2);
+ memcpy(pdata, data, len);
+ pdata[len] = '\n';
+ pdata[len + 1] = '\0';
+ }
if (prefix != '+') {
- ecb->lno_in_preimage++;
- emit_del_line(ecb, data, len);
+ ecbdata->lno_in_preimage++;
+ emit_del_line(ecbdata, pdata ? pdata : data, plen);
} else {
- ecb->lno_in_postimage++;
- emit_add_line(ecb, data, len);
+ 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;
}
diff --git a/diff.h b/diff.h
index 31eedd5c0c..b3a4c6335b 100644
--- a/diff.h
+++ b/diff.h
@@ -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;
diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh
index 9de7f73f42..3c8eb02e4f 100755
--- a/t/t4015-diff-whitespace.sh
+++ b/t/t4015-diff-whitespace.sh
@@ -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>
diff --git a/t/t4124-apply-ws-rule.sh b/t/t4124-apply-ws-rule.sh
index 485c7d2d12..115a0f8579 100755
--- a/t/t4124-apply-ws-rule.sh
+++ b/t/t4124-apply-ws-rule.sh
@@ -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 70acee3337..6cc2466c0c 100644
--- a/ws.c
+++ b/ws.c
@@ -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] == ' ')
@@ -292,6 +301,17 @@ void ws_fix_copy(struct strbuf *dst, const char *src, int len, unsigned ws_rule,
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
*/
if (ws_rule & WS_BLANK_AT_EOL) {
diff --git a/ws.h b/ws.h
index 5ba676c559..06d5cb73f8 100644
--- a/ws.h
+++ b/ws.h
@@ -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 *);