Skip to content

Commit d3038d2

Browse files
peffgitster
authored andcommitted
prune: keep objects reachable from recent objects
Our current strategy with prune is that an object falls into one of three categories: 1. Reachable (from ref tips, reflogs, index, etc). 2. Not reachable, but recent (based on the --expire time). 3. Not reachable and not recent. We keep objects from (1) and (2), but prune objects in (3). The point of (2) is that these objects may be part of an in-progress operation that has not yet updated any refs. However, it is not always the case that objects for an in-progress operation will have a recent mtime. For example, the object database may have an old copy of a blob (from an abandoned operation, a branch that was deleted, etc). If we create a new tree that points to it, a simultaneous prune will leave our tree, but delete the blob. Referencing that tree with a commit will then work (we check that the tree is in the object database, but not that all of its referred objects are), as will mentioning the commit in a ref. But the resulting repo is corrupt; we are missing the blob reachable from a ref. One way to solve this is to be more thorough when referencing a sha1: make sure that not only do we have that sha1, but that we have objects it refers to, and so forth recursively. The problem is that this is very expensive. Creating a parent link would require traversing the entire object graph! Instead, this patch pushes the extra work onto prune, which runs less frequently (and has to look at the whole object graph anyway). It creates a new category of objects: objects which are not recent, but which are reachable from a recent object. We do not prune these objects, just like the reachable and recent ones. This lets us avoid the recursive check above, because if we have an object, even if it is unreachable, we should have its referent. We can make a simple inductive argument that with this patch, this property holds (that there are no objects with missing referents in the repository): 0. When we have no objects, we have nothing to refer or be referred to, so the property holds. 1. If we add objects to the repository, their direct referents must generally exist (e.g., if you create a tree, the blobs it references must exist; if you create a commit to point at the tree, the tree must exist). This is already the case before this patch. And it is not 100% foolproof (you can make bogus objects using `git hash-object`, for example), but it should be the case for normal usage. Therefore for any sequence of object additions, the property will continue to hold. 2. If we remove objects from the repository, then we will not remove a child object (like a blob) if an object that refers to it is being kept. That is the part implemented by this patch. Note, however, that our reachability check and the actual pruning are not atomic. So it _is_ still possible to violate the property (e.g., an object becomes referenced just as we are deleting it). This patch is shooting for eliminating problems where the mtimes of dependent objects differ by hours or days, and one is dropped without the other. It does nothing to help with short races. Naively, the simplest way to implement this would be to add all recent objects as tips to the reachability traversal. However, this does not perform well. In a recently-packed repository, all reachable objects will also be recent, and therefore we have to look at each object twice. This patch instead performs the reachability traversal, then follows up with a second traversal for recent objects, skipping any that have already been marked. Signed-off-by: Jeff King <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 660c889 commit d3038d2

File tree

5 files changed

+204
-3
lines changed

5 files changed

+204
-3
lines changed

builtin/prune.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
135135
if (show_progress)
136136
progress = start_progress_delay(_("Checking connectivity"), 0, 0, 2);
137137

138-
mark_reachable_objects(&revs, 1, progress);
138+
mark_reachable_objects(&revs, 1, expire, progress);
139139
stop_progress(&progress);
140140
for_each_loose_file_in_objdir(get_object_directory(), prune_object,
141141
prune_cruft, prune_subdir, NULL);

builtin/reflog.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix)
649649
init_revisions(&cb.revs, prefix);
650650
if (cb.verbose)
651651
printf("Marking reachable objects...");
652-
mark_reachable_objects(&cb.revs, 0, NULL);
652+
mark_reachable_objects(&cb.revs, 0, 0, NULL);
653653
if (cb.verbose)
654654
putchar('\n');
655655
}

reachable.c

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,109 @@ static void mark_commit(struct commit *c, void *data)
9797
mark_object(&c->object, NULL, NULL, data);
9898
}
9999

100+
struct recent_data {
101+
struct rev_info *revs;
102+
unsigned long timestamp;
103+
};
104+
105+
static void add_recent_object(const unsigned char *sha1,
106+
unsigned long mtime,
107+
struct recent_data *data)
108+
{
109+
struct object *obj;
110+
enum object_type type;
111+
112+
if (mtime <= data->timestamp)
113+
return;
114+
115+
/*
116+
* We do not want to call parse_object here, because
117+
* inflating blobs and trees could be very expensive.
118+
* However, we do need to know the correct type for
119+
* later processing, and the revision machinery expects
120+
* commits and tags to have been parsed.
121+
*/
122+
type = sha1_object_info(sha1, NULL);
123+
if (type < 0)
124+
die("unable to get object info for %s", sha1_to_hex(sha1));
125+
126+
switch (type) {
127+
case OBJ_TAG:
128+
case OBJ_COMMIT:
129+
obj = parse_object_or_die(sha1, NULL);
130+
break;
131+
case OBJ_TREE:
132+
obj = (struct object *)lookup_tree(sha1);
133+
break;
134+
case OBJ_BLOB:
135+
obj = (struct object *)lookup_blob(sha1);
136+
break;
137+
default:
138+
die("unknown object type for %s: %s",
139+
sha1_to_hex(sha1), typename(type));
140+
}
141+
142+
if (!obj)
143+
die("unable to lookup %s", sha1_to_hex(sha1));
144+
145+
add_pending_object(data->revs, obj, "");
146+
}
147+
148+
static int add_recent_loose(const unsigned char *sha1,
149+
const char *path, void *data)
150+
{
151+
struct stat st;
152+
struct object *obj = lookup_object(sha1);
153+
154+
if (obj && obj->flags & SEEN)
155+
return 0;
156+
157+
if (stat(path, &st) < 0) {
158+
/*
159+
* It's OK if an object went away during our iteration; this
160+
* could be due to a simultaneous repack. But anything else
161+
* we should abort, since we might then fail to mark objects
162+
* which should not be pruned.
163+
*/
164+
if (errno == ENOENT)
165+
return 0;
166+
return error("unable to stat %s: %s",
167+
sha1_to_hex(sha1), strerror(errno));
168+
}
169+
170+
add_recent_object(sha1, st.st_mtime, data);
171+
return 0;
172+
}
173+
174+
static int add_recent_packed(const unsigned char *sha1,
175+
struct packed_git *p, uint32_t pos,
176+
void *data)
177+
{
178+
struct object *obj = lookup_object(sha1);
179+
180+
if (obj && obj->flags & SEEN)
181+
return 0;
182+
add_recent_object(sha1, p->mtime, data);
183+
return 0;
184+
}
185+
186+
static int add_unseen_recent_objects_to_traversal(struct rev_info *revs,
187+
unsigned long timestamp)
188+
{
189+
struct recent_data data;
190+
int r;
191+
192+
data.revs = revs;
193+
data.timestamp = timestamp;
194+
195+
r = for_each_loose_object(add_recent_loose, &data);
196+
if (r)
197+
return r;
198+
return for_each_packed_object(add_recent_packed, &data);
199+
}
200+
100201
void mark_reachable_objects(struct rev_info *revs, int mark_reflog,
202+
unsigned long mark_recent,
101203
struct progress *progress)
102204
{
103205
struct connectivity_progress cp;
@@ -133,5 +235,15 @@ void mark_reachable_objects(struct rev_info *revs, int mark_reflog,
133235
if (prepare_revision_walk(revs))
134236
die("revision walk setup failed");
135237
traverse_commit_list(revs, mark_commit, mark_object, &cp);
238+
239+
if (mark_recent) {
240+
revs->ignore_missing_links = 1;
241+
if (add_unseen_recent_objects_to_traversal(revs, mark_recent))
242+
die("unable to mark recent objects");
243+
if (prepare_revision_walk(revs))
244+
die("revision walk setup failed");
245+
traverse_commit_list(revs, mark_commit, mark_object, &cp);
246+
}
247+
136248
display_progress(cp.progress, cp.count);
137249
}

reachable.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#define REACHEABLE_H
33

44
struct progress;
5-
extern void mark_reachable_objects(struct rev_info *revs, int mark_reflog, struct progress *);
5+
extern void mark_reachable_objects(struct rev_info *revs, int mark_reflog,
6+
unsigned long mark_recent, struct progress *);
67

78
#endif

t/t6501-freshen-objects.sh

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/bin/sh
2+
#
3+
# This test covers the handling of objects which might have old
4+
# mtimes in the filesystem (because they were used previously)
5+
# and are just now becoming referenced again.
6+
#
7+
# We're going to do two things that are a little bit "fake" to
8+
# help make our simulation easier:
9+
#
10+
# 1. We'll turn off reflogs. You can still run into
11+
# problems with reflogs on, but your objects
12+
# don't get pruned until both the reflog expiration
13+
# has passed on their references, _and_ they are out
14+
# of prune's expiration period. Dropping reflogs
15+
# means we only have to deal with one variable in our tests,
16+
# but the results generalize.
17+
#
18+
# 2. We'll use a temporary index file to create our
19+
# works-in-progress. Most workflows would mention
20+
# referenced objects in the index, which prune takes
21+
# into account. However, many operations don't. For
22+
# example, a partial commit with "git commit foo"
23+
# will use a temporary index. Or they may not need
24+
# an index at all (e.g., creating a new commit
25+
# to refer to an existing tree).
26+
27+
test_description='check pruning of dependent objects'
28+
. ./test-lib.sh
29+
30+
# We care about reachability, so we do not want to use
31+
# the normal test_commit, which creates extra tags.
32+
add () {
33+
echo "$1" >"$1" &&
34+
git add "$1"
35+
}
36+
commit () {
37+
test_tick &&
38+
add "$1" &&
39+
git commit -m "$1"
40+
}
41+
42+
test_expect_success 'disable reflogs' '
43+
git config core.logallrefupdates false &&
44+
rm -rf .git/logs
45+
'
46+
47+
test_expect_success 'setup basic history' '
48+
commit base
49+
'
50+
51+
test_expect_success 'create and abandon some objects' '
52+
git checkout -b experiment &&
53+
commit abandon &&
54+
git checkout master &&
55+
git branch -D experiment
56+
'
57+
58+
test_expect_success 'simulate time passing' '
59+
find .git/objects -type f |
60+
xargs test-chmtime -v -86400
61+
'
62+
63+
test_expect_success 'start writing new commit with old blob' '
64+
tree=$(
65+
GIT_INDEX_FILE=index.tmp &&
66+
export GIT_INDEX_FILE &&
67+
git read-tree HEAD &&
68+
add unrelated &&
69+
add abandon &&
70+
git write-tree
71+
)
72+
'
73+
74+
test_expect_success 'simultaneous gc' '
75+
git gc --prune=12.hours.ago
76+
'
77+
78+
test_expect_success 'finish writing out commit' '
79+
commit=$(echo foo | git commit-tree -p HEAD $tree) &&
80+
git update-ref HEAD $commit
81+
'
82+
83+
# "abandon" blob should have been rescued by reference from new tree
84+
test_expect_success 'repository passes fsck' '
85+
git fsck
86+
'
87+
88+
test_done

0 commit comments

Comments
 (0)