From 5bd39784cda151203aa6d97e24c21bf7fddc11de Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Fri, 12 Jun 2026 17:49:12 -0400 Subject: [PATCH 01/35] commit-reach: reject cycles in contains walk The memoized contains traversal used by git tag assumes that commit ancestry is acyclic. Replacement refs can violate that assumption, causing it to keep pushing an already active commit until memory is exhausted. Mark commits while they are active and die if the traversal encounters an active commit. Other failures in this walk already die through parse_commit_or_die(); using a second reachability walk would only add a separate policy for malformed history. Suggested-by: Kristofer Karlsson Signed-off-by: Tamir Duberstein Signed-off-by: Junio C Hamano --- commit-reach.c | 12 +++++++++--- commit-reach.h | 3 ++- t/t7004-tag.sh | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/commit-reach.c b/commit-reach.c index 5df471a313cf6b..58553dff05692e 100644 --- a/commit-reach.c +++ b/commit-reach.c @@ -757,7 +757,8 @@ static int in_commit_list(const struct commit_list *want, struct commit *c) /* * Test whether the candidate is contained in the list. - * Do not recurse to find out, though, but return -1 if inconclusive. + * Do not recurse to find out, though, but return CONTAINS_UNKNOWN if + * inconclusive. */ static enum contains_result contains_test(struct commit *candidate, const struct commit_list *want, @@ -814,6 +815,7 @@ static enum contains_result contains_tag_algo(struct commit *candidate, if (result != CONTAINS_UNKNOWN) return result; + *contains_cache_at(cache, candidate) = CONTAINS_IN_PROGRESS; push_to_contains_stack(candidate, &contains_stack); while (contains_stack.nr) { struct contains_stack_entry *entry = &contains_stack.contains_stack[contains_stack.nr - 1]; @@ -825,8 +827,8 @@ static enum contains_result contains_tag_algo(struct commit *candidate, contains_stack.nr--; } /* - * If we just popped the stack, parents->item has been marked, - * therefore contains_test will return a meaningful yes/no. + * A parent may have just been popped and marked, or may still + * be active when replacement refs create a cycle. */ else switch (contains_test(parents->item, want, cache, cutoff)) { case CONTAINS_YES: @@ -836,7 +838,11 @@ static enum contains_result contains_tag_algo(struct commit *candidate, case CONTAINS_NO: entry->parents = parents->next; break; + case CONTAINS_IN_PROGRESS: + die(_("commit ancestry contains a cycle")); case CONTAINS_UNKNOWN: + *contains_cache_at(cache, parents->item) = + CONTAINS_IN_PROGRESS; push_to_contains_stack(parents->item, &contains_stack); break; } diff --git a/commit-reach.h b/commit-reach.h index 3f3a563d8a5dd1..f908d305b16902 100644 --- a/commit-reach.h +++ b/commit-reach.h @@ -73,7 +73,8 @@ int ref_newer(const struct object_id *new_oid, const struct object_id *old_oid); enum contains_result { CONTAINS_UNKNOWN = 0, CONTAINS_NO, - CONTAINS_YES + CONTAINS_YES, + CONTAINS_IN_PROGRESS }; define_commit_slab(contains_cache, enum contains_result); diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh index d918005dd9859a..67309494d283c7 100755 --- a/t/t7004-tag.sh +++ b/t/t7004-tag.sh @@ -1611,6 +1611,24 @@ test_expect_success 'checking that first commit is in all tags (hash)' ' test_cmp expected actual ' +test_expect_success 'tag --contains rejects cyclic replacement histories' ' + first=$(git rev-parse HEAD~2) && + second=$(git rev-parse HEAD~) && + third=$(git rev-parse HEAD) && + test_when_finished " + git replace -d $first && + git replace -d $third && + git tag -d cycle-a cycle-b + " && + git tag cycle-a "$first" && + git tag cycle-b "$third" && + git replace --graft "$first" "$third" "$second" && + git replace --graft "$third" "$first" && + test_must_fail git tag --contains="$second" --list "cycle-*" \ + >/dev/null 2>err && + test_grep "fatal: commit ancestry contains a cycle" err +' + # other ways of specifying the commit test_expect_success 'checking that first commit is in all tags (tag)' ' cat >expected <<-\EOF && From ca96eeee79bc231f8096d9e9d0b501e0495e2db7 Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Fri, 12 Jun 2026 17:49:13 -0400 Subject: [PATCH 02/35] ref-filter: memoize --contains with generations git branch and git for-each-ref run a separate reachability walk for each ref considered by --contains and --no-contains. Refs with shared history therefore traverse the same commits repeatedly. git tag instead uses a depth-first walk that caches results across refs. That walk can perform poorly without generation numbers: a negative check may walk to the root instead of stopping at a nearby divergence. Generation numbers let it stop below the oldest target. Use the memoized walk for all ref-filter callers when generation numbers are available. Keep git tag on its existing path without generations. Caching still helps when many tags share deep history: ffc4b8012d (tag: speed up --contains calculation, 2011-06-11) reduced git tag --contains HEAD~200 in linux-2.6 from 15.417 to 5.329 seconds. The new shared-history perf test improves from 0.72 to 0.03 seconds. In a repository with 62,174 remote-tracking refs, running: git branch -r --contains c78ae85f3ce7e improves from 104.365 seconds to 468 milliseconds. Suggested-by: Jeff King Signed-off-by: Tamir Duberstein Signed-off-by: Junio C Hamano --- commit-reach.c | 3 ++- t/perf/p1500-graph-walks.sh | 28 +++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/commit-reach.c b/commit-reach.c index 58553dff05692e..349ca68a3761b0 100644 --- a/commit-reach.c +++ b/commit-reach.c @@ -854,7 +854,8 @@ static enum contains_result contains_tag_algo(struct commit *candidate, int commit_contains(struct ref_filter *filter, struct commit *commit, struct commit_list *list, struct contains_cache *cache) { - if (filter->with_commit_tag_algo) + if (filter->with_commit_tag_algo || + generation_numbers_enabled(the_repository)) return contains_tag_algo(commit, list, cache) == CONTAINS_YES; return repo_is_descendant_of(the_repository, commit, list); } diff --git a/t/perf/p1500-graph-walks.sh b/t/perf/p1500-graph-walks.sh index 5b23ce5db93ad3..d167b4f7e1f2ad 100755 --- a/t/perf/p1500-graph-walks.sh +++ b/t/perf/p1500-graph-walks.sh @@ -32,7 +32,16 @@ test_expect_success 'setup' ' echo "X:$line" >>test-tool-tags || return 1 done && - commit=$(git commit-tree $(git rev-parse HEAD^{tree})) && + git rev-list --first-parent --max-count=8192 HEAD >contains-commits && + test_file_not_empty contains-commits && + git update-ref refs/contains-perf-base "$(tail -n 1 contains-commits)" && + awk "{ + printf \"update refs/contains-perf/%04d %s\\n\", NR, \$1 + }" contains-commits | + git update-ref --stdin && + git pack-refs --include "refs/contains-perf/*" && + + commit=$(git commit-tree HEAD^{tree}) && git update-ref refs/heads/disjoint-base $commit && git commit-graph write --reachable @@ -62,6 +71,23 @@ test_perf 'contains: git tag --merged' ' xargs git tag --merged=HEAD /dev/null +' + test_perf 'is-base check: test-tool reach (refs)' ' test-tool reach get_branch_base_for_tip Date: Fri, 12 Jun 2026 17:49:14 -0400 Subject: [PATCH 03/35] commit-reach: die on contains walk errors Without generation numbers, repo_is_descendant_of() can return -1 when it cannot read commit ancestry. commit_contains() exposes that result through a Boolean interface, so ref-filter treats it as true. This can include a ref for --contains or exclude it for --no-contains without failing the command. Die when repo_is_descendant_of() reports an error. The memoized walk already dies when it cannot parse a commit, so callers of the non-memoized path no longer turn a failed walk into a match. Reported-by: Jeff King Signed-off-by: Tamir Duberstein Signed-off-by: Junio C Hamano --- commit-reach.c | 8 +++++++- t/t6301-for-each-ref-errors.sh | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/commit-reach.c b/commit-reach.c index 349ca68a3761b0..4acf1c02d57ee2 100644 --- a/commit-reach.c +++ b/commit-reach.c @@ -854,10 +854,16 @@ static enum contains_result contains_tag_algo(struct commit *candidate, int commit_contains(struct ref_filter *filter, struct commit *commit, struct commit_list *list, struct contains_cache *cache) { + int result; + if (filter->with_commit_tag_algo || generation_numbers_enabled(the_repository)) return contains_tag_algo(commit, list, cache) == CONTAINS_YES; - return repo_is_descendant_of(the_repository, commit, list); + + result = repo_is_descendant_of(the_repository, commit, list); + if (result < 0) + die(_("failed to check reachability")); + return result; } int can_all_from_reach_with_flag(struct object_array *from, diff --git a/t/t6301-for-each-ref-errors.sh b/t/t6301-for-each-ref-errors.sh index e06feb06e91956..72b27c8be37d44 100755 --- a/t/t6301-for-each-ref-errors.sh +++ b/t/t6301-for-each-ref-errors.sh @@ -52,6 +52,28 @@ test_expect_success 'Missing objects are reported correctly' ' test_must_be_empty brief-err ' +test_expect_success 'missing ancestors are reported by contains filters' ' + test_when_finished "git update-ref -d refs/heads/missing-parent" && + { + echo "tree $(git rev-parse HEAD^{tree})" && + echo "parent $MISSING" && + git cat-file commit HEAD | + sed -n -e "/^author /p" -e "/^committer /p" && + echo && + echo "missing parent" + } >commit && + broken=$(git hash-object -t commit -w commit) && + git update-ref refs/heads/missing-parent "$broken" && + for option in --contains --no-contains + do + test_must_fail git for-each-ref "$option=HEAD" \ + refs/heads/missing-parent >out 2>err && + test_must_be_empty out && + test_grep "parse commit $MISSING" err || + return 1 + done +' + test_expect_success 'ahead-behind requires an argument' ' test_must_fail git for-each-ref \ --format="%(ahead-behind)" 2>err && From 641588bbcd1e80e084881d547e10aca8b32c328e Mon Sep 17 00:00:00 2001 From: Pablo Sabater Date: Tue, 14 Jul 2026 14:09:32 +0200 Subject: [PATCH 04/35] lib-log-graph: move check_graph function check_graph is a function shared in the test files t4215 and t6016 used to format the output graph, but instead of being in a file called by both test, the function code is repeated in each file. Move check_graph to lib-log-graph.sh file which both tests already import graph functions from, renaming it to lib_test_check_graph. This function is needed for the following commit which includes graph tests in a new file and requires check_graph. Mentored-by: Karthik Nayak Mentored-by: Chandra Pratap Signed-off-by: Pablo Sabater Signed-off-by: Junio C Hamano --- t/lib-log-graph.sh | 5 ++++ t/t4215-log-skewed-merges.sh | 33 +++++++++------------- t/t6016-rev-list-graph-simplify-history.sh | 25 +++++++--------- 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/t/lib-log-graph.sh b/t/lib-log-graph.sh index bf952ef9204dbb..1eae8f60c21bae 100644 --- a/t/lib-log-graph.sh +++ b/t/lib-log-graph.sh @@ -26,3 +26,8 @@ lib_test_cmp_colored_graph () { test_decode_color output.colors && test_cmp expect.colors output.colors } + +lib_test_check_graph () { + cat >expect && + lib_test_cmp_graph --format=%s "$@" +} diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh index 1612f05f1b39ce..eebab71039438b 100755 --- a/t/t4215-log-skewed-merges.sh +++ b/t/t4215-log-skewed-merges.sh @@ -5,11 +5,6 @@ test_description='git log --graph of skewed merges' . ./test-lib.sh . "$TEST_DIRECTORY"/lib-log-graph.sh -check_graph () { - cat >expect && - lib_test_cmp_graph --format=%s "$@" -} - test_expect_success 'log --graph with merge fusing with its left and right neighbors' ' git checkout --orphan _p && test_commit A && @@ -21,7 +16,7 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh git checkout _p && git merge --no-ff _r -m G && git checkout @^^ && git merge --no-ff _p -m H && - check_graph <<-\EOF + lib_test_check_graph <<-\EOF * H |\ | * G @@ -49,7 +44,7 @@ test_expect_success 'log --graph with left-skewed merge' ' git checkout 0_p && git merge --no-ff 0_s -m 0_G && git checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H && - check_graph <<-\EOF + lib_test_check_graph <<-\EOF *-----. 0_H |\ \ \ \ | | | | * 0_G @@ -83,7 +78,7 @@ test_expect_success 'log --graph with nested left-skewed merge' ' git checkout 1_p && git merge --no-ff 1_r -m 1_G && git checkout @^^ && git merge --no-ff 1_p -m 1_H && - check_graph <<-\EOF + lib_test_check_graph <<-\EOF * 1_H |\ | * 1_G @@ -115,7 +110,7 @@ test_expect_success 'log --graph with nested left-skewed merge following normal git checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J && git checkout 2_p && git merge --no-ff 2_s -m 2_K && - check_graph <<-\EOF + lib_test_check_graph <<-\EOF * 2_K |\ | * 2_J @@ -151,7 +146,7 @@ test_expect_success 'log --graph with nested right-skewed merge following left-s git checkout 3_p && git merge --no-ff 3_r -m 3_H && git checkout @^^ && git merge --no-ff 3_p -m 3_J && - check_graph <<-\EOF + lib_test_check_graph <<-\EOF * 3_J |\ | * 3_H @@ -182,7 +177,7 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed git merge --no-ff 4_p -m 4_G && git checkout @^^ && git merge --no-ff 4_s -m 4_H && - check_graph --date-order <<-\EOF + lib_test_check_graph --date-order <<-\EOF * 4_H |\ | * 4_G @@ -218,7 +213,7 @@ test_expect_success 'log --graph with octopus merge with column joining its penu git checkout 5_r && git merge --no-ff 5_s -m 5_H && - check_graph <<-\EOF + lib_test_check_graph <<-\EOF * 5_H |\ | *-. 5_G @@ -257,7 +252,7 @@ test_expect_success 'log --graph with multiple tips' ' git checkout 6_1 && git merge --no-ff 6_2 -m 6_I && - check_graph 6_1 6_3 6_5 <<-\EOF + lib_test_check_graph 6_1 6_3 6_5 <<-\EOF * 6_I |\ | | * 6_H @@ -334,7 +329,7 @@ test_expect_success 'log --graph with multiple tips' ' git checkout -b M_7 7_1 && git merge --no-ff 7_2 7_3 -m 7_M4 && - check_graph M_1 M_3 M_5 M_7 <<-\EOF + lib_test_check_graph M_1 M_3 M_5 M_7 <<-\EOF * 7_M1 |\ | | * 7_M2 @@ -371,7 +366,7 @@ test_expect_success 'log --graph with multiple tips' ' ' test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' ' - check_graph --graph-lane-limit=2 M_7 <<-\EOF + lib_test_check_graph --graph-lane-limit=2 M_7 <<-\EOF *-. 7_M4 |\ \ | | * 7_G @@ -388,7 +383,7 @@ test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' ' ' test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge' ' - check_graph --graph-lane-limit=1 M_7 <<-\EOF + lib_test_check_graph --graph-lane-limit=1 M_7 <<-\EOF *-~ 7_M4 |\~ | ~ 7_G @@ -405,7 +400,7 @@ test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge ' test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' ' - check_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\EOF + lib_test_check_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\EOF * 7_M1 |\ | | * 7_M2 @@ -441,7 +436,7 @@ test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' ' ' test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows first of 3 parent merge' ' - check_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\EOF + lib_test_check_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\EOF * 7_M1 |\ | | * 7_M2 @@ -478,7 +473,7 @@ test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows fir ' test_expect_success 'log --graph --graph-lane-limit=7 check if it shows all 3 parent merge' ' - check_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\EOF + lib_test_check_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\EOF * 7_M1 |\ | | * 7_M2 diff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh index 54b0a6f5f8a4b2..e0d9c3c1acc88c 100755 --- a/t/t6016-rev-list-graph-simplify-history.sh +++ b/t/t6016-rev-list-graph-simplify-history.sh @@ -13,11 +13,6 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME . ./test-lib.sh . "$TEST_DIRECTORY"/lib-log-graph.sh -check_graph () { - cat >expect && - lib_test_cmp_graph --format=%s "$@" -} - test_expect_success 'set up rev-list --graph test' ' # 3 commits on branch A test_commit A1 foo.txt && @@ -54,7 +49,7 @@ test_expect_success 'set up rev-list --graph test' ' ' test_expect_success '--graph --all' ' - check_graph --all <<-\EOF + lib_test_check_graph --all <<-\EOF * A7 * A6 |\ @@ -82,7 +77,7 @@ test_expect_success '--graph --all' ' # that undecorated merges are interesting, even with --simplify-by-decoration test_expect_success '--graph --simplify-by-decoration' ' git tag -d A4 && - check_graph --all --simplify-by-decoration <<-\EOF + lib_test_check_graph --all --simplify-by-decoration <<-\EOF * A7 * A6 |\ @@ -114,7 +109,7 @@ test_expect_success 'setup: get rid of decorations on B' ' # Graph with branch B simplified away test_expect_success '--graph --simplify-by-decoration prune branch B' ' - check_graph --simplify-by-decoration --all <<-\EOF + lib_test_check_graph --simplify-by-decoration --all <<-\EOF * A7 * A6 |\ @@ -133,7 +128,7 @@ test_expect_success '--graph --simplify-by-decoration prune branch B' ' ' test_expect_success '--graph --full-history -- bar.txt' ' - check_graph --full-history --all -- bar.txt <<-\EOF + lib_test_check_graph --full-history --all -- bar.txt <<-\EOF * A7 * A6 |\ @@ -148,7 +143,7 @@ test_expect_success '--graph --full-history -- bar.txt' ' ' test_expect_success '--graph --full-history --simplify-merges -- bar.txt' ' - check_graph --full-history --simplify-merges --all -- bar.txt <<-\EOF + lib_test_check_graph --full-history --simplify-merges --all -- bar.txt <<-\EOF * A7 * A6 |\ @@ -161,7 +156,7 @@ test_expect_success '--graph --full-history --simplify-merges -- bar.txt' ' ' test_expect_success '--graph -- bar.txt' ' - check_graph --all -- bar.txt <<-\EOF + lib_test_check_graph --all -- bar.txt <<-\EOF * A7 * A5 * A3 @@ -172,7 +167,7 @@ test_expect_success '--graph -- bar.txt' ' ' test_expect_success '--graph --sparse -- bar.txt' ' - check_graph --sparse --all -- bar.txt <<-\EOF + lib_test_check_graph --sparse --all -- bar.txt <<-\EOF * A7 * A6 * A5 @@ -189,7 +184,7 @@ test_expect_success '--graph --sparse -- bar.txt' ' ' test_expect_success '--graph ^C4' ' - check_graph --all ^C4 <<-\EOF + lib_test_check_graph --all ^C4 <<-\EOF * A7 * A6 * A5 @@ -202,7 +197,7 @@ test_expect_success '--graph ^C4' ' ' test_expect_success '--graph ^C3' ' - check_graph --all ^C3 <<-\EOF + lib_test_check_graph --all ^C3 <<-\EOF * A7 * A6 |\ @@ -220,7 +215,7 @@ test_expect_success '--graph ^C3' ' # that important, but this test depends on it. If the ordering ever changes # in the code, we'll need to update this test. test_expect_success '--graph --boundary ^C3' ' - check_graph --boundary --all ^C3 <<-\EOF + lib_test_check_graph --boundary --all ^C3 <<-\EOF * A7 * A6 |\ From 08a0bef796e3a1de6b5d08d7958bc122faf32f15 Mon Sep 17 00:00:00 2001 From: Pablo Sabater Date: Tue, 14 Jul 2026 14:09:33 +0200 Subject: [PATCH 05/35] revision: add next_commit_to_show() get_revision() gets its commits from two sources depending on the mode: 1. Normally it gets the commits from get_revision_internal(). 2. --max-count-oldest which was introduced at bb4ce23284 (revision.c: implement --max-count-oldest, 2026-05-19) gets the commits by popping from a saved list at revs->commits marking SHOWN and CHILD_SHOWN on each popped commit. Extract the choice logic into a helper, next_commit_to_show(), which returns the next commit regardless of the source it comes from. This has no change in behavior. The helper is needed in a subsequent commit that pre-fetches two commits into a buffer for lookahead purposes and needs to pre-fetch from the same source. The --reverse branch keeps its own pop loop. Using the helper for --reverse would additionally set SHOWN and CHILD_SHOWN which is not desired and a behavior change. Signed-off-by: Pablo Sabater Signed-off-by: Junio C Hamano --- revision.c | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/revision.c b/revision.c index 0c95edef5947fa..288935943f4fba 100644 --- a/revision.c +++ b/revision.c @@ -4658,12 +4658,34 @@ static void retrieve_oldest_commits(struct rev_info *revs, commit_list_insert(c, queue); } +/* + * Returns the next commit that will be shown, regardless of whether it comes + * directly from the revision walk or from the list saved by the staged output + * of --max-count-oldest. + */ +static struct commit *next_commit_to_show(struct rev_info *revs) +{ + struct commit *c; + struct commit_list *p; + + if (!revs->max_count_stage) + return get_revision_internal(revs); + + c = pop_commit(&revs->commits); + if (c) { + c->object.flags |= SHOWN; + if (!(c->object.flags & BOUNDARY)) + for (p = c->parents; p; p = p->next) + p->item->object.flags |= CHILD_SHOWN; + } + return c; +} + struct commit *get_revision(struct rev_info *revs) { struct commit *c; struct commit_list *reversed; struct commit_list *queue = NULL; - struct commit_list *p; if (revs->max_count_type == 1 && !revs->max_count_stage) { retrieve_oldest_commits(revs, &queue); @@ -4693,17 +4715,7 @@ struct commit *get_revision(struct rev_info *revs) return c; } - if (revs->max_count_stage) { - c = pop_commit(&revs->commits); - if (c) { - c->object.flags |= SHOWN; - if (!(c->object.flags & BOUNDARY)) - for (p = c->parents; p; p = p->next) - p->item->object.flags |= CHILD_SHOWN; - } - } else { - c = get_revision_internal(revs); - } + c = next_commit_to_show(revs); if (c && revs->graph) graph_update(revs->graph, c); From bf3c696b44eb82f90b25101e36cd79651d1480f8 Mon Sep 17 00:00:00 2001 From: Pablo Sabater Date: Tue, 14 Jul 2026 14:09:34 +0200 Subject: [PATCH 06/35] graph: add a 2 commit buffer for lookahead In a subsequent commit the graph renderer needs to know if the next commit is a visual root or if it is the last commit to be shown. This requires peeking 2 commits ahead. Commits are pre-fetched in get_revision() through next_commit_to_show() where they are also marked as SHOWN, regardless the source they come from. Update graph_is_interesting() so it considers commits inside the lookahead buffer as interesting as well. Helped-by: Kristofer Karlsson Signed-off-by: Pablo Sabater Signed-off-by: Junio C Hamano --- graph.c | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ graph.h | 17 +++++++++++++++++ revision.c | 18 ++++++++++++++++-- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/graph.c b/graph.c index 842282685f6cef..89ebcf7540589d 100644 --- a/graph.c +++ b/graph.c @@ -315,6 +315,14 @@ struct git_graph { * diff_output_prefix_callback(). */ struct strbuf prefix_buf; + + /* + * Lookahead buffer: up to 2 pre-fetched commits that will be shown. + * Populated by get_revision() so graph_peek_next_visible() can use + * actual walk results instead of peeking at rev_info internals. + */ + struct commit *lookahead[2]; + int lookahead_nr; }; static inline int graph_needs_truncation(struct git_graph *graph, int lane) @@ -388,6 +396,9 @@ struct git_graph *graph_init(struct rev_info *opt) graph->num_columns = 0; graph->num_new_columns = 0; graph->mapping_size = 0; + graph->lookahead[0] = NULL; + graph->lookahead[1] = NULL; + graph->lookahead_nr = 0; /* * Start the column color at the maximum value, since we'll * always increment it for the first commit we output. @@ -456,6 +467,15 @@ static void graph_ensure_capacity(struct git_graph *graph, int num_columns) */ static int graph_is_interesting(struct git_graph *graph, struct commit *commit) { + /* + * Commits in the lookahead buffer have been pre-fetched by + * get_revision() and will be shown in the future. They already have + * the SHOWN flag set when they were pre-fetched but the graph still + * needs to treat them as interesting parents. + */ + for (int i = 0; i < graph->lookahead_nr; i++) + if (graph->lookahead[i] == commit) + return 1; /* * If revs->boundary is set, commits whose children have * been shown are always interesting, even if they have the @@ -763,6 +783,37 @@ static int graph_needs_pre_commit_line(struct git_graph *graph) graph->expansion_row < graph_num_expansion_rows(graph); } +struct commit *graph_pop_lookahead(struct git_graph *graph) +{ + struct commit *c; + + if (!graph->lookahead_nr) + return NULL; + + c = graph->lookahead[0]; + if (!c) + BUG("lookahead buffer has %d entries but the first one is NULL", + graph->lookahead_nr); + + graph->lookahead[0] = graph->lookahead[1]; + graph->lookahead[1] = NULL; + graph->lookahead_nr--; + return c; +} + +int graph_get_lookahead_room(struct git_graph *graph) +{ + return (int)ARRAY_SIZE(graph->lookahead) - graph->lookahead_nr; +} + +void graph_push_lookahead(struct git_graph *graph, struct commit *c) +{ + if (!graph_get_lookahead_room(graph)) + BUG("pushing into lookahead buffer when it is already full"); + + graph->lookahead[graph->lookahead_nr++] = c; +} + void graph_update(struct git_graph *graph, struct commit *commit) { struct commit_list *parent; diff --git a/graph.h b/graph.h index 3fd1dcb2e94d43..1193711fb8892c 100644 --- a/graph.h +++ b/graph.h @@ -262,4 +262,21 @@ void graph_show_commit_msg(struct git_graph *graph, FILE *file, struct strbuf const *sb); +/* + * Pop the first commit from the graph's lookahead buffer. + * Returns NULL if the buffer is empty. + */ +struct commit *graph_pop_lookahead(struct git_graph *graph); + +/* + * Returns how many more commits can be added to the lookahead buffer. + */ +int graph_get_lookahead_room(struct git_graph *graph); + +/* + * Push a commit into the lookahead buffer. Must only be called when + * graph_get_lookahead_room() returns > 0. + */ +void graph_push_lookahead(struct git_graph *graph, struct commit *c); + #endif /* GRAPH_H */ diff --git a/revision.c b/revision.c index 288935943f4fba..258c3cf782894b 100644 --- a/revision.c +++ b/revision.c @@ -4715,10 +4715,24 @@ struct commit *get_revision(struct rev_info *revs) return c; } - c = next_commit_to_show(revs); + if (revs->graph) { + c = graph_pop_lookahead(revs->graph); + if (!c) + c = next_commit_to_show(revs); + } else { + c = next_commit_to_show(revs); + } - if (c && revs->graph) + if (c && revs->graph) { + while (graph_get_lookahead_room(revs->graph)) { + struct commit *next = next_commit_to_show(revs); + if (!next) + break; + graph_push_lookahead(revs->graph, next); + } graph_update(revs->graph, c); + } + if (!c) { free_saved_parents(revs); commit_list_free(revs->previous_parents); From a34c00d96851009e94ec7618ad5c1464f920c80d Mon Sep 17 00:00:00 2001 From: Pablo Sabater Date: Tue, 14 Jul 2026 14:09:35 +0200 Subject: [PATCH 07/35] graph: indent visual root in graph When rendering a graph, if the history contains multiple "visual roots", actual roots or commits that look like roots (i.e. have their parents filtered out) can end up being vertically adjacent to unrelated commits, falsely appearing to be related. A fix for this issue was already attempted [1] a while ago. This happens because the commits fill the space from left to right and when a visual root ends, its column becomes free for the following commit even if they are not related. Once this happens the unrelated commit is rendered below the visual root. Because there is no special character or way to identify when a visual root is rendered making the graph confusing. By indenting the visual roots when there are still commits to show the vertical adjacency can be avoided. Add is_visual_root flag to git_graph making it visible in all graph states, give graph_update() a new function, graph_is_visual_root() to know if the current commit is a visual root and set is_visual_root. The different handled cases are: - If a visual root has children: similar to GRAPH_PRE_COMMIT state when octopus merges need space, an edge row needs to be printed to connect the child with the indented visual root. A new state GRAPH_PRE_ROOT is needed to connect the child with the visual root: * child of the visual root \ GRAPH_PRE_ROOT * visual root indented - If a visual root is child-less we can skip GRAPH_PRE_ROOT state and render the indented commit directly. * visual root indented * unrelated commit - If two or more visual roots are adjacent: by having a lookahead to the next commit that will be rendered, if the next commit is also a visual root and we are on a visual root, meaning two visual root adjacent in the history, the top one can omit the indent, making the one below to indent only once, if there are more adjacent visual commits, the indentation will increase for each adjacent one, cascading. * visual root * visual root * visual root * last commit Even if the last commit is a root, because there is nothing that will be rendered below we can omit the indentation on purpose. [1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/ Helped-by: Kristofer Karlsson Mentored-by: Karthik Nayak Mentored-by: Chandra Pratap Signed-off-by: Pablo Sabater Signed-off-by: Junio C Hamano --- graph.c | 244 +++++++++++++++ t/meson.build | 1 + t/t4218-log-graph-indentation.sh | 514 +++++++++++++++++++++++++++++++ 3 files changed, 759 insertions(+) create mode 100755 t/t4218-log-graph-indentation.sh diff --git a/graph.c b/graph.c index 89ebcf7540589d..087094189f3467 100644 --- a/graph.c +++ b/graph.c @@ -60,12 +60,23 @@ struct column { * index into column_colors. */ unsigned short color; + /* + * Marks if a commit is a non-first parent of a merge. These columns are + * already visually connected to the merge commit and do not need + * indentation. + * + * The first parent is the one that inherits the column and it can need + * indentation if turns out to be a visual root and there's still + * commits to render. + */ + unsigned int is_merge_parent:1; }; enum graph_state { GRAPH_PADDING, GRAPH_SKIP, GRAPH_PRE_COMMIT, + GRAPH_PRE_ROOT, GRAPH_COMMIT, GRAPH_POST_MERGE, GRAPH_COLLAPSING @@ -323,6 +334,51 @@ struct git_graph { */ struct commit *lookahead[2]; int lookahead_nr; + + /* + * If a commit is a visual root, we need to indent it to prevent + * unrelated commits from being vertically adjacent to it. + */ + unsigned int is_visual_root:1; + + /* + * Indentation increases for each visual root adjacent to another visual + * root, making visual root commits indentation cascade. + */ + unsigned int visual_root_depth; + + /* + * When a visual root is adjacent to other visual roots, the first one + * can avoid indentation and the rest cascades, increasing the indentation + * for each one. + */ + unsigned int visual_root_cascade:1; + + /* + * Set when the current commit was already present in graph->columns + * before being processed. + */ + unsigned int commit_in_columns:1; +}; + +struct graph_lookahead_flags { + + /* + * Set when there will be a commit after the current one that will be + * rendered. + */ + unsigned int is_next_visible:1; + + /* + * Set when the next visible commit is candidate to be a visual root. + */ + unsigned int is_next_visual_root:1; + + /* + * Set when the next visible commit will be rendered under the current + * commit. + */ + unsigned int next_has_column:1; }; static inline int graph_needs_truncation(struct git_graph *graph, int lane) @@ -399,6 +455,8 @@ struct git_graph *graph_init(struct rev_info *opt) graph->lookahead[0] = NULL; graph->lookahead[1] = NULL; graph->lookahead_nr = 0; + graph->visual_root_depth = 0; + graph->visual_root_cascade = 0; /* * Start the column color at the maximum value, since we'll * always increment it for the first commit we output. @@ -581,6 +639,11 @@ static void graph_insert_into_new_columns(struct git_graph *graph, struct commit *commit, int idx) { + /* + * Get the initial merge_layout before it's modified to know if this + * is a merge. + */ + int initial_merge_layout = graph->merge_layout; int i = graph_find_new_column_by_commit(graph, commit); int mapping_idx; @@ -592,6 +655,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph, i = graph->num_new_columns++; graph->new_columns[i].commit = commit; graph->new_columns[i].color = graph_find_commit_color(graph, commit); + graph->new_columns[i].is_merge_parent = 0; } if (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) { @@ -630,6 +694,12 @@ static void graph_insert_into_new_columns(struct git_graph *graph, } graph->mapping[mapping_idx] = i; + + /* + * Mark non-first parents of a merge. + */ + if (graph->num_parents > 1 && initial_merge_layout >= 0 && idx > -1) + graph->new_columns[i].is_merge_parent = 1; } static void graph_update_columns(struct git_graph *graph) @@ -721,10 +791,20 @@ static void graph_update_columns(struct git_graph *graph) if (graph->num_parents == 0) graph->width += 2; } else { + int j; graph_insert_into_new_columns(graph, col_commit, -1); + /* + * This column is not the current commit, but we need to + * propagate the flag until the commit is processed. + */ + j = graph_find_new_column_by_commit(graph, col_commit); + if (j >= 0 && graph->columns[i].is_merge_parent) + graph->new_columns[j].is_merge_parent = 1; } } + graph->commit_in_columns = is_commit_in_columns; + /* * If graph_max_lanes is set, cap the width */ @@ -814,9 +894,113 @@ void graph_push_lookahead(struct git_graph *graph, struct commit *c) graph->lookahead[graph->lookahead_nr++] = c; } +/* + * A commit can be a visual root when: + * + * - It has no parents. + * + * - It has parents but they are all filtered out and + * commit->parents arrives NULL. + * + * - Its parents are uninteresting. + * + * - It is not a boundary commit. Boundary commits also have no visible + * parents, but they are not selected as visual roots because they cannot + * cause the ambiguity of being vertically adjacent because: + * + * 1. A boundary only appears because an included commit is its child. + * Children are always above, and the renderer draws an edge down to + * the boundary from that child. Rather than starting a column like a + * visual root would do, it inherits its child column. + * + * 2. Included commits cannot appear below a boundary. Boundaries are + * ancestors of the exclusion point; if an included commit were an + * ancestor of the boundary it would be excluded and not rendered. + * Boundaries therefore always sink to the bottom. + */ +static int graph_is_visual_root_candidate(struct commit *c, struct git_graph *graph) +{ + struct commit_list *p; + + if (c->object.flags & BOUNDARY) + return 0; + for (p = c->parents; p; p = p->next) + if (graph_is_interesting(graph, p->item)) + return 0; + return 1; +} + +static int graph_is_visual_root(struct git_graph *graph, + struct graph_lookahead_flags *flags) +{ + /* + * This must be only called for the current commit as graph contains + * the state for the current commit only. + * + * To check if a commit is a visual root, call graph_is_visual_root_candidate() + * but we won't know if it is really a visual root until we get to the + * next commit state. + * + * The current commit is an actual visual root if it is a candidate and + * the commit is not a non-first parent of a merge. + * + * * + * |\ + * | * <- it is a visual root candidate but it shouldn't be indented + * * because it is already connected by an edge. + * ^ if commit_in_columns && is_merge_parent means the commit + * | was put by a merge and is connected. + * | + * `-------- if !is_next_visible means we're on the last commit, avoid + * indentation unless the one before is a visual root, then + * we need to differentiate from the one above. + * + * If next_has_columns means that the next commit has + * already a column, so it will not be rendered below, the + * current commit has to act as the last commit and omit + * indentation. + */ + return graph_is_visual_root_candidate(graph->commit, graph) && + !(graph->commit_in_columns && + graph->columns[graph->commit_index].is_merge_parent) && + flags->is_next_visible && + (!flags->next_has_column || graph->visual_root_depth > 0); +} + +/* + * Peeks the next commits via the lookahead buffer and sets the lookahead flags. + */ +static void graph_peek_next_visible(struct git_graph *graph, + struct graph_lookahead_flags *flags) +{ + flags->is_next_visible = 0; + flags->is_next_visual_root = 0; + flags->next_has_column = 0; + + if (!graph->lookahead_nr) + return; + + flags->is_next_visible = 1; + flags->next_has_column = + graph_find_new_column_by_commit(graph, graph->lookahead[0]) >= 0; + + if (!graph_is_visual_root_candidate(graph->lookahead[0], graph)) + return; + + if (graph->lookahead_nr >= 2) + flags->is_next_visual_root = 1; +} + +static int graph_needs_pre_root_line(struct git_graph *graph) +{ + return graph->commit_in_columns && graph->is_visual_root && + graph->num_columns > 0 && !graph->visual_root_cascade; +} + void graph_update(struct git_graph *graph, struct commit *commit) { struct commit_list *parent; + struct graph_lookahead_flags flags; /* * Set the new commit @@ -847,6 +1031,23 @@ void graph_update(struct git_graph *graph, struct commit *commit) */ graph_update_columns(graph); + graph_peek_next_visible(graph, &flags); + + graph->is_visual_root = graph_is_visual_root(graph, &flags); + + if (graph->is_visual_root) { + /* + * If next is a visual root we can omit the indent for the first + * visual root and start cascading. + */ + if (!graph->visual_root_depth && flags.is_next_visual_root) + graph->visual_root_cascade = 1; + graph->visual_root_depth++; + } else { + graph->visual_root_depth = 0; + graph->visual_root_cascade = 0; + } + graph->expansion_row = 0; /* @@ -864,11 +1065,16 @@ void graph_update(struct git_graph *graph, struct commit *commit) * room for it. We need to do this only if there is a branch row * (or more) to the right of this commit. * + * If it is a visual root, we need to print an extra row to + * connect the indentation. + * * If there are less than 3 parents, we can immediately print the * commit line. */ if (graph->state != GRAPH_PADDING) graph->state = GRAPH_SKIP; + else if (graph_needs_pre_root_line(graph)) + graph->state = GRAPH_PRE_ROOT; else if (graph_needs_pre_commit_line(graph)) graph->state = GRAPH_PRE_COMMIT; else @@ -1116,6 +1322,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line if (col_commit == graph->commit) { seen_this = 1; + if (graph->is_visual_root) { + int depth = graph->visual_root_depth; + /* + * Each visual column is 2 characters wide. + * Omit the indentation for the first visual + * root in cascade mode. + */ + int padding = (depth - graph->visual_root_cascade) * 2; + graph_line_addchars(line, ' ', padding); + graph->width += padding; + } graph_output_commit_char(graph, line); if (graph_needs_truncation(graph, i)) { @@ -1487,6 +1704,30 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l graph_update_state(graph, GRAPH_PADDING); } +static void graph_output_pre_root_line(struct git_graph *graph, struct graph_line *line) +{ + /* + * This function adds a row before a visual root, to connect the + * branch to the indented commit. It must only be called on a + * visual root. + */ + if (!graph->is_visual_root) + BUG("commit must be a visual root to call pre_root_line"); + + for (int i = 0; i < graph->num_columns; i++) { + struct column *col = &graph->columns[i]; + if (col->commit == graph->commit) { + graph_line_addch(line, ' '); + graph_line_write_column(line, col, '\\'); + } else { + graph_line_write_column(line, col, '|'); + } + graph_line_addch(line, ' '); + } + + graph_update_state(graph, GRAPH_COMMIT); +} + int graph_next_line(struct git_graph *graph, struct strbuf *sb) { int shown_commit_line = 0; @@ -1512,6 +1753,9 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb) case GRAPH_PRE_COMMIT: graph_output_pre_commit_line(graph, &line); break; + case GRAPH_PRE_ROOT: + graph_output_pre_root_line(graph, &line); + break; case GRAPH_COMMIT: graph_output_commit_line(graph, &line); shown_commit_line = 1; diff --git a/t/meson.build b/t/meson.build index 7c3c070426dc9e..cce5ba71f961c7 100644 --- a/t/meson.build +++ b/t/meson.build @@ -577,6 +577,7 @@ integration_tests = [ 't4215-log-skewed-merges.sh', 't4216-log-bloom.sh', 't4217-log-limit.sh', + 't4218-log-graph-indentation.sh', 't4219-log-follow-merge.sh', 't4252-am-options.sh', 't4253-am-keep-cr-dos.sh', diff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh new file mode 100755 index 00000000000000..60c7d84af7c9d6 --- /dev/null +++ b/t/t4218-log-graph-indentation.sh @@ -0,0 +1,514 @@ +#!/bin/sh + +test_description='git log --graph visual root indentations' + +. ./test-lib.sh +. "$TEST_DIRECTORY"/lib-log-graph.sh + +check_graph_with_description () { + cat >expect && + lib_test_cmp_graph --format="%s%ndescription%nsecond-line" "$@" +} + +create_orphan () { + git checkout --orphan "$1" && + test_might_fail git rm -rf . +} + +# disable commit-graph topo order to have the graph to render in different +# ways (used in --first-parent tests to have multiple visual roots while a +# column is active at the same time). +unset_commit_graph () { + sane_unset GIT_TEST_COMMIT_GRAPH && + rm -f .git/objects/info/commit-graph && + rm -rf .git/objects/info/commit-graphs +} + +test_expect_success 'single root commit is not indented' ' + create_orphan _1 && test_commit 1_A && + lib_test_check_graph _1 <<-\EOF + * 1_A + EOF +' + +test_expect_success 'visual root indented before unrelated branch' ' + create_orphan _2 && test_commit 2_A && test_commit 2_B && + create_orphan _3 && test_commit 3_A && + lib_test_check_graph _2 _3 <<-\EOF + * 3_A + * 2_B + * 2_A + EOF +' + +test_expect_success 'visual root indentation with --left-right' ' + lib_test_check_graph --left-right _2..._3 <<-\EOF + > 3_A + < 2_B + < 2_A + EOF +' + +# A better case of why indentation is still needed with '--left-right' flag is +# that unrelated branches can be on the same side, so it's needed to +# differentiate visual roots on the same side. +test_expect_success 'visual root indentation with --left-right having unrelated commits on the same side' ' + lib_test_check_graph --left-right _2..._3 _1 <<-\EOF + > 3_A + < 2_B + \ + < 2_A + > 1_A + EOF +' + +test_expect_success 'visual root indents the description also' ' + check_graph_with_description _2 _3 <<-\EOF + * 3_A + description + second-line + * 2_B + | description + | second-line + * 2_A + description + second-line + EOF +' + +test_expect_success 'indented visual root parent gets connected to its child' ' + create_orphan _4 && test_commit 4_A && test_commit 4_B && + create_orphan _5 && test_commit 5_A && test_commit 5_B && + lib_test_check_graph _4 _5 <<-\EOF + * 5_B + \ + * 5_A + * 4_B + * 4_A + EOF +' + +test_expect_success 'indented visual root parent gets connected to its child with description' ' + check_graph_with_description _4 _5 <<-\EOF + * 5_B + | description + | second-line + \ + * 5_A + description + second-line + * 4_B + | description + | second-line + * 4_A + description + second-line + EOF +' + +test_expect_success 'visual roots cascade and last root does not' ' + create_orphan _7 && test_commit 7_A && test_commit 7_B && + create_orphan _8 && test_commit 8_A && + create_orphan _9 && test_commit 9_A && + create_orphan _10 && test_commit 10_A && + lib_test_check_graph _7 _8 _9 _10 <<-\EOF + * 10_A + * 9_A + * 8_A + * 7_B + * 7_A + EOF +' + +test_expect_success 'last root does not cascade' ' + lib_test_check_graph _8 _9 _10 <<-\EOF + * 10_A + * 9_A + * 8_A + EOF +' + +test_expect_success 'merge parents are roots between them but they do not indent' ' + create_orphan _11 && test_commit 11_A && + create_orphan _12 && test_commit 12_A && + create_orphan _13 && test_commit 13_A && + git checkout _11 && + TREE=$(git write-tree) && + MERGE=$(git commit-tree $TREE -p _11 -p _12 -p _13 -m 11_octopus) && + git reset --hard $MERGE && + lib_test_check_graph _11 <<-\EOF + *-. 11_octopus + |\ \ + | | * 13_A + | * 12_A + * 11_A + EOF +' + +# The last parent of a merge can be indented if nothing related to it needs to +# be rendered after, if it's another visual root, merge parent must not get +# indented but rather activate cascading. +test_expect_success 'merge then unrelated visual root and unrelated branch' ' + create_orphan _16 && test_commit 16_A && test_commit 16_B && + create_orphan _17 && test_commit 17_A && + create_orphan _18 && test_commit 18_A && + create_orphan _19 && test_commit 19_A && + create_orphan _20 && test_commit 20_A && + git checkout _18 && + TREE=$(git write-tree) && + MERGE=$(git commit-tree $TREE -p _18 -p _19 -p _20 -m 18_octopus) && + git reset --hard $MERGE && + lib_test_check_graph _18 _17 _16 <<-\EOF + *-. 18_octopus + |\ \ + | | * 20_A + | * 19_A + * 18_A + * 17_A + * 16_B + * 16_A + EOF +' + +# The last commit root does not get indented, if the next thing after the root +# merge parent is the last commit, indent the merge parent. +test_expect_success 'merge then unrelated root indents merge parent' ' + lib_test_check_graph _18 _17 <<-\EOF + *-. 18_octopus + |\ \ + | | * 20_A + | * 19_A + \ + * 18_A + * 17_A + EOF +' + +test_expect_success 'merge then unrelated branch indents merge parent' ' + lib_test_check_graph _18 _16 <<-\EOF + *-. 18_octopus + |\ \ + | | * 20_A + | * 19_A + \ + * 18_A + * 16_B + * 16_A + EOF +' + +test_expect_success 'two-parent merge of orphans' ' + create_orphan _21 && test_commit 21_A && + create_orphan _22 && test_commit 22_A && + git checkout _21 && + TREE=$(git write-tree) && + MERGE=$(git commit-tree $TREE -p _21 -p _22 -m 21_merge) && + git reset --hard $MERGE && + lib_test_check_graph _21 <<-\EOF + * 21_merge + |\ + | * 22_A + * 21_A + EOF +' + +test_expect_success 'commit with filtered parent becomes a visual root' ' + create_orphan _23 && + echo test >other.txt && + git add other.txt && + git commit -m "23_A" && + echo test >foo.txt && + git add foo.txt && + git commit -m "23_B" && + create_orphan _24 && + echo test >foo.txt && + git add foo.txt && + git commit -m "24_A" && + lib_test_check_graph _23 _24 -- foo.txt <<-\EOF + * 23_B + * 24_A + EOF +' + +test_expect_success 'filtered parent cascading edge case' ' + create_orphan _27 && + echo test >foo.txt && + git add foo.txt && + test_tick && + git commit -m "D (last)" && + + create_orphan _25 && + echo test >other.txt && + git add other.txt && + test_tick && + git commit -m "C-filtered" && + + echo test >foo.txt && + git add foo.txt && + test_tick && + git commit -m "B (child of filtered)" && + + create_orphan _26 && + echo test >foo.txt && + git add foo.txt && + test_tick && + git commit -m "A (visual root)" && + + lib_test_check_graph _25 _26 _27 -- foo.txt <<-\EOF + * A (visual root) + * B (child of filtered) + * D (last) + EOF +' + +test_expect_success 'multiple filtered parents in sequence' ' + create_orphan _44 && + echo a >other.txt && git add other.txt && git commit -m "44_F" && + echo b >foo.txt && git add foo.txt && git commit -m "44_C" && + + create_orphan _45 && + echo c >other.txt && git add other.txt && git commit -m "45_F" && + echo d >foo.txt && git add foo.txt && git commit -m "45_C" && + + create_orphan _46 && + echo e >foo.txt && git add foo.txt && git commit -m "46_A" && + + lib_test_check_graph _44 _45 _46 -- foo.txt <<-\EOF + * 44_C + * 45_C + * 46_A + EOF +' + +# These tests prove why there is no need to have indentation for boundary +# commits. +# +# Boundary commits rather than starting a column they 'inherit' the one of +# its child so there will always be an edge that connects it removing the +# ambiguity. +test_expect_success 'unrelated boundaries are not ambiguous' ' + create_orphan _28 && test_commit 28_A && test_commit 28_B && + test_commit 28_C && + create_orphan _29 && test_commit 29_A && test_commit 29_B && + lib_test_check_graph --boundary 28_A.._28 29_A.._29 <<-\EOF + * 29_B + | * 28_C + | * 28_B + | o 28_A + o 29_A + EOF +' + +# Same structure as t6016 +test_expect_success 'boundary commits big test' ' + # 3 commits on branch _30 + create_orphan _30 && + test_commit 30_A && + test_commit 30_B && + test_commit 30_C && + + # 2 commits on branch _31, started from 30_A + git checkout -b _31 30_A && + test_commit 31_A && + test_commit 31_B && + + # 2 commits on branch _32, started from 30_B + git checkout -b _32 30_B && + test_commit 32_A && + test_commit 32_B && + + # Octopus merge _31 and _32 into -30 + git checkout _30 && + git merge _31 _32 -m 30_D && + git tag 30_D && + test_commit 30_E && + + # More commits on _32, then merge _32 into _30 + git checkout _32 && + test_commit 32_C && + test_commit 32_D && + git checkout _30 && + git merge -s ours _32 -m 30_F && + git tag 30_F && + test_commit 30_G && + lib_test_check_graph --boundary _30 _31 _32 ^32_C <<-\EOF + * 30_G + * 30_F + |\ + | * 32_D + * | 30_E + | | + | \ + *-. \ 30_D + |\ \ \ + | * | | 31_B + | * | | 31_A + * | | | 30_C + o | | | 30_B + |/ / / + o / / 30_A + / / + | o 32_C + |/ + o 32_B + EOF +' + +# Filter by --first-parent and then forcing the filtered parents to be shown. +test_expect_success '--first-parent flag with the filtered parents' ' + ( + unset_commit_graph && + create_orphan _35 && test_commit 35_A && test_commit 35_B && + create_orphan _36 && test_commit 36_A && + create_orphan _37 && test_commit 37_A && + git checkout _35 && + TREE=$(git write-tree) && + MERGE=$(git commit-tree $TREE -p _35 -p _36 -p _37 -m 35_octopus) && + git reset --hard $MERGE && + lib_test_check_graph --first-parent _35 _36 _37 <<-\EOF + * 35_octopus + | * 37_A + | * 36_A + * 35_B + * 35_A + EOF + ) +' + +test_expect_success '--first-parent with filtered parents but one has a child' ' + ( + unset_commit_graph && + create_orphan _38 && test_commit 38_A && test_commit 38_B && + create_orphan _39 && test_commit 39_A && + create_orphan _40 && test_commit 40_A && test_commit 40_B && + git checkout _38 && + TREE=$(git write-tree) && + MERGE=$(git commit-tree $TREE -p _38 -p _39 -p _40 -m 38_octopus) && + git reset --hard $MERGE && + lib_test_check_graph --first-parent _38 _39 _40 <<-\EOF + * 38_octopus + | * 40_B + | * 40_A + | * 39_A + * 38_B + * 38_A + EOF + ) +' + +test_expect_success '--first-parent with filtered parents but both have children' ' + ( + unset_commit_graph && + create_orphan _41 && test_commit 41_A && test_commit 41_B && + create_orphan _42 && test_commit 42_A && test_commit 42_B && + create_orphan _43 && test_commit 43_A && test_commit 43_B && + git checkout _41 && + TREE=$(git write-tree) && + MERGE=$(git commit-tree $TREE -p _41 -p _42 -p _43 -m 41_octopus) && + git reset --hard $MERGE && + lib_test_check_graph --first-parent _41 _42 _43 <<-\EOF + * 41_octopus + | * 43_B + | \ + | * 43_A + | * 42_B + | * 42_A + * 41_B + * 41_A + EOF + ) +' + +test_expect_success 'two unrelated merges' ' + create_orphan _50 && test_commit 50_A && + git checkout -b _51 && + test_commit 51_A && test_commit 51_B && + git checkout _50 && + git merge --no-ff _51 -m 50_B && + + create_orphan _52 && test_commit 52_A && + git checkout -b _53 && + test_commit 53_A && test_commit 53_B && + git checkout _52 && + git merge --no-ff _53 -m 52_B && + + lib_test_check_graph _52 _50 <<-\EOF + * 52_B + |\ + | * 53_B + | * 53_A + |/ + \ + * 52_A + * 50_B + |\ + | * 51_B + | * 51_A + |/ + * 50_A + EOF +' + +test_expect_success '--max-count treats the last visible commit as the last commit' ' + lib_test_check_graph --max-count=2 _8 _9 _10 <<-\EOF + * 10_A + * 9_A + EOF +' + +test_expect_success '--max-count=1 shows a single root without indentation' ' + lib_test_check_graph --max-count=1 _8 _9 _10 <<-\EOF + * 10_A + EOF +' + +test_expect_success '--max-count-oldest indents visual roots' ' + lib_test_check_graph --max-count-oldest=3 _8 _9 _10 <<-\EOF + * 10_A + * 9_A + * 8_A + EOF +' + +# when the graph commits are filtered with regex options like --author, the +# commit parents do not come NULL so it is needed to check if the parents are +# interesting. +test_expect_success '--author skipped parent makes a visual root' ' + create_orphan _55 && + test_tick && + git commit --allow-empty -m 55_A && + create_orphan _54 && + test_tick && + git commit --allow-empty --author="Other " -m 54_A && + test_tick && + git commit --allow-empty -m 54_B && + test_tick && + git commit --allow-empty -m 54_C && + lib_test_check_graph --author="A U Thor" _54 _55 <<-\EOF + * 54_C + \ + * 54_B + * 55_A + EOF +' + +test_expect_success '--grep skipped parent makes a visual root' ' + create_orphan _57 && + test_tick && + git commit --allow-empty -m 57_keep_A && + create_orphan _56 && + test_tick && + git commit --allow-empty -m 56_skip && + test_tick && + git commit --allow-empty -m 56_keep_A && + test_tick && + git commit --allow-empty -m 56_keep_B && + lib_test_check_graph --grep=keep _56 _57 <<-\EOF + * 56_keep_B + \ + * 56_keep_A + * 57_keep_A + EOF +' + +test_done From 233cc403310efbb12be7ecba997f7e378c6cc390 Mon Sep 17 00:00:00 2001 From: Pablo Sabater Date: Tue, 14 Jul 2026 14:09:36 +0200 Subject: [PATCH 08/35] graph: wrap cascading commits after 4 columns Currently the visual root commits in a graph cascade indefinitely until a commit which is not a visual root or the last commit appears. On filters like --author where one author might contribute mostly on single patches this can become a visual issue. Make the cascading wrap after 4 columns. There are two possible cases of the wrap: 1. No ambiguity: * A * B * C * D * E * F 2. Ambiguous conflict: If F happens to not be a visual root and E gets wrapped back to the initial column then E and F would be vertically adjacent. The solution is to forcefully indent E one level: * A * B * C * D * E * F * F The magic number 4 comes as the minimum number of columns to wrap where the output shows clearly the commits are unrelated and doesn't cause too much "pyramid" effects Signed-off-by: Pablo Sabater Signed-off-by: Junio C Hamano --- graph.c | 22 +++++++++++++++++++++- t/t4218-log-graph-indentation.sh | 29 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/graph.c b/graph.c index 087094189f3467..e3e206170c8453 100644 --- a/graph.c +++ b/graph.c @@ -1042,6 +1042,23 @@ void graph_update(struct git_graph *graph, struct commit *commit) */ if (!graph->visual_root_depth && flags.is_next_visual_root) graph->visual_root_cascade = 1; + + /* + * We wrap the cascading at a max of four columns at most, after + * that we wrap it back to the initial column. + * + * This could cause ambiguity in case of the next commit not + * being a visual root and be at the initial column after the + * first wrap. + * + * In case of being a non-visual-root the next, stop the + * cascading to get the commit indented. + */ + if (!flags.is_next_visual_root && + graph->visual_root_depth && + !(graph->visual_root_depth % 4)) + graph->visual_root_cascade = 0; + graph->visual_root_depth++; } else { graph->visual_root_depth = 0; @@ -1328,8 +1345,11 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line * Each visual column is 2 characters wide. * Omit the indentation for the first visual * root in cascade mode. + * + * Have a max of 4 columns when cascading, after + * that wrap it and repeat. */ - int padding = (depth - graph->visual_root_cascade) * 2; + int padding = ((depth - graph->visual_root_cascade) % 4) * 2; graph_line_addchars(line, ' ', padding); graph->width += padding; } diff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh index 60c7d84af7c9d6..d4c850c0d46e9b 100755 --- a/t/t4218-log-graph-indentation.sh +++ b/t/t4218-log-graph-indentation.sh @@ -511,4 +511,33 @@ test_expect_success '--grep skipped parent makes a visual root' ' EOF ' +# The cascading wraps after 4 columns and when wraping (column % 4 == 0) if the +# next is a non visual-root, force indentation to avoid an ambiguous graph +# (commit 59_A is forcefully indented) +test_expect_success 'visual root cascading gets wrapped after 4 columns' ' + create_orphan _58 && test_commit 58_A && test_commit 58_B && + create_orphan _59 && test_commit 59_A && + create_orphan _60 && test_commit 60_A && + create_orphan _61 && test_commit 61_A && + create_orphan _62 && test_commit 62_A && + create_orphan _63 && test_commit 63_A && + create_orphan _64 && test_commit 64_A && + create_orphan _65 && test_commit 65_A && + create_orphan _66 && test_commit 66_A && + create_orphan _67 && test_commit 67_A && + lib_test_check_graph _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\EOF + * 67_A + * 66_A + * 65_A + * 64_A + * 63_A + * 62_A + * 61_A + * 60_A + * 59_A + * 58_B + * 58_A + EOF +' + test_done From f3a03302ada79d69cf97e601a0cd766330430ef7 Mon Sep 17 00:00:00 2001 From: Pablo Sabater Date: Tue, 14 Jul 2026 14:09:37 +0200 Subject: [PATCH 09/35] graph: move config reading into graph_read_config() Move the repo_config_get_string() call out of graph_init() and into graph_read_config(). This simplifies graph_init() and provides a function for future graph-related config opt. This commit is a preparatory commit for a subsequent one. Signed-off-by: Pablo Sabater Signed-off-by: Junio C Hamano --- graph.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/graph.c b/graph.c index e3e206170c8453..c14be934a05d4f 100644 --- a/graph.c +++ b/graph.c @@ -417,13 +417,11 @@ void graph_setup_line_prefix(struct diff_options *diffopt) diffopt->output_prefix = diff_output_prefix_callback; } -struct git_graph *graph_init(struct rev_info *opt) +static void graph_read_config(struct rev_info *revs) { - struct git_graph *graph = xmalloc(sizeof(struct git_graph)); - if (!column_colors) { char *string; - if (repo_config_get_string(opt->repo, "log.graphcolors", &string)) { + if (repo_config_get_string(revs->repo, "log.graphcolors", &string)) { /* not configured -- use default */ graph_set_column_colors(column_colors_ansi, column_colors_ansi_max); @@ -437,6 +435,13 @@ struct git_graph *graph_init(struct rev_info *opt) custom_colors.nr - 1); } } +} + +struct git_graph *graph_init(struct rev_info *opt) +{ + struct git_graph *graph = xmalloc(sizeof(struct git_graph)); + + graph_read_config(opt); graph->commit = NULL; graph->revs = opt; From e9b5720bef6c7e2821d086a8c67a6404debef905 Mon Sep 17 00:00:00 2001 From: Pablo Sabater Date: Tue, 14 Jul 2026 14:09:38 +0200 Subject: [PATCH 10/35] graph: add --[no-]graph-indent and log.graphIndent Some users may prefer to not have graph indentation. Add "log.graphIndent" config variable to graph_read_config() to read the default preference. By default is graph indentation is true. Add --graph-indent and --no-graph-indent options to overwrite the default preference. Signed-off-by: Pablo Sabater Signed-off-by: Junio C Hamano --- Documentation/config/log.adoc | 4 +++ Documentation/rev-list-options.adoc | 8 +++++ graph.c | 10 ++++-- revision.c | 9 +++++ revision.h | 2 ++ t/t4218-log-graph-indentation.sh | 53 +++++++++++++++++++++++++++++ 6 files changed, 84 insertions(+), 2 deletions(-) diff --git a/Documentation/config/log.adoc b/Documentation/config/log.adoc index 757a7be196ab38..f7dfce69b5f95e 100644 --- a/Documentation/config/log.adoc +++ b/Documentation/config/log.adoc @@ -59,6 +59,10 @@ This is the same as the `--decorate` option of the `git log`. A list of colors, separated by commas, that can be used to draw history lines in `git log --graph`. +`log.graphIndent`:: + If `true`, indent visual roots when rendering the graphs with `--graph`. + Set true by default. It can be overriden with `--[no-]graph-indent`. + `log.showRoot`:: If true, the initial commit will be shown as a big creation event. This is equivalent to a diff against an empty tree. diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc index eaee6ee8399c57..fd831f0ec64744 100644 --- a/Documentation/rev-list-options.adoc +++ b/Documentation/rev-list-options.adoc @@ -1269,6 +1269,14 @@ This implies the `--topo-order` option by default, but the By default it is set to 0 (no limit), zero and negative values are ignored and treated as no limit. +`--no-graph-indent`:: +`--graph-indent`:: + When used with `--graph`, indent visual roots (commits with no parents + or whose parents are not shown) to differentiate them from commits that + are vertically adjacent but unrelated. Enabled by default. Use + `--no-graph-indent` to disable or set `log.graphIndent` to set a + default preference. + ifdef::git-rev-list[] `--count`:: Print a number stating how many commits would have been diff --git a/graph.c b/graph.c index c14be934a05d4f..28bef1b88f5a7a 100644 --- a/graph.c +++ b/graph.c @@ -419,6 +419,8 @@ void graph_setup_line_prefix(struct diff_options *diffopt) static void graph_read_config(struct rev_info *revs) { + int val; + if (!column_colors) { char *string; if (repo_config_get_string(revs->repo, "log.graphcolors", &string)) { @@ -435,6 +437,9 @@ static void graph_read_config(struct rev_info *revs) custom_colors.nr - 1); } } + + if (!repo_config_get_bool(revs->repo, "log.graphIndent", &val)) + revs->no_graph_indent = !val; } struct git_graph *graph_init(struct rev_info *opt) @@ -999,7 +1004,8 @@ static void graph_peek_next_visible(struct git_graph *graph, static int graph_needs_pre_root_line(struct git_graph *graph) { return graph->commit_in_columns && graph->is_visual_root && - graph->num_columns > 0 && !graph->visual_root_cascade; + graph->num_columns > 0 && !graph->visual_root_cascade && + !graph->revs->no_graph_indent; } void graph_update(struct git_graph *graph, struct commit *commit) @@ -1344,7 +1350,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line if (col_commit == graph->commit) { seen_this = 1; - if (graph->is_visual_root) { + if (graph->is_visual_root && !graph->revs->no_graph_indent) { int depth = graph->visual_root_depth; /* * Each visual column is 2 characters wide. diff --git a/revision.c b/revision.c index 258c3cf782894b..37f7ea45d13133 100644 --- a/revision.c +++ b/revision.c @@ -2627,6 +2627,12 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg revs->graph = NULL; } else if (skip_prefix(arg, "--graph-lane-limit=", &optarg)) { revs->graph_max_lanes = parse_count(optarg); + } else if (!strcmp(arg, "--graph-indent")) { + revs->no_graph_indent = 0; + revs->graph_indent_set = 1; + } else if (!strcmp(arg, "--no-graph-indent")) { + revs->no_graph_indent = 1; + revs->graph_indent_set = 1; } else if (!strcmp(arg, "--encode-email-headers")) { revs->encode_email_headers = 1; } else if (!strcmp(arg, "--no-encode-email-headers")) { @@ -3201,6 +3207,9 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s if (revs->graph_max_lanes > 0 && !revs->graph) die(_("the option '%s' requires '%s'"), "--graph-lane-limit", "--graph"); + if (revs->graph_indent_set && !revs->graph) + die(_("the option '%s' requires '%s'"), "--[no-]graph-indent", "--graph"); + if (!revs->reflog_info && revs->grep_filter.use_reflog_filter) die(_("the option '%s' requires '%s'"), "--grep-reflog", "--walk-reflogs"); diff --git a/revision.h b/revision.h index 569b3fa1cb5a33..acf6d06b24126c 100644 --- a/revision.h +++ b/revision.h @@ -314,6 +314,8 @@ struct rev_info { /* Display history graph */ struct git_graph *graph; int graph_max_lanes; + unsigned int no_graph_indent:1; + unsigned int graph_indent_set:1; /* special limits */ int skip_count; diff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh index d4c850c0d46e9b..24dc9b497d9dbc 100755 --- a/t/t4218-log-graph-indentation.sh +++ b/t/t4218-log-graph-indentation.sh @@ -540,4 +540,57 @@ test_expect_success 'visual root cascading gets wrapped after 4 columns' ' EOF ' +test_expect_success '--no-graph-indent disables indentation' ' + lib_test_check_graph --no-graph-indent _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\EOF + * 67_A + * 66_A + * 65_A + * 64_A + * 63_A + * 62_A + * 61_A + * 60_A + * 59_A + * 58_B + * 58_A + EOF +' + +test_expect_success 'log.graphIndent config disables indentation' ' + test_config log.graphIndent false && + lib_test_check_graph _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\EOF + * 67_A + * 66_A + * 65_A + * 64_A + * 63_A + * 62_A + * 61_A + * 60_A + * 59_A + * 58_B + * 58_A + EOF +' + +test_expect_success '--graph-indent forces indentation when graph.indent is unset' ' + test_config log.graphIndent false && + lib_test_check_graph --graph-indent _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\EOF + * 67_A + * 66_A + * 65_A + * 64_A + * 63_A + * 62_A + * 61_A + * 60_A + * 59_A + * 58_B + * 58_A + EOF +' + +# log.graphIndent unset and no --option (which activates graph indentation) is +# the default state. + test_done From 88101d339a07c9ccc46ac37f2a5e922fae053a71 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 15 Jul 2026 16:21:55 +0100 Subject: [PATCH 11/35] t3400: restore coverage for note copying with apply backend Now that the merge backend is the default, we have lost coverage for "git rebase --apply" copying notes. Fix this by replacing "-m" with "--apply" as the previous test which uses the default backend now checks the merge backend. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- t/t3400-rebase.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh index c0c00fbb7b1e4e..f0e7fcf649aebc 100755 --- a/t/t3400-rebase.sh +++ b/t/t3400-rebase.sh @@ -270,9 +270,9 @@ test_expect_success 'rebase can copy notes' ' test "a note" = "$(git notes show HEAD)" ' -test_expect_success 'rebase -m can copy notes' ' +test_expect_success 'rebase --apply can copy notes' ' git reset --hard n3 && - git rebase -m --onto n1 n2 && + git rebase --apply --onto n1 n2 && test "a note" = "$(git notes show HEAD)" ' From 1da85922fddd6f693739385fa08c0d15ad066247 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 15 Jul 2026 16:21:56 +0100 Subject: [PATCH 12/35] sequencer: be more careful with external merge If an external merge strategy cannot merge (for example because it would overwrite an untracked file) it exits with a non-zero exit code other than 1. This should be treated differently from a merge with conflicts, which is signaled by an exit code of 1, because, as the merge failed, we need to reschedule the last pick. The caller expects us to return -1 in this case. Also reschedule without trying to merge if the commit message cannot be written as that prevents us from successfully picking the commit. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- sequencer.c | 19 +++++++++++++++---- t/t3404-rebase-interactive.sh | 11 +++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/sequencer.c b/sequencer.c index 57855b0066ac98..eaffa8ebb844ef 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2453,14 +2453,25 @@ static int do_pick_commit(struct repository *r, struct commit_list *common = NULL; struct commit_list *remotes = NULL; - res = write_message(ctx->message.buf, ctx->message.len, - git_path_merge_msg(r), 0); + if (write_message(ctx->message.buf, ctx->message.len, + git_path_merge_msg(r), 0)) { + res = -1; + goto leave; + } commit_list_insert(base, &common); commit_list_insert(next, &remotes); - res |= try_merge_command(r, opts->strategy, - opts->xopts.nr, opts->xopts.v, + res = try_merge_command(r, opts->strategy, + opts->xopts.nr, opts->xopts.v, common, oid_to_hex(&head), remotes); + /* + * If there were conflicts, try_merge_command() returns 1, + * any other no-zero return code means that either the merge + * command could not be run, or it failed to merge. + */ + if (res && res != 1) + res = -1; + commit_list_free(common); commit_list_free(remotes); } diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 58b3bb0c271aae..297b84e60d59cc 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1251,6 +1251,17 @@ test_expect_success 'interrupted rebase -i with --strategy and -X' ' test $(cat file1) = Z ' +test_expect_success 'failing pick with --strategy is rescheduled' ' + test_when_finished "rm -rf bin; test_might_fail git rebase --abort" && + mkdir bin && + echo exit 2 | write_script bin/git-merge-fail && + git log -1 --format="pick %H # %s" HEAD >expect && + test_must_fail env PATH="$PWD/bin:$PATH" \ + git rebase --no-ff --strategy fail HEAD^ && + test_cmp expect .git/rebase-merge/git-rebase-todo && + test_cmp expect .git/rebase-merge/done +' + test_expect_success 'rebase -i error on commits with \ in message' ' current_head=$(git rev-parse HEAD) && test_when_finished "git rebase --abort; git reset --hard $current_head; rm -f error" && From 18f5750beb14c13f1a140361b6a0459764ec0364 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 15 Jul 2026 16:21:57 +0100 Subject: [PATCH 13/35] sequencer: never reschedule on failed commit If "git commit" fails to run then run_git_commit() returns -1 which causes the current command to be rescheduled. This is incorrect as we have successfully picked the commit and have written all the state files we need to successfully commit when the user continues. Fix this by converting -1 to 1 which matches what do_merge() does. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- sequencer.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sequencer.c b/sequencer.c index eaffa8ebb844ef..1db844100ad0df 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2542,6 +2542,12 @@ static int do_pick_commit(struct repository *r, res = run_git_commit(NULL, reflog_action, opts, flags); *check_todo = 1; } + /* + * If "git commit" failed to run then res == -1, but we don't + * want reschedule the last command because the picking the + * commit was successful. + */ + res = !!res; } From 6bb740dffb908a670e52b88f309f521f6d29a912 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 15 Jul 2026 16:21:58 +0100 Subject: [PATCH 14/35] sequencer: remove unnecessary "or" in pick_one_commit() If error_with_patch(..., res, ...) succeeds then it returns "res", if it fails then it returns -1. This means that or-ing the return value with "res" is pointless as the result is the same as the return value. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- sequencer.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sequencer.c b/sequencer.c index 1db844100ad0df..70e12eab0ecf31 100644 --- a/sequencer.c +++ b/sequencer.c @@ -5007,9 +5007,8 @@ static int pick_one_commit(struct repository *r, oideq(&opts->squash_onto, &oid)))) to_amend = 1; - return res | error_with_patch(r, item->commit, - arg, item->arg_len, opts, - res, to_amend); + return error_with_patch(r, item->commit, arg, item->arg_len, + opts, res, to_amend); } return res; } From dc0e2ac15dc5c3770b2117535a0b316e3bf6aa7a Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 15 Jul 2026 16:21:59 +0100 Subject: [PATCH 15/35] sequencer: simplify handling of fixup with conflicts Commit e032abd5a0 (rebase: fix rewritten list for failed pick, 2023-09-06) introduced an early return when res == -1, so if we enter this conditional block then res is positive. After the last couple of commits the only possible positive value is 1. That means we can simplify the code by removing the conditional call to intend_to_amend() and have error_failed_squash() request that it is called in error_with_patch() instead. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- sequencer.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sequencer.c b/sequencer.c index 70e12eab0ecf31..a00e3622c87d0d 100644 --- a/sequencer.c +++ b/sequencer.c @@ -3874,7 +3874,7 @@ static int error_failed_squash(struct repository *r, return error(_("could not copy '%s' to '%s'"), rebase_path_message(), git_path_merge_msg(r)); - return error_with_patch(r, commit, subject, subject_len, opts, 1, 0); + return error_with_patch(r, commit, subject, subject_len, opts, 1, 1); } static int do_exec(struct repository *r, const char *command_line, int quiet) @@ -4986,8 +4986,6 @@ static int pick_one_commit(struct repository *r, record_in_rewritten(&item->commit->object.oid, peek_command(todo_list, 1)); if (res && is_fixup(item->command)) { - if (res == 1) - intend_to_amend(); return error_failed_squash(r, item->commit, opts, item->arg_len, arg); } else if (res && is_rebase_i(opts) && item->commit) { From 871f9009d3fca0ac70d4df7f75c82b1e31213add Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 15 Jul 2026 16:22:00 +0100 Subject: [PATCH 16/35] sequencer: remove unnecessary condition in pick_one_commit() item->commit holds the commit to be picked and so it must be non-NULL otherwise pick_one_commit() would not know which commit to pick. It is also unconditionally dereferenced in do_pick_commit() which is called at the top of this function. Therefore the check to see if it is non-NULL is superfluous. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- sequencer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequencer.c b/sequencer.c index a00e3622c87d0d..8f3eed205e7c7e 100644 --- a/sequencer.c +++ b/sequencer.c @@ -4988,7 +4988,7 @@ static int pick_one_commit(struct repository *r, if (res && is_fixup(item->command)) { return error_failed_squash(r, item->commit, opts, item->arg_len, arg); - } else if (res && is_rebase_i(opts) && item->commit) { + } else if (res && is_rebase_i(opts)) { int to_amend = 0; struct object_id oid; From 034deee6c7630fad36ce2db5b9d1ab4afd78a09e Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 15 Jul 2026 16:22:01 +0100 Subject: [PATCH 17/35] sequencer: simplify pick_one_commit() Unless we're rebasing, all we do in pick_one_commit() is call do_pick_commit() and return its result. Simplify the code by returning early if we're not rebasing so that we don't have to repeatedly call is_rebase_i() in the rest of the function. Note that there are a couple of conditions that do not call is_rebase_i() but they check for either an "edit" or a "fixup" command, both of which imply we're rebasing. The only block that does not return early is the one guarded by "!res". Move the return into that block to make it clear that after recording the commit as rewritten, all we do is return from the function. As the conditional blocks are all mutually exclusive (either the conditions are mutually exclusive, or an earlier conditional block that would match a later one contains a "return" statement) chain them together with "else if" to make that clear. While we could remove "res" from the conditions below "if (!res)" they are left alone because, when we start using an enum in the next commit, it makes it clear that these clauses are handling cases where there are conflicts. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- sequencer.c | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/sequencer.c b/sequencer.c index 8f3eed205e7c7e..9016af9b5d7834 100644 --- a/sequencer.c +++ b/sequencer.c @@ -4966,12 +4966,14 @@ static int pick_one_commit(struct repository *r, res = do_pick_commit(r, item, opts, is_final_fixup(todo_list), check_todo); - if (is_rebase_i(opts) && res < 0) { + if (!is_rebase_i(opts)) + return res; + + if (res < 0) { /* Reschedule */ *reschedule = 1; return -1; - } - if (item->command == TODO_EDIT) { + } else if (item->command == TODO_EDIT) { struct commit *commit = item->commit; if (!res) { if (!opts->verbose) @@ -4981,14 +4983,14 @@ static int pick_one_commit(struct repository *r, } return error_with_patch(r, commit, arg, item->arg_len, opts, res, !res); - } - if (is_rebase_i(opts) && !res) + } else if (!res) { record_in_rewritten(&item->commit->object.oid, peek_command(todo_list, 1)); - if (res && is_fixup(item->command)) { + return 0; + } else if (res && is_fixup(item->command)) { return error_failed_squash(r, item->commit, opts, item->arg_len, arg); - } else if (res && is_rebase_i(opts)) { + } else if (res) { int to_amend = 0; struct object_id oid; @@ -5008,7 +5010,8 @@ static int pick_one_commit(struct repository *r, return error_with_patch(r, item->commit, arg, item->arg_len, opts, res, to_amend); } - return res; + + BUG("Unhandled return value from do_pick_commit()"); } static int pick_commits(struct repository *r, From a7dbb3a462eab29d5ca79c9cba175e0baeff6751 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 15 Jul 2026 16:22:02 +0100 Subject: [PATCH 18/35] sequencer: use an enum to represent result of picking a commit Rather than using an integer where -1 is an error, 0 is success and 1 indicates there were conflicts, use an enum. This is clearer and lets us add a separate return value for commits that are dropped because they become empty in the next commit. Note we continue to use "return error(...)" to return errors and take advantage of C's lax typing of enums Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- sequencer.c | 61 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/sequencer.c b/sequencer.c index 9016af9b5d7834..4b3092dc9bb706 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2260,10 +2260,16 @@ static const char *reflog_message(struct replay_opts *opts, return buf.buf; } -static int do_pick_commit(struct repository *r, - struct todo_item *item, - struct replay_opts *opts, - int final_fixup, int *check_todo) +enum pick_result { + PICK_RESULT_ERROR = -1, + PICK_RESULT_OK, + PICK_RESULT_CONFLICTS, +}; + +static enum pick_result do_pick_commit(struct repository *r, + struct todo_item *item, + struct replay_opts *opts, + int final_fixup, int *check_todo) { struct replay_ctx *ctx = opts->ctx; unsigned int flags = should_edit(opts) ? EDIT_MSG : 0; @@ -2564,7 +2570,12 @@ static int do_pick_commit(struct repository *r, free(author); update_abort_safety_file(); - return res; + if (res < 0) + return PICK_RESULT_ERROR; + else if (res > 0) + return PICK_RESULT_CONFLICTS; + else + return PICK_RESULT_OK; } static int prepare_revs(struct replay_opts *opts) @@ -4960,22 +4971,31 @@ static int pick_one_commit(struct repository *r, struct replay_opts *opts, int *check_todo, int* reschedule) { - int res; + enum pick_result pick_res; struct todo_item *item = todo_list->items + todo_list->current; const char *arg = todo_item_get_arg(todo_list, item); - res = do_pick_commit(r, item, opts, is_final_fixup(todo_list), - check_todo); + pick_res = do_pick_commit(r, item, opts, is_final_fixup(todo_list), + check_todo); if (!is_rebase_i(opts)) - return res; + switch (pick_res) { + case PICK_RESULT_ERROR: + return -1; + case PICK_RESULT_CONFLICTS: + return 1; + default: + return 0; + } - if (res < 0) { + if (pick_res == PICK_RESULT_ERROR) { /* Reschedule */ *reschedule = 1; return -1; } else if (item->command == TODO_EDIT) { struct commit *commit = item->commit; - if (!res) { + int res = pick_res == PICK_RESULT_CONFLICTS; + + if (pick_res == PICK_RESULT_OK) { if (!opts->verbose) term_clear_line(); fprintf(stderr, _("Stopped at %s... %.*s\n"), @@ -4983,14 +5003,15 @@ static int pick_one_commit(struct repository *r, } return error_with_patch(r, commit, arg, item->arg_len, opts, res, !res); - } else if (!res) { + } else if (pick_res == PICK_RESULT_OK) { record_in_rewritten(&item->commit->object.oid, peek_command(todo_list, 1)); return 0; - } else if (res && is_fixup(item->command)) { + } else if (pick_res == PICK_RESULT_CONFLICTS && + is_fixup(item->command)) { return error_failed_squash(r, item->commit, opts, item->arg_len, arg); - } else if (res) { + } else if (pick_res == PICK_RESULT_CONFLICTS) { int to_amend = 0; struct object_id oid; @@ -5008,7 +5029,7 @@ static int pick_one_commit(struct repository *r, to_amend = 1; return error_with_patch(r, item->commit, arg, item->arg_len, - opts, res, to_amend); + opts, 1, to_amend); } BUG("Unhandled return value from do_pick_commit()"); @@ -5547,7 +5568,15 @@ static int single_pick(struct repository *r, TODO_PICK : TODO_REVERT; item.commit = cmit; - return do_pick_commit(r, &item, opts, 0, &check_todo); + switch (do_pick_commit(r, &item, opts, 0, &check_todo)) { + case PICK_RESULT_ERROR: + return -1; + case PICK_RESULT_CONFLICTS: + return 1; + default: + return 0; + } + } int sequencer_pick_revisions(struct repository *r, From 42554b78fd2c3ce252647c9c5afbf04d2f2885f5 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 15 Jul 2026 16:22:03 +0100 Subject: [PATCH 19/35] sequencer: do not record dropped commits as rewritten MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a commit gets dropped because its changes are already upstream then we should not record it as rewritten. As well as confusing any post-rewrite hooks, it means we end up copying the notes from the dropped commit to the commit that was picked immediately before the one that was dropped. While we do not want to record the dropped commit as rewritten, if it is the final commit in a chain of fixups then we need to flush the list of rewritten commits. The behavior of an "edit" command where the commit is dropped is changed so that "rebase --continue" will not amend the previous pick. However, as the code comment notes it will still be erroneously recorded as rewritten when the rebase continues. That will need to be addressed separately along with not recording skipped commits as rewritten. The initialization of "drop_commit" is moved to ensure it is initialized when rewording a fast-forwarded commit. Reported-by: Uwe Kleine-König Tested-by: Uwe Kleine-König Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- sequencer.c | 24 +++++++++++++++++++----- t/t3400-rebase.sh | 12 ++++++++++++ t/t5407-post-rewrite-hook.sh | 23 +++++++++++++++++++++++ 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/sequencer.c b/sequencer.c index 4b3092dc9bb706..7a5898b215d935 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2264,6 +2264,7 @@ enum pick_result { PICK_RESULT_ERROR = -1, PICK_RESULT_OK, PICK_RESULT_CONFLICTS, + PICK_RESULT_DROPPED, }; static enum pick_result do_pick_commit(struct repository *r, @@ -2279,7 +2280,7 @@ static enum pick_result do_pick_commit(struct repository *r, const char *base_label, *next_label, *reflog_action; char *author = NULL; struct commit_message msg = { NULL, NULL, NULL, NULL }; - int res, unborn = 0, reword = 0, allow, drop_commit; + int res, unborn = 0, reword = 0, allow, drop_commit = 0; enum todo_command command = item->command; struct commit *commit = item->commit; @@ -2509,7 +2510,6 @@ static enum pick_result do_pick_commit(struct repository *r, goto leave; } - drop_commit = 0; allow = allow_empty(r, opts, commit); if (allow < 0) { res = allow; @@ -2574,6 +2574,8 @@ static enum pick_result do_pick_commit(struct repository *r, return PICK_RESULT_ERROR; else if (res > 0) return PICK_RESULT_CONFLICTS; + else if (drop_commit) + return PICK_RESULT_DROPPED; else return PICK_RESULT_OK; } @@ -4994,19 +4996,31 @@ static int pick_one_commit(struct repository *r, } else if (item->command == TODO_EDIT) { struct commit *commit = item->commit; int res = pick_res == PICK_RESULT_CONFLICTS; + int to_amend = pick_res != PICK_RESULT_CONFLICTS && + pick_res != PICK_RESULT_DROPPED; - if (pick_res == PICK_RESULT_OK) { + /* + * NEEDSWORK: Do not record the commit as rewritten when + * continuing if it was dropped. Does it even make sense + * to stop if the commit was dropped? + */ + if (pick_res == PICK_RESULT_OK || + pick_res == PICK_RESULT_DROPPED) { if (!opts->verbose) term_clear_line(); fprintf(stderr, _("Stopped at %s... %.*s\n"), short_commit_name(r, commit), item->arg_len, arg); } - return error_with_patch(r, commit, - arg, item->arg_len, opts, res, !res); + return error_with_patch(r, commit, arg, item->arg_len, opts, + res, to_amend); } else if (pick_res == PICK_RESULT_OK) { record_in_rewritten(&item->commit->object.oid, peek_command(todo_list, 1)); return 0; + } else if (pick_res == PICK_RESULT_DROPPED) { + if (is_final_fixup(todo_list)) + flush_rewritten_pending(); + return 0; } else if (pick_res == PICK_RESULT_CONFLICTS && is_fixup(item->command)) { return error_failed_squash(r, item->commit, opts, diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh index f0e7fcf649aebc..1d09886ea35141 100755 --- a/t/t3400-rebase.sh +++ b/t/t3400-rebase.sh @@ -276,6 +276,18 @@ test_expect_success 'rebase --apply can copy notes' ' test "a note" = "$(git notes show HEAD)" ' +test_expect_success 'rebase drops notes of dropped commits' ' + git checkout n1 && + echo n3 >n3.t && + echo n4 >n4.t && + git add n3.t n4.t && + git commit -m n34 && + git rebase HEAD n3 && + test_commit_message HEAD -m n2 && + test_must_fail git notes list HEAD >actual && + test_must_be_empty actual +' + test_expect_success 'rebase commit with an ancient timestamp' ' git reset --hard && diff --git a/t/t5407-post-rewrite-hook.sh b/t/t5407-post-rewrite-hook.sh index ad7f8c6f00202c..51991956d1d28d 100755 --- a/t/t5407-post-rewrite-hook.sh +++ b/t/t5407-post-rewrite-hook.sh @@ -310,4 +310,27 @@ test_expect_success 'git rebase -i (exec)' ' verify_hook_input ' +test_expect_success 'rebase with commits that become empty' ' + cat >todo <<-\EOF && + pick H + pick E + fixup I + fixup H + pick G + pick I + EOF + ( + set_replace_editor todo && + git rebase -i --empty=drop A A + ) && + echo rebase >expected.args && + cat >expected.data <<-EOF && + $(git rev-parse H) $(git rev-parse HEAD~2) + $(git rev-parse E) $(git rev-parse HEAD~1) + $(git rev-parse I) $(git rev-parse HEAD~1) + $(git rev-parse G) $(git rev-parse HEAD) + EOF + verify_hook_input +' + test_done From 3279c13c00f0d9c4ba7d2b67c73c36ec308e5366 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 15 Jul 2026 13:10:06 -0700 Subject: [PATCH 20/35] submodule--helper: avoid use of %zu for now Since d7d850e2b9 (CodingGuidelines: mention C99 features we can't use, 2022-10-10), our CodingGuidelines document has explicitly forbidden the use of '%z' and '%zu' printf() format specifiers, even though C99 does support them. However, a new instance crept in via 82c36fa0a9 (submodule: hash the submodule name for the gitdir path, 2026-01-12). We could claim that this is an unintentional weather balloon that nobody has complained about for the past six months since Git 2.54, proving that it is now safe to use these format specifiers. But (1) it is probably too early to make that claim, as distributions often stick to a stale version for several releases, and (2) it is unlikely that a failure in this code path would manifest as a major user-visible breakage that would trigger a failure report to percolate down to us. Instead, let's stick to the established workaround recommended by our CodingGuidelines, which is to cast the value to (uintmax_t) and format it with PRIuMAX, at least for now. Even if we eventually perform a bulk update using a Coccinelle script to transition to %z and %zu in the future, adding one more instance to the pile that will need such a conversion is hardly a tragedy. Signed-off-by: Junio C Hamano --- builtin/submodule--helper.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index 2f589e3b378d3f..49463b7d2b568d 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -549,7 +549,8 @@ static void create_default_gitdir_config(const char *submodule_name) } /* Case 2.4: If all the above failed, try a hash of the name as a last resort */ - header_len = snprintf(header, sizeof(header), "blob %zu", strlen(submodule_name)); + header_len = snprintf(header, sizeof(header), + "blob %"PRIuMAX, (uintmax_t)strlen(submodule_name)); the_hash_algo->init_fn(&ctx); the_hash_algo->update_fn(&ctx, header, header_len); the_hash_algo->update_fn(&ctx, "\0", 1); From f749a83291681bd19ab8712e2a4e1c931bb8b241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Wed, 15 Jul 2026 06:41:17 +0200 Subject: [PATCH 21/35] remote-curl: simplify passing of push specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The push specs are kept in a strvec, whose array is NULL-terminated. Pass only that to the protocol handlers, which avoids dealing with item counts and their conversions from size_t to int, slightly simplifying the code. Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- remote-curl.c | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/remote-curl.c b/remote-curl.c index 9e614c5567417f..2c35dd52400f83 100644 --- a/remote-curl.c +++ b/remote-curl.c @@ -1340,10 +1340,9 @@ static void parse_get(const char *arg) fflush(stdout); } -static int push_dav(int nr_spec, const char **specs) +static int push_dav(const char **specs) { struct child_process child = CHILD_PROCESS_INIT; - size_t i; child.git_cmd = 1; strvec_push(&child.args, "http-push"); @@ -1353,15 +1352,14 @@ static int push_dav(int nr_spec, const char **specs) if (options.verbosity > 1) strvec_push(&child.args, "--verbose"); strvec_push(&child.args, url.buf); - for (i = 0; i < nr_spec; i++) - strvec_push(&child.args, specs[i]); + strvec_pushv(&child.args, specs); if (run_command(&child)) die(_("git-http-push failed")); return 0; } -static int push_git(struct discovery *heads, int nr_spec, const char **specs) +static int push_git(struct discovery *heads, const char **specs) { struct rpc_state rpc = RPC_STATE_INIT; int i, err; @@ -1400,8 +1398,8 @@ static int push_git(struct discovery *heads, int nr_spec, const char **specs) strvec_push(&args, "--force-if-includes"); strvec_push(&args, "--stdin"); - for (i = 0; i < nr_spec; i++) - packet_buf_write(&preamble, "%s\n", specs[i]); + for (; *specs; specs++) + packet_buf_write(&preamble, "%s\n", *specs); packet_buf_flush(&preamble); memset(&rpc, 0, sizeof(rpc)); @@ -1416,15 +1414,15 @@ static int push_git(struct discovery *heads, int nr_spec, const char **specs) return err; } -static int push(int nr_spec, const char **specs) +static int push(const char **specs) { struct discovery *heads = discover_refs("git-receive-pack", 1); int ret; if (heads->proto_git) - ret = push_git(heads, nr_spec, specs); + ret = push_git(heads, specs); else - ret = push_dav(nr_spec, specs); + ret = push_dav(specs); free_discovery(heads); return ret; } @@ -1448,7 +1446,7 @@ static void parse_push(struct strbuf *buf) break; } while (1); - ret = push(specs.nr, specs.v); + ret = push(specs.v); printf("\n"); fflush(stdout); From 5abbd7c3a2e0b484e23377ae405af2b282286274 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 16 Jul 2026 07:33:02 +0200 Subject: [PATCH 22/35] refs/packed: de-globalize handling of "core.packedRefsTimeout" When locking the "packed-refs" file we allow the user to configure a timeout for how long we try taking the lock. This is configurable via "core.packedRefsTimeout", which we parse in `packed_refs_lock()`. The parsed value is stored in function-static variables though, which of course has the effect that we'll only ever use the timeout configured in the first packed reference store that we see. Consequently, if we ever were to handle stores from different repositories, then we'd use the same configuration for both stores even if they diverge. This is of course a somewhat theoretical concern -- we don't typically handle multiple packed stores, and even if we did it's very unlikely that the user has configured different timeout values for each of them. But still, this is a code smell, and an unnecessary one, too. Fix the issue by moving the value into `struct packed_ref_store` so that it can be parsed per store. This removes the last callsite that still used `the_repository`, so drop the `USE_THE_REPOSITORY_VARIABLE` define. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- refs/packed-backend.c | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/refs/packed-backend.c b/refs/packed-backend.c index 499cb55dface4f..c5d96793fa5841 100644 --- a/refs/packed-backend.c +++ b/refs/packed-backend.c @@ -1,4 +1,3 @@ -#define USE_THE_REPOSITORY_VARIABLE #define DISABLE_SIGN_COMPARE_WARNINGS #include "../git-compat-util.h" @@ -162,6 +161,13 @@ struct packed_ref_store { * `packed_ref_store`) must not be freed. */ struct tempfile *tempfile; + + /* + * Timeout when taking the "packed-refs.lock" file. configurable via + * "core.packedRefsTimeout". + */ + bool timeout_configured; + int timeout_value; }; /* @@ -1233,12 +1239,12 @@ int packed_refs_lock(struct ref_store *ref_store, int flags, struct strbuf *err) struct packed_ref_store *refs = packed_downcast(ref_store, REF_STORE_WRITE | REF_STORE_MAIN, "packed_refs_lock"); - static int timeout_configured = 0; - static int timeout_value = 1000; - if (!timeout_configured) { - repo_config_get_int(the_repository, "core.packedrefstimeout", &timeout_value); - timeout_configured = 1; + if (!refs->timeout_configured) { + if (repo_config_get_int(ref_store->repo, "core.packedrefstimeout", + &refs->timeout_value)) + refs->timeout_value = 1000; + refs->timeout_configured = true; } /* @@ -1249,7 +1255,7 @@ int packed_refs_lock(struct ref_store *ref_store, int flags, struct strbuf *err) if (hold_lock_file_for_update_timeout( &refs->lock, refs->path, - flags, timeout_value) < 0) { + flags, refs->timeout_value) < 0) { unable_to_lock_message(refs->path, errno, err); return -1; } From af57f7361d51ad76bead03829c6b1618a0c8adbb Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 16 Jul 2026 07:33:03 +0200 Subject: [PATCH 23/35] refs/files: drop `USE_THE_REPOSITORY_VARIABLE` We have a bunch of users of `the_repository` in the "files" backend, all of which are trivial to convert to instead use the backend's own repo. Do so. There is one more dependency on global state though via `ignore_case`, and thus we can't trivially remove `USE_THE_REPOSITORY_VARIABLE`. But this is the only use of global state, and we want to ensure that we don't unwittingly reintroduce a dependency on `the_repository` going forward. Add an extern declaration for `ignore_case` so that it becomes accessible even without `USE_THE_REPOSITORY_VARIABLE` and drop the define itself. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- refs/files-backend.c | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/refs/files-backend.c b/refs/files-backend.c index 3df56c25c8c585..09e1be838a98a6 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -1,4 +1,3 @@ -#define USE_THE_REPOSITORY_VARIABLE #define DISABLE_SIGN_COMPARE_WARNINGS #include "../git-compat-util.h" @@ -29,6 +28,9 @@ #include "../revision.h" #include +/* So that we can drop `USE_THE_REPOSITORY_VARIABLE`. */ +extern int ignore_case; + /* * This backend uses the following flags in `ref_update::flags` for * internal bookkeeping purposes. Their numerical values must not @@ -788,7 +790,7 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs, files_ref_path(refs, &ref_file, refname); retry: - switch (safe_create_leading_directories(the_repository, ref_file.buf)) { + switch (safe_create_leading_directories(refs->base.repo, ref_file.buf)) { case SCLD_OK: break; /* success */ case SCLD_EXISTS: @@ -1164,7 +1166,8 @@ typedef int create_file_fn(const char *path, void *cb); * recent call of fn. fn is always called at least once, and will be * called more than once if it returns ENOENT or EISDIR. */ -static int raceproof_create_file(const char *path, create_file_fn fn, void *cb) +static int raceproof_create_file(struct files_ref_store *refs, + const char *path, create_file_fn fn, void *cb) { /* * The number of times we will try to remove empty directories @@ -1220,7 +1223,7 @@ static int raceproof_create_file(const char *path, create_file_fn fn, void *cb) strbuf_addstr(&path_copy, path); do { - scld_result = safe_create_leading_directories(the_repository, path_copy.buf); + scld_result = safe_create_leading_directories(refs->base.repo, path_copy.buf); if (scld_result == SCLD_OK) goto retry_fn; } while (scld_result == SCLD_VANISHED && create_directories_remaining-- > 0); @@ -1289,7 +1292,7 @@ static struct ref_lock *lock_ref_oid_basic(struct files_ref_store *refs, cb_data.lk = &lock->lk; cb_data.repo = refs->base.repo; - if (raceproof_create_file(ref_file.buf, create_reflock, &cb_data)) { + if (raceproof_create_file(refs, ref_file.buf, create_reflock, &cb_data)) { unable_to_lock_message(ref_file.buf, errno, err); goto error_return; } @@ -1383,7 +1386,7 @@ static void prune_ref(struct files_ref_store *refs, struct ref_to_prune *r) ref_transaction_add_update( transaction, r->name, REF_NO_DEREF | REF_HAVE_NEW | REF_HAVE_OLD | REF_IS_PRUNING, - null_oid(the_hash_algo), &r->oid, NULL, NULL, NULL, + null_oid(refs->base.repo->hash_algo), &r->oid, NULL, NULL, NULL, NULL, NULL); if (ref_transaction_commit(transaction, &err)) goto cleanup; @@ -1629,7 +1632,7 @@ static int rename_tmp_log(struct files_ref_store *refs, const char *newrefname) files_reflog_path(refs, &path, newrefname); files_reflog_path(refs, &tmp, TMP_RENAMED_LOG); cb.tmp_renamed_log = tmp.buf; - ret = raceproof_create_file(path.buf, rename_tmp_log_callback, &cb); + ret = raceproof_create_file(refs, path.buf, rename_tmp_log_callback, &cb); if (ret) { if (errno == EISDIR) error("directory not empty: %s", path.buf); @@ -1916,13 +1919,13 @@ static int log_ref_setup(struct files_ref_store *refs, char *logfile; if (log_refs_cfg == LOG_REFS_UNSET) - log_refs_cfg = is_bare_repository(the_repository) ? LOG_REFS_NONE : LOG_REFS_NORMAL; + log_refs_cfg = is_bare_repository(refs->base.repo) ? LOG_REFS_NONE : LOG_REFS_NORMAL; files_reflog_path(refs, &logfile_sb, refname); logfile = strbuf_detach(&logfile_sb, NULL); if (force_create || should_autocreate_reflog(log_refs_cfg, refname)) { - if (raceproof_create_file(logfile, open_or_create_logfile, logfd)) { + if (raceproof_create_file(refs, logfile, open_or_create_logfile, logfd)) { if (errno == ENOENT) strbuf_addf(err, "unable to create directory for '%s': " "%s", logfile, strerror(errno)); @@ -1955,7 +1958,7 @@ static int log_ref_setup(struct files_ref_store *refs, } if (*logfd >= 0) - adjust_shared_perm(the_repository, logfile); + adjust_shared_perm(refs->base.repo, logfile); free(logfile); return 0; @@ -3672,8 +3675,8 @@ static int files_ref_store_create_on_disk(struct ref_store *ref_store, * they do not understand the reference format extension. */ strbuf_addf(&sb, "%s/refs", ref_store->gitdir); - safe_create_dir(the_repository, sb.buf, 1); - adjust_shared_perm(the_repository, sb.buf); + safe_create_dir(refs->base.repo, sb.buf, 1); + adjust_shared_perm(refs->base.repo, sb.buf); /* * There is no need to create directories for common refs when creating @@ -3685,11 +3688,11 @@ static int files_ref_store_create_on_disk(struct ref_store *ref_store, */ strbuf_reset(&sb); files_ref_path(refs, &sb, "refs/heads"); - safe_create_dir(the_repository, sb.buf, 1); + safe_create_dir(refs->base.repo, sb.buf, 1); strbuf_reset(&sb); files_ref_path(refs, &sb, "refs/tags"); - safe_create_dir(the_repository, sb.buf, 1); + safe_create_dir(refs->base.repo, sb.buf, 1); } strbuf_release(&sb); From 28723dad6465d98d15208d2c7692a3b873b03e3b Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 16 Jul 2026 07:33:04 +0200 Subject: [PATCH 24/35] worktree: refactor code to use available repositories In "worktree.c" we have lots of users of `the_repository` that already have a repository available to them. Convert all of them to use that repository instead. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- worktree.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/worktree.c b/worktree.c index 30125827fd39ed..8b10dea1797d51 100644 --- a/worktree.c +++ b/worktree.c @@ -392,7 +392,7 @@ int validate_worktree(const struct worktree *wt, struct strbuf *errmsg, if (!is_absolute_path(wt->path)) { strbuf_addf_gently(errmsg, _("'%s' file does not contain absolute path to the working tree location"), - repo_common_path_replace(the_repository, &buf, "worktrees/%s/gitdir", wt->id)); + repo_common_path_replace(wt->repo, &buf, "worktrees/%s/gitdir", wt->id)); goto done; } @@ -414,12 +414,12 @@ int validate_worktree(const struct worktree *wt, struct strbuf *errmsg, goto done; } - strbuf_realpath(&realpath, repo_common_path_replace(the_repository, &buf, "worktrees/%s", wt->id), 1); + strbuf_realpath(&realpath, repo_common_path_replace(wt->repo, &buf, "worktrees/%s", wt->id), 1); ret = fspathcmp(path, realpath.buf); if (ret) strbuf_addf_gently(errmsg, _("'%s' does not point back to '%s'"), - wt->path, repo_common_path_replace(the_repository, &buf, + wt->path, repo_common_path_replace(wt->repo, &buf, "worktrees/%s", wt->id)); done: free(path); @@ -440,7 +440,7 @@ void update_worktree_location(struct worktree *wt, const char *path_, if (is_main_worktree(wt)) BUG("can't relocate main worktree"); - wt_gitdir = repo_common_path(the_repository, "worktrees/%s/gitdir", wt->id); + wt_gitdir = repo_common_path(wt->repo, "worktrees/%s/gitdir", wt->id); strbuf_realpath(&gitdir, wt_gitdir, 1); strbuf_realpath(&path, path_, 1); strbuf_addf(&dotgit, "%s/.git", path.buf); @@ -658,7 +658,7 @@ static void repair_gitfile(struct worktree *wt, goto done; } - path = repo_common_path(the_repository, "worktrees/%s", wt->id); + path = repo_common_path(wt->repo, "worktrees/%s", wt->id); strbuf_realpath(&repo, path, 1); strbuf_addf(&dotgit, "%s/.git", wt->path); strbuf_addf(&gitdir, "%s/gitdir", repo.buf); @@ -727,7 +727,7 @@ void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path if (is_main_worktree(wt)) goto done; - path = repo_common_path(the_repository, "worktrees/%s/gitdir", wt->id); + path = repo_common_path(wt->repo, "worktrees/%s/gitdir", wt->id); strbuf_realpath(&gitdir, path, 1); if (strbuf_read_file(&dotgit, gitdir.buf, 0) < 0) @@ -1042,7 +1042,7 @@ int init_worktree_config(struct repository *r) */ if (r->repository_format_worktree_config) return 0; - if ((res = repo_config_set_gently(the_repository, "extensions.worktreeConfig", "true"))) + if ((res = repo_config_set_gently(r, "extensions.worktreeConfig", "true"))) return error(_("failed to set extensions.worktreeConfig setting")); common_config_file = xstrfmt("%s/config", r->commondir); From 2cec1c92e63dec350ab044170a08d610381349b1 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 16 Jul 2026 07:33:05 +0200 Subject: [PATCH 25/35] worktree: pass repository to file-local functions We have a bunch of file-local functions that use `the_repository`. Adapt them so that the repository is instead passed as a parameter so that we can get rid of this dependency. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- worktree.c | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/worktree.c b/worktree.c index 8b10dea1797d51..ebbf9e27e9089c 100644 --- a/worktree.c +++ b/worktree.c @@ -111,27 +111,28 @@ static int is_main_worktree_bare(struct repository *repo) /** * get the main worktree */ -static struct worktree *get_main_worktree(int skip_reading_head) +static struct worktree *get_main_worktree(struct repository *repo, + int skip_reading_head) { struct worktree *worktree = NULL; struct strbuf worktree_path = STRBUF_INIT; - strbuf_add_real_path(&worktree_path, repo_get_common_dir(the_repository)); + strbuf_add_real_path(&worktree_path, repo_get_common_dir(repo)); strbuf_strip_suffix(&worktree_path, "/.git"); CALLOC_ARRAY(worktree, 1); - worktree->repo = the_repository; + worktree->repo = repo; worktree->path = strbuf_detach(&worktree_path, NULL); worktree->is_current = is_current_worktree(worktree); - worktree->is_bare = (the_repository->bare_cfg == 1) || - is_bare_repository(the_repository) || + worktree->is_bare = (repo->bare_cfg == 1) || + is_bare_repository(repo) || /* * When in a secondary worktree we have to also verify if the main * worktree is bare in $commondir/config.worktree. * This check is unnecessary if we're currently in the main worktree, * as prior checks already consulted all configs of the current worktree. */ - (!worktree->is_current && is_main_worktree_bare(the_repository)); + (!worktree->is_current && is_main_worktree_bare(repo)); if (!skip_reading_head) add_head_info(worktree); @@ -182,7 +183,8 @@ struct worktree *get_linked_worktree(const char *id, * retrieving worktree metadata that could be used when the worktree is known * to not be in a healthy state, e.g. when creating or repairing it. */ -static struct worktree **get_worktrees_internal(int skip_reading_head) +static struct worktree **get_worktrees_internal(struct repository *repo, + int skip_reading_head) { struct worktree **list = NULL; struct strbuf path = STRBUF_INIT; @@ -192,9 +194,9 @@ static struct worktree **get_worktrees_internal(int skip_reading_head) ALLOC_ARRAY(list, alloc); - list[counter++] = get_main_worktree(skip_reading_head); + list[counter++] = get_main_worktree(repo, skip_reading_head); - strbuf_addf(&path, "%s/worktrees", repo_get_common_dir(the_repository)); + strbuf_addf(&path, "%s/worktrees", repo_get_common_dir(repo)); dir = opendir(path.buf); strbuf_release(&path); if (dir) { @@ -216,12 +218,12 @@ static struct worktree **get_worktrees_internal(int skip_reading_head) struct worktree **get_worktrees(void) { - return get_worktrees_internal(0); + return get_worktrees_internal(the_repository, 0); } struct worktree **get_worktrees_without_reading_head(void) { - return get_worktrees_internal(1); + return get_worktrees_internal(the_repository, 1); } char *get_worktree_git_dir(const struct worktree *wt) @@ -707,7 +709,7 @@ static void repair_noop(int iserr UNUSED, void repair_worktrees(worktree_repair_fn fn, void *cb_data, int use_relative_paths) { - struct worktree **worktrees = get_worktrees_internal(1); + struct worktree **worktrees = get_worktrees_internal(the_repository, 1); struct worktree **wt = worktrees + 1; /* +1 skips main worktree */ if (!fn) @@ -752,7 +754,7 @@ void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path void repair_worktrees_after_gitdir_move(const char *old_path) { - struct worktree **worktrees = get_worktrees_internal(1); + struct worktree **worktrees = get_worktrees_internal(the_repository, 1); struct worktree **wt = worktrees + 1; /* +1 skips main worktree */ for (; *wt; wt++) @@ -786,7 +788,9 @@ static int is_main_worktree_path(const char *path) * * Returns -1 on failure and strbuf.len on success. */ -static ssize_t infer_backlink(const char *gitfile, struct strbuf *inferred) +static ssize_t infer_backlink(struct repository *repo, + const char *gitfile, + struct strbuf *inferred) { struct strbuf actual = STRBUF_INIT; const char *id; @@ -801,7 +805,7 @@ static ssize_t infer_backlink(const char *gitfile, struct strbuf *inferred) id++; /* advance past '/' to point at */ if (!*id) goto error; - repo_common_path_replace(the_repository, inferred, "worktrees/%s", id); + repo_common_path_replace(repo, inferred, "worktrees/%s", id); if (!is_directory(inferred->buf)) goto error; @@ -842,7 +846,7 @@ void repair_worktree_at_path(const char *path, goto done; } - infer_backlink(dotgit.buf, &inferred_backlink); + infer_backlink(the_repository, dotgit.buf, &inferred_backlink); strbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0); dotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err)); if (dotgit_contents) { @@ -1017,12 +1021,13 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, return rc; } -static int move_config_setting(const char *key, const char *value, +static int move_config_setting(struct repository *repo, + const char *key, const char *value, const char *from_file, const char *to_file) { - if (repo_config_set_in_file_gently(the_repository, to_file, key, NULL, value)) + if (repo_config_set_in_file_gently(repo, to_file, key, NULL, value)) return error(_("unable to set %s in '%s'"), key, to_file); - if (repo_config_set_in_file_gently(the_repository, from_file, key, NULL, NULL)) + if (repo_config_set_in_file_gently(repo, from_file, key, NULL, NULL)) return error(_("unable to unset %s in '%s'"), key, from_file); return 0; } @@ -1058,7 +1063,7 @@ int init_worktree_config(struct repository *r) * _could_ be negating a global core.bare=true. */ if (!git_configset_get_bool(&cs, "core.bare", &bare) && bare) { - if ((res = move_config_setting("core.bare", "true", + if ((res = move_config_setting(r, "core.bare", "true", common_config_file, main_worktree_file))) goto cleanup; @@ -1070,7 +1075,7 @@ int init_worktree_config(struct repository *r) * upgrade to worktree config. */ if (!git_configset_get_value(&cs, "core.worktree", &core_worktree, NULL)) { - if ((res = move_config_setting("core.worktree", core_worktree, + if ((res = move_config_setting(r, "core.worktree", core_worktree, common_config_file, main_worktree_file))) goto cleanup; From e2533e0915ab1e40cf058669c4e8755d693366a6 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 16 Jul 2026 07:33:06 +0200 Subject: [PATCH 26/35] worktree: pass repository to public functions Refactor remaining public functions that still depend on `the_repository` to instead receive a repository as parameter. This allows us to get rid of `USE_THE_REPOSITORY_VARIABLE`. Adapt callers accordingly. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- branch.c | 4 +- builtin/branch.c | 2 +- builtin/config.c | 2 +- builtin/fsck.c | 6 +- builtin/gc.c | 2 +- builtin/notes.c | 2 +- builtin/receive-pack.c | 2 +- builtin/reflog.c | 4 +- builtin/refs.c | 2 +- builtin/worktree.c | 24 ++++---- reachable.c | 4 +- ref-filter.c | 2 +- refs.c | 2 +- revision.c | 6 +- setup.c | 7 ++- submodule.c | 2 +- t/helper/test-ref-store.c | 2 +- worktree.c | 115 +++++++++++++++++++++----------------- worktree.h | 27 +++++---- 19 files changed, 120 insertions(+), 97 deletions(-) diff --git a/branch.c b/branch.c index 243db7d0fc0226..b2ac403b197363 100644 --- a/branch.c +++ b/branch.c @@ -394,7 +394,7 @@ static void prepare_checked_out_branches(void) return; initialized_checked_out_branches = 1; - worktrees = get_worktrees(); + worktrees = get_worktrees(the_repository); while (worktrees[i]) { char *old, *wt_gitdir; @@ -846,7 +846,7 @@ void remove_branch_state(struct repository *r, int verbose) void die_if_checked_out(const char *branch, int ignore_current_worktree) { - struct worktree **worktrees = get_worktrees(); + struct worktree **worktrees = get_worktrees(the_repository); for (int i = 0; worktrees[i]; i++) { if (worktrees[i]->is_current && ignore_current_worktree) diff --git a/builtin/branch.c b/builtin/branch.c index 1572a4f9ef2ab6..c8fddf7f946781 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -579,7 +579,7 @@ static void copy_or_rename_branch(const char *oldname, const char *newname, int const char *interpreted_oldname = NULL; const char *interpreted_newname = NULL; int recovery = 0, oldref_usage = 0; - struct worktree **worktrees = get_worktrees(); + struct worktree **worktrees = get_worktrees(the_repository); if (check_branch_ref(&oldref, oldname)) { /* diff --git a/builtin/config.c b/builtin/config.c index 8d8ec0beead220..0882899c3fbd2a 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -974,7 +974,7 @@ static void location_options_init(struct config_location_options *opts, opts->source.file = opts->file_to_free = repo_git_path(the_repository, "config"); opts->source.scope = CONFIG_SCOPE_LOCAL; } else if (opts->use_worktree_config) { - struct worktree **worktrees = get_worktrees(); + struct worktree **worktrees = get_worktrees(the_repository); if (the_repository->repository_format_worktree_config) opts->source.file = opts->file_to_free = repo_git_path(the_repository, "config.worktree"); diff --git a/builtin/fsck.c b/builtin/fsck.c index 76b723f36d3dca..a6c054e45bf8c0 100644 --- a/builtin/fsck.c +++ b/builtin/fsck.c @@ -632,7 +632,7 @@ static void snapshot_refs(struct repository *repo, refs_for_each_ref_ext(get_main_ref_store(repo), snapshot_ref, &data, &opts); - worktrees = get_worktrees(); + worktrees = get_worktrees(repo); for (p = worktrees; *p; p++) { struct worktree *wt = *p; struct strbuf refname = STRBUF_INIT; @@ -685,7 +685,7 @@ static void process_refs(struct repository *repo, struct snapshot *snap) } if (include_reflogs) { - worktrees = get_worktrees(); + worktrees = get_worktrees(repo); for (p = worktrees; *p; p++) { struct worktree *wt = *p; @@ -1121,7 +1121,7 @@ int cmd_fsck(int argc, verify_index_checksum = 1; verify_ce_order = 1; - worktrees = get_worktrees(); + worktrees = get_worktrees(repo); for (p = worktrees; *p; p++) { struct worktree *wt = *p; struct index_state istate = diff --git a/builtin/gc.c b/builtin/gc.c index d32af422af5e58..46999a99abff42 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -412,7 +412,7 @@ static int worktree_prune_condition(struct gc_config *cfg) while (limit && (d = readdir_skip_dot_and_dotdot(dir))) { char *wtpath; strbuf_reset(&buf); - if (should_prune_worktree(d->d_name, &buf, &wtpath, expiry_date)) + if (should_prune_worktree(the_repository, d->d_name, &buf, &wtpath, expiry_date)) limit--; free(wtpath); } diff --git a/builtin/notes.c b/builtin/notes.c index 962df867c85843..9f1f0ec840b533 100644 --- a/builtin/notes.c +++ b/builtin/notes.c @@ -989,7 +989,7 @@ static int merge(int argc, const char **argv, const char *prefix, "NOTES_MERGE_PARTIAL", &result_oid, NULL, 0, UPDATE_REFS_DIE_ON_ERR); /* Store ref-to-be-updated into .git/NOTES_MERGE_REF */ - worktrees = get_worktrees(); + worktrees = get_worktrees(the_repository); wt = find_shared_symref(worktrees, "NOTES_MERGE_REF", notes_ref); if (wt) diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 19eb6a1b61c3a7..b246c1ccae30a8 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -1503,7 +1503,7 @@ static const char *update(struct command *cmd, struct shallow_info *si) struct object_id *old_oid = &cmd->old_oid; struct object_id *new_oid = &cmd->new_oid; int do_update_worktree = 0; - struct worktree **worktrees = get_worktrees(); + struct worktree **worktrees = get_worktrees(the_repository); const struct worktree *worktree = find_shared_symref(worktrees, "HEAD", name); diff --git a/builtin/reflog.c b/builtin/reflog.c index dcbfe89339f9da..1211c58fa45e3c 100644 --- a/builtin/reflog.c +++ b/builtin/reflog.c @@ -250,7 +250,7 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix, struct string_list_item *item; struct worktree **worktrees, **p; - worktrees = get_worktrees(); + worktrees = get_worktrees(the_repository); for (p = worktrees; *p; p++) { if (single_worktree && !(*p)->is_current) continue; @@ -374,7 +374,7 @@ static int cmd_reflog_drop(int argc, const char **argv, const char *prefix, struct string_list_item *item; struct worktree **worktrees, **p; - worktrees = get_worktrees(); + worktrees = get_worktrees(the_repository); for (p = worktrees; *p; p++) { if (single_worktree && !(*p)->is_current) continue; diff --git a/builtin/refs.c b/builtin/refs.c index a9ca2058eeb55c..5cd21c25fe53dc 100644 --- a/builtin/refs.c +++ b/builtin/refs.c @@ -113,7 +113,7 @@ static int cmd_refs_verify(int argc, const char **argv, const char *prefix, repo_config(repo, git_fsck_config, &fsck_refs_options); prepare_repo_settings(repo); - worktrees = get_worktrees_without_reading_head(); + worktrees = get_worktrees_without_reading_head(repo); for (size_t i = 0; worktrees[i]; i++) ret |= refs_fsck(get_worktree_ref_store(worktrees[i]), &fsck_refs_options, worktrees[i]); diff --git a/builtin/worktree.c b/builtin/worktree.c index d21c43fde38b5e..0689b3d3e079fb 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -226,7 +226,7 @@ static void prune_worktrees(void) while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL) { char *path; strbuf_reset(&reason); - if (should_prune_worktree(d->d_name, &reason, &path, expire)) + if (should_prune_worktree(the_repository, d->d_name, &reason, &path, expire)) prune_worktree(d->d_name, reason.buf); else if (path) string_list_append_nodup(&kept, path)->util = xstrdup(d->d_name); @@ -475,7 +475,7 @@ static int add_worktree(const char *path, const char *refname, struct ref_store *wt_refs; struct repo_config_values *cfg = repo_config_values(the_repository); - worktrees = get_worktrees(); + worktrees = get_worktrees(the_repository); check_candidate_path(path, opts->force, worktrees, "add"); free_worktrees(worktrees); worktrees = NULL; @@ -539,7 +539,8 @@ static int add_worktree(const char *path, const char *refname, strbuf_reset(&sb); strbuf_addf(&sb, "%s/gitdir", sb_repo.buf); - write_worktree_linking_files(sb_git.buf, sb.buf, opts->relative_paths); + write_worktree_linking_files(the_repository, sb_git.buf, + sb.buf, opts->relative_paths); strbuf_reset(&sb); strbuf_addf(&sb, "%s/commondir", sb_repo.buf); write_file(sb.buf, "../.."); @@ -547,7 +548,7 @@ static int add_worktree(const char *path, const char *refname, /* * Set up the ref store of the worktree and create the HEAD reference. */ - wt = get_linked_worktree(name, 1); + wt = get_linked_worktree(the_repository, name, 1); if (!wt) { ret = error(_("could not find created worktree '%s'"), name); goto done; @@ -1103,7 +1104,7 @@ static int list(int ac, const char **av, const char *prefix, else if (!line_terminator && !porcelain) die(_("the option '%s' requires '%s'"), "-z", "--porcelain"); else { - struct worktree **worktrees = get_worktrees(); + struct worktree **worktrees = get_worktrees(the_repository); int path_maxwidth = 0, abbrev = DEFAULT_ABBREV, i; struct worktree_display *display = NULL; @@ -1146,7 +1147,7 @@ static int lock_worktree(int ac, const char **av, const char *prefix, if (ac != 1) usage_with_options(git_worktree_lock_usage, options); - worktrees = get_worktrees(); + worktrees = get_worktrees(the_repository); wt = find_worktree(worktrees, prefix, av[0]); if (!wt) die(_("'%s' is not a working tree"), av[0]); @@ -1183,7 +1184,7 @@ static int unlock_worktree(int ac, const char **av, const char *prefix, if (ac != 1) usage_with_options(git_worktree_unlock_usage, options); - worktrees = get_worktrees(); + worktrees = get_worktrees(the_repository); wt = find_worktree(worktrees, prefix, av[0]); if (!wt) die(_("'%s' is not a working tree"), av[0]); @@ -1269,7 +1270,7 @@ static int move_worktree(int ac, const char **av, const char *prefix, strbuf_addstr(&dst, path); free(path); - worktrees = get_worktrees(); + worktrees = get_worktrees(the_repository); wt = find_worktree(worktrees, prefix, av[0]); if (!wt) die(_("'%s' is not a working tree"), av[0]); @@ -1394,7 +1395,7 @@ static int remove_worktree(int ac, const char **av, const char *prefix, if (ac != 1) usage_with_options(git_worktree_remove_usage, options); - worktrees = get_worktrees(); + worktrees = get_worktrees(the_repository); wt = find_worktree(worktrees, prefix, av[0]); if (!wt) die(_("'%s' is not a working tree"), av[0]); @@ -1456,8 +1457,9 @@ static int repair(int ac, const char **av, const char *prefix, ac = parse_options(ac, av, prefix, options, git_worktree_repair_usage, 0); p = ac > 0 ? av : self; for (; *p; p++) - repair_worktree_at_path(*p, report_repair, &rc, use_relative_paths); - repair_worktrees(report_repair, &rc, use_relative_paths); + repair_worktree_at_path(the_repository, *p, report_repair, + &rc, use_relative_paths); + repair_worktrees(the_repository, report_repair, &rc, use_relative_paths); return rc; } diff --git a/reachable.c b/reachable.c index 101cfc272715fb..be87f487d857fa 100644 --- a/reachable.c +++ b/reachable.c @@ -62,7 +62,7 @@ static void add_rebase_files(struct rev_info *revs) "rebase-merge/autostash", "rebase-merge/orig-head", }; - struct worktree **worktrees = get_worktrees(); + struct worktree **worktrees = get_worktrees(the_repository); for (struct worktree **wt = worktrees; *wt; wt++) { char *wt_gitdir = get_worktree_git_dir(*wt); @@ -319,7 +319,7 @@ void mark_reachable_objects(struct rev_info *revs, int mark_reflog, /* detached HEAD is not included in the list above */ refs_head_ref(get_main_ref_store(the_repository), add_one_ref, revs); - other_head_refs(add_one_ref, revs); + other_head_refs(the_repository, add_one_ref, revs); /* rebase autostash and orig-head */ add_rebase_files(revs); diff --git a/ref-filter.c b/ref-filter.c index 284796c49b2986..29aca08ce7b333 100644 --- a/ref-filter.c +++ b/ref-filter.c @@ -2402,7 +2402,7 @@ static void lazy_init_worktree_map(void) if (ref_to_worktree_map.worktrees) return; - ref_to_worktree_map.worktrees = get_worktrees(); + ref_to_worktree_map.worktrees = get_worktrees(the_repository); hashmap_init(&(ref_to_worktree_map.map), ref_to_worktree_map_cmpfnc, NULL, 0); populate_worktree_map(&(ref_to_worktree_map.map), ref_to_worktree_map.worktrees); } diff --git a/refs.c b/refs.c index 1d2463789167c9..d9957a266c59ab 100644 --- a/refs.c +++ b/refs.c @@ -3328,7 +3328,7 @@ static int move_files(const char *from_path, const char *to_path, struct strbuf static int has_worktrees(void) { - struct worktree **worktrees = get_worktrees(); + struct worktree **worktrees = get_worktrees(the_repository); int ret = 0; size_t i; diff --git a/revision.c b/revision.c index 0c95edef5947fa..7dd40a31d302fc 100644 --- a/revision.c +++ b/revision.c @@ -1711,7 +1711,7 @@ static void add_other_reflogs_to_pending(struct all_refs_cb *cb) { struct worktree **worktrees, **p; - worktrees = get_worktrees(); + worktrees = get_worktrees(the_repository); for (p = worktrees; *p; p++) { struct worktree *wt = *p; @@ -1837,7 +1837,7 @@ void add_index_objects_to_pending(struct rev_info *revs, unsigned int flags) if (revs->single_worktree) return; - worktrees = get_worktrees(); + worktrees = get_worktrees(the_repository); for (p = worktrees; *p; p++) { struct worktree *wt = *p; struct index_state istate = INDEX_STATE_INIT(revs->repo); @@ -2813,7 +2813,7 @@ static int handle_revision_pseudo_opt(struct rev_info *revs, struct all_refs_cb cb; init_all_refs_cb(&cb, revs, *flags); - other_head_refs(handle_one_ref, &cb); + other_head_refs(the_repository, handle_one_ref, &cb); } clear_ref_exclusions(&revs->ref_excludes); } else if (!strcmp(arg, "--branches")) { diff --git a/setup.c b/setup.c index 0de56a074f7c15..505e8d7bf20bda 100644 --- a/setup.c +++ b/setup.c @@ -2650,7 +2650,8 @@ static void create_object_directory(struct repository *repo) strbuf_release(&path); } -static void separate_git_dir(const char *git_dir, const char *git_link) +static void separate_git_dir(struct repository *repo, + const char *git_dir, const char *git_link) { struct stat st; @@ -2666,7 +2667,7 @@ static void separate_git_dir(const char *git_dir, const char *git_link) if (rename(src, git_dir)) die_errno(_("unable to move %s to %s"), src, git_dir); - repair_worktrees_after_gitdir_move(src); + repair_worktrees_after_gitdir_move(repo, src); } write_file(git_link, "gitdir: %s", git_dir); @@ -2823,7 +2824,7 @@ int init_db(struct repository *repo, set_git_dir(repo, real_git_dir, 1); git_dir = repo_get_git_dir(repo); - separate_git_dir(git_dir, original_git_dir); + separate_git_dir(repo, git_dir, original_git_dir); } else { set_git_dir(repo, git_dir, 1); diff --git a/submodule.c b/submodule.c index 93d03610726b8a..c6dda4d1567133 100644 --- a/submodule.c +++ b/submodule.c @@ -2494,7 +2494,7 @@ static void relocate_single_git_dir_into_superproject(const char *path, if (validate_submodule_path(path) < 0) exit(128); - if (submodule_uses_worktrees(path)) + if (submodule_uses_worktrees(the_repository, path)) die(_("relocate_gitdir for submodule '%s' with " "more than one worktree not supported"), path); diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c index 3866d0aca49bc2..5a9a3053d9d81a 100644 --- a/t/helper/test-ref-store.c +++ b/t/helper/test-ref-store.c @@ -84,7 +84,7 @@ static const char **get_store(const char **argv, struct ref_store **refs) *refs = repo_get_submodule_ref_store(the_repository, gitdir); } else if (skip_prefix(argv[0], "worktree:", &gitdir)) { - struct worktree **p, **worktrees = get_worktrees(); + struct worktree **p, **worktrees = get_worktrees(the_repository); for (p = worktrees; *p; p++) { struct worktree *wt = *p; diff --git a/worktree.c b/worktree.c index ebbf9e27e9089c..cbf95328a331ad 100644 --- a/worktree.c +++ b/worktree.c @@ -1,4 +1,3 @@ -#define USE_THE_REPOSITORY_VARIABLE #define DISABLE_SIGN_COMPARE_WARNINGS #include "git-compat-util.h" @@ -139,7 +138,8 @@ static struct worktree *get_main_worktree(struct repository *repo, return worktree; } -struct worktree *get_linked_worktree(const char *id, +struct worktree *get_linked_worktree(struct repository *repo, + const char *id, int skip_reading_head) { struct worktree *worktree = NULL; @@ -149,7 +149,7 @@ struct worktree *get_linked_worktree(const char *id, if (!id) die("Missing linked worktree name"); - repo_common_path_append(the_repository, &path, "worktrees/%s/gitdir", id); + repo_common_path_append(repo, &path, "worktrees/%s/gitdir", id); if (strbuf_read_file(&worktree_path, path.buf, 0) <= 0) /* invalid gitdir file */ goto done; @@ -163,7 +163,7 @@ struct worktree *get_linked_worktree(const char *id, } CALLOC_ARRAY(worktree, 1); - worktree->repo = the_repository; + worktree->repo = repo; worktree->path = strbuf_detach(&worktree_path, NULL); worktree->id = xstrdup(id); worktree->is_current = is_current_worktree(worktree); @@ -203,7 +203,7 @@ static struct worktree **get_worktrees_internal(struct repository *repo, while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL) { struct worktree *linked = NULL; - if ((linked = get_linked_worktree(d->d_name, skip_reading_head))) { + if ((linked = get_linked_worktree(repo, d->d_name, skip_reading_head))) { ALLOC_GROW(list, counter + 1, alloc); list[counter++] = linked; } @@ -216,14 +216,14 @@ static struct worktree **get_worktrees_internal(struct repository *repo, return list; } -struct worktree **get_worktrees(void) +struct worktree **get_worktrees(struct repository *repo) { - return get_worktrees_internal(the_repository, 0); + return get_worktrees_internal(repo, 0); } -struct worktree **get_worktrees_without_reading_head(void) +struct worktree **get_worktrees_without_reading_head(struct repository *repo) { - return get_worktrees_internal(the_repository, 1); + return get_worktrees_internal(repo, 1); } char *get_worktree_git_dir(const struct worktree *wt) @@ -336,7 +336,7 @@ const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire) if (wt->prune_reason_valid) return wt->prune_reason; - if (should_prune_worktree(wt->id, &reason, &path, expire)) + if (should_prune_worktree(wt->repo, wt->id, &reason, &path, expire)) wt->prune_reason = strbuf_detach(&reason, NULL); wt->prune_reason_valid = 1; @@ -447,7 +447,8 @@ void update_worktree_location(struct worktree *wt, const char *path_, strbuf_realpath(&path, path_, 1); strbuf_addf(&dotgit, "%s/.git", path.buf); if (fspathcmp(wt->path, path.buf)) { - write_worktree_linking_files(dotgit.buf, gitdir.buf, use_relative_paths); + write_worktree_linking_files(wt->repo, dotgit.buf, + gitdir.buf, use_relative_paths); free(wt->path); wt->path = strbuf_detach(&path, NULL); @@ -535,7 +536,8 @@ const struct worktree *find_shared_symref(struct worktree **worktrees, return NULL; } -int submodule_uses_worktrees(const char *path) +int submodule_uses_worktrees(struct repository *repo, + const char *path) { char *submodule_gitdir; struct strbuf sb = STRBUF_INIT, err = STRBUF_INIT; @@ -544,7 +546,7 @@ int submodule_uses_worktrees(const char *path) int ret = 0; struct repository_format format = REPOSITORY_FORMAT_INIT; - submodule_gitdir = repo_submodule_path(the_repository, + submodule_gitdir = repo_submodule_path(repo, path, "%s", ""); if (!submodule_gitdir) return 0; @@ -597,13 +599,14 @@ void strbuf_worktree_ref(const struct worktree *wt, strbuf_addstr(sb, refname); } -int other_head_refs(refs_for_each_cb fn, void *cb_data) +int other_head_refs(struct repository *repo, + refs_for_each_cb fn, void *cb_data) { struct worktree **worktrees, **p; struct strbuf refname = STRBUF_INIT; int ret = 0; - worktrees = get_worktrees(); + worktrees = get_worktrees(repo); for (p = worktrees; *p; p++) { struct worktree *wt = *p; struct object_id oid; @@ -614,7 +617,7 @@ int other_head_refs(refs_for_each_cb fn, void *cb_data) strbuf_reset(&refname); strbuf_worktree_ref(wt, &refname, "HEAD"); - if (refs_resolve_ref_unsafe(get_main_ref_store(the_repository), + if (refs_resolve_ref_unsafe(get_main_ref_store(repo), refname.buf, RESOLVE_REF_READING, &oid, &flag)) { @@ -687,7 +690,8 @@ static void repair_gitfile(struct worktree *wt, if (repair) { fn(0, wt->path, repair, cb_data); - write_worktree_linking_files(dotgit.buf, gitdir.buf, use_relative_paths); + write_worktree_linking_files(wt->repo, dotgit.buf, + gitdir.buf, use_relative_paths); } done: @@ -707,9 +711,10 @@ static void repair_noop(int iserr UNUSED, /* nothing */ } -void repair_worktrees(worktree_repair_fn fn, void *cb_data, int use_relative_paths) +void repair_worktrees(struct repository *repo, worktree_repair_fn fn, + void *cb_data, int use_relative_paths) { - struct worktree **worktrees = get_worktrees_internal(the_repository, 1); + struct worktree **worktrees = get_worktrees_internal(repo, 1); struct worktree **wt = worktrees + 1; /* +1 skips main worktree */ if (!fn) @@ -745,16 +750,17 @@ void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path if (!file_exists(dotgit.buf)) goto done; - write_worktree_linking_files(dotgit.buf, gitdir.buf, is_relative_path); + write_worktree_linking_files(wt->repo, dotgit.buf, + gitdir.buf, is_relative_path); done: strbuf_release(&gitdir); strbuf_release(&dotgit); free(path); } -void repair_worktrees_after_gitdir_move(const char *old_path) +void repair_worktrees_after_gitdir_move(struct repository *repo, const char *old_path) { - struct worktree **worktrees = get_worktrees_internal(the_repository, 1); + struct worktree **worktrees = get_worktrees_internal(repo, 1); struct worktree **wt = worktrees + 1; /* +1 skips main worktree */ for (; *wt; wt++) @@ -762,7 +768,7 @@ void repair_worktrees_after_gitdir_move(const char *old_path) free_worktrees(worktrees); } -static int is_main_worktree_path(const char *path) +static int is_main_worktree_path(struct repository *repo, const char *path) { struct strbuf target = STRBUF_INIT; struct strbuf maindir = STRBUF_INIT; @@ -770,7 +776,7 @@ static int is_main_worktree_path(const char *path) strbuf_add_real_path(&target, path); strbuf_strip_suffix(&target, "/.git"); - strbuf_add_real_path(&maindir, repo_get_common_dir(the_repository)); + strbuf_add_real_path(&maindir, repo_get_common_dir(repo)); strbuf_strip_suffix(&maindir, "/.git"); cmp = fspathcmp(maindir.buf, target.buf); @@ -821,7 +827,8 @@ static ssize_t infer_backlink(struct repository *repo, * Repair /worktrees//gitdir if missing, corrupt, or not pointing at * the worktree's path. */ -void repair_worktree_at_path(const char *path, +void repair_worktree_at_path(struct repository *repo, + const char *path, worktree_repair_fn fn, void *cb_data, int use_relative_paths) { @@ -837,7 +844,7 @@ void repair_worktree_at_path(const char *path, if (!fn) fn = repair_noop; - if (is_main_worktree_path(path)) + if (is_main_worktree_path(repo, path)) goto done; strbuf_addf(&dotgit, "%s/.git", path); @@ -846,7 +853,7 @@ void repair_worktree_at_path(const char *path, goto done; } - infer_backlink(the_repository, dotgit.buf, &inferred_backlink); + infer_backlink(repo, dotgit.buf, &inferred_backlink); strbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0); dotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err)); if (dotgit_contents) { @@ -919,7 +926,8 @@ void repair_worktree_at_path(const char *path, if (repair) { fn(0, gitdir.buf, repair, cb_data); - write_worktree_linking_files(dotgit.buf, gitdir.buf, use_relative_paths); + write_worktree_linking_files(repo, dotgit.buf, + gitdir.buf, use_relative_paths); } done: free(dotgit_contents); @@ -930,12 +938,16 @@ void repair_worktree_at_path(const char *path, strbuf_release(&dotgit); } -int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire) +int should_prune_worktree(struct repository *repo, + const char *id, + struct strbuf *reason, + char **wtpath, + timestamp_t expire) { struct stat st; struct strbuf dotgit = STRBUF_INIT; struct strbuf gitdir = STRBUF_INIT; - struct strbuf repo = STRBUF_INIT; + struct strbuf repo_path = STRBUF_INIT; struct strbuf file = STRBUF_INIT; char *path = NULL; int rc = 0; @@ -945,17 +957,17 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, *wtpath = NULL; - path = repo_common_path(the_repository, "worktrees/%s", id); - strbuf_realpath(&repo, path, 1); + path = repo_common_path(repo, "worktrees/%s", id); + strbuf_realpath(&repo_path, path, 1); FREE_AND_NULL(path); - strbuf_addf(&gitdir, "%s/gitdir", repo.buf); - if (!is_directory(repo.buf)) { + strbuf_addf(&gitdir, "%s/gitdir", repo_path.buf); + if (!is_directory(repo_path.buf)) { strbuf_addstr(reason, _("not a valid directory")); rc = 1; goto done; } - strbuf_addf(&file, "%s/locked", repo.buf); + strbuf_addf(&file, "%s/locked", repo_path.buf); if (file_exists(file.buf)) { goto done; } @@ -999,12 +1011,12 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, if (is_absolute_path(path)) { strbuf_addstr(&dotgit, path); } else { - strbuf_addf(&dotgit, "%s/%s", repo.buf, path); + strbuf_addf(&dotgit, "%s/%s", repo_path.buf, path); strbuf_realpath_forgiving(&dotgit, dotgit.buf, 0); } if (!file_exists(dotgit.buf)) { strbuf_reset(&file); - strbuf_addf(&file, "%s/index", repo.buf); + strbuf_addf(&file, "%s/index", repo_path.buf); if (stat(file.buf, &st) || st.st_mtime <= expire) { strbuf_addstr(reason, _("gitdir file points to non-existent location")); rc = 1; @@ -1016,7 +1028,7 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, free(path); strbuf_release(&dotgit); strbuf_release(&gitdir); - strbuf_release(&repo); + strbuf_release(&repo_path); strbuf_release(&file); return rc; } @@ -1094,37 +1106,38 @@ int init_worktree_config(struct repository *r) return res; } -void write_worktree_linking_files(const char *dotgit, const char *gitdir, +void write_worktree_linking_files(struct repository *repo, + const char *dotgit, const char *gitdir, int use_relative_paths) { struct strbuf path = STRBUF_INIT; - struct strbuf repo = STRBUF_INIT; + struct strbuf repo_path = STRBUF_INIT; struct strbuf tmp = STRBUF_INIT; strbuf_addstr(&path, dotgit); strbuf_strip_suffix(&path, "/.git"); strbuf_realpath(&path, path.buf, 1); - strbuf_addstr(&repo, gitdir); - strbuf_strip_suffix(&repo, "/gitdir"); - strbuf_realpath(&repo, repo.buf, 1); + strbuf_addstr(&repo_path, gitdir); + strbuf_strip_suffix(&repo_path, "/gitdir"); + strbuf_realpath(&repo_path, repo_path.buf, 1); - if (use_relative_paths && !the_repository->repository_format_relative_worktrees) { - if (upgrade_repository_format(the_repository, 1) < 0) + if (use_relative_paths && !repo->repository_format_relative_worktrees) { + if (upgrade_repository_format(repo, 1) < 0) die(_("unable to upgrade repository format to support relative worktrees")); - if (repo_config_set_gently(the_repository, "extensions.relativeWorktrees", "true")) + if (repo_config_set_gently(repo, "extensions.relativeWorktrees", "true")) die(_("unable to set extensions.relativeWorktrees setting")); - the_repository->repository_format_relative_worktrees = 1; + repo->repository_format_relative_worktrees = 1; } if (use_relative_paths) { - write_file(gitdir, "%s/.git", relative_path(path.buf, repo.buf, &tmp)); - write_file(dotgit, "gitdir: %s", relative_path(repo.buf, path.buf, &tmp)); + write_file(gitdir, "%s/.git", relative_path(path.buf, repo_path.buf, &tmp)); + write_file(dotgit, "gitdir: %s", relative_path(repo_path.buf, path.buf, &tmp)); } else { write_file(gitdir, "%s/.git", path.buf); - write_file(dotgit, "gitdir: %s", repo.buf); + write_file(dotgit, "gitdir: %s", repo_path.buf); } strbuf_release(&path); - strbuf_release(&repo); + strbuf_release(&repo_path); strbuf_release(&tmp); } diff --git a/worktree.h b/worktree.h index 1075409f9aa2b7..fbb2757f5bf3c3 100644 --- a/worktree.h +++ b/worktree.h @@ -28,7 +28,7 @@ struct worktree { * The caller is responsible for freeing the memory from the returned * worktrees by calling free_worktrees(). */ -struct worktree **get_worktrees(void); +struct worktree **get_worktrees(struct repository *repo); /* * Like `get_worktrees`, but does not read HEAD. Skip reading HEAD allows to @@ -36,7 +36,7 @@ struct worktree **get_worktrees(void); * the HEAD ref. This is useful in contexts where it is assumed that the * refdb may not be in a consistent state. */ -struct worktree **get_worktrees_without_reading_head(void); +struct worktree **get_worktrees_without_reading_head(struct repository *repo); /* * Construct a struct worktree corresponding to repo->gitdir and @@ -47,7 +47,7 @@ struct worktree *get_current_worktree(struct repository *repo); /* * Returns 1 if linked worktrees exist, 0 otherwise. */ -int submodule_uses_worktrees(const char *path); +int submodule_uses_worktrees(struct repository *repo, const char *path); /* * Return git dir of the worktree. Note that the path may be relative. @@ -76,7 +76,8 @@ struct worktree *find_worktree(struct worktree **list, * Look up the worktree corresponding to `id`, or NULL of no such worktree * exists. */ -struct worktree *get_linked_worktree(const char *id, +struct worktree *get_linked_worktree(struct repository *repo, + const char *id, int skip_reading_head); /* @@ -112,7 +113,8 @@ const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire); * `expire` defines a grace period to prune the worktree when its path * does not exist. */ -int should_prune_worktree(const char *id, +int should_prune_worktree(struct repository *repo, + const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire); @@ -142,12 +144,14 @@ typedef void (* worktree_repair_fn)(int iserr, const char *path, * function, if non-NULL, is called with the path of the worktree and a * description of the repair or error, along with the callback user-data. */ -void repair_worktrees(worktree_repair_fn, void *cb_data, int use_relative_paths); +void repair_worktrees(struct repository *repo, worktree_repair_fn, + void *cb_data, int use_relative_paths); /* * Repair the linked worktrees after the gitdir has been moved. */ -void repair_worktrees_after_gitdir_move(const char *old_path); +void repair_worktrees_after_gitdir_move(struct repository *repo, + const char *old_path); /* * Repair the linked worktree after the gitdir has been moved. @@ -164,7 +168,9 @@ void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path * worktree and a description of the repair or error, along with the callback * user-data. */ -void repair_worktree_at_path(const char *, worktree_repair_fn, +void repair_worktree_at_path(struct repository *repo, + const char *path, + worktree_repair_fn fn, void *cb_data, int use_relative_paths); /* @@ -196,7 +202,7 @@ int is_shared_symref(const struct worktree *wt, * Similar to head_ref() for all HEADs _except_ one from the current * worktree, which is covered by head_ref(). */ -int other_head_refs(refs_for_each_cb fn, void *cb_data); +int other_head_refs(struct repository *repo, refs_for_each_cb fn, void *cb_data); int is_worktree_being_rebased(const struct worktree *wt, const char *target); int is_worktree_being_bisected(const struct worktree *wt, const char *target); @@ -239,7 +245,8 @@ int init_worktree_config(struct repository *r); * dotgit: "/path/to/foo/.git" * gitdir: "/path/to/repo/worktrees/foo/gitdir" */ -void write_worktree_linking_files(const char *dotgit, const char *gitdir, +void write_worktree_linking_files(struct repository *repo, + const char *dotgit, const char *gitdir, int use_relative_paths); #endif From b1296cb1aafd29c69e3b2a526b19c091f7fe5cf9 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 16 Jul 2026 07:33:07 +0200 Subject: [PATCH 27/35] refs: remove remaining uses of `the_repository` There are still a couple of callsites that use `the_repository`. Convert these to instead use a repository injected by the caller. This allows us to remove `USE_THE_REPOSITORY_VARIABLE`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- branch.c | 2 +- builtin/branch.c | 14 +++++++++----- builtin/check-ref-format.c | 2 +- builtin/checkout.c | 2 +- builtin/merge.c | 2 +- builtin/worktree.c | 8 ++++---- refs.c | 23 +++++++++-------------- refs.h | 5 +++-- 8 files changed, 29 insertions(+), 29 deletions(-) diff --git a/branch.c b/branch.c index b2ac403b197363..4f38905bad928d 100644 --- a/branch.c +++ b/branch.c @@ -372,7 +372,7 @@ int read_branch_desc(struct strbuf *buf, const char *branch_name) */ int validate_branchname(const char *name, struct strbuf *ref) { - if (check_branch_ref(ref, name)) { + if (check_branch_ref(the_repository, ref, name)) { int code = die_message(_("'%s' is not a valid branch name"), name); advise_if_enabled(ADVICE_REF_SYNTAX, _("See 'git help check-ref-format'")); diff --git a/builtin/branch.c b/builtin/branch.c index c8fddf7f946781..be26ec0750a7e8 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -259,7 +259,8 @@ static int delete_branches(int argc, const char **argv, int force, int kinds, char *target = NULL; int flags = 0; - copy_branchname(&bname, argv[i], allowed_interpret); + copy_branchname(the_repository, &bname, + argv[i], allowed_interpret); free(name); name = mkpathdup(fmt, bname.buf); @@ -581,7 +582,7 @@ static void copy_or_rename_branch(const char *oldname, const char *newname, int int recovery = 0, oldref_usage = 0; struct worktree **worktrees = get_worktrees(the_repository); - if (check_branch_ref(&oldref, oldname)) { + if (check_branch_ref(the_repository, &oldref, oldname)) { /* * Bad name --- this could be an attempt to rename a * ref that we used to allow to be created by accident. @@ -898,7 +899,8 @@ int cmd_branch(int argc, die(_("cannot give description to detached HEAD")); branch_name = head; } else if (argc == 1) { - copy_branchname(&buf, argv[0], INTERPRET_BRANCH_LOCAL); + copy_branchname(the_repository, &buf, argv[0], + INTERPRET_BRANCH_LOCAL); branch_name = buf.buf; } else { die(_("cannot edit description of more than one branch")); @@ -941,7 +943,8 @@ int cmd_branch(int argc, if (!argc) branch = branch_get(NULL); else if (argc == 1) { - copy_branchname(&buf, argv[0], INTERPRET_BRANCH_LOCAL); + copy_branchname(the_repository, &buf, argv[0], + INTERPRET_BRANCH_LOCAL); branch = branch_get(buf.buf); } else die(_("too many arguments to set new upstream")); @@ -971,7 +974,8 @@ int cmd_branch(int argc, if (!argc) branch = branch_get(NULL); else if (argc == 1) { - copy_branchname(&buf, argv[0], INTERPRET_BRANCH_LOCAL); + copy_branchname(the_repository, &buf, argv[0], + INTERPRET_BRANCH_LOCAL); branch = branch_get(buf.buf); } else die(_("too many arguments to unset upstream")); diff --git a/builtin/check-ref-format.c b/builtin/check-ref-format.c index e42b0444ead269..fd1c9c0e0c8e60 100644 --- a/builtin/check-ref-format.c +++ b/builtin/check-ref-format.c @@ -45,7 +45,7 @@ static int check_ref_format_branch(const char *arg) int nongit; setup_git_directory_gently(the_repository, &nongit); - if (check_branch_ref(&sb, arg) || + if (check_branch_ref(the_repository, &sb, arg) || !skip_prefix(sb.buf, "refs/heads/", &name)) die("'%s' is not a valid branch name", arg); printf("%s\n", name); diff --git a/builtin/checkout.c b/builtin/checkout.c index aee84ca89742b0..55e3a89a852712 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -805,7 +805,7 @@ static void setup_branch_path(struct branch_info *branch) &branch->oid, &branch->refname, 0)) repo_get_oid_committish(the_repository, branch->name, &branch->oid); - copy_branchname(&buf, branch->name, INTERPRET_BRANCH_LOCAL); + copy_branchname(the_repository, &buf, branch->name, INTERPRET_BRANCH_LOCAL); if (strcmp(buf.buf, branch->name)) { free(branch->name); branch->name = xstrdup(buf.buf); diff --git a/builtin/merge.c b/builtin/merge.c index 5b46a596f0bdf4..58d1b7bb07d90f 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -553,7 +553,7 @@ static void merge_name(const char *remote, struct strbuf *msg) char *found_ref = NULL; int len, early; - copy_branchname(&bname, remote, 0); + copy_branchname(the_repository, &bname, remote, 0); remote = bname.buf; oidclr(&branch_head, the_repository->hash_algo); diff --git a/builtin/worktree.c b/builtin/worktree.c index 0689b3d3e079fb..6397e149a8a1fd 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -481,7 +481,7 @@ static int add_worktree(const char *path, const char *refname, worktrees = NULL; /* is 'refname' a branch or commit? */ - if (!opts->detach && !check_branch_ref(&symref, refname) && + if (!opts->detach && !check_branch_ref(the_repository, &symref, refname) && refs_ref_exists(get_main_ref_store(the_repository), symref.buf)) { is_branch = 1; if (!opts->force) @@ -650,7 +650,7 @@ static void print_preparing_worktree_line(int detach, fprintf_ln(stderr, _("Preparing worktree (new branch '%s')"), new_branch); } else { struct strbuf s = STRBUF_INIT; - if (!detach && !check_branch_ref(&s, branch) && + if (!detach && !check_branch_ref(the_repository, &s, branch) && refs_ref_exists(get_main_ref_store(the_repository), s.buf)) fprintf_ln(stderr, _("Preparing worktree (checking out '%s')"), branch); @@ -772,7 +772,7 @@ static char *dwim_branch(const char *path, char **new_branch) char *branchname = xstrndup(s, n); struct strbuf ref = STRBUF_INIT; - branch_exists = !check_branch_ref(&ref, branchname) && + branch_exists = !check_branch_ref(the_repository, &ref, branchname) && refs_ref_exists(get_main_ref_store(the_repository), ref.buf); strbuf_release(&ref); @@ -869,7 +869,7 @@ static int add(int ac, const char **av, const char *prefix, new_branch = new_branch_force; if (!opts.force && - !check_branch_ref(&symref, new_branch) && + !check_branch_ref(the_repository, &symref, new_branch) && refs_ref_exists(get_main_ref_store(the_repository), symref.buf)) die_if_checked_out(symref.buf, 0); strbuf_release(&symref); diff --git a/refs.c b/refs.c index d9957a266c59ab..92d5df5b71fa4b 100644 --- a/refs.c +++ b/refs.c @@ -2,8 +2,6 @@ * The backend-independent part of the reference module. */ -#define USE_THE_REPOSITORY_VARIABLE - #include "git-compat-util.h" #include "abspath.h" #include "advice.h" @@ -744,14 +742,15 @@ static char *substitute_branch_name(struct repository *r, return NULL; } -void copy_branchname(struct strbuf *sb, const char *name, +void copy_branchname(struct repository *repo, + struct strbuf *sb, const char *name, enum interpret_branch_kind allowed) { int len = strlen(name); struct interpret_branch_name_options options = { .allowed = allowed }; - int used = repo_interpret_branch_name(the_repository, name, len, sb, + int used = repo_interpret_branch_name(repo, name, len, sb, &options); if (used < 0) @@ -759,10 +758,10 @@ void copy_branchname(struct strbuf *sb, const char *name, strbuf_add(sb, name + used, len - used); } -int check_branch_ref(struct strbuf *sb, const char *name) +int check_branch_ref(struct repository *repo, struct strbuf *sb, const char *name) { if (startup_info->have_repository) - copy_branchname(sb, name, INTERPRET_BRANCH_LOCAL); + copy_branchname(repo, sb, name, INTERPRET_BRANCH_LOCAL); else strbuf_addstr(sb, name); @@ -3326,9 +3325,9 @@ static int move_files(const char *from_path, const char *to_path, struct strbuf return ret; } -static int has_worktrees(void) +static int has_worktrees(struct repository *repo) { - struct worktree **worktrees = get_worktrees(the_repository); + struct worktree **worktrees = get_worktrees(repo); int ret = 0; size_t i; @@ -3373,12 +3372,8 @@ int repo_migrate_ref_storage_format(struct repository *repo, * Worktrees complicate the migration because every worktree has a * separate ref storage. While it should be feasible to implement, this * is pushed out to a future iteration. - * - * TODO: we should really be passing the caller-provided repository to - * `has_worktrees()`, but our worktree subsystem doesn't yet support - * that. */ - if (has_worktrees()) { + if (has_worktrees(repo)) { strbuf_addstr(errbuf, "migrating repositories with worktrees is not supported yet"); ret = -1; goto done; @@ -3503,7 +3498,7 @@ int repo_migrate_ref_storage_format(struct repository *repo, * repository format so that clients will use the new ref store. * We also need to swap out the repository's main ref store. */ - initialize_repository_version(the_repository, hash_algo_by_ptr(repo->hash_algo), format, 1); + initialize_repository_version(repo, hash_algo_by_ptr(repo->hash_algo), format, 1); /* * Unset the old ref store and release it. `get_main_ref_store()` will diff --git a/refs.h b/refs.h index a381022c77065a..9979446d15fd3b 100644 --- a/refs.h +++ b/refs.h @@ -234,7 +234,8 @@ char *repo_default_branch_name(struct repository *r, int quiet); * If "allowed" is non-zero, restrict the set of allowed expansions. See * repo_interpret_branch_name() for details. */ -void copy_branchname(struct strbuf *sb, const char *name, +void copy_branchname(struct repository *repo, + struct strbuf *sb, const char *name, enum interpret_branch_kind allowed); /* @@ -243,7 +244,7 @@ void copy_branchname(struct strbuf *sb, const char *name, * * The return value is "0" if the result is valid, and "-1" otherwise. */ -int check_branch_ref(struct strbuf *sb, const char *name); +int check_branch_ref(struct repository *repo, struct strbuf *sb, const char *name); /* * Similar for a tag name in refs/tags/. From 991ec2741d723d0737bd5410f01b7f98b89d9186 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 16 Jul 2026 14:38:02 +0200 Subject: [PATCH 28/35] refspec: group related structures and functions Reorganize the refspec header a bit so that structures and their related functions are grouped closer together. While at it, fix a couple of style violations. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- refspec.h | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/refspec.h b/refspec.h index 8b04f9995ef2a8..832d6f923ce083 100644 --- a/refspec.h +++ b/refspec.h @@ -1,6 +1,9 @@ #ifndef REFSPEC_H #define REFSPEC_H +struct string_list; +struct strvec; + #define TAG_REFSPEC "refs/tags/*:refs/tags/*" /** @@ -30,10 +33,9 @@ struct refspec_item { char *raw; }; -struct string_list; - -#define REFSPEC_INIT_FETCH { .fetch = 1 } -#define REFSPEC_INIT_PUSH { .fetch = 0 } +int refspec_item_init_fetch(struct refspec_item *item, const char *refspec); +int refspec_item_init_push(struct refspec_item *item, const char *refspec); +void refspec_item_clear(struct refspec_item *item); /** * An array of strings can be parsed into a struct refspec using @@ -47,20 +49,20 @@ struct refspec { unsigned fetch : 1; }; -int refspec_item_init_fetch(struct refspec_item *item, const char *refspec); -int refspec_item_init_push(struct refspec_item *item, const char *refspec); -void refspec_item_clear(struct refspec_item *item); +#define REFSPEC_INIT_FETCH { .fetch = 1 } +#define REFSPEC_INIT_PUSH { .fetch = 0 } + void refspec_init_fetch(struct refspec *rs); void refspec_init_push(struct refspec *rs); +void refspec_clear(struct refspec *rs); + void refspec_append(struct refspec *rs, const char *refspec); __attribute__((format (printf,2,3))) void refspec_appendf(struct refspec *rs, const char *fmt, ...); void refspec_appendn(struct refspec *rs, const char **refspecs, int nr); -void refspec_clear(struct refspec *rs); int valid_fetch_refspec(const char *refspec); -struct strvec; /* * Determine what values to pass to the peer in ref-prefix lines * (see linkgit:gitprotocol-v2[5]). @@ -76,7 +78,7 @@ int refname_matches_negative_refspec_item(const char *refname, struct refspec *r * Returns 1 if refname matches pattern, 0 otherwise. */ int match_refname_with_pattern(const char *pattern, const char *refname, - const char *replacement, char **result); + const char *replacement, char **result); /* * Queries a refspec for a match and updates the query item. @@ -89,8 +91,8 @@ int refspec_find_match(struct refspec *rs, struct refspec_item *query); * list. */ void refspec_find_all_matches(struct refspec *rs, - struct refspec_item *query, - struct string_list *results); + struct refspec_item *query, + struct string_list *results); /* * Remove all entries in the input list which match any negative refspec in From bb71c3f19e6a693fc1d32e4448be8520132d946e Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 16 Jul 2026 14:38:03 +0200 Subject: [PATCH 29/35] refspec: let callers pass in hash algorithm when parsing items When parsing a refspec item we need to know about the hash algorithm used by the repository so that we can decide whether or not a given string is an exact object ID. We use `the_hash_algo` for this, which makes the code implicitly depend on `the_repository`. Refactor `refspec_item_init_fetch()`, `refspec_item_init_push()` and `valid_fetch_refspec()` so that callers have to pass in the hash algorithm explicitly and adapt callers accordingly. For now, all of the callers simply pass `the_hash_algo`, so there is no change in behaviour. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/fetch.c | 3 ++- builtin/pull.c | 2 +- refspec.c | 30 +++++++++++++++++------------- refspec.h | 9 ++++++--- remote.c | 2 +- 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/builtin/fetch.c b/builtin/fetch.c index 8e676b79ba1a91..1d4a129039b16e 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -601,7 +601,8 @@ static struct ref *get_ref_map(struct remote *remote, struct refspec_item tag_refspec; /* also fetch all tags */ - refspec_item_init_push(&tag_refspec, TAG_REFSPEC); + refspec_item_init_push(&tag_refspec, TAG_REFSPEC, + the_hash_algo); get_fetch_map(remote_refs, &tag_refspec, &tail, 0); refspec_item_clear(&tag_refspec); } else if (tags == TAGS_DEFAULT && *autotags) { diff --git a/builtin/pull.c b/builtin/pull.c index d49b09114ae7d8..db3ee0aab3ed91 100644 --- a/builtin/pull.c +++ b/builtin/pull.c @@ -612,7 +612,7 @@ static const char *get_tracking_branch(const char *remote, const char *refspec) const char *spec_src; const char *merge_branch; - if (!refspec_item_init_fetch(&spec, refspec)) + if (!refspec_item_init_fetch(&spec, refspec, the_hash_algo)) die(_("invalid refspec '%s'"), refspec); spec_src = spec.src; if (!*spec_src || !strcmp(spec_src, "HEAD")) diff --git a/refspec.c b/refspec.c index fb89bce1db23fb..33a6fb8e457887 100644 --- a/refspec.c +++ b/refspec.c @@ -16,7 +16,8 @@ * Parses the provided refspec 'refspec' and populates the refspec_item 'item'. * Returns 1 if successful and 0 if the refspec is invalid. */ -static int parse_refspec(struct refspec_item *item, const char *refspec, int fetch) +static int parse_refspec(struct refspec_item *item, const char *refspec, + const struct git_hash_algo *algo, int fetch) { size_t llen; int is_glob; @@ -84,7 +85,7 @@ static int parse_refspec(struct refspec_item *item, const char *refspec, int fet */ if (!*item->src) return 0; /* negative refspecs must not be empty */ - else if (llen == the_hash_algo->hexsz && !get_oid_hex(item->src, &unused)) + else if (llen == algo->hexsz && !get_oid_hex_algop(item->src, &unused, algo)) return 0; /* negative refspecs cannot be exact sha1 */ else if (!check_refname_format(item->src, flags)) ; /* valid looking ref is ok */ @@ -101,7 +102,7 @@ static int parse_refspec(struct refspec_item *item, const char *refspec, int fet /* LHS */ if (!*item->src) ; /* empty is ok; it means "HEAD" */ - else if (llen == the_hash_algo->hexsz && !get_oid_hex(item->src, &unused)) + else if (llen == algo->hexsz && !get_oid_hex_algop(item->src, &unused, algo)) item->exact_sha1 = 1; /* ok */ else if (!check_refname_format(item->src, flags)) ; /* valid looking ref is ok */ @@ -154,21 +155,23 @@ static int parse_refspec(struct refspec_item *item, const char *refspec, int fet } static int refspec_item_init(struct refspec_item *item, const char *refspec, - int fetch) + const struct git_hash_algo *algo, int fetch) { memset(item, 0, sizeof(*item)); item->raw = xstrdup(refspec); - return parse_refspec(item, refspec, fetch); + return parse_refspec(item, refspec, algo, fetch); } -int refspec_item_init_fetch(struct refspec_item *item, const char *refspec) +int refspec_item_init_fetch(struct refspec_item *item, const char *refspec, + const struct git_hash_algo *algo) { - return refspec_item_init(item, refspec, 1); + return refspec_item_init(item, refspec, algo, 1); } -int refspec_item_init_push(struct refspec_item *item, const char *refspec) +int refspec_item_init_push(struct refspec_item *item, const char *refspec, + const struct git_hash_algo *algo) { - return refspec_item_init(item, refspec, 0); + return refspec_item_init(item, refspec, algo, 0); } void refspec_item_clear(struct refspec_item *item) @@ -200,9 +203,9 @@ void refspec_append(struct refspec *rs, const char *refspec) int ret; if (rs->fetch) - ret = refspec_item_init_fetch(&item, refspec); + ret = refspec_item_init_fetch(&item, refspec, the_hash_algo); else - ret = refspec_item_init_push(&item, refspec); + ret = refspec_item_init_push(&item, refspec, the_hash_algo); if (!ret) die(_("invalid refspec '%s'"), refspec); @@ -246,10 +249,11 @@ void refspec_clear(struct refspec *rs) rs->fetch = 0; } -int valid_fetch_refspec(const char *fetch_refspec_str) +int valid_fetch_refspec(const char *fetch_refspec_str, + const struct git_hash_algo *algo) { struct refspec_item refspec; - int ret = refspec_item_init_fetch(&refspec, fetch_refspec_str); + int ret = refspec_item_init_fetch(&refspec, fetch_refspec_str, algo); refspec_item_clear(&refspec); return ret; } diff --git a/refspec.h b/refspec.h index 832d6f923ce083..e482b720a87b86 100644 --- a/refspec.h +++ b/refspec.h @@ -1,6 +1,7 @@ #ifndef REFSPEC_H #define REFSPEC_H +struct git_hash_algo; struct string_list; struct strvec; @@ -33,8 +34,10 @@ struct refspec_item { char *raw; }; -int refspec_item_init_fetch(struct refspec_item *item, const char *refspec); -int refspec_item_init_push(struct refspec_item *item, const char *refspec); +int refspec_item_init_fetch(struct refspec_item *item, const char *refspec, + const struct git_hash_algo *algo); +int refspec_item_init_push(struct refspec_item *item, const char *refspec, + const struct git_hash_algo *algo); void refspec_item_clear(struct refspec_item *item); /** @@ -61,7 +64,7 @@ __attribute__((format (printf,2,3))) void refspec_appendf(struct refspec *rs, const char *fmt, ...); void refspec_appendn(struct refspec *rs, const char **refspecs, int nr); -int valid_fetch_refspec(const char *refspec); +int valid_fetch_refspec(const char *refspec, const struct git_hash_algo *algo); /* * Determine what values to pass to the peer in ref-prefix lines diff --git a/remote.c b/remote.c index e6c52c850cac63..b4dff1e5f990fa 100644 --- a/remote.c +++ b/remote.c @@ -3039,7 +3039,7 @@ int valid_remote_name(const char *name) int result; struct strbuf refspec = STRBUF_INIT; strbuf_addf(&refspec, "refs/heads/test:refs/remotes/%s/test", name); - result = valid_fetch_refspec(refspec.buf); + result = valid_fetch_refspec(refspec.buf, the_hash_algo); strbuf_release(&refspec); return result; } From 01f4b61d0302af16b30b2b5141a53e3a88f9f98a Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 16 Jul 2026 14:38:04 +0200 Subject: [PATCH 30/35] refspec: stop depending on `the_repository` The only remaining user of `the_hash_algo` in "refspec.c" is `refspec_append()`, which needs to know the hash algorithm so that it can parse the appended refspec item. In contrast to the functions adapted in the preceding commit, this function always operates on a `struct refspec`. As that structure is expected to only ever contain refspecs that all use the same hash function it doesn't make sense though to adapt each caller. Instead, adapt the structure itself so that it gets initialized with a hash function and use that hash function to parse new refspec items. Adapt callers accordingly. This removes the final dependency on the global repository variable in "refspec.c", so we can drop `USE_THE_REPOSITORY_VARIABLE`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/fast-export.c | 4 +++- builtin/fetch.c | 6 ++++-- builtin/push.c | 6 ++++-- builtin/send-pack.c | 5 ++++- builtin/submodule--helper.c | 2 +- http-push.c | 2 +- refspec.c | 13 ++++++------- refspec.h | 17 ++++++++++++----- remote.c | 4 ++-- transport-helper.c | 2 +- 10 files changed, 38 insertions(+), 23 deletions(-) diff --git a/builtin/fast-export.c b/builtin/fast-export.c index 0be43104dc0d23..8f4da4cfac8283 100644 --- a/builtin/fast-export.c +++ b/builtin/fast-export.c @@ -51,7 +51,7 @@ static int show_original_ids; static int mark_tags; static struct string_list extra_refs = STRING_LIST_INIT_DUP; static struct string_list tag_refs = STRING_LIST_INIT_DUP; -static struct refspec refspecs = REFSPEC_INIT_FETCH; +static struct refspec refspecs; static int anonymize; static struct hashmap anonymized_seeds; static struct revision_sources revision_sources; @@ -1372,6 +1372,8 @@ int cmd_fast_export(int argc, /* we handle encodings */ repo_config(the_repository, git_default_config, NULL); + refspec_init_fetch(&refspecs, the_hash_algo); + repo_init_revisions(the_repository, &revs, prefix); init_revision_sources(&revision_sources); revs.topo_order = 1; diff --git a/builtin/fetch.c b/builtin/fetch.c index 1d4a129039b16e..6e1a224553f13f 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -96,7 +96,7 @@ static struct string_list deepen_not = STRING_LIST_INIT_NODUP; static struct strbuf default_rla = STRBUF_INIT; static struct transport *gtransport; static struct transport *gsecondary; -static struct refspec refmap = REFSPEC_INIT_FETCH; +static struct refspec refmap; static struct string_list server_options = STRING_LIST_INIT_DUP; static struct string_list negotiation_restrict = STRING_LIST_INIT_NODUP; static struct string_list negotiation_include = STRING_LIST_INIT_NODUP; @@ -2429,7 +2429,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv, const struct fetch_config *config, struct list_objects_filter_options *filter_options) { - struct refspec rs = REFSPEC_INIT_FETCH; + struct refspec rs = REFSPEC_INIT_FETCH(the_hash_algo); int i; int exit_code; int maybe_prune_tags; @@ -2631,6 +2631,8 @@ int cmd_fetch(int argc, filter_options.allow_auto_filter = 1; + refspec_init_fetch(&refmap, the_hash_algo); + packet_trace_identity("fetch"); /* Record the command line for the reflog */ diff --git a/builtin/push.c b/builtin/push.c index 1b2ad3b8df7c55..8ccdb07c404874 100644 --- a/builtin/push.c +++ b/builtin/push.c @@ -66,7 +66,7 @@ static enum transport_family family; static struct push_cas_option cas; -static struct refspec rs = REFSPEC_INIT_PUSH; +static struct refspec rs; static struct string_list push_options_config = STRING_LIST_INIT_DUP; @@ -749,6 +749,8 @@ int cmd_push(int argc, : &push_options_config); set_push_cert_flags(&flags, push_cert); + refspec_init_push(&rs, the_hash_algo); + die_for_incompatible_opt4(deleterefs, "--delete", tags, "--tags", flags & TRANSPORT_PUSH_ALL, "--all/--branches", @@ -855,7 +857,7 @@ int cmd_push(int argc, } refspec_clear(&rs); - rs = (struct refspec) REFSPEC_INIT_PUSH; + rs = (struct refspec) REFSPEC_INIT_PUSH(the_hash_algo); if (tags) refspec_append(&rs, "refs/tags/*"); diff --git a/builtin/send-pack.c b/builtin/send-pack.c index 1412b49bc845b1..d6cdbae472f725 100644 --- a/builtin/send-pack.c +++ b/builtin/send-pack.c @@ -153,7 +153,7 @@ int cmd_send_pack(int argc, const char *prefix, struct repository *repo) { - struct refspec rs = REFSPEC_INIT_PUSH; + struct refspec rs; const char *remote_name = NULL; struct remote *remote = NULL; const char *dest = NULL; @@ -214,6 +214,9 @@ int cmd_send_pack(int argc, repo_config(repo, send_pack_config, NULL); argc = parse_options(argc, argv, prefix, options, send_pack_usage, 0); + + refspec_init_push(&rs, repo->hash_algo); + if (argc > 0) { dest = argv[0]; refspec_appendn(&rs, argv + 1, argc - 1); diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index 1cc82a134db22e..c396b826ba7637 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -3150,7 +3150,7 @@ static int push_check(int argc, const char **argv, const char *prefix UNUSED, if (argc > 2) { int i; struct ref *local_refs = get_local_heads(); - struct refspec refspec = REFSPEC_INIT_PUSH; + struct refspec refspec = REFSPEC_INIT_PUSH(the_hash_algo); refspec_appendn(&refspec, argv + 2, argc - 2); diff --git a/http-push.c b/http-push.c index 3c23cbba27a9ec..969d984cb9ae0f 100644 --- a/http-push.c +++ b/http-push.c @@ -1716,7 +1716,7 @@ int cmd_main(int argc, const char **argv) { struct transfer_request *request; struct transfer_request *next_request; - struct refspec rs = REFSPEC_INIT_PUSH; + struct refspec rs = REFSPEC_INIT_PUSH(the_hash_algo); struct remote_lock *ref_lock = NULL; struct remote_lock *info_ref_lock = NULL; int delete_branch = 0; diff --git a/refspec.c b/refspec.c index 33a6fb8e457887..7cb479983b507b 100644 --- a/refspec.c +++ b/refspec.c @@ -1,4 +1,3 @@ -#define USE_THE_REPOSITORY_VARIABLE #define DISABLE_SIGN_COMPARE_WARNINGS #include "git-compat-util.h" @@ -185,15 +184,15 @@ void refspec_item_clear(struct refspec_item *item) item->exact_sha1 = 0; } -void refspec_init_fetch(struct refspec *rs) +void refspec_init_fetch(struct refspec *rs, const struct git_hash_algo *algo) { - struct refspec blank = REFSPEC_INIT_FETCH; + struct refspec blank = REFSPEC_INIT_FETCH(algo); memcpy(rs, &blank, sizeof(*rs)); } -void refspec_init_push(struct refspec *rs) +void refspec_init_push(struct refspec *rs, const struct git_hash_algo *algo) { - struct refspec blank = REFSPEC_INIT_PUSH; + struct refspec blank = REFSPEC_INIT_PUSH(algo); memcpy(rs, &blank, sizeof(*rs)); } @@ -203,9 +202,9 @@ void refspec_append(struct refspec *rs, const char *refspec) int ret; if (rs->fetch) - ret = refspec_item_init_fetch(&item, refspec, the_hash_algo); + ret = refspec_item_init_fetch(&item, refspec, rs->hash_algo); else - ret = refspec_item_init_push(&item, refspec, the_hash_algo); + ret = refspec_item_init_push(&item, refspec, rs->hash_algo); if (!ret) die(_("invalid refspec '%s'"), refspec); diff --git a/refspec.h b/refspec.h index e482b720a87b86..fadef67933c079 100644 --- a/refspec.h +++ b/refspec.h @@ -49,14 +49,21 @@ struct refspec { int alloc; int nr; + const struct git_hash_algo *hash_algo; unsigned fetch : 1; }; -#define REFSPEC_INIT_FETCH { .fetch = 1 } -#define REFSPEC_INIT_PUSH { .fetch = 0 } - -void refspec_init_fetch(struct refspec *rs); -void refspec_init_push(struct refspec *rs); +#define REFSPEC_INIT_FETCH(algo) { \ + .fetch = 1, \ + .hash_algo = (algo), \ +} +#define REFSPEC_INIT_PUSH(algo) { \ + .fetch = 0, \ + .hash_algo = (algo), \ +} + +void refspec_init_fetch(struct refspec *rs, const struct git_hash_algo *hash_algo); +void refspec_init_push(struct refspec *rs, const struct git_hash_algo *hash_algo); void refspec_clear(struct refspec *rs); void refspec_append(struct refspec *rs, const char *refspec); diff --git a/remote.c b/remote.c index b4dff1e5f990fa..d151b1f9d9bce5 100644 --- a/remote.c +++ b/remote.c @@ -150,8 +150,8 @@ static struct remote *make_remote(struct remote_state *remote_state, ret->prune = -1; /* unspecified */ ret->prune_tags = -1; /* unspecified */ ret->name = xstrndup(name, len); - refspec_init_push(&ret->push); - refspec_init_fetch(&ret->fetch); + refspec_init_push(&ret->push, the_hash_algo); + refspec_init_fetch(&ret->fetch, the_hash_algo); string_list_init_dup(&ret->server_options); string_list_init_dup(&ret->negotiation_restrict); string_list_init_dup(&ret->negotiation_include); diff --git a/transport-helper.c b/transport-helper.c index 80f90eb7bace6f..8a25707b038aac 100644 --- a/transport-helper.c +++ b/transport-helper.c @@ -162,7 +162,7 @@ static struct child_process *get_helper(struct transport *transport) data->helper = helper; data->no_disconnect_req = 0; - refspec_init_fetch(&data->rs); + refspec_init_fetch(&data->rs, the_hash_algo); /* * Open the output as FILE* so strbuf_getline_*() family of From 0717b3595d4ebfca396134e4356e473c794aa1c9 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 16 Jul 2026 17:28:23 +0200 Subject: [PATCH 31/35] copy: drop dependency on `the_repository` When copying a file we need to potentially adapt permissions of the new file based on whether or not "core.shared" is enabled. Parsing this configuration makes us implicitly depend on `the_repository`. Refactor the code to instead require the caller to pass in a repository so that we can remove `USE_THE_REPOSITORY_VARIABLE`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/clone.c | 2 +- builtin/difftool.c | 4 ++-- builtin/worktree.c | 4 ++-- bundle-uri.c | 2 +- copy.c | 12 ++++++------ copy.h | 8 ++++++-- refs/files-backend.c | 2 +- rerere.c | 2 +- sequencer.c | 6 +++--- setup.c | 2 +- 10 files changed, 24 insertions(+), 20 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index d60d1b60bc238c..18603dd4ce715f 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -335,7 +335,7 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest, die_errno(_("failed to create link '%s'"), dest->buf); option_no_hardlinks = 1; } - if (copy_file_with_time(dest->buf, src->buf, 0666)) + if (copy_file_with_time(the_repository, dest->buf, src->buf, 0666)) die_errno(_("failed to copy file to '%s'"), dest->buf); } diff --git a/builtin/difftool.c b/builtin/difftool.c index 26778f8515deef..5e7777fbe48daa 100644 --- a/builtin/difftool.c +++ b/builtin/difftool.c @@ -552,7 +552,7 @@ static int run_dir_diff(struct repository *repo, struct stat st; if (stat(wtdir.buf, &st)) st.st_mode = 0644; - if (copy_file(rdir.buf, wtdir.buf, + if (copy_file(repo, rdir.buf, wtdir.buf, st.st_mode)) { ret = error("could not copy '%s' to '%s'", wtdir.buf, rdir.buf); goto finish; @@ -658,7 +658,7 @@ static int run_dir_diff(struct repository *repo, warning("%s", ""); err = 1; } else if (unlink(wtdir.buf) || - copy_file(wtdir.buf, rdir.buf, st.st_mode)) + copy_file(repo, wtdir.buf, rdir.buf, st.st_mode)) warning_errno(_("could not copy '%s' to '%s'"), rdir.buf, wtdir.buf); } diff --git a/builtin/worktree.c b/builtin/worktree.c index d21c43fde38b5e..84b01960fb2fba 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -349,7 +349,7 @@ static void copy_sparse_checkout(const char *worktree_git_dir) if (file_exists(from_file)) { if (safe_create_leading_directories(the_repository, to_file) || - copy_file(to_file, from_file, 0666)) + copy_file(the_repository, to_file, from_file, 0666)) error(_("failed to copy '%s' to '%s'; sparse-checkout may not work correctly"), from_file, to_file); } @@ -368,7 +368,7 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir) int bare; if (safe_create_leading_directories(the_repository, to_file) || - copy_file(to_file, from_file, 0666)) { + copy_file(the_repository, to_file, from_file, 0666)) { error(_("failed to copy worktree config from '%s' to '%s'"), from_file, to_file); goto worktree_copy_cleanup; diff --git a/bundle-uri.c b/bundle-uri.c index 3b2e347288c3b7..ef37aebf30f6ac 100644 --- a/bundle-uri.c +++ b/bundle-uri.c @@ -396,7 +396,7 @@ static int copy_uri_to_file(const char *filename, const char *uri) uri = out; /* Copy as a file */ - return copy_file(filename, uri, 0); + return copy_file(the_repository, filename, uri, 0); } static int unbundle_from_file(struct repository *r, const char *file) diff --git a/copy.c b/copy.c index b668209b6c24fd..6074132050ca6f 100644 --- a/copy.c +++ b/copy.c @@ -1,5 +1,3 @@ -#define USE_THE_REPOSITORY_VARIABLE - #include "git-compat-util.h" #include "copy.h" #include "path.h" @@ -35,7 +33,8 @@ static int copy_times(const char *dst, const char *src) return 0; } -int copy_file(const char *dst, const char *src, int mode) +int copy_file(struct repository *repo, + const char *dst, const char *src, int mode) { int fdi, fdo, status; @@ -59,15 +58,16 @@ int copy_file(const char *dst, const char *src, int mode) if (close(fdo) != 0) return error_errno("%s: close error", dst); - if (!status && adjust_shared_perm(the_repository, dst)) + if (!status && adjust_shared_perm(repo, dst)) return -1; return status; } -int copy_file_with_time(const char *dst, const char *src, int mode) +int copy_file_with_time(struct repository *repo, + const char *dst, const char *src, int mode) { - int status = copy_file(dst, src, mode); + int status = copy_file(repo, dst, src, mode); if (!status) return copy_times(dst, src); return status; diff --git a/copy.h b/copy.h index 2af77cba8649b3..1059b118d615ec 100644 --- a/copy.h +++ b/copy.h @@ -1,10 +1,14 @@ #ifndef COPY_H #define COPY_H +struct repository; + #define COPY_READ_ERROR (-2) #define COPY_WRITE_ERROR (-3) int copy_fd(int ifd, int ofd); -int copy_file(const char *dst, const char *src, int mode); -int copy_file_with_time(const char *dst, const char *src, int mode); +int copy_file(struct repository *repo, + const char *dst, const char *src, int mode); +int copy_file_with_time(struct repository *repo, + const char *dst, const char *src, int mode); #endif /* COPY_H */ diff --git a/refs/files-backend.c b/refs/files-backend.c index 3df56c25c8c585..442c98414ecc08 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -1736,7 +1736,7 @@ static int files_copy_or_rename_ref(struct ref_store *ref_store, goto out; } - if (copy && log && copy_file(tmp_renamed_log.buf, sb_oldref.buf, 0644)) { + if (copy && log && copy_file(refs->base.repo, tmp_renamed_log.buf, sb_oldref.buf, 0644)) { ret = error("unable to copy logfile logs/%s to logs/"TMP_RENAMED_LOG": %s", oldrefname, strerror(errno)); goto out; diff --git a/rerere.c b/rerere.c index 8232542585cad4..bf5cfc6e513103 100644 --- a/rerere.c +++ b/rerere.c @@ -756,7 +756,7 @@ static void do_rerere_one_path(struct index_state *istate, /* Has the user resolved it already? */ if (variant >= 0) { if (!handle_file(istate, path, NULL, NULL)) { - copy_file(rerere_path(&buf, id, "postimage"), path, 0666); + copy_file(the_repository, rerere_path(&buf, id, "postimage"), path, 0666); id->collection->status[variant] |= RR_HAS_POSTIMAGE; fprintf_ln(stderr, _("Recorded resolution for '%s'."), path); free_rerere_id(rr_item); diff --git a/sequencer.c b/sequencer.c index 1355a99a092268..63bc1ef215b3fc 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2419,7 +2419,7 @@ static int do_pick_commit(struct repository *r, } else { const char *dest = git_path_squash_msg(r); unlink(dest); - if (copy_file(dest, rebase_path_squash_msg(), 0666)) { + if (copy_file(r, dest, rebase_path_squash_msg(), 0666)) { res = error(_("could not copy '%s' to '%s'"), rebase_path_squash_msg(), dest); goto leave; @@ -3864,11 +3864,11 @@ static int error_failed_squash(struct repository *r, int subject_len, const char *subject) { - if (copy_file(rebase_path_message(), rebase_path_squash_msg(), 0666)) + if (copy_file(r, rebase_path_message(), rebase_path_squash_msg(), 0666)) return error(_("could not copy '%s' to '%s'"), rebase_path_squash_msg(), rebase_path_message()); unlink(git_path_merge_msg(r)); - if (copy_file(git_path_merge_msg(r), rebase_path_message(), 0666)) + if (copy_file(r, git_path_merge_msg(r), rebase_path_message(), 0666)) return error(_("could not copy '%s' to '%s'"), rebase_path_message(), git_path_merge_msg(r)); diff --git a/setup.c b/setup.c index 0de56a074f7c15..91d61a5939067e 100644 --- a/setup.c +++ b/setup.c @@ -2331,7 +2331,7 @@ static void copy_templates_1(struct repository *repo, strbuf_release(&lnk); } else if (S_ISREG(st_template.st_mode)) { - if (copy_file(path->buf, template_path->buf, st_template.st_mode)) + if (copy_file(repo, path->buf, template_path->buf, st_template.st_mode)) die_errno(_("cannot copy '%s' to '%s'"), template_path->buf, path->buf); } From 7b2648f7454e4d2bfbb78e303ec23d9997e4c985 Mon Sep 17 00:00:00 2001 From: Sahitya Chandra Date: Sat, 18 Jul 2026 13:44:49 +0530 Subject: [PATCH 32/35] wt-status: avoid repeated insertion for untracked paths wt_status_collect_untracked() copies entries from dir.entries and dir.ignored into string_lists using string_list_insert(). At first glance this seems quadratic, because inserting into the sorted list may shift the backing array, incurring O(n) work for each insert. In practice, though, the entries in the dir struct are already sorted, so we should not have to shift the array and only pay the O(log n) lookup cost for each insertion. But this is subtle and depends on the behavior of fill_directory(). Collect the entries with string_list_append() instead, then sort and deduplicate each list once with string_list_sort_u(). This preserves the sorted, duplicate-free result while making the collection strategy explicit. Signed-off-by: Sahitya Chandra Signed-off-by: Junio C Hamano --- wt-status.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/wt-status.c b/wt-status.c index 58461e02f886ae..57772c7501fdba 100644 --- a/wt-status.c +++ b/wt-status.c @@ -832,14 +832,16 @@ static void wt_status_collect_untracked(struct wt_status *s) for (i = 0; i < dir.nr; i++) { struct dir_entry *ent = dir.entries[i]; if (index_name_is_other(istate, ent->name, ent->len)) - string_list_insert(&s->untracked, ent->name); + string_list_append(&s->untracked, ent->name); } + string_list_sort_u(&s->untracked, 0); for (i = 0; i < dir.ignored_nr; i++) { struct dir_entry *ent = dir.ignored[i]; if (index_name_is_other(istate, ent->name, ent->len)) - string_list_insert(&s->ignored, ent->name); + string_list_append(&s->ignored, ent->name); } + string_list_sort_u(&s->ignored, 0); dir_clear(&dir); From 251e7af9924f9d3132a7b2f1f0f9563c829bc419 Mon Sep 17 00:00:00 2001 From: "brian m. carlson" Date: Sun, 19 Jul 2026 01:08:41 +0000 Subject: [PATCH 33/35] hash: initialize context before cloning Our C-based clone helper requires that the context be initialized, but we neglect to do that in our Clone implementation for CryptoHasher. This does not matter when using our default block SHA-256 implementation, but it does cause a crash when using OpenSSL as the backend. Fix this by properly initializing the context before cloning into it. Signed-off-by: brian m. carlson Signed-off-by: Junio C Hamano --- src/hash.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/hash.rs b/src/hash.rs index dea2998de4cadd..4d14e4b4fabc8b 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -181,7 +181,10 @@ impl CryptoDigest for CryptoHasher { impl Clone for CryptoHasher { fn clone(&self) -> Self { let ctx = unsafe { c::git_hash_alloc() }; - unsafe { c::git_hash_clone(ctx, self.ctx) }; + unsafe { + c::git_hash_init(ctx, self.algo.hash_algo_ptr()); + c::git_hash_clone(ctx, self.ctx) + }; Self { algo: self.algo, ctx, From fdfcd7543e8c2c045ea215b17e6e772a9770018a Mon Sep 17 00:00:00 2001 From: "brian m. carlson" Date: Sun, 19 Jul 2026 01:08:42 +0000 Subject: [PATCH 34/35] rust: discard hash context when finished When we allocate a context but then abandon it, we never discard it, which means that the underlying crypto library context may leak. This doesn't happen with our default block code, but it may with OpenSSL. Note that we do call git_hash_free, which frees the memory we called from git_hash_alloc, but doesn't discard the underlying context itself. This can be seen with the following command when compiling with OpenSSL and running with nightly Rust: RUSTFLAGS='-Z sanitizer=leak' cargo test Discard the context in our context handler. Note that it is fine to do so even after finalizing the context, so our final functions which take self instead of &mut self will not mishandle memory. Signed-off-by: brian m. carlson Signed-off-by: Junio C Hamano --- src/hash.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/hash.rs b/src/hash.rs index 4d14e4b4fabc8b..e1f2d31fc36239 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -194,7 +194,10 @@ impl Clone for CryptoHasher { impl Drop for CryptoHasher { fn drop(&mut self) { - unsafe { c::git_hash_free(self.ctx) }; + unsafe { + c::git_hash_discard(self.ctx); + c::git_hash_free(self.ctx); + }; } } @@ -356,6 +359,7 @@ pub mod c { pub fn git_hash_clone(dst: *mut c_void, src: *const c_void); pub fn git_hash_update(ctx: *mut c_void, inp: *const c_void, len: usize); pub fn git_hash_final(hash: *mut u8, ctx: *mut c_void); + pub fn git_hash_discard(ctx: *mut c_void); pub fn git_hash_final_oid(hash: *mut c_void, ctx: *mut c_void); } } @@ -450,6 +454,7 @@ mod tests { h.update(&data[2..]); let h2 = h.clone(); + let h3 = h2.clone(); let actual_oid = h.into_oid(); assert_eq!(**oid, actual_oid); @@ -463,6 +468,7 @@ mod tests { let actual_oid = h.into_oid(); assert_eq!(**oid, actual_oid); + std::mem::drop(h3); } } } From 13c7afec212fc97ce257d15601659314c6673d6c Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 27 Jul 2026 09:08:48 -0700 Subject: [PATCH 35/35] The 8th batch Signed-off-by: Junio C Hamano --- Documentation/RelNotes/2.56.0.adoc | 51 ++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/Documentation/RelNotes/2.56.0.adoc b/Documentation/RelNotes/2.56.0.adoc index 811f74bc7dbcb6..cf2da31b1a54a9 100644 --- a/Documentation/RelNotes/2.56.0.adoc +++ b/Documentation/RelNotes/2.56.0.adoc @@ -56,6 +56,11 @@ UI, Workflows & Features standardized to make them consistent with each other and with other commands. + * 'git log --graph' has been modified to visually distinguish parentless + 'root' commits (and commits that become roots due to history + simplification) by indenting them, preventing them from appearing + falsely related to unrelated commits rendered immediately above them. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -206,6 +211,36 @@ Performance, Internal Implementation, Development Support etc. pathspec filtering, which was lost when the streaming walk was refactored. + * The ref subsystem and the worktree API have been refactored to pass a + repository pointer down the call chain, allowing them to drop + references to the global 'the_repository' variable. As part of this, + the handling of the 'core.packedRefsTimeout' configuration has been + moved into the per-repository ref store structure. + + * 'git branch --contains' and 'git for-each-ref --contains' have been + optimized to use the memoized commit traversal previously used only by + 'git tag --contains', significantly speeding up connectivity checks + across many candidate refs with shared history. + + * The passing of push destination specifications in the 'remote-curl' + helper has been simplified by removing the explicit 'count' parameter + and relying on the NULL-termination of the array. + + * The dependency on the global 'the_repository' variable in the + 'refspec.c' API has been removed by passing the hash algorithm + explicitly to refspec-parsing functions and storing it in 'struct + refspec'. + + * The enumeration of untracked and ignored files in 'git status' has + been optimized by avoiding quadratic complexity when inserting into + string lists, reducing the construction cost from O(n^2) to O(n log + n). + + * The copy_file() and copy_file_with_time() functions have been + refactored to take a repository parameter, allowing the removal of the + implicit dependency on the global 'the_repository' variable in + 'copy.c'. + Fixes since v2.55 ----------------- @@ -350,3 +385,19 @@ Fixes since v2.55 'git submodule update' command until it was broken in a modernization of the option-parsing code, has been restored. (merge ff1da37f58 dm/submodule-update-i-shorthand later to maint). + + * An accidental use of the '%zu' format specifier in 'git + submodule--helper' has been corrected to use 'PRIuMAX' and cast the + value to 'uintmax_t' to avoid portability issues. + (merge 3279c13c00 jc/submodule-helper-avoid-zu later to maint). + + * The rebase post-rewrite notes-copying logic has been corrected. When + a commit is dropped during rebase (e.g., because its changes are + already upstream), it is no longer recorded as rewritten, preventing + its notes from being copied to an unrelated commit. + (merge 42554b78fd pw/rebase-drop-notes-with-commit later to maint). + + * A few memory problems in the Rust interface to C hash functions have + been corrected. The 'Clone' implementation of 'CryptoHasher' now + properly initializes the context before cloning, and its 'Drop' + implementation now discards the context to prevent leaks.