path-walk: mark trees and blobs as UNINTERESTING

When the input rev_info has UNINTERESTING starting points, we want to be sure that the UNINTERESTING flag is passed appropriately through the objects. To match how this is done in places such as 'git pack-objects', we use the mark_edges_uninteresting() method. This method has an option for using the "sparse" walk, which is similar in spirit to the path-walk API's walk. To be sure to keep it independent, add a new 'prune_all_uninteresting' option to the path_walk_info struct. To check how the UNINTERSTING flag is spread through our objects, extend the 'test-tool path-walk' command to output whether or not an object has that flag. This changes our tests significantly, including the removal of some objects that were previously visited due to the incomplete implementation. Signed-off-by: Derrick Stolee <stolee@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed Dec 20, 2024 at 16:21 UTC 6333e7ae0bb1027b3299d482be5a4a0937116ab0
5 files changed +159 -22
Documentation/technical/api-path-walk.txt
+8
@@ -48,6 +48,14 @@ commits.
48 While it is possible to walk only commits in this way, consumers would be
49 better off using the revision walk API instead.
50
51 +`prune_all_uninteresting`::
52 + By default, all reachable paths are emitted by the path-walk API.
53 + This option allows consumers to declare that they are not
54 + interested in paths where all included objects are marked with the
55 + `UNINTERESTING` flag. This requires using the `boundary` option in
56 + the revision walk so that the walk emits commits marked with the
57 + `UNINTERESTING` flag.
58 +
59 Examples
60 --------
61
path-walk.c
+74
@@ -8,6 +8,7 @@
8 #include "dir.h"
9 #include "hashmap.h"
10 #include "hex.h"
11 +#include "list-objects.h"
12 #include "object.h"
13 #include "oid-array.h"
14 #include "revision.h"
@@ -23,6 +24,7 @@ static const char *root_path = "";
24 struct type_and_oid_list {
25 enum object_type type;
26 struct oid_array oids;
27 + int maybe_interesting;
28 };
29
30 #define TYPE_AND_OID_LIST_INIT { \
@@ -142,6 +144,10 @@ static int add_tree_entries(struct path_walk_context *ctx,
144 strmap_put(&ctx->paths_to_lists, path.buf, list);
145 }
146 push_to_stack(ctx, path.buf);
147 +
148 + if (!(o->flags & UNINTERESTING))
149 + list->maybe_interesting = 1;
150 +
151 oid_array_append(&list->oids, &entry.oid);
152 }
153
@@ -169,6 +175,43 @@ static int walk_path(struct path_walk_context *ctx,
175 if (!list->oids.nr)
176 return 0;
177
178 + if (ctx->info->prune_all_uninteresting) {
179 + /*
180 + * This is true if all objects were UNINTERESTING
181 + * when added to the list.
182 + */
183 + if (!list->maybe_interesting)
184 + return 0;
185 +
186 + /*
187 + * But it's still possible that the objects were set
188 + * as UNINTERESTING after being added. Do a quick check.
189 + */
190 + list->maybe_interesting = 0;
191 + for (size_t i = 0;
192 + !list->maybe_interesting && i < list->oids.nr;
193 + i++) {
194 + if (list->type == OBJ_TREE) {
195 + struct tree *t = lookup_tree(ctx->repo,
196 + &list->oids.oid[i]);
197 + if (t && !(t->object.flags & UNINTERESTING))
198 + list->maybe_interesting = 1;
199 + } else if (list->type == OBJ_BLOB) {
200 + struct blob *b = lookup_blob(ctx->repo,
201 + &list->oids.oid[i]);
202 + if (b && !(b->object.flags & UNINTERESTING))
203 + list->maybe_interesting = 1;
204 + } else {
205 + /* Tags are always interesting if visited. */
206 + list->maybe_interesting = 1;
207 + }
208 + }
209 +
210 + /* We have confirmed that all objects are UNINTERESTING. */
211 + if (!list->maybe_interesting)
212 + return 0;
213 + }
214 +
215 /* Evaluate function pointer on this data, if requested. */
216 if ((list->type == OBJ_TREE && ctx->info->trees) ||
217 (list->type == OBJ_BLOB && ctx->info->blobs) ||
@@ -203,6 +246,26 @@ static void clear_paths_to_lists(struct strmap *map)
246 strmap_init(map);
247 }
248
249 +static struct repository *edge_repo;
250 +static struct type_and_oid_list *edge_tree_list;
251 +
252 +static void show_edge(struct commit *commit)
253 +{
254 + struct tree *t = repo_get_commit_tree(edge_repo, commit);
255 +
256 + if (!t)
257 + return;
258 +
259 + if (commit->object.flags & UNINTERESTING)
260 + t->object.flags |= UNINTERESTING;
261 +
262 + if (t->object.flags & SEEN)
263 + return;
264 + t->object.flags |= SEEN;
265 +
266 + oid_array_append(&edge_tree_list->oids, &t->object.oid);
267 +}
268 +
269 static int setup_pending_objects(struct path_walk_info *info,
270 struct path_walk_context *ctx)
271 {
@@ -314,6 +377,7 @@ static int setup_pending_objects(struct path_walk_info *info,
377 if (tagged_blobs->oids.nr) {
378 const char *tagged_blob_path = "/tagged-blobs";
379 tagged_blobs->type = OBJ_BLOB;
380 + tagged_blobs->maybe_interesting = 1;
381 push_to_stack(ctx, tagged_blob_path);
382 strmap_put(&ctx->paths_to_lists, tagged_blob_path, tagged_blobs);
383 } else {
@@ -325,6 +389,7 @@ static int setup_pending_objects(struct path_walk_info *info,
389 if (tags->oids.nr) {
390 const char *tag_path = "/tags";
391 tags->type = OBJ_TAG;
392 + tags->maybe_interesting = 1;
393 push_to_stack(ctx, tag_path);
394 strmap_put(&ctx->paths_to_lists, tag_path, tags);
395 } else {
@@ -369,6 +434,7 @@ int walk_objects_by_path(struct path_walk_info *info)
434 /* Insert a single list for the root tree into the paths. */
435 CALLOC_ARRAY(root_tree_list, 1);
436 root_tree_list->type = OBJ_TREE;
437 + root_tree_list->maybe_interesting = 1;
438 strmap_put(&ctx.paths_to_lists, root_path, root_tree_list);
439 push_to_stack(&ctx, root_path);
440
@@ -382,6 +448,14 @@ int walk_objects_by_path(struct path_walk_info *info)
448 if (prepare_revision_walk(info->revs))
449 die(_("failed to setup revision walk"));
450
451 + /* Walk trees to mark them as UNINTERESTING. */
452 + edge_repo = info->revs->repo;
453 + edge_tree_list = root_tree_list;
454 + mark_edges_uninteresting(info->revs, show_edge,
455 + info->prune_all_uninteresting);
456 + edge_repo = NULL;
457 + edge_tree_list = NULL;
458 +
459 info->revs->blob_objects = info->revs->tree_objects = 0;
460
461 trace2_region_enter("path-walk", "pending-walk", info->revs->repo);
path-walk.h
+8
@@ -40,6 +40,14 @@ struct path_walk_info {
40 int trees;
41 int blobs;
42 int tags;
43 +
44 + /**
45 + * When 'prune_all_uninteresting' is set and a path has all objects
46 + * marked as UNINTERESTING, then the path-walk will not visit those
47 + * objects. It will not call path_fn on those objects and will not
48 + * walk the children of such trees.
49 + */
50 + int prune_all_uninteresting;
51 };
52
53 #define PATH_WALK_INFO_INIT { \
t/helper/test-path-walk.c
+9 -3
@@ -50,10 +50,14 @@ static int emit_block(const char *path, struct oid_array *oids,
50 printf("%"PRIuMAX":%s:%s:EMPTY\n",
51 tdata->batch_nr, typestr, path);
52
53 - for (size_t i = 0; i < oids->nr; i++)
54 - printf("%"PRIuMAX":%s:%s:%s\n",
53 + for (size_t i = 0; i < oids->nr; i++) {
54 + struct object *o = lookup_unknown_object(the_repository,
55 + &oids->oid[i]);
56 + printf("%"PRIuMAX":%s:%s:%s%s\n",
57 tdata->batch_nr, typestr, path,
56 - oid_to_hex(&oids->oid[i]));
58 + oid_to_hex(&oids->oid[i]),
59 + o->flags & UNINTERESTING ? ":UNINTERESTING" : "");
60 + }
61
62 tdata->batch_nr++;
63 return 0;
@@ -74,6 +78,8 @@ int cmd__path_walk(int argc, const char **argv)
78 N_("toggle inclusion of tag objects")),
79 OPT_BOOL(0, "trees", &info.trees,
80 N_("toggle inclusion of tree objects")),
81 + OPT_BOOL(0, "prune", &info.prune_all_uninteresting,
82 + N_("toggle pruning of uninteresting paths")),
83 OPT_END(),
84 };
85
t/t6601-path-walk.sh
+60 -19
@@ -211,11 +211,11 @@ test_expect_success 'topic, not base' '
211 0:commit::$(git rev-parse topic)
212 1:tree::$(git rev-parse topic^{tree})
213 2:tree:right/:$(git rev-parse topic:right)
214 - 3:blob:right/d:$(git rev-parse topic:right/d)
214 + 3:blob:right/d:$(git rev-parse topic:right/d):UNINTERESTING
215 4:blob:right/c:$(git rev-parse topic:right/c)
216 - 5:tree:left/:$(git rev-parse topic:left)
217 - 6:blob:left/b:$(git rev-parse topic:left/b)
218 - 7:blob:a:$(git rev-parse topic:a)
216 + 5:tree:left/:$(git rev-parse topic:left):UNINTERESTING
217 + 6:blob:left/b:$(git rev-parse topic:left/b):UNINTERESTING
218 + 7:blob:a:$(git rev-parse topic:a):UNINTERESTING
219 blobs:4
220 commits:1
221 tags:0
@@ -225,15 +225,38 @@ test_expect_success 'topic, not base' '
225 test_cmp_sorted expect out
226 '
227
228 +test_expect_success 'fourth, blob-tag2, not base' '
229 + test-tool path-walk -- fourth blob-tag2 --not base >out &&
230 +
231 + cat >expect <<-EOF &&
232 + 0:commit::$(git rev-parse topic)
233 + 1:tag:/tags:$(git rev-parse fourth)
234 + 2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})
235 + 3:tree::$(git rev-parse topic^{tree})
236 + 4:tree:right/:$(git rev-parse topic:right)
237 + 5:blob:right/d:$(git rev-parse base~1:right/d):UNINTERESTING
238 + 6:blob:right/c:$(git rev-parse topic:right/c)
239 + 7:tree:left/:$(git rev-parse base~1:left):UNINTERESTING
240 + 8:blob:left/b:$(git rev-parse base~1:left/b):UNINTERESTING
241 + 9:blob:a:$(git rev-parse base~1:a):UNINTERESTING
242 + blobs:5
243 + commits:1
244 + tags:1
245 + trees:3
246 + EOF
247 +
248 + test_cmp_sorted expect out
249 +'
250 +
251 test_expect_success 'topic, not base, only blobs' '
252 test-tool path-walk --no-trees --no-commits \
253 -- topic --not base >out &&
254
255 cat >expect <<-EOF &&
233 - 0:blob:right/d:$(git rev-parse topic:right/d)
256 + 0:blob:right/d:$(git rev-parse topic:right/d):UNINTERESTING
257 1:blob:right/c:$(git rev-parse topic:right/c)
235 - 2:blob:left/b:$(git rev-parse topic:left/b)
236 - 3:blob:a:$(git rev-parse topic:a)
258 + 2:blob:left/b:$(git rev-parse topic:left/b):UNINTERESTING
259 + 3:blob:a:$(git rev-parse topic:a):UNINTERESTING
260 blobs:4
261 commits:0
262 tags:0
@@ -267,7 +290,7 @@ test_expect_success 'topic, not base, only trees' '
290 cat >expect <<-EOF &&
291 0:tree::$(git rev-parse topic^{tree})
292 1:tree:right/:$(git rev-parse topic:right)
270 - 2:tree:left/:$(git rev-parse topic:left)
293 + 2:tree:left/:$(git rev-parse topic:left):UNINTERESTING
294 commits:0
295 blobs:0
296 tags:0
@@ -282,17 +305,17 @@ test_expect_success 'topic, not base, boundary' '
305
306 cat >expect <<-EOF &&
307 0:commit::$(git rev-parse topic)
285 - 0:commit::$(git rev-parse base~1)
308 + 0:commit::$(git rev-parse base~1):UNINTERESTING
309 1:tree::$(git rev-parse topic^{tree})
287 - 1:tree::$(git rev-parse base~1^{tree})
310 + 1:tree::$(git rev-parse base~1^{tree}):UNINTERESTING
311 2:tree:right/:$(git rev-parse topic:right)
289 - 2:tree:right/:$(git rev-parse base~1:right)
290 - 3:blob:right/d:$(git rev-parse base~1:right/d)
291 - 4:blob:right/c:$(git rev-parse base~1:right/c)
312 + 2:tree:right/:$(git rev-parse base~1:right):UNINTERESTING
313 + 3:blob:right/d:$(git rev-parse base~1:right/d):UNINTERESTING
314 + 4:blob:right/c:$(git rev-parse base~1:right/c):UNINTERESTING
315 4:blob:right/c:$(git rev-parse topic:right/c)
293 - 5:tree:left/:$(git rev-parse base~1:left)
294 - 6:blob:left/b:$(git rev-parse base~1:left/b)
295 - 7:blob:a:$(git rev-parse base~1:a)
316 + 5:tree:left/:$(git rev-parse base~1:left):UNINTERESTING
317 + 6:blob:left/b:$(git rev-parse base~1:left/b):UNINTERESTING
318 + 7:blob:a:$(git rev-parse base~1:a):UNINTERESTING
319 blobs:5
320 commits:2
321 tags:0
@@ -302,6 +325,27 @@ test_expect_success 'topic, not base, boundary' '
325 test_cmp_sorted expect out
326 '
327
328 +test_expect_success 'topic, not base, boundary with pruning' '
329 + test-tool path-walk --prune -- --boundary topic --not base >out &&
330 +
331 + cat >expect <<-EOF &&
332 + 0:commit::$(git rev-parse topic)
333 + 0:commit::$(git rev-parse base~1):UNINTERESTING
334 + 1:tree::$(git rev-parse topic^{tree})
335 + 1:tree::$(git rev-parse base~1^{tree}):UNINTERESTING
336 + 2:tree:right/:$(git rev-parse topic:right)
337 + 2:tree:right/:$(git rev-parse base~1:right):UNINTERESTING
338 + 3:blob:right/c:$(git rev-parse base~1:right/c):UNINTERESTING
339 + 3:blob:right/c:$(git rev-parse topic:right/c)
340 + blobs:2
341 + commits:2
342 + tags:0
343 + trees:4
344 + EOF
345 +
346 + test_cmp_sorted expect out
347 +'
348 +
349 test_expect_success 'trees are reported exactly once' '
350 test_when_finished "rm -rf unique-trees" &&
351 test_create_repo unique-trees &&
@@ -309,15 +353,12 @@ test_expect_success 'trees are reported exactly once' '
353 cd unique-trees &&
354 mkdir initial &&
355 test_commit initial/file &&
312 -
356 git switch -c move-to-top &&
357 git mv initial/file.t ./ &&
358 test_tick &&
359 git commit -m moved &&
317 -
360 git update-ref refs/heads/other HEAD
361 ) &&
320 -
362 test-tool -C unique-trees path-walk -- --all >out &&
363 tree=$(git -C unique-trees rev-parse HEAD:) &&
364 grep "$tree" out >out-filtered &&