intersect_paths: respect mode in git's tree-sort
authorJeff King <peff@peff.net>
Wed, 20 Aug 2014 02:14:30 +0000 (22:14 -0400)
committerJunio C Hamano <gitster@pobox.com>
Wed, 20 Aug 2014 20:38:37 +0000 (13:38 -0700)
When we do a combined diff, we individually diff against
each parent, and then use intersect_paths to do a parallel
walk through the sorted results and come up with a final
list of interesting paths.

The sort order here is that returned by the diffs, which
means it is in git's tree-order which sorts sub-trees as if
their paths have "/" at the end. When we do our parallel
walk, we need to use a comparison function which provides
the same order.

Since 8518ff8 (combine-diff: optimize combine_diff_path sets
intersection, 2014-01-20), we use a simple strcmp to
compare the pathnames, and get this wrong. It's somewhat
hard to trigger because normally a diff does not produce
tree entries at all, and therefore the sort order is the
same as a strcmp. However, if the "-t" option is used with
the diff, then we will produce diff_filepairs for both trees
and files.

We can use base_name_compare to do the comparison, just as
the tree-diff code does. Even though what we have are not
technically base names (they are full paths within the
tree), the end result is the same (we do not care about
interior slashes at all, only about the final character).

However, since we do not have the length of each path
stored, we take a slight shortcut: if neither of the entries
is a sub-tree then the comparison is equivalent to a strcmp.
This lets us skip the extra strlen calls in the common case
without having to reimplement base_name_compare from
scratch.

Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
combine-diff.c
t/t4038-diff-combined.sh
index 24ca7e2334b68e06afd24051ad1aafcc129bcba4..4e043060f06fef8b5e921435284339b44b464b62 100644 (file)
 #include "sha1-array.h"
 #include "revision.h"
 
+static int compare_paths(const struct combine_diff_path *one,
+                         const struct diff_filespec *two)
+{
+       if (!S_ISDIR(one->mode) && !S_ISDIR(two->mode))
+               return strcmp(one->path, two->path);
+
+       return base_name_compare(one->path, strlen(one->path), one->mode,
+                                two->path, strlen(two->path), two->mode);
+}
+
 static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr, int n, int num_parent)
 {
        struct diff_queue_struct *q = &diff_queued_diff;
@@ -52,7 +62,7 @@ static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr,
        i = 0;
        while ((p = *tail) != NULL) {
                cmp = ((i >= q->nr)
-                      ? -1 : strcmp(p->path, q->queue[i]->two->path));
+                      ? -1 : compare_paths(p, q->queue[i]->two));
 
                if (cmp < 0) {
                        /* p->path not in q->queue[]; drop it */
index 1019d7b35fcb350761a44cf4ccd5ae156ce8d577..c5eacb80f62ac5764bef5ba3e69f5b5546e73e9a 100755 (executable)
@@ -401,4 +401,38 @@ test_expect_success 'combine diff missing delete bug' '
        compare_diff_patch expected actual
 '
 
+test_expect_success 'combine diff gets tree sorting right' '
+       # create a directory and a file that sort differently in trees
+       # versus byte-wise (implied "/" sorts after ".")
+       git checkout -f master &&
+       mkdir foo &&
+       echo base >foo/one &&
+       echo base >foo/two &&
+       echo base >foo.ext &&
+       git add foo foo.ext &&
+       git commit -m base &&
+
+       # one side modifies a file in the directory, along with the root
+       # file...
+       echo master >foo/one &&
+       echo master >foo.ext &&
+       git commit -a -m master &&
+
+       # the other side modifies the other file in the directory
+       git checkout -b other HEAD^ &&
+       echo other >foo/two &&
+       git commit -a -m other &&
+
+       # And now we merge. The files in the subdirectory will resolve cleanly,
+       # meaning that a combined diff will not find them interesting. But it
+       # will find the tree itself interesting, because it had to be merged.
+       git checkout master &&
+       git merge other &&
+
+       printf "MM\tfoo\n" >expect &&
+       git diff-tree -c --name-status -t HEAD >actual.tmp &&
+       sed 1d <actual.tmp >actual &&
+       test_cmp expect actual
+'
+
 test_done