Merge branch 'mk/combine-diff-context-horizon-fix' into maint
authorJunio C Hamano <gitster@pobox.com>
Thu, 27 Jun 2013 21:37:55 +0000 (14:37 -0700)
committerJunio C Hamano <gitster@pobox.com>
Thu, 27 Jun 2013 21:37:56 +0000 (14:37 -0700)
* mk/combine-diff-context-horizon-fix:
combine-diff.c: Fix output when changes are exactly 3 lines apart

1  2 
combine-diff.c
t/t4038-diff-combined.sh
diff --combined combine-diff.c
index 77d7872aafe659045e9ec228de97e87c9cea00a1,87a668319b37f75a4b9161ae8e88d2e63be64ced..3e8bb17831aa5f2f39ee3a07eeebdda90ae39556
@@@ -5,7 -5,6 +5,7 @@@
  #include "diffcore.h"
  #include "quote.h"
  #include "xdiff-interface.h"
 +#include "xdiff/xmacros.h"
  #include "log-tree.h"
  #include "refs.h"
  #include "userdiff.h"
@@@ -74,24 -73,16 +74,24 @@@ static struct combine_diff_path *inters
  
  /* Lines lost from parent */
  struct lline {
 -      struct lline *next;
 +      struct lline *next, *prev;
        int len;
        unsigned long parent_map;
        char line[FLEX_ARRAY];
  };
  
 +/* Lines lost from current parent (before coalescing) */
 +struct plost {
 +      struct lline *lost_head, *lost_tail;
 +      int len;
 +};
 +
  /* Lines surviving in the merge result */
  struct sline {
 -      struct lline *lost_head, **lost_tail;
 -      struct lline *next_lost;
 +      /* Accumulated and coalesced lost lines */
 +      struct lline *lost;
 +      int lenlost;
 +      struct plost plost;
        char *bol;
        int len;
        /* bit 0 up to (N-1) are on if the parent has this line (i.e.
        unsigned long *p_lno;
  };
  
 +static int match_string_spaces(const char *line1, int len1,
 +                             const char *line2, int len2,
 +                             long flags)
 +{
 +      if (flags & XDF_WHITESPACE_FLAGS) {
 +              for (; len1 > 0 && XDL_ISSPACE(line1[len1 - 1]); len1--);
 +              for (; len2 > 0 && XDL_ISSPACE(line2[len2 - 1]); len2--);
 +      }
 +
 +      if (!(flags & (XDF_IGNORE_WHITESPACE | XDF_IGNORE_WHITESPACE_CHANGE)))
 +              return (len1 == len2 && !memcmp(line1, line2, len1));
 +
 +      while (len1 > 0 && len2 > 0) {
 +              len1--;
 +              len2--;
 +              if (XDL_ISSPACE(line1[len1]) || XDL_ISSPACE(line2[len2])) {
 +                      if ((flags & XDF_IGNORE_WHITESPACE_CHANGE) &&
 +                          (!XDL_ISSPACE(line1[len1]) || !XDL_ISSPACE(line2[len2])))
 +                              return 0;
 +
 +                      for (; len1 > 0 && XDL_ISSPACE(line1[len1]); len1--);
 +                      for (; len2 > 0 && XDL_ISSPACE(line2[len2]); len2--);
 +              }
 +              if (line1[len1] != line2[len2])
 +                      return 0;
 +      }
 +
 +      if (flags & XDF_IGNORE_WHITESPACE) {
 +              /* Consume remaining spaces */
 +              for (; len1 > 0 && XDL_ISSPACE(line1[len1 - 1]); len1--);
 +              for (; len2 > 0 && XDL_ISSPACE(line2[len2 - 1]); len2--);
 +      }
 +
 +      /* We matched full line1 and line2 */
 +      if (!len1 && !len2)
 +              return 1;
 +
 +      return 0;
 +}
 +
 +enum coalesce_direction { MATCH, BASE, NEW };
 +
 +/* Coalesce new lines into base by finding LCS */
 +static struct lline *coalesce_lines(struct lline *base, int *lenbase,
 +                                  struct lline *new, int lennew,
 +                                  unsigned long parent, long flags)
 +{
 +      int **lcs;
 +      enum coalesce_direction **directions;
 +      struct lline *baseend, *newend = NULL;
 +      int i, j, origbaselen = *lenbase;
 +
 +      if (new == NULL)
 +              return base;
 +
 +      if (base == NULL) {
 +              *lenbase = lennew;
 +              return new;
 +      }
 +
 +      /*
 +       * Coalesce new lines into base by finding the LCS
 +       * - Create the table to run dynamic programing
 +       * - Compute the LCS
 +       * - Then reverse read the direction structure:
 +       *   - If we have MATCH, assign parent to base flag, and consume
 +       *   both baseend and newend
 +       *   - Else if we have BASE, consume baseend
 +       *   - Else if we have NEW, insert newend lline into base and
 +       *   consume newend
 +       */
 +      lcs = xcalloc(origbaselen + 1, sizeof(int*));
 +      directions = xcalloc(origbaselen + 1, sizeof(enum coalesce_direction*));
 +      for (i = 0; i < origbaselen + 1; i++) {
 +              lcs[i] = xcalloc(lennew + 1, sizeof(int));
 +              directions[i] = xcalloc(lennew + 1, sizeof(enum coalesce_direction));
 +              directions[i][0] = BASE;
 +      }
 +      for (j = 1; j < lennew + 1; j++)
 +              directions[0][j] = NEW;
 +
 +      for (i = 1, baseend = base; i < origbaselen + 1; i++) {
 +              for (j = 1, newend = new; j < lennew + 1; j++) {
 +                      if (match_string_spaces(baseend->line, baseend->len,
 +                                              newend->line, newend->len, flags)) {
 +                              lcs[i][j] = lcs[i - 1][j - 1] + 1;
 +                              directions[i][j] = MATCH;
 +                      } else if (lcs[i][j - 1] >= lcs[i - 1][j]) {
 +                              lcs[i][j] = lcs[i][j - 1];
 +                              directions[i][j] = NEW;
 +                      } else {
 +                              lcs[i][j] = lcs[i - 1][j];
 +                              directions[i][j] = BASE;
 +                      }
 +                      if (newend->next)
 +                              newend = newend->next;
 +              }
 +              if (baseend->next)
 +                      baseend = baseend->next;
 +      }
 +
 +      for (i = 0; i < origbaselen + 1; i++)
 +              free(lcs[i]);
 +      free(lcs);
 +
 +      /* At this point, baseend and newend point to the end of each lists */
 +      i--;
 +      j--;
 +      while (i != 0 || j != 0) {
 +              if (directions[i][j] == MATCH) {
 +                      baseend->parent_map |= 1<<parent;
 +                      baseend = baseend->prev;
 +                      newend = newend->prev;
 +                      i--;
 +                      j--;
 +              } else if (directions[i][j] == NEW) {
 +                      struct lline *lline;
 +
 +                      lline = newend;
 +                      /* Remove lline from new list and update newend */
 +                      if (lline->prev)
 +                              lline->prev->next = lline->next;
 +                      else
 +                              new = lline->next;
 +                      if (lline->next)
 +                              lline->next->prev = lline->prev;
 +
 +                      newend = lline->prev;
 +                      j--;
 +
 +                      /* Add lline to base list */
 +                      if (baseend) {
 +                              lline->next = baseend->next;
 +                              lline->prev = baseend;
 +                              if (lline->prev)
 +                                      lline->prev->next = lline;
 +                      }
 +                      else {
 +                              lline->next = base;
 +                              base = lline;
 +                      }
 +                      (*lenbase)++;
 +
 +                      if (lline->next)
 +                              lline->next->prev = lline;
 +
 +              } else {
 +                      baseend = baseend->prev;
 +                      i--;
 +              }
 +      }
 +
 +      newend = new;
 +      while (newend) {
 +              struct lline *lline = newend;
 +              newend = newend->next;
 +              free(lline);
 +      }
 +
 +      for (i = 0; i < origbaselen + 1; i++)
 +              free(directions[i]);
 +      free(directions);
 +
 +      return base;
 +}
 +
  static char *grab_blob(const unsigned char *sha1, unsigned int mode,
                       unsigned long *size, struct userdiff_driver *textconv,
                       const char *path)
@@@ -304,19 -129,29 +304,19 @@@ static void append_lost(struct sline *s
        if (line[len-1] == '\n')
                len--;
  
 -      /* Check to see if we can squash things */
 -      if (sline->lost_head) {
 -              lline = sline->next_lost;
 -              while (lline) {
 -                      if (lline->len == len &&
 -                          !memcmp(lline->line, line, len)) {
 -                              lline->parent_map |= this_mask;
 -                              sline->next_lost = lline->next;
 -                              return;
 -                      }
 -                      lline = lline->next;
 -              }
 -      }
 -
        lline = xmalloc(sizeof(*lline) + len + 1);
        lline->len = len;
        lline->next = NULL;
 +      lline->prev = sline->plost.lost_tail;
 +      if (lline->prev)
 +              lline->prev->next = lline;
 +      else
 +              sline->plost.lost_head = lline;
 +      sline->plost.lost_tail = lline;
 +      sline->plost.len++;
        lline->parent_map = this_mask;
        memcpy(lline->line, line, len);
        lline->line[len] = 0;
 -      *sline->lost_tail = lline;
 -      sline->lost_tail = &lline->next;
 -      sline->next_lost = NULL;
  }
  
  struct combine_diff_state {
@@@ -359,6 -194,7 +359,6 @@@ static void consume_line(void *state_, 
                                xcalloc(state->num_parent,
                                        sizeof(unsigned long));
                state->sline[state->nb-1].p_lno[state->n] = state->ob;
 -              state->lost_bucket->next_lost = state->lost_bucket->lost_head;
                return;
        }
        if (!state->lost_bucket)
@@@ -379,7 -215,7 +379,7 @@@ static void combine_diff(const unsigne
                         struct sline *sline, unsigned int cnt, int n,
                         int num_parent, int result_deleted,
                         struct userdiff_driver *textconv,
 -                       const char *path)
 +                       const char *path, long flags)
  {
        unsigned int p_lno, lno;
        unsigned long nmask = (1UL << n);
        parent_file.ptr = grab_blob(parent, mode, &sz, textconv, path);
        parent_file.size = sz;
        memset(&xpp, 0, sizeof(xpp));
 -      xpp.flags = 0;
 +      xpp.flags = flags;
        memset(&xecfg, 0, sizeof(xecfg));
        memset(&state, 0, sizeof(state));
        state.nmask = nmask;
                struct lline *ll;
                sline[lno].p_lno[n] = p_lno;
  
 +              /* Coalesce new lines */
 +              if (sline[lno].plost.lost_head) {
 +                      struct sline *sl = &sline[lno];
 +                      sl->lost = coalesce_lines(sl->lost, &sl->lenlost,
 +                                                sl->plost.lost_head,
 +                                                sl->plost.len, n, flags);
 +                      sl->plost.lost_head = sl->plost.lost_tail = NULL;
 +                      sl->plost.len = 0;
 +              }
 +
                /* How many lines would this sline advance the p_lno? */
 -              ll = sline[lno].lost_head;
 +              ll = sline[lno].lost;
                while (ll) {
                        if (ll->parent_map & nmask)
                                p_lno++; /* '-' means parent had it */
@@@ -450,7 -276,7 +450,7 @@@ static int interesting(struct sline *sl
        /* If some parents lost lines here, or if we have added to
         * some parent, it is interesting.
         */
 -      return ((sline->flag & all_mask) || sline->lost_head);
 +      return ((sline->flag & all_mask) || sline->lost);
  }
  
  static unsigned long adjust_hunk_tail(struct sline *sline,
@@@ -518,8 -344,11 +518,11 @@@ static int give_context(struct sline *s
                unsigned long k;
  
                /* Paint a few lines before the first interesting line. */
-               while (j < i)
-                       sline[j++].flag |= mark | no_pre_delete;
+               while (j < i) {
+                       if (!(sline[j].flag & mark))
+                               sline[j].flag |= no_pre_delete;
+                       sline[j++].flag |= mark;
+               }
  
        again:
                /* we know up to i is to be included.  where does the
@@@ -633,7 -462,7 +636,7 @@@ static int make_hunks(struct sline *sli
                has_interesting = 0;
                for (j = i; j < hunk_end && !has_interesting; j++) {
                        unsigned long this_diff = sline[j].flag & all_mask;
 -                      struct lline *ll = sline[j].lost_head;
 +                      struct lline *ll = sline[j].lost;
                        if (this_diff) {
                                /* This has some changes.  Is it the
                                 * same as others?
@@@ -700,8 -529,7 +703,8 @@@ static void show_line_to_eol(const cha
               saw_cr_at_eol ? "\r" : "");
  }
  
 -static void dump_sline(struct sline *sline, unsigned long cnt, int num_parent,
 +static void dump_sline(struct sline *sline, const char *line_prefix,
 +                     unsigned long cnt, int num_parent,
                       int use_color, int result_deleted)
  {
        unsigned long mark = (1UL<<num_parent);
                        rlines -= null_context;
                }
  
 -              fputs(c_frag, stdout);
 +              printf("%s%s", line_prefix, c_frag);
                for (i = 0; i <= num_parent; i++) putchar(combine_marker);
                for (i = 0; i < num_parent; i++)
                        show_parent_lno(sline, lno, hunk_end, i, null_context);
                        int j;
                        unsigned long p_mask;
                        struct sline *sl = &sline[lno++];
 -                      ll = (sl->flag & no_pre_delete) ? NULL : sl->lost_head;
 +                      ll = (sl->flag & no_pre_delete) ? NULL : sl->lost;
                        while (ll) {
 -                              fputs(c_old, stdout);
 +                              printf("%s%s", line_prefix, c_old);
                                for (j = 0; j < num_parent; j++) {
                                        if (ll->parent_map & (1UL<<j))
                                                putchar('-');
                        if (cnt < lno)
                                break;
                        p_mask = 1;
 +                      fputs(line_prefix, stdout);
                        if (!(sl->flag & (mark-1))) {
                                /*
                                 * This sline was here to hang the
@@@ -838,7 -665,7 +841,7 @@@ static void reuse_combine_diff(struct s
        jmask = (1UL<<j);
  
        for (lno = 0; lno <= cnt; lno++) {
 -              struct lline *ll = sline->lost_head;
 +              struct lline *ll = sline->lost;
                sline->p_lno[i] = sline->p_lno[j];
                while (ll) {
                        if (ll->parent_map & jmask)
  static void dump_quoted_path(const char *head,
                             const char *prefix,
                             const char *path,
 +                           const char *line_prefix,
                             const char *c_meta, const char *c_reset)
  {
        static struct strbuf buf = STRBUF_INIT;
  
        strbuf_reset(&buf);
 +      strbuf_addstr(&buf, line_prefix);
        strbuf_addstr(&buf, c_meta);
        strbuf_addstr(&buf, head);
        quote_two_c_style(&buf, prefix, path, 0);
@@@ -874,7 -699,6 +877,7 @@@ static void show_combined_header(struc
                                 int num_parent,
                                 int dense,
                                 struct rev_info *rev,
 +                               const char *line_prefix,
                                 int mode_differs,
                                 int show_file_header)
  {
                show_log(rev);
  
        dump_quoted_path(dense ? "diff --cc " : "diff --combined ",
 -                       "", elem->path, c_meta, c_reset);
 -      printf("%sindex ", c_meta);
 +                       "", elem->path, line_prefix, c_meta, c_reset);
 +      printf("%s%sindex ", line_prefix, c_meta);
        for (i = 0; i < num_parent; i++) {
                abb = find_unique_abbrev(elem->parent[i].sha1,
                                         abbrev);
                            DIFF_STATUS_ADDED)
                                added = 0;
                if (added)
 -                      printf("%snew file mode %06o",
 -                             c_meta, elem->mode);
 +                      printf("%s%snew file mode %06o",
 +                             line_prefix, c_meta, elem->mode);
                else {
                        if (deleted)
 -                              printf("%sdeleted file ", c_meta);
 +                              printf("%s%sdeleted file ",
 +                                     line_prefix, c_meta);
                        printf("mode ");
                        for (i = 0; i < num_parent; i++) {
                                printf("%s%06o", i ? "," : "",
  
        if (added)
                dump_quoted_path("--- ", "", "/dev/null",
 -                               c_meta, c_reset);
 +                               line_prefix, c_meta, c_reset);
        else
                dump_quoted_path("--- ", a_prefix, elem->path,
 -                               c_meta, c_reset);
 +                               line_prefix, c_meta, c_reset);
        if (deleted)
                dump_quoted_path("+++ ", "", "/dev/null",
 -                               c_meta, c_reset);
 +                               line_prefix, c_meta, c_reset);
        else
                dump_quoted_path("+++ ", b_prefix, elem->path,
 -                               c_meta, c_reset);
 +                               line_prefix, c_meta, c_reset);
  }
  
  static void show_patch_diff(struct combine_diff_path *elem, int num_parent,
        struct userdiff_driver *userdiff;
        struct userdiff_driver *textconv = NULL;
        int is_binary;
 +      const char *line_prefix = diff_line_prefix(opt);
  
        context = opt->context;
        userdiff = userdiff_find_by_path(elem->path);
        }
        if (is_binary) {
                show_combined_header(elem, num_parent, dense, rev,
 -                                   mode_differs, 0);
 +                                   line_prefix, mode_differs, 0);
                printf("Binary files differ\n");
                free(result);
                return;
  
        sline = xcalloc(cnt+2, sizeof(*sline));
        sline[0].bol = result;
 -      for (lno = 0; lno <= cnt + 1; lno++) {
 -              sline[lno].lost_tail = &sline[lno].lost_head;
 -              sline[lno].flag = 0;
 -      }
        for (lno = 0, cp = result; cp < result + result_size; cp++) {
                if (*cp == '\n') {
                        sline[lno].len = cp - sline[lno].bol;
                                     elem->parent[i].mode,
                                     &result_file, sline,
                                     cnt, i, num_parent, result_deleted,
 -                                   textconv, elem->path);
 +                                   textconv, elem->path, opt->xdl_opts);
        }
  
        show_hunks = make_hunks(sline, cnt, num_parent, dense);
  
        if (show_hunks || mode_differs || working_tree_file) {
                show_combined_header(elem, num_parent, dense, rev,
 -                                   mode_differs, 1);
 -              dump_sline(sline, cnt, num_parent,
 +                                   line_prefix, mode_differs, 1);
 +              dump_sline(sline, line_prefix, cnt, num_parent,
                           opt->use_color, result_deleted);
        }
        free(result);
  
        for (lno = 0; lno < cnt; lno++) {
 -              if (sline[lno].lost_head) {
 -                      struct lline *ll = sline[lno].lost_head;
 +              if (sline[lno].lost) {
 +                      struct lline *ll = sline[lno].lost;
                        while (ll) {
                                struct lline *tmp = ll;
                                ll = ll->next;
        free(sline);
  }
  
 -#define COLONS "::::::::::::::::::::::::::::::::"
 -
  static void show_raw_diff(struct combine_diff_path *p, int num_parent, struct rev_info *rev)
  {
        struct diff_options *opt = &rev->diffopt;
 -      int i, offset;
 -      const char *prefix;
 -      int line_termination, inter_name_termination;
 +      int line_termination, inter_name_termination, i;
 +      const char *line_prefix = diff_line_prefix(opt);
  
        line_termination = opt->line_termination;
        inter_name_termination = '\t';
        if (rev->loginfo && !rev->no_commit_id)
                show_log(rev);
  
 +
        if (opt->output_format & DIFF_FORMAT_RAW) {
 -              offset = strlen(COLONS) - num_parent;
 -              if (offset < 0)
 -                      offset = 0;
 -              prefix = COLONS + offset;
 +              printf("%s", line_prefix);
 +
 +              /* As many colons as there are parents */
 +              for (i = 0; i < num_parent; i++)
 +                      putchar(':');
  
                /* Show the modes */
 -              for (i = 0; i < num_parent; i++) {
 -                      printf("%s%06o", prefix, p->parent[i].mode);
 -                      prefix = " ";
 -              }
 -              printf("%s%06o", prefix, p->mode);
 +              for (i = 0; i < num_parent; i++)
 +                      printf("%06o ", p->parent[i].mode);
 +              printf("%06o", p->mode);
  
                /* Show sha1's */
                for (i = 0; i < num_parent; i++)
@@@ -1214,7 -1043,6 +1217,7 @@@ void show_combined_diff(struct combine_
                       struct rev_info *rev)
  {
        struct diff_options *opt = &rev->diffopt;
 +
        if (!p->len)
                return;
        if (opt->output_format & (DIFF_FORMAT_RAW |
@@@ -1325,10 -1153,8 +1328,10 @@@ void diff_tree_combined(const unsigned 
  
                if (show_log_first && i == 0) {
                        show_log(rev);
 +
                        if (rev->verbose_header && opt->output_format)
 -                              putchar(opt->line_termination);
 +                              printf("%s%c", diff_line_prefix(opt),
 +                                     opt->line_termination);
                }
                diff_flush(&diffopts);
        }
  
                if (opt->output_format & DIFF_FORMAT_PATCH) {
                        if (needsep)
 -                              putchar(opt->line_termination);
 +                              printf("%s%c", diff_line_prefix(opt),
 +                                     opt->line_termination);
                        for (p = paths; p; p = p->next) {
                                if (p->len)
                                        show_patch_diff(p, num_parent, dense,
diff --combined t/t4038-diff-combined.sh
index 1261dbbdf5869b671e36cd7128bfde61fd67b8de,2ffc692bdeb39cd9c3b592d139dece4740cc3e71..1019d7b35fcb350761a44cf4ccd5ae156ce8d577
@@@ -3,7 -3,6 +3,7 @@@
  test_description='combined diff'
  
  . ./test-lib.sh
 +. "$TEST_DIRECTORY"/diff-lib.sh
  
  setup_helper () {
        one=$1 branch=$2 side=$3 &&
@@@ -90,267 -89,52 +90,315 @@@ test_expect_success 'diagnose truncate
        grep "diff --cc file" out
  '
  
 -      test_cmp expected actual
 +test_expect_success 'setup for --cc --raw' '
 +      blob=$(echo file | git hash-object --stdin -w) &&
 +      base_tree=$(echo "100644 blob $blob     file" | git mktree) &&
 +      trees= &&
 +      for i in `test_seq 1 40`
 +      do
 +              blob=$(echo file$i | git hash-object --stdin -w) &&
 +              trees="$trees$(echo "100644 blob $blob  file" | git mktree)$LF"
 +      done
 +'
 +
 +test_expect_success 'check --cc --raw with four trees' '
 +      four_trees=$(echo "$trees" | sed -e 4q) &&
 +      git diff --cc --raw $four_trees $base_tree >out &&
 +      # Check for four leading colons in the output:
 +      grep "^::::[^:]" out
 +'
 +
 +test_expect_success 'check --cc --raw with forty trees' '
 +      git diff --cc --raw $trees $base_tree >out &&
 +      # Check for forty leading colons in the output:
 +      grep "^::::::::::::::::::::::::::::::::::::::::[^:]" out
 +'
 +
 +test_expect_success 'setup combined ignore spaces' '
 +      git checkout master &&
 +      >test &&
 +      git add test &&
 +      git commit -m initial &&
 +
 +      tr -d Q <<-\EOF >test &&
 +      always coalesce
 +      eol space coalesce Q
 +      space  change coalesce
 +      all spa ces coalesce
 +      eol spaces Q
 +      space  change
 +      all spa ces
 +      EOF
 +      git commit -m "test space change" -a &&
 +
 +      git checkout -b side HEAD^ &&
 +      tr -d Q <<-\EOF >test &&
 +      always coalesce
 +      eol space coalesce
 +      space change coalesce
 +      all spaces coalesce
 +      eol spaces
 +      space change
 +      all spaces
 +      EOF
 +      git commit -m "test other space changes" -a &&
 +
 +      test_must_fail git merge master &&
 +      tr -d Q <<-\EOF >test &&
 +      eol spaces Q
 +      space  change
 +      all spa ces
 +      EOF
 +      git commit -m merged -a
 +'
 +
 +test_expect_success 'check combined output (no ignore space)' '
 +      git show >actual.tmp &&
 +      sed -e "1,/^@@@/d" < actual.tmp >actual &&
 +      tr -d Q <<-\EOF >expected &&
 +      --always coalesce
 +      - eol space coalesce
 +      - space change coalesce
 +      - all spaces coalesce
 +      - eol spaces
 +      - space change
 +      - all spaces
 +       -eol space coalesce Q
 +       -space  change coalesce
 +       -all spa ces coalesce
 +      + eol spaces Q
 +      + space  change
 +      + all spa ces
 +      EOF
 +      compare_diff_patch expected actual
 +'
 +
 +test_expect_success 'check combined output (ignore space at eol)' '
 +      git show --ignore-space-at-eol >actual.tmp &&
 +      sed -e "1,/^@@@/d" < actual.tmp >actual &&
 +      tr -d Q <<-\EOF >expected &&
 +      --always coalesce
 +      --eol space coalesce
 +      - space change coalesce
 +      - all spaces coalesce
 +       -space  change coalesce
 +       -all spa ces coalesce
 +        eol spaces Q
 +      - space change
 +      - all spaces
 +      + space  change
 +      + all spa ces
 +      EOF
 +      compare_diff_patch expected actual
 +'
 +
 +test_expect_success 'check combined output (ignore space change)' '
 +      git show -b >actual.tmp &&
 +      sed -e "1,/^@@@/d" < actual.tmp >actual &&
 +      tr -d Q <<-\EOF >expected &&
 +      --always coalesce
 +      --eol space coalesce
 +      --space change coalesce
 +      - all spaces coalesce
 +       -all spa ces coalesce
 +        eol spaces Q
 +        space  change
 +      - all spaces
 +      + all spa ces
 +      EOF
 +      compare_diff_patch expected actual
 +'
 +
 +test_expect_success 'check combined output (ignore all spaces)' '
 +      git show -w >actual.tmp &&
 +      sed -e "1,/^@@@/d" < actual.tmp >actual &&
 +      tr -d Q <<-\EOF >expected &&
 +      --always coalesce
 +      --eol space coalesce
 +      --space change coalesce
 +      --all spaces coalesce
 +        eol spaces Q
 +        space  change
 +        all spa ces
 +      EOF
 +      compare_diff_patch expected actual
 +'
 +
 +test_expect_success 'combine diff coalesce simple' '
 +      >test &&
 +      git add test &&
 +      git commit -m initial &&
 +      test_seq 4 >test &&
 +      git commit -a -m empty1 &&
 +      git branch side1 &&
 +      git checkout HEAD^ &&
 +      test_seq 5 >test &&
 +      git commit -a -m empty2 &&
 +      test_must_fail git merge side1 &&
 +      >test &&
 +      git commit -a -m merge &&
 +      git show >actual.tmp &&
 +      sed -e "1,/^@@@/d" < actual.tmp >actual &&
 +      tr -d Q <<-\EOF >expected &&
 +      --1
 +      --2
 +      --3
 +      --4
 +      - 5
 +      EOF
 +      compare_diff_patch expected actual
 +'
 +
 +test_expect_success 'combine diff coalesce tricky' '
 +      >test &&
 +      git add test &&
 +      git commit -m initial --allow-empty &&
 +      cat <<-\EOF >test &&
 +      3
 +      1
 +      2
 +      3
 +      4
 +      EOF
 +      git commit -a -m empty1 &&
 +      git branch -f side1 &&
 +      git checkout HEAD^ &&
 +      cat <<-\EOF >test &&
 +      1
 +      3
 +      5
 +      4
 +      EOF
 +      git commit -a -m empty2 &&
 +      git branch -f side2 &&
 +      test_must_fail git merge side1 &&
 +      >test &&
 +      git commit -a -m merge &&
 +      git show >actual.tmp &&
 +      sed -e "1,/^@@@/d" < actual.tmp >actual &&
 +      tr -d Q <<-\EOF >expected &&
 +       -3
 +      --1
 +       -2
 +      --3
 +      - 5
 +      --4
 +      EOF
 +      compare_diff_patch expected actual &&
 +      git checkout -f side1 &&
 +      test_must_fail git merge side2 &&
 +      >test &&
 +      git commit -a -m merge &&
 +      git show >actual.tmp &&
 +      sed -e "1,/^@@@/d" < actual.tmp >actual &&
 +      tr -d Q <<-\EOF >expected &&
 +      - 3
 +      --1
 +      - 2
 +      --3
 +       -5
 +      --4
 +      EOF
 +      compare_diff_patch expected actual
 +'
 +
 +test_expect_failure 'combine diff coalesce three parents' '
 +      >test &&
 +      git add test &&
 +      git commit -m initial --allow-empty &&
 +      cat <<-\EOF >test &&
 +      3
 +      1
 +      2
 +      3
 +      4
 +      EOF
 +      git commit -a -m empty1 &&
 +      git checkout -B side1 &&
 +      git checkout HEAD^ &&
 +      cat <<-\EOF >test &&
 +      1
 +      3
 +      7
 +      5
 +      4
 +      EOF
 +      git commit -a -m empty2 &&
 +      git branch -f side2 &&
 +      git checkout HEAD^ &&
 +      cat <<-\EOF >test &&
 +      3
 +      1
 +      6
 +      5
 +      4
 +      EOF
 +      git commit -a -m empty3 &&
 +      >test &&
 +      git add test &&
 +      TREE=$(git write-tree) &&
 +      COMMIT=$(git commit-tree -p HEAD -p side1 -p side2 -m merge $TREE) &&
 +      git show $COMMIT >actual.tmp &&
 +      sed -e "1,/^@@@/d" < actual.tmp >actual &&
 +      tr -d Q <<-\EOF >expected &&
 +      -- 3
 +      ---1
 +      -  6
 +       - 2
 +       --3
 +        -7
 +      - -5
 +      ---4
 +      EOF
 +      compare_diff_patch expected actual
 +'
 +
+ # Test for a bug reported at
+ # http://thread.gmane.org/gmane.comp.version-control.git/224410
+ # where a delete lines were missing from combined diff output when they
+ # occurred exactly before the context lines of a later change.
+ test_expect_success 'combine diff missing delete bug' '
+       git commit -m initial --allow-empty &&
+       cat <<-\EOF >test &&
+       1
+       2
+       3
+       4
+       EOF
+       git add test &&
+       git commit -a -m side1 &&
+       git checkout -B side1 &&
+       git checkout HEAD^ &&
+       cat <<-\EOF >test &&
+       0
+       1
+       2
+       3
+       4modified
+       EOF
+       git add test &&
+       git commit -m side2 &&
+       git branch -f side2 &&
+       test_must_fail git merge --no-commit side1 &&
+       cat <<-\EOF >test &&
+       1
+       2
+       3
+       4modified
+       EOF
+       git add test &&
+       git commit -a -m merge &&
+       git diff-tree -c -p HEAD >actual.tmp &&
+       sed -e "1,/^@@@/d" < actual.tmp >actual &&
+       tr -d Q <<-\EOF >expected &&
+       - 0
+         1
+         2
+         3
+        -4
+        +4modified
+       EOF
++      compare_diff_patch expected actual
+ '
  test_done