Skip to content

Commit 538569b

Browse files
committed
Merge branch 'jk/delta-chain-limit'
"git repack --depth=<n>" for a long time busted the specified depth when reusing delta from existing packs. This has been corrected. * jk/delta-chain-limit: pack-objects: convert recursion to iteration in break_delta_chain() pack-objects: enforce --depth limit in reused deltas
2 parents 1b32498 + 42b766d commit 538569b

File tree

3 files changed

+207
-23
lines changed

3 files changed

+207
-23
lines changed

builtin/pack-objects.c

Lines changed: 110 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,6 +1539,8 @@ static int pack_offset_sort(const void *_a, const void *_b)
15391539
* 2. Updating our size/type to the non-delta representation. These were
15401540
* either not recorded initially (size) or overwritten with the delta type
15411541
* (type) when check_object() decided to reuse the delta.
1542+
*
1543+
* 3. Resetting our delta depth, as we are now a base object.
15421544
*/
15431545
static void drop_reused_delta(struct object_entry *entry)
15441546
{
@@ -1552,6 +1554,7 @@ static void drop_reused_delta(struct object_entry *entry)
15521554
p = &(*p)->delta_sibling;
15531555
}
15541556
entry->delta = NULL;
1557+
entry->depth = 0;
15551558

15561559
oi.sizep = &entry->size;
15571560
oi.typep = &entry->type;
@@ -1570,39 +1573,123 @@ static void drop_reused_delta(struct object_entry *entry)
15701573
* Follow the chain of deltas from this entry onward, throwing away any links
15711574
* that cause us to hit a cycle (as determined by the DFS state flags in
15721575
* the entries).
1576+
*
1577+
* We also detect too-long reused chains that would violate our --depth
1578+
* limit.
15731579
*/
15741580
static void break_delta_chains(struct object_entry *entry)
15751581
{
1576-
/* If it's not a delta, it can't be part of a cycle. */
1577-
if (!entry->delta) {
1578-
entry->dfs_state = DFS_DONE;
1579-
return;
1580-
}
1582+
/*
1583+
* The actual depth of each object we will write is stored as an int,
1584+
* as it cannot exceed our int "depth" limit. But before we break
1585+
* changes based no that limit, we may potentially go as deep as the
1586+
* number of objects, which is elsewhere bounded to a uint32_t.
1587+
*/
1588+
uint32_t total_depth;
1589+
struct object_entry *cur, *next;
1590+
1591+
for (cur = entry, total_depth = 0;
1592+
cur;
1593+
cur = cur->delta, total_depth++) {
1594+
if (cur->dfs_state == DFS_DONE) {
1595+
/*
1596+
* We've already seen this object and know it isn't
1597+
* part of a cycle. We do need to append its depth
1598+
* to our count.
1599+
*/
1600+
total_depth += cur->depth;
1601+
break;
1602+
}
15811603

1582-
switch (entry->dfs_state) {
1583-
case DFS_NONE:
15841604
/*
1585-
* This is the first time we've seen the object. We mark it as
1586-
* part of the active potential cycle and recurse.
1605+
* We break cycles before looping, so an ACTIVE state (or any
1606+
* other cruft which made its way into the state variable)
1607+
* is a bug.
15871608
*/
1588-
entry->dfs_state = DFS_ACTIVE;
1589-
break_delta_chains(entry->delta);
1590-
entry->dfs_state = DFS_DONE;
1591-
break;
1609+
if (cur->dfs_state != DFS_NONE)
1610+
die("BUG: confusing delta dfs state in first pass: %d",
1611+
cur->dfs_state);
15921612

1593-
case DFS_DONE:
1594-
/* object already examined, and not part of a cycle */
1595-
break;
1613+
/*
1614+
* Now we know this is the first time we've seen the object. If
1615+
* it's not a delta, we're done traversing, but we'll mark it
1616+
* done to save time on future traversals.
1617+
*/
1618+
if (!cur->delta) {
1619+
cur->dfs_state = DFS_DONE;
1620+
break;
1621+
}
15961622

1597-
case DFS_ACTIVE:
15981623
/*
1599-
* We found a cycle that needs broken. It would be correct to
1600-
* break any link in the chain, but it's convenient to
1601-
* break this one.
1624+
* Mark ourselves as active and see if the next step causes
1625+
* us to cycle to another active object. It's important to do
1626+
* this _before_ we loop, because it impacts where we make the
1627+
* cut, and thus how our total_depth counter works.
1628+
* E.g., We may see a partial loop like:
1629+
*
1630+
* A -> B -> C -> D -> B
1631+
*
1632+
* Cutting B->C breaks the cycle. But now the depth of A is
1633+
* only 1, and our total_depth counter is at 3. The size of the
1634+
* error is always one less than the size of the cycle we
1635+
* broke. Commits C and D were "lost" from A's chain.
1636+
*
1637+
* If we instead cut D->B, then the depth of A is correct at 3.
1638+
* We keep all commits in the chain that we examined.
16021639
*/
1603-
drop_reused_delta(entry);
1604-
entry->dfs_state = DFS_DONE;
1605-
break;
1640+
cur->dfs_state = DFS_ACTIVE;
1641+
if (cur->delta->dfs_state == DFS_ACTIVE) {
1642+
drop_reused_delta(cur);
1643+
cur->dfs_state = DFS_DONE;
1644+
break;
1645+
}
1646+
}
1647+
1648+
/*
1649+
* And now that we've gone all the way to the bottom of the chain, we
1650+
* need to clear the active flags and set the depth fields as
1651+
* appropriate. Unlike the loop above, which can quit when it drops a
1652+
* delta, we need to keep going to look for more depth cuts. So we need
1653+
* an extra "next" pointer to keep going after we reset cur->delta.
1654+
*/
1655+
for (cur = entry; cur; cur = next) {
1656+
next = cur->delta;
1657+
1658+
/*
1659+
* We should have a chain of zero or more ACTIVE states down to
1660+
* a final DONE. We can quit after the DONE, because either it
1661+
* has no bases, or we've already handled them in a previous
1662+
* call.
1663+
*/
1664+
if (cur->dfs_state == DFS_DONE)
1665+
break;
1666+
else if (cur->dfs_state != DFS_ACTIVE)
1667+
die("BUG: confusing delta dfs state in second pass: %d",
1668+
cur->dfs_state);
1669+
1670+
/*
1671+
* If the total_depth is more than depth, then we need to snip
1672+
* the chain into two or more smaller chains that don't exceed
1673+
* the maximum depth. Most of the resulting chains will contain
1674+
* (depth + 1) entries (i.e., depth deltas plus one base), and
1675+
* the last chain (i.e., the one containing entry) will contain
1676+
* whatever entries are left over, namely
1677+
* (total_depth % (depth + 1)) of them.
1678+
*
1679+
* Since we are iterating towards decreasing depth, we need to
1680+
* decrement total_depth as we go, and we need to write to the
1681+
* entry what its final depth will be after all of the
1682+
* snipping. Since we're snipping into chains of length (depth
1683+
* + 1) entries, the final depth of an entry will be its
1684+
* original depth modulo (depth + 1). Any time we encounter an
1685+
* entry whose final depth is supposed to be zero, we snip it
1686+
* from its delta base, thereby making it so.
1687+
*/
1688+
cur->depth = (total_depth--) % (depth + 1);
1689+
if (!cur->depth)
1690+
drop_reused_delta(cur);
1691+
1692+
cur->dfs_state = DFS_DONE;
16061693
}
16071694
}
16081695

pack-objects.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,16 @@ struct object_entry {
3030

3131
/*
3232
* State flags for depth-first search used for analyzing delta cycles.
33+
*
34+
* The depth is measured in delta-links to the base (so if A is a delta
35+
* against B, then A has a depth of 1, and B a depth of 0).
3336
*/
3437
enum {
3538
DFS_NONE = 0,
3639
DFS_ACTIVE,
3740
DFS_DONE
3841
} dfs_state;
42+
int depth;
3943
};
4044

4145
struct packing_data {

t/t5316-pack-delta-depth.sh

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/bin/sh
2+
3+
test_description='pack-objects breaks long cross-pack delta chains'
4+
. ./test-lib.sh
5+
6+
# This mirrors a repeated push setup:
7+
#
8+
# 1. A client repeatedly modifies some files, makes a
9+
# commit, and pushes the result. It does this N times
10+
# before we get around to repacking.
11+
#
12+
# 2. Each push generates a thin pack with the new version of
13+
# various objects. Let's consider some file in the root tree
14+
# which is updated in each commit.
15+
#
16+
# When generating push number X, we feed commit X-1 (and
17+
# thus blob X-1) as a preferred base. The resulting pack has
18+
# blob X as a thin delta against blob X-1.
19+
#
20+
# On the receiving end, "index-pack --fix-thin" will
21+
# complete the pack with a base copy of blob X-1.
22+
#
23+
# 3. In older versions of git, if we used the delta from
24+
# pack X, then we'd always find blob X-1 as a base in the
25+
# same pack (and generate a fresh delta).
26+
#
27+
# But with the pack mru, we jump from delta to delta
28+
# following the traversal order:
29+
#
30+
# a. We grab blob X from pack X as a delta, putting it at
31+
# the tip of our mru list.
32+
#
33+
# b. Eventually we move onto commit X-1. We need other
34+
# objects which are only in pack X-1 (in the test code
35+
# below, it's the containing tree). That puts pack X-1
36+
# at the tip of our mru list.
37+
#
38+
# c. Eventually we look for blob X-1, and we find the
39+
# version in pack X-1 (because it's the mru tip).
40+
#
41+
# Now we have blob X as a delta against X-1, which is a delta
42+
# against X-2, and so forth.
43+
#
44+
# In the real world, these small pushes would get exploded by
45+
# unpack-objects rather than "index-pack --fix-thin", but the
46+
# same principle applies to larger pushes (they only need one
47+
# repeatedly-modified file to generate the delta chain).
48+
49+
test_expect_success 'create series of packs' '
50+
test-genrandom foo 4096 >content &&
51+
prev= &&
52+
for i in $(test_seq 1 10)
53+
do
54+
cat content >file &&
55+
echo $i >>file &&
56+
git add file &&
57+
git commit -m $i &&
58+
cur=$(git rev-parse HEAD^{tree}) &&
59+
{
60+
test -n "$prev" && echo "-$prev"
61+
echo $cur
62+
echo "$(git rev-parse :file) file"
63+
} | git pack-objects --stdout >tmp &&
64+
git index-pack --stdin --fix-thin <tmp || return 1
65+
prev=$cur
66+
done
67+
'
68+
69+
max_chain() {
70+
git index-pack --verify-stat-only "$1" >output &&
71+
perl -lne '
72+
/chain length = (\d+)/ and $len = $1;
73+
END { print $len }
74+
' output
75+
}
76+
77+
# Note that this whole setup is pretty reliant on the current
78+
# packing heuristics. We double-check that our test case
79+
# actually produces a long chain. If it doesn't, it should be
80+
# adjusted (or scrapped if the heuristics have become too unreliable)
81+
test_expect_success 'packing produces a long delta' '
82+
# Use --window=0 to make sure we are seeing reused deltas,
83+
# not computing a new long chain.
84+
pack=$(git pack-objects --all --window=0 </dev/null pack) &&
85+
test 9 = "$(max_chain pack-$pack.pack)"
86+
'
87+
88+
test_expect_success '--depth limits depth' '
89+
pack=$(git pack-objects --all --depth=5 </dev/null pack) &&
90+
test 5 = "$(max_chain pack-$pack.pack)"
91+
'
92+
93+
test_done

0 commit comments

Comments
 (0)