Skip to content

Commit 29bd8fb

Browse files
committed
Merge branch 'tc/diff-tree-max-depth' into jch
"git diff-tree" learned "--max-depth" option. Comments? * tc/diff-tree-max-depth: diff: teach tree-diff a max-depth parameter within_depth: fix return for empty path combine-diff: zero memory used for callback filepairs
2 parents 246acf0 + a1dfa54 commit 29bd8fb

File tree

11 files changed

+308
-5
lines changed

11 files changed

+308
-5
lines changed

Documentation/diff-options.adoc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,5 +893,33 @@ endif::git-format-patch[]
893893
reverted with `--ita-visible-in-index`. Both options are
894894
experimental and could be removed in future.
895895

896+
--max-depth=<depth>::
897+
For each pathspec given on command line, descend at most `<depth>`
898+
levels of directories. A value of `-1` means no limit.
899+
Cannot be combined with wildcards in the pathspec.
900+
Given a tree containing `foo/bar/baz`, the following list shows the
901+
matches generated by each set of options:
902+
+
903+
--
904+
- `--max-depth=0 -- foo`: `foo`
905+
906+
- `--max-depth=1 -- foo`: `foo/bar`
907+
908+
- `--max-depth=1 -- foo/bar`: `foo/bar/baz`
909+
910+
- `--max-depth=1 -- foo foo/bar`: `foo/bar/baz`
911+
912+
- `--max-depth=2 -- foo`: `foo/bar/baz`
913+
--
914+
+
915+
If no pathspec is given, the depth is measured as if all
916+
top-level entries were specified. Note that this is different
917+
than measuring from the root, in that `--max-depth=0` would
918+
still return `foo`. This allows you to still limit depth while
919+
asking for a subset of the top-level entries.
920+
+
921+
Note that this option is only supported for diffs between tree objects,
922+
not against the index or working tree.
923+
896924
For more detailed explanation on these common options, see also
897925
linkgit:gitdiffcore[7].

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,6 +1354,7 @@ THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%
13541354
THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%
13551355

13561356
CLAR_TEST_SUITES += u-ctype
1357+
CLAR_TEST_SUITES += u-dir
13571358
CLAR_TEST_SUITES += u-example-decorate
13581359
CLAR_TEST_SUITES += u-hash
13591360
CLAR_TEST_SUITES += u-hashmap

combine-diff.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1315,7 +1315,7 @@ static struct diff_filepair *combined_pair(struct combine_diff_path *p,
13151315
struct diff_filepair *pair;
13161316
struct diff_filespec *pool;
13171317

1318-
pair = xmalloc(sizeof(*pair));
1318+
CALLOC_ARRAY(pair, 1);
13191319
CALLOC_ARRAY(pool, st_add(num_parent, 1));
13201320
pair->one = pool + 1;
13211321
pair->two = pool;

diff-lib.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ void run_diff_files(struct rev_info *revs, unsigned int option)
115115
uint64_t start = getnanotime();
116116
struct index_state *istate = revs->diffopt.repo->index;
117117

118+
if (revs->diffopt.max_depth_valid)
119+
die(_("max-depth is not supported for worktree diffs"));
120+
118121
diff_set_mnemonic_prefix(&revs->diffopt, "i/", "w/");
119122

120123
refresh_fsmonitor(istate);
@@ -560,6 +563,8 @@ static int diff_cache(struct rev_info *revs,
560563
opts.dst_index = NULL;
561564
opts.pathspec = &revs->diffopt.pathspec;
562565
opts.pathspec->recursive = 1;
566+
if (revs->diffopt.max_depth_valid)
567+
die(_("max-depth is not supported for index diffs"));
563568

564569
init_tree_desc(&t, &tree->object.oid, tree->buffer, tree->size);
565570
return unpack_trees(1, &t, &opts);

diff.c

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4982,6 +4982,9 @@ void diff_setup_done(struct diff_options *options)
49824982
options->filter = ~filter_bit[DIFF_STATUS_FILTER_AON];
49834983
options->filter &= ~options->filter_not;
49844984
}
4985+
4986+
if (options->pathspec.has_wildcard && options->max_depth_valid)
4987+
die("max-depth cannot be used with wildcard pathspecs");
49854988
}
49864989

49874990
int parse_long_opt(const char *opt, const char **argv,
@@ -5616,6 +5619,23 @@ static int diff_opt_rotate_to(const struct option *opt, const char *arg, int uns
56165619
return 0;
56175620
}
56185621

5622+
static int diff_opt_max_depth(const struct option *opt,
5623+
const char *arg, int unset)
5624+
{
5625+
struct diff_options *options = opt->value;
5626+
5627+
BUG_ON_OPT_NEG(unset);
5628+
5629+
if (!git_parse_int(arg, &options->max_depth))
5630+
return error(_("invalid value for '%s': '%s'"),
5631+
"--max-depth", arg);
5632+
5633+
options->flags.recursive = 1;
5634+
options->max_depth_valid = options->max_depth >= 0;
5635+
5636+
return 0;
5637+
}
5638+
56195639
/*
56205640
* Consider adding new flags to __git_diff_common_options
56215641
* in contrib/completion/git-completion.bash
@@ -5888,6 +5908,10 @@ struct option *add_diff_options(const struct option *opts,
58885908
OPT_CALLBACK_F(0, "diff-filter", options, N_("[(A|C|D|M|R|T|U|X|B)...[*]]"),
58895909
N_("select files by diff type"),
58905910
PARSE_OPT_NONEG, diff_opt_diff_filter),
5911+
OPT_CALLBACK_F(0, "max-depth", options, N_("<depth>"),
5912+
N_("maximum tree depth to recurse"),
5913+
PARSE_OPT_NONEG, diff_opt_max_depth),
5914+
58915915
{
58925916
.type = OPTION_CALLBACK,
58935917
.long_name = "output",

diff.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,14 @@ struct diff_options {
404404
struct strmap *additional_path_headers;
405405

406406
int no_free;
407+
408+
/*
409+
* The value '0' is a valid max-depth (for no recursion), and value '-1'
410+
* also (for unlimited recursion), so the extra "valid" flag is used to
411+
* determined whether the user specified option --max-depth.
412+
*/
413+
int max_depth;
414+
int max_depth_valid;
407415
};
408416

409417
unsigned diff_filter_bit(char status);

dir.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ int within_depth(const char *name, int namelen,
277277
if (depth > max_depth)
278278
return 0;
279279
}
280-
return 1;
280+
return depth <= max_depth;
281281
}
282282

283283
/*

t/meson.build

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
clar_test_suites = [
22
'unit-tests/u-ctype.c',
3+
'unit-tests/u-dir.c',
34
'unit-tests/u-example-decorate.c',
45
'unit-tests/u-hash.c',
56
'unit-tests/u-hashmap.c',
@@ -487,6 +488,7 @@ integration_tests = [
487488
't4069-remerge-diff.sh',
488489
't4070-diff-pairs.sh',
489490
't4071-diff-minimal.sh',
491+
't4072-diff-max-depth.sh',
490492
't4100-apply-stat.sh',
491493
't4101-apply-nonl.sh',
492494
't4102-apply-rename.sh',

t/t4072-diff-max-depth.sh

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/bin/sh
2+
3+
test_description='check that diff --max-depth will limit recursion'
4+
. ./test-lib.sh
5+
6+
make_dir() {
7+
mkdir -p "$1" &&
8+
echo "$2" >"$1/file"
9+
}
10+
11+
make_files() {
12+
echo "$1" >file &&
13+
make_dir one "$1" &&
14+
make_dir one/two "$1" &&
15+
make_dir one/two/three "$1"
16+
}
17+
18+
test_expect_success 'setup' '
19+
git commit --allow-empty -m empty &&
20+
git tag empty &&
21+
make_files added &&
22+
git add . &&
23+
git commit -m added &&
24+
make_files modified &&
25+
git add . &&
26+
git commit -m modified &&
27+
make_files index &&
28+
git add . &&
29+
make_files worktree
30+
'
31+
32+
test_expect_success '--max-depth is disallowed with wildcard pathspecs' '
33+
test_must_fail git diff-tree --max-depth=0 HEAD^ HEAD -- "f*"
34+
'
35+
36+
check_one() {
37+
type=$1; shift
38+
args=$1; shift
39+
path=$1; shift
40+
depth=$1; shift
41+
test_expect_${expect:-success} "diff-$type $args, path=$path, depth=$depth" "
42+
for i in $*; do echo \$i; done >expect &&
43+
git diff-$type --max-depth=$depth --name-only $args -- $path >actual &&
44+
test_cmp expect actual
45+
"
46+
}
47+
48+
# For tree comparisons, we expect to see subtrees at the boundary
49+
# get their own entry.
50+
check_trees() {
51+
check_one tree "$*" '' 0 file one
52+
check_one tree "$*" '' 1 file one/file one/two
53+
check_one tree "$*" '' 2 file one/file one/two/file one/two/three
54+
check_one tree "$*" '' 3 file one/file one/two/file one/two/three/file
55+
check_one tree "$*" '' -1 file one/file one/two/file one/two/three/file
56+
check_one tree "$*" one 0 one
57+
check_one tree "$*" one 1 one/file one/two
58+
check_one tree "$*" one 2 one/file one/two/file one/two/three
59+
check_one tree "$*" one 3 one/file one/two/file one/two/three/file
60+
check_one tree "$*" one/two 0 one/two
61+
check_one tree "$*" one/two 1 one/two/file one/two/three
62+
check_one tree "$*" one/two 2 one/two/file one/two/three/file
63+
check_one tree "$*" one/two 2 one/two/file one/two/three/file
64+
check_one tree "$*" one/two/three 0 one/two/three
65+
check_one tree "$*" one/two/three 1 one/two/three/file
66+
}
67+
68+
# But for index comparisons, we do not store subtrees at all, so we do not
69+
# expect them.
70+
check_index() {
71+
check_one "$@" '' 0 file
72+
check_one "$@" '' 1 file one/file
73+
check_one "$@" '' 2 file one/file one/two/file
74+
check_one "$@" '' 3 file one/file one/two/file one/two/three/file
75+
check_one "$@" one 0
76+
check_one "$@" one 1 one/file
77+
check_one "$@" one 2 one/file one/two/file
78+
check_one "$@" one 3 one/file one/two/file one/two/three/file
79+
check_one "$@" one/two 0
80+
check_one "$@" one/two 1 one/two/file
81+
check_one "$@" one/two 2 one/two/file one/two/three/file
82+
check_one "$@" one/two/three 0
83+
check_one "$@" one/two/three 1 one/two/three/file
84+
85+
# Value '-1' for '--max-depth is the same as recursion without limit,
86+
# and thus should always succeed.
87+
local expect=
88+
check_one "$@" '' -1 file one/file one/two/file one/two/three/file
89+
}
90+
91+
# Check as a modification...
92+
check_trees HEAD^ HEAD
93+
# ...and as an addition...
94+
check_trees empty HEAD
95+
# ...and as a deletion.
96+
check_trees HEAD empty
97+
98+
# We currently only implement max-depth for trees.
99+
expect=failure
100+
# Check index against a tree
101+
check_index index "--cached HEAD"
102+
# and index against the worktree
103+
check_index files ""
104+
expect=
105+
106+
test_expect_success 'find shortest path within embedded pathspecs' '
107+
cat >expect <<-\EOF &&
108+
one/file
109+
one/two/file
110+
one/two/three/file
111+
EOF
112+
git diff-tree --max-depth=2 --name-only HEAD^ HEAD -- one one/two >actual &&
113+
test_cmp expect actual
114+
'
115+
116+
test_done

t/unit-tests/u-dir.c

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#include "unit-test.h"
2+
#include "dir.h"
3+
4+
#define TEST_WITHIN_DEPTH(path, depth, max_depth, expect) do { \
5+
int actual = within_depth(path, strlen(path), \
6+
depth, max_depth); \
7+
if (actual != expect) \
8+
cl_failf("path '%s' with depth '%d' and max-depth '%d': expected %d, got %d", \
9+
path, depth, max_depth, expect, actual); \
10+
} while (0)
11+
12+
void test_dir__within_depth(void)
13+
{
14+
/* depth = 0; max_depth = 0 */
15+
TEST_WITHIN_DEPTH("", 0, 0, 1);
16+
TEST_WITHIN_DEPTH("file", 0, 0, 1);
17+
TEST_WITHIN_DEPTH("a", 0, 0, 1);
18+
TEST_WITHIN_DEPTH("a/file", 0, 0, 0);
19+
TEST_WITHIN_DEPTH("a/b", 0, 0, 0);
20+
TEST_WITHIN_DEPTH("a/b/file", 0, 0, 0);
21+
22+
/* depth = 0; max_depth = 1 */
23+
TEST_WITHIN_DEPTH("", 0, 1, 1);
24+
TEST_WITHIN_DEPTH("file", 0, 1, 1);
25+
TEST_WITHIN_DEPTH("a", 0, 1, 1);
26+
TEST_WITHIN_DEPTH("a/file", 0, 1, 1);
27+
TEST_WITHIN_DEPTH("a/b", 0, 1, 1);
28+
TEST_WITHIN_DEPTH("a/b/file", 0, 1, 0);
29+
30+
/* depth = 1; max_depth = 1 */
31+
TEST_WITHIN_DEPTH("", 1, 1, 1);
32+
TEST_WITHIN_DEPTH("file", 1, 1, 1);
33+
TEST_WITHIN_DEPTH("a", 1, 1, 1);
34+
TEST_WITHIN_DEPTH("a/file", 1, 1, 0);
35+
TEST_WITHIN_DEPTH("a/b", 1, 1, 0);
36+
TEST_WITHIN_DEPTH("a/b/file", 1, 1, 0);
37+
38+
/* depth = 1; max_depth = 0 */
39+
TEST_WITHIN_DEPTH("", 1, 0, 0);
40+
TEST_WITHIN_DEPTH("file", 1, 0, 0);
41+
TEST_WITHIN_DEPTH("a", 1, 0, 0);
42+
TEST_WITHIN_DEPTH("a/file", 1, 0, 0);
43+
TEST_WITHIN_DEPTH("a/b", 1, 0, 0);
44+
TEST_WITHIN_DEPTH("a/b/file", 1, 0, 0);
45+
46+
47+
}

0 commit comments

Comments
 (0)