Skip to content

Commit 1457886

Browse files
committed
Merge branch 'ct/diff-with-merge-base-clarification'
"git diff" used to take arguments in random and nonsense range notation, e.g. "git diff A..B C", "git diff A..B C...D", etc., which has been cleaned up. * ct/diff-with-merge-base-clarification: Documentation: usage for diff combined commits git diff: improve range handling t/t3430: avoid undefined git diff behavior
2 parents 5367469 + b7e10b2 commit 1457886

File tree

4 files changed

+226
-19
lines changed

4 files changed

+226
-19
lines changed

Documentation/git-diff.txt

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ SYNOPSIS
1111
[verse]
1212
'git diff' [<options>] [<commit>] [--] [<path>...]
1313
'git diff' [<options>] --cached [<commit>] [--] [<path>...]
14-
'git diff' [<options>] <commit> <commit> [--] [<path>...]
14+
'git diff' [<options>] <commit> [<commit>...] <commit> [--] [<path>...]
15+
'git diff' [<options>] <commit>...<commit> [--] [<path>...]
1516
'git diff' [<options>] <blob> <blob>
1617
'git diff' [<options>] --no-index [--] <path> <path>
1718

1819
DESCRIPTION
1920
-----------
2021
Show changes between the working tree and the index or a tree, changes
21-
between the index and a tree, changes between two trees, changes between
22-
two blob objects, or changes between two files on disk.
22+
between the index and a tree, changes between two trees, changes resulting
23+
from a merge, changes between two blob objects, or changes between two
24+
files on disk.
2325

2426
'git diff' [<options>] [--] [<path>...]::
2527

@@ -67,6 +69,15 @@ two blob objects, or changes between two files on disk.
6769
one side is omitted, it will have the same effect as
6870
using HEAD instead.
6971

72+
'git diff' [<options>] <commit> [<commit>...] <commit> [--] [<path>...]::
73+
74+
This form is to view the results of a merge commit. The first
75+
listed <commit> must be the merge itself; the remaining two or
76+
more commits should be its parents. A convenient way to produce
77+
the desired set of revisions is to use the {caret}@ suffix.
78+
For instance, if `master` names a merge commit, `git diff master
79+
master^@` gives the same combined diff as `git show master`.
80+
7081
'git diff' [<options>] <commit>\...<commit> [--] [<path>...]::
7182

7283
This form is to view the changes on the branch containing
@@ -196,7 +207,8 @@ linkgit:git-difftool[1],
196207
linkgit:git-log[1],
197208
linkgit:gitdiffcore[7],
198209
linkgit:git-format-patch[1],
199-
linkgit:git-apply[1]
210+
linkgit:git-apply[1],
211+
linkgit:git-show[1]
200212

201213
GIT
202214
---

builtin/diff.c

Lines changed: 118 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#define USE_THE_INDEX_COMPATIBILITY_MACROS
77
#include "cache.h"
88
#include "config.h"
9+
#include "ewah/ewok.h"
910
#include "lockfile.h"
1011
#include "color.h"
1112
#include "commit.h"
@@ -23,7 +24,13 @@
2324
#define DIFF_NO_INDEX_IMPLICIT 2
2425

2526
static const char builtin_diff_usage[] =
26-
"git diff [<options>] [<commit> [<commit>]] [--] [<path>...]";
27+
"git diff [<options>] [<commit>] [--] [<path>...]\n"
28+
" or: git diff [<options>] --cached [<commit>] [--] [<path>...]\n"
29+
" or: git diff [<options>] <commit> [<commit>...] <commit> [--] [<path>...]\n"
30+
" or: git diff [<options>] <commit>...<commit>] [--] [<path>...]\n"
31+
" or: git diff [<options>] <blob> <blob>]\n"
32+
" or: git diff [<options>] --no-index [--] <path> <path>]\n"
33+
COMMON_DIFF_OPTIONS_HELP;
2734

2835
static const char *blob_path(struct object_array_entry *entry)
2936
{
@@ -254,6 +261,108 @@ static int builtin_diff_files(struct rev_info *revs, int argc, const char **argv
254261
return run_diff_files(revs, options);
255262
}
256263

264+
struct symdiff {
265+
struct bitmap *skip;
266+
int warn;
267+
const char *base, *left, *right;
268+
};
269+
270+
/*
271+
* Check for symmetric-difference arguments, and if present, arrange
272+
* everything we need to know to handle them correctly. As a bonus,
273+
* weed out all bogus range-based revision specifications, e.g.,
274+
* "git diff A..B C..D" or "git diff A..B C" get rejected.
275+
*
276+
* For an actual symmetric diff, *symdiff is set this way:
277+
*
278+
* - its skip is non-NULL and marks *all* rev->pending.objects[i]
279+
* indices that the caller should ignore (extra merge bases, of
280+
* which there might be many, and A in A...B). Note that the
281+
* chosen merge base and right side are NOT marked.
282+
* - warn is set if there are multiple merge bases.
283+
* - base, left, and right point to the names to use in a
284+
* warning about multiple merge bases.
285+
*
286+
* If there is no symmetric diff argument, sym->skip is NULL and
287+
* sym->warn is cleared. The remaining fields are not set.
288+
*/
289+
static void symdiff_prepare(struct rev_info *rev, struct symdiff *sym)
290+
{
291+
int i, is_symdiff = 0, basecount = 0, othercount = 0;
292+
int lpos = -1, rpos = -1, basepos = -1;
293+
struct bitmap *map = NULL;
294+
295+
/*
296+
* Use the whence fields to find merge bases and left and
297+
* right parts of symmetric difference, so that we do not
298+
* depend on the order that revisions are parsed. If there
299+
* are any revs that aren't from these sources, we have a
300+
* "git diff C A...B" or "git diff A...B C" case. Or we
301+
* could even get "git diff A...B C...E", for instance.
302+
*
303+
* If we don't have just one merge base, we pick one
304+
* at random.
305+
*
306+
* NB: REV_CMD_LEFT, REV_CMD_RIGHT are also used for A..B,
307+
* so we must check for SYMMETRIC_LEFT too. The two arrays
308+
* rev->pending.objects and rev->cmdline.rev are parallel.
309+
*/
310+
for (i = 0; i < rev->cmdline.nr; i++) {
311+
struct object *obj = rev->pending.objects[i].item;
312+
switch (rev->cmdline.rev[i].whence) {
313+
case REV_CMD_MERGE_BASE:
314+
if (basepos < 0)
315+
basepos = i;
316+
basecount++;
317+
break; /* do mark all bases */
318+
case REV_CMD_LEFT:
319+
if (lpos >= 0)
320+
usage(builtin_diff_usage);
321+
lpos = i;
322+
if (obj->flags & SYMMETRIC_LEFT) {
323+
is_symdiff = 1;
324+
break; /* do mark A */
325+
}
326+
continue;
327+
case REV_CMD_RIGHT:
328+
if (rpos >= 0)
329+
usage(builtin_diff_usage);
330+
rpos = i;
331+
continue; /* don't mark B */
332+
case REV_CMD_PARENTS_ONLY:
333+
case REV_CMD_REF:
334+
case REV_CMD_REV:
335+
othercount++;
336+
continue;
337+
}
338+
if (map == NULL)
339+
map = bitmap_new();
340+
bitmap_set(map, i);
341+
}
342+
343+
/*
344+
* Forbid any additional revs for both A...B and A..B.
345+
*/
346+
if (lpos >= 0 && othercount > 0)
347+
usage(builtin_diff_usage);
348+
349+
if (!is_symdiff) {
350+
bitmap_free(map);
351+
sym->warn = 0;
352+
sym->skip = NULL;
353+
return;
354+
}
355+
356+
sym->left = rev->pending.objects[lpos].name;
357+
sym->right = rev->pending.objects[rpos].name;
358+
sym->base = rev->pending.objects[basepos].name;
359+
if (basecount == 0)
360+
die(_("%s...%s: no merge base"), sym->left, sym->right);
361+
bitmap_unset(map, basepos); /* unmark the base we want */
362+
sym->warn = basecount > 1;
363+
sym->skip = map;
364+
}
365+
257366
int cmd_diff(int argc, const char **argv, const char *prefix)
258367
{
259368
int i;
@@ -263,6 +372,7 @@ int cmd_diff(int argc, const char **argv, const char *prefix)
263372
struct object_array_entry *blob[2];
264373
int nongit = 0, no_index = 0;
265374
int result = 0;
375+
struct symdiff sdiff;
266376

267377
/*
268378
* We could get N tree-ish in the rev.pending_objects list.
@@ -382,6 +492,7 @@ int cmd_diff(int argc, const char **argv, const char *prefix)
382492
}
383493
}
384494

495+
symdiff_prepare(&rev, &sdiff);
385496
for (i = 0; i < rev.pending.nr; i++) {
386497
struct object_array_entry *entry = &rev.pending.objects[i];
387498
struct object *obj = entry->item;
@@ -396,6 +507,8 @@ int cmd_diff(int argc, const char **argv, const char *prefix)
396507
obj = &get_commit_tree(((struct commit *)obj))->object;
397508

398509
if (obj->type == OBJ_TREE) {
510+
if (sdiff.skip && bitmap_get(sdiff.skip, i))
511+
continue;
399512
obj->flags |= flags;
400513
add_object_array(obj, name, &ent);
401514
} else if (obj->type == OBJ_BLOB) {
@@ -437,21 +550,12 @@ int cmd_diff(int argc, const char **argv, const char *prefix)
437550
usage(builtin_diff_usage);
438551
else if (ent.nr == 1)
439552
result = builtin_diff_index(&rev, argc, argv);
440-
else if (ent.nr == 2)
553+
else if (ent.nr == 2) {
554+
if (sdiff.warn)
555+
warning(_("%s...%s: multiple merge bases, using %s"),
556+
sdiff.left, sdiff.right, sdiff.base);
441557
result = builtin_diff_tree(&rev, argc, argv,
442558
&ent.objects[0], &ent.objects[1]);
443-
else if (ent.objects[0].item->flags & UNINTERESTING) {
444-
/*
445-
* diff A...B where there is at least one merge base
446-
* between A and B. We have ent.objects[0] ==
447-
* merge-base, ent.objects[ents-2] == A, and
448-
* ent.objects[ents-1] == B. Show diff between the
449-
* base and B. Note that we pick one merge base at
450-
* random if there are more than one.
451-
*/
452-
result = builtin_diff_tree(&rev, argc, argv,
453-
&ent.objects[0],
454-
&ent.objects[ent.nr-1]);
455559
} else
456560
result = builtin_diff_combined(&rev, argc, argv,
457561
ent.objects, ent.nr);

t/t3430-rebase-merges.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ test_expect_success 'with --autosquash and --exec' '
420420
git commit --fixup B B.t &&
421421
write_script show.sh <<-\EOF &&
422422
subject="$(git show -s --format=%s HEAD)"
423-
content="$(git diff HEAD^! | tail -n 1)"
423+
content="$(git diff HEAD^ HEAD | tail -n 1)"
424424
echo "$subject: $content"
425425
EOF
426426
test_tick &&

t/t4068-diff-symmetric.sh

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/bin/sh
2+
3+
test_description='behavior of diff with symmetric-diff setups'
4+
5+
. ./test-lib.sh
6+
7+
# build these situations:
8+
# - normal merge with one merge base (br1...b2r);
9+
# - criss-cross merge ie 2 merge bases (br1...master);
10+
# - disjoint subgraph (orphan branch, br3...master).
11+
#
12+
# B---E <-- master
13+
# / \ /
14+
# A X
15+
# \ / \
16+
# C---D--G <-- br1
17+
# \ /
18+
# ---F <-- br2
19+
#
20+
# H <-- br3
21+
#
22+
# We put files into a few commits so that we can verify the
23+
# output as well.
24+
25+
test_expect_success setup '
26+
git commit --allow-empty -m A &&
27+
echo b >b &&
28+
git add b &&
29+
git commit -m B &&
30+
git checkout -b br1 HEAD^ &&
31+
echo c >c &&
32+
git add c &&
33+
git commit -m C &&
34+
git tag commit-C &&
35+
git merge -m D master &&
36+
git tag commit-D &&
37+
git checkout master &&
38+
git merge -m E commit-C &&
39+
git checkout -b br2 commit-C &&
40+
echo f >f &&
41+
git add f &&
42+
git commit -m F &&
43+
git checkout br1 &&
44+
git merge -m G br2 &&
45+
git checkout --orphan br3 &&
46+
git commit -m H
47+
'
48+
49+
test_expect_success 'diff with one merge base' '
50+
git diff commit-D...br1 >tmp &&
51+
tail -n 1 tmp >actual &&
52+
echo +f >expect &&
53+
test_cmp expect actual
54+
'
55+
56+
# The output (in tmp) can have +b or +c depending
57+
# on which merge base (commit B or C) is picked.
58+
# It should have one of those two, which comes out
59+
# to seven lines.
60+
test_expect_success 'diff with two merge bases' '
61+
git diff br1...master >tmp 2>err &&
62+
test_line_count = 7 tmp &&
63+
test_line_count = 1 err
64+
'
65+
66+
test_expect_success 'diff with no merge bases' '
67+
test_must_fail git diff br2...br3 >tmp 2>err &&
68+
test_i18ngrep "fatal: br2...br3: no merge base" err
69+
'
70+
71+
test_expect_success 'diff with too many symmetric differences' '
72+
test_must_fail git diff br1...master br2...br3 >tmp 2>err &&
73+
test_i18ngrep "usage" err
74+
'
75+
76+
test_expect_success 'diff with symmetric difference and extraneous arg' '
77+
test_must_fail git diff master br1...master >tmp 2>err &&
78+
test_i18ngrep "usage" err
79+
'
80+
81+
test_expect_success 'diff with two ranges' '
82+
test_must_fail git diff master br1..master br2..br3 >tmp 2>err &&
83+
test_i18ngrep "usage" err
84+
'
85+
86+
test_expect_success 'diff with ranges and extra arg' '
87+
test_must_fail git diff master br1..master commit-D >tmp 2>err &&
88+
test_i18ngrep "usage" err
89+
'
90+
91+
test_done

0 commit comments

Comments
 (0)