diff options
author | Chris Torek <chris.torek@gmail.com> | 2020-06-12 16:19:59 +0000 |
---|---|---|
committer | Junio C Hamano <gitster@pobox.com> | 2020-06-12 10:53:44 -0700 |
commit | 8bfcb3a690126e6222f0d4f7012b0f68bb748018 (patch) | |
tree | 23e39a459c38a0cc16a14c17ba87836bc5079f98 /builtin/diff.c | |
parent | t/t3430: avoid undefined git diff behavior (diff) | |
download | tgif-8bfcb3a690126e6222f0d4f7012b0f68bb748018.tar.xz |
git diff: improve range handling
When git diff is given a symmetric difference A...B, it chooses
some merge base from the two specified commits (as documented).
This fails, however, if there is *no* merge base: instead, you
see the differences between A and B, which is certainly not what
is expected.
Moreover, if additional revisions are specified on the command
line ("git diff A...B C"), the results get a bit weird:
* If there is a symmetric difference merge base, this is used
as the left side of the diff. The last final ref is used as
the right side.
* If there is no merge base, the symmetric status is completely
lost. We will produce a combined diff instead.
Similar weirdness occurs if you use, e.g., "git diff C A...B D".
Likewise, using multiple two-dot ranges, or tossing extra
revision specifiers into the command line with two-dot ranges,
or mixing two and three dot ranges, all produce nonsense.
To avoid all this, add a routine to catch the range cases and
verify that that the arguments make sense. As a side effect,
produce a warning showing *which* merge base is being used when
there are multiple choices; die if there is no merge base.
Signed-off-by: Chris Torek <chris.torek@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Diffstat (limited to 'builtin/diff.c')
-rw-r--r-- | builtin/diff.c | 124 |
1 files changed, 111 insertions, 13 deletions
diff --git a/builtin/diff.c b/builtin/diff.c index 42ac803091..57bd6c8dfa 100644 --- a/builtin/diff.c +++ b/builtin/diff.c @@ -6,6 +6,7 @@ #define USE_THE_INDEX_COMPATIBILITY_MACROS #include "cache.h" #include "config.h" +#include "ewah/ewok.h" #include "lockfile.h" #include "color.h" #include "commit.h" @@ -254,6 +255,108 @@ static int builtin_diff_files(struct rev_info *revs, int argc, const char **argv return run_diff_files(revs, options); } +struct symdiff { + struct bitmap *skip; + int warn; + const char *base, *left, *right; +}; + +/* + * Check for symmetric-difference arguments, and if present, arrange + * everything we need to know to handle them correctly. As a bonus, + * weed out all bogus range-based revision specifications, e.g., + * "git diff A..B C..D" or "git diff A..B C" get rejected. + * + * For an actual symmetric diff, *symdiff is set this way: + * + * - its skip is non-NULL and marks *all* rev->pending.objects[i] + * indices that the caller should ignore (extra merge bases, of + * which there might be many, and A in A...B). Note that the + * chosen merge base and right side are NOT marked. + * - warn is set if there are multiple merge bases. + * - base, left, and right point to the names to use in a + * warning about multiple merge bases. + * + * If there is no symmetric diff argument, sym->skip is NULL and + * sym->warn is cleared. The remaining fields are not set. + */ +static void symdiff_prepare(struct rev_info *rev, struct symdiff *sym) +{ + int i, is_symdiff = 0, basecount = 0, othercount = 0; + int lpos = -1, rpos = -1, basepos = -1; + struct bitmap *map = NULL; + + /* + * Use the whence fields to find merge bases and left and + * right parts of symmetric difference, so that we do not + * depend on the order that revisions are parsed. If there + * are any revs that aren't from these sources, we have a + * "git diff C A...B" or "git diff A...B C" case. Or we + * could even get "git diff A...B C...E", for instance. + * + * If we don't have just one merge base, we pick one + * at random. + * + * NB: REV_CMD_LEFT, REV_CMD_RIGHT are also used for A..B, + * so we must check for SYMMETRIC_LEFT too. The two arrays + * rev->pending.objects and rev->cmdline.rev are parallel. + */ + for (i = 0; i < rev->cmdline.nr; i++) { + struct object *obj = rev->pending.objects[i].item; + switch (rev->cmdline.rev[i].whence) { + case REV_CMD_MERGE_BASE: + if (basepos < 0) + basepos = i; + basecount++; + break; /* do mark all bases */ + case REV_CMD_LEFT: + if (lpos >= 0) + usage(builtin_diff_usage); + lpos = i; + if (obj->flags & SYMMETRIC_LEFT) { + is_symdiff = 1; + break; /* do mark A */ + } + continue; + case REV_CMD_RIGHT: + if (rpos >= 0) + usage(builtin_diff_usage); + rpos = i; + continue; /* don't mark B */ + case REV_CMD_PARENTS_ONLY: + case REV_CMD_REF: + case REV_CMD_REV: + othercount++; + continue; + } + if (map == NULL) + map = bitmap_new(); + bitmap_set(map, i); + } + + /* + * Forbid any additional revs for both A...B and A..B. + */ + if (lpos >= 0 && othercount > 0) + usage(builtin_diff_usage); + + if (!is_symdiff) { + bitmap_free(map); + sym->warn = 0; + sym->skip = NULL; + return; + } + + sym->left = rev->pending.objects[lpos].name; + sym->right = rev->pending.objects[rpos].name; + sym->base = rev->pending.objects[basepos].name; + if (basecount == 0) + die(_("%s...%s: no merge base"), sym->left, sym->right); + bitmap_unset(map, basepos); /* unmark the base we want */ + sym->warn = basecount > 1; + sym->skip = map; +} + int cmd_diff(int argc, const char **argv, const char *prefix) { int i; @@ -263,6 +366,7 @@ int cmd_diff(int argc, const char **argv, const char *prefix) struct object_array_entry *blob[2]; int nongit = 0, no_index = 0; int result = 0; + struct symdiff sdiff; /* * We could get N tree-ish in the rev.pending_objects list. @@ -382,6 +486,7 @@ int cmd_diff(int argc, const char **argv, const char *prefix) } } + symdiff_prepare(&rev, &sdiff); for (i = 0; i < rev.pending.nr; i++) { struct object_array_entry *entry = &rev.pending.objects[i]; struct object *obj = entry->item; @@ -396,6 +501,8 @@ int cmd_diff(int argc, const char **argv, const char *prefix) obj = &get_commit_tree(((struct commit *)obj))->object; if (obj->type == OBJ_TREE) { + if (sdiff.skip && bitmap_get(sdiff.skip, i)) + continue; obj->flags |= flags; add_object_array(obj, name, &ent); } else if (obj->type == OBJ_BLOB) { @@ -437,21 +544,12 @@ int cmd_diff(int argc, const char **argv, const char *prefix) usage(builtin_diff_usage); else if (ent.nr == 1) result = builtin_diff_index(&rev, argc, argv); - else if (ent.nr == 2) + else if (ent.nr == 2) { + if (sdiff.warn) + warning(_("%s...%s: multiple merge bases, using %s"), + sdiff.left, sdiff.right, sdiff.base); result = builtin_diff_tree(&rev, argc, argv, &ent.objects[0], &ent.objects[1]); - else if (ent.objects[0].item->flags & UNINTERESTING) { - /* - * diff A...B where there is at least one merge base - * between A and B. We have ent.objects[0] == - * merge-base, ent.objects[ents-2] == A, and - * ent.objects[ents-1] == B. Show diff between the - * base and B. Note that we pick one merge base at - * random if there are more than one. - */ - result = builtin_diff_tree(&rev, argc, argv, - &ent.objects[0], - &ent.objects[ent.nr-1]); } else result = builtin_diff_combined(&rev, argc, argv, ent.objects, ent.nr); |