Merge branch 'gb/apply-ignore-whitespace'
authorJunio C Hamano <gitster@pobox.com>
Sat, 22 Aug 2009 01:47:48 +0000 (18:47 -0700)
committerJunio C Hamano <gitster@pobox.com>
Sat, 22 Aug 2009 01:47:48 +0000 (18:47 -0700)
* gb/apply-ignore-whitespace:
git apply: option to ignore whitespace differences

1  2 
Documentation/config.txt
builtin-apply.c
cache.h
contrib/completion/git-completion.bash
git-am.sh
git-rebase.sh
diff --combined Documentation/config.txt
index 2632c5149e380dd9d07b38803614b42db99f2658,0b53cab6af60f0a0040de895ec7aa180a037b8a7..5256c7fb811ae6f5777bf528a22b19b16b132f26
@@@ -461,6 -461,14 +461,14 @@@ it will be treated as a shell command
  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].
@@@ -605,7 -613,7 +613,7 @@@ color.interactive.<slot>:
        Use customized color for 'git-add --interactive'
        output. `<slot>` may be `prompt`, `header`, `help` or `error`, for
        four distinct types of normal output from interactive
 -      programs.  The values of these variables may be specified as
 +      commands.  The values of these variables may be specified as
        in color.branch.<slot>.
  
  color.pager::
@@@ -1113,7 -1121,7 +1121,7 @@@ instaweb.port:
        linkgit:git-instaweb[1].
  
  interactive.singlekey::
 -      In interactive programs, allow the user to provide one-letter
 +      In interactive commands, allow the user to provide one-letter
        input with a single key (i.e., without hitting enter).
        Currently this is used only by the `\--patch` mode of
        linkgit:git-add[1].  Note that this setting is silently
@@@ -1218,20 -1226,12 +1226,20 @@@ pack.compression:
  
  pack.deltaCacheSize::
        The maximum memory in bytes used for caching deltas in
 -      linkgit:git-pack-objects[1].
 -      A value of 0 means no limit. Defaults to 0.
 +      linkgit:git-pack-objects[1] before writing them out to a pack.
 +      This cache is used to speed up the writing object phase by not
 +      having to recompute the final delta result once the best match
 +      for all objects is found.  Repacking large repositories on machines
 +      which are tight with memory might be badly impacted by this though,
 +      especially if this cache pushes the system into swapping.
 +      A value of 0 means no limit. The smallest size of 1 byte may be
 +      used to virtually disable this cache. Defaults to 256 MiB.
  
  pack.deltaCacheLimit::
        The maximum size of a delta, that is cached in
 -      linkgit:git-pack-objects[1]. Defaults to 1000.
 +      linkgit:git-pack-objects[1]. This cache is used to speed up the
 +      writing object phase by not having to recompute the final delta
 +      result once the best match for all objects is found. Defaults to 1000.
  
  pack.threads::
        Specifies the number of threads to spawn when searching for best
diff --combined builtin-apply.c
index 39dc96ae0237235eb429b504ec28c9d66c32f4e1,d9303578b6a1cb7b333371f93081535dba558c48..ae11b41ef29215fcb2147387676228ca6965d86d
@@@ -61,6 -61,13 +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;
@@@ -97,6 -104,21 +104,21 @@@ static void parse_whitespace_option(con
        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)
@@@ -214,6 -236,62 +236,62 @@@ static uint32_t hash_line(const char *c
        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);
@@@ -457,76 -535,6 +535,76 @@@ static int guess_p_value(const char *na
        return val;
  }
  
 +/*
 + * Does the ---/+++ line has the POSIX timestamp after the last HT?
 + * GNU diff puts epoch there to signal a creation/deletion event.  Is
 + * this such a timestamp?
 + */
 +static int has_epoch_timestamp(const char *nameline)
 +{
 +      /*
 +       * We are only interested in epoch timestamp; any non-zero
 +       * fraction cannot be one, hence "(\.0+)?" in the regexp below.
 +       * For the same reason, the date must be either 1969-12-31 or
 +       * 1970-01-01, and the seconds part must be "00".
 +       */
 +      const char stamp_regexp[] =
 +              "^(1969-12-31|1970-01-01)"
 +              " "
 +              "[0-2][0-9]:[0-5][0-9]:00(\\.0+)?"
 +              " "
 +              "([-+][0-2][0-9][0-5][0-9])\n";
 +      const char *timestamp = NULL, *cp;
 +      static regex_t *stamp;
 +      regmatch_t m[10];
 +      int zoneoffset;
 +      int hourminute;
 +      int status;
 +
 +      for (cp = nameline; *cp != '\n'; cp++) {
 +              if (*cp == '\t')
 +                      timestamp = cp + 1;
 +      }
 +      if (!timestamp)
 +              return 0;
 +      if (!stamp) {
 +              stamp = xmalloc(sizeof(*stamp));
 +              if (regcomp(stamp, stamp_regexp, REG_EXTENDED)) {
 +                      warning("Cannot prepare timestamp regexp %s",
 +                              stamp_regexp);
 +                      return 0;
 +              }
 +      }
 +
 +      status = regexec(stamp, timestamp, ARRAY_SIZE(m), m, 0);
 +      if (status) {
 +              if (status != REG_NOMATCH)
 +                      warning("regexec returned %d for input: %s",
 +                              status, timestamp);
 +              return 0;
 +      }
 +
 +      zoneoffset = strtol(timestamp + m[3].rm_so + 1, NULL, 10);
 +      zoneoffset = (zoneoffset / 100) * 60 + (zoneoffset % 100);
 +      if (timestamp[m[3].rm_so] == '-')
 +              zoneoffset = -zoneoffset;
 +
 +      /*
 +       * YYYY-MM-DD hh:mm:ss must be from either 1969-12-31
 +       * (west of GMT) or 1970-01-01 (east of GMT)
 +       */
 +      if ((zoneoffset < 0 && memcmp(timestamp, "1969-12-31", 10)) ||
 +          (0 <= zoneoffset && memcmp(timestamp, "1970-01-01", 10)))
 +              return 0;
 +
 +      hourminute = (strtol(timestamp + 11, NULL, 10) * 60 +
 +                    strtol(timestamp + 14, NULL, 10) -
 +                    zoneoffset);
 +
 +      return ((zoneoffset < 0 && hourminute == 1440) ||
 +              (0 <= zoneoffset && !hourminute));
 +}
 +
  /*
   * Get the name etc info from the ---/+++ lines of a traditional patch header
   *
@@@ -563,17 -571,7 +641,17 @@@ static void parse_traditional_patch(con
        } else {
                name = find_name(first, NULL, p_value, TERM_SPACE | TERM_TAB);
                name = find_name(second, name, p_value, TERM_SPACE | TERM_TAB);
 -              patch->old_name = patch->new_name = name;
 +              if (has_epoch_timestamp(first)) {
 +                      patch->is_new = 1;
 +                      patch->is_delete = 0;
 +                      patch->new_name = name;
 +              } else if (has_epoch_timestamp(second)) {
 +                      patch->is_new = 0;
 +                      patch->is_delete = 1;
 +                      patch->old_name = name;
 +              } else {
 +                      patch->old_name = patch->new_name = name;
 +              }
        }
        if (!name)
                die("unable to find filename in patch at line %d", linenr);
@@@ -1672,10 -1670,17 +1750,17 @@@ static int read_old_data(struct stat *s
        }
  }
  
+ /*
+  * 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;
        *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;
@@@ -1773,12 -1786,58 +1866,58 @@@ static int match_fragment(struct image 
            !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;
         * 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:
@@@ -3272,6 -3331,8 +3411,8 @@@ static int git_apply_config(const char 
  {
        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);
  }
  
@@@ -3308,6 -3369,16 +3449,16 @@@ static int option_parse_z(const struct 
        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)
  {
@@@ -3384,6 -3455,12 +3535,12 @@@ int cmd_apply(int argc, const char **ar
                { 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,
        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);
diff --combined cache.h
index 9222774e6cd8a0d6edeedbe62e95136c198a87f6,695212fca9a54d96198cb8dd5cadf347ce17c159..eee717c81b5882bd59009fb52d0484f0fb1041cb
+++ b/cache.h
@@@ -468,9 -468,6 +468,9 @@@ extern int index_fd(unsigned char *sha1
  extern int index_path(unsigned char *sha1, const char *path, struct stat *st, int write_object);
  extern void fill_stat_cache_info(struct cache_entry *ce, struct stat *st);
  
 +/* "careful lstat()" */
 +extern int check_path(const char *path, int len, struct stat *st);
 +
  #define REFRESH_REALLY                0x0001  /* ignore_valid */
  #define REFRESH_UNMERGED      0x0002  /* allow unmerged */
  #define REFRESH_QUIET         0x0004  /* be quiet about it */
@@@ -512,6 -509,7 +512,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;
index 5543dc4d14e22b065cbc41ed8af0f695c0464152,dd7ec5dd66425040e5bbdf0466f7ca822b3a6370..bf688e12e630c3eadf3dbdcafe90aaff2f498d54
@@@ -674,6 -674,7 +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=
                        "
@@@ -695,6 -696,7 +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
@@@ -1047,7 -1049,6 +1049,7 @@@ _git_grep (
                        --extended-regexp --basic-regexp --fixed-strings
                        --files-with-matches --name-only
                        --files-without-match
 +                      --max-depth
                        --count
                        --and --or --not --all-match
                        "
@@@ -1537,6 -1538,7 +1539,7 @@@ _git_config (
        __gitcomp "
                add.ignore-errors
                alias.
+               apply.ignorewhitespace
                apply.whitespace
                branch.autosetupmerge
                branch.autosetuprebase
diff --combined git-am.sh
index 5396be227931ebc2a302cd1aebd86e028733a69c,ab39b3c2fa3cb6c6e68f6193e0c4038edf8af512..3c03f3e0df96f7b993db1c3822d3f3dab8741a4b
+++ b/git-am.sh
@@@ -16,6 -16,8 +16,8 @@@ s,signoff       add a Signed-off-by lin
  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
@@@ -191,33 -193,13 +193,33 @@@ check_patch_format () 
                        esac
                        ;;
                esac
 +              if test -z "$patch_format" &&
 +                      test -n "$l1" &&
 +                      test -n "$l2" &&
 +                      test -n "$l3"
 +              then
 +                      # This begins with three non-empty lines.  Is this a
 +                      # piece of e-mail a-la RFC2822?  Grab all the headers,
 +                      # discarding the indented remainder of folded lines,
 +                      # and see if it looks like that they all begin with the
 +                      # header field names...
 +                      sed -n -e '/^$/q' -e '/^[       ]/d' -e p "$1" |
 +                      egrep -v '^[A-Za-z]+(-[A-Za-z]+)*:' >/dev/null ||
 +                      patch_format=mbox
 +              fi
        } < "$1" || clean_abort
  }
  
  split_patches () {
        case "$patch_format" in
        mbox)
 -              git mailsplit -d"$prec" -o"$dotest" -b -- "$@" > "$dotest/last" ||
 +              case "$rebasing" in
 +              '')
 +                      keep_cr= ;;
 +              ?*)
 +                      keep_cr=--keep-cr ;;
 +              esac
 +              git mailsplit -d"$prec" -o"$dotest" -b $keep_cr -- "$@" > "$dotest/last" ||
                clean_abort
                ;;
        stgit-series)
                msgnum=
                ;;
        *)
 -              clean_abort "Patch format $patch_format is not supported."
 +              if test -n "$parse_patch" ; then
 +                      clean_abort "Patch format $patch_format is not supported."
 +              else
 +                      clean_abort "Patch format detection failed."
 +              fi
                ;;
        esac
  }
@@@ -327,7 -305,7 +329,7 @@@ d
                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 ;;
diff --combined git-rebase.sh
index 3555d17a5d0f4f6e79cd2283efec04d89668017c,d741752c7c8f2713a42d82db09c9057f741a7d2d..2315d95a9fb28e15190461a94a43e28e57fbe6c3
@@@ -333,6 -333,9 +333,9 @@@ d
                        ;;
                esac
                ;;
+       --ignore-whitespace)
+               git_am_opt="$git_am_opt $1"
+               ;;
        --committer-date-is-author-date|--ignore-date)
                git_am_opt="$git_am_opt $1"
                force_rebase=t
@@@ -382,10 -385,8 +385,10 @@@ els
  fi
  
  # The tree must be really really clean.
 -if ! git update-index --ignore-submodules --refresh; then
 -      die "cannot rebase: you have unstaged changes"
 +if ! git update-index --ignore-submodules --refresh > /dev/null; then
 +      echo >&2 "cannot rebase: you have unstaged changes"
 +      git diff --name-status -r --ignore-submodules -- >&2
 +      exit 1
  fi
  diff=$(git diff-index --cached --name-status -r --ignore-submodules HEAD --)
  case "$diff" in