From eede1e69fe4b852f14cade6c18abd156fbcc8cd1 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 6 Jul 2026 13:50:52 +0000 Subject: [PATCH 1/8] sparse-index: avoid crash on intent-to-add entry outside the cone When collapsing a full index to a sparse index, the recursive convert_to_sparse_rec() walks the cache tree to determine if any of the cache tree entries can be used to represent a sparse directory. As it goes, the method tracks how many cache entries are being represented by the cache tree entry. The cache tree node's 'entry_count' represents how many cache entries are covered by the node. However, this value can be negative, representing that a node is invalid, and is no longer reflecting the number of cache entries fit within. This can happen when the user uses 'git add --intent-to-add' to mark an untracked file with the intent-to-add bit to avoid committing without finishing the add. When such an intent-to-add file exists and the sparse-checkout changes to no longer contain its parent directory, this leads to a segfault. Two tests are added to demonstrate this fault: * One test is added to t3705-add-sparse-checkout.sh to demonstrate how 'git add' behaves with sparse-checkout. * One test is added to t1092-sparse-checkout-compatibility.sh to demonstrate the interaction with the sparse index and to compare it directly to how the commands behave with a full index or no sparse-checkout. The fix involves engaging with the loop that iterates over all cache entries within the parent cache tree node (from 'start' to 'end') and to set the 'span' variable slightly earlier. At this point, the cache entry is for a file that is at least one directory deeper than the current cache tree node. The path is also not in the sparse-checkout because of an earlier path_in_sparse_checkout() check above the loop. So we are trying to collapse this directory by recursively calling convert_to_sparse_rec() over that span of entries, but the negative value prevents us from predicting that number without scanning. Theoretically, we could scan to find the range of entries that match this directory and determine if they truly do have an intent-to-add bit and then collapse as many child trees as possible (the ones with valid cache tree nodes). That would be a non-trivial change for performance-only benefit. Since this combination of the intent-to-add and sparse index features has so far gone undetected by real users, this scenario is unlikely to be worth such a change. We settle for the simplest change that prevents a bug: don't try to collapse a node that is invalid for this reason. The tests that would demonstrate a segfault now pass. Further, they demonstrate that the intent-to-add bit persists in the index file after changing the sparse-checkout scope. The test in t1092 demonstrates how some sparse directories could be collapsed further with a more involved fix, if so desired in the future. Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- sparse-index.c | 9 ++++- t/t1092-sparse-checkout-compatibility.sh | 48 ++++++++++++++++++++++++ t/t3705-add-sparse-checkout.sh | 26 +++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/sparse-index.c b/sparse-index.c index 1ed769b78d8de1..c1fa231a89fc07 100644 --- a/sparse-index.c +++ b/sparse-index.c @@ -113,10 +113,17 @@ static int convert_to_sparse_rec(struct index_state *istate, continue; } + span = ct->down[pos]->cache_tree->entry_count; + if (span < 0) { + /* cache-tree entry is invalidated, cannot collapse. */ + istate->cache[num_converted++] = ce; + i++; + continue; + } + strbuf_setlen(&child_path, 0); strbuf_add(&child_path, ce->name, slash - ce->name + 1); - span = ct->down[pos]->cache_tree->entry_count; count = convert_to_sparse_rec(istate, num_converted, i, i + span, child_path.buf, child_path.len, diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh index 8186da5c887c56..c433de2c1e02cb 100755 --- a/t/t1092-sparse-checkout-compatibility.sh +++ b/t/t1092-sparse-checkout-compatibility.sh @@ -384,6 +384,54 @@ test_expect_success 'add, commit, checkout' ' test_all_match git checkout - ' +test_expect_success 'intent-to-add entries outside sparse-checkout' ' + init_repos && + + write_script edit-contents <<-\EOF && + echo text >>$1 + EOF + + test_sparse_match git sparse-checkout set deep folder1 && + run_on_sparse mkdir -p folder1 && + run_on_all ../edit-contents folder1/newita && + test_sparse_match git add -N folder1/newita && + + test_sparse_match git sparse-checkout set deep && + test_sparse_match git status --porcelain=v2 && + test_sparse_match git ls-files --stage +' + +test_expect_success 'intent-to-add with --sparse outside sparse-checkout' ' + init_repos && + + write_script edit-contents <<-\EOF && + echo text >>$1 + EOF + + run_on_all mkdir -p folder1 && + run_on_all ../edit-contents folder1/newita && + test_all_match git add --sparse --intent-to-add folder1/newita && + + test_all_match git status --porcelain=v2 && + test_all_match git ls-files --stage && + test_all_match git diff --cached --stat && + + # Ensure sparse index stores correct sparse directories and + # intent-to-add path. + git -C sparse-index ls-files --format="%(path)" --sparse >out && + + # These paths should be present in index as-is. + test_grep "^before/\$" out && + test_grep "^folder1/newita\$" out && + test_grep "^folder2/\$" out && + test_grep "^x/\$" out && + + # folder/0/ could theoretically be collapsed to a sparse + # directory entry, but the current implementation avoids the + # reduction because of folder1/newita + test_grep "^folder1/0/0/0\$" out +' + test_expect_success 'git add, checkout, and reset with -p' ' init_repos && diff --git a/t/t3705-add-sparse-checkout.sh b/t/t3705-add-sparse-checkout.sh index 53a4782267b705..cf3f42a353da78 100755 --- a/t/t3705-add-sparse-checkout.sh +++ b/t/t3705-add-sparse-checkout.sh @@ -233,4 +233,30 @@ test_expect_success 'refuse to add non-skip-worktree file from sparse dir' ' test_cmp expect stderr ' +test_expect_success 'intent-to-add entry and sparse index' ' + test_when_finished "git sparse-checkout disable" && + test_when_finished "git reset --hard" && + + git sparse-checkout disable && + mkdir -p in out && + echo base >in/file && + echo base >out/file && + git add in/file out/file && + git commit -m "in and out directories" && + + # enable sparse-checkout, but with all child directories. + git config index.sparse true && + git sparse-checkout set in out && + + # create a new path and set intent-to-add bit + echo new >out/newita && + git add -N out/newita && + + # collapse sparse-checkout, and make sure that the sparse index + # maintains the intent-to-add bit. + git sparse-checkout set in && + git ls-files --error-unmatch out/newita && + git status --porcelain +' + test_done From da0fb7e36e9864f7f41d3a2dd291ffd34fcec97f Mon Sep 17 00:00:00 2001 From: Michael Montalbo Date: Wed, 15 Jul 2026 19:29:52 +0000 Subject: [PATCH 2/8] revision: make get_commit_action() a pure predicate get_commit_action() reads as a predicate that decides whether a commit is shown or ignored, but for a line-level log without parent rewriting it also calls line_log_process_ranges_arbitrary_commit(), which mutates the tracked line ranges. That hidden side effect makes it unsafe to evaluate ahead of the walk, the way a lookahead would. get_commit_action() was split out of simplify_commit() in beb5af43a6 (graph API: fix bug in graph_is_interesting(), 2009-08-18) as the show/ignore decision minus the parent rewriting, so the graph renderer could reuse it; line-level log later routed its filtering through it as well, in 3cb9d2b6 (line-log: more responsive, incremental 'git log -L', 2020-05-11). Besides simplify_commit(), the walk driver, graph_is_interesting() is its only other caller, and it runs only under --graph, which sets rewrite_parents and therefore want_ancestry(); the "-L without ancestry" branch that holds the side effect never fires there, so it is dormant today. The line-level processing folds a commit's tracked ranges onto its parents, which must happen even for a commit that get_commit_action() filters from the output, or the ranges never reach the parents. Move it to simplify_commit() and run it before get_commit_action(), gated by get_commit_action()'s leading checks (already shown, uninteresting, and the like) so a commit ignored by those is not folded, as before; factor those checks out as commit_early_ignore(). get_commit_action() is then side-effect free. commit_early_ignore() runs twice on the -L path, once for that gate and once inside get_commit_action(), but it reads only object flags and pack membership, disjoint from the TREESAME flag the fold sets, so the repeat is harmless. Add a "line-log-peek" subcommand to the revision-walking test helper that evaluates get_commit_action() on a commit the walk has not reached yet, plus a t4211 check that the call leaves the commit's flags unchanged. The flags are compared rather than the commit list because add_line_range() merges ranges by union, which is idempotent, so the side effect never changed which commits a linear -L history shows. Suggested-by: Junio C Hamano Signed-off-by: Michael Montalbo Signed-off-by: Junio C Hamano --- revision.c | 70 ++++++++++++++++++++------------ t/helper/test-revision-walking.c | 63 ++++++++++++++++++++++++++++ t/t4211-line-log.sh | 20 +++++++++ 3 files changed, 127 insertions(+), 26 deletions(-) diff --git a/revision.c b/revision.c index 137a86d33bbbe1..18f3ef44c8f1be 100644 --- a/revision.c +++ b/revision.c @@ -4174,37 +4174,39 @@ static timestamp_t comparison_date(const struct rev_info *revs, commit->date; } -enum commit_action get_commit_action(struct rev_info *revs, struct commit *commit) +/* + * Whether the commit is ignored by the cheap checks that read only its + * traversal flags and pack membership (e.g. already shown, or marked + * uninteresting), before any check that examines the commit's date, + * parents, message, or diff. + */ +static int commit_early_ignore(struct rev_info *revs, struct commit *commit) { if (commit->object.flags & SHOWN) - return commit_ignore; + return 1; if (revs->maximal_only && (commit->object.flags & CHILD_VISITED)) - return commit_ignore; + return 1; if (revs->unpacked && has_object_pack(revs->repo, &commit->object.oid)) - return commit_ignore; - if (revs->no_kept_objects) { - if (has_object_kept_pack(revs->repo, &commit->object.oid, - revs->keep_pack_cache_flags)) - return commit_ignore; - } + return 1; + if (revs->no_kept_objects && + has_object_kept_pack(revs->repo, &commit->object.oid, + revs->keep_pack_cache_flags)) + return 1; if (commit->object.flags & UNINTERESTING) + return 1; + return 0; +} + +/* + * Decide whether this commit is shown or ignored. Keep it a pure + * predicate: callers such as the commit graph depend on it having no + * side effects, so per-commit mutations (such as -L range tracking) + * belong in the caller, simplify_commit(), not here. + */ +enum commit_action get_commit_action(struct rev_info *revs, struct commit *commit) +{ + if (commit_early_ignore(revs, commit)) return commit_ignore; - if (revs->line_level_traverse && !want_ancestry(revs)) { - /* - * In case of line-level log with parent rewriting - * prepare_revision_walk() already took care of all line-level - * log filtering, and there is nothing left to do here. - * - * If parent rewriting was not requested, then this is the - * place to perform the line-level log filtering. Notably, - * this check, though expensive, must come before the other, - * cheaper filtering conditions, because the tracked line - * ranges must be adjusted even when the commit will end up - * being ignored based on other conditions. - */ - if (!line_log_process_ranges_arbitrary_commit(revs, commit)) - return commit_ignore; - } if (revs->min_age != -1 && comparison_date(revs, commit) > revs->min_age) return commit_ignore; @@ -4313,7 +4315,23 @@ struct commit_list *get_saved_parents(struct rev_info *revs, const struct commit enum commit_action simplify_commit(struct rev_info *revs, struct commit *commit) { - enum commit_action action = get_commit_action(revs, commit); + enum commit_action action; + + /* + * For a line-level log without parent rewriting, fold each commit's + * ranges as the walk reaches it (parent rewriting does this eagerly in + * prepare_revision_walk()). Fold before get_commit_action() so the + * ranges carry across a commit that a later, cheaper check ignores; + * the commit_early_ignore() guard skips a commit get_commit_action() + * would ignore outright. + */ + if (revs->line_level_traverse && !want_ancestry(revs) && + !commit_early_ignore(revs, commit)) { + if (!line_log_process_ranges_arbitrary_commit(revs, commit)) + return commit_ignore; + } + + action = get_commit_action(revs, commit); if (action == commit_show && revs->prune && revs->dense && want_ancestry(revs)) { diff --git a/t/helper/test-revision-walking.c b/t/helper/test-revision-walking.c index 70051eeaf848e7..24d7f294178dda 100644 --- a/t/helper/test-revision-walking.c +++ b/t/helper/test-revision-walking.c @@ -13,9 +13,12 @@ #include "test-tool.h" #include "commit.h" #include "diff.h" +#include "line-log.h" +#include "object-name.h" #include "repository.h" #include "revision.h" #include "setup.h" +#include "string-list.h" static void print_commit(struct commit *commit) { @@ -51,6 +54,60 @@ static int run_revision_walk(void) return got_revision; } +/* + * Check that get_commit_action() is a pure predicate by evaluating it on a + * commit the walk has not reached yet. No git command makes that out-of-order + * call, so this probe does it deliberately, and reports whether the call + * mutated the peeked commit: a pure get_commit_action() leaves it untouched. + * We compare the commit's flags rather than the emitted commit list because + * range merges are idempotent, so a side effect would not change which commits + * are shown. Only meaningful for a plain "-L" walk with no parent rewriting. + */ +static int line_log_peek(const char **argv) +{ + struct repository *repo = the_repository; + struct rev_info rev; + struct string_list range_args = STRING_LIST_INIT_DUP; + struct object_id oid; + struct commit *peek; + const char *rev_argv[3]; + unsigned before, after; + + if (repo_get_oid(repo, argv[0], &oid)) + die("bad peek commit: %s", argv[0]); + peek = lookup_commit_reference(repo, &oid); + if (!peek || repo_parse_commit(repo, peek)) + die("cannot parse peek commit: %s", argv[0]); + + repo_init_revisions(repo, &rev, NULL); + rev.diffopt.flags.recursive = 1; + rev.line_level_traverse = 1; + string_list_append(&range_args, argv[1]); + + rev_argv[0] = "line-log-peek"; + rev_argv[1] = argv[2]; + rev_argv[2] = NULL; + setup_revisions(2, rev_argv, &rev, NULL); + + line_log_init(&rev, NULL, &range_args); + + if (rev.rewrite_parents || rev.children.name) + die("line-log-peek requires a non-ancestry (-L, no --graph) walk"); + + if (prepare_revision_walk(&rev)) + die("prepare_revision_walk failed"); + + before = peek->object.flags; + get_commit_action(&rev, peek); + after = peek->object.flags; + + printf("mutated %d\n", before != after); + + release_revisions(&rev); + string_list_clear(&range_args, 0); + return 0; +} + int cmd__revision_walking(int argc, const char **argv) { if (argc < 2) @@ -69,6 +126,12 @@ int cmd__revision_walking(int argc, const char **argv) return 0; } + if (!strcmp(argv[1], "line-log-peek")) { + if (argc != 5) + die("usage: test-tool revision-walking line-log-peek "); + return line_log_peek(argv + 2); + } + fprintf(stderr, "check usage\n"); return 1; } diff --git a/t/t4211-line-log.sh b/t/t4211-line-log.sh index ca4eb7bbc713ef..f4a7d8ab61d027 100755 --- a/t/t4211-line-log.sh +++ b/t/t4211-line-log.sh @@ -781,4 +781,24 @@ test_expect_success '--summary shows new file on root commit' ' test_grep "create mode 100644 file.c" actual ' +test_expect_success 'get_commit_action() does not mutate a not-yet-walked commit' ' + git init peek && + ( + cd peek && + test_write_lines 1 2 3 4 5 >f.c && + git add f.c && test_tick && git commit -m base && + test_write_lines 1 two 3 4 5 >f.c && + test_tick && git commit -am change && + + # Peek HEAD^, which the walk has not reached (the out-of-order + # call a lookahead makes), and confirm get_commit_action() leaves + # it untouched. A side effect is invisible in the commit list + # (range merges are idempotent), so the helper reports whether the + # call mutated the peeked commit at all. + echo "mutated 0" >expect && + test-tool revision-walking line-log-peek HEAD^ 1,3:f.c HEAD >actual && + test_cmp expect actual + ) +' + test_done From 640177a987270564a8ef90cdc359677209fbe0fe Mon Sep 17 00:00:00 2001 From: Jeff King Date: Sun, 26 Jul 2026 04:46:22 -0400 Subject: [PATCH 3/8] diff-lib: drop stale comment about advancing o->pos The comment above oneway_diff() claims that the callback must advance o->pos to skip index entries it has already processed. That stopped being true in da165f470e (unpack-trees.c: prepare for looking ahead in the index, 2010-01-07), which moved that bookkeeping into unpack_trees(). Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- diff-lib.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/diff-lib.c b/diff-lib.c index 0e868b28b6b004..bcab7c8500ae03 100644 --- a/diff-lib.c +++ b/diff-lib.c @@ -508,11 +508,9 @@ static void do_oneway_diff(struct unpack_trees_options *o, * For diffing, the index is more important, and we only have a * single tree. * - * We're supposed to advance o->pos to skip what we have already processed. - * * This wrapper makes it all more readable, and takes care of all * the fairly complex unpack_trees() semantic requirements, including - * the skipping, the path matching, the type conflict cases etc. + * the path matching, the type conflict cases etc. */ static int oneway_diff(const struct cache_entry * const *src, struct unpack_trees_options *o) From 151726d3ca54cbd279d527dc048133ead5c8b582 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Sun, 26 Jul 2026 04:47:05 -0400 Subject: [PATCH 4/8] diff-lib: skip paths outside prefix in oneway_diff() Commit 8174627b3d (diff-lib: ignore paths that are outside $cwd if --relative asked, 2021-08-22) taught run_diff_files() to skip entries outside the requested prefix before processing them. Do the same in oneway_diff(), which handles the diff-index code path. The lower-level diff queue functions already reject such paths, but checking here avoids unnecessary work and keeps them out of every do_oneway_diff() code path. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- diff-lib.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/diff-lib.c b/diff-lib.c index bcab7c8500ae03..d07e5d8d5b0aae 100644 --- a/diff-lib.c +++ b/diff-lib.c @@ -538,6 +538,11 @@ static int oneway_diff(const struct cache_entry * const *src, if (!idx && !tree) BUG("oneway_diff with neither idx nor tree"); + if (revs->diffopt.prefix && + strncmp((idx ? idx : tree)->name, revs->diffopt.prefix, + revs->diffopt.prefix_length)) + return 0; + if (ce_path_match(revs->diffopt.repo->index, idx ? idx : tree, &revs->prune_data, NULL)) { From 2abc7f0304606e23199416ffa3dbc84fa2d431d3 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Tue, 28 Jul 2026 11:00:31 -0400 Subject: [PATCH 5/8] cat-file: handle content request for --batch-command without type The batch mode of cat-file needs to know the object's type in order to print the contents (because it decides whether to stream or not based on object type). The default batch output contains %(objecttype), so we get the type info automatically. But when it doesn't, we have to ask for it explicitly. In the --batch code path, we check while setting up the object_info struct whether we will print the contents, and if so set "typep" to get the value. This comes from 6554dfa97a (cat-file: handle --batch format with missing type/size, 2013-12-12). But later we added a --batch-command mode, which does not do the same trick. The decision about whether to retrieve the contents is made per-command (a "contents" vs "info" command), so we can't decide when building the object_info originally. As a result, asking for: echo "contents HEAD" | git cat-file --batch-command="%(objectname)" will fail the assertion in print_object_or_die() that the type was actually filled in. We can fix it by tweaking the object_info on the fly as we receive each command. But we should be careful to restore it afterwards; otherwise a sequence of commands like: contents $one info $two info $three will pay the type-lookup price for $two and $three when it does not need to. This wouldn't be incorrect, but just slightly inefficient (and hence there are no tests for that part, because the externally-visible behavior is the same). Reported-by: Alan Stokes Helped-by: Pablo Sabater Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- builtin/cat-file.c | 3 +++ t/t1006-cat-file.sh | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/builtin/cat-file.c b/builtin/cat-file.c index d6ef8414ee5a0b..826a1c6620fcc2 100644 --- a/builtin/cat-file.c +++ b/builtin/cat-file.c @@ -687,8 +687,11 @@ static void parse_cmd_contents(struct batch_options *opt, struct strbuf *output, struct expand_data *data) { + enum object_type *saved_typep = data->info.typep; + data->info.typep = &data->type; opt->batch_mode = BATCH_MODE_CONTENTS; batch_one_object(line, output, opt, data); + data->info.typep = saved_typep; } static void parse_cmd_info(struct batch_options *opt, diff --git a/t/t1006-cat-file.sh b/t/t1006-cat-file.sh index 8e2c52652c5185..d0db1f2a2917da 100755 --- a/t/t1006-cat-file.sh +++ b/t/t1006-cat-file.sh @@ -1351,6 +1351,14 @@ test_expect_success 'batch-command flush without --buffer' ' grep "^fatal:.*flush is only for --buffer mode.*" err ' +test_expect_success 'batch-command contents auto-handles type' ' + echo "HEAD" | + git cat-file --batch="%(objectname)" >expect && + echo "contents HEAD" | + git cat-file --batch-command="%(objectname)" >actual && + test_cmp expect actual +' + perl_script=' use warnings; use strict; From 68cce04a028cac13fa1fd7a368801fac3d8b156b Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Tue, 28 Jul 2026 15:00:04 +0200 Subject: [PATCH 6/8] merge: fix leak with merge.defaultToUpstream By default the setting 'merge.defaultToUpstream' for git-merge(1) is set to 'true', which means when `git merge` is invoked with no arguments it merges the upstream branch configured for the current branch. With this configuration set to 'true', setup_with_upstream() is called. That function allocates an array of arguments and hands it back to cmd_merge() via its `argv` parameter. This array is never freed, so cmd_merge() leaks it on every invocation. Track the allocated array in a separate variable and free it at the end. The leak has been present since 93e535a5b7 (merge: merge with the default upstream branch without argument, 2011-03-24). Although the leak sanitizer was enabled for tests in fc1ddf42af (t: remove TEST_PASSES_SANITIZE_LEAK annotations, 2024-11-21), it went unnoticed because no test calls `git merge` without arguments, exercising the default-to-upstream path. Add such a test in t7600, which fails under the leak sanitizer without this fix. Signed-off-by: Toon Claes Acked-by: Jeff King Signed-off-by: Junio C Hamano --- builtin/merge.c | 7 +++++-- t/t7600-merge.sh | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/builtin/merge.c b/builtin/merge.c index 2cbce56f8da9f7..ecd7a897d05fa9 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -1372,7 +1372,7 @@ int cmd_merge(int argc, struct commit_list *common = NULL; const char *best_strategy = NULL, *wt_strategy = NULL; struct commit_list *remoteheads = NULL, *p; - void *branch_to_free; + void *branch_to_free, *argv_to_free = NULL; int orig_argc = argc; int merge_log_config = -1; @@ -1516,8 +1516,10 @@ int cmd_merge(int argc, option_commit = 1; if (!argc) { - if (default_to_upstream) + if (default_to_upstream) { argc = setup_with_upstream(&argv); + argv_to_free = argv; + } else die(_("No commit specified and merge.defaultToUpstream not set.")); } else if (argc == 1 && !strcmp(argv[0], "-")) { @@ -1885,6 +1887,7 @@ int cmd_merge(int argc, } strbuf_release(&buf); free(branch_to_free); + free(argv_to_free); free(pull_twohead); free(pull_octopus); discard_index(the_repository->index); diff --git a/t/t7600-merge.sh b/t/t7600-merge.sh index 9838094b66ac39..b3a2164e0836cd 100755 --- a/t/t7600-merge.sh +++ b/t/t7600-merge.sh @@ -1165,4 +1165,21 @@ test_expect_success 'suggested names are not ambiguous' ' grep remotes/origin/not-local stderr ' +test_expect_success 'merge with no argument defaults to upstream' ' + test_when_finished "rm -rf upstream downstream" && + git init upstream && + ( + cd upstream && + test_commit one && + test_commit two + ) && + git clone upstream downstream && + ( + cd downstream && + git reset --hard HEAD^ && + git merge && + test_cmp_rev origin/main HEAD + ) +' + test_done From b56b48301ea221ae3dd6dc01f887f0b72e10e729 Mon Sep 17 00:00:00 2001 From: David Lin Date: Tue, 28 Jul 2026 09:52:48 -0400 Subject: [PATCH 7/8] pack-bitmap: handle objects at bitmap position zero `bitmap_position()` only returns a negative value when an object is not present in the bitmap index. In `find_objects()`, we have added a check (11d45a6e6a) to avoid processing a root whose reachability is already represented by the base bitmap, but accidentally uses `pos > 0`. Consequently, it never performs the membership test for an object at position zero. If that object has an individual reachability bitmap, we unnecessarily OR that bitmap into the base again. Otherwise, we add the object to the not-mapped list, only for the subsequent pass to recognize that it is already present. The latter pass correctly treats all non-negative positions as valid, so this does not change the resulting object set, but an off-by-one edge case. Treat position zero as valid by changing the condition to `pos >= 0`. The existing pseudo-merge traversal test exercises this case. Its position-zero commit is presented through multiple roots. Before this change, each occurrence is counted as a bitmap hit; afterwards, only the first occurrence is counted. Assert the resulting hit count to cover the boundary condition. Also cover the non-pseudo-merge case by passing `HEAD` twice. The first occurrence initializes the base from its stored bitmap, and the second must recognize that position zero is already present. Helped-by: Taylor Blau Signed-off-by: David Lin Signed-off-by: Junio C Hamano --- pack-bitmap.c | 2 +- t/t5333-pseudo-merge-bitmaps.sh | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pack-bitmap.c b/pack-bitmap.c index e8a82945cc319e..9c9b27b7239131 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -1559,7 +1559,7 @@ static struct bitmap *find_objects(struct bitmap_index *bitmap_git, if (base) { int pos = bitmap_position(bitmap_git, &object->oid); - if (pos > 0 && bitmap_get(base, pos)) { + if (pos >= 0 && bitmap_get(base, pos)) { object->flags |= SEEN; continue; } diff --git a/t/t5333-pseudo-merge-bitmaps.sh b/t/t5333-pseudo-merge-bitmaps.sh index 305d6771082d55..5b2a17f90a1091 100755 --- a/t/t5333-pseudo-merge-bitmaps.sh +++ b/t/t5333-pseudo-merge-bitmaps.sh @@ -50,7 +50,15 @@ test_expect_success 'bitmap traversal without pseudo-merges' ' test_pseudo_merges_cascades 0 merges && test_must_be_empty merges && - test_cmp expect actual + test_cmp expect actual && + + : >trace2.txt && + GIT_TRACE2_EVENT=$PWD/trace2.txt \ + git rev-list --objects --use-bitmap-index HEAD HEAD >/dev/null && + + # The first HEAD initializes base from its position-zero bitmap. The + # duplicate root should not count as another bitmap hit. + test_trace2_data bitmap bitmap/hits 1 Date: Fri, 7 Aug 2026 12:02:39 -0700 Subject: [PATCH 8/8] The 12th batch Signed-off-by: Junio C Hamano --- Documentation/RelNotes/2.56.0.adoc | 42 +++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/Documentation/RelNotes/2.56.0.adoc b/Documentation/RelNotes/2.56.0.adoc index 87ac9067f194eb..9073520c251035 100644 --- a/Documentation/RelNotes/2.56.0.adoc +++ b/Documentation/RelNotes/2.56.0.adoc @@ -298,6 +298,22 @@ Performance, Internal Implementation, Development Support etc. the image upgrade. (merge 1a1579c42d jk/ci-static-analysis-image-bump later to maint). + * The alias tests in 't/t0014-alias.sh' have been updated to dynamically + query the list of deprecated commands using 'git + --list-cmds=deprecated' to avoid test failures when running with + 'WITH_BREAKING_CHANGES' in a build directory that contains stale + executables of formerly deprecated commands. + (merge bc57ecb915 jk/t0014-dynamic-deprecated-cmds later to maint). + + * The code path that deals with relative paths in the diff-lib has + been cleaned up. + + * The get_commit_action() function has been refactored to be a pure + predicate by moving the side-effecting line-level log range folding to + simplify_commit(). This ensures that evaluating a commit's action + before the walk reaches it does not prematurely mutate its tracked + line ranges, making it safer for potential lookahead evaluations. + Fixes since v2.55 ----------------- @@ -496,14 +512,26 @@ Fixes since v2.55 and another prevented the editor from opening when the final command in a chain containing 'fixup -c' was skipped. - * The alias tests in 't/t0014-alias.sh' have been updated to dynamically - query the list of deprecated commands using 'git - --list-cmds=deprecated' to avoid test failures when running with - 'WITH_BREAKING_CHANGES' in a build directory that contains stale - executables of formerly deprecated commands. - (merge bc57ecb915 jk/t0014-dynamic-deprecated-cmds later to maint). - * Git for Windows has been updated to avoid auto-detecting the symlink type if the target path starts with a slash, preventing NTLM credential leaks when checking out repositories with crafted symbolic links pointing to network shares. + + * 'git cat-file --batch-command' that asked for 'contents' without + 'type' segfaults, which has been corrected. + (merge 2abc7f0304 jk/cat-file-batch-wo-type-fix later to maint). + + * A memory leak in 'git merge' when run without arguments (which + triggers the default-to-upstream path) has been fixed. A test has + been added to cover this case. + (merge 68cce04a02 tc/merge-default-to-upstream-leakfix later to maint). + + * A boundary case check in reachability bitmap traversal has been + corrected to properly handle the object at position zero, which was + previously skipped, leading to redundant bitmap loading. + (merge b56b48301e dl/pack-bitmap-position-zero later to maint). + + * A crash in the 'sparse-index' collapse code when encountering an + invalidated cache-tree node (due to an intent-to-add path) has been + fixed by avoiding collapsing such subtrees. + (merge eede1e69fe ds/sparse-index-ita-crash later to maint).