{"thread":{"id":"62282","subject":"[PATCH 00/17] pack-objects: add --path-walk option for better deltas","startedAt":"2024-10-08T14:12:07Z","lastAt":"2024-10-31T21:07:53Z","messageCount":55,"participants":["Derrick Stolee via GitGitGadget","Taylor Blau","Derrick Stolee","Patrick Steinhardt","Jonathan Tan"],"isPatch":true,"patchVersion":1,"patchTotal":17},"messages":[{"id":"504426","messageId":"pull.1813.git.1728396723.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":null,"subject":"[PATCH 00/17] pack-objects: add --path-walk option for better deltas","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:11:46Z","receivedAt":"2024-10-08T14:12:07Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"This is a reviewable series introducing the path-walk API and its\napplication within the 'git pack-objects --path-walk' machinery. This API\nwas previously discussed in the path-walk RFC [1] and the patch series\naround the --full-name-hash option (branch ds/pack-name-hash-tweak) [2].\nThis series conflicts with ds/pack-name-hash-tweak, but that was on hold\nbecause it did not seem as critical based on community interest.\n\n[1]\nhttps://lore.kernel.org/git/pull.1786.git.1725935335.gitgitgadget@gmail.com\n\n[2]\nhttps://lore.kernel.org/git/pull.1785.git.1725890210.gitgitgadget@gmail.com\n\nThe primary motivation for this feature is its use to shrink the packfile\ncreated by 'git push' when there are many name-hash collisions. This need\nwas discovered in several Javascript repositories that use the beachball\ntool [3] to generate CHANGELOG.md and CHANGELOG.json files. When a batch of\nthese files are created at the same time and pushed to a release branch, the\n'git pack-objects' process has many collisions among these files and delta\nbases are selected poorly.\n\n[3] https://github.com/microsoft/beachball\n\nIn some cases, 'git push' is pushing 60-100 MB when the new path-walk\nalgorithm will identify better delta bases and pack the same objects into a\nthin pack less than 1 MB. This was the most extreme example we could find\nand is present in a private monorepo. However, the microsoft/fluentui repo\n[4] is a good example for demonstrating similar improvements. The patch\ndescriptions frequently refer to this repo and which commit should be\nchecked out to reproduce this behavior.\n\n[4] https://github.com/microsoft/fluentui\n\nThe path-walk API is a new way to walk objects. Traditionally, the revision\nAPI walks objects by visiting a commit, then visiting its root tree and\nrecursing through trees to visit reachable objects that were not previously\nvisited. The path-walk API visits objects in batches based on the path\nwalked from a root tree to that object. (Only the first discovered path is\nchosen; this avoids certain kinds of Git bombs.)\n\nThis has an immediate application to 'git pack-objects'.\n\nWhen using the traditional revision API to walk objects, each object is\nemitted with an associated path. Since this path may appear for many objects\nspread across the full list, a heuristic is used: the \"name-hash\" is stored\nfor that object instead of the full path name. This name-hash will group\nobjects at the same path together, but also has a form of \"locality\" to\ngroup likely-similar objects together. When there are few collisions in the\nname-hash function, this helps compress objects that appear at the same path\nas well as help compress objects across different paths that have similar\nsuffixes. When there are many versions of the same path, then finding delta\nbases across that family of objects is very important. When there are few\nversions of the same path, then finding cross-name delta bases is also\nimportant. The former is important for clones and repacks while the latter\nis important for shallow clones. They can both be important for pushes. In\nall cases, this approach is fraught when there are many name-hash collisions\nas the window size becomes a limiting factor for finding quality delta\nbases.\n\nWhen using the path-walk API to walk objects, we group objects by the same\npath from the start. We don't need to store the path name, since we have the\nobjects already in a group. We can compute deltas within that group and then\nuse the name-hash approach to resort the object list and look for\nopportunistic cross-path deltas. Thus, the path-walk approach allows finding\ndelta bases at least as good as the traditional revision API approach.\n(Caveat: if we assume delta reuse and the existing deltas were computed with\nthe revision API approach, then the path-walk API approach may result in\nslightly worse delta compression. The performance tests in this series use\n--no-reuse-delta for this reason.)\n\nOnce 'git pack-objects --path-walk' exists, we have a few ways to take\nadvantage of it in other places:\n\n * The new 'pack.usePathWalk' config option will assume the --path-walk\n   option. This allows 'git push' to assume this value and get the effect we\n   want. This is similar to the 'pack.useSparse' config option that uses a\n   similar path-based walk to limit the set of boundary objects.\n\n * 'git repack' learns a '--path-walk' option to pass to its child 'git\n   pack-objects' process. This is also implied by 'pack.usePathWalk' but\n   allows for testing without modifying repository config.\n\nI'll repeat the following table of repacking results when using 'git repack\n-adf [--path-walk]' on a set of repositories with many name-hash collisions.\nOnly the microsoft/fluentui repository is publicly available for testing, so\nthe others are left as Repo B/C/D.\n\n| Repo     | Standard Repack | With --path-walk |\n|----------|-----------------|------------------|\n| fluentui |         438 MB  |          148 MB  |\n| Repo B   |       6,255 MB  |          778 MB  |\n| Repo C   |      37,737 MB  |        6,158 MB  |\n| Repo D   |     130,049 MB  |        4,432 MB  |\n\n\nWhile this series is replacing ds/pack-name-hash-tweak and its introduction\nof the --full-name-hash option, it is worth comparing that option to the\n--path-walk option.\n\n * The --full-name-hash option is a much smaller code change, as it drops\n   into the existing uses of the name-hash function.\n\n * The --full-name-hash option is more likely to integrate with server-side\n   features such as delta islands and reachability bitmaps due to not\n   changing the object walk. It was already noted that the .bitmap files\n   store name-hash values, so there is some compatibility required to\n   integrate with bitmaps. The --path-walk option will be more difficult to\n   integrate with those options (at least during a repack), but maybe is not\n   impossible; the --path-walk option will not work when reading\n   reachability bitmaps, since we are avoiding walking trees entirely.\n\n * The --full-name-hash option is good when there are many name-hash\n   collisions and many versions of the paths with those collisions. When\n   creating a shallow clone or certain kinds of pushes, the --full-name-hash\n   option is much worse at finding cross-path delta bases since it loses the\n   locality of the standard name-hash function. The --path-walk option\n   includes a second pass of delta computation using the standard name-hash\n   function and thus finds good cross-path delta bases when they improve\n   upon the same-path delta bases.\n\nThere are a few differences from the RFC version of this series:\n\n 1. The last two patches refactor the approach to perform delta calculations\n    by path after the object walk and then allows those delta calculations\n    to happen in a threaded manner.\n\n 2. Both 'git pack-objects' and 'git repack' are removed from the t0450\n    exclusion list.\n\n 3. The path-walk API has improved technical documentation that is extended\n    as its functionality is expanded.\n\n 4. Various bugs have been patched with matching tests. The new 'test-tool\n    path-walk' helper allows for careful testing of the API separately from\n    its use within other commands.\n\nThanks, - Stolee\n\nDerrick Stolee (17):\n  path-walk: introduce an object walk by path\n  t6601: add helper for testing path-walk API\n  path-walk: allow consumer to specify object types\n  path-walk: allow visiting tags\n  revision: create mark_trees_uninteresting_dense()\n  path-walk: add prune_all_uninteresting option\n  pack-objects: extract should_attempt_deltas()\n  pack-objects: add --path-walk option\n  pack-objects: update usage to match docs\n  p5313: add performance tests for --path-walk\n  pack-objects: introduce GIT_TEST_PACK_PATH_WALK\n  repack: add --path-walk option\n  repack: update usage to match docs\n  pack-objects: enable --path-walk via config\n  scalar: enable path-walk during push via config\n  pack-objects: refactor path-walk delta phase\n  pack-objects: thread the path-based compression\n\n Documentation/config/feature.txt          |   4 +\n Documentation/config/pack.txt             |   8 +\n Documentation/git-pack-objects.txt        |  23 +-\n Documentation/git-repack.txt              |  17 +-\n Documentation/technical/api-path-walk.txt |  73 ++++\n Makefile                                  |   2 +\n builtin/pack-objects.c                    | 410 ++++++++++++++++++++--\n builtin/repack.c                          |   9 +-\n ci/run-build-and-tests.sh                 |   1 +\n pack-objects.h                            |  12 +\n path-walk.c                               | 404 +++++++++++++++++++++\n path-walk.h                               |  64 ++++\n repo-settings.c                           |   3 +\n repo-settings.h                           |   1 +\n revision.c                                |  15 +\n revision.h                                |   1 +\n scalar.c                                  |   1 +\n t/README                                  |   4 +\n t/helper/test-path-walk.c                 | 114 ++++++\n t/helper/test-tool.c                      |   1 +\n t/helper/test-tool.h                      |   1 +\n t/perf/p5313-pack-objects.sh              |  77 ++++\n t/t0411-clone-from-partial.sh             |   6 +\n t/t0450/txt-help-mismatches               |   2 -\n t/t5300-pack-object.sh                    |  21 ++\n t/t5306-pack-nobase.sh                    |   5 +\n t/t5310-pack-bitmaps.sh                   |  13 +-\n t/t5316-pack-delta-depth.sh               |   9 +-\n t/t5332-multi-pack-reuse.sh               |   7 +\n t/t6601-path-walk.sh                      | 301 ++++++++++++++++\n t/t7406-submodule-update.sh               |   4 +\n 31 files changed, 1563 insertions(+), 50 deletions(-)\n create mode 100644 Documentation/technical/api-path-walk.txt\n create mode 100644 path-walk.c\n create mode 100644 path-walk.h\n create mode 100644 t/helper/test-path-walk.c\n create mode 100755 t/perf/p5313-pack-objects.sh\n create mode 100755 t/t6601-path-walk.sh\n\n\nbase-commit: e9356ba3ea2a6754281ff7697b3e5a1697b21e24\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-1813%2Fderrickstolee%2Fpath-walk-upstream-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1813/derrickstolee/path-walk-upstream-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/1813\n-- \ngitgitgadget\n"},{"id":"504427","messageId":"98bdc94a773d797152bebfbfda88fa1bb0707821.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 01/17] path-walk: introduce an object walk by path","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:11:47Z","receivedAt":"2024-10-08T14:12:08Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nIn anticipation of a few planned applications, introduce the most basic form\nof a path-walk API. It currently assumes that there are no UNINTERESTING\nobjects and does not include any complicated filters. It calls a function\npointer on groups of tree and blob objects as grouped by path. This only\nincludes objects the first time they are discovered, so an object that\nappears at multiple paths will not be included in two batches.\n\nThere are many future adaptations that could be made, but they are left for\nfuture updates when consumers are ready to take advantage of those features.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/technical/api-path-walk.txt |  54 +++++\n Makefile                                  |   1 +\n path-walk.c                               | 241 ++++++++++++++++++++++\n path-walk.h                               |  43 ++++\n 4 files changed, 339 insertions(+)\n create mode 100644 Documentation/technical/api-path-walk.txt\n create mode 100644 path-walk.c\n create mode 100644 path-walk.h\n\ndiff --git a/Documentation/technical/api-path-walk.txt b/Documentation/technical/api-path-walk.txt\nnew file mode 100644\nindex 00000000000..6472222ae6d\n--- /dev/null\n+++ b/Documentation/technical/api-path-walk.txt\n@@ -0,0 +1,54 @@\n+Path-Walk API\n+=============\n+\n+The path-walk API is used to walk reachable objects, but to visit objects\n+in batches based on a common path they appear in, or by type.\n+\n+For example, all reachable commits are visited in a group. All tags are\n+visited in a group. Then, all root trees are visited. At some point, all\n+blobs reachable via a path `my/dir/to/A` are visited. When there are\n+multiple paths possible to reach the same object, then only one of those\n+paths is used to visit the object.\n+\n+When walking a range of commits with some `UNINTERESTING` objects, the\n+objects with the `UNINTERESTING` flag are included in these batches. In\n+order to walk `UNINTERESTING` objects, the `--boundary` option must be\n+used in the commit walk in order to visit `UNINTERESTING` commits.\n+\n+Basics\n+------\n+\n+To use the path-walk API, include `path-walk.h` and call\n+`walk_objects_by_path()` with a customized `path_walk_info` struct. The\n+struct is used to set all of the options for how the walk should proceed.\n+Let's dig into the different options and their use.\n+\n+`path_fn` and `path_fn_data`::\n+\tThe most important option is the `path_fn` option, which is a\n+\tfunction pointer to the callback that can execute logic on the\n+\tobject IDs for objects grouped by type and path. This function\n+\talso receives a `data` value that corresponds to the\n+\t`path_fn_data` member, for providing custom data structures to\n+\tthis callback function.\n+\n+`revs`::\n+\tTo configure the exact details of the reachable set of objects,\n+\tuse the `revs` member and initialize it using the revision\n+\tmachinery in `revision.h`. Initialize `revs` using calls such as\n+\t`setup_revisions()` or `parse_revision_opt()`. Do not call\n+\t`prepare_revision_walk()`, as that will be called within\n+\t`walk_objects_by_path()`.\n++\n+It is also important that you do not specify the `--objects` flag for the\n+`revs` struct. The revision walk should only be used to walk commits, and\n+the objects will be walked in a separate way based on those starting\n+commits.\n++\n+If you want the path-walk API to emit `UNINTERESTING` objects based on the\n+commit walk's boundary, be sure to set `revs.boundary` so the boundary\n+commits are emitted.\n+\n+Examples\n+--------\n+\n+See example usages in future changes.\ndiff --git a/Makefile b/Makefile\nindex 7344a7f7257..d0d8d6888e3 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1094,6 +1094,7 @@ LIB_OBJS += parse-options.o\n LIB_OBJS += patch-delta.o\n LIB_OBJS += patch-ids.o\n LIB_OBJS += path.o\n+LIB_OBJS += path-walk.o\n LIB_OBJS += pathspec.o\n LIB_OBJS += pkt-line.o\n LIB_OBJS += preload-index.o\ndiff --git a/path-walk.c b/path-walk.c\nnew file mode 100644\nindex 00000000000..66840187e28\n--- /dev/null\n+++ b/path-walk.c\n@@ -0,0 +1,241 @@\n+/*\n+ * path-walk.c: implementation for path-based walks of the object graph.\n+ */\n+#include \"git-compat-util.h\"\n+#include \"path-walk.h\"\n+#include \"blob.h\"\n+#include \"commit.h\"\n+#include \"dir.h\"\n+#include \"hashmap.h\"\n+#include \"hex.h\"\n+#include \"object.h\"\n+#include \"oid-array.h\"\n+#include \"revision.h\"\n+#include \"string-list.h\"\n+#include \"strmap.h\"\n+#include \"trace2.h\"\n+#include \"tree.h\"\n+#include \"tree-walk.h\"\n+\n+struct type_and_oid_list\n+{\n+\tenum object_type type;\n+\tstruct oid_array oids;\n+};\n+\n+#define TYPE_AND_OID_LIST_INIT { \\\n+\t.type = OBJ_NONE, \t \\\n+\t.oids = OID_ARRAY_INIT\t \\\n+}\n+\n+struct path_walk_context {\n+\t/**\n+\t * Repeats of data in 'struct path_walk_info' for\n+\t * access with fewer characters.\n+\t */\n+\tstruct repository *repo;\n+\tstruct rev_info *revs;\n+\tstruct path_walk_info *info;\n+\n+\t/**\n+\t * Map a path to a 'struct type_and_oid_list'\n+\t * containing the objects discovered at that\n+\t * path.\n+\t */\n+\tstruct strmap paths_to_lists;\n+\n+\t/**\n+\t * Store the current list of paths in a stack, to\n+\t * facilitate depth-first-search without recursion.\n+\t */\n+\tstruct string_list path_stack;\n+};\n+\n+static int add_children(struct path_walk_context *ctx,\n+\t\t\tconst char *base_path,\n+\t\t\tstruct object_id *oid)\n+{\n+\tstruct tree_desc desc;\n+\tstruct name_entry entry;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tsize_t base_len;\n+\tstruct tree *tree = lookup_tree(ctx->repo, oid);\n+\n+\tif (!tree) {\n+\t\terror(_(\"failed to walk children of tree %s: not found\"),\n+\t\t      oid_to_hex(oid));\n+\t\treturn -1;\n+\t} else if (parse_tree_gently(tree, 1)) {\n+\t\tdie(\"bad tree object %s\", oid_to_hex(oid));\n+\t}\n+\n+\tstrbuf_addstr(&path, base_path);\n+\tbase_len = path.len;\n+\n+\tparse_tree(tree);\n+\tinit_tree_desc(&desc, &tree->object.oid, tree->buffer, tree->size);\n+\twhile (tree_entry(&desc, &entry)) {\n+\t\tstruct type_and_oid_list *list;\n+\t\tstruct object *o;\n+\t\t/* Not actually true, but we will ignore submodules later. */\n+\t\tenum object_type type = S_ISDIR(entry.mode) ? OBJ_TREE : OBJ_BLOB;\n+\n+\t\t/* Skip submodules. */\n+\t\tif (S_ISGITLINK(entry.mode))\n+\t\t\tcontinue;\n+\n+\t\tif (type == OBJ_TREE) {\n+\t\t\tstruct tree *child = lookup_tree(ctx->repo, &entry.oid);\n+\t\t\to = child ? &child->object : NULL;\n+\t\t} else if (type == OBJ_BLOB) {\n+\t\t\tstruct blob *child = lookup_blob(ctx->repo, &entry.oid);\n+\t\t\to = child ? &child->object : NULL;\n+\t\t} else {\n+\t\t\t/* Wrong type? */\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tif (!o) /* report error?*/\n+\t\t\tcontinue;\n+\n+\t\t/* Skip this object if already seen. */\n+\t\tif (o->flags & SEEN)\n+\t\t\tcontinue;\n+\t\to->flags |= SEEN;\n+\n+\t\tstrbuf_setlen(&path, base_len);\n+\t\tstrbuf_add(&path, entry.path, entry.pathlen);\n+\n+\t\t/*\n+\t\t * Trees will end with \"/\" for concatenation and distinction\n+\t\t * from blobs at the same path.\n+\t\t */\n+\t\tif (type == OBJ_TREE)\n+\t\t\tstrbuf_addch(&path, '/');\n+\n+\t\tif (!(list = strmap_get(&ctx->paths_to_lists, path.buf))) {\n+\t\t\tCALLOC_ARRAY(list, 1);\n+\t\t\tlist->type = type;\n+\t\t\tstrmap_put(&ctx->paths_to_lists, path.buf, list);\n+\t\t\tstring_list_append(&ctx->path_stack, path.buf);\n+\t\t}\n+\t\toid_array_append(&list->oids, &entry.oid);\n+\t}\n+\n+\tfree_tree_buffer(tree);\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+/*\n+ * For each path in paths_to_explore, walk the trees another level\n+ * and add any found blobs to the batch (but only if they exist and\n+ * haven't been added yet).\n+ */\n+static int walk_path(struct path_walk_context *ctx,\n+\t\t     const char *path)\n+{\n+\tstruct type_and_oid_list *list;\n+\tint ret = 0;\n+\n+\tlist = strmap_get(&ctx->paths_to_lists, path);\n+\n+\t/* Evaluate function pointer on this data. */\n+\tret = ctx->info->path_fn(path, &list->oids, list->type,\n+\t\t\t\t ctx->info->path_fn_data);\n+\n+\t/* Expand data for children. */\n+\tif (list->type == OBJ_TREE) {\n+\t\tfor (size_t i = 0; i < list->oids.nr; i++) {\n+\t\t\tret |= add_children(ctx,\n+\t\t\t\t\t    path,\n+\t\t\t\t\t    &list->oids.oid[i]);\n+\t\t}\n+\t}\n+\n+\toid_array_clear(&list->oids);\n+\tstrmap_remove(&ctx->paths_to_lists, path, 1);\n+\treturn ret;\n+}\n+\n+static void clear_strmap(struct strmap *map)\n+{\n+\tstruct hashmap_iter iter;\n+\tstruct strmap_entry *e;\n+\n+\thashmap_for_each_entry(&map->map, &iter, e, ent) {\n+\t\tstruct type_and_oid_list *list = e->value;\n+\t\toid_array_clear(&list->oids);\n+\t}\n+\tstrmap_clear(map, 1);\n+\tstrmap_init(map);\n+}\n+\n+/**\n+ * Given the configuration of 'info', walk the commits based on 'info->revs' and\n+ * call 'info->path_fn' on each discovered path.\n+ *\n+ * Returns nonzero on an error.\n+ */\n+int walk_objects_by_path(struct path_walk_info *info)\n+{\n+\tconst char *root_path = \"\";\n+\tint ret = 0;\n+\tsize_t commits_nr = 0, paths_nr = 0;\n+\tstruct commit *c;\n+\tstruct type_and_oid_list *root_tree_list;\n+\tstruct path_walk_context ctx = {\n+\t\t.repo = info->revs->repo,\n+\t\t.revs = info->revs,\n+\t\t.info = info,\n+\t\t.path_stack = STRING_LIST_INIT_DUP,\n+\t\t.paths_to_lists = STRMAP_INIT\n+\t};\n+\n+\ttrace2_region_enter(\"path-walk\", \"commit-walk\", info->revs->repo);\n+\n+\t/* Insert a single list for the root tree into the paths. */\n+\tCALLOC_ARRAY(root_tree_list, 1);\n+\troot_tree_list->type = OBJ_TREE;\n+\tstrmap_put(&ctx.paths_to_lists, root_path, root_tree_list);\n+\n+\tif (prepare_revision_walk(info->revs))\n+\t\tdie(_(\"failed to setup revision walk\"));\n+\n+\twhile ((c = get_revision(info->revs))) {\n+\t\tstruct object_id *oid = get_commit_tree_oid(c);\n+\t\tstruct tree *t = lookup_tree(info->revs->repo, oid);\n+\t\tcommits_nr++;\n+\n+\t\tif (t) {\n+\t\t\tif (t->object.flags & SEEN)\n+\t\t\t\tcontinue;\n+\t\t\tt->object.flags |= SEEN;\n+\t\t\toid_array_append(&root_tree_list->oids, oid);\n+\t\t} else {\n+\t\t\twarning(\"could not find tree %s\", oid_to_hex(oid));\n+\t\t}\n+\t}\n+\n+\ttrace2_data_intmax(\"path-walk\", ctx.repo, \"commits\", commits_nr);\n+\ttrace2_region_leave(\"path-walk\", \"commit-walk\", info->revs->repo);\n+\n+\tstring_list_append(&ctx.path_stack, root_path);\n+\n+\ttrace2_region_enter(\"path-walk\", \"path-walk\", info->revs->repo);\n+\twhile (!ret && ctx.path_stack.nr) {\n+\t\tchar *path = ctx.path_stack.items[ctx.path_stack.nr - 1].string;\n+\t\tctx.path_stack.nr--;\n+\t\tpaths_nr++;\n+\n+\t\tret = walk_path(&ctx, path);\n+\n+\t\tfree(path);\n+\t}\n+\ttrace2_data_intmax(\"path-walk\", ctx.repo, \"paths\", paths_nr);\n+\ttrace2_region_leave(\"path-walk\", \"path-walk\", info->revs->repo);\n+\n+\tclear_strmap(&ctx.paths_to_lists);\n+\tstring_list_clear(&ctx.path_stack, 0);\n+\treturn ret;\n+}\ndiff --git a/path-walk.h b/path-walk.h\nnew file mode 100644\nindex 00000000000..c9e94a98bc8\n--- /dev/null\n+++ b/path-walk.h\n@@ -0,0 +1,43 @@\n+/*\n+ * path-walk.h : Methods and structures for walking the object graph in batches\n+ * by the paths that can reach those objects.\n+ */\n+#include \"object.h\" /* Required for 'enum object_type'. */\n+\n+struct rev_info;\n+struct oid_array;\n+\n+/**\n+ * The type of a function pointer for the method that is called on a list of\n+ * objects reachable at a given path.\n+ */\n+typedef int (*path_fn)(const char *path,\n+\t\t       struct oid_array *oids,\n+\t\t       enum object_type type,\n+\t\t       void *data);\n+\n+struct path_walk_info {\n+\t/**\n+\t * revs provides the definitions for the commit walk, including\n+\t * which commits are UNINTERESTING or not.\n+\t */\n+\tstruct rev_info *revs;\n+\n+\t/**\n+\t * The caller wishes to execute custom logic on objects reachable at a\n+\t * given path. Every reachable object will be visited exactly once, and\n+\t * the first path to see an object wins. This may not be a stable choice.\n+\t */\n+\tpath_fn path_fn;\n+\tvoid *path_fn_data;\n+};\n+\n+#define PATH_WALK_INFO_INIT { 0 }\n+\n+/**\n+ * Given the configuration of 'info', walk the commits based on 'info->revs' and\n+ * call 'info->path_fn' on each discovered path.\n+ *\n+ * Returns nonzero on an error.\n+ */\n+int walk_objects_by_path(struct path_walk_info *info);\n-- \ngitgitgadget\n\n"},{"id":"504428","messageId":"a00ab0c62c9f9c495981d9e565fdd10303ebe079.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 02/17] t6601: add helper for testing path-walk API","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:11:48Z","receivedAt":"2024-10-08T14:12:09Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nAdd some tests based on the current behavior, doing interesting checks\nfor different sets of branches, ranges, and the --boundary option. This\nsets a baseline for the behavior and we can extend it as new options are\nintroduced.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/technical/api-path-walk.txt |   3 +-\n Makefile                                  |   1 +\n t/helper/test-path-walk.c                 |  86 ++++++++++++++\n t/helper/test-tool.c                      |   1 +\n t/helper/test-tool.h                      |   1 +\n t/t6601-path-walk.sh                      | 130 ++++++++++++++++++++++\n 6 files changed, 221 insertions(+), 1 deletion(-)\n create mode 100644 t/helper/test-path-walk.c\n create mode 100755 t/t6601-path-walk.sh\n\ndiff --git a/Documentation/technical/api-path-walk.txt b/Documentation/technical/api-path-walk.txt\nindex 6472222ae6d..e588897ab8d 100644\n--- a/Documentation/technical/api-path-walk.txt\n+++ b/Documentation/technical/api-path-walk.txt\n@@ -51,4 +51,5 @@ commits are emitted.\n Examples\n --------\n \n-See example usages in future changes.\n+See example usages in:\n+\t`t/helper/test-path-walk.c`\ndiff --git a/Makefile b/Makefile\nindex d0d8d6888e3..50413d96492 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -818,6 +818,7 @@ TEST_BUILTINS_OBJS += test-parse-options.o\n TEST_BUILTINS_OBJS += test-parse-pathspec-file.o\n TEST_BUILTINS_OBJS += test-partial-clone.o\n TEST_BUILTINS_OBJS += test-path-utils.o\n+TEST_BUILTINS_OBJS += test-path-walk.o\n TEST_BUILTINS_OBJS += test-pcre2-config.o\n TEST_BUILTINS_OBJS += test-pkt-line.o\n TEST_BUILTINS_OBJS += test-proc-receive.o\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nnew file mode 100644\nindex 00000000000..3c48f017fa0\n--- /dev/null\n+++ b/t/helper/test-path-walk.c\n@@ -0,0 +1,86 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n+#include \"test-tool.h\"\n+#include \"environment.h\"\n+#include \"hex.h\"\n+#include \"object-name.h\"\n+#include \"object.h\"\n+#include \"pretty.h\"\n+#include \"revision.h\"\n+#include \"setup.h\"\n+#include \"parse-options.h\"\n+#include \"path-walk.h\"\n+#include \"oid-array.h\"\n+\n+static const char * const path_walk_usage[] = {\n+\tN_(\"test-tool path-walk <options> -- <revision-options>\"),\n+\tNULL\n+};\n+\n+struct path_walk_test_data {\n+\tuintmax_t tree_nr;\n+\tuintmax_t blob_nr;\n+};\n+\n+static int emit_block(const char *path, struct oid_array *oids,\n+\t\t      enum object_type type, void *data)\n+{\n+\tstruct path_walk_test_data *tdata = data;\n+\tconst char *typestr;\n+\n+\tswitch (type) {\n+\tcase OBJ_TREE:\n+\t\ttypestr = \"TREE\";\n+\t\ttdata->tree_nr += oids->nr;\n+\t\tbreak;\n+\n+\tcase OBJ_BLOB:\n+\t\ttypestr = \"BLOB\";\n+\t\ttdata->blob_nr += oids->nr;\n+\t\tbreak;\n+\n+\tdefault:\n+\t\tBUG(\"we do not understand this type\");\n+\t}\n+\n+\tfor (size_t i = 0; i < oids->nr; i++)\n+\t\tprintf(\"%s:%s:%s\\n\", typestr, path, oid_to_hex(&oids->oid[i]));\n+\n+\treturn 0;\n+}\n+\n+int cmd__path_walk(int argc, const char **argv)\n+{\n+\tint res;\n+\tstruct rev_info revs = REV_INFO_INIT;\n+\tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n+\tstruct path_walk_test_data data = { 0 };\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\tinitialize_repository(the_repository);\n+\tsetup_git_directory();\n+\trevs.repo = the_repository;\n+\n+\targc = parse_options(argc, argv, NULL,\n+\t\t\t     options, path_walk_usage,\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_ARGV0);\n+\n+\tif (argc > 1)\n+\t\tsetup_revisions(argc, argv, &revs, NULL);\n+\telse\n+\t\tusage(path_walk_usage[0]);\n+\n+\tinfo.revs = &revs;\n+\tinfo.path_fn = emit_block;\n+\tinfo.path_fn_data = &data;\n+\n+\tres = walk_objects_by_path(&info);\n+\n+\tprintf(\"trees:%\" PRIuMAX \"\\n\"\n+\t       \"blobs:%\" PRIuMAX \"\\n\",\n+\t       data.tree_nr, data.blob_nr);\n+\n+\treturn res;\n+}\ndiff --git a/t/helper/test-tool.c b/t/helper/test-tool.c\nindex 1ebb69a5dc4..43676e7b93a 100644\n--- a/t/helper/test-tool.c\n+++ b/t/helper/test-tool.c\n@@ -52,6 +52,7 @@ static struct test_cmd cmds[] = {\n \t{ \"parse-subcommand\", cmd__parse_subcommand },\n \t{ \"partial-clone\", cmd__partial_clone },\n \t{ \"path-utils\", cmd__path_utils },\n+\t{ \"path-walk\", cmd__path_walk },\n \t{ \"pcre2-config\", cmd__pcre2_config },\n \t{ \"pkt-line\", cmd__pkt_line },\n \t{ \"proc-receive\", cmd__proc_receive },\ndiff --git a/t/helper/test-tool.h b/t/helper/test-tool.h\nindex 21802ac27da..9cfc5da6e57 100644\n--- a/t/helper/test-tool.h\n+++ b/t/helper/test-tool.h\n@@ -45,6 +45,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);\n int cmd__parse_subcommand(int argc, const char **argv);\n int cmd__partial_clone(int argc, const char **argv);\n int cmd__path_utils(int argc, const char **argv);\n+int cmd__path_walk(int argc, const char **argv);\n int cmd__pcre2_config(int argc, const char **argv);\n int cmd__pkt_line(int argc, const char **argv);\n int cmd__proc_receive(int argc, const char **argv);\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nnew file mode 100755\nindex 00000000000..ca18b61c3f1\n--- /dev/null\n+++ b/t/t6601-path-walk.sh\n@@ -0,0 +1,130 @@\n+#!/bin/sh\n+\n+test_description='direct path-walk API tests'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup test repository' '\n+\tgit checkout -b base &&\n+\n+\tmkdir left &&\n+\tmkdir right &&\n+\techo a >a &&\n+\techo b >left/b &&\n+\techo c >right/c &&\n+\tgit add . &&\n+\tgit commit -m \"first\" &&\n+\n+\techo d >right/d &&\n+\tgit add right &&\n+\tgit commit -m \"second\" &&\n+\n+\techo bb >left/b &&\n+\tgit commit -a -m \"third\" &&\n+\n+\tgit checkout -b topic HEAD~1 &&\n+\techo cc >right/c &&\n+\tgit commit -a -m \"topic\"\n+'\n+\n+test_expect_success 'all' '\n+\ttest-tool path-walk -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tTREE::$(git rev-parse topic^{tree})\n+\tTREE::$(git rev-parse base^{tree})\n+\tTREE::$(git rev-parse base~1^{tree})\n+\tTREE::$(git rev-parse base~2^{tree})\n+\tTREE:left/:$(git rev-parse base:left)\n+\tTREE:left/:$(git rev-parse base~2:left)\n+\tTREE:right/:$(git rev-parse topic:right)\n+\tTREE:right/:$(git rev-parse base~1:right)\n+\tTREE:right/:$(git rev-parse base~2:right)\n+\ttrees:9\n+\tBLOB:a:$(git rev-parse base~2:a)\n+\tBLOB:left/b:$(git rev-parse base~2:left/b)\n+\tBLOB:left/b:$(git rev-parse base:left/b)\n+\tBLOB:right/c:$(git rev-parse base~2:right/c)\n+\tBLOB:right/c:$(git rev-parse topic:right/c)\n+\tBLOB:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:6\n+\tEOF\n+\n+\tsort expect >expect.sorted &&\n+\tsort out >out.sorted &&\n+\n+\ttest_cmp expect.sorted out.sorted\n+'\n+\n+test_expect_success 'topic only' '\n+\ttest-tool path-walk -- topic >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tTREE::$(git rev-parse topic^{tree})\n+\tTREE::$(git rev-parse base~1^{tree})\n+\tTREE::$(git rev-parse base~2^{tree})\n+\tTREE:left/:$(git rev-parse base~2:left)\n+\tTREE:right/:$(git rev-parse topic:right)\n+\tTREE:right/:$(git rev-parse base~1:right)\n+\tTREE:right/:$(git rev-parse base~2:right)\n+\ttrees:7\n+\tBLOB:a:$(git rev-parse base~2:a)\n+\tBLOB:left/b:$(git rev-parse base~2:left/b)\n+\tBLOB:right/c:$(git rev-parse base~2:right/c)\n+\tBLOB:right/c:$(git rev-parse topic:right/c)\n+\tBLOB:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:5\n+\tEOF\n+\n+\tsort expect >expect.sorted &&\n+\tsort out >out.sorted &&\n+\n+\ttest_cmp expect.sorted out.sorted\n+'\n+\n+test_expect_success 'topic, not base' '\n+\ttest-tool path-walk -- topic --not base >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tTREE::$(git rev-parse topic^{tree})\n+\tTREE:left/:$(git rev-parse topic:left)\n+\tTREE:right/:$(git rev-parse topic:right)\n+\ttrees:3\n+\tBLOB:a:$(git rev-parse topic:a)\n+\tBLOB:left/b:$(git rev-parse topic:left/b)\n+\tBLOB:right/c:$(git rev-parse topic:right/c)\n+\tBLOB:right/d:$(git rev-parse topic:right/d)\n+\tblobs:4\n+\tEOF\n+\n+\tsort expect >expect.sorted &&\n+\tsort out >out.sorted &&\n+\n+\ttest_cmp expect.sorted out.sorted\n+'\n+\n+test_expect_success 'topic, not base, boundary' '\n+\ttest-tool path-walk -- --boundary topic --not base >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tTREE::$(git rev-parse topic^{tree})\n+\tTREE::$(git rev-parse base~1^{tree})\n+\tTREE:left/:$(git rev-parse base~1:left)\n+\tTREE:right/:$(git rev-parse topic:right)\n+\tTREE:right/:$(git rev-parse base~1:right)\n+\ttrees:5\n+\tBLOB:a:$(git rev-parse base~1:a)\n+\tBLOB:left/b:$(git rev-parse base~1:left/b)\n+\tBLOB:right/c:$(git rev-parse base~1:right/c)\n+\tBLOB:right/c:$(git rev-parse topic:right/c)\n+\tBLOB:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:5\n+\tEOF\n+\n+\tsort expect >expect.sorted &&\n+\tsort out >out.sorted &&\n+\n+\ttest_cmp expect.sorted out.sorted\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"504429","messageId":"14375d19392c406d903f6cb26be5a39c4c2ff1e9.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 03/17] path-walk: allow consumer to specify object types","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:11:49Z","receivedAt":"2024-10-08T14:12:10Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nWe add the ability to filter the object types in the path-walk API so\nthe callback function is called fewer times.\n\nThis adds the ability to ask for the commits in a list, as well. Future\nchanges will add the ability to visit annotated tags.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/technical/api-path-walk.txt |  9 +++\n path-walk.c                               | 39 ++++++++++--\n path-walk.h                               | 13 +++-\n t/helper/test-path-walk.c                 | 17 +++++-\n t/t6601-path-walk.sh                      | 72 +++++++++++++++++++++++\n 5 files changed, 141 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/technical/api-path-walk.txt b/Documentation/technical/api-path-walk.txt\nindex e588897ab8d..b7ae476ea0a 100644\n--- a/Documentation/technical/api-path-walk.txt\n+++ b/Documentation/technical/api-path-walk.txt\n@@ -48,6 +48,15 @@ If you want the path-walk API to emit `UNINTERESTING` objects based on the\n commit walk's boundary, be sure to set `revs.boundary` so the boundary\n commits are emitted.\n \n+`commits`, `blobs`, `trees`::\n+\tBy default, these members are enabled and signal that the path-walk\n+\tAPI should call the `path_fn` on objects of these types. Specialized\n+\tapplications could disable some options to make it simpler to walk\n+\tthe objects or to have fewer calls to `path_fn`.\n++\n+While it is possible to walk only commits in this way, consumers would be\n+better off using the revision walk API instead.\n+\n Examples\n --------\n \ndiff --git a/path-walk.c b/path-walk.c\nindex 66840187e28..22e1aa13f31 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -84,6 +84,10 @@ static int add_children(struct path_walk_context *ctx,\n \t\tif (S_ISGITLINK(entry.mode))\n \t\t\tcontinue;\n \n+\t\t/* If the caller doesn't want blobs, then don't bother. */\n+\t\tif (!ctx->info->blobs && type == OBJ_BLOB)\n+\t\t\tcontinue;\n+\n \t\tif (type == OBJ_TREE) {\n \t\t\tstruct tree *child = lookup_tree(ctx->repo, &entry.oid);\n \t\t\to = child ? &child->object : NULL;\n@@ -140,9 +144,11 @@ static int walk_path(struct path_walk_context *ctx,\n \n \tlist = strmap_get(&ctx->paths_to_lists, path);\n \n-\t/* Evaluate function pointer on this data. */\n-\tret = ctx->info->path_fn(path, &list->oids, list->type,\n-\t\t\t\t ctx->info->path_fn_data);\n+\t/* Evaluate function pointer on this data, if requested. */\n+\tif ((list->type == OBJ_TREE && ctx->info->trees) ||\n+\t    (list->type == OBJ_BLOB && ctx->info->blobs))\n+\t\tret = ctx->info->path_fn(path, &list->oids, list->type,\n+\t\t\t\t\tctx->info->path_fn_data);\n \n \t/* Expand data for children. */\n \tif (list->type == OBJ_TREE) {\n@@ -184,6 +190,7 @@ int walk_objects_by_path(struct path_walk_info *info)\n \tsize_t commits_nr = 0, paths_nr = 0;\n \tstruct commit *c;\n \tstruct type_and_oid_list *root_tree_list;\n+\tstruct type_and_oid_list *commit_list;\n \tstruct path_walk_context ctx = {\n \t\t.repo = info->revs->repo,\n \t\t.revs = info->revs,\n@@ -194,19 +201,32 @@ int walk_objects_by_path(struct path_walk_info *info)\n \n \ttrace2_region_enter(\"path-walk\", \"commit-walk\", info->revs->repo);\n \n+\tCALLOC_ARRAY(commit_list, 1);\n+\tcommit_list->type = OBJ_COMMIT;\n+\n \t/* Insert a single list for the root tree into the paths. */\n \tCALLOC_ARRAY(root_tree_list, 1);\n \troot_tree_list->type = OBJ_TREE;\n \tstrmap_put(&ctx.paths_to_lists, root_path, root_tree_list);\n-\n \tif (prepare_revision_walk(info->revs))\n \t\tdie(_(\"failed to setup revision walk\"));\n \n \twhile ((c = get_revision(info->revs))) {\n-\t\tstruct object_id *oid = get_commit_tree_oid(c);\n-\t\tstruct tree *t = lookup_tree(info->revs->repo, oid);\n+\t\tstruct object_id *oid;\n+\t\tstruct tree *t;\n \t\tcommits_nr++;\n \n+\t\tif (info->commits)\n+\t\t\toid_array_append(&commit_list->oids,\n+\t\t\t\t\t &c->object.oid);\n+\n+\t\t/* If we only care about commits, then skip trees. */\n+\t\tif (!info->trees && !info->blobs)\n+\t\t\tcontinue;\n+\n+\t\toid = get_commit_tree_oid(c);\n+\t\tt = lookup_tree(info->revs->repo, oid);\n+\n \t\tif (t) {\n \t\t\tif (t->object.flags & SEEN)\n \t\t\t\tcontinue;\n@@ -220,6 +240,13 @@ int walk_objects_by_path(struct path_walk_info *info)\n \ttrace2_data_intmax(\"path-walk\", ctx.repo, \"commits\", commits_nr);\n \ttrace2_region_leave(\"path-walk\", \"commit-walk\", info->revs->repo);\n \n+\t/* Track all commits. */\n+\tif (info->commits)\n+\t\tret = info->path_fn(\"\", &commit_list->oids, OBJ_COMMIT,\n+\t\t\t\t    info->path_fn_data);\n+\toid_array_clear(&commit_list->oids);\n+\tfree(commit_list);\n+\n \tstring_list_append(&ctx.path_stack, root_path);\n \n \ttrace2_region_enter(\"path-walk\", \"path-walk\", info->revs->repo);\ndiff --git a/path-walk.h b/path-walk.h\nindex c9e94a98bc8..6ef372d8942 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -30,9 +30,20 @@ struct path_walk_info {\n \t */\n \tpath_fn path_fn;\n \tvoid *path_fn_data;\n+\t/**\n+\t * Initialize which object types the path_fn should be called on. This\n+\t * could also limit the walk to skip blobs if not set.\n+\t */\n+\tint commits;\n+\tint trees;\n+\tint blobs;\n };\n \n-#define PATH_WALK_INFO_INIT { 0 }\n+#define PATH_WALK_INFO_INIT {   \\\n+\t.blobs = 1,\t\t\\\n+\t.trees = 1,\t\t\\\n+\t.commits = 1,\t\t\\\n+}\n \n /**\n  * Given the configuration of 'info', walk the commits based on 'info->revs' and\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex 3c48f017fa0..37c5e3e31e8 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -18,6 +18,7 @@ static const char * const path_walk_usage[] = {\n };\n \n struct path_walk_test_data {\n+\tuintmax_t commit_nr;\n \tuintmax_t tree_nr;\n \tuintmax_t blob_nr;\n };\n@@ -29,6 +30,11 @@ static int emit_block(const char *path, struct oid_array *oids,\n \tconst char *typestr;\n \n \tswitch (type) {\n+\tcase OBJ_COMMIT:\n+\t\ttypestr = \"COMMIT\";\n+\t\ttdata->commit_nr += oids->nr;\n+\t\tbreak;\n+\n \tcase OBJ_TREE:\n \t\ttypestr = \"TREE\";\n \t\ttdata->tree_nr += oids->nr;\n@@ -56,6 +62,12 @@ int cmd__path_walk(int argc, const char **argv)\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n \tstruct path_walk_test_data data = { 0 };\n \tstruct option options[] = {\n+\t\tOPT_BOOL(0, \"blobs\", &info.blobs,\n+\t\t\t N_(\"toggle inclusion of blob objects\")),\n+\t\tOPT_BOOL(0, \"commits\", &info.commits,\n+\t\t\t N_(\"toggle inclusion of commit objects\")),\n+\t\tOPT_BOOL(0, \"trees\", &info.trees,\n+\t\t\t N_(\"toggle inclusion of tree objects\")),\n \t\tOPT_END(),\n \t};\n \n@@ -78,9 +90,10 @@ int cmd__path_walk(int argc, const char **argv)\n \n \tres = walk_objects_by_path(&info);\n \n-\tprintf(\"trees:%\" PRIuMAX \"\\n\"\n+\tprintf(\"commits:%\" PRIuMAX \"\\n\"\n+\t       \"trees:%\" PRIuMAX \"\\n\"\n \t       \"blobs:%\" PRIuMAX \"\\n\",\n-\t       data.tree_nr, data.blob_nr);\n+\t       data.commit_nr, data.tree_nr, data.blob_nr);\n \n \treturn res;\n }\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex ca18b61c3f1..e4788664f93 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -31,6 +31,11 @@ test_expect_success 'all' '\n \ttest-tool path-walk -- --all >out &&\n \n \tcat >expect <<-EOF &&\n+\tCOMMIT::$(git rev-parse topic)\n+\tCOMMIT::$(git rev-parse base)\n+\tCOMMIT::$(git rev-parse base~1)\n+\tCOMMIT::$(git rev-parse base~2)\n+\tcommits:4\n \tTREE::$(git rev-parse topic^{tree})\n \tTREE::$(git rev-parse base^{tree})\n \tTREE::$(git rev-parse base~1^{tree})\n@@ -60,6 +65,10 @@ test_expect_success 'topic only' '\n \ttest-tool path-walk -- topic >out &&\n \n \tcat >expect <<-EOF &&\n+\tCOMMIT::$(git rev-parse topic)\n+\tCOMMIT::$(git rev-parse base~1)\n+\tCOMMIT::$(git rev-parse base~2)\n+\tcommits:3\n \tTREE::$(git rev-parse topic^{tree})\n \tTREE::$(git rev-parse base~1^{tree})\n \tTREE::$(git rev-parse base~2^{tree})\n@@ -86,6 +95,8 @@ test_expect_success 'topic, not base' '\n \ttest-tool path-walk -- topic --not base >out &&\n \n \tcat >expect <<-EOF &&\n+\tCOMMIT::$(git rev-parse topic)\n+\tcommits:1\n \tTREE::$(git rev-parse topic^{tree})\n \tTREE:left/:$(git rev-parse topic:left)\n \tTREE:right/:$(git rev-parse topic:right)\n@@ -103,10 +114,71 @@ test_expect_success 'topic, not base' '\n \ttest_cmp expect.sorted out.sorted\n '\n \n+test_expect_success 'topic, not base, only blobs' '\n+\ttest-tool path-walk --no-trees --no-commits \\\n+\t\t-- topic --not base >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tcommits:0\n+\ttrees:0\n+\tBLOB:a:$(git rev-parse topic:a)\n+\tBLOB:left/b:$(git rev-parse topic:left/b)\n+\tBLOB:right/c:$(git rev-parse topic:right/c)\n+\tBLOB:right/d:$(git rev-parse topic:right/d)\n+\tblobs:4\n+\tEOF\n+\n+\tsort expect >expect.sorted &&\n+\tsort out >out.sorted &&\n+\n+\ttest_cmp expect.sorted out.sorted\n+'\n+\n+# No, this doesn't make a lot of sense for the path-walk API,\n+# but it is possible to do.\n+test_expect_success 'topic, not base, only commits' '\n+\ttest-tool path-walk --no-blobs --no-trees \\\n+\t\t-- topic --not base >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tCOMMIT::$(git rev-parse topic)\n+\tcommits:1\n+\ttrees:0\n+\tblobs:0\n+\tEOF\n+\n+\tsort expect >expect.sorted &&\n+\tsort out >out.sorted &&\n+\n+\ttest_cmp expect.sorted out.sorted\n+'\n+\n+test_expect_success 'topic, not base, only trees' '\n+\ttest-tool path-walk --no-blobs --no-commits \\\n+\t\t-- topic --not base >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tcommits:0\n+\tTREE::$(git rev-parse topic^{tree})\n+\tTREE:left/:$(git rev-parse topic:left)\n+\tTREE:right/:$(git rev-parse topic:right)\n+\ttrees:3\n+\tblobs:0\n+\tEOF\n+\n+\tsort expect >expect.sorted &&\n+\tsort out >out.sorted &&\n+\n+\ttest_cmp expect.sorted out.sorted\n+'\n+\n test_expect_success 'topic, not base, boundary' '\n \ttest-tool path-walk -- --boundary topic --not base >out &&\n \n \tcat >expect <<-EOF &&\n+\tCOMMIT::$(git rev-parse topic)\n+\tCOMMIT::$(git rev-parse base~1)\n+\tcommits:2\n \tTREE::$(git rev-parse topic^{tree})\n \tTREE::$(git rev-parse base~1^{tree})\n \tTREE:left/:$(git rev-parse base~1:left)\n-- \ngitgitgadget\n\n"},{"id":"504430","messageId":"6f48cddadc0ad1a6dc3caec9d81e014b668c1f03.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 04/17] path-walk: allow visiting tags","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:11:50Z","receivedAt":"2024-10-08T14:12:11Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nIn anticipation of using the path-walk API to analyze tags or include\nthem in a pack-file, add the ability to walk the tags that were included\nin the revision walk.\n\nWhen these tag objects point to blobs or trees, we need to make sure\nthose objects are also visited. Treat tagged trees as root trees, but\nput the tagged blobs in their own category.\n\nBe careful about objects that are referred to by multiple references.\n\nCo-authored-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>\nSigned-off-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/technical/api-path-walk.txt |  2 +-\n path-walk.c                               | 76 ++++++++++++++++++++\n path-walk.h                               |  2 +\n t/helper/test-path-walk.c                 | 13 +++-\n t/t6601-path-walk.sh                      | 85 +++++++++++++++++++++--\n 5 files changed, 170 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/technical/api-path-walk.txt b/Documentation/technical/api-path-walk.txt\nindex b7ae476ea0a..5fea1d1db17 100644\n--- a/Documentation/technical/api-path-walk.txt\n+++ b/Documentation/technical/api-path-walk.txt\n@@ -48,7 +48,7 @@ If you want the path-walk API to emit `UNINTERESTING` objects based on the\n commit walk's boundary, be sure to set `revs.boundary` so the boundary\n commits are emitted.\n \n-`commits`, `blobs`, `trees`::\n+`commits`, `blobs`, `trees`, `tags`::\n \tBy default, these members are enabled and signal that the path-walk\n \tAPI should call the `path_fn` on objects of these types. Specialized\n \tapplications could disable some options to make it simpler to walk\ndiff --git a/path-walk.c b/path-walk.c\nindex 22e1aa13f31..7cd461adf47 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -13,6 +13,7 @@\n #include \"revision.h\"\n #include \"string-list.h\"\n #include \"strmap.h\"\n+#include \"tag.h\"\n #include \"trace2.h\"\n #include \"tree.h\"\n #include \"tree-walk.h\"\n@@ -204,13 +205,88 @@ int walk_objects_by_path(struct path_walk_info *info)\n \tCALLOC_ARRAY(commit_list, 1);\n \tcommit_list->type = OBJ_COMMIT;\n \n+\tif (info->tags)\n+\t\tinfo->revs->tag_objects = 1;\n+\n \t/* Insert a single list for the root tree into the paths. */\n \tCALLOC_ARRAY(root_tree_list, 1);\n \troot_tree_list->type = OBJ_TREE;\n \tstrmap_put(&ctx.paths_to_lists, root_path, root_tree_list);\n+\n+\t/*\n+\t * Set these values before preparing the walk to catch\n+\t * lightweight tags pointing to non-commits.\n+\t */\n+\tinfo->revs->blob_objects = info->blobs;\n+\tinfo->revs->tree_objects = info->trees;\n+\n \tif (prepare_revision_walk(info->revs))\n \t\tdie(_(\"failed to setup revision walk\"));\n \n+\tinfo->revs->blob_objects = info->revs->tree_objects = 0;\n+\n+\tif (info->tags) {\n+\t\tstruct oid_array tagged_blob_list = OID_ARRAY_INIT;\n+\t\tstruct oid_array tags = OID_ARRAY_INIT;\n+\n+\t\ttrace2_region_enter(\"path-walk\", \"tag-walk\", info->revs->repo);\n+\n+\t\t/*\n+\t\t * Walk any pending objects at this point, but they should only\n+\t\t * be tags.\n+\t\t */\n+\t\tfor (size_t i = 0; i < info->revs->pending.nr; i++) {\n+\t\t\tstruct object_array_entry *pending = info->revs->pending.objects + i;\n+\t\t\tstruct object *obj = pending->item;\n+\n+\t\t\tif (obj->type == OBJ_COMMIT || obj->flags & SEEN)\n+\t\t\t\tcontinue;\n+\n+\t\t\tobj->flags |= SEEN;\n+\n+\t\t\twhile (obj->type == OBJ_TAG) {\n+\t\t\t\tstruct tag *tag = lookup_tag(info->revs->repo,\n+\t\t\t\t\t\t\t     &obj->oid);\n+\t\t\t\tif (oid_array_lookup(&tags, &obj->oid) < 0)\n+\t\t\t\t\toid_array_append(&tags, &obj->oid);\n+\t\t\t\tobj = tag->tagged;\n+\t\t\t}\n+\n+\t\t\tswitch (obj->type) {\n+\t\t\tcase OBJ_TREE:\n+\t\t\t\tif (info->trees &&\n+\t\t\t\t    oid_array_lookup(&root_tree_list->oids, &obj->oid) < 0)\n+\t\t\t\t\toid_array_append(&root_tree_list->oids, &obj->oid);\n+\t\t\t\tbreak;\n+\n+\t\t\tcase OBJ_BLOB:\n+\t\t\t\tif (info->blobs &&\n+\t\t\t\t    oid_array_lookup(&tagged_blob_list, &obj->oid) < 0)\n+\t\t\t\t\toid_array_append(&tagged_blob_list, &obj->oid);\n+\t\t\t\tbreak;\n+\n+\t\t\tcase OBJ_COMMIT:\n+\t\t\t\t/* Make sure it is in the object walk */\n+\t\t\t\tadd_pending_object(info->revs, obj, \"\");\n+\t\t\t\tbreak;\n+\n+\t\t\tdefault:\n+\t\t\t\tBUG(\"should not see any other type here\");\n+\t\t\t}\n+\t\t}\n+\n+\t\tinfo->path_fn(\"\", &tags, OBJ_TAG, info->path_fn_data);\n+\n+\t\tif (tagged_blob_list.nr && info->blobs)\n+\t\t\tinfo->path_fn(\"/tagged-blobs\", &tagged_blob_list, OBJ_BLOB,\n+\t\t\t\t      info->path_fn_data);\n+\n+\t\ttrace2_data_intmax(\"path-walk\", ctx.repo, \"tags\", tags.nr);\n+\t\ttrace2_region_leave(\"path-walk\", \"tag-walk\", info->revs->repo);\n+\t\toid_array_clear(&tags);\n+\t\toid_array_clear(&tagged_blob_list);\n+\t}\n+\n \twhile ((c = get_revision(info->revs))) {\n \t\tstruct object_id *oid;\n \t\tstruct tree *t;\ndiff --git a/path-walk.h b/path-walk.h\nindex 6ef372d8942..3f3b63180ef 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -37,12 +37,14 @@ struct path_walk_info {\n \tint commits;\n \tint trees;\n \tint blobs;\n+\tint tags;\n };\n \n #define PATH_WALK_INFO_INIT {   \\\n \t.blobs = 1,\t\t\\\n \t.trees = 1,\t\t\\\n \t.commits = 1,\t\t\\\n+\t.tags = 1,\t\t\\\n }\n \n /**\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex 37c5e3e31e8..c6c60d68749 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -21,6 +21,7 @@ struct path_walk_test_data {\n \tuintmax_t commit_nr;\n \tuintmax_t tree_nr;\n \tuintmax_t blob_nr;\n+\tuintmax_t tag_nr;\n };\n \n static int emit_block(const char *path, struct oid_array *oids,\n@@ -45,6 +46,11 @@ static int emit_block(const char *path, struct oid_array *oids,\n \t\ttdata->blob_nr += oids->nr;\n \t\tbreak;\n \n+\tcase OBJ_TAG:\n+\t\ttypestr = \"TAG\";\n+\t\ttdata->tag_nr += oids->nr;\n+\t\tbreak;\n+\n \tdefault:\n \t\tBUG(\"we do not understand this type\");\n \t}\n@@ -66,6 +72,8 @@ int cmd__path_walk(int argc, const char **argv)\n \t\t\t N_(\"toggle inclusion of blob objects\")),\n \t\tOPT_BOOL(0, \"commits\", &info.commits,\n \t\t\t N_(\"toggle inclusion of commit objects\")),\n+\t\tOPT_BOOL(0, \"tags\", &info.tags,\n+\t\t\t N_(\"toggle inclusion of tag objects\")),\n \t\tOPT_BOOL(0, \"trees\", &info.trees,\n \t\t\t N_(\"toggle inclusion of tree objects\")),\n \t\tOPT_END(),\n@@ -92,8 +100,9 @@ int cmd__path_walk(int argc, const char **argv)\n \n \tprintf(\"commits:%\" PRIuMAX \"\\n\"\n \t       \"trees:%\" PRIuMAX \"\\n\"\n-\t       \"blobs:%\" PRIuMAX \"\\n\",\n-\t       data.commit_nr, data.tree_nr, data.blob_nr);\n+\t       \"blobs:%\" PRIuMAX \"\\n\"\n+\t       \"tags:%\" PRIuMAX \"\\n\",\n+\t       data.commit_nr, data.tree_nr, data.blob_nr, data.tag_nr);\n \n \treturn res;\n }\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex e4788664f93..7758e2529ee 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -7,24 +7,55 @@ test_description='direct path-walk API tests'\n test_expect_success 'setup test repository' '\n \tgit checkout -b base &&\n \n+\t# Make some objects that will only be reachable\n+\t# via non-commit tags.\n+\tmkdir child &&\n+\techo file >child/file &&\n+\tgit add child &&\n+\tgit commit -m \"will abandon\" &&\n+\tgit tag -a -m \"tree\" tree-tag HEAD^{tree} &&\n+\techo file2 >file2 &&\n+\tgit add file2 &&\n+\tgit commit --amend -m \"will abandon\" &&\n+\tgit tag tree-tag2 HEAD^{tree} &&\n+\n+\techo blob >file &&\n+\tblob_oid=$(git hash-object -t blob -w --stdin <file) &&\n+\tgit tag -a -m \"blob\" blob-tag \"$blob_oid\" &&\n+\techo blob2 >file2 &&\n+\tblob2_oid=$(git hash-object -t blob -w --stdin <file2) &&\n+\tgit tag blob-tag2 \"$blob2_oid\" &&\n+\n+\trm -fr child file file2 &&\n+\n \tmkdir left &&\n \tmkdir right &&\n \techo a >a &&\n \techo b >left/b &&\n \techo c >right/c &&\n \tgit add . &&\n-\tgit commit -m \"first\" &&\n+\tgit commit --amend -m \"first\" &&\n+\tgit tag -m \"first\" first HEAD &&\n \n \techo d >right/d &&\n \tgit add right &&\n \tgit commit -m \"second\" &&\n+\tgit tag -a -m \"second (under)\" second.1 HEAD &&\n+\tgit tag -a -m \"second (top)\" second.2 second.1 &&\n \n+\t# Set up file/dir collision in history.\n+\trm a &&\n+\tmkdir a &&\n+\techo a >a/a &&\n \techo bb >left/b &&\n-\tgit commit -a -m \"third\" &&\n+\tgit add a left &&\n+\tgit commit -m \"third\" &&\n+\tgit tag -a -m \"third\" third &&\n \n \tgit checkout -b topic HEAD~1 &&\n \techo cc >right/c &&\n-\tgit commit -a -m \"topic\"\n+\tgit commit -a -m \"topic\" &&\n+\tgit tag -a -m \"fourth\" fourth\n '\n \n test_expect_success 'all' '\n@@ -40,19 +71,35 @@ test_expect_success 'all' '\n \tTREE::$(git rev-parse base^{tree})\n \tTREE::$(git rev-parse base~1^{tree})\n \tTREE::$(git rev-parse base~2^{tree})\n+\tTREE::$(git rev-parse refs/tags/tree-tag^{})\n+\tTREE::$(git rev-parse refs/tags/tree-tag2^{})\n+\tTREE:a/:$(git rev-parse base:a)\n \tTREE:left/:$(git rev-parse base:left)\n \tTREE:left/:$(git rev-parse base~2:left)\n \tTREE:right/:$(git rev-parse topic:right)\n \tTREE:right/:$(git rev-parse base~1:right)\n \tTREE:right/:$(git rev-parse base~2:right)\n-\ttrees:9\n+\tTREE:child/:$(git rev-parse refs/tags/tree-tag^{}:child)\n+\ttrees:13\n \tBLOB:a:$(git rev-parse base~2:a)\n+\tBLOB:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n \tBLOB:left/b:$(git rev-parse base~2:left/b)\n \tBLOB:left/b:$(git rev-parse base:left/b)\n \tBLOB:right/c:$(git rev-parse base~2:right/c)\n \tBLOB:right/c:$(git rev-parse topic:right/c)\n \tBLOB:right/d:$(git rev-parse base~1:right/d)\n-\tblobs:6\n+\tBLOB:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\tBLOB:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\tBLOB:child/file:$(git rev-parse refs/tags/tree-tag^{}:child/file)\n+\tblobs:10\n+\tTAG::$(git rev-parse refs/tags/first)\n+\tTAG::$(git rev-parse refs/tags/second.1)\n+\tTAG::$(git rev-parse refs/tags/second.2)\n+\tTAG::$(git rev-parse refs/tags/third)\n+\tTAG::$(git rev-parse refs/tags/fourth)\n+\tTAG::$(git rev-parse refs/tags/tree-tag)\n+\tTAG::$(git rev-parse refs/tags/blob-tag)\n+\ttags:7\n \tEOF\n \n \tsort expect >expect.sorted &&\n@@ -83,6 +130,7 @@ test_expect_success 'topic only' '\n \tBLOB:right/c:$(git rev-parse topic:right/c)\n \tBLOB:right/d:$(git rev-parse base~1:right/d)\n \tblobs:5\n+\ttags:0\n \tEOF\n \n \tsort expect >expect.sorted &&\n@@ -106,6 +154,7 @@ test_expect_success 'topic, not base' '\n \tBLOB:right/c:$(git rev-parse topic:right/c)\n \tBLOB:right/d:$(git rev-parse topic:right/d)\n \tblobs:4\n+\ttags:0\n \tEOF\n \n \tsort expect >expect.sorted &&\n@@ -126,6 +175,7 @@ test_expect_success 'topic, not base, only blobs' '\n \tBLOB:right/c:$(git rev-parse topic:right/c)\n \tBLOB:right/d:$(git rev-parse topic:right/d)\n \tblobs:4\n+\ttags:0\n \tEOF\n \n \tsort expect >expect.sorted &&\n@@ -145,6 +195,7 @@ test_expect_success 'topic, not base, only commits' '\n \tcommits:1\n \ttrees:0\n \tblobs:0\n+\ttags:0\n \tEOF\n \n \tsort expect >expect.sorted &&\n@@ -164,6 +215,7 @@ test_expect_success 'topic, not base, only trees' '\n \tTREE:right/:$(git rev-parse topic:right)\n \ttrees:3\n \tblobs:0\n+\ttags:0\n \tEOF\n \n \tsort expect >expect.sorted &&\n@@ -191,6 +243,7 @@ test_expect_success 'topic, not base, boundary' '\n \tBLOB:right/c:$(git rev-parse topic:right/c)\n \tBLOB:right/d:$(git rev-parse base~1:right/d)\n \tblobs:5\n+\ttags:0\n \tEOF\n \n \tsort expect >expect.sorted &&\n@@ -199,4 +252,26 @@ test_expect_success 'topic, not base, boundary' '\n \ttest_cmp expect.sorted out.sorted\n '\n \n+test_expect_success 'trees are reported exactly once' '\n+\ttest_when_finished \"rm -rf unique-trees\" &&\n+\ttest_create_repo unique-trees &&\n+\t(\n+\t\tcd unique-trees &&\n+\t\tmkdir initial &&\n+\t\ttest_commit initial/file &&\n+\n+\t\tgit switch -c move-to-top &&\n+\t\tgit mv initial/file.t ./ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m moved &&\n+\n+\t\tgit update-ref refs/heads/other HEAD\n+\t) &&\n+\n+\ttest-tool -C unique-trees path-walk -- --all >out &&\n+\ttree=$(git -C unique-trees rev-parse HEAD:) &&\n+\tgrep \"$tree\" out >out-filtered &&\n+\ttest_line_count = 1 out-filtered\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"504431","messageId":"cd98447f7c8a3ee6b7566b12e00eaecd6e8c0c38.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 05/17] revision: create mark_trees_uninteresting_dense()","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:11:51Z","receivedAt":"2024-10-08T14:12:12Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe sparse tree walk algorithm was created in d5d2e93577e (revision:\nimplement sparse algorithm, 2019-01-16) and involves using the\nmark_trees_uninteresting_sparse() method. This method takes a repository\nand an oidset of tree IDs, some of which have the UNINTERESTING flag and\nsome of which do not.\n\nCreate a method that has an equivalent set of preconditions but uses a\n\"dense\" walk (recursively visits all reachable trees, as long as they\nhave not previously been marked UNINTERESTING). This is an important\ndifference from mark_tree_uninteresting(), which short-circuits if the\ngiven tree has the UNINTERESTING flag.\n\nA use of this method will be added in a later change, with a condition\nset whether the sparse or dense approach should be used.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n revision.c | 15 +++++++++++++++\n revision.h |  1 +\n 2 files changed, 16 insertions(+)\n\ndiff --git a/revision.c b/revision.c\nindex 2d7ad2bddff..bdc312f1538 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -219,6 +219,21 @@ static void add_children_by_path(struct repository *r,\n \tfree_tree_buffer(tree);\n }\n \n+void mark_trees_uninteresting_dense(struct repository *r,\n+\t\t\t\t    struct oidset *trees)\n+{\n+\tstruct object_id *oid;\n+\tstruct oidset_iter iter;\n+\n+\toidset_iter_init(trees, &iter);\n+\twhile ((oid = oidset_iter_next(&iter))) {\n+\t\tstruct tree *tree = lookup_tree(r, oid);\n+\n+\t\tif (tree && (tree->object.flags & UNINTERESTING))\n+\t\t\tmark_tree_contents_uninteresting(r, tree);\n+\t}\n+}\n+\n void mark_trees_uninteresting_sparse(struct repository *r,\n \t\t\t\t     struct oidset *trees)\n {\ndiff --git a/revision.h b/revision.h\nindex 71e984c452b..8938b2db112 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -487,6 +487,7 @@ void put_revision_mark(const struct rev_info *revs,\n \n void mark_parents_uninteresting(struct rev_info *revs, struct commit *commit);\n void mark_tree_uninteresting(struct repository *r, struct tree *tree);\n+void mark_trees_uninteresting_dense(struct repository *r, struct oidset *trees);\n void mark_trees_uninteresting_sparse(struct repository *r, struct oidset *trees);\n \n void show_object_with_name(FILE *, struct object *, const char *);\n-- \ngitgitgadget\n\n"},{"id":"504432","messageId":"214e10a99841f09361081a6a0f182f66f420aa09.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 06/17] path-walk: add prune_all_uninteresting option","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:11:52Z","receivedAt":"2024-10-08T14:12:12Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThis option causes the path-walk API to act like the sparse tree-walk\nalgorithm implemented by mark_trees_uninteresting_sparse() in\nlist-objects.c.\n\nStarting from the commits marked as UNINTERESTING, their root trees and\nall objects reachable from those trees are UNINTERSTING, at least as we\nwalk path-by-path. When we reach a path where all objects associated\nwith that path are marked UNINTERESTING, then do no continue walking the\nchildren of that path.\n\nWe need to be careful to pass the UNINTERESTING flag in a deep way on\nthe UNINTERESTING objects before we start the path-walk, or else the\ndepth-first search for the path-walk API may accidentally report some\nobjects as interesting.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/technical/api-path-walk.txt |  8 +++\n path-walk.c                               | 64 ++++++++++++++++++++++-\n path-walk.h                               |  8 +++\n t/helper/test-path-walk.c                 | 10 +++-\n t/t6601-path-walk.sh                      | 40 +++++++++++---\n 5 files changed, 118 insertions(+), 12 deletions(-)\n\ndiff --git a/Documentation/technical/api-path-walk.txt b/Documentation/technical/api-path-walk.txt\nindex 5fea1d1db17..c51f92cd649 100644\n--- a/Documentation/technical/api-path-walk.txt\n+++ b/Documentation/technical/api-path-walk.txt\n@@ -57,6 +57,14 @@ commits are emitted.\n While it is possible to walk only commits in this way, consumers would be\n better off using the revision walk API instead.\n \n+`prune_all_uninteresting`::\n+\tBy default, all reachable paths are emitted by the path-walk API.\n+\tThis option allows consumers to declare that they are not\n+\tinterested in paths where all included objects are marked with the\n+\t`UNINTERESTING` flag. This requires using the `boundary` option in\n+\tthe revision walk so that the walk emits commits marked with the\n+\t`UNINTERESTING` flag.\n+\n Examples\n --------\n \ndiff --git a/path-walk.c b/path-walk.c\nindex 7cd461adf47..dce0840937e 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -22,6 +22,7 @@ struct type_and_oid_list\n {\n \tenum object_type type;\n \tstruct oid_array oids;\n+\tint maybe_interesting;\n };\n \n #define TYPE_AND_OID_LIST_INIT { \\\n@@ -124,6 +125,8 @@ static int add_children(struct path_walk_context *ctx,\n \t\t\tstrmap_put(&ctx->paths_to_lists, path.buf, list);\n \t\t\tstring_list_append(&ctx->path_stack, path.buf);\n \t\t}\n+\t\tif (!(o->flags & UNINTERESTING))\n+\t\t\tlist->maybe_interesting = 1;\n \t\toid_array_append(&list->oids, &entry.oid);\n \t}\n \n@@ -145,6 +148,40 @@ static int walk_path(struct path_walk_context *ctx,\n \n \tlist = strmap_get(&ctx->paths_to_lists, path);\n \n+\tif (ctx->info->prune_all_uninteresting) {\n+\t\t/*\n+\t\t * This is true if all objects were UNINTERESTING\n+\t\t * when added to the list.\n+\t\t */\n+\t\tif (!list->maybe_interesting)\n+\t\t\treturn 0;\n+\n+\t\t/*\n+\t\t * But it's still possible that the objects were set\n+\t\t * as UNINTERESTING after being added. Do a quick check.\n+\t\t */\n+\t\tlist->maybe_interesting = 0;\n+\t\tfor (size_t i = 0;\n+\t\t     !list->maybe_interesting && i < list->oids.nr;\n+\t\t     i++) {\n+\t\t\tif (list->type == OBJ_TREE) {\n+\t\t\t\tstruct tree *t = lookup_tree(ctx->repo,\n+\t\t\t\t\t\t\t     &list->oids.oid[i]);\n+\t\t\t\tif (t && !(t->object.flags & UNINTERESTING))\n+\t\t\t\t\tlist->maybe_interesting = 1;\n+\t\t\t} else {\n+\t\t\t\tstruct blob *b = lookup_blob(ctx->repo,\n+\t\t\t\t\t\t\t     &list->oids.oid[i]);\n+\t\t\t\tif (b && !(b->object.flags & UNINTERESTING))\n+\t\t\t\t\tlist->maybe_interesting = 1;\n+\t\t\t}\n+\t\t}\n+\n+\t\t/* We have confirmed that all objects are UNINTERESTING. */\n+\t\tif (!list->maybe_interesting)\n+\t\t\treturn 0;\n+\t}\n+\n \t/* Evaluate function pointer on this data, if requested. */\n \tif ((list->type == OBJ_TREE && ctx->info->trees) ||\n \t    (list->type == OBJ_BLOB && ctx->info->blobs))\n@@ -187,7 +224,7 @@ static void clear_strmap(struct strmap *map)\n int walk_objects_by_path(struct path_walk_info *info)\n {\n \tconst char *root_path = \"\";\n-\tint ret = 0;\n+\tint ret = 0, has_uninteresting = 0;\n \tsize_t commits_nr = 0, paths_nr = 0;\n \tstruct commit *c;\n \tstruct type_and_oid_list *root_tree_list;\n@@ -199,6 +236,7 @@ int walk_objects_by_path(struct path_walk_info *info)\n \t\t.path_stack = STRING_LIST_INIT_DUP,\n \t\t.paths_to_lists = STRMAP_INIT\n \t};\n+\tstruct oidset root_tree_set = OIDSET_INIT;\n \n \ttrace2_region_enter(\"path-walk\", \"commit-walk\", info->revs->repo);\n \n@@ -211,6 +249,7 @@ int walk_objects_by_path(struct path_walk_info *info)\n \t/* Insert a single list for the root tree into the paths. */\n \tCALLOC_ARRAY(root_tree_list, 1);\n \troot_tree_list->type = OBJ_TREE;\n+\troot_tree_list->maybe_interesting = 1;\n \tstrmap_put(&ctx.paths_to_lists, root_path, root_tree_list);\n \n \t/*\n@@ -304,10 +343,16 @@ int walk_objects_by_path(struct path_walk_info *info)\n \t\tt = lookup_tree(info->revs->repo, oid);\n \n \t\tif (t) {\n+\t\t\tif ((c->object.flags & UNINTERESTING)) {\n+\t\t\t\tt->object.flags |= UNINTERESTING;\n+\t\t\t\thas_uninteresting = 1;\n+\t\t\t}\n+\n \t\t\tif (t->object.flags & SEEN)\n \t\t\t\tcontinue;\n \t\t\tt->object.flags |= SEEN;\n-\t\t\toid_array_append(&root_tree_list->oids, oid);\n+\t\t\tif (!oidset_insert(&root_tree_set, oid))\n+\t\t\t\toid_array_append(&root_tree_list->oids, oid);\n \t\t} else {\n \t\t\twarning(\"could not find tree %s\", oid_to_hex(oid));\n \t\t}\n@@ -323,6 +368,21 @@ int walk_objects_by_path(struct path_walk_info *info)\n \toid_array_clear(&commit_list->oids);\n \tfree(commit_list);\n \n+\t/*\n+\t * Before performing a DFS of our paths and emitting them as interesting,\n+\t * do a full walk of the trees to distribute the UNINTERESTING bit. Use\n+\t * the sparse algorithm if prune_all_uninteresting was set.\n+\t */\n+\tif (has_uninteresting) {\n+\t\ttrace2_region_enter(\"path-walk\", \"uninteresting-walk\", info->revs->repo);\n+\t\tif (info->prune_all_uninteresting)\n+\t\t\tmark_trees_uninteresting_sparse(ctx.repo, &root_tree_set);\n+\t\telse\n+\t\t\tmark_trees_uninteresting_dense(ctx.repo, &root_tree_set);\n+\t\ttrace2_region_leave(\"path-walk\", \"uninteresting-walk\", info->revs->repo);\n+\t}\n+\toidset_clear(&root_tree_set);\n+\n \tstring_list_append(&ctx.path_stack, root_path);\n \n \ttrace2_region_enter(\"path-walk\", \"path-walk\", info->revs->repo);\ndiff --git a/path-walk.h b/path-walk.h\nindex 3f3b63180ef..3e44c4b8a58 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -38,6 +38,14 @@ struct path_walk_info {\n \tint trees;\n \tint blobs;\n \tint tags;\n+\n+\t/**\n+\t * When 'prune_all_uninteresting' is set and a path has all objects\n+\t * marked as UNINTERESTING, then the path-walk will not visit those\n+\t * objects. It will not call path_fn on those objects and will not\n+\t * walk the children of such trees.\n+\t */\n+\tint prune_all_uninteresting;\n };\n \n #define PATH_WALK_INFO_INIT {   \\\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex c6c60d68749..06b103d8760 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -55,8 +55,12 @@ static int emit_block(const char *path, struct oid_array *oids,\n \t\tBUG(\"we do not understand this type\");\n \t}\n \n-\tfor (size_t i = 0; i < oids->nr; i++)\n-\t\tprintf(\"%s:%s:%s\\n\", typestr, path, oid_to_hex(&oids->oid[i]));\n+\tfor (size_t i = 0; i < oids->nr; i++) {\n+\t\tstruct object *o = lookup_unknown_object(the_repository,\n+\t\t\t\t\t\t\t &oids->oid[i]);\n+\t\tprintf(\"%s:%s:%s%s\\n\", typestr, path, oid_to_hex(&oids->oid[i]),\n+\t\t       o->flags & UNINTERESTING ? \":UNINTERESTING\" : \"\");\n+\t}\n \n \treturn 0;\n }\n@@ -76,6 +80,8 @@ int cmd__path_walk(int argc, const char **argv)\n \t\t\t N_(\"toggle inclusion of tag objects\")),\n \t\tOPT_BOOL(0, \"trees\", &info.trees,\n \t\t\t N_(\"toggle inclusion of tree objects\")),\n+\t\tOPT_BOOL(0, \"prune\", &info.prune_all_uninteresting,\n+\t\t\t N_(\"toggle pruning of uninteresting paths\")),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 7758e2529ee..943adc6c8f1 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -229,19 +229,19 @@ test_expect_success 'topic, not base, boundary' '\n \n \tcat >expect <<-EOF &&\n \tCOMMIT::$(git rev-parse topic)\n-\tCOMMIT::$(git rev-parse base~1)\n+\tCOMMIT::$(git rev-parse base~1):UNINTERESTING\n \tcommits:2\n \tTREE::$(git rev-parse topic^{tree})\n-\tTREE::$(git rev-parse base~1^{tree})\n-\tTREE:left/:$(git rev-parse base~1:left)\n+\tTREE::$(git rev-parse base~1^{tree}):UNINTERESTING\n+\tTREE:left/:$(git rev-parse base~1:left):UNINTERESTING\n \tTREE:right/:$(git rev-parse topic:right)\n-\tTREE:right/:$(git rev-parse base~1:right)\n+\tTREE:right/:$(git rev-parse base~1:right):UNINTERESTING\n \ttrees:5\n-\tBLOB:a:$(git rev-parse base~1:a)\n-\tBLOB:left/b:$(git rev-parse base~1:left/b)\n-\tBLOB:right/c:$(git rev-parse base~1:right/c)\n+\tBLOB:a:$(git rev-parse base~1:a):UNINTERESTING\n+\tBLOB:left/b:$(git rev-parse base~1:left/b):UNINTERESTING\n+\tBLOB:right/c:$(git rev-parse base~1:right/c):UNINTERESTING\n \tBLOB:right/c:$(git rev-parse topic:right/c)\n-\tBLOB:right/d:$(git rev-parse base~1:right/d)\n+\tBLOB:right/d:$(git rev-parse base~1:right/d):UNINTERESTING\n \tblobs:5\n \ttags:0\n \tEOF\n@@ -252,6 +252,30 @@ test_expect_success 'topic, not base, boundary' '\n \ttest_cmp expect.sorted out.sorted\n '\n \n+test_expect_success 'topic, not base, boundary with pruning' '\n+\ttest-tool path-walk --prune -- --boundary topic --not base >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tCOMMIT::$(git rev-parse topic)\n+\tCOMMIT::$(git rev-parse base~1):UNINTERESTING\n+\tcommits:2\n+\tTREE::$(git rev-parse topic^{tree})\n+\tTREE::$(git rev-parse base~1^{tree}):UNINTERESTING\n+\tTREE:right/:$(git rev-parse topic:right)\n+\tTREE:right/:$(git rev-parse base~1:right):UNINTERESTING\n+\ttrees:4\n+\tBLOB:right/c:$(git rev-parse base~1:right/c):UNINTERESTING\n+\tBLOB:right/c:$(git rev-parse topic:right/c)\n+\tblobs:2\n+\ttags:0\n+\tEOF\n+\n+\tsort expect >expect.sorted &&\n+\tsort out >out.sorted &&\n+\n+\ttest_cmp expect.sorted out.sorted\n+'\n+\n test_expect_success 'trees are reported exactly once' '\n \ttest_when_finished \"rm -rf unique-trees\" &&\n \ttest_create_repo unique-trees &&\n-- \ngitgitgadget\n\n"},{"id":"504433","messageId":"cd360ad104017ceaa9fb43ae036be79f767b557b.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 07/17] pack-objects: extract should_attempt_deltas()","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:11:53Z","receivedAt":"2024-10-08T14:12:13Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThis will be helpful in a future change that introduces a new way to\ncompute deltas.\n\nBe careful to preserve the nr_deltas counting logic in the existing\nmethod, but take the rest of the logic wholesale.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/pack-objects.c | 53 +++++++++++++++++++++++-------------------\n 1 file changed, 29 insertions(+), 24 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 0fc0680b402..82f4ca04000 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -3167,6 +3167,33 @@ static int add_ref_tag(const char *tag UNUSED, const char *referent UNUSED, cons\n \treturn 0;\n }\n \n+static int should_attempt_deltas(struct object_entry *entry)\n+{\n+\tif (DELTA(entry))\n+\t\treturn 0;\n+\n+\tif (!entry->type_valid ||\n+\t    oe_size_less_than(&to_pack, entry, 50))\n+\t\treturn 0;\n+\n+\tif (entry->no_try_delta)\n+\t\treturn 0;\n+\n+\tif (!entry->preferred_base) {\n+\t\tif (oe_type(entry) < 0)\n+\t\t\tdie(_(\"unable to get type of object %s\"),\n+\t\t\t\toid_to_hex(&entry->idx.oid));\n+\t} else if (oe_type(entry) < 0) {\n+\t\t/*\n+\t\t * This object is not found, but we\n+\t\t * don't have to include it anyway.\n+\t\t */\n+\t\treturn 0;\n+\t}\n+\n+\treturn 1;\n+}\n+\n static void prepare_pack(int window, int depth)\n {\n \tstruct object_entry **delta_list;\n@@ -3197,33 +3224,11 @@ static void prepare_pack(int window, int depth)\n \tfor (i = 0; i < to_pack.nr_objects; i++) {\n \t\tstruct object_entry *entry = to_pack.objects + i;\n \n-\t\tif (DELTA(entry))\n-\t\t\t/* This happens if we decided to reuse existing\n-\t\t\t * delta from a pack.  \"reuse_delta &&\" is implied.\n-\t\t\t */\n-\t\t\tcontinue;\n-\n-\t\tif (!entry->type_valid ||\n-\t\t    oe_size_less_than(&to_pack, entry, 50))\n+\t\tif (!should_attempt_deltas(entry))\n \t\t\tcontinue;\n \n-\t\tif (entry->no_try_delta)\n-\t\t\tcontinue;\n-\n-\t\tif (!entry->preferred_base) {\n+\t\tif (!entry->preferred_base)\n \t\t\tnr_deltas++;\n-\t\t\tif (oe_type(entry) < 0)\n-\t\t\t\tdie(_(\"unable to get type of object %s\"),\n-\t\t\t\t    oid_to_hex(&entry->idx.oid));\n-\t\t} else {\n-\t\t\tif (oe_type(entry) < 0) {\n-\t\t\t\t/*\n-\t\t\t\t * This object is not found, but we\n-\t\t\t\t * don't have to include it anyway.\n-\t\t\t\t */\n-\t\t\t\tcontinue;\n-\t\t\t}\n-\t\t}\n \n \t\tdelta_list[n++] = entry;\n \t}\n-- \ngitgitgadget\n\n"},{"id":"504434","messageId":"f8ee11d3003e743affb39835ba3583dd2498e576.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 08/17] pack-objects: add --path-walk option","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:11:54Z","receivedAt":"2024-10-08T14:12:15Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nIn order to more easily compute delta bases among objects that appear at the\nexact same path, add a --path-walk option to 'git pack-objects'.\n\nThis option will use the path-walk API instead of the object walk given by\nthe revision machinery. Since objects will be provided in batches\nrepresenting a common path, those objects can be tested for delta bases\nimmediately instead of waiting for a sort of the full object list by\nname-hash. This has multiple benefits, including avoiding collisions by\nname-hash.\n\nThe objects marked as UNINTERESTING are included in these batches, so we\nare guaranteeing some locality to find good delta bases.\n\nAfter the individual passes are done on a per-path basis, the default\nname-hash is used to find other opportunistic delta bases that did not\nmatch exactly by the full path name.\n\nThe current implementation performs delta calculations while walking\nobjects, which is not ideal for a few reasons. First, this will cause\nthe \"Enumerating objects\" phase to be much longer than usual. Second, it\ndoes not take advantage of threading during the path-scoped delta\ncalculations. Even with this lack of threading, the path-walk option is\nsometimes faster than the usual approach. Future changes will refactor\nthis code to allow for threading, but that complexity is deferred until\nlater to keep this patch as simple as possible.\n\nThis new walk is incompatible with some features and is ignored by\nothers:\n\n * Object filters are not currently integrated with the path-walk API,\n   such as sparse-checkout or tree depth. A blobless packfile could be\n   integrated easily, but that is deferred for later.\n\n * Server-focused features such as delta islands, shallow packs, and\n   using a bitmap index are incompatible with the path-walk API.\n\n * The path walk API is only compatible with the --revs option, not\n   taking object lists or pack lists over stdin. These alternative ways\n   to specify the objects currently ignores the --path-walk option\n   without even a warning.\n\nFuture changes will create performance tests that demonstrate the power\nof this approach.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.txt        |  13 +-\n Documentation/technical/api-path-walk.txt |   3 +-\n builtin/pack-objects.c                    | 147 ++++++++++++++++++++--\n t/t5300-pack-object.sh                    |  17 +++\n 4 files changed, 169 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.txt b/Documentation/git-pack-objects.txt\nindex e32404c6aae..f2fda800a43 100644\n--- a/Documentation/git-pack-objects.txt\n+++ b/Documentation/git-pack-objects.txt\n@@ -15,7 +15,8 @@ SYNOPSIS\n \t[--revs [--unpacked | --all]] [--keep-pack=<pack-name>]\n \t[--cruft] [--cruft-expiration=<time>]\n \t[--stdout [--filter=<filter-spec>] | <base-name>]\n-\t[--shallow] [--keep-true-parents] [--[no-]sparse] < <object-list>\n+\t[--shallow] [--keep-true-parents] [--[no-]sparse]\n+\t[--path-walk] < <object-list>\n \n \n DESCRIPTION\n@@ -345,6 +346,16 @@ raise an error.\n \tRestrict delta matches based on \"islands\". See DELTA ISLANDS\n \tbelow.\n \n+--path-walk::\n+\tBy default, `git pack-objects` walks objects in an order that\n+\tpresents trees and blobs in an order unrelated to the path they\n+\tappear relative to a commit's root tree. The `--path-walk` option\n+\tenables a different walking algorithm that organizes trees and\n+\tblobs by path. This has the potential to improve delta compression\n+\tespecially in the presence of filenames that cause collisions in\n+\tGit's default name-hash algorithm. Due to changing how the objects\n+\tare walked, this option is not compatible with `--delta-islands`,\n+\t`--shallow`, or `--filter`.\n \n DELTA ISLANDS\n -------------\ndiff --git a/Documentation/technical/api-path-walk.txt b/Documentation/technical/api-path-walk.txt\nindex c51f92cd649..2d25281774d 100644\n--- a/Documentation/technical/api-path-walk.txt\n+++ b/Documentation/technical/api-path-walk.txt\n@@ -69,4 +69,5 @@ Examples\n --------\n \n See example usages in:\n-\t`t/helper/test-path-walk.c`\n+\t`t/helper/test-path-walk.c`,\n+\t`builtin/pack-objects.c`\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 82f4ca04000..103263666f6 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -39,6 +39,9 @@\n #include \"promisor-remote.h\"\n #include \"pack-mtimes.h\"\n #include \"parse-options.h\"\n+#include \"blob.h\"\n+#include \"tree.h\"\n+#include \"path-walk.h\"\n \n /*\n  * Objects we are going to pack are collected in the `to_pack` structure.\n@@ -215,6 +218,7 @@ static int delta_search_threads;\n static int pack_to_stdout;\n static int sparse;\n static int thin;\n+static int path_walk;\n static int num_preferred_base;\n static struct progress *progress_state;\n \n@@ -4143,6 +4147,105 @@ static void mark_bitmap_preferred_tips(void)\n \t}\n }\n \n+static inline int is_oid_interesting(struct repository *repo,\n+\t\t\t\t     struct object_id *oid)\n+{\n+\tstruct object *o = lookup_object(repo, oid);\n+\treturn o && !(o->flags & UNINTERESTING);\n+}\n+\n+static int add_objects_by_path(const char *path,\n+\t\t\t       struct oid_array *oids,\n+\t\t\t       enum object_type type,\n+\t\t\t       void *data)\n+{\n+\tstruct object_entry **delta_list;\n+\tsize_t oe_start = to_pack.nr_objects;\n+\tsize_t oe_end;\n+\tunsigned int sub_list_size;\n+\tunsigned int *processed = data;\n+\n+\t/*\n+\t * First, add all objects to the packing data, including the ones\n+\t * marked UNINTERESTING (translated to 'exclude') as they can be\n+\t * used as delta bases.\n+\t */\n+\tfor (size_t i = 0; i < oids->nr; i++) {\n+\t\tint exclude;\n+\t\tstruct object_info oi = OBJECT_INFO_INIT;\n+\t\tstruct object_id *oid = &oids->oid[i];\n+\n+\t\t/* Skip objects that do not exist locally. */\n+\t\tif (exclude_promisor_objects &&\n+\t\t    oid_object_info_extended(the_repository, oid, &oi,\n+\t\t\t\t\t     OBJECT_INFO_FOR_PREFETCH) < 0)\n+\t\t\tcontinue;\n+\n+\t\texclude = !is_oid_interesting(the_repository, oid);\n+\n+\t\tif (exclude && !thin)\n+\t\t\tcontinue;\n+\n+\t\tadd_object_entry(oid, type, path, exclude);\n+\t}\n+\n+\toe_end = to_pack.nr_objects;\n+\n+\t/* We can skip delta calculations if it is a no-op. */\n+\tif (oe_end == oe_start || !window)\n+\t\treturn 0;\n+\n+\tsub_list_size = 0;\n+\tALLOC_ARRAY(delta_list, oe_end - oe_start);\n+\n+\tfor (size_t i = 0; i < oe_end - oe_start; i++) {\n+\t\tstruct object_entry *entry = to_pack.objects + oe_start + i;\n+\n+\t\tif (!should_attempt_deltas(entry))\n+\t\t\tcontinue;\n+\n+\t\tdelta_list[sub_list_size++] = entry;\n+\t}\n+\n+\t/*\n+\t * Find delta bases among this list of objects that all match the same\n+\t * path. This causes the delta compression to be interleaved in the\n+\t * object walk, which can lead to confusing progress indicators. This is\n+\t * also incompatible with threaded delta calculations. In the future,\n+\t * consider creating a list of regions in the full to_pack.objects array\n+\t * that could be picked up by the threaded delta computation.\n+\t */\n+\tif (sub_list_size && window) {\n+\t\tQSORT(delta_list, sub_list_size, type_size_sort);\n+\t\tfind_deltas(delta_list, &sub_list_size, window, depth, processed);\n+\t}\n+\n+\tfree(delta_list);\n+\treturn 0;\n+}\n+\n+static void get_object_list_path_walk(struct rev_info *revs)\n+{\n+\tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n+\tunsigned int processed = 0;\n+\n+\tinfo.revs = revs;\n+\tinfo.path_fn = add_objects_by_path;\n+\tinfo.path_fn_data = &processed;\n+\trevs->tag_objects = 1;\n+\n+\t/*\n+\t * Allow the --[no-]sparse option to be interesting here, if only\n+\t * for testing purposes. Paths with no interesting objects will not\n+\t * contribute to the resulting pack, but only create noisy preferred\n+\t * base objects.\n+\t */\n+\tinfo.prune_all_uninteresting = sparse;\n+\n+\tif (walk_objects_by_path(&info))\n+\t\tdie(_(\"failed to pack objects via path-walk\"));\n+}\n+\n static void get_object_list(struct rev_info *revs, int ac, const char **av)\n {\n \tstruct setup_revision_opt s_r_opt = {\n@@ -4189,7 +4292,7 @@ static void get_object_list(struct rev_info *revs, int ac, const char **av)\n \n \twarn_on_object_refname_ambiguity = save_warning;\n \n-\tif (use_bitmap_index && !get_object_list_from_bitmap(revs))\n+\tif (use_bitmap_index && !path_walk && !get_object_list_from_bitmap(revs))\n \t\treturn;\n \n \tif (use_delta_islands)\n@@ -4198,15 +4301,19 @@ static void get_object_list(struct rev_info *revs, int ac, const char **av)\n \tif (write_bitmap_index)\n \t\tmark_bitmap_preferred_tips();\n \n-\tif (prepare_revision_walk(revs))\n-\t\tdie(_(\"revision walk setup failed\"));\n-\tmark_edges_uninteresting(revs, show_edge, sparse);\n-\n \tif (!fn_show_object)\n \t\tfn_show_object = show_object;\n-\ttraverse_commit_list(revs,\n-\t\t\t     show_commit, fn_show_object,\n-\t\t\t     NULL);\n+\n+\tif (path_walk) {\n+\t\tget_object_list_path_walk(revs);\n+\t} else {\n+\t\tif (prepare_revision_walk(revs))\n+\t\t\tdie(_(\"revision walk setup failed\"));\n+\t\tmark_edges_uninteresting(revs, show_edge, sparse);\n+\t\ttraverse_commit_list(revs,\n+\t\t\t\tshow_commit, fn_show_object,\n+\t\t\t\tNULL);\n+\t}\n \n \tif (unpack_unreachable_expiration) {\n \t\trevs->ignore_missing_links = 1;\n@@ -4404,6 +4511,8 @@ int cmd_pack_objects(int argc,\n \t\t\t N_(\"use the sparse reachability algorithm\")),\n \t\tOPT_BOOL(0, \"thin\", &thin,\n \t\t\t N_(\"create thin packs\")),\n+\t\tOPT_BOOL(0, \"path-walk\", &path_walk,\n+\t\t\t N_(\"use the path-walk API to walk objects when possible\")),\n \t\tOPT_BOOL(0, \"shallow\", &shallow,\n \t\t\t N_(\"create packs suitable for shallow fetches\")),\n \t\tOPT_BOOL(0, \"honor-pack-keep\", &ignore_packed_keep_on_disk,\n@@ -4484,7 +4593,27 @@ int cmd_pack_objects(int argc,\n \t\twindow = 0;\n \n \tstrvec_push(&rp, \"pack-objects\");\n-\tif (thin) {\n+\n+\tif (path_walk && filter_options.choice) {\n+\t\twarning(_(\"cannot use --filter with --path-walk\"));\n+\t\tpath_walk = 0;\n+\t}\n+\tif (path_walk && use_delta_islands) {\n+\t\twarning(_(\"cannot use delta islands with --path-walk\"));\n+\t\tpath_walk = 0;\n+\t}\n+\tif (path_walk && shallow) {\n+\t\twarning(_(\"cannot use --shallow with --path-walk\"));\n+\t\tpath_walk = 0;\n+\t}\n+\tif (path_walk) {\n+\t\tstrvec_push(&rp, \"--boundary\");\n+\t\t /*\n+\t\t  * We must disable the bitmaps because we are removing\n+\t\t  * the --objects / --objects-edge[-aggressive] options.\n+\t\t  */\n+\t\tuse_bitmap_index = 0;\n+\t} else if (thin) {\n \t\tuse_internal_rev_list = 1;\n \t\tstrvec_push(&rp, shallow\n \t\t\t\t? \"--objects-edge-aggressive\"\ndiff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh\nindex 3b9dae331a5..5f6914acae7 100755\n--- a/t/t5300-pack-object.sh\n+++ b/t/t5300-pack-object.sh\n@@ -674,4 +674,21 @@ do\n \t'\n done\n \n+# Basic \"repack everything\" test\n+test_expect_success '--path-walk pack everything' '\n+\tgit -C server rev-parse HEAD >in &&\n+\tgit -C server pack-objects --stdout --revs --path-walk <in >out.pack &&\n+\tgit -C server index-pack --stdin <out.pack\n+'\n+\n+# Basic \"thin pack\" test\n+test_expect_success '--path-walk thin pack' '\n+\tcat >in <<-EOF &&\n+\t$(git -C server rev-parse HEAD)\n+\t^$(git -C server rev-parse HEAD~2)\n+\tEOF\n+\tgit -C server pack-objects --thin --stdout --revs --path-walk <in >out.pack &&\n+\tgit -C server index-pack --fix-thin --stdin <out.pack\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"504435","messageId":"eaeb40980f4049c790d477d4e6a9860c471c73b2.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 09/17] pack-objects: update usage to match docs","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:11:55Z","receivedAt":"2024-10-08T14:12:16Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe t0450 test script verifies that builtin usage matches the synopsis\nin the documentation. Adjust the builtin to match and then remove 'git\npack-objects' from the exception list.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.txt | 14 +++++++-------\n builtin/pack-objects.c             | 10 ++++++++--\n t/t0450/txt-help-mismatches        |  1 -\n 3 files changed, 15 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.txt b/Documentation/git-pack-objects.txt\nindex f2fda800a43..68d86ed8838 100644\n--- a/Documentation/git-pack-objects.txt\n+++ b/Documentation/git-pack-objects.txt\n@@ -10,13 +10,13 @@ SYNOPSIS\n --------\n [verse]\n 'git pack-objects' [-q | --progress | --all-progress] [--all-progress-implied]\n-\t[--no-reuse-delta] [--delta-base-offset] [--non-empty]\n-\t[--local] [--incremental] [--window=<n>] [--depth=<n>]\n-\t[--revs [--unpacked | --all]] [--keep-pack=<pack-name>]\n-\t[--cruft] [--cruft-expiration=<time>]\n-\t[--stdout [--filter=<filter-spec>] | <base-name>]\n-\t[--shallow] [--keep-true-parents] [--[no-]sparse]\n-\t[--path-walk] < <object-list>\n+\t\t   [--no-reuse-delta] [--delta-base-offset] [--non-empty]\n+\t\t   [--local] [--incremental] [--window=<n>] [--depth=<n>]\n+\t\t   [--revs [--unpacked | --all]] [--keep-pack=<pack-name>]\n+\t\t   [--cruft] [--cruft-expiration=<time>]\n+\t\t   [--stdout [--filter=<filter-spec>] | <base-name>]\n+\t\t   [--shallow] [--keep-true-parents] [--[no-]sparse]\n+\t\t   [--path-walk] < <object-list>\n \n \n DESCRIPTION\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 103263666f6..77fb1217b2e 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -185,8 +185,14 @@ static inline void oe_set_delta_size(struct packing_data *pack,\n #define SET_DELTA_SIBLING(obj, val) oe_set_delta_sibling(&to_pack, obj, val)\n \n static const char *pack_usage[] = {\n-\tN_(\"git pack-objects --stdout [<options>] [< <ref-list> | < <object-list>]\"),\n-\tN_(\"git pack-objects [<options>] <base-name> [< <ref-list> | < <object-list>]\"),\n+\tN_(\"git pack-objects [-q | --progress | --all-progress] [--all-progress-implied]\\n\"\n+\t   \"                 [--no-reuse-delta] [--delta-base-offset] [--non-empty]\\n\"\n+\t   \"                 [--local] [--incremental] [--window=<n>] [--depth=<n>]\\n\"\n+\t   \"                 [--revs [--unpacked | --all]] [--keep-pack=<pack-name>]\\n\"\n+\t   \"                 [--cruft] [--cruft-expiration=<time>]\\n\"\n+\t   \"                 [--stdout [--filter=<filter-spec>] | <base-name>]\\n\"\n+\t   \"                 [--shallow] [--keep-true-parents] [--[no-]sparse]\\n\"\n+\t   \"                 [--path-walk] < <object-list>\"),\n \tNULL\n };\n \ndiff --git a/t/t0450/txt-help-mismatches b/t/t0450/txt-help-mismatches\nindex 28003f18c92..285ae81a6b5 100644\n--- a/t/t0450/txt-help-mismatches\n+++ b/t/t0450/txt-help-mismatches\n@@ -38,7 +38,6 @@ merge-one-file\n multi-pack-index\n name-rev\n notes\n-pack-objects\n push\n range-diff\n rebase\n-- \ngitgitgadget\n\n"},{"id":"504436","messageId":"3113ead1e0168eeec5c3be1199918b80fa845264.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 10/17] p5313: add performance tests for --path-walk","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:11:56Z","receivedAt":"2024-10-08T14:12:16Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe previous change added a --path-walk option to 'git pack-objects'.\nCreate a performance test that demonstrates the time and space benefits\nof the feature.\n\nIn order to get an appropriate comparison, we need to avoid reusing\ndeltas and recompute them from scratch.\n\nCompare the creation of a thin pack representing a small push and the\ncreation of a relatively large non-thin pack.\n\nRunning on my copy of the Git repository results in this data:\n\nTest                                      this tree\n---------------------------------------------------------\n5313.2: thin pack                         0.01(0.00+0.00)\n5313.3: thin pack size                               1.1K\n5313.4: thin pack with --path-walk        0.01(0.01+0.00)\n5313.5: thin pack size with --path-walk              1.1K\n5313.6: big pack                          2.52(6.59+0.38)\n5313.7: big pack size                               14.1M\n5313.8: big pack with --path-walk         4.90(5.76+0.26)\n5313.9: big pack size with --path-walk              13.2M\n\nNote that the timing is slower because there is no threading in the\n--path-walk case (yet).\n\nThe cases where the --path-walk option really shines is when the default\nname-hash is overwhelmed with collisions. An open source example can be\nfound in the microsoft/fluentui repo [1] at a certain commit [2].\n\n[1] https://github.com/microsoft/fluentui\n[2] e70848ebac1cd720875bccaa3026f4a9ed700e08\n\nRunning the tests on this repo results in the following output:\n\nTest                                      this tree\n----------------------------------------------------------\n5313.2: thin pack                         0.28(0.38+0.02)\n5313.3: thin pack size                               1.2M\n5313.4: thin pack with --path-walk        0.08(0.06+0.01)\n5313.5: thin pack size with --path-walk             18.4K\n5313.6: big pack                          4.05(29.62+0.43)\n5313.7: big pack size                               20.0M\n5313.8: big pack with --path-walk         5.99(9.06+0.24)\n5313.9: big pack size with --path-walk              16.4M\n\nNotice in particular that in the small thin pack, the time performance\nhas improved from 0.28s to 0.08s and this is likely due to the improved\nsize of the resulting pack: 18.4K instead of 1.2M.\n\nFinally, running this on a copy of the Linux kernel repository results\nin these data points:\n\nTest                                      this tree\n-----------------------------------------------------------\n5313.2: thin pack                         0.00(0.00+0.00)\n5313.3: thin pack size                               5.8K\n5313.4: thin pack with --path-walk        0.00(0.01+0.00)\n5313.5: thin pack size with --path-walk              5.8K\n5313.6: big pack                          24.39(65.81+1.31)\n5313.7: big pack size                              155.7M\n5313.8: big pack with --path-walk         41.07(60.69+0.68)\n5313.9: big pack size with --path-walk             150.8M\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n t/perf/p5313-pack-objects.sh | 59 ++++++++++++++++++++++++++++++++++++\n 1 file changed, 59 insertions(+)\n create mode 100755 t/perf/p5313-pack-objects.sh\n\ndiff --git a/t/perf/p5313-pack-objects.sh b/t/perf/p5313-pack-objects.sh\nnew file mode 100755\nindex 00000000000..840075f5691\n--- /dev/null\n+++ b/t/perf/p5313-pack-objects.sh\n@@ -0,0 +1,59 @@\n+#!/bin/sh\n+\n+test_description='Tests pack performance using bitmaps'\n+. ./perf-lib.sh\n+\n+GIT_TEST_PASSING_SANITIZE_LEAK=0\n+export GIT_TEST_PASSING_SANITIZE_LEAK\n+\n+test_perf_large_repo\n+\n+test_expect_success 'create rev input' '\n+\tcat >in-thin <<-EOF &&\n+\t$(git rev-parse HEAD)\n+\t^$(git rev-parse HEAD~1)\n+\tEOF\n+\n+\tcat >in-big <<-EOF\n+\t$(git rev-parse HEAD)\n+\t^$(git rev-parse HEAD~1000)\n+\tEOF\n+'\n+\n+test_perf 'thin pack' '\n+\tgit pack-objects --thin --stdout --no-reuse-delta \\\n+\t\t--revs --sparse <in-thin >out\n+'\n+\n+test_size 'thin pack size' '\n+\ttest_file_size out\n+'\n+\n+test_perf 'thin pack with --path-walk' '\n+\tgit pack-objects --thin --stdout --no-reuse-delta \\\n+\t\t--revs --sparse --path-walk <in-thin >out\n+'\n+\n+test_size 'thin pack size with --path-walk' '\n+\ttest_file_size out\n+'\n+\n+test_perf 'big pack' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs \\\n+\t\t--sparse <in-big >out\n+'\n+\n+test_size 'big pack size' '\n+\ttest_file_size out\n+'\n+\n+test_perf 'big pack with --path-walk' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs \\\n+\t\t--sparse --path-walk <in-big >out\n+'\n+\n+test_size 'big pack size with --path-walk' '\n+\ttest_file_size out\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"504437","messageId":"211a16ae889b23423dc23bc01f223eb9c2c684f6.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 11/17] pack-objects: introduce GIT_TEST_PACK_PATH_WALK","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:11:57Z","receivedAt":"2024-10-08T14:12:17Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThere are many tests that validate whether 'git pack-objects' works as\nexpected. Instead of duplicating these tests, add a new test environment\nvariable, GIT_TEST_PACK_PATH_WALK, that implies --path-walk by default\nwhen specified.\n\nThis was useful in testing the implementation of the --path-walk\nimplementation, especially in conjunction with test such as:\n\n - t0411-clone-from-partial.sh : One test fetches from a repo that does\n   not have the boundary objects. This causes the path-based walk to\n   fail. Disable the variable for this test.\n\n - t5306-pack-nobase.sh : Similar to t0411, one test fetches from a repo\n   without a boundary object.\n\n - t5310-pack-bitmaps.sh : One test compares the case when packing with\n   bitmaps to the case when packing without them. Since we disable the\n   test variable when writing bitmaps, this causes a difference in the\n   object list (the --path-walk option adds an extra object). Specify\n   --no-path-walk in both processes for the comparison. Another test\n   checks for a specific delta base, but when computing dynamically\n   without using bitmaps, the base object it too small to be considered\n   in the delta calculations so no base is used.\n\n - t5316-pack-delta-depth.sh : This script cares about certain delta\n   choices and their chain lengths. The --path-walk option changes how\n   these chains are selected, and thus changes the results of this test.\n\n - t5322-pack-objects-sparse.sh : This demonstrates the effectiveness of\n   the --sparse option and how it combines with --path-walk.\n\n - t5332-multi-pack-reuse.sh : This test verifies that the preferred\n   pack is used for delta reuse when possible. The --path-walk option is\n   not currently aware of the preferred pack at all, so finds a\n   different delta base.\n\n - t7406-submodule-update.sh : When using the variable, the --depth\n   option collides with the --path-walk feature, resulting in a warning\n   message. Disable the variable so this warning does not appear.\n\nI want to call out one specific test change that is only temporary:\n\n - t5530-upload-pack-error.sh : One test cares specifically about an\n   \"unable to read\" error message. Since the current implementation\n   performs delta calculations within the path-walk API callback, a\n   different \"unable to get size\" error message appears. When this\n   is changed in a future refactoring, this test change can be reverted.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/pack-objects.c        | 12 ++++++++++--\n ci/run-build-and-tests.sh     |  1 +\n t/README                      |  4 ++++\n t/t0411-clone-from-partial.sh |  6 ++++++\n t/t5306-pack-nobase.sh        |  5 +++++\n t/t5310-pack-bitmaps.sh       | 13 +++++++++++--\n t/t5316-pack-delta-depth.sh   |  9 ++++++---\n t/t5332-multi-pack-reuse.sh   |  7 +++++++\n t/t5530-upload-pack-error.sh  |  6 ++++++\n t/t7406-submodule-update.sh   |  4 ++++\n 10 files changed, 60 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 77fb1217b2e..b97bec5661e 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -224,7 +224,7 @@ static int delta_search_threads;\n static int pack_to_stdout;\n static int sparse;\n static int thin;\n-static int path_walk;\n+static int path_walk = -1;\n static int num_preferred_base;\n static struct progress *progress_state;\n \n@@ -4182,7 +4182,7 @@ static int add_objects_by_path(const char *path,\n \t\tstruct object_id *oid = &oids->oid[i];\n \n \t\t/* Skip objects that do not exist locally. */\n-\t\tif (exclude_promisor_objects &&\n+\t\tif ((exclude_promisor_objects || arg_missing_action != MA_ERROR) &&\n \t\t    oid_object_info_extended(the_repository, oid, &oi,\n \t\t\t\t\t     OBJECT_INFO_FOR_PREFETCH) < 0)\n \t\t\tcontinue;\n@@ -4583,6 +4583,14 @@ int cmd_pack_objects(int argc,\n \tif (pack_to_stdout != !base_name || argc)\n \t\tusage_with_options(pack_usage, pack_objects_options);\n \n+\tif (path_walk < 0) {\n+\t\tif (use_bitmap_index > 0 ||\n+\t\t    !use_internal_rev_list)\n+\t\t\tpath_walk = 0;\n+\t\telse\n+\t\t\tpath_walk = git_env_bool(\"GIT_TEST_PACK_PATH_WALK\", 0);\n+\t}\n+\n \tif (depth < 0)\n \t\tdepth = 0;\n \tif (depth >= (1 << OE_DEPTH_BITS)) {\ndiff --git a/ci/run-build-and-tests.sh b/ci/run-build-and-tests.sh\nindex 2e28d02b20f..7c75492f366 100755\n--- a/ci/run-build-and-tests.sh\n+++ b/ci/run-build-and-tests.sh\n@@ -30,6 +30,7 @@ linux-TEST-vars)\n \texport GIT_TEST_NO_WRITE_REV_INDEX=1\n \texport GIT_TEST_CHECKOUT_WORKERS=2\n \texport GIT_TEST_PACK_USE_BITMAP_BOUNDARY_TRAVERSAL=1\n+\texport GIT_TEST_PACK_PATH_WALK=1\n \t;;\n linux-clang)\n \texport GIT_TEST_DEFAULT_HASH=sha1\ndiff --git a/t/README b/t/README\nindex 8dcb778e260..bec31955d2d 100644\n--- a/t/README\n+++ b/t/README\n@@ -436,6 +436,10 @@ GIT_TEST_PACK_SPARSE=<boolean> if disabled will default the pack-objects\n builtin to use the non-sparse object walk. This can still be overridden by\n the --sparse command-line argument.\n \n+GIT_TEST_PACK_PATH_WALK=<boolean> if enabled will default the pack-objects\n+builtin to use the path-walk API for the object walk. This can still be\n+overridden by the --no-path-walk command-line argument.\n+\n GIT_TEST_PRELOAD_INDEX=<boolean> exercises the preload-index code path\n by overriding the minimum number of cache entries required per thread.\n \ndiff --git a/t/t0411-clone-from-partial.sh b/t/t0411-clone-from-partial.sh\nindex 932bf2067da..342d8d2997c 100755\n--- a/t/t0411-clone-from-partial.sh\n+++ b/t/t0411-clone-from-partial.sh\n@@ -63,6 +63,12 @@ test_expect_success 'pack-objects should fetch from promisor remote and execute\n \n test_expect_success 'clone from promisor remote does not lazy-fetch by default' '\n \trm -f script-executed &&\n+\n+\t# The --path-walk feature of \"git pack-objects\" is not\n+\t# compatible with this kind of fetch from an incomplete repo.\n+\tGIT_TEST_PACK_PATH_WALK=0 &&\n+\texport GIT_TEST_PACK_PATH_WALK &&\n+\n \ttest_must_fail git clone evil no-lazy 2>err &&\n \ttest_grep \"lazy fetching disabled\" err &&\n \ttest_path_is_missing script-executed\ndiff --git a/t/t5306-pack-nobase.sh b/t/t5306-pack-nobase.sh\nindex 0d50c6b4bca..429be5ce724 100755\n--- a/t/t5306-pack-nobase.sh\n+++ b/t/t5306-pack-nobase.sh\n@@ -60,6 +60,11 @@ test_expect_success 'indirectly clone patch_clone' '\n \t git pull ../.git &&\n \t test $(git rev-parse HEAD) = $B &&\n \n+\t# The --path-walk feature of \"git pack-objects\" is not\n+\t# compatible with this kind of fetch from an incomplete repo.\n+\tGIT_TEST_PACK_PATH_WALK=0 &&\n+\texport GIT_TEST_PACK_PATH_WALK &&\n+\n \t git pull ../patch_clone/.git &&\n \t test $(git rev-parse HEAD) = $C\n \t)\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex a6de7c57643..881b3f9c8d1 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -128,8 +128,9 @@ test_bitmap_cases () {\n \t\tls .git/objects/pack/ | grep bitmap >output &&\n \t\ttest_line_count = 1 output &&\n \t\t# verify equivalent packs are generated with/without using bitmap index\n-\t\tpackasha1=$(git pack-objects --no-use-bitmap-index --all packa </dev/null) &&\n-\t\tpackbsha1=$(git pack-objects --use-bitmap-index --all packb </dev/null) &&\n+\t\t# Be careful to not use the path-walk option in either case.\n+\t\tpackasha1=$(git pack-objects --no-use-bitmap-index --no-path-walk --all packa </dev/null) &&\n+\t\tpackbsha1=$(git pack-objects --use-bitmap-index --no-path-walk --all packb </dev/null) &&\n \t\tlist_packed_objects packa-$packasha1.idx >packa.objects &&\n \t\tlist_packed_objects packb-$packbsha1.idx >packb.objects &&\n \t\ttest_cmp packa.objects packb.objects\n@@ -358,6 +359,14 @@ test_bitmap_cases () {\n \t\tgit init --bare client.git &&\n \t\t(\n \t\t\tcd client.git &&\n+\n+\t\t\t# This test relies on reusing a delta, but if the\n+\t\t\t# path-walk machinery is engaged, the base object\n+\t\t\t# is considered too small to use during the\n+\t\t\t# dynamic computation, so is not used.\n+\t\t\tGIT_TEST_PACK_PATH_WALK=0 &&\n+\t\t\texport GIT_TEST_PACK_PATH_WALK &&\n+\n \t\t\tgit config transfer.unpackLimit 1 &&\n \t\t\tgit fetch .. delta-reuse-old:delta-reuse-old &&\n \t\t\tgit fetch .. delta-reuse-new:delta-reuse-new &&\ndiff --git a/t/t5316-pack-delta-depth.sh b/t/t5316-pack-delta-depth.sh\nindex eb4ef3dda4d..12a6901fecb 100755\n--- a/t/t5316-pack-delta-depth.sh\n+++ b/t/t5316-pack-delta-depth.sh\n@@ -90,15 +90,18 @@ max_chain() {\n # adjusted (or scrapped if the heuristics have become too unreliable)\n test_expect_success 'packing produces a long delta' '\n \t# Use --window=0 to make sure we are seeing reused deltas,\n-\t# not computing a new long chain.\n-\tpack=$(git pack-objects --all --window=0 </dev/null pack) &&\n+\t# not computing a new long chain. (Also avoid the --path-walk\n+\t# option as it may break delta chains.)\n+\tpack=$(git pack-objects --all --window=0 --no-path-walk </dev/null pack) &&\n \techo 9 >expect &&\n \tmax_chain pack-$pack.pack >actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success '--depth limits depth' '\n-\tpack=$(git pack-objects --all --depth=5 </dev/null pack) &&\n+\t# Avoid --path-walk to avoid breaking delta chains across path\n+\t# boundaries.\n+\tpack=$(git pack-objects --all --depth=5 --no-path-walk </dev/null pack) &&\n \techo 5 >expect &&\n \tmax_chain pack-$pack.pack >actual &&\n \ttest_cmp expect actual\ndiff --git a/t/t5332-multi-pack-reuse.sh b/t/t5332-multi-pack-reuse.sh\nindex 955ea42769b..df7dcb4b487 100755\n--- a/t/t5332-multi-pack-reuse.sh\n+++ b/t/t5332-multi-pack-reuse.sh\n@@ -8,6 +8,13 @@ TEST_PASSES_SANITIZE_LEAK=true\n \n GIT_TEST_MULTI_PACK_INDEX=0\n GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL=0\n+\n+# The --path-walk option does not consider the preferred pack\n+# at all for reusing deltas, so this variable changes the\n+# behavior of this test, if enabled.\n+GIT_TEST_PACK_PATH_WALK=0\n+export GIT_TEST_PACK_PATH_WALK\n+\n objdir=.git/objects\n packdir=$objdir/pack\n \ndiff --git a/t/t5530-upload-pack-error.sh b/t/t5530-upload-pack-error.sh\nindex 7172780d550..356b96cb741 100755\n--- a/t/t5530-upload-pack-error.sh\n+++ b/t/t5530-upload-pack-error.sh\n@@ -35,6 +35,12 @@ test_expect_success 'upload-pack fails due to error in pack-objects packing' '\n \thexsz=$(test_oid hexsz) &&\n \tprintf \"%04xwant %s\\n00000009done\\n0000\" \\\n \t\t$(($hexsz + 10)) $head >input &&\n+\n+\t# The current implementation of path-walk causes a different\n+\t# error message. This will be changed by a future refactoring.\n+\tGIT_TEST_PACK_PATH_WALK=0 &&\n+\texport GIT_TEST_PACK_PATH_WALK &&\n+\n \ttest_must_fail git upload-pack . <input >/dev/null 2>output.err &&\n \ttest_grep \"unable to read\" output.err &&\n \ttest_grep \"pack-objects died\" output.err\ndiff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh\nindex 297c6c3b5cc..d2284e67d3d 100755\n--- a/t/t7406-submodule-update.sh\n+++ b/t/t7406-submodule-update.sh\n@@ -1093,12 +1093,16 @@ test_expect_success 'submodule update --quiet passes quietness to fetch with a s\n \t) &&\n \tgit clone super4 super5 &&\n \t(cd super5 &&\n+\t # This test variable will create a \"warning\" message to stderr\n+\t GIT_TEST_PACK_PATH_WALK=0 \\\n \t git submodule update --quiet --init --depth=1 submodule3 >out 2>err &&\n \t test_must_be_empty out &&\n \t test_must_be_empty err\n \t) &&\n \tgit clone super4 super6 &&\n \t(cd super6 &&\n+\t # This test variable will create a \"warning\" message to stderr\n+\t GIT_TEST_PACK_PATH_WALK=0 \\\n \t git submodule update --init --depth=1 submodule3 >out 2>err &&\n \t test_file_not_empty out &&\n \t test_file_not_empty err\n-- \ngitgitgadget\n\n"},{"id":"504438","messageId":"507ed0f6f90e922162a484ec462e8542be2b6421.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 12/17] repack: add --path-walk option","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:11:58Z","receivedAt":"2024-10-08T14:12:18Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nSince 'git pack-objects' supports a --path-walk option, allow passing it\nthrough in 'git repack'. This presents interesting testing opportunities for\ncomparing the different repacking strategies against each other.\n\nIn my copy of the Git repository, the new tests in p5313 show these\nresults:\n\nTest                                      this tree\n-------------------------------------------------------------\n5313.10: repack                           27.88(150.23+2.70)\n5313.11: repack size                               228.2M\n5313.12: repack with --path-walk          134.59(148.77+0.81)\n5313.13: repack size with --path-walk              209.7M\n\nNote that the 'git pack-objects --path-walk' feature is not integrated\nwith threads. Look forward to a future change that will introduce\nthreading to improve the time performance of this feature with\nequivalent space performance.\n\nFor the microsoft/fluentui repo [1] had some interesting aspects for the\nprevious tests in p5313, so here are the repack results:\n\nTest                                      this tree\n-------------------------------------------------------------\n5313.10: repack                           91.76(680.94+2.48)\n5313.11: repack size                               439.1M\n5313.12: repack with --path-walk          110.35(130.46+0.74)\n5313.13: repack size with --path-walk              155.3M\n\n[1] https://github.com/microsoft/fluentui\n\nHere, we see the significant improvement of a full repack using this\nstrategy. The name-hash collisions in this repo cause the space\nproblems. Those collisions also cause the repack command to spend a lot\nof cycles trying to find delta bases among files that are not actually\nvery similar, so the lack of threading with the --path-walk feature is\nless pronounced in the process time.\n\nFor the Linux kernel repository, we have these stats:\n\nTest                                      this tree\n---------------------------------------------------------------\n5313.10: repack                           553.61(1929.41+30.31)\n5313.11: repack size                                 2.5G\n5313.12: repack with --path-walk          1777.63(2044.16+7.47)\n5313.13: repack size with --path-walk                2.5G\n\nThis demonstrates that the --path-walk feature does not always present\nmeasurable improvements, especially in cases where the name-hash has\nvery few collisions.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-repack.txt | 17 ++++++++++++++++-\n builtin/repack.c             |  9 ++++++++-\n t/perf/p5313-pack-objects.sh | 18 ++++++++++++++++++\n 3 files changed, 42 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-repack.txt b/Documentation/git-repack.txt\nindex c902512a9e8..4ec59cd27b1 100644\n--- a/Documentation/git-repack.txt\n+++ b/Documentation/git-repack.txt\n@@ -9,7 +9,9 @@ git-repack - Pack unpacked objects in a repository\n SYNOPSIS\n --------\n [verse]\n-'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m] [--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>] [--write-midx]\n+'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n+\t[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\n+\t[--write-midx] [--path-walk]\n \n DESCRIPTION\n -----------\n@@ -249,6 +251,19 @@ linkgit:git-multi-pack-index[1]).\n \tWrite a multi-pack index (see linkgit:git-multi-pack-index[1])\n \tcontaining the non-redundant packs.\n \n+--path-walk::\n+\tThis option passes the `--path-walk` option to the underlying\n+\t`git pack-options` process (see linkgit:git-pack-objects[1]).\n+\tBy default, `git pack-objects` walks objects in an order that\n+\tpresents trees and blobs in an order unrelated to the path they\n+\tappear relative to a commit's root tree. The `--path-walk` option\n+\tenables a different walking algorithm that organizes trees and\n+\tblobs by path. This has the potential to improve delta compression\n+\tespecially in the presence of filenames that cause collisions in\n+\tGit's default name-hash algorithm. Due to changing how the objects\n+\tare walked, this option is not compatible with `--delta-islands`\n+\tor `--filter`.\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex cb4420f0856..af3f218ced7 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -39,7 +39,9 @@ static int run_update_server_info = 1;\n static char *packdir, *packtmp_name, *packtmp;\n \n static const char *const git_repack_usage[] = {\n-\tN_(\"git repack [<options>]\"),\n+\tN_(\"git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\\n\"\n+\t   \"[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\\n\"\n+\t   \"[--write-midx] [--full-path-walk]\"),\n \tNULL\n };\n \n@@ -58,6 +60,7 @@ struct pack_objects_args {\n \tint no_reuse_object;\n \tint quiet;\n \tint local;\n+\tint path_walk;\n \tstruct list_objects_filter_options filter_options;\n };\n \n@@ -289,6 +292,8 @@ static void prepare_pack_objects(struct child_process *cmd,\n \t\tstrvec_pushf(&cmd->args, \"--no-reuse-delta\");\n \tif (args->no_reuse_object)\n \t\tstrvec_pushf(&cmd->args, \"--no-reuse-object\");\n+\tif (args->path_walk)\n+\t\tstrvec_pushf(&cmd->args, \"--path-walk\");\n \tif (args->local)\n \t\tstrvec_push(&cmd->args,  \"--local\");\n \tif (args->quiet)\n@@ -1182,6 +1187,8 @@ int cmd_repack(int argc,\n \t\t\t\tN_(\"pass --no-reuse-delta to git-pack-objects\")),\n \t\tOPT_BOOL('F', NULL, &po_args.no_reuse_object,\n \t\t\t\tN_(\"pass --no-reuse-object to git-pack-objects\")),\n+\t\tOPT_BOOL(0, \"path-walk\", &po_args.path_walk,\n+\t\t\t\tN_(\"pass --path-walk to git-pack-objects\")),\n \t\tOPT_NEGBIT('n', NULL, &run_update_server_info,\n \t\t\t\tN_(\"do not run git-update-server-info\"), 1),\n \t\tOPT__QUIET(&po_args.quiet, N_(\"be quiet\")),\ndiff --git a/t/perf/p5313-pack-objects.sh b/t/perf/p5313-pack-objects.sh\nindex 840075f5691..b588066ddb0 100755\n--- a/t/perf/p5313-pack-objects.sh\n+++ b/t/perf/p5313-pack-objects.sh\n@@ -56,4 +56,22 @@ test_size 'big pack size with --path-walk' '\n \ttest_file_size out\n '\n \n+test_perf 'repack' '\n+\tgit repack -adf\n+'\n+\n+test_size 'repack size' '\n+\tpack=$(ls .git/objects/pack/pack-*.pack) &&\n+\ttest_file_size \"$pack\"\n+'\n+\n+test_perf 'repack with --path-walk' '\n+\tgit repack -adf --path-walk\n+'\n+\n+test_size 'repack size with --path-walk' '\n+\tpack=$(ls .git/objects/pack/pack-*.pack) &&\n+\ttest_file_size \"$pack\"\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"504439","messageId":"eae96e8214fc87cbc02e55f0957d6f2bd2eba472.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 13/17] repack: update usage to match docs","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:11:59Z","receivedAt":"2024-10-08T14:12:19Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe t0450 test script verifies that the builtin usage matches the\nsynopsis in the documentation. Update 'git repack' to match and remove\nit from the exception list.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/repack.c            | 2 +-\n t/t0450/txt-help-mismatches | 1 -\n 2 files changed, 1 insertion(+), 2 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex af3f218ced7..50f208b48b4 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -41,7 +41,7 @@ static char *packdir, *packtmp_name, *packtmp;\n static const char *const git_repack_usage[] = {\n \tN_(\"git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\\n\"\n \t   \"[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\\n\"\n-\t   \"[--write-midx] [--full-path-walk]\"),\n+\t   \"[--write-midx] [--path-walk]\"),\n \tNULL\n };\n \ndiff --git a/t/t0450/txt-help-mismatches b/t/t0450/txt-help-mismatches\nindex 285ae81a6b5..06b469bdee2 100644\n--- a/t/t0450/txt-help-mismatches\n+++ b/t/t0450/txt-help-mismatches\n@@ -44,7 +44,6 @@ rebase\n remote\n remote-ext\n remote-fd\n-repack\n reset\n restore\n rev-parse\n-- \ngitgitgadget\n\n"},{"id":"504440","messageId":"2a9ae02217fe5d1e3abb49e65410ffd7ffe05f24.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 14/17] pack-objects: enable --path-walk via config","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:12:00Z","receivedAt":"2024-10-08T14:12:19Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nUsers may want to enable the --path-walk option for 'git pack-objects' by\ndefault, especially underneath commands like 'git push' or 'git repack'.\n\nThis should be limited to client repositories, since the --path-walk option\ndisables bitmap walks, so would be bad to include in Git servers when\nserving fetches and clones. There is potential that it may be helpful to\nconsider when repacking the repository, to take advantage of improved deltas\nacross historical versions of the same files.\n\nMuch like how \"pack.useSparse\" was introduced and included in\n\"feature.experimental\" before being enabled by default, use the repository\nsettings infrastructure to make the new \"pack.usePathWalk\" config enabled by\n\"feature.experimental\" and \"feature.manyFiles\".\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/config/feature.txt | 4 ++++\n Documentation/config/pack.txt    | 8 ++++++++\n builtin/pack-objects.c           | 3 +++\n repo-settings.c                  | 3 +++\n repo-settings.h                  | 1 +\n 5 files changed, 19 insertions(+)\n\ndiff --git a/Documentation/config/feature.txt b/Documentation/config/feature.txt\nindex f061b64b748..cb49ff2604a 100644\n--- a/Documentation/config/feature.txt\n+++ b/Documentation/config/feature.txt\n@@ -20,6 +20,10 @@ walking fewer objects.\n +\n * `pack.allowPackReuse=multi` may improve the time it takes to create a pack by\n reusing objects from multiple packs instead of just one.\n++\n+* `pack.usePathWalk` may speed up packfile creation and make the packfiles be\n+significantly smaller in the presence of certain filename collisions with Git's\n+default name-hash.\n \n feature.manyFiles::\n \tEnable config options that optimize for repos with many files in the\ndiff --git a/Documentation/config/pack.txt b/Documentation/config/pack.txt\nindex da527377faf..08d06271177 100644\n--- a/Documentation/config/pack.txt\n+++ b/Documentation/config/pack.txt\n@@ -155,6 +155,14 @@ pack.useSparse::\n \tcommits contain certain types of direct renames. Default is\n \t`true`.\n \n+pack.usePathWalk::\n+\tWhen true, git will default to using the '--path-walk' option in\n+\t'git pack-objects' when the '--revs' option is present. This\n+\talgorithm groups objects by path to maximize the ability to\n+\tcompute delta chains across historical versions of the same\n+\tobject. This may disable other options, such as using bitmaps to\n+\tenumerate objects.\n+\n pack.preferBitmapTips::\n \tWhen selecting which commits will receive bitmaps, prefer a\n \tcommit at the tip of any reference that is a suffix of any value\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex b97bec5661e..6805a55c60d 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -4587,6 +4587,9 @@ int cmd_pack_objects(int argc,\n \t\tif (use_bitmap_index > 0 ||\n \t\t    !use_internal_rev_list)\n \t\t\tpath_walk = 0;\n+\t\telse if (the_repository->gitdir &&\n+\t\t\t the_repository->settings.pack_use_path_walk)\n+\t\t\tpath_walk = 1;\n \t\telse\n \t\t\tpath_walk = git_env_bool(\"GIT_TEST_PACK_PATH_WALK\", 0);\n \t}\ndiff --git a/repo-settings.c b/repo-settings.c\nindex 4699b4b3650..d8123b9323d 100644\n--- a/repo-settings.c\n+++ b/repo-settings.c\n@@ -45,11 +45,13 @@ void prepare_repo_settings(struct repository *r)\n \t\tr->settings.fetch_negotiation_algorithm = FETCH_NEGOTIATION_SKIPPING;\n \t\tr->settings.pack_use_bitmap_boundary_traversal = 1;\n \t\tr->settings.pack_use_multi_pack_reuse = 1;\n+\t\tr->settings.pack_use_path_walk = 1;\n \t}\n \tif (manyfiles) {\n \t\tr->settings.index_version = 4;\n \t\tr->settings.index_skip_hash = 1;\n \t\tr->settings.core_untracked_cache = UNTRACKED_CACHE_WRITE;\n+\t\tr->settings.pack_use_path_walk = 1;\n \t}\n \n \t/* Commit graph config or default, does not cascade (simple) */\n@@ -64,6 +66,7 @@ void prepare_repo_settings(struct repository *r)\n \n \t/* Boolean config or default, does not cascade (simple)  */\n \trepo_cfg_bool(r, \"pack.usesparse\", &r->settings.pack_use_sparse, 1);\n+\trepo_cfg_bool(r, \"pack.usepathwalk\", &r->settings.pack_use_path_walk, 0);\n \trepo_cfg_bool(r, \"core.multipackindex\", &r->settings.core_multi_pack_index, 1);\n \trepo_cfg_bool(r, \"index.sparse\", &r->settings.sparse_index, 0);\n \trepo_cfg_bool(r, \"index.skiphash\", &r->settings.index_skip_hash, r->settings.index_skip_hash);\ndiff --git a/repo-settings.h b/repo-settings.h\nindex 51d6156a117..ae5c74ba60d 100644\n--- a/repo-settings.h\n+++ b/repo-settings.h\n@@ -53,6 +53,7 @@ struct repo_settings {\n \tenum untracked_cache_setting core_untracked_cache;\n \n \tint pack_use_sparse;\n+\tint pack_use_path_walk;\n \tenum fetch_negotiation_setting fetch_negotiation_algorithm;\n \n \tint core_multi_pack_index;\n-- \ngitgitgadget\n\n"},{"id":"504441","messageId":"adcb316780959c02fb32a17fc51088e6599f787d.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 15/17] scalar: enable path-walk during push via config","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:12:01Z","receivedAt":"2024-10-08T14:12:20Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nRepositories registered with Scalar are expected to be client-only\nrepositories that are rather large. This means that they are more likely to\nbe good candidates for using the --path-walk option when running 'git\npack-objects', especially under the hood of 'git push'. Enable this config\nin Scalar repositories.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n scalar.c | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/scalar.c b/scalar.c\nindex 73b79a5d4c9..7db9c5bbfe0 100644\n--- a/scalar.c\n+++ b/scalar.c\n@@ -170,6 +170,7 @@ static int set_recommended_config(int reconfigure)\n \t\t{ \"core.autoCRLF\", \"false\" },\n \t\t{ \"core.safeCRLF\", \"false\" },\n \t\t{ \"fetch.showForcedUpdates\", \"false\" },\n+\t\t{ \"push.usePathWalk\", \"true\" },\n \t\t{ NULL, NULL },\n \t};\n \tint i;\n-- \ngitgitgadget\n\n"},{"id":"504442","messageId":"c3f24dc364793850d32b8b6d867b9ff2f9a6817d.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 16/17] pack-objects: refactor path-walk delta phase","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:12:02Z","receivedAt":"2024-10-08T14:12:21Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nPreviously, the --path-walk option to 'git pack-objects' would compute\ndeltas inline with the path-walk logic. This would make the progress\nindicator look like it is taking a long time to enumerate objects, and\nthen very quickly computed deltas.\n\nInstead of computing deltas on each region of objects organized by tree,\nstore a list of regions corresponding to these groups. These can later\nbe pulled from the list for delta compression before doing the \"global\"\ndelta search.\n\nThis presents a new progress indicator that can be used in tests to\nverify that this stage is happening.\n\nThe current implementation is not integrated with threads, but could be\ndone in a future update.\n\nSince we do not attempt to sort objects by size until after exploring\nall trees, we can remove the previous change to t5530 due to a different\nerror message appearing first.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/pack-objects.c       | 81 +++++++++++++++++++++++++-----------\n pack-objects.h               | 12 ++++++\n t/t5300-pack-object.sh       |  8 +++-\n t/t5530-upload-pack-error.sh |  6 ---\n 4 files changed, 74 insertions(+), 33 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 6805a55c60d..5c413ac07e6 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -3204,6 +3204,50 @@ static int should_attempt_deltas(struct object_entry *entry)\n \treturn 1;\n }\n \n+static void find_deltas_for_region(struct object_entry *list UNUSED,\n+\t\t\t\t   struct packing_region *region,\n+\t\t\t\t   unsigned int *processed)\n+{\n+\tstruct object_entry **delta_list;\n+\tuint32_t delta_list_nr = 0;\n+\n+\tALLOC_ARRAY(delta_list, region->nr);\n+\tfor (uint32_t i = 0; i < region->nr; i++) {\n+\t\tstruct object_entry *entry = to_pack.objects + region->start + i;\n+\t\tif (should_attempt_deltas(entry))\n+\t\t\tdelta_list[delta_list_nr++] = entry;\n+\t}\n+\n+\tQSORT(delta_list, delta_list_nr, type_size_sort);\n+\tfind_deltas(delta_list, &delta_list_nr, window, depth, processed);\n+\tfree(delta_list);\n+}\n+\n+static void find_deltas_by_region(struct object_entry *list,\n+\t\t\t\t  struct packing_region *regions,\n+\t\t\t\t  uint32_t start, uint32_t nr)\n+{\n+\tunsigned int processed = 0;\n+\tuint32_t progress_nr;\n+\n+\tif (!nr)\n+\t\treturn;\n+\n+\tprogress_nr = regions[nr - 1].start + regions[nr - 1].nr;\n+\n+\tif (progress)\n+\t\tprogress_state = start_progress(_(\"Compressing objects by path\"),\n+\t\t\t\t\t\tprogress_nr);\n+\n+\twhile (nr--)\n+\t\tfind_deltas_for_region(list,\n+\t\t\t\t       &regions[start++],\n+\t\t\t\t       &processed);\n+\n+\tdisplay_progress(progress_state, progress_nr);\n+\tstop_progress(&progress_state);\n+}\n+\n static void prepare_pack(int window, int depth)\n {\n \tstruct object_entry **delta_list;\n@@ -3228,6 +3272,10 @@ static void prepare_pack(int window, int depth)\n \tif (!to_pack.nr_objects || !window || !depth)\n \t\treturn;\n \n+\tif (path_walk)\n+\t\tfind_deltas_by_region(to_pack.objects, to_pack.regions,\n+\t\t\t\t      0, to_pack.nr_regions);\n+\n \tALLOC_ARRAY(delta_list, to_pack.nr_objects);\n \tnr_deltas = n = 0;\n \n@@ -4165,10 +4213,8 @@ static int add_objects_by_path(const char *path,\n \t\t\t       enum object_type type,\n \t\t\t       void *data)\n {\n-\tstruct object_entry **delta_list;\n \tsize_t oe_start = to_pack.nr_objects;\n \tsize_t oe_end;\n-\tunsigned int sub_list_size;\n \tunsigned int *processed = data;\n \n \t/*\n@@ -4201,32 +4247,17 @@ static int add_objects_by_path(const char *path,\n \tif (oe_end == oe_start || !window)\n \t\treturn 0;\n \n-\tsub_list_size = 0;\n-\tALLOC_ARRAY(delta_list, oe_end - oe_start);\n+\tALLOC_GROW(to_pack.regions,\n+\t\t   to_pack.nr_regions + 1,\n+\t\t   to_pack.nr_regions_alloc);\n \n-\tfor (size_t i = 0; i < oe_end - oe_start; i++) {\n-\t\tstruct object_entry *entry = to_pack.objects + oe_start + i;\n+\tto_pack.regions[to_pack.nr_regions].start = oe_start;\n+\tto_pack.regions[to_pack.nr_regions].nr = oe_end - oe_start;\n+\tto_pack.nr_regions++;\n \n-\t\tif (!should_attempt_deltas(entry))\n-\t\t\tcontinue;\n+\t*processed += oids->nr;\n+\tdisplay_progress(progress_state, *processed);\n \n-\t\tdelta_list[sub_list_size++] = entry;\n-\t}\n-\n-\t/*\n-\t * Find delta bases among this list of objects that all match the same\n-\t * path. This causes the delta compression to be interleaved in the\n-\t * object walk, which can lead to confusing progress indicators. This is\n-\t * also incompatible with threaded delta calculations. In the future,\n-\t * consider creating a list of regions in the full to_pack.objects array\n-\t * that could be picked up by the threaded delta computation.\n-\t */\n-\tif (sub_list_size && window) {\n-\t\tQSORT(delta_list, sub_list_size, type_size_sort);\n-\t\tfind_deltas(delta_list, &sub_list_size, window, depth, processed);\n-\t}\n-\n-\tfree(delta_list);\n \treturn 0;\n }\n \ndiff --git a/pack-objects.h b/pack-objects.h\nindex b9898a4e64b..bde4ba19755 100644\n--- a/pack-objects.h\n+++ b/pack-objects.h\n@@ -118,11 +118,23 @@ struct object_entry {\n \tunsigned ext_base:1; /* delta_idx points outside packlist */\n };\n \n+/**\n+ * A packing region is a section of the packing_data.objects array\n+ * as given by a starting index and a number of elements.\n+ */\n+struct packing_region {\n+\tuint32_t start;\n+\tuint32_t nr;\n+};\n+\n struct packing_data {\n \tstruct repository *repo;\n \tstruct object_entry *objects;\n \tuint32_t nr_objects, nr_alloc;\n \n+\tstruct packing_region *regions;\n+\tuint32_t nr_regions, nr_regions_alloc;\n+\n \tint32_t *index;\n \tuint32_t index_size;\n \ndiff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh\nindex 5f6914acae7..4f81613eab1 100755\n--- a/t/t5300-pack-object.sh\n+++ b/t/t5300-pack-object.sh\n@@ -677,7 +677,9 @@ done\n # Basic \"repack everything\" test\n test_expect_success '--path-walk pack everything' '\n \tgit -C server rev-parse HEAD >in &&\n-\tgit -C server pack-objects --stdout --revs --path-walk <in >out.pack &&\n+\tGIT_PROGRESS_DELAY=0 git -C server pack-objects \\\n+\t\t--stdout --revs --path-walk --progress <in >out.pack 2>err &&\n+\tgrep \"Compressing objects by path\" err &&\n \tgit -C server index-pack --stdin <out.pack\n '\n \n@@ -687,7 +689,9 @@ test_expect_success '--path-walk thin pack' '\n \t$(git -C server rev-parse HEAD)\n \t^$(git -C server rev-parse HEAD~2)\n \tEOF\n-\tgit -C server pack-objects --thin --stdout --revs --path-walk <in >out.pack &&\n+\tGIT_PROGRESS_DELAY=0 git -C server pack-objects \\\n+\t\t--thin --stdout --revs --path-walk --progress <in >out.pack 2>err &&\n+\tgrep \"Compressing objects by path\" err &&\n \tgit -C server index-pack --fix-thin --stdin <out.pack\n '\n \ndiff --git a/t/t5530-upload-pack-error.sh b/t/t5530-upload-pack-error.sh\nindex 356b96cb741..7172780d550 100755\n--- a/t/t5530-upload-pack-error.sh\n+++ b/t/t5530-upload-pack-error.sh\n@@ -35,12 +35,6 @@ test_expect_success 'upload-pack fails due to error in pack-objects packing' '\n \thexsz=$(test_oid hexsz) &&\n \tprintf \"%04xwant %s\\n00000009done\\n0000\" \\\n \t\t$(($hexsz + 10)) $head >input &&\n-\n-\t# The current implementation of path-walk causes a different\n-\t# error message. This will be changed by a future refactoring.\n-\tGIT_TEST_PACK_PATH_WALK=0 &&\n-\texport GIT_TEST_PACK_PATH_WALK &&\n-\n \ttest_must_fail git upload-pack . <input >/dev/null 2>output.err &&\n \ttest_grep \"unable to read\" output.err &&\n \ttest_grep \"pack-objects died\" output.err\n-- \ngitgitgadget\n\n"},{"id":"504443","messageId":"264affbf058e01450722ed68ca45e001c67cf190.1728396724.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH 17/17] pack-objects: thread the path-based compression","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-08T14:12:03Z","receivedAt":"2024-10-08T14:12:22Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nAdapting the implementation of ll_find_deltas(), create a threaded\nversion of the --path-walk compression step in 'git pack-objects'.\n\nThis involves adding a 'regions' member to the thread_params struct,\nallowing each thread to own a section of paths. We can simplify the way\njobs are split because there is no value in extending the batch based on\nname-hash the way sections of the object entry array are attempted to be\ngrouped. We re-use the 'list_size' and 'remaining' items for the purpose\nof borrowing work in progress from other \"victim\" threads when a thread\nhas finished its batch of work more quickly.\n\nUsing the Git repository as a test repo, the p5313 performance test\nshows that the resulting size of the repo is the same, but the threaded\nimplementation gives gains of varying degrees depending on the number of\nobjects being packed. (This was tested on a 16-core machine.)\n\nTest                                      HEAD~1    HEAD\n---------------------------------------------------------------\n5313.2: thin pack                           0.01    0.01  +0.0%\n5313.4: thin pack with --path-walk          0.01    0.01  +0.0%\n5313.6: big pack                            2.54    2.60  +2.4%\n5313.8: big pack with --path-walk           4.70    3.09 -34.3%\n5313.10: repack                            28.75   28.55  -0.7%\n5313.12: repack with --path-walk          108.55   46.14 -57.5%\n\nOn the microsoft/fluentui repo, where the --path-walk feature has been\nshown to be more effective in space savings, we get these results:\n\nTest                                     HEAD~1    HEAD\n----------------------------------------------------------------\n5313.2: thin pack                         0.39     0.40  +2.6%\n5313.4: thin pack with --path-walk        0.08     0.07 -12.5%\n5313.6: big pack                          4.15     4.15  +0.0%\n5313.8: big pack with --path-walk         6.41     3.21 -49.9%\n5313.10: repack                          90.69    90.83  +0.2%\n5313.12: repack with --path-walk        108.23    49.09 -54.6%\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/pack-objects.c | 162 ++++++++++++++++++++++++++++++++++++++++-\n 1 file changed, 160 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 5c413ac07e6..443ce17063a 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -2935,6 +2935,7 @@ static void find_deltas(struct object_entry **list, unsigned *list_size,\n struct thread_params {\n \tpthread_t thread;\n \tstruct object_entry **list;\n+\tstruct packing_region *regions;\n \tunsigned list_size;\n \tunsigned remaining;\n \tint window;\n@@ -3248,6 +3249,163 @@ static void find_deltas_by_region(struct object_entry *list,\n \tstop_progress(&progress_state);\n }\n \n+static void *threaded_find_deltas_by_path(void *arg)\n+{\n+\tstruct thread_params *me = arg;\n+\n+\tprogress_lock();\n+\twhile (me->remaining) {\n+\t\twhile (me->remaining) {\n+\t\t\tprogress_unlock();\n+\t\t\tfind_deltas_for_region(to_pack.objects,\n+\t\t\t\t\t       me->regions,\n+\t\t\t\t\t       me->processed);\n+\t\t\tprogress_lock();\n+\t\t\tme->remaining--;\n+\t\t\tme->regions++;\n+\t\t}\n+\n+\t\tme->working = 0;\n+\t\tpthread_cond_signal(&progress_cond);\n+\t\tprogress_unlock();\n+\n+\t\t/*\n+\t\t * We must not set ->data_ready before we wait on the\n+\t\t * condition because the main thread may have set it to 1\n+\t\t * before we get here. In order to be sure that new\n+\t\t * work is available if we see 1 in ->data_ready, it\n+\t\t * was initialized to 0 before this thread was spawned\n+\t\t * and we reset it to 0 right away.\n+\t\t */\n+\t\tpthread_mutex_lock(&me->mutex);\n+\t\twhile (!me->data_ready)\n+\t\t\tpthread_cond_wait(&me->cond, &me->mutex);\n+\t\tme->data_ready = 0;\n+\t\tpthread_mutex_unlock(&me->mutex);\n+\n+\t\tprogress_lock();\n+\t}\n+\tprogress_unlock();\n+\t/* leave ->working 1 so that this doesn't get more work assigned */\n+\treturn NULL;\n+}\n+\n+static void ll_find_deltas_by_region(struct object_entry *list,\n+\t\t\t\t     struct packing_region *regions,\n+\t\t\t\t     uint32_t start, uint32_t nr)\n+{\n+\tstruct thread_params *p;\n+\tint i, ret, active_threads = 0;\n+\tunsigned int processed = 0;\n+\tuint32_t progress_nr;\n+\tinit_threaded_search();\n+\n+\tif (!nr)\n+\t\treturn;\n+\n+\tprogress_nr =  regions[nr - 1].start + regions[nr - 1].nr;\n+\tif (delta_search_threads <= 1) {\n+\t\tfind_deltas_by_region(list, regions, start, nr);\n+\t\tcleanup_threaded_search();\n+\t\treturn;\n+\t}\n+\n+\tif (progress > pack_to_stdout)\n+\t\tfprintf_ln(stderr, _(\"Path-based delta compression using up to %d threads\"),\n+\t\t\t   delta_search_threads);\n+\tCALLOC_ARRAY(p, delta_search_threads);\n+\n+\tif (progress)\n+\t\tprogress_state = start_progress(_(\"Compressing objects by path\"),\n+\t\t\t\t\t\tprogress_nr);\n+\t/* Partition the work amongst work threads. */\n+\tfor (i = 0; i < delta_search_threads; i++) {\n+\t\tunsigned sub_size = nr / (delta_search_threads - i);\n+\n+\t\tp[i].window = window;\n+\t\tp[i].depth = depth;\n+\t\tp[i].processed = &processed;\n+\t\tp[i].working = 1;\n+\t\tp[i].data_ready = 0;\n+\n+\t\tp[i].regions = regions;\n+\t\tp[i].list_size = sub_size;\n+\t\tp[i].remaining = sub_size;\n+\n+\t\tregions += sub_size;\n+\t\tnr -= sub_size;\n+\t}\n+\n+\t/* Start work threads. */\n+\tfor (i = 0; i < delta_search_threads; i++) {\n+\t\tif (!p[i].list_size)\n+\t\t\tcontinue;\n+\t\tpthread_mutex_init(&p[i].mutex, NULL);\n+\t\tpthread_cond_init(&p[i].cond, NULL);\n+\t\tret = pthread_create(&p[i].thread, NULL,\n+\t\t\t\t     threaded_find_deltas_by_path, &p[i]);\n+\t\tif (ret)\n+\t\t\tdie(_(\"unable to create thread: %s\"), strerror(ret));\n+\t\tactive_threads++;\n+\t}\n+\n+\t/*\n+\t * Now let's wait for work completion.  Each time a thread is done\n+\t * with its work, we steal half of the remaining work from the\n+\t * thread with the largest number of unprocessed objects and give\n+\t * it to that newly idle thread.  This ensure good load balancing\n+\t * until the remaining object list segments are simply too short\n+\t * to be worth splitting anymore.\n+\t */\n+\twhile (active_threads) {\n+\t\tstruct thread_params *target = NULL;\n+\t\tstruct thread_params *victim = NULL;\n+\t\tunsigned sub_size = 0;\n+\n+\t\tprogress_lock();\n+\t\tfor (;;) {\n+\t\t\tfor (i = 0; !target && i < delta_search_threads; i++)\n+\t\t\t\tif (!p[i].working)\n+\t\t\t\t\ttarget = &p[i];\n+\t\t\tif (target)\n+\t\t\t\tbreak;\n+\t\t\tpthread_cond_wait(&progress_cond, &progress_mutex);\n+\t\t}\n+\n+\t\tfor (i = 0; i < delta_search_threads; i++)\n+\t\t\tif (p[i].remaining > 2*window &&\n+\t\t\t    (!victim || victim->remaining < p[i].remaining))\n+\t\t\t\tvictim = &p[i];\n+\t\tif (victim) {\n+\t\t\tsub_size = victim->remaining / 2;\n+\t\t\ttarget->regions = victim->regions + victim->remaining - sub_size;\n+\t\t\tvictim->list_size -= sub_size;\n+\t\t\tvictim->remaining -= sub_size;\n+\t\t}\n+\t\ttarget->list_size = sub_size;\n+\t\ttarget->remaining = sub_size;\n+\t\ttarget->working = 1;\n+\t\tprogress_unlock();\n+\n+\t\tpthread_mutex_lock(&target->mutex);\n+\t\ttarget->data_ready = 1;\n+\t\tpthread_cond_signal(&target->cond);\n+\t\tpthread_mutex_unlock(&target->mutex);\n+\n+\t\tif (!sub_size) {\n+\t\t\tpthread_join(target->thread, NULL);\n+\t\t\tpthread_cond_destroy(&target->cond);\n+\t\t\tpthread_mutex_destroy(&target->mutex);\n+\t\t\tactive_threads--;\n+\t\t}\n+\t}\n+\tcleanup_threaded_search();\n+\tfree(p);\n+\n+\tdisplay_progress(progress_state, progress_nr);\n+\tstop_progress(&progress_state);\n+}\n+\n static void prepare_pack(int window, int depth)\n {\n \tstruct object_entry **delta_list;\n@@ -3273,8 +3431,8 @@ static void prepare_pack(int window, int depth)\n \t\treturn;\n \n \tif (path_walk)\n-\t\tfind_deltas_by_region(to_pack.objects, to_pack.regions,\n-\t\t\t\t      0, to_pack.nr_regions);\n+\t\tll_find_deltas_by_region(to_pack.objects, to_pack.regions,\n+\t\t\t\t\t 0, to_pack.nr_regions);\n \n \tALLOC_ARRAY(delta_list, to_pack.nr_objects);\n \tnr_deltas = n = 0;\n-- \ngitgitgadget\n"},{"id":"505539","messageId":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.git.1728396723.gitgitgadget@gmail.com","subject":"[PATCH v2 00/17] pack-objects: add --path-walk option for better deltas","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:13Z","receivedAt":"2024-10-20T13:43:35Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"This is a reviewable series introducing the path-walk API and its\napplication within the 'git pack-objects --path-walk' machinery. This API\nwas previously discussed in the path-walk RFC [1] and the patch series\naround the --full-name-hash option (branch ds/pack-name-hash-tweak) [2].\nThis series conflicts with ds/pack-name-hash-tweak, but that was on hold\nbecause it did not seem as critical based on community interest.\n\n[1]\nhttps://lore.kernel.org/git/pull.1786.git.1725935335.gitgitgadget@gmail.com\n\n[2]\nhttps://lore.kernel.org/git/pull.1785.git.1725890210.gitgitgadget@gmail.com\n\nThe primary motivation for this feature is its use to shrink the packfile\ncreated by 'git push' when there are many name-hash collisions. This need\nwas discovered in several Javascript repositories that use the beachball\ntool [3] to generate CHANGELOG.md and CHANGELOG.json files. When a batch of\nthese files are created at the same time and pushed to a release branch, the\n'git pack-objects' process has many collisions among these files and delta\nbases are selected poorly.\n\n[3] https://github.com/microsoft/beachball\n\nIn some cases, 'git push' is pushing 60-100 MB when the new path-walk\nalgorithm will identify better delta bases and pack the same objects into a\nthin pack less than 1 MB. This was the most extreme example we could find\nand is present in a private monorepo. However, the microsoft/fluentui repo\n[4] is a good example for demonstrating similar improvements. The patch\ndescriptions frequently refer to this repo and which commit should be\nchecked out to reproduce this behavior.\n\n[4] https://github.com/microsoft/fluentui\n\nThe path-walk API is a new way to walk objects. Traditionally, the revision\nAPI walks objects by visiting a commit, then visiting its root tree and\nrecursing through trees to visit reachable objects that were not previously\nvisited. The path-walk API visits objects in batches based on the path\nwalked from a root tree to that object. (Only the first discovered path is\nchosen; this avoids certain kinds of Git bombs.)\n\nThis has an immediate application to 'git pack-objects'.\n\nWhen using the traditional revision API to walk objects, each object is\nemitted with an associated path. Since this path may appear for many objects\nspread across the full list, a heuristic is used: the \"name-hash\" is stored\nfor that object instead of the full path name. This name-hash will group\nobjects at the same path together, but also has a form of \"locality\" to\ngroup likely-similar objects together. When there are few collisions in the\nname-hash function, this helps compress objects that appear at the same path\nas well as help compress objects across different paths that have similar\nsuffixes. When there are many versions of the same path, then finding delta\nbases across that family of objects is very important. When there are few\nversions of the same path, then finding cross-name delta bases is also\nimportant. The former is important for clones and repacks while the latter\nis important for shallow clones. They can both be important for pushes. In\nall cases, this approach is fraught when there are many name-hash collisions\nas the window size becomes a limiting factor for finding quality delta\nbases.\n\nWhen using the path-walk API to walk objects, we group objects by the same\npath from the start. We don't need to store the path name, since we have the\nobjects already in a group. We can compute deltas within that group and then\nuse the name-hash approach to resort the object list and look for\nopportunistic cross-path deltas. Thus, the path-walk approach allows finding\ndelta bases at least as good as the traditional revision API approach.\n(Caveat: if we assume delta reuse and the existing deltas were computed with\nthe revision API approach, then the path-walk API approach may result in\nslightly worse delta compression. The performance tests in this series use\n--no-reuse-delta for this reason.)\n\nOnce 'git pack-objects --path-walk' exists, we have a few ways to take\nadvantage of it in other places:\n\n * The new 'pack.usePathWalk' config option will assume the --path-walk\n   option. This allows 'git push' to assume this value and get the effect we\n   want. This is similar to the 'pack.useSparse' config option that uses a\n   similar path-based walk to limit the set of boundary objects.\n\n * 'git repack' learns a '--path-walk' option to pass to its child 'git\n   pack-objects' process. This is also implied by 'pack.usePathWalk' but\n   allows for testing without modifying repository config.\n\nI'll repeat the following table of repacking results when using 'git repack\n-adf [--path-walk]' on a set of repositories with many name-hash collisions.\nOnly the microsoft/fluentui repository is publicly available for testing, so\nthe others are left as Repo B/C/D.\n\n| Repo     | Standard Repack | With --path-walk |\n|----------|-----------------|------------------|\n| fluentui |         438 MB  |          148 MB  |\n| Repo B   |       6,255 MB  |          778 MB  |\n| Repo C   |      37,737 MB  |        6,158 MB  |\n| Repo D   |     130,049 MB  |        4,432 MB  |\n\n\nWhile this series is replacing ds/pack-name-hash-tweak and its introduction\nof the --full-name-hash option, it is worth comparing that option to the\n--path-walk option.\n\n * The --full-name-hash option is a much smaller code change, as it drops\n   into the existing uses of the name-hash function.\n\n * The --full-name-hash option is more likely to integrate with server-side\n   features such as delta islands and reachability bitmaps due to not\n   changing the object walk. It was already noted that the .bitmap files\n   store name-hash values, so there is some compatibility required to\n   integrate with bitmaps. The --path-walk option will be more difficult to\n   integrate with those options (at least during a repack), but maybe is not\n   impossible; the --path-walk option will not work when reading\n   reachability bitmaps, since we are avoiding walking trees entirely.\n\n * The --full-name-hash option is good when there are many name-hash\n   collisions and many versions of the paths with those collisions. When\n   creating a shallow clone or certain kinds of pushes, the --full-name-hash\n   option is much worse at finding cross-path delta bases since it loses the\n   locality of the standard name-hash function. The --path-walk option\n   includes a second pass of delta computation using the standard name-hash\n   function and thus finds good cross-path delta bases when they improve\n   upon the same-path delta bases.\n\nThere are a few differences from the RFC version of this series:\n\n 1. The last two patches refactor the approach to perform delta calculations\n    by path after the object walk and then allows those delta calculations\n    to happen in a threaded manner.\n\n 2. Both 'git pack-objects' and 'git repack' are removed from the t0450\n    exclusion list.\n\n 3. The path-walk API has improved technical documentation that is extended\n    as its functionality is expanded.\n\n 4. Various bugs have been patched with matching tests. The new 'test-tool\n    path-walk' helper allows for careful testing of the API separately from\n    its use within other commands.\n\n\nUpdates in v2\n=============\n\nI'm sending this v2 to request some review feedback on the series. I'm sorry\nit's so long.\n\nThere are two updates in this version:\n\n * Fixed a performance issue in the presence of many annotated tags. This is\n   caught by p5313 when run on a repo with 10,000+ annotated tags.\n * The Scalar config was previously wrong and should be pack.usePathWalk,\n   not push.usePathWalk.\n\nThanks, - Stolee\n\nDerrick Stolee (17):\n  path-walk: introduce an object walk by path\n  t6601: add helper for testing path-walk API\n  path-walk: allow consumer to specify object types\n  path-walk: allow visiting tags\n  revision: create mark_trees_uninteresting_dense()\n  path-walk: add prune_all_uninteresting option\n  pack-objects: extract should_attempt_deltas()\n  pack-objects: add --path-walk option\n  pack-objects: update usage to match docs\n  p5313: add performance tests for --path-walk\n  pack-objects: introduce GIT_TEST_PACK_PATH_WALK\n  repack: add --path-walk option\n  repack: update usage to match docs\n  pack-objects: enable --path-walk via config\n  scalar: enable path-walk during push via config\n  pack-objects: refactor path-walk delta phase\n  pack-objects: thread the path-based compression\n\n Documentation/config/feature.txt          |   4 +\n Documentation/config/pack.txt             |   8 +\n Documentation/git-pack-objects.txt        |  23 +-\n Documentation/git-repack.txt              |  17 +-\n Documentation/technical/api-path-walk.txt |  73 ++++\n Makefile                                  |   2 +\n builtin/pack-objects.c                    | 410 ++++++++++++++++++++--\n builtin/repack.c                          |   9 +-\n ci/run-build-and-tests.sh                 |   1 +\n pack-objects.h                            |  12 +\n path-walk.c                               | 406 +++++++++++++++++++++\n path-walk.h                               |  64 ++++\n repo-settings.c                           |   3 +\n repo-settings.h                           |   1 +\n revision.c                                |  15 +\n revision.h                                |   1 +\n scalar.c                                  |   1 +\n t/README                                  |   4 +\n t/helper/test-path-walk.c                 | 114 ++++++\n t/helper/test-tool.c                      |   1 +\n t/helper/test-tool.h                      |   1 +\n t/perf/p5313-pack-objects.sh              |  77 ++++\n t/t0411-clone-from-partial.sh             |   6 +\n t/t0450/txt-help-mismatches               |   2 -\n t/t5300-pack-object.sh                    |  21 ++\n t/t5306-pack-nobase.sh                    |   5 +\n t/t5310-pack-bitmaps.sh                   |  13 +-\n t/t5316-pack-delta-depth.sh               |   9 +-\n t/t5332-multi-pack-reuse.sh               |   7 +\n t/t6601-path-walk.sh                      | 301 ++++++++++++++++\n t/t7406-submodule-update.sh               |   4 +\n 31 files changed, 1565 insertions(+), 50 deletions(-)\n create mode 100644 Documentation/technical/api-path-walk.txt\n create mode 100644 path-walk.c\n create mode 100644 path-walk.h\n create mode 100644 t/helper/test-path-walk.c\n create mode 100755 t/perf/p5313-pack-objects.sh\n create mode 100755 t/t6601-path-walk.sh\n\n\nbase-commit: e9356ba3ea2a6754281ff7697b3e5a1697b21e24\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-1813%2Fderrickstolee%2Fpath-walk-upstream-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1813/derrickstolee/path-walk-upstream-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/1813\n\nRange-diff vs v1:\n\n  1:  98bdc94a773 =  1:  98bdc94a773 path-walk: introduce an object walk by path\n  2:  a00ab0c62c9 =  2:  a00ab0c62c9 t6601: add helper for testing path-walk API\n  3:  14375d19392 =  3:  14375d19392 path-walk: allow consumer to specify object types\n  4:  6f48cddadc0 !  4:  c321f58c62d path-walk: allow visiting tags\n     @@ path-walk.c: int walk_objects_by_path(struct path_walk_info *info)\n      +\t\t\tif (obj->type == OBJ_COMMIT || obj->flags & SEEN)\n      +\t\t\t\tcontinue;\n      +\n     -+\t\t\tobj->flags |= SEEN;\n     -+\n      +\t\t\twhile (obj->type == OBJ_TAG) {\n      +\t\t\t\tstruct tag *tag = lookup_tag(info->revs->repo,\n      +\t\t\t\t\t\t\t     &obj->oid);\n     -+\t\t\t\tif (oid_array_lookup(&tags, &obj->oid) < 0)\n     ++\t\t\t\tif (!(obj->flags & SEEN)) {\n     ++\t\t\t\t\tobj->flags |= SEEN;\n      +\t\t\t\t\toid_array_append(&tags, &obj->oid);\n     ++\t\t\t\t}\n      +\t\t\t\tobj = tag->tagged;\n      +\t\t\t}\n      +\n     ++\t\t\tif ((obj->flags & SEEN))\n     ++\t\t\t\tcontinue;\n     ++\t\t\tobj->flags |= SEEN;\n     ++\n      +\t\t\tswitch (obj->type) {\n      +\t\t\tcase OBJ_TREE:\n     -+\t\t\t\tif (info->trees &&\n     -+\t\t\t\t    oid_array_lookup(&root_tree_list->oids, &obj->oid) < 0)\n     ++\t\t\t\tif (info->trees)\n      +\t\t\t\t\toid_array_append(&root_tree_list->oids, &obj->oid);\n      +\t\t\t\tbreak;\n      +\n      +\t\t\tcase OBJ_BLOB:\n     -+\t\t\t\tif (info->blobs &&\n     -+\t\t\t\t    oid_array_lookup(&tagged_blob_list, &obj->oid) < 0)\n     ++\t\t\t\tif (info->blobs)\n      +\t\t\t\t\toid_array_append(&tagged_blob_list, &obj->oid);\n      +\t\t\t\tbreak;\n      +\n  5:  cd98447f7c8 =  5:  6e89fb219b5 revision: create mark_trees_uninteresting_dense()\n  6:  214e10a9984 =  6:  238d7d95715 path-walk: add prune_all_uninteresting option\n  7:  cd360ad1040 =  7:  3fdb57edbc5 pack-objects: extract should_attempt_deltas()\n  8:  f8ee11d3003 =  8:  a0475c7cba8 pack-objects: add --path-walk option\n  9:  eaeb40980f4 =  9:  73c8b61e87b pack-objects: update usage to match docs\n 10:  3113ead1e01 = 10:  21dc3723c36 p5313: add performance tests for --path-walk\n 11:  211a16ae889 = 11:  6f96b1c227a pack-objects: introduce GIT_TEST_PACK_PATH_WALK\n 12:  507ed0f6f90 = 12:  834c9ea2709 repack: add --path-walk option\n 13:  eae96e8214f = 13:  6ef8d67af4b repack: update usage to match docs\n 14:  2a9ae02217f = 14:  1db90e361ba pack-objects: enable --path-walk via config\n 15:  adcb3167809 ! 15:  0f3040b4b90 scalar: enable path-walk during push via config\n     @@ scalar.c: static int set_recommended_config(int reconfigure)\n       \t\t{ \"core.autoCRLF\", \"false\" },\n       \t\t{ \"core.safeCRLF\", \"false\" },\n       \t\t{ \"fetch.showForcedUpdates\", \"false\" },\n     -+\t\t{ \"push.usePathWalk\", \"true\" },\n     ++\t\t{ \"pack.usePathWalk\", \"true\" },\n       \t\t{ NULL, NULL },\n       \t};\n       \tint i;\n 16:  c3f24dc3647 = 16:  030d8ec238e pack-objects: refactor path-walk delta phase\n 17:  264affbf058 = 17:  fddc320eb0b pack-objects: thread the path-based compression\n\n-- \ngitgitgadget\n"},{"id":"505540","messageId":"98bdc94a773d797152bebfbfda88fa1bb0707821.1729431810.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 01/17] path-walk: introduce an object walk by path","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:14Z","receivedAt":"2024-10-20T13:43:36Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nIn anticipation of a few planned applications, introduce the most basic form\nof a path-walk API. It currently assumes that there are no UNINTERESTING\nobjects and does not include any complicated filters. It calls a function\npointer on groups of tree and blob objects as grouped by path. This only\nincludes objects the first time they are discovered, so an object that\nappears at multiple paths will not be included in two batches.\n\nThere are many future adaptations that could be made, but they are left for\nfuture updates when consumers are ready to take advantage of those features.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/technical/api-path-walk.txt |  54 +++++\n Makefile                                  |   1 +\n path-walk.c                               | 241 ++++++++++++++++++++++\n path-walk.h                               |  43 ++++\n 4 files changed, 339 insertions(+)\n create mode 100644 Documentation/technical/api-path-walk.txt\n create mode 100644 path-walk.c\n create mode 100644 path-walk.h\n\ndiff --git a/Documentation/technical/api-path-walk.txt b/Documentation/technical/api-path-walk.txt\nnew file mode 100644\nindex 00000000000..6472222ae6d\n--- /dev/null\n+++ b/Documentation/technical/api-path-walk.txt\n@@ -0,0 +1,54 @@\n+Path-Walk API\n+=============\n+\n+The path-walk API is used to walk reachable objects, but to visit objects\n+in batches based on a common path they appear in, or by type.\n+\n+For example, all reachable commits are visited in a group. All tags are\n+visited in a group. Then, all root trees are visited. At some point, all\n+blobs reachable via a path `my/dir/to/A` are visited. When there are\n+multiple paths possible to reach the same object, then only one of those\n+paths is used to visit the object.\n+\n+When walking a range of commits with some `UNINTERESTING` objects, the\n+objects with the `UNINTERESTING` flag are included in these batches. In\n+order to walk `UNINTERESTING` objects, the `--boundary` option must be\n+used in the commit walk in order to visit `UNINTERESTING` commits.\n+\n+Basics\n+------\n+\n+To use the path-walk API, include `path-walk.h` and call\n+`walk_objects_by_path()` with a customized `path_walk_info` struct. The\n+struct is used to set all of the options for how the walk should proceed.\n+Let's dig into the different options and their use.\n+\n+`path_fn` and `path_fn_data`::\n+\tThe most important option is the `path_fn` option, which is a\n+\tfunction pointer to the callback that can execute logic on the\n+\tobject IDs for objects grouped by type and path. This function\n+\talso receives a `data` value that corresponds to the\n+\t`path_fn_data` member, for providing custom data structures to\n+\tthis callback function.\n+\n+`revs`::\n+\tTo configure the exact details of the reachable set of objects,\n+\tuse the `revs` member and initialize it using the revision\n+\tmachinery in `revision.h`. Initialize `revs` using calls such as\n+\t`setup_revisions()` or `parse_revision_opt()`. Do not call\n+\t`prepare_revision_walk()`, as that will be called within\n+\t`walk_objects_by_path()`.\n++\n+It is also important that you do not specify the `--objects` flag for the\n+`revs` struct. The revision walk should only be used to walk commits, and\n+the objects will be walked in a separate way based on those starting\n+commits.\n++\n+If you want the path-walk API to emit `UNINTERESTING` objects based on the\n+commit walk's boundary, be sure to set `revs.boundary` so the boundary\n+commits are emitted.\n+\n+Examples\n+--------\n+\n+See example usages in future changes.\ndiff --git a/Makefile b/Makefile\nindex 7344a7f7257..d0d8d6888e3 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1094,6 +1094,7 @@ LIB_OBJS += parse-options.o\n LIB_OBJS += patch-delta.o\n LIB_OBJS += patch-ids.o\n LIB_OBJS += path.o\n+LIB_OBJS += path-walk.o\n LIB_OBJS += pathspec.o\n LIB_OBJS += pkt-line.o\n LIB_OBJS += preload-index.o\ndiff --git a/path-walk.c b/path-walk.c\nnew file mode 100644\nindex 00000000000..66840187e28\n--- /dev/null\n+++ b/path-walk.c\n@@ -0,0 +1,241 @@\n+/*\n+ * path-walk.c: implementation for path-based walks of the object graph.\n+ */\n+#include \"git-compat-util.h\"\n+#include \"path-walk.h\"\n+#include \"blob.h\"\n+#include \"commit.h\"\n+#include \"dir.h\"\n+#include \"hashmap.h\"\n+#include \"hex.h\"\n+#include \"object.h\"\n+#include \"oid-array.h\"\n+#include \"revision.h\"\n+#include \"string-list.h\"\n+#include \"strmap.h\"\n+#include \"trace2.h\"\n+#include \"tree.h\"\n+#include \"tree-walk.h\"\n+\n+struct type_and_oid_list\n+{\n+\tenum object_type type;\n+\tstruct oid_array oids;\n+};\n+\n+#define TYPE_AND_OID_LIST_INIT { \\\n+\t.type = OBJ_NONE, \t \\\n+\t.oids = OID_ARRAY_INIT\t \\\n+}\n+\n+struct path_walk_context {\n+\t/**\n+\t * Repeats of data in 'struct path_walk_info' for\n+\t * access with fewer characters.\n+\t */\n+\tstruct repository *repo;\n+\tstruct rev_info *revs;\n+\tstruct path_walk_info *info;\n+\n+\t/**\n+\t * Map a path to a 'struct type_and_oid_list'\n+\t * containing the objects discovered at that\n+\t * path.\n+\t */\n+\tstruct strmap paths_to_lists;\n+\n+\t/**\n+\t * Store the current list of paths in a stack, to\n+\t * facilitate depth-first-search without recursion.\n+\t */\n+\tstruct string_list path_stack;\n+};\n+\n+static int add_children(struct path_walk_context *ctx,\n+\t\t\tconst char *base_path,\n+\t\t\tstruct object_id *oid)\n+{\n+\tstruct tree_desc desc;\n+\tstruct name_entry entry;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tsize_t base_len;\n+\tstruct tree *tree = lookup_tree(ctx->repo, oid);\n+\n+\tif (!tree) {\n+\t\terror(_(\"failed to walk children of tree %s: not found\"),\n+\t\t      oid_to_hex(oid));\n+\t\treturn -1;\n+\t} else if (parse_tree_gently(tree, 1)) {\n+\t\tdie(\"bad tree object %s\", oid_to_hex(oid));\n+\t}\n+\n+\tstrbuf_addstr(&path, base_path);\n+\tbase_len = path.len;\n+\n+\tparse_tree(tree);\n+\tinit_tree_desc(&desc, &tree->object.oid, tree->buffer, tree->size);\n+\twhile (tree_entry(&desc, &entry)) {\n+\t\tstruct type_and_oid_list *list;\n+\t\tstruct object *o;\n+\t\t/* Not actually true, but we will ignore submodules later. */\n+\t\tenum object_type type = S_ISDIR(entry.mode) ? OBJ_TREE : OBJ_BLOB;\n+\n+\t\t/* Skip submodules. */\n+\t\tif (S_ISGITLINK(entry.mode))\n+\t\t\tcontinue;\n+\n+\t\tif (type == OBJ_TREE) {\n+\t\t\tstruct tree *child = lookup_tree(ctx->repo, &entry.oid);\n+\t\t\to = child ? &child->object : NULL;\n+\t\t} else if (type == OBJ_BLOB) {\n+\t\t\tstruct blob *child = lookup_blob(ctx->repo, &entry.oid);\n+\t\t\to = child ? &child->object : NULL;\n+\t\t} else {\n+\t\t\t/* Wrong type? */\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tif (!o) /* report error?*/\n+\t\t\tcontinue;\n+\n+\t\t/* Skip this object if already seen. */\n+\t\tif (o->flags & SEEN)\n+\t\t\tcontinue;\n+\t\to->flags |= SEEN;\n+\n+\t\tstrbuf_setlen(&path, base_len);\n+\t\tstrbuf_add(&path, entry.path, entry.pathlen);\n+\n+\t\t/*\n+\t\t * Trees will end with \"/\" for concatenation and distinction\n+\t\t * from blobs at the same path.\n+\t\t */\n+\t\tif (type == OBJ_TREE)\n+\t\t\tstrbuf_addch(&path, '/');\n+\n+\t\tif (!(list = strmap_get(&ctx->paths_to_lists, path.buf))) {\n+\t\t\tCALLOC_ARRAY(list, 1);\n+\t\t\tlist->type = type;\n+\t\t\tstrmap_put(&ctx->paths_to_lists, path.buf, list);\n+\t\t\tstring_list_append(&ctx->path_stack, path.buf);\n+\t\t}\n+\t\toid_array_append(&list->oids, &entry.oid);\n+\t}\n+\n+\tfree_tree_buffer(tree);\n+\tstrbuf_release(&path);\n+\treturn 0;\n+}\n+\n+/*\n+ * For each path in paths_to_explore, walk the trees another level\n+ * and add any found blobs to the batch (but only if they exist and\n+ * haven't been added yet).\n+ */\n+static int walk_path(struct path_walk_context *ctx,\n+\t\t     const char *path)\n+{\n+\tstruct type_and_oid_list *list;\n+\tint ret = 0;\n+\n+\tlist = strmap_get(&ctx->paths_to_lists, path);\n+\n+\t/* Evaluate function pointer on this data. */\n+\tret = ctx->info->path_fn(path, &list->oids, list->type,\n+\t\t\t\t ctx->info->path_fn_data);\n+\n+\t/* Expand data for children. */\n+\tif (list->type == OBJ_TREE) {\n+\t\tfor (size_t i = 0; i < list->oids.nr; i++) {\n+\t\t\tret |= add_children(ctx,\n+\t\t\t\t\t    path,\n+\t\t\t\t\t    &list->oids.oid[i]);\n+\t\t}\n+\t}\n+\n+\toid_array_clear(&list->oids);\n+\tstrmap_remove(&ctx->paths_to_lists, path, 1);\n+\treturn ret;\n+}\n+\n+static void clear_strmap(struct strmap *map)\n+{\n+\tstruct hashmap_iter iter;\n+\tstruct strmap_entry *e;\n+\n+\thashmap_for_each_entry(&map->map, &iter, e, ent) {\n+\t\tstruct type_and_oid_list *list = e->value;\n+\t\toid_array_clear(&list->oids);\n+\t}\n+\tstrmap_clear(map, 1);\n+\tstrmap_init(map);\n+}\n+\n+/**\n+ * Given the configuration of 'info', walk the commits based on 'info->revs' and\n+ * call 'info->path_fn' on each discovered path.\n+ *\n+ * Returns nonzero on an error.\n+ */\n+int walk_objects_by_path(struct path_walk_info *info)\n+{\n+\tconst char *root_path = \"\";\n+\tint ret = 0;\n+\tsize_t commits_nr = 0, paths_nr = 0;\n+\tstruct commit *c;\n+\tstruct type_and_oid_list *root_tree_list;\n+\tstruct path_walk_context ctx = {\n+\t\t.repo = info->revs->repo,\n+\t\t.revs = info->revs,\n+\t\t.info = info,\n+\t\t.path_stack = STRING_LIST_INIT_DUP,\n+\t\t.paths_to_lists = STRMAP_INIT\n+\t};\n+\n+\ttrace2_region_enter(\"path-walk\", \"commit-walk\", info->revs->repo);\n+\n+\t/* Insert a single list for the root tree into the paths. */\n+\tCALLOC_ARRAY(root_tree_list, 1);\n+\troot_tree_list->type = OBJ_TREE;\n+\tstrmap_put(&ctx.paths_to_lists, root_path, root_tree_list);\n+\n+\tif (prepare_revision_walk(info->revs))\n+\t\tdie(_(\"failed to setup revision walk\"));\n+\n+\twhile ((c = get_revision(info->revs))) {\n+\t\tstruct object_id *oid = get_commit_tree_oid(c);\n+\t\tstruct tree *t = lookup_tree(info->revs->repo, oid);\n+\t\tcommits_nr++;\n+\n+\t\tif (t) {\n+\t\t\tif (t->object.flags & SEEN)\n+\t\t\t\tcontinue;\n+\t\t\tt->object.flags |= SEEN;\n+\t\t\toid_array_append(&root_tree_list->oids, oid);\n+\t\t} else {\n+\t\t\twarning(\"could not find tree %s\", oid_to_hex(oid));\n+\t\t}\n+\t}\n+\n+\ttrace2_data_intmax(\"path-walk\", ctx.repo, \"commits\", commits_nr);\n+\ttrace2_region_leave(\"path-walk\", \"commit-walk\", info->revs->repo);\n+\n+\tstring_list_append(&ctx.path_stack, root_path);\n+\n+\ttrace2_region_enter(\"path-walk\", \"path-walk\", info->revs->repo);\n+\twhile (!ret && ctx.path_stack.nr) {\n+\t\tchar *path = ctx.path_stack.items[ctx.path_stack.nr - 1].string;\n+\t\tctx.path_stack.nr--;\n+\t\tpaths_nr++;\n+\n+\t\tret = walk_path(&ctx, path);\n+\n+\t\tfree(path);\n+\t}\n+\ttrace2_data_intmax(\"path-walk\", ctx.repo, \"paths\", paths_nr);\n+\ttrace2_region_leave(\"path-walk\", \"path-walk\", info->revs->repo);\n+\n+\tclear_strmap(&ctx.paths_to_lists);\n+\tstring_list_clear(&ctx.path_stack, 0);\n+\treturn ret;\n+}\ndiff --git a/path-walk.h b/path-walk.h\nnew file mode 100644\nindex 00000000000..c9e94a98bc8\n--- /dev/null\n+++ b/path-walk.h\n@@ -0,0 +1,43 @@\n+/*\n+ * path-walk.h : Methods and structures for walking the object graph in batches\n+ * by the paths that can reach those objects.\n+ */\n+#include \"object.h\" /* Required for 'enum object_type'. */\n+\n+struct rev_info;\n+struct oid_array;\n+\n+/**\n+ * The type of a function pointer for the method that is called on a list of\n+ * objects reachable at a given path.\n+ */\n+typedef int (*path_fn)(const char *path,\n+\t\t       struct oid_array *oids,\n+\t\t       enum object_type type,\n+\t\t       void *data);\n+\n+struct path_walk_info {\n+\t/**\n+\t * revs provides the definitions for the commit walk, including\n+\t * which commits are UNINTERESTING or not.\n+\t */\n+\tstruct rev_info *revs;\n+\n+\t/**\n+\t * The caller wishes to execute custom logic on objects reachable at a\n+\t * given path. Every reachable object will be visited exactly once, and\n+\t * the first path to see an object wins. This may not be a stable choice.\n+\t */\n+\tpath_fn path_fn;\n+\tvoid *path_fn_data;\n+};\n+\n+#define PATH_WALK_INFO_INIT { 0 }\n+\n+/**\n+ * Given the configuration of 'info', walk the commits based on 'info->revs' and\n+ * call 'info->path_fn' on each discovered path.\n+ *\n+ * Returns nonzero on an error.\n+ */\n+int walk_objects_by_path(struct path_walk_info *info);\n-- \ngitgitgadget\n\n"},{"id":"505541","messageId":"a00ab0c62c9f9c495981d9e565fdd10303ebe079.1729431810.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 02/17] t6601: add helper for testing path-walk API","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:15Z","receivedAt":"2024-10-20T13:43:37Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nAdd some tests based on the current behavior, doing interesting checks\nfor different sets of branches, ranges, and the --boundary option. This\nsets a baseline for the behavior and we can extend it as new options are\nintroduced.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/technical/api-path-walk.txt |   3 +-\n Makefile                                  |   1 +\n t/helper/test-path-walk.c                 |  86 ++++++++++++++\n t/helper/test-tool.c                      |   1 +\n t/helper/test-tool.h                      |   1 +\n t/t6601-path-walk.sh                      | 130 ++++++++++++++++++++++\n 6 files changed, 221 insertions(+), 1 deletion(-)\n create mode 100644 t/helper/test-path-walk.c\n create mode 100755 t/t6601-path-walk.sh\n\ndiff --git a/Documentation/technical/api-path-walk.txt b/Documentation/technical/api-path-walk.txt\nindex 6472222ae6d..e588897ab8d 100644\n--- a/Documentation/technical/api-path-walk.txt\n+++ b/Documentation/technical/api-path-walk.txt\n@@ -51,4 +51,5 @@ commits are emitted.\n Examples\n --------\n \n-See example usages in future changes.\n+See example usages in:\n+\t`t/helper/test-path-walk.c`\ndiff --git a/Makefile b/Makefile\nindex d0d8d6888e3..50413d96492 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -818,6 +818,7 @@ TEST_BUILTINS_OBJS += test-parse-options.o\n TEST_BUILTINS_OBJS += test-parse-pathspec-file.o\n TEST_BUILTINS_OBJS += test-partial-clone.o\n TEST_BUILTINS_OBJS += test-path-utils.o\n+TEST_BUILTINS_OBJS += test-path-walk.o\n TEST_BUILTINS_OBJS += test-pcre2-config.o\n TEST_BUILTINS_OBJS += test-pkt-line.o\n TEST_BUILTINS_OBJS += test-proc-receive.o\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nnew file mode 100644\nindex 00000000000..3c48f017fa0\n--- /dev/null\n+++ b/t/helper/test-path-walk.c\n@@ -0,0 +1,86 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n+#include \"test-tool.h\"\n+#include \"environment.h\"\n+#include \"hex.h\"\n+#include \"object-name.h\"\n+#include \"object.h\"\n+#include \"pretty.h\"\n+#include \"revision.h\"\n+#include \"setup.h\"\n+#include \"parse-options.h\"\n+#include \"path-walk.h\"\n+#include \"oid-array.h\"\n+\n+static const char * const path_walk_usage[] = {\n+\tN_(\"test-tool path-walk <options> -- <revision-options>\"),\n+\tNULL\n+};\n+\n+struct path_walk_test_data {\n+\tuintmax_t tree_nr;\n+\tuintmax_t blob_nr;\n+};\n+\n+static int emit_block(const char *path, struct oid_array *oids,\n+\t\t      enum object_type type, void *data)\n+{\n+\tstruct path_walk_test_data *tdata = data;\n+\tconst char *typestr;\n+\n+\tswitch (type) {\n+\tcase OBJ_TREE:\n+\t\ttypestr = \"TREE\";\n+\t\ttdata->tree_nr += oids->nr;\n+\t\tbreak;\n+\n+\tcase OBJ_BLOB:\n+\t\ttypestr = \"BLOB\";\n+\t\ttdata->blob_nr += oids->nr;\n+\t\tbreak;\n+\n+\tdefault:\n+\t\tBUG(\"we do not understand this type\");\n+\t}\n+\n+\tfor (size_t i = 0; i < oids->nr; i++)\n+\t\tprintf(\"%s:%s:%s\\n\", typestr, path, oid_to_hex(&oids->oid[i]));\n+\n+\treturn 0;\n+}\n+\n+int cmd__path_walk(int argc, const char **argv)\n+{\n+\tint res;\n+\tstruct rev_info revs = REV_INFO_INIT;\n+\tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n+\tstruct path_walk_test_data data = { 0 };\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\tinitialize_repository(the_repository);\n+\tsetup_git_directory();\n+\trevs.repo = the_repository;\n+\n+\targc = parse_options(argc, argv, NULL,\n+\t\t\t     options, path_walk_usage,\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_ARGV0);\n+\n+\tif (argc > 1)\n+\t\tsetup_revisions(argc, argv, &revs, NULL);\n+\telse\n+\t\tusage(path_walk_usage[0]);\n+\n+\tinfo.revs = &revs;\n+\tinfo.path_fn = emit_block;\n+\tinfo.path_fn_data = &data;\n+\n+\tres = walk_objects_by_path(&info);\n+\n+\tprintf(\"trees:%\" PRIuMAX \"\\n\"\n+\t       \"blobs:%\" PRIuMAX \"\\n\",\n+\t       data.tree_nr, data.blob_nr);\n+\n+\treturn res;\n+}\ndiff --git a/t/helper/test-tool.c b/t/helper/test-tool.c\nindex 1ebb69a5dc4..43676e7b93a 100644\n--- a/t/helper/test-tool.c\n+++ b/t/helper/test-tool.c\n@@ -52,6 +52,7 @@ static struct test_cmd cmds[] = {\n \t{ \"parse-subcommand\", cmd__parse_subcommand },\n \t{ \"partial-clone\", cmd__partial_clone },\n \t{ \"path-utils\", cmd__path_utils },\n+\t{ \"path-walk\", cmd__path_walk },\n \t{ \"pcre2-config\", cmd__pcre2_config },\n \t{ \"pkt-line\", cmd__pkt_line },\n \t{ \"proc-receive\", cmd__proc_receive },\ndiff --git a/t/helper/test-tool.h b/t/helper/test-tool.h\nindex 21802ac27da..9cfc5da6e57 100644\n--- a/t/helper/test-tool.h\n+++ b/t/helper/test-tool.h\n@@ -45,6 +45,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);\n int cmd__parse_subcommand(int argc, const char **argv);\n int cmd__partial_clone(int argc, const char **argv);\n int cmd__path_utils(int argc, const char **argv);\n+int cmd__path_walk(int argc, const char **argv);\n int cmd__pcre2_config(int argc, const char **argv);\n int cmd__pkt_line(int argc, const char **argv);\n int cmd__proc_receive(int argc, const char **argv);\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nnew file mode 100755\nindex 00000000000..ca18b61c3f1\n--- /dev/null\n+++ b/t/t6601-path-walk.sh\n@@ -0,0 +1,130 @@\n+#!/bin/sh\n+\n+test_description='direct path-walk API tests'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup test repository' '\n+\tgit checkout -b base &&\n+\n+\tmkdir left &&\n+\tmkdir right &&\n+\techo a >a &&\n+\techo b >left/b &&\n+\techo c >right/c &&\n+\tgit add . &&\n+\tgit commit -m \"first\" &&\n+\n+\techo d >right/d &&\n+\tgit add right &&\n+\tgit commit -m \"second\" &&\n+\n+\techo bb >left/b &&\n+\tgit commit -a -m \"third\" &&\n+\n+\tgit checkout -b topic HEAD~1 &&\n+\techo cc >right/c &&\n+\tgit commit -a -m \"topic\"\n+'\n+\n+test_expect_success 'all' '\n+\ttest-tool path-walk -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tTREE::$(git rev-parse topic^{tree})\n+\tTREE::$(git rev-parse base^{tree})\n+\tTREE::$(git rev-parse base~1^{tree})\n+\tTREE::$(git rev-parse base~2^{tree})\n+\tTREE:left/:$(git rev-parse base:left)\n+\tTREE:left/:$(git rev-parse base~2:left)\n+\tTREE:right/:$(git rev-parse topic:right)\n+\tTREE:right/:$(git rev-parse base~1:right)\n+\tTREE:right/:$(git rev-parse base~2:right)\n+\ttrees:9\n+\tBLOB:a:$(git rev-parse base~2:a)\n+\tBLOB:left/b:$(git rev-parse base~2:left/b)\n+\tBLOB:left/b:$(git rev-parse base:left/b)\n+\tBLOB:right/c:$(git rev-parse base~2:right/c)\n+\tBLOB:right/c:$(git rev-parse topic:right/c)\n+\tBLOB:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:6\n+\tEOF\n+\n+\tsort expect >expect.sorted &&\n+\tsort out >out.sorted &&\n+\n+\ttest_cmp expect.sorted out.sorted\n+'\n+\n+test_expect_success 'topic only' '\n+\ttest-tool path-walk -- topic >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tTREE::$(git rev-parse topic^{tree})\n+\tTREE::$(git rev-parse base~1^{tree})\n+\tTREE::$(git rev-parse base~2^{tree})\n+\tTREE:left/:$(git rev-parse base~2:left)\n+\tTREE:right/:$(git rev-parse topic:right)\n+\tTREE:right/:$(git rev-parse base~1:right)\n+\tTREE:right/:$(git rev-parse base~2:right)\n+\ttrees:7\n+\tBLOB:a:$(git rev-parse base~2:a)\n+\tBLOB:left/b:$(git rev-parse base~2:left/b)\n+\tBLOB:right/c:$(git rev-parse base~2:right/c)\n+\tBLOB:right/c:$(git rev-parse topic:right/c)\n+\tBLOB:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:5\n+\tEOF\n+\n+\tsort expect >expect.sorted &&\n+\tsort out >out.sorted &&\n+\n+\ttest_cmp expect.sorted out.sorted\n+'\n+\n+test_expect_success 'topic, not base' '\n+\ttest-tool path-walk -- topic --not base >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tTREE::$(git rev-parse topic^{tree})\n+\tTREE:left/:$(git rev-parse topic:left)\n+\tTREE:right/:$(git rev-parse topic:right)\n+\ttrees:3\n+\tBLOB:a:$(git rev-parse topic:a)\n+\tBLOB:left/b:$(git rev-parse topic:left/b)\n+\tBLOB:right/c:$(git rev-parse topic:right/c)\n+\tBLOB:right/d:$(git rev-parse topic:right/d)\n+\tblobs:4\n+\tEOF\n+\n+\tsort expect >expect.sorted &&\n+\tsort out >out.sorted &&\n+\n+\ttest_cmp expect.sorted out.sorted\n+'\n+\n+test_expect_success 'topic, not base, boundary' '\n+\ttest-tool path-walk -- --boundary topic --not base >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tTREE::$(git rev-parse topic^{tree})\n+\tTREE::$(git rev-parse base~1^{tree})\n+\tTREE:left/:$(git rev-parse base~1:left)\n+\tTREE:right/:$(git rev-parse topic:right)\n+\tTREE:right/:$(git rev-parse base~1:right)\n+\ttrees:5\n+\tBLOB:a:$(git rev-parse base~1:a)\n+\tBLOB:left/b:$(git rev-parse base~1:left/b)\n+\tBLOB:right/c:$(git rev-parse base~1:right/c)\n+\tBLOB:right/c:$(git rev-parse topic:right/c)\n+\tBLOB:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:5\n+\tEOF\n+\n+\tsort expect >expect.sorted &&\n+\tsort out >out.sorted &&\n+\n+\ttest_cmp expect.sorted out.sorted\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"505542","messageId":"14375d19392c406d903f6cb26be5a39c4c2ff1e9.1729431810.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 03/17] path-walk: allow consumer to specify object types","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:16Z","receivedAt":"2024-10-20T13:43:39Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nWe add the ability to filter the object types in the path-walk API so\nthe callback function is called fewer times.\n\nThis adds the ability to ask for the commits in a list, as well. Future\nchanges will add the ability to visit annotated tags.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/technical/api-path-walk.txt |  9 +++\n path-walk.c                               | 39 ++++++++++--\n path-walk.h                               | 13 +++-\n t/helper/test-path-walk.c                 | 17 +++++-\n t/t6601-path-walk.sh                      | 72 +++++++++++++++++++++++\n 5 files changed, 141 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/technical/api-path-walk.txt b/Documentation/technical/api-path-walk.txt\nindex e588897ab8d..b7ae476ea0a 100644\n--- a/Documentation/technical/api-path-walk.txt\n+++ b/Documentation/technical/api-path-walk.txt\n@@ -48,6 +48,15 @@ If you want the path-walk API to emit `UNINTERESTING` objects based on the\n commit walk's boundary, be sure to set `revs.boundary` so the boundary\n commits are emitted.\n \n+`commits`, `blobs`, `trees`::\n+\tBy default, these members are enabled and signal that the path-walk\n+\tAPI should call the `path_fn` on objects of these types. Specialized\n+\tapplications could disable some options to make it simpler to walk\n+\tthe objects or to have fewer calls to `path_fn`.\n++\n+While it is possible to walk only commits in this way, consumers would be\n+better off using the revision walk API instead.\n+\n Examples\n --------\n \ndiff --git a/path-walk.c b/path-walk.c\nindex 66840187e28..22e1aa13f31 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -84,6 +84,10 @@ static int add_children(struct path_walk_context *ctx,\n \t\tif (S_ISGITLINK(entry.mode))\n \t\t\tcontinue;\n \n+\t\t/* If the caller doesn't want blobs, then don't bother. */\n+\t\tif (!ctx->info->blobs && type == OBJ_BLOB)\n+\t\t\tcontinue;\n+\n \t\tif (type == OBJ_TREE) {\n \t\t\tstruct tree *child = lookup_tree(ctx->repo, &entry.oid);\n \t\t\to = child ? &child->object : NULL;\n@@ -140,9 +144,11 @@ static int walk_path(struct path_walk_context *ctx,\n \n \tlist = strmap_get(&ctx->paths_to_lists, path);\n \n-\t/* Evaluate function pointer on this data. */\n-\tret = ctx->info->path_fn(path, &list->oids, list->type,\n-\t\t\t\t ctx->info->path_fn_data);\n+\t/* Evaluate function pointer on this data, if requested. */\n+\tif ((list->type == OBJ_TREE && ctx->info->trees) ||\n+\t    (list->type == OBJ_BLOB && ctx->info->blobs))\n+\t\tret = ctx->info->path_fn(path, &list->oids, list->type,\n+\t\t\t\t\tctx->info->path_fn_data);\n \n \t/* Expand data for children. */\n \tif (list->type == OBJ_TREE) {\n@@ -184,6 +190,7 @@ int walk_objects_by_path(struct path_walk_info *info)\n \tsize_t commits_nr = 0, paths_nr = 0;\n \tstruct commit *c;\n \tstruct type_and_oid_list *root_tree_list;\n+\tstruct type_and_oid_list *commit_list;\n \tstruct path_walk_context ctx = {\n \t\t.repo = info->revs->repo,\n \t\t.revs = info->revs,\n@@ -194,19 +201,32 @@ int walk_objects_by_path(struct path_walk_info *info)\n \n \ttrace2_region_enter(\"path-walk\", \"commit-walk\", info->revs->repo);\n \n+\tCALLOC_ARRAY(commit_list, 1);\n+\tcommit_list->type = OBJ_COMMIT;\n+\n \t/* Insert a single list for the root tree into the paths. */\n \tCALLOC_ARRAY(root_tree_list, 1);\n \troot_tree_list->type = OBJ_TREE;\n \tstrmap_put(&ctx.paths_to_lists, root_path, root_tree_list);\n-\n \tif (prepare_revision_walk(info->revs))\n \t\tdie(_(\"failed to setup revision walk\"));\n \n \twhile ((c = get_revision(info->revs))) {\n-\t\tstruct object_id *oid = get_commit_tree_oid(c);\n-\t\tstruct tree *t = lookup_tree(info->revs->repo, oid);\n+\t\tstruct object_id *oid;\n+\t\tstruct tree *t;\n \t\tcommits_nr++;\n \n+\t\tif (info->commits)\n+\t\t\toid_array_append(&commit_list->oids,\n+\t\t\t\t\t &c->object.oid);\n+\n+\t\t/* If we only care about commits, then skip trees. */\n+\t\tif (!info->trees && !info->blobs)\n+\t\t\tcontinue;\n+\n+\t\toid = get_commit_tree_oid(c);\n+\t\tt = lookup_tree(info->revs->repo, oid);\n+\n \t\tif (t) {\n \t\t\tif (t->object.flags & SEEN)\n \t\t\t\tcontinue;\n@@ -220,6 +240,13 @@ int walk_objects_by_path(struct path_walk_info *info)\n \ttrace2_data_intmax(\"path-walk\", ctx.repo, \"commits\", commits_nr);\n \ttrace2_region_leave(\"path-walk\", \"commit-walk\", info->revs->repo);\n \n+\t/* Track all commits. */\n+\tif (info->commits)\n+\t\tret = info->path_fn(\"\", &commit_list->oids, OBJ_COMMIT,\n+\t\t\t\t    info->path_fn_data);\n+\toid_array_clear(&commit_list->oids);\n+\tfree(commit_list);\n+\n \tstring_list_append(&ctx.path_stack, root_path);\n \n \ttrace2_region_enter(\"path-walk\", \"path-walk\", info->revs->repo);\ndiff --git a/path-walk.h b/path-walk.h\nindex c9e94a98bc8..6ef372d8942 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -30,9 +30,20 @@ struct path_walk_info {\n \t */\n \tpath_fn path_fn;\n \tvoid *path_fn_data;\n+\t/**\n+\t * Initialize which object types the path_fn should be called on. This\n+\t * could also limit the walk to skip blobs if not set.\n+\t */\n+\tint commits;\n+\tint trees;\n+\tint blobs;\n };\n \n-#define PATH_WALK_INFO_INIT { 0 }\n+#define PATH_WALK_INFO_INIT {   \\\n+\t.blobs = 1,\t\t\\\n+\t.trees = 1,\t\t\\\n+\t.commits = 1,\t\t\\\n+}\n \n /**\n  * Given the configuration of 'info', walk the commits based on 'info->revs' and\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex 3c48f017fa0..37c5e3e31e8 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -18,6 +18,7 @@ static const char * const path_walk_usage[] = {\n };\n \n struct path_walk_test_data {\n+\tuintmax_t commit_nr;\n \tuintmax_t tree_nr;\n \tuintmax_t blob_nr;\n };\n@@ -29,6 +30,11 @@ static int emit_block(const char *path, struct oid_array *oids,\n \tconst char *typestr;\n \n \tswitch (type) {\n+\tcase OBJ_COMMIT:\n+\t\ttypestr = \"COMMIT\";\n+\t\ttdata->commit_nr += oids->nr;\n+\t\tbreak;\n+\n \tcase OBJ_TREE:\n \t\ttypestr = \"TREE\";\n \t\ttdata->tree_nr += oids->nr;\n@@ -56,6 +62,12 @@ int cmd__path_walk(int argc, const char **argv)\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n \tstruct path_walk_test_data data = { 0 };\n \tstruct option options[] = {\n+\t\tOPT_BOOL(0, \"blobs\", &info.blobs,\n+\t\t\t N_(\"toggle inclusion of blob objects\")),\n+\t\tOPT_BOOL(0, \"commits\", &info.commits,\n+\t\t\t N_(\"toggle inclusion of commit objects\")),\n+\t\tOPT_BOOL(0, \"trees\", &info.trees,\n+\t\t\t N_(\"toggle inclusion of tree objects\")),\n \t\tOPT_END(),\n \t};\n \n@@ -78,9 +90,10 @@ int cmd__path_walk(int argc, const char **argv)\n \n \tres = walk_objects_by_path(&info);\n \n-\tprintf(\"trees:%\" PRIuMAX \"\\n\"\n+\tprintf(\"commits:%\" PRIuMAX \"\\n\"\n+\t       \"trees:%\" PRIuMAX \"\\n\"\n \t       \"blobs:%\" PRIuMAX \"\\n\",\n-\t       data.tree_nr, data.blob_nr);\n+\t       data.commit_nr, data.tree_nr, data.blob_nr);\n \n \treturn res;\n }\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex ca18b61c3f1..e4788664f93 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -31,6 +31,11 @@ test_expect_success 'all' '\n \ttest-tool path-walk -- --all >out &&\n \n \tcat >expect <<-EOF &&\n+\tCOMMIT::$(git rev-parse topic)\n+\tCOMMIT::$(git rev-parse base)\n+\tCOMMIT::$(git rev-parse base~1)\n+\tCOMMIT::$(git rev-parse base~2)\n+\tcommits:4\n \tTREE::$(git rev-parse topic^{tree})\n \tTREE::$(git rev-parse base^{tree})\n \tTREE::$(git rev-parse base~1^{tree})\n@@ -60,6 +65,10 @@ test_expect_success 'topic only' '\n \ttest-tool path-walk -- topic >out &&\n \n \tcat >expect <<-EOF &&\n+\tCOMMIT::$(git rev-parse topic)\n+\tCOMMIT::$(git rev-parse base~1)\n+\tCOMMIT::$(git rev-parse base~2)\n+\tcommits:3\n \tTREE::$(git rev-parse topic^{tree})\n \tTREE::$(git rev-parse base~1^{tree})\n \tTREE::$(git rev-parse base~2^{tree})\n@@ -86,6 +95,8 @@ test_expect_success 'topic, not base' '\n \ttest-tool path-walk -- topic --not base >out &&\n \n \tcat >expect <<-EOF &&\n+\tCOMMIT::$(git rev-parse topic)\n+\tcommits:1\n \tTREE::$(git rev-parse topic^{tree})\n \tTREE:left/:$(git rev-parse topic:left)\n \tTREE:right/:$(git rev-parse topic:right)\n@@ -103,10 +114,71 @@ test_expect_success 'topic, not base' '\n \ttest_cmp expect.sorted out.sorted\n '\n \n+test_expect_success 'topic, not base, only blobs' '\n+\ttest-tool path-walk --no-trees --no-commits \\\n+\t\t-- topic --not base >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tcommits:0\n+\ttrees:0\n+\tBLOB:a:$(git rev-parse topic:a)\n+\tBLOB:left/b:$(git rev-parse topic:left/b)\n+\tBLOB:right/c:$(git rev-parse topic:right/c)\n+\tBLOB:right/d:$(git rev-parse topic:right/d)\n+\tblobs:4\n+\tEOF\n+\n+\tsort expect >expect.sorted &&\n+\tsort out >out.sorted &&\n+\n+\ttest_cmp expect.sorted out.sorted\n+'\n+\n+# No, this doesn't make a lot of sense for the path-walk API,\n+# but it is possible to do.\n+test_expect_success 'topic, not base, only commits' '\n+\ttest-tool path-walk --no-blobs --no-trees \\\n+\t\t-- topic --not base >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tCOMMIT::$(git rev-parse topic)\n+\tcommits:1\n+\ttrees:0\n+\tblobs:0\n+\tEOF\n+\n+\tsort expect >expect.sorted &&\n+\tsort out >out.sorted &&\n+\n+\ttest_cmp expect.sorted out.sorted\n+'\n+\n+test_expect_success 'topic, not base, only trees' '\n+\ttest-tool path-walk --no-blobs --no-commits \\\n+\t\t-- topic --not base >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tcommits:0\n+\tTREE::$(git rev-parse topic^{tree})\n+\tTREE:left/:$(git rev-parse topic:left)\n+\tTREE:right/:$(git rev-parse topic:right)\n+\ttrees:3\n+\tblobs:0\n+\tEOF\n+\n+\tsort expect >expect.sorted &&\n+\tsort out >out.sorted &&\n+\n+\ttest_cmp expect.sorted out.sorted\n+'\n+\n test_expect_success 'topic, not base, boundary' '\n \ttest-tool path-walk -- --boundary topic --not base >out &&\n \n \tcat >expect <<-EOF &&\n+\tCOMMIT::$(git rev-parse topic)\n+\tCOMMIT::$(git rev-parse base~1)\n+\tcommits:2\n \tTREE::$(git rev-parse topic^{tree})\n \tTREE::$(git rev-parse base~1^{tree})\n \tTREE:left/:$(git rev-parse base~1:left)\n-- \ngitgitgadget\n\n"},{"id":"505543","messageId":"c321f58c62d3205f00f3272ae5d560c7ccb79000.1729431810.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 04/17] path-walk: allow visiting tags","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:17Z","receivedAt":"2024-10-20T13:43:39Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nIn anticipation of using the path-walk API to analyze tags or include\nthem in a pack-file, add the ability to walk the tags that were included\nin the revision walk.\n\nWhen these tag objects point to blobs or trees, we need to make sure\nthose objects are also visited. Treat tagged trees as root trees, but\nput the tagged blobs in their own category.\n\nBe careful about objects that are referred to by multiple references.\n\nCo-authored-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>\nSigned-off-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/technical/api-path-walk.txt |  2 +-\n path-walk.c                               | 78 +++++++++++++++++++++\n path-walk.h                               |  2 +\n t/helper/test-path-walk.c                 | 13 +++-\n t/t6601-path-walk.sh                      | 85 +++++++++++++++++++++--\n 5 files changed, 172 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/technical/api-path-walk.txt b/Documentation/technical/api-path-walk.txt\nindex b7ae476ea0a..5fea1d1db17 100644\n--- a/Documentation/technical/api-path-walk.txt\n+++ b/Documentation/technical/api-path-walk.txt\n@@ -48,7 +48,7 @@ If you want the path-walk API to emit `UNINTERESTING` objects based on the\n commit walk's boundary, be sure to set `revs.boundary` so the boundary\n commits are emitted.\n \n-`commits`, `blobs`, `trees`::\n+`commits`, `blobs`, `trees`, `tags`::\n \tBy default, these members are enabled and signal that the path-walk\n \tAPI should call the `path_fn` on objects of these types. Specialized\n \tapplications could disable some options to make it simpler to walk\ndiff --git a/path-walk.c b/path-walk.c\nindex 22e1aa13f31..55758f50abd 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -13,6 +13,7 @@\n #include \"revision.h\"\n #include \"string-list.h\"\n #include \"strmap.h\"\n+#include \"tag.h\"\n #include \"trace2.h\"\n #include \"tree.h\"\n #include \"tree-walk.h\"\n@@ -204,13 +205,90 @@ int walk_objects_by_path(struct path_walk_info *info)\n \tCALLOC_ARRAY(commit_list, 1);\n \tcommit_list->type = OBJ_COMMIT;\n \n+\tif (info->tags)\n+\t\tinfo->revs->tag_objects = 1;\n+\n \t/* Insert a single list for the root tree into the paths. */\n \tCALLOC_ARRAY(root_tree_list, 1);\n \troot_tree_list->type = OBJ_TREE;\n \tstrmap_put(&ctx.paths_to_lists, root_path, root_tree_list);\n+\n+\t/*\n+\t * Set these values before preparing the walk to catch\n+\t * lightweight tags pointing to non-commits.\n+\t */\n+\tinfo->revs->blob_objects = info->blobs;\n+\tinfo->revs->tree_objects = info->trees;\n+\n \tif (prepare_revision_walk(info->revs))\n \t\tdie(_(\"failed to setup revision walk\"));\n \n+\tinfo->revs->blob_objects = info->revs->tree_objects = 0;\n+\n+\tif (info->tags) {\n+\t\tstruct oid_array tagged_blob_list = OID_ARRAY_INIT;\n+\t\tstruct oid_array tags = OID_ARRAY_INIT;\n+\n+\t\ttrace2_region_enter(\"path-walk\", \"tag-walk\", info->revs->repo);\n+\n+\t\t/*\n+\t\t * Walk any pending objects at this point, but they should only\n+\t\t * be tags.\n+\t\t */\n+\t\tfor (size_t i = 0; i < info->revs->pending.nr; i++) {\n+\t\t\tstruct object_array_entry *pending = info->revs->pending.objects + i;\n+\t\t\tstruct object *obj = pending->item;\n+\n+\t\t\tif (obj->type == OBJ_COMMIT || obj->flags & SEEN)\n+\t\t\t\tcontinue;\n+\n+\t\t\twhile (obj->type == OBJ_TAG) {\n+\t\t\t\tstruct tag *tag = lookup_tag(info->revs->repo,\n+\t\t\t\t\t\t\t     &obj->oid);\n+\t\t\t\tif (!(obj->flags & SEEN)) {\n+\t\t\t\t\tobj->flags |= SEEN;\n+\t\t\t\t\toid_array_append(&tags, &obj->oid);\n+\t\t\t\t}\n+\t\t\t\tobj = tag->tagged;\n+\t\t\t}\n+\n+\t\t\tif ((obj->flags & SEEN))\n+\t\t\t\tcontinue;\n+\t\t\tobj->flags |= SEEN;\n+\n+\t\t\tswitch (obj->type) {\n+\t\t\tcase OBJ_TREE:\n+\t\t\t\tif (info->trees)\n+\t\t\t\t\toid_array_append(&root_tree_list->oids, &obj->oid);\n+\t\t\t\tbreak;\n+\n+\t\t\tcase OBJ_BLOB:\n+\t\t\t\tif (info->blobs)\n+\t\t\t\t\toid_array_append(&tagged_blob_list, &obj->oid);\n+\t\t\t\tbreak;\n+\n+\t\t\tcase OBJ_COMMIT:\n+\t\t\t\t/* Make sure it is in the object walk */\n+\t\t\t\tadd_pending_object(info->revs, obj, \"\");\n+\t\t\t\tbreak;\n+\n+\t\t\tdefault:\n+\t\t\t\tBUG(\"should not see any other type here\");\n+\t\t\t}\n+\t\t}\n+\n+\t\tinfo->path_fn(\"\", &tags, OBJ_TAG, info->path_fn_data);\n+\n+\t\tif (tagged_blob_list.nr && info->blobs)\n+\t\t\tinfo->path_fn(\"/tagged-blobs\", &tagged_blob_list, OBJ_BLOB,\n+\t\t\t\t      info->path_fn_data);\n+\n+\t\ttrace2_data_intmax(\"path-walk\", ctx.repo, \"tags\", tags.nr);\n+\t\ttrace2_region_leave(\"path-walk\", \"tag-walk\", info->revs->repo);\n+\t\toid_array_clear(&tags);\n+\t\toid_array_clear(&tagged_blob_list);\n+\t}\n+\n \twhile ((c = get_revision(info->revs))) {\n \t\tstruct object_id *oid;\n \t\tstruct tree *t;\ndiff --git a/path-walk.h b/path-walk.h\nindex 6ef372d8942..3f3b63180ef 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -37,12 +37,14 @@ struct path_walk_info {\n \tint commits;\n \tint trees;\n \tint blobs;\n+\tint tags;\n };\n \n #define PATH_WALK_INFO_INIT {   \\\n \t.blobs = 1,\t\t\\\n \t.trees = 1,\t\t\\\n \t.commits = 1,\t\t\\\n+\t.tags = 1,\t\t\\\n }\n \n /**\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex 37c5e3e31e8..c6c60d68749 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -21,6 +21,7 @@ struct path_walk_test_data {\n \tuintmax_t commit_nr;\n \tuintmax_t tree_nr;\n \tuintmax_t blob_nr;\n+\tuintmax_t tag_nr;\n };\n \n static int emit_block(const char *path, struct oid_array *oids,\n@@ -45,6 +46,11 @@ static int emit_block(const char *path, struct oid_array *oids,\n \t\ttdata->blob_nr += oids->nr;\n \t\tbreak;\n \n+\tcase OBJ_TAG:\n+\t\ttypestr = \"TAG\";\n+\t\ttdata->tag_nr += oids->nr;\n+\t\tbreak;\n+\n \tdefault:\n \t\tBUG(\"we do not understand this type\");\n \t}\n@@ -66,6 +72,8 @@ int cmd__path_walk(int argc, const char **argv)\n \t\t\t N_(\"toggle inclusion of blob objects\")),\n \t\tOPT_BOOL(0, \"commits\", &info.commits,\n \t\t\t N_(\"toggle inclusion of commit objects\")),\n+\t\tOPT_BOOL(0, \"tags\", &info.tags,\n+\t\t\t N_(\"toggle inclusion of tag objects\")),\n \t\tOPT_BOOL(0, \"trees\", &info.trees,\n \t\t\t N_(\"toggle inclusion of tree objects\")),\n \t\tOPT_END(),\n@@ -92,8 +100,9 @@ int cmd__path_walk(int argc, const char **argv)\n \n \tprintf(\"commits:%\" PRIuMAX \"\\n\"\n \t       \"trees:%\" PRIuMAX \"\\n\"\n-\t       \"blobs:%\" PRIuMAX \"\\n\",\n-\t       data.commit_nr, data.tree_nr, data.blob_nr);\n+\t       \"blobs:%\" PRIuMAX \"\\n\"\n+\t       \"tags:%\" PRIuMAX \"\\n\",\n+\t       data.commit_nr, data.tree_nr, data.blob_nr, data.tag_nr);\n \n \treturn res;\n }\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex e4788664f93..7758e2529ee 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -7,24 +7,55 @@ test_description='direct path-walk API tests'\n test_expect_success 'setup test repository' '\n \tgit checkout -b base &&\n \n+\t# Make some objects that will only be reachable\n+\t# via non-commit tags.\n+\tmkdir child &&\n+\techo file >child/file &&\n+\tgit add child &&\n+\tgit commit -m \"will abandon\" &&\n+\tgit tag -a -m \"tree\" tree-tag HEAD^{tree} &&\n+\techo file2 >file2 &&\n+\tgit add file2 &&\n+\tgit commit --amend -m \"will abandon\" &&\n+\tgit tag tree-tag2 HEAD^{tree} &&\n+\n+\techo blob >file &&\n+\tblob_oid=$(git hash-object -t blob -w --stdin <file) &&\n+\tgit tag -a -m \"blob\" blob-tag \"$blob_oid\" &&\n+\techo blob2 >file2 &&\n+\tblob2_oid=$(git hash-object -t blob -w --stdin <file2) &&\n+\tgit tag blob-tag2 \"$blob2_oid\" &&\n+\n+\trm -fr child file file2 &&\n+\n \tmkdir left &&\n \tmkdir right &&\n \techo a >a &&\n \techo b >left/b &&\n \techo c >right/c &&\n \tgit add . &&\n-\tgit commit -m \"first\" &&\n+\tgit commit --amend -m \"first\" &&\n+\tgit tag -m \"first\" first HEAD &&\n \n \techo d >right/d &&\n \tgit add right &&\n \tgit commit -m \"second\" &&\n+\tgit tag -a -m \"second (under)\" second.1 HEAD &&\n+\tgit tag -a -m \"second (top)\" second.2 second.1 &&\n \n+\t# Set up file/dir collision in history.\n+\trm a &&\n+\tmkdir a &&\n+\techo a >a/a &&\n \techo bb >left/b &&\n-\tgit commit -a -m \"third\" &&\n+\tgit add a left &&\n+\tgit commit -m \"third\" &&\n+\tgit tag -a -m \"third\" third &&\n \n \tgit checkout -b topic HEAD~1 &&\n \techo cc >right/c &&\n-\tgit commit -a -m \"topic\"\n+\tgit commit -a -m \"topic\" &&\n+\tgit tag -a -m \"fourth\" fourth\n '\n \n test_expect_success 'all' '\n@@ -40,19 +71,35 @@ test_expect_success 'all' '\n \tTREE::$(git rev-parse base^{tree})\n \tTREE::$(git rev-parse base~1^{tree})\n \tTREE::$(git rev-parse base~2^{tree})\n+\tTREE::$(git rev-parse refs/tags/tree-tag^{})\n+\tTREE::$(git rev-parse refs/tags/tree-tag2^{})\n+\tTREE:a/:$(git rev-parse base:a)\n \tTREE:left/:$(git rev-parse base:left)\n \tTREE:left/:$(git rev-parse base~2:left)\n \tTREE:right/:$(git rev-parse topic:right)\n \tTREE:right/:$(git rev-parse base~1:right)\n \tTREE:right/:$(git rev-parse base~2:right)\n-\ttrees:9\n+\tTREE:child/:$(git rev-parse refs/tags/tree-tag^{}:child)\n+\ttrees:13\n \tBLOB:a:$(git rev-parse base~2:a)\n+\tBLOB:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n \tBLOB:left/b:$(git rev-parse base~2:left/b)\n \tBLOB:left/b:$(git rev-parse base:left/b)\n \tBLOB:right/c:$(git rev-parse base~2:right/c)\n \tBLOB:right/c:$(git rev-parse topic:right/c)\n \tBLOB:right/d:$(git rev-parse base~1:right/d)\n-\tblobs:6\n+\tBLOB:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\tBLOB:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\tBLOB:child/file:$(git rev-parse refs/tags/tree-tag^{}:child/file)\n+\tblobs:10\n+\tTAG::$(git rev-parse refs/tags/first)\n+\tTAG::$(git rev-parse refs/tags/second.1)\n+\tTAG::$(git rev-parse refs/tags/second.2)\n+\tTAG::$(git rev-parse refs/tags/third)\n+\tTAG::$(git rev-parse refs/tags/fourth)\n+\tTAG::$(git rev-parse refs/tags/tree-tag)\n+\tTAG::$(git rev-parse refs/tags/blob-tag)\n+\ttags:7\n \tEOF\n \n \tsort expect >expect.sorted &&\n@@ -83,6 +130,7 @@ test_expect_success 'topic only' '\n \tBLOB:right/c:$(git rev-parse topic:right/c)\n \tBLOB:right/d:$(git rev-parse base~1:right/d)\n \tblobs:5\n+\ttags:0\n \tEOF\n \n \tsort expect >expect.sorted &&\n@@ -106,6 +154,7 @@ test_expect_success 'topic, not base' '\n \tBLOB:right/c:$(git rev-parse topic:right/c)\n \tBLOB:right/d:$(git rev-parse topic:right/d)\n \tblobs:4\n+\ttags:0\n \tEOF\n \n \tsort expect >expect.sorted &&\n@@ -126,6 +175,7 @@ test_expect_success 'topic, not base, only blobs' '\n \tBLOB:right/c:$(git rev-parse topic:right/c)\n \tBLOB:right/d:$(git rev-parse topic:right/d)\n \tblobs:4\n+\ttags:0\n \tEOF\n \n \tsort expect >expect.sorted &&\n@@ -145,6 +195,7 @@ test_expect_success 'topic, not base, only commits' '\n \tcommits:1\n \ttrees:0\n \tblobs:0\n+\ttags:0\n \tEOF\n \n \tsort expect >expect.sorted &&\n@@ -164,6 +215,7 @@ test_expect_success 'topic, not base, only trees' '\n \tTREE:right/:$(git rev-parse topic:right)\n \ttrees:3\n \tblobs:0\n+\ttags:0\n \tEOF\n \n \tsort expect >expect.sorted &&\n@@ -191,6 +243,7 @@ test_expect_success 'topic, not base, boundary' '\n \tBLOB:right/c:$(git rev-parse topic:right/c)\n \tBLOB:right/d:$(git rev-parse base~1:right/d)\n \tblobs:5\n+\ttags:0\n \tEOF\n \n \tsort expect >expect.sorted &&\n@@ -199,4 +252,26 @@ test_expect_success 'topic, not base, boundary' '\n \ttest_cmp expect.sorted out.sorted\n '\n \n+test_expect_success 'trees are reported exactly once' '\n+\ttest_when_finished \"rm -rf unique-trees\" &&\n+\ttest_create_repo unique-trees &&\n+\t(\n+\t\tcd unique-trees &&\n+\t\tmkdir initial &&\n+\t\ttest_commit initial/file &&\n+\n+\t\tgit switch -c move-to-top &&\n+\t\tgit mv initial/file.t ./ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m moved &&\n+\n+\t\tgit update-ref refs/heads/other HEAD\n+\t) &&\n+\n+\ttest-tool -C unique-trees path-walk -- --all >out &&\n+\ttree=$(git -C unique-trees rev-parse HEAD:) &&\n+\tgrep \"$tree\" out >out-filtered &&\n+\ttest_line_count = 1 out-filtered\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"505544","messageId":"6e89fb219b55316c6c67a6a4b87b6928ff5b6f93.1729431810.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 05/17] revision: create mark_trees_uninteresting_dense()","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:18Z","receivedAt":"2024-10-20T13:43:41Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe sparse tree walk algorithm was created in d5d2e93577e (revision:\nimplement sparse algorithm, 2019-01-16) and involves using the\nmark_trees_uninteresting_sparse() method. This method takes a repository\nand an oidset of tree IDs, some of which have the UNINTERESTING flag and\nsome of which do not.\n\nCreate a method that has an equivalent set of preconditions but uses a\n\"dense\" walk (recursively visits all reachable trees, as long as they\nhave not previously been marked UNINTERESTING). This is an important\ndifference from mark_tree_uninteresting(), which short-circuits if the\ngiven tree has the UNINTERESTING flag.\n\nA use of this method will be added in a later change, with a condition\nset whether the sparse or dense approach should be used.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n revision.c | 15 +++++++++++++++\n revision.h |  1 +\n 2 files changed, 16 insertions(+)\n\ndiff --git a/revision.c b/revision.c\nindex 2d7ad2bddff..bdc312f1538 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -219,6 +219,21 @@ static void add_children_by_path(struct repository *r,\n \tfree_tree_buffer(tree);\n }\n \n+void mark_trees_uninteresting_dense(struct repository *r,\n+\t\t\t\t    struct oidset *trees)\n+{\n+\tstruct object_id *oid;\n+\tstruct oidset_iter iter;\n+\n+\toidset_iter_init(trees, &iter);\n+\twhile ((oid = oidset_iter_next(&iter))) {\n+\t\tstruct tree *tree = lookup_tree(r, oid);\n+\n+\t\tif (tree && (tree->object.flags & UNINTERESTING))\n+\t\t\tmark_tree_contents_uninteresting(r, tree);\n+\t}\n+}\n+\n void mark_trees_uninteresting_sparse(struct repository *r,\n \t\t\t\t     struct oidset *trees)\n {\ndiff --git a/revision.h b/revision.h\nindex 71e984c452b..8938b2db112 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -487,6 +487,7 @@ void put_revision_mark(const struct rev_info *revs,\n \n void mark_parents_uninteresting(struct rev_info *revs, struct commit *commit);\n void mark_tree_uninteresting(struct repository *r, struct tree *tree);\n+void mark_trees_uninteresting_dense(struct repository *r, struct oidset *trees);\n void mark_trees_uninteresting_sparse(struct repository *r, struct oidset *trees);\n \n void show_object_with_name(FILE *, struct object *, const char *);\n-- \ngitgitgadget\n\n"},{"id":"505545","messageId":"238d7d95715d3e161489a5ef8d788c0cac4f7a03.1729431810.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 06/17] path-walk: add prune_all_uninteresting option","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:19Z","receivedAt":"2024-10-20T13:43:42Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThis option causes the path-walk API to act like the sparse tree-walk\nalgorithm implemented by mark_trees_uninteresting_sparse() in\nlist-objects.c.\n\nStarting from the commits marked as UNINTERESTING, their root trees and\nall objects reachable from those trees are UNINTERSTING, at least as we\nwalk path-by-path. When we reach a path where all objects associated\nwith that path are marked UNINTERESTING, then do no continue walking the\nchildren of that path.\n\nWe need to be careful to pass the UNINTERESTING flag in a deep way on\nthe UNINTERESTING objects before we start the path-walk, or else the\ndepth-first search for the path-walk API may accidentally report some\nobjects as interesting.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/technical/api-path-walk.txt |  8 +++\n path-walk.c                               | 64 ++++++++++++++++++++++-\n path-walk.h                               |  8 +++\n t/helper/test-path-walk.c                 | 10 +++-\n t/t6601-path-walk.sh                      | 40 +++++++++++---\n 5 files changed, 118 insertions(+), 12 deletions(-)\n\ndiff --git a/Documentation/technical/api-path-walk.txt b/Documentation/technical/api-path-walk.txt\nindex 5fea1d1db17..c51f92cd649 100644\n--- a/Documentation/technical/api-path-walk.txt\n+++ b/Documentation/technical/api-path-walk.txt\n@@ -57,6 +57,14 @@ commits are emitted.\n While it is possible to walk only commits in this way, consumers would be\n better off using the revision walk API instead.\n \n+`prune_all_uninteresting`::\n+\tBy default, all reachable paths are emitted by the path-walk API.\n+\tThis option allows consumers to declare that they are not\n+\tinterested in paths where all included objects are marked with the\n+\t`UNINTERESTING` flag. This requires using the `boundary` option in\n+\tthe revision walk so that the walk emits commits marked with the\n+\t`UNINTERESTING` flag.\n+\n Examples\n --------\n \ndiff --git a/path-walk.c b/path-walk.c\nindex 55758f50abd..910dfc6fdc9 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -22,6 +22,7 @@ struct type_and_oid_list\n {\n \tenum object_type type;\n \tstruct oid_array oids;\n+\tint maybe_interesting;\n };\n \n #define TYPE_AND_OID_LIST_INIT { \\\n@@ -124,6 +125,8 @@ static int add_children(struct path_walk_context *ctx,\n \t\t\tstrmap_put(&ctx->paths_to_lists, path.buf, list);\n \t\t\tstring_list_append(&ctx->path_stack, path.buf);\n \t\t}\n+\t\tif (!(o->flags & UNINTERESTING))\n+\t\t\tlist->maybe_interesting = 1;\n \t\toid_array_append(&list->oids, &entry.oid);\n \t}\n \n@@ -145,6 +148,40 @@ static int walk_path(struct path_walk_context *ctx,\n \n \tlist = strmap_get(&ctx->paths_to_lists, path);\n \n+\tif (ctx->info->prune_all_uninteresting) {\n+\t\t/*\n+\t\t * This is true if all objects were UNINTERESTING\n+\t\t * when added to the list.\n+\t\t */\n+\t\tif (!list->maybe_interesting)\n+\t\t\treturn 0;\n+\n+\t\t/*\n+\t\t * But it's still possible that the objects were set\n+\t\t * as UNINTERESTING after being added. Do a quick check.\n+\t\t */\n+\t\tlist->maybe_interesting = 0;\n+\t\tfor (size_t i = 0;\n+\t\t     !list->maybe_interesting && i < list->oids.nr;\n+\t\t     i++) {\n+\t\t\tif (list->type == OBJ_TREE) {\n+\t\t\t\tstruct tree *t = lookup_tree(ctx->repo,\n+\t\t\t\t\t\t\t     &list->oids.oid[i]);\n+\t\t\t\tif (t && !(t->object.flags & UNINTERESTING))\n+\t\t\t\t\tlist->maybe_interesting = 1;\n+\t\t\t} else {\n+\t\t\t\tstruct blob *b = lookup_blob(ctx->repo,\n+\t\t\t\t\t\t\t     &list->oids.oid[i]);\n+\t\t\t\tif (b && !(b->object.flags & UNINTERESTING))\n+\t\t\t\t\tlist->maybe_interesting = 1;\n+\t\t\t}\n+\t\t}\n+\n+\t\t/* We have confirmed that all objects are UNINTERESTING. */\n+\t\tif (!list->maybe_interesting)\n+\t\t\treturn 0;\n+\t}\n+\n \t/* Evaluate function pointer on this data, if requested. */\n \tif ((list->type == OBJ_TREE && ctx->info->trees) ||\n \t    (list->type == OBJ_BLOB && ctx->info->blobs))\n@@ -187,7 +224,7 @@ static void clear_strmap(struct strmap *map)\n int walk_objects_by_path(struct path_walk_info *info)\n {\n \tconst char *root_path = \"\";\n-\tint ret = 0;\n+\tint ret = 0, has_uninteresting = 0;\n \tsize_t commits_nr = 0, paths_nr = 0;\n \tstruct commit *c;\n \tstruct type_and_oid_list *root_tree_list;\n@@ -199,6 +236,7 @@ int walk_objects_by_path(struct path_walk_info *info)\n \t\t.path_stack = STRING_LIST_INIT_DUP,\n \t\t.paths_to_lists = STRMAP_INIT\n \t};\n+\tstruct oidset root_tree_set = OIDSET_INIT;\n \n \ttrace2_region_enter(\"path-walk\", \"commit-walk\", info->revs->repo);\n \n@@ -211,6 +249,7 @@ int walk_objects_by_path(struct path_walk_info *info)\n \t/* Insert a single list for the root tree into the paths. */\n \tCALLOC_ARRAY(root_tree_list, 1);\n \troot_tree_list->type = OBJ_TREE;\n+\troot_tree_list->maybe_interesting = 1;\n \tstrmap_put(&ctx.paths_to_lists, root_path, root_tree_list);\n \n \t/*\n@@ -306,10 +345,16 @@ int walk_objects_by_path(struct path_walk_info *info)\n \t\tt = lookup_tree(info->revs->repo, oid);\n \n \t\tif (t) {\n+\t\t\tif ((c->object.flags & UNINTERESTING)) {\n+\t\t\t\tt->object.flags |= UNINTERESTING;\n+\t\t\t\thas_uninteresting = 1;\n+\t\t\t}\n+\n \t\t\tif (t->object.flags & SEEN)\n \t\t\t\tcontinue;\n \t\t\tt->object.flags |= SEEN;\n-\t\t\toid_array_append(&root_tree_list->oids, oid);\n+\t\t\tif (!oidset_insert(&root_tree_set, oid))\n+\t\t\t\toid_array_append(&root_tree_list->oids, oid);\n \t\t} else {\n \t\t\twarning(\"could not find tree %s\", oid_to_hex(oid));\n \t\t}\n@@ -325,6 +370,21 @@ int walk_objects_by_path(struct path_walk_info *info)\n \toid_array_clear(&commit_list->oids);\n \tfree(commit_list);\n \n+\t/*\n+\t * Before performing a DFS of our paths and emitting them as interesting,\n+\t * do a full walk of the trees to distribute the UNINTERESTING bit. Use\n+\t * the sparse algorithm if prune_all_uninteresting was set.\n+\t */\n+\tif (has_uninteresting) {\n+\t\ttrace2_region_enter(\"path-walk\", \"uninteresting-walk\", info->revs->repo);\n+\t\tif (info->prune_all_uninteresting)\n+\t\t\tmark_trees_uninteresting_sparse(ctx.repo, &root_tree_set);\n+\t\telse\n+\t\t\tmark_trees_uninteresting_dense(ctx.repo, &root_tree_set);\n+\t\ttrace2_region_leave(\"path-walk\", \"uninteresting-walk\", info->revs->repo);\n+\t}\n+\toidset_clear(&root_tree_set);\n+\n \tstring_list_append(&ctx.path_stack, root_path);\n \n \ttrace2_region_enter(\"path-walk\", \"path-walk\", info->revs->repo);\ndiff --git a/path-walk.h b/path-walk.h\nindex 3f3b63180ef..3e44c4b8a58 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -38,6 +38,14 @@ struct path_walk_info {\n \tint trees;\n \tint blobs;\n \tint tags;\n+\n+\t/**\n+\t * When 'prune_all_uninteresting' is set and a path has all objects\n+\t * marked as UNINTERESTING, then the path-walk will not visit those\n+\t * objects. It will not call path_fn on those objects and will not\n+\t * walk the children of such trees.\n+\t */\n+\tint prune_all_uninteresting;\n };\n \n #define PATH_WALK_INFO_INIT {   \\\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex c6c60d68749..06b103d8760 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -55,8 +55,12 @@ static int emit_block(const char *path, struct oid_array *oids,\n \t\tBUG(\"we do not understand this type\");\n \t}\n \n-\tfor (size_t i = 0; i < oids->nr; i++)\n-\t\tprintf(\"%s:%s:%s\\n\", typestr, path, oid_to_hex(&oids->oid[i]));\n+\tfor (size_t i = 0; i < oids->nr; i++) {\n+\t\tstruct object *o = lookup_unknown_object(the_repository,\n+\t\t\t\t\t\t\t &oids->oid[i]);\n+\t\tprintf(\"%s:%s:%s%s\\n\", typestr, path, oid_to_hex(&oids->oid[i]),\n+\t\t       o->flags & UNINTERESTING ? \":UNINTERESTING\" : \"\");\n+\t}\n \n \treturn 0;\n }\n@@ -76,6 +80,8 @@ int cmd__path_walk(int argc, const char **argv)\n \t\t\t N_(\"toggle inclusion of tag objects\")),\n \t\tOPT_BOOL(0, \"trees\", &info.trees,\n \t\t\t N_(\"toggle inclusion of tree objects\")),\n+\t\tOPT_BOOL(0, \"prune\", &info.prune_all_uninteresting,\n+\t\t\t N_(\"toggle pruning of uninteresting paths\")),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 7758e2529ee..943adc6c8f1 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -229,19 +229,19 @@ test_expect_success 'topic, not base, boundary' '\n \n \tcat >expect <<-EOF &&\n \tCOMMIT::$(git rev-parse topic)\n-\tCOMMIT::$(git rev-parse base~1)\n+\tCOMMIT::$(git rev-parse base~1):UNINTERESTING\n \tcommits:2\n \tTREE::$(git rev-parse topic^{tree})\n-\tTREE::$(git rev-parse base~1^{tree})\n-\tTREE:left/:$(git rev-parse base~1:left)\n+\tTREE::$(git rev-parse base~1^{tree}):UNINTERESTING\n+\tTREE:left/:$(git rev-parse base~1:left):UNINTERESTING\n \tTREE:right/:$(git rev-parse topic:right)\n-\tTREE:right/:$(git rev-parse base~1:right)\n+\tTREE:right/:$(git rev-parse base~1:right):UNINTERESTING\n \ttrees:5\n-\tBLOB:a:$(git rev-parse base~1:a)\n-\tBLOB:left/b:$(git rev-parse base~1:left/b)\n-\tBLOB:right/c:$(git rev-parse base~1:right/c)\n+\tBLOB:a:$(git rev-parse base~1:a):UNINTERESTING\n+\tBLOB:left/b:$(git rev-parse base~1:left/b):UNINTERESTING\n+\tBLOB:right/c:$(git rev-parse base~1:right/c):UNINTERESTING\n \tBLOB:right/c:$(git rev-parse topic:right/c)\n-\tBLOB:right/d:$(git rev-parse base~1:right/d)\n+\tBLOB:right/d:$(git rev-parse base~1:right/d):UNINTERESTING\n \tblobs:5\n \ttags:0\n \tEOF\n@@ -252,6 +252,30 @@ test_expect_success 'topic, not base, boundary' '\n \ttest_cmp expect.sorted out.sorted\n '\n \n+test_expect_success 'topic, not base, boundary with pruning' '\n+\ttest-tool path-walk --prune -- --boundary topic --not base >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tCOMMIT::$(git rev-parse topic)\n+\tCOMMIT::$(git rev-parse base~1):UNINTERESTING\n+\tcommits:2\n+\tTREE::$(git rev-parse topic^{tree})\n+\tTREE::$(git rev-parse base~1^{tree}):UNINTERESTING\n+\tTREE:right/:$(git rev-parse topic:right)\n+\tTREE:right/:$(git rev-parse base~1:right):UNINTERESTING\n+\ttrees:4\n+\tBLOB:right/c:$(git rev-parse base~1:right/c):UNINTERESTING\n+\tBLOB:right/c:$(git rev-parse topic:right/c)\n+\tblobs:2\n+\ttags:0\n+\tEOF\n+\n+\tsort expect >expect.sorted &&\n+\tsort out >out.sorted &&\n+\n+\ttest_cmp expect.sorted out.sorted\n+'\n+\n test_expect_success 'trees are reported exactly once' '\n \ttest_when_finished \"rm -rf unique-trees\" &&\n \ttest_create_repo unique-trees &&\n-- \ngitgitgadget\n\n"},{"id":"505546","messageId":"3fdb57edbc504fbad572f21f309b8ff6a0ce1a72.1729431810.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 07/17] pack-objects: extract should_attempt_deltas()","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:20Z","receivedAt":"2024-10-20T13:43:44Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThis will be helpful in a future change that introduces a new way to\ncompute deltas.\n\nBe careful to preserve the nr_deltas counting logic in the existing\nmethod, but take the rest of the logic wholesale.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/pack-objects.c | 53 +++++++++++++++++++++++-------------------\n 1 file changed, 29 insertions(+), 24 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 0fc0680b402..82f4ca04000 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -3167,6 +3167,33 @@ static int add_ref_tag(const char *tag UNUSED, const char *referent UNUSED, cons\n \treturn 0;\n }\n \n+static int should_attempt_deltas(struct object_entry *entry)\n+{\n+\tif (DELTA(entry))\n+\t\treturn 0;\n+\n+\tif (!entry->type_valid ||\n+\t    oe_size_less_than(&to_pack, entry, 50))\n+\t\treturn 0;\n+\n+\tif (entry->no_try_delta)\n+\t\treturn 0;\n+\n+\tif (!entry->preferred_base) {\n+\t\tif (oe_type(entry) < 0)\n+\t\t\tdie(_(\"unable to get type of object %s\"),\n+\t\t\t\toid_to_hex(&entry->idx.oid));\n+\t} else if (oe_type(entry) < 0) {\n+\t\t/*\n+\t\t * This object is not found, but we\n+\t\t * don't have to include it anyway.\n+\t\t */\n+\t\treturn 0;\n+\t}\n+\n+\treturn 1;\n+}\n+\n static void prepare_pack(int window, int depth)\n {\n \tstruct object_entry **delta_list;\n@@ -3197,33 +3224,11 @@ static void prepare_pack(int window, int depth)\n \tfor (i = 0; i < to_pack.nr_objects; i++) {\n \t\tstruct object_entry *entry = to_pack.objects + i;\n \n-\t\tif (DELTA(entry))\n-\t\t\t/* This happens if we decided to reuse existing\n-\t\t\t * delta from a pack.  \"reuse_delta &&\" is implied.\n-\t\t\t */\n-\t\t\tcontinue;\n-\n-\t\tif (!entry->type_valid ||\n-\t\t    oe_size_less_than(&to_pack, entry, 50))\n+\t\tif (!should_attempt_deltas(entry))\n \t\t\tcontinue;\n \n-\t\tif (entry->no_try_delta)\n-\t\t\tcontinue;\n-\n-\t\tif (!entry->preferred_base) {\n+\t\tif (!entry->preferred_base)\n \t\t\tnr_deltas++;\n-\t\t\tif (oe_type(entry) < 0)\n-\t\t\t\tdie(_(\"unable to get type of object %s\"),\n-\t\t\t\t    oid_to_hex(&entry->idx.oid));\n-\t\t} else {\n-\t\t\tif (oe_type(entry) < 0) {\n-\t\t\t\t/*\n-\t\t\t\t * This object is not found, but we\n-\t\t\t\t * don't have to include it anyway.\n-\t\t\t\t */\n-\t\t\t\tcontinue;\n-\t\t\t}\n-\t\t}\n \n \t\tdelta_list[n++] = entry;\n \t}\n-- \ngitgitgadget\n\n"},{"id":"505547","messageId":"73c8b61e87b1cada72fed695a450b0541bac19b8.1729431810.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 09/17] pack-objects: update usage to match docs","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:22Z","receivedAt":"2024-10-20T13:43:45Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe t0450 test script verifies that builtin usage matches the synopsis\nin the documentation. Adjust the builtin to match and then remove 'git\npack-objects' from the exception list.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.txt | 14 +++++++-------\n builtin/pack-objects.c             | 10 ++++++++--\n t/t0450/txt-help-mismatches        |  1 -\n 3 files changed, 15 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.txt b/Documentation/git-pack-objects.txt\nindex f2fda800a43..68d86ed8838 100644\n--- a/Documentation/git-pack-objects.txt\n+++ b/Documentation/git-pack-objects.txt\n@@ -10,13 +10,13 @@ SYNOPSIS\n --------\n [verse]\n 'git pack-objects' [-q | --progress | --all-progress] [--all-progress-implied]\n-\t[--no-reuse-delta] [--delta-base-offset] [--non-empty]\n-\t[--local] [--incremental] [--window=<n>] [--depth=<n>]\n-\t[--revs [--unpacked | --all]] [--keep-pack=<pack-name>]\n-\t[--cruft] [--cruft-expiration=<time>]\n-\t[--stdout [--filter=<filter-spec>] | <base-name>]\n-\t[--shallow] [--keep-true-parents] [--[no-]sparse]\n-\t[--path-walk] < <object-list>\n+\t\t   [--no-reuse-delta] [--delta-base-offset] [--non-empty]\n+\t\t   [--local] [--incremental] [--window=<n>] [--depth=<n>]\n+\t\t   [--revs [--unpacked | --all]] [--keep-pack=<pack-name>]\n+\t\t   [--cruft] [--cruft-expiration=<time>]\n+\t\t   [--stdout [--filter=<filter-spec>] | <base-name>]\n+\t\t   [--shallow] [--keep-true-parents] [--[no-]sparse]\n+\t\t   [--path-walk] < <object-list>\n \n \n DESCRIPTION\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 103263666f6..77fb1217b2e 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -185,8 +185,14 @@ static inline void oe_set_delta_size(struct packing_data *pack,\n #define SET_DELTA_SIBLING(obj, val) oe_set_delta_sibling(&to_pack, obj, val)\n \n static const char *pack_usage[] = {\n-\tN_(\"git pack-objects --stdout [<options>] [< <ref-list> | < <object-list>]\"),\n-\tN_(\"git pack-objects [<options>] <base-name> [< <ref-list> | < <object-list>]\"),\n+\tN_(\"git pack-objects [-q | --progress | --all-progress] [--all-progress-implied]\\n\"\n+\t   \"                 [--no-reuse-delta] [--delta-base-offset] [--non-empty]\\n\"\n+\t   \"                 [--local] [--incremental] [--window=<n>] [--depth=<n>]\\n\"\n+\t   \"                 [--revs [--unpacked | --all]] [--keep-pack=<pack-name>]\\n\"\n+\t   \"                 [--cruft] [--cruft-expiration=<time>]\\n\"\n+\t   \"                 [--stdout [--filter=<filter-spec>] | <base-name>]\\n\"\n+\t   \"                 [--shallow] [--keep-true-parents] [--[no-]sparse]\\n\"\n+\t   \"                 [--path-walk] < <object-list>\"),\n \tNULL\n };\n \ndiff --git a/t/t0450/txt-help-mismatches b/t/t0450/txt-help-mismatches\nindex 28003f18c92..285ae81a6b5 100644\n--- a/t/t0450/txt-help-mismatches\n+++ b/t/t0450/txt-help-mismatches\n@@ -38,7 +38,6 @@ merge-one-file\n multi-pack-index\n name-rev\n notes\n-pack-objects\n push\n range-diff\n rebase\n-- \ngitgitgadget\n\n"},{"id":"505548","messageId":"a0475c7cba85970b0b2daa10e502b0203e4eb81c.1729431810.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 08/17] pack-objects: add --path-walk option","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:21Z","receivedAt":"2024-10-20T13:43:45Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nIn order to more easily compute delta bases among objects that appear at the\nexact same path, add a --path-walk option to 'git pack-objects'.\n\nThis option will use the path-walk API instead of the object walk given by\nthe revision machinery. Since objects will be provided in batches\nrepresenting a common path, those objects can be tested for delta bases\nimmediately instead of waiting for a sort of the full object list by\nname-hash. This has multiple benefits, including avoiding collisions by\nname-hash.\n\nThe objects marked as UNINTERESTING are included in these batches, so we\nare guaranteeing some locality to find good delta bases.\n\nAfter the individual passes are done on a per-path basis, the default\nname-hash is used to find other opportunistic delta bases that did not\nmatch exactly by the full path name.\n\nThe current implementation performs delta calculations while walking\nobjects, which is not ideal for a few reasons. First, this will cause\nthe \"Enumerating objects\" phase to be much longer than usual. Second, it\ndoes not take advantage of threading during the path-scoped delta\ncalculations. Even with this lack of threading, the path-walk option is\nsometimes faster than the usual approach. Future changes will refactor\nthis code to allow for threading, but that complexity is deferred until\nlater to keep this patch as simple as possible.\n\nThis new walk is incompatible with some features and is ignored by\nothers:\n\n * Object filters are not currently integrated with the path-walk API,\n   such as sparse-checkout or tree depth. A blobless packfile could be\n   integrated easily, but that is deferred for later.\n\n * Server-focused features such as delta islands, shallow packs, and\n   using a bitmap index are incompatible with the path-walk API.\n\n * The path walk API is only compatible with the --revs option, not\n   taking object lists or pack lists over stdin. These alternative ways\n   to specify the objects currently ignores the --path-walk option\n   without even a warning.\n\nFuture changes will create performance tests that demonstrate the power\nof this approach.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.txt        |  13 +-\n Documentation/technical/api-path-walk.txt |   3 +-\n builtin/pack-objects.c                    | 147 ++++++++++++++++++++--\n t/t5300-pack-object.sh                    |  17 +++\n 4 files changed, 169 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.txt b/Documentation/git-pack-objects.txt\nindex e32404c6aae..f2fda800a43 100644\n--- a/Documentation/git-pack-objects.txt\n+++ b/Documentation/git-pack-objects.txt\n@@ -15,7 +15,8 @@ SYNOPSIS\n \t[--revs [--unpacked | --all]] [--keep-pack=<pack-name>]\n \t[--cruft] [--cruft-expiration=<time>]\n \t[--stdout [--filter=<filter-spec>] | <base-name>]\n-\t[--shallow] [--keep-true-parents] [--[no-]sparse] < <object-list>\n+\t[--shallow] [--keep-true-parents] [--[no-]sparse]\n+\t[--path-walk] < <object-list>\n \n \n DESCRIPTION\n@@ -345,6 +346,16 @@ raise an error.\n \tRestrict delta matches based on \"islands\". See DELTA ISLANDS\n \tbelow.\n \n+--path-walk::\n+\tBy default, `git pack-objects` walks objects in an order that\n+\tpresents trees and blobs in an order unrelated to the path they\n+\tappear relative to a commit's root tree. The `--path-walk` option\n+\tenables a different walking algorithm that organizes trees and\n+\tblobs by path. This has the potential to improve delta compression\n+\tespecially in the presence of filenames that cause collisions in\n+\tGit's default name-hash algorithm. Due to changing how the objects\n+\tare walked, this option is not compatible with `--delta-islands`,\n+\t`--shallow`, or `--filter`.\n \n DELTA ISLANDS\n -------------\ndiff --git a/Documentation/technical/api-path-walk.txt b/Documentation/technical/api-path-walk.txt\nindex c51f92cd649..2d25281774d 100644\n--- a/Documentation/technical/api-path-walk.txt\n+++ b/Documentation/technical/api-path-walk.txt\n@@ -69,4 +69,5 @@ Examples\n --------\n \n See example usages in:\n-\t`t/helper/test-path-walk.c`\n+\t`t/helper/test-path-walk.c`,\n+\t`builtin/pack-objects.c`\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 82f4ca04000..103263666f6 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -39,6 +39,9 @@\n #include \"promisor-remote.h\"\n #include \"pack-mtimes.h\"\n #include \"parse-options.h\"\n+#include \"blob.h\"\n+#include \"tree.h\"\n+#include \"path-walk.h\"\n \n /*\n  * Objects we are going to pack are collected in the `to_pack` structure.\n@@ -215,6 +218,7 @@ static int delta_search_threads;\n static int pack_to_stdout;\n static int sparse;\n static int thin;\n+static int path_walk;\n static int num_preferred_base;\n static struct progress *progress_state;\n \n@@ -4143,6 +4147,105 @@ static void mark_bitmap_preferred_tips(void)\n \t}\n }\n \n+static inline int is_oid_interesting(struct repository *repo,\n+\t\t\t\t     struct object_id *oid)\n+{\n+\tstruct object *o = lookup_object(repo, oid);\n+\treturn o && !(o->flags & UNINTERESTING);\n+}\n+\n+static int add_objects_by_path(const char *path,\n+\t\t\t       struct oid_array *oids,\n+\t\t\t       enum object_type type,\n+\t\t\t       void *data)\n+{\n+\tstruct object_entry **delta_list;\n+\tsize_t oe_start = to_pack.nr_objects;\n+\tsize_t oe_end;\n+\tunsigned int sub_list_size;\n+\tunsigned int *processed = data;\n+\n+\t/*\n+\t * First, add all objects to the packing data, including the ones\n+\t * marked UNINTERESTING (translated to 'exclude') as they can be\n+\t * used as delta bases.\n+\t */\n+\tfor (size_t i = 0; i < oids->nr; i++) {\n+\t\tint exclude;\n+\t\tstruct object_info oi = OBJECT_INFO_INIT;\n+\t\tstruct object_id *oid = &oids->oid[i];\n+\n+\t\t/* Skip objects that do not exist locally. */\n+\t\tif (exclude_promisor_objects &&\n+\t\t    oid_object_info_extended(the_repository, oid, &oi,\n+\t\t\t\t\t     OBJECT_INFO_FOR_PREFETCH) < 0)\n+\t\t\tcontinue;\n+\n+\t\texclude = !is_oid_interesting(the_repository, oid);\n+\n+\t\tif (exclude && !thin)\n+\t\t\tcontinue;\n+\n+\t\tadd_object_entry(oid, type, path, exclude);\n+\t}\n+\n+\toe_end = to_pack.nr_objects;\n+\n+\t/* We can skip delta calculations if it is a no-op. */\n+\tif (oe_end == oe_start || !window)\n+\t\treturn 0;\n+\n+\tsub_list_size = 0;\n+\tALLOC_ARRAY(delta_list, oe_end - oe_start);\n+\n+\tfor (size_t i = 0; i < oe_end - oe_start; i++) {\n+\t\tstruct object_entry *entry = to_pack.objects + oe_start + i;\n+\n+\t\tif (!should_attempt_deltas(entry))\n+\t\t\tcontinue;\n+\n+\t\tdelta_list[sub_list_size++] = entry;\n+\t}\n+\n+\t/*\n+\t * Find delta bases among this list of objects that all match the same\n+\t * path. This causes the delta compression to be interleaved in the\n+\t * object walk, which can lead to confusing progress indicators. This is\n+\t * also incompatible with threaded delta calculations. In the future,\n+\t * consider creating a list of regions in the full to_pack.objects array\n+\t * that could be picked up by the threaded delta computation.\n+\t */\n+\tif (sub_list_size && window) {\n+\t\tQSORT(delta_list, sub_list_size, type_size_sort);\n+\t\tfind_deltas(delta_list, &sub_list_size, window, depth, processed);\n+\t}\n+\n+\tfree(delta_list);\n+\treturn 0;\n+}\n+\n+static void get_object_list_path_walk(struct rev_info *revs)\n+{\n+\tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n+\tunsigned int processed = 0;\n+\n+\tinfo.revs = revs;\n+\tinfo.path_fn = add_objects_by_path;\n+\tinfo.path_fn_data = &processed;\n+\trevs->tag_objects = 1;\n+\n+\t/*\n+\t * Allow the --[no-]sparse option to be interesting here, if only\n+\t * for testing purposes. Paths with no interesting objects will not\n+\t * contribute to the resulting pack, but only create noisy preferred\n+\t * base objects.\n+\t */\n+\tinfo.prune_all_uninteresting = sparse;\n+\n+\tif (walk_objects_by_path(&info))\n+\t\tdie(_(\"failed to pack objects via path-walk\"));\n+}\n+\n static void get_object_list(struct rev_info *revs, int ac, const char **av)\n {\n \tstruct setup_revision_opt s_r_opt = {\n@@ -4189,7 +4292,7 @@ static void get_object_list(struct rev_info *revs, int ac, const char **av)\n \n \twarn_on_object_refname_ambiguity = save_warning;\n \n-\tif (use_bitmap_index && !get_object_list_from_bitmap(revs))\n+\tif (use_bitmap_index && !path_walk && !get_object_list_from_bitmap(revs))\n \t\treturn;\n \n \tif (use_delta_islands)\n@@ -4198,15 +4301,19 @@ static void get_object_list(struct rev_info *revs, int ac, const char **av)\n \tif (write_bitmap_index)\n \t\tmark_bitmap_preferred_tips();\n \n-\tif (prepare_revision_walk(revs))\n-\t\tdie(_(\"revision walk setup failed\"));\n-\tmark_edges_uninteresting(revs, show_edge, sparse);\n-\n \tif (!fn_show_object)\n \t\tfn_show_object = show_object;\n-\ttraverse_commit_list(revs,\n-\t\t\t     show_commit, fn_show_object,\n-\t\t\t     NULL);\n+\n+\tif (path_walk) {\n+\t\tget_object_list_path_walk(revs);\n+\t} else {\n+\t\tif (prepare_revision_walk(revs))\n+\t\t\tdie(_(\"revision walk setup failed\"));\n+\t\tmark_edges_uninteresting(revs, show_edge, sparse);\n+\t\ttraverse_commit_list(revs,\n+\t\t\t\tshow_commit, fn_show_object,\n+\t\t\t\tNULL);\n+\t}\n \n \tif (unpack_unreachable_expiration) {\n \t\trevs->ignore_missing_links = 1;\n@@ -4404,6 +4511,8 @@ int cmd_pack_objects(int argc,\n \t\t\t N_(\"use the sparse reachability algorithm\")),\n \t\tOPT_BOOL(0, \"thin\", &thin,\n \t\t\t N_(\"create thin packs\")),\n+\t\tOPT_BOOL(0, \"path-walk\", &path_walk,\n+\t\t\t N_(\"use the path-walk API to walk objects when possible\")),\n \t\tOPT_BOOL(0, \"shallow\", &shallow,\n \t\t\t N_(\"create packs suitable for shallow fetches\")),\n \t\tOPT_BOOL(0, \"honor-pack-keep\", &ignore_packed_keep_on_disk,\n@@ -4484,7 +4593,27 @@ int cmd_pack_objects(int argc,\n \t\twindow = 0;\n \n \tstrvec_push(&rp, \"pack-objects\");\n-\tif (thin) {\n+\n+\tif (path_walk && filter_options.choice) {\n+\t\twarning(_(\"cannot use --filter with --path-walk\"));\n+\t\tpath_walk = 0;\n+\t}\n+\tif (path_walk && use_delta_islands) {\n+\t\twarning(_(\"cannot use delta islands with --path-walk\"));\n+\t\tpath_walk = 0;\n+\t}\n+\tif (path_walk && shallow) {\n+\t\twarning(_(\"cannot use --shallow with --path-walk\"));\n+\t\tpath_walk = 0;\n+\t}\n+\tif (path_walk) {\n+\t\tstrvec_push(&rp, \"--boundary\");\n+\t\t /*\n+\t\t  * We must disable the bitmaps because we are removing\n+\t\t  * the --objects / --objects-edge[-aggressive] options.\n+\t\t  */\n+\t\tuse_bitmap_index = 0;\n+\t} else if (thin) {\n \t\tuse_internal_rev_list = 1;\n \t\tstrvec_push(&rp, shallow\n \t\t\t\t? \"--objects-edge-aggressive\"\ndiff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh\nindex 3b9dae331a5..5f6914acae7 100755\n--- a/t/t5300-pack-object.sh\n+++ b/t/t5300-pack-object.sh\n@@ -674,4 +674,21 @@ do\n \t'\n done\n \n+# Basic \"repack everything\" test\n+test_expect_success '--path-walk pack everything' '\n+\tgit -C server rev-parse HEAD >in &&\n+\tgit -C server pack-objects --stdout --revs --path-walk <in >out.pack &&\n+\tgit -C server index-pack --stdin <out.pack\n+'\n+\n+# Basic \"thin pack\" test\n+test_expect_success '--path-walk thin pack' '\n+\tcat >in <<-EOF &&\n+\t$(git -C server rev-parse HEAD)\n+\t^$(git -C server rev-parse HEAD~2)\n+\tEOF\n+\tgit -C server pack-objects --thin --stdout --revs --path-walk <in >out.pack &&\n+\tgit -C server index-pack --fix-thin --stdin <out.pack\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"505549","messageId":"21dc3723c365f28d7e31db15044744060a977a3d.1729431810.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 10/17] p5313: add performance tests for --path-walk","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:23Z","receivedAt":"2024-10-20T13:43:46Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe previous change added a --path-walk option to 'git pack-objects'.\nCreate a performance test that demonstrates the time and space benefits\nof the feature.\n\nIn order to get an appropriate comparison, we need to avoid reusing\ndeltas and recompute them from scratch.\n\nCompare the creation of a thin pack representing a small push and the\ncreation of a relatively large non-thin pack.\n\nRunning on my copy of the Git repository results in this data:\n\nTest                                      this tree\n---------------------------------------------------------\n5313.2: thin pack                         0.01(0.00+0.00)\n5313.3: thin pack size                               1.1K\n5313.4: thin pack with --path-walk        0.01(0.01+0.00)\n5313.5: thin pack size with --path-walk              1.1K\n5313.6: big pack                          2.52(6.59+0.38)\n5313.7: big pack size                               14.1M\n5313.8: big pack with --path-walk         4.90(5.76+0.26)\n5313.9: big pack size with --path-walk              13.2M\n\nNote that the timing is slower because there is no threading in the\n--path-walk case (yet).\n\nThe cases where the --path-walk option really shines is when the default\nname-hash is overwhelmed with collisions. An open source example can be\nfound in the microsoft/fluentui repo [1] at a certain commit [2].\n\n[1] https://github.com/microsoft/fluentui\n[2] e70848ebac1cd720875bccaa3026f4a9ed700e08\n\nRunning the tests on this repo results in the following output:\n\nTest                                      this tree\n----------------------------------------------------------\n5313.2: thin pack                         0.28(0.38+0.02)\n5313.3: thin pack size                               1.2M\n5313.4: thin pack with --path-walk        0.08(0.06+0.01)\n5313.5: thin pack size with --path-walk             18.4K\n5313.6: big pack                          4.05(29.62+0.43)\n5313.7: big pack size                               20.0M\n5313.8: big pack with --path-walk         5.99(9.06+0.24)\n5313.9: big pack size with --path-walk              16.4M\n\nNotice in particular that in the small thin pack, the time performance\nhas improved from 0.28s to 0.08s and this is likely due to the improved\nsize of the resulting pack: 18.4K instead of 1.2M.\n\nFinally, running this on a copy of the Linux kernel repository results\nin these data points:\n\nTest                                      this tree\n-----------------------------------------------------------\n5313.2: thin pack                         0.00(0.00+0.00)\n5313.3: thin pack size                               5.8K\n5313.4: thin pack with --path-walk        0.00(0.01+0.00)\n5313.5: thin pack size with --path-walk              5.8K\n5313.6: big pack                          24.39(65.81+1.31)\n5313.7: big pack size                              155.7M\n5313.8: big pack with --path-walk         41.07(60.69+0.68)\n5313.9: big pack size with --path-walk             150.8M\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n t/perf/p5313-pack-objects.sh | 59 ++++++++++++++++++++++++++++++++++++\n 1 file changed, 59 insertions(+)\n create mode 100755 t/perf/p5313-pack-objects.sh\n\ndiff --git a/t/perf/p5313-pack-objects.sh b/t/perf/p5313-pack-objects.sh\nnew file mode 100755\nindex 00000000000..840075f5691\n--- /dev/null\n+++ b/t/perf/p5313-pack-objects.sh\n@@ -0,0 +1,59 @@\n+#!/bin/sh\n+\n+test_description='Tests pack performance using bitmaps'\n+. ./perf-lib.sh\n+\n+GIT_TEST_PASSING_SANITIZE_LEAK=0\n+export GIT_TEST_PASSING_SANITIZE_LEAK\n+\n+test_perf_large_repo\n+\n+test_expect_success 'create rev input' '\n+\tcat >in-thin <<-EOF &&\n+\t$(git rev-parse HEAD)\n+\t^$(git rev-parse HEAD~1)\n+\tEOF\n+\n+\tcat >in-big <<-EOF\n+\t$(git rev-parse HEAD)\n+\t^$(git rev-parse HEAD~1000)\n+\tEOF\n+'\n+\n+test_perf 'thin pack' '\n+\tgit pack-objects --thin --stdout --no-reuse-delta \\\n+\t\t--revs --sparse <in-thin >out\n+'\n+\n+test_size 'thin pack size' '\n+\ttest_file_size out\n+'\n+\n+test_perf 'thin pack with --path-walk' '\n+\tgit pack-objects --thin --stdout --no-reuse-delta \\\n+\t\t--revs --sparse --path-walk <in-thin >out\n+'\n+\n+test_size 'thin pack size with --path-walk' '\n+\ttest_file_size out\n+'\n+\n+test_perf 'big pack' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs \\\n+\t\t--sparse <in-big >out\n+'\n+\n+test_size 'big pack size' '\n+\ttest_file_size out\n+'\n+\n+test_perf 'big pack with --path-walk' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs \\\n+\t\t--sparse --path-walk <in-big >out\n+'\n+\n+test_size 'big pack size with --path-walk' '\n+\ttest_file_size out\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"505550","messageId":"6f96b1c227a98dfd2a1980e6784577b0d97f0ac8.1729431810.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 11/17] pack-objects: introduce GIT_TEST_PACK_PATH_WALK","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:24Z","receivedAt":"2024-10-20T13:43:47Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThere are many tests that validate whether 'git pack-objects' works as\nexpected. Instead of duplicating these tests, add a new test environment\nvariable, GIT_TEST_PACK_PATH_WALK, that implies --path-walk by default\nwhen specified.\n\nThis was useful in testing the implementation of the --path-walk\nimplementation, especially in conjunction with test such as:\n\n - t0411-clone-from-partial.sh : One test fetches from a repo that does\n   not have the boundary objects. This causes the path-based walk to\n   fail. Disable the variable for this test.\n\n - t5306-pack-nobase.sh : Similar to t0411, one test fetches from a repo\n   without a boundary object.\n\n - t5310-pack-bitmaps.sh : One test compares the case when packing with\n   bitmaps to the case when packing without them. Since we disable the\n   test variable when writing bitmaps, this causes a difference in the\n   object list (the --path-walk option adds an extra object). Specify\n   --no-path-walk in both processes for the comparison. Another test\n   checks for a specific delta base, but when computing dynamically\n   without using bitmaps, the base object it too small to be considered\n   in the delta calculations so no base is used.\n\n - t5316-pack-delta-depth.sh : This script cares about certain delta\n   choices and their chain lengths. The --path-walk option changes how\n   these chains are selected, and thus changes the results of this test.\n\n - t5322-pack-objects-sparse.sh : This demonstrates the effectiveness of\n   the --sparse option and how it combines with --path-walk.\n\n - t5332-multi-pack-reuse.sh : This test verifies that the preferred\n   pack is used for delta reuse when possible. The --path-walk option is\n   not currently aware of the preferred pack at all, so finds a\n   different delta base.\n\n - t7406-submodule-update.sh : When using the variable, the --depth\n   option collides with the --path-walk feature, resulting in a warning\n   message. Disable the variable so this warning does not appear.\n\nI want to call out one specific test change that is only temporary:\n\n - t5530-upload-pack-error.sh : One test cares specifically about an\n   \"unable to read\" error message. Since the current implementation\n   performs delta calculations within the path-walk API callback, a\n   different \"unable to get size\" error message appears. When this\n   is changed in a future refactoring, this test change can be reverted.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/pack-objects.c        | 12 ++++++++++--\n ci/run-build-and-tests.sh     |  1 +\n t/README                      |  4 ++++\n t/t0411-clone-from-partial.sh |  6 ++++++\n t/t5306-pack-nobase.sh        |  5 +++++\n t/t5310-pack-bitmaps.sh       | 13 +++++++++++--\n t/t5316-pack-delta-depth.sh   |  9 ++++++---\n t/t5332-multi-pack-reuse.sh   |  7 +++++++\n t/t5530-upload-pack-error.sh  |  6 ++++++\n t/t7406-submodule-update.sh   |  4 ++++\n 10 files changed, 60 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 77fb1217b2e..b97bec5661e 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -224,7 +224,7 @@ static int delta_search_threads;\n static int pack_to_stdout;\n static int sparse;\n static int thin;\n-static int path_walk;\n+static int path_walk = -1;\n static int num_preferred_base;\n static struct progress *progress_state;\n \n@@ -4182,7 +4182,7 @@ static int add_objects_by_path(const char *path,\n \t\tstruct object_id *oid = &oids->oid[i];\n \n \t\t/* Skip objects that do not exist locally. */\n-\t\tif (exclude_promisor_objects &&\n+\t\tif ((exclude_promisor_objects || arg_missing_action != MA_ERROR) &&\n \t\t    oid_object_info_extended(the_repository, oid, &oi,\n \t\t\t\t\t     OBJECT_INFO_FOR_PREFETCH) < 0)\n \t\t\tcontinue;\n@@ -4583,6 +4583,14 @@ int cmd_pack_objects(int argc,\n \tif (pack_to_stdout != !base_name || argc)\n \t\tusage_with_options(pack_usage, pack_objects_options);\n \n+\tif (path_walk < 0) {\n+\t\tif (use_bitmap_index > 0 ||\n+\t\t    !use_internal_rev_list)\n+\t\t\tpath_walk = 0;\n+\t\telse\n+\t\t\tpath_walk = git_env_bool(\"GIT_TEST_PACK_PATH_WALK\", 0);\n+\t}\n+\n \tif (depth < 0)\n \t\tdepth = 0;\n \tif (depth >= (1 << OE_DEPTH_BITS)) {\ndiff --git a/ci/run-build-and-tests.sh b/ci/run-build-and-tests.sh\nindex 2e28d02b20f..7c75492f366 100755\n--- a/ci/run-build-and-tests.sh\n+++ b/ci/run-build-and-tests.sh\n@@ -30,6 +30,7 @@ linux-TEST-vars)\n \texport GIT_TEST_NO_WRITE_REV_INDEX=1\n \texport GIT_TEST_CHECKOUT_WORKERS=2\n \texport GIT_TEST_PACK_USE_BITMAP_BOUNDARY_TRAVERSAL=1\n+\texport GIT_TEST_PACK_PATH_WALK=1\n \t;;\n linux-clang)\n \texport GIT_TEST_DEFAULT_HASH=sha1\ndiff --git a/t/README b/t/README\nindex 8dcb778e260..bec31955d2d 100644\n--- a/t/README\n+++ b/t/README\n@@ -436,6 +436,10 @@ GIT_TEST_PACK_SPARSE=<boolean> if disabled will default the pack-objects\n builtin to use the non-sparse object walk. This can still be overridden by\n the --sparse command-line argument.\n \n+GIT_TEST_PACK_PATH_WALK=<boolean> if enabled will default the pack-objects\n+builtin to use the path-walk API for the object walk. This can still be\n+overridden by the --no-path-walk command-line argument.\n+\n GIT_TEST_PRELOAD_INDEX=<boolean> exercises the preload-index code path\n by overriding the minimum number of cache entries required per thread.\n \ndiff --git a/t/t0411-clone-from-partial.sh b/t/t0411-clone-from-partial.sh\nindex 932bf2067da..342d8d2997c 100755\n--- a/t/t0411-clone-from-partial.sh\n+++ b/t/t0411-clone-from-partial.sh\n@@ -63,6 +63,12 @@ test_expect_success 'pack-objects should fetch from promisor remote and execute\n \n test_expect_success 'clone from promisor remote does not lazy-fetch by default' '\n \trm -f script-executed &&\n+\n+\t# The --path-walk feature of \"git pack-objects\" is not\n+\t# compatible with this kind of fetch from an incomplete repo.\n+\tGIT_TEST_PACK_PATH_WALK=0 &&\n+\texport GIT_TEST_PACK_PATH_WALK &&\n+\n \ttest_must_fail git clone evil no-lazy 2>err &&\n \ttest_grep \"lazy fetching disabled\" err &&\n \ttest_path_is_missing script-executed\ndiff --git a/t/t5306-pack-nobase.sh b/t/t5306-pack-nobase.sh\nindex 0d50c6b4bca..429be5ce724 100755\n--- a/t/t5306-pack-nobase.sh\n+++ b/t/t5306-pack-nobase.sh\n@@ -60,6 +60,11 @@ test_expect_success 'indirectly clone patch_clone' '\n \t git pull ../.git &&\n \t test $(git rev-parse HEAD) = $B &&\n \n+\t# The --path-walk feature of \"git pack-objects\" is not\n+\t# compatible with this kind of fetch from an incomplete repo.\n+\tGIT_TEST_PACK_PATH_WALK=0 &&\n+\texport GIT_TEST_PACK_PATH_WALK &&\n+\n \t git pull ../patch_clone/.git &&\n \t test $(git rev-parse HEAD) = $C\n \t)\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex a6de7c57643..881b3f9c8d1 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -128,8 +128,9 @@ test_bitmap_cases () {\n \t\tls .git/objects/pack/ | grep bitmap >output &&\n \t\ttest_line_count = 1 output &&\n \t\t# verify equivalent packs are generated with/without using bitmap index\n-\t\tpackasha1=$(git pack-objects --no-use-bitmap-index --all packa </dev/null) &&\n-\t\tpackbsha1=$(git pack-objects --use-bitmap-index --all packb </dev/null) &&\n+\t\t# Be careful to not use the path-walk option in either case.\n+\t\tpackasha1=$(git pack-objects --no-use-bitmap-index --no-path-walk --all packa </dev/null) &&\n+\t\tpackbsha1=$(git pack-objects --use-bitmap-index --no-path-walk --all packb </dev/null) &&\n \t\tlist_packed_objects packa-$packasha1.idx >packa.objects &&\n \t\tlist_packed_objects packb-$packbsha1.idx >packb.objects &&\n \t\ttest_cmp packa.objects packb.objects\n@@ -358,6 +359,14 @@ test_bitmap_cases () {\n \t\tgit init --bare client.git &&\n \t\t(\n \t\t\tcd client.git &&\n+\n+\t\t\t# This test relies on reusing a delta, but if the\n+\t\t\t# path-walk machinery is engaged, the base object\n+\t\t\t# is considered too small to use during the\n+\t\t\t# dynamic computation, so is not used.\n+\t\t\tGIT_TEST_PACK_PATH_WALK=0 &&\n+\t\t\texport GIT_TEST_PACK_PATH_WALK &&\n+\n \t\t\tgit config transfer.unpackLimit 1 &&\n \t\t\tgit fetch .. delta-reuse-old:delta-reuse-old &&\n \t\t\tgit fetch .. delta-reuse-new:delta-reuse-new &&\ndiff --git a/t/t5316-pack-delta-depth.sh b/t/t5316-pack-delta-depth.sh\nindex eb4ef3dda4d..12a6901fecb 100755\n--- a/t/t5316-pack-delta-depth.sh\n+++ b/t/t5316-pack-delta-depth.sh\n@@ -90,15 +90,18 @@ max_chain() {\n # adjusted (or scrapped if the heuristics have become too unreliable)\n test_expect_success 'packing produces a long delta' '\n \t# Use --window=0 to make sure we are seeing reused deltas,\n-\t# not computing a new long chain.\n-\tpack=$(git pack-objects --all --window=0 </dev/null pack) &&\n+\t# not computing a new long chain. (Also avoid the --path-walk\n+\t# option as it may break delta chains.)\n+\tpack=$(git pack-objects --all --window=0 --no-path-walk </dev/null pack) &&\n \techo 9 >expect &&\n \tmax_chain pack-$pack.pack >actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success '--depth limits depth' '\n-\tpack=$(git pack-objects --all --depth=5 </dev/null pack) &&\n+\t# Avoid --path-walk to avoid breaking delta chains across path\n+\t# boundaries.\n+\tpack=$(git pack-objects --all --depth=5 --no-path-walk </dev/null pack) &&\n \techo 5 >expect &&\n \tmax_chain pack-$pack.pack >actual &&\n \ttest_cmp expect actual\ndiff --git a/t/t5332-multi-pack-reuse.sh b/t/t5332-multi-pack-reuse.sh\nindex 955ea42769b..df7dcb4b487 100755\n--- a/t/t5332-multi-pack-reuse.sh\n+++ b/t/t5332-multi-pack-reuse.sh\n@@ -8,6 +8,13 @@ TEST_PASSES_SANITIZE_LEAK=true\n \n GIT_TEST_MULTI_PACK_INDEX=0\n GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL=0\n+\n+# The --path-walk option does not consider the preferred pack\n+# at all for reusing deltas, so this variable changes the\n+# behavior of this test, if enabled.\n+GIT_TEST_PACK_PATH_WALK=0\n+export GIT_TEST_PACK_PATH_WALK\n+\n objdir=.git/objects\n packdir=$objdir/pack\n \ndiff --git a/t/t5530-upload-pack-error.sh b/t/t5530-upload-pack-error.sh\nindex 7172780d550..356b96cb741 100755\n--- a/t/t5530-upload-pack-error.sh\n+++ b/t/t5530-upload-pack-error.sh\n@@ -35,6 +35,12 @@ test_expect_success 'upload-pack fails due to error in pack-objects packing' '\n \thexsz=$(test_oid hexsz) &&\n \tprintf \"%04xwant %s\\n00000009done\\n0000\" \\\n \t\t$(($hexsz + 10)) $head >input &&\n+\n+\t# The current implementation of path-walk causes a different\n+\t# error message. This will be changed by a future refactoring.\n+\tGIT_TEST_PACK_PATH_WALK=0 &&\n+\texport GIT_TEST_PACK_PATH_WALK &&\n+\n \ttest_must_fail git upload-pack . <input >/dev/null 2>output.err &&\n \ttest_grep \"unable to read\" output.err &&\n \ttest_grep \"pack-objects died\" output.err\ndiff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh\nindex 297c6c3b5cc..d2284e67d3d 100755\n--- a/t/t7406-submodule-update.sh\n+++ b/t/t7406-submodule-update.sh\n@@ -1093,12 +1093,16 @@ test_expect_success 'submodule update --quiet passes quietness to fetch with a s\n \t) &&\n \tgit clone super4 super5 &&\n \t(cd super5 &&\n+\t # This test variable will create a \"warning\" message to stderr\n+\t GIT_TEST_PACK_PATH_WALK=0 \\\n \t git submodule update --quiet --init --depth=1 submodule3 >out 2>err &&\n \t test_must_be_empty out &&\n \t test_must_be_empty err\n \t) &&\n \tgit clone super4 super6 &&\n \t(cd super6 &&\n+\t # This test variable will create a \"warning\" message to stderr\n+\t GIT_TEST_PACK_PATH_WALK=0 \\\n \t git submodule update --init --depth=1 submodule3 >out 2>err &&\n \t test_file_not_empty out &&\n \t test_file_not_empty err\n-- \ngitgitgadget\n\n"},{"id":"505551","messageId":"834c9ea270932e3d25e2018cca4e74831345f592.1729431810.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 12/17] repack: add --path-walk option","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:25Z","receivedAt":"2024-10-20T13:43:48Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nSince 'git pack-objects' supports a --path-walk option, allow passing it\nthrough in 'git repack'. This presents interesting testing opportunities for\ncomparing the different repacking strategies against each other.\n\nIn my copy of the Git repository, the new tests in p5313 show these\nresults:\n\nTest                                      this tree\n-------------------------------------------------------------\n5313.10: repack                           27.88(150.23+2.70)\n5313.11: repack size                               228.2M\n5313.12: repack with --path-walk          134.59(148.77+0.81)\n5313.13: repack size with --path-walk              209.7M\n\nNote that the 'git pack-objects --path-walk' feature is not integrated\nwith threads. Look forward to a future change that will introduce\nthreading to improve the time performance of this feature with\nequivalent space performance.\n\nFor the microsoft/fluentui repo [1] had some interesting aspects for the\nprevious tests in p5313, so here are the repack results:\n\nTest                                      this tree\n-------------------------------------------------------------\n5313.10: repack                           91.76(680.94+2.48)\n5313.11: repack size                               439.1M\n5313.12: repack with --path-walk          110.35(130.46+0.74)\n5313.13: repack size with --path-walk              155.3M\n\n[1] https://github.com/microsoft/fluentui\n\nHere, we see the significant improvement of a full repack using this\nstrategy. The name-hash collisions in this repo cause the space\nproblems. Those collisions also cause the repack command to spend a lot\nof cycles trying to find delta bases among files that are not actually\nvery similar, so the lack of threading with the --path-walk feature is\nless pronounced in the process time.\n\nFor the Linux kernel repository, we have these stats:\n\nTest                                      this tree\n---------------------------------------------------------------\n5313.10: repack                           553.61(1929.41+30.31)\n5313.11: repack size                                 2.5G\n5313.12: repack with --path-walk          1777.63(2044.16+7.47)\n5313.13: repack size with --path-walk                2.5G\n\nThis demonstrates that the --path-walk feature does not always present\nmeasurable improvements, especially in cases where the name-hash has\nvery few collisions.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-repack.txt | 17 ++++++++++++++++-\n builtin/repack.c             |  9 ++++++++-\n t/perf/p5313-pack-objects.sh | 18 ++++++++++++++++++\n 3 files changed, 42 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-repack.txt b/Documentation/git-repack.txt\nindex c902512a9e8..4ec59cd27b1 100644\n--- a/Documentation/git-repack.txt\n+++ b/Documentation/git-repack.txt\n@@ -9,7 +9,9 @@ git-repack - Pack unpacked objects in a repository\n SYNOPSIS\n --------\n [verse]\n-'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m] [--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>] [--write-midx]\n+'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n+\t[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\n+\t[--write-midx] [--path-walk]\n \n DESCRIPTION\n -----------\n@@ -249,6 +251,19 @@ linkgit:git-multi-pack-index[1]).\n \tWrite a multi-pack index (see linkgit:git-multi-pack-index[1])\n \tcontaining the non-redundant packs.\n \n+--path-walk::\n+\tThis option passes the `--path-walk` option to the underlying\n+\t`git pack-options` process (see linkgit:git-pack-objects[1]).\n+\tBy default, `git pack-objects` walks objects in an order that\n+\tpresents trees and blobs in an order unrelated to the path they\n+\tappear relative to a commit's root tree. The `--path-walk` option\n+\tenables a different walking algorithm that organizes trees and\n+\tblobs by path. This has the potential to improve delta compression\n+\tespecially in the presence of filenames that cause collisions in\n+\tGit's default name-hash algorithm. Due to changing how the objects\n+\tare walked, this option is not compatible with `--delta-islands`\n+\tor `--filter`.\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex cb4420f0856..af3f218ced7 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -39,7 +39,9 @@ static int run_update_server_info = 1;\n static char *packdir, *packtmp_name, *packtmp;\n \n static const char *const git_repack_usage[] = {\n-\tN_(\"git repack [<options>]\"),\n+\tN_(\"git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\\n\"\n+\t   \"[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\\n\"\n+\t   \"[--write-midx] [--full-path-walk]\"),\n \tNULL\n };\n \n@@ -58,6 +60,7 @@ struct pack_objects_args {\n \tint no_reuse_object;\n \tint quiet;\n \tint local;\n+\tint path_walk;\n \tstruct list_objects_filter_options filter_options;\n };\n \n@@ -289,6 +292,8 @@ static void prepare_pack_objects(struct child_process *cmd,\n \t\tstrvec_pushf(&cmd->args, \"--no-reuse-delta\");\n \tif (args->no_reuse_object)\n \t\tstrvec_pushf(&cmd->args, \"--no-reuse-object\");\n+\tif (args->path_walk)\n+\t\tstrvec_pushf(&cmd->args, \"--path-walk\");\n \tif (args->local)\n \t\tstrvec_push(&cmd->args,  \"--local\");\n \tif (args->quiet)\n@@ -1182,6 +1187,8 @@ int cmd_repack(int argc,\n \t\t\t\tN_(\"pass --no-reuse-delta to git-pack-objects\")),\n \t\tOPT_BOOL('F', NULL, &po_args.no_reuse_object,\n \t\t\t\tN_(\"pass --no-reuse-object to git-pack-objects\")),\n+\t\tOPT_BOOL(0, \"path-walk\", &po_args.path_walk,\n+\t\t\t\tN_(\"pass --path-walk to git-pack-objects\")),\n \t\tOPT_NEGBIT('n', NULL, &run_update_server_info,\n \t\t\t\tN_(\"do not run git-update-server-info\"), 1),\n \t\tOPT__QUIET(&po_args.quiet, N_(\"be quiet\")),\ndiff --git a/t/perf/p5313-pack-objects.sh b/t/perf/p5313-pack-objects.sh\nindex 840075f5691..b588066ddb0 100755\n--- a/t/perf/p5313-pack-objects.sh\n+++ b/t/perf/p5313-pack-objects.sh\n@@ -56,4 +56,22 @@ test_size 'big pack size with --path-walk' '\n \ttest_file_size out\n '\n \n+test_perf 'repack' '\n+\tgit repack -adf\n+'\n+\n+test_size 'repack size' '\n+\tpack=$(ls .git/objects/pack/pack-*.pack) &&\n+\ttest_file_size \"$pack\"\n+'\n+\n+test_perf 'repack with --path-walk' '\n+\tgit repack -adf --path-walk\n+'\n+\n+test_size 'repack size with --path-walk' '\n+\tpack=$(ls .git/objects/pack/pack-*.pack) &&\n+\ttest_file_size \"$pack\"\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"505552","messageId":"6ef8d67af4b55f91b681c980301d3d190dd5a4e2.1729431811.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 13/17] repack: update usage to match docs","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:26Z","receivedAt":"2024-10-20T13:43:49Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe t0450 test script verifies that the builtin usage matches the\nsynopsis in the documentation. Update 'git repack' to match and remove\nit from the exception list.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/repack.c            | 2 +-\n t/t0450/txt-help-mismatches | 1 -\n 2 files changed, 1 insertion(+), 2 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex af3f218ced7..50f208b48b4 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -41,7 +41,7 @@ static char *packdir, *packtmp_name, *packtmp;\n static const char *const git_repack_usage[] = {\n \tN_(\"git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\\n\"\n \t   \"[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\\n\"\n-\t   \"[--write-midx] [--full-path-walk]\"),\n+\t   \"[--write-midx] [--path-walk]\"),\n \tNULL\n };\n \ndiff --git a/t/t0450/txt-help-mismatches b/t/t0450/txt-help-mismatches\nindex 285ae81a6b5..06b469bdee2 100644\n--- a/t/t0450/txt-help-mismatches\n+++ b/t/t0450/txt-help-mismatches\n@@ -44,7 +44,6 @@ rebase\n remote\n remote-ext\n remote-fd\n-repack\n reset\n restore\n rev-parse\n-- \ngitgitgadget\n\n"},{"id":"505553","messageId":"1db90e361bae53bebc03bd93ed09e94d570950f9.1729431811.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 14/17] pack-objects: enable --path-walk via config","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:27Z","receivedAt":"2024-10-20T13:43:49Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nUsers may want to enable the --path-walk option for 'git pack-objects' by\ndefault, especially underneath commands like 'git push' or 'git repack'.\n\nThis should be limited to client repositories, since the --path-walk option\ndisables bitmap walks, so would be bad to include in Git servers when\nserving fetches and clones. There is potential that it may be helpful to\nconsider when repacking the repository, to take advantage of improved deltas\nacross historical versions of the same files.\n\nMuch like how \"pack.useSparse\" was introduced and included in\n\"feature.experimental\" before being enabled by default, use the repository\nsettings infrastructure to make the new \"pack.usePathWalk\" config enabled by\n\"feature.experimental\" and \"feature.manyFiles\".\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/config/feature.txt | 4 ++++\n Documentation/config/pack.txt    | 8 ++++++++\n builtin/pack-objects.c           | 3 +++\n repo-settings.c                  | 3 +++\n repo-settings.h                  | 1 +\n 5 files changed, 19 insertions(+)\n\ndiff --git a/Documentation/config/feature.txt b/Documentation/config/feature.txt\nindex f061b64b748..cb49ff2604a 100644\n--- a/Documentation/config/feature.txt\n+++ b/Documentation/config/feature.txt\n@@ -20,6 +20,10 @@ walking fewer objects.\n +\n * `pack.allowPackReuse=multi` may improve the time it takes to create a pack by\n reusing objects from multiple packs instead of just one.\n++\n+* `pack.usePathWalk` may speed up packfile creation and make the packfiles be\n+significantly smaller in the presence of certain filename collisions with Git's\n+default name-hash.\n \n feature.manyFiles::\n \tEnable config options that optimize for repos with many files in the\ndiff --git a/Documentation/config/pack.txt b/Documentation/config/pack.txt\nindex da527377faf..08d06271177 100644\n--- a/Documentation/config/pack.txt\n+++ b/Documentation/config/pack.txt\n@@ -155,6 +155,14 @@ pack.useSparse::\n \tcommits contain certain types of direct renames. Default is\n \t`true`.\n \n+pack.usePathWalk::\n+\tWhen true, git will default to using the '--path-walk' option in\n+\t'git pack-objects' when the '--revs' option is present. This\n+\talgorithm groups objects by path to maximize the ability to\n+\tcompute delta chains across historical versions of the same\n+\tobject. This may disable other options, such as using bitmaps to\n+\tenumerate objects.\n+\n pack.preferBitmapTips::\n \tWhen selecting which commits will receive bitmaps, prefer a\n \tcommit at the tip of any reference that is a suffix of any value\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex b97bec5661e..6805a55c60d 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -4587,6 +4587,9 @@ int cmd_pack_objects(int argc,\n \t\tif (use_bitmap_index > 0 ||\n \t\t    !use_internal_rev_list)\n \t\t\tpath_walk = 0;\n+\t\telse if (the_repository->gitdir &&\n+\t\t\t the_repository->settings.pack_use_path_walk)\n+\t\t\tpath_walk = 1;\n \t\telse\n \t\t\tpath_walk = git_env_bool(\"GIT_TEST_PACK_PATH_WALK\", 0);\n \t}\ndiff --git a/repo-settings.c b/repo-settings.c\nindex 4699b4b3650..d8123b9323d 100644\n--- a/repo-settings.c\n+++ b/repo-settings.c\n@@ -45,11 +45,13 @@ void prepare_repo_settings(struct repository *r)\n \t\tr->settings.fetch_negotiation_algorithm = FETCH_NEGOTIATION_SKIPPING;\n \t\tr->settings.pack_use_bitmap_boundary_traversal = 1;\n \t\tr->settings.pack_use_multi_pack_reuse = 1;\n+\t\tr->settings.pack_use_path_walk = 1;\n \t}\n \tif (manyfiles) {\n \t\tr->settings.index_version = 4;\n \t\tr->settings.index_skip_hash = 1;\n \t\tr->settings.core_untracked_cache = UNTRACKED_CACHE_WRITE;\n+\t\tr->settings.pack_use_path_walk = 1;\n \t}\n \n \t/* Commit graph config or default, does not cascade (simple) */\n@@ -64,6 +66,7 @@ void prepare_repo_settings(struct repository *r)\n \n \t/* Boolean config or default, does not cascade (simple)  */\n \trepo_cfg_bool(r, \"pack.usesparse\", &r->settings.pack_use_sparse, 1);\n+\trepo_cfg_bool(r, \"pack.usepathwalk\", &r->settings.pack_use_path_walk, 0);\n \trepo_cfg_bool(r, \"core.multipackindex\", &r->settings.core_multi_pack_index, 1);\n \trepo_cfg_bool(r, \"index.sparse\", &r->settings.sparse_index, 0);\n \trepo_cfg_bool(r, \"index.skiphash\", &r->settings.index_skip_hash, r->settings.index_skip_hash);\ndiff --git a/repo-settings.h b/repo-settings.h\nindex 51d6156a117..ae5c74ba60d 100644\n--- a/repo-settings.h\n+++ b/repo-settings.h\n@@ -53,6 +53,7 @@ struct repo_settings {\n \tenum untracked_cache_setting core_untracked_cache;\n \n \tint pack_use_sparse;\n+\tint pack_use_path_walk;\n \tenum fetch_negotiation_setting fetch_negotiation_algorithm;\n \n \tint core_multi_pack_index;\n-- \ngitgitgadget\n\n"},{"id":"505554","messageId":"0f3040b4b90916610af518ef724256cff537a648.1729431811.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 15/17] scalar: enable path-walk during push via config","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:28Z","receivedAt":"2024-10-20T13:43:50Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nRepositories registered with Scalar are expected to be client-only\nrepositories that are rather large. This means that they are more likely to\nbe good candidates for using the --path-walk option when running 'git\npack-objects', especially under the hood of 'git push'. Enable this config\nin Scalar repositories.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n scalar.c | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/scalar.c b/scalar.c\nindex 73b79a5d4c9..daec7a4d08e 100644\n--- a/scalar.c\n+++ b/scalar.c\n@@ -170,6 +170,7 @@ static int set_recommended_config(int reconfigure)\n \t\t{ \"core.autoCRLF\", \"false\" },\n \t\t{ \"core.safeCRLF\", \"false\" },\n \t\t{ \"fetch.showForcedUpdates\", \"false\" },\n+\t\t{ \"pack.usePathWalk\", \"true\" },\n \t\t{ NULL, NULL },\n \t};\n \tint i;\n-- \ngitgitgadget\n\n"},{"id":"505555","messageId":"030d8ec238e7692e029ae063e475f58e4c172542.1729431811.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 16/17] pack-objects: refactor path-walk delta phase","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:29Z","receivedAt":"2024-10-20T13:43:52Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nPreviously, the --path-walk option to 'git pack-objects' would compute\ndeltas inline with the path-walk logic. This would make the progress\nindicator look like it is taking a long time to enumerate objects, and\nthen very quickly computed deltas.\n\nInstead of computing deltas on each region of objects organized by tree,\nstore a list of regions corresponding to these groups. These can later\nbe pulled from the list for delta compression before doing the \"global\"\ndelta search.\n\nThis presents a new progress indicator that can be used in tests to\nverify that this stage is happening.\n\nThe current implementation is not integrated with threads, but could be\ndone in a future update.\n\nSince we do not attempt to sort objects by size until after exploring\nall trees, we can remove the previous change to t5530 due to a different\nerror message appearing first.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/pack-objects.c       | 81 +++++++++++++++++++++++++-----------\n pack-objects.h               | 12 ++++++\n t/t5300-pack-object.sh       |  8 +++-\n t/t5530-upload-pack-error.sh |  6 ---\n 4 files changed, 74 insertions(+), 33 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 6805a55c60d..5c413ac07e6 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -3204,6 +3204,50 @@ static int should_attempt_deltas(struct object_entry *entry)\n \treturn 1;\n }\n \n+static void find_deltas_for_region(struct object_entry *list UNUSED,\n+\t\t\t\t   struct packing_region *region,\n+\t\t\t\t   unsigned int *processed)\n+{\n+\tstruct object_entry **delta_list;\n+\tuint32_t delta_list_nr = 0;\n+\n+\tALLOC_ARRAY(delta_list, region->nr);\n+\tfor (uint32_t i = 0; i < region->nr; i++) {\n+\t\tstruct object_entry *entry = to_pack.objects + region->start + i;\n+\t\tif (should_attempt_deltas(entry))\n+\t\t\tdelta_list[delta_list_nr++] = entry;\n+\t}\n+\n+\tQSORT(delta_list, delta_list_nr, type_size_sort);\n+\tfind_deltas(delta_list, &delta_list_nr, window, depth, processed);\n+\tfree(delta_list);\n+}\n+\n+static void find_deltas_by_region(struct object_entry *list,\n+\t\t\t\t  struct packing_region *regions,\n+\t\t\t\t  uint32_t start, uint32_t nr)\n+{\n+\tunsigned int processed = 0;\n+\tuint32_t progress_nr;\n+\n+\tif (!nr)\n+\t\treturn;\n+\n+\tprogress_nr = regions[nr - 1].start + regions[nr - 1].nr;\n+\n+\tif (progress)\n+\t\tprogress_state = start_progress(_(\"Compressing objects by path\"),\n+\t\t\t\t\t\tprogress_nr);\n+\n+\twhile (nr--)\n+\t\tfind_deltas_for_region(list,\n+\t\t\t\t       &regions[start++],\n+\t\t\t\t       &processed);\n+\n+\tdisplay_progress(progress_state, progress_nr);\n+\tstop_progress(&progress_state);\n+}\n+\n static void prepare_pack(int window, int depth)\n {\n \tstruct object_entry **delta_list;\n@@ -3228,6 +3272,10 @@ static void prepare_pack(int window, int depth)\n \tif (!to_pack.nr_objects || !window || !depth)\n \t\treturn;\n \n+\tif (path_walk)\n+\t\tfind_deltas_by_region(to_pack.objects, to_pack.regions,\n+\t\t\t\t      0, to_pack.nr_regions);\n+\n \tALLOC_ARRAY(delta_list, to_pack.nr_objects);\n \tnr_deltas = n = 0;\n \n@@ -4165,10 +4213,8 @@ static int add_objects_by_path(const char *path,\n \t\t\t       enum object_type type,\n \t\t\t       void *data)\n {\n-\tstruct object_entry **delta_list;\n \tsize_t oe_start = to_pack.nr_objects;\n \tsize_t oe_end;\n-\tunsigned int sub_list_size;\n \tunsigned int *processed = data;\n \n \t/*\n@@ -4201,32 +4247,17 @@ static int add_objects_by_path(const char *path,\n \tif (oe_end == oe_start || !window)\n \t\treturn 0;\n \n-\tsub_list_size = 0;\n-\tALLOC_ARRAY(delta_list, oe_end - oe_start);\n+\tALLOC_GROW(to_pack.regions,\n+\t\t   to_pack.nr_regions + 1,\n+\t\t   to_pack.nr_regions_alloc);\n \n-\tfor (size_t i = 0; i < oe_end - oe_start; i++) {\n-\t\tstruct object_entry *entry = to_pack.objects + oe_start + i;\n+\tto_pack.regions[to_pack.nr_regions].start = oe_start;\n+\tto_pack.regions[to_pack.nr_regions].nr = oe_end - oe_start;\n+\tto_pack.nr_regions++;\n \n-\t\tif (!should_attempt_deltas(entry))\n-\t\t\tcontinue;\n+\t*processed += oids->nr;\n+\tdisplay_progress(progress_state, *processed);\n \n-\t\tdelta_list[sub_list_size++] = entry;\n-\t}\n-\n-\t/*\n-\t * Find delta bases among this list of objects that all match the same\n-\t * path. This causes the delta compression to be interleaved in the\n-\t * object walk, which can lead to confusing progress indicators. This is\n-\t * also incompatible with threaded delta calculations. In the future,\n-\t * consider creating a list of regions in the full to_pack.objects array\n-\t * that could be picked up by the threaded delta computation.\n-\t */\n-\tif (sub_list_size && window) {\n-\t\tQSORT(delta_list, sub_list_size, type_size_sort);\n-\t\tfind_deltas(delta_list, &sub_list_size, window, depth, processed);\n-\t}\n-\n-\tfree(delta_list);\n \treturn 0;\n }\n \ndiff --git a/pack-objects.h b/pack-objects.h\nindex b9898a4e64b..bde4ba19755 100644\n--- a/pack-objects.h\n+++ b/pack-objects.h\n@@ -118,11 +118,23 @@ struct object_entry {\n \tunsigned ext_base:1; /* delta_idx points outside packlist */\n };\n \n+/**\n+ * A packing region is a section of the packing_data.objects array\n+ * as given by a starting index and a number of elements.\n+ */\n+struct packing_region {\n+\tuint32_t start;\n+\tuint32_t nr;\n+};\n+\n struct packing_data {\n \tstruct repository *repo;\n \tstruct object_entry *objects;\n \tuint32_t nr_objects, nr_alloc;\n \n+\tstruct packing_region *regions;\n+\tuint32_t nr_regions, nr_regions_alloc;\n+\n \tint32_t *index;\n \tuint32_t index_size;\n \ndiff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh\nindex 5f6914acae7..4f81613eab1 100755\n--- a/t/t5300-pack-object.sh\n+++ b/t/t5300-pack-object.sh\n@@ -677,7 +677,9 @@ done\n # Basic \"repack everything\" test\n test_expect_success '--path-walk pack everything' '\n \tgit -C server rev-parse HEAD >in &&\n-\tgit -C server pack-objects --stdout --revs --path-walk <in >out.pack &&\n+\tGIT_PROGRESS_DELAY=0 git -C server pack-objects \\\n+\t\t--stdout --revs --path-walk --progress <in >out.pack 2>err &&\n+\tgrep \"Compressing objects by path\" err &&\n \tgit -C server index-pack --stdin <out.pack\n '\n \n@@ -687,7 +689,9 @@ test_expect_success '--path-walk thin pack' '\n \t$(git -C server rev-parse HEAD)\n \t^$(git -C server rev-parse HEAD~2)\n \tEOF\n-\tgit -C server pack-objects --thin --stdout --revs --path-walk <in >out.pack &&\n+\tGIT_PROGRESS_DELAY=0 git -C server pack-objects \\\n+\t\t--thin --stdout --revs --path-walk --progress <in >out.pack 2>err &&\n+\tgrep \"Compressing objects by path\" err &&\n \tgit -C server index-pack --fix-thin --stdin <out.pack\n '\n \ndiff --git a/t/t5530-upload-pack-error.sh b/t/t5530-upload-pack-error.sh\nindex 356b96cb741..7172780d550 100755\n--- a/t/t5530-upload-pack-error.sh\n+++ b/t/t5530-upload-pack-error.sh\n@@ -35,12 +35,6 @@ test_expect_success 'upload-pack fails due to error in pack-objects packing' '\n \thexsz=$(test_oid hexsz) &&\n \tprintf \"%04xwant %s\\n00000009done\\n0000\" \\\n \t\t$(($hexsz + 10)) $head >input &&\n-\n-\t# The current implementation of path-walk causes a different\n-\t# error message. This will be changed by a future refactoring.\n-\tGIT_TEST_PACK_PATH_WALK=0 &&\n-\texport GIT_TEST_PACK_PATH_WALK &&\n-\n \ttest_must_fail git upload-pack . <input >/dev/null 2>output.err &&\n \ttest_grep \"unable to read\" output.err &&\n \ttest_grep \"pack-objects died\" output.err\n-- \ngitgitgadget\n\n"},{"id":"505556","messageId":"fddc320eb0bc07b1f8314a07cdda1cd8fbf77e9f.1729431811.git.gitgitgadget@gmail.com","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"[PATCH v2 17/17] pack-objects: thread the path-based compression","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-10-20T13:43:30Z","receivedAt":"2024-10-20T13:43:52Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nAdapting the implementation of ll_find_deltas(), create a threaded\nversion of the --path-walk compression step in 'git pack-objects'.\n\nThis involves adding a 'regions' member to the thread_params struct,\nallowing each thread to own a section of paths. We can simplify the way\njobs are split because there is no value in extending the batch based on\nname-hash the way sections of the object entry array are attempted to be\ngrouped. We re-use the 'list_size' and 'remaining' items for the purpose\nof borrowing work in progress from other \"victim\" threads when a thread\nhas finished its batch of work more quickly.\n\nUsing the Git repository as a test repo, the p5313 performance test\nshows that the resulting size of the repo is the same, but the threaded\nimplementation gives gains of varying degrees depending on the number of\nobjects being packed. (This was tested on a 16-core machine.)\n\nTest                                      HEAD~1    HEAD\n---------------------------------------------------------------\n5313.2: thin pack                           0.01    0.01  +0.0%\n5313.4: thin pack with --path-walk          0.01    0.01  +0.0%\n5313.6: big pack                            2.54    2.60  +2.4%\n5313.8: big pack with --path-walk           4.70    3.09 -34.3%\n5313.10: repack                            28.75   28.55  -0.7%\n5313.12: repack with --path-walk          108.55   46.14 -57.5%\n\nOn the microsoft/fluentui repo, where the --path-walk feature has been\nshown to be more effective in space savings, we get these results:\n\nTest                                     HEAD~1    HEAD\n----------------------------------------------------------------\n5313.2: thin pack                         0.39     0.40  +2.6%\n5313.4: thin pack with --path-walk        0.08     0.07 -12.5%\n5313.6: big pack                          4.15     4.15  +0.0%\n5313.8: big pack with --path-walk         6.41     3.21 -49.9%\n5313.10: repack                          90.69    90.83  +0.2%\n5313.12: repack with --path-walk        108.23    49.09 -54.6%\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/pack-objects.c | 162 ++++++++++++++++++++++++++++++++++++++++-\n 1 file changed, 160 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 5c413ac07e6..443ce17063a 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -2935,6 +2935,7 @@ static void find_deltas(struct object_entry **list, unsigned *list_size,\n struct thread_params {\n \tpthread_t thread;\n \tstruct object_entry **list;\n+\tstruct packing_region *regions;\n \tunsigned list_size;\n \tunsigned remaining;\n \tint window;\n@@ -3248,6 +3249,163 @@ static void find_deltas_by_region(struct object_entry *list,\n \tstop_progress(&progress_state);\n }\n \n+static void *threaded_find_deltas_by_path(void *arg)\n+{\n+\tstruct thread_params *me = arg;\n+\n+\tprogress_lock();\n+\twhile (me->remaining) {\n+\t\twhile (me->remaining) {\n+\t\t\tprogress_unlock();\n+\t\t\tfind_deltas_for_region(to_pack.objects,\n+\t\t\t\t\t       me->regions,\n+\t\t\t\t\t       me->processed);\n+\t\t\tprogress_lock();\n+\t\t\tme->remaining--;\n+\t\t\tme->regions++;\n+\t\t}\n+\n+\t\tme->working = 0;\n+\t\tpthread_cond_signal(&progress_cond);\n+\t\tprogress_unlock();\n+\n+\t\t/*\n+\t\t * We must not set ->data_ready before we wait on the\n+\t\t * condition because the main thread may have set it to 1\n+\t\t * before we get here. In order to be sure that new\n+\t\t * work is available if we see 1 in ->data_ready, it\n+\t\t * was initialized to 0 before this thread was spawned\n+\t\t * and we reset it to 0 right away.\n+\t\t */\n+\t\tpthread_mutex_lock(&me->mutex);\n+\t\twhile (!me->data_ready)\n+\t\t\tpthread_cond_wait(&me->cond, &me->mutex);\n+\t\tme->data_ready = 0;\n+\t\tpthread_mutex_unlock(&me->mutex);\n+\n+\t\tprogress_lock();\n+\t}\n+\tprogress_unlock();\n+\t/* leave ->working 1 so that this doesn't get more work assigned */\n+\treturn NULL;\n+}\n+\n+static void ll_find_deltas_by_region(struct object_entry *list,\n+\t\t\t\t     struct packing_region *regions,\n+\t\t\t\t     uint32_t start, uint32_t nr)\n+{\n+\tstruct thread_params *p;\n+\tint i, ret, active_threads = 0;\n+\tunsigned int processed = 0;\n+\tuint32_t progress_nr;\n+\tinit_threaded_search();\n+\n+\tif (!nr)\n+\t\treturn;\n+\n+\tprogress_nr =  regions[nr - 1].start + regions[nr - 1].nr;\n+\tif (delta_search_threads <= 1) {\n+\t\tfind_deltas_by_region(list, regions, start, nr);\n+\t\tcleanup_threaded_search();\n+\t\treturn;\n+\t}\n+\n+\tif (progress > pack_to_stdout)\n+\t\tfprintf_ln(stderr, _(\"Path-based delta compression using up to %d threads\"),\n+\t\t\t   delta_search_threads);\n+\tCALLOC_ARRAY(p, delta_search_threads);\n+\n+\tif (progress)\n+\t\tprogress_state = start_progress(_(\"Compressing objects by path\"),\n+\t\t\t\t\t\tprogress_nr);\n+\t/* Partition the work amongst work threads. */\n+\tfor (i = 0; i < delta_search_threads; i++) {\n+\t\tunsigned sub_size = nr / (delta_search_threads - i);\n+\n+\t\tp[i].window = window;\n+\t\tp[i].depth = depth;\n+\t\tp[i].processed = &processed;\n+\t\tp[i].working = 1;\n+\t\tp[i].data_ready = 0;\n+\n+\t\tp[i].regions = regions;\n+\t\tp[i].list_size = sub_size;\n+\t\tp[i].remaining = sub_size;\n+\n+\t\tregions += sub_size;\n+\t\tnr -= sub_size;\n+\t}\n+\n+\t/* Start work threads. */\n+\tfor (i = 0; i < delta_search_threads; i++) {\n+\t\tif (!p[i].list_size)\n+\t\t\tcontinue;\n+\t\tpthread_mutex_init(&p[i].mutex, NULL);\n+\t\tpthread_cond_init(&p[i].cond, NULL);\n+\t\tret = pthread_create(&p[i].thread, NULL,\n+\t\t\t\t     threaded_find_deltas_by_path, &p[i]);\n+\t\tif (ret)\n+\t\t\tdie(_(\"unable to create thread: %s\"), strerror(ret));\n+\t\tactive_threads++;\n+\t}\n+\n+\t/*\n+\t * Now let's wait for work completion.  Each time a thread is done\n+\t * with its work, we steal half of the remaining work from the\n+\t * thread with the largest number of unprocessed objects and give\n+\t * it to that newly idle thread.  This ensure good load balancing\n+\t * until the remaining object list segments are simply too short\n+\t * to be worth splitting anymore.\n+\t */\n+\twhile (active_threads) {\n+\t\tstruct thread_params *target = NULL;\n+\t\tstruct thread_params *victim = NULL;\n+\t\tunsigned sub_size = 0;\n+\n+\t\tprogress_lock();\n+\t\tfor (;;) {\n+\t\t\tfor (i = 0; !target && i < delta_search_threads; i++)\n+\t\t\t\tif (!p[i].working)\n+\t\t\t\t\ttarget = &p[i];\n+\t\t\tif (target)\n+\t\t\t\tbreak;\n+\t\t\tpthread_cond_wait(&progress_cond, &progress_mutex);\n+\t\t}\n+\n+\t\tfor (i = 0; i < delta_search_threads; i++)\n+\t\t\tif (p[i].remaining > 2*window &&\n+\t\t\t    (!victim || victim->remaining < p[i].remaining))\n+\t\t\t\tvictim = &p[i];\n+\t\tif (victim) {\n+\t\t\tsub_size = victim->remaining / 2;\n+\t\t\ttarget->regions = victim->regions + victim->remaining - sub_size;\n+\t\t\tvictim->list_size -= sub_size;\n+\t\t\tvictim->remaining -= sub_size;\n+\t\t}\n+\t\ttarget->list_size = sub_size;\n+\t\ttarget->remaining = sub_size;\n+\t\ttarget->working = 1;\n+\t\tprogress_unlock();\n+\n+\t\tpthread_mutex_lock(&target->mutex);\n+\t\ttarget->data_ready = 1;\n+\t\tpthread_cond_signal(&target->cond);\n+\t\tpthread_mutex_unlock(&target->mutex);\n+\n+\t\tif (!sub_size) {\n+\t\t\tpthread_join(target->thread, NULL);\n+\t\t\tpthread_cond_destroy(&target->cond);\n+\t\t\tpthread_mutex_destroy(&target->mutex);\n+\t\t\tactive_threads--;\n+\t\t}\n+\t}\n+\tcleanup_threaded_search();\n+\tfree(p);\n+\n+\tdisplay_progress(progress_state, progress_nr);\n+\tstop_progress(&progress_state);\n+}\n+\n static void prepare_pack(int window, int depth)\n {\n \tstruct object_entry **delta_list;\n@@ -3273,8 +3431,8 @@ static void prepare_pack(int window, int depth)\n \t\treturn;\n \n \tif (path_walk)\n-\t\tfind_deltas_by_region(to_pack.objects, to_pack.regions,\n-\t\t\t\t      0, to_pack.nr_regions);\n+\t\tll_find_deltas_by_region(to_pack.objects, to_pack.regions,\n+\t\t\t\t\t 0, to_pack.nr_regions);\n \n \tALLOC_ARRAY(delta_list, to_pack.nr_objects);\n \tnr_deltas = n = 0;\n-- \ngitgitgadget\n"},{"id":"505780","messageId":"ZxbLFLDgCm6wLmB9@nand.local","threadId":"62282","inReplyTo":"pull.1813.v2.git.1729431810.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 00/17] pack-objects: add --path-walk option for better deltas","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2024-10-21T21:43:48Z","receivedAt":"2024-10-21T21:43:50Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Sun, Oct 20, 2024 at 01:43:13PM +0000, Derrick Stolee via GitGitGadget wrote:\n> Updates in v2\n> =============\n>\n> I'm sending this v2 to request some review feedback on the series. I'm sorry\n> it's so long.\n>\n> There are two updates in this version:\n>\n>  * Fixed a performance issue in the presence of many annotated tags. This is\n>    caught by p5313 when run on a repo with 10,000+ annotated tags.\n>  * The Scalar config was previously wrong and should be pack.usePathWalk,\n>    not push.usePathWalk.\n\nThanks. I queued the new round. As an aside, I would like to find the\ntime to review this series in depth, but haven't been able to do so\nwhile also trying to keep up with the volume of the rest of the list.\n\nI know that this topic was split out of a larger one. It may be worth\nseeing if there is a way to split this topic out into multiple series\nthat are more easily review-able, but still sensible on their own.\n\nI haven't looked in enough depth to know myself whether such a cut\nexists, but it is worth thinking about if you haven't done so already.\n\nThanks,\nTaylor\n"},{"id":"506041","messageId":"dd18a858-31bf-497d-bb19-d371102dfa2a@gmail.com","threadId":"62282","inReplyTo":"ZxbLFLDgCm6wLmB9@nand.local","subject":"Re: [PATCH v2 00/17] pack-objects: add --path-walk option for better deltas","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2024-10-24T13:29:02Z","receivedAt":"2024-10-24T13:29:12Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 10/21/24 5:43 PM, Taylor Blau wrote:\n> On Sun, Oct 20, 2024 at 01:43:13PM +0000, Derrick Stolee via GitGitGadget wrote:\n>> Updates in v2\n>> =============\n>>\n>> I'm sending this v2 to request some review feedback on the series. I'm sorry\n>> it's so long.\n>>\n>> There are two updates in this version:\n>>\n>>   * Fixed a performance issue in the presence of many annotated tags. This is\n>>     caught by p5313 when run on a repo with 10,000+ annotated tags.\n>>   * The Scalar config was previously wrong and should be pack.usePathWalk,\n>>     not push.usePathWalk.\n> \n> Thanks. I queued the new round. As an aside, I would like to find the\n> time to review this series in depth, but haven't been able to do so\n> while also trying to keep up with the volume of the rest of the list.\n> \n> I know that this topic was split out of a larger one. It may be worth\n> seeing if there is a way to split this topic out into multiple series\n> that are more easily review-able, but still sensible on their own.\n\nI'll see what I can do. I needed to re-roll after discovering an issue\nwhen trying to integrate the algorithm with shallow clones. The solution\nends up simplifying the code, which is nice.\n\n> I haven't looked in enough depth to know myself whether such a cut\n> exists, but it is worth thinking about if you haven't done so already.\n\nIn the current series, there's a natural cut between patches 1-4\nand the rest, if we want to put the API in without a non-test consumer.\n\nI could also split out the 'git repack' changes into a third series.\n\nFinally, the threading implementation could be done separately, but I\nthink it's not complicated enough to leave out from the first version\nof the --path-walk option in 'git pack-objects'.\n\nThanks,\n-Stolee\n\n"},{"id":"506046","messageId":"ZxptTxbVZt96e8ZQ@nand.local","threadId":"62282","inReplyTo":"dd18a858-31bf-497d-bb19-d371102dfa2a@gmail.com","subject":"Re: [PATCH v2 00/17] pack-objects: add --path-walk option for better deltas","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2024-10-24T15:52:47Z","receivedAt":"2024-10-24T15:52:49Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Thu, Oct 24, 2024 at 09:29:02AM -0400, Derrick Stolee wrote:\n> > I know that this topic was split out of a larger one. It may be worth\n> > seeing if there is a way to split this topic out into multiple series\n> > that are more easily review-able, but still sensible on their own.\n>\n> I'll see what I can do. I needed to re-roll after discovering an issue\n> when trying to integrate the algorithm with shallow clones. The solution\n> ends up simplifying the code, which is nice.\n\nIt's always nice when that happens :-).\n\nShould we avoid reviewing the current round in anticipation of a\nsomewhat restructured series, or would you like us to review the current\nround as well?\n\n> > I haven't looked in enough depth to know myself whether such a cut\n> > exists, but it is worth thinking about if you haven't done so already.\n>\n> In the current series, there's a natural cut between patches 1-4\n> and the rest, if we want to put the API in without a non-test consumer.\n>\n> I could also split out the 'git repack' changes into a third series.\n>\n> Finally, the threading implementation could be done separately, but I\n> think it's not complicated enough to leave out from the first version\n> of the --path-walk option in 'git pack-objects'.\n\nI'd suggest erring on the side of more smaller series rather than a\nsingle large one. If you feel like there are cut points where we can\nreview them in isolation and still see some benefit, or at least\nclearly how they each fit into the larger puzzle, I think that is worth\ndoing.\n\nBut I trust your judgement here, so if you think that the series is best\nreviewed as a whole, then that's fine too. Just my $.02 :-).\n\nThanks,\nTaylor\n"},{"id":"506196","messageId":"Zx8lHzDyqyBpzrAE@pks.im","threadId":"62282","inReplyTo":"ZxbLFLDgCm6wLmB9@nand.local","subject":"Re: [PATCH v2 00/17] pack-objects: add --path-walk option for better deltas","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-10-28T05:46:07Z","receivedAt":"2024-10-28T05:46:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Oct 21, 2024 at 05:43:48PM -0400, Taylor Blau wrote:\n> On Sun, Oct 20, 2024 at 01:43:13PM +0000, Derrick Stolee via GitGitGadget wrote:\n> > Updates in v2\n> > =============\n> >\n> > I'm sending this v2 to request some review feedback on the series. I'm sorry\n> > it's so long.\n> >\n> > There are two updates in this version:\n> >\n> >  * Fixed a performance issue in the presence of many annotated tags. This is\n> >    caught by p5313 when run on a repo with 10,000+ annotated tags.\n> >  * The Scalar config was previously wrong and should be pack.usePathWalk,\n> >    not push.usePathWalk.\n> \n> Thanks. I queued the new round. As an aside, I would like to find the\n> time to review this series in depth, but haven't been able to do so\n> while also trying to keep up with the volume of the rest of the list.\n> \n> I know that this topic was split out of a larger one. It may be worth\n> seeing if there is a way to split this topic out into multiple series\n> that are more easily review-able, but still sensible on their own.\n> \n> I haven't looked in enough depth to know myself whether such a cut\n> exists, but it is worth thinking about if you haven't done so already.\n\nI'm in the same boat -- I want to review this, but somehow never find\nthe time to sit down and do it. I definitely won't get to it this week\nas I'll be out-of-office for most of the part.\n\nI've flagged this internally now at GitLab so that we can provide some\nmore data with some of the repos that are on the bigger side to check\nwhether we can confirm the findings and to prioritize its review.\n\nPatrick\n"},{"id":"506231","messageId":"Zx/AGrGTN2mQZqGs@nand.local","threadId":"62282","inReplyTo":"Zx8lHzDyqyBpzrAE@pks.im","subject":"Re: [PATCH v2 00/17] pack-objects: add --path-walk option for better deltas","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2024-10-28T16:47:22Z","receivedAt":"2024-10-28T16:47:26Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Mon, Oct 28, 2024 at 06:46:07AM +0100, Patrick Steinhardt wrote:\n> I've flagged this internally now at GitLab so that we can provide some\n> more data with some of the repos that are on the bigger side to check\n> whether we can confirm the findings and to prioritize its review.\n\nI suspect that you'll end up measuring no change assuming that you\n(AFAIK) use bitmaps and (I imagine) delta islands in your production\nconfiguration? This series is not compatible with either of those\nfeatures to my knowledge.\n\nThanks,\nTaylor\n"},{"id":"506235","messageId":"00d80855-0530-4384-8d2f-5370ace9c0d5@gmail.com","threadId":"62282","inReplyTo":"Zx/AGrGTN2mQZqGs@nand.local","subject":"Re: [PATCH v2 00/17] pack-objects: add --path-walk option for better deltas","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2024-10-28T17:13:15Z","receivedAt":"2024-10-28T17:13:17Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 10/28/24 12:47 PM, Taylor Blau wrote:\n> On Mon, Oct 28, 2024 at 06:46:07AM +0100, Patrick Steinhardt wrote:\n>> I've flagged this internally now at GitLab so that we can provide some\n>> more data with some of the repos that are on the bigger side to check\n>> whether we can confirm the findings and to prioritize its review.\n> \n> I suspect that you'll end up measuring no change assuming that you\n> (AFAIK) use bitmaps and (I imagine) delta islands in your production\n> configuration? This series is not compatible with either of those\n> features to my knowledge.\nYou are correct that this is not compatible with those features as-is.\n_Maybe_ there is potential to integrate them in the future, but that\nwould require better understanding of whether the new compression\nmechanism valuable in enough cases (final storage size or maybe even\nin repacking time).\n\nAt the very least, it would be helpful if some other large repos were\ntested to see how commonly this could help client-side users. Are\nthere other aspects to a repo's structure that could be important to\nhow effective this approach is?\n\nThanks,\n-Stolee\n"},{"id":"506236","messageId":"Zx/I+9icWGsRz11x@nand.local","threadId":"62282","inReplyTo":"00d80855-0530-4384-8d2f-5370ace9c0d5@gmail.com","subject":"Re: [PATCH v2 00/17] pack-objects: add --path-walk option for better deltas","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2024-10-28T17:25:15Z","receivedAt":"2024-10-28T17:25:19Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Mon, Oct 28, 2024 at 01:13:15PM -0400, Derrick Stolee wrote:\n> On 10/28/24 12:47 PM, Taylor Blau wrote:\n> > On Mon, Oct 28, 2024 at 06:46:07AM +0100, Patrick Steinhardt wrote:\n> > > I've flagged this internally now at GitLab so that we can provide some\n> > > more data with some of the repos that are on the bigger side to check\n> > > whether we can confirm the findings and to prioritize its review.\n> >\n> > I suspect that you'll end up measuring no change assuming that you\n> > (AFAIK) use bitmaps and (I imagine) delta islands in your production\n> > configuration? This series is not compatible with either of those\n> > features to my knowledge.\n> You are correct that this is not compatible with those features as-is.\n> _Maybe_ there is potential to integrate them in the future, but that\n> would require better understanding of whether the new compression\n> mechanism valuable in enough cases (final storage size or maybe even\n> in repacking time).\n\nI think the bitmap thing is not too big of a hurdle. The .bitmap file is\nthe only spot we store name-hash values on-disk in the \"hashcache\"\nextension.\n\nUnfortunately, there is no easy way to reuse the format of the existing\nhashcache extension as-is to indicate to the reader whether they are\nrecording traditional name-hash values, or the new --path-walk hash\nvalues.\n\nI suspect that you could either add a new extension for --path-walk hash\nvalues, or add a new variant of the hashcache extension that has a flag\nto indicate what kind of hash value it's recording.\n\nOf the two, I think the latter is preferred, since it would allow us to\ngrow new hash functions on paths in the future without needing to add an\nadditional extension (only a new bit in the existing one).\n\n> At the very least, it would be helpful if some other large repos were\n> tested to see how commonly this could help client-side users. Are\n> there other aspects to a repo's structure that could be important to\n> how effective this approach is?\n\nWhat measurements are you looking for here? I thought that you had\nalready done an extensive job of measuring the client-side impact of\npushing smaller packs and faster local repacks, no?\n\nThanks,\nTaylor\n"},{"id":"506252","messageId":"85d3c75c-d7c2-498f-8afe-34c49337ba37@gmail.com","threadId":"62282","inReplyTo":"Zx/I+9icWGsRz11x@nand.local","subject":"Re: [PATCH v2 00/17] pack-objects: add --path-walk option for better deltas","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2024-10-28T19:46:11Z","receivedAt":"2024-10-28T19:46:14Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 10/28/24 1:25 PM, Taylor Blau wrote:\n> On Mon, Oct 28, 2024 at 01:13:15PM -0400, Derrick Stolee wrote:\n\n>> You are correct that this is not compatible with those features as-is.\n>> _Maybe_ there is potential to integrate them in the future, but that\n>> would require better understanding of whether the new compression\n>> mechanism valuable in enough cases (final storage size or maybe even\n>> in repacking time).\n> \n> I think the bitmap thing is not too big of a hurdle. The .bitmap file is\n> the only spot we store name-hash values on-disk in the \"hashcache\"\n> extension.\n> \n> Unfortunately, there is no easy way to reuse the format of the existing\n> hashcache extension as-is to indicate to the reader whether they are\n> recording traditional name-hash values, or the new --path-walk hash\n> values.\n\nThe --path-walk option does not mess with the name-hash. You're thinking\nof the --full-name-hash feature [1] that was pulled out due to a lack of\ninterest (and better results with --path-walk).\n\n[1] https://lore.kernel.org/git/pull.1785.git.1725890210.gitgitgadget@gmail.com/\n\n>> At the very least, it would be helpful if some other large repos were\n>> tested to see how commonly this could help client-side users. Are\n>> there other aspects to a repo's structure that could be important to\n>> how effective this approach is?\n> \n> What measurements are you looking for here? I thought that you had\n> already done an extensive job of measuring the client-side impact of\n> pushing smaller packs and faster local repacks, no?\nI've done what I can with the repos I know about, but perhaps other\nfolks have other repos they like to test that might present new\naspects to the problem.\n\nFor example, a colleague was testing this in a variety of Javascript\nrepos and found that the node repo [2] was slightly worse with the\n--path-walk option. I've since discovered that this is only true when\nusing a checked-out copy and the .git/index file is iterated, as some\nlarge source files with few versions become split across the boundary\nof \"in the index\" or \"in commit history\". (I am fixing this aspect as\nwell in the next iteration, hence some reason for its delay.)\n\n[2] https://github.com/nodejs/node\n\nThanks,\n-Stolee\n\n"},{"id":"506254","messageId":"20241028195404.4119175-1-jonathantanmy@google.com","threadId":"62282","inReplyTo":"f8ee11d3003e743affb39835ba3583dd2498e576.1728396724.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 08/17] pack-objects: add --path-walk option","fromName":"Jonathan Tan","fromEmail":"jonathantanmy@google.com","sentAt":"2024-10-28T19:54:04Z","receivedAt":"2024-10-28T19:54:07Z","isPatch":true,"sender":{"key":"jonathantanmy@fastmail.com","avatar":null},"body":"\"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> This new walk is incompatible with some features and is ignored by\n> others:\n> \n>  * Object filters are not currently integrated with the path-walk API,\n>    such as sparse-checkout or tree depth. A blobless packfile could be\n>    integrated easily, but that is deferred for later.\n> \n>  * Server-focused features such as delta islands, shallow packs, and\n>    using a bitmap index are incompatible with the path-walk API.\n> \n>  * The path walk API is only compatible with the --revs option, not\n>    taking object lists or pack lists over stdin. These alternative ways\n>    to specify the objects currently ignores the --path-walk option\n>    without even a warning.\n\nIt might be better to declare --path-walk as \"internal use only\" and\nonly supporting what send-pack.c (used by \"git push\") and \"git repack\"\nneeds. (From this list, it seems that there is a lot of incompatibility,\nsome of which can happen without a warning to the user, so it sounds\nbetter to be up-front and say that we only support what send-pack.c\nneeds. This also makes reviewing easier, as we don't have to think about\nthe possible interactions with every other rev-list feature - only what\nis used by send-pack.c.)\n\nAlso from a reviewer perspective, it might be better to restrict this\npatch set to what send-pack.c needs and leave \"git repack\" for a future\npatch set. This means that we would not need features such as blob\nand tree exclusions, and possibly not even bitmap use or delta reuse\n(assuming that the user would typically push recently-created objects\nthat have not been repacked).\n\n> +\t\t/* Skip objects that do not exist locally. */\n> +\t\tif (exclude_promisor_objects &&\n> +\t\t    oid_object_info_extended(the_repository, oid, &oi,\n> +\t\t\t\t\t     OBJECT_INFO_FOR_PREFETCH) < 0)\n> +\t\t\tcontinue;\n\nThis functionality is typically triggered by --missing=allow;\n--exclude_promisor_objects means (among other things) that we allow\na missing link only if that object is known to be a promisor object\n(because another promisor object refers to it) (see Documentation/\nrev-list-options.txt, and also get_reference() and elsewhere in\nrevision.c - notice how is_promisor_object() is paired with it)\n\nHaving said that, we should probably just fail outright on missing\nobjects, whether or not we have exclude_promisor_objects. If we have\ncomputed that we need to push an object, that object needs to exist.\n(Same for repack.)\n"},{"id":"506295","messageId":"ZyEjHOcf9A4eMSFG@nand.local","threadId":"62282","inReplyTo":"85d3c75c-d7c2-498f-8afe-34c49337ba37@gmail.com","subject":"Re: [PATCH v2 00/17] pack-objects: add --path-walk option for better deltas","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2024-10-29T18:02:04Z","receivedAt":"2024-10-29T18:02:07Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Mon, Oct 28, 2024 at 03:46:11PM -0400, Derrick Stolee wrote:\n> On 10/28/24 1:25 PM, Taylor Blau wrote:\n> > On Mon, Oct 28, 2024 at 01:13:15PM -0400, Derrick Stolee wrote:\n>\n> > > You are correct that this is not compatible with those features as-is.\n> > > _Maybe_ there is potential to integrate them in the future, but that\n> > > would require better understanding of whether the new compression\n> > > mechanism valuable in enough cases (final storage size or maybe even\n> > > in repacking time).\n> >\n> > I think the bitmap thing is not too big of a hurdle. The .bitmap file is\n> > the only spot we store name-hash values on-disk in the \"hashcache\"\n> > extension.\n> >\n> > Unfortunately, there is no easy way to reuse the format of the existing\n> > hashcache extension as-is to indicate to the reader whether they are\n> > recording traditional name-hash values, or the new --path-walk hash\n> > values.\n>\n> The --path-walk option does not mess with the name-hash. You're thinking\n> of the --full-name-hash feature [1] that was pulled out due to a lack of\n> interest (and better results with --path-walk).\n>\n> [1] https://lore.kernel.org/git/pull.1785.git.1725890210.gitgitgadget@gmail.com/\n\nAh, gotcha. Thanks for clarifying.\n\nWhat is the incompatibility between the two, then? Is it just that\nbitmaps give us the objects in pack- or pseudo-pack order, and we don't\nhave a way to permute that back into the order that --path-walk would\ngive us?\n\nIf so, a couple of thoughts:\n\n  - You could consider storing the path information for each blob and\n    tree object in the bitmap using a trie-like structure. This would\n    give you enough information to reconstruct the path-walk order (I\n    think) at read-time, but at significant cost in terms of the on-disk\n    size of the .bitmap.\n\n  - Alternatively, if you construct the bitmap from a pack or packs that\n    were generated in path-walk order, then you could store a\n    permutation between pack order and path-walk order in the bitmap\n    itself.\n\n  - Alternatively still: if the actual pack *order* were dictated solely\n    by path-walk, then neither of these would be necessary.\n\nThat all said, I'm still not sure that there is a compatibility blocker\nhere. Is the goal is to ensure that packs generated with\n--use-bitmap-index are still compact in the same way that they would be\nwith your new --path-walk option?\n\nIf so, I think matching the object order in a pack to the path walk\norder would achieve that goal, since the chunks that you end up reusing\nverbatim as a result of pack-reuse driven by the bitmap would already be\ndelta-ified according to the --path-walk rules, so the resulting pack\nwould appear similarly.\n\nOTOH, the order in which we pack objects is extremely important to\nperformance as you no doubt are aware of. So changing that order to more\nclosely match the --path-walk option should be done with great care.\n\nAnyway. All of that is to say that I want to better understand what does\nand doesn't work together between bitmaps and path-walk. Given my\ncurrent understanding, it seems there are a couple of approaches to\nunifying these two things together, so it would be nice to be able to\ndo so if possible.\n\nThanks,\nTaylor\n"},{"id":"506296","messageId":"ZyEkWyB/C/lGb36b@nand.local","threadId":"62282","inReplyTo":"20241028195404.4119175-1-jonathantanmy@google.com","subject":"Re: [PATCH 08/17] pack-objects: add --path-walk option","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2024-10-29T18:07:23Z","receivedAt":"2024-10-29T18:07:26Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Mon, Oct 28, 2024 at 12:54:04PM -0700, Jonathan Tan wrote:\n> \"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> > This new walk is incompatible with some features and is ignored by\n> > others:\n> >\n> >  * Object filters are not currently integrated with the path-walk API,\n> >    such as sparse-checkout or tree depth. A blobless packfile could be\n> >    integrated easily, but that is deferred for later.\n> >\n> >  * Server-focused features such as delta islands, shallow packs, and\n> >    using a bitmap index are incompatible with the path-walk API.\n> >\n> >  * The path walk API is only compatible with the --revs option, not\n> >    taking object lists or pack lists over stdin. These alternative ways\n> >    to specify the objects currently ignores the --path-walk option\n> >    without even a warning.\n>\n> It might be better to declare --path-walk as \"internal use only\" and\n> only supporting what send-pack.c (used by \"git push\") and \"git repack\"\n> needs. (From this list, it seems that there is a lot of incompatibility,\n> some of which can happen without a warning to the user, so it sounds\n> better to be up-front and say that we only support what send-pack.c\n> needs. This also makes reviewing easier, as we don't have to think about\n> the possible interactions with every other rev-list feature - only what\n> is used by send-pack.c.)\n\nIs the thinking there that we care mostly about 'git push' and 'git\nrepack' on the client-side?\n\nI don't think it's unreasonable necessarily, but I would add that\nclient-side users definitely do use bitmaps (though not delta islands),\neither when working in a bare repository (where bitmaps are the default)\nor when using 'git gc' (and/or through 'git maintenance') when\n'repack.writeBitmaps' is enabled.\n\nSo I think the approach here would be to limit it to some cases of\nclient side behavior, but we should keep in mind that it will not cover\nall cases.\n\nMy feeling is that it would be nice to pull on the incompatibility\nstring a little more and see if we can't make the two work together\nwithout too much effort.\n\nThanks,\nTaylor\n"},{"id":"506306","messageId":"20241029213657.1303762-1-jonathantanmy@google.com","threadId":"62282","inReplyTo":"ZyEkWyB/C/lGb36b@nand.local","subject":"Re: [PATCH 08/17] pack-objects: add --path-walk option","fromName":"Jonathan Tan","fromEmail":"jonathantanmy@google.com","sentAt":"2024-10-29T21:36:57Z","receivedAt":"2024-10-29T21:37:00Z","isPatch":true,"sender":{"key":"jonathantanmy@fastmail.com","avatar":null},"body":"Taylor Blau <me@ttaylorr.com> writes:\n> Is the thinking there that we care mostly about 'git push' and 'git\n> repack' on the client-side?\n\nI would go further - for the initial patch set, we should only care\nabout \"git push\" on the client side. Stolee said [1] that the \"primary\nmotivation for this feature is its use to shrink the packfile created\nby 'git push' when there are many name-hash collisions\", and in thinking\nabout how to reduce the patch set for easier review, I thought that to\nbe a good scope.\n\nSubsequent patch set(s) can implement \"git repack\", useful for both\nclient and server.\n\n[1] https://lore.kernel.org/git/pull.1813.v2.git.1729431810.gitgitgadget@gmail.com/\n\n> I don't think it's unreasonable necessarily, but I would add that\n> client-side users definitely do use bitmaps (though not delta islands),\n> either when working in a bare repository (where bitmaps are the default)\n> or when using 'git gc' (and/or through 'git maintenance') when\n> 'repack.writeBitmaps' is enabled.\n\nI was thinking that a typical use case would be to create the commits\n(using the tool Stolee mentioned, \"beachball\") and then immediately push\nthem. In this case, I don't think there would be much opportunity for\na bitmap write to be triggered, meaning that the pushed commits are not\ncovered by bitmaps.\n\nBut in any case, this was motivated by a desire to reduce the patch set\n- I don't have a fundamental objection to including support for bitmaps\nin the first patch set.\n\n> So I think the approach here would be to limit it to some cases of\n> client side behavior, but we should keep in mind that it will not cover\n> all cases.\n\nYeah, that was my approach too.\n\n> My feeling is that it would be nice to pull on the incompatibility\n> string a little more and see if we can't make the two work together\n> without too much effort.\n> \n> Thanks,\n> Taylor\n\nBy incompatibility, do you mean the incompatibility between bitmaps\nand the overall --path-walk feature as implemented collectively by the\npatches in Stolee's patch set? If so, I suspect that we will need a\nparallel code path that takes in the \"want\" and \"uninteresting\" commits\nand emits the list of objects (possibly before sorting the objects by\npath hash), much like in builtin/pack-objects.c, so I think there will\nbe some effort involved in making the two work together.\n"},{"id":"506307","messageId":"ZyFet+Gamc8g7Hg6@nand.local","threadId":"62282","inReplyTo":"20241029213657.1303762-1-jonathantanmy@google.com","subject":"Re: [PATCH 08/17] pack-objects: add --path-walk option","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2024-10-29T22:16:23Z","receivedAt":"2024-10-29T22:16:31Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Tue, Oct 29, 2024 at 02:36:57PM -0700, Jonathan Tan wrote:\n> By incompatibility, do you mean the incompatibility between bitmaps\n> and the overall --path-walk feature as implemented collectively by the\n> patches in Stolee's patch set? If so, I suspect that we will need a\n> parallel code path that takes in the \"want\" and \"uninteresting\" commits\n> and emits the list of objects (possibly before sorting the objects by\n> path hash), much like in builtin/pack-objects.c, so I think there will\n> be some effort involved in making the two work together.\n\nI am not sure yet, in all honesty, because I haven't had enough time to\nspend yet reviewing these patches to have anything intelligent to say.\n\nThanks,\nTaylor\n"},{"id":"506343","messageId":"eff447c5-fa0b-446b-989e-b3e34a685811@gmail.com","threadId":"62282","inReplyTo":"ZyFet+Gamc8g7Hg6@nand.local","subject":"Re: [PATCH 08/17] pack-objects: add --path-walk option","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2024-10-31T02:04:10Z","receivedAt":"2024-10-31T02:04:13Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 10/29/24 6:16 PM, Taylor Blau wrote:\n> On Tue, Oct 29, 2024 at 02:36:57PM -0700, Jonathan Tan wrote:\n>> By incompatibility, do you mean the incompatibility between bitmaps\n>> and the overall --path-walk feature as implemented collectively by the\n>> patches in Stolee's patch set? If so, I suspect that we will need a\n>> parallel code path that takes in the \"want\" and \"uninteresting\" commits\n>> and emits the list of objects (possibly before sorting the objects by\n>> path hash), much like in builtin/pack-objects.c, so I think there will\n>> be some effort involved in making the two work together.\n> \n> I am not sure yet, in all honesty, because I haven't had enough time to\n> spend yet reviewing these patches to have anything intelligent to say.\n\nI think that the --path-walk option is fundamentally incompatible with\nthe --use-bitmap-index option (using reachability bitmaps to reduce how\nmany objects we parse to discover reachability) but is not necessarily\nincompatible with writing bitmaps. But it would require testing to be\nsure that there are no surprises due to something like an object order\nchanging or something like that.\n\nThe feature is also not currently integrated with delta islands, so\nthat would need integration and testing to make sure things are\ngrouped together and delta chains go in the right direction.\n\nThese things may be something possible to overcome in the future, but\nthe lack of current integration points and testing makes me want to\nleave this version with guard rails that prevent users from getting\ninto a bind.\n\nThanks,\n-Stolee\n\n\n"},{"id":"506344","messageId":"59c0b17a-491e-48ed-840e-7eec1c2d1de2@gmail.com","threadId":"62282","inReplyTo":"20241028195404.4119175-1-jonathantanmy@google.com","subject":"Re: [PATCH 08/17] pack-objects: add --path-walk option","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2024-10-31T02:12:38Z","receivedAt":"2024-10-31T02:12:41Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 10/28/24 3:54 PM, Jonathan Tan wrote:\n> \"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>> This new walk is incompatible with some features and is ignored by\n>> others:\n>>\n>>   * Object filters are not currently integrated with the path-walk API,\n>>     such as sparse-checkout or tree depth. A blobless packfile could be\n>>     integrated easily, but that is deferred for later.\n>>\n>>   * Server-focused features such as delta islands, shallow packs, and\n>>     using a bitmap index are incompatible with the path-walk API.\n>>\n>>   * The path walk API is only compatible with the --revs option, not\n>>     taking object lists or pack lists over stdin. These alternative ways\n>>     to specify the objects currently ignores the --path-walk option\n>>     without even a warning.\n> \n> It might be better to declare --path-walk as \"internal use only\" and\n> only supporting what send-pack.c (used by \"git push\") and \"git repack\"\n> needs. (From this list, it seems that there is a lot of incompatibility,\n> some of which can happen without a warning to the user, so it sounds\n> better to be up-front and say that we only support what send-pack.c\n> needs. This also makes reviewing easier, as we don't have to think about\n> the possible interactions with every other rev-list feature - only what\n> is used by send-pack.c.)\n\nI do wonder what the value of doing this would be. I consider 'gitpack-objects' \nto already be a plumbing command, so marking any option\nas \"internal use only\" seems like overkill. It takes effort to combine\nthe options carefully for the right effect. The tests in p5313 are not\nterribly simple, such as needing --no-reuse-delta to guarantee we are\nusing the desired delta algorithm.\n\n> Also from a reviewer perspective, it might be better to restrict this\n> patch set to what send-pack.c needs and leave \"git repack\" for a future\n> patch set. This means that we would not need features such as blob\n> and tree exclusions, and possibly not even bitmap use or delta reuse\n> (assuming that the user would typically push recently-created objects\n> that have not been repacked).\n\nWhile I can understand that as being a potential place to split the\npatch series, the integration to add 'git repack --path-walk' is actually\nvery simple. Repacking \"everything\" needs to happen to be able to push a\nrepo to an empty remote, after all.\n\nThere are some subtleties around indexed objects, reflogs, and the like\nthat add some complexity, but they also are handled in the path-walk API\nlayer. Some of that complexity was helpful to know about during repack\ntests.\n\nFinally, the 'git repack --path-walk' use case is a great one for\ndemonstrating the benefits to threading the 'git pack-objects\n--path-walk' algorithm.\n\n>> +\t\t/* Skip objects that do not exist locally. */\n>> +\t\tif (exclude_promisor_objects &&\n>> +\t\t    oid_object_info_extended(the_repository, oid, &oi,\n>> +\t\t\t\t\t     OBJECT_INFO_FOR_PREFETCH) < 0)\n>> +\t\t\tcontinue;\n> \n> This functionality is typically triggered by --missing=allow;\n> --exclude_promisor_objects means (among other things) that we allow\n> a missing link only if that object is known to be a promisor object\n> (because another promisor object refers to it) (see Documentation/\n> rev-list-options.txt, and also get_reference() and elsewhere in\n> revision.c - notice how is_promisor_object() is paired with it)\n> \n> Having said that, we should probably just fail outright on missing\n> objects, whether or not we have exclude_promisor_objects. If we have\n> computed that we need to push an object, that object needs to exist.\n> (Same for repack.)\n\nI think that this is not a reasonable assumption that a hard fail\nshould be expected. Someone could create a blobless clone with a\nsparse-checkout and then add a new file outside their sparse-checkout\nwithout ever having the previous version downloaded.\n\nWhen pushing, they won't be able to use the previous version as a\ndelta base, but they would certainly be confused about an object\nbeing downloaded during 'git push'.\n\nWhile the example I bring up is somewhat contrived, I can easily\nimagine cases where missing objects are part of the commit boundary\nand could be marked as UNINTERESTING but would still be sent in the\nbatch to be considered as a delta base.\n\nThanks,\n-Stolee\n\n"},{"id":"506345","messageId":"1dd36099-1f3f-4078-9009-6c142723430c@gmail.com","threadId":"62282","inReplyTo":"ZyEkWyB/C/lGb36b@nand.local","subject":"Re: [PATCH 08/17] pack-objects: add --path-walk option","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2024-10-31T02:14:08Z","receivedAt":"2024-10-31T02:14:11Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 10/29/24 2:07 PM, Taylor Blau wrote:\n> On Mon, Oct 28, 2024 at 12:54:04PM -0700, Jonathan Tan wrote:\n\n> Is the thinking there that we care mostly about 'git push' and 'git\n> repack' on the client-side?\n> \n> I don't think it's unreasonable necessarily, but I would add that\n> client-side users definitely do use bitmaps (though not delta islands),\n> either when working in a bare repository (where bitmaps are the default)\n> or when using 'git gc' (and/or through 'git maintenance') when\n> 'repack.writeBitmaps' is enabled.\nI suppose some users do use bitmaps, but in my experience, client-side\npushes are slower with bitmaps because a typical target branch is\nfaster to compute by doing a commit walk, at least when the bitmaps are\nolder than the new commits in the topic branch. This may be outdated by\nnow, as it has been a few years since I did a client-side test of\nbitmaps.\n\nThanks,\n-Stolee\n"},{"id":"506346","messageId":"9aa2471b-0850-4707-9733-d3b33609f5f2@gmail.com","threadId":"62282","inReplyTo":"ZyEjHOcf9A4eMSFG@nand.local","subject":"Re: [PATCH v2 00/17] pack-objects: add --path-walk option for better deltas","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2024-10-31T02:28:22Z","receivedAt":"2024-10-31T02:28:25Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 10/29/24 2:02 PM, Taylor Blau wrote:\n > On Mon, Oct 28, 2024 at 03:46:11PM -0400, Derrick Stolee wrote:\n >> On 10/28/24 1:25 PM, Taylor Blau wrote:\n >>> Unfortunately, there is no easy way to reuse the format of the existing\n >>> hashcache extension as-is to indicate to the reader whether they are\n >>> recording traditional name-hash values, or the new --path-walk hash\n >>> values.\n >>\n >> The --path-walk option does not mess with the name-hash. You're thinking\n >> of the --full-name-hash feature [1] that was pulled out due to a lack of\n >> interest (and better results with --path-walk).\n >>\n >> [1] https://lore.kernel.org/git/pull.1785.git.1725890210.gitgitgadget@gmail.com/\n >\n > Ah, gotcha. Thanks for clarifying.\n >\n > What is the incompatibility between the two, then? Is it just that\n > bitmaps give us the objects in pack- or pseudo-pack order, and we don't\n > have a way to permute that back into the order that --path-walk would\n > give us?\n\nThe incompatibility of reading bitmaps and using the path-walk API is\nthat the path-walk API does not check a bitmap to see if an object is\nalready discovered. Thus, it does not use the reachability information\nfrom the bitmap at all and would parse commits and trees to find the\nobjects that should be in the pack-file.\n\nIt should also be worth noting that using something like 'git repack\n--path-walk' does not mean that future 'git pack-objects' executions\nfrom that packfile data need to use the --path-walk option. I expect\nthat it should be painless to write bitmaps on top of a packfile created\nwith 'git repack -adf --path-walk', but since most places doing so also\nlikely want delta islands, I have not explored this option thoroughly.\n\n(Delta islands are their own challenge, since the path-walk API is not\nspreading the reachability information across the objects it walks.\nHowever, this could be remedied by doing a separate walk to identify\nislands using the normal method. I believe Peff had an idea in that\ndirection in another thread. This requires some integration and testing\nthat I don't have the expertise to provide.)\n\n > If so, a couple of thoughts:\n > ...\n\nSince the incompatibility is in a different direction, I don't think\nthese thoughts were relevant to the problem.\n\n > OTOH, the order in which we pack objects is extremely important to\n > performance as you no doubt are aware of. So changing that order to more\n > closely match the --path-walk option should be done with great care.\n\nThis is a place where I'm unsure about how the --path-walk option adjusts\nthe object order within the pack. The packing list gets resorted to match\nthe typical method, at least for how the delta compression window works.\n\nThis would be another good reason to consider the --path-walk option in\nserver environments very carefully. My patch series puts up guard rails\nspecifically because it makes no claim to be effective in all of the\ndimensions that matter for those scenarios. Hopefully, others will be\nmotivated enough to determine if the compression that's possible with\nthis algorithm could be achieved in a way that is compatible with server\nneeds.\n\n > Anyway. All of that is to say that I want to better understand what does\n > and doesn't work together between bitmaps and path-walk. Given my\n > current understanding, it seems there are a couple of approaches to\n > unifying these two things together, so it would be nice to be able to\n > do so if possible.\n\nI think this is an excellent opportunity for testing and debugging to\nbuild up more intuition with how the path-walk API works. When I submit\nthe next version later tonight, the path-walk algorithm will be better\ndocumented.\n\nThat said, I don't have any personal motivation to integrate the two\ntogether, so I don't expect to be contributing that integration point\nmyself. I think that the results speak for themselves in the very\ncommon environment of a Git client without bitmaps.\n\nThanks,\n-Stolee\n\n"},{"id":"506408","messageId":"ZyPwUmYI59Ov0peR@nand.local","threadId":"62282","inReplyTo":"1dd36099-1f3f-4078-9009-6c142723430c@gmail.com","subject":"Re: [PATCH 08/17] pack-objects: add --path-walk option","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2024-10-31T21:02:10Z","receivedAt":"2024-10-31T21:02:13Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Wed, Oct 30, 2024 at 10:14:08PM -0400, Derrick Stolee wrote:\n> On 10/29/24 2:07 PM, Taylor Blau wrote:\n> > On Mon, Oct 28, 2024 at 12:54:04PM -0700, Jonathan Tan wrote:\n>\n> > Is the thinking there that we care mostly about 'git push' and 'git\n> > repack' on the client-side?\n> >\n> > I don't think it's unreasonable necessarily, but I would add that\n> > client-side users definitely do use bitmaps (though not delta islands),\n> > either when working in a bare repository (where bitmaps are the default)\n> > or when using 'git gc' (and/or through 'git maintenance') when\n> > 'repack.writeBitmaps' is enabled.\n> I suppose some users do use bitmaps, but in my experience, client-side\n> pushes are slower with bitmaps because a typical target branch is\n> faster to compute by doing a commit walk, at least when the bitmaps are\n> older than the new commits in the topic branch. This may be outdated by\n> now, as it has been a few years since I did a client-side test of\n> bitmaps.\n\nAll true, though it's hard to estimate the size of \"some\". I share your\nintuition that bitmaps are often a drag on performance for the\nclient-side because doing a pure commit walk is often faster, especially\nif the client has a reasonably up-to-date commit graph.\n\nThanks,\nTaylor\n"},{"id":"506409","messageId":"ZyPxprSncGa1CKep@nand.local","threadId":"62282","inReplyTo":"9aa2471b-0850-4707-9733-d3b33609f5f2@gmail.com","subject":"Re: [PATCH v2 00/17] pack-objects: add --path-walk option for better deltas","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2024-10-31T21:07:50Z","receivedAt":"2024-10-31T21:07:53Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Wed, Oct 30, 2024 at 10:28:22PM -0400, Derrick Stolee wrote:\n> On 10/29/24 2:02 PM, Taylor Blau wrote:\n> > On Mon, Oct 28, 2024 at 03:46:11PM -0400, Derrick Stolee wrote:\n> >> On 10/28/24 1:25 PM, Taylor Blau wrote:\n> >>> Unfortunately, there is no easy way to reuse the format of the existing\n> >>> hashcache extension as-is to indicate to the reader whether they are\n> >>> recording traditional name-hash values, or the new --path-walk hash\n> >>> values.\n> >>\n> >> The --path-walk option does not mess with the name-hash. You're thinking\n> >> of the --full-name-hash feature [1] that was pulled out due to a lack of\n> >> interest (and better results with --path-walk).\n> >>\n> >> [1] https://lore.kernel.org/git/pull.1785.git.1725890210.gitgitgadget@gmail.com/\n> >\n> > Ah, gotcha. Thanks for clarifying.\n> >\n> > What is the incompatibility between the two, then? Is it just that\n> > bitmaps give us the objects in pack- or pseudo-pack order, and we don't\n> > have a way to permute that back into the order that --path-walk would\n> > give us?\n>\n> The incompatibility of reading bitmaps and using the path-walk API is\n> that the path-walk API does not check a bitmap to see if an object is\n> already discovered. Thus, it does not use the reachability information\n> from the bitmap at all and would parse commits and trees to find the\n> objects that should be in the pack-file.\n\nSure, I think what I'm trying to understand here is whether this\n\"incapability\" is a fundamental one, or just that we haven't implemented\nchecking bitmaps in the path-walk API yet.\n\nThanks,\nTaylor\n"}]}