Skip to content
This repository was archived by the owner on Nov 9, 2017. It is now read-only.

Commit a054ed8

Browse files
lanfeust69kasal
authored andcommitted
git tag --contains : avoid stack overflow
In large repos, the recursion implementation of contains(commit, commit_list) may result in a stack overflow. Replace the recursion with a loop to fix it. This problem is more apparent on Windows than on Linux, where the stack is more limited by default. See also this thread on the msysGit list: https://groups.google.com/d/topic/msysgit/FqT6boJrb2g/discussion [jes: re-written to imitate the original recursion more closely] Signed-off-by: Jean-Jacques Lafay <[email protected]> Signed-off-by: Johannes Schindelin <[email protected]> Tested-by: Stepan Kasal <[email protected]>
1 parent d4c3651 commit a054ed8

File tree

2 files changed

+90
-14
lines changed

2 files changed

+90
-14
lines changed

builtin/tag.c

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,38 +73,92 @@ static int in_commit_list(const struct commit_list *want, struct commit *c)
7373
return 0;
7474
}
7575

76-
static int contains_recurse(struct commit *candidate,
76+
/*
77+
* Test whether the candidate or one of its parents is contained in the list.
78+
* Do not recurse to find out, though, but return -1 if inconclusive.
79+
*/
80+
static int contains_test(struct commit *candidate,
7781
const struct commit_list *want)
7882
{
79-
struct commit_list *p;
80-
8183
/* was it previously marked as containing a want commit? */
8284
if (candidate->object.flags & TMP_MARK)
8385
return 1;
8486
/* or marked as not possibly containing a want commit? */
8587
if (candidate->object.flags & UNINTERESTING)
8688
return 0;
8789
/* or are we it? */
88-
if (in_commit_list(want, candidate))
90+
if (in_commit_list(want, candidate)) {
91+
candidate->object.flags |= TMP_MARK;
8992
return 1;
93+
}
9094

9195
if (parse_commit(candidate) < 0)
9296
return 0;
9397

94-
/* Otherwise recurse and mark ourselves for future traversals. */
95-
for (p = candidate->parents; p; p = p->next) {
96-
if (contains_recurse(p->item, want)) {
97-
candidate->object.flags |= TMP_MARK;
98-
return 1;
99-
}
100-
}
101-
candidate->object.flags |= UNINTERESTING;
102-
return 0;
98+
return -1;
99+
}
100+
101+
/*
102+
* This stack on the heap mimics the real stack in the previous implementation
103+
* of contains_recurse() that had to be replaced because it easily led to a
104+
* stack overflow.
105+
*
106+
* Every stack item points at a certain commit whose parents are iterated over.
107+
* Each item's current parent is the current next stack item's commit.
108+
*/
109+
struct stack {
110+
int nr, alloc;
111+
struct stack_entry {
112+
struct commit *commit;
113+
struct commit_list *parents;
114+
} *stack;
115+
};
116+
117+
static void push_to_stack(struct commit *candidate, struct stack *stack)
118+
{
119+
int index = stack->nr++;
120+
ALLOC_GROW(stack->stack, stack->nr, stack->alloc);
121+
stack->stack[index].commit = candidate;
122+
stack->stack[index].parents = candidate->parents;
103123
}
104124

105125
static int contains(struct commit *candidate, const struct commit_list *want)
106126
{
107-
return contains_recurse(candidate, want);
127+
struct stack stack = { 0, 0, NULL };
128+
int result = contains_test(candidate, want);
129+
130+
if (result >= 0)
131+
return result;
132+
133+
push_to_stack(candidate, &stack);
134+
while (stack.nr) {
135+
struct stack_entry *entry = &stack.stack[stack.nr - 1];
136+
struct commit *commit = entry->commit;
137+
struct commit_list *parents = entry->parents;
138+
139+
if (!parents) {
140+
commit->object.flags = UNINTERESTING;
141+
stack.nr--;
142+
}
143+
/*
144+
* If we just popped the stack, parents->item has been marked,
145+
* therefore contains_test will return a meaningful 0 or 1.
146+
*/
147+
else switch (contains_test(parents->item, want)) {
148+
case 1:
149+
commit->object.flags |= TMP_MARK;
150+
stack.nr--;
151+
break;
152+
case 0:
153+
entry->parents = parents->next;
154+
break;
155+
default:
156+
push_to_stack(parents->item, &stack);
157+
break;
158+
}
159+
}
160+
free(stack.stack);
161+
return contains_test(candidate, want);
108162
}
109163

110164
static void show_tag_lines(const unsigned char *sha1, int lines)

t/t7004-tag.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1380,4 +1380,26 @@ test_expect_success 'multiple --points-at are OR-ed together' '
13801380
test_cmp expect actual
13811381
'
13821382
1383+
# what about a deep repo ?
1384+
1385+
>expect
1386+
test_expect_success MINGW '--contains works in a deep repo' '
1387+
ulimit -s 64
1388+
i=1 &&
1389+
while test $i -lt 1000
1390+
do
1391+
echo "commit refs/heads/master
1392+
committer A U Thor <[email protected]> $((1000000000 + $i * 100)) +0200
1393+
data <<EOF
1394+
commit #$i
1395+
EOF"
1396+
test $i = 1 && echo "from refs/heads/master^0"
1397+
i=$(($i + 1))
1398+
done | git fast-import &&
1399+
git checkout master &&
1400+
git tag far-far-away HEAD^ &&
1401+
git tag --contains HEAD >actual &&
1402+
test_cmp expect actual
1403+
'
1404+
13831405
test_done

0 commit comments

Comments
 (0)