From df67d73ca3268eec5c924d6fe9d2c050ce23f3b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 May 2026 15:21:11 +0200 Subject: [PATCH 001/280] config: retry acquiring config.lock, configurable via core.configLockTimeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Concurrent config writers race for the ".lock" file, which is taken with open(O_EXCL) and no retry, so the losers fail right away with "could not lock config file". This shows up with parallel "git worktree add -b" against the same repository: each one writes a couple of branch.* keys and the losers fail at random. Worse, "git worktree add" doesn't propagate that failure to its exit code, so the tracking config is silently dropped. (The swallowed error is a separate bug.) Retry instead of giving up on the first EEXIST. The lock is only held while rewriting a small file, so the loser only has to wait out the other writers. Same approach as 4ff0f01cb7 (refs: retry acquiring reference locks for 100ms, 2017-08-21). On the semantics: the on-disk config is read only after the lock is taken, so writers touching different keys can't lose each other's change. Writers touching the same key still get last-writer-wins, but that is already the case today and would need a compare-and-swap config API to fix. The retry only turns hard failures into successes. Default to 1000ms, like core.packedRefsTimeout: same shape of problem, one shared file everyone serializes through. A larger timeout only costs anything when a stale lock is left behind by a crash, which is rare; a smaller one fails spuriously on slow filesystems (NTFS has been seen needing more than 100ms). Make it configurable as core.configLockTimeout. There is no chicken-and-egg problem: we read the config before we lock it. microsoft/git carries a similar patch (core.configWriteLockTimeoutMS, default off) for Scalar's tests. Defaulting to non-zero here because the worktree case fails silently. Helped-by: Patrick Steinhardt Helped-by: Johannes Schindelin Signed-off-by: Jörg Thalheim Signed-off-by: Junio C Hamano --- Documentation/config/core.adoc | 8 ++++++++ config.c | 24 ++++++++++++++++++++++-- t/t1300-config.sh | 17 +++++++++++++++++ t/t3200-branch.sh | 6 ++++-- t/t5505-remote.sh | 3 ++- 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc index a0ebf03e2eb050..340329edc38143 100644 --- a/Documentation/config/core.adoc +++ b/Documentation/config/core.adoc @@ -589,6 +589,14 @@ core.packedRefsTimeout:: all; -1 means to try indefinitely. Default is 1000 (i.e., retry for 1 second). +core.configLockTimeout:: + The length of time, in milliseconds, to retry when trying to + lock a configuration file for writing. Value 0 means not to + retry at all; -1 means to try indefinitely. Default is 1000 + (i.e., retry for 1 second). This is read from the configuration + that is already on disk before the lock is taken, so it can be + set persistently like any other option. + core.pager:: Text viewer for use by Git commands (e.g., 'less'). The value is meant to be interpreted by the shell. The order of preference diff --git a/config.c b/config.c index 156f2a24fa0027..ac9563781bfb21 100644 --- a/config.c +++ b/config.c @@ -2903,6 +2903,24 @@ char *git_config_prepare_comment_string(const char *comment) return prepared; } +/* + * How long to retry acquiring config.lock when another process holds + * it. Default matches core.packedRefsTimeout; override via + * core.configLockTimeout. + */ +static long config_lock_timeout_ms(struct repository *r) +{ + static int configured; + static int timeout_ms = 1000; + + if (!configured) { + repo_config_get_int(r, "core.configlocktimeout", &timeout_ms); + configured = 1; + } + + return timeout_ms; +} + static void validate_comment_string(const char *comment) { size_t leading_blanks; @@ -2986,7 +3004,8 @@ int repo_config_set_multivar_in_file_gently(struct repository *r, * The lock serves a purpose in addition to locking: the new * contents of .git/config will be written into it. */ - fd = hold_lock_file_for_update(&lock, config_filename, 0); + fd = hold_lock_file_for_update_timeout(&lock, config_filename, 0, + config_lock_timeout_ms(r)); if (fd < 0) { error_errno(_("could not lock config file %s"), config_filename); ret = CONFIG_NO_LOCK; @@ -3331,7 +3350,8 @@ static int repo_config_copy_or_rename_section_in_file( if (!config_filename) config_filename = filename_buf = repo_git_path(r, "config"); - out_fd = hold_lock_file_for_update(&lock, config_filename, 0); + out_fd = hold_lock_file_for_update_timeout(&lock, config_filename, 0, + config_lock_timeout_ms(r)); if (out_fd < 0) { ret = error(_("could not lock config file %s"), config_filename); goto out; diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 128971ee12fa6c..12a1cb85809b8b 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -2939,4 +2939,21 @@ test_expect_success 'writing value with trailing CR not stripped on read' ' test_cmp expect actual ' +test_expect_success 'writing config fails immediately with core.configLockTimeout=0' ' + test_when_finished "rm -f .git/config.lock" && + >.git/config.lock && + test_must_fail git -c core.configLockTimeout=0 config foo.bar baz 2>err && + test_grep "could not lock config file" err +' + +test_expect_success 'writing config retries until lock is released' ' + test_when_finished "rm -f .git/config.lock" && + >.git/config.lock && + { + ( sleep 1 && rm -f .git/config.lock ) & + } && + git -c core.configLockTimeout=5000 config retried.key value && + test "$(git config retried.key)" = value +' + test_done diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index e7829c2c4bfdc3..5f8a31c21d0eaf 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -1037,7 +1037,8 @@ test_expect_success '--set-upstream-to fails on locked config' ' test_when_finished "rm -f .git/config.lock" && >.git/config.lock && git branch locked && - test_must_fail git branch --set-upstream-to locked 2>err && + test_must_fail git -c core.configLockTimeout=0 \ + branch --set-upstream-to locked 2>err && test_grep "could not lock config file .git/config" err ' @@ -1068,7 +1069,8 @@ test_expect_success '--unset-upstream should fail if config is locked' ' test_when_finished "rm -f .git/config.lock" && git branch --set-upstream-to locked && >.git/config.lock && - test_must_fail git branch --unset-upstream 2>err && + test_must_fail git -c core.configLockTimeout=0 \ + branch --unset-upstream 2>err && test_grep "could not lock config file .git/config" err ' diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh index e592c0bcde91e9..aea9222649f7bb 100755 --- a/t/t5505-remote.sh +++ b/t/t5505-remote.sh @@ -1327,7 +1327,8 @@ test_expect_success 'remote set-url with locked config' ' test_when_finished "rm -f .git/config.lock" && git config --get-all remote.someremote.url >expect && >.git/config.lock && - test_must_fail git remote set-url someremote baz && + test_must_fail git -c core.configLockTimeout=0 \ + remote set-url someremote baz && git config --get-all remote.someremote.url >actual && cmp expect actual ' From 5bd39784cda151203aa6d97e24c21bf7fddc11de Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Fri, 12 Jun 2026 17:49:12 -0400 Subject: [PATCH 002/280] 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 003/280] 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 004/280] 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 eede1e69fe4b852f14cade6c18abd156fbcc8cd1 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Mon, 6 Jul 2026 13:50:52 +0000 Subject: [PATCH 005/280] sparse-index: avoid crash on intent-to-add entry outside the cone When collapsing a full index to a sparse index, the recursive convert_to_sparse_rec() walks the cache tree to determine if any of the cache tree entries can be used to represent a sparse directory. As it goes, the method tracks how many cache entries are being represented by the cache tree entry. The cache tree node's 'entry_count' represents how many cache entries are covered by the node. However, this value can be negative, representing that a node is invalid, and is no longer reflecting the number of cache entries fit within. This can happen when the user uses 'git add --intent-to-add' to mark an untracked file with the intent-to-add bit to avoid committing without finishing the add. When such an intent-to-add file exists and the sparse-checkout changes to no longer contain its parent directory, this leads to a segfault. Two tests are added to demonstrate this fault: * One test is added to t3705-add-sparse-checkout.sh to demonstrate how 'git add' behaves with sparse-checkout. * One test is added to t1092-sparse-checkout-compatibility.sh to demonstrate the interaction with the sparse index and to compare it directly to how the commands behave with a full index or no sparse-checkout. The fix involves engaging with the loop that iterates over all cache entries within the parent cache tree node (from 'start' to 'end') and to set the 'span' variable slightly earlier. At this point, the cache entry is for a file that is at least one directory deeper than the current cache tree node. The path is also not in the sparse-checkout because of an earlier path_in_sparse_checkout() check above the loop. So we are trying to collapse this directory by recursively calling convert_to_sparse_rec() over that span of entries, but the negative value prevents us from predicting that number without scanning. Theoretically, we could scan to find the range of entries that match this directory and determine if they truly do have an intent-to-add bit and then collapse as many child trees as possible (the ones with valid cache tree nodes). That would be a non-trivial change for performance-only benefit. Since this combination of the intent-to-add and sparse index features has so far gone undetected by real users, this scenario is unlikely to be worth such a change. We settle for the simplest change that prevents a bug: don't try to collapse a node that is invalid for this reason. The tests that would demonstrate a segfault now pass. Further, they demonstrate that the intent-to-add bit persists in the index file after changing the sparse-checkout scope. The test in t1092 demonstrates how some sparse directories could be collapsed further with a more involved fix, if so desired in the future. Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- sparse-index.c | 9 ++++- t/t1092-sparse-checkout-compatibility.sh | 48 ++++++++++++++++++++++++ t/t3705-add-sparse-checkout.sh | 26 +++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/sparse-index.c b/sparse-index.c index 1ed769b78d8de1..c1fa231a89fc07 100644 --- a/sparse-index.c +++ b/sparse-index.c @@ -113,10 +113,17 @@ static int convert_to_sparse_rec(struct index_state *istate, continue; } + span = ct->down[pos]->cache_tree->entry_count; + if (span < 0) { + /* cache-tree entry is invalidated, cannot collapse. */ + istate->cache[num_converted++] = ce; + i++; + continue; + } + strbuf_setlen(&child_path, 0); strbuf_add(&child_path, ce->name, slash - ce->name + 1); - span = ct->down[pos]->cache_tree->entry_count; count = convert_to_sparse_rec(istate, num_converted, i, i + span, child_path.buf, child_path.len, diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh index 8186da5c887c56..c433de2c1e02cb 100755 --- a/t/t1092-sparse-checkout-compatibility.sh +++ b/t/t1092-sparse-checkout-compatibility.sh @@ -384,6 +384,54 @@ test_expect_success 'add, commit, checkout' ' test_all_match git checkout - ' +test_expect_success 'intent-to-add entries outside sparse-checkout' ' + init_repos && + + write_script edit-contents <<-\EOF && + echo text >>$1 + EOF + + test_sparse_match git sparse-checkout set deep folder1 && + run_on_sparse mkdir -p folder1 && + run_on_all ../edit-contents folder1/newita && + test_sparse_match git add -N folder1/newita && + + test_sparse_match git sparse-checkout set deep && + test_sparse_match git status --porcelain=v2 && + test_sparse_match git ls-files --stage +' + +test_expect_success 'intent-to-add with --sparse outside sparse-checkout' ' + init_repos && + + write_script edit-contents <<-\EOF && + echo text >>$1 + EOF + + run_on_all mkdir -p folder1 && + run_on_all ../edit-contents folder1/newita && + test_all_match git add --sparse --intent-to-add folder1/newita && + + test_all_match git status --porcelain=v2 && + test_all_match git ls-files --stage && + test_all_match git diff --cached --stat && + + # Ensure sparse index stores correct sparse directories and + # intent-to-add path. + git -C sparse-index ls-files --format="%(path)" --sparse >out && + + # These paths should be present in index as-is. + test_grep "^before/\$" out && + test_grep "^folder1/newita\$" out && + test_grep "^folder2/\$" out && + test_grep "^x/\$" out && + + # folder/0/ could theoretically be collapsed to a sparse + # directory entry, but the current implementation avoids the + # reduction because of folder1/newita + test_grep "^folder1/0/0/0\$" out +' + test_expect_success 'git add, checkout, and reset with -p' ' init_repos && diff --git a/t/t3705-add-sparse-checkout.sh b/t/t3705-add-sparse-checkout.sh index 53a4782267b705..cf3f42a353da78 100755 --- a/t/t3705-add-sparse-checkout.sh +++ b/t/t3705-add-sparse-checkout.sh @@ -233,4 +233,30 @@ test_expect_success 'refuse to add non-skip-worktree file from sparse dir' ' test_cmp expect stderr ' +test_expect_success 'intent-to-add entry and sparse index' ' + test_when_finished "git sparse-checkout disable" && + test_when_finished "git reset --hard" && + + git sparse-checkout disable && + mkdir -p in out && + echo base >in/file && + echo base >out/file && + git add in/file out/file && + git commit -m "in and out directories" && + + # enable sparse-checkout, but with all child directories. + git config index.sparse true && + git sparse-checkout set in out && + + # create a new path and set intent-to-add bit + echo new >out/newita && + git add -N out/newita && + + # collapse sparse-checkout, and make sure that the sparse index + # maintains the intent-to-add bit. + git sparse-checkout set in && + git ls-files --error-unmatch out/newita && + git status --porcelain +' + test_done From 7454b68b6a08abdc87b8fea6ebc2c54f334860af Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Mon, 6 Jul 2026 17:34:01 +0000 Subject: [PATCH 006/280] t0213: skip ancestry tests under user-mode emulation The tests added in 3c8c638df6 (t0213: add trace2 cmd_ancestry tests, 2026-02-13) expect the cmd_ancestry event to name "test-tool" and "git". On Linux those names come from the "comm" field of /proc//stat. Under user-mode emulation (e.g. qemu-user) /proc reports the emulator ("qemu-riscv64") instead, so the event is still emitted, the TRACE2_ANCESTRY probe enables the tests, and tests 2-5 fail even though they pass on native riscv64. Require the probe to see "test-tool" in the ancestry of a test-tool spawned from test-tool, so the tests skip when the names are unreliable. Cc: Matthew John Cheetham Signed-off-by: Jamie Magee Signed-off-by: Junio C Hamano --- t/t0213-trace2-ancestry.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/t/t0213-trace2-ancestry.sh b/t/t0213-trace2-ancestry.sh index a2b9536da83152..2eb86c195274c2 100755 --- a/t/t0213-trace2-ancestry.sh +++ b/t/t0213-trace2-ancestry.sh @@ -31,12 +31,15 @@ PATH="$TTDIR:$PATH" && export PATH # no cmd_ancestry event is emitted. We detect this at runtime and # skip the format-specific tests accordingly. -# Determine if cmd_ancestry is supported on this platform. +# Enable these tests only when cmd_ancestry reports real process names. +# The procinfo stub emits no event; under user-mode emulation (e.g. +# qemu-user) /proc reports the emulator, not the guest. Spawn test-tool +# from test-tool and require "test-tool" in the child's ancestry. test_expect_success 'detect cmd_ancestry support' ' test_when_finished "rm -f trace.detect" && GIT_TRACE2_BRIEF=1 GIT_TRACE2="$(pwd)/trace.detect" \ - test-tool trace2 001return 0 && - if grep -q "^cmd_ancestry" trace.detect + test-tool trace2 004child test-tool trace2 001return 0 && + if grep -q "^cmd_ancestry.*test-tool" trace.detect then test_set_prereq TRACE2_ANCESTRY fi From 368565e55d5cff377efaee5cf86f092709480314 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 13 Jul 2026 07:52:04 +0200 Subject: [PATCH 007/280] t7900: simplify how we check for maintenance tasks We have several tests in t7900 that verify whether specific maintenance tasks did or did not run. This is done rather ad-hoc by checking for spawned Git commands, which is awfully fragile: - We have to adjust tests whenever arguments to the spawned Git commands change. - We don't have a way to verify that negative matches are still working as expected. - We rely on maintenance tasks spawning a Git command in the first place. We can do much better though, as we already have trace2 regions for each of the maintenance tasks. Introduce a helper function that extracts all such regions so that we can get a direct list of all maintenance tasks that a certain command ran. Adapt tests that care about whether or not a specific task ran to use this new helper. Note that many tests still use `test_subcommand` though, as they really care about the exact command that was executed. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/t7900-maintenance.sh | 194 ++++++++++++++++++++++------------------- 1 file changed, 102 insertions(+), 92 deletions(-) diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh index d7f82e1bec163f..129829f1f42f34 100755 --- a/t/t7900-maintenance.sh +++ b/t/t7900-maintenance.sh @@ -23,6 +23,12 @@ test_xmllint () { fi } +test_maintenance_tasks () { + cat >expect && + sed -ne "s/.*\"region_enter\".*\"category\":\"maintenance\([^\"]*\)\".*\"label\":\"\([^\"][^\"]*\)\".*/\2\1/p" "$1" >actual && + test_cmp expect actual +} + test_lazy_prereq SYSTEMD_ANALYZE ' systemd-analyze verify /lib/systemd/system/basic.target ' @@ -180,8 +186,9 @@ test_expect_success 'maintenance..enabled' ' git config maintenance.gc.enabled false && git config maintenance.commit-graph.enabled true && GIT_TRACE2_EVENT="$(pwd)/run-config.txt" git maintenance run 2>err && - test_subcommand ! git gc --quiet ' ' @@ -189,16 +196,20 @@ test_expect_success 'run --task=' ' git maintenance run --task=commit-graph 2>/dev/null && GIT_TRACE2_EVENT="$(pwd)/run-gc.txt" \ git maintenance run --task=gc 2>/dev/null && - GIT_TRACE2_EVENT="$(pwd)/run-commit-graph.txt" \ - git maintenance run --task=commit-graph 2>/dev/null && GIT_TRACE2_EVENT="$(pwd)/run-both.txt" \ git maintenance run --task=commit-graph --task=gc 2>/dev/null && - test_subcommand ! git gc --quiet --no-detach --skip-foreground-tasks daily -> hourly' ' GIT_TRACE2_EVENT="$(pwd)/hourly.txt" \ git maintenance run --schedule=hourly 2>/dev/null && - test_subcommand git prune-packed --quiet /dev/null && - test_subcommand git prune-packed --quiet /dev/null && - test_subcommand git prune-packed --quiet expect && rm -f trace2.txt && GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ git -c maintenance.strategy=$STRATEGY maintenance run --quiet "$@" && - sed -n 's/{"event":"child_start","sid":"[^/"]*",.*,"argv":\["\(.*\)\"]}/\1/p' actual - test_cmp expect actual + test_maintenance_tasks trace2.txt } test_expect_success 'maintenance.strategy is respected' ' @@ -1017,48 +1031,44 @@ test_expect_success 'maintenance.strategy is respected' ' test_grep "unknown maintenance strategy: .unknown." err && test_strategy incremental <<-\EOF && - git pack-refs --all --prune - git reflog expire --all - git gc --quiet --no-detach --skip-foreground-tasks + gc foreground + gc EOF test_strategy incremental --schedule=weekly <<-\EOF && - git pack-refs --all --prune - git prune-packed --quiet - git multi-pack-index write --no-progress - git multi-pack-index expire --no-progress - git multi-pack-index repack --no-progress --batch-size=1 - git commit-graph write --split --reachable --no-progress + pack-refs foreground + prefetch + loose-objects + incremental-repack + commit-graph EOF test_strategy gc <<-\EOF && - git pack-refs --all --prune - git reflog expire --all - git gc --quiet --no-detach --skip-foreground-tasks + gc foreground + gc EOF test_strategy gc --schedule=weekly <<-\EOF && - git pack-refs --all --prune - git reflog expire --all - git gc --quiet --no-detach --skip-foreground-tasks + gc foreground + gc EOF test_strategy geometric <<-\EOF && - git pack-refs --all --prune - git reflog expire --all - git repack -d -l --geometric=2 --quiet --write-midx - git commit-graph write --split --reachable --no-progress - git worktree prune --expire 3.months.ago - git rerere gc + pack-refs foreground + reflog-expire foreground + geometric-repack + commit-graph + worktree-prune + rerere-gc EOF test_strategy geometric --schedule=weekly <<-\EOF - git pack-refs --all --prune - git reflog expire --all - git repack -d -l --geometric=2 --quiet --write-midx - git commit-graph write --split --reachable --no-progress - git worktree prune --expire 3.months.ago - git rerere gc + pack-refs foreground + reflog-expire foreground + geometric-repack + commit-graph + worktree-prune + rerere-gc EOF ) ' From 37deb9b4be807643ef264738f7fa3dc97588e33d Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 13 Jul 2026 07:52:05 +0200 Subject: [PATCH 008/280] odb: run "pre-auto-gc" hook for all maintenance tasks The "pre-auto-gc" hook is supposed to run before auto-maintenance starts. The intent of this is to give users the ability to intercept running maintenance in case there's for example an event that is not supposed to run in parallel with repository maintenance. This hook runs via `need_to_gc()`, which is invoked via two paths: - It is called directly by git-gc(1). - It is called indirectly by git-maintenance(1) via the "gc" task. While the former makes sense, the latter is somewhat off. While the hook is indeed strongly tied to gc'ing a repository, the original intent of the hook is rather to inhibit any kind of automated garbage collection. That noticeably also includes all the other maintenance tasks that our new infrastructure may run, but those aren't getting intercepted at all. The move towards our new maintenance strategy has thus somewhat neutered the effectiveness of the hook. Fix this issue by running the hook before the first auto-maintenance task that would run as determined by the tasks's auto condition. Note that this requires us to lift the call to `run_hooks()` out of `needs_to_gc()`, as the hook would otherwise potentially run multiple times. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/gc.c | 35 +++++++++--- t/t7900-maintenance.sh | 126 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 9 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index d32af422af5e58..77d0a5c9484375 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -709,8 +709,6 @@ static int need_to_gc(struct gc_config *cfg, struct strvec *repack_args) else return 0; - if (run_hooks(the_repository, "pre-auto-gc")) - return 0; return 1; } @@ -933,7 +931,8 @@ int cmd_gc(int argc, /* * Auto-gc should be least intrusive as possible. */ - if (!need_to_gc(&cfg, &repack_args)) { + if (!need_to_gc(&cfg, &repack_args) || + run_hooks(the_repository, "pre-auto-gc")) { ret = 0; goto out; } @@ -1755,11 +1754,18 @@ enum task_phase { TASK_PHASE_BACKGROUND, }; +enum auto_gc_hook_result { + AUTO_GC_HOOK_UNDECIDED = 0, + AUTO_GC_HOOK_RUN = 1, + AUTO_GC_HOOK_SKIP = 2, +}; + static int maybe_run_task(const struct maintenance_task *task, struct repository *repo, struct maintenance_run_opts *opts, struct gc_config *cfg, - enum task_phase phase) + enum task_phase phase, + enum auto_gc_hook_result *auto_gc_hook_result) { int foreground = (phase == TASK_PHASE_FOREGROUND); maintenance_task_fn fn = foreground ? task->foreground : task->background; @@ -1768,9 +1774,19 @@ static int maybe_run_task(const struct maintenance_task *task, if (!fn) return 0; - if (opts->auto_flag && - (!task->auto_condition || !task->auto_condition(cfg))) - return 0; + if (opts->auto_flag) { + if (*auto_gc_hook_result == AUTO_GC_HOOK_SKIP) + return 0; + + if (!task->auto_condition || !task->auto_condition(cfg)) + return 0; + + if (*auto_gc_hook_result == AUTO_GC_HOOK_UNDECIDED) + *auto_gc_hook_result = run_hooks(repo, "pre-auto-gc") ? + AUTO_GC_HOOK_SKIP : AUTO_GC_HOOK_RUN; + if (*auto_gc_hook_result == AUTO_GC_HOOK_SKIP) + return 0; + } trace2_region_enter(region, task->name, repo); if (fn(opts, cfg)) { @@ -1789,6 +1805,7 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts, struct lock_file lk; struct repository *r = the_repository; char *lock_path = xstrfmt("%s/maintenance", r->objects->sources->path); + enum auto_gc_hook_result auto_gc_hook_result = AUTO_GC_HOOK_UNDECIDED; if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) { /* @@ -1808,7 +1825,7 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts, for (size_t i = 0; i < opts->tasks_nr; i++) if (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg, - TASK_PHASE_FOREGROUND)) + TASK_PHASE_FOREGROUND, &auto_gc_hook_result)) result = 1; /* Failure to daemonize is ok, we'll continue in foreground. */ @@ -1820,7 +1837,7 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts, for (size_t i = 0; i < opts->tasks_nr; i++) if (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg, - TASK_PHASE_BACKGROUND)) + TASK_PHASE_BACKGROUND, &auto_gc_hook_result)) result = 1; rollback_lock_file(&lk); diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh index 129829f1f42f34..2d52e7918a33ff 100755 --- a/t/t7900-maintenance.sh +++ b/t/t7900-maintenance.sh @@ -758,6 +758,132 @@ test_expect_success 'geometric repacking honors configured split factor' ' ) ' +test_expect_success 'pre-auto-gc hook runs exactly once' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + write_script .git/hooks/pre-auto-gc <<-\EOF && + echo hook >>hook.log + EOF + + # Satisfy the auto condition for multiple tasks, both in the + # foreground and in the background phase. + git config set maintenance.reflog-expire.auto -1 && + git config set maintenance.geometric-repack.auto -1 && + git config set maintenance.rerere-gc.auto -1 && + + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git maintenance run --auto 2>/dev/null && + + # The successful hook does not inhibit any of the tasks... + test_maintenance_tasks trace2.txt <<-\EOF && + reflog-expire foreground + geometric-repack + rerere-gc + EOF + # ... but it must only have been executed a single time. + test_line_count = 1 hook.log + ) +' + +test_expect_success 'pre-auto-gc hook can inhibit geometric strategy' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + write_script .git/hooks/pre-auto-gc <<-\EOF && + echo hook >>hook.log + exit 1 + EOF + + git config set maintenance.reflog-expire.auto -1 && + git config set maintenance.geometric-repack.auto -1 && + git config set maintenance.rerere-gc.auto -1 && + + # Maintenance would be required... + git maintenance is-needed --auto && + + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git maintenance run --auto 2>/dev/null && + + # ... but the failing hook inhibits all tasks. The hook itself + # is expected to be the only child process being spawned, and + # it must only run a single time. + test_grep "child_start.*pre-auto-gc" trace2.txt && + test_maintenance_tasks trace2.txt <<-\EOF && + EOF + test_line_count = 1 hook.log + ) +' + +test_expect_success 'pre-auto-gc hook can inhibit gc strategy' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + write_script .git/hooks/pre-auto-gc <<-\EOF && + echo hook >>hook.log + exit 1 + EOF + + git config set maintenance.strategy gc && + git config set maintenance.auto false && + git config set gc.auto 3 && + + test_oid_init && + + # We need to create two objects whose hashes start with 17 + # since this is what the gc task counts. + test_commit "$(test_oid blob17_1)" && + test_commit "$(test_oid blob17_2)" && + + # Maintenance would be required... + git maintenance is-needed --auto && + + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git maintenance run --auto 2>/dev/null && + + # ... but the failing hook inhibits all tasks. The hook itself + # is expected to be the only child process being spawned, and + # it must only run a single time. + test_grep "child_start.*pre-auto-gc" trace2.txt && + test_maintenance_tasks trace2.txt <<-\EOF && + EOF + test_subcommand_flex ! git trace2 && + test_line_count = 1 hook.log + ) +' + +test_expect_success 'pre-auto-gc hook does not run when no maintenance is needed' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + write_script .git/hooks/pre-auto-gc <<-\EOF && + echo hook >>hook.log + EOF + test_must_fail git maintenance is-needed --auto && + git maintenance run --auto 2>/dev/null && + test_path_is_missing hook.log + ) +' + +test_expect_success 'pre-auto-gc hook does not run without --auto' ' + test_when_finished "rm -rf repo" && + git init repo && + test_hook -C repo pre-auto-gc <<-\EOF && + echo hook >>hook.log + EOF + ( + cd repo && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git maintenance run 2>/dev/null && + test_grep "\[\"git\",\"repack\"," trace2.txt && + test_path_is_missing hook.log + ) +' + test_expect_success 'pack-refs task' ' for n in $(test_seq 1 5) do From 3f8696804331ee9dacf49fa0c6fec4300a37b709 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 13 Jul 2026 07:52:06 +0200 Subject: [PATCH 009/280] builtin/gc: move worktree and rerere tasks before object optimizations In subsequent patches we'll consolidate all tasks that relate to maintenance of the object database and move it into the "files" backend. The relevant code is somewhat scattered though, as several other tasks are interspersed between. Refactor the code so that all object database optimizations are grouped together, which requires us to move worktree pruning and rerere garbage collection around. In theory, rearranging this code can have an effect on the object database optimizations: - Rerere entries really shouldn't impact garbage collection at all, as these entries are not stored in the object database. - The index and HEAD reference of pruned worktrees may reference objects that become unreachable. That being said, the impact should be overall rather negligible. If the user was asking us to prune objects with immediate expiration time then we might now prune objects that were previously still kept alive by the worktree. But besides being a very specific edge case, it's arguably not even the wrong thing to also prune any potentially-unreachable objects immediately. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/gc.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index 77d0a5c9484375..8f568003eef52f 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -1011,6 +1011,13 @@ int cmd_gc(int argc, if (opts.detach <= 0 && !skip_foreground_tasks) gc_foreground_tasks(&opts, &cfg); + if (cfg.prune_worktrees_expire && + maintenance_task_worktree_prune(&opts, &cfg)) + die(FAILED_RUN, "worktree"); + + if (maintenance_task_rerere_gc(&opts, &cfg)) + die(FAILED_RUN, "rerere"); + if (!the_repository->repository_format_precious_objects) { struct child_process repack_cmd = CHILD_PROCESS_INIT; @@ -1038,13 +1045,6 @@ int cmd_gc(int argc, } } - if (cfg.prune_worktrees_expire && - maintenance_task_worktree_prune(&opts, &cfg)) - die(FAILED_RUN, "worktree"); - - if (maintenance_task_rerere_gc(&opts, &cfg)) - die(FAILED_RUN, "rerere"); - report_garbage = report_pack_garbage; odb_reprepare(the_repository->objects); if (pack_garbage.nr > 0) { From e86fe6fe62d0fb5b54a1afed6166d3e27d28f081 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 13 Jul 2026 07:52:07 +0200 Subject: [PATCH 010/280] builtin/gc: extract object database optimizations into separate function Extract the object database optimization logic from `cmd_gc()` into a new `maintenance_task_odb()` helper function. This is a pure refactoring with no intended functional change. Note that the message that notifies the user about too many loose objects is moved into the new function, as well. It is inherently an implementation detail of how the "files" source works, and as a consequence we'll move it around in a later commit, as well. This reordering means that the warning may now be printed at a different point in time, but it's not expected that this will have any practical implications. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/gc.c | 79 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index 8f568003eef52f..2ff98fa727b1e0 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -839,6 +839,53 @@ static int gc_foreground_tasks(struct maintenance_run_opts *opts, return 0; } +static int maintenance_task_odb(struct maintenance_run_opts *opts, + struct gc_config *cfg, + struct strvec *repack_args) +{ + struct child_process repack_cmd = CHILD_PROCESS_INIT; + int ret; + + if (the_repository->repository_format_precious_objects) + return 0; + + repack_cmd.git_cmd = 1; + repack_cmd.odb_to_close = the_repository->objects; + strvec_pushv(&repack_cmd.args, repack_args->v); + if (run_command(&repack_cmd)) { + ret = error(FAILED_RUN, repack_args->v[0]); + goto out; + } + + if (cfg->prune_expire) { + struct child_process prune_cmd = CHILD_PROCESS_INIT; + + strvec_pushl(&prune_cmd.args, "prune", "--expire", NULL); + /* run `git prune` even if using cruft packs */ + strvec_push(&prune_cmd.args, cfg->prune_expire); + if (opts->quiet) + strvec_push(&prune_cmd.args, "--no-progress"); + if (repo_has_promisor_remote(the_repository)) + strvec_push(&prune_cmd.args, + "--exclude-promisor-objects"); + prune_cmd.git_cmd = 1; + + if (run_command(&prune_cmd)) { + ret = error(FAILED_RUN, prune_cmd.args.v[0]); + goto out; + } + } + + if (opts->auto_flag && too_many_loose_objects(cfg->gc_auto_threshold)) + warning(_("There are too many unreachable loose objects; " + "run 'git prune' to remove them.")); + + ret = 0; + +out: + return ret; +} + int cmd_gc(int argc, const char **argv, const char *prefix, @@ -1018,32 +1065,8 @@ int cmd_gc(int argc, if (maintenance_task_rerere_gc(&opts, &cfg)) die(FAILED_RUN, "rerere"); - if (!the_repository->repository_format_precious_objects) { - struct child_process repack_cmd = CHILD_PROCESS_INIT; - - repack_cmd.git_cmd = 1; - repack_cmd.odb_to_close = the_repository->objects; - strvec_pushv(&repack_cmd.args, repack_args.v); - if (run_command(&repack_cmd)) - die(FAILED_RUN, repack_args.v[0]); - - if (cfg.prune_expire) { - struct child_process prune_cmd = CHILD_PROCESS_INIT; - - strvec_pushl(&prune_cmd.args, "prune", "--expire", NULL); - /* run `git prune` even if using cruft packs */ - strvec_push(&prune_cmd.args, cfg.prune_expire); - if (opts.quiet) - strvec_push(&prune_cmd.args, "--no-progress"); - if (repo_has_promisor_remote(the_repository)) - strvec_push(&prune_cmd.args, - "--exclude-promisor-objects"); - prune_cmd.git_cmd = 1; - - if (run_command(&prune_cmd)) - die(FAILED_RUN, prune_cmd.args.v[0]); - } - } + if (maintenance_task_odb(&opts, &cfg, &repack_args)) + die(NULL); report_garbage = report_pack_garbage; odb_reprepare(the_repository->objects); @@ -1057,10 +1080,6 @@ int cmd_gc(int argc, !opts.quiet && !daemonized ? COMMIT_GRAPH_WRITE_PROGRESS : 0, NULL); - if (opts.auto_flag && too_many_loose_objects(cfg.gc_auto_threshold)) - warning(_("There are too many unreachable loose objects; " - "run 'git prune' to remove them.")); - if (!daemonized) { char *path = repo_git_path(the_repository, "gc.log"); unlink(path); From 4d93bf9873696a33a9959c912e7a3fa4fb56a82b Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 13 Jul 2026 07:52:08 +0200 Subject: [PATCH 011/280] builtin/gc: make repack arguments self-contained When optimizing the object database most of the heavy-lifting is done by git-repack(1). The arguments we pass to this function are assembled in global scope, which is hard to follow. Refactor the logic by moving the vector into `maintenance_task_odb()`. While that means we have to pass more arguments to this function, it has the upside that the logic becomes self-contained without any kind of global interdependencies. This is a pure refactoring with no intended functional change. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/gc.c | 156 +++++++++++++++++++++++++-------------------------- 1 file changed, 75 insertions(+), 81 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index 2ff98fa727b1e0..25a59caea696c9 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -661,7 +661,7 @@ static void add_repack_incremental_option(struct strvec *args) strvec_push(args, "--no-write-bitmap-index"); } -static int need_to_gc(struct gc_config *cfg, struct strvec *repack_args) +static int need_to_gc(struct gc_config *cfg) { /* * Setting gc.auto to 0 or negative can disable the @@ -669,46 +669,8 @@ static int need_to_gc(struct gc_config *cfg, struct strvec *repack_args) */ if (cfg->gc_auto_threshold <= 0) return 0; - - /* - * If there are too many loose objects, but not too many - * packs, we run "repack -d -l". If there are too many packs, - * we run "repack -A -d -l". Otherwise we tell the caller - * there is no need. - */ - if (too_many_packs(cfg)) { - struct string_list keep_pack = STRING_LIST_INIT_NODUP; - - if (cfg->big_pack_threshold) { - find_base_packs(&keep_pack, cfg->big_pack_threshold); - if (keep_pack.nr >= cfg->gc_auto_pack_limit) { - cfg->big_pack_threshold = 0; - string_list_clear(&keep_pack, 0); - find_base_packs(&keep_pack, 0); - } - } else { - struct packed_git *p = find_base_packs(&keep_pack, 0); - uint64_t mem_have, mem_want; - - mem_have = total_ram(); - mem_want = estimate_repack_memory(cfg, p); - - /* - * Only allow 1/2 of memory for pack-objects, leave - * the rest for the OS and other processes in the - * system. - */ - if (!mem_have || mem_want < mem_have / 2) - string_list_clear(&keep_pack, 0); - } - - add_repack_all_option(cfg, &keep_pack, repack_args); - string_list_clear(&keep_pack, 0); - } else if (too_many_loose_objects(cfg->gc_auto_threshold)) - add_repack_incremental_option(repack_args); - else + if (!too_many_packs(cfg) && !too_many_loose_objects(cfg->gc_auto_threshold)) return 0; - return 1; } @@ -841,7 +803,8 @@ static int gc_foreground_tasks(struct maintenance_run_opts *opts, static int maintenance_task_odb(struct maintenance_run_opts *opts, struct gc_config *cfg, - struct strvec *repack_args) + int keep_largest_pack, + int aggressive) { struct child_process repack_cmd = CHILD_PROCESS_INIT; int ret; @@ -851,9 +814,75 @@ static int maintenance_task_odb(struct maintenance_run_opts *opts, repack_cmd.git_cmd = 1; repack_cmd.odb_to_close = the_repository->objects; - strvec_pushv(&repack_cmd.args, repack_args->v); + + strvec_pushl(&repack_cmd.args, "repack", "-d", "-l", NULL); + if (aggressive) { + strvec_push(&repack_cmd.args, "-f"); + if (cfg->aggressive_depth > 0) + strvec_pushf(&repack_cmd.args, "--depth=%d", cfg->aggressive_depth); + if (cfg->aggressive_window > 0) + strvec_pushf(&repack_cmd.args, "--window=%d", cfg->aggressive_window); + } + if (opts->quiet) + strvec_push(&repack_cmd.args, "-q"); + + /* + * There's three cases we need to consider: + * + * - If we're invoked without `--auto` we'll need to perform a full + * repack. + * + * - If we're invoked with `--auto` and there's too many packs, then + * we perform a full repack, as well. + * + * - Otherwise we perform an incremental repack. + */ + if (!opts->auto_flag) { + struct string_list keep_pack = STRING_LIST_INIT_NODUP; + + if (keep_largest_pack != -1) { + if (keep_largest_pack) + find_base_packs(&keep_pack, 0); + } else if (cfg->big_pack_threshold) { + find_base_packs(&keep_pack, cfg->big_pack_threshold); + } + + add_repack_all_option(cfg, &keep_pack, &repack_cmd.args); + string_list_clear(&keep_pack, 0); + } else if (too_many_packs(cfg)) { + struct string_list keep_pack = STRING_LIST_INIT_NODUP; + + if (cfg->big_pack_threshold) { + find_base_packs(&keep_pack, cfg->big_pack_threshold); + if (keep_pack.nr >= cfg->gc_auto_pack_limit) { + cfg->big_pack_threshold = 0; + string_list_clear(&keep_pack, 0); + find_base_packs(&keep_pack, 0); + } + } else { + struct packed_git *p = find_base_packs(&keep_pack, 0); + uint64_t mem_have, mem_want; + + mem_have = total_ram(); + mem_want = estimate_repack_memory(cfg, p); + + /* + * Only allow 1/2 of memory for pack-objects, leave + * the rest for the OS and other processes in the + * system. + */ + if (!mem_have || mem_want < mem_have / 2) + string_list_clear(&keep_pack, 0); + } + + add_repack_all_option(cfg, &keep_pack, &repack_cmd.args); + string_list_clear(&keep_pack, 0); + } else { + add_repack_incremental_option(&repack_cmd.args); + } + if (run_command(&repack_cmd)) { - ret = error(FAILED_RUN, repack_args->v[0]); + ret = error(FAILED_RUN, repack_cmd.args.v[0]); goto out; } @@ -899,7 +928,6 @@ int cmd_gc(int argc, int keep_largest_pack = -1; int skip_foreground_tasks = 0; timestamp_t dummy; - struct strvec repack_args = STRVEC_INIT; struct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT; struct gc_config cfg = GC_CONFIG_INIT; const char *prune_expire_sentinel = "sentinel"; @@ -939,8 +967,6 @@ int cmd_gc(int argc, show_usage_with_options_if_asked(argc, argv, builtin_gc_usage, builtin_gc_options); - strvec_pushl(&repack_args, "repack", "-d", "-l", NULL); - gc_config(&cfg); if (parse_expiry_date(cfg.gc_log_expire, &gc_log_expire_time)) @@ -961,16 +987,6 @@ int cmd_gc(int argc, if (cfg.prune_expire && parse_expiry_date(cfg.prune_expire, &dummy)) die(_("failed to parse prune expiry value %s"), cfg.prune_expire); - if (aggressive) { - strvec_push(&repack_args, "-f"); - if (cfg.aggressive_depth > 0) - strvec_pushf(&repack_args, "--depth=%d", cfg.aggressive_depth); - if (cfg.aggressive_window > 0) - strvec_pushf(&repack_args, "--window=%d", cfg.aggressive_window); - } - if (opts.quiet) - strvec_push(&repack_args, "-q"); - if (opts.auto_flag) { if (cfg.detach_auto && opts.detach < 0) opts.detach = 1; @@ -978,8 +994,7 @@ int cmd_gc(int argc, /* * Auto-gc should be least intrusive as possible. */ - if (!need_to_gc(&cfg, &repack_args) || - run_hooks(the_repository, "pre-auto-gc")) { + if (!need_to_gc(&cfg) || run_hooks(the_repository, "pre-auto-gc")) { ret = 0; goto out; } @@ -991,18 +1006,6 @@ int cmd_gc(int argc, fprintf(stderr, _("Auto packing the repository for optimum performance.\n")); fprintf(stderr, _("See \"git help gc\" for manual housekeeping.\n")); } - } else { - struct string_list keep_pack = STRING_LIST_INIT_NODUP; - - if (keep_largest_pack != -1) { - if (keep_largest_pack) - find_base_packs(&keep_pack, 0); - } else if (cfg.big_pack_threshold) { - find_base_packs(&keep_pack, cfg.big_pack_threshold); - } - - add_repack_all_option(&cfg, &keep_pack, &repack_args); - string_list_clear(&keep_pack, 0); } if (opts.detach > 0) { @@ -1065,7 +1068,7 @@ int cmd_gc(int argc, if (maintenance_task_rerere_gc(&opts, &cfg)) die(FAILED_RUN, "rerere"); - if (maintenance_task_odb(&opts, &cfg, &repack_args)) + if (maintenance_task_odb(&opts, &cfg, keep_largest_pack, aggressive)) die(NULL); report_garbage = report_pack_garbage; @@ -1088,7 +1091,6 @@ int cmd_gc(int argc, out: maintenance_run_opts_release(&opts); - strvec_clear(&repack_args); gc_config_release(&cfg); return 0; } @@ -1291,15 +1293,7 @@ static int maintenance_task_gc_background(struct maintenance_run_opts *opts, static int gc_condition(struct gc_config *cfg) { - /* - * Note that it's fine to drop the repack arguments here, as we execute - * git-gc(1) as a separate child process anyway. So it knows to compute - * these arguments again. - */ - struct strvec repack_args = STRVEC_INIT; - int ret = need_to_gc(cfg, &repack_args); - strvec_clear(&repack_args); - return ret; + return need_to_gc(cfg); } static int prune_packed(struct maintenance_run_opts *opts) From 57ca517baca6c28ed11e923ed7f25bb5c7af8909 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 13 Jul 2026 07:52:09 +0200 Subject: [PATCH 012/280] builtin/gc: inline config values specific to the "files" backend The `struct gc_config` contains a set of values that we read via the Git repository's configuration. Several of those values that are consumed by the object database optimization logic are inherently specific to the "files" config. In a later commit we'll make the logic to optimize object databases pluggable. So by carrying these "files"-backend specific values in the generic config struct means that other backends would have to worry about these values, too. This feels somewhat dirty, as implementation- specific details should live with the backends themselves. Inline these values directly at the call sites that need them instead. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/gc.c | 115 ++++++++++++++++++++++++--------------------------- 1 file changed, 53 insertions(+), 62 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index 25a59caea696c9..5d445edaa06923 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -130,22 +130,11 @@ struct gc_config { unsigned long max_cruft_size; int aggressive_depth; int aggressive_window; - int gc_auto_threshold; - int gc_auto_pack_limit; int detach_auto; char *gc_log_expire; char *prune_expire; char *prune_worktrees_expire; - char *repack_filter; - char *repack_filter_to; char *repack_expire_to; - unsigned long big_pack_threshold; - unsigned long max_delta_cache_size; - /* - * Remove this member from gc_config once repo_settings is passed - * through the callchain. - */ - size_t delta_base_cache_limit; }; #define GC_CONFIG_INIT { \ @@ -154,14 +143,10 @@ struct gc_config { .cruft_packs = 1, \ .aggressive_depth = 50, \ .aggressive_window = 250, \ - .gc_auto_threshold = 6700, \ - .gc_auto_pack_limit = 50, \ .detach_auto = 1, \ .gc_log_expire = xstrdup("1.day.ago"), \ .prune_expire = xstrdup("2.weeks.ago"), \ .prune_worktrees_expire = xstrdup("3.months.ago"), \ - .max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE, \ - .delta_base_cache_limit = DEFAULT_DELTA_BASE_CACHE_LIMIT, \ } static void gc_config_release(struct gc_config *cfg) @@ -169,15 +154,12 @@ static void gc_config_release(struct gc_config *cfg) free(cfg->gc_log_expire); free(cfg->prune_expire); free(cfg->prune_worktrees_expire); - free(cfg->repack_filter); - free(cfg->repack_filter_to); } static void gc_config(struct gc_config *cfg) { const char *value; char *owned = NULL; - unsigned long ulongval; if (!repo_config_get_value(the_repository, "gc.packrefs", &value)) { if (value && !strcmp(value, "notbare")) @@ -192,8 +174,6 @@ static void gc_config(struct gc_config *cfg) repo_config_get_int(the_repository, "gc.aggressivewindow", &cfg->aggressive_window); repo_config_get_int(the_repository, "gc.aggressivedepth", &cfg->aggressive_depth); - repo_config_get_int(the_repository, "gc.auto", &cfg->gc_auto_threshold); - repo_config_get_int(the_repository, "gc.autopacklimit", &cfg->gc_auto_pack_limit); repo_config_get_bool(the_repository, "gc.autodetach", &cfg->detach_auto); repo_config_get_bool(the_repository, "gc.cruftpacks", &cfg->cruft_packs); repo_config_get_ulong(the_repository, "gc.maxcruftsize", &cfg->max_cruft_size); @@ -213,22 +193,6 @@ static void gc_config(struct gc_config *cfg) cfg->gc_log_expire = owned; } - repo_config_get_ulong(the_repository, "gc.bigpackthreshold", &cfg->big_pack_threshold); - repo_config_get_ulong(the_repository, "pack.deltacachesize", &cfg->max_delta_cache_size); - - if (!repo_config_get_ulong(the_repository, "core.deltabasecachelimit", &ulongval)) - cfg->delta_base_cache_limit = ulongval; - - if (!repo_config_get_string(the_repository, "gc.repackfilter", &owned)) { - free(cfg->repack_filter); - cfg->repack_filter = owned; - } - - if (!repo_config_get_string(the_repository, "gc.repackfilterto", &owned)) { - free(cfg->repack_filter_to); - cfg->repack_filter_to = owned; - } - repo_config(the_repository, git_default_config, NULL); } @@ -504,12 +468,12 @@ static struct packed_git *find_base_packs(struct string_list *packs, return base; } -static int too_many_packs(struct gc_config *cfg) +static int too_many_packs(int gc_auto_pack_limit) { struct packed_git *p; int cnt = 0; - if (cfg->gc_auto_pack_limit <= 0) + if (gc_auto_pack_limit <= 0) return 0; repo_for_each_pack(the_repository, p) { @@ -523,7 +487,7 @@ static int too_many_packs(struct gc_config *cfg) */ cnt++; } - return cfg->gc_auto_pack_limit < cnt; + return gc_auto_pack_limit < cnt; } static uint64_t total_ram(void) @@ -571,9 +535,10 @@ static uint64_t total_ram(void) return 0; } -static uint64_t estimate_repack_memory(struct gc_config *cfg, - struct packed_git *pack) +static uint64_t estimate_repack_memory(struct packed_git *pack) { + unsigned long max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE; + unsigned long delta_base_cache_limit = DEFAULT_DELTA_BASE_CACHE_LIMIT; unsigned long nr_objects; size_t os_cache, heap; @@ -584,6 +549,9 @@ static uint64_t estimate_repack_memory(struct gc_config *cfg, if (!pack || !nr_objects) return 0; + repo_config_get_ulong(the_repository, "pack.deltacachesize", &max_delta_cache_size); + repo_config_get_ulong(the_repository, "core.deltabasecachelimit", &delta_base_cache_limit); + /* * First we have to scan through at least one pack. * Assume enough room in OS file cache to keep the entire pack @@ -611,9 +579,9 @@ static uint64_t estimate_repack_memory(struct gc_config *cfg, * read_sha1_file() (either at delta calculation phase, or * writing phase) also fills up the delta base cache */ - heap += cfg->delta_base_cache_limit; + heap += delta_base_cache_limit; /* and of course pack-objects has its own delta cache */ - heap += cfg->max_delta_cache_size; + heap += max_delta_cache_size; return os_cache + heap; } @@ -629,6 +597,12 @@ static void add_repack_all_option(struct gc_config *cfg, struct string_list *keep_pack, struct strvec *args) { + char *repack_filter = NULL; + char *repack_filter_to = NULL; + + repo_config_get_string(the_repository, "gc.repackfilter", &repack_filter); + repo_config_get_string(the_repository, "gc.repackfilterto", &repack_filter_to); + if (cfg->prune_expire && !strcmp(cfg->prune_expire, "now") && !(cfg->cruft_packs && cfg->repack_expire_to)) strvec_push(args, "-a"); @@ -650,10 +624,13 @@ static void add_repack_all_option(struct gc_config *cfg, if (keep_pack) for_each_string_list(keep_pack, keep_one_pack, args); - if (cfg->repack_filter && *cfg->repack_filter) - strvec_pushf(args, "--filter=%s", cfg->repack_filter); - if (cfg->repack_filter_to && *cfg->repack_filter_to) - strvec_pushf(args, "--filter-to=%s", cfg->repack_filter_to); + if (repack_filter && *repack_filter) + strvec_pushf(args, "--filter=%s", repack_filter); + if (repack_filter_to && *repack_filter_to) + strvec_pushf(args, "--filter-to=%s", repack_filter_to); + + free(repack_filter); + free(repack_filter_to); } static void add_repack_incremental_option(struct strvec *args) @@ -661,16 +638,24 @@ static void add_repack_incremental_option(struct strvec *args) strvec_push(args, "--no-write-bitmap-index"); } -static int need_to_gc(struct gc_config *cfg) +static int need_to_gc(struct repository *repo) { + int gc_auto_threshold = 6700; + int gc_auto_pack_limit = 50; + + repo_config_get_int(repo, "gc.auto", &gc_auto_threshold); + repo_config_get_int(repo, "gc.autopacklimit", &gc_auto_pack_limit); + /* * Setting gc.auto to 0 or negative can disable the * automatic gc. */ - if (cfg->gc_auto_threshold <= 0) + if (gc_auto_threshold <= 0) return 0; - if (!too_many_packs(cfg) && !too_many_loose_objects(cfg->gc_auto_threshold)) + if (!too_many_packs(gc_auto_pack_limit) && + !too_many_loose_objects(gc_auto_threshold)) return 0; + return 1; } @@ -807,8 +792,15 @@ static int maintenance_task_odb(struct maintenance_run_opts *opts, int aggressive) { struct child_process repack_cmd = CHILD_PROCESS_INIT; + unsigned long big_pack_threshold = 0; + int gc_auto_threshold = 6700; + int gc_auto_pack_limit = 50; int ret; + repo_config_get_int(the_repository, "gc.auto", &gc_auto_threshold); + repo_config_get_int(the_repository, "gc.autopacklimit", &gc_auto_pack_limit); + repo_config_get_ulong(the_repository, "gc.bigpackthreshold", &big_pack_threshold); + if (the_repository->repository_format_precious_objects) return 0; @@ -843,19 +835,18 @@ static int maintenance_task_odb(struct maintenance_run_opts *opts, if (keep_largest_pack != -1) { if (keep_largest_pack) find_base_packs(&keep_pack, 0); - } else if (cfg->big_pack_threshold) { - find_base_packs(&keep_pack, cfg->big_pack_threshold); + } else if (big_pack_threshold) { + find_base_packs(&keep_pack, big_pack_threshold); } add_repack_all_option(cfg, &keep_pack, &repack_cmd.args); string_list_clear(&keep_pack, 0); - } else if (too_many_packs(cfg)) { + } else if (too_many_packs(gc_auto_pack_limit)) { struct string_list keep_pack = STRING_LIST_INIT_NODUP; - if (cfg->big_pack_threshold) { - find_base_packs(&keep_pack, cfg->big_pack_threshold); - if (keep_pack.nr >= cfg->gc_auto_pack_limit) { - cfg->big_pack_threshold = 0; + if (big_pack_threshold) { + find_base_packs(&keep_pack, big_pack_threshold); + if (keep_pack.nr >= gc_auto_pack_limit) { string_list_clear(&keep_pack, 0); find_base_packs(&keep_pack, 0); } @@ -864,7 +855,7 @@ static int maintenance_task_odb(struct maintenance_run_opts *opts, uint64_t mem_have, mem_want; mem_have = total_ram(); - mem_want = estimate_repack_memory(cfg, p); + mem_want = estimate_repack_memory(p); /* * Only allow 1/2 of memory for pack-objects, leave @@ -905,7 +896,7 @@ static int maintenance_task_odb(struct maintenance_run_opts *opts, } } - if (opts->auto_flag && too_many_loose_objects(cfg->gc_auto_threshold)) + if (opts->auto_flag && too_many_loose_objects(gc_auto_threshold)) warning(_("There are too many unreachable loose objects; " "run 'git prune' to remove them.")); @@ -994,7 +985,7 @@ int cmd_gc(int argc, /* * Auto-gc should be least intrusive as possible. */ - if (!need_to_gc(&cfg) || run_hooks(the_repository, "pre-auto-gc")) { + if (!need_to_gc(the_repository) || run_hooks(the_repository, "pre-auto-gc")) { ret = 0; goto out; } @@ -1291,9 +1282,9 @@ static int maintenance_task_gc_background(struct maintenance_run_opts *opts, return run_command(&child); } -static int gc_condition(struct gc_config *cfg) +static int gc_condition(struct gc_config *cfg UNUSED) { - return need_to_gc(cfg); + return need_to_gc(the_repository); } static int prune_packed(struct maintenance_run_opts *opts) From cc78c5029e62e6e1283c7d05bf60d845922aac9b Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 13 Jul 2026 07:52:10 +0200 Subject: [PATCH 013/280] builtin/gc: introduce object database optimization options Introduce `struct odb_optimize_options` to decouple the options that are specific to optimizing the object database from `struct gc_config`. This structure will be moved into the object database layer in a subsequent commit. Note that there are a small set of backend-specific options in this structure. In an ideal world those of course wouldn't exist, but as we're introducing the object database abstractions retroactively we are somewhat forced to keep them. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/gc.c | 181 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 120 insertions(+), 61 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index 5d445edaa06923..17490106fc9c91 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -593,7 +593,39 @@ static int keep_one_pack(struct string_list_item *item, void *data) return 0; } -static void add_repack_all_option(struct gc_config *cfg, +enum odb_optimize_flags { + /* Enable verbose logging and progress reporting. */ + ODB_OPTIMIZE_VERBOSE = (1 << 0), + + /* Perform auto-maintenance, only optimizing objects as required. */ + ODB_OPTIMIZE_AUTO = (1 << 1), + + /* Recompute existing deltas. */ + ODB_OPTIMIZE_NO_REUSE_DELTAS = (1 << 2), +}; + +struct odb_optimize_options { + enum odb_optimize_flags flags; + const char *prune_expire; + const char *expire_to; + int depth; + int window; + + /* Backend-specific options. */ + int keep_largest_pack; + int cruft_packs; + unsigned long max_cruft_size; +}; + +#define OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, aggressive) \ + .prune_expire = (cfg)->prune_expire, \ + .expire_to = (cfg)->repack_expire_to, \ + .cruft_packs = (cfg)->cruft_packs, \ + .max_cruft_size = (cfg)->max_cruft_size, \ + .window = (aggressive) ? (cfg)->aggressive_window : 0, \ + .depth = (aggressive) ? (cfg)->aggressive_depth : 0 + +static void add_repack_all_option(const struct odb_optimize_options *opts, struct string_list *keep_pack, struct strvec *args) { @@ -603,22 +635,22 @@ static void add_repack_all_option(struct gc_config *cfg, repo_config_get_string(the_repository, "gc.repackfilter", &repack_filter); repo_config_get_string(the_repository, "gc.repackfilterto", &repack_filter_to); - if (cfg->prune_expire && !strcmp(cfg->prune_expire, "now") - && !(cfg->cruft_packs && cfg->repack_expire_to)) + if (opts->prune_expire && !strcmp(opts->prune_expire, "now") && + !(opts->cruft_packs && opts->expire_to)) strvec_push(args, "-a"); - else if (cfg->cruft_packs) { + else if (opts->cruft_packs) { strvec_push(args, "--cruft"); - if (cfg->prune_expire) - strvec_pushf(args, "--cruft-expiration=%s", cfg->prune_expire); - if (cfg->max_cruft_size) + if (opts->prune_expire) + strvec_pushf(args, "--cruft-expiration=%s", opts->prune_expire); + if (opts->max_cruft_size) strvec_pushf(args, "--max-cruft-size=%lu", - cfg->max_cruft_size); - if (cfg->repack_expire_to) - strvec_pushf(args, "--expire-to=%s", cfg->repack_expire_to); + opts->max_cruft_size); + if (opts->expire_to) + strvec_pushf(args, "--expire-to=%s", opts->expire_to); } else { strvec_push(args, "-A"); - if (cfg->prune_expire) - strvec_pushf(args, "--unpack-unreachable=%s", cfg->prune_expire); + if (opts->prune_expire) + strvec_pushf(args, "--unpack-unreachable=%s", opts->prune_expire); } if (keep_pack) @@ -786,10 +818,8 @@ static int gc_foreground_tasks(struct maintenance_run_opts *opts, return 0; } -static int maintenance_task_odb(struct maintenance_run_opts *opts, - struct gc_config *cfg, - int keep_largest_pack, - int aggressive) +static int odb_optimize(struct object_database *odb, + const struct odb_optimize_options *opts) { struct child_process repack_cmd = CHILD_PROCESS_INIT; unsigned long big_pack_threshold = 0; @@ -801,21 +831,20 @@ static int maintenance_task_odb(struct maintenance_run_opts *opts, repo_config_get_int(the_repository, "gc.autopacklimit", &gc_auto_pack_limit); repo_config_get_ulong(the_repository, "gc.bigpackthreshold", &big_pack_threshold); - if (the_repository->repository_format_precious_objects) + if (odb->repo->repository_format_precious_objects) return 0; repack_cmd.git_cmd = 1; repack_cmd.odb_to_close = the_repository->objects; strvec_pushl(&repack_cmd.args, "repack", "-d", "-l", NULL); - if (aggressive) { + if (opts->flags & ODB_OPTIMIZE_NO_REUSE_DELTAS) strvec_push(&repack_cmd.args, "-f"); - if (cfg->aggressive_depth > 0) - strvec_pushf(&repack_cmd.args, "--depth=%d", cfg->aggressive_depth); - if (cfg->aggressive_window > 0) - strvec_pushf(&repack_cmd.args, "--window=%d", cfg->aggressive_window); - } - if (opts->quiet) + if (opts->depth > 0) + strvec_pushf(&repack_cmd.args, "--depth=%d", opts->depth); + if (opts->window > 0) + strvec_pushf(&repack_cmd.args, "--window=%d", opts->window); + if (!(opts->flags & ODB_OPTIMIZE_VERBOSE)) strvec_push(&repack_cmd.args, "-q"); /* @@ -829,47 +858,49 @@ static int maintenance_task_odb(struct maintenance_run_opts *opts, * * - Otherwise we perform an incremental repack. */ - if (!opts->auto_flag) { + if (!(opts->flags & ODB_OPTIMIZE_AUTO)) { struct string_list keep_pack = STRING_LIST_INIT_NODUP; - if (keep_largest_pack != -1) { - if (keep_largest_pack) + if (opts->keep_largest_pack != -1) { + if (opts->keep_largest_pack) find_base_packs(&keep_pack, 0); } else if (big_pack_threshold) { find_base_packs(&keep_pack, big_pack_threshold); } - add_repack_all_option(cfg, &keep_pack, &repack_cmd.args); + add_repack_all_option(opts, &keep_pack, &repack_cmd.args); string_list_clear(&keep_pack, 0); - } else if (too_many_packs(gc_auto_pack_limit)) { - struct string_list keep_pack = STRING_LIST_INIT_NODUP; - - if (big_pack_threshold) { - find_base_packs(&keep_pack, big_pack_threshold); - if (keep_pack.nr >= gc_auto_pack_limit) { - string_list_clear(&keep_pack, 0); - find_base_packs(&keep_pack, 0); + } else { + if (too_many_packs(gc_auto_pack_limit)) { + struct string_list keep_pack = STRING_LIST_INIT_NODUP; + + if (big_pack_threshold) { + find_base_packs(&keep_pack, big_pack_threshold); + if (keep_pack.nr >= gc_auto_pack_limit) { + string_list_clear(&keep_pack, 0); + find_base_packs(&keep_pack, 0); + } + } else { + struct packed_git *p = find_base_packs(&keep_pack, 0); + uint64_t mem_have, mem_want; + + mem_have = total_ram(); + mem_want = estimate_repack_memory(p); + + /* + * Only allow 1/2 of memory for pack-objects, leave + * the rest for the OS and other processes in the + * system. + */ + if (!mem_have || mem_want < mem_have / 2) + string_list_clear(&keep_pack, 0); } - } else { - struct packed_git *p = find_base_packs(&keep_pack, 0); - uint64_t mem_have, mem_want; - - mem_have = total_ram(); - mem_want = estimate_repack_memory(p); - /* - * Only allow 1/2 of memory for pack-objects, leave - * the rest for the OS and other processes in the - * system. - */ - if (!mem_have || mem_want < mem_have / 2) - string_list_clear(&keep_pack, 0); + add_repack_all_option(opts, &keep_pack, &repack_cmd.args); + string_list_clear(&keep_pack, 0); + } else { + add_repack_incremental_option(&repack_cmd.args); } - - add_repack_all_option(cfg, &keep_pack, &repack_cmd.args); - string_list_clear(&keep_pack, 0); - } else { - add_repack_incremental_option(&repack_cmd.args); } if (run_command(&repack_cmd)) { @@ -877,13 +908,13 @@ static int maintenance_task_odb(struct maintenance_run_opts *opts, goto out; } - if (cfg->prune_expire) { + if (opts->prune_expire) { struct child_process prune_cmd = CHILD_PROCESS_INIT; strvec_pushl(&prune_cmd.args, "prune", "--expire", NULL); /* run `git prune` even if using cruft packs */ - strvec_push(&prune_cmd.args, cfg->prune_expire); - if (opts->quiet) + strvec_push(&prune_cmd.args, opts->prune_expire); + if (!(opts->flags & ODB_OPTIMIZE_VERBOSE)) strvec_push(&prune_cmd.args, "--no-progress"); if (repo_has_promisor_remote(the_repository)) strvec_push(&prune_cmd.args, @@ -896,7 +927,7 @@ static int maintenance_task_odb(struct maintenance_run_opts *opts, } } - if (opts->auto_flag && too_many_loose_objects(gc_auto_threshold)) + if (opts->flags & ODB_OPTIMIZE_AUTO && too_many_loose_objects(gc_auto_threshold)) warning(_("There are too many unreachable loose objects; " "run 'git prune' to remove them.")); @@ -906,6 +937,26 @@ static int maintenance_task_odb(struct maintenance_run_opts *opts, return ret; } +static int maintenance_task_odb(struct maintenance_run_opts *opts, + struct gc_config *cfg, + int keep_largest_pack, + int aggressive) +{ + struct odb_optimize_options odb_opts = { + .keep_largest_pack = keep_largest_pack, + OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, aggressive), + }; + + if (opts->auto_flag) + odb_opts.flags |= ODB_OPTIMIZE_AUTO; + if (!opts->quiet) + odb_opts.flags |= ODB_OPTIMIZE_VERBOSE; + if (aggressive) + odb_opts.flags |= ODB_OPTIMIZE_NO_REUSE_DELTAS; + + return odb_optimize(the_repository->objects, &odb_opts); +} + int cmd_gc(int argc, const char **argv, const char *prefix, @@ -1596,11 +1647,19 @@ static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts, child.odb_to_close = the_repository->objects; strvec_pushl(&child.args, "repack", "-d", "-l", NULL); - if (geometry.split < geometry.pack_nr) + if (geometry.split < geometry.pack_nr) { strvec_pushf(&child.args, "--geometric=%d", geometry.split_factor); - else - add_repack_all_option(cfg, NULL, &child.args); + } else { + struct odb_optimize_options odb_opts = { + OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, 0), + }; + + if (!opts->quiet) + odb_opts.flags |= ODB_OPTIMIZE_VERBOSE; + + add_repack_all_option(&odb_opts, NULL, &child.args); + } if (opts->quiet) strvec_push(&child.args, "--quiet"); if (the_repository->settings.core_multi_pack_index) From 7d663dd83ba2ae459178c98f96c0761d044698b4 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 13 Jul 2026 07:52:11 +0200 Subject: [PATCH 014/280] builtin/gc: move geometric repacking into `odb_optimize()` We have two major object database optimization strategies: - The legacy strategy used by git-gc(1), which absorbs loose objects into packfiles, and eventually merges all packfiles once we have too many of them. - The more recent "geometric" strategy used by git-maintenance(1), which merges packfiles using a geometric sequence. These two strategies are still using completely separate code paths. In a subsequent commit we'll want to make both strategies pluggable though. Prepare for this change by merging the "geometric" strategy into `odb_optimize()`. This also allows us to reuse some of the logic we have in that function. Note that this change requires us to adapt tests because we're now using "-q" instead of "--quiet". Naturally though, these invocations are of course equivalent to one another. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/gc.c | 171 +++++++++++++++++++++-------------------- t/t7900-maintenance.sh | 18 ++--- 2 files changed, 96 insertions(+), 93 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index 17490106fc9c91..c8504f4456b0d0 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -593,6 +593,11 @@ static int keep_one_pack(struct string_list_item *item, void *data) return 0; } +enum odb_optimize_strategy { + ODB_OPTIMIZE_INCREMENTAL, + ODB_OPTIMIZE_GEOMETRIC, +}; + enum odb_optimize_flags { /* Enable verbose logging and progress reporting. */ ODB_OPTIMIZE_VERBOSE = (1 << 0), @@ -605,6 +610,7 @@ enum odb_optimize_flags { }; struct odb_optimize_options { + enum odb_optimize_strategy strategy; enum odb_optimize_flags flags; const char *prune_expire; const char *expire_to; @@ -858,49 +864,87 @@ static int odb_optimize(struct object_database *odb, * * - Otherwise we perform an incremental repack. */ - if (!(opts->flags & ODB_OPTIMIZE_AUTO)) { - struct string_list keep_pack = STRING_LIST_INIT_NODUP; - - if (opts->keep_largest_pack != -1) { - if (opts->keep_largest_pack) - find_base_packs(&keep_pack, 0); - } else if (big_pack_threshold) { - find_base_packs(&keep_pack, big_pack_threshold); - } - - add_repack_all_option(opts, &keep_pack, &repack_cmd.args); - string_list_clear(&keep_pack, 0); - } else { - if (too_many_packs(gc_auto_pack_limit)) { + switch (opts->strategy) { + case ODB_OPTIMIZE_INCREMENTAL: + if (!(opts->flags & ODB_OPTIMIZE_AUTO)) { struct string_list keep_pack = STRING_LIST_INIT_NODUP; - if (big_pack_threshold) { - find_base_packs(&keep_pack, big_pack_threshold); - if (keep_pack.nr >= gc_auto_pack_limit) { - string_list_clear(&keep_pack, 0); + if (opts->keep_largest_pack != -1) { + if (opts->keep_largest_pack) find_base_packs(&keep_pack, 0); - } - } else { - struct packed_git *p = find_base_packs(&keep_pack, 0); - uint64_t mem_have, mem_want; - - mem_have = total_ram(); - mem_want = estimate_repack_memory(p); - - /* - * Only allow 1/2 of memory for pack-objects, leave - * the rest for the OS and other processes in the - * system. - */ - if (!mem_have || mem_want < mem_have / 2) - string_list_clear(&keep_pack, 0); + } else if (big_pack_threshold) { + find_base_packs(&keep_pack, big_pack_threshold); } add_repack_all_option(opts, &keep_pack, &repack_cmd.args); string_list_clear(&keep_pack, 0); } else { - add_repack_incremental_option(&repack_cmd.args); + if (too_many_packs(gc_auto_pack_limit)) { + struct string_list keep_pack = STRING_LIST_INIT_NODUP; + + if (big_pack_threshold) { + find_base_packs(&keep_pack, big_pack_threshold); + if (keep_pack.nr >= gc_auto_pack_limit) { + string_list_clear(&keep_pack, 0); + find_base_packs(&keep_pack, 0); + } + } else { + struct packed_git *p = find_base_packs(&keep_pack, 0); + uint64_t mem_have, mem_want; + + mem_have = total_ram(); + mem_want = estimate_repack_memory(p); + + /* + * Only allow 1/2 of memory for pack-objects, leave + * the rest for the OS and other processes in the + * system. + */ + if (!mem_have || mem_want < mem_have / 2) + string_list_clear(&keep_pack, 0); + } + + add_repack_all_option(opts, &keep_pack, &repack_cmd.args); + string_list_clear(&keep_pack, 0); + } else { + add_repack_incremental_option(&repack_cmd.args); + } } + + break; + case ODB_OPTIMIZE_GEOMETRIC: { + struct pack_geometry geometry = { + .split_factor = 2, + }; + struct pack_objects_args po_args = { + .local = 1, + }; + struct existing_packs existing_packs = EXISTING_PACKS_INIT; + struct string_list kept_packs = STRING_LIST_INIT_DUP; + + repo_config_get_int(the_repository, "maintenance.geometric-repack.splitFactor", + &geometry.split_factor); + + existing_packs.repo = the_repository; + existing_packs_collect(&existing_packs, &kept_packs); + pack_geometry_init(&geometry, &existing_packs, &po_args); + pack_geometry_split(&geometry); + + if (geometry.split < geometry.pack_nr) { + strvec_pushf(&repack_cmd.args, "--geometric=%d", + geometry.split_factor); + } else { + add_repack_all_option(opts, NULL, &repack_cmd.args); + } + if (the_repository->settings.core_multi_pack_index) + strvec_push(&repack_cmd.args, "--write-midx"); + + existing_packs_release(&existing_packs); + pack_geometry_release(&geometry); + break; + } + default: + die("unknown maintenance strategy '%d'", opts->strategy); } if (run_command(&repack_cmd)) { @@ -908,7 +952,8 @@ static int odb_optimize(struct object_database *odb, goto out; } - if (opts->prune_expire) { + /* Geometric repacking uses cruft packs, so we don't have to prune separately. */ + if (opts->strategy != ODB_OPTIMIZE_GEOMETRIC && opts->prune_expire) { struct child_process prune_cmd = CHILD_PROCESS_INIT; strvec_pushl(&prune_cmd.args, "prune", "--expire", NULL); @@ -943,6 +988,7 @@ static int maintenance_task_odb(struct maintenance_run_opts *opts, int aggressive) { struct odb_optimize_options odb_opts = { + .strategy = ODB_OPTIMIZE_INCREMENTAL, .keep_largest_pack = keep_largest_pack, OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, aggressive), }; @@ -1624,58 +1670,15 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts, struct gc_config *cfg) { - struct pack_geometry geometry = { - .split_factor = 2, - }; - struct pack_objects_args po_args = { - .local = 1, + struct odb_optimize_options odb_opts = { + .strategy = ODB_OPTIMIZE_GEOMETRIC, + OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, 0), }; - struct existing_packs existing_packs = EXISTING_PACKS_INIT; - struct string_list kept_packs = STRING_LIST_INIT_DUP; - struct child_process child = CHILD_PROCESS_INIT; - int ret; - - repo_config_get_int(the_repository, "maintenance.geometric-repack.splitFactor", - &geometry.split_factor); - - existing_packs.repo = the_repository; - existing_packs_collect(&existing_packs, &kept_packs); - pack_geometry_init(&geometry, &existing_packs, &po_args); - pack_geometry_split(&geometry); - - child.git_cmd = 1; - child.odb_to_close = the_repository->objects; - - strvec_pushl(&child.args, "repack", "-d", "-l", NULL); - if (geometry.split < geometry.pack_nr) { - strvec_pushf(&child.args, "--geometric=%d", - geometry.split_factor); - } else { - struct odb_optimize_options odb_opts = { - OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, 0), - }; - if (!opts->quiet) - odb_opts.flags |= ODB_OPTIMIZE_VERBOSE; - - add_repack_all_option(&odb_opts, NULL, &child.args); - } - if (opts->quiet) - strvec_push(&child.args, "--quiet"); - if (the_repository->settings.core_multi_pack_index) - strvec_push(&child.args, "--write-midx"); - - if (run_command(&child)) { - ret = error(_("failed to perform geometric repack")); - goto out; - } - - ret = 0; + if (!opts->quiet) + odb_opts.flags |= ODB_OPTIMIZE_VERBOSE; -out: - existing_packs_release(&existing_packs); - pack_geometry_release(&geometry); - return ret; + return odb_optimize(the_repository->objects, &odb_opts); } static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED) diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh index 2d52e7918a33ff..6d87da2ae4d508 100755 --- a/t/t7900-maintenance.sh +++ b/t/t7900-maintenance.sh @@ -574,8 +574,8 @@ run_and_verify_geometric_pack () { rm -f "trace2.txt" && GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ git maintenance run --task=geometric-repack 2>/dev/null && - test_subcommand git repack -d -l --geometric=2 \ - --quiet --write-midx packfiles && @@ -606,8 +606,8 @@ test_expect_success 'geometric repacking task' ' # The initial repack causes an all-into-one repack. GIT_TRACE2_EVENT="$(pwd)/initial-repack.txt" \ git maintenance run --task=geometric-repack 2>/dev/null && - test_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \ - --quiet --write-midx /dev/null && - test_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \ - --quiet --write-midx /dev/null && - test_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \ - --quiet --write-midx packs && test_line_count = 2 packs && ls .git/objects/pack/*.mtimes >cruft && @@ -754,7 +754,7 @@ test_expect_success 'geometric repacking honors configured split factor' ' test_geometric_repack_needed false splitFactor=2 && test_geometric_repack_needed true splitFactor=3 && - test_subcommand git repack -d -l --geometric=3 --quiet --write-midx Date: Mon, 13 Jul 2026 07:52:12 +0200 Subject: [PATCH 015/280] builtin/gc: introduce `odb_optimize_required()` When invoking either git-gc(1) or git-maintenance(1) with the "--auto" flag then we only perform those maintenance tasks that are actually required. This logic is inherently an implementation detail of the object database backend that's in use. But the logic is scattered around multiple different functions, which makes it hard to make the logic pluggable. Introduce a new `odb_optimize_required()` function that allows us to check these conditions in a generic way. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/gc.c | 160 +++++++++++++++++++++++++++++---------------------- 1 file changed, 92 insertions(+), 68 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index c8504f4456b0d0..e119930adc816e 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -676,25 +676,84 @@ static void add_repack_incremental_option(struct strvec *args) strvec_push(args, "--no-write-bitmap-index"); } -static int need_to_gc(struct repository *repo) +static bool odb_optimize_required(struct object_database *odb, + const struct odb_optimize_options *opts) { - int gc_auto_threshold = 6700; - int gc_auto_pack_limit = 50; + switch (opts->strategy) { + case ODB_OPTIMIZE_INCREMENTAL: { + int gc_auto_threshold = 6700; + int gc_auto_pack_limit = 50; - repo_config_get_int(repo, "gc.auto", &gc_auto_threshold); - repo_config_get_int(repo, "gc.autopacklimit", &gc_auto_pack_limit); + repo_config_get_int(odb->repo, "gc.auto", &gc_auto_threshold); + repo_config_get_int(odb->repo, "gc.autopacklimit", &gc_auto_pack_limit); - /* - * Setting gc.auto to 0 or negative can disable the - * automatic gc. - */ - if (gc_auto_threshold <= 0) - return 0; - if (!too_many_packs(gc_auto_pack_limit) && - !too_many_loose_objects(gc_auto_threshold)) - return 0; + /* + * Setting gc.auto to 0 or negative can disable the + * automatic gc. + */ + if (gc_auto_threshold <= 0) + return false; + if (!too_many_packs(gc_auto_pack_limit) && + !too_many_loose_objects(gc_auto_threshold)) + return false; - return 1; + return true; + } + case ODB_OPTIMIZE_GEOMETRIC: { + struct pack_geometry geometry = { + .split_factor = 2, + }; + struct pack_objects_args po_args = { + .local = 1, + }; + struct existing_packs existing_packs = EXISTING_PACKS_INIT; + struct string_list kept_packs = STRING_LIST_INIT_DUP; + int auto_value = 100; + bool ret; + + repo_config_get_int(odb->repo, "maintenance.geometric-repack.auto", + &auto_value); + if (!auto_value) + return false; + if (auto_value < 0) + return true; + + repo_config_get_int(odb->repo, "maintenance.geometric-repack.splitFactor", + &geometry.split_factor); + + existing_packs.repo = odb->repo; + existing_packs_collect(&existing_packs, &kept_packs); + pack_geometry_init(&geometry, &existing_packs, &po_args); + pack_geometry_split(&geometry); + + /* + * When we'd merge at least two packs with one another we always + * perform the repack. + */ + if (geometry.split) { + ret = true; + goto out; + } + + /* + * Otherwise, we estimate the number of loose objects to determine + * whether we want to create a new packfile or not. + */ + if (too_many_loose_objects(auto_value)) { + ret = true; + goto out; + } + + ret = false; + + out: + existing_packs_release(&existing_packs); + pack_geometry_release(&geometry); + return ret; + } + default: + BUG("unknown maintenance strategy '%d'", opts->strategy); + } } /* return NULL on success, else hostname running the gc */ @@ -1076,13 +1135,19 @@ int cmd_gc(int argc, die(_("failed to parse prune expiry value %s"), cfg.prune_expire); if (opts.auto_flag) { + struct odb_optimize_options optimize_opts = { + .strategy = ODB_OPTIMIZE_INCREMENTAL, + OPTIMIZE_FIELDS_FROM_GC_CONFIG(&cfg, 0), + }; + if (cfg.detach_auto && opts.detach < 0) opts.detach = 1; /* * Auto-gc should be least intrusive as possible. */ - if (!need_to_gc(the_repository) || run_hooks(the_repository, "pre-auto-gc")) { + if (!odb_optimize_required(the_repository->objects, &optimize_opts) || + run_hooks(the_repository, "pre-auto-gc")) { ret = 0; goto out; } @@ -1379,9 +1444,13 @@ static int maintenance_task_gc_background(struct maintenance_run_opts *opts, return run_command(&child); } -static int gc_condition(struct gc_config *cfg UNUSED) +static int gc_condition(struct gc_config *cfg) { - return need_to_gc(the_repository); + struct odb_optimize_options opts = { + .strategy = ODB_OPTIMIZE_INCREMENTAL, + OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, 0), + }; + return odb_optimize_required(the_repository->objects, &opts); } static int prune_packed(struct maintenance_run_opts *opts) @@ -1681,58 +1750,13 @@ static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts, return odb_optimize(the_repository->objects, &odb_opts); } -static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED) +static int geometric_repack_auto_condition(struct gc_config *cfg) { - struct pack_geometry geometry = { - .split_factor = 2, - }; - struct pack_objects_args po_args = { - .local = 1, + struct odb_optimize_options opts = { + .strategy = ODB_OPTIMIZE_GEOMETRIC, + OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, 0), }; - struct existing_packs existing_packs = EXISTING_PACKS_INIT; - struct string_list kept_packs = STRING_LIST_INIT_DUP; - int auto_value = 100; - int ret; - - repo_config_get_int(the_repository, "maintenance.geometric-repack.auto", - &auto_value); - if (!auto_value) - return 0; - if (auto_value < 0) - return 1; - - repo_config_get_int(the_repository, "maintenance.geometric-repack.splitFactor", - &geometry.split_factor); - - existing_packs.repo = the_repository; - existing_packs_collect(&existing_packs, &kept_packs); - pack_geometry_init(&geometry, &existing_packs, &po_args); - pack_geometry_split(&geometry); - - /* - * When we'd merge at least two packs with one another we always - * perform the repack. - */ - if (geometry.split) { - ret = 1; - goto out; - } - - /* - * Otherwise, we estimate the number of loose objects to determine - * whether we want to create a new packfile or not. - */ - if (too_many_loose_objects(auto_value)) { - ret = 1; - goto out; - } - - ret = 0; - -out: - existing_packs_release(&existing_packs); - pack_geometry_release(&geometry); - return ret; + return odb_optimize_required(the_repository->objects, &opts); } typedef int (*maintenance_task_fn)(struct maintenance_run_opts *opts, From 0a778894b9676a1e93870589851731dad05e086a Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 13 Jul 2026 07:52:13 +0200 Subject: [PATCH 016/280] builtin/gc: refactor ODB optimizations to operate on "files" source We have a couple of functions that are implementation details of how the "files" object database source performs optimizations. These functions often use global state like `the_repository` and implicitly derive the source they are supposed to optimize. Refactor these interfaces to accept a "files" source directly. This will make it easier to move around the whole logic into "odb/source-files.c" in a subsequent step. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/gc.c | 79 +++++++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index e119930adc816e..32071824887cda 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -428,9 +428,8 @@ static int rerere_gc_condition(struct gc_config *cfg UNUSED) return should_gc; } -static int too_many_loose_objects(int limit) +static int too_many_loose_objects(struct odb_source_files *files, int limit) { - struct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources); /* * This is weird, but stems from legacy behaviour: the GC auto * threshold was always essentially interpreted as if it was rounded up @@ -446,19 +445,21 @@ static int too_many_loose_objects(int limit) return loose_count > auto_threshold; } -static struct packed_git *find_base_packs(struct string_list *packs, +static struct packed_git *find_base_packs(struct odb_source_files *files, + struct string_list *packs, unsigned long limit) { - struct packed_git *p, *base = NULL; + struct packfile_list_entry *e; + struct packed_git *base = NULL; - repo_for_each_pack(the_repository, p) { - if (!p->pack_local || p->is_cruft) + for (e = packfile_store_get_packs(files->packed); e; e = e->next) { + if (e->pack->is_cruft) continue; if (limit) { - if (p->pack_size >= limit) - string_list_append(packs, p->pack_name); - } else if (!base || base->pack_size < p->pack_size) { - base = p; + if (e->pack->pack_size >= limit) + string_list_append(packs, e->pack->pack_name); + } else if (!base || base->pack_size < e->pack->pack_size) { + base = e->pack; } } @@ -468,18 +469,16 @@ static struct packed_git *find_base_packs(struct string_list *packs, return base; } -static int too_many_packs(int gc_auto_pack_limit) +static int too_many_packs(struct odb_source_files *files, int gc_auto_pack_limit) { - struct packed_git *p; + struct packfile_list_entry *e; int cnt = 0; if (gc_auto_pack_limit <= 0) return 0; - repo_for_each_pack(the_repository, p) { - if (!p->pack_local) - continue; - if (p->pack_keep) + for (e = packfile_store_get_packs(files->packed); e; e = e->next) { + if (e->pack->pack_keep) continue; /* * Perhaps check the size of the pack and count only @@ -535,15 +534,16 @@ static uint64_t total_ram(void) return 0; } -static uint64_t estimate_repack_memory(struct packed_git *pack) +static uint64_t estimate_repack_memory(struct odb_source_files *files, + struct packed_git *pack) { unsigned long max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE; unsigned long delta_base_cache_limit = DEFAULT_DELTA_BASE_CACHE_LIMIT; unsigned long nr_objects; size_t os_cache, heap; - if (odb_count_objects(the_repository->objects, - ODB_COUNT_OBJECTS_APPROXIMATE, &nr_objects) < 0) + if (odb_source_count_objects(&files->base, ODB_COUNT_OBJECTS_APPROXIMATE, + &nr_objects) < 0) return 0; if (!pack || !nr_objects) @@ -679,6 +679,8 @@ static void add_repack_incremental_option(struct strvec *args) static bool odb_optimize_required(struct object_database *odb, const struct odb_optimize_options *opts) { + struct odb_source_files *files = odb_source_files_downcast(odb->sources); + switch (opts->strategy) { case ODB_OPTIMIZE_INCREMENTAL: { int gc_auto_threshold = 6700; @@ -693,8 +695,8 @@ static bool odb_optimize_required(struct object_database *odb, */ if (gc_auto_threshold <= 0) return false; - if (!too_many_packs(gc_auto_pack_limit) && - !too_many_loose_objects(gc_auto_threshold)) + if (!too_many_packs(files, gc_auto_pack_limit) && + !too_many_loose_objects(files, gc_auto_threshold)) return false; return true; @@ -739,7 +741,7 @@ static bool odb_optimize_required(struct object_database *odb, * Otherwise, we estimate the number of loose objects to determine * whether we want to create a new packfile or not. */ - if (too_many_loose_objects(auto_value)) { + if (too_many_loose_objects(files, auto_value)) { ret = true; goto out; } @@ -886,21 +888,22 @@ static int gc_foreground_tasks(struct maintenance_run_opts *opts, static int odb_optimize(struct object_database *odb, const struct odb_optimize_options *opts) { + struct odb_source_files *files = odb_source_files_downcast(odb->sources); struct child_process repack_cmd = CHILD_PROCESS_INIT; unsigned long big_pack_threshold = 0; int gc_auto_threshold = 6700; int gc_auto_pack_limit = 50; int ret; - repo_config_get_int(the_repository, "gc.auto", &gc_auto_threshold); - repo_config_get_int(the_repository, "gc.autopacklimit", &gc_auto_pack_limit); - repo_config_get_ulong(the_repository, "gc.bigpackthreshold", &big_pack_threshold); + repo_config_get_int(odb->repo, "gc.auto", &gc_auto_threshold); + repo_config_get_int(odb->repo, "gc.autopacklimit", &gc_auto_pack_limit); + repo_config_get_ulong(odb->repo, "gc.bigpackthreshold", &big_pack_threshold); if (odb->repo->repository_format_precious_objects) return 0; repack_cmd.git_cmd = 1; - repack_cmd.odb_to_close = the_repository->objects; + repack_cmd.odb_to_close = odb->repo->objects; strvec_pushl(&repack_cmd.args, "repack", "-d", "-l", NULL); if (opts->flags & ODB_OPTIMIZE_NO_REUSE_DELTAS) @@ -930,29 +933,29 @@ static int odb_optimize(struct object_database *odb, if (opts->keep_largest_pack != -1) { if (opts->keep_largest_pack) - find_base_packs(&keep_pack, 0); + find_base_packs(files, &keep_pack, 0); } else if (big_pack_threshold) { - find_base_packs(&keep_pack, big_pack_threshold); + find_base_packs(files, &keep_pack, big_pack_threshold); } add_repack_all_option(opts, &keep_pack, &repack_cmd.args); string_list_clear(&keep_pack, 0); } else { - if (too_many_packs(gc_auto_pack_limit)) { + if (too_many_packs(files, gc_auto_pack_limit)) { struct string_list keep_pack = STRING_LIST_INIT_NODUP; if (big_pack_threshold) { - find_base_packs(&keep_pack, big_pack_threshold); + find_base_packs(files, &keep_pack, big_pack_threshold); if (keep_pack.nr >= gc_auto_pack_limit) { string_list_clear(&keep_pack, 0); - find_base_packs(&keep_pack, 0); + find_base_packs(files, &keep_pack, 0); } } else { - struct packed_git *p = find_base_packs(&keep_pack, 0); + struct packed_git *p = find_base_packs(files, &keep_pack, 0); uint64_t mem_have, mem_want; mem_have = total_ram(); - mem_want = estimate_repack_memory(p); + mem_want = estimate_repack_memory(files, p); /* * Only allow 1/2 of memory for pack-objects, leave @@ -981,10 +984,10 @@ static int odb_optimize(struct object_database *odb, struct existing_packs existing_packs = EXISTING_PACKS_INIT; struct string_list kept_packs = STRING_LIST_INIT_DUP; - repo_config_get_int(the_repository, "maintenance.geometric-repack.splitFactor", + repo_config_get_int(odb->repo, "maintenance.geometric-repack.splitFactor", &geometry.split_factor); - existing_packs.repo = the_repository; + existing_packs.repo = odb->repo; existing_packs_collect(&existing_packs, &kept_packs); pack_geometry_init(&geometry, &existing_packs, &po_args); pack_geometry_split(&geometry); @@ -995,7 +998,7 @@ static int odb_optimize(struct object_database *odb, } else { add_repack_all_option(opts, NULL, &repack_cmd.args); } - if (the_repository->settings.core_multi_pack_index) + if (odb->repo->settings.core_multi_pack_index) strvec_push(&repack_cmd.args, "--write-midx"); existing_packs_release(&existing_packs); @@ -1020,7 +1023,7 @@ static int odb_optimize(struct object_database *odb, strvec_push(&prune_cmd.args, opts->prune_expire); if (!(opts->flags & ODB_OPTIMIZE_VERBOSE)) strvec_push(&prune_cmd.args, "--no-progress"); - if (repo_has_promisor_remote(the_repository)) + if (repo_has_promisor_remote(odb->repo)) strvec_push(&prune_cmd.args, "--exclude-promisor-objects"); prune_cmd.git_cmd = 1; @@ -1031,7 +1034,7 @@ static int odb_optimize(struct object_database *odb, } } - if (opts->flags & ODB_OPTIMIZE_AUTO && too_many_loose_objects(gc_auto_threshold)) + if (opts->flags & ODB_OPTIMIZE_AUTO && too_many_loose_objects(files, gc_auto_threshold)) warning(_("There are too many unreachable loose objects; " "run 'git prune' to remove them.")); From 7534d456816d49f20716e49900d43cedb02e8c42 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 13 Jul 2026 07:52:14 +0200 Subject: [PATCH 017/280] builtin/gc: fix signedness issues in ODB-related functionality There are a couple of signedness issues in ODB-related functionality. These are not a problem because we disable -Wsign-compare in this file, but once we move these functions into "odb/source-files.c" they will result in warnings. Fix those issues: - In `too_many_loose_objects()` we receive a signed limit, but compare it with the unsigned actual number of loose objects. This is fixed by bailing out immediately when the limit is smaller than or equal to zero, which we also do similarly in other places. The warning is then squelched via a cast. - In `find_base_packs()` we compare the signed size of the pack against the unsigned limit. As the pack size is always going to be a positive file size it's safe to cast it to an unsigned value. - In `odb_optimize()` we compare the unsigned `keep_pack.nr` value against the signed `gc_auto_pack_limit`. We only reach this code when `too_many_packs()` returns true-ish, and that can only happen when `gc_auto_pack_limit > 0`. Consequently, we can fix the warning by casting the limit to an unsigned value. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/gc.c | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index 32071824887cda..8cf3781313bc53 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -430,19 +430,21 @@ static int rerere_gc_condition(struct gc_config *cfg UNUSED) static int too_many_loose_objects(struct odb_source_files *files, int limit) { - /* - * This is weird, but stems from legacy behaviour: the GC auto - * threshold was always essentially interpreted as if it was rounded up - * to the next multiple 256 of, so we retain this behaviour for now. - */ - int auto_threshold = DIV_ROUND_UP(limit, 256) * 256; unsigned long loose_count; + if (limit <= 0) + return 0; + if (odb_source_count_objects(&files->loose->base, ODB_COUNT_OBJECTS_APPROXIMATE, &loose_count) < 0) return 0; - return loose_count > auto_threshold; + /* + * This is weird, but stems from legacy behaviour: the GC auto + * threshold was always essentially interpreted as if it was rounded up + * to the next multiple 256 of, so we retain this behaviour for now. + */ + return loose_count > (DIV_ROUND_UP(((unsigned long) limit), 256) * 256); } static struct packed_git *find_base_packs(struct odb_source_files *files, @@ -456,7 +458,7 @@ static struct packed_git *find_base_packs(struct odb_source_files *files, if (e->pack->is_cruft) continue; if (limit) { - if (e->pack->pack_size >= limit) + if ((uintmax_t) e->pack->pack_size >= limit) string_list_append(packs, e->pack->pack_name); } else if (!base || base->pack_size < e->pack->pack_size) { base = e->pack; @@ -946,7 +948,7 @@ static int odb_optimize(struct object_database *odb, if (big_pack_threshold) { find_base_packs(files, &keep_pack, big_pack_threshold); - if (keep_pack.nr >= gc_auto_pack_limit) { + if (keep_pack.nr >= (unsigned long) gc_auto_pack_limit) { string_list_clear(&keep_pack, 0); find_base_packs(files, &keep_pack, 0); } From 56f3fc9520a52a43aef2cabaf30b71a79c9eca57 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 13 Jul 2026 07:52:15 +0200 Subject: [PATCH 018/280] odb: make optimizations pluggable Move `odb_optimize()` and `odb_optimize_required()` from "builtin/gc.c" into the "files" source and wire them up via newly introduced vtable pointers for the object database sources. This makes the logic pluggable and thus allows other backends to have their own, custom implementation. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/gc.c | 490 +-------------------------------------------- odb.c | 12 ++ odb.h | 45 +++++ odb/source-files.c | 470 +++++++++++++++++++++++++++++++++++++++++++ odb/source-files.h | 15 ++ odb/source.h | 36 ++++ 6 files changed, 579 insertions(+), 489 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index 8cf3781313bc53..ac1a21e91267f7 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -30,16 +30,11 @@ #include "commit-graph.h" #include "packfile.h" #include "object-file.h" -#include "pack.h" -#include "pack-objects.h" +#include "odb.h" #include "path.h" #include "reflog.h" -#include "repack.h" #include "rerere.h" #include "revision.h" -#include "blob.h" -#include "tree.h" -#include "promisor-remote.h" #include "refs.h" #include "remote.h" #include "exec-cmd.h" @@ -428,203 +423,6 @@ static int rerere_gc_condition(struct gc_config *cfg UNUSED) return should_gc; } -static int too_many_loose_objects(struct odb_source_files *files, int limit) -{ - unsigned long loose_count; - - if (limit <= 0) - return 0; - - if (odb_source_count_objects(&files->loose->base, ODB_COUNT_OBJECTS_APPROXIMATE, - &loose_count) < 0) - return 0; - - /* - * This is weird, but stems from legacy behaviour: the GC auto - * threshold was always essentially interpreted as if it was rounded up - * to the next multiple 256 of, so we retain this behaviour for now. - */ - return loose_count > (DIV_ROUND_UP(((unsigned long) limit), 256) * 256); -} - -static struct packed_git *find_base_packs(struct odb_source_files *files, - struct string_list *packs, - unsigned long limit) -{ - struct packfile_list_entry *e; - struct packed_git *base = NULL; - - for (e = packfile_store_get_packs(files->packed); e; e = e->next) { - if (e->pack->is_cruft) - continue; - if (limit) { - if ((uintmax_t) e->pack->pack_size >= limit) - string_list_append(packs, e->pack->pack_name); - } else if (!base || base->pack_size < e->pack->pack_size) { - base = e->pack; - } - } - - if (base) - string_list_append(packs, base->pack_name); - - return base; -} - -static int too_many_packs(struct odb_source_files *files, int gc_auto_pack_limit) -{ - struct packfile_list_entry *e; - int cnt = 0; - - if (gc_auto_pack_limit <= 0) - return 0; - - for (e = packfile_store_get_packs(files->packed); e; e = e->next) { - if (e->pack->pack_keep) - continue; - /* - * Perhaps check the size of the pack and count only - * very small ones here? - */ - cnt++; - } - return gc_auto_pack_limit < cnt; -} - -static uint64_t total_ram(void) -{ -#if defined(HAVE_SYSINFO) - struct sysinfo si; - - if (!sysinfo(&si)) { - uint64_t total = si.totalram; - - if (si.mem_unit > 1) - total *= (uint64_t)si.mem_unit; - return total; - } -#elif defined(HAVE_BSD_SYSCTL) && (defined(HW_MEMSIZE) || defined(HW_PHYSMEM) || defined(HW_PHYSMEM64)) - uint64_t physical_memory; - int mib[2]; - size_t length; - - mib[0] = CTL_HW; -# if defined(HW_MEMSIZE) - mib[1] = HW_MEMSIZE; -# elif defined(HW_PHYSMEM64) - mib[1] = HW_PHYSMEM64; -# else - mib[1] = HW_PHYSMEM; -# endif - length = sizeof(physical_memory); - if (!sysctl(mib, 2, &physical_memory, &length, NULL, 0)) { - if (length == 4) { - uint32_t mem; - - if (!sysctl(mib, 2, &mem, &length, NULL, 0)) - physical_memory = mem; - } - return physical_memory; - } -#elif defined(GIT_WINDOWS_NATIVE) - MEMORYSTATUSEX memInfo; - - memInfo.dwLength = sizeof(MEMORYSTATUSEX); - if (GlobalMemoryStatusEx(&memInfo)) - return memInfo.ullTotalPhys; -#endif - return 0; -} - -static uint64_t estimate_repack_memory(struct odb_source_files *files, - struct packed_git *pack) -{ - unsigned long max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE; - unsigned long delta_base_cache_limit = DEFAULT_DELTA_BASE_CACHE_LIMIT; - unsigned long nr_objects; - size_t os_cache, heap; - - if (odb_source_count_objects(&files->base, ODB_COUNT_OBJECTS_APPROXIMATE, - &nr_objects) < 0) - return 0; - - if (!pack || !nr_objects) - return 0; - - repo_config_get_ulong(the_repository, "pack.deltacachesize", &max_delta_cache_size); - repo_config_get_ulong(the_repository, "core.deltabasecachelimit", &delta_base_cache_limit); - - /* - * First we have to scan through at least one pack. - * Assume enough room in OS file cache to keep the entire pack - * or we may accidentally evict data of other processes from - * the cache. - */ - os_cache = pack->pack_size + pack->index_size; - /* then pack-objects needs lots more for book keeping */ - heap = sizeof(struct object_entry) * nr_objects; - /* - * internal rev-list --all --objects takes up some memory too, - * let's say half of it is for blobs - */ - heap += sizeof(struct blob) * nr_objects / 2; - /* - * and the other half is for trees (commits and tags are - * usually insignificant) - */ - heap += sizeof(struct tree) * nr_objects / 2; - /* and then obj_hash[], underestimated in fact */ - heap += sizeof(struct object *) * nr_objects; - /* revindex is used also */ - heap += (sizeof(off_t) + sizeof(uint32_t)) * nr_objects; - /* - * read_sha1_file() (either at delta calculation phase, or - * writing phase) also fills up the delta base cache - */ - heap += delta_base_cache_limit; - /* and of course pack-objects has its own delta cache */ - heap += max_delta_cache_size; - - return os_cache + heap; -} - -static int keep_one_pack(struct string_list_item *item, void *data) -{ - struct strvec *args = data; - strvec_pushf(args, "--keep-pack=%s", basename(item->string)); - return 0; -} - -enum odb_optimize_strategy { - ODB_OPTIMIZE_INCREMENTAL, - ODB_OPTIMIZE_GEOMETRIC, -}; - -enum odb_optimize_flags { - /* Enable verbose logging and progress reporting. */ - ODB_OPTIMIZE_VERBOSE = (1 << 0), - - /* Perform auto-maintenance, only optimizing objects as required. */ - ODB_OPTIMIZE_AUTO = (1 << 1), - - /* Recompute existing deltas. */ - ODB_OPTIMIZE_NO_REUSE_DELTAS = (1 << 2), -}; - -struct odb_optimize_options { - enum odb_optimize_strategy strategy; - enum odb_optimize_flags flags; - const char *prune_expire; - const char *expire_to; - int depth; - int window; - - /* Backend-specific options. */ - int keep_largest_pack; - int cruft_packs; - unsigned long max_cruft_size; -}; - #define OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, aggressive) \ .prune_expire = (cfg)->prune_expire, \ .expire_to = (cfg)->repack_expire_to, \ @@ -633,133 +431,6 @@ struct odb_optimize_options { .window = (aggressive) ? (cfg)->aggressive_window : 0, \ .depth = (aggressive) ? (cfg)->aggressive_depth : 0 -static void add_repack_all_option(const struct odb_optimize_options *opts, - struct string_list *keep_pack, - struct strvec *args) -{ - char *repack_filter = NULL; - char *repack_filter_to = NULL; - - repo_config_get_string(the_repository, "gc.repackfilter", &repack_filter); - repo_config_get_string(the_repository, "gc.repackfilterto", &repack_filter_to); - - if (opts->prune_expire && !strcmp(opts->prune_expire, "now") && - !(opts->cruft_packs && opts->expire_to)) - strvec_push(args, "-a"); - else if (opts->cruft_packs) { - strvec_push(args, "--cruft"); - if (opts->prune_expire) - strvec_pushf(args, "--cruft-expiration=%s", opts->prune_expire); - if (opts->max_cruft_size) - strvec_pushf(args, "--max-cruft-size=%lu", - opts->max_cruft_size); - if (opts->expire_to) - strvec_pushf(args, "--expire-to=%s", opts->expire_to); - } else { - strvec_push(args, "-A"); - if (opts->prune_expire) - strvec_pushf(args, "--unpack-unreachable=%s", opts->prune_expire); - } - - if (keep_pack) - for_each_string_list(keep_pack, keep_one_pack, args); - - if (repack_filter && *repack_filter) - strvec_pushf(args, "--filter=%s", repack_filter); - if (repack_filter_to && *repack_filter_to) - strvec_pushf(args, "--filter-to=%s", repack_filter_to); - - free(repack_filter); - free(repack_filter_to); -} - -static void add_repack_incremental_option(struct strvec *args) -{ - strvec_push(args, "--no-write-bitmap-index"); -} - -static bool odb_optimize_required(struct object_database *odb, - const struct odb_optimize_options *opts) -{ - struct odb_source_files *files = odb_source_files_downcast(odb->sources); - - switch (opts->strategy) { - case ODB_OPTIMIZE_INCREMENTAL: { - int gc_auto_threshold = 6700; - int gc_auto_pack_limit = 50; - - repo_config_get_int(odb->repo, "gc.auto", &gc_auto_threshold); - repo_config_get_int(odb->repo, "gc.autopacklimit", &gc_auto_pack_limit); - - /* - * Setting gc.auto to 0 or negative can disable the - * automatic gc. - */ - if (gc_auto_threshold <= 0) - return false; - if (!too_many_packs(files, gc_auto_pack_limit) && - !too_many_loose_objects(files, gc_auto_threshold)) - return false; - - return true; - } - case ODB_OPTIMIZE_GEOMETRIC: { - struct pack_geometry geometry = { - .split_factor = 2, - }; - struct pack_objects_args po_args = { - .local = 1, - }; - struct existing_packs existing_packs = EXISTING_PACKS_INIT; - struct string_list kept_packs = STRING_LIST_INIT_DUP; - int auto_value = 100; - bool ret; - - repo_config_get_int(odb->repo, "maintenance.geometric-repack.auto", - &auto_value); - if (!auto_value) - return false; - if (auto_value < 0) - return true; - - repo_config_get_int(odb->repo, "maintenance.geometric-repack.splitFactor", - &geometry.split_factor); - - existing_packs.repo = odb->repo; - existing_packs_collect(&existing_packs, &kept_packs); - pack_geometry_init(&geometry, &existing_packs, &po_args); - pack_geometry_split(&geometry); - - /* - * When we'd merge at least two packs with one another we always - * perform the repack. - */ - if (geometry.split) { - ret = true; - goto out; - } - - /* - * Otherwise, we estimate the number of loose objects to determine - * whether we want to create a new packfile or not. - */ - if (too_many_loose_objects(files, auto_value)) { - ret = true; - goto out; - } - - ret = false; - - out: - existing_packs_release(&existing_packs); - pack_geometry_release(&geometry); - return ret; - } - default: - BUG("unknown maintenance strategy '%d'", opts->strategy); - } -} - /* return NULL on success, else hostname running the gc */ static const char *lock_repo_for_gc(int force, pid_t* ret_pid) { @@ -887,165 +558,6 @@ static int gc_foreground_tasks(struct maintenance_run_opts *opts, return 0; } -static int odb_optimize(struct object_database *odb, - const struct odb_optimize_options *opts) -{ - struct odb_source_files *files = odb_source_files_downcast(odb->sources); - struct child_process repack_cmd = CHILD_PROCESS_INIT; - unsigned long big_pack_threshold = 0; - int gc_auto_threshold = 6700; - int gc_auto_pack_limit = 50; - int ret; - - repo_config_get_int(odb->repo, "gc.auto", &gc_auto_threshold); - repo_config_get_int(odb->repo, "gc.autopacklimit", &gc_auto_pack_limit); - repo_config_get_ulong(odb->repo, "gc.bigpackthreshold", &big_pack_threshold); - - if (odb->repo->repository_format_precious_objects) - return 0; - - repack_cmd.git_cmd = 1; - repack_cmd.odb_to_close = odb->repo->objects; - - strvec_pushl(&repack_cmd.args, "repack", "-d", "-l", NULL); - if (opts->flags & ODB_OPTIMIZE_NO_REUSE_DELTAS) - strvec_push(&repack_cmd.args, "-f"); - if (opts->depth > 0) - strvec_pushf(&repack_cmd.args, "--depth=%d", opts->depth); - if (opts->window > 0) - strvec_pushf(&repack_cmd.args, "--window=%d", opts->window); - if (!(opts->flags & ODB_OPTIMIZE_VERBOSE)) - strvec_push(&repack_cmd.args, "-q"); - - /* - * There's three cases we need to consider: - * - * - If we're invoked without `--auto` we'll need to perform a full - * repack. - * - * - If we're invoked with `--auto` and there's too many packs, then - * we perform a full repack, as well. - * - * - Otherwise we perform an incremental repack. - */ - switch (opts->strategy) { - case ODB_OPTIMIZE_INCREMENTAL: - if (!(opts->flags & ODB_OPTIMIZE_AUTO)) { - struct string_list keep_pack = STRING_LIST_INIT_NODUP; - - if (opts->keep_largest_pack != -1) { - if (opts->keep_largest_pack) - find_base_packs(files, &keep_pack, 0); - } else if (big_pack_threshold) { - find_base_packs(files, &keep_pack, big_pack_threshold); - } - - add_repack_all_option(opts, &keep_pack, &repack_cmd.args); - string_list_clear(&keep_pack, 0); - } else { - if (too_many_packs(files, gc_auto_pack_limit)) { - struct string_list keep_pack = STRING_LIST_INIT_NODUP; - - if (big_pack_threshold) { - find_base_packs(files, &keep_pack, big_pack_threshold); - if (keep_pack.nr >= (unsigned long) gc_auto_pack_limit) { - string_list_clear(&keep_pack, 0); - find_base_packs(files, &keep_pack, 0); - } - } else { - struct packed_git *p = find_base_packs(files, &keep_pack, 0); - uint64_t mem_have, mem_want; - - mem_have = total_ram(); - mem_want = estimate_repack_memory(files, p); - - /* - * Only allow 1/2 of memory for pack-objects, leave - * the rest for the OS and other processes in the - * system. - */ - if (!mem_have || mem_want < mem_have / 2) - string_list_clear(&keep_pack, 0); - } - - add_repack_all_option(opts, &keep_pack, &repack_cmd.args); - string_list_clear(&keep_pack, 0); - } else { - add_repack_incremental_option(&repack_cmd.args); - } - } - - break; - case ODB_OPTIMIZE_GEOMETRIC: { - struct pack_geometry geometry = { - .split_factor = 2, - }; - struct pack_objects_args po_args = { - .local = 1, - }; - struct existing_packs existing_packs = EXISTING_PACKS_INIT; - struct string_list kept_packs = STRING_LIST_INIT_DUP; - - repo_config_get_int(odb->repo, "maintenance.geometric-repack.splitFactor", - &geometry.split_factor); - - existing_packs.repo = odb->repo; - existing_packs_collect(&existing_packs, &kept_packs); - pack_geometry_init(&geometry, &existing_packs, &po_args); - pack_geometry_split(&geometry); - - if (geometry.split < geometry.pack_nr) { - strvec_pushf(&repack_cmd.args, "--geometric=%d", - geometry.split_factor); - } else { - add_repack_all_option(opts, NULL, &repack_cmd.args); - } - if (odb->repo->settings.core_multi_pack_index) - strvec_push(&repack_cmd.args, "--write-midx"); - - existing_packs_release(&existing_packs); - pack_geometry_release(&geometry); - break; - } - default: - die("unknown maintenance strategy '%d'", opts->strategy); - } - - if (run_command(&repack_cmd)) { - ret = error(FAILED_RUN, repack_cmd.args.v[0]); - goto out; - } - - /* Geometric repacking uses cruft packs, so we don't have to prune separately. */ - if (opts->strategy != ODB_OPTIMIZE_GEOMETRIC && opts->prune_expire) { - struct child_process prune_cmd = CHILD_PROCESS_INIT; - - strvec_pushl(&prune_cmd.args, "prune", "--expire", NULL); - /* run `git prune` even if using cruft packs */ - strvec_push(&prune_cmd.args, opts->prune_expire); - if (!(opts->flags & ODB_OPTIMIZE_VERBOSE)) - strvec_push(&prune_cmd.args, "--no-progress"); - if (repo_has_promisor_remote(odb->repo)) - strvec_push(&prune_cmd.args, - "--exclude-promisor-objects"); - prune_cmd.git_cmd = 1; - - if (run_command(&prune_cmd)) { - ret = error(FAILED_RUN, prune_cmd.args.v[0]); - goto out; - } - } - - if (opts->flags & ODB_OPTIMIZE_AUTO && too_many_loose_objects(files, gc_auto_threshold)) - warning(_("There are too many unreachable loose objects; " - "run 'git prune' to remove them.")); - - ret = 0; - -out: - return ret; -} - static int maintenance_task_odb(struct maintenance_run_opts *opts, struct gc_config *cfg, int keep_largest_pack, diff --git a/odb.c b/odb.c index 7d555be09feaea..89660981feac02 100644 --- a/odb.c +++ b/odb.c @@ -1003,6 +1003,18 @@ int odb_write_object_stream(struct object_database *odb, return odb_source_write_object_stream(odb->sources, stream, len, oid); } +int odb_optimize(struct object_database *odb, + const struct odb_optimize_options *opts) +{ + return odb_source_optimize(odb->sources, opts); +} + +bool odb_optimize_required(struct object_database *odb, + const struct odb_optimize_options *opts) +{ + return odb_source_optimize_required(odb->sources, opts); +} + struct object_database *odb_new(struct repository *repo, const char *primary_source, const char *secondary_sources) diff --git a/odb.h b/odb.h index 3834a0dcbf033b..7e1c85c22e844f 100644 --- a/odb.h +++ b/odb.h @@ -117,6 +117,51 @@ struct object_database *odb_new(struct repository *repo, /* Free the object database and release all resources. */ void odb_free(struct object_database *o); +enum odb_optimize_strategy { + ODB_OPTIMIZE_INCREMENTAL, + ODB_OPTIMIZE_GEOMETRIC, +}; + +enum odb_optimize_flags { + /* Enable verbose logging and progress reporting. */ + ODB_OPTIMIZE_VERBOSE = (1 << 0), + + /* Perform auto-maintenance, only optimizing objects as required. */ + ODB_OPTIMIZE_AUTO = (1 << 1), + + /* Recompute existing deltas. */ + ODB_OPTIMIZE_NO_REUSE_DELTAS = (1 << 2), +}; + +struct odb_optimize_options { + enum odb_optimize_strategy strategy; + enum odb_optimize_flags flags; + const char *prune_expire; + const char *expire_to; + int depth; + int window; + + /* Backend-specific options. */ + int keep_largest_pack; + int cruft_packs; + unsigned long max_cruft_size; +}; + +/* + * Optimize the object database. Returns 0 on success, a negative error code + * otherwise. + */ +int odb_optimize(struct object_database *odb, + const struct odb_optimize_options *opts); + +/* + * Check whether optimization of the object database is required given the + * provided options. Returns true if optimization should be performed, false + * otherwise. + */ +bool odb_optimize_required(struct object_database *odb, + const struct odb_optimize_options *opts); + /* * Close the object database and all of its sources so that any held resources * will be released. The database can still be used after closing it, in which diff --git a/odb/source-files.c b/odb/source-files.c index bbd1784b337c6d..82cf61da4af9dd 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -1,6 +1,8 @@ #include "git-compat-util.h" #include "abspath.h" +#include "blob.h" #include "chdir-notify.h" +#include "config.h" #include "gettext.h" #include "lockfile.h" #include "object-file.h" @@ -8,8 +10,16 @@ #include "odb/source.h" #include "odb/source-files.h" #include "odb/source-loose.h" +#include "pack-objects.h" #include "packfile.h" +#include "path.h" +#include "promisor-remote.h" +#include "repack.h" +#include "run-command.h" #include "strbuf.h" +#include "string-list.h" +#include "strvec.h" +#include "tree.h" #include "write-or-die.h" static void odb_source_files_reparent(const char *name UNUSED, @@ -260,6 +270,464 @@ static int odb_source_files_write_alternate(struct odb_source *source, return ret; } +static int too_many_loose_objects(struct odb_source_files *files, int limit) +{ + unsigned long loose_count; + + if (limit <= 0) + return 0; + + if (odb_source_count_objects(&files->loose->base, ODB_COUNT_OBJECTS_APPROXIMATE, + &loose_count) < 0) + return 0; + + /* + * This is weird, but stems from legacy behaviour: the GC auto + * threshold was always essentially interpreted as if it was rounded up + * to the next multiple 256 of, so we retain this behaviour for now. + */ + return loose_count > (DIV_ROUND_UP(((unsigned long) limit), 256) * 256); +} + +static struct packed_git *find_base_packs(struct odb_source_files *files, + struct string_list *packs, + unsigned long limit) +{ + struct packfile_list_entry *e; + struct packed_git *base = NULL; + + for (e = packfile_store_get_packs(files->packed); e; e = e->next) { + if (e->pack->is_cruft) + continue; + if (limit) { + if ((uintmax_t) e->pack->pack_size >= limit) + string_list_append(packs, e->pack->pack_name); + } else if (!base || base->pack_size < e->pack->pack_size) { + base = e->pack; + } + } + + if (base) + string_list_append(packs, base->pack_name); + + return base; +} + +static int too_many_packs(struct odb_source_files *files, int gc_auto_pack_limit) +{ + struct packfile_list_entry *e; + int cnt = 0; + + if (gc_auto_pack_limit <= 0) + return 0; + + for (e = packfile_store_get_packs(files->packed); e; e = e->next) { + if (e->pack->pack_keep) + continue; + /* + * Perhaps check the size of the pack and count only + * very small ones here? + */ + cnt++; + } + return gc_auto_pack_limit < cnt; +} + +static uint64_t total_ram(void) +{ +#if defined(HAVE_SYSINFO) + struct sysinfo si; + + if (!sysinfo(&si)) { + uint64_t total = si.totalram; + + if (si.mem_unit > 1) + total *= (uint64_t)si.mem_unit; + return total; + } +#elif defined(HAVE_BSD_SYSCTL) && (defined(HW_MEMSIZE) || defined(HW_PHYSMEM) || defined(HW_PHYSMEM64)) + uint64_t physical_memory; + int mib[2]; + size_t length; + + mib[0] = CTL_HW; +# if defined(HW_MEMSIZE) + mib[1] = HW_MEMSIZE; +# elif defined(HW_PHYSMEM64) + mib[1] = HW_PHYSMEM64; +# else + mib[1] = HW_PHYSMEM; +# endif + length = sizeof(physical_memory); + if (!sysctl(mib, 2, &physical_memory, &length, NULL, 0)) { + if (length == 4) { + uint32_t mem; + + if (!sysctl(mib, 2, &mem, &length, NULL, 0)) + physical_memory = mem; + } + return physical_memory; + } +#elif defined(GIT_WINDOWS_NATIVE) + MEMORYSTATUSEX memInfo; + + memInfo.dwLength = sizeof(MEMORYSTATUSEX); + if (GlobalMemoryStatusEx(&memInfo)) + return memInfo.ullTotalPhys; +#endif + return 0; +} + +static uint64_t estimate_repack_memory(struct odb_source_files *files, + struct packed_git *pack) +{ + unsigned long max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE; + unsigned long delta_base_cache_limit = DEFAULT_DELTA_BASE_CACHE_LIMIT; + unsigned long nr_objects; + size_t os_cache, heap; + + if (odb_source_count_objects(&files->base, ODB_COUNT_OBJECTS_APPROXIMATE, + &nr_objects) < 0) + return 0; + + if (!pack || !nr_objects) + return 0; + + repo_config_get_ulong(files->base.odb->repo, "pack.deltacachesize", + &max_delta_cache_size); + repo_config_get_ulong(files->base.odb->repo, "core.deltabasecachelimit", + &delta_base_cache_limit); + + /* + * First we have to scan through at least one pack. + * Assume enough room in OS file cache to keep the entire pack + * or we may accidentally evict data of other processes from + * the cache. + */ + os_cache = pack->pack_size + pack->index_size; + /* then pack-objects needs lots more for book keeping */ + heap = sizeof(struct object_entry) * nr_objects; + /* + * internal rev-list --all --objects takes up some memory too, + * let's say half of it is for blobs + */ + heap += sizeof(struct blob) * nr_objects / 2; + /* + * and the other half is for trees (commits and tags are + * usually insignificant) + */ + heap += sizeof(struct tree) * nr_objects / 2; + /* and then obj_hash[], underestimated in fact */ + heap += sizeof(struct object *) * nr_objects; + /* revindex is used also */ + heap += (sizeof(off_t) + sizeof(uint32_t)) * nr_objects; + /* + * read_sha1_file() (either at delta calculation phase, or + * writing phase) also fills up the delta base cache + */ + heap += delta_base_cache_limit; + /* and of course pack-objects has its own delta cache */ + heap += max_delta_cache_size; + + return os_cache + heap; +} + +static int keep_one_pack(struct string_list_item *item, void *data) +{ + struct strvec *args = data; + strvec_pushf(args, "--keep-pack=%s", basename(item->string)); + return 0; +} + +static void add_repack_all_option(struct repository *repo, + const struct odb_optimize_options *opts, + struct string_list *keep_pack, + struct strvec *args) +{ + char *repack_filter = NULL; + char *repack_filter_to = NULL; + + repo_config_get_string(repo, "gc.repackfilter", &repack_filter); + repo_config_get_string(repo, "gc.repackfilterto", &repack_filter_to); + + if (opts->prune_expire && !strcmp(opts->prune_expire, "now") && + !(opts->cruft_packs && opts->expire_to)) + strvec_push(args, "-a"); + else if (opts->cruft_packs) { + strvec_push(args, "--cruft"); + if (opts->prune_expire) + strvec_pushf(args, "--cruft-expiration=%s", opts->prune_expire); + if (opts->max_cruft_size) + strvec_pushf(args, "--max-cruft-size=%lu", + opts->max_cruft_size); + if (opts->expire_to) + strvec_pushf(args, "--expire-to=%s", opts->expire_to); + } else { + strvec_push(args, "-A"); + if (opts->prune_expire) + strvec_pushf(args, "--unpack-unreachable=%s", opts->prune_expire); + } + + if (keep_pack) + for_each_string_list(keep_pack, keep_one_pack, args); + + if (repack_filter && *repack_filter) + strvec_pushf(args, "--filter=%s", repack_filter); + if (repack_filter_to && *repack_filter_to) + strvec_pushf(args, "--filter-to=%s", repack_filter_to); + + free(repack_filter); + free(repack_filter_to); +} + +static void add_repack_incremental_option(struct strvec *args) +{ + strvec_push(args, "--no-write-bitmap-index"); +} + +bool odb_source_files_optimize_required(struct odb_source *source, + const struct odb_optimize_options *opts) +{ + struct odb_source_files *files = odb_source_files_downcast(source); + struct repository *repo = source->odb->repo; + + switch (opts->strategy) { + case ODB_OPTIMIZE_INCREMENTAL: { + int gc_auto_threshold = 6700; + int gc_auto_pack_limit = 50; + + repo_config_get_int(repo, "gc.auto", &gc_auto_threshold); + repo_config_get_int(repo, "gc.autopacklimit", &gc_auto_pack_limit); + + /* + * Setting gc.auto to 0 or negative can disable the + * automatic gc. + */ + if (gc_auto_threshold <= 0) + return false; + if (!too_many_packs(files, gc_auto_pack_limit) && + !too_many_loose_objects(files, gc_auto_threshold)) + return false; + + return true; + } + case ODB_OPTIMIZE_GEOMETRIC: { + struct pack_geometry geometry = { + .split_factor = 2, + }; + struct pack_objects_args po_args = { + .local = 1, + }; + struct existing_packs existing_packs = EXISTING_PACKS_INIT; + struct string_list kept_packs = STRING_LIST_INIT_DUP; + int auto_value = 100; + bool ret; + + repo_config_get_int(repo, "maintenance.geometric-repack.auto", + &auto_value); + if (!auto_value) + return false; + if (auto_value < 0) + return true; + + repo_config_get_int(repo, "maintenance.geometric-repack.splitFactor", + &geometry.split_factor); + + existing_packs.repo = repo; + existing_packs_collect(&existing_packs, &kept_packs); + pack_geometry_init(&geometry, &existing_packs, &po_args); + pack_geometry_split(&geometry); + + /* + * When we'd merge at least two packs with one another we always + * perform the repack. + */ + if (geometry.split) { + ret = true; + goto out; + } + + /* + * Otherwise, we estimate the number of loose objects to determine + * whether we want to create a new packfile or not. + */ + if (too_many_loose_objects(files, auto_value)) { + ret = true; + goto out; + } + + ret = false; + + out: + existing_packs_release(&existing_packs); + pack_geometry_release(&geometry); + return ret; + } + default: + BUG("unknown maintenance strategy '%d'", opts->strategy); + } +} + +int odb_source_files_optimize(struct odb_source *source, + const struct odb_optimize_options *opts) +{ + struct odb_source_files *files = odb_source_files_downcast(source); + struct repository *repo = source->odb->repo; + struct child_process repack_cmd = CHILD_PROCESS_INIT; + unsigned long big_pack_threshold = 0; + int gc_auto_threshold = 6700; + int gc_auto_pack_limit = 50; + int ret; + + repo_config_get_int(repo, "gc.auto", &gc_auto_threshold); + repo_config_get_int(repo, "gc.autopacklimit", &gc_auto_pack_limit); + repo_config_get_ulong(repo, "gc.bigpackthreshold", &big_pack_threshold); + + if (repo->repository_format_precious_objects) + return 0; + + repack_cmd.git_cmd = 1; + repack_cmd.odb_to_close = repo->objects; + + strvec_pushl(&repack_cmd.args, "repack", "-d", "-l", NULL); + if (opts->flags & ODB_OPTIMIZE_NO_REUSE_DELTAS) + strvec_push(&repack_cmd.args, "-f"); + if (opts->depth > 0) + strvec_pushf(&repack_cmd.args, "--depth=%d", opts->depth); + if (opts->window > 0) + strvec_pushf(&repack_cmd.args, "--window=%d", opts->window); + if (!(opts->flags & ODB_OPTIMIZE_VERBOSE)) + strvec_push(&repack_cmd.args, "-q"); + + /* + * There's three cases we need to consider: + * + * - If we're invoked without `--auto` we'll need to perform a full + * repack. + * + * - If we're invoked with `--auto` and there's too many packs, then + * we perform a full repack, as well. + * + * - Otherwise we perform an incremental repack. + */ + switch (opts->strategy) { + case ODB_OPTIMIZE_INCREMENTAL: + if (!(opts->flags & ODB_OPTIMIZE_AUTO)) { + struct string_list keep_pack = STRING_LIST_INIT_NODUP; + + if (opts->keep_largest_pack != -1) { + if (opts->keep_largest_pack) + find_base_packs(files, &keep_pack, 0); + } else if (big_pack_threshold) { + find_base_packs(files, &keep_pack, big_pack_threshold); + } + + add_repack_all_option(repo, opts, &keep_pack, &repack_cmd.args); + string_list_clear(&keep_pack, 0); + } else { + if (too_many_packs(files, gc_auto_pack_limit)) { + struct string_list keep_pack = STRING_LIST_INIT_NODUP; + + if (big_pack_threshold) { + find_base_packs(files, &keep_pack, big_pack_threshold); + if (keep_pack.nr >= (unsigned long) gc_auto_pack_limit) { + string_list_clear(&keep_pack, 0); + find_base_packs(files, &keep_pack, 0); + } + } else { + struct packed_git *p = find_base_packs(files, &keep_pack, 0); + uint64_t mem_have, mem_want; + + mem_have = total_ram(); + mem_want = estimate_repack_memory(files, p); + + /* + * Only allow 1/2 of memory for pack-objects, leave + * the rest for the OS and other processes in the + * system. + */ + if (!mem_have || mem_want < mem_have / 2) + string_list_clear(&keep_pack, 0); + } + + add_repack_all_option(repo, opts, &keep_pack, &repack_cmd.args); + string_list_clear(&keep_pack, 0); + } else { + add_repack_incremental_option(&repack_cmd.args); + } + } + + break; + case ODB_OPTIMIZE_GEOMETRIC: { + struct pack_geometry geometry = { + .split_factor = 2, + }; + struct pack_objects_args po_args = { + .local = 1, + }; + struct existing_packs existing_packs = EXISTING_PACKS_INIT; + struct string_list kept_packs = STRING_LIST_INIT_DUP; + + repo_config_get_int(repo, "maintenance.geometric-repack.splitFactor", + &geometry.split_factor); + + existing_packs.repo = repo; + existing_packs_collect(&existing_packs, &kept_packs); + pack_geometry_init(&geometry, &existing_packs, &po_args); + pack_geometry_split(&geometry); + + if (geometry.split < geometry.pack_nr) { + strvec_pushf(&repack_cmd.args, "--geometric=%d", + geometry.split_factor); + } else { + add_repack_all_option(repo, opts, NULL, &repack_cmd.args); + } + if (repo->settings.core_multi_pack_index) + strvec_push(&repack_cmd.args, "--write-midx"); + + existing_packs_release(&existing_packs); + pack_geometry_release(&geometry); + break; + } + default: + die("unknown maintenance strategy '%d'", opts->strategy); + } + + if (run_command(&repack_cmd)) { + ret = error("failed to run %s", repack_cmd.args.v[0]); + goto out; + } + + /* Geometric repacking uses cruft packs, so we don't have to prune separately. */ + if (opts->strategy != ODB_OPTIMIZE_GEOMETRIC && opts->prune_expire) { + struct child_process prune_cmd = CHILD_PROCESS_INIT; + + strvec_pushl(&prune_cmd.args, "prune", "--expire", NULL); + /* run `git prune` even if using cruft packs */ + strvec_push(&prune_cmd.args, opts->prune_expire); + if (!(opts->flags & ODB_OPTIMIZE_VERBOSE)) + strvec_push(&prune_cmd.args, "--no-progress"); + if (repo_has_promisor_remote(repo)) + strvec_push(&prune_cmd.args, + "--exclude-promisor-objects"); + prune_cmd.git_cmd = 1; + + if (run_command(&prune_cmd)) { + ret = error("failed to run %s", prune_cmd.args.v[0]); + goto out; + } + } + + if (opts->flags & ODB_OPTIMIZE_AUTO && too_many_loose_objects(files, gc_auto_threshold)) + warning(_("There are too many unreachable loose objects; " + "run 'git prune' to remove them.")); + + ret = 0; + +out: + return ret; +} + struct odb_source_files *odb_source_files_new(struct object_database *odb, const char *path, bool local) @@ -285,6 +753,8 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb, files->base.begin_transaction = odb_source_files_begin_transaction; files->base.read_alternates = odb_source_files_read_alternates; files->base.write_alternate = odb_source_files_write_alternate; + files->base.optimize = odb_source_files_optimize; + files->base.optimize_required = odb_source_files_optimize_required; /* * Ideally, we would only ever store absolute paths in the source. This diff --git a/odb/source-files.h b/odb/source-files.h index d7ac3c1c81d892..044242bc36e4a7 100644 --- a/odb/source-files.h +++ b/odb/source-files.h @@ -21,6 +21,21 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb, const char *path, bool local); +/* + * Optimize the files object database source by repacking loose objects and + * packfiles as needed. Returns 0 on success, a negative error code otherwise. + */ +int odb_source_files_optimize(struct odb_source *source, + const struct odb_optimize_options *opts); + +/* + * Check whether optimization of the files object database source is required + * given the provided options. Returns true if optimization should be + * performed, false otherwise. + */ +bool odb_source_files_optimize_required(struct odb_source *source, + const struct odb_optimize_options *opts); + /* * Cast the given object database source to the files backend. This will cause * a BUG in case the source doesn't use this backend. diff --git a/odb/source.h b/odb/source.h index 8767708c9c769c..88a48ba3c3075b 100644 --- a/odb/source.h +++ b/odb/source.h @@ -258,6 +258,21 @@ struct odb_source { */ int (*write_alternate)(struct odb_source *source, const char *alternate); + + /* + * This callback is expected to optimize the object database source. + * Returns 0 on success, a negative error code otherwise. + */ + int (*optimize)(struct odb_source *source, + const struct odb_optimize_options *opts); + + /* + * This callback is expected to check whether optimization of the + * object database source is required given the provided options. + * Returns true if optimization should be performed, false otherwise. + */ + bool (*optimize_required)(struct odb_source *source, + const struct odb_optimize_options *opts); }; /* @@ -475,4 +490,25 @@ static inline int odb_source_begin_transaction(struct odb_source *source, return source->begin_transaction(source, out); } +/* + * Optimize the object database source. Returns 0 on success, a negative error + * code otherwise. + */ +static inline int odb_source_optimize(struct odb_source *source, + const struct odb_optimize_options *opts) +{ + return source->optimize(source, opts); +} + +/* + * Check whether optimization of the object database source is required given + * the provided options. Returns true if optimization should be performed, + * false otherwise. + */ +static inline bool odb_source_optimize_required(struct odb_source *source, + const struct odb_optimize_options *opts) +{ + return source->optimize_required(source, opts); +} + #endif From 0201d2783e6317c7b76e9c511cbfcfa73fdc17b1 Mon Sep 17 00:00:00 2001 From: Tian Yuchen Date: Tue, 14 Jul 2026 11:25:16 +0800 Subject: [PATCH 019/280] repository: introduce repo_config_values_clear() As part of the ongoing libification effort, dynamically allocated global configuration variables are being moved into 'struct repo_config_values'. To prevent memory leaks, we need a destructor to free these heap-allocated variables when a repository instance is torn down. Introduce 'repo_config_values_clear()' in environment.c and invoke it from 'repo_clear()' in repository.c. As a starting point, update this new function to handle the cleanup of 'attributes_file'. Mentored-by: Christian Couder Mentored-by: Ayush Chandekar Mentored-by: Olamide Caleb Bello Signed-off-by: Tian Yuchen Signed-off-by: Junio C Hamano --- environment.c | 5 +++++ environment.h | 9 +++++++++ repository.c | 1 + 3 files changed, 15 insertions(+) diff --git a/environment.c b/environment.c index ba2c60103ff51c..ae05f16d041858 100644 --- a/environment.c +++ b/environment.c @@ -726,3 +726,8 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->sparse_expect_files_outside_of_patterns = 0; cfg->warn_on_object_refname_ambiguity = 1; } + +void repo_config_values_clear(struct repo_config_values *cfg) +{ + FREE_AND_NULL(cfg->attributes_file); +} diff --git a/environment.h b/environment.h index 6f182869558395..9169d7f62d9941 100644 --- a/environment.h +++ b/environment.h @@ -135,6 +135,15 @@ int git_default_core_config(const char *var, const char *value, void repo_config_values_init(struct repo_config_values *cfg); +/* + * Frees memory allocated for dynamically loaded configuration values + * inside `repo_config_values`. + * + * As dynamically allocated variables are migrated into this struct, + * their FREE_AND_NULL() calls should be appended here. + */ +void repo_config_values_clear(struct repo_config_values *cfg); + /* * TODO: All the below state either explicitly or implicitly relies on * `the_repository`. We should eventually get rid of these and make the diff --git a/repository.c b/repository.c index 187dd471c4e607..669e2d12004d77 100644 --- a/repository.c +++ b/repository.c @@ -388,6 +388,7 @@ void repo_clear(struct repository *repo) FREE_AND_NULL(repo->parsed_objects); repo_settings_clear(repo); + repo_config_values_clear(&repo->config_values_private_); if (repo->config) { git_configset_clear(repo->config); From 7488b9d1fd505ce1194b98893508fa5b194cf0cd Mon Sep 17 00:00:00 2001 From: Tian Yuchen Date: Tue, 14 Jul 2026 11:25:17 +0800 Subject: [PATCH 020/280] environment: move excludes_file into repo_config_values The global variable 'excludes_file' is used to track the path to the global ignore file. If this variable is NULL, 'setup_standard_excludes()' in 'dir.c' forcefully evaluates and assigns the XDG default path to it. Continue the libification effort by encapsulating this lazy-loading fallback logic into a proper getter and moving the variable into 'struct repo_config_values'. Since 'excludes_file' is a dynamically allocated string, it requires proper heap memory management. It is safely freed using the newly introduced 'repo_config_values_clear()' function when the repository is torn down. Mentored-by: Christian Couder Mentored-by: Ayush Chandekar Mentored-by: Olamide Caleb Bello Signed-off-by: Tian Yuchen Signed-off-by: Junio C Hamano --- dir.c | 4 ++-- environment.c | 17 ++++++++++++++--- environment.h | 4 +++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/dir.c b/dir.c index 32430090dcdf26..baaf899d9c47c4 100644 --- a/dir.c +++ b/dir.c @@ -3481,11 +3481,11 @@ static GIT_PATH_FUNC(git_path_info_exclude, "info/exclude") void setup_standard_excludes(struct dir_struct *dir) { + const char *excludes_file = repo_excludes_file(the_repository); + dir->exclude_per_dir = ".gitignore"; /* core.excludesfile defaulting to $XDG_CONFIG_HOME/git/ignore */ - if (!excludes_file) - excludes_file = xdg_config_home("ignore"); if (excludes_file && !access_or_warn(excludes_file, R_OK, 0)) add_patterns_from_file_1(dir, excludes_file, dir->untracked ? &dir->internal.ss_excludes_file : NULL); diff --git a/environment.c b/environment.c index ae05f16d041858..275931c213fb5c 100644 --- a/environment.c +++ b/environment.c @@ -57,7 +57,6 @@ enum fsync_method fsync_method = FSYNC_METHOD_DEFAULT; enum fsync_component fsync_components = FSYNC_COMPONENTS_DEFAULT; char *editor_program; char *askpass_program; -char *excludes_file; enum auto_crlf auto_crlf = AUTO_CRLF_FALSE; enum eol core_eol = EOL_UNSET; int global_conv_flags_eol = CONV_EOL_RNDTRP_WARN; @@ -134,6 +133,16 @@ int is_bare_repository(void) return is_bare_repository_cfg && !repo_get_work_tree(the_repository); } +const char *repo_excludes_file(struct repository *repo) +{ + struct repo_config_values *cfg = repo_config_values(repo); + + if (!cfg->excludes_file) + cfg->excludes_file = xdg_config_home("ignore"); + + return cfg->excludes_file; +} + int have_git_dir(void) { return startup_info->have_repository @@ -461,8 +470,8 @@ int git_default_core_config(const char *var, const char *value, } if (!strcmp(var, "core.excludesfile")) { - FREE_AND_NULL(excludes_file); - return git_config_pathname(&excludes_file, var, value); + FREE_AND_NULL(cfg->excludes_file); + return git_config_pathname(&cfg->excludes_file, var, value); } if (!strcmp(var, "core.whitespace")) { @@ -715,6 +724,7 @@ int git_default_config(const char *var, const char *value, void repo_config_values_init(struct repo_config_values *cfg) { cfg->attributes_file = NULL; + cfg->excludes_file = NULL; cfg->apply_sparse_checkout = 0; cfg->branch_track = BRANCH_TRACK_REMOTE; cfg->trust_ctime = 1; @@ -730,4 +740,5 @@ void repo_config_values_init(struct repo_config_values *cfg) void repo_config_values_clear(struct repo_config_values *cfg) { FREE_AND_NULL(cfg->attributes_file); + FREE_AND_NULL(cfg->excludes_file); } diff --git a/environment.h b/environment.h index 9169d7f62d9941..4776ccc6579051 100644 --- a/environment.h +++ b/environment.h @@ -90,6 +90,7 @@ struct repository; struct repo_config_values { /* section "core" config values */ char *attributes_file; + char *excludes_file; int apply_sparse_checkout; int trust_ctime; int check_stat; @@ -133,6 +134,8 @@ int git_default_config(const char *, const char *, int git_default_core_config(const char *var, const char *value, const struct config_context *ctx, void *cb); +const char *repo_excludes_file(struct repository *repo); + void repo_config_values_init(struct repo_config_values *cfg); /* @@ -217,7 +220,6 @@ extern char *git_log_output_encoding; extern char *editor_program; extern char *askpass_program; -extern char *excludes_file; /* * The character that begins a commented line in user-editable file From 469e95c2573e938c0963ba52e8d06dd6aa7ba262 Mon Sep 17 00:00:00 2001 From: Tian Yuchen Date: Tue, 14 Jul 2026 11:25:18 +0800 Subject: [PATCH 021/280] environment: move editor_program into repo_config_values The global variable 'editor_program' holds the path to the user's preferred editor. Move 'editor_program' into 'struct repo_config_values' to continue the libification effort. There have been discussions on whether external programs like editors truly need to be configured on a per-repository basis within the same process. While a single process might rarely invoke different editors, this migration is necessary for two reasons: 1. Developers frequently use different toolchains for different projects. Per-repo configuration respects this. 2. Moving this string into 'repo_config_values' eliminates mutable global state. As the codebase moves toward becoming a long-running processes, managing multiple repositories concurrently must not overwrite each other's program configurations. No standalone getter function is introduced. Callers directly access the field via 'repo_config_values()'. Heap memory is safely reclaimed in 'repo_config_values_clear()'. Mentored-by: Christian Couder Mentored-by: Ayush Chandekar Mentored-by: Olamide Caleb Bello Signed-off-by: Tian Yuchen Signed-off-by: Junio C Hamano --- editor.c | 4 ++-- environment.c | 7 ++++--- environment.h | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/editor.c b/editor.c index fd174e6a034f1c..0d1cb8768db1a0 100644 --- a/editor.c +++ b/editor.c @@ -29,8 +29,8 @@ const char *git_editor(void) const char *editor = getenv("GIT_EDITOR"); int terminal_is_dumb = is_terminal_dumb(); - if (!editor && editor_program) - editor = editor_program; + if (!editor) + editor = repo_config_values(the_repository)->editor_program; if (!editor && !terminal_is_dumb) editor = getenv("VISUAL"); if (!editor) diff --git a/environment.c b/environment.c index 275931c213fb5c..a65d575af4a80d 100644 --- a/environment.c +++ b/environment.c @@ -55,7 +55,6 @@ int fsync_object_files = -1; int use_fsync = -1; enum fsync_method fsync_method = FSYNC_METHOD_DEFAULT; enum fsync_component fsync_components = FSYNC_COMPONENTS_DEFAULT; -char *editor_program; char *askpass_program; enum auto_crlf auto_crlf = AUTO_CRLF_FALSE; enum eol core_eol = EOL_UNSET; @@ -437,8 +436,8 @@ int git_default_core_config(const char *var, const char *value, } if (!strcmp(var, "core.editor")) { - FREE_AND_NULL(editor_program); - return git_config_string(&editor_program, var, value); + FREE_AND_NULL(cfg->editor_program); + return git_config_string(&cfg->editor_program, var, value); } if (!strcmp(var, "core.commentchar") || @@ -725,6 +724,7 @@ void repo_config_values_init(struct repo_config_values *cfg) { cfg->attributes_file = NULL; cfg->excludes_file = NULL; + cfg->editor_program = NULL; cfg->apply_sparse_checkout = 0; cfg->branch_track = BRANCH_TRACK_REMOTE; cfg->trust_ctime = 1; @@ -741,4 +741,5 @@ void repo_config_values_clear(struct repo_config_values *cfg) { FREE_AND_NULL(cfg->attributes_file); FREE_AND_NULL(cfg->excludes_file); + FREE_AND_NULL(cfg->editor_program); } diff --git a/environment.h b/environment.h index 4776ccc6579051..8178ebab76bba4 100644 --- a/environment.h +++ b/environment.h @@ -91,6 +91,7 @@ struct repo_config_values { /* section "core" config values */ char *attributes_file; char *excludes_file; + char *editor_program; int apply_sparse_checkout; int trust_ctime; int check_stat; @@ -218,7 +219,6 @@ const char *get_commit_output_encoding(void); extern char *git_commit_encoding; extern char *git_log_output_encoding; -extern char *editor_program; extern char *askpass_program; /* From e57125d804f29b8a6c70a47a03037cf31ef153d7 Mon Sep 17 00:00:00 2001 From: Tian Yuchen Date: Tue, 14 Jul 2026 11:25:19 +0800 Subject: [PATCH 022/280] environment: move pager_program into repo_config_values The 'pager_program' variable is currently defined as a file-scoped static string in pager.c. Move it into 'struct repo_config_values'. The configuration parsing logic remains strictly within pager.c to respect subsystem boundaries. The read/write operations are simply redirected to the repository-specific structure using 'repo_config_values()'. All current callers indeed pass 'the_repository', so this new enforcement does not harm them. Similar to the recent editor_program migration, no standalone getter is introduced to keep the code minimal. The dynamically allocated memory is now managed by 'repo_config_values_clear()'. On top of that, fix memory leaks in pager.c while we are at it. Mentored-by: Christian Couder Mentored-by: Ayush Chandekar Mentored-by: Olamide Caleb Bello Signed-off-by: Tian Yuchen Signed-off-by: Junio C Hamano --- environment.c | 2 ++ environment.h | 1 + pager.c | 32 +++++++++++++++++++++++--------- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/environment.c b/environment.c index a65d575af4a80d..975c9cb9ebdb85 100644 --- a/environment.c +++ b/environment.c @@ -725,6 +725,7 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->attributes_file = NULL; cfg->excludes_file = NULL; cfg->editor_program = NULL; + cfg->pager_program = NULL; cfg->apply_sparse_checkout = 0; cfg->branch_track = BRANCH_TRACK_REMOTE; cfg->trust_ctime = 1; @@ -742,4 +743,5 @@ void repo_config_values_clear(struct repo_config_values *cfg) FREE_AND_NULL(cfg->attributes_file); FREE_AND_NULL(cfg->excludes_file); FREE_AND_NULL(cfg->editor_program); + FREE_AND_NULL(cfg->pager_program); } diff --git a/environment.h b/environment.h index 8178ebab76bba4..39b6691b478544 100644 --- a/environment.h +++ b/environment.h @@ -92,6 +92,7 @@ struct repo_config_values { char *attributes_file; char *excludes_file; char *editor_program; + char *pager_program; int apply_sparse_checkout; int trust_ctime; int check_stat; diff --git a/pager.c b/pager.c index 35b210e0484f90..543ef129366a19 100644 --- a/pager.c +++ b/pager.c @@ -5,6 +5,8 @@ #include "run-command.h" #include "sigchain.h" #include "alias.h" +#include "repository.h" +#include "environment.h" int pager_use_color = 1; @@ -13,7 +15,6 @@ int pager_use_color = 1; #endif static struct child_process pager_process; -static char *pager_program; static int old_fd1 = -1, old_fd2 = -1; /* Is the value coming back from term_columns() just a guess? */ @@ -75,10 +76,17 @@ static void wait_for_pager_signal(int signo) static int core_pager_config(const char *var, const char *value, const struct config_context *ctx UNUSED, - void *data UNUSED) + void *data) { - if (!strcmp(var, "core.pager")) - return git_config_string(&pager_program, var, value); + struct repository *r = data; + + if (!strcmp(var, "core.pager")) { + struct repo_config_values *cfg = repo_config_values(r); + + FREE_AND_NULL(cfg->pager_program); + return git_config_string(&cfg->pager_program, var, value); + } + return 0; } @@ -91,10 +99,12 @@ const char *git_pager(struct repository *r, int stdout_is_tty) pager = getenv("GIT_PAGER"); if (!pager) { - if (!pager_program) + struct repo_config_values *cfg = repo_config_values(r); + + if (!cfg->pager_program) read_early_config(r, - core_pager_config, NULL); - pager = pager_program; + core_pager_config, r); + pager = cfg->pager_program; } if (!pager) pager = getenv("PAGER"); @@ -302,7 +312,11 @@ int check_pager_config(struct repository *r, const char *cmd) read_early_config(r, pager_command_config, &data); - if (data.value) - pager_program = data.value; + if (data.value) { + struct repo_config_values *cfg = repo_config_values(r); + + free(cfg->pager_program); + cfg->pager_program = data.value; + } return data.want; } From 48cbe400794bd055d259143a01024da71da5fe1a Mon Sep 17 00:00:00 2001 From: Tian Yuchen Date: Tue, 14 Jul 2026 11:25:20 +0800 Subject: [PATCH 023/280] environment: move askpass_program into repo_config_values The global variable 'askpass_program' stores the path to the program used to prompt the user for credentials. Move it into repo_config_values to continue the libification effort. While it is uncommon for a single process to require different askpass programs for different repositories, maintaining this value as a mutable global string is a blocker for libification. Global heap-allocated strings introduce thread-safety issues in a multi-repo environment. Move 'askpass_program' into 'struct repo_config_values' to eliminate this global state. The memory is now safely managed and freed via 'repo_config_values_clear()'. Mentored-by: Christian Couder Mentored-by: Ayush Chandekar Mentored-by: Olamide Caleb Bello Signed-off-by: Tian Yuchen Signed-off-by: Junio C Hamano --- environment.c | 7 ++++--- environment.h | 3 +-- prompt.c | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/environment.c b/environment.c index 975c9cb9ebdb85..3857818da3f96c 100644 --- a/environment.c +++ b/environment.c @@ -55,7 +55,6 @@ int fsync_object_files = -1; int use_fsync = -1; enum fsync_method fsync_method = FSYNC_METHOD_DEFAULT; enum fsync_component fsync_components = FSYNC_COMPONENTS_DEFAULT; -char *askpass_program; enum auto_crlf auto_crlf = AUTO_CRLF_FALSE; enum eol core_eol = EOL_UNSET; int global_conv_flags_eol = CONV_EOL_RNDTRP_WARN; @@ -464,8 +463,8 @@ int git_default_core_config(const char *var, const char *value, } if (!strcmp(var, "core.askpass")) { - FREE_AND_NULL(askpass_program); - return git_config_string(&askpass_program, var, value); + FREE_AND_NULL(cfg->askpass_program); + return git_config_string(&cfg->askpass_program, var, value); } if (!strcmp(var, "core.excludesfile")) { @@ -726,6 +725,7 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->excludes_file = NULL; cfg->editor_program = NULL; cfg->pager_program = NULL; + cfg->askpass_program = NULL; cfg->apply_sparse_checkout = 0; cfg->branch_track = BRANCH_TRACK_REMOTE; cfg->trust_ctime = 1; @@ -744,4 +744,5 @@ void repo_config_values_clear(struct repo_config_values *cfg) FREE_AND_NULL(cfg->excludes_file); FREE_AND_NULL(cfg->editor_program); FREE_AND_NULL(cfg->pager_program); + FREE_AND_NULL(cfg->askpass_program); } diff --git a/environment.h b/environment.h index 39b6691b478544..856dc70cc47170 100644 --- a/environment.h +++ b/environment.h @@ -93,6 +93,7 @@ struct repo_config_values { char *excludes_file; char *editor_program; char *pager_program; + char *askpass_program; int apply_sparse_checkout; int trust_ctime; int check_stat; @@ -220,8 +221,6 @@ const char *get_commit_output_encoding(void); extern char *git_commit_encoding; extern char *git_log_output_encoding; -extern char *askpass_program; - /* * The character that begins a commented line in user-editable file * that is subject to stripspace. diff --git a/prompt.c b/prompt.c index 706fba2a5030c7..d8d74c7e379dbe 100644 --- a/prompt.c +++ b/prompt.c @@ -3,6 +3,7 @@ #include "git-compat-util.h" #include "parse.h" #include "environment.h" +#include "repository.h" #include "run-command.h" #include "strbuf.h" #include "prompt.h" @@ -51,7 +52,7 @@ char *git_prompt(const char *prompt, int flags) askpass = getenv("GIT_ASKPASS"); if (!askpass) - askpass = askpass_program; + askpass = repo_config_values(the_repository)->askpass_program; if (!askpass) askpass = getenv("SSH_ASKPASS"); if (askpass && *askpass) From a9f5e90fb9068fd1a1a9a7c9a60f005e7ed392dd Mon Sep 17 00:00:00 2001 From: Tian Yuchen Date: Tue, 14 Jul 2026 11:25:21 +0800 Subject: [PATCH 024/280] environment: migrate apply_default_whitespace and apply_default_ignorewhitespace The global variables 'apply_default_whitespace' and 'apply_default_ignorewhitespace' are used to store the default whitespace configuration for 'git apply'. Move these variables into 'struct repo_config_values' to continue the libification effort. Dynamically allocated strings fetched via 'repo_config_get_string()' are now tracked per-repository and safely freed in 'repo_config_values_clear()'. As part of this transition, update 'git_apply_config()' to accept a 'struct repository *' argument rather than relying on the 'the_repository' global. Mentored-by: Christian Couder Mentored-by: Ayush Chandekar Mentored-by: Olamide Caleb Bello Signed-off-by: Tian Yuchen Signed-off-by: Junio C Hamano --- apply.c | 28 ++++++++++++++++++++-------- environment.c | 6 ++++-- environment.h | 4 ++-- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/apply.c b/apply.c index 5e54453f799ea2..7e5b12323ea79f 100644 --- a/apply.c +++ b/apply.c @@ -47,11 +47,17 @@ struct gitdiff_data { int p_value; }; -static void git_apply_config(void) +static void git_apply_config(struct repository *repo) { - repo_config_get_string(the_repository, "apply.whitespace", &apply_default_whitespace); - repo_config_get_string(the_repository, "apply.ignorewhitespace", &apply_default_ignorewhitespace); - repo_config(the_repository, git_xmerge_config, NULL); + struct repo_config_values *cfg = repo_config_values(repo); + + FREE_AND_NULL(cfg->apply_default_whitespace); + repo_config_get_string(repo, "apply.whitespace", + &cfg->apply_default_whitespace); + FREE_AND_NULL(cfg->apply_default_ignorewhitespace); + repo_config_get_string(repo, "apply.ignorewhitespace", + &cfg->apply_default_ignorewhitespace); + repo_config(repo, git_xmerge_config, NULL); } static int parse_whitespace_option(struct apply_state *state, const char *option) @@ -109,6 +115,8 @@ int init_apply_state(struct apply_state *state, struct repository *repo, const char *prefix) { + struct repo_config_values *cfg = repo_config_values(repo); + memset(state, 0, sizeof(*state)); state->prefix = prefix; state->repo = repo; @@ -126,10 +134,13 @@ int init_apply_state(struct apply_state *state, strset_init(&state->kept_symlinks); strbuf_init(&state->root, 0); - git_apply_config(); - if (apply_default_whitespace && parse_whitespace_option(state, apply_default_whitespace)) + git_apply_config(repo); + + if (cfg->apply_default_whitespace && + parse_whitespace_option(state, cfg->apply_default_whitespace)) return -1; - if (apply_default_ignorewhitespace && parse_ignorewhitespace_option(state, apply_default_ignorewhitespace)) + if (cfg->apply_default_ignorewhitespace && + parse_ignorewhitespace_option(state, cfg->apply_default_ignorewhitespace)) return -1; return 0; } @@ -192,7 +203,8 @@ int check_apply_state(struct apply_state *state, int force_apply) static void set_default_whitespace_mode(struct apply_state *state) { - if (!state->whitespace_option && !apply_default_whitespace) + if (!state->whitespace_option && + !repo_config_values(state->repo)->apply_default_whitespace) state->ws_error_action = (state->apply ? warn_on_ws_error : nowarn_ws_error); } diff --git a/environment.c b/environment.c index 3857818da3f96c..20500658a20d67 100644 --- a/environment.c +++ b/environment.c @@ -49,8 +49,6 @@ int assume_unchanged; int is_bare_repository_cfg = -1; /* unspecified */ char *git_commit_encoding; char *git_log_output_encoding; -char *apply_default_whitespace; -char *apply_default_ignorewhitespace; int fsync_object_files = -1; int use_fsync = -1; enum fsync_method fsync_method = FSYNC_METHOD_DEFAULT; @@ -726,6 +724,8 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->editor_program = NULL; cfg->pager_program = NULL; cfg->askpass_program = NULL; + cfg->apply_default_whitespace = NULL; + cfg->apply_default_ignorewhitespace = NULL; cfg->apply_sparse_checkout = 0; cfg->branch_track = BRANCH_TRACK_REMOTE; cfg->trust_ctime = 1; @@ -745,4 +745,6 @@ void repo_config_values_clear(struct repo_config_values *cfg) FREE_AND_NULL(cfg->editor_program); FREE_AND_NULL(cfg->pager_program); FREE_AND_NULL(cfg->askpass_program); + FREE_AND_NULL(cfg->apply_default_whitespace); + FREE_AND_NULL(cfg->apply_default_ignorewhitespace); } diff --git a/environment.h b/environment.h index 856dc70cc47170..f450242ac03bad 100644 --- a/environment.h +++ b/environment.h @@ -94,6 +94,8 @@ struct repo_config_values { char *editor_program; char *pager_program; char *askpass_program; + char *apply_default_whitespace; + char *apply_default_ignorewhitespace; int apply_sparse_checkout; int trust_ctime; int check_stat; @@ -182,8 +184,6 @@ extern int has_symlinks; extern int minimum_abbrev, default_abbrev; extern int ignore_case; extern int assume_unchanged; -extern char *apply_default_whitespace; -extern char *apply_default_ignorewhitespace; extern unsigned long pack_size_limit_cfg; extern int protect_hfs; From 1a6c84e98dffe06fbe1acdf03817dffa10e180ef Mon Sep 17 00:00:00 2001 From: Tian Yuchen Date: Tue, 14 Jul 2026 11:25:22 +0800 Subject: [PATCH 025/280] environment: move push_default into repo_config_values The global variable 'push_default' specifies the default behavior of 'git push' when no explicit refspec is provided. Move 'push_default' into 'struct repo_config_values' to continue the libification effort. While 'enum push_default_type' ideally belongs in 'remote.h', moving it there introduces a circular dependency chain: remote.h -> hash.h -> repository.h -> environment.h. Therefore, the enum definition is kept in 'environment.h' just above 'struct repo_config_values' with a NEEDSWORK comment for future cleanup. Modify the configuration parsing in environment.c to update the per-repository structure directly, and update caller across the codebase to access the value via 'repo_config_values()'. Mentored-by: Christian Couder Mentored-by: Ayush Chandekar Mentored-by: Olamide Caleb Bello Signed-off-by: Tian Yuchen Signed-off-by: Junio C Hamano --- builtin/push.c | 10 ++++++---- environment.c | 16 +++++++++------- environment.h | 26 ++++++++++++++++---------- remote.c | 2 +- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/builtin/push.c b/builtin/push.c index 6021b71d668455..7578ff38c42ef3 100644 --- a/builtin/push.c +++ b/builtin/push.c @@ -73,6 +73,7 @@ static void refspec_append_mapped(struct refspec *refspec, const char *ref, struct remote *remote, struct ref *matched) { const char *branch_name; + struct repo_config_values *cfg = repo_config_values(the_repository); if (remote->push.nr) { struct refspec_item query = { @@ -88,7 +89,7 @@ static void refspec_append_mapped(struct refspec *refspec, const char *ref, } } - if (push_default == PUSH_DEFAULT_UPSTREAM && + if (cfg->push_default == PUSH_DEFAULT_UPSTREAM && skip_prefix(matched->name, "refs/heads/", &branch_name)) { struct branch *branch = branch_get(branch_name); if (branch->merge_nr == 1 && branch->merge[0]->src) { @@ -160,7 +161,7 @@ static NORETURN void die_push_simple(struct branch *branch, * Don't show advice for people who explicitly set * push.default. */ - if (push_default == PUSH_DEFAULT_UNSPECIFIED) + if (cfg->push_default == PUSH_DEFAULT_UNSPECIFIED) advice_pushdefault_maybe = _("\n" "To choose either option permanently, " "see push.default in 'git help config'.\n"); @@ -231,8 +232,9 @@ static void setup_default_push_refspecs(int *flags, struct remote *remote) struct branch *branch; const char *dst; int same_remote; + struct repo_config_values *cfg = repo_config_values(the_repository); - switch (push_default) { + switch (cfg->push_default) { case PUSH_DEFAULT_MATCHING: refspec_append(&rs, ":"); return; @@ -252,7 +254,7 @@ static void setup_default_push_refspecs(int *flags, struct remote *remote) dst = branch->refname; same_remote = !strcmp(remote->name, remote_for_branch(branch, NULL)); - switch (push_default) { + switch (cfg->push_default) { default: case PUSH_DEFAULT_UNSPECIFIED: case PUSH_DEFAULT_SIMPLE: diff --git a/environment.c b/environment.c index 20500658a20d67..66c1ac1ab8c0ee 100644 --- a/environment.c +++ b/environment.c @@ -58,7 +58,6 @@ enum eol core_eol = EOL_UNSET; int global_conv_flags_eol = CONV_EOL_RNDTRP_WARN; char *check_roundtrip_encoding; enum rebase_setup_type autorebase = AUTOREBASE_NEVER; -enum push_default_type push_default = PUSH_DEFAULT_UNSPECIFIED; #ifndef OBJECT_CREATION_MODE #define OBJECT_CREATION_MODE OBJECT_CREATION_USES_HARDLINKS #endif @@ -620,21 +619,23 @@ static int git_default_branch_config(const char *var, const char *value) static int git_default_push_config(const char *var, const char *value) { + struct repo_config_values *cfg = repo_config_values(the_repository); + if (!strcmp(var, "push.default")) { if (!value) return config_error_nonbool(var); else if (!strcmp(value, "nothing")) - push_default = PUSH_DEFAULT_NOTHING; + cfg->push_default = PUSH_DEFAULT_NOTHING; else if (!strcmp(value, "matching")) - push_default = PUSH_DEFAULT_MATCHING; + cfg->push_default = PUSH_DEFAULT_MATCHING; else if (!strcmp(value, "simple")) - push_default = PUSH_DEFAULT_SIMPLE; + cfg->push_default = PUSH_DEFAULT_SIMPLE; else if (!strcmp(value, "upstream")) - push_default = PUSH_DEFAULT_UPSTREAM; + cfg->push_default = PUSH_DEFAULT_UPSTREAM; else if (!strcmp(value, "tracking")) /* deprecated */ - push_default = PUSH_DEFAULT_UPSTREAM; + cfg->push_default = PUSH_DEFAULT_UPSTREAM; else if (!strcmp(value, "current")) - push_default = PUSH_DEFAULT_CURRENT; + cfg->push_default = PUSH_DEFAULT_CURRENT; else { error(_("malformed value for %s: %s"), var, value); return error(_("must be one of nothing, matching, simple, " @@ -726,6 +727,7 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->askpass_program = NULL; cfg->apply_default_whitespace = NULL; cfg->apply_default_ignorewhitespace = NULL; + cfg->push_default = PUSH_DEFAULT_UNSPECIFIED; cfg->apply_sparse_checkout = 0; cfg->branch_track = BRANCH_TRACK_REMOTE; cfg->trust_ctime = 1; diff --git a/environment.h b/environment.h index f450242ac03bad..17a3a628d2d299 100644 --- a/environment.h +++ b/environment.h @@ -87,6 +87,21 @@ extern const char * const local_repo_env[]; struct strvec; struct repository; + +/* + * NEEDSWORK: It would be better if these definitions could be moved to + * other more specific files, but care is needed to avoid circular + * inclusion issues. + */ +enum push_default_type { + PUSH_DEFAULT_NOTHING = 0, + PUSH_DEFAULT_MATCHING, + PUSH_DEFAULT_SIMPLE, + PUSH_DEFAULT_UPSTREAM, + PUSH_DEFAULT_CURRENT, + PUSH_DEFAULT_UNSPECIFIED +}; + struct repo_config_values { /* section "core" config values */ char *attributes_file; @@ -96,6 +111,7 @@ struct repo_config_values { char *askpass_program; char *apply_default_whitespace; char *apply_default_ignorewhitespace; + enum push_default_type push_default; int apply_sparse_checkout; int trust_ctime; int check_stat; @@ -197,16 +213,6 @@ enum rebase_setup_type { }; extern enum rebase_setup_type autorebase; -enum push_default_type { - PUSH_DEFAULT_NOTHING = 0, - PUSH_DEFAULT_MATCHING, - PUSH_DEFAULT_SIMPLE, - PUSH_DEFAULT_UPSTREAM, - PUSH_DEFAULT_CURRENT, - PUSH_DEFAULT_UNSPECIFIED -}; -extern enum push_default_type push_default; - enum object_creation_mode { OBJECT_CREATION_USES_HARDLINKS = 0, OBJECT_CREATION_USES_RENAMES = 1 diff --git a/remote.c b/remote.c index 00723b385e1d52..d48c01d37559df 100644 --- a/remote.c +++ b/remote.c @@ -1933,7 +1933,7 @@ static char *branch_get_push_1(struct repository *repo, if (remote->mirror) return tracking_for_push_dest(remote, branch->refname, err); - switch (push_default) { + switch (repo_config_values(repo)->push_default) { case PUSH_DEFAULT_NOTHING: return error_buf(err, _("push has no destination (push.default is 'nothing')")); From 1840ca005fbabc651f9018d88f0f9d8adb91b015 Mon Sep 17 00:00:00 2001 From: Tian Yuchen Date: Tue, 14 Jul 2026 11:25:23 +0800 Subject: [PATCH 026/280] environment: move autorebase into repo_config_values The global variable 'autorebase' dictates whether a newly created branch should be configured to automatically rebase by default. Move it into 'struct repo_config_values' to continue the libification effort. The 'enum rebase_setup_type' definition is moved higher up in 'environment.h' so that it is visible to the repository-specific structure. The default state AUTOREBASE_NEVER is now correctly initialized in 'repo_config_values_init()'. Configuration parsing in 'git_default_branch_config()' is updated to write directly to the repository's configuration instance. Mentored-by: Christian Couder Mentored-by: Ayush Chandekar Mentored-by: Olamide Caleb Bello Signed-off-by: Tian Yuchen Signed-off-by: Junio C Hamano --- branch.c | 2 +- environment.c | 10 +++++----- environment.h | 16 ++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/branch.c b/branch.c index 243db7d0fc0226..e1c1f8c89dbc56 100644 --- a/branch.c +++ b/branch.c @@ -61,7 +61,7 @@ static int find_tracked_branch(struct remote *remote, void *priv) static int should_setup_rebase(const char *origin) { - switch (autorebase) { + switch (repo_config_values(the_repository)->autorebase) { case AUTOREBASE_NEVER: return 0; case AUTOREBASE_LOCAL: diff --git a/environment.c b/environment.c index 66c1ac1ab8c0ee..c0bf7577b7ad40 100644 --- a/environment.c +++ b/environment.c @@ -57,7 +57,6 @@ enum auto_crlf auto_crlf = AUTO_CRLF_FALSE; enum eol core_eol = EOL_UNSET; int global_conv_flags_eol = CONV_EOL_RNDTRP_WARN; char *check_roundtrip_encoding; -enum rebase_setup_type autorebase = AUTOREBASE_NEVER; #ifndef OBJECT_CREATION_MODE #define OBJECT_CREATION_MODE OBJECT_CREATION_USES_HARDLINKS #endif @@ -601,13 +600,13 @@ static int git_default_branch_config(const char *var, const char *value) if (!value) return config_error_nonbool(var); else if (!strcmp(value, "never")) - autorebase = AUTOREBASE_NEVER; + cfg->autorebase = AUTOREBASE_NEVER; else if (!strcmp(value, "local")) - autorebase = AUTOREBASE_LOCAL; + cfg->autorebase = AUTOREBASE_LOCAL; else if (!strcmp(value, "remote")) - autorebase = AUTOREBASE_REMOTE; + cfg->autorebase = AUTOREBASE_REMOTE; else if (!strcmp(value, "always")) - autorebase = AUTOREBASE_ALWAYS; + cfg->autorebase = AUTOREBASE_ALWAYS; else return error(_("malformed value for %s"), var); return 0; @@ -728,6 +727,7 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->apply_default_whitespace = NULL; cfg->apply_default_ignorewhitespace = NULL; cfg->push_default = PUSH_DEFAULT_UNSPECIFIED; + cfg->autorebase = AUTOREBASE_NEVER; cfg->apply_sparse_checkout = 0; cfg->branch_track = BRANCH_TRACK_REMOTE; cfg->trust_ctime = 1; diff --git a/environment.h b/environment.h index 17a3a628d2d299..46b2f0d861089e 100644 --- a/environment.h +++ b/environment.h @@ -102,6 +102,13 @@ enum push_default_type { PUSH_DEFAULT_UNSPECIFIED }; +enum rebase_setup_type { + AUTOREBASE_NEVER = 0, + AUTOREBASE_LOCAL, + AUTOREBASE_REMOTE, + AUTOREBASE_ALWAYS +}; + struct repo_config_values { /* section "core" config values */ char *attributes_file; @@ -112,6 +119,7 @@ struct repo_config_values { char *apply_default_whitespace; char *apply_default_ignorewhitespace; enum push_default_type push_default; + enum rebase_setup_type autorebase; int apply_sparse_checkout; int trust_ctime; int check_stat; @@ -205,14 +213,6 @@ extern unsigned long pack_size_limit_cfg; extern int protect_hfs; extern int protect_ntfs; -enum rebase_setup_type { - AUTOREBASE_NEVER = 0, - AUTOREBASE_LOCAL, - AUTOREBASE_REMOTE, - AUTOREBASE_ALWAYS -}; -extern enum rebase_setup_type autorebase; - enum object_creation_mode { OBJECT_CREATION_USES_HARDLINKS = 0, OBJECT_CREATION_USES_RENAMES = 1 From b5efc34b10b3484e18e7a55260f0f06e423eba28 Mon Sep 17 00:00:00 2001 From: Tian Yuchen Date: Tue, 14 Jul 2026 11:25:24 +0800 Subject: [PATCH 027/280] environment: move object_creation_mode into repo_config_values The global variable 'object_creation_mode' controls how Git creates object files, specifically determining whether to use hardlinks or renames when moving temporary files into the object database. Move it into 'struct repo_config_values' to continue the libification effort. Move the 'enum object_creation_mode' definition higher up in 'environment.h' to ensure it is visible to the structure. Initialize the per-repository value to its default macro value OBJECT_CREATION_MODE inside 'repo_config_values_init()'. Update configuration parsing in 'git_default_core_config()' to write directly to the repository-specific configuration structure. Mentored-by: Christian Couder Mentored-by: Ayush Chandekar Mentored-by: Olamide Caleb Bello Signed-off-by: Tian Yuchen Signed-off-by: Junio C Hamano --- environment.c | 6 +++--- environment.h | 12 ++++++------ object-file.c | 3 ++- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/environment.c b/environment.c index c0bf7577b7ad40..ef3c032e0c0329 100644 --- a/environment.c +++ b/environment.c @@ -60,7 +60,6 @@ char *check_roundtrip_encoding; #ifndef OBJECT_CREATION_MODE #define OBJECT_CREATION_MODE OBJECT_CREATION_USES_HARDLINKS #endif -enum object_creation_mode object_creation_mode = OBJECT_CREATION_MODE; int grafts_keep_true_parents; unsigned long pack_size_limit_cfg; @@ -512,9 +511,9 @@ int git_default_core_config(const char *var, const char *value, if (!value) return config_error_nonbool(var); if (!strcmp(value, "rename")) - object_creation_mode = OBJECT_CREATION_USES_RENAMES; + cfg->object_creation_mode = OBJECT_CREATION_USES_RENAMES; else if (!strcmp(value, "link")) - object_creation_mode = OBJECT_CREATION_USES_HARDLINKS; + cfg->object_creation_mode = OBJECT_CREATION_USES_HARDLINKS; else die(_("invalid mode for object creation: %s"), value); return 0; @@ -728,6 +727,7 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->apply_default_ignorewhitespace = NULL; cfg->push_default = PUSH_DEFAULT_UNSPECIFIED; cfg->autorebase = AUTOREBASE_NEVER; + cfg->object_creation_mode = OBJECT_CREATION_MODE; cfg->apply_sparse_checkout = 0; cfg->branch_track = BRANCH_TRACK_REMOTE; cfg->trust_ctime = 1; diff --git a/environment.h b/environment.h index 46b2f0d861089e..a47a5c83dbd469 100644 --- a/environment.h +++ b/environment.h @@ -109,6 +109,11 @@ enum rebase_setup_type { AUTOREBASE_ALWAYS }; +enum object_creation_mode { + OBJECT_CREATION_USES_HARDLINKS = 0, + OBJECT_CREATION_USES_RENAMES = 1 +}; + struct repo_config_values { /* section "core" config values */ char *attributes_file; @@ -120,6 +125,7 @@ struct repo_config_values { char *apply_default_ignorewhitespace; enum push_default_type push_default; enum rebase_setup_type autorebase; + enum object_creation_mode object_creation_mode; int apply_sparse_checkout; int trust_ctime; int check_stat; @@ -213,12 +219,6 @@ extern unsigned long pack_size_limit_cfg; extern int protect_hfs; extern int protect_ntfs; -enum object_creation_mode { - OBJECT_CREATION_USES_HARDLINKS = 0, - OBJECT_CREATION_USES_RENAMES = 1 -}; -extern enum object_creation_mode object_creation_mode; - extern int grafts_keep_true_parents; const char *get_log_output_encoding(void); diff --git a/object-file.c b/object-file.c index e3d92bbda25f0a..26780904681a22 100644 --- a/object-file.c +++ b/object-file.c @@ -411,11 +411,12 @@ int finalize_object_file_flags(struct repository *repo, { unsigned retries = 0; int ret; + struct repo_config_values *cfg = repo_config_values(repo); retry: ret = 0; - if (object_creation_mode == OBJECT_CREATION_USES_RENAMES) + if (cfg->object_creation_mode == OBJECT_CREATION_USES_RENAMES) goto try_rename; else if (link(tmpfile, filename)) ret = errno; From 5eac7ac7151a27025386d48e21c4608115a16db6 Mon Sep 17 00:00:00 2001 From: Tian Yuchen Date: Tue, 14 Jul 2026 11:25:25 +0800 Subject: [PATCH 028/280] repository: adjust the comment of config_values_private_ The configurations in 'struct config_values_private_' are not all parsed in 'git_default_config()'. For example, 'pager_program' is now parsed in 'pager.c'. Therefore, update the comment. Mentored-by: Christian Couder Mentored-by: Ayush Chandekar Mentored-by: Olamide Caleb Bello Signed-off-by: Tian Yuchen Signed-off-by: Junio C Hamano --- repository.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repository.h b/repository.h index 36e2db26332c0e..9093e6af93db96 100644 --- a/repository.h +++ b/repository.h @@ -152,7 +152,7 @@ struct repository { /* Repository's compatibility hash algorithm. */ const struct git_hash_algo *compat_hash_algo; - /* Repository's config values parsed by git_default_config() */ + /* Repository-specific configuration values. */ struct repo_config_values config_values_private_; /* Repository's reference storage format, as serialized on disk. */ From 641588bbcd1e80e084881d547e10aca8b32c328e Mon Sep 17 00:00:00 2001 From: Pablo Sabater Date: Tue, 14 Jul 2026 14:09:32 +0200 Subject: [PATCH 029/280] 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 030/280] 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 031/280] 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 032/280] 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 033/280] 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 034/280] 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 035/280] 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 8aad1dfc006e129d3276ae6dd5cc2ba7ca2a8f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Tue, 14 Jul 2026 19:59:52 +0200 Subject: [PATCH 036/280] tempfile: add repo_create_tempfile{,_mode}() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add variants of create_tempfile_mode() that handle arbitrary repositories. Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- tempfile.c | 8 +++++++- tempfile.h | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/tempfile.c b/tempfile.c index f0fdf582794ba5..3132eb43710036 100644 --- a/tempfile.c +++ b/tempfile.c @@ -135,6 +135,12 @@ static void deactivate_tempfile(struct tempfile *tempfile) /* Make sure errno contains a meaningful value on error */ struct tempfile *create_tempfile_mode(const char *path, int mode) +{ + return repo_create_tempfile_mode(the_repository, path, mode); +} + +struct tempfile *repo_create_tempfile_mode(struct repository *r, + const char *path, int mode) { struct tempfile *tempfile = new_tempfile(); @@ -150,7 +156,7 @@ struct tempfile *create_tempfile_mode(const char *path, int mode) return NULL; } activate_tempfile(tempfile); - if (adjust_shared_perm(the_repository, tempfile->filename.buf)) { + if (adjust_shared_perm(r, tempfile->filename.buf)) { int save_errno = errno; error("cannot fix permission bits on %s", tempfile->filename.buf); delete_tempfile(&tempfile); diff --git a/tempfile.h b/tempfile.h index 2227a095fd4289..2d17e4dad3faa5 100644 --- a/tempfile.h +++ b/tempfile.h @@ -4,6 +4,8 @@ #include "list.h" #include "strbuf.h" +struct repository; + /* * Handle temporary files. * @@ -94,11 +96,20 @@ struct tempfile { */ struct tempfile *create_tempfile_mode(const char *path, int mode); +struct tempfile *repo_create_tempfile_mode(struct repository *r, + const char *path, int mode); + static inline struct tempfile *create_tempfile(const char *path) { return create_tempfile_mode(path, 0666); } +static inline struct tempfile *repo_create_tempfile(struct repository *r, + const char *path) +{ + return repo_create_tempfile_mode(r, path, 0666); +} + /* * Register an existing file as a tempfile, meaning that it will be * deleted when the program exits. The tempfile is considered closed, From 8432a4dee1c1206730c3e6bc5845c32272330bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Tue, 14 Jul 2026 19:59:53 +0200 Subject: [PATCH 037/280] refs/packed: use repo_create_tempfile() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the config setting core.sharedRepository from the ref store base repository at hand instead of from the_repository. Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- refs/packed-backend.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/refs/packed-backend.c b/refs/packed-backend.c index 0acde48c452513..7c4e8aca873555 100644 --- a/refs/packed-backend.c +++ b/refs/packed-backend.c @@ -1379,7 +1379,7 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re packed_refs_path = get_locked_file_path(&refs->lock); strbuf_addf(&sb, "%s.new", packed_refs_path); free(packed_refs_path); - refs->tempfile = create_tempfile(sb.buf); + refs->tempfile = repo_create_tempfile(refs->base.repo, sb.buf); if (!refs->tempfile) { strbuf_addf(err, "unable to create file %s: %s", sb.buf, strerror(errno)); From d43f701d326ef3fe1bf04c1de24fa025ab228af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Tue, 14 Jul 2026 19:59:54 +0200 Subject: [PATCH 038/280] lockfile: add repo_hold_lock_file_for_update{,_timeout}{,_mode}() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add variants of hold_lock_file_for_update_timeout_mode() that handle arbitrary repositories. Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- lockfile.c | 30 ++++++++++++++++++++++-------- lockfile.h | 31 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/lockfile.c b/lockfile.c index 7add2f136abd85..100f60377174ef 100644 --- a/lockfile.c +++ b/lockfile.c @@ -2,11 +2,14 @@ * Copyright (c) 2005, Junio C Hamano */ +#define USE_THE_REPOSITORY_VARIABLE + #include "git-compat-util.h" #include "abspath.h" #include "gettext.h" #include "lockfile.h" #include "parse.h" +#include "repository.h" #include "strbuf.h" #include "wrapper.h" @@ -162,8 +165,8 @@ static int read_lock_pid(const char *pid_path, uintmax_t *pid_out) } /* Make sure errno contains a meaningful value on error */ -static int lock_file(struct lock_file *lk, const char *path, int flags, - int mode) +static int lock_file(struct repository *r, struct lock_file *lk, + const char *path, int flags, int mode) { struct strbuf base_path = STRBUF_INIT; struct strbuf lock_path = STRBUF_INIT; @@ -176,7 +179,7 @@ static int lock_file(struct lock_file *lk, const char *path, int flags, get_lock_path(&lock_path, base_path.buf); get_pid_path(&pid_path, base_path.buf); - lk->tempfile = create_tempfile_mode(lock_path.buf, mode); + lk->tempfile = repo_create_tempfile_mode(r, lock_path.buf, mode); if (lk->tempfile) lk->pid_tempfile = create_lock_pid_file(pid_path.buf, mode); @@ -200,8 +203,9 @@ static int lock_file(struct lock_file *lk, const char *path, int flags, * timeout_ms milliseconds. If timeout_ms is 0, try locking the file * exactly once. If timeout_ms is -1, try indefinitely. */ -static int lock_file_timeout(struct lock_file *lk, const char *path, - int flags, long timeout_ms, int mode) +static int lock_file_timeout(struct repository *r, struct lock_file *lk, + const char *path, int flags, long timeout_ms, + int mode) { int n = 1; int multiplier = 1; @@ -209,7 +213,7 @@ static int lock_file_timeout(struct lock_file *lk, const char *path, static int random_initialized = 0; if (timeout_ms == 0) - return lock_file(lk, path, flags, mode); + return lock_file(r, lk, path, flags, mode); if (!random_initialized) { srand((unsigned int)getpid()); @@ -223,7 +227,7 @@ static int lock_file_timeout(struct lock_file *lk, const char *path, long backoff_ms, wait_ms; int fd; - fd = lock_file(lk, path, flags, mode); + fd = lock_file(r, lk, path, flags, mode); if (fd >= 0) return fd; /* success */ @@ -308,7 +312,17 @@ int hold_lock_file_for_update_timeout_mode(struct lock_file *lk, const char *path, int flags, long timeout_ms, int mode) { - int fd = lock_file_timeout(lk, path, flags, timeout_ms, mode); + return repo_hold_lock_file_for_update_timeout_mode(the_repository, + lk, path, flags, + timeout_ms, mode); +} + +int repo_hold_lock_file_for_update_timeout_mode(struct repository *r, + struct lock_file *lk, + const char *path, int flags, + long timeout_ms, int mode) +{ + int fd = lock_file_timeout(r, lk, path, flags, timeout_ms, mode); if (fd < 0) { if (flags & LOCK_DIE_ON_ERROR) unable_to_lock_die(path, errno); diff --git a/lockfile.h b/lockfile.h index e7233f28dea5c7..1667612674b52a 100644 --- a/lockfile.h +++ b/lockfile.h @@ -189,6 +189,11 @@ int hold_lock_file_for_update_timeout_mode( struct lock_file *lk, const char *path, int flags, long timeout_ms, int mode); +int repo_hold_lock_file_for_update_timeout_mode(struct repository *r, + struct lock_file *lk, + const char *path, int flags, + long timeout_ms, int mode); + static inline int hold_lock_file_for_update_timeout( struct lock_file *lk, const char *path, int flags, long timeout_ms) @@ -197,6 +202,16 @@ static inline int hold_lock_file_for_update_timeout( timeout_ms, 0666); } +static inline int repo_hold_lock_file_for_update_timeout(struct repository *r, + struct lock_file *lk, + const char *path, + int flags, + long timeout_ms) +{ + return repo_hold_lock_file_for_update_timeout_mode(r, lk, path, flags, + timeout_ms, 0666); +} + /* * Attempt to create a lockfile for the file at `path` and return a * file descriptor for writing to it, or -1 on error. The flags @@ -208,6 +223,13 @@ static inline int hold_lock_file_for_update( return hold_lock_file_for_update_timeout(lk, path, flags, 0); } +static inline int repo_hold_lock_file_for_update(struct repository *r, + struct lock_file *lk, + const char *path, int flags) +{ + return repo_hold_lock_file_for_update_timeout(r, lk, path, flags, 0); +} + static inline int hold_lock_file_for_update_mode( struct lock_file *lk, const char *path, int flags, int mode) @@ -215,6 +237,15 @@ static inline int hold_lock_file_for_update_mode( return hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode); } +static inline int repo_hold_lock_file_for_update_mode(struct repository *r, + struct lock_file *lk, + const char *path, + int flags, int mode) +{ + return repo_hold_lock_file_for_update_timeout_mode(r, lk, path, flags, + 0, mode); +} + /* * Return a nonzero value iff `lk` is currently locked. */ From 75e05f6bc3b3e04301d25327f54b2c9998471c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Tue, 14 Jul 2026 19:59:55 +0200 Subject: [PATCH 039/280] tempfile: stop using the_repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the compatibility wrappers create_tempfile_mode() and create_tempfile() that have become unused. Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- tempfile.c | 7 ------- tempfile.h | 7 ------- 2 files changed, 14 deletions(-) diff --git a/tempfile.c b/tempfile.c index 3132eb43710036..dc9ca4e6459c64 100644 --- a/tempfile.c +++ b/tempfile.c @@ -42,8 +42,6 @@ * file created by its parent. */ -#define USE_THE_REPOSITORY_VARIABLE - #include "git-compat-util.h" #include "abspath.h" #include "path.h" @@ -134,11 +132,6 @@ static void deactivate_tempfile(struct tempfile *tempfile) } /* Make sure errno contains a meaningful value on error */ -struct tempfile *create_tempfile_mode(const char *path, int mode) -{ - return repo_create_tempfile_mode(the_repository, path, mode); -} - struct tempfile *repo_create_tempfile_mode(struct repository *r, const char *path, int mode) { diff --git a/tempfile.h b/tempfile.h index 2d17e4dad3faa5..f571f3c609c04a 100644 --- a/tempfile.h +++ b/tempfile.h @@ -94,16 +94,9 @@ struct tempfile { * `core.sharedRepository`, so it is not guaranteed to have the given * mode. */ -struct tempfile *create_tempfile_mode(const char *path, int mode); - struct tempfile *repo_create_tempfile_mode(struct repository *r, const char *path, int mode); -static inline struct tempfile *create_tempfile(const char *path) -{ - return create_tempfile_mode(path, 0666); -} - static inline struct tempfile *repo_create_tempfile(struct repository *r, const char *path) { From 2e486bfbf75c5098406a07e6967ecb80a28c316a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Tue, 14 Jul 2026 19:59:56 +0200 Subject: [PATCH 040/280] use repo_hold_lock_file_for_update{,_mode,_timeout}() with custom repos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the config setting core.sharedRepository from the repository at hand instead of from the_repository. Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- apply.c | 10 ++++++---- builtin/difftool.c | 2 +- builtin/gc.c | 2 +- builtin/history.c | 2 +- builtin/sparse-checkout.c | 3 ++- bundle.c | 4 ++-- commit-graph.c | 9 +++++---- config.c | 4 ++-- loose.c | 6 ++++-- midx-write.c | 7 ++++--- odb/source-files.c | 3 ++- refs/files-backend.c | 10 ++++++---- refs/packed-backend.c | 7 +++---- refs/packed-backend.h | 2 +- repack-midx.c | 3 ++- repository.c | 2 +- rerere.c | 6 +++--- 17 files changed, 46 insertions(+), 36 deletions(-) diff --git a/apply.c b/apply.c index 5e54453f799ea2..ac1bfc7f85a5d5 100644 --- a/apply.c +++ b/apply.c @@ -4287,7 +4287,8 @@ static int build_fake_ancestor(struct apply_state *state, struct patch *list) } } - hold_lock_file_for_update(&lock, state->fake_ancestor, LOCK_DIE_ON_ERROR); + repo_hold_lock_file_for_update(state->repo, &lock, state->fake_ancestor, + LOCK_DIE_ON_ERROR); res = write_locked_index(&result, &lock, COMMIT_LOCK); discard_index(&result); @@ -4945,9 +4946,10 @@ static int apply_patch(struct apply_state *state, state->update_index = (state->check_index || state->ita_only) && state->apply; if (state->update_index && !is_lock_file_locked(&state->lock_file)) { if (state->index_file) - hold_lock_file_for_update(&state->lock_file, - state->index_file, - LOCK_DIE_ON_ERROR); + repo_hold_lock_file_for_update(state->repo, + &state->lock_file, + state->index_file, + LOCK_DIE_ON_ERROR); else repo_hold_locked_index(state->repo, &state->lock_file, LOCK_DIE_ON_ERROR); diff --git a/builtin/difftool.c b/builtin/difftool.c index 26778f8515deef..99c01c92ef212a 100644 --- a/builtin/difftool.c +++ b/builtin/difftool.c @@ -636,7 +636,7 @@ static int run_dir_diff(struct repository *repo, struct lock_file lock = LOCK_INIT; strbuf_reset(&buf); strbuf_addf(&buf, "%s/wtindex", tmpdir.buf); - if (hold_lock_file_for_update(&lock, buf.buf, 0) < 0 || + if (repo_hold_lock_file_for_update(repo, &lock, buf.buf, 0) < 0 || write_locked_index(&wtindex, &lock, COMMIT_LOCK)) { ret = error("could not write %s", buf.buf); goto finish; diff --git a/builtin/gc.c b/builtin/gc.c index c26c93ee0fe4a3..01d034829c609b 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -1790,7 +1790,7 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts, struct repository *r = the_repository; char *lock_path = xstrfmt("%s/maintenance", r->objects->sources->path); - if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) { + if (repo_hold_lock_file_for_update(r, &lk, lock_path, LOCK_NO_DEREF) < 0) { /* * Another maintenance command is running. * diff --git a/builtin/history.c b/builtin/history.c index 091465a59e2f96..5b070fb3351408 100644 --- a/builtin/history.c +++ b/builtin/history.c @@ -764,7 +764,7 @@ static int write_ondisk_index(struct repository *repo, prime_cache_tree(repo, &index, tree); - if (hold_lock_file_for_update(&lock, path, 0) < 0) { + if (repo_hold_lock_file_for_update(repo, &lock, path, 0) < 0) { ret = error_errno(_("unable to acquire index lock")); goto out; } diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c index 0863d0fb460cf8..cb4a037b770291 100644 --- a/builtin/sparse-checkout.c +++ b/builtin/sparse-checkout.c @@ -341,7 +341,8 @@ static int write_patterns_and_update(struct repository *repo, if (safe_create_leading_directories(repo, sparse_filename)) die(_("failed to create directory for sparse-checkout file")); - hold_lock_file_for_update(&lk, sparse_filename, LOCK_DIE_ON_ERROR); + repo_hold_lock_file_for_update(repo, &lk, sparse_filename, + LOCK_DIE_ON_ERROR); result = update_working_directory(repo, pl); if (result) { diff --git a/bundle.c b/bundle.c index fd2db2c837df60..b64716f252b78f 100644 --- a/bundle.c +++ b/bundle.c @@ -519,8 +519,8 @@ int create_bundle(struct repository *r, const char *path, if (bundle_to_stdout) bundle_fd = 1; else - bundle_fd = hold_lock_file_for_update(&lock, path, - LOCK_DIE_ON_ERROR); + bundle_fd = repo_hold_lock_file_for_update(r, &lock, path, + LOCK_DIE_ON_ERROR); if (version == -1) version = min_version; diff --git a/commit-graph.c b/commit-graph.c index 801471a09802d9..063b545eb75c1e 100644 --- a/commit-graph.c +++ b/commit-graph.c @@ -2122,8 +2122,8 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx) if (ctx->split) { char *lock_name = get_commit_graph_chain_filename(ctx->odb_source); - hold_lock_file_for_update_mode(&lk, lock_name, - LOCK_DIE_ON_ERROR, 0444); + repo_hold_lock_file_for_update_mode(ctx->r, &lk, lock_name, + LOCK_DIE_ON_ERROR, 0444); free(lock_name); graph_layer = mks_tempfile_m(ctx->graph_name, 0444); @@ -2141,8 +2141,9 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx) f = hashfd(ctx->r->hash_algo, get_tempfile_fd(graph_layer), get_tempfile_path(graph_layer)); } else { - hold_lock_file_for_update_mode(&lk, ctx->graph_name, - LOCK_DIE_ON_ERROR, 0444); + repo_hold_lock_file_for_update_mode(ctx->r, &lk, + ctx->graph_name, + LOCK_DIE_ON_ERROR, 0444); f = hashfd(ctx->r->hash_algo, get_lock_file_fd(&lk), get_lock_file_path(&lk)); } diff --git a/config.c b/config.c index 6a0de86e3ae958..d29425ab8e8408 100644 --- a/config.c +++ b/config.c @@ -3034,7 +3034,7 @@ int repo_config_set_multivar_in_file_gently(struct repository *r, * The lock serves a purpose in addition to locking: the new * contents of .git/config will be written into it. */ - fd = hold_lock_file_for_update(&lock, config_filename, 0); + fd = repo_hold_lock_file_for_update(r, &lock, config_filename, 0); if (fd < 0) { error_errno(_("could not lock config file %s"), config_filename); ret = CONFIG_NO_LOCK; @@ -3379,7 +3379,7 @@ static int repo_config_copy_or_rename_section_in_file( if (!config_filename) config_filename = filename_buf = repo_git_path(r, "config"); - out_fd = hold_lock_file_for_update(&lock, config_filename, 0); + out_fd = repo_hold_lock_file_for_update(r, &lock, config_filename, 0); if (out_fd < 0) { ret = error(_("could not lock config file %s"), config_filename); goto out; diff --git a/loose.c b/loose.c index 0b626c1b854642..a79cafd38aae98 100644 --- a/loose.c +++ b/loose.c @@ -138,7 +138,8 @@ int repo_write_loose_object_map(struct repository *repo) return 0; repo_common_path_replace(repo, &path, "objects/loose-object-idx"); - fd = hold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1); + fd = repo_hold_lock_file_for_update_timeout(repo, &lock, path.buf, + LOCK_DIE_ON_ERROR, -1); iter = kh_begin(map); if (write_in_full(fd, loose_object_header, strlen(loose_object_header)) < 0) goto errout; @@ -180,7 +181,8 @@ static int write_one_object(struct odb_source_loose *loose, struct strbuf buf = STRBUF_INIT, path = STRBUF_INIT; strbuf_addf(&path, "%s/loose-object-idx", loose->base.path); - hold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1); + repo_hold_lock_file_for_update_timeout(loose->base.odb->repo, &lock, + path.buf, LOCK_DIE_ON_ERROR, -1); fd = open(path.buf, O_WRONLY | O_CREAT | O_APPEND, 0666); if (fd < 0) diff --git a/midx-write.c b/midx-write.c index 19e1cd10b7dee4..e0730d02545687 100644 --- a/midx-write.c +++ b/midx-write.c @@ -1627,8 +1627,8 @@ static int write_midx_internal(struct write_midx_opts *opts) struct strbuf lock_name = STRBUF_INIT; get_midx_chain_filename(opts->source, &lock_name); - hold_lock_file_for_update(&lk, lock_name.buf, - LOCK_DIE_ON_ERROR); + repo_hold_lock_file_for_update(r, &lk, lock_name.buf, + LOCK_DIE_ON_ERROR); strbuf_release(&lock_name); } @@ -1647,7 +1647,8 @@ static int write_midx_internal(struct write_midx_opts *opts) f = hashfd(r->hash_algo, get_tempfile_fd(incr), get_tempfile_path(incr)); } else { - hold_lock_file_for_update(&lk, midx_name.buf, LOCK_DIE_ON_ERROR); + repo_hold_lock_file_for_update(r, &lk, midx_name.buf, + LOCK_DIE_ON_ERROR); f = hashfd(r->hash_algo, get_lock_file_fd(&lk), get_lock_file_path(&lk)); } diff --git a/odb/source-files.c b/odb/source-files.c index 5bdd0429225397..3dd9e71e7341e5 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -217,7 +217,8 @@ static int odb_source_files_write_alternate(struct odb_source *source, int found = 0; int ret; - hold_lock_file_for_update(&lock, path, LOCK_DIE_ON_ERROR); + repo_hold_lock_file_for_update(source->odb->repo, &lock, path, + LOCK_DIE_ON_ERROR); out = fdopen_lock_file(&lock, "w"); if (!out) { ret = error_errno(_("unable to fdopen alternates lockfile")); diff --git a/refs/files-backend.c b/refs/files-backend.c index a4c7858787127d..2e82258844e872 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -791,7 +791,7 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs, goto error_return; } - if (hold_lock_file_for_update_timeout( + if (repo_hold_lock_file_for_update_timeout(refs->base.repo, &lock->lk, ref_file.buf, LOCK_NO_DEREF, get_files_ref_lock_timeout_ms(transaction->ref_store->repo)) < 0) { int myerr = errno; @@ -1199,8 +1199,8 @@ struct create_reflock_cb { static int create_reflock(const char *path, void *cb) { struct create_reflock_cb *data = cb; - return hold_lock_file_for_update_timeout( - data->lk, path, LOCK_NO_DEREF, + return repo_hold_lock_file_for_update_timeout( + data->repo, data->lk, path, LOCK_NO_DEREF, get_files_ref_lock_timeout_ms(data->repo)) < 0 ? -1 : 0; } @@ -3529,7 +3529,9 @@ static int files_reflog_expire(struct ref_store *ref_store, * work we need, including cleaning up if the program * exits unexpectedly. */ - if (hold_lock_file_for_update(&reflog_lock, log_file, 0) < 0) { + if (repo_hold_lock_file_for_update(ref_store->repo, + &reflog_lock, log_file, + 0) < 0) { struct strbuf err = STRBUF_INIT; unable_to_lock_message(log_file, errno, &err); error("%s", err.buf); diff --git a/refs/packed-backend.c b/refs/packed-backend.c index 7c4e8aca873555..117f2d43681a6b 100644 --- a/refs/packed-backend.c +++ b/refs/packed-backend.c @@ -1232,10 +1232,9 @@ int packed_refs_lock(struct ref_store *ref_store, int flags, struct strbuf *err) * don't write new content to it, but rather to a separate * tempfile. */ - if (hold_lock_file_for_update_timeout( - &refs->lock, - refs->path, - flags, timeout_value) < 0) { + if (repo_hold_lock_file_for_update_timeout(ref_store->repo, &refs->lock, + refs->path, flags, + timeout_value) < 0) { unable_to_lock_message(refs->path, errno, err); return -1; } diff --git a/refs/packed-backend.h b/refs/packed-backend.h index 1db48e801d63d0..8a7b3238254d3d 100644 --- a/refs/packed-backend.h +++ b/refs/packed-backend.h @@ -21,7 +21,7 @@ struct ref_store *packed_ref_store_init(struct repository *repo, /* * Lock the packed-refs file for writing. Flags is passed to - * hold_lock_file_for_update(). Return 0 on success. On errors, write + * repo_hold_lock_file_for_update(). Return 0 on success. On errors, write * an error message to `err` and return a nonzero value. */ int packed_refs_lock(struct ref_store *ref_store, int flags, struct strbuf *err); diff --git a/repack-midx.c b/repack-midx.c index b6b1de718058da..78dbed3df6a71e 100644 --- a/repack-midx.c +++ b/repack-midx.c @@ -951,7 +951,8 @@ static int write_midx_incremental(struct repack_write_midx_opts *opts) lock_name.buf)) die_errno(_("unable to create leading directories of %s"), lock_name.buf); - hold_lock_file_for_update(&lf, lock_name.buf, LOCK_DIE_ON_ERROR); + repo_hold_lock_file_for_update(opts->existing->repo, &lf, lock_name.buf, + LOCK_DIE_ON_ERROR); if (!fdopen_lock_file(&lf, "w")) { ret = error_errno(_("unable to open multi-pack-index chain file")); diff --git a/repository.c b/repository.c index 187dd471c4e607..25583073085f61 100644 --- a/repository.c +++ b/repository.c @@ -466,5 +466,5 @@ int repo_hold_locked_index(struct repository *repo, { if (!repo->index_file) BUG("the repo hasn't been setup"); - return hold_lock_file_for_update(lf, repo->index_file, flags); + return repo_hold_lock_file_for_update(repo, lf, repo->index_file, flags); } diff --git a/rerere.c b/rerere.c index 8232542585cad4..2d1e99ec114294 100644 --- a/rerere.c +++ b/rerere.c @@ -911,9 +911,9 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags) if (flags & RERERE_READONLY) fd = 0; else - fd = hold_lock_file_for_update(&write_lock, - git_path_merge_rr(r), - LOCK_DIE_ON_ERROR); + fd = repo_hold_lock_file_for_update(r, &write_lock, + git_path_merge_rr(r), + LOCK_DIE_ON_ERROR); read_rr(r, merge_rr); return fd; } From 88efab5c3b7b7585a3b4f45db89c108fef9a04f2 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Wed, 15 Jul 2026 02:05:23 -0400 Subject: [PATCH 041/280] diff: ignore unmerged paths outside prefix with --relative --cached A diff using --relative ignores entries outside the current directory. This results in a segfault when we try to process an unmerged entry that's outside of our prefix, since we end up with a NULL diff_filepair and use it without checking that it's valid. I think this bug goes back to 76399c0195 (diff.c: return filepair from diff_unmerge(), 2011-04-22). Prior to that, diff_unmerge() knew to skip entries outside of our prefix, due to cd676a5136 (diff --relative: output paths as relative to the current subdirectory, 2008-02-12). Back then the caller didn't care that we hadn't added anything to the queue. In 76399c0195 that changed; we now returned the pair (or NULL), and the caller in do_oneway_diff() was then called fill_filespec() itself. And it does so without checking for NULL, causing a segfault. The obvious fix is to skip the fill_filespec() call (after which we just return), which this patch does. There's another call to diff_unmerge() in run_diff_files(). That case was already fixed by 8174627b3d (diff-lib: ignore paths that are outside $cwd if --relative asked, 2021-08-22), but of course it didn't help us for --cached. That commit also claims that checking the result of diff_unmerge() is not enough, as we'd want other code paths to skip the entry, too (even if they wouldn't segfault). But as far as I can tell, that is not true for --cached. We eventually end up in diff_queue_addremove() or in diff_queue_change(), both of which know to return early when we're outside of the prefix. Arguably we could be checking at the top of oneway_diff() whether the path is interesting at all. That would not only avoid this code path entirely, but would also possibly save a small amount of work. But since everything else appears to work OK, I went for the smallest fix here to avoid any regression. Specifically, a comment in oneway_diff() claims we're supposed to advance o->pos, which we might fail to do if we return early. Though that "advance" seems to have gone away in da165f470e (unpack-trees.c: prepare for looking ahead in the index, 2010-01-07), so it is possible the comment is simply out of date. We can explore that separately; checking for a NULL return from diff_unmerge() seems like a sensible thing to do regardless. We can piggy-back on the tests added by 8174627b3d; we're just checking the --cached variant. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- diff-lib.c | 2 +- t/t4045-diff-relative.sh | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/diff-lib.c b/diff-lib.c index ae91027a024eec..a23119b8522012 100644 --- a/diff-lib.c +++ b/diff-lib.c @@ -467,7 +467,7 @@ static void do_oneway_diff(struct unpack_trees_options *o, if (cached && idx && ce_stage(idx)) { struct diff_filepair *pair; pair = diff_unmerge(&revs->diffopt, idx->name); - if (tree) + if (pair && tree) fill_filespec(pair->one, &tree->oid, 1, tree->ce_mode); return; diff --git a/t/t4045-diff-relative.sh b/t/t4045-diff-relative.sh index 2c8493fe66c441..167be0bdcce586 100755 --- a/t/t4045-diff-relative.sh +++ b/t/t4045-diff-relative.sh @@ -245,4 +245,13 @@ test_expect_failure 'diff --relative with change in subdir' ' test_cmp expected out ' +test_expect_success 'diff --relative --cached with change in subdir' ' + git switch br3 && + test_when_finished "git merge --abort" && + test_must_fail git merge sub1 && + echo file0 >expected && + git -C subdir diff --relative --name-only --cached >out && + test_cmp expected out +' + 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 042/280] 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 043/280] 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 044/280] 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 045/280] 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 046/280] 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 047/280] 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 048/280] 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 049/280] 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 050/280] 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 051/280] 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 052/280] 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 da0fb7e36e9864f7f41d3a2dd291ffd34fcec97f Mon Sep 17 00:00:00 2001 From: Michael Montalbo Date: Wed, 15 Jul 2026 19:29:52 +0000 Subject: [PATCH 053/280] revision: make get_commit_action() a pure predicate get_commit_action() reads as a predicate that decides whether a commit is shown or ignored, but for a line-level log without parent rewriting it also calls line_log_process_ranges_arbitrary_commit(), which mutates the tracked line ranges. That hidden side effect makes it unsafe to evaluate ahead of the walk, the way a lookahead would. get_commit_action() was split out of simplify_commit() in beb5af43a6 (graph API: fix bug in graph_is_interesting(), 2009-08-18) as the show/ignore decision minus the parent rewriting, so the graph renderer could reuse it; line-level log later routed its filtering through it as well, in 3cb9d2b6 (line-log: more responsive, incremental 'git log -L', 2020-05-11). Besides simplify_commit(), the walk driver, graph_is_interesting() is its only other caller, and it runs only under --graph, which sets rewrite_parents and therefore want_ancestry(); the "-L without ancestry" branch that holds the side effect never fires there, so it is dormant today. The line-level processing folds a commit's tracked ranges onto its parents, which must happen even for a commit that get_commit_action() filters from the output, or the ranges never reach the parents. Move it to simplify_commit() and run it before get_commit_action(), gated by get_commit_action()'s leading checks (already shown, uninteresting, and the like) so a commit ignored by those is not folded, as before; factor those checks out as commit_early_ignore(). get_commit_action() is then side-effect free. commit_early_ignore() runs twice on the -L path, once for that gate and once inside get_commit_action(), but it reads only object flags and pack membership, disjoint from the TREESAME flag the fold sets, so the repeat is harmless. Add a "line-log-peek" subcommand to the revision-walking test helper that evaluates get_commit_action() on a commit the walk has not reached yet, plus a t4211 check that the call leaves the commit's flags unchanged. The flags are compared rather than the commit list because add_line_range() merges ranges by union, which is idempotent, so the side effect never changed which commits a linear -L history shows. Suggested-by: Junio C Hamano Signed-off-by: Michael Montalbo Signed-off-by: Junio C Hamano --- revision.c | 70 ++++++++++++++++++++------------ t/helper/test-revision-walking.c | 63 ++++++++++++++++++++++++++++ t/t4211-line-log.sh | 20 +++++++++ 3 files changed, 127 insertions(+), 26 deletions(-) diff --git a/revision.c b/revision.c index 137a86d33bbbe1..18f3ef44c8f1be 100644 --- a/revision.c +++ b/revision.c @@ -4174,37 +4174,39 @@ static timestamp_t comparison_date(const struct rev_info *revs, commit->date; } -enum commit_action get_commit_action(struct rev_info *revs, struct commit *commit) +/* + * Whether the commit is ignored by the cheap checks that read only its + * traversal flags and pack membership (e.g. already shown, or marked + * uninteresting), before any check that examines the commit's date, + * parents, message, or diff. + */ +static int commit_early_ignore(struct rev_info *revs, struct commit *commit) { if (commit->object.flags & SHOWN) - return commit_ignore; + return 1; if (revs->maximal_only && (commit->object.flags & CHILD_VISITED)) - return commit_ignore; + return 1; if (revs->unpacked && has_object_pack(revs->repo, &commit->object.oid)) - return commit_ignore; - if (revs->no_kept_objects) { - if (has_object_kept_pack(revs->repo, &commit->object.oid, - revs->keep_pack_cache_flags)) - return commit_ignore; - } + return 1; + if (revs->no_kept_objects && + has_object_kept_pack(revs->repo, &commit->object.oid, + revs->keep_pack_cache_flags)) + return 1; if (commit->object.flags & UNINTERESTING) + return 1; + return 0; +} + +/* + * Decide whether this commit is shown or ignored. Keep it a pure + * predicate: callers such as the commit graph depend on it having no + * side effects, so per-commit mutations (such as -L range tracking) + * belong in the caller, simplify_commit(), not here. + */ +enum commit_action get_commit_action(struct rev_info *revs, struct commit *commit) +{ + if (commit_early_ignore(revs, commit)) return commit_ignore; - if (revs->line_level_traverse && !want_ancestry(revs)) { - /* - * In case of line-level log with parent rewriting - * prepare_revision_walk() already took care of all line-level - * log filtering, and there is nothing left to do here. - * - * If parent rewriting was not requested, then this is the - * place to perform the line-level log filtering. Notably, - * this check, though expensive, must come before the other, - * cheaper filtering conditions, because the tracked line - * ranges must be adjusted even when the commit will end up - * being ignored based on other conditions. - */ - if (!line_log_process_ranges_arbitrary_commit(revs, commit)) - return commit_ignore; - } if (revs->min_age != -1 && comparison_date(revs, commit) > revs->min_age) return commit_ignore; @@ -4313,7 +4315,23 @@ struct commit_list *get_saved_parents(struct rev_info *revs, const struct commit enum commit_action simplify_commit(struct rev_info *revs, struct commit *commit) { - enum commit_action action = get_commit_action(revs, commit); + enum commit_action action; + + /* + * For a line-level log without parent rewriting, fold each commit's + * ranges as the walk reaches it (parent rewriting does this eagerly in + * prepare_revision_walk()). Fold before get_commit_action() so the + * ranges carry across a commit that a later, cheaper check ignores; + * the commit_early_ignore() guard skips a commit get_commit_action() + * would ignore outright. + */ + if (revs->line_level_traverse && !want_ancestry(revs) && + !commit_early_ignore(revs, commit)) { + if (!line_log_process_ranges_arbitrary_commit(revs, commit)) + return commit_ignore; + } + + action = get_commit_action(revs, commit); if (action == commit_show && revs->prune && revs->dense && want_ancestry(revs)) { diff --git a/t/helper/test-revision-walking.c b/t/helper/test-revision-walking.c index 70051eeaf848e7..24d7f294178dda 100644 --- a/t/helper/test-revision-walking.c +++ b/t/helper/test-revision-walking.c @@ -13,9 +13,12 @@ #include "test-tool.h" #include "commit.h" #include "diff.h" +#include "line-log.h" +#include "object-name.h" #include "repository.h" #include "revision.h" #include "setup.h" +#include "string-list.h" static void print_commit(struct commit *commit) { @@ -51,6 +54,60 @@ static int run_revision_walk(void) return got_revision; } +/* + * Check that get_commit_action() is a pure predicate by evaluating it on a + * commit the walk has not reached yet. No git command makes that out-of-order + * call, so this probe does it deliberately, and reports whether the call + * mutated the peeked commit: a pure get_commit_action() leaves it untouched. + * We compare the commit's flags rather than the emitted commit list because + * range merges are idempotent, so a side effect would not change which commits + * are shown. Only meaningful for a plain "-L" walk with no parent rewriting. + */ +static int line_log_peek(const char **argv) +{ + struct repository *repo = the_repository; + struct rev_info rev; + struct string_list range_args = STRING_LIST_INIT_DUP; + struct object_id oid; + struct commit *peek; + const char *rev_argv[3]; + unsigned before, after; + + if (repo_get_oid(repo, argv[0], &oid)) + die("bad peek commit: %s", argv[0]); + peek = lookup_commit_reference(repo, &oid); + if (!peek || repo_parse_commit(repo, peek)) + die("cannot parse peek commit: %s", argv[0]); + + repo_init_revisions(repo, &rev, NULL); + rev.diffopt.flags.recursive = 1; + rev.line_level_traverse = 1; + string_list_append(&range_args, argv[1]); + + rev_argv[0] = "line-log-peek"; + rev_argv[1] = argv[2]; + rev_argv[2] = NULL; + setup_revisions(2, rev_argv, &rev, NULL); + + line_log_init(&rev, NULL, &range_args); + + if (rev.rewrite_parents || rev.children.name) + die("line-log-peek requires a non-ancestry (-L, no --graph) walk"); + + if (prepare_revision_walk(&rev)) + die("prepare_revision_walk failed"); + + before = peek->object.flags; + get_commit_action(&rev, peek); + after = peek->object.flags; + + printf("mutated %d\n", before != after); + + release_revisions(&rev); + string_list_clear(&range_args, 0); + return 0; +} + int cmd__revision_walking(int argc, const char **argv) { if (argc < 2) @@ -69,6 +126,12 @@ int cmd__revision_walking(int argc, const char **argv) return 0; } + if (!strcmp(argv[1], "line-log-peek")) { + if (argc != 5) + die("usage: test-tool revision-walking line-log-peek "); + return line_log_peek(argv + 2); + } + fprintf(stderr, "check usage\n"); return 1; } diff --git a/t/t4211-line-log.sh b/t/t4211-line-log.sh index ca4eb7bbc713ef..f4a7d8ab61d027 100755 --- a/t/t4211-line-log.sh +++ b/t/t4211-line-log.sh @@ -781,4 +781,24 @@ test_expect_success '--summary shows new file on root commit' ' test_grep "create mode 100644 file.c" actual ' +test_expect_success 'get_commit_action() does not mutate a not-yet-walked commit' ' + git init peek && + ( + cd peek && + test_write_lines 1 2 3 4 5 >f.c && + git add f.c && test_tick && git commit -m base && + test_write_lines 1 two 3 4 5 >f.c && + test_tick && git commit -am change && + + # Peek HEAD^, which the walk has not reached (the out-of-order + # call a lookahead makes), and confirm get_commit_action() leaves + # it untouched. A side effect is invisible in the commit list + # (range merges are idempotent), so the helper reports whether the + # call mutated the peeked commit at all. + echo "mutated 0" >expect && + test-tool revision-walking line-log-peek HEAD^ 1,3:f.c HEAD >actual && + test_cmp expect actual + ) +' + test_done From 5abbd7c3a2e0b484e23377ae405af2b282286274 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 16 Jul 2026 07:33:02 +0200 Subject: [PATCH 054/280] 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 055/280] 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 056/280] 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 057/280] 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 058/280] 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 059/280] 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 060/280] 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 061/280] 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 062/280] 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 063/280] 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 fda513d6fea267f2b59a1c5a212d98e4b6ae4187 Mon Sep 17 00:00:00 2001 From: Travor Liu Date: Fri, 17 Jul 2026 21:52:45 +0800 Subject: [PATCH 064/280] gitweb: shorten index hashes with trailing file modes Diff index lines have included a trailing file mode since ec1fcc16af (Show original and resulting blob object info in diff output, 2005-10-07) when the old and new file modes match: index .. 100644 gitweb recognizes that trailing mode before it tries to shorten and link the object IDs. This appends the file-type annotation first, but the object-ID matcher requires the ID range to end the line. As a result, this common form keeps both full object IDs as plain text. That is inconsistent with other hash displays and makes commitdiff output wider than necessary. Recent gitweb changes have fixed mobile overflow in log, commit, blob and diff views; leaving two full object IDs in this header preserves an avoidable long line in the diff header. * gitweb/gitweb.perl: Remove the trailing mode before matching the index IDs, then append it again after the IDs have been shortened and linked. This preserves the mode display while letting ordinary and combined index lines use the existing object-ID formatting paths. * t/t9502-gitweb-standalone-parse-output.sh: Add coverage for that common form by rendering a commitdiff for a regular file modification. Check that the visible index line contains linked short blob IDs followed by the mode and file-type annotation, and that the full unlinked form is not emitted. Signed-off-by: Travor Liu Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 18 +++++++++++++----- t/t9502-gitweb-standalone-parse-output.sh | 13 +++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index fde804593b6ff3..8c2d9b8eefe4db 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -2339,12 +2339,14 @@ sub format_extended_diff_header_line { $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"}, esc_path($to->{'file'})); } - # match single - if ($line =~ m/\s(\d{6})$/) { - $line .= ' (' . - file_type_long($1) . - ')'; + + # Temporarily remove a trailing so an index line ends with its + # object IDs and can be shortened below. + my $mode; + if ($line =~ s/\s(\d{6})$//) { + $mode = $1; } + # match if ($line =~ oid_nlen_prefix_infix_regex($sha1_len, "index ", ",") | $line =~ oid_nlen_prefix_infix_regex($sha256_len, "index ", ",")) { @@ -2388,6 +2390,12 @@ sub format_extended_diff_header_line { my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'}); $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!; } + if (defined $mode) { + $line .= " $mode" . + ' (' . + file_type_long($mode) . + ')'; + } return $line . "
\n"; } diff --git a/t/t9502-gitweb-standalone-parse-output.sh b/t/t9502-gitweb-standalone-parse-output.sh index 81d562555799be..85f771650de034 100755 --- a/t/t9502-gitweb-standalone-parse-output.sh +++ b/t/t9502-gitweb-standalone-parse-output.sh @@ -115,6 +115,19 @@ test_expect_success 'snapshot: hierarchical branch name (xx/test)' ' ' test_debug 'cat gitweb.headers' +test_expect_success 'commitdiff: index line shortens hashes with mode' ' + old_blob=$(git rev-parse HEAD:foo) && + old_short=$(git rev-parse --short=7 HEAD:foo) && + echo changed >foo && + git commit -am "change foo" && + new_blob=$(git rev-parse HEAD:foo) && + new_short=$(git rev-parse --short=7 HEAD:foo) && + gitweb_run "p=.git;a=commitdiff;h=HEAD" && + test_grep ">${old_short}\\.\\.]*>${new_short} 100644 (file)" \ + gitweb.body && + test_grep ! "index ${old_blob}\\.\\.${new_blob} 100644" gitweb.body +' + # ---------------------------------------------------------------------- # forks of projects From 215d305f450ff0691d15daaa6ef72f77e8441b39 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Fri, 17 Jul 2026 11:32:09 +0200 Subject: [PATCH 065/280] odb: compute compat object ID in `odb_write_object_ext()` Repositories can have a compatibility hash configured, which means that such a repository is expected to maintain a mapping between canonical and compatibility object hashes. Maintaining this mapping is the responsibility of the object database sources, where we either store them as part of the loose objects map or in packfile indices v3 (once we gain support for this feature). But besides storing these compatibility hashes, the sources are also responsible for generating the compatibility hash in the first place. This is somewhat unnecessary though, as the compatibility hash should be computed the same no matter which source is being used. The consequence is that we need to duplicate this functionality across the different backends, which does not make a lot of sense. Refactor the code so that we instead compute the compatibility hash in `odb_write_object_ext()` and then pass the computed value to the sources. No callers need adjustment as there are none that write objects via the source interfaces directly. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb.c | 26 ++++++++++++++++++++++++-- odb.h | 10 ++++++---- odb/source-files.c | 2 +- odb/source-inmemory.c | 2 +- odb/source-loose.c | 24 +++--------------------- odb/source-packed.c | 2 +- odb/source.h | 4 ++-- 7 files changed, 38 insertions(+), 32 deletions(-) diff --git a/odb.c b/odb.c index cf6e7938c01e56..1d6538163b54ea 100644 --- a/odb.c +++ b/odb.c @@ -989,11 +989,33 @@ int odb_write_object_ext(struct object_database *odb, const void *buf, unsigned long len, enum object_type type, struct object_id *oid, - struct object_id *compat_oid, + const struct object_id *compat_oid_in, enum odb_write_object_flags flags) { + const struct git_hash_algo *compat = odb->repo->compat_hash_algo; + struct object_id compat_oid, *compat_oid_p = NULL; + + if (compat) { + const struct git_hash_algo *algo = odb->repo->hash_algo; + + if (compat_oid_in) { + oidcpy(&compat_oid, compat_oid_in); + } else if (type == OBJ_BLOB) { + hash_object_file(compat, buf, len, type, &compat_oid); + } else { + struct strbuf converted = STRBUF_INIT; + convert_object_file(odb->repo, &converted, algo, compat, + buf, len, type, 0); + hash_object_file(compat, converted.buf, converted.len, + type, &compat_oid); + strbuf_release(&converted); + } + + compat_oid_p = &compat_oid; + } + return odb_source_write_object(odb->sources, buf, len, type, - oid, compat_oid, flags); + oid, compat_oid_p, flags); } int odb_write_object_stream(struct object_database *odb, diff --git a/odb.h b/odb.h index 94754643d24997..066560113ebc78 100644 --- a/odb.h +++ b/odb.h @@ -585,9 +585,11 @@ enum odb_write_object_flags { /* * Write an object into the object database. The object is being written into - * the local alternate of the repository. If provided, the converted object ID - * as well as the compatibility object ID are written to the respective - * pointers. + * the local alternate of the repository. If provided, the object ID of the + * final object is written into `oid`. + * + * If the caller provides a `compat_oid`, then this compatibility object hash + * will be stored instead of computing the compatibility hash ad-hoc. * * Returns 0 on success, a negative error code otherwise. */ @@ -595,7 +597,7 @@ int odb_write_object_ext(struct object_database *odb, const void *buf, unsigned long len, enum object_type type, struct object_id *oid, - struct object_id *compat_oid, + const struct object_id *compat_oid, enum odb_write_object_flags flags); static inline int odb_write_object(struct object_database *odb, diff --git a/odb/source-files.c b/odb/source-files.c index 413875851135a5..3d9f5eca327948 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -163,7 +163,7 @@ static int odb_source_files_write_object(struct odb_source *source, const void *buf, size_t len, enum object_type type, struct object_id *oid, - struct object_id *compat_oid, + const struct object_id *compat_oid, enum odb_write_object_flags flags) { struct odb_source_files *files = odb_source_files_downcast(source); diff --git a/odb/source-inmemory.c b/odb/source-inmemory.c index e47bfd8fccabf0..e727aba427a17d 100644 --- a/odb/source-inmemory.c +++ b/odb/source-inmemory.c @@ -231,7 +231,7 @@ static int odb_source_inmemory_write_object(struct odb_source *source, const void *buf, size_t len, enum object_type type, struct object_id *oid, - struct object_id *compat_oid UNUSED, + const struct object_id *compat_oid UNUSED, enum odb_write_object_flags flags UNUSED) { struct odb_source_inmemory *inmemory = odb_source_inmemory_downcast(source); diff --git a/odb/source-loose.c b/odb/source-loose.c index 3f7d04a56e36ce..ca223109cd46e4 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -585,32 +585,14 @@ static int odb_source_loose_freshen_object(struct odb_source *source, static int odb_source_loose_write_object(struct odb_source *source, const void *buf, size_t len, enum object_type type, struct object_id *oid, - struct object_id *compat_oid_in, + const struct object_id *compat_oid, enum odb_write_object_flags flags) { struct odb_source_loose *loose = odb_source_loose_downcast(source); const struct git_hash_algo *algo = source->odb->repo->hash_algo; - const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; - struct object_id compat_oid; char hdr[MAX_HEADER_LEN]; size_t hdrlen = sizeof(hdr); - /* Generate compat_oid */ - if (compat) { - if (compat_oid_in) - oidcpy(&compat_oid, compat_oid_in); - else if (type == OBJ_BLOB) - hash_object_file(compat, buf, len, type, &compat_oid); - else { - struct strbuf converted = STRBUF_INIT; - convert_object_file(source->odb->repo, &converted, algo, compat, - buf, len, type, 0); - hash_object_file(compat, converted.buf, converted.len, - type, &compat_oid); - strbuf_release(&converted); - } - } - /* Normally if we have it in the pack then we do not bother writing * it out into .git/objects/??/?{38} file. */ @@ -619,8 +601,8 @@ static int odb_source_loose_write_object(struct odb_source *source, return 0; if (write_loose_object(loose, oid, hdr, hdrlen, buf, len, 0, flags)) return -1; - if (compat) - return repo_add_loose_object_map(loose, oid, &compat_oid); + if (compat_oid) + return repo_add_loose_object_map(loose, oid, compat_oid); return 0; } diff --git a/odb/source-packed.c b/odb/source-packed.c index 8d9ce197cc2be6..af0d533375176f 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -530,7 +530,7 @@ static int odb_source_packed_write_object(struct odb_source *source UNUSED, size_t len UNUSED, enum object_type type UNUSED, struct object_id *oid UNUSED, - struct object_id *compat_oid UNUSED, + const struct object_id *compat_oid UNUSED, unsigned flags UNUSED) { return error("packed backend cannot write objects"); diff --git a/odb/source.h b/odb/source.h index cd63dba91f4e2f..b3c1ca3a6604fc 100644 --- a/odb/source.h +++ b/odb/source.h @@ -207,7 +207,7 @@ struct odb_source { const void *buf, size_t len, enum object_type type, struct object_id *oid, - struct object_id *compat_oid, + const struct object_id *compat_oid, enum odb_write_object_flags flags); /* @@ -417,7 +417,7 @@ static inline int odb_source_write_object(struct odb_source *source, const void *buf, unsigned long len, enum object_type type, struct object_id *oid, - struct object_id *compat_oid, + const struct object_id *compat_oid, enum odb_write_object_flags flags) { return source->write_object(source, buf, len, type, oid, From cb5192ea9ad42f6f7aa539496a5c59d76b8eaf0f Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Fri, 17 Jul 2026 11:32:10 +0200 Subject: [PATCH 066/280] t/u-odb-inmemory: implement wrapper for writing objects In the next commit we're about to change how objects are being written into the object database source. Prepare for this refactoring by introducing a wrapper function into our unit tests so that we don't have to adjust all callsites. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/unit-tests/u-odb-inmemory.c | 48 +++++++++++++++++------------------ 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/t/unit-tests/u-odb-inmemory.c b/t/unit-tests/u-odb-inmemory.c index 6844bfc37ccfdc..2dbc3ab1df689a 100644 --- a/t/unit-tests/u-odb-inmemory.c +++ b/t/unit-tests/u-odb-inmemory.c @@ -1,5 +1,6 @@ #include "unit-test.h" #include "hex.h" +#include "object-file.h" #include "odb/source-inmemory.h" #include "odb/streaming.h" #include "oidset.h" @@ -36,6 +37,16 @@ static void cl_assert_object_info(struct odb_source_inmemory *source, free(actual_content); } +static void cl_assert_write_object(struct odb_source_inmemory *source, + const char *content, + enum object_type type, + struct object_id *oid) +{ + size_t content_len = strlen(content); + cl_must_pass(odb_source_write_object(&source->base, content, content_len, + type, oid, NULL, 0)); +} + void test_odb_inmemory__initialize(void) { odb = odb_new(&repo, "", ""); @@ -78,8 +89,7 @@ void test_odb_inmemory__read_written_object(void) const char data[] = "foobar"; struct object_id written_oid; - cl_must_pass(odb_source_write_object(&source->base, data, strlen(data), - OBJ_BLOB, &written_oid, NULL, 0)); + cl_assert_write_object(source, data, OBJ_BLOB, &written_oid); cl_assert_equal_s(oid_to_hex(&written_oid), FOOBAR_OID); cl_assert_object_info(source, &written_oid, OBJ_BLOB, "foobar"); @@ -94,8 +104,7 @@ void test_odb_inmemory__read_stream_object(void) const char data[] = "foobar"; char buf[3] = { 0 }; - cl_must_pass(odb_source_write_object(&source->base, data, strlen(data), - OBJ_BLOB, &written_oid, NULL, 0)); + cl_assert_write_object(source, data, OBJ_BLOB, &written_oid); cl_must_pass(odb_source_read_object_stream(&stream, &source->base, &written_oid)); @@ -141,8 +150,7 @@ void test_odb_inmemory__for_each_object(void) strbuf_reset(&buf); strbuf_addf(&buf, "%d", i); - cl_must_pass(odb_source_write_object(&source->base, buf.buf, buf.len, - OBJ_BLOB, &written_oid, NULL, 0)); + cl_assert_write_object(source, buf.buf, OBJ_BLOB, &written_oid); cl_must_pass(oidset_insert(&expected_oids, &written_oid)); } @@ -174,12 +182,9 @@ void test_odb_inmemory__for_each_object_can_abort_iteration(void) struct object_id written_oid; unsigned counter = 0; - cl_must_pass(odb_source_write_object(&source->base, "1", 1, - OBJ_BLOB, &written_oid, NULL, 0)); - cl_must_pass(odb_source_write_object(&source->base, "2", 1, - OBJ_BLOB, &written_oid, NULL, 0)); - cl_must_pass(odb_source_write_object(&source->base, "3", 1, - OBJ_BLOB, &written_oid, NULL, 0)); + cl_assert_write_object(source, "1", OBJ_BLOB, &written_oid); + cl_assert_write_object(source, "2", OBJ_BLOB, &written_oid); + cl_assert_write_object(source, "3", OBJ_BLOB, &written_oid); cl_assert_equal_i(odb_source_for_each_object(&source->base, NULL, abort_after_two_objects, @@ -199,12 +204,9 @@ void test_odb_inmemory__count_objects(void) cl_must_pass(odb_source_count_objects(&source->base, 0, &count)); cl_assert_equal_u(count, 0); - cl_must_pass(odb_source_write_object(&source->base, "1", 1, - OBJ_BLOB, &written_oid, NULL, 0)); - cl_must_pass(odb_source_write_object(&source->base, "2", 1, - OBJ_BLOB, &written_oid, NULL, 0)); - cl_must_pass(odb_source_write_object(&source->base, "3", 1, - OBJ_BLOB, &written_oid, NULL, 0)); + cl_assert_write_object(source, "1", OBJ_BLOB, &written_oid); + cl_assert_write_object(source, "2", OBJ_BLOB, &written_oid); + cl_assert_write_object(source, "3", OBJ_BLOB, &written_oid); cl_must_pass(odb_source_count_objects(&source->base, 0, &count)); cl_assert_equal_u(count, 3); @@ -228,8 +230,7 @@ void test_odb_inmemory__find_abbrev_len(void) * * With only one blob written we expect a length of 4. */ - cl_must_pass(odb_source_write_object(&source->base, "368317", strlen("368317"), - OBJ_BLOB, &oid1, NULL, 0)); + cl_assert_write_object(source, "368317", OBJ_BLOB, &oid1); cl_must_pass(odb_source_find_abbrev_len(&source->base, &oid1, 4, &abbrev_len)); cl_assert_equal_u(abbrev_len, 4); @@ -238,8 +239,7 @@ void test_odb_inmemory__find_abbrev_len(void) * With both objects present, the shared 10-character prefix means we * need at least 11 characters to uniquely identify either object. */ - cl_must_pass(odb_source_write_object(&source->base, "514796", strlen("514796"), - OBJ_BLOB, &oid2, NULL, 0)); + cl_assert_write_object(source, "514796", OBJ_BLOB, &oid2); cl_must_pass(odb_source_find_abbrev_len(&source->base, &oid1, 4, &abbrev_len)); cl_assert_equal_u(abbrev_len, 11); @@ -257,9 +257,7 @@ void test_odb_inmemory__freshen_object(void) cl_must_pass(parse_oid_hex_algop(RANDOM_OID, &oid, &end, repo.hash_algo)); cl_assert_equal_i(odb_source_freshen_object(&source->base, &oid), 0); - cl_must_pass(odb_source_write_object(&source->base, "foobar", - strlen("foobar"), OBJ_BLOB, - &written_oid, NULL, 0)); + cl_assert_write_object(source, "foobar", OBJ_BLOB, &written_oid); cl_assert_equal_i(odb_source_freshen_object(&source->base, &written_oid), 1); From 46a586a7199a78d2280ddb22368378693541fff0 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Fri, 17 Jul 2026 11:32:11 +0200 Subject: [PATCH 067/280] odb: compute object hash in `odb_write_object_ext()` Same as in a preceding commit, compute the object hash in `odb_write_object_ext()` so that we can unify this logic. Besides unification, this change also allows us to lift the object existence check out of the "loose" backend into the generic layer, which will happen in the next commit. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- object-file.c | 35 ++++++++--------------------------- object-file.h | 4 ---- odb.c | 2 ++ odb/source-files.c | 2 +- odb/source-inmemory.c | 6 +++--- odb/source-loose.c | 12 +++++++----- odb/source-packed.c | 2 +- odb/source.h | 4 ++-- t/unit-tests/u-odb-inmemory.c | 1 + 9 files changed, 25 insertions(+), 43 deletions(-) diff --git a/object-file.c b/object-file.c index 5283292f1ea02d..9ca14f484dbd5d 100644 --- a/object-file.c +++ b/object-file.c @@ -316,31 +316,6 @@ int parse_loose_header(const char *hdr, struct object_info *oi) return 0; } -static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_ctx *c, - const void *buf, size_t len, - struct object_id *oid, - char *hdr, size_t *hdrlen) -{ - git_hash_init(c, algo); - git_hash_update(c, hdr, *hdrlen); - git_hash_update(c, buf, len); - git_hash_final_oid(oid, c); -} - -void write_object_file_prepare(const struct git_hash_algo *algo, - const void *buf, size_t len, - enum object_type type, struct object_id *oid, - char *hdr, size_t *hdrlen) -{ - struct git_hash_ctx c; - - /* Generate the header */ - *hdrlen = format_object_header(hdr, *hdrlen, type, len); - - /* Hash (function pointers) computation */ - hash_object_body(algo, &c, buf, len, oid, hdr, hdrlen); -} - #define CHECK_COLLISION_DEST_VANISHED -2 static int check_collision(const char *source, const char *dest) @@ -476,10 +451,16 @@ void hash_object_file(const struct git_hash_algo *algo, const void *buf, size_t len, enum object_type type, struct object_id *oid) { + struct git_hash_ctx c; char hdr[MAX_HEADER_LEN]; - size_t hdrlen = sizeof(hdr); + int hdrlen; + + hdrlen = format_object_header(hdr, sizeof(hdr), type, len); - write_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen); + git_hash_init(&c, algo); + git_hash_update(&c, hdr, hdrlen); + git_hash_update(&c, buf, len); + git_hash_final_oid(oid, &c); } struct transaction_packfile { diff --git a/object-file.h b/object-file.h index d04ffa6493ff17..08aafcda0df67b 100644 --- a/object-file.h +++ b/object-file.h @@ -134,10 +134,6 @@ int finalize_object_file_flags(struct repository *repo, void hash_object_file(const struct git_hash_algo *algo, const void *buf, size_t len, enum object_type type, struct object_id *oid); -void write_object_file_prepare(const struct git_hash_algo *algo, - const void *buf, size_t len, - enum object_type type, struct object_id *oid, - char *hdr, size_t *hdrlen); int write_loose_object(struct odb_source_loose *loose, const struct object_id *oid, char *hdr, int hdrlen, const void *buf, unsigned long len, diff --git a/odb.c b/odb.c index 1d6538163b54ea..4adbdf8a648982 100644 --- a/odb.c +++ b/odb.c @@ -995,6 +995,8 @@ int odb_write_object_ext(struct object_database *odb, const struct git_hash_algo *compat = odb->repo->compat_hash_algo; struct object_id compat_oid, *compat_oid_p = NULL; + hash_object_file(odb->repo->hash_algo, buf, len, type, oid); + if (compat) { const struct git_hash_algo *algo = odb->repo->hash_algo; diff --git a/odb/source-files.c b/odb/source-files.c index 3d9f5eca327948..06dfc8dd78aecb 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -162,7 +162,7 @@ static int odb_source_files_freshen_object(struct odb_source *source, static int odb_source_files_write_object(struct odb_source *source, const void *buf, size_t len, enum object_type type, - struct object_id *oid, + const struct object_id *oid, const struct object_id *compat_oid, enum odb_write_object_flags flags) { diff --git a/odb/source-inmemory.c b/odb/source-inmemory.c index e727aba427a17d..963d5203178d97 100644 --- a/odb/source-inmemory.c +++ b/odb/source-inmemory.c @@ -230,15 +230,13 @@ static int odb_source_inmemory_count_objects(struct odb_source *source, static int odb_source_inmemory_write_object(struct odb_source *source, const void *buf, size_t len, enum object_type type, - struct object_id *oid, + const struct object_id *oid, const struct object_id *compat_oid UNUSED, enum odb_write_object_flags flags UNUSED) { struct odb_source_inmemory *inmemory = odb_source_inmemory_downcast(source); struct inmemory_object *object; - hash_object_file(source->odb->repo->hash_algo, buf, len, type, oid); - if (!inmemory->objects) { CALLOC_ARRAY(inmemory->objects, 1); oidtree_init(inmemory->objects); @@ -285,6 +283,8 @@ static int odb_source_inmemory_write_object_stream(struct odb_source *source, goto out; } + hash_object_file(source->odb->repo->hash_algo, data, total_read, OBJ_BLOB, oid); + ret = odb_source_inmemory_write_object(source, data, len, OBJ_BLOB, oid, NULL, 0); if (ret < 0) diff --git a/odb/source-loose.c b/odb/source-loose.c index ca223109cd46e4..d4715da6d1c55e 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -584,19 +584,21 @@ static int odb_source_loose_freshen_object(struct odb_source *source, static int odb_source_loose_write_object(struct odb_source *source, const void *buf, size_t len, - enum object_type type, struct object_id *oid, + enum object_type type, + const struct object_id *oid, const struct object_id *compat_oid, enum odb_write_object_flags flags) { struct odb_source_loose *loose = odb_source_loose_downcast(source); - const struct git_hash_algo *algo = source->odb->repo->hash_algo; char hdr[MAX_HEADER_LEN]; - size_t hdrlen = sizeof(hdr); + int hdrlen; + + hdrlen = format_object_header(hdr, sizeof(hdr), type, len); - /* Normally if we have it in the pack then we do not bother writing + /* + * Normally if we have it in the pack then we do not bother writing * it out into .git/objects/??/?{38} file. */ - write_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen); if (odb_freshen_object(source->odb, oid)) return 0; if (write_loose_object(loose, oid, hdr, hdrlen, buf, len, 0, flags)) diff --git a/odb/source-packed.c b/odb/source-packed.c index af0d533375176f..f7f17064472f07 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -529,7 +529,7 @@ static int odb_source_packed_write_object(struct odb_source *source UNUSED, const void *buf UNUSED, size_t len UNUSED, enum object_type type UNUSED, - struct object_id *oid UNUSED, + const struct object_id *oid UNUSED, const struct object_id *compat_oid UNUSED, unsigned flags UNUSED) { diff --git a/odb/source.h b/odb/source.h index b3c1ca3a6604fc..c4e94c9d0d4615 100644 --- a/odb/source.h +++ b/odb/source.h @@ -206,7 +206,7 @@ struct odb_source { int (*write_object)(struct odb_source *source, const void *buf, size_t len, enum object_type type, - struct object_id *oid, + const struct object_id *oid, const struct object_id *compat_oid, enum odb_write_object_flags flags); @@ -416,7 +416,7 @@ static inline int odb_source_freshen_object(struct odb_source *source, static inline int odb_source_write_object(struct odb_source *source, const void *buf, unsigned long len, enum object_type type, - struct object_id *oid, + const struct object_id *oid, const struct object_id *compat_oid, enum odb_write_object_flags flags) { diff --git a/t/unit-tests/u-odb-inmemory.c b/t/unit-tests/u-odb-inmemory.c index 2dbc3ab1df689a..28a69fc24477c4 100644 --- a/t/unit-tests/u-odb-inmemory.c +++ b/t/unit-tests/u-odb-inmemory.c @@ -43,6 +43,7 @@ static void cl_assert_write_object(struct odb_source_inmemory *source, struct object_id *oid) { size_t content_len = strlen(content); + hash_object_file(repo.hash_algo, content, content_len, type, oid); cl_must_pass(odb_source_write_object(&source->base, content, content_len, type, oid, NULL, 0)); } From b688086b8fd56e1bbab36e6bb26e12844e6e6965 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Fri, 17 Jul 2026 11:32:12 +0200 Subject: [PATCH 068/280] odb: lift object existence check out of the "loose" backend Before writing a new loose object we first check whether the object already exists in any of the sources attached to the object database. This results in a couple of issues: - We have a layering violation, where the source needs to be aware of objects stored in any of the other sources. - Every backend would have to reimplement this check, which feels somewhat pointless. - It is not possible to easily write an object into a source in case the same object already exists in another source. Refactor the code and lift up the object existence check from the "loose" backend into the generic ODB layer. No callers need adjustment as none of them write via a specific source, but via the ODB layer. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb.c | 7 +++++++ odb/source-loose.c | 8 ++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/odb.c b/odb.c index 4adbdf8a648982..bfeca76f4e4af9 100644 --- a/odb.c +++ b/odb.c @@ -997,6 +997,13 @@ int odb_write_object_ext(struct object_database *odb, hash_object_file(odb->repo->hash_algo, buf, len, type, oid); + /* + * We can skip the write in case we already have the object available. + * In that case, we only freshen its mtime. + */ + if (odb_freshen_object(odb, oid)) + return 0; + if (compat) { const struct git_hash_algo *algo = odb->repo->hash_algo; diff --git a/odb/source-loose.c b/odb/source-loose.c index d4715da6d1c55e..04af1a54a3f631 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -595,16 +595,12 @@ static int odb_source_loose_write_object(struct odb_source *source, hdrlen = format_object_header(hdr, sizeof(hdr), type, len); - /* - * Normally if we have it in the pack then we do not bother writing - * it out into .git/objects/??/?{38} file. - */ - if (odb_freshen_object(source->odb, oid)) - return 0; if (write_loose_object(loose, oid, hdr, hdrlen, buf, len, 0, flags)) return -1; + if (compat_oid) return repo_add_loose_object_map(loose, oid, compat_oid); + return 0; } From 8d9135dce3be50c961f23f5e89352b0fd1d61117 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Fri, 17 Jul 2026 11:32:13 +0200 Subject: [PATCH 069/280] odb: support setting mtime when writing objects The function `force_object_loose()` is used to loosen packed objects before repacking. It passes the pack's mtime along so that the newly written loose object inherits the same timestamp. This matters for object pruning, which uses the mtime to determine whether an object is old enough to be pruned. In a subsequent commit, `force_object_loose()` will be converted to use the generic `odb_source_write_object()` interface instead of calling `write_loose_object()` directly. But the generic interface doesn't yet support setting a specific mtime, which makes it impossible to implement the logic as of now. Prepare for the change by introducing a new `mtime` parameter to this function that we plumb through the stack. If set, the backends are instructed to set the object's mtime accordingly. If unset, the backends are expected to use the current time instead. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/pack-objects.c | 2 +- object-file.c | 29 ++++++++++++++++++++--------- object-file.h | 8 +++++--- odb.c | 6 +++--- odb/source-files.c | 10 ++++++---- odb/source-inmemory.c | 6 ++++-- odb/source-loose.c | 8 +++++--- odb/source-packed.c | 13 +++++++++++-- odb/source.h | 12 ++++++++---- read-cache.c | 2 +- t/unit-tests/u-odb-inmemory.c | 6 +++--- 11 files changed, 67 insertions(+), 35 deletions(-) diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index ea5eab4cf841bf..e64a96f1a727ec 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -4642,7 +4642,7 @@ static void loosen_unused_packed_objects(void) !has_sha1_pack_kept_or_nonlocal(&oid) && !loosened_object_can_be_discarded(&oid, p->mtime)) { if (force_object_loose(the_repository->objects->sources, - &oid, p->mtime)) + &oid, &p->mtime)) die(_("unable to force loose object")); loosened_objects_nr++; } diff --git a/object-file.c b/object-file.c index 9ca14f484dbd5d..5b075309506334 100644 --- a/object-file.c +++ b/object-file.c @@ -67,9 +67,17 @@ const char *odb_loose_path(struct odb_source_loose *loose, } /* Returns 1 if we have successfully freshened the file, 0 otherwise. */ -static int freshen_file(const char *fn) +static int freshen_file(const char *fn, const time_t *mtime) { - return !utime(fn, NULL); + struct utimbuf times, *timesp = NULL; + + if (mtime) { + times.actime = *mtime; + times.modtime = *mtime; + timesp = × + } + + return !utime(fn, timesp); } /* @@ -79,11 +87,12 @@ static int freshen_file(const char *fn) * either does not exist on disk, or has a stale mtime and may be subject to * pruning). */ -int check_and_freshen_file(const char *fn, int freshen) +int check_and_freshen_file(const char *fn, int freshen, + const time_t *mtime) { if (access(fn, F_OK)) return 0; - if (freshen && !freshen_file(fn)) + if (freshen && !freshen_file(fn, mtime)) return 0; return 1; } @@ -706,7 +715,7 @@ static int end_loose_object_common(struct odb_source_loose *loose, int write_loose_object(struct odb_source_loose *loose, const struct object_id *oid, char *hdr, int hdrlen, const void *buf, unsigned long len, - time_t mtime, unsigned flags) + const time_t *mtime, unsigned flags) { int fd, ret; unsigned char compressed[4096]; @@ -751,9 +760,11 @@ int write_loose_object(struct odb_source_loose *loose, close_loose_object(loose, fd, tmp_file.buf); if (mtime) { - struct utimbuf utb; - utb.actime = mtime; - utb.modtime = mtime; + struct utimbuf utb = { + .actime = *mtime, + .modtime = *mtime, + }; + if (utime(tmp_file.buf, &utb) < 0 && !(flags & ODB_WRITE_OBJECT_SILENT)) warning_errno(_("failed utime() on %s"), tmp_file.buf); @@ -883,7 +894,7 @@ int odb_source_loose_write_stream(struct odb_source_loose *loose, } int force_object_loose(struct odb_source *source, - const struct object_id *oid, time_t mtime) + const struct object_id *oid, const time_t *mtime) { struct odb_source_files *files = odb_source_files_downcast(source); const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; diff --git a/object-file.h b/object-file.h index 08aafcda0df67b..9fd540afb6c6e7 100644 --- a/object-file.h +++ b/object-file.h @@ -99,7 +99,8 @@ int format_object_header(char *str, size_t size, enum object_type type, size_t objsize); int force_object_loose(struct odb_source *source, - const struct object_id *oid, time_t mtime); + const struct object_id *oid, + const time_t *mtime); /** * With in-core object data in "buf", rehash it to make sure the @@ -137,10 +138,11 @@ void hash_object_file(const struct git_hash_algo *algo, const void *buf, int write_loose_object(struct odb_source_loose *loose, const struct object_id *oid, char *hdr, int hdrlen, const void *buf, unsigned long len, - time_t mtime, unsigned flags); + const time_t *mtime, unsigned flags); /* Helper to check and "touch" a file */ -int check_and_freshen_file(const char *fn, int freshen); +int check_and_freshen_file(const char *fn, int freshen, + const time_t *mtime); /* * Open the loose object at path, check its hash, and return the contents, diff --git a/odb.c b/odb.c index bfeca76f4e4af9..dabd481f57dbc4 100644 --- a/odb.c +++ b/odb.c @@ -738,7 +738,7 @@ int odb_pretend_object(struct object_database *odb, return 0; return odb_source_write_object(odb->inmemory_objects, - buf, len, type, oid, NULL, 0); + buf, len, type, oid, NULL, NULL, 0); } void *odb_read_object(struct object_database *odb, @@ -829,7 +829,7 @@ int odb_freshen_object(struct object_database *odb, struct odb_source *source; odb_prepare_alternates(odb); for (source = odb->sources; source; source = source->next) - if (odb_source_freshen_object(source, oid)) + if (odb_source_freshen_object(source, oid, NULL)) return 1; return 0; } @@ -1024,7 +1024,7 @@ int odb_write_object_ext(struct object_database *odb, } return odb_source_write_object(odb->sources, buf, len, type, - oid, compat_oid_p, flags); + oid, compat_oid_p, NULL, flags); } int odb_write_object_stream(struct object_database *odb, diff --git a/odb/source-files.c b/odb/source-files.c index 06dfc8dd78aecb..4df4e1af6cf76b 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -150,11 +150,12 @@ static int odb_source_files_find_abbrev_len(struct odb_source *source, } static int odb_source_files_freshen_object(struct odb_source *source, - const struct object_id *oid) + const struct object_id *oid, + const time_t *mtime) { struct odb_source_files *files = odb_source_files_downcast(source); - if (odb_source_freshen_object(&files->packed->base, oid) || - odb_source_freshen_object(&files->loose->base, oid)) + if (odb_source_freshen_object(&files->packed->base, oid, mtime) || + odb_source_freshen_object(&files->loose->base, oid, mtime)) return 1; return 0; } @@ -164,11 +165,12 @@ static int odb_source_files_write_object(struct odb_source *source, enum object_type type, const struct object_id *oid, const struct object_id *compat_oid, + const time_t *mtime, enum odb_write_object_flags flags) { struct odb_source_files *files = odb_source_files_downcast(source); return odb_source_write_object(&files->loose->base, buf, len, type, - oid, compat_oid, flags); + oid, compat_oid, mtime, flags); } static int odb_source_files_write_object_stream(struct odb_source *source, diff --git a/odb/source-inmemory.c b/odb/source-inmemory.c index 963d5203178d97..3e71611b8e0071 100644 --- a/odb/source-inmemory.c +++ b/odb/source-inmemory.c @@ -232,6 +232,7 @@ static int odb_source_inmemory_write_object(struct odb_source *source, enum object_type type, const struct object_id *oid, const struct object_id *compat_oid UNUSED, + const time_t *mtime UNUSED, enum odb_write_object_flags flags UNUSED) { struct odb_source_inmemory *inmemory = odb_source_inmemory_downcast(source); @@ -286,7 +287,7 @@ static int odb_source_inmemory_write_object_stream(struct odb_source *source, hash_object_file(source->odb->repo->hash_algo, data, total_read, OBJ_BLOB, oid); ret = odb_source_inmemory_write_object(source, data, len, OBJ_BLOB, oid, - NULL, 0); + NULL, NULL, 0); if (ret < 0) goto out; @@ -296,7 +297,8 @@ static int odb_source_inmemory_write_object_stream(struct odb_source *source, } static int odb_source_inmemory_freshen_object(struct odb_source *source, - const struct object_id *oid) + const struct object_id *oid, + const time_t *mtime UNUSED) { struct odb_source_inmemory *inmemory = odb_source_inmemory_downcast(source); if (find_cached_object(inmemory, oid)) diff --git a/odb/source-loose.c b/odb/source-loose.c index 04af1a54a3f631..520a30157cf0d6 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -574,12 +574,13 @@ static int odb_source_loose_count_objects(struct odb_source *source, } static int odb_source_loose_freshen_object(struct odb_source *source, - const struct object_id *oid) + const struct object_id *oid, + const time_t *mtime) { struct odb_source_loose *loose = odb_source_loose_downcast(source); static struct strbuf path = STRBUF_INIT; odb_loose_path(loose, &path, oid); - return !!check_and_freshen_file(path.buf, 1); + return !!check_and_freshen_file(path.buf, 1, mtime); } static int odb_source_loose_write_object(struct odb_source *source, @@ -587,6 +588,7 @@ static int odb_source_loose_write_object(struct odb_source *source, enum object_type type, const struct object_id *oid, const struct object_id *compat_oid, + const time_t *mtime, enum odb_write_object_flags flags) { struct odb_source_loose *loose = odb_source_loose_downcast(source); @@ -595,7 +597,7 @@ static int odb_source_loose_write_object(struct odb_source *source, hdrlen = format_object_header(hdr, sizeof(hdr), type, len); - if (write_loose_object(loose, oid, hdr, hdrlen, buf, len, 0, flags)) + if (write_loose_object(loose, oid, hdr, hdrlen, buf, len, mtime, flags)) return -1; if (compat_oid) diff --git a/odb/source-packed.c b/odb/source-packed.c index f7f17064472f07..5e5da9bc547f9a 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -507,18 +507,26 @@ static int odb_source_packed_find_abbrev_len(struct odb_source *source, } static int odb_source_packed_freshen_object(struct odb_source *source, - const struct object_id *oid) + const struct object_id *oid, + const time_t *mtime) { struct odb_source_packed *packed = odb_source_packed_downcast(source); + struct utimbuf times, *timesp = NULL; struct pack_entry e; + if (mtime) { + times.actime = *mtime; + times.modtime = *mtime; + timesp = × + } + if (!find_pack_entry(packed, oid, &e)) return 0; if (e.p->is_cruft) return 0; if (e.p->freshened) return 1; - if (utime(e.p->pack_name, NULL)) + if (utime(e.p->pack_name, timesp)) return 0; e.p->freshened = 1; @@ -531,6 +539,7 @@ static int odb_source_packed_write_object(struct odb_source *source UNUSED, enum object_type type UNUSED, const struct object_id *oid UNUSED, const struct object_id *compat_oid UNUSED, + const time_t *mtime UNUSED, unsigned flags UNUSED) { return error("packed backend cannot write objects"); diff --git a/odb/source.h b/odb/source.h index c4e94c9d0d4615..fc04dd5cda8800 100644 --- a/odb/source.h +++ b/odb/source.h @@ -190,7 +190,8 @@ struct odb_source { * has been freshened. */ int (*freshen_object)(struct odb_source *source, - const struct object_id *oid); + const struct object_id *oid, + const time_t *mtime); /* * This callback is expected to persist the given object into the @@ -208,6 +209,7 @@ struct odb_source { enum object_type type, const struct object_id *oid, const struct object_id *compat_oid, + const time_t *mtime, enum odb_write_object_flags flags); /* @@ -403,9 +405,10 @@ static inline int odb_source_find_abbrev_len(struct odb_source *source, * not exist. */ static inline int odb_source_freshen_object(struct odb_source *source, - const struct object_id *oid) + const struct object_id *oid, + const time_t *mtime) { - return source->freshen_object(source, oid); + return source->freshen_object(source, oid, mtime); } /* @@ -418,10 +421,11 @@ static inline int odb_source_write_object(struct odb_source *source, enum object_type type, const struct object_id *oid, const struct object_id *compat_oid, + const time_t *mtime, enum odb_write_object_flags flags) { return source->write_object(source, buf, len, type, oid, - compat_oid, flags); + compat_oid, mtime, flags); } /* diff --git a/read-cache.c b/read-cache.c index 3510b49edf70be..c67930177f7639 100644 --- a/read-cache.c +++ b/read-cache.c @@ -2342,7 +2342,7 @@ int do_read_index(struct index_state *istate, const char *path, int must_exist) */ static void freshen_shared_index(const char *shared_index, int warn) { - if (!check_and_freshen_file(shared_index, 1) && warn) + if (!check_and_freshen_file(shared_index, 1, NULL) && warn) warning(_("could not freshen shared index '%s'"), shared_index); } diff --git a/t/unit-tests/u-odb-inmemory.c b/t/unit-tests/u-odb-inmemory.c index 28a69fc24477c4..ddf2db5c811fb8 100644 --- a/t/unit-tests/u-odb-inmemory.c +++ b/t/unit-tests/u-odb-inmemory.c @@ -45,7 +45,7 @@ static void cl_assert_write_object(struct odb_source_inmemory *source, size_t content_len = strlen(content); hash_object_file(repo.hash_algo, content, content_len, type, oid); cl_must_pass(odb_source_write_object(&source->base, content, content_len, - type, oid, NULL, 0)); + type, oid, NULL, NULL, 0)); } void test_odb_inmemory__initialize(void) @@ -256,11 +256,11 @@ void test_odb_inmemory__freshen_object(void) const char *end; cl_must_pass(parse_oid_hex_algop(RANDOM_OID, &oid, &end, repo.hash_algo)); - cl_assert_equal_i(odb_source_freshen_object(&source->base, &oid), 0); + cl_assert_equal_i(odb_source_freshen_object(&source->base, &oid, NULL), 0); cl_assert_write_object(source, "foobar", OBJ_BLOB, &written_oid); cl_assert_equal_i(odb_source_freshen_object(&source->base, - &written_oid), 1); + &written_oid, NULL), 1); odb_source_free(&source->base); } From 0e002f6dc74841324687284e6a3ae4a6061c0c73 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Fri, 17 Jul 2026 11:32:14 +0200 Subject: [PATCH 070/280] object-file: fix memory leak in `force_object_loose()` We return an error when converting the given object to the compatibility hash algorithm fails. This early return causes a memory leak though, because we don't free the content buffer that we've already read before via `odb_read_object_info_extended()`. Plug the memory leak by creating a common exit path where the buffer gets free'd. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- object-file.c | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/object-file.c b/object-file.c index 5b075309506334..067a63a4f1e13b 100644 --- a/object-file.c +++ b/object-file.c @@ -898,7 +898,7 @@ int force_object_loose(struct odb_source *source, { struct odb_source_files *files = odb_source_files_downcast(source); const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; - void *buf; + void *buf = NULL; size_t len; struct object_info oi = OBJECT_INFO_INIT; struct object_id compat_oid; @@ -916,19 +916,29 @@ int force_object_loose(struct odb_source *source, oi.typep = &type; oi.sizep = &len; oi.contentp = &buf; - if (odb_read_object_info_extended(source->odb, oid, &oi, 0)) - return error(_("cannot read object for %s"), oid_to_hex(oid)); + if (odb_read_object_info_extended(source->odb, oid, &oi, 0)) { + ret = error(_("cannot read object for %s"), oid_to_hex(oid)); + goto out; + } + if (compat) { - if (repo_oid_to_algop(source->odb->repo, oid, compat, &compat_oid)) - return error(_("cannot map object %s to %s"), - oid_to_hex(oid), compat->name); + if (repo_oid_to_algop(source->odb->repo, oid, compat, &compat_oid)) { + ret = error(_("cannot map object %s to %s"), + oid_to_hex(oid), compat->name); + goto out; + } } + hdrlen = format_object_header(hdr, sizeof(hdr), type, len); ret = write_loose_object(files->loose, oid, hdr, hdrlen, buf, len, mtime, 0); - if (!ret && compat) + if (ret) + goto out; + + if (compat) ret = repo_add_loose_object_map(files->loose, oid, &compat_oid); - free(buf); +out: + free(buf); return ret; } From 627d3b7ab5dc76f0d4e4ef7a42184524df8f2ab1 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Fri, 17 Jul 2026 11:32:15 +0200 Subject: [PATCH 071/280] object-file: force objects loose via generic interface When repacking objects we may end up "loosening" objects via `force_objects_loose()`. The implementation of this logic still sits with "object-file.c" even though it is ultimately an implementation detail of the "files" backend. Moving this logic around is non-trivial though as we depend on `write_loose_object()`, which is an internal implementation detail of how we write loose objects. Until now it wasn't possible to use the generic function `odb_source_write_object()` though, because the "loose" implementation thereof would skip writing the object in case it already exists in any other source. This restriction was lifted over the preceding commits though, where this object existence check is now handled on the object database level and not on the individual source level anymore. Consequently, it is now possible to use generic interfaces. Refactor the code accordingly so that we can move the logic around in a subsequent commit. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- object-file.c | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/object-file.c b/object-file.c index 067a63a4f1e13b..89825feed0c1b1 100644 --- a/object-file.c +++ b/object-file.c @@ -898,13 +898,11 @@ int force_object_loose(struct odb_source *source, { struct odb_source_files *files = odb_source_files_downcast(source); const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; - void *buf = NULL; - size_t len; struct object_info oi = OBJECT_INFO_INIT; - struct object_id compat_oid; + struct object_id compat_oid, *compat_oid_p = NULL; enum object_type type; - char hdr[MAX_HEADER_LEN]; - int hdrlen; + void *buf = NULL; + size_t len; int ret; for (struct odb_source *s = source->odb->sources; s; s = s->next) { @@ -927,15 +925,12 @@ int force_object_loose(struct odb_source *source, oid_to_hex(oid), compat->name); goto out; } - } - hdrlen = format_object_header(hdr, sizeof(hdr), type, len); - ret = write_loose_object(files->loose, oid, hdr, hdrlen, buf, len, mtime, 0); - if (ret) - goto out; + compat_oid_p = &compat_oid; + } - if (compat) - ret = repo_add_loose_object_map(files->loose, oid, &compat_oid); + ret = odb_source_write_object(&files->loose->base, buf, len, type, oid, + compat_oid_p, mtime, 0); out: free(buf); From a6c1e16b480a797a61a47616772e7254f7759053 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Fri, 17 Jul 2026 11:32:16 +0200 Subject: [PATCH 072/280] object-file: move `force_object_loose()` In the preceding commits we have refactored `force_object_loose()` to not call internal functions anymore for writing the object. Instead, it now only uses generic functions that are accessible to all callers. Consequently, we can now easily move the function to its only caller, which is git-pack-objects(1). Do so. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/pack-objects.c | 46 ++++++++++++++++++++++++++++++++++++++++++ object-file.c | 44 ---------------------------------------- object-file.h | 4 ---- 3 files changed, 46 insertions(+), 48 deletions(-) diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index e64a96f1a727ec..bb3bc486e8be90 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -32,6 +32,7 @@ #include "list.h" #include "packfile.h" #include "object-file.h" +#include "object-file-convert.h" #include "odb.h" #include "odb/streaming.h" #include "replace-object.h" @@ -4622,6 +4623,51 @@ static int loosened_object_can_be_discarded(const struct object_id *oid, return 1; } +static int force_object_loose(struct odb_source *source, + const struct object_id *oid, + const time_t *mtime) +{ + struct odb_source_files *files = odb_source_files_downcast(source); + const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; + struct object_info oi = OBJECT_INFO_INIT; + struct object_id compat_oid, *compat_oid_p = NULL; + enum object_type type; + void *buf = NULL; + size_t len; + int ret; + + for (struct odb_source *s = source->odb->sources; s; s = s->next) { + struct odb_source_files *files = odb_source_files_downcast(s); + if (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0)) + return 0; + } + + oi.typep = &type; + oi.sizep = &len; + oi.contentp = &buf; + if (odb_read_object_info_extended(source->odb, oid, &oi, 0)) { + ret = error(_("cannot read object for %s"), oid_to_hex(oid)); + goto out; + } + + if (compat) { + if (repo_oid_to_algop(source->odb->repo, oid, compat, &compat_oid)) { + ret = error(_("cannot map object %s to %s"), + oid_to_hex(oid), compat->name); + goto out; + } + + compat_oid_p = &compat_oid; + } + + ret = odb_source_write_object(&files->loose->base, buf, len, type, oid, + compat_oid_p, mtime, 0); + +out: + free(buf); + return ret; +} + static void loosen_unused_packed_objects(void) { struct packed_git *p; diff --git a/object-file.c b/object-file.c index 89825feed0c1b1..b867d8d9de10af 100644 --- a/object-file.c +++ b/object-file.c @@ -893,50 +893,6 @@ int odb_source_loose_write_stream(struct odb_source_loose *loose, return err; } -int force_object_loose(struct odb_source *source, - const struct object_id *oid, const time_t *mtime) -{ - struct odb_source_files *files = odb_source_files_downcast(source); - const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; - struct object_info oi = OBJECT_INFO_INIT; - struct object_id compat_oid, *compat_oid_p = NULL; - enum object_type type; - void *buf = NULL; - size_t len; - int ret; - - for (struct odb_source *s = source->odb->sources; s; s = s->next) { - struct odb_source_files *files = odb_source_files_downcast(s); - if (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0)) - return 0; - } - - oi.typep = &type; - oi.sizep = &len; - oi.contentp = &buf; - if (odb_read_object_info_extended(source->odb, oid, &oi, 0)) { - ret = error(_("cannot read object for %s"), oid_to_hex(oid)); - goto out; - } - - if (compat) { - if (repo_oid_to_algop(source->odb->repo, oid, compat, &compat_oid)) { - ret = error(_("cannot map object %s to %s"), - oid_to_hex(oid), compat->name); - goto out; - } - - compat_oid_p = &compat_oid; - } - - ret = odb_source_write_object(&files->loose->base, buf, len, type, oid, - compat_oid_p, mtime, 0); - -out: - free(buf); - return ret; -} - /* * We can't use the normal fsck_error_function() for index_mem(), * because we don't yet have a valid oid for it to report. Instead, diff --git a/object-file.h b/object-file.h index 9fd540afb6c6e7..31781a9c5308d8 100644 --- a/object-file.h +++ b/object-file.h @@ -98,10 +98,6 @@ int for_each_file_in_obj_subdir(unsigned int subdir_nr, int format_object_header(char *str, size_t size, enum object_type type, size_t objsize); -int force_object_loose(struct odb_source *source, - const struct object_id *oid, - const time_t *mtime); - /** * With in-core object data in "buf", rehash it to make sure the * object name actually matches "oid" to detect object corruption. From 810f8b203303f27543537d3ceadc90b065ed9c8f Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Fri, 17 Jul 2026 11:32:17 +0200 Subject: [PATCH 073/280] object-file: move logic to write loose objects The logic to write loose objects is split up across "object-file.c" and "odb/source-loose.c". This split is somewhat weird, but it is the result of two things: - `force_object_loose()` used to reach into internals of how exactly we write objects. - The logic of writing objects is intertwined with potentially starting a transaction. We have refactored `force_object_loose()` over preceding commits to work via generic interfaces now, so this reason doesn't exist anymore. But the second reason still does, as our management of "files" transactions and their ad-hoc creation is still very messy. This area definitely requires further work, and that work is indeed ongoing. That being said, we can already move the writing logic into the "loose" backend rather easily. All we have to do is to expose two functions that relate to the transactions. Expose these two functions and move the writing logic into the "loose" backend accordingly so that it becomes more self-contained. Note that this requires us to drop a reference to `the_repository` in favor of using the source's repository in `start_loose_object_common()`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- object-file.c | 360 +-------------------------------------------- object-file.h | 22 +-- odb/source-loose.c | 354 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 357 insertions(+), 379 deletions(-) diff --git a/object-file.c b/object-file.c index b867d8d9de10af..bdc97d7943af25 100644 --- a/object-file.c +++ b/object-file.c @@ -491,7 +491,7 @@ struct odb_transaction_files { const char *prefix; }; -static int odb_transaction_files_prepare(struct odb_transaction *base) +int odb_transaction_files_prepare(struct odb_transaction *base) { struct odb_transaction_files *transaction = container_of_or_null(base, struct odb_transaction_files, base); @@ -514,8 +514,8 @@ static int odb_transaction_files_prepare(struct odb_transaction *base) return 0; } -static void odb_transaction_files_fsync(struct odb_transaction *base, - int fd, const char *filename) +void odb_transaction_files_fsync(struct odb_transaction *base, + int fd, const char *filename) { struct odb_transaction_files *transaction = container_of_or_null(base, struct odb_transaction_files, base); @@ -539,360 +539,6 @@ static void odb_transaction_files_fsync(struct odb_transaction *base, } } -/* Finalize a file on disk, and close it. */ -static void close_loose_object(struct odb_source_loose *loose, - int fd, const char *filename) -{ - if (loose->base.will_destroy) - goto out; - - if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT)) - odb_transaction_files_fsync(loose->base.odb->transaction, fd, filename); - else if (fsync_object_files > 0) - fsync_or_die(fd, filename); - else - fsync_component_or_die(FSYNC_COMPONENT_LOOSE_OBJECT, fd, - filename); - -out: - if (close(fd) != 0) - die_errno(_("error when closing loose object file")); -} - -/* Size of directory component, including the ending '/' */ -static inline int directory_size(const char *filename) -{ - const char *s = strrchr(filename, '/'); - if (!s) - return 0; - return s - filename + 1; -} - -/* - * This creates a temporary file in the same directory as the final - * 'filename' - * - * We want to avoid cross-directory filename renames, because those - * can have problems on various filesystems (FAT, NFS, Coda). - */ -static int create_tmpfile(struct repository *repo, - struct strbuf *tmp, const char *filename) -{ - int fd, dirlen = directory_size(filename); - - strbuf_reset(tmp); - strbuf_add(tmp, filename, dirlen); - strbuf_addstr(tmp, "tmp_obj_XXXXXX"); - fd = git_mkstemp_mode(tmp->buf, 0444); - if (fd < 0 && dirlen && errno == ENOENT) { - /* - * Make sure the directory exists; note that the contents - * of the buffer are undefined after mkstemp returns an - * error, so we have to rewrite the whole buffer from - * scratch. - */ - strbuf_reset(tmp); - strbuf_add(tmp, filename, dirlen - 1); - if (mkdir(tmp->buf, 0777) && errno != EEXIST) - return -1; - if (adjust_shared_perm(repo, tmp->buf)) - return -1; - - /* Try again */ - strbuf_addstr(tmp, "/tmp_obj_XXXXXX"); - fd = git_mkstemp_mode(tmp->buf, 0444); - } - return fd; -} - -/** - * Common steps for loose object writers to start writing loose - * objects: - * - * - Create tmpfile for the loose object. - * - Setup zlib stream for compression. - * - Start to feed header to zlib stream. - * - * Returns a "fd", which should later be provided to - * end_loose_object_common(). - */ -static int start_loose_object_common(struct odb_source_loose *loose, - struct strbuf *tmp_file, - const char *filename, unsigned flags, - git_zstream *stream, - unsigned char *buf, size_t buflen, - struct git_hash_ctx *c, struct git_hash_ctx *compat_c, - char *hdr, int hdrlen) -{ - const struct git_hash_algo *algo = loose->base.odb->repo->hash_algo; - const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo; - int fd; - struct repo_config_values *cfg = repo_config_values(the_repository); - - fd = create_tmpfile(loose->base.odb->repo, tmp_file, filename); - if (fd < 0) { - if (flags & ODB_WRITE_OBJECT_SILENT) - return -1; - else if (errno == EACCES) - return error(_("insufficient permission for adding " - "an object to repository database %s"), - loose->base.path); - else - return error_errno( - _("unable to create temporary file")); - } - - /* Setup zlib stream for compression */ - git_deflate_init(stream, cfg->zlib_compression_level); - stream->next_out = buf; - stream->avail_out = buflen; - git_hash_init(c, algo); - if (compat && compat_c) - git_hash_init(compat_c, compat); - - /* Start to feed header to zlib stream */ - stream->next_in = (unsigned char *)hdr; - stream->avail_in = hdrlen; - while (git_deflate(stream, 0) == Z_OK) - ; /* nothing */ - git_hash_update(c, hdr, hdrlen); - if (compat && compat_c) - git_hash_update(compat_c, hdr, hdrlen); - - return fd; -} - -/** - * Common steps for the inner git_deflate() loop for writing loose - * objects. Returns what git_deflate() returns. - */ -static int write_loose_object_common(struct odb_source_loose *loose, - struct git_hash_ctx *c, struct git_hash_ctx *compat_c, - git_zstream *stream, const int flush, - unsigned char *in0, const int fd, - unsigned char *compressed, - const size_t compressed_len) -{ - const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo; - int ret; - - ret = git_deflate(stream, flush ? Z_FINISH : 0); - git_hash_update(c, in0, stream->next_in - in0); - if (compat && compat_c) - git_hash_update(compat_c, in0, stream->next_in - in0); - if (write_in_full(fd, compressed, stream->next_out - compressed) < 0) - die_errno(_("unable to write loose object file")); - stream->next_out = compressed; - stream->avail_out = compressed_len; - - return ret; -} - -/** - * Common steps for loose object writers to end writing loose objects: - * - * - End the compression of zlib stream. - * - Get the calculated oid to "oid". - */ -static int end_loose_object_common(struct odb_source_loose *loose, - struct git_hash_ctx *c, struct git_hash_ctx *compat_c, - git_zstream *stream, struct object_id *oid, - struct object_id *compat_oid) -{ - const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo; - int ret; - - ret = git_deflate_end_gently(stream); - if (ret != Z_OK) - return ret; - git_hash_final_oid(oid, c); - if (compat && compat_c) - git_hash_final_oid(compat_oid, compat_c); - - return Z_OK; -} - -int write_loose_object(struct odb_source_loose *loose, - const struct object_id *oid, char *hdr, - int hdrlen, const void *buf, unsigned long len, - const time_t *mtime, unsigned flags) -{ - int fd, ret; - unsigned char compressed[4096]; - git_zstream stream; - struct git_hash_ctx c; - struct object_id parano_oid; - static struct strbuf tmp_file = STRBUF_INIT; - static struct strbuf filename = STRBUF_INIT; - - if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT)) - odb_transaction_files_prepare(loose->base.odb->transaction); - - odb_loose_path(loose, &filename, oid); - - fd = start_loose_object_common(loose, &tmp_file, filename.buf, flags, - &stream, compressed, sizeof(compressed), - &c, NULL, hdr, hdrlen); - if (fd < 0) - return -1; - - /* Then the data itself.. */ - stream.next_in = (void *)buf; - stream.avail_in = len; - do { - unsigned char *in0 = stream.next_in; - - ret = write_loose_object_common(loose, &c, NULL, &stream, 1, in0, fd, - compressed, sizeof(compressed)); - } while (ret == Z_OK); - - if (ret != Z_STREAM_END) - die(_("unable to deflate new object %s (%d)"), oid_to_hex(oid), - ret); - ret = end_loose_object_common(loose, &c, NULL, &stream, ¶no_oid, NULL); - if (ret != Z_OK) - die(_("deflateEnd on object %s failed (%d)"), oid_to_hex(oid), - ret); - if (!oideq(oid, ¶no_oid)) - die(_("confused by unstable object source data for %s"), - oid_to_hex(oid)); - - close_loose_object(loose, fd, tmp_file.buf); - - if (mtime) { - struct utimbuf utb = { - .actime = *mtime, - .modtime = *mtime, - }; - - if (utime(tmp_file.buf, &utb) < 0 && - !(flags & ODB_WRITE_OBJECT_SILENT)) - warning_errno(_("failed utime() on %s"), tmp_file.buf); - } - - return finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf, - FOF_SKIP_COLLISION_CHECK); -} - -int odb_source_loose_write_stream(struct odb_source_loose *loose, - struct odb_write_stream *in_stream, size_t len, - struct object_id *oid) -{ - const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo; - struct object_id compat_oid; - int fd, ret, err = 0, flush = 0; - unsigned char compressed[4096]; - git_zstream stream; - struct git_hash_ctx c, compat_c; - struct strbuf tmp_file = STRBUF_INIT; - struct strbuf filename = STRBUF_INIT; - unsigned char buf[8192]; - int dirlen; - char hdr[MAX_HEADER_LEN]; - int hdrlen; - - if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT)) - odb_transaction_files_prepare(loose->base.odb->transaction); - - /* Since oid is not determined, save tmp file to odb path. */ - strbuf_addf(&filename, "%s/", loose->base.path); - hdrlen = format_object_header(hdr, sizeof(hdr), OBJ_BLOB, len); - - /* - * Common steps for write_loose_object and stream_loose_object to - * start writing loose objects: - * - * - Create tmpfile for the loose object. - * - Setup zlib stream for compression. - * - Start to feed header to zlib stream. - */ - fd = start_loose_object_common(loose, &tmp_file, filename.buf, 0, - &stream, compressed, sizeof(compressed), - &c, &compat_c, hdr, hdrlen); - if (fd < 0) { - err = -1; - goto cleanup; - } - - /* Then the data itself.. */ - do { - unsigned char *in0 = stream.next_in; - - if (!stream.avail_in && !in_stream->is_finished) { - ssize_t read_len = odb_write_stream_read(in_stream, buf, - sizeof(buf)); - if (read_len < 0) { - close(fd); - err = -1; - goto cleanup; - } - - stream.avail_in = read_len; - stream.next_in = buf; - in0 = buf; - /* All data has been read. */ - if (in_stream->is_finished) - flush = 1; - } - ret = write_loose_object_common(loose, &c, &compat_c, &stream, flush, in0, fd, - compressed, sizeof(compressed)); - /* - * Unlike write_loose_object(), we do not have the entire - * buffer. If we get Z_BUF_ERROR due to too few input bytes, - * then we'll replenish them in the next input_stream->read() - * call when we loop. - */ - } while (ret == Z_OK || ret == Z_BUF_ERROR); - - if (stream.total_in != len + hdrlen) - die(_("write stream object %"PRIuMAX" != %"PRIuMAX), (uintmax_t)stream.total_in, - (uintmax_t)len + hdrlen); - - /* - * Common steps for write_loose_object and stream_loose_object to - * end writing loose object: - * - * - End the compression of zlib stream. - * - Get the calculated oid. - */ - if (ret != Z_STREAM_END) - die(_("unable to stream deflate new object (%d)"), ret); - ret = end_loose_object_common(loose, &c, &compat_c, &stream, oid, &compat_oid); - if (ret != Z_OK) - die(_("deflateEnd on stream object failed (%d)"), ret); - close_loose_object(loose, fd, tmp_file.buf); - - if (odb_freshen_object(loose->base.odb, oid)) { - unlink_or_warn(tmp_file.buf); - goto cleanup; - } - odb_loose_path(loose, &filename, oid); - - /* We finally know the object path, and create the missing dir. */ - dirlen = directory_size(filename.buf); - if (dirlen) { - struct strbuf dir = STRBUF_INIT; - strbuf_add(&dir, filename.buf, dirlen); - - if (safe_create_dir_in_gitdir(loose->base.odb->repo, dir.buf) && - errno != EEXIST) { - err = error_errno(_("unable to create directory %s"), dir.buf); - strbuf_release(&dir); - goto cleanup; - } - strbuf_release(&dir); - } - - err = finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf, - FOF_SKIP_COLLISION_CHECK); - if (!err && compat) - err = repo_add_loose_object_map(loose, oid, &compat_oid); -cleanup: - strbuf_release(&tmp_file); - strbuf_release(&filename); - return err; -} - /* * We can't use the normal fsck_error_function() for index_mem(), * because we don't yet have a valid oid for it to report. Instead, diff --git a/object-file.h b/object-file.h index 31781a9c5308d8..805f2cfa289661 100644 --- a/object-file.h +++ b/object-file.h @@ -24,20 +24,6 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa struct object_info; struct odb_source; -/* - * Write the given stream into the loose object source. The only difference - * from the generic implementation of this function is that we don't perform an - * object existence check here. - * - * TODO: We should stop exposing this function altogether and move it into - * "odb/source-loose.c". This requires a couple of refactorings though to make - * `force_object_loose()` generic and is thus postponed to a later point in - * time. - */ -int odb_source_loose_write_stream(struct odb_source_loose *source, - struct odb_write_stream *stream, size_t len, - struct object_id *oid); - /* * Put in `buf` the name of the file in the local object database that * would be used to store a loose object with the specified oid. @@ -131,10 +117,6 @@ int finalize_object_file_flags(struct repository *repo, void hash_object_file(const struct git_hash_algo *algo, const void *buf, size_t len, enum object_type type, struct object_id *oid); -int write_loose_object(struct odb_source_loose *loose, - const struct object_id *oid, char *hdr, - int hdrlen, const void *buf, unsigned long len, - const time_t *mtime, unsigned flags); /* Helper to check and "touch" a file */ int check_and_freshen_file(const char *fn, int freshen, @@ -195,4 +177,8 @@ int odb_transaction_files_begin(struct odb_source *source, struct odb_transaction **out, enum odb_transaction_flags flags); +int odb_transaction_files_prepare(struct odb_transaction *base); +void odb_transaction_files_fsync(struct odb_transaction *base, + int fd, const char *filename); + #endif /* OBJECT_FILE_H */ diff --git a/odb/source-loose.c b/odb/source-loose.c index 520a30157cf0d6..ef0e9192777c4a 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -11,8 +11,11 @@ #include "odb/source-loose.h" #include "odb/streaming.h" #include "oidtree.h" +#include "path.h" #include "repository.h" #include "strbuf.h" +#include "tempfile.h" +#include "write-or-die.h" static int append_loose_object(const struct object_id *oid, const char *path UNUSED, @@ -583,6 +586,241 @@ static int odb_source_loose_freshen_object(struct odb_source *source, return !!check_and_freshen_file(path.buf, 1, mtime); } +/* Finalize a file on disk, and close it. */ +static void close_loose_object(struct odb_source_loose *loose, + int fd, const char *filename) +{ + if (loose->base.will_destroy) + goto out; + + if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT)) + odb_transaction_files_fsync(loose->base.odb->transaction, fd, filename); + else if (fsync_object_files > 0) + fsync_or_die(fd, filename); + else + fsync_component_or_die(FSYNC_COMPONENT_LOOSE_OBJECT, fd, + filename); + +out: + if (close(fd) != 0) + die_errno(_("error when closing loose object file")); +} + +/* Size of directory component, including the ending '/' */ +static inline int directory_size(const char *filename) +{ + const char *s = strrchr(filename, '/'); + if (!s) + return 0; + return s - filename + 1; +} + +/* + * This creates a temporary file in the same directory as the final + * 'filename' + * + * We want to avoid cross-directory filename renames, because those + * can have problems on various filesystems (FAT, NFS, Coda). + */ +static int create_tmpfile(struct repository *repo, + struct strbuf *tmp, const char *filename) +{ + int fd, dirlen = directory_size(filename); + + strbuf_reset(tmp); + strbuf_add(tmp, filename, dirlen); + strbuf_addstr(tmp, "tmp_obj_XXXXXX"); + fd = git_mkstemp_mode(tmp->buf, 0444); + if (fd < 0 && dirlen && errno == ENOENT) { + /* + * Make sure the directory exists; note that the contents + * of the buffer are undefined after mkstemp returns an + * error, so we have to rewrite the whole buffer from + * scratch. + */ + strbuf_reset(tmp); + strbuf_add(tmp, filename, dirlen - 1); + if (mkdir(tmp->buf, 0777) && errno != EEXIST) + return -1; + if (adjust_shared_perm(repo, tmp->buf)) + return -1; + + /* Try again */ + strbuf_addstr(tmp, "/tmp_obj_XXXXXX"); + fd = git_mkstemp_mode(tmp->buf, 0444); + } + return fd; +} + +/** + * Common steps for loose object writers to start writing loose + * objects: + * + * - Create tmpfile for the loose object. + * - Setup zlib stream for compression. + * - Start to feed header to zlib stream. + * + * Returns a "fd", which should later be provided to + * end_loose_object_common(). + */ +static int start_loose_object_common(struct odb_source_loose *loose, + struct strbuf *tmp_file, + const char *filename, unsigned flags, + git_zstream *stream, + unsigned char *buf, size_t buflen, + struct git_hash_ctx *c, struct git_hash_ctx *compat_c, + char *hdr, int hdrlen) +{ + const struct git_hash_algo *algo = loose->base.odb->repo->hash_algo; + const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo; + int fd; + struct repo_config_values *cfg = repo_config_values(loose->base.odb->repo); + + fd = create_tmpfile(loose->base.odb->repo, tmp_file, filename); + if (fd < 0) { + if (flags & ODB_WRITE_OBJECT_SILENT) + return -1; + else if (errno == EACCES) + return error(_("insufficient permission for adding " + "an object to repository database %s"), + loose->base.path); + else + return error_errno( + _("unable to create temporary file")); + } + + /* Setup zlib stream for compression */ + git_deflate_init(stream, cfg->zlib_compression_level); + stream->next_out = buf; + stream->avail_out = buflen; + git_hash_init(c, algo); + if (compat && compat_c) + git_hash_init(compat_c, compat); + + /* Start to feed header to zlib stream */ + stream->next_in = (unsigned char *)hdr; + stream->avail_in = hdrlen; + while (git_deflate(stream, 0) == Z_OK) + ; /* nothing */ + git_hash_update(c, hdr, hdrlen); + if (compat && compat_c) + git_hash_update(compat_c, hdr, hdrlen); + + return fd; +} + +/** + * Common steps for the inner git_deflate() loop for writing loose + * objects. Returns what git_deflate() returns. + */ +static int write_loose_object_common(struct odb_source_loose *loose, + struct git_hash_ctx *c, struct git_hash_ctx *compat_c, + git_zstream *stream, const int flush, + unsigned char *in0, const int fd, + unsigned char *compressed, + const size_t compressed_len) +{ + const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo; + int ret; + + ret = git_deflate(stream, flush ? Z_FINISH : 0); + git_hash_update(c, in0, stream->next_in - in0); + if (compat && compat_c) + git_hash_update(compat_c, in0, stream->next_in - in0); + if (write_in_full(fd, compressed, stream->next_out - compressed) < 0) + die_errno(_("unable to write loose object file")); + stream->next_out = compressed; + stream->avail_out = compressed_len; + + return ret; +} + +/** + * Common steps for loose object writers to end writing loose objects: + * + * - End the compression of zlib stream. + * - Get the calculated oid to "oid". + */ +static int end_loose_object_common(struct odb_source_loose *loose, + struct git_hash_ctx *c, struct git_hash_ctx *compat_c, + git_zstream *stream, struct object_id *oid, + struct object_id *compat_oid) +{ + const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo; + int ret; + + ret = git_deflate_end_gently(stream); + if (ret != Z_OK) + return ret; + git_hash_final_oid(oid, c); + if (compat && compat_c) + git_hash_final_oid(compat_oid, compat_c); + + return Z_OK; +} + +static int write_loose_object(struct odb_source_loose *loose, + const struct object_id *oid, char *hdr, + int hdrlen, const void *buf, unsigned long len, + const time_t *mtime, unsigned flags) +{ + int fd, ret; + unsigned char compressed[4096]; + git_zstream stream; + struct git_hash_ctx c; + struct object_id parano_oid; + static struct strbuf tmp_file = STRBUF_INIT; + static struct strbuf filename = STRBUF_INIT; + + if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT)) + odb_transaction_files_prepare(loose->base.odb->transaction); + + odb_loose_path(loose, &filename, oid); + + fd = start_loose_object_common(loose, &tmp_file, filename.buf, flags, + &stream, compressed, sizeof(compressed), + &c, NULL, hdr, hdrlen); + if (fd < 0) + return -1; + + /* Then the data itself.. */ + stream.next_in = (void *)buf; + stream.avail_in = len; + do { + unsigned char *in0 = stream.next_in; + + ret = write_loose_object_common(loose, &c, NULL, &stream, 1, in0, fd, + compressed, sizeof(compressed)); + } while (ret == Z_OK); + + if (ret != Z_STREAM_END) + die(_("unable to deflate new object %s (%d)"), oid_to_hex(oid), + ret); + ret = end_loose_object_common(loose, &c, NULL, &stream, ¶no_oid, NULL); + if (ret != Z_OK) + die(_("deflateEnd on object %s failed (%d)"), oid_to_hex(oid), + ret); + if (!oideq(oid, ¶no_oid)) + die(_("confused by unstable object source data for %s"), + oid_to_hex(oid)); + + close_loose_object(loose, fd, tmp_file.buf); + + if (mtime) { + struct utimbuf utb = { + .actime = *mtime, + .modtime = *mtime, + }; + + if (utime(tmp_file.buf, &utb) < 0 && + !(flags & ODB_WRITE_OBJECT_SILENT)) + warning_errno(_("failed utime() on %s"), tmp_file.buf); + } + + return finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf, + FOF_SKIP_COLLISION_CHECK); +} + static int odb_source_loose_write_object(struct odb_source *source, const void *buf, size_t len, enum object_type type, @@ -611,12 +849,120 @@ static int odb_source_loose_write_object_stream(struct odb_source *source, size_t len, struct object_id *oid) { + struct odb_source_loose *loose = odb_source_loose_downcast(source); + const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo; + struct object_id compat_oid; + int fd, ret, err = 0, flush = 0; + unsigned char compressed[4096]; + git_zstream stream; + struct git_hash_ctx c, compat_c; + struct strbuf tmp_file = STRBUF_INIT; + struct strbuf filename = STRBUF_INIT; + unsigned char buf[8192]; + int dirlen; + char hdr[MAX_HEADER_LEN]; + int hdrlen; + + if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT)) + odb_transaction_files_prepare(loose->base.odb->transaction); + + /* Since oid is not determined, save tmp file to odb path. */ + strbuf_addf(&filename, "%s/", loose->base.path); + hdrlen = format_object_header(hdr, sizeof(hdr), OBJ_BLOB, len); + /* - * TODO: the implementation should be moved here, see the comment on - * the called function in "object-file.h". + * Common steps for write_loose_object and stream_loose_object to + * start writing loose objects: + * + * - Create tmpfile for the loose object. + * - Setup zlib stream for compression. + * - Start to feed header to zlib stream. */ - struct odb_source_loose *loose = odb_source_loose_downcast(source); - return odb_source_loose_write_stream(loose, in_stream, len, oid); + fd = start_loose_object_common(loose, &tmp_file, filename.buf, 0, + &stream, compressed, sizeof(compressed), + &c, &compat_c, hdr, hdrlen); + if (fd < 0) { + err = -1; + goto cleanup; + } + + /* Then the data itself.. */ + do { + unsigned char *in0 = stream.next_in; + + if (!stream.avail_in && !in_stream->is_finished) { + ssize_t read_len = odb_write_stream_read(in_stream, buf, + sizeof(buf)); + if (read_len < 0) { + close(fd); + err = -1; + goto cleanup; + } + + stream.avail_in = read_len; + stream.next_in = buf; + in0 = buf; + /* All data has been read. */ + if (in_stream->is_finished) + flush = 1; + } + ret = write_loose_object_common(loose, &c, &compat_c, &stream, flush, in0, fd, + compressed, sizeof(compressed)); + /* + * Unlike write_loose_object(), we do not have the entire + * buffer. If we get Z_BUF_ERROR due to too few input bytes, + * then we'll replenish them in the next input_stream->read() + * call when we loop. + */ + } while (ret == Z_OK || ret == Z_BUF_ERROR); + + if (stream.total_in != len + hdrlen) + die(_("write stream object %"PRIuMAX" != %"PRIuMAX), (uintmax_t)stream.total_in, + (uintmax_t)len + hdrlen); + + /* + * Common steps for write_loose_object and stream_loose_object to + * end writing loose object: + * + * - End the compression of zlib stream. + * - Get the calculated oid. + */ + if (ret != Z_STREAM_END) + die(_("unable to stream deflate new object (%d)"), ret); + ret = end_loose_object_common(loose, &c, &compat_c, &stream, oid, &compat_oid); + if (ret != Z_OK) + die(_("deflateEnd on stream object failed (%d)"), ret); + close_loose_object(loose, fd, tmp_file.buf); + + if (odb_freshen_object(loose->base.odb, oid)) { + unlink_or_warn(tmp_file.buf); + goto cleanup; + } + odb_loose_path(loose, &filename, oid); + + /* We finally know the object path, and create the missing dir. */ + dirlen = directory_size(filename.buf); + if (dirlen) { + struct strbuf dir = STRBUF_INIT; + strbuf_add(&dir, filename.buf, dirlen); + + if (safe_create_dir_in_gitdir(loose->base.odb->repo, dir.buf) && + errno != EEXIST) { + err = error_errno(_("unable to create directory %s"), dir.buf); + strbuf_release(&dir); + goto cleanup; + } + strbuf_release(&dir); + } + + err = finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf, + FOF_SKIP_COLLISION_CHECK); + if (!err && compat) + err = repo_add_loose_object_map(loose, oid, &compat_oid); +cleanup: + strbuf_release(&tmp_file); + strbuf_release(&filename); + return err; } static int odb_source_loose_begin_transaction(struct odb_source *source UNUSED, From 7b2648f7454e4d2bfbb78e303ec23d9997e4c985 Mon Sep 17 00:00:00 2001 From: Sahitya Chandra Date: Sat, 18 Jul 2026 13:44:49 +0530 Subject: [PATCH 074/280] 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 075/280] 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 076/280] 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 8817d7931aab41d4423fbf588f1ab2247070d816 Mon Sep 17 00:00:00 2001 From: Tian Yuchen Date: Mon, 20 Jul 2026 18:53:32 +0800 Subject: [PATCH 077/280] read-cache: remove redundant extern declarations The 'read-cache.c' file already includes 'environment.h', which provides the extern declarations for variables like 'trust_executable_bit' and 'has_symlinks'. Remove the redundant extern declarations inside 'st_mode_from_ce()' to clean up the code. Mentored-by: Christian Couder Mentored-by: Ayush Chandekar Mentored-by: Olamide Caleb Bello Signed-off-by: Tian Yuchen Signed-off-by: Junio C Hamano --- read-cache.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/read-cache.c b/read-cache.c index 21829102ae275e..1ced8c630ef637 100644 --- a/read-cache.c +++ b/read-cache.c @@ -205,8 +205,6 @@ void fill_stat_cache_info(struct index_state *istate, struct cache_entry *ce, st static unsigned int st_mode_from_ce(const struct cache_entry *ce) { - extern int trust_executable_bit, has_symlinks; - switch (ce->ce_mode & S_IFMT) { case S_IFLNK: return has_symlinks ? S_IFLNK : (S_IFREG | 0644); From b7b6e9e02f6b6b84848b2458d5d2b3d47260d4bb Mon Sep 17 00:00:00 2001 From: Tian Yuchen Date: Mon, 20 Jul 2026 18:53:33 +0800 Subject: [PATCH 078/280] read-cache: pass 'repo' to 'ce_mode_from_stat()' The ce_mode_from_stat() function is a performance-critical static inline helper in 'read-cache.h'. As we migrate configuration variables into the repository struct, this helper needs access to the repository context. Update the signature of ce_mode_from_stat() to take a 'struct repository *' parameter, and update all callers to pass the appropriate repository instance. To prepare for the overhead of replacing cheap global variable accesses with getter functions, the boolean expressions are reordered to evaluate 'S_ISREG(mode)' first. While at it, add a comment for ce_mode_from_stat(). Mentored-by: Christian Couder Mentored-by: Ayush Chandekar Mentored-by: Olamide Caleb Bello Signed-off-by: Tian Yuchen Signed-off-by: Junio C Hamano --- apply.c | 2 +- builtin/update-index.c | 2 +- diff-lib.c | 10 +++++----- read-cache.c | 2 +- read-cache.h | 15 ++++++++++++--- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/apply.c b/apply.c index 249248d4f205ca..26286eb57b4f52 100644 --- a/apply.c +++ b/apply.c @@ -3894,7 +3894,7 @@ static int check_preimage(struct apply_state *state, BUG("ce_mode == 0 for path '%s'", old_name); if (trust_executable_bit || !S_ISREG(st->st_mode)) - st_mode = ce_mode_from_stat(*ce, st->st_mode); + st_mode = ce_mode_from_stat(state->repo, *ce, st->st_mode); else if (*ce) st_mode = (*ce)->ce_mode; else diff --git a/builtin/update-index.c b/builtin/update-index.c index 3d6646c318b98e..afd672a9145b4f 100644 --- a/builtin/update-index.c +++ b/builtin/update-index.c @@ -294,7 +294,7 @@ static int add_one_path(const struct cache_entry *old, const char *path, int len ce->ce_flags = create_ce_flags(0); ce->ce_namelen = len; fill_stat_cache_info(the_repository->index, ce, st); - ce->ce_mode = ce_mode_from_stat(old, st->st_mode); + ce->ce_mode = ce_mode_from_stat(the_repository, old, st->st_mode); if (index_path(the_repository->index, &ce->oid, path, st, info_only ? 0 : INDEX_WRITE_OBJECT)) { diff --git a/diff-lib.c b/diff-lib.c index ae91027a024eec..46cae637ecda83 100644 --- a/diff-lib.c +++ b/diff-lib.c @@ -160,7 +160,7 @@ void run_diff_files(struct rev_info *revs, unsigned int option) changed = check_removed(ce, &st); if (!changed) - wt_mode = ce_mode_from_stat(ce, st.st_mode); + wt_mode = ce_mode_from_stat(revs->repo, ce, st.st_mode); else { if (changed < 0) { perror(ce->name); @@ -193,7 +193,7 @@ void run_diff_files(struct rev_info *revs, unsigned int option) num_compare_stages++; oidcpy(&dpath->parent[stage - 2].oid, &nce->oid); - dpath->parent[stage-2].mode = ce_mode_from_stat(nce, mode); + dpath->parent[stage-2].mode = ce_mode_from_stat(revs->repo, nce, mode); dpath->parent[stage-2].status = DIFF_STATUS_MODIFIED; } @@ -262,7 +262,7 @@ void run_diff_files(struct rev_info *revs, unsigned int option) continue; } else if (revs->diffopt.ita_invisible_in_index && ce_intent_to_add(ce)) { - newmode = ce_mode_from_stat(ce, st.st_mode); + newmode = ce_mode_from_stat(revs->repo, ce, st.st_mode); diff_addremove(&revs->diffopt, '+', newmode, null_oid(the_hash_algo), 0, ce->name, 0); continue; @@ -270,7 +270,7 @@ void run_diff_files(struct rev_info *revs, unsigned int option) changed = match_stat_with_submodule(&revs->diffopt, ce, &st, ce_option, &dirty_submodule); - newmode = ce_mode_from_stat(ce, st.st_mode); + newmode = ce_mode_from_stat(revs->repo, ce, st.st_mode); } if (!changed && !dirty_submodule) { @@ -338,7 +338,7 @@ static int get_stat_data(const struct cache_entry *ce, changed = match_stat_with_submodule(diffopt, ce, &st, 0, dirty_submodule); if (changed) { - mode = ce_mode_from_stat(ce, st.st_mode); + mode = ce_mode_from_stat(diffopt->repo, ce, st.st_mode); oid = null_oid(the_hash_algo); } } diff --git a/read-cache.c b/read-cache.c index 1ced8c630ef637..a9f059174305eb 100644 --- a/read-cache.c +++ b/read-cache.c @@ -750,7 +750,7 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st, int pos = index_name_pos_also_unmerged(istate, path, namelen); ent = (0 <= pos) ? istate->cache[pos] : NULL; - ce->ce_mode = ce_mode_from_stat(ent, st_mode); + ce->ce_mode = ce_mode_from_stat(istate->repo, ent, st_mode); } /* When core.ignorecase=true, determine if a directory of the same name but differing diff --git a/read-cache.h b/read-cache.h index 043da1f1aae706..af8c657ecbeb48 100644 --- a/read-cache.h +++ b/read-cache.h @@ -4,15 +4,24 @@ #include "read-cache-ll.h" #include "object.h" #include "pathspec.h" +#include "environment.h" -static inline unsigned int ce_mode_from_stat(const struct cache_entry *ce, +/* + * Determine the appropriate index mode for a file based on its stat() + * information and the existing cache entry (if any). + * + * This function handles degradation for filesystems that lack + * symlink support or reliable executable bits. + */ +static inline unsigned int ce_mode_from_stat(struct repository *repo UNUSED, + const struct cache_entry *ce, unsigned int mode) { extern int trust_executable_bit, has_symlinks; - if (!has_symlinks && S_ISREG(mode) && + if (S_ISREG(mode) && !has_symlinks && ce && S_ISLNK(ce->ce_mode)) return ce->ce_mode; - if (!trust_executable_bit && S_ISREG(mode)) { + if (S_ISREG(mode) && !trust_executable_bit) { if (ce && S_ISREG(ce->ce_mode)) return ce->ce_mode; return create_ce_mode(0666); From 99c79dabb09a298863cd96398305d2957e60d022 Mon Sep 17 00:00:00 2001 From: Tian Yuchen Date: Mon, 20 Jul 2026 18:53:34 +0800 Subject: [PATCH 079/280] environment: move trust_executable_bit into repo_config_values Move the global 'trust_executable_bit' configuration into the repository-specific 'repo_config_values' struct. To ensure code readability, the getter function 'repo_trust_executable_bit()' has been introduced. Callers access this configuration by passing in 'repo' when possible, and explicitly fall back to 'the_repository' the rest of time. Mentored-by: Christian Couder Mentored-by: Ayush Chandekar Mentored-by: Olamide Caleb Bello Signed-off-by: Tian Yuchen Signed-off-by: Junio C Hamano --- apply.c | 2 +- environment.c | 11 +++++++++-- environment.h | 4 +++- read-cache.c | 6 +++--- read-cache.h | 6 +++--- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/apply.c b/apply.c index 26286eb57b4f52..edb1502414e4be 100644 --- a/apply.c +++ b/apply.c @@ -3893,7 +3893,7 @@ static int check_preimage(struct apply_state *state, if (*ce && !(*ce)->ce_mode) BUG("ce_mode == 0 for path '%s'", old_name); - if (trust_executable_bit || !S_ISREG(st->st_mode)) + if (repo_trust_executable_bit(state->repo) || !S_ISREG(st->st_mode)) st_mode = ce_mode_from_stat(state->repo, *ce, st->st_mode); else if (*ce) st_mode = (*ce)->ce_mode; diff --git a/environment.c b/environment.c index fc3ed8bb1c7a66..32b110c40505be 100644 --- a/environment.c +++ b/environment.c @@ -41,7 +41,6 @@ static int pack_compression_seen; static int zlib_compression_seen; -int trust_executable_bit = 1; int trust_ctime = 1; int check_stat = 1; int has_symlinks = 1; @@ -142,6 +141,13 @@ int is_bare_repository(void) return is_bare_repository_cfg && !repo_get_work_tree(the_repository); } +int repo_trust_executable_bit(struct repository *repo) +{ + return repo->initialized + ? repo_config_values(repo)->trust_executable_bit + : 1; +} + int have_git_dir(void) { return startup_info->have_repository @@ -305,7 +311,7 @@ int git_default_core_config(const char *var, const char *value, /* This needs a better name */ if (!strcmp(var, "core.filemode")) { - trust_executable_bit = git_config_bool(var, value); + cfg->trust_executable_bit = git_config_bool(var, value); return 0; } if (!strcmp(var, "core.trustctime")) { @@ -720,5 +726,6 @@ void repo_config_values_init(struct repo_config_values *cfg) { cfg->attributes_file = NULL; cfg->apply_sparse_checkout = 0; + cfg->trust_executable_bit = 1; cfg->branch_track = BRANCH_TRACK_REMOTE; } diff --git a/environment.h b/environment.h index 9eb97b3869c9b1..c15456fc0d0b39 100644 --- a/environment.h +++ b/environment.h @@ -91,6 +91,7 @@ struct repo_config_values { /* section "core" config values */ char *attributes_file; int apply_sparse_checkout; + int trust_executable_bit; /* section "branch" config values */ enum branch_track branch_track; @@ -123,6 +124,8 @@ int git_default_config(const char *, const char *, int git_default_core_config(const char *var, const char *value, const struct config_context *ctx, void *cb); +int repo_trust_executable_bit(struct repository *repo); + void repo_config_values_init(struct repo_config_values *cfg); /* @@ -158,7 +161,6 @@ int is_bare_repository(void); extern char *git_work_tree_cfg; /* Environment bits from configuration mechanism */ -extern int trust_executable_bit; extern int trust_ctime; extern int check_stat; extern int has_symlinks; diff --git a/read-cache.c b/read-cache.c index a9f059174305eb..90789ff049edd0 100644 --- a/read-cache.c +++ b/read-cache.c @@ -209,7 +209,7 @@ static unsigned int st_mode_from_ce(const struct cache_entry *ce) case S_IFLNK: return has_symlinks ? S_IFLNK : (S_IFREG | 0644); case S_IFREG: - return (ce->ce_mode & (trust_executable_bit ? 0755 : 0644)) | S_IFREG; + return (ce->ce_mode & (repo_trust_executable_bit(the_repository) ? 0755 : 0644)) | S_IFREG; case S_IFGITLINK: return S_IFDIR | 0755; case S_IFDIR: @@ -319,7 +319,7 @@ static int ce_match_stat_basic(const struct cache_entry *ce, struct stat *st) /* We consider only the owner x bit to be relevant for * "mode changes" */ - if (trust_executable_bit && + if (repo_trust_executable_bit(the_repository) && (0100 & (ce->ce_mode ^ st->st_mode))) changed |= MODE_CHANGED; break; @@ -740,7 +740,7 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st, ce->ce_flags |= CE_INTENT_TO_ADD; - if (trust_executable_bit && has_symlinks) { + if (repo_trust_executable_bit(istate->repo) && has_symlinks) { ce->ce_mode = create_ce_mode(st_mode); } else { /* If there is an existing entry, pick the mode bits and type diff --git a/read-cache.h b/read-cache.h index af8c657ecbeb48..4b54cfc57c8de0 100644 --- a/read-cache.h +++ b/read-cache.h @@ -13,15 +13,15 @@ * This function handles degradation for filesystems that lack * symlink support or reliable executable bits. */ -static inline unsigned int ce_mode_from_stat(struct repository *repo UNUSED, +static inline unsigned int ce_mode_from_stat(struct repository *repo, const struct cache_entry *ce, unsigned int mode) { - extern int trust_executable_bit, has_symlinks; + extern int has_symlinks; if (S_ISREG(mode) && !has_symlinks && ce && S_ISLNK(ce->ce_mode)) return ce->ce_mode; - if (S_ISREG(mode) && !trust_executable_bit) { + if (S_ISREG(mode) && !repo_trust_executable_bit(repo)) { if (ce && S_ISREG(ce->ce_mode)) return ce->ce_mode; return create_ce_mode(0666); From df2bc04e6937ba35b9a905a65dd0137627b46c8b Mon Sep 17 00:00:00 2001 From: Tian Yuchen Date: Mon, 20 Jul 2026 18:53:35 +0800 Subject: [PATCH 080/280] environment: move has_symlinks into repo_config_values Move the global 'has_symlinks' configuration into the repository-specific 'repo_config_values' struct. Introduce 'repo_has_symlinks()' getter for readability. Callers access this configuration by passing in 'repo' when possible, and explicitly fall back to 'the_repository' the rest of the time. Introduce 'platform_has_symlinks()' macro to allow platform specific-customization, primarily to help MinGW. Platforms can override this in their respective headers. Mentored-by: Christian Couder Mentored-by: Ayush Chandekar Mentored-by: Olamide Caleb Bello Signed-off-by: Tian Yuchen Signed-off-by: Junio C Hamano --- apply.c | 2 +- combine-diff.c | 2 +- compat/mingw.c | 17 +++++++++++++---- compat/mingw.h | 3 +++ entry.c | 3 ++- environment.c | 12 ++++++++++-- environment.h | 4 +++- git-compat-util.h | 4 ++++ read-cache.c | 7 ++++--- read-cache.h | 3 +-- 10 files changed, 42 insertions(+), 15 deletions(-) diff --git a/apply.c b/apply.c index edb1502414e4be..b748192ee2c1f6 100644 --- a/apply.c +++ b/apply.c @@ -4511,7 +4511,7 @@ static int try_create_file(struct apply_state *state, const char *path, return !!mkdir(path, 0777); } - if (has_symlinks && S_ISLNK(mode)) + if (repo_has_symlinks(state->repo) && S_ISLNK(mode)) /* Although buf:size is counted string, it also is NUL * terminated. */ diff --git a/combine-diff.c b/combine-diff.c index b7998620687ed7..80e5c46e9b26ff 100644 --- a/combine-diff.c +++ b/combine-diff.c @@ -1078,7 +1078,7 @@ static void show_patch_diff(struct combine_diff_path *elem, int num_parent, /* if symlinks don't work, assume symlink if all parents * are symlinks */ - is_file = has_symlinks; + is_file = repo_has_symlinks(rev->repo); for (i = 0; !is_file && i < num_parent; i++) is_file = !S_ISLNK(elem->parent[i].mode); if (!is_file) diff --git a/compat/mingw.c b/compat/mingw.c index aa7525f419cb64..47819119292d8e 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -7,6 +7,7 @@ #include "config.h" #include "dir.h" #include "environment.h" +#include "repository.h" #include "gettext.h" #include "run-command.h" #include "strbuf.h" @@ -1043,7 +1044,7 @@ int mingw_chdir(const char *dirname) if (xutftowcs_path(wdirname, dirname) < 0) return -1; - if (has_symlinks) { + if (repo_has_symlinks(the_repository)) { HANDLE hnd = CreateFileW(wdirname, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); @@ -2903,7 +2904,7 @@ int symlink(const char *target, const char *link) int len; /* fail if symlinks are disabled or API is not supported (WinXP) */ - if (!has_symlinks) { + if (!repo_has_symlinks(the_repository)) { errno = ENOSYS; return -1; } @@ -3173,15 +3174,23 @@ static void setup_windows_environment(void) if (!tmp && (tmp = getenv("USERPROFILE"))) setenv("HOME", tmp, 1); } +} +int mingw_platform_has_symlinks(void) +{ + static int has_symlinks = -1; /* * Change 'core.symlinks' default to false, unless native symlinks are * enabled in MSys2 (via 'MSYS=winsymlinks:nativestrict'). Thus we can * run the test suite (which doesn't obey config files) with or without * symlink support. */ - if (!(tmp = getenv("MSYS")) || !strstr(tmp, "winsymlinks:nativestrict")) - has_symlinks = 0; + if (has_symlinks < 0) { + const char *tmp = getenv("MSYS"); + has_symlinks = (tmp && strstr(tmp, "winsymlinks:nativestrict")) ? 1 : 0; + } + + return has_symlinks; } static void get_current_user_sid(PSID *sid, HANDLE *linked_token) diff --git a/compat/mingw.h b/compat/mingw.h index 444daedfa52469..df02aeb632d8c3 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -208,6 +208,9 @@ void open_in_gdb(void); */ int err_win_to_posix(DWORD winerr); +int mingw_platform_has_symlinks(void); +#define platform_has_symlinks() mingw_platform_has_symlinks() + #ifndef NO_UNIX_SOCKETS int mingw_have_unix_sockets(void); #undef have_unix_sockets diff --git a/entry.c b/entry.c index 7817aee362ed9e..5913a8b51f1895 100644 --- a/entry.c +++ b/entry.c @@ -321,7 +321,8 @@ static int write_entry(struct cache_entry *ce, char *path, struct conv_attrs *ca * We can't make a real symlink; write out a regular file entry * with the symlink destination as its contents. */ - if (!has_symlinks || to_tempfile) + if (!repo_has_symlinks(state->istate && state->istate->repo ? + state->istate->repo : the_repository) || to_tempfile) goto write_file_entry; ret = symlink(new_blob, path); diff --git a/environment.c b/environment.c index 32b110c40505be..e351043446001d 100644 --- a/environment.c +++ b/environment.c @@ -43,7 +43,6 @@ static int zlib_compression_seen; int trust_ctime = 1; int check_stat = 1; -int has_symlinks = 1; int minimum_abbrev = 4, default_abbrev = -1; int ignore_case; int assume_unchanged; @@ -148,6 +147,13 @@ int repo_trust_executable_bit(struct repository *repo) : 1; } +int repo_has_symlinks(struct repository *repo) +{ + return repo->initialized + ? repo_config_values(repo)->has_symlinks + : platform_has_symlinks(); +} + int have_git_dir(void) { return startup_info->have_repository @@ -336,7 +342,8 @@ int git_default_core_config(const char *var, const char *value, } if (!strcmp(var, "core.symlinks")) { - has_symlinks = git_config_bool(var, value); + struct repo_config_values *cfg = repo_config_values(the_repository); + cfg->has_symlinks = git_config_bool(var, value); return 0; } @@ -727,5 +734,6 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->attributes_file = NULL; cfg->apply_sparse_checkout = 0; cfg->trust_executable_bit = 1; + cfg->has_symlinks = platform_has_symlinks(); cfg->branch_track = BRANCH_TRACK_REMOTE; } diff --git a/environment.h b/environment.h index c15456fc0d0b39..8f54c481e92a06 100644 --- a/environment.h +++ b/environment.h @@ -92,6 +92,7 @@ struct repo_config_values { char *attributes_file; int apply_sparse_checkout; int trust_executable_bit; + int has_symlinks; /* section "branch" config values */ enum branch_track branch_track; @@ -126,6 +127,8 @@ int git_default_core_config(const char *var, const char *value, int repo_trust_executable_bit(struct repository *repo); +int repo_has_symlinks(struct repository *repo); + void repo_config_values_init(struct repo_config_values *cfg); /* @@ -163,7 +166,6 @@ extern char *git_work_tree_cfg; /* Environment bits from configuration mechanism */ extern int trust_ctime; extern int check_stat; -extern int has_symlinks; extern int minimum_abbrev, default_abbrev; extern int ignore_case; extern int assume_unchanged; diff --git a/git-compat-util.h b/git-compat-util.h index 88097764078538..a0f901ce793ed6 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -245,6 +245,10 @@ static inline int git_is_dir_sep(int c) #define is_dir_sep git_is_dir_sep #endif +#ifndef platform_has_symlinks +#define platform_has_symlinks() 1 +#endif + #ifndef offset_1st_component static inline int git_offset_1st_component(const char *path) { diff --git a/read-cache.c b/read-cache.c index 90789ff049edd0..046b1e649e7552 100644 --- a/read-cache.c +++ b/read-cache.c @@ -207,7 +207,7 @@ static unsigned int st_mode_from_ce(const struct cache_entry *ce) { switch (ce->ce_mode & S_IFMT) { case S_IFLNK: - return has_symlinks ? S_IFLNK : (S_IFREG | 0644); + return repo_has_symlinks(the_repository) ? S_IFLNK : (S_IFREG | 0644); case S_IFREG: return (ce->ce_mode & (repo_trust_executable_bit(the_repository) ? 0755 : 0644)) | S_IFREG; case S_IFGITLINK: @@ -325,7 +325,7 @@ static int ce_match_stat_basic(const struct cache_entry *ce, struct stat *st) break; case S_IFLNK: if (!S_ISLNK(st->st_mode) && - (has_symlinks || !S_ISREG(st->st_mode))) + (repo_has_symlinks(the_repository) || !S_ISREG(st->st_mode))) changed |= TYPE_CHANGED; break; case S_IFGITLINK: @@ -740,7 +740,8 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st, ce->ce_flags |= CE_INTENT_TO_ADD; - if (repo_trust_executable_bit(istate->repo) && has_symlinks) { + if (repo_trust_executable_bit(istate->repo) && + repo_has_symlinks(istate->repo)) { ce->ce_mode = create_ce_mode(st_mode); } else { /* If there is an existing entry, pick the mode bits and type diff --git a/read-cache.h b/read-cache.h index 4b54cfc57c8de0..ab9d40aa81f95e 100644 --- a/read-cache.h +++ b/read-cache.h @@ -17,8 +17,7 @@ static inline unsigned int ce_mode_from_stat(struct repository *repo, const struct cache_entry *ce, unsigned int mode) { - extern int has_symlinks; - if (S_ISREG(mode) && !has_symlinks && + if (S_ISREG(mode) && !repo_has_symlinks(repo) && ce && S_ISLNK(ce->ce_mode)) return ce->ce_mode; if (S_ISREG(mode) && !repo_trust_executable_bit(repo)) { From dd674df3267112248b0012ce614168055a416920 Mon Sep 17 00:00:00 2001 From: Ted Nyman Date: Mon, 20 Jul 2026 15:31:20 -0700 Subject: [PATCH 081/280] pathspec: use match for sparse-index expansion checks The pathspec parser computes `len` and `nowildcard_len` from `item.match`, which includes any prefix added when a command is run from a subdirectory. `item.original` can still contain the shorter, unprefixed argument. Using `item.original + item.nowildcard_len` in `pathspec_needs_expanded_index()` can therefore read past the end of the allocation. AddressSanitizer reports a heap-buffer-overflow for prefixed wildcard pathspecs passed to `git rm` and `git reset` with a sparse index. The mismatch dates back to 4d1cfc1351 ("reset: make --mixed sparse-aware", 2021-11-29), which introduced the helper using `item.original`. b29ad38322 ("pathspec.h: move pathspec_needs_expanded_index() from reset.c to here", 2022-08-07) later moved it to `pathspec.c` and preserved the affected comparisons. Use `item.match` consistently when checking whether a pathspec can match a sparse-directory entry. Add coverage for prefixed wildcard pathspecs so both commands keep the index sparse. Signed-off-by: Ted Nyman Reviewed-by: Taylor Blau Signed-off-by: Junio C Hamano --- pathspec.c | 12 ++++++------ t/t1092-sparse-checkout-compatibility.sh | 7 +++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/pathspec.c b/pathspec.c index f78b22709ccb67..281858f21f9c59 100644 --- a/pathspec.c +++ b/pathspec.c @@ -847,9 +847,9 @@ int pathspec_needs_expanded_index(struct index_state *istate, * - not-in-cone/bar*: may need expanded index * - **.c: may need expanded index */ - if (strspn(item.original + item.nowildcard_len, "*") == + if (strspn(item.match + item.nowildcard_len, "*") == (unsigned int)(item.len - item.nowildcard_len) && - path_in_cone_mode_sparse_checkout(item.original, istate)) + path_in_cone_mode_sparse_checkout(item.match, istate)) continue; for (pos = 0; pos < istate->cache_nr; pos++) { @@ -865,7 +865,7 @@ int pathspec_needs_expanded_index(struct index_state *istate, */ if ((unsigned int)item.nowildcard_len > ce_namelen(ce) && - !strncmp(item.original, ce->name, + !strncmp(item.match, ce->name, ce_namelen(ce))) { res = 1; break; @@ -876,13 +876,13 @@ int pathspec_needs_expanded_index(struct index_state *istate, * directory and the pathspec does not match the whole * directory, need to expand the index. */ - if (!strncmp(item.original, ce->name, item.nowildcard_len) && - wildmatch(item.original, ce->name, 0)) { + if (!strncmp(item.match, ce->name, item.nowildcard_len) && + wildmatch(item.match, ce->name, 0)) { res = 1; break; } } - } else if (!path_in_cone_mode_sparse_checkout(item.original, istate) && + } else if (!path_in_cone_mode_sparse_checkout(item.match, istate) && !matches_skip_worktree(pathspec, i, &skip_worktree_seen)) res = 1; diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh index 8186da5c887c56..d4a9c0bbf2d0fd 100755 --- a/t/t1092-sparse-checkout-compatibility.sh +++ b/t/t1092-sparse-checkout-compatibility.sh @@ -2119,6 +2119,13 @@ test_expect_success 'sparse index is not expanded: rm' ' ensure_not_expanded rm -r deep ' +test_expect_success 'sparse index is not expanded: prefixed wildcard pathspec' ' + init_repos && + + ensure_not_expanded -C deep rm --dry-run -- "a*" && + ensure_not_expanded -C deep reset base -- "a*" +' + test_expect_success 'grep with and --cached' ' init_repos && From 1be6ebe264031dc2175fec50c36df25fafd25084 Mon Sep 17 00:00:00 2001 From: Ted Nyman Date: Mon, 20 Jul 2026 15:31:21 -0700 Subject: [PATCH 082/280] stash: avoid sparse-index expansion for in-cone paths `git stash push -- ` expands a sparse index before checking whether the pathspec matches any tracked paths. This is unnecessary when the pathspec is wholly inside the sparse-checkout cone and makes a path-limited stash proportional to the size of the full index. Use `pathspec_needs_expanded_index()` to expand only when a pathspec can match part of a sparse-directory entry, as `git rm` and `git reset` already do. Keep the full-index behavior for pathspecs that need it. Add compatibility coverage for literal, prefixed, wildcard, file, multiple, staged, and missing pathspecs. Add the corresponding path-limited stash case to p2000. On a cone-mode repository with 349,525 tracked paths and 49 sparse index entries, the best of three runs changed from 18.87s to 0.06s. Trace2 reported four index expansions before this change and none after it. Signed-off-by: Ted Nyman Reviewed-by: Taylor Blau Signed-off-by: Junio C Hamano --- builtin/stash.c | 4 +- t/perf/p2000-sparse-operations.sh | 1 + t/t1092-sparse-checkout-compatibility.sh | 55 ++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/builtin/stash.c b/builtin/stash.c index c4809f299a313b..72c52571f8c06c 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -1702,8 +1702,8 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q if (!include_untracked && ps->nr) { char *ps_matched = xcalloc(ps->nr, 1); - /* TODO: audit for interaction with sparse-index. */ - ensure_full_index(the_repository->index); + if (pathspec_needs_expanded_index(the_repository->index, ps)) + ensure_full_index(the_repository->index); for (size_t i = 0; i < the_repository->index->cache_nr; i++) ce_path_match(the_repository->index, the_repository->index->cache[i], ps, ps_matched); diff --git a/t/perf/p2000-sparse-operations.sh b/t/perf/p2000-sparse-operations.sh index aadf22bc2f0bb2..548a61cd9064bc 100755 --- a/t/perf/p2000-sparse-operations.sh +++ b/t/perf/p2000-sparse-operations.sh @@ -108,6 +108,7 @@ test_perf_on_all () { test_perf_on_all git status test_perf_on_all 'git stash && git stash pop' +test_perf_on_all "git stash push -- $SPARSE_CONE/a && git stash pop" test_perf_on_all 'echo >>new && git stash -u && git stash pop' test_perf_on_all git add -A test_perf_on_all git add . diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh index d4a9c0bbf2d0fd..c3f7bfe551a247 100755 --- a/t/t1092-sparse-checkout-compatibility.sh +++ b/t/t1092-sparse-checkout-compatibility.sh @@ -1598,6 +1598,61 @@ test_expect_success 'sparse-index is not expanded: stash' ' ensure_not_expanded stash pop ' +test_expect_success 'sparse-index is not expanded: stash in-cone pathspec' ' + init_repos && + + echo unrelated >>sparse-index/deep/e && + echo literal >>sparse-index/deep/a && + ensure_not_expanded stash push -- deep/a && + test_grep ! literal sparse-index/deep/a && + test_grep unrelated sparse-index/deep/e && + ensure_not_expanded stash pop && + test_grep literal sparse-index/deep/a && + + echo prefixed >>sparse-index/deep/a && + ensure_not_expanded -C deep stash push -- a && + test_grep ! prefixed sparse-index/deep/a && + test_grep unrelated sparse-index/deep/e && + ensure_not_expanded stash pop && + test_grep prefixed sparse-index/deep/a && + + echo wildcard >>sparse-index/deep/a && + ensure_not_expanded stash push -- "deep/a*" && + test_grep ! wildcard sparse-index/deep/a && + test_grep unrelated sparse-index/deep/e && + ensure_not_expanded stash pop && + test_grep wildcard sparse-index/deep/a && + + echo pathspec-file >>sparse-index/deep/a && + echo deep/a >pathspec-file && + ensure_not_expanded stash push --pathspec-from-file=../pathspec-file && + test_grep ! pathspec-file sparse-index/deep/a && + test_grep unrelated sparse-index/deep/e && + ensure_not_expanded stash pop && + test_grep pathspec-file sparse-index/deep/a && + + echo multiple-a >>sparse-index/deep/a && + echo multiple-e >>sparse-index/deep/e && + ensure_not_expanded stash push -- deep/a deep/e && + test_grep ! multiple-a sparse-index/deep/a && + test_grep ! multiple-e sparse-index/deep/e && + ensure_not_expanded stash pop && + test_grep multiple-a sparse-index/deep/a && + test_grep multiple-e sparse-index/deep/e && + + echo staged >>sparse-index/deep/a && + git -C sparse-index add deep/a && + ensure_not_expanded stash push --staged -- deep/a && + test_grep ! staged sparse-index/deep/a && + test_grep unrelated sparse-index/deep/e && + ensure_not_expanded stash pop --index && + test_grep staged sparse-index/deep/a && + test_must_fail git -C sparse-index diff --cached --quiet -- deep/a && + + ensure_not_expanded ! stash push -- deep/does-not-exist && + test_grep "did not match any file" sparse-index-error +' + test_expect_success 'describe tested on all' ' init_repos && From 8391f01768864373d27cf7b1267ea69d54675cf6 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Wed, 22 Jul 2026 18:08:57 +0000 Subject: [PATCH 083/280] remote: pass repository to push tracking helper The next commit needs tracking_for_push_dest() to inspect the repository's configured remotes. Pass the repository through the existing callers and mark the new parameter as unused. No change in behavior. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- remote.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/remote.c b/remote.c index b17648d6ef32e4..0dc36956c346c0 100644 --- a/remote.c +++ b/remote.c @@ -1887,7 +1887,8 @@ const char *branch_get_upstream(struct branch *branch, struct strbuf *err) return branch->merge[0]->dst; } -static char *tracking_for_push_dest(struct remote *remote, +static char *tracking_for_push_dest(struct repository *repo UNUSED, + struct remote *remote, const char *refname, struct strbuf *err) { @@ -1925,13 +1926,13 @@ static char *branch_get_push_1(struct repository *repo, _("push refspecs for '%s' do not include '%s'"), remote->name, branch->name); - ret = tracking_for_push_dest(remote, dst, err); + ret = tracking_for_push_dest(repo, remote, dst, err); free(dst); return ret; } if (remote->mirror) - return tracking_for_push_dest(remote, branch->refname, err); + return tracking_for_push_dest(repo, remote, branch->refname, err); switch (push_default) { case PUSH_DEFAULT_NOTHING: @@ -1939,7 +1940,7 @@ static char *branch_get_push_1(struct repository *repo, case PUSH_DEFAULT_MATCHING: case PUSH_DEFAULT_CURRENT: - return tracking_for_push_dest(remote, branch->refname, err); + return tracking_for_push_dest(repo, remote, branch->refname, err); case PUSH_DEFAULT_UPSTREAM: return xstrdup_or_null(branch_get_upstream(branch, err)); @@ -1953,7 +1954,7 @@ static char *branch_get_push_1(struct repository *repo, up = branch_get_upstream(branch, err); if (!up) return NULL; - cur = tracking_for_push_dest(remote, branch->refname, err); + cur = tracking_for_push_dest(repo, remote, branch->refname, err); if (!cur) return NULL; if (strcmp(cur, up)) { From 93775e35b7ed7983a8aaa33a566f45a8a1152718 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Wed, 22 Jul 2026 18:08:58 +0000 Subject: [PATCH 084/280] remote: find tracking branches for URL push destinations Git accepts a repository URL as branch..pushRemote and can push to it. This branch setting takes precedence over remote.pushDefault. A branch can be configured with a URL-valued pushRemote before any push occurs. If the remotes are later rearranged with "git remote rename" and "git remote add", the newly added remote may use that URL. The URL value is unaffected by the rename and continues to take precedence over remote.pushDefault. The URL and the remote then point to the same repository, but Git does not connect them for tracking. Pushing works, but @{push} cannot identify the remote's tracking branch. As a result, "git status" cannot show the push branch, and an up-to-date push can leave its tracking information stale. When exactly one configured remote would push to the same URL, use that remote for push tracking. Continue to push to the URL so the configured remote's push settings do not change existing behavior. Keep the current behavior when no remote matches or multiple remotes match. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- Documentation/config/branch.adoc | 1 + Documentation/revisions.adoc | 3 + remote.c | 45 +++++++++- remote.h | 2 + t/t5505-remote.sh | 144 +++++++++++++++++++++++++++++++ transport.c | 5 +- 6 files changed, 198 insertions(+), 2 deletions(-) diff --git a/Documentation/config/branch.adoc b/Documentation/config/branch.adoc index a4db9fa5c87eab..5a85fde8de9286 100644 --- a/Documentation/config/branch.adoc +++ b/Documentation/config/branch.adoc @@ -55,6 +55,7 @@ This option defaults to `never`. repository), you would want to set `remote.pushDefault` to specify the remote to push to for all branches, and use this option to override it for a specific branch. + The value may be the name of a configured remote or a repository URL. `branch..merge`:: Defines, together with `branch..remote`, the upstream branch diff --git a/Documentation/revisions.adoc b/Documentation/revisions.adoc index 6ea6c7cead1647..3fbfbd3d5fc091 100644 --- a/Documentation/revisions.adoc +++ b/Documentation/revisions.adoc @@ -127,6 +127,9 @@ some output processing may assume ref names in UTF-8. `git push` were run while `branchname` was checked out (or the current `HEAD` if no branchname is specified). Like for '@\{upstream\}', we report the remote-tracking branch that corresponds to that branch at the remote. + If the push destination is a URL and exactly one configured remote uses + that URL for pushing, '@\{push}' reports that remote's remote-tracking + branch. + Here's an example to make it more clear: + diff --git a/remote.c b/remote.c index 0dc36956c346c0..3a6abf12589999 100644 --- a/remote.c +++ b/remote.c @@ -954,6 +954,17 @@ struct strvec *push_url_of_remote(struct remote *remote) return remote->pushurl.nr ? &remote->pushurl : &remote->url; } +static bool remote_has_push_url(struct remote *remote, const char *url) +{ + const struct strvec *push_urls = push_url_of_remote(remote); + + for (size_t i = 0; i < push_urls->nr; i++) { + if (!strcmp(push_urls->v[i], url)) + return true; + } + return false; +} + void ref_push_report_free(struct ref_push_report *report) { while (report) { @@ -1887,13 +1898,45 @@ const char *branch_get_upstream(struct branch *branch, struct strbuf *err) return branch->merge[0]->dst; } -static char *tracking_for_push_dest(struct repository *repo UNUSED, +struct remote *repo_remote_for_push_tracking(struct repository *repo, + struct remote *remote) +{ + const struct strvec *push_urls; + struct remote *first_match = NULL; + struct remote_state *remote_state = repo->remote_state; + const char *check_url; + + if (remote->origin != REMOTE_UNCONFIGURED) + return remote; + + push_urls = push_url_of_remote(remote); + if (push_urls->nr != 1) + return remote; + check_url = push_urls->v[0]; + + for (int i = 0; i < remote_state->remotes_nr; i++) { + struct remote *candidate = remote_state->remotes[i]; + + if (!candidate || candidate == remote || + !remote_is_configured(candidate, 0) || + !remote_has_push_url(candidate, check_url)) + continue; + if (first_match) + return remote; + first_match = candidate; + } + + return first_match ? first_match : remote; +} + +static char *tracking_for_push_dest(struct repository *repo, struct remote *remote, const char *refname, struct strbuf *err) { char *ret; + remote = repo_remote_for_push_tracking(repo, remote); ret = apply_refspecs(&remote->fetch, refname); if (!ret) return error_buf(err, diff --git a/remote.h b/remote.h index 72a54d84ad511d..cca02033b9d73b 100644 --- a/remote.h +++ b/remote.h @@ -345,6 +345,8 @@ char *remote_ref_for_branch(struct branch *branch, int for_push); const char *repo_default_remote(struct repository *repo); const char *repo_remote_from_url(struct repository *repo, const char *url); +struct remote *repo_remote_for_push_tracking(struct repository *repo, + struct remote *remote); /* returns true if the given branch has merge configuration given. */ int branch_has_merge_config(struct branch *branch); diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh index 6f5e86dedeb209..9c2f140d5abe42 100755 --- a/t/t5505-remote.sh +++ b/t/t5505-remote.sh @@ -24,6 +24,28 @@ setup_repository () { ) } +setup_url_pushremote () { + rm -rf fork.git client && + git clone --bare one fork.git && + git clone one client && + fork_url="file://$TRASH_DIRECTORY/fork.git" && + ( + cd client && + git checkout -b topic --track origin/main && + git commit --allow-empty -m topic-change && + git config push.default current && + git config status.compareBranches "@{upstream} @{push}" && + git config branch.topic.pushRemote "$fork_url" && + git push + ) +} + +check_status () { + git -C client status >actual && + cat >expected && + test_cmp expected actual +} + tokens_match () { echo "$1" | tr ' ' '\012' | sort | sed -e '/^$/d' >expect && echo "$2" | tr ' ' '\012' | sort | sed -e '/^$/d' >actual && @@ -1018,6 +1040,128 @@ test_expect_success 'rename a remote renames repo remote.pushDefault but keeps g ) ' +test_expect_success 'URL-valued pushRemote without matching remote is not trackable' ' + setup_url_pushremote && + + check_status <<-EOF + On branch topic + Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit. + (use "git push" to publish your local commits) + + nothing to commit, working tree clean + EOF +' + +test_expect_success 'adding matching remote makes URL-valued pushRemote trackable' ' + setup_url_pushremote && + + ( + cd client && + git remote rename origin upstream && + git remote add -f origin "$fork_url" + ) && + + check_status <<-EOF + On branch topic + Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit. + + Your branch is up to date with ${SQ}origin/topic${SQ}. + + nothing to commit, working tree clean + EOF +' + +test_expect_success 'configured pushurl makes URL-valued pushRemote trackable' ' + setup_url_pushremote && + + ( + cd client && + git remote rename origin upstream && + git remote add -f origin ../fork.git && + git remote set-url --push origin "$fork_url" + ) && + + check_status <<-EOF + On branch topic + Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit. + + Your branch is up to date with ${SQ}origin/topic${SQ}. + + nothing to commit, working tree clean + EOF +' + +test_expect_success 'pushInsteadOf URL pushRemote is trackable' ' + setup_url_pushremote && + ( + cd client && + git remote rename origin upstream && + git remote add -f origin "$fork_url" && + git config "url.$fork_url.pushInsteadOf" fork: && + git config branch.topic.pushRemote fork: + ) && + + check_status <<-EOF + On branch topic + Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit. + + Your branch is up to date with ${SQ}origin/topic${SQ}. + + nothing to commit, working tree clean + EOF +' + +test_expect_success 'up-to-date URL push refreshes stale tracking branch' ' + setup_url_pushremote && + ( + cd client && + git remote rename origin upstream && + git remote add -f origin "$fork_url" && + git commit --allow-empty -m another-topic-change && + git -C ../fork.git fetch ../client topic:topic + ) && + + check_status <<-EOF && + On branch topic + Your branch is ahead of ${SQ}upstream/main${SQ} by 2 commits. + + Your branch is ahead of ${SQ}origin/topic${SQ} by 1 commit. + (use "git push" to publish your local commits) + + nothing to commit, working tree clean + EOF + + git -C client push >actual 2>&1 && + test_grep "Everything up-to-date" actual && + + check_status <<-EOF + On branch topic + Your branch is ahead of ${SQ}upstream/main${SQ} by 2 commits. + + Your branch is up to date with ${SQ}origin/topic${SQ}. + + nothing to commit, working tree clean + EOF +' + +test_expect_success 'duplicate remote URL leaves URL-valued pushRemote ambiguous' ' + setup_url_pushremote && + ( + cd client && + git remote rename origin upstream && + git remote add -f origin "$fork_url" && + git remote add duplicate "$fork_url" + ) && + + check_status <<-EOF + On branch topic + Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit. + (use "git push" to publish your local commits) + + nothing to commit, working tree clean + EOF +' + test_expect_success 'rename handles remote without fetch refspec' ' git clone --bare one no-refspec.git && # confirm assumption that bare clone does not create refspec diff --git a/transport.c b/transport.c index fc144f0aedabd9..30a4ab2cd53b9f 100644 --- a/transport.c +++ b/transport.c @@ -1553,8 +1553,11 @@ int transport_push(struct repository *r, if (!(flags & (TRANSPORT_PUSH_DRY_RUN | TRANSPORT_RECURSE_SUBMODULES_ONLY))) { struct ref *ref; + struct remote *tracking_remote = repo_remote_for_push_tracking( + r, transport->remote); + for (ref = remote_refs; ref; ref = ref->next) - transport_update_tracking_ref(transport->remote, ref, verbose); + transport_update_tracking_ref(tracking_remote, ref, verbose); } if (porcelain && !push_ret) From 47382f7398df0c8509c30aac6aafa75272099ca5 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 22 Jul 2026 10:03:13 -0700 Subject: [PATCH 085/280] revision: honor --exclude-first-parent-only with SEEN first parent The '--exclude-first-parent-only' option instructs the revision walker to follow only the first parent of a merge commit to propagate down the UNINTERESTING bit. However, if the first parent has already been marked SEEN (for example, because it was explicitly specified on the command line), process_parents() skips it with a 'continue' statement. But the loop then continues on to process the second parent, because the check for the '--exclude-first-parent-only' option is near the end of the loop, which the 'continue' statement skips. Consequently, we end up marking the second parent as UNINTERESTING. Break out of the loop instead of continuing when the first parent is already SEEN or fails to parse. This ensures that we do not process subsequent parents and mark them as UNINTERESTING. Signed-off-by: Junio C Hamano Reviewed-by: Jerry Zhang Signed-off-by: Junio C Hamano --- revision.c | 10 ++++++++-- t/t6012-rev-list-simplify.sh | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/revision.c b/revision.c index 599b3a66c369ca..9b306636697242 100644 --- a/revision.c +++ b/revision.c @@ -1152,12 +1152,18 @@ static int process_parents(struct rev_info *revs, struct commit *commit, if (p) p->object.flags |= UNINTERESTING | CHILD_VISITED; - if (repo_parse_commit_gently(revs->repo, p, 1) < 0) + if (repo_parse_commit_gently(revs->repo, p, 1) < 0) { + if (revs->exclude_first_parent_only) + break; continue; + } if (p->parents) mark_parents_uninteresting(revs, p); - if (p->object.flags & SEEN) + if (p->object.flags & SEEN) { + if (revs->exclude_first_parent_only) + break; continue; + } p->object.flags |= (SEEN | NOT_USER_GIVEN); if (list) commit_list_insert_by_date(p, list); diff --git a/t/t6012-rev-list-simplify.sh b/t/t6012-rev-list-simplify.sh index 4cecb6224cb464..2284bbba12c726 100755 --- a/t/t6012-rev-list-simplify.sh +++ b/t/t6012-rev-list-simplify.sh @@ -285,4 +285,22 @@ test_expect_success 'log --graph --simplify-merges --show-pulls' ' test_cmp expect actual ' +test_expect_success 'exclude-first-parent-only with parent already seen' ' + git checkout --orphan test-seen && + git rm -rf . && + test_commit r1 && + git checkout -b branch-f && + test_commit f && + git checkout test-seen && + git merge --no-ff --no-edit -m r2 branch-f && + git tag r2 && + + git rev-list --exclude-first-parent-only f ^r2 >actual && + git rev-parse f >expect && + test_cmp expect actual && + + git rev-list --exclude-first-parent-only f r1 ^r2 >actual2 && + test_cmp expect actual2 +' + test_done From 2dbd1c85e12d3329bda20fa2ff049d3083522237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20NICOLAS?= Date: Thu, 23 Jul 2026 02:21:32 +0200 Subject: [PATCH 086/280] submodule: resolve insteadOf aliases when matching remote MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When ca62f524c1 (submodule: look up remotes by URL first, 2025-06-23) introduced a mechanism to identify which remote is to be used by a submodule, it compared the URL stored in the .gitmodules inventory to that of each available remote. The URLs of remotes are rewritten according to url..insteadOf, whereas those stored in the .gitmodules aren't. When such aliasing applies, no match can be made between the two corresponding sides, and the procedure degrades to its fallback logic electing either the only configured remote if there is only one, or "origin" otherwise. That behaviour is unfortunate when no remote is called "origin", because its last resort will have a submodule update command look for a non-existent remote-tracking reference and fail to proceed, instead of using the remote whose rewritten URL matches. Resolve the alias in the URL inventoried in .gitmodules before comparing it against those of the corresponding submodule's configured remotes. Signed-off-by: Éric NICOLAS Signed-off-by: Junio C Hamano --- remote.c | 14 +++++++++++--- t/t7406-submodule-update.sh | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/remote.c b/remote.c index b17648d6ef32e4..b1fed58e79e35e 100644 --- a/remote.c +++ b/remote.c @@ -1821,17 +1821,25 @@ const char *repo_default_remote(struct repository *repo) const char *repo_remote_from_url(struct repository *repo, const char *url) { + char *rewritten_url; + const char *remote_name = NULL; + read_config(repo, 0); + if ((rewritten_url = alias_url(url, &repo->remote_state->rewrites))) + url = rewritten_url; for (int i = 0; i < repo->remote_state->remotes_nr; i++) { struct remote *remote = repo->remote_state->remotes[i]; if (!remote) continue; - if (remote_has_url(remote, url)) - return remote->name; + if (remote_has_url(remote, url)) { + remote_name = remote->name; + break; + } } - return NULL; + free(rewritten_url); + return remote_name; } int branch_has_merge_config(struct branch *branch) diff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh index 9554720152f6f0..10adeabf0fcc85 100755 --- a/t/t7406-submodule-update.sh +++ b/t/t7406-submodule-update.sh @@ -256,6 +256,25 @@ test_expect_success 'submodule update --remote should fetch upstream changes' ' ) ' +test_expect_success 'submodule update --remote resolves URL rewrites' ' + test_config_global "url.$(pwd)/.insteadOf" local: && + mkdir alias-super alias-submodule && + ( + cd alias-submodule && + git init && + git commit --allow-empty --message "Initial commit" + ) && + ( + cd alias-super && + git init && + git submodule add local:alias-submodule submodule && + git submodule update --force && + git -C submodule remote rename origin upstream && + git -C submodule remote add fork user@host && + git submodule update --remote + ) +' + test_expect_success 'submodule update --remote should fetch upstream changes with .' ' ( cd super && From 113d62b141a80bf136268df51b4597dbec4355d0 Mon Sep 17 00:00:00 2001 From: Shlok Kulshreshtha Date: Tue, 21 Jul 2026 12:27:36 +0530 Subject: [PATCH 087/280] userdiff: add support for Swift Add a built-in userdiff driver for the Swift programming language so that diff hunk headers and word diffs work out of the box for ".swift" files. The funcname pattern is built for Swift's own declaration grammar: an optional run of attributes ("@objc", "@available(iOS 13, *)", ...), followed by an optional run of lowercase modifiers ("public", "static", "final", ...), followed by a declaration keyword (func, class, struct, enum, protocol, extension, actor, init, deinit, subscript). The keyword is followed by a boundary that allows whitespace, "(" (init/subscript), "?" or "!" (failable init), or "<" (generics), while still acting as a word boundary so e.g. "initialize(" does not match. The word regex recognizes Swift identifiers, hexadecimal, octal, binary, integer and floating-point literals, and the language's operators. Signed-off-by: Shlok Kulshreshtha Acked-by: Johannes Sixt Signed-off-by: Junio C Hamano --- Documentation/gitattributes.adoc | 2 ++ t/t4018/swift-actor | 5 +++++ t/t4018/swift-attribute-with-args | 7 +++++++ t/t4018/swift-class | 5 +++++ t/t4018/swift-enum | 5 +++++ t/t4018/swift-extension | 5 +++++ t/t4018/swift-failable-init | 7 +++++++ t/t4018/swift-func | 5 +++++ t/t4018/swift-generic-subscript | 7 +++++++ t/t4018/swift-init | 7 +++++++ t/t4018/swift-inline-attribute | 7 +++++++ t/t4018/swift-modifiers | 4 ++++ t/t4018/swift-protocol | 5 +++++ t/t4018/swift-struct | 5 +++++ userdiff.c | 10 ++++++++++ 15 files changed, 86 insertions(+) create mode 100644 t/t4018/swift-actor create mode 100644 t/t4018/swift-attribute-with-args create mode 100644 t/t4018/swift-class create mode 100644 t/t4018/swift-enum create mode 100644 t/t4018/swift-extension create mode 100644 t/t4018/swift-failable-init create mode 100644 t/t4018/swift-func create mode 100644 t/t4018/swift-generic-subscript create mode 100644 t/t4018/swift-init create mode 100644 t/t4018/swift-inline-attribute create mode 100644 t/t4018/swift-modifiers create mode 100644 t/t4018/swift-protocol create mode 100644 t/t4018/swift-struct diff --git a/Documentation/gitattributes.adoc b/Documentation/gitattributes.adoc index bd76167a45eb71..9fea75f96f928f 100644 --- a/Documentation/gitattributes.adoc +++ b/Documentation/gitattributes.adoc @@ -914,6 +914,8 @@ patterns are available: - `scheme` suitable for source code in most Lisp dialects, including Scheme, Emacs Lisp, Common Lisp, and Clojure. +- `swift` suitable for source code in the Swift language. + - `tex` suitable for source code for LaTeX documents. diff --git a/t/t4018/swift-actor b/t/t4018/swift-actor new file mode 100644 index 00000000000000..e4852f40a7ebd8 --- /dev/null +++ b/t/t4018/swift-actor @@ -0,0 +1,5 @@ +actor RIGHT { + let a = 1 + // a comment + let b = ChangeMe +} diff --git a/t/t4018/swift-attribute-with-args b/t/t4018/swift-attribute-with-args new file mode 100644 index 00000000000000..22b1ee32f110bd --- /dev/null +++ b/t/t4018/swift-attribute-with-args @@ -0,0 +1,7 @@ +struct View { + @available(iOS 13, *) public func RIGHT() { + let a = 1 + // a comment + print(ChangeMe) + } +} diff --git a/t/t4018/swift-class b/t/t4018/swift-class new file mode 100644 index 00000000000000..c3a93360277f81 --- /dev/null +++ b/t/t4018/swift-class @@ -0,0 +1,5 @@ +class RIGHT { + let a = 1 + // a comment + let b = ChangeMe +} diff --git a/t/t4018/swift-enum b/t/t4018/swift-enum new file mode 100644 index 00000000000000..0a843029930409 --- /dev/null +++ b/t/t4018/swift-enum @@ -0,0 +1,5 @@ +enum RIGHT { + case first + // a comment + case ChangeMe +} diff --git a/t/t4018/swift-extension b/t/t4018/swift-extension new file mode 100644 index 00000000000000..cbc18ab6ef42c8 --- /dev/null +++ b/t/t4018/swift-extension @@ -0,0 +1,5 @@ +extension RIGHT { + static let a = 1 + // a comment + static let b = ChangeMe +} diff --git a/t/t4018/swift-failable-init b/t/t4018/swift-failable-init new file mode 100644 index 00000000000000..4bbd6217c9147a --- /dev/null +++ b/t/t4018/swift-failable-init @@ -0,0 +1,7 @@ +class Bar { + init?(RIGHT: Int) { + let x = 0 + // a comment + print(ChangeMe) + } +} diff --git a/t/t4018/swift-func b/t/t4018/swift-func new file mode 100644 index 00000000000000..1fecae0911d411 --- /dev/null +++ b/t/t4018/swift-func @@ -0,0 +1,5 @@ +func RIGHT(x: Int) -> Int { + let y = x + // a comment + return ChangeMe +} diff --git a/t/t4018/swift-generic-subscript b/t/t4018/swift-generic-subscript new file mode 100644 index 00000000000000..423cb589412ea5 --- /dev/null +++ b/t/t4018/swift-generic-subscript @@ -0,0 +1,7 @@ +struct Container { + subscript(index: Int) -> Int { + let a = 0 + // a comment + return ChangeMe + } +} diff --git a/t/t4018/swift-init b/t/t4018/swift-init new file mode 100644 index 00000000000000..dc7a298f3806b5 --- /dev/null +++ b/t/t4018/swift-init @@ -0,0 +1,7 @@ +class Foo { + init(RIGHT: Int) { + let x = 0 + // a comment + print(ChangeMe) + } +} diff --git a/t/t4018/swift-inline-attribute b/t/t4018/swift-inline-attribute new file mode 100644 index 00000000000000..2374c4b6036efa --- /dev/null +++ b/t/t4018/swift-inline-attribute @@ -0,0 +1,7 @@ +class Service { + @objc func RIGHT() { + let path = "/api" + // a comment + log(ChangeMe) + } +} diff --git a/t/t4018/swift-modifiers b/t/t4018/swift-modifiers new file mode 100644 index 00000000000000..9d80685a786bea --- /dev/null +++ b/t/t4018/swift-modifiers @@ -0,0 +1,4 @@ +public static func RIGHT() -> Int { + // a comment + return ChangeMe +} diff --git a/t/t4018/swift-protocol b/t/t4018/swift-protocol new file mode 100644 index 00000000000000..07c39ec2a3ca89 --- /dev/null +++ b/t/t4018/swift-protocol @@ -0,0 +1,5 @@ +protocol RIGHT { + var first: Int { get } + // a comment + var second: ChangeMe { get } +} diff --git a/t/t4018/swift-struct b/t/t4018/swift-struct new file mode 100644 index 00000000000000..e399ed77597656 --- /dev/null +++ b/t/t4018/swift-struct @@ -0,0 +1,5 @@ +struct RIGHT { + let a = 1 + // a comment + let b = ChangeMe +} diff --git a/userdiff.c b/userdiff.c index b5412e6bc3ecd3..7129bf148266b7 100644 --- a/userdiff.c +++ b/userdiff.c @@ -362,6 +362,16 @@ PATTERNS("scheme", "\\|([^|\\\\]|\\\\.)*\\|" /* All other words should be delimited by spaces or parentheses. */ "|([^][)(}{ \t])+"), +PATTERNS("swift", + "^[ \t]*((@[A-Za-z_][A-Za-z0-9_]*(\\([^()]*\\))?[ \t]+)*([a-z]+[ \t]+)*(func|init|deinit|subscript|class|struct|enum|protocol|extension|actor)[ \t(?!<].*)$", + /* -- */ + "[a-zA-Z_][a-zA-Z0-9_]*" + /* hexadecimal, octal, and binary literals */ + "|0[xX][0-9a-fA-F_]+|0[oO][0-7_]+|0[bB][01_]+" + /* integers and floating-point numbers */ + "|[0-9][0-9_]*([.][0-9_]+)?([eE][-+]?[0-9]+)?" + /* unary and binary operators */ + "|[-+*/%<>=!&|^~?]=|&&|\\|\\||<<=?|>>=?|\\?\\?|\\.\\.[.<]|->"), PATTERNS("tex", "^(\\\\((sub)*section|chapter|part)\\*{0,1}\\{.*)$", "\\\\[a-zA-Z@]+|\\\\.|([a-zA-Z0-9]|[^\x01-\x7f])+"), { .name = "default", .binary = -1 }, From b71b9d79cf2217cb55d050c231c2a0bbaf2882be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= Date: Thu, 23 Jul 2026 21:19:23 +0000 Subject: [PATCH 088/280] doc: convert git-imap-send synopsis and options to new style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert git-imap-send from [verse]/single-quote style to the modern synopsis-block style: - Replace [verse] with [synopsis] in SYNOPSIS block - Backtick-quote all OPTIONS terms - Backtick-quote all config keys in config/imap.adoc - Backtick-quote bare config key references in prose Signed-off-by: Jean-Noël Avila Signed-off-by: Junio C Hamano --- Documentation/config/imap.adoc | 2 +- Documentation/git-imap-send.adoc | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Documentation/config/imap.adoc b/Documentation/config/imap.adoc index cb8f5e2700ae13..6b97776bc3ab47 100644 --- a/Documentation/config/imap.adoc +++ b/Documentation/config/imap.adoc @@ -41,7 +41,7 @@ `imap.authMethod`:: Specify the authentication method for authenticating with the IMAP server. If Git was built with the NO_CURL option, or if your curl version is older - than 7.34.0, or if you're running git-imap-send with the `--no-curl` + than 7.34.0, or if you're running `git-imap-send` with the `--no-curl` option, the only supported methods are `PLAIN`, `CRAM-MD5`, `OAUTHBEARER` and `XOAUTH2`. If this is not set then `git imap-send` uses the basic IMAP plaintext `LOGIN` command. diff --git a/Documentation/git-imap-send.adoc b/Documentation/git-imap-send.adoc index 538b91afc06dde..1814d94491f750 100644 --- a/Documentation/git-imap-send.adoc +++ b/Documentation/git-imap-send.adoc @@ -24,9 +24,9 @@ that order. Typical usage is something like: ------- +---- $ git format-patch --signoff --stdout --attach origin | git imap-send ------- +---- OPTIONS @@ -138,13 +138,13 @@ have set up) may not be verified correctly. Using Gmail's IMAP interface: ---------- +---- [imap] folder = "[Gmail]/Drafts" host = imaps://imap.gmail.com user = user@gmail.com port = 993 ---------- +---- Gmail does not allow using your regular password for `git imap-send`. If you have multi-factor authentication set up on your Gmail account, you @@ -167,32 +167,34 @@ than using app-specific passwords, and also does not enforce the need of having multi-factor authentication. You will have to use an OAuth2.0 access token in place of your password when using this authentication. ---------- +---- [imap] folder = "[Gmail]/Drafts" host = imaps://imap.gmail.com user = user@gmail.com port = 993 authmethod = OAUTHBEARER ---------- +---- Using Outlook's IMAP interface: Unlike Gmail, Outlook only supports OAuth2.0 based authentication. Also, it supports only `XOAUTH2` as the mechanism. ---------- +---- [imap] folder = "Drafts" host = imaps://outlook.office365.com user = user@outlook.com port = 993 authmethod = XOAUTH2 ---------- +---- Once the commits are ready to be sent, run the following command: - $ git format-patch --cover-letter -M --stdout origin/master | git imap-send +---- +$ git format-patch --cover-letter -M --stdout origin/master | git imap-send +---- Just make sure to disable line wrapping in the email client (Gmail's web interface will wrap lines no matter what, so you need to use a real @@ -217,7 +219,7 @@ users may wish to visit this web page for more information: SEE ALSO -------- -linkgit:git-format-patch[1], linkgit:git-send-email[1], mbox(5) +linkgit:git-format-patch[1], linkgit:git-send-email[1], `mbox`(5) GIT --- From e9a99ef2dafecf03adc00229a6d5ceb10a8f3058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= Date: Thu, 23 Jul 2026 21:19:24 +0000 Subject: [PATCH 089/280] doc: convert git-format-patch synopsis and options to new style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace [verse] with [synopsis] in the SYNOPSIS block and remove single-quote formatting from the command name. Backtick-quote all option terms in the OPTIONS section, convert standalone placeholders to __ form, and convert single-quoted commands and tools in prose to backtick form. Also update the included files: - diff-options.adoc: backtick-quote the git-format-patch-specific option terms (-p, --no-stat, --max-depth=) - format-patch-caveats.adoc: convert patch(1) to `patch`(1) Signed-off-by: Jean-Noël Avila Signed-off-by: Junio C Hamano --- Documentation/diff-options.adoc | 8 +- Documentation/git-format-patch.adoc | 289 ++++++++++++++-------------- 2 files changed, 148 insertions(+), 149 deletions(-) diff --git a/Documentation/diff-options.adoc b/Documentation/diff-options.adoc index c8242e24627eef..e605d2867801d0 100644 --- a/Documentation/diff-options.adoc +++ b/Documentation/diff-options.adoc @@ -13,8 +13,8 @@ endif::git-diff[] endif::git-format-patch[] ifdef::git-format-patch[] --p:: ---no-stat:: +`-p`:: +`--no-stat`:: Generate plain patches without any diffstats. endif::git-format-patch[] @@ -893,8 +893,8 @@ endif::git-format-patch[] reverted with `--ita-visible-in-index`. Both options are experimental and could be removed in future. ---max-depth=:: - For each pathspec given on command line, descend at most `` +`--max-depth=`:: + For each pathspec given on command line, descend at most __ levels of directories. A value of `-1` means no limit. Cannot be combined with wildcards in the pathspec. Given a tree containing `foo/bar/baz`, the following list shows the diff --git a/Documentation/git-format-patch.adoc b/Documentation/git-format-patch.adoc index f7905c0f7c0322..191f64b77d1758 100644 --- a/Documentation/git-format-patch.adoc +++ b/Documentation/git-format-patch.adoc @@ -8,8 +8,8 @@ git-format-patch - Prepare patches for e-mail submission SYNOPSIS -------- -[verse] -'git format-patch' [-k] [(-o|--output-directory) | --stdout] +[synopsis] +git format-patch [-k] [(-o|--output-directory) | --stdout] [--no-thread | --thread[=