Skip to content

Commit

Permalink
git apply: option to ignore whitespace differences
Browse files Browse the repository at this point in the history
Introduce --ignore-whitespace option and corresponding config bool to
ignore whitespace differences while applying patches, akin to the
'patch' program.

'git am', 'git rebase' and the bash git completion are made aware of
this option.

Signed-off-by: Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
  • Loading branch information
Giuseppe Bilotta authored and Junio C Hamano committed Aug 5, 2009
1 parent 07a4a3b commit 86c91f9
Show file tree
Hide file tree
Showing 11 changed files with 389 additions and 10 deletions.
8 changes: 8 additions & 0 deletions Documentation/config.txt
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,14 @@ it will be treated as a shell command. For example, defining
executed from the top-level directory of a repository, which may
not necessarily be the current directory.

apply.ignorewhitespace::
When set to 'change', tells 'git-apply' to ignore changes in
whitespace, in the same way as the '--ignore-space-change'
option.
When set to one of: no, none, never, false tells 'git-apply' to
respect all whitespace differences.
See linkgit:git-apply[1].

apply.whitespace::
Tells 'git-apply' how to handle whitespaces, in the same way
as the '--whitespace' option. See linkgit:git-apply[1].
Expand Down
5 changes: 4 additions & 1 deletion Documentation/git-am.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ SYNOPSIS
[verse]
'git am' [--signoff] [--keep] [--utf8 | --no-utf8]
[--3way] [--interactive] [--committer-date-is-author-date]
[--ignore-date]
[--ignore-date] [--ignore-space-change | --ignore-whitespace]
[--whitespace=<option>] [-C<n>] [-p<n>] [--directory=<dir>]
[--reject] [-q | --quiet]
[<mbox> | <Maildir>...]
Expand Down Expand Up @@ -65,6 +65,9 @@ default. You can use `--no-utf8` to override this.
it is supposed to apply to and we have those blobs
available locally.

--ignore-date::
--ignore-space-change::
--ignore-whitespace::
--whitespace=<option>::
-C<n>::
-p<n>::
Expand Down
13 changes: 13 additions & 0 deletions Documentation/git-apply.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ SYNOPSIS
[--apply] [--no-add] [--build-fake-ancestor=<file>] [-R | --reverse]
[--allow-binary-replacement | --binary] [--reject] [-z]
[-pNUM] [-CNUM] [--inaccurate-eof] [--recount] [--cached]
[--ignore-space-change | --ignore-whitespace ]
[--whitespace=<nowarn|warn|fix|error|error-all>]
[--exclude=PATH] [--include=PATH] [--directory=<root>]
[--verbose] [<patch>...]
Expand Down Expand Up @@ -149,6 +150,14 @@ patch to each path is used. A patch to a path that does not match any
include/exclude pattern is used by default if there is no include pattern
on the command line, and ignored if there is any include pattern.

--ignore-space-change::
--ignore-whitespace::
When applying a patch, ignore changes in whitespace in context
lines if necessary.
Context lines will preserve their whitespace, and they will not
undergo whitespace fixing regardless of the value of the
`--whitespace` option. New lines will still be fixed, though.

--whitespace=<action>::
When applying a patch, detect a new or modified line that has
whitespace errors. What are considered whitespace errors is
Expand Down Expand Up @@ -205,6 +214,10 @@ running `git apply --directory=modules/git-gui`.
Configuration
-------------

apply.ignorewhitespace::
Set to 'change' if you want changes in whitespace to be ignored by default.
Set to one of: no, none, never, false if you want changes in
whitespace to be significant.
apply.whitespace::
When no `--whitespace` flag is given from the command
line, this configuration item is used as the default.
Expand Down
3 changes: 2 additions & 1 deletion Documentation/git-rebase.txt
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,9 @@ OPTIONS
exit with the message "Current branch is up to date" in such a
situation.

--ignore-whitespace::
--whitespace=<option>::
This flag is passed to the 'git-apply' program
These flag are passed to the 'git-apply' program
(see linkgit:git-apply[1]) that applies the patch.
Incompatible with the --interactive option.

Expand Down
173 changes: 166 additions & 7 deletions builtin-apply.c
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ static enum ws_error_action {
static int whitespace_error;
static int squelch_whitespace_errors = 5;
static int applied_after_fixing_ws;

static enum ws_ignore {
ignore_ws_none,
ignore_ws_change,
} ws_ignore_action = ignore_ws_none;


static const char *patch_input_file;
static const char *root;
static int root_len;
Expand Down Expand Up @@ -97,6 +104,21 @@ static void parse_whitespace_option(const char *option)
die("unrecognized whitespace option '%s'", option);
}

static void parse_ignorewhitespace_option(const char *option)
{
if (!option || !strcmp(option, "no") ||
!strcmp(option, "false") || !strcmp(option, "never") ||
!strcmp(option, "none")) {
ws_ignore_action = ignore_ws_none;
return;
}
if (!strcmp(option, "change")) {
ws_ignore_action = ignore_ws_change;
return;
}
die("unrecognized whitespace ignore option '%s'", option);
}

static void set_default_whitespace_mode(const char *whitespace_option)
{
if (!whitespace_option && !apply_default_whitespace)
Expand Down Expand Up @@ -214,6 +236,62 @@ static uint32_t hash_line(const char *cp, size_t len)
return h;
}

/*
* Compare lines s1 of length n1 and s2 of length n2, ignoring
* whitespace difference. Returns 1 if they match, 0 otherwise
*/
static int fuzzy_matchlines(const char *s1, size_t n1,
const char *s2, size_t n2)
{
const char *last1 = s1 + n1 - 1;
const char *last2 = s2 + n2 - 1;
int result = 0;

if (n1 < 0 || n2 < 0)
return 0;

/* ignore line endings */
while ((*last1 == '\r') || (*last1 == '\n'))
last1--;
while ((*last2 == '\r') || (*last2 == '\n'))
last2--;

/* skip leading whitespace */
while (isspace(*s1) && (s1 <= last1))
s1++;
while (isspace(*s2) && (s2 <= last2))
s2++;
/* early return if both lines are empty */
if ((s1 > last1) && (s2 > last2))
return 1;
while (!result) {
result = *s1++ - *s2++;
/*
* Skip whitespace inside. We check for whitespace on
* both buffers because we don't want "a b" to match
* "ab"
*/
if (isspace(*s1) && isspace(*s2)) {
while (isspace(*s1) && s1 <= last1)
s1++;
while (isspace(*s2) && s2 <= last2)
s2++;
}
/*
* If we reached the end on one side only,
* lines don't match
*/
if (
((s2 > last2) && (s1 <= last1)) ||
((s1 > last1) && (s2 <= last2)))
return 0;
if ((s1 > last1) && (s2 > last2))
break;
}

return !result;
}

static void add_line_info(struct image *img, const char *bol, size_t len, unsigned flag)
{
ALLOC_GROW(img->line_allocated, img->nr + 1, img->alloc);
Expand Down Expand Up @@ -1592,10 +1670,17 @@ static int read_old_data(struct stat *st, const char *path, struct strbuf *buf)
}
}

/*
* Update the preimage, and the common lines in postimage,
* from buffer buf of length len. If postlen is 0 the postimage
* is updated in place, otherwise it's updated on a new buffer
* of length postlen
*/

static void update_pre_post_images(struct image *preimage,
struct image *postimage,
char *buf,
size_t len)
size_t len, size_t postlen)
{
int i, ctx;
char *new, *old, *fixed;
Expand All @@ -1614,11 +1699,19 @@ static void update_pre_post_images(struct image *preimage,
*preimage = fixed_preimage;

/*
* Adjust the common context lines in postimage, in place.
* This is possible because whitespace fixing does not make
* the string grow.
* Adjust the common context lines in postimage. This can be
* done in-place when we are just doing whitespace fixing,
* which does not make the string grow, but needs a new buffer
* when ignoring whitespace causes the update, since in this case
* we could have e.g. tabs converted to multiple spaces.
* We trust the caller to tell us if the update can be done
* in place (postlen==0) or not.
*/
new = old = postimage->buf;
old = postimage->buf;
if (postlen)
new = postimage->buf = xmalloc(postlen);
else
new = old;
fixed = preimage->buf;
for (i = ctx = 0; i < postimage->nr; i++) {
size_t len = postimage->line[i].len;
Expand Down Expand Up @@ -1693,12 +1786,58 @@ static int match_fragment(struct image *img,
!memcmp(img->buf + try, preimage->buf, preimage->len))
return 1;

/*
* No exact match. If we are ignoring whitespace, run a line-by-line
* fuzzy matching. We collect all the line length information because
* we need it to adjust whitespace if we match.
*/
if (ws_ignore_action == ignore_ws_change) {
size_t imgoff = 0;
size_t preoff = 0;
size_t postlen = postimage->len;
size_t imglen[preimage->nr];
for (i = 0; i < preimage->nr; i++) {
size_t prelen = preimage->line[i].len;

imglen[i] = img->line[try_lno+i].len;
if (!fuzzy_matchlines(
img->buf + try + imgoff, imglen[i],
preimage->buf + preoff, prelen))
return 0;
if (preimage->line[i].flag & LINE_COMMON)
postlen += imglen[i] - prelen;
imgoff += imglen[i];
preoff += prelen;
}

/*
* Ok, the preimage matches with whitespace fuzz. Update it and
* the common postimage lines to use the same whitespace as the
* target. imgoff now holds the true length of the target that
* matches the preimage, and we need to update the line lengths
* of the preimage to match the target ones.
*/
fixed_buf = xmalloc(imgoff);
memcpy(fixed_buf, img->buf + try, imgoff);
for (i = 0; i < preimage->nr; i++)
preimage->line[i].len = imglen[i];

/*
* Update the preimage buffer and the postimage context lines.
*/
update_pre_post_images(preimage, postimage,
fixed_buf, imgoff, postlen);
return 1;
}

if (ws_error_action != correct_ws_error)
return 0;

/*
* The hunk does not apply byte-by-byte, but the hash says
* it might with whitespace fuzz.
* it might with whitespace fuzz. We haven't been asked to
* ignore whitespace, we were asked to correct whitespace
* errors, so let's try matching after whitespace correction.
*/
fixed_buf = xmalloc(preimage->len + 1);
buf = fixed_buf;
Expand Down Expand Up @@ -1750,7 +1889,7 @@ static int match_fragment(struct image *img,
* hunk match. Update the context lines in the postimage.
*/
update_pre_post_images(preimage, postimage,
fixed_buf, buf - fixed_buf);
fixed_buf, buf - fixed_buf, 0);
return 1;

unmatch_exit:
Expand Down Expand Up @@ -3192,6 +3331,8 @@ static int git_apply_config(const char *var, const char *value, void *cb)
{
if (!strcmp(var, "apply.whitespace"))
return git_config_string(&apply_default_whitespace, var, value);
else if (!strcmp(var, "apply.ignorewhitespace"))
return git_config_string(&apply_default_ignorewhitespace, var, value);
return git_default_config(var, value, cb);
}

Expand Down Expand Up @@ -3228,6 +3369,16 @@ static int option_parse_z(const struct option *opt,
return 0;
}

static int option_parse_space_change(const struct option *opt,
const char *arg, int unset)
{
if (unset)
ws_ignore_action = ignore_ws_none;
else
ws_ignore_action = ignore_ws_change;
return 0;
}

static int option_parse_whitespace(const struct option *opt,
const char *arg, int unset)
{
Expand Down Expand Up @@ -3304,6 +3455,12 @@ int cmd_apply(int argc, const char **argv, const char *unused_prefix)
{ OPTION_CALLBACK, 0, "whitespace", &whitespace_option, "action",
"detect new or modified lines that have whitespace errors",
0, option_parse_whitespace },
{ OPTION_CALLBACK, 0, "ignore-space-change", NULL, NULL,
"ignore changes in whitespace when finding context",
PARSE_OPT_NOARG, option_parse_space_change },
{ OPTION_CALLBACK, 0, "ignore-whitespace", NULL, NULL,
"ignore changes in whitespace when finding context",
PARSE_OPT_NOARG, option_parse_space_change },
OPT_BOOLEAN('R', "reverse", &apply_in_reverse,
"apply the patch in reverse"),
OPT_BOOLEAN(0, "unidiff-zero", &unidiff_zero,
Expand All @@ -3328,6 +3485,8 @@ int cmd_apply(int argc, const char **argv, const char *unused_prefix)
git_config(git_apply_config, NULL);
if (apply_default_whitespace)
parse_whitespace_option(apply_default_whitespace);
if (apply_default_ignorewhitespace)
parse_ignorewhitespace_option(apply_default_ignorewhitespace);

argc = parse_options(argc, argv, prefix, builtin_apply_options,
apply_usage, 0);
Expand Down
1 change: 1 addition & 0 deletions cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ extern int log_all_ref_updates;
extern int warn_ambiguous_refs;
extern int shared_repository;
extern const char *apply_default_whitespace;
extern const char *apply_default_ignorewhitespace;
extern int zlib_compression_level;
extern int core_compression_level;
extern int core_compression_seen;
Expand Down
3 changes: 3 additions & 0 deletions contrib/completion/git-completion.bash
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,7 @@ _git_am ()
--*)
__gitcomp "
--3way --committer-date-is-author-date --ignore-date
--ignore-whitespace --ignore-space-change
--interactive --keep --no-utf8 --signoff --utf8
--whitespace=
"
Expand All @@ -695,6 +696,7 @@ _git_apply ()
--stat --numstat --summary --check --index
--cached --index-info --reverse --reject --unidiff-zero
--apply --no-add --exclude=
--ignore-whitespace --ignore-space-change
--whitespace= --inaccurate-eof --verbose
"
return
Expand Down Expand Up @@ -1536,6 +1538,7 @@ _git_config ()
__gitcomp "
add.ignore-errors
alias.
apply.ignorewhitespace
apply.whitespace
branch.autosetupmerge
branch.autosetuprebase
Expand Down
1 change: 1 addition & 0 deletions environment.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const char *git_commit_encoding;
const char *git_log_output_encoding;
int shared_repository = PERM_UMASK;
const char *apply_default_whitespace;
const char *apply_default_ignorewhitespace;
int zlib_compression_level = Z_BEST_SPEED;
int core_compression_level;
int core_compression_seen;
Expand Down
4 changes: 3 additions & 1 deletion git-am.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ s,signoff add a Signed-off-by line to the commit message
u,utf8 recode into utf8 (default)
k,keep pass -k flag to git-mailinfo
whitespace= pass it through git-apply
ignore-space-change pass it through git-apply
ignore-whitespace pass it through git-apply
directory= pass it through git-apply
C= pass it through git-apply
p= pass it through git-apply
Expand Down Expand Up @@ -303,7 +305,7 @@ do
git_apply_opt="$git_apply_opt $(sq "$1$2")"; shift ;;
--patch-format)
shift ; patch_format="$1" ;;
--reject)
--reject|--ignore-whitespace|--ignore-space-change)
git_apply_opt="$git_apply_opt $1" ;;
--committer-date-is-author-date)
committer_date_is_author_date=t ;;
Expand Down
Loading

0 comments on commit 86c91f9

Please sign in to comment.