Git fork

Merge branch 'ds/path-walk-1'

Introduce a new API to visit objects in batches based on a common
path, or by type.

* ds/path-walk-1:
path-walk: drop redundant parse_tree() call
path-walk: reorder object visits
path-walk: mark trees and blobs as UNINTERESTING
path-walk: visit tags and cached objects
path-walk: allow consumer to specify object types
t6601: add helper for testing path-walk API
test-lib-functions: add test_cmp_sorted
path-walk: introduce an object walk by path

+1220
+63
Documentation/technical/api-path-walk.txt
··· 1 + Path-Walk API 2 + ============= 3 + 4 + The path-walk API is used to walk reachable objects, but to visit objects 5 + in batches based on a common path they appear in, or by type. 6 + 7 + For example, all reachable commits are visited in a group. All tags are 8 + visited in a group. Then, all root trees are visited. At some point, all 9 + blobs reachable via a path `my/dir/to/A` are visited. When there are 10 + multiple paths possible to reach the same object, then only one of those 11 + paths is used to visit the object. 12 + 13 + Basics 14 + ------ 15 + 16 + To use the path-walk API, include `path-walk.h` and call 17 + `walk_objects_by_path()` with a customized `path_walk_info` struct. The 18 + struct is used to set all of the options for how the walk should proceed. 19 + Let's dig into the different options and their use. 20 + 21 + `path_fn` and `path_fn_data`:: 22 + The most important option is the `path_fn` option, which is a 23 + function pointer to the callback that can execute logic on the 24 + object IDs for objects grouped by type and path. This function 25 + also receives a `data` value that corresponds to the 26 + `path_fn_data` member, for providing custom data structures to 27 + this callback function. 28 + 29 + `revs`:: 30 + To configure the exact details of the reachable set of objects, 31 + use the `revs` member and initialize it using the revision 32 + machinery in `revision.h`. Initialize `revs` using calls such as 33 + `setup_revisions()` or `parse_revision_opt()`. Do not call 34 + `prepare_revision_walk()`, as that will be called within 35 + `walk_objects_by_path()`. 36 + + 37 + It is also important that you do not specify the `--objects` flag for the 38 + `revs` struct. The revision walk should only be used to walk commits, and 39 + the objects will be walked in a separate way based on those starting 40 + commits. 41 + 42 + `commits`, `blobs`, `trees`, `tags`:: 43 + By default, these members are enabled and signal that the path-walk 44 + API should call the `path_fn` on objects of these types. Specialized 45 + applications could disable some options to make it simpler to walk 46 + the objects or to have fewer calls to `path_fn`. 47 + + 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 + 62 + See example usages in: 63 + `t/helper/test-path-walk.c`
+2
Makefile
··· 818 818 TEST_BUILTINS_OBJS += test-parse-pathspec-file.o 819 819 TEST_BUILTINS_OBJS += test-partial-clone.o 820 820 TEST_BUILTINS_OBJS += test-path-utils.o 821 + TEST_BUILTINS_OBJS += test-path-walk.o 821 822 TEST_BUILTINS_OBJS += test-pcre2-config.o 822 823 TEST_BUILTINS_OBJS += test-pkt-line.o 823 824 TEST_BUILTINS_OBJS += test-proc-receive.o ··· 1094 1095 LIB_OBJS += patch-delta.o 1095 1096 LIB_OBJS += patch-ids.o 1096 1097 LIB_OBJS += path.o 1098 + LIB_OBJS += path-walk.o 1097 1099 LIB_OBJS += pathspec.o 1098 1100 LIB_OBJS += pkt-line.o 1099 1101 LIB_OBJS += preload-index.o
+1
meson.build
··· 358 358 'patch-delta.c', 359 359 'patch-ids.c', 360 360 'path.c', 361 + 'path-walk.c', 361 362 'pathspec.c', 362 363 'pkt-line.c', 363 364 'preload-index.c',
+591
path-walk.c
··· 1 + /* 2 + * path-walk.c: implementation for path-based walks of the object graph. 3 + */ 4 + #include "git-compat-util.h" 5 + #include "path-walk.h" 6 + #include "blob.h" 7 + #include "commit.h" 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 "prio-queue.h" 15 + #include "revision.h" 16 + #include "string-list.h" 17 + #include "strmap.h" 18 + #include "tag.h" 19 + #include "trace2.h" 20 + #include "tree.h" 21 + #include "tree-walk.h" 22 + 23 + static const char *root_path = ""; 24 + 25 + struct type_and_oid_list { 26 + enum object_type type; 27 + struct oid_array oids; 28 + int maybe_interesting; 29 + }; 30 + 31 + #define TYPE_AND_OID_LIST_INIT { \ 32 + .type = OBJ_NONE, \ 33 + .oids = OID_ARRAY_INIT \ 34 + } 35 + 36 + struct path_walk_context { 37 + /** 38 + * Repeats of data in 'struct path_walk_info' for 39 + * access with fewer characters. 40 + */ 41 + struct repository *repo; 42 + struct rev_info *revs; 43 + struct path_walk_info *info; 44 + 45 + /** 46 + * Map a path to a 'struct type_and_oid_list' 47 + * containing the objects discovered at that 48 + * path. 49 + */ 50 + struct strmap paths_to_lists; 51 + 52 + /** 53 + * Store the current list of paths in a priority queue, 54 + * using object type as a sorting mechanism, mostly to 55 + * make sure blobs are popped off the stack first. No 56 + * other sort is made, so within each object type it acts 57 + * like a stack and performs a DFS within the trees. 58 + * 59 + * Use path_stack_pushed to indicate whether a path 60 + * was previously added to path_stack. 61 + */ 62 + struct prio_queue path_stack; 63 + struct strset path_stack_pushed; 64 + }; 65 + 66 + static int compare_by_type(const void *one, const void *two, void *cb_data) 67 + { 68 + struct type_and_oid_list *list1, *list2; 69 + const char *str1 = one; 70 + const char *str2 = two; 71 + struct path_walk_context *ctx = cb_data; 72 + 73 + list1 = strmap_get(&ctx->paths_to_lists, str1); 74 + list2 = strmap_get(&ctx->paths_to_lists, str2); 75 + 76 + /* 77 + * If object types are equal, then use path comparison. 78 + */ 79 + if (!list1 || !list2 || list1->type == list2->type) 80 + return strcmp(str1, str2); 81 + 82 + /* Prefer tags to be popped off first. */ 83 + if (list1->type == OBJ_TAG) 84 + return -1; 85 + if (list2->type == OBJ_TAG) 86 + return 1; 87 + 88 + /* Prefer blobs to be popped off second. */ 89 + if (list1->type == OBJ_BLOB) 90 + return -1; 91 + if (list2->type == OBJ_BLOB) 92 + return 1; 93 + 94 + return 0; 95 + } 96 + 97 + static void push_to_stack(struct path_walk_context *ctx, 98 + const char *path) 99 + { 100 + if (strset_contains(&ctx->path_stack_pushed, path)) 101 + return; 102 + 103 + strset_add(&ctx->path_stack_pushed, path); 104 + prio_queue_put(&ctx->path_stack, xstrdup(path)); 105 + } 106 + 107 + static int add_tree_entries(struct path_walk_context *ctx, 108 + const char *base_path, 109 + struct object_id *oid) 110 + { 111 + struct tree_desc desc; 112 + struct name_entry entry; 113 + struct strbuf path = STRBUF_INIT; 114 + size_t base_len; 115 + struct tree *tree = lookup_tree(ctx->repo, oid); 116 + 117 + if (!tree) { 118 + error(_("failed to walk children of tree %s: not found"), 119 + oid_to_hex(oid)); 120 + return -1; 121 + } else if (parse_tree_gently(tree, 1)) { 122 + error("bad tree object %s", oid_to_hex(oid)); 123 + return -1; 124 + } 125 + 126 + strbuf_addstr(&path, base_path); 127 + base_len = path.len; 128 + 129 + init_tree_desc(&desc, &tree->object.oid, tree->buffer, tree->size); 130 + while (tree_entry(&desc, &entry)) { 131 + struct type_and_oid_list *list; 132 + struct object *o; 133 + /* Not actually true, but we will ignore submodules later. */ 134 + enum object_type type = S_ISDIR(entry.mode) ? OBJ_TREE : OBJ_BLOB; 135 + 136 + /* Skip submodules. */ 137 + if (S_ISGITLINK(entry.mode)) 138 + continue; 139 + 140 + /* If the caller doesn't want blobs, then don't bother. */ 141 + if (!ctx->info->blobs && type == OBJ_BLOB) 142 + continue; 143 + 144 + if (type == OBJ_TREE) { 145 + struct tree *child = lookup_tree(ctx->repo, &entry.oid); 146 + o = child ? &child->object : NULL; 147 + } else if (type == OBJ_BLOB) { 148 + struct blob *child = lookup_blob(ctx->repo, &entry.oid); 149 + o = child ? &child->object : NULL; 150 + } else { 151 + BUG("invalid type for tree entry: %d", type); 152 + } 153 + 154 + if (!o) { 155 + error(_("failed to find object %s"), 156 + oid_to_hex(&o->oid)); 157 + return -1; 158 + } 159 + 160 + /* Skip this object if already seen. */ 161 + if (o->flags & SEEN) 162 + continue; 163 + o->flags |= SEEN; 164 + 165 + strbuf_setlen(&path, base_len); 166 + strbuf_add(&path, entry.path, entry.pathlen); 167 + 168 + /* 169 + * Trees will end with "/" for concatenation and distinction 170 + * from blobs at the same path. 171 + */ 172 + if (type == OBJ_TREE) 173 + strbuf_addch(&path, '/'); 174 + 175 + if (!(list = strmap_get(&ctx->paths_to_lists, path.buf))) { 176 + CALLOC_ARRAY(list, 1); 177 + list->type = type; 178 + strmap_put(&ctx->paths_to_lists, path.buf, list); 179 + } 180 + push_to_stack(ctx, path.buf); 181 + 182 + if (!(o->flags & UNINTERESTING)) 183 + list->maybe_interesting = 1; 184 + 185 + oid_array_append(&list->oids, &entry.oid); 186 + } 187 + 188 + free_tree_buffer(tree); 189 + strbuf_release(&path); 190 + return 0; 191 + } 192 + 193 + /* 194 + * For each path in paths_to_explore, walk the trees another level 195 + * and add any found blobs to the batch (but only if they exist and 196 + * haven't been added yet). 197 + */ 198 + static int walk_path(struct path_walk_context *ctx, 199 + const char *path) 200 + { 201 + struct type_and_oid_list *list; 202 + int ret = 0; 203 + 204 + list = strmap_get(&ctx->paths_to_lists, path); 205 + 206 + if (!list) 207 + BUG("provided path '%s' that had no associated list", path); 208 + 209 + if (!list->oids.nr) 210 + return 0; 211 + 212 + if (ctx->info->prune_all_uninteresting) { 213 + /* 214 + * This is true if all objects were UNINTERESTING 215 + * when added to the list. 216 + */ 217 + if (!list->maybe_interesting) 218 + return 0; 219 + 220 + /* 221 + * But it's still possible that the objects were set 222 + * as UNINTERESTING after being added. Do a quick check. 223 + */ 224 + list->maybe_interesting = 0; 225 + for (size_t i = 0; 226 + !list->maybe_interesting && i < list->oids.nr; 227 + i++) { 228 + if (list->type == OBJ_TREE) { 229 + struct tree *t = lookup_tree(ctx->repo, 230 + &list->oids.oid[i]); 231 + if (t && !(t->object.flags & UNINTERESTING)) 232 + list->maybe_interesting = 1; 233 + } else if (list->type == OBJ_BLOB) { 234 + struct blob *b = lookup_blob(ctx->repo, 235 + &list->oids.oid[i]); 236 + if (b && !(b->object.flags & UNINTERESTING)) 237 + list->maybe_interesting = 1; 238 + } else { 239 + /* Tags are always interesting if visited. */ 240 + list->maybe_interesting = 1; 241 + } 242 + } 243 + 244 + /* We have confirmed that all objects are UNINTERESTING. */ 245 + if (!list->maybe_interesting) 246 + return 0; 247 + } 248 + 249 + /* Evaluate function pointer on this data, if requested. */ 250 + if ((list->type == OBJ_TREE && ctx->info->trees) || 251 + (list->type == OBJ_BLOB && ctx->info->blobs) || 252 + (list->type == OBJ_TAG && ctx->info->tags)) 253 + ret = ctx->info->path_fn(path, &list->oids, list->type, 254 + ctx->info->path_fn_data); 255 + 256 + /* Expand data for children. */ 257 + if (list->type == OBJ_TREE) { 258 + for (size_t i = 0; i < list->oids.nr; i++) { 259 + ret |= add_tree_entries(ctx, 260 + path, 261 + &list->oids.oid[i]); 262 + } 263 + } 264 + 265 + oid_array_clear(&list->oids); 266 + strmap_remove(&ctx->paths_to_lists, path, 1); 267 + return ret; 268 + } 269 + 270 + static void clear_paths_to_lists(struct strmap *map) 271 + { 272 + struct hashmap_iter iter; 273 + struct strmap_entry *e; 274 + 275 + hashmap_for_each_entry(&map->map, &iter, e, ent) { 276 + struct type_and_oid_list *list = e->value; 277 + oid_array_clear(&list->oids); 278 + } 279 + strmap_clear(map, 1); 280 + strmap_init(map); 281 + } 282 + 283 + static struct repository *edge_repo; 284 + static struct type_and_oid_list *edge_tree_list; 285 + 286 + static void show_edge(struct commit *commit) 287 + { 288 + struct tree *t = repo_get_commit_tree(edge_repo, commit); 289 + 290 + if (!t) 291 + return; 292 + 293 + if (commit->object.flags & UNINTERESTING) 294 + t->object.flags |= UNINTERESTING; 295 + 296 + if (t->object.flags & SEEN) 297 + return; 298 + t->object.flags |= SEEN; 299 + 300 + oid_array_append(&edge_tree_list->oids, &t->object.oid); 301 + } 302 + 303 + static int setup_pending_objects(struct path_walk_info *info, 304 + struct path_walk_context *ctx) 305 + { 306 + struct type_and_oid_list *tags = NULL; 307 + struct type_and_oid_list *tagged_blobs = NULL; 308 + struct type_and_oid_list *root_tree_list = NULL; 309 + 310 + if (info->tags) 311 + CALLOC_ARRAY(tags, 1); 312 + if (info->blobs) 313 + CALLOC_ARRAY(tagged_blobs, 1); 314 + if (info->trees) 315 + root_tree_list = strmap_get(&ctx->paths_to_lists, root_path); 316 + 317 + /* 318 + * Pending objects include: 319 + * * Commits at branch tips. 320 + * * Annotated tags at tag tips. 321 + * * Any kind of object at lightweight tag tips. 322 + * * Trees and blobs in the index (with an associated path). 323 + */ 324 + for (size_t i = 0; i < info->revs->pending.nr; i++) { 325 + struct object_array_entry *pending = info->revs->pending.objects + i; 326 + struct object *obj = pending->item; 327 + 328 + /* Commits will be picked up by revision walk. */ 329 + if (obj->type == OBJ_COMMIT) 330 + continue; 331 + 332 + /* Navigate annotated tag object chains. */ 333 + while (obj->type == OBJ_TAG) { 334 + struct tag *tag = lookup_tag(info->revs->repo, &obj->oid); 335 + if (!tag) { 336 + error(_("failed to find tag %s"), 337 + oid_to_hex(&obj->oid)); 338 + return -1; 339 + } 340 + if (tag->object.flags & SEEN) 341 + break; 342 + tag->object.flags |= SEEN; 343 + 344 + if (tags) 345 + oid_array_append(&tags->oids, &obj->oid); 346 + obj = tag->tagged; 347 + } 348 + 349 + if (obj->type == OBJ_TAG) 350 + continue; 351 + 352 + /* We are now at a non-tag object. */ 353 + if (obj->flags & SEEN) 354 + continue; 355 + obj->flags |= SEEN; 356 + 357 + switch (obj->type) { 358 + case OBJ_TREE: 359 + if (!info->trees) 360 + continue; 361 + if (pending->path) { 362 + struct type_and_oid_list *list; 363 + char *path = *pending->path ? xstrfmt("%s/", pending->path) 364 + : xstrdup(""); 365 + if (!(list = strmap_get(&ctx->paths_to_lists, path))) { 366 + CALLOC_ARRAY(list, 1); 367 + list->type = OBJ_TREE; 368 + strmap_put(&ctx->paths_to_lists, path, list); 369 + } 370 + oid_array_append(&list->oids, &obj->oid); 371 + free(path); 372 + } else { 373 + /* assume a root tree, such as a lightweight tag. */ 374 + oid_array_append(&root_tree_list->oids, &obj->oid); 375 + } 376 + break; 377 + 378 + case OBJ_BLOB: 379 + if (!info->blobs) 380 + continue; 381 + if (pending->path) { 382 + struct type_and_oid_list *list; 383 + char *path = pending->path; 384 + if (!(list = strmap_get(&ctx->paths_to_lists, path))) { 385 + CALLOC_ARRAY(list, 1); 386 + list->type = OBJ_BLOB; 387 + strmap_put(&ctx->paths_to_lists, path, list); 388 + } 389 + oid_array_append(&list->oids, &obj->oid); 390 + } else { 391 + /* assume a root tree, such as a lightweight tag. */ 392 + oid_array_append(&tagged_blobs->oids, &obj->oid); 393 + } 394 + break; 395 + 396 + case OBJ_COMMIT: 397 + /* Make sure it is in the object walk */ 398 + if (obj != pending->item) 399 + add_pending_object(info->revs, obj, ""); 400 + break; 401 + 402 + default: 403 + BUG("should not see any other type here"); 404 + } 405 + } 406 + 407 + /* 408 + * Add tag objects and tagged blobs if they exist. 409 + */ 410 + if (tagged_blobs) { 411 + if (tagged_blobs->oids.nr) { 412 + const char *tagged_blob_path = "/tagged-blobs"; 413 + tagged_blobs->type = OBJ_BLOB; 414 + tagged_blobs->maybe_interesting = 1; 415 + strmap_put(&ctx->paths_to_lists, tagged_blob_path, tagged_blobs); 416 + push_to_stack(ctx, tagged_blob_path); 417 + } else { 418 + oid_array_clear(&tagged_blobs->oids); 419 + free(tagged_blobs); 420 + } 421 + } 422 + if (tags) { 423 + if (tags->oids.nr) { 424 + const char *tag_path = "/tags"; 425 + tags->type = OBJ_TAG; 426 + tags->maybe_interesting = 1; 427 + strmap_put(&ctx->paths_to_lists, tag_path, tags); 428 + push_to_stack(ctx, tag_path); 429 + } else { 430 + oid_array_clear(&tags->oids); 431 + free(tags); 432 + } 433 + } 434 + 435 + return 0; 436 + } 437 + 438 + /** 439 + * Given the configuration of 'info', walk the commits based on 'info->revs' and 440 + * call 'info->path_fn' on each discovered path. 441 + * 442 + * Returns nonzero on an error. 443 + */ 444 + int walk_objects_by_path(struct path_walk_info *info) 445 + { 446 + int ret; 447 + size_t commits_nr = 0, paths_nr = 0; 448 + struct commit *c; 449 + struct type_and_oid_list *root_tree_list; 450 + struct type_and_oid_list *commit_list; 451 + struct path_walk_context ctx = { 452 + .repo = info->revs->repo, 453 + .revs = info->revs, 454 + .info = info, 455 + .path_stack = { 456 + .compare = compare_by_type, 457 + .cb_data = &ctx 458 + }, 459 + .path_stack_pushed = STRSET_INIT, 460 + .paths_to_lists = STRMAP_INIT 461 + }; 462 + 463 + trace2_region_enter("path-walk", "commit-walk", info->revs->repo); 464 + 465 + CALLOC_ARRAY(commit_list, 1); 466 + commit_list->type = OBJ_COMMIT; 467 + 468 + if (info->tags) 469 + info->revs->tag_objects = 1; 470 + 471 + /* Insert a single list for the root tree into the paths. */ 472 + CALLOC_ARRAY(root_tree_list, 1); 473 + root_tree_list->type = OBJ_TREE; 474 + root_tree_list->maybe_interesting = 1; 475 + strmap_put(&ctx.paths_to_lists, root_path, root_tree_list); 476 + push_to_stack(&ctx, root_path); 477 + 478 + /* 479 + * Set these values before preparing the walk to catch 480 + * lightweight tags pointing to non-commits and indexed objects. 481 + */ 482 + info->revs->blob_objects = info->blobs; 483 + info->revs->tree_objects = info->trees; 484 + 485 + if (prepare_revision_walk(info->revs)) 486 + die(_("failed to setup revision walk")); 487 + 488 + /* Walk trees to mark them as UNINTERESTING. */ 489 + edge_repo = info->revs->repo; 490 + edge_tree_list = root_tree_list; 491 + mark_edges_uninteresting(info->revs, show_edge, 492 + info->prune_all_uninteresting); 493 + edge_repo = NULL; 494 + edge_tree_list = NULL; 495 + 496 + info->revs->blob_objects = info->revs->tree_objects = 0; 497 + 498 + trace2_region_enter("path-walk", "pending-walk", info->revs->repo); 499 + ret = setup_pending_objects(info, &ctx); 500 + trace2_region_leave("path-walk", "pending-walk", info->revs->repo); 501 + 502 + if (ret) 503 + return ret; 504 + 505 + while ((c = get_revision(info->revs))) { 506 + struct object_id *oid; 507 + struct tree *t; 508 + commits_nr++; 509 + 510 + if (info->commits) 511 + oid_array_append(&commit_list->oids, 512 + &c->object.oid); 513 + 514 + /* If we only care about commits, then skip trees. */ 515 + if (!info->trees && !info->blobs) 516 + continue; 517 + 518 + oid = get_commit_tree_oid(c); 519 + t = lookup_tree(info->revs->repo, oid); 520 + 521 + if (!t) { 522 + error("could not find tree %s", oid_to_hex(oid)); 523 + return -1; 524 + } 525 + 526 + if (t->object.flags & SEEN) 527 + continue; 528 + t->object.flags |= SEEN; 529 + oid_array_append(&root_tree_list->oids, oid); 530 + } 531 + 532 + trace2_data_intmax("path-walk", ctx.repo, "commits", commits_nr); 533 + trace2_region_leave("path-walk", "commit-walk", info->revs->repo); 534 + 535 + /* Track all commits. */ 536 + if (info->commits && commit_list->oids.nr) 537 + ret = info->path_fn("", &commit_list->oids, OBJ_COMMIT, 538 + info->path_fn_data); 539 + oid_array_clear(&commit_list->oids); 540 + free(commit_list); 541 + 542 + trace2_region_enter("path-walk", "path-walk", info->revs->repo); 543 + while (!ret && ctx.path_stack.nr) { 544 + char *path = prio_queue_get(&ctx.path_stack); 545 + paths_nr++; 546 + 547 + ret = walk_path(&ctx, path); 548 + 549 + free(path); 550 + } 551 + 552 + /* Are there paths remaining? Likely they are from indexed objects. */ 553 + if (!strmap_empty(&ctx.paths_to_lists)) { 554 + struct hashmap_iter iter; 555 + struct strmap_entry *entry; 556 + 557 + strmap_for_each_entry(&ctx.paths_to_lists, &iter, entry) 558 + push_to_stack(&ctx, entry->key); 559 + 560 + while (!ret && ctx.path_stack.nr) { 561 + char *path = prio_queue_get(&ctx.path_stack); 562 + paths_nr++; 563 + 564 + ret = walk_path(&ctx, path); 565 + 566 + free(path); 567 + } 568 + } 569 + 570 + trace2_data_intmax("path-walk", ctx.repo, "paths", paths_nr); 571 + trace2_region_leave("path-walk", "path-walk", info->revs->repo); 572 + 573 + clear_paths_to_lists(&ctx.paths_to_lists); 574 + strset_clear(&ctx.path_stack_pushed); 575 + clear_prio_queue(&ctx.path_stack); 576 + return ret; 577 + } 578 + 579 + void path_walk_info_init(struct path_walk_info *info) 580 + { 581 + struct path_walk_info empty = PATH_WALK_INFO_INIT; 582 + memcpy(info, &empty, sizeof(empty)); 583 + } 584 + 585 + void path_walk_info_clear(struct path_walk_info *info UNUSED) 586 + { 587 + /* 588 + * This destructor is empty for now, as info->revs 589 + * is not owned by 'struct path_walk_info'. 590 + */ 591 + }
+69
path-walk.h
··· 1 + /* 2 + * path-walk.h : Methods and structures for walking the object graph in batches 3 + * by the paths that can reach those objects. 4 + */ 5 + #include "object.h" /* Required for 'enum object_type'. */ 6 + 7 + struct rev_info; 8 + struct oid_array; 9 + 10 + /** 11 + * The type of a function pointer for the method that is called on a list of 12 + * objects reachable at a given path. 13 + */ 14 + typedef int (*path_fn)(const char *path, 15 + struct oid_array *oids, 16 + enum object_type type, 17 + void *data); 18 + 19 + struct path_walk_info { 20 + /** 21 + * revs provides the definitions for the commit walk, including 22 + * which commits are UNINTERESTING or not. This structure is 23 + * expected to be owned by the caller. 24 + */ 25 + struct rev_info *revs; 26 + 27 + /** 28 + * The caller wishes to execute custom logic on objects reachable at a 29 + * given path. Every reachable object will be visited exactly once, and 30 + * the first path to see an object wins. This may not be a stable choice. 31 + */ 32 + path_fn path_fn; 33 + void *path_fn_data; 34 + 35 + /** 36 + * Initialize which object types the path_fn should be called on. This 37 + * could also limit the walk to skip blobs if not set. 38 + */ 39 + int commits; 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 { \ 54 + .blobs = 1, \ 55 + .trees = 1, \ 56 + .commits = 1, \ 57 + .tags = 1, \ 58 + } 59 + 60 + void path_walk_info_init(struct path_walk_info *info); 61 + void path_walk_info_clear(struct path_walk_info *info); 62 + 63 + /** 64 + * Given the configuration of 'info', walk the commits based on 'info->revs' and 65 + * call 'info->path_fn' on each discovered path. 66 + * 67 + * Returns nonzero on an error. 68 + */ 69 + int walk_objects_by_path(struct path_walk_info *info);
+1
t/helper/meson.build
··· 40 40 'test-parse-pathspec-file.c', 41 41 'test-partial-clone.c', 42 42 'test-path-utils.c', 43 + 'test-path-walk.c', 43 44 'test-pcre2-config.c', 44 45 'test-pkt-line.c', 45 46 'test-proc-receive.c',
+112
t/helper/test-path-walk.c
··· 1 + #define USE_THE_REPOSITORY_VARIABLE 2 + 3 + #include "test-tool.h" 4 + #include "environment.h" 5 + #include "hex.h" 6 + #include "object-name.h" 7 + #include "object.h" 8 + #include "pretty.h" 9 + #include "revision.h" 10 + #include "setup.h" 11 + #include "parse-options.h" 12 + #include "path-walk.h" 13 + #include "oid-array.h" 14 + 15 + static const char * const path_walk_usage[] = { 16 + N_("test-tool path-walk <options> -- <revision-options>"), 17 + NULL 18 + }; 19 + 20 + struct path_walk_test_data { 21 + uintmax_t batch_nr; 22 + 23 + uintmax_t commit_nr; 24 + uintmax_t tree_nr; 25 + uintmax_t blob_nr; 26 + uintmax_t tag_nr; 27 + }; 28 + 29 + static int emit_block(const char *path, struct oid_array *oids, 30 + enum object_type type, void *data) 31 + { 32 + struct path_walk_test_data *tdata = data; 33 + const char *typestr; 34 + 35 + if (type == OBJ_TREE) 36 + tdata->tree_nr += oids->nr; 37 + else if (type == OBJ_BLOB) 38 + tdata->blob_nr += oids->nr; 39 + else if (type == OBJ_COMMIT) 40 + tdata->commit_nr += oids->nr; 41 + else if (type == OBJ_TAG) 42 + tdata->tag_nr += oids->nr; 43 + else 44 + BUG("we do not understand this type"); 45 + 46 + typestr = type_name(type); 47 + 48 + /* This should never be output during tests. */ 49 + if (!oids->nr) 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 + 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, 58 + oid_to_hex(&oids->oid[i]), 59 + o->flags & UNINTERESTING ? ":UNINTERESTING" : ""); 60 + } 61 + 62 + tdata->batch_nr++; 63 + return 0; 64 + } 65 + 66 + int cmd__path_walk(int argc, const char **argv) 67 + { 68 + int res; 69 + struct rev_info revs = REV_INFO_INIT; 70 + struct path_walk_info info = PATH_WALK_INFO_INIT; 71 + struct path_walk_test_data data = { 0 }; 72 + struct option options[] = { 73 + OPT_BOOL(0, "blobs", &info.blobs, 74 + N_("toggle inclusion of blob objects")), 75 + OPT_BOOL(0, "commits", &info.commits, 76 + N_("toggle inclusion of commit objects")), 77 + OPT_BOOL(0, "tags", &info.tags, 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 + 86 + setup_git_directory(); 87 + revs.repo = the_repository; 88 + 89 + argc = parse_options(argc, argv, NULL, 90 + options, path_walk_usage, 91 + PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_ARGV0); 92 + 93 + if (argc > 1) 94 + setup_revisions(argc, argv, &revs, NULL); 95 + else 96 + usage(path_walk_usage[0]); 97 + 98 + info.revs = &revs; 99 + info.path_fn = emit_block; 100 + info.path_fn_data = &data; 101 + 102 + res = walk_objects_by_path(&info); 103 + 104 + printf("commits:%" PRIuMAX "\n" 105 + "trees:%" PRIuMAX "\n" 106 + "blobs:%" PRIuMAX "\n" 107 + "tags:%" PRIuMAX "\n", 108 + data.commit_nr, data.tree_nr, data.blob_nr, data.tag_nr); 109 + 110 + release_revisions(&revs); 111 + return res; 112 + }
+1
t/helper/test-tool.c
··· 52 52 { "parse-subcommand", cmd__parse_subcommand }, 53 53 { "partial-clone", cmd__partial_clone }, 54 54 { "path-utils", cmd__path_utils }, 55 + { "path-walk", cmd__path_walk }, 55 56 { "pcre2-config", cmd__pcre2_config }, 56 57 { "pkt-line", cmd__pkt_line }, 57 58 { "proc-receive", cmd__proc_receive },
+1
t/helper/test-tool.h
··· 45 45 int cmd__parse_subcommand(int argc, const char **argv); 46 46 int cmd__partial_clone(int argc, const char **argv); 47 47 int cmd__path_utils(int argc, const char **argv); 48 + int cmd__path_walk(int argc, const char **argv); 48 49 int cmd__pcre2_config(int argc, const char **argv); 49 50 int cmd__pkt_line(int argc, const char **argv); 50 51 int cmd__proc_receive(int argc, const char **argv);
+1
t/meson.build
··· 829 829 't6500-gc.sh', 830 830 't6501-freshen-objects.sh', 831 831 't6600-test-reach.sh', 832 + 't6601-path-walk.sh', 832 833 't6700-tree-depth.sh', 833 834 't7001-mv.sh', 834 835 't7002-mv-sparse-checkout.sh',
+368
t/t6601-path-walk.sh
··· 1 + #!/bin/sh 2 + 3 + TEST_PASSES_SANITIZE_LEAK=true 4 + 5 + test_description='direct path-walk API tests' 6 + 7 + . ./test-lib.sh 8 + 9 + test_expect_success 'setup test repository' ' 10 + git checkout -b base && 11 + 12 + # Make some objects that will only be reachable 13 + # via non-commit tags. 14 + mkdir child && 15 + echo file >child/file && 16 + git add child && 17 + git commit -m "will abandon" && 18 + git tag -a -m "tree" tree-tag HEAD^{tree} && 19 + echo file2 >file2 && 20 + git add file2 && 21 + git commit --amend -m "will abandon" && 22 + git tag tree-tag2 HEAD^{tree} && 23 + 24 + echo blob >file && 25 + blob_oid=$(git hash-object -t blob -w --stdin <file) && 26 + git tag -a -m "blob" blob-tag "$blob_oid" && 27 + echo blob2 >file2 && 28 + blob2_oid=$(git hash-object -t blob -w --stdin <file2) && 29 + git tag blob-tag2 "$blob2_oid" && 30 + 31 + rm -fr child file file2 && 32 + 33 + mkdir left && 34 + mkdir right && 35 + echo a >a && 36 + echo b >left/b && 37 + echo c >right/c && 38 + git add . && 39 + git commit --amend -m "first" && 40 + git tag -m "first" first HEAD && 41 + 42 + echo d >right/d && 43 + git add right && 44 + git commit -m "second" && 45 + git tag -a -m "second (under)" second.1 HEAD && 46 + git tag -a -m "second (top)" second.2 second.1 && 47 + 48 + # Set up file/dir collision in history. 49 + rm a && 50 + mkdir a && 51 + echo a >a/a && 52 + echo bb >left/b && 53 + git add a left && 54 + git commit -m "third" && 55 + git tag -a -m "third" third && 56 + 57 + git checkout -b topic HEAD~1 && 58 + echo cc >right/c && 59 + git commit -a -m "topic" && 60 + git tag -a -m "fourth" fourth 61 + ' 62 + 63 + test_expect_success 'all' ' 64 + test-tool path-walk -- --all >out && 65 + 66 + cat >expect <<-EOF && 67 + 0:commit::$(git rev-parse topic) 68 + 0:commit::$(git rev-parse base) 69 + 0:commit::$(git rev-parse base~1) 70 + 0:commit::$(git rev-parse base~2) 71 + 1:tag:/tags:$(git rev-parse refs/tags/first) 72 + 1:tag:/tags:$(git rev-parse refs/tags/second.1) 73 + 1:tag:/tags:$(git rev-parse refs/tags/second.2) 74 + 1:tag:/tags:$(git rev-parse refs/tags/third) 75 + 1:tag:/tags:$(git rev-parse refs/tags/fourth) 76 + 1:tag:/tags:$(git rev-parse refs/tags/tree-tag) 77 + 1:tag:/tags:$(git rev-parse refs/tags/blob-tag) 78 + 2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{}) 79 + 2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{}) 80 + 3:tree::$(git rev-parse topic^{tree}) 81 + 3:tree::$(git rev-parse base^{tree}) 82 + 3:tree::$(git rev-parse base~1^{tree}) 83 + 3:tree::$(git rev-parse base~2^{tree}) 84 + 3:tree::$(git rev-parse refs/tags/tree-tag^{}) 85 + 3:tree::$(git rev-parse refs/tags/tree-tag2^{}) 86 + 4:blob:a:$(git rev-parse base~2:a) 87 + 5:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2) 88 + 6:tree:a/:$(git rev-parse base:a) 89 + 7:tree:child/:$(git rev-parse refs/tags/tree-tag:child) 90 + 8:blob:child/file:$(git rev-parse refs/tags/tree-tag:child/file) 91 + 9:tree:left/:$(git rev-parse base:left) 92 + 9:tree:left/:$(git rev-parse base~2:left) 93 + 10:blob:left/b:$(git rev-parse base~2:left/b) 94 + 10:blob:left/b:$(git rev-parse base:left/b) 95 + 11:tree:right/:$(git rev-parse topic:right) 96 + 11:tree:right/:$(git rev-parse base~1:right) 97 + 11:tree:right/:$(git rev-parse base~2:right) 98 + 12:blob:right/c:$(git rev-parse base~2:right/c) 99 + 12:blob:right/c:$(git rev-parse topic:right/c) 100 + 13:blob:right/d:$(git rev-parse base~1:right/d) 101 + blobs:10 102 + commits:4 103 + tags:7 104 + trees:13 105 + EOF 106 + 107 + test_cmp_sorted expect out 108 + ' 109 + 110 + test_expect_success 'indexed objects' ' 111 + test_when_finished git reset --hard && 112 + 113 + # stage change into index, adding a blob but 114 + # also invalidating the cache-tree for the root 115 + # and the "left" directory. 116 + echo bogus >left/c && 117 + git add left && 118 + 119 + test-tool path-walk -- --indexed-objects >out && 120 + 121 + cat >expect <<-EOF && 122 + 0:blob:a:$(git rev-parse HEAD:a) 123 + 1:blob:left/b:$(git rev-parse HEAD:left/b) 124 + 2:blob:left/c:$(git rev-parse :left/c) 125 + 3:blob:right/c:$(git rev-parse HEAD:right/c) 126 + 4:blob:right/d:$(git rev-parse HEAD:right/d) 127 + 5:tree:right/:$(git rev-parse topic:right) 128 + blobs:5 129 + commits:0 130 + tags:0 131 + trees:1 132 + EOF 133 + 134 + test_cmp_sorted expect out 135 + ' 136 + 137 + test_expect_success 'branches and indexed objects mix well' ' 138 + test_when_finished git reset --hard && 139 + 140 + # stage change into index, adding a blob but 141 + # also invalidating the cache-tree for the root 142 + # and the "right" directory. 143 + echo fake >right/d && 144 + git add right && 145 + 146 + test-tool path-walk -- --indexed-objects --branches >out && 147 + 148 + cat >expect <<-EOF && 149 + 0:commit::$(git rev-parse topic) 150 + 0:commit::$(git rev-parse base) 151 + 0:commit::$(git rev-parse base~1) 152 + 0:commit::$(git rev-parse base~2) 153 + 1:tree::$(git rev-parse topic^{tree}) 154 + 1:tree::$(git rev-parse base^{tree}) 155 + 1:tree::$(git rev-parse base~1^{tree}) 156 + 1:tree::$(git rev-parse base~2^{tree}) 157 + 2:tree:a/:$(git rev-parse refs/tags/third:a) 158 + 3:tree:left/:$(git rev-parse base:left) 159 + 3:tree:left/:$(git rev-parse base~2:left) 160 + 4:blob:left/b:$(git rev-parse base:left/b) 161 + 4:blob:left/b:$(git rev-parse base~2:left/b) 162 + 5:tree:right/:$(git rev-parse topic:right) 163 + 5:tree:right/:$(git rev-parse base~1:right) 164 + 5:tree:right/:$(git rev-parse base~2:right) 165 + 6:blob:right/c:$(git rev-parse base~2:right/c) 166 + 6:blob:right/c:$(git rev-parse topic:right/c) 167 + 7:blob:right/d:$(git rev-parse base~1:right/d) 168 + 7:blob:right/d:$(git rev-parse :right/d) 169 + 8:blob:a:$(git rev-parse base~2:a) 170 + blobs:7 171 + commits:4 172 + tags:0 173 + trees:10 174 + EOF 175 + 176 + test_cmp_sorted expect out 177 + ' 178 + 179 + test_expect_success 'topic only' ' 180 + test-tool path-walk -- topic >out && 181 + 182 + cat >expect <<-EOF && 183 + 0:commit::$(git rev-parse topic) 184 + 0:commit::$(git rev-parse base~1) 185 + 0:commit::$(git rev-parse base~2) 186 + 1:tree::$(git rev-parse topic^{tree}) 187 + 1:tree::$(git rev-parse base~1^{tree}) 188 + 1:tree::$(git rev-parse base~2^{tree}) 189 + 2:blob:a:$(git rev-parse base~2:a) 190 + 3:tree:left/:$(git rev-parse base~2:left) 191 + 4:blob:left/b:$(git rev-parse base~2:left/b) 192 + 5:tree:right/:$(git rev-parse topic:right) 193 + 5:tree:right/:$(git rev-parse base~1:right) 194 + 5:tree:right/:$(git rev-parse base~2:right) 195 + 6:blob:right/c:$(git rev-parse base~2:right/c) 196 + 6:blob:right/c:$(git rev-parse topic:right/c) 197 + 7:blob:right/d:$(git rev-parse base~1:right/d) 198 + blobs:5 199 + commits:3 200 + tags:0 201 + trees:7 202 + EOF 203 + 204 + test_cmp_sorted expect out 205 + ' 206 + 207 + test_expect_success 'topic, not base' ' 208 + test-tool path-walk -- topic --not base >out && 209 + 210 + cat >expect <<-EOF && 211 + 0:commit::$(git rev-parse topic) 212 + 1:tree::$(git rev-parse topic^{tree}) 213 + 2:blob:a:$(git rev-parse topic:a):UNINTERESTING 214 + 3:tree:left/:$(git rev-parse topic:left):UNINTERESTING 215 + 4:blob:left/b:$(git rev-parse topic:left/b):UNINTERESTING 216 + 5:tree:right/:$(git rev-parse topic:right) 217 + 6:blob:right/c:$(git rev-parse topic:right/c) 218 + 7:blob:right/d:$(git rev-parse topic:right/d):UNINTERESTING 219 + blobs:4 220 + commits:1 221 + tags:0 222 + trees:3 223 + EOF 224 + 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:blob:a:$(git rev-parse base~1:a):UNINTERESTING 237 + 5:tree:left/:$(git rev-parse base~1:left):UNINTERESTING 238 + 6:blob:left/b:$(git rev-parse base~1:left/b):UNINTERESTING 239 + 7:tree:right/:$(git rev-parse topic:right) 240 + 8:blob:right/c:$(git rev-parse topic:right/c) 241 + 9:blob:right/d:$(git rev-parse base~1:right/d):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 && 256 + 0:blob:a:$(git rev-parse topic:a):UNINTERESTING 257 + 1:blob:left/b:$(git rev-parse topic:left/b):UNINTERESTING 258 + 2:blob:right/c:$(git rev-parse topic:right/c) 259 + 3:blob:right/d:$(git rev-parse topic:right/d):UNINTERESTING 260 + blobs:4 261 + commits:0 262 + tags:0 263 + trees:0 264 + EOF 265 + 266 + test_cmp_sorted expect out 267 + ' 268 + 269 + # No, this doesn't make a lot of sense for the path-walk API, 270 + # but it is possible to do. 271 + test_expect_success 'topic, not base, only commits' ' 272 + test-tool path-walk --no-blobs --no-trees \ 273 + -- topic --not base >out && 274 + 275 + cat >expect <<-EOF && 276 + 0:commit::$(git rev-parse topic) 277 + commits:1 278 + blobs:0 279 + tags:0 280 + trees:0 281 + EOF 282 + 283 + test_cmp_sorted expect out 284 + ' 285 + 286 + test_expect_success 'topic, not base, only trees' ' 287 + test-tool path-walk --no-blobs --no-commits \ 288 + -- topic --not base >out && 289 + 290 + cat >expect <<-EOF && 291 + 0:tree::$(git rev-parse topic^{tree}) 292 + 1:tree:left/:$(git rev-parse topic:left):UNINTERESTING 293 + 2:tree:right/:$(git rev-parse topic:right) 294 + commits:0 295 + blobs:0 296 + tags:0 297 + trees:3 298 + EOF 299 + 300 + test_cmp_sorted expect out 301 + ' 302 + 303 + test_expect_success 'topic, not base, boundary' ' 304 + test-tool path-walk -- --boundary topic --not base >out && 305 + 306 + cat >expect <<-EOF && 307 + 0:commit::$(git rev-parse topic) 308 + 0:commit::$(git rev-parse base~1):UNINTERESTING 309 + 1:tree::$(git rev-parse topic^{tree}) 310 + 1:tree::$(git rev-parse base~1^{tree}):UNINTERESTING 311 + 2:blob:a:$(git rev-parse base~1:a):UNINTERESTING 312 + 3:tree:left/:$(git rev-parse base~1:left):UNINTERESTING 313 + 4:blob:left/b:$(git rev-parse base~1:left/b):UNINTERESTING 314 + 5:tree:right/:$(git rev-parse topic:right) 315 + 5:tree:right/:$(git rev-parse base~1:right):UNINTERESTING 316 + 6:blob:right/c:$(git rev-parse base~1:right/c):UNINTERESTING 317 + 6:blob:right/c:$(git rev-parse topic:right/c) 318 + 7:blob:right/d:$(git rev-parse base~1:right/d):UNINTERESTING 319 + blobs:5 320 + commits:2 321 + tags:0 322 + trees:5 323 + EOF 324 + 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 && 352 + ( 353 + cd unique-trees && 354 + mkdir initial && 355 + test_commit initial/file && 356 + git switch -c move-to-top && 357 + git mv initial/file.t ./ && 358 + test_tick && 359 + git commit -m moved && 360 + git update-ref refs/heads/other HEAD 361 + ) && 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 && 365 + test_line_count = 1 out-filtered 366 + ' 367 + 368 + test_done
+10
t/test-lib-functions.sh
··· 1268 1268 eval "$GIT_TEST_CMP" '"$@"' 1269 1269 } 1270 1270 1271 + # test_cmp_sorted runs test_cmp on sorted versions of the two 1272 + # input files. Uses "$1.sorted" and "$2.sorted" as temp files. 1273 + 1274 + test_cmp_sorted () { 1275 + sort <"$1" >"$1.sorted" && 1276 + sort <"$2" >"$2.sorted" && 1277 + test_cmp "$1.sorted" "$2.sorted" && 1278 + rm "$1.sorted" "$2.sorted" 1279 + } 1280 + 1271 1281 # Check that the given config key has the expected value. 1272 1282 # 1273 1283 # test_cmp_config [-C <dir>] <expected-value>