* Re: [PATCH v2 1/2] branch: suggest <remote>/<branch> on upstream slip
From: Junio C Hamano @ 2026-06-25 21:16 UTC (permalink / raw)
To: Harald Nordgren; +Cc: Harald Nordgren via GitGitGadget, git
In-Reply-To: <CAHwyqnXZ_eGUPOhq1hXs==uYuYbRBWw120fXRQa=apWKekxVAQ@mail.gmail.com>
Harald Nordgren <haraldnordgren@gmail.com> writes:
>> Do we still need the _if_enabled() thing here? Isn't the caller
>> gated with the same condition in this version?
>>
>> > + strbuf_release(&remote_ref);
>> > + exit(code);
>> > +}
>> > +
>> > int cmd_branch(int argc,
>> > const char **argv,
>> > const char *prefix,
>> > @@ -957,6 +980,9 @@ int cmd_branch(int argc,
>> > if (!refs_ref_exists(get_main_ref_store(the_repository), branch->refname)) {
>> > if (!argc || branch_checked_out(branch->refname))
>> > die(_("no commit on branch '%s' yet"), branch->name);
>> > + if (argc == 1 &&
>> > + advice_enabled(ADVICE_SET_UPSTREAM_FAILURE))
>> > + die_if_upstream_looks_like_remote(new_upstream, argv[0]);
>> > die(_("branch '%s' does not exist"), branch->name);
>> > }
>
> I think we do, so it will give the advice and tell the user that it
> can be disabled in the standard format.
I was hoping that unconditional advise() should be sufficient, but
the caller there needs to say if_enabled, even though it _knows_
that it is enabled, only to give the turn-off instructions.
I wonder if future readers would be confused just like I was,
without a comment on the callsite of _if_enabled() added by this
patch?
Thanks.
^ permalink raw reply
* What's cooking in git.git (Jun 2026, #10)
From: Junio C Hamano @ 2026-06-26 0:30 UTC (permalink / raw)
To: git
Here are the topics that have been cooking in my tree. Commits
prefixed with '+' are in 'next' (being in 'next' is a sign that a
topic is stable enough to be used and is a candidate to be in a
future release). Commits prefixed with '-' are only in 'seen', and
aren't considered "accepted" at all and may be annotated with a URL
to a message that raises issues but they are by no means exhaustive.
A topic without enough support may be discarded after a long period
of no activity (of course they can be resubmitted when new interests
arise).
Git 2.55-rc2 has been tagged. The tree is in deep feature-freeze,
and remaining topics in 'next' will stay in "Will cook in 'next'"
instead of "Will merge to 'master'" state, until Git 2.55 final.
Copies of the source code to Git live in many repositories, and the
following is a list of the ones I push into or their mirrors. Some
repositories have only a subset of branches.
With maint, master, next, seen, todo:
git://git.kernel.org/pub/scm/git/git.git/
git://repo.or.cz/alt-git.git/
https://kernel.googlesource.com/pub/scm/git/git/
https://github.com/git/git/
https://gitlab.com/git-scm/git/
With all the integration branches and topics broken out:
https://github.com/gitster/git/
Even though the preformatted documentation in HTML and man format
are not sources, they are published in these repositories for
convenience (replace "htmldocs" with "manpages" for the manual
pages):
git://git.kernel.org/pub/scm/git/git-htmldocs.git/
https://github.com/gitster/git-htmldocs.git/
Release tarballs are available at:
https://www.kernel.org/pub/software/scm/git/
--------------------------------------------------
[New Topics]
* ad/gpg-strip-cr-before-lf (2026-06-24) 1 commit
- gpg-interface: fix strip_cr_before_lf to only remove CR before LF
The GPG and SSH signature parsing code has been corrected to strip
carriage return characters only when they immediately precede line
feeds, instead of unconditionally stripping all carriage returns.
Needs review.
source: <20260624093618.17456-1-antonio.destefani08@gmail.com>
* jt/receive-pack-use-odb-transactions (2026-06-23) 6 commits
- builtin/receive-pack: stage incoming objects via ODB transactions
- odb/transaction: add transaction env interface
- odb/transaction: propagate commit errors
- odb/transaction: propagate begin errors
- object-file: propagate files transaction errors
- object-file: rename files transaction prepare function
git-receive-pack has been refactored to use ODB transaction
interfaces instead of directly managing tmp_objdir for staging
incoming objects, bringing it closer to being ODB backend agnostic.
Waiting for response(s) to review comment(s).
cf. <aju_AmlKVi5UZaiQ@pks.im>
source: <20260624041920.2601961-1-jltobler@gmail.com>
* ps/odb-drop-whence (2026-06-24) 7 commits
- odb: document object info fields
- odb: drop `whence` field from object info
- treewide: convert users of `whence` to the new source field
- odb: add `source` field to struct object_info_source
- odb: make backend-specific fields optional
- packfile: thread odb_source_packed through packed_object_info()
- Merge branch 'ps/odb-source-packed' into ps/odb-drop-whence
(this branch uses ps/odb-source-packed.)
The whence field in struct object_info has been removed,
refactoring backend-specific object information retrieval into an
opt-in struct object_info_source structure.
Needs review.
source: <20260624-b4-pks-odb-drop-whence-v1-0-8d1877b790ac@pks.im>
* ps/reftable-hardening (2026-06-24) 11 commits
- reftable/table: fix OOB read on truncated table
- reftable/table: fix NULL pointer access when seeking to bogus offsets
- reftable/block: fix OOB read with bogus restart offset
- reftable/block: fix use of uninitialized memory when binsearch fails
- reftable/block: fix OOB read with bogus restart count
- reftable/block: fix OOB read with bogus block size
- reftable/block: fix OOB write with bogus inflated log size
- reftable/record: don't abort when decoding invalid ref value type
- reftable/basics: fix OOB read on binary search of empty range
- oss-fuzz: add fuzzer for parsing reftables
- meson: support building fuzzers with libFuzzer
The reftable code has been hardened against corrupted tables by
fixing out-of-bounds writes, out-of-bounds reads, and abort calls
during parsing.
Needs review.
source: <20260624-pks-reftable-hardening-v1-0-66e4ce87c6b9@pks.im>
* hn/branch-push-slip-advice (2026-06-24) 3 commits
- SQUASH??? use test_grep
- push: suggest <remote> <branch> for a slash slip
- branch: suggest <remote>/<branch> on upstream slip
"git push origin/main" and "git branch origin main" could both be
an obvious typo, in which case offer the obvious typofix.
Waiting for response(s) to review comment(s).
cf. <xmqqfr2ae2wp.fsf@gitster.g>
source: <pull.2331.v2.git.git.1782338114.gitgitgadget@gmail.com>
* jc/history-message-prep-fix (2026-06-25) 1 commit
- history: streamline message preparation and plug file stream leak
Code clean-up with leakfix for a write file stream.
Needs review.
source: <xmqqh5mqfkpv.fsf@gitster.g>
* ty/migrate-excludes-file (2026-06-25) 2 commits
- environment: move excludes_file into repo_config_values
- dir: encapsulate excludes_file lazy-load
Move excludes_file global variable into per-repository structure.
Waiting for response(s) to review comment(s).
cf. <xmqqwlvme4lz.fsf@gitster.g>
source: <20260625161845.7543-1-cat@malon.dev>
--------------------------------------------------
[Stalled]
* jt/config-lock-timeout (2026-05-17) 1 commit
- config: retry acquiring config.lock, configurable via core.configLockTimeout
Configuration file locking now retries for a short period, avoiding
failures when multiple processes attempt to update the configuration
simultaneously.
Waiting for response(s) to review comment(s) for too long, stalled.
cf. <agrIrGwSMFlKTx9x@pks.im>
source: <20260517132111.1014901-1-joerg@thalheim.io>
* js/parseopt-subcommand-autocorrection (2026-04-27) 11 commits
- SQUASH???
- doc: document autocorrect API
- parseopt: add tests for subcommand autocorrection
- parseopt: enable subcommand autocorrection for git-remote and git-notes
- parseopt: autocorrect mistyped subcommands
- autocorrect: provide config resolution API
- autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINT
- autocorrect: use mode and delay instead of magic numbers
- help: move tty check for autocorrection to autocorrect.c
- help: make autocorrect handling reusable
- parseopt: extract subcommand handling from parse_options_step()
The parse-options library learned to auto-correct misspelled
subcommand names.
Waiting for response(s) to review comment(s) for too long, stalled.
cf. <xmqq33yzd9yf.fsf@gitster.g>
cf. <SY0P300MB0801E50FCB7EB2F45CD15208CE042@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM>
source: <SY0P300MB0801677A2A1E0FD38D06A841CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM>
* cl/conditional-config-on-worktree-path (2026-05-24) 2 commits
- config: add "worktree" and "worktree/i" includeIf conditions
- config: refactor include_by_gitdir() into include_by_path()
The [includeIf "condition"] conditional inclusion facility for
configuration files has learned to use the location of worktree
in its condition.
Waiting for response(s) to review comment(s) for too long, stalled.
cf. <xmqq8q97et9b.fsf@gitster.g>
source: <20260525-includeif-worktree-v5-0-1efe525d025a@black-desk.cn>
--------------------------------------------------
[Cooking]
* kk/merge-base-exhaustion (2026-06-24) 7 commits
- commit-reach: terminate merge-base walk when one paint side is exhausted
- commit-reach: remove unused nonstale_queue dedup wrappers
- commit-reach: introduce struct paint_state with per-side counters
- commit-reach: add trace2 instrumentation to paint_down_to_common()
- t6099, t6600: add side-exhaustion regression tests
- t6600: add test cases for side-exhaustion edge cases
- Documentation/technical: add paint-down-to-common doc
The merge-base computation has been optimized by stopping the walk
early when one side's exclusive commits in the queue are exhausted,
yielding significant speedups for queries with one-sided histories.
Expecting a reroll.
cf. <CAL71e4MnA36ZchLaUsMSoLcb9LO77aac274jES8+oV=yxuigOA@mail.gmail.com>
source: <pull.2149.v2.git.1782303254.gitgitgadget@gmail.com>
* dk/meson-enable-use-nsec-build (2026-06-20) 1 commit
- meson: wire up USE_NSEC build knob
The USE_NSEC build knob, which enables support for sub-second file
timestamp resolution, has been wired up to the Meson build system.
Waiting for response(s) to review comment(s).
cf. <ajjuoS5Qc3K0nCRl@pks.im>
source: <c4c5ade901ff95b0f95939ea818870e4f3d59da1.1781971201.git.ben.knoble+github@gmail.com>
* ps/connected-generic-promisor-checks (2026-06-25) 5 commits
- connected: search promisor objects generically
- connected: split out promisor-based connectivity check
- odb/source-packed: support flags when iterating an object prefix
- odb/source-packed: extract logic to skip certain packs
- Merge branch 'ps/odb-source-packed' into ps/connected-generic-promisor-checks
(this branch uses ps/odb-source-packed.)
The connectivity check has been refactored to search for promisor
objects in a generic way using the object database interface,
rather than iterating packfiles directly. This allows connectivity
checks to work properly in repositories that do not use packfiles.
Will merge to 'next'?
cf. <xmqq4iiqfk0l.fsf@gitster.g>
source: <20260625-pks-connected-generic-promisor-checks-v3-0-7308f3b9dc44@pks.im>
* ps/libgit-in-subdir (2026-06-22) 3 commits
- Move libgit.a sources into separate "lib/" directory
- t/helper: prepare "test-example-tap.c" for introduction of "lib/"
- Merge branch 'ps/odb-source-packed' into ps/libgit-in-subdir
(this branch uses ps/odb-source-packed.)
The source files for libgit.a have been moved into a new "lib/"
directory to clean up the top-level directory and clearly separate
library code.
Needs review.
source: <20260622-pks-libgit-in-subdir-v2-0-cb946c51ee7b@pks.im>
* ps/odb-generalize-prepare (2026-06-22) 3 commits
- odb: introduce `odb_prepare()`
- odb/source: generalize `reprepare()` callback
- Merge branch 'ps/odb-source-packed' into ps/odb-generalize-prepare
(this branch uses ps/odb-source-packed.)
The `reprepare()` callback for object database sources has been
generalized into a `prepare()` callback with an optional flush cache
flag, and a new `odb_prepare()` wrapper has been introduced to
allow pre-opening object database sources.
Needs review.
source: <20260622-b4-pks-odb-generalize-prepare-v1-0-d2a5c5d13144@pks.im>
* jc/submittingpatches-design-critiques (2026-06-20) 1 commit
(merged to 'next' on 2026-06-22 at 7495b5f9d6)
+ SubmittingPatches: address design critiques
The documentation in SubmittingPatches has been updated to clarify how
patch contributors should respond to design and viability critiques,
and how the resolution of such critiques should be recorded in the
final commit messages.
Will cook in 'next'.
cf. <ajjwYGWZ6hQWr600@pks.im>
source: <xmqqeci0g4mz.fsf@gitster.g>
* wy/doc-clarify-review-replies (2026-06-21) 2 commits
- doc: advise batching patch rerolls
- doc: encourage review replies before rerolling
Documentation on community contribution guidelines has been updated to
encourage replying to review comments before rerolling, and to advise
a default limit of at most one reroll per day to give reviewers across
different time zones enough time to participate.
Will merge to 'next'?
cf. <ajvDuUiDsmyf5LnX@pks.im>
source: <cover.1782028813.git.wy@wyuan.org>
* ty/migrate-ignorecase (2026-06-19) 2 commits
- config: use repo_ignore_case() to access core.ignorecase
- environment: move ignore_case into repo_config_values
The global configuration variable ignore_case (representing the
core.ignorecase configuration) has been migrated into struct
repo_config_values to tie it to a specific repository instance.
Waiting for comments from Johannes.
cf. <xmqqzf0mzc7j.fsf@gitster.g>
source: <20260619155152.642760-1-cat@malon.dev>
* mm/line-log-limited-ops (2026-06-18) 7 commits
- diffcore-pickaxe: scope -G to the -L tracked range
- diff: support --check with -L line ranges
- line-log: support diff stat formats with -L
- diff: extract a line-range diff helper for reuse
- diff: emit -L hunk headers via xdiff's formatter
- diff: simplify the line-range filter by classifying removals immediately
- diff: rename and group the line-range filter for clarity
"git log -L<range>:<path>" learned to limit various "diff" operations
like --stat, --check, -G, to the specified range:path.
Expecting a reroll.
cf. <CAC2QwmKEHb+LL4ZkQwq+Rw8eyDXzdBp_nxa_d+Ecx0K1icNqQA@mail.gmail.com>
source: <pull.2152.git.1781806593.gitgitgadget@gmail.com>
* hn/history-squash (2026-06-24) 4 commits
- history: re-edit a squash with every message
- history: add squash subcommand to fold a range
- history: give commit_tree_ext a message template
- history: extract helper for a commit's parent tree
The experimental "git history" command has been taught a new
"squash" subcommand to fold a range of commits into a single commit,
replaying any descendants on top.
Needs review.
source: <pull.2337.v5.git.git.1782338102.gitgitgadget@gmail.com>
* ps/t4216-tap-fix (2026-06-19) 1 commit
(merged to 'next' on 2026-06-25 at 42296fc12e)
+ t4216: fix no-op test that breaks TAP output
TAP output breakage fix.
Will merge to 'master'.
cf. <xmqq8q82fk8r.fsf@gitster.g>
source: <20260619-pks-t4216-drop-unused-prereq-v1-1-2ce0d7bea088@pks.im>
* mh/fetch-follow-remote-head-config (2026-06-19) 8 commits
(merged to 'next' on 2026-06-22 at 423079e1c8)
+ fetch: fixup a misaligned comment
+ fetch: add configuration variable fetch.followRemoteHEAD
+ fetch: refactor do_fetch handling of followRemoteHEAD
+ fetch: return 0 on known git_fetch_config
+ fetch: rename function report_set_head
+ t5510: cleanup remote in followRemoteHEAD dangling ref test
+ doc: explain fetchRemoteHEADWarn advice
+ fetch: fixup set_head advice for warn-if-not-branch
The `fetch.followRemoteHEAD` configuration variable has been added to
provide a default for the per-remote `remote.<name>.followRemoteHEAD`
setting.
Will cook in 'next'.
cf. <xmqqcxxp1j2t.fsf@gitster.g>
source: <20260619094751.2996804-1-m@lfurio.us>
* ps/refs-writing-subcommands (2026-06-17) 5 commits
- builtin/refs: add "rename" subcommand
- builtin/refs: add "create" subcommand
- builtin/refs: add "update" subcommand
- builtin/refs: add "delete" subcommand
- builtin/refs: drop `the_repository`
The "git refs" toolbox has been extended with new "create", "delete",
"update", and "rename" subcommands to create, delete, update, and
rename references, respectively.
Needs review.
source: <20260617-pks-refs-writing-subcommands-v2-0-07f3d18336f9@pks.im>
* po/hash-object-size-t (2026-06-16) 6 commits
(merged to 'next' on 2026-06-21 at b780a276b9)
+ hash-object: add a >4GB/LLP64 test case using filtered input
+ hash-object: add another >4GB/LLP64 test case
+ hash-object --stdin: verify that it works with >4GB/LLP64
+ hash algorithms: use size_t for section lengths
+ object-file.c: use size_t for header lengths
+ hash-object: demonstrate a >4GB/LLP64 problem
Support for hashing loose or packed objects larger than 4GB on Windows
and other LLP64 platforms has been improved by converting object header
buffers and data-handling functions from 'unsigned long' to 'size_t'.
Will cook in 'next'.
cf. <ajOQthRjhD3hRM9w@pks.im>
source: <pull.2138.v2.git.1781621398.gitgitgadget@gmail.com>
* kh/submittingpatches-trailers (2026-06-18) 5 commits
(merged to 'next' on 2026-06-22 at 2cd4a152c9)
+ SubmittingPatches: note that trailer order matters
+ SubmittingPatches: be consistent with trailer markup
+ SubmittingPatches: document Based-on-patch-by trailer
+ SubmittingPatches: discourage common Linux trailers
+ SubmittingPatches: encourage trailer use for substantial help
The trailer sections in SubmittingPatches have been updated to
encourage use of standard trailers.
Will cook in 'next'.
cf. <xmqq4ij0vo8f.fsf@gitster.g>
source: <V3_CV_SubPatches_trailers.9ec@msgid.xyz>
* mv/log-follow-mergy (2026-06-21) 1 commit
(merged to 'next' on 2026-06-22 at f7e984a003)
+ log: improve --follow following renames for non-linear history
"git log --follow" has been updated to handle non-linear history, in
which the path being tracked gets renamed differently in multiple
history lines, better.
Will cook in 'next'.
source: <ajjU4w2B0NlZffw1@collabora.com>
* wy/doc-myfirstcontribution-trim-quotes (2026-06-11) 1 commit
- MyFirstContribution: mention trimming quoted text in replies
The contributor guide has been updated to advise new contributors to
trim irrelevant quoted text when replying to review comments, matching
the existing advice given to reviewers.
Comments?
cf. <xmqqcxxwljue.fsf@gitster.g>
source: <080402ff0ac8127b654dccea59a1bf643df62a5c.1781186476.git.wy@wyuan.org>
* tb/midx-incremental-custom-base (2026-06-12) 3 commits
- midx-write: include packs above custom incremental base
- midx: pass custom '--base' through incremental writes
- t5334: expose shared `nth_line()` helper
The `git multi-pack-index write --incremental` command has been
corrected to properly honor the `--base` option. Previously, the
custom base was ignored by the normal write path, and the pack
exclusion logic incorrectly skipped packs from layers above the
selected base, breaking reachability closure for bitmaps.
Needs review.
source: <cover.1781294771.git.me@ttaylorr.com>
* mm/test-grep-lint (2026-06-12) 6 commits
- t: add greplint to detect bare grep assertions
- t: convert grep assertions to test_grep
- t: fix Lexer line count for $() inside double-quoted strings
- t: extract chainlint's parser into shared module
- t: fix grep assertions missing file arguments
- t/README: document test_grep helper
Needs review.
source: <pull.2135.v2.git.1781323575.gitgitgadget@gmail.com>
* rs/cat-file-default-format-optim (2026-06-14) 1 commit
(merged to 'next' on 2026-06-17 at 43ed8b3969)
+ cat-file: speed up default format
Will cook in 'next'.
cf. <20260615165326.GA91269@coredump.intra.peff.net>
source: <5a7ed929-6fe0-496c-83bd-65dee57c2241@web.de>
* kk/prio-queue-get-put-fusion (2026-06-08) 2 commits
- prio-queue: fold lazy_queue into prio_queue for automatic get+put fusion
- prio-queue: rename .nr to .nr_ and add accessor helpers
The lazy priority queue optimization pattern (deferring actual removal
in prio_queue_get() to allow get+put fusion) has been folded directly
into prio_queue itself, speeding up commit traversal workflows and
simplifying callers.
On hold, waiting for kk/prio-queue-get-put-fusion to land first.
cf. <CAL71e4MYNiScZjTwkApjDAjRh2LM0_SP59h5HCTywV-Pua03tw@mail.gmail.com>
source: <pull.2140.v4.git.1780945851.gitgitgadget@gmail.com>
* td/ref-filter-memoize-contains (2026-06-12) 3 commits
- commit-reach: die on contains walk errors
- ref-filter: memoize --contains with generations
- commit-reach: reject cycles in contains walk
'git branch --contains' and 'git for-each-ref --contains' have
been optimized to use the memoized commit traversal previously
used only by 'git tag --contains', significantly speeding up
connectivity checks across many candidate refs with shared
history.
Needs review.
source: <20260612-ref-filter-memoized-contains-v4-0-5ed39fd001dd@gmail.com>
* tc/replay-linearize (2026-06-22) 3 commits
- replay: offer an option to linearize the commit topology
- replay: add helper to put entry into mapped_commits
- replay: refactor enum replay_mode into a bool
git replay learns --linearize option to drop merge commits and
linearize the replayed history, mimicking git rebase
--no-rebase-merges.
Expecting a reroll.
cf. <87tsqrycke.fsf@emacs.iotcl.com>
source: <20260622-toon-git-replay-drop-merges-v4-0-ff257f534319@iotcl.com>
* ps/setup-drop-global-state (2026-06-10) 8 commits
(merged to 'next' on 2026-06-15 at d9a8b88d47)
+ treewide: drop USE_THE_REPOSITORY_VARIABLE
+ environment: stop using `the_repository` in `is_bare_repository()`
+ environment: split up concerns of `is_bare_repository_cfg`
+ builtin/init: stop modifying `is_bare_repository_cfg`
+ setup: remove global `git_work_tree_cfg` variable
+ builtin/init: simplify logic to configure worktree
+ builtin/init: stop modifying global `git_work_tree_cfg` variable
+ Merge branch 'ps/setup-centralize-odb-creation' into ps/setup-drop-global-state
Continuation of "setup.c" refactoring to drop remaining global state
(`git_work_tree_cfg`, `is_bare_repository_cfg`). The most notable
outcome is that `is_bare_repository()` has been updated to no longer
implicitly rely on `the_repository`.
Will cook in 'next'.
cf. <airVOrTboNDDGBak@denethor>
cf. <87ldckyygk.fsf@emacs.iotcl.com>
source: <20260611-b4-pks-setup-drop-global-state-v2-0-a6f7269c841d@pks.im>
* ps/refs-avoid-chdir-notify-reparent (2026-06-25) 12 commits
- refs: protect against chicken-and-egg recursion
- refs/reftable: lazy-load configuration to fix chicken-and-egg
- reftable: split up write options
- refs/files: lazy-load configuration to fix chicken-and-egg
- refs: move parsing of "core.logAllRefUpdates" back into ref stores
- repository: free main reference database
- chdir-notify: drop unused `chdir_notify_reparent()`
- refs: unregister reference stores from "chdir_notify"
- setup: don't apply "GIT_REFERENCE_BACKEND" without a repository
- setup: stop applying repository format twice
- setup: inline `check_and_apply_repository_format()`
- Merge branch 'ps/setup-centralize-odb-creation' into ps/refs-avoid-chdir-notify-reparent
The reference backends have been converted to always use absolute
paths internally. This allows dropping the calls to
`chdir_notify_reparent()` and fixes a memory leak in how the
reference database is constructed with an "onbranch" condition.
Will merge to 'next'?
cf. <aj1DuUzusBUqmF_C@denethor>
source: <20260625-b4-pks-refs-avoid-chdir-notify-reparent-v6-0-41fbca3cf5e3@pks.im>
* ps/odb-source-packed (2026-06-16) 18 commits
(merged to 'next' on 2026-06-19 at dcf0c084e4)
+ odb/source-packed: drop pointer to "files" parent source
+ midx: refactor interfaces to work on "packed" source
+ odb/source-packed: stub out remaining functions
+ odb/source-packed: wire up `freshen_object()` callback
+ odb/source-packed: wire up `find_abbrev_len()` callback
+ odb/source-packed: wire up `count_objects()` callback
+ odb/source-packed: wire up `for_each_object()` callback
+ odb/source-packed: wire up `read_object_stream()` callback
+ odb/source-packed: wire up `read_object_info()` callback
+ packfile: use higher-level interface to implement `has_object_pack()`
+ odb/source-packed: wire up `reprepare()` callback
+ odb/source-packed: wire up `close()` callback
+ odb/source-packed: start converting to a proper `struct odb_source`
+ odb/source-packed: store pointer to "files" instead of generic source
+ packfile: move packed source into "odb/" subsystem
+ packfile: split out packfile list logic
+ packfile: rename `struct packfile_store` to `odb_source_packed`
+ Merge branch 'ps/odb-source-loose' into ps/odb-source-packed
(this branch is used by ps/connected-generic-promisor-checks, ps/libgit-in-subdir, ps/odb-drop-whence and ps/odb-generalize-prepare.)
The packed object source has been refactored into a proper struct
odb_source.
Will cook in 'next'.
cf. <ajK2QKdW-TdflfR0@denethor>
source: <20260617-pks-odb-source-packed-v3-0-b5c7583cd795@pks.im>
* td/ref-filter-restore-prefix-iteration (2026-06-12) 1 commit
(merged to 'next' on 2026-06-19 at a19dbb4193)
+ ref-filter: restore prefix-scoped iteration
Commands that list branches and tags (like git branch and git tag)
have been optimized to pass the namespace prefix when initializing
their ref iterator, avoiding a loose-ref scaling regression in
repositories with many unrelated loose references.
Will cook in 'next'.
cf. <xmqqik7fsv2m.fsf@gitster.g>
source: <20260612-fix-git-branch-regression-v4-1-f150038c02f4@gmail.com>
* ty/move-protect-hfs-ntfs (2026-06-20) 2 commits
(merged to 'next' on 2026-06-20 at d8ca0d5180)
+ environment: use 'repo->initialized' for repo_protect_hfs() and repo_protect_ntfs()
(merged to 'next' on 2026-06-15 at c2a30ca954)
+ environment: move 'protect_hfs' and 'protect_ntfs' into 'repo_config_values'
The global configuration variables protect_hfs and protect_ntfs have
been migrated into struct repo_config_values to tie them to
per-repository configuration state.
Will cook in 'next'.
cf. <CAP8UFD35Tiy1_fqpjq8P-z=ZhzR3MTiThqfCs977652umRoSEQ@mail.gmail.com>
cf. <xmqqse6uwdnz.fsf@gitster.g>
source: <20260610124353.149874-2-cat@malon.dev>
source: <20260620140957.667820-1-cat@malon.dev>
* ps/cat-file-remote-object-info (2026-06-25) 13 commits
- cat-file: make remote-object-info allow-list dynamic
- cat-file: validate remote atoms with allow_list
- cat-file: add remote-object-info to batch-command
- transport: add client support for object-info
- serve: advertise object-info feature
- fetch-pack: move fetch initialization
- connect: refactor packet writing
- fetch-pack: move function to connect.c
- fetch-pack: prepare function to be moved
- t1006: split test utility functions into new "lib-cat-file.sh"
- cat-file: declare loop counter inside for()
- git-compat-util: add strtoul_szt() with error handling
- transport-helper: fix memory leak of helper on disconnect
The `remote-object-info` command has been added to `git cat-file
--batch-command`, allowing clients to request object metadata
(currently size) from a remote server via protocol v2 without
downloading the entire object.
The client dynamically filters format placeholders based on
server-advertised capabilities and safely returns empty strings for
inapplicable or unsupported fields.
Waiting for response(s) to review comment(s).
cf. <xmqqjyrme393.fsf@gitster.g>
source: <20260625-ps-eric-work-rebase-v14-0-09f7ffe21a53@gmail.com>
* ap/http-redirect-wwwauth-fix (2026-06-02) 1 commit
- http: preserve wwwauth_headers across redirects
When cURL follows a redirect, the WWW-Authenticate headers from the
redirect target were lost because credential_from_url() cleared the
credential state. This has been fixed by preserving the collected
headers across the redirect update.
Expecting a reroll.
cf. <5144a29d-a53f-4446-beff-e1f549345bf9@nvidia.com>
source: <20260602161150.1527493-1-aplattner@nvidia.com>
* ps/doc-recommend-b4 (2026-06-15) 3 commits
(merged to 'next' on 2026-06-17 at dd9a463369)
+ b4: introduce configuration for the Git project
+ MyFirstContribution: recommend the use of b4
+ MyFirstContribution: recommend shallow threading of cover letters
Project-specific configuration for b4 has been introduced, and the
documentation has been updated to recommend using it as a
streamlined method for submitting patches.
Will cook in 'next'.
cf. <87eci7yomp.fsf@emacs.iotcl.com>
source: <20260615-pks-b4-v4-0-22cfca8f19c5@pks.im>
* sn/rebase-update-refs-symrefs (2026-06-03) 1 commit
- rebase: skip branch symref aliases
"git rebase --update-refs" has been taught to resolve local branch
symrefs to their referents before queuing updates. This correctly
skips aliases of the current branch and avoids duplicate updates for
underlying real branches, fixing failures when branch aliases (like a
default branch rename) are present.
Waiting for response(s) to review comment(s).
cf. <f982c386-e329-4ab0-b695-e540bcb9de3d@gmail.com>
source: <pull.2126.v2.git.1780482436865.gitgitgadget@gmail.com>
* mm/diff-process-hunks (2026-06-14) 6 commits
- blame: consult diff process for no-hunk detection
- diff: bypass diff process with --no-ext-diff and in format-patch
- diff: add long-running diff process via diff.<driver>.process
- sub-process: separate process lifecycle from hashmap management
- userdiff: add diff.<driver>.process config
- xdiff: support external hunks via xpparam_t
A new `diff.<driver>.process` configuration has been introduced to
allow a long-running external process to act as a hunk provider to
allows external tools to control which lines Git considers changed
while leaving all output formatting (word diff, color, blame, etc.) to
Git's standard pipeline.
Expecting a reroll.
cf. <CAC2Qwm+P=fZOtpfMPeMiSXf3Afk6OLYpTP8Br78_PRA8WNL1Wg@mail.gmail.com>
source: <pull.2120.v4.git.1781463564.gitgitgadget@gmail.com>
* tb/pack-path-walk-bitmap-delta-islands (2026-06-21) 5 commits
(merged to 'next' on 2026-06-22 at 59cf1663e7)
+ pack-objects: support `--delta-islands` with `--path-walk`
+ pack-objects: extract `record_tree_depth()` helper
+ pack-objects: support reachability bitmaps with `--path-walk`
+ t/perf: drop p5311's lookup-table permutation
+ Merge branch 'ds/path-walk-filters' into tb/pack-path-walk-bitmap-delta-islands
The pack-objects command now supports using reachability bitmaps and
delta-islands concurrently with the `--path-walk` option, allowing
faster packaging by falling back to path-walk when bitmaps cannot
fully satisfy the request.
Will cook in 'next'.
cf. <xmqqwlvq1qyy.fsf@gitster.g>
source: <cover.1782082975.git.me@ttaylorr.com>
* ty/migrate-trust-executable-bit (2026-06-19) 3 commits
- environment: move trust_executable_bit into repo_config_values
- read-cache: move 'ce_mode_from_stat()' to 'read-cache.c'
- read-cache: remove redundant extern declarations
The 'trust_executable_bit' (coming from 'core.filemode'
configuration) has been migrated into 'repo_config_values' to tie it
to a specific repository instance.
Needs review.
source: <20260619162105.648495-1-cat@malon.dev>
* kk/prio-queue-cascade-sift (2026-06-01) 1 commit
- prio-queue: use cascade-down for faster extract-min
prio_queue_get() has been optimized by using a cascade-down approach
(promoting the smaller child at each level and sifting up the last
element from the leaf vacancy), which halves the number of comparisons
per extract-min operation in the common case.
On hold, waiting for kk/prio-queue-get-put-fusion to land first.
cf. <CAL71e4MYNiScZjTwkApjDAjRh2LM0_SP59h5HCTywV-Pua03tw@mail.gmail.com>
source: <pull.2132.v2.git.1780301856444.gitgitgadget@gmail.com>
* jk/repo-info-path-keys (2026-06-23) 3 commits
- repo: add path.gitdir with absolute and relative suffix formatting
- repo: add path.commondir with absolute and relative suffix formatting
- path: extract format_path() and use in rev-parse
The "git repo info" command has been taught new keys to output both
absolute and relative paths for "gitdir" and "commondir", supported by
a new path-formatting helper extracted from "git rev-parse".
Will merge to 'next'?
cf. <xmqqy0g3iz38.fsf@gitster.g>
source: <20260624033748.108281-1-jayatheerthkulkarni2005@gmail.com>
* ps/history-drop (2026-06-15) 10 commits
- builtin/history: implement "drop" subcommand
- builtin/history: split handling of ref updates into two phases
- reset: stop assuming that the caller passes in a clean index
- reset: allow the caller to specify the current HEAD object
- reset: introduce ability to skip updating HEAD
- reset: introduce dry-run mode
- reset: modernize flags passed to `reset_working_tree()`
- reset: rename `reset_head()`
- reset: drop `USE_THE_REPOSITORY_VARIABLE`
- read-cache: split out function to drop unmerged entries to stage 0
The experimental "git history" command has been taught a new "drop"
subcommand to remove a commit and replay its descendants onto its
parent.
Waiting for response(s) to review comment(s).
cf. <CAP8UFD1evTZqj1ymW9g5g2RmMkYMaE0rPa0Hzt+irH94M6LD6A@mail.gmail.com>
source: <20260615-b4-pks-history-drop-v6-0-2e329e536d78@pks.im>
* jk/setup-gitfile-diag-fix (2026-06-16) 1 commit
(merged to 'next' on 2026-06-18 at b63b3d1f25)
+ read_gitfile(): simplify NOT_A_REPO error message
A regression in the error diagnosis code for invalid .git files has
been fixed, avoiding a potential NULL-pointer crash when reporting
that a .git file does not point to a valid repository.
Will cook in 'next'.
cf. <xmqqjyry4hax.fsf@gitster.g>
source: <20260616123516.GA2301231@coredump.intra.peff.net>
* kh/doc-trailers (2026-06-10) 10 commits
- doc: interpret-trailers: document comment line treatment
- doc: interpret-trailers: commit to “trailer block” term
- doc: interpret-trailers: join new-trailers again
- doc: interpret-trailers: add key format example
- doc: interpret-trailers: explain key format
- doc: interpret-trailers: explain the format after the intro
- doc: interpret-trailers: not just for commit messages
- doc: interpret-trailers: use “metadata” in Name as well
- doc: interpret-trailers: replace “lines” with “metadata”
- doc: interpret-trailers: stop fixating on RFC 822
Documentation updates.
Expecting a reroll.
cf. <729baf6b-53ea-4e8d-95ab-5935667e66c2@app.fastmail.com>
source: <V3_CV_doc_int-tr_key_format.8a3@msgid.xyz>
* za/completion-hide-dotfiles (2026-06-20) 2 commits
- completion: hide dotfiles by default for path completion
- completion: hide dotfiles for selected path completion
The path completion for commands like `git rm` and `git mv`, is being
updated to hide dotfiles by default, unless the user explicitly starts
the path with a dot, matching standard shell-completion behavior.
Waiting for response(s) to review comment(s).
cf. <xmqq1pe0g08t.fsf@gitster.g>
source: <pull.2311.v3.git.git.1781978156.gitgitgadget@gmail.com>
* ec/commit-fixup-options (2026-05-26) 2 commits
- commit: allow -c/-C for all kinds of --fixup
- commit: allow -m/-F for all kinds of --fixup
The -m/-F/-c/-C options to supply commit log message from outside the
editor are now supported for all "git commit --fixup" variations.
Needs review.
source: <cover.1779792311.git.erik@cervined.in>
* kh/doc-replay-config (2026-06-05) 4 commits
- doc: replay: move “default” to the right-hand side
- doc: replay: use a nested description list
- doc: replay: improve config description
- doc: link to config for git-replay(1)
Doc update for "git replay" to actually refer to its configuration
variables.
Needs review.
source: <V3_CV_doc_replay_config.780@msgid.xyz>
* hn/status-pull-advice-qualified (2026-05-21) 1 commit
(merged to 'next' on 2026-06-15 at 898a4df940)
+ remote: qualify "git pull" advice for non-upstream compareBranches
Advice shown by "git status" when the local branch is behind or has
diverged from its push branch has been updated to suggest "git pull
<remote> <branch>".
Will cook in 'next'.
cf. <xmqq7bo6xuok.fsf@gitster.g>
source: <pull.2301.v4.git.git.1779372367317.gitgitgadget@gmail.com>
* hn/branch-delete-merged (2026-06-24) 7 commits
- branch: add --dry-run for --delete-merged
- branch: add branch.<name>.deleteMerged opt-out
- branch: add --delete-merged <branch>
- branch: prepare delete_branches for a bulk caller
- branch: let delete_branches skip unmerged branches on bulk refusal
- branch: convert delete_branches() to a flags argument
- branch: add --forked filter for --list mode
"git branch" command learned "--delete-merged" option to remove
local branches that have already been merged to the remote-tracking
branches they track.
Needs review.
source: <pull.2285.v18.git.git.1782338106.gitgitgadget@gmail.com>
* cc/promisor-auto-config-url-more (2026-05-27) 8 commits
(merged to 'next' on 2026-06-15 at d1c99e75cc)
+ doc: promisor: improve acceptFromServer entry
+ promisor-remote: auto-configure unknown remotes
+ promisor-remote: trust known remotes matching acceptFromServerUrl
+ promisor-remote: introduce promisor.acceptFromServerUrl
+ promisor-remote: add 'local_name' to 'struct promisor_info'
+ urlmatch: add url_normalize_pattern() helper
+ urlmatch: change 'allow_globs' arg to bool
+ t5710: simplify 'mkdir X' followed by 'git -C X init'
The handling of promisor-remote protocol capability has been
loosened to allow the other side to add to the list of promisor
remotes via the promisor.acceptFromServerURL configuration
variable.
Will cook in 'next'.
cf. <877bo7294j.fsf@emacs.iotcl.com>
cf. <xmqqh5naxwfc.fsf@gitster.g>
source: <20260527140820.1438165-1-christian.couder@gmail.com>
* hn/checkout-track-fetch (2026-06-24) 2 commits
- checkout: extend --track with a "fetch" mode to refresh start-point
- branch: expose helpers for finding the remote owning a tracking ref
"git checkout --track=..." learned to optionally fetch the branch
from the remote the new branch will work with.
Waiting for response(s) to review comment(s).
cf. <12998c3a-ff69-4a98-9ed6-18aa0224e75e@gmail.com>
source: <pull.2281.v15.git.git.1782338098.gitgitgadget@gmail.com>
* en/ort-harden-against-corrupt-trees (2026-06-13) 5 commits
(merged to 'next' on 2026-06-18 at e51bee59ca)
+ cache-tree: fix verify_cache() to catch non-adjacent D/F conflicts
+ merge-ort: abort merge when trees have duplicate entries
+ merge-ort: free diff pairs queue in clear_or_reinit_internal_opts()
+ merge-ort: drop unnecessary show_all_errors from collect_merge_info()
+ merge-ort: propagate callback errors from traverse_trees_wrapper()
"ort" merge backend handles merging corrupt trees better by
aborting when it should.
Will cook in 'next'.
cf. <xmqq5x3ldu4h.fsf@gitster.g>
source: <pull.2096.v2.git.1781419047.gitgitgadget@gmail.com>
* pw/status-rebase-todo (2026-06-23) 2 commits
(merged to 'next' on 2026-06-23 at a0fcde09dc)
+ status: improve rebase todo list parsing
+ sequencer: factor out parsing of todo commands
The display of the rebase todo list in "git status" has been
improved to correctly abbreviate object IDs for more commands and
avoid misinterpreting refs as object IDs.
Will cook in 'next'.
source: <cover.1782230024.git.phillip.wood@dunelm.org.uk>
* ps/shift-root-in-graph (2026-06-20) 3 commits
- graph: indent visual root in graph
- revision: add peek functions for lookahead
- lib-log-graph: move check_graph function
"git log --graph" has been modified to visually distinguish
parentless "root" commits (and commits that become roots due to
history simplification) by indenting them, preventing them from
appearing falsely related to unrelated commits rendered immediately
above them.
Waiting for response(s) to review comment(s).
The peek-ahead approach may need to be scratched.
cf. <CAN5EUNSj-2hkEBF7N_M6RLsuujDNFNUF3w53zR7SN1_5i2BRyg@mail.gmail.com>
cf. <CAL71e4OQ_kGb+UwHgikHG236-8BVtc7P9OdpV4i4UzYRCoPczw@mail.gmail.com>
source: <20260620-ps-pre-commit-indent-v6-0-cdc6d8fd5fbc@gmail.com>
^ permalink raw reply
* Re: [RFH] Why do osx CI jobs so unreliable?
From: Michael Montalbo @ 2026-06-26 3:27 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: Jeff King, git, Junio C Hamano
In-Reply-To: <ajkOoRhqaAcy6gBg@pks.im>
Patrick Steinhardt <ps@pks.im> writes:
> I think the issue is rather simple: we're hitting timeouts in Apache.
> [...]
> This is because our keepalive mechanisms aren't helping [...]
> Whether that's the same issue like we see in macOS sometimes is a
> different question.
I think that is the trigger for issues we've been seeing. I spent
some time investigating the Apache side over the last week and maybe
found a mod_http2 bug, which I filed upstream with a potential fix:
bug: https://bz.apache.org/bugzilla/show_bug.cgi?id=70131
fix: https://github.com/mmontalbo/httpd/pull/2
To Patrick's earlier question of whether this is a Git, curl, or Apache
bug: as best I can tell it's Apache. I could reproduce it with no Git
involved at all (just Apache and a small CGI that goes quiet past the
Timeout), and across several curl versions (8.6.0, which is what the
GitHub runners use, up to 8.20.0), so I don't think bumping curl would
help. It also seems to wear two faces from the same trigger: over
HTTP/1.1 Apache closes the connection and curl bails with the
"transfer closed" error (which looks like what you hit with Timeout=1,
and the recent failures on both macOS and Linux), and over HTTP/2 it
does not reliably reset the stream, so the client just waits, which is
the six-hour macOS hang. I share the pessimism from earlier in the
thread, though: I think the real fix is upstream in Apache, and
anything we do on our side mostly just bounds the symptom in the
meantime.
Given there could be a potential reliability issue with an upstream
dependency like Apache, I was considering what mitigation strategies
might help:
- Enforce some kind of lower bound speed limit and a client-side
timeout so runs that wedge fail fast (and loudly) instead of
hanging.
- Potentially provide some affordance for retrying flaky tests
that might fail due to upstream dependencies. Git already has
some HTTP retry support (http.maxRetries and friends, added
recently), but as far as I can tell it only triggers on HTTP 429
rate limiting, so it would not catch a stall like this on its
own. A test-level retry is not something I like that much, since
it might encourage papering over flakiness that should be
resolved, but it was a consideration vs requiring a fresh CI run
to resolve the flake.
- Make slow tests faster by optimizing the test itself and/or
the test runner configuration (e.g., job number matching
cores) so wedges become less likely.
For the first one, I think Git already provides some affordances. There
is a stall-based timeout that just ships disabled: as I understand it
http.lowSpeedLimit sets a bytes/sec floor and http.lowSpeedTime how long
a transfer can sit below it before curl gives up, so it would catch a
wedged connection without punishing one that is just slow. Enabling it
for the http tests might look something like:
diff --git a/t/lib-httpd.sh b/t/lib-httpd.sh
@@ GIT_TRACE=$GIT_TRACE; export GIT_TRACE
+# Abort a transfer that makes essentially no progress for a while,
+# so a wedged connection fails in seconds instead of hanging to the
+# job cap. Tiny limit, generous window, so it only trips on a true
+# stall; override either var, or set the limit to 0, to disable.
+GIT_HTTP_LOW_SPEED_LIMIT=${GIT_HTTP_LOW_SPEED_LIMIT-1}
+GIT_HTTP_LOW_SPEED_TIME=${GIT_HTTP_LOW_SPEED_TIME-60}
+export GIT_HTTP_LOW_SPEED_LIMIT GIT_HTTP_LOW_SPEED_TIME
I went conservative on the values on purpose: a floor of 1 byte/sec
should only really fire on a true zero-progress stall, not on something
that is just crawling on a slow runner, and the 60s window is generous
for the same reason. When I tried it locally against a stall-proxy it
did turn an otherwise indefinite hang into a bounded abort (a tighter
limit/window brings that down to single-digit seconds). It probably does
not need to be suite-wide either; it could be scoped per-command with
git -c, which the http tests already lean on for this kind of thing
(t5551 passes http.postbuffer and http.extraheader that way), if a
narrower blast radius feels safer.
I only dug into the first option in any depth, since I wanted to
sanity-check the direction before writing patches. Does turning on a
stall timeout for the http tests seem reasonable? Are there other
strategies that we should implement?
^ permalink raw reply
* Re: [PATCH v6 00/11] refs: fix "onbranch" conditions
From: Jeff King @ 2026-06-26 5:06 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git, Karthik Nayak, Justin Tobler
In-Reply-To: <20260625-b4-pks-refs-avoid-chdir-notify-reparent-v6-0-41fbca3cf5e3@pks.im>
On Thu, Jun 25, 2026 at 11:19:58AM +0200, Patrick Steinhardt wrote:
> - Fix the "onbranch" recursion properly: instead of papering over the
> issue, this series now refactors reference store initialization to
> not read any configuration at all anymore. Instead, the config is
> now parsed lazily. This fixes the recursion, but also makes us
> respect configuration guarded by "onbranch" conditions properly.
Sorry, I was offline and missed reviewing some of the intermediate
stages.
The approach you take in v6 looks good to me, and I'm glad the result
does not look too painful to maintain. If we ever add a config option
that is absolutely required for initializing the reading side of the
backends, we'll be back to our chicken-and-egg problem. But I won't be
surprised if that never happens, and if it does, we can decide on the
user-visible interaction with onbranch includes then (and whatever the
result is, document it).
Thanks for taking the time to re-work all of this.
-Peff
^ permalink raw reply
* Re: [RFH] Why do osx CI jobs so unreliable?
From: Jeff King @ 2026-06-26 5:16 UTC (permalink / raw)
To: Michael Montalbo; +Cc: Patrick Steinhardt, git, Junio C Hamano
In-Reply-To: <CAC2QwmJA2TH6BmO0O61qRYvV2pqURUk0dTXpkJtb9e-TZNZDZQ@mail.gmail.com>
On Thu, Jun 25, 2026 at 08:27:35PM -0700, Michael Montalbo wrote:
> I think that is the trigger for issues we've been seeing. I spent
> some time investigating the Apache side over the last week and maybe
> found a mod_http2 bug, which I filed upstream with a potential fix:
>
> bug: https://bz.apache.org/bugzilla/show_bug.cgi?id=70131
> fix: https://github.com/mmontalbo/httpd/pull/2
Thanks both of you for digging into this. I'm not familiar enough with
Apache's code to pass confident judgement, but your findings certainly
convinced me that this is just an apache bug.
> Given there could be a potential reliability issue with an upstream
> dependency like Apache, I was considering what mitigation strategies
> might help:
> [...]
Depending on how widespread the Apache bug is, another option might just
be: do nothing and wait for it to get fixed.
Trying to make the wedged state fail fast and loudly is mostly just
punting on the problem. We'd still see spurious failures. We've so far
resisted the urge to do any automatic flaky-test retries, preferring
instead to just try to root out the flakes. I'm a little hesitant to
start now, because I think our strategy has mostly been good so far, and
I've seen some horrible counter-examples where flakes and retries become
a routine drag on development (and I'm afraid that accommodating flakes
might make them more common).
> - Make slow tests faster by optimizing the test itself and/or
> the test runner configuration (e.g., job number matching
> cores) so wedges become less likely.
It sounds like the bad state is triggered when Apache hits a timeout,
and we hit that timeout because the system is slow or busy. We could try
to make things less slow, but would it work equally well to increase
that timeout?
-Peff
^ permalink raw reply
* Re: [PATCH v4 3/3] replay: offer an option to linearize the commit topology
From: Toon Claes @ 2026-06-26 5:36 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git, Elijah Newren, Johannes Schindelin
In-Reply-To: <ajk-a4a3KSJ2u7Ju@pks.im>
Patrick Steinhardt <ps@pks.im> writes:
> git-rebase(1) essentially knows about three different modes:
>
> - "--no-rebase-merges", which is the default and maps to your
> "--linearize".
>
> - "--rebase-merges", which by default doesn't rebase cousins by using
> "--ancestry-path" internally.
>
> - "--rebase-merges=rebase-cousins", which doesn't pass the above
> option.
>
> So it's not a simple boolean there, which makes me wonder whether we
> should mirror the same interface so that all of git-rebase(1)'s modes
> can be represented, as well.
That's a valid question, although I don't know a good answer to that.
Basically you're asking for what the command line options will look
like? Allow me to think out loud.
In this series I'm adding --linearize to git-replay(1). As mentioned, I
don't think it makes sense to add it to git-history(1) as well. Without
this option, the process aborts when it encounters a merge.
Dscho sent a patch series to properly replay (2-way) merges. I think
this should become the default for both git-replay(1) and
git-history(1).
But then, do we want to have an option that brings back the current
behavior of aborting at merges? Maybe with --no-merges?
Then there's the option of rebasing cousins left. That's something that
isn't covered by Dscho's series yet. Maybe --replay-cousins?
To reiterate what the final design could look like:
* <nothing>: replay merges preserving topology.
* "--linearize": flattens merges (only git-replay(1)).
* "--no-merges": dies when the process tries to replay a merge.
* "--replay-cousins": does what --rebase-merges=rebase-cousins does.
Now, all these options are (I think) mutually exclusive, so we could
consider an option "--replay-merges=<mode>", but personally I find
"--<option>=<value>" arguments harder to use than specifying separate
options.
I think I'm avoiding your question, because the design of the command
line parameters doesn't need tot 1-on-1 correlate to the internal
datastructure. And I agree the mode isn't a boolean, but does that mean
we want to use an enum internally? Well, I don't know. And I also don't
think that matters right now. Code is easy to change, I think the
command line options should be designed with the future in mind, which I
believe we do with "--linearize".
Sorry for this long-winded rambling, but bottom line I think it's fine
to add --linearize and in the future add more options and see how the
code should evolve to support those.
>> diff --git a/replay.c b/replay.c
>> index 7921d7dba3..5539daff00 100644
>> --- a/replay.c
>> +++ b/replay.c
>> @@ -277,12 +277,16 @@ static struct commit *pick_regular_commit(struct repository *repo,
>> struct commit *onto,
>> struct merge_options *merge_opt,
>> struct merge_result *result,
>> + struct commit *replayed_base,
>> bool reverse,
>> enum replay_empty_commit_action empty)
>> {
>> - struct commit *base, *replayed_base;
>> + struct commit *base;
>> struct tree *pickme_tree, *base_tree, *replayed_base_tree;
>>
>> + if (replayed_base && reverse)
>> + BUG("Linearizing commits is not supported when replaying in reverse");
>
> Nit: Error messages should typically start with a lower-case letter.
Thanks.
>> @@ -430,12 +435,25 @@ int replay_revisions(struct rev_info *revs,
>> while ((commit = get_revision(revs))) {
>> const struct name_decoration *decoration;
>>
>> - if (commit->parents && commit->parents->next)
>> - die(_("replaying merge commits is not supported yet!"));
>> + if (commit->parents && commit->parents->next) {
>> + if (!opts->linearize)
>> + die(_("replaying merge commits is not supported yet!"));
>> + /*
>> + * Drop the merge commit: do not pick it and leave
>> + * last_commit unchanged, so its children (and any ref
>> + * pointing at it) are reparented onto the previous
>> + * non-merge commit, which the ref-update loop below uses.
>> + */
>
> One could add a hint here that tells the user to pass the option. But I
> guess that might be somewhat weird, as we cannot assume that we're
> called by git-replay(1) here.
Yeah, true...
--
Cheers,
Toon
^ permalink raw reply
* [PATCH v5 0/3] Teach git-replay(1) to linearize merge commits
From: Toon Claes @ 2026-06-26 5:48 UTC (permalink / raw)
To: git; +Cc: Elijah Newren, Toon Claes, Johannes Schindelin,
Johannes Schindelin
In-Reply-To: <20260622-toon-git-replay-drop-merges-v4-0-ff257f534319@iotcl.com>
As an alternative to dscho's patch series to replay merges[1], add
option to git-replay(1) to linearize merges. This mimics what
git-rebase(1) does too with --no-rebase-merges (the default).
The first two patches do some refactoring. The third patch implements
the actual change. This patch was kindly provided by Dscho, which I've
tweaked to be upstreamed.
The --linearize option is only added to git-replay(1) and not to
git-history(1) because in my opinion it doesn't make much sense to do
so, but I'm happy to hear if anyone disagrees.
This series might conflict with Kristoffer's series to make
documentation changes[2], but should be trivial to resolve. And I don't
think there's a conflict with Patrick's series on adding "drop" to
git-history(1)[3].
dscho's series to replay merges[1] needs a bit of rework to fit on top
of this, but I'm happy to help figuring that out. We've been discussing
to either name the option --flatten or --linearize, but I've decided on
"linearize" because the documentation of git-rebase(1) also mentions
"linearize".
[1]: <pull.2106.git.1778107405.gitgitgadget@gmail.com>
[2]: <V2_CV_doc_replay_config.767@msgid.xyz>
[3]: <20260603-b4-pks-history-drop-v2-0-742cb5b5176d@pks.im>
---
Changes in v5:
- Dropped the enum->bool patch and instead added a patch that better
explains how pick_regular_commit() picks a base.
- Order of commits is shuffled.
- (BIGGEST CHANGE) When working on a refactor to undo the enum->bool
patch, I extended the code comments to explain how things work. This
made me realize the use of the "replayed_base" was incorrect when
multiple branches are rebased with --onto. This is fixed now and a
test is added for this scenario.
- Link to v4: https://patch.msgid.link/20260622-toon-git-replay-drop-merges-v4-0-ff257f534319@iotcl.com
Changes in v4:
- Use test_grep instead of a bare grep in the range-diff test, to
prepare for mm/test-grep-lint.
- Link to v3: https://patch.msgid.link/20260616-toon-git-replay-drop-merges-v3-0-153e9eb99ce1@iotcl.com
Changes in v3:
- Add --linearize to Documentation SYNOPSIS, and mention it's
incompatible with --revert.
- Small language change in help message for --linearize.
- Rephrase comment to include last_commit isn't modified when
linearizing merges.
- Remove test that was added in earlier versions, but actually is
a duplicate of 'replaying merge commits is not supported yet'.
- Add test to verify --revert and --linearize are incompatible.
- Properly test that replaying down to root with --linearize works.
- Add test for --linearize with --advance.
- Add test that uses git-range-diff(1) to verify the patches created by
--linearize are correct.
- Link to v2: https://patch.msgid.link/20260610-toon-git-replay-drop-merges-v2-0-5714a71c6d83@iotcl.com
Changes in v2:
- Restructured the conditions to detect merge commits and added a line
of comment why the loop continues.
- Rewrote tests to use the history from the setup step and added a few
test cases.
- Re-added Johannes's Signed-off-by trailer. Johannes gave me the
patches with this trailer, and if I understand correctly, I can keep
it. Please let me know if that wrong.
- Link to v1: https://patch.msgid.link/20260608-toon-git-replay-drop-merges-v1-0-e3ee71fce7b4@iotcl.com
---
Johannes Schindelin (1):
replay: offer an option to linearize the commit topology
Toon Claes (2):
replay: add helper to put entry into mapped_commits
replay: better explain how pick_regular_commit() picks a base
Documentation/git-replay.adoc | 8 ++++-
builtin/replay.c | 6 +++-
replay.c | 69 ++++++++++++++++++++++++++---------
replay.h | 5 +++
t/t3650-replay-basics.sh | 84 ++++++++++++++++++++++++++++++++++++++++++-
5 files changed, 152 insertions(+), 20 deletions(-)
Range-diff versus v4:
1: a08bc22330 < -: ---------- replay: refactor enum replay_mode into a bool
2: 3117fddcc5 = 1: bbd5a710bd replay: add helper to put entry into mapped_commits
-: ---------- > 2: e08c7b46c0 replay: better explain how pick_regular_commit() picks a base
3: acbb1df6a9 ! 3: 043cf63c1c replay: offer an option to linearize the commit topology
@@ builtin/replay.c: int cmd_replay(int argc,
ref_mode = get_ref_action_mode(repo, ref_action);
## replay.c ##
-@@ replay.c: static struct commit *pick_regular_commit(struct repository *repo,
- struct commit *onto,
- struct merge_options *merge_opt,
- struct merge_result *result,
-+ struct commit *replayed_base,
- bool reverse,
- enum replay_empty_commit_action empty)
- {
-- struct commit *base, *replayed_base;
-+ struct commit *base;
- struct tree *pickme_tree, *base_tree, *replayed_base_tree;
-
-+ if (replayed_base && reverse)
-+ BUG("Linearizing commits is not supported when replaying in reverse");
-+
- if (pickme->parents) {
- base = pickme->parents->item;
- base_tree = repo_get_commit_tree(repo, base);
-@@ replay.c: static struct commit *pick_regular_commit(struct repository *repo,
- base_tree = lookup_tree(repo, repo->hash_algo->empty_tree);
- }
-
-- replayed_base = get_mapped_commit(replayed_commits, base, onto);
-+ if (!replayed_base)
-+ replayed_base = get_mapped_commit(replayed_commits, base, onto);
- replayed_base_tree = repo_get_commit_tree(repo, replayed_base);
- pickme_tree = repo_get_commit_tree(repo, pickme);
-
@@ replay.c: int replay_revisions(struct rev_info *revs,
while ((commit = get_revision(revs))) {
const struct name_decoration *decoration;
+- /*
+- * pick_regular_commit() looks up the parent of `commit` in
+- * `replayed_commits` to determine the ancestor to replay onto.
+- * The `default_base` parameter is used when no ancestor is found,
+- * which happens for the first commit in the revision range.
+- * When reverting, commits are replayed in reverse order, so the
+- * lookup never succeeds, and we need to pass `last_commit`.
+- */
+- struct commit *base = onto;
+- if (mode == REPLAY_MODE_REVERT)
+- base = last_commit;
+-
- if (commit->parents && commit->parents->next)
- die(_("replaying merge commits is not supported yet!"));
+-
+- last_commit = pick_regular_commit(revs->repo, commit, base,
+- replayed_commits,
+- &merge_opt, &result, mode, opts->empty);
+ if (commit->parents && commit->parents->next) {
+ if (!opts->linearize)
+ die(_("replaying merge commits is not supported yet!"));
+ /*
-+ * Drop the merge commit: do not pick it and leave
-+ * last_commit unchanged, so its children (and any ref
-+ * pointing at it) are reparented onto the previous
-+ * non-merge commit, which the ref-update loop below uses.
++ * Drop the merge commit: do not pick it, leave
++ * `last_commit` unchanged, and fall through to the
++ * rest of the loop. As a result:
++ * - the merge commit is mapped to `last_commit` in
++ * `replayed_commits`, this will become the parent for
++ * the child commits.
++ * - refs previously pointing to the merge commit are
++ * rewritten to point to the previous non-merge commit.
+ */
+ } else {
-+ struct commit *to_pick = reverse ? last_commit : onto;
-+ last_commit =
-+ pick_regular_commit(revs->repo, commit,
-+ replayed_commits, to_pick,
-+ &merge_opt, &result,
-+ opts->linearize ? last_commit : NULL,
-+ reverse, opts->empty);
++ /*
++ * pick_regular_commit() looks up the parent of `commit` in
++ * `replayed_commits` to determine the ancestor to replay onto.
++ * The `default_base` parameter is used when no ancestor is found,
++ * which happens for the first commit in the revision range.
++ * When reverting, commits are replayed in reverse order, so the
++ * lookup never succeeds, and we need to pass `last_commit`.
++ */
++ struct commit *base = onto;
++ if (mode == REPLAY_MODE_REVERT)
++ base = last_commit;
++
++ last_commit = pick_regular_commit(revs->repo, commit, base,
++ replayed_commits,
++ &merge_opt, &result,
++ mode, opts->empty);
+ }
-
-- last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
-- reverse ? last_commit : onto,
-- &merge_opt, &result, reverse, opts->empty);
++
if (!last_commit)
break;
@@ t/t3650-replay-basics.sh: test_expect_success '--onto with --ref rejects multipl
+ git log --oneline main..$tip >out &&
+ test_line_count = 3 out
+'
++
++test_expect_success 'replay with --linearize to rebase multiple divergent branches' '
++ git replay --ref-action=print --linearize \
++ --onto main ^B topic2 topic-with-merge >result &&
++
++ test_line_count = 2 result &&
++ cut -f 3 -d " " result >new-branch-tips &&
++
++ git log --format=%s $(head -n 1 new-branch-tips) >actual &&
++ test_write_lines E D C M L B A >expect &&
++ test_cmp expect actual &&
++
++ git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
++ test_write_lines O N J I M L B A >expect &&
++ test_cmp expect actual
++'
+
test_done
---
base-commit: ab776a62a78576513ee121424adb19597fbb7613
change-id: 20260604-toon-git-replay-drop-merges-807fa008d395
^ permalink raw reply
* [PATCH v5 1/3] replay: add helper to put entry into mapped_commits
From: Toon Claes @ 2026-06-26 5:48 UTC (permalink / raw)
To: git; +Cc: Elijah Newren, Toon Claes
In-Reply-To: <20260626-toon-git-replay-drop-merges-v5-0-5e120738b9d0@iotcl.com>
The function replay_revisions() in replay.c is rather lengthy. Extract
the logic to put a commit entry into mapped_commits into a helper
function put_mapped_commit().
While at it, rename mapped_commit() to get_mapped_commit() to pair with
this new function.
Signed-off-by: Toon Claes <toon@iotcl.com>
---
replay.c | 31 ++++++++++++++++++++-----------
1 file changed, 20 insertions(+), 11 deletions(-)
diff --git a/replay.c b/replay.c
index da531d5bc6..7bde1c7e93 100644
--- a/replay.c
+++ b/replay.c
@@ -250,9 +250,9 @@ static void set_up_replay_mode(struct repository *repo,
strset_clear(&rinfo.positive_refs);
}
-static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
- struct commit *commit,
- struct commit *fallback)
+static struct commit *get_mapped_commit(kh_oid_map_t *replayed_commits,
+ struct commit *commit,
+ struct commit *fallback)
{
khint_t pos;
if (!commit)
@@ -263,6 +263,21 @@ static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
return kh_value(replayed_commits, pos);
}
+static void put_mapped_commit(kh_oid_map_t *replayed_commits,
+ struct commit *commit,
+ struct commit *new_commit)
+{
+ khint_t pos;
+ int ret;
+
+ pos = kh_put_oid_map(replayed_commits, commit->object.oid, &ret);
+ if (ret == 0)
+ BUG("Duplicate rewritten commit: %s\n",
+ oid_to_hex(&commit->object.oid));
+
+ kh_value(replayed_commits, pos) = new_commit;
+}
+
static struct commit *pick_regular_commit(struct repository *repo,
struct commit *pickme,
kh_oid_map_t *replayed_commits,
@@ -283,7 +298,7 @@ static struct commit *pick_regular_commit(struct repository *repo,
base_tree = lookup_tree(repo, repo->hash_algo->empty_tree);
}
- replayed_base = mapped_commit(replayed_commits, base, onto);
+ replayed_base = get_mapped_commit(replayed_commits, base, onto);
replayed_base_tree = repo_get_commit_tree(repo, replayed_base);
pickme_tree = repo_get_commit_tree(repo, pickme);
@@ -423,8 +438,6 @@ int replay_revisions(struct rev_info *revs,
replayed_commits = kh_init_oid_map();
while ((commit = get_revision(revs))) {
const struct name_decoration *decoration;
- khint_t pos;
- int hr;
if (commit->parents && commit->parents->next)
die(_("replaying merge commits is not supported yet!"));
@@ -436,11 +449,7 @@ int replay_revisions(struct rev_info *revs,
break;
/* Record commit -> last_commit mapping */
- pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
- if (hr == 0)
- BUG("Duplicate rewritten commit: %s\n",
- oid_to_hex(&commit->object.oid));
- kh_value(replayed_commits, pos) = last_commit;
+ put_mapped_commit(replayed_commits, commit, last_commit);
/* Update any necessary branches */
if (ref)
--
2.53.0.1323.g189a785ab5
^ permalink raw reply related
* [PATCH v5 2/3] replay: better explain how pick_regular_commit() picks a base
From: Toon Claes @ 2026-06-26 5:48 UTC (permalink / raw)
To: git; +Cc: Elijah Newren, Toon Claes
In-Reply-To: <20260626-toon-git-replay-drop-merges-v5-0-5e120738b9d0@iotcl.com>
The function pick_regular_commit() will replay the `pickme` commit. To
determine the ancestor where to replay this commit on, it takes the
parent of the commit and looks up its replayed result in
`replayed_commits`. If no ancestor is found, the `onto` parameter is
used as fallback.
The name `onto` is rather confusing, so rename it to `default_base`. And
while at it, shuffle the function parameters so `struct commit`
parameters are immediate siblings.
When in mode REPLAY_MODE_REVERT, the fallback `default_base` will always
be used. This happens because commits are replayed in reverse order, so
looking up the `pickme`'s parent in `replayed_commits` will always
return empty. And to make these commits stack on top of each other, we
need to pass in `last_commit`.
Signed-off-by: Toon Claes <toon@iotcl.com>
---
replay.c | 20 ++++++++++++++++----
1 file changed, 16 insertions(+), 4 deletions(-)
diff --git a/replay.c b/replay.c
index 7bde1c7e93..86fba47fb9 100644
--- a/replay.c
+++ b/replay.c
@@ -280,8 +280,8 @@ static void put_mapped_commit(kh_oid_map_t *replayed_commits,
static struct commit *pick_regular_commit(struct repository *repo,
struct commit *pickme,
+ struct commit *default_base,
kh_oid_map_t *replayed_commits,
- struct commit *onto,
struct merge_options *merge_opt,
struct merge_result *result,
enum replay_mode mode,
@@ -298,7 +298,7 @@ static struct commit *pick_regular_commit(struct repository *repo,
base_tree = lookup_tree(repo, repo->hash_algo->empty_tree);
}
- replayed_base = get_mapped_commit(replayed_commits, base, onto);
+ replayed_base = get_mapped_commit(replayed_commits, base, default_base);
replayed_base_tree = repo_get_commit_tree(repo, replayed_base);
pickme_tree = repo_get_commit_tree(repo, pickme);
@@ -439,11 +439,23 @@ int replay_revisions(struct rev_info *revs,
while ((commit = get_revision(revs))) {
const struct name_decoration *decoration;
+ /*
+ * pick_regular_commit() looks up the parent of `commit` in
+ * `replayed_commits` to determine the ancestor to replay onto.
+ * The `default_base` parameter is used when no ancestor is found,
+ * which happens for the first commit in the revision range.
+ * When reverting, commits are replayed in reverse order, so the
+ * lookup never succeeds, and we need to pass `last_commit`.
+ */
+ struct commit *base = onto;
+ if (mode == REPLAY_MODE_REVERT)
+ base = last_commit;
+
if (commit->parents && commit->parents->next)
die(_("replaying merge commits is not supported yet!"));
- last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
- mode == REPLAY_MODE_REVERT ? last_commit : onto,
+ last_commit = pick_regular_commit(revs->repo, commit, base,
+ replayed_commits,
&merge_opt, &result, mode, opts->empty);
if (!last_commit)
break;
--
2.53.0.1323.g189a785ab5
^ permalink raw reply related
* [PATCH v5 3/3] replay: offer an option to linearize the commit topology
From: Toon Claes @ 2026-06-26 5:48 UTC (permalink / raw)
To: git; +Cc: Elijah Newren, Johannes Schindelin, Toon Claes,
Johannes Schindelin
In-Reply-To: <20260626-toon-git-replay-drop-merges-v5-0-5e120738b9d0@iotcl.com>
From: Johannes Schindelin <Johannes.Schindelin@gmx.de>
One of the stated goals of git-replay(1) is to allow implementing the
git-rebase(1) functionality on the server side.
The default mode of git-rebase(1) is to act as if `--no-rebase-merges`
was given. This mode drops merge commits instead of replaying them, and
linearizes the commit history into a sequence of the
regular (single-parent) commits.
Add option `--linearize` to git-replay(1) to do the same.
Co-authored-by: Toon Claes <toon@iotcl.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Signed-off-by: Toon Claes <toon@iotcl.com>
---
Documentation/git-replay.adoc | 8 ++++-
builtin/replay.c | 6 +++-
replay.c | 50 ++++++++++++++++----------
replay.h | 5 +++
t/t3650-replay-basics.sh | 84 ++++++++++++++++++++++++++++++++++++++++++-
5 files changed, 132 insertions(+), 21 deletions(-)
diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index a32f72aead..ef56ee0f1b 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -10,7 +10,7 @@ SYNOPSIS
--------
[verse]
(EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
- [--ref=<ref>] [--ref-action=<mode>] <revision-range>
+ [--ref=<ref>] [--ref-action=<mode>] [--linearize] <revision-range>
DESCRIPTION
-----------
@@ -88,6 +88,12 @@ incompatible with `--contained` (which is a modifier for `--onto` only).
+
The default mode can be configured via the `replay.refAction` configuration variable.
+--linearize::
+ In this mode, `git replay` imitates `git rebase --no-rebase-merges`,
+ i.e. it cherry-picks only non-merge commits, each one on top of the
+ previous one.
+ This option is incompatible with `--revert`.
+
<revision-range>::
Range of commits to replay; see "Specifying Ranges" in
linkgit:git-rev-parse[1]. In `--advance=<branch>` or
diff --git a/builtin/replay.c b/builtin/replay.c
index 39e3a86f6c..62962c73c7 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -85,7 +85,7 @@ int cmd_replay(int argc,
const char *const replay_usage[] = {
N_("(EXPERIMENTAL!) git replay "
"([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
- "[--ref=<ref>] [--ref-action=<mode>] <revision-range>"),
+ "[--ref=<ref>] [--ref-action=<mode>] [--linearize] <revision-range>"),
NULL
};
struct option replay_options[] = {
@@ -111,6 +111,8 @@ int cmd_replay(int argc,
N_("mode"),
N_("control ref update behavior (update|print)"),
PARSE_OPT_NONEG),
+ OPT_BOOL(0, "linearize", &opts.linearize,
+ N_("drop merge commits, replaying only non-merge commits")),
OPT_END()
};
@@ -132,6 +134,8 @@ int cmd_replay(int argc,
opts.contained, "--contained");
die_for_incompatible_opt2(!!opts.ref, "--ref",
!!opts.contained, "--contained");
+ die_for_incompatible_opt2(!!opts.revert, "--revert",
+ opts.linearize, "--linearize");
/* Parse ref action mode from command line or config */
ref_mode = get_ref_action_mode(repo, ref_action);
diff --git a/replay.c b/replay.c
index 86fba47fb9..d803e0312f 100644
--- a/replay.c
+++ b/replay.c
@@ -439,24 +439,38 @@ int replay_revisions(struct rev_info *revs,
while ((commit = get_revision(revs))) {
const struct name_decoration *decoration;
- /*
- * pick_regular_commit() looks up the parent of `commit` in
- * `replayed_commits` to determine the ancestor to replay onto.
- * The `default_base` parameter is used when no ancestor is found,
- * which happens for the first commit in the revision range.
- * When reverting, commits are replayed in reverse order, so the
- * lookup never succeeds, and we need to pass `last_commit`.
- */
- struct commit *base = onto;
- if (mode == REPLAY_MODE_REVERT)
- base = last_commit;
-
- if (commit->parents && commit->parents->next)
- die(_("replaying merge commits is not supported yet!"));
-
- last_commit = pick_regular_commit(revs->repo, commit, base,
- replayed_commits,
- &merge_opt, &result, mode, opts->empty);
+ if (commit->parents && commit->parents->next) {
+ if (!opts->linearize)
+ die(_("replaying merge commits is not supported yet!"));
+ /*
+ * Drop the merge commit: do not pick it, leave
+ * `last_commit` unchanged, and fall through to the
+ * rest of the loop. As a result:
+ * - the merge commit is mapped to `last_commit` in
+ * `replayed_commits`, this will become the parent for
+ * the child commits.
+ * - refs previously pointing to the merge commit are
+ * rewritten to point to the previous non-merge commit.
+ */
+ } else {
+ /*
+ * pick_regular_commit() looks up the parent of `commit` in
+ * `replayed_commits` to determine the ancestor to replay onto.
+ * The `default_base` parameter is used when no ancestor is found,
+ * which happens for the first commit in the revision range.
+ * When reverting, commits are replayed in reverse order, so the
+ * lookup never succeeds, and we need to pass `last_commit`.
+ */
+ struct commit *base = onto;
+ if (mode == REPLAY_MODE_REVERT)
+ base = last_commit;
+
+ last_commit = pick_regular_commit(revs->repo, commit, base,
+ replayed_commits,
+ &merge_opt, &result,
+ mode, opts->empty);
+ }
+
if (!last_commit)
break;
diff --git a/replay.h b/replay.h
index faf95c7459..64f42b6512 100644
--- a/replay.h
+++ b/replay.h
@@ -62,6 +62,11 @@ struct replay_revisions_options {
* Defaults to REPLAY_EMPTY_COMMIT_DROP.
*/
enum replay_empty_commit_action empty;
+
+ /*
+ * Whether to linearize the commits (i.e. drop merge commits).
+ */
+ int linearize;
};
/* This struct is used as an out-parameter by `replay_revisions()`. */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 3353bc4a4d..34c038eab9 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -52,8 +52,12 @@ test_expect_success 'setup' '
test_merge P O --no-ff &&
git switch main &&
+ git switch --orphan unrelated &&
+ test_commit unrelated-root &&
+
git switch -c conflict B &&
- test_commit C.conflict C.t conflict
+ test_commit C.conflict C.t conflict &&
+ git branch -D unrelated
'
test_expect_success 'setup bare' '
@@ -97,6 +101,12 @@ test_expect_success '--advance and --contained cannot be used together' '
test_grep "cannot be used together" actual
'
+test_expect_success '--revert and --linearize cannot be used together' '
+ test_must_fail git replay --revert=main --linearize \
+ topic1..topic2 2>actual &&
+ test_grep "cannot be used together" actual
+'
+
test_expect_success 'cannot advance target ... ordering would be ill-defined' '
echo "fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect &&
test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
@@ -565,4 +575,76 @@ test_expect_success '--onto with --ref rejects multiple revision ranges' '
test_grep "cannot be used with multiple revision ranges" err
'
+test_expect_success 'replay to rebase merge commit with --linearize' '
+ git replay --ref-action=print --linearize \
+ --onto main I..topic-with-merge >result &&
+
+ test_line_count = 1 result &&
+
+ git log --format=%s $(cut -f 3 -d " " result) >actual &&
+ test_write_lines O N J M L B A >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'replay to rebase merge commit with --linearize down to the root commit' '
+ git replay --ref-action=print --linearize \
+ --onto unrelated-root topic-with-merge >result &&
+
+ test_line_count = 1 result &&
+
+ git log --format=%s $(cut -f 3 -d " " result) >actual &&
+ test_write_lines O N J I B A unrelated-root >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'replay to cherry-pick merge commit with --linearize' '
+ git replay --ref-action=print --linearize \
+ --advance main I..topic-with-merge >result &&
+
+ test_line_count = 1 result &&
+
+ git log --format=%s $(cut -f 3 -d " " result) >actual &&
+ test_write_lines O N J M L B A >expect &&
+ test_cmp expect actual &&
+
+ printf "update refs/heads/main " >expect &&
+ printf "%s " $(cut -f 3 -d " " result) >>expect &&
+ git rev-parse main >>expect &&
+ test_cmp expect result
+'
+
+test_expect_success 'replay --linearize produces the same patches' '
+ git replay --ref-action=print --linearize \
+ --onto main I..topic-with-merge >result &&
+
+ test_line_count = 1 result &&
+ tip=$(cut -f 3 -d " " result) &&
+
+ # range-diff does not care about the dropped merge,
+ # so the original commits (I..topic-with-merge)
+ # and the replayed chain (main..tip) must produce identical patches.
+ git range-diff I..topic-with-merge main..$tip >out &&
+ test_file_not_empty out &&
+ test_grep ! -v "=" out &&
+
+ git log --oneline main..$tip >out &&
+ test_line_count = 3 out
+'
+
+test_expect_success 'replay with --linearize to rebase multiple divergent branches' '
+ git replay --ref-action=print --linearize \
+ --onto main ^B topic2 topic-with-merge >result &&
+
+ test_line_count = 2 result &&
+ cut -f 3 -d " " result >new-branch-tips &&
+
+ git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+ test_write_lines E D C M L B A >expect &&
+ test_cmp expect actual &&
+
+ git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+ test_write_lines O N J I M L B A >expect &&
+ test_cmp expect actual
+'
+
test_done
--
2.53.0.1323.g189a785ab5
^ permalink raw reply related
* Re: [PATCH v3 4/4] connected: search promisor objects generically
From: Christian Couder @ 2026-06-26 6:55 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Patrick Steinhardt, git, Christian Couder
In-Reply-To: <xmqq4iiqfk0l.fsf@gitster.g>
On Thu, Jun 25, 2026 at 10:22 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Patrick Steinhardt <ps@pks.im> writes:
>
> > When performing connectivity checks we have to figure out whether any of
> > the new objects are promisor objects, as we cannot assume full
> > connectivity if so.
> >
> > This check is performed by iterating through all packfiles in the
> > repository and searching each of them for the given object. Of course,
> > this mechanism is quite specific to implementation details of the object
> > database, as we assume that it uses packfiles in the first place.
> >
> > Refactor the logic so that we instead use `odb_for_each_object_ext()`
> > with an object prefix filter and the `ODB_FOR_EACH_OBJECT_PROMISOR_ONLY`
> > flag. This will yield all objects that have the exact object name and
> > that are part of a promisor pack in a generic way.
> >
> > Add a test to verify that we indeed use the optimization.
>
> OK. The new test is a good way to catch the issue we noticed in the
> previous round, I guess. Looking good.
Yeah, it looks ready to me too.
Thanks.
^ permalink raw reply
* Re: [PATCH 05/11] reftable/block: fix OOB write with bogus inflated log size
From: Christian Couder @ 2026-06-26 7:48 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git, oxsignal
In-Reply-To: <20260624-pks-reftable-hardening-v1-5-66e4ce87c6b9@pks.im>
On Wed, Jun 24, 2026 at 10:24 AM Patrick Steinhardt <ps@pks.im> wrote:
> diff --git a/t/unit-tests/u-reftable-block.c b/t/unit-tests/u-reftable-block.c
> index f4bded7d26..40274af5c0 100644
> --- a/t/unit-tests/u-reftable-block.c
> +++ b/t/unit-tests/u-reftable-block.c
> @@ -456,3 +456,47 @@ void test_reftable_block__iterator(void)
> block_writer_release(&writer);
> reftable_buf_release(&data);
> }
> +
> +void test_reftable_block__corrupt_log_block_size(void)
> +{
> + struct reftable_block_source source = { 0 };
> + struct block_writer writer = {
> + .last_key = REFTABLE_BUF_INIT,
> + };
> + struct reftable_record rec = {
> + .type = REFTABLE_BLOCK_TYPE_LOG,
> + .u.log = {
> + .refname = (char *) "refs/heads/main",
> + .update_index = 1,
> + .value_type = REFTABLE_LOG_UPDATE,
> + },
> + };
> + struct reftable_block block = { 0 };
> + struct reftable_buf data;
> +
> + data.len = 1024;
> + REFTABLE_CALLOC_ARRAY(data.buf, data.len);
> + cl_assert(data.buf != NULL);
> +
> + cl_must_pass(block_writer_init(&writer, REFTABLE_BLOCK_TYPE_LOG,
> + (uint8_t *) data.buf, data.len,
> + 0, hash_size(REFTABLE_HASH_SHA1)));
> + cl_must_pass(block_writer_add(&writer, &rec));
> + cl_assert(block_writer_finish(&writer) > 0);
It looks like some of the block writing code above could be simplified
using an helper function like:
int cl_reftable_write_block(struct reftable_buf *buf, uint8_t block_type,
size_t block_size, uint32_t header_off,
struct reftable_record *recs, size_t nrecs)
{
struct block_writer writer = {
.last_key = REFTABLE_BUF_INIT,
};
int block_end;
REFTABLE_CALLOC_ARRAY(buf->buf, block_size);
cl_assert(buf->buf != NULL);
buf->len = block_size;
cl_must_pass(block_writer_init(&writer, block_type, (uint8_t *) buf->buf,
block_size, header_off,
hash_size(REFTABLE_HASH_SHA1)));
for (size_t i = 0; i < nrecs; i++)
cl_must_pass(block_writer_add(&writer, &recs[i]));
block_end = block_writer_finish(&writer);
cl_assert(block_end > 0);
block_writer_release(&writer);
return block_end;
}
This function could be introduced by a preparatory commit in
t/unit-tests/lib-reftable.{c,h}. It would be kind of similar to the
existing cl_reftable_write_to_buf() helper in those files.
It looks like it could already simplify existing tests like:
- test_reftable_block__log_read_write
- test_reftable_block__obj_read_write
- test_reftable_block__ref_read_write
- test_reftable_block__iterator
and it could simplify the new tests introduced by other patches in this series:
- 06/11 reftable/block: fix OOB read with bogus block size
- 07/11 reftable/block: fix OOB read with bogus restart count
- 09/11 reftable/block: fix OOB read with bogus restart offset
> + /*
> + * Log blocks store their inflated size as a big-endian 24-bit integer
> + * right after the one-byte block type. Rewrite it to claim a size that
> + * is smaller than the block header.
> + */
> + reftable_put_be24((uint8_t *) data.buf + 1, 1);
> +
> + block_source_from_buf(&source, &data);
> + cl_assert_equal_i(reftable_block_init(&block, &source, 0, 0, data.len,
> + REFTABLE_HASH_SIZE_SHA1, REFTABLE_BLOCK_TYPE_LOG),
> + REFTABLE_FORMAT_ERROR);
> +
> + reftable_block_release(&block);
> + block_writer_release(&writer);
> + reftable_buf_release(&data);
> +}
Otherwise the series looks great to me.
Thanks.
^ permalink raw reply
* [PATCH v2 0/2] environment: move excludes_file into repo_config_values
From: Tian Yuchen @ 2026-06-26 7:50 UTC (permalink / raw)
To: git
Cc: cirnovskyv, Tian Yuchen, Christian Couder, Ayush Chandekar,
Olamide Caleb Bello
This series continues the libification effort by migrating the global
string variable 'excludes_file' into 'struct repo_config_values'. Since
this is a dynamically allocated variable, the migration requires proper
heap memory management.
The series is structured in two commits:
- Abstract the XDG fallback lazy-loading logic out of dir.c into a proper
getter.
- Move the variables into the struct and introduce 'repo_config_values_clear()'.
Note on Submodules: A temporary shield 'if (repo != the_repository)' is
included in both the getter and the clear function. This prevents
uninitialized submodules from triggering the BUG() assertion.
(Inspiration: [1])
Changes since V1:
- fix a typo in the second commit.
- initialize excludes_file to NULL in repo_config_values_init().
- call FREE_AND_NULL() to free attributes_file as well in
repo_config_values_clear().
Thanks!
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Ayush Chandekar <ayu.chandekar@gmail.com>
Mentored-by: Olamide Caleb Bello <belkid98@gmail.com>
Signed-off-by: Tian Yuchen <cat@malon.dev>
[1] https://lore.kernel.org/git/c95a7730-7b14-4be0-a4e4-861b2f5430ea@gmail.com/
Tian Yuchen (2):
dir: encapsulate excludes_file lazy-load
environment: move excludes_file into repo_config_values
dir.c | 4 ++--
environment.c | 32 +++++++++++++++++++++++++++++---
environment.h | 13 ++++++++++++-
repository.c | 1 +
4 files changed, 44 insertions(+), 6 deletions(-)
--
2.43.0
^ permalink raw reply
* [PATCH v2 1/2] dir: encapsulate excludes_file lazy-load
From: Tian Yuchen @ 2026-06-26 7:50 UTC (permalink / raw)
To: git
Cc: cirnovskyv, Tian Yuchen, Christian Couder, Ayush Chandekar,
Olamide Caleb Bello
In-Reply-To: <20260626075037.532164-1-cat@malon.dev>
The global variable 'excludes_file' is used to track the path to the
global ignore file, 'core.excludesfile'. If this variable is NULL,
setup_standard_excludes() in dir.c forcefully evaluates and assigns
the XDG default path to it.
Introduce repo_excludes_file() as a getter to encapsulate this
lazy-loading logic. This prepares the variable to be safely moved
into 'struct repo_config_values' in the subsequent commit.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Ayush Chandekar <ayu.chandekar@gmail.com>
Mentored-by: Olamide Caleb Bello <belkid98@gmail.com>
Signed-off-by: Tian Yuchen <cat@malon.dev>
---
dir.c | 4 ++--
environment.c | 7 +++++++
environment.h | 5 +++++
3 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/dir.c b/dir.c
index 7a73690fbc..4f87a52b3c 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 ba2c60103f..8efcaeafa6 100644
--- a/environment.c
+++ b/environment.c
@@ -134,6 +134,13 @@ int is_bare_repository(void)
return is_bare_repository_cfg && !repo_get_work_tree(the_repository);
}
+const char *repo_excludes_file(struct repository *repo)
+{
+ if (!excludes_file)
+ excludes_file = xdg_config_home("ignore");
+ return excludes_file;
+}
+
int have_git_dir(void)
{
return startup_info->have_repository
diff --git a/environment.h b/environment.h
index 6f18286955..52d531e4ea 100644
--- a/environment.h
+++ b/environment.h
@@ -133,6 +133,11 @@ 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);
+/*
+ * TODO: This still relies on the global state.
+ */
+const char *repo_excludes_file(struct repository *repo);
+
void repo_config_values_init(struct repo_config_values *cfg);
/*
--
2.43.0
^ permalink raw reply related
* [PATCH v2 2/2] environment: move excludes_file into repo_config_values
From: Tian Yuchen @ 2026-06-26 7:50 UTC (permalink / raw)
To: git
Cc: cirnovskyv, Tian Yuchen, Christian Couder, Ayush Chandekar,
Olamide Caleb Bello
In-Reply-To: <20260626075037.532164-1-cat@malon.dev>
Continue the libification effort by moving the 'excludes_file' global
variable into 'struct repo_config_values'.
Since 'excludes_file' is a dynamically allocated string (char *), it
requires proper memory management. Introduce repo_config_values_clear()
to safely free the heap memory when repository instance is destroyed.
Note:
- 'if (repo != the_repository)' fallback logic is temporarily added
in both the getter and the clear function. This prevents calling
repo_config_values() on uninitialized submodules, which triggers BUG().
- 'attribute_file' is another string variable that was migrated
earlier. Its FREE_AND_NULL() call is also added to
repo_config_values_clear().
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Ayush Chandekar <ayu.chandekar@gmail.com>
Mentored-by: Olamide Caleb Bello <belkid98@gmail.com>
Signed-off-by: Tian Yuchen <cat@malon.dev>
---
environment.c | 31 +++++++++++++++++++++++++------
environment.h | 14 ++++++++++----
repository.c | 1 +
3 files changed, 36 insertions(+), 10 deletions(-)
diff --git a/environment.c b/environment.c
index 8efcaeafa6..b8dfa3e213 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;
@@ -136,9 +135,13 @@ int is_bare_repository(void)
const char *repo_excludes_file(struct repository *repo)
{
- if (!excludes_file)
- excludes_file = xdg_config_home("ignore");
- return excludes_file;
+ if (!repo || !repo->initialized || repo != the_repository)
+ return NULL;
+
+ if (!repo_config_values(repo)->excludes_file)
+ repo_config_values(repo)->excludes_file = xdg_config_home("ignore");
+
+ return repo_config_values(repo)->excludes_file;
}
int have_git_dir(void)
@@ -468,8 +471,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")) {
@@ -722,6 +725,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;
@@ -733,3 +737,18 @@ 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 repository *repo)
+{
+ struct repo_config_values *cfg;
+
+ if (repo != the_repository)
+ return;
+
+ cfg = repo_config_values(repo);
+ if (!cfg)
+ return;
+
+ FREE_AND_NULL(cfg->attributes_file);
+ FREE_AND_NULL(cfg->excludes_file);
+}
diff --git a/environment.h b/environment.h
index 52d531e4ea..2e8352de7f 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,13 +134,19 @@ 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);
-/*
- * TODO: This still relies on the global state.
- */
const char *repo_excludes_file(struct repository *repo);
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 repository *repo);
+
/*
* TODO: All the below state either explicitly or implicitly relies on
* `the_repository`. We should eventually get rid of these and make the
@@ -213,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
diff --git a/repository.c b/repository.c
index 187dd471c4..b31f1b7852 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);
if (repo->config) {
git_configset_clear(repo->config);
--
2.43.0
^ permalink raw reply related
* Re: [PATCH v5 0/4] history: add squash subcommand to fold a range
From: Phillip Wood @ 2026-06-26 8:52 UTC (permalink / raw)
To: Harald Nordgren via GitGitGadget, git; +Cc: Harald Nordgren, Patrick Steinhardt
In-Reply-To: <pull.2337.v5.git.git.1782338102.gitgitgadget@gmail.com>
Hi Harald
On 24/06/2026 22:54, Harald Nordgren via GitGitGadget wrote:
> Adds git history squash <revision-range> to fold a range of commits.
It would be helpful to give a bit more detail here about the command so
that the reader has an overview of what is actually being implemented.
- what does it do with fixup!, squash! and amend! commits? Can it use
the message from amend! commits to reword the commit?
- can the user reword the commit message?
- what happens if a merge commit inside the range has a parent outside
the range?
- what happens to branches that point to commits inside the range?
I had a quick play and found that it accepts ranges that containing a
single commit (e.g. @^!) where there is nothing to squash. It also
accepts ranges that are not ancestors of HEAD (e.g. checkout master and
run "git history squash --dry-run origin/seen^2^!") without printing an
error message. Only accepting a single argument is quite limiting as one
cannot say
git history squash ^:/base :/tip
Thanks
Phillip
> Changes in v5:
>
> * The range walk now uses --ancestry-path, so only commits descended from
> the base are folded; a single revision such as HEAD or HEAD~1 is now
> rejected as "not a <base>..<tip> range" rather than treated as a squash
> down to the root.
> * This adopts the --ancestry-path suggestion; the multi-base rejection is
> unchanged, so a side branch that forked before the base and merged in is
> still refused.
> * Added tests covering more merge topologies: two interior merges, a nested
> merge, an octopus merge, an octopus arm forked before the base, a merge
> among the descendants replayed above the range, and a ref pointing at an
> interior merge commit.
>
> Changes in v4:
>
> * git history squash now detects when another ref points at a commit inside
> the range being folded and refuses, with an advice.historyUpdateRefs hint
> to use --update-refs=head.
> * A merge inside the range is folded fine as long as the range has a single
> base; a range with merge commit at the tip or base also folds correctly.
> Only a range with more than one base is rejected.
>
> Changes in v3:
>
> * Moved the feature out of git rebase and into a new git history squash
> <revision-range> subcommand, per the list discussion. git rebase --squash
> is dropped.
> * Takes an arbitrary range (git history squash @~3.., git history squash
> @~5..@~2), folding it into the oldest commit and replaying any
> descendants on top.
> * Implemented as a single tree operation rather than picking each commit,
> so there are no repeated conflict stops (addresses Phillip's efficiency
> point).
> * A merge inside the range is folded fine, only a range with more than one
> base is rejected.
> * --reedit-message seeds the editor with every folded-in message, not just
> the oldest.
>
> Harald Nordgren (4):
> history: extract helper for a commit's parent tree
> history: give commit_tree_ext a message template
> history: add squash subcommand to fold a range
> history: re-edit a squash with every message
>
> Documentation/config/advice.adoc | 4 +
> Documentation/git-history.adoc | 26 ++
> advice.c | 1 +
> advice.h | 1 +
> builtin/history.c | 341 ++++++++++++++++++---
> t/meson.build | 1 +
> t/t3455-history-squash.sh | 497 +++++++++++++++++++++++++++++++
> 7 files changed, 833 insertions(+), 38 deletions(-)
> create mode 100755 t/t3455-history-squash.sh
>
>
> base-commit: 26d8d94e94df5535eecd036f16627493506a0614
> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2337%2FHaraldNordgren%2Frebase-fixup-fold-v5
> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2337/HaraldNordgren/rebase-fixup-fold-v5
> Pull-Request: https://github.com/git/git/pull/2337
>
> Range-diff vs v4:
>
> 1: fc2801c0b1 = 1: 0f1ae9b05a history: extract helper for a commit's parent tree
> 2: ee591e83b4 = 2: a97ffab1e6 history: give commit_tree_ext a message template
> 3: 80bfea642e ! 3: 04e18ef979 history: add squash subcommand to fold a range
> @@ builtin/history.c: out:
> + struct rev_info revs;
> + struct commit *commit, *base = NULL, *oldest = NULL, *tip = NULL;
> + struct strvec args = STRVEC_INIT;
> ++ size_t i;
> + int ret;
> +
> + repo_init_revisions(repo, &revs, NULL);
> @@ builtin/history.c: out:
> + strvec_push(&args, "--reverse");
> + strvec_push(&args, "--topo-order");
> + strvec_push(&args, "--boundary");
> ++ strvec_push(&args, "--ancestry-path");
> + strvec_push(&args, range);
> + setup_revisions_from_strvec(&args, &revs, NULL);
> + if (args.nr != 1) {
> @@ builtin/history.c: out:
> + goto out;
> + }
> +
> ++ /*
> ++ * A squash needs a base to reparent onto, so the argument has to
> ++ * exclude something, as in "<base>..<tip>". A single revision has no
> ++ * such bottom commit and cannot be squashed.
> ++ */
> ++ for (i = 0; i < revs.cmdline.nr; i++)
> ++ if (revs.cmdline.rev[i].flags & UNINTERESTING)
> ++ break;
> ++ if (i == revs.cmdline.nr) {
> ++ ret = error(_("'%s' is not a '<base>..<tip>' range"), range);
> ++ goto out;
> ++ }
> ++
> + if (prepare_revision_walk(&revs) < 0) {
> + ret = error(_("error preparing revisions"));
> + goto out;
> @@ builtin/history.c: out:
> + goto out;
> + }
> +
> -+ if (!base) {
> -+ ret = error(_("cannot squash the root commit"));
> -+ goto out;
> -+ }
> ++ if (!base)
> ++ BUG("a non-empty range must have a boundary commit");
> +
> + *base_out = base;
> + *oldest_out = oldest;
> @@ t/t3455-history-squash.sh (new)
> + test_grep "the range .* is empty" err
> +'
> +
> -+test_expect_success 'errors when the range includes the root commit' '
> ++test_expect_success 'errors on a single revision that is not a range' '
> + test_must_fail git history squash HEAD 2>err &&
> -+ test_grep "cannot squash the root commit" err
> ++ test_grep "is not a .*range" err &&
> ++ test_must_fail git history squash HEAD~1 2>err &&
> ++ test_grep "is not a .*range" err
> +'
> +
> +test_expect_success 'squashes a range into a single commit without changing the tree' '
> @@ t/t3455-history-squash.sh (new)
> + test_path_is_file inner
> +'
> +
> ++test_expect_success 'folds a merge of a branch that forked at the base' '
> ++ git reset --hard start &&
> ++ git checkout -b base-fork-side &&
> ++ test_commit --no-tag base-fork-side side x &&
> ++ git checkout - &&
> ++ test_commit --no-tag base-fork-main file b &&
> ++ git merge --no-ff -m "merge base-fork-side" base-fork-side &&
> ++ git branch -D base-fork-side &&
> ++ test_commit --no-tag base-fork-tail file c &&
> ++ tip_tree=$(git rev-parse HEAD^{tree}) &&
> ++
> ++ git history squash start.. &&
> ++
> ++ git rev-list --count start..HEAD >count &&
> ++ echo 1 >expect &&
> ++ test_cmp expect count &&
> ++ test_cmp_rev start HEAD^ &&
> ++ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
> ++ test_path_is_file side
> ++'
> ++
> +test_expect_success 'folds a range whose tip is a merge commit' '
> + git reset --hard start &&
> + test_commit --no-tag tipmerge-base file b &&
> @@ t/t3455-history-squash.sh (new)
> + test_cmp_rev "$merged" HEAD
> +'
> +
> ++test_expect_success 'folds a range with two interior merges' '
> ++ git reset --hard start &&
> ++ test_commit --no-tag two-merge-a file a1 &&
> ++ git checkout -b two-merge-s1 &&
> ++ test_commit --no-tag two-merge-s1 s1 x &&
> ++ git checkout - &&
> ++ git merge --no-ff -m "merge s1" two-merge-s1 &&
> ++ test_commit --no-tag two-merge-b file b1 &&
> ++ git checkout -b two-merge-s2 &&
> ++ test_commit --no-tag two-merge-s2 s2 y &&
> ++ git checkout - &&
> ++ git merge --no-ff -m "merge s2" two-merge-s2 &&
> ++ git branch -D two-merge-s1 two-merge-s2 &&
> ++ tip_tree=$(git rev-parse HEAD^{tree}) &&
> ++
> ++ git history squash start.. &&
> ++
> ++ git rev-list --count start..HEAD >count &&
> ++ echo 1 >expect &&
> ++ test_cmp expect count &&
> ++ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
> ++ test_path_is_file s1 &&
> ++ test_path_is_file s2
> ++'
> ++
> ++test_expect_success 'folds a range with a nested merge' '
> ++ git reset --hard start &&
> ++ main=$(git symbolic-ref --short HEAD) &&
> ++ git checkout -b nested-outer &&
> ++ test_commit --no-tag nested-outer outer x &&
> ++ git checkout -b nested-inner &&
> ++ test_commit --no-tag nested-inner inner y &&
> ++ git checkout nested-outer &&
> ++ git merge --no-ff -m "merge inner" nested-inner &&
> ++ git checkout "$main" &&
> ++ test_commit --no-tag nested-main file b1 &&
> ++ git merge --no-ff -m "merge outer" nested-outer &&
> ++ git branch -D nested-outer nested-inner &&
> ++ tip_tree=$(git rev-parse HEAD^{tree}) &&
> ++
> ++ git history squash start.. &&
> ++
> ++ git rev-list --count start..HEAD >count &&
> ++ echo 1 >expect &&
> ++ test_cmp expect count &&
> ++ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
> ++ test_path_is_file outer &&
> ++ test_path_is_file inner
> ++'
> ++
> ++test_expect_success 'folds a range with an octopus merge' '
> ++ git reset --hard start &&
> ++ main=$(git symbolic-ref --short HEAD) &&
> ++ test_commit --no-tag octo-base file a1 &&
> ++ git checkout -b octo-1 &&
> ++ test_commit --no-tag octo-1 o1 x &&
> ++ git checkout "$main" &&
> ++ git checkout -b octo-2 &&
> ++ test_commit --no-tag octo-2 o2 y &&
> ++ git checkout "$main" &&
> ++ git merge --no-ff -m octopus octo-1 octo-2 &&
> ++ git branch -D octo-1 octo-2 &&
> ++ tip_tree=$(git rev-parse HEAD^{tree}) &&
> ++
> ++ git history squash start.. &&
> ++
> ++ git rev-list --count start..HEAD >count &&
> ++ echo 1 >expect &&
> ++ test_cmp expect count &&
> ++ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
> ++ test_path_is_file o1 &&
> ++ test_path_is_file o2
> ++'
> ++
> ++test_expect_success 'refuses an octopus merge with an arm forked before the base' '
> ++ git reset --hard start &&
> ++ main=$(git symbolic-ref --short HEAD) &&
> ++ git checkout -b octo-pre &&
> ++ test_commit octo-pre-side pside x &&
> ++ git checkout "$main" &&
> ++ test_commit octo-pre-main file b1 &&
> ++ octo_base=$(git rev-parse HEAD) &&
> ++ git checkout -b octo-within &&
> ++ test_commit --no-tag octo-within wside y &&
> ++ git checkout "$main" &&
> ++ git merge --no-ff -m octopus octo-pre octo-within &&
> ++ merged=$(git rev-parse HEAD) &&
> ++ git branch -D octo-pre octo-within &&
> ++
> ++ test_must_fail git history squash "$octo_base.." 2>err &&
> ++ test_grep "more than one base" err &&
> ++ test_cmp_rev "$merged" HEAD
> ++'
> ++
> ++test_expect_success 'refuses when a descendant above the range is a merge' '
> ++ git reset --hard start &&
> ++ main=$(git symbolic-ref --short HEAD) &&
> ++ test_commit --no-tag desc-base file b &&
> ++ git tag desc-tip &&
> ++ git checkout -b desc-above &&
> ++ test_commit --no-tag desc-above above x &&
> ++ git checkout "$main" &&
> ++ test_commit --no-tag desc-main file c &&
> ++ git merge --no-ff -m "merge desc-above" desc-above &&
> ++ git branch -D desc-above &&
> ++ head_before=$(git rev-parse HEAD) &&
> ++
> ++ test_must_fail git history squash start..desc-tip 2>err &&
> ++ test_grep "merge commits is not supported" err &&
> ++ test_cmp_rev "$head_before" HEAD
> ++'
> ++
> ++test_expect_success 'refuses to fold a range a ref points into at a merge' '
> ++ git reset --hard start &&
> ++ main=$(git symbolic-ref --short HEAD) &&
> ++ test_commit --no-tag refmerge-base file b &&
> ++ git checkout -b refmerge-side &&
> ++ test_commit --no-tag refmerge-side side x &&
> ++ git checkout "$main" &&
> ++ test_commit --no-tag refmerge-main file c &&
> ++ git merge --no-ff -m "interior merge" refmerge-side &&
> ++ git branch -D refmerge-side &&
> ++ git branch at-merge HEAD &&
> ++ test_commit --no-tag refmerge-tail file d &&
> ++ head_before=$(git rev-parse HEAD) &&
> ++
> ++ test_must_fail git history squash start.. 2>err &&
> ++ test_grep "at-merge" err &&
> ++ test_grep "points into the squashed range" err &&
> ++ test_cmp_rev "$head_before" HEAD &&
> ++
> ++ git branch -D at-merge
> ++'
> ++
> +test_done
> 4: 85c7817d7e = 4: a758e1f084 history: re-edit a squash with every message
>
^ permalink raw reply
* Re: [PATCH v5 0/4] history: add squash subcommand to fold a range
From: Harald Nordgren @ 2026-06-26 9:57 UTC (permalink / raw)
To: phillip.wood; +Cc: Harald Nordgren via GitGitGadget, git, Patrick Steinhardt
In-Reply-To: <d37e8f4f-d1f9-45aa-8c95-ebe676d54671@gmail.com>
On Fri, Jun 26, 2026 at 10:53 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> Hi Harald
>
> On 24/06/2026 22:54, Harald Nordgren via GitGitGadget wrote:
> > Adds git history squash <revision-range> to fold a range of commits.
>
> It would be helpful to give a bit more detail here about the command so
> that the reader has an overview of what is actually being implemented.
>
> - what does it do with fixup!, squash! and amend! commits? Can it use
> the message from amend! commits to reword the commit?
> - can the user reword the commit message?
> - what happens if a merge commit inside the range has a parent outside
> the range?
> - what happens to branches that point to commits inside the range?
>
> I had a quick play and found that it accepts ranges that containing a
> single commit (e.g. @^!) where there is nothing to squash. It also
> accepts ranges that are not ancestors of HEAD (e.g. checkout master and
> run "git history squash --dry-run origin/seen^2^!") without printing an
> error message.
Good points, I will take a look at clarifying or giving an error (like
in the case of ancestor not in history of HEAD).
Only accepting a single argument is quite limiting as one
> cannot say
>
> git history squash ^:/base :/tip
I don't understand why this is limiting? It thought it was clear that
it should be one argument REF1..REF2 ? What does '^:/base :/tip'
achieve that '^:/base..:/tip' cannot?
Harald
^ permalink raw reply
* Re: [RFH] Why do osx CI jobs so unreliable?
From: Patrick Steinhardt @ 2026-06-26 10:50 UTC (permalink / raw)
To: Jeff King; +Cc: Michael Montalbo, git, Junio C Hamano
In-Reply-To: <20260626051657.GB3138423@coredump.intra.peff.net>
On Fri, Jun 26, 2026 at 01:16:57AM -0400, Jeff King wrote:
> On Thu, Jun 25, 2026 at 08:27:35PM -0700, Michael Montalbo wrote:
>
> > I think that is the trigger for issues we've been seeing. I spent
> > some time investigating the Apache side over the last week and maybe
> > found a mod_http2 bug, which I filed upstream with a potential fix:
> >
> > bug: https://bz.apache.org/bugzilla/show_bug.cgi?id=70131
> > fix: https://github.com/mmontalbo/httpd/pull/2
>
> Thanks both of you for digging into this. I'm not familiar enough with
> Apache's code to pass confident judgement, but your findings certainly
> convinced me that this is just an apache bug.
The bug manifests both with HTTP/1.1 and HTTP/2 though, so this wouldn't
fully fix the flakes we see, right?
> > Given there could be a potential reliability issue with an upstream
> > dependency like Apache, I was considering what mitigation strategies
> > might help:
> > [...]
>
> Depending on how widespread the Apache bug is, another option might just
> be: do nothing and wait for it to get fixed.
>
> Trying to make the wedged state fail fast and loudly is mostly just
> punting on the problem. We'd still see spurious failures. We've so far
> resisted the urge to do any automatic flaky-test retries, preferring
> instead to just try to root out the flakes. I'm a little hesitant to
> start now, because I think our strategy has mostly been good so far, and
> I've seen some horrible counter-examples where flakes and retries become
> a routine drag on development (and I'm afraid that accommodating flakes
> might make them more common).
I agree. I'm not a fan of retry logic, as every flaky test may mask an
actual bug that we haven't fully investigated yet.
> > - Make slow tests faster by optimizing the test itself and/or
> > the test runner configuration (e.g., job number matching
> > cores) so wedges become less likely.
>
> It sounds like the bad state is triggered when Apache hits a timeout,
> and we hit that timeout because the system is slow or busy. We could try
> to make things less slow, but would it work equally well to increase
> that timeout?
I was also wondering whether we can maybe work around the issue by
increasing the Apache timeout value. That sounds like an easy potential
solution to try, and from all we've discovered so far it doesn't feel
like this is something we can address on the Git side.
Patrick
^ permalink raw reply
* Re: [PATCH GSoC v14 02/13] git-compat-util: add strtoul_szt() with error handling
From: Pablo Sabater @ 2026-06-26 12:00 UTC (permalink / raw)
To: Junio C Hamano
Cc: git, chandrapratap3519, chriscool, eric.peijian, jltobler,
karthik.188, peff, toon
In-Reply-To: <xmqqjyrme393.fsf@gitster.g>
El jue, 25 jun 2026 a las 23:09, Junio C Hamano (<gitster@pobox.com>) escribió:
>
> Pablo Sabater <pabloosabaterr@gmail.com> writes:
>
> > From: Eric Ju <eric.peijian@gmail.com>
> >
> > We already have strtoul_ui() and similar functions that provide proper
> > error handling using strtoul from the standard library. However,
> > there isn't currently a variant that returns an unsigned long.
>
> But this one no longer returns an unsigned long anymore ;-)
True, I missed that.
>
> > This variant is needed in a subsequent commit to enable returning an
> > size_t with proper error handling.
>
> I think it would allow a lot of code paths that want to deal with
> size_t not to worry about "is ulong large enough?" to have a
> function like this, but for that to happen, the implementation of
> the function must carefully think through if these steps do sensible
> things on platforms with too small ulong (which often is OK when we
> are coming from decimal string to ulong and then to size_t) and too
> large ulong (which is not OK, when coming from decimal string to
> ulong which might be fine, but will bust the size of the final
> type), etc.
Ok, so the strtoul_szt() is not a bad idea but it is not ok how I did
it. What about using uintmax_t and strtoumax() and after that (before
casting) check if it fits into a size_t?
Something like:
static inline int strtoumax_szt(char const *s, int base, size_t *result)
{
uintmax_t val;
char *p;
errno = 0;
/* negative values would be accepted by strtoul */
if (strchr(s, '-'))
return -1;
val = strtoumax(s, &p, base);
if ((errno || *p || p == s) || val > SIZE_MAX)
return -1;
*result = val;
return 0;
}
Alternatively I could go back to the unsigned long version and make
the relevant checks on the caller which is only one place at
`fetch_object_info()`
>
> Also, would it make sense to add yet another "static inline" like
> this? After the dust settles, we may want to rethink these strtoX
> wrappers we have, benchmark, and possibly make them into a proper
> library function, not "static inline" that may bloat the runtime.
Yeah, I thought the same, I kept it on the header also to avoid
bloating this series too much because it's kinda big already. But a
follow-up after would be a good idea tho.
>
> > diff --git a/git-compat-util.h b/git-compat-util.h
> > index 8809776407..7f417f1acf 100644
> > --- a/git-compat-util.h
> > +++ b/git-compat-util.h
> > @@ -975,6 +975,26 @@ static inline int strtoul_ui(char const *s, int base, unsigned int *result)
> > return 0;
> > }
> >
> > +/*
> > + * Convert a string to a size_t using the standard library's strtoul, with
> > + * additional error handling to ensure robustness.
> > + */
> > +static inline int strtoul_szt(char const *s, int base, size_t *result)
> > +{
> > + unsigned long ul;
> > + char *p;
> > +
> > + errno = 0;
> > + /* negative values would be accepted by strtoul */
> > + if (strchr(s, '-'))
> > + return -1;
> > + ul = strtoul(s, &p, base);
> > + if (errno || *p || p == s)
> > + return -1;
> > + *result = ul;
> > + return 0;
> > +}
> > +
> > static inline int strtol_i(char const *s, int base, int *result)
> > {
> > long ul;
Thanks for the review,
Pablo.
^ permalink raw reply
* Re: [PATCH 2/2] odb: introduce `odb_prepare()`
From: Toon Claes @ 2026-06-26 12:09 UTC (permalink / raw)
To: Patrick Steinhardt, git
In-Reply-To: <20260622-b4-pks-odb-generalize-prepare-v1-2-d2a5c5d13144@pks.im>
Patrick Steinhardt <ps@pks.im> writes:
> Introduce `odb_prepare()` as a simple wrapper to prepare alternates and
> then prepare each individual source. Adapt git-grep(1) to use it.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> builtin/grep.c | 9 ++-------
> odb.c | 18 ++++++++++++------
> odb.h | 8 ++++++--
> 3 files changed, 20 insertions(+), 15 deletions(-)
>
> diff --git a/builtin/grep.c b/builtin/grep.c
> index 7361bf071e..a7252d56a1 100644
> --- a/builtin/grep.c
> +++ b/builtin/grep.c
> @@ -1356,13 +1356,8 @@ int cmd_grep(int argc,
> if (recurse_submodules)
> repo_read_gitmodules(the_repository, 1);
>
> - if (startup_info->have_repository) {
> - struct odb_source *source;
> -
> - odb_prepare_alternates(the_repository->objects);
> - for (source = the_repository->objects->sources; source; source = source->next)
> - odb_source_prepare(source, 0);
> - }
> + if (startup_info->have_repository)
> + odb_prepare(the_repository->objects, 0);
>
> start_threads(&opt);
> } else {
> diff --git a/odb.c b/odb.c
> index 7b45390e12..11414c49a8 100644
> --- a/odb.c
> +++ b/odb.c
> @@ -1070,7 +1070,7 @@ void odb_free(struct object_database *o)
> free(o);
> }
>
> -void odb_reprepare(struct object_database *o)
> +void odb_prepare(struct object_database *o, enum odb_prepare_flags flags)
> {
> struct odb_source *source;
>
> @@ -1082,13 +1082,19 @@ void odb_reprepare(struct object_database *o)
> * the linked list, so existing odbs will continue to exist for
> * the lifetime of the process.
> */
> - o->loaded_alternates = 0;
> - odb_prepare_alternates(o);
> + if (flags & ODB_PREPARE_FLUSH_CACHES) {
> + o->loaded_alternates = 0;
> + o->object_count_valid = 0;
> + }
>
> + odb_prepare_alternates(o);
> for (source = o->sources; source; source = source->next)
> - odb_source_prepare(source, ODB_PREPARE_FLUSH_CACHES);
> -
> - o->object_count_valid = 0;
> + odb_source_prepare(source, flags);
>
> obj_read_unlock();
> }
> +
> +void odb_reprepare(struct object_database *o)
> +{
> + odb_prepare(o, ODB_PREPARE_FLUSH_CACHES);
> +}
> diff --git a/odb.h b/odb.h
> index c14c9030e4..b1c0f3767b 100644
> --- a/odb.h
> +++ b/odb.h
> @@ -133,9 +133,13 @@ enum odb_prepare_flags {
> };
>
> /*
> - * Clear caches, reload alternates and then reload object sources so that new
> - * objects may become accessible.
> + * Prepare the object database for use. Calling this function is generally not
> + * needed, but can be useful in case the caller wants to pre-open individual
> + * sources.
> */
> +void odb_prepare(struct object_database *o, enum odb_prepare_flags flags);
> +
> +/* Equivalent to `odb_prepare(o, ODB_PREPARE_FLUSH_CACHES)`. */
> void odb_reprepare(struct object_database *o);
According to my grep results are there 17 callsites for odb_reprepare(),
then I agree it makes sense to create this wrapper.
--
Cheers,
Toon
^ permalink raw reply
* Re: [PATCH 1/2] odb/source: generalize `reprepare()` callback
From: Toon Claes @ 2026-06-26 12:10 UTC (permalink / raw)
To: Patrick Steinhardt, git
In-Reply-To: <20260622-b4-pks-odb-generalize-prepare-v1-1-d2a5c5d13144@pks.im>
Patrick Steinhardt <ps@pks.im> writes:
> The `reprepare()` callback function can be used to flush caches of a
> given object source and then prepare it anew. This is for example used
> when a concurrent process may have written new objects. Ultimately, this
> can be seen as doing two separate steps:
>
> 1. We drop any caches.
>
> 2. We prepare the source.
>
> We have one callsite in git-grep(1) though that really only want to do
> (2). This is done by reaching into the "files" backend directly and then
> calling `odb_source_packed_prepare()`, which of course may not work with
> alternate backends.
>
> We could in theory just call `reprepare()` here, and that would likely
> not have any significant downside. But this would certainly feel like a
> code smell.
>
> Instead, generalize the `reprepare()` callback to `prepare()` with a
> flag that optionally instructs the backend to also flush the caches,
> which allows us to drop the external `odb_source_packed_prepare()`
> declaration.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> builtin/grep.c | 9 +++------
> midx.c | 2 +-
> odb.c | 2 +-
> odb.h | 8 ++++++++
> odb/source-files.c | 9 +++++----
> odb/source-inmemory.c | 5 +++--
> odb/source-loose.c | 8 +++++---
> odb/source-packed.c | 34 ++++++++++++++++------------------
> odb/source-packed.h | 9 ---------
> odb/source.h | 16 +++++++++-------
> packfile.c | 2 +-
> 11 files changed, 52 insertions(+), 52 deletions(-)
>
> diff --git a/builtin/grep.c b/builtin/grep.c
> index 8080d1bf5e..7361bf071e 100644
> --- a/builtin/grep.c
> +++ b/builtin/grep.c
> @@ -25,12 +25,11 @@
> #include "setup.h"
> #include "submodule.h"
> #include "submodule-config.h"
> -#include "object-file.h"
> #include "object-name.h"
> #include "odb.h"
> +#include "odb/source.h"
> #include "oid-array.h"
> #include "oidset.h"
> -#include "packfile.h"
> #include "pager.h"
> #include "path.h"
> #include "promisor-remote.h"
> @@ -1361,10 +1360,8 @@ int cmd_grep(int argc,
> struct odb_source *source;
>
> odb_prepare_alternates(the_repository->objects);
> - for (source = the_repository->objects->sources; source; source = source->next) {
> - struct odb_source_files *files = odb_source_files_downcast(source);
So you're downcasting inside the implementation by the backends itself.
That makes sense, but would it be worth to say something about that in
the commit message?
> - odb_source_packed_prepare(files->packed);
> - }
> + for (source = the_repository->objects->sources; source; source = source->next)
> + odb_source_prepare(source, 0);
> }
>
> start_threads(&opt);
> diff --git a/midx.c b/midx.c
> index cc6b94f9dd..76c3f92cc3 100644
> --- a/midx.c
> +++ b/midx.c
> @@ -101,7 +101,7 @@ static int midx_read_object_offsets(const unsigned char *chunk_start,
>
> struct multi_pack_index *get_multi_pack_index(struct odb_source_packed *source)
> {
> - odb_source_packed_prepare(source);
> + odb_source_prepare(&source->base, 0);
> return source->midx;
> }
>
> diff --git a/odb.c b/odb.c
> index 965ef68e4e..7b45390e12 100644
> --- a/odb.c
> +++ b/odb.c
> @@ -1086,7 +1086,7 @@ void odb_reprepare(struct object_database *o)
> odb_prepare_alternates(o);
>
> for (source = o->sources; source; source = source->next)
> - odb_source_reprepare(source);
> + odb_source_prepare(source, ODB_PREPARE_FLUSH_CACHES);
>
> o->object_count_valid = 0;
>
> diff --git a/odb.h b/odb.h
> index 0030467a52..c14c9030e4 100644
> --- a/odb.h
> +++ b/odb.h
> @@ -124,6 +124,14 @@ void odb_free(struct object_database *o);
> */
> void odb_close(struct object_database *o);
>
> +enum odb_prepare_flags {
> + /*
> + * Flush caches, reload alternates and then re-prepare each object
> + * source so that new objects may become accessible.
> + */
> + ODB_PREPARE_FLUSH_CACHES = (1 << 0),
> +};
> +
> /*
> * Clear caches, reload alternates and then reload object sources so that new
> * objects may become accessible.
> diff --git a/odb/source-files.c b/odb/source-files.c
> index 3bc6419dd7..ad9e0b52f9 100644
> --- a/odb/source-files.c
> +++ b/odb/source-files.c
> @@ -41,11 +41,12 @@ static void odb_source_files_close(struct odb_source *source)
> odb_source_close(&files->packed->base);
> }
>
> -static void odb_source_files_reprepare(struct odb_source *source)
> +static void odb_source_files_prepare(struct odb_source *source,
> + enum odb_prepare_flags flags)
> {
> struct odb_source_files *files = odb_source_files_downcast(source);
> - odb_source_reprepare(&files->loose->base);
> - odb_source_reprepare(&files->packed->base);
> + odb_source_prepare(&files->loose->base, flags);
> + odb_source_prepare(&files->packed->base, flags);
> }
>
> static int odb_source_files_read_object_info(struct odb_source *source,
> @@ -273,7 +274,7 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb,
>
> files->base.free = odb_source_files_free;
> files->base.close = odb_source_files_close;
> - files->base.reprepare = odb_source_files_reprepare;
> + files->base.prepare = odb_source_files_prepare;
> files->base.read_object_info = odb_source_files_read_object_info;
> files->base.read_object_stream = odb_source_files_read_object_stream;
> files->base.for_each_object = odb_source_files_for_each_object;
> diff --git a/odb/source-inmemory.c b/odb/source-inmemory.c
> index e004566d76..cc5e9e62cb 100644
> --- a/odb/source-inmemory.c
> +++ b/odb/source-inmemory.c
> @@ -325,7 +325,8 @@ static void odb_source_inmemory_close(struct odb_source *source UNUSED)
> {
> }
>
> -static void odb_source_inmemory_reprepare(struct odb_source *source UNUSED)
> +static void odb_source_inmemory_prepare(struct odb_source *source UNUSED,
> + enum odb_prepare_flags flags UNUSED)
> {
> }
>
> @@ -365,7 +366,7 @@ struct odb_source_inmemory *odb_source_inmemory_new(struct object_database *odb)
>
> source->base.free = odb_source_inmemory_free;
> source->base.close = odb_source_inmemory_close;
> - source->base.reprepare = odb_source_inmemory_reprepare;
> + source->base.prepare = odb_source_inmemory_prepare;
> source->base.read_object_info = odb_source_inmemory_read_object_info;
> source->base.read_object_stream = odb_source_inmemory_read_object_stream;
> source->base.for_each_object = odb_source_inmemory_for_each_object;
> diff --git a/odb/source-loose.c b/odb/source-loose.c
> index 7d7ea2fb84..af46316e35 100644
> --- a/odb/source-loose.c
> +++ b/odb/source-loose.c
> @@ -672,10 +672,12 @@ static void odb_source_loose_clear_cache(struct odb_source_loose *loose)
> sizeof(loose->subdir_seen));
> }
>
> -static void odb_source_loose_reprepare(struct odb_source *source)
> +static void odb_source_loose_prepare(struct odb_source *source,
> + enum odb_prepare_flags flags)
> {
> struct odb_source_loose *loose = odb_source_loose_downcast(source);
> - odb_source_loose_clear_cache(loose);
> + if (flags & ODB_PREPARE_FLUSH_CACHES)
> + odb_source_loose_clear_cache(loose);
> }
>
> static void odb_source_loose_close(struct odb_source *source UNUSED)
> @@ -716,7 +718,7 @@ struct odb_source_loose *odb_source_loose_new(struct object_database *odb,
>
> loose->base.free = odb_source_loose_free;
> loose->base.close = odb_source_loose_close;
> - loose->base.reprepare = odb_source_loose_reprepare;
> + loose->base.prepare = odb_source_loose_prepare;
> loose->base.read_object_info = odb_source_loose_read_object_info;
> loose->base.read_object_stream = odb_source_loose_read_object_stream;
> loose->base.for_each_object = odb_source_loose_for_each_object;
> diff --git a/odb/source-packed.c b/odb/source-packed.c
> index 42c28fba0e..fa5a072488 100644
> --- a/odb/source-packed.c
> +++ b/odb/source-packed.c
> @@ -15,7 +15,7 @@ static int find_pack_entry(struct odb_source_packed *store,
> {
> struct packfile_list_entry *l;
>
> - odb_source_packed_prepare(store);
> + odb_source_prepare(&store->base, 0);
Why are you not using ODB_PREPARE_FLUSH_CACHES here? It used to do
before?
> if (store->midx && fill_midx_entry(store->midx, oid, e))
> return 1;
>
> @@ -47,7 +47,7 @@ static int odb_source_packed_read_object_info(struct odb_source *source,
> * been added since the last time we have prepared the packfile store.
> */
> if (flags & OBJECT_INFO_SECOND_READ)
> - odb_source_reprepare(source);
> + odb_source_prepare(source, ODB_PREPARE_FLUSH_CACHES);
I think the new code is correct, but why wasn't `packed` used here in
the past? The old odb_source_reprepare() expected a downcasted, didn't
it?
>
> if (!find_pack_entry(packed, oid, &e))
> return 1;
> @@ -668,27 +668,25 @@ static int sort_pack(const struct packfile_list_entry *a,
> return -1;
> }
>
> -void odb_source_packed_prepare(struct odb_source_packed *source)
> +static void odb_source_packed_prepare(struct odb_source *source,
> + enum odb_prepare_flags flags)
> {
> - if (source->initialized)
> + struct odb_source_packed *packed = odb_source_packed_downcast(source);
> +
> + if (flags & ODB_PREPARE_FLUSH_CACHES)
> + packed->initialized = false;
> + if (packed->initialized)
> return;
>
> - prepare_multi_pack_index_one(source);
> - prepare_packed_git_one(source);
> + prepare_multi_pack_index_one(packed);
> + prepare_packed_git_one(packed);
>
> - sort_packs(&source->packs.head, sort_pack);
> - for (struct packfile_list_entry *e = source->packs.head; e; e = e->next)
> + sort_packs(&packed->packs.head, sort_pack);
> + for (struct packfile_list_entry *e = packed->packs.head; e; e = e->next)
> if (!e->next)
> - source->packs.tail = e;
> + packed->packs.tail = e;
>
> - source->initialized = true;
> -}
> -
> -static void odb_source_packed_reprepare(struct odb_source *source)
> -{
> - struct odb_source_packed *packed = odb_source_packed_downcast(source);
> - packed->initialized = false;
> - odb_source_packed_prepare(packed);
> + packed->initialized = true;
> }
>
> static void odb_source_packed_reparent(const char *name UNUSED,
> @@ -744,7 +742,7 @@ struct odb_source_packed *odb_source_packed_new(struct object_database *odb,
>
> packed->base.free = odb_source_packed_free;
> packed->base.close = odb_source_packed_close;
> - packed->base.reprepare = odb_source_packed_reprepare;
> + packed->base.prepare = odb_source_packed_prepare;
> packed->base.read_object_info = odb_source_packed_read_object_info;
> packed->base.read_object_stream = odb_source_packed_read_object_stream;
> packed->base.for_each_object = odb_source_packed_for_each_object;
> diff --git a/odb/source-packed.h b/odb/source-packed.h
> index 88994098c1..d5230ac68c 100644
> --- a/odb/source-packed.h
> +++ b/odb/source-packed.h
> @@ -82,13 +82,4 @@ static inline struct odb_source_packed *odb_source_packed_downcast(struct odb_so
> return container_of(source, struct odb_source_packed, base);
> }
>
> -/*
> - * Prepare the source by loading packfiles and multi-pack indices for
> - * all alternates. This becomes a no-op if the source is already prepared.
> - *
> - * It shouldn't typically be necessary to call this function directly, as
> - * functions that access the source know to prepare it.
> - */
> -void odb_source_packed_prepare(struct odb_source_packed *source);
> -
> #endif
> diff --git a/odb/source.h b/odb/source.h
> index b9a7642b2c..bbf1da3819 100644
> --- a/odb/source.h
> +++ b/odb/source.h
> @@ -83,11 +83,12 @@ struct odb_source {
> void (*close)(struct odb_source *source);
>
> /*
> - * This callback is expected to clear underlying caches of the object
> - * database source. The function is called when the repository has for
> - * example just been repacked so that new objects will become visible.
> + * This callback is expected to prepare the source so that it becomes
> + * ready for use. It optionally clears underlying caches of the object
> + * database source.
> */
> - void (*reprepare)(struct odb_source *source);
> + void (*prepare)(struct odb_source *source,
> + enum odb_prepare_flags flags);
>
> /*
> * This callback is expected to read object information from the object
> @@ -308,13 +309,14 @@ static inline void odb_source_close(struct odb_source *source)
> }
>
> /*
> - * Reprepare the object database source and clear any caches. Depending on the
> + * Prepare the object database source and clear any caches. Depending on the
> * backend used this may have the effect that concurrently-written objects
> * become visible.
> */
> -static inline void odb_source_reprepare(struct odb_source *source)
> +static inline void odb_source_prepare(struct odb_source *source,
> + enum odb_prepare_flags flags)
> {
> - source->reprepare(source);
> + source->prepare(source, flags);
> }
>
> /*
> diff --git a/packfile.c b/packfile.c
> index 59cee7925d..d78fae981a 100644
> --- a/packfile.c
> +++ b/packfile.c
> @@ -855,7 +855,7 @@ void for_each_file_in_pack_dir(const char *objdir,
>
> struct packfile_list_entry *packfile_store_get_packs(struct odb_source_packed *store)
> {
> - odb_source_packed_prepare(store);
> + odb_source_prepare(&store->base, 0);
>
> if (store->midx) {
> struct multi_pack_index *m = store->midx;
>
> --
> 2.55.0.rc1.745.g43192e7977.dirty
>
>
--
Cheers,
Toon
^ permalink raw reply
* Re: [PATCH GSoC v14 06/13] fetch-pack: move function to connect.c
From: Chandra Pratap @ 2026-06-26 12:14 UTC (permalink / raw)
To: Pablo Sabater
Cc: git, chriscool, eric.peijian, gitster, jltobler, karthik.188,
peff, toon, Jonathan Tan, Calvin Wan
In-Reply-To: <20260625-ps-eric-work-rebase-v14-6-09f7ffe21a53@gmail.com>
On Thu, 25 Jun 2026 at 17:43, Pablo Sabater <pabloosabaterr@gmail.com> wrote:
>
> write_fetch_command_and_capabilities will be refactored in a subsequent
Nit: The paragraph below and the preceding patches refer to this function
as `write_fetch_command_and_capabilities()`. It will be nice to maintain
consistency throughout this series.
> commit where it will become a more general-purpose function, making it
> more accessible to additional commands in the future.
>
> Move `write_fetch_command_and_capabilities()` to `connect.c`, where
> there are similar purpose functions.
>
> Because string_list is only used as a pointer, use a forward
> declaration [1].
>
> [1]: https://lore.kernel.org/git/Z0RIqUAoEob8lGfM@pks.im/
>
> Helped-by: Jonathan Tan <jonathantanmy@google.com>
> Helped-by: Christian Couder <chriscool@tuxfamily.org>
> Signed-off-by: Calvin Wan <calvinwan@google.com>
> Signed-off-by: Eric Ju <eric.peijian@gmail.com>
> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>
> ---
[snip]
^ permalink raw reply
* Re: [PATCH GSoC v14 05/13] fetch-pack: prepare function to be moved
From: Chandra Pratap @ 2026-06-26 12:14 UTC (permalink / raw)
To: Pablo Sabater
Cc: git, chriscool, eric.peijian, gitster, jltobler, karthik.188,
peff, toon, Jonathan Tan, Calvin Wan
In-Reply-To: <20260625-ps-eric-work-rebase-v14-5-09f7ffe21a53@gmail.com>
On Thu, 25 Jun 2026 at 17:43, Pablo Sabater <pabloosabaterr@gmail.com> wrote:
>
> `write_fetch_command_and_capabilities()` will be refactored and moved in
> subsequent commits where it will become a more general-purpose function,
> making it more accessible to additional commands in the future.
>
> To move `write_fetch_command_and_capabilities()` to `connect.c`, we
> previously need to adjust how `advertise_sid` is managed. Currently in
> `fetch_pack.c`, `advertise_sid` is a static variable, modified using
> `repo_config_get_bool()`.
>
> Initialize `advertise_sid` at the begining by directly using
> `repo_config_get_bool()`. This change is safe because:
>
> In the original `fetch-pack.c` code, there are only two places that write
> `advertise_sid`:
>
> 1. In function `do_fetch_pack()`:
> if (!server_supports("session_id"))
> advertise_sid = 0;
> 2. In function `fetch_pack_config()`:
> repo_config_get_bool("transfer.advertisesid", &advertise_sid);
>
> About 1, since `do_fetch_pack()` is only relevant for protocol v1, this
> assignment can be ignored, as `write_fetch_command_and_capabilities()`
> is only used in v2.
>
> About 2, `repo_config_get_bool()` is from `config.h` and it's an
> out-of-box dependency of `connect.c`, so we can reuse it directly.
Nit: This only explains the `advertise_sid` change in this patch. We should
also add a few lines explaining the `hash_algo` change. Maybe something
like:
While at it, change `hash_algo`'s type to `hash_algo_by_name()`'s actual
return type (`unsigned int`) and make it `const`.
> Helped-by: Jonathan Tan <jonathantanmy@google.com>
> Helped-by: Christian Couder <chriscool@tuxfamily.org>
> Signed-off-by: Calvin Wan <calvinwan@google.com>
> Signed-off-by: Eric Ju <eric.peijian@gmail.com>
> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>
> ---
> fetch-pack.c | 5 ++++-
> 1 file changed, 4 insertions(+), 1 deletion(-)
>
> diff --git a/fetch-pack.c b/fetch-pack.c
> index f13951d154..ad07603755 100644
> --- a/fetch-pack.c
> +++ b/fetch-pack.c
> @@ -1380,6 +1380,9 @@ static void write_fetch_command_and_capabilities(struct strbuf *req_buf,
> const struct string_list *server_options)
> {
> const char *hash_name;
> + int advertise_sid;
> +
> + repo_config_get_bool(the_repository, "transfer.advertisesid", &advertise_sid);
>
> ensure_server_supports_v2("fetch");
> packet_buf_write(req_buf, "command=fetch");
> @@ -1395,7 +1398,7 @@ static void write_fetch_command_and_capabilities(struct strbuf *req_buf,
> }
>
> if (server_feature_v2("object-format", &hash_name)) {
> - int hash_algo = hash_algo_by_name(hash_name);
> + const unsigned int hash_algo = hash_algo_by_name(hash_name);
> if (hash_algo_by_ptr(the_hash_algo) != hash_algo)
> die(_("mismatched algorithms: client %s; server %s"),
> the_hash_algo->name, hash_name);
>
> --
> 2.54.0
^ permalink raw reply
* [PATCH v3 0/8] commit-reach: terminate merge-base walk when one side is exhausted
From: Kristofer Karlsson via GitGitGadget @ 2026-06-26 13:07 UTC (permalink / raw)
To: git; +Cc: Derrick Stolee, Elijah Newren, Kristofer Karlsson
In-Reply-To: <pull.2149.v2.git.1782303254.gitgitgadget@gmail.com>
commit-reach: terminate merge-base walk when one paint side is exhausted
Optimize paint_down_to_common() for merge-base queries that hit large
one-sided histories.
When the walk from one side reaches a commit with a very low generation
number that the other side never paints, the walk is forced to drain most of
the graph. A common trigger is a repository import that grafts a separate
history with its own root, but any merge that introduces a low-generation
commit never painted by the other side has the same effect.
A new merge-base candidate can only be discovered when exclusive PARENT1 and
PARENT2 paint meet. This series teaches paint_down_to_common() to stop as
soon as one side has no exclusive commits left in the queue; once one side
is exhausted, no further candidates can appear.
origin/HEAD o o PR HEAD
| |
(import) o :
/ \ /
| o merge-base
| |
: : (~2.5M commits)
| |
import root main root
In the RFC thread [1], Derrick Stolee provided a criss-cross counterexample
that sharpened the halt condition, and Elijah Newren independently
discovered the same optimization and shared an implementation in PR #2150
[2]. Patches 2-3 incorporate test cases from Elijah's branch.
This series implements the optimization only after the walk enters the
finite-generation region, where generation ordering guarantees that paint on
visited commits is final.
Patch layout:
1/8 Documentation/technical: add paint-down-to-common doc 2/8 t6600: add
test cases for side-exhaustion edge cases 3/8 t6099, t6600: add
side-exhaustion regression tests 4/8 commit-reach: add trace2
instrumentation to paint_down_to_common() 5/8 commit-reach: introduce struct
paint_state with per-side counters 6/8 commit-reach: remove unused
nonstale_queue dedup wrappers 7/8 commit-reach: terminate merge-base walk
when one paint side is exhausted 8/8 commit-reach: move min_generation check
into paint_queue_get()
Benchmarks
Step counts are deterministic (measured via trace2_data_intmax added in
patch 4). Wall-clock times are best-of-11 runs.
2.6M-commit monorepo with commit-graph:
steps wall-clock
merge-base --all (across import) 2143438 -> 3 3.67s -> 5ms
merge-base --all (1000 apart) 2692915 -> 1035 4.41s -> 7ms
merge-base --all (5000 apart) 2692915 -> 6401 4.45s -> 13ms
merge-base --all (HEAD vs import) 2698872 -> 45960 4.50s -> 79ms
merge-tree (across import) 2143438 -> 3 4.42s -> 11ms
git.git (88k commits, commit-graph):
steps wall-clock
merge-base --all v2.0.0 v2.55.0-rc1 72264 -> 44589 110ms -> 68ms
merge-base --all HEAD HEAD~1000 9891 -> 3828 18ms -> 10ms
merge-base --all HEAD HEAD~10000 72303 -> 41487 101ms -> 50ms
Changes since v2:
* New patch 8/8: moved the min_generation termination check and the
last_gen monotonicity assertion into paint_queue_get(), consolidating
halt conditions. commit_graph_generation() is now called once per
dequeued commit and shared across all checks.
* Widened the generation-monotonicity BUG assertion to fire
unconditionally, not only when min_generation is set. The side-exhaustion
optimization depends on correct generation ordering, so the assertion
should always be active. This is a behavior change: the BUG() now fires
for any generation ordering violation, regardless of the caller.
* Moved all halt conditions inside paint_queue_get() with the "pop first"
form: pop, check, then decrement counters. This keeps the optimization
commit's diff minimal (just inserting the new checks between pop and
decrement).
* Shortened the doc comment on paint_queue_get() to describe what it does
rather than how. Inline comments on each return NULL explain the specific
halt condition.
* Replaced the manual commit-graph setup in the step-count test with
run_all_modes, which now sets GIT_TRACE2_EVENT per mode and produces
trace-mode-{none,full,half,no-gdat}.txt files.
* Added a test_paint_down_steps helper for concise 4-mode step assertions
with diagnostic output on mismatch (prints "expected X, got Y" instead of
a silent grep failure).
* Added step-count assertions to the single-walk edge-case tests:
in_merge_bases_many:self, pending-stale, infinity-both-sides,
mixed-finite-infinity.
* Included step counts alongside wall-clock times in the benchmark tables.
Changes since v1:
* Reordered patches: documentation first (describing the existing
algorithm), tests before code changes, so they demonstrate passing with
old logic first.
* Dropped the ahead_behind decoupling patch. paint_state is now a NEW
struct alongside nonstale_queue instead of replacing it. ahead_behind()
is completely untouched.
* Removed nonstale_queue_put_dedup() and nonstale_queue_get_dedup() (dead
code after the conversion) in a separate commit.
* Renamed: struct paint_queue -> paint_state, field pq -> queue,
paint_count_add/remove -> paint_count_update (single function with signed
delta parameter).
* Split the old paint_count_transition (which handled both old and new
flags in one call) into separate remove/add calls with a signed delta.
This eliminates the need for the case 0 handler (which tracked "not in
the queue") and allows an exhaustive switch on (PARENT1 | PARENT2 |
STALE) that documents all valid flag combinations, with BUG() in default.
* Added trace2_data_intmax() instrumentation to report the number of
commits visited per paint walk (separate commit), with deterministic
step-count assertions in t6600.
* Expanded switch statements to multi-line format per .clang-format.
* Used !count style throughout instead of count == 0.
* Updated technical documentation alongside code changes.
[1]
https://lore.kernel.org/git/CAL71e4Ps-2_0+uuZu43N9pFnXBemoAohPs_eyRJf8taXHJPAXQ@mail.gmail.com/T/#u
[2] https://github.com/gitgitgadget/git/pull/2150
Elijah Newren (1):
t6600: add test cases for side-exhaustion edge cases
Kristofer Karlsson (7):
Documentation/technical: add paint-down-to-common doc
t6099, t6600: add side-exhaustion regression tests
commit-reach: add trace2 instrumentation to paint_down_to_common()
commit-reach: introduce struct paint_state with per-side counters
commit-reach: remove unused nonstale_queue dedup wrappers
commit-reach: terminate merge-base walk when one paint side is
exhausted
commit-reach: move min_generation check into paint_queue_get()
Documentation/Makefile | 1 +
Documentation/technical/meson.build | 1 +
.../technical/paint-down-to-common.adoc | 137 +++++++++++++
commit-reach.c | 147 ++++++++++----
t/meson.build | 1 +
t/t6099-merge-base-side-exhaustion.sh | 82 ++++++++
t/t6600-test-reach.sh | 181 ++++++++++++++++--
7 files changed, 501 insertions(+), 49 deletions(-)
create mode 100644 Documentation/technical/paint-down-to-common.adoc
create mode 100755 t/t6099-merge-base-side-exhaustion.sh
base-commit: 6c3d7b73556db708feb3b16232fab1efc4353428
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2149%2Fspkrka%2Fside-exhaust-pr-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2149/spkrka/side-exhaust-pr-v3
Pull-Request: https://github.com/gitgitgadget/git/pull/2149
Range-diff vs v2:
1: 19ed743bd1 = 1: 2593866bce Documentation/technical: add paint-down-to-common doc
2: 6151b8e0a3 = 2: 9efc084850 t6600: add test cases for side-exhaustion edge cases
3: 90f09ecb5c = 3: 14b0d86b93 t6099, t6600: add side-exhaustion regression tests
4: 6ade4df2ed ! 4: 2592264cda commit-reach: add trace2 instrumentation to paint_down_to_common()
@@ Commit message
Add a step counter and trace2_data_intmax() call so that the number
of commits visited during the paint walk is observable via
- GIT_TRACE2_PERF. This provides a way to measure the impact of
+ GIT_TRACE2_EVENT. This provides a way to measure the impact of
future optimizations without relying on wall-clock benchmarks alone.
Signed-off-by: Kristofer Karlsson <krka@spotify.com>
@@ commit-reach.c: static int paint_down_to_common(struct repository *r,
}
## t/t6600-test-reach.sh ##
-@@ t/t6600-test-reach.sh: test_expect_success 'get_merge_bases_many:mixed-finite-infinity' '
- test_all_modes get_merge_bases_many
+@@ t/t6600-test-reach.sh: test_expect_success 'setup' '
'
-+test_expect_success 'merge-base --all commit-walk steps' '
-+ test_when_finished rm -rf .git/objects/info/commit-graph \
-+ .git/objects/info/commit-graphs &&
-+ rm -rf .git/objects/info/commit-graph \
-+ .git/objects/info/commit-graphs &&
-+
-+ GIT_TRACE2_EVENT="$(pwd)/trace-none.txt" \
-+ git merge-base --all commit-9-9 commit-9-1 >actual &&
-+ test_trace2_data paint_down_to_common steps 81 <trace-none.txt &&
+ run_all_modes () {
+- test_when_finished rm -rf .git/objects/info/commit-graph &&
+- "$@" <input >actual &&
+- test_cmp expect actual &&
+- cp commit-graph-full .git/objects/info/commit-graph &&
+- "$@" <input >actual &&
+- test_cmp expect actual &&
+- cp commit-graph-half .git/objects/info/commit-graph &&
+- "$@" <input >actual &&
+- test_cmp expect actual &&
+- cp commit-graph-no-gdat .git/objects/info/commit-graph &&
+- "$@" <input >actual &&
+- test_cmp expect actual
++ graph=.git/objects/info/commit-graph &&
++ test_when_finished rm -rf "$graph" "${graph}s" &&
++ rm -f trace-mode-*.txt &&
+
-+ cp commit-graph-full .git/objects/info/commit-graph &&
-+ GIT_TRACE2_EVENT="$(pwd)/trace-full.txt" \
-+ git merge-base --all commit-9-9 commit-9-1 >actual &&
-+ test_trace2_data paint_down_to_common steps 80 <trace-full.txt &&
++ for mode in none full half no-gdat
++ do
++ rm -rf "$graph" "${graph}s" &&
++ cp "commit-graph-${mode}" "$graph" 2>/dev/null ||
++ true &&
++ GIT_TRACE2_EVENT="$(pwd)/trace-mode-${mode}.txt" \
++ "$@" <input >actual &&
++ test_cmp expect actual || return 1
++ done
+ }
+
+ test_all_modes () {
+ run_all_modes test-tool reach "$@"
+ }
+
++test_paint_down_steps () {
++ for mode in none full half no-gdat
++ do
++ test_trace2_data paint_down_to_common steps "$1" \
++ <"trace-mode-${mode}.txt" || return 1
++ shift
++ done
++}
+
-+ cp commit-graph-half .git/objects/info/commit-graph &&
-+ GIT_TRACE2_EVENT="$(pwd)/trace-half.txt" \
-+ git merge-base --all commit-9-9 commit-9-1 >actual &&
-+ test_trace2_data paint_down_to_common steps 81 <trace-half.txt
+ test_expect_success 'ref_newer:miss' '
+ cat >input <<-\EOF &&
+ A:commit-5-7
+@@ t/t6600-test-reach.sh: test_expect_success 'in_merge_bases_many:self' '
+ X:commit-6-8
+ EOF
+ echo "in_merge_bases_many(A,X):1" >expect &&
+- test_all_modes in_merge_bases_many
++ test_all_modes in_merge_bases_many &&
++ test_paint_down_steps 45 2 25 3
+ '
+
+ test_expect_success 'is_descendant_of:hit' '
+@@ t/t6600-test-reach.sh: test_expect_success 'get_merge_bases_many:pending-stale' '
+ echo "get_merge_bases_many(A,X):" &&
+ git rev-parse ps-B
+ } >expect &&
+- test_all_modes get_merge_bases_many
++ test_all_modes get_merge_bases_many &&
++ test_paint_down_steps 6 6 6 6
+ '
+
+ test_expect_success 'get_merge_bases_many:infinity-both-sides' '
+@@ t/t6600-test-reach.sh: test_expect_success 'get_merge_bases_many:infinity-both-sides' '
+ echo "get_merge_bases_many(A,X):" &&
+ git rev-parse pi-B
+ } >expect &&
+- test_all_modes get_merge_bases_many
++ test_all_modes get_merge_bases_many &&
++ test_paint_down_steps 5 5 5 5
+ '
+
+ test_expect_success 'setup mixed finite/INFINITY topology' '
+@@ t/t6600-test-reach.sh: test_expect_success 'get_merge_bases_many:mixed-finite-infinity' '
+ echo "get_merge_bases_many(A,X):" &&
+ git rev-parse ps-X
+ } >expect &&
+- test_all_modes get_merge_bases_many
++ test_all_modes get_merge_bases_many &&
++ test_paint_down_steps 3 3 3 3
+'
+
++test_expect_success 'merge-base --all commit-walk steps' '
++ >input &&
++ git rev-parse commit-9-1 >expect &&
++ run_all_modes git merge-base --all commit-9-9 commit-9-1 &&
++ test_paint_down_steps 81 80 81 81
+ '
+
test_expect_success 'reduce_heads' '
- cat >input <<-\EOF &&
- X:commit-1-10
5: f24edd45f0 ! 5: e82e0c72b6 commit-reach: introduce struct paint_state with per-side counters
@@ Commit message
(max_nonstale). This is equivalent behavior -- both conditions
detect that no non-stale entries remain.
- The existing nonstale_queue is left in place for ahead_behind().
+ paint_queue_get() uses a "pop first" form: it dequeues a commit,
+ then checks the counters. This means the loop exits one iteration
+ earlier than the old code in some topologies (the popped stale
+ commit is never processed), so a few step counts drop by one.
- Step counts (via trace2 from the previous commit) are identical
- before and after this refactoring, confirming no behavioral change.
+ The existing nonstale_queue is left in place for ahead_behind().
Signed-off-by: Kristofer Karlsson <krka@spotify.com>
@@ commit-reach.c: static struct commit *nonstale_queue_get_dedup(struct nonstale_q
+
+static struct commit *paint_queue_get(struct paint_state *state)
+{
-+ struct commit *commit;
++ struct commit *commit = prio_queue_get(&state->queue);
++
++ if (!commit)
++ return NULL;
++
++ commit->object.flags &= ~ENQUEUED;
+
+ if (!state->p1_count && !state->p2_count &&
+ !state->pending_merge_bases)
+ return NULL;
+
-+ commit = prio_queue_get(&state->queue);
-+ if (commit) {
-+ commit->object.flags &= ~ENQUEUED;
-+ paint_count_update(state, commit->object.flags, -1);
-+ }
++ paint_count_update(state, commit->object.flags, -1);
+ return commit;
+}
+
@@ commit-reach.c: static int paint_down_to_common(struct repository *r,
trace2_data_intmax("paint_down_to_common", r,
"steps", steps);
commit_list_sort_by_date(result);
+
+ ## t/t6600-test-reach.sh ##
+@@ t/t6600-test-reach.sh: test_expect_success 'get_merge_bases_many:pending-stale' '
+ git rev-parse ps-B
+ } >expect &&
+ test_all_modes get_merge_bases_many &&
+- test_paint_down_steps 6 6 6 6
++ test_paint_down_steps 5 5 5 5
+ '
+
+ test_expect_success 'get_merge_bases_many:infinity-both-sides' '
+@@ t/t6600-test-reach.sh: test_expect_success 'get_merge_bases_many:infinity-both-sides' '
+ git rev-parse pi-B
+ } >expect &&
+ test_all_modes get_merge_bases_many &&
+- test_paint_down_steps 5 5 5 5
++ test_paint_down_steps 5 4 5 5
+ '
+
+ test_expect_success 'setup mixed finite/INFINITY topology' '
6: 8c72f01083 = 6: e6181bf3c1 commit-reach: remove unused nonstale_queue dedup wrappers
7: d84b932e5b ! 7: f3572a8a89 commit-reach: terminate merge-base walk when one paint side is exhausted
@@ Commit message
once the walk enters the finite-generation region where ordering
guarantees hold.
+ Widen the existing generation-monotonicity BUG assertion to fire
+ unconditionally, not only when min_generation is set. The
+ side-exhaustion optimization depends on correct generation ordering,
+ so the assertion should always be active.
+
Step counts measured with trace2 on git.git with commit-graph:
merge-base --all v2.0.0 v2.55.0-rc1:
@@ Documentation/technical/paint-down-to-common.adoc: existing candidates by provin
## commit-reach.c ##
@@ commit-reach.c: static void paint_queue_put(struct paint_state *state,
+ }
+ }
++/*
++ * Dequeue the next commit for the paint walk, or return NULL when
++ * no more merge bases can be discovered.
++ */
static struct commit *paint_queue_get(struct paint_state *state)
{
-- struct commit *commit;
-+ struct commit *commit = prio_queue_get(&state->queue);
+ struct commit *commit = prio_queue_get(&state->queue);
+@@ commit-reach.c: static struct commit *paint_queue_get(struct paint_state *state)
+
+ commit->object.flags &= ~ENQUEUED;
- if (!state->p1_count && !state->p2_count &&
- !state->pending_merge_bases)
-+ if (!commit)
- return NULL;
-
-- commit = prio_queue_get(&state->queue);
-- if (commit) {
-- commit->object.flags &= ~ENQUEUED;
-- paint_count_update(state, commit->object.flags, -1);
-+ commit->object.flags &= ~ENQUEUED;
-+
+- return NULL;
+ if (!state->pending_merge_bases) {
++ /* only stale entries remain */
+ if (!state->p1_count && !state->p2_count)
+ return NULL;
-+ /*
-+ * Side exhaustion: a new merge-base can only form
-+ * when both PARENT1-only and PARENT2-only commits
-+ * remain in the queue. In the finite-generation
-+ * region the queue is ordered topologically, so
-+ * no future step can add paint to visited commits
-+ * and an exhausted side cannot reappear.
-+ */
++
++ /* one side is exhausted */
+ if ((!state->p1_count || !state->p2_count) &&
+ commit_graph_generation(commit) < GENERATION_NUMBER_INFINITY)
+ return NULL;
- }
-+
-+ paint_count_update(state, commit->object.flags, -1);
++ }
+
+ paint_count_update(state, commit->object.flags, -1);
return commit;
- }
+@@ commit-reach.c: static int paint_down_to_common(struct repository *r,
+ timestamp_t generation = commit_graph_generation(commit);
+ steps++;
+- if (min_generation && generation > last_gen)
++ if (generation > last_gen)
+ BUG("bad generation skip %"PRItime" > %"PRItime" at %s",
+ generation, last_gen,
+ oid_to_hex(&commit->object.oid));
## t/t6600-test-reach.sh ##
-@@ t/t6600-test-reach.sh: test_expect_success 'merge-base --all commit-walk steps' '
- cp commit-graph-full .git/objects/info/commit-graph &&
- GIT_TRACE2_EVENT="$(pwd)/trace-full.txt" \
- git merge-base --all commit-9-9 commit-9-1 >actual &&
-- test_trace2_data paint_down_to_common steps 80 <trace-full.txt &&
-+ test_trace2_data paint_down_to_common steps 9 <trace-full.txt &&
+@@ t/t6600-test-reach.sh: test_expect_success 'in_merge_bases_many:self' '
+ EOF
+ echo "in_merge_bases_many(A,X):1" >expect &&
+ test_all_modes in_merge_bases_many &&
+- test_paint_down_steps 45 2 25 3
++ test_paint_down_steps 45 1 25 1
+ '
- cp commit-graph-half .git/objects/info/commit-graph &&
- GIT_TRACE2_EVENT="$(pwd)/trace-half.txt" \
- git merge-base --all commit-9-9 commit-9-1 >actual &&
-- test_trace2_data paint_down_to_common steps 81 <trace-half.txt
-+ test_trace2_data paint_down_to_common steps 57 <trace-half.txt
+ test_expect_success 'is_descendant_of:hit' '
+@@ t/t6600-test-reach.sh: test_expect_success 'merge-base --all commit-walk steps' '
+ >input &&
+ git rev-parse commit-9-1 >expect &&
+ run_all_modes git merge-base --all commit-9-9 commit-9-1 &&
+- test_paint_down_steps 81 80 81 81
++ test_paint_down_steps 81 9 57 10
'
test_expect_success 'reduce_heads' '
-: ---------- > 8: 4b9f192d98 commit-reach: move min_generation check into paint_queue_get()
--
gitgitgadget
^ permalink raw reply
* [PATCH v3 1/8] Documentation/technical: add paint-down-to-common doc
From: Kristofer Karlsson via GitGitGadget @ 2026-06-26 13:07 UTC (permalink / raw)
To: git; +Cc: Derrick Stolee, Elijah Newren, Kristofer Karlsson,
Kristofer Karlsson
In-Reply-To: <pull.2149.v3.git.1782479286.gitgitgadget@gmail.com>
From: Kristofer Karlsson <krka@spotify.com>
Add a technical document describing the paint_down_to_common()
algorithm used for merge-base computation, covering the paint
walk, generation number regions, and termination conditions.
Signed-off-by: Kristofer Karlsson <krka@spotify.com>
---
Documentation/Makefile | 1 +
Documentation/technical/meson.build | 1 +
.../technical/paint-down-to-common.adoc | 114 ++++++++++++++++++
commit-reach.c | 6 +-
4 files changed, 121 insertions(+), 1 deletion(-)
create mode 100644 Documentation/technical/paint-down-to-common.adoc
diff --git a/Documentation/Makefile b/Documentation/Makefile
index 2699f0b24a..f8dea4b395 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -129,6 +129,7 @@ TECH_DOCS += technical/long-running-process-protocol
TECH_DOCS += technical/multi-pack-index
TECH_DOCS += technical/packfile-uri
TECH_DOCS += technical/pack-heuristics
+TECH_DOCS += technical/paint-down-to-common
TECH_DOCS += technical/parallel-checkout
TECH_DOCS += technical/partial-clone
TECH_DOCS += technical/platform-support
diff --git a/Documentation/technical/meson.build b/Documentation/technical/meson.build
index ec07088c57..9ce11d5e48 100644
--- a/Documentation/technical/meson.build
+++ b/Documentation/technical/meson.build
@@ -18,6 +18,7 @@ articles = [
'multi-pack-index.adoc',
'packfile-uri.adoc',
'pack-heuristics.adoc',
+ 'paint-down-to-common.adoc',
'parallel-checkout.adoc',
'partial-clone.adoc',
'platform-support.adoc',
diff --git a/Documentation/technical/paint-down-to-common.adoc b/Documentation/technical/paint-down-to-common.adoc
new file mode 100644
index 0000000000..c10d5d2887
--- /dev/null
+++ b/Documentation/technical/paint-down-to-common.adoc
@@ -0,0 +1,114 @@
+Merge-Base Computation and paint_down_to_common()
+==================================================
+
+The function `paint_down_to_common()` in `commit-reach.c` computes merge
+bases by walking the commit graph backwards from two sets of tips and
+finding where their ancestry meets.
+
+Use cases
+---------
+
+Computing merge bases is used in two different ways:
+
+ 1. *Finding all merge bases* (`merge-base --all`, `merge-tree`,
+ `merge`, `rebase`). A merge base is a common ancestor that is
+ not itself an ancestor of another common ancestor.
+
+ 2. *Ancestry checks* (`in_merge_bases`, used by `merge-base
+ --is-ancestor`, `branch -d`, `fetch`). These ask: "is commit A
+ an ancestor of commit B?" If a common ancestor equals one of the
+ inputs, that input is necessarily the only merge base -- no other
+ common ancestor can be both as recent and not an ancestor of it.
+
+Both use cases share the same algorithm and implementation.
+
+Algorithm
+---------
+
+Given a commit `one` and a set of commits `twos[]`, the walk paints
+commits with two colors:
+
+ - PARENT1: reachable from `one`
+ - PARENT2: reachable from any commit in `twos[]`
+
+The walk uses a priority queue ordered by generation number (falling
+back to commit date when generation numbers are unavailable). Each
+step dequeues the highest-priority commit (this is when we say a
+commit is "visited") and propagates its paint flags to its parents,
+enqueuing them if they gained new flags. When a commit receives
+both PARENT1 and PARENT2, it is a merge-base candidate. A candidate
+gains the STALE flag so its ancestors propagate staleness -- any
+deeper common ancestor is necessarily redundant.
+
+INFINITY and finite generation regions
+--------------------------------------
+
+The commit-graph stores a generation number for each commit. Commits
+not in the commit-graph have generation `GENERATION_NUMBER_INFINITY`. The
+graph is closed under reachability: if a commit is in the graph, all
+its ancestors are too. This partitions the commit graph into two regions:
+
+....
+ +---------------------------------------+
+ | INFINITY region |
+ | generation = INFINITY |
+ | queue order: heuristic (commit date) |
+ +---------------------------------------+
+ |
+ v
+ +---------------------------------------+
+ | Finite region |
+ | generation = finite |
+ | queue order: topological |
+ +---------------------------------------+
+....
+
+When the commit-graph is enabled, the INFINITY region is typically
+very small -- it only contains commits added since the last
+commit-graph refresh.
+
+All reachable INFINITY-generation commits are visited before any
+finite-generation commit, because INFINITY is larger than any finite
+value. Once the walk crosses into the finite region, it stays there.
+
+In the finite region, generation ordering guarantees topological
+traversal: children are always visited before their parents. This
+means that paint on already-visited commits is final -- no future
+traversal step can add paint to them.
+
+In the INFINITY region, commit-date ordering can violate this: a
+parent with a later date can be visited before a child with an earlier
+date. Paint flags are therefore NOT final at visit time, and a
+commit visited with only one side's paint may later gain the other.
+
+Paint flags are only added, never removed. Since each flag can be set
+at most once per commit, the number of times a commit can be
+re-enqueued is bounded by the number of flag transitions.
+
+Termination
+-----------
+
+The walk uses a `nonstale_queue` wrapper around `prio_queue` that
+tracks `max_nonstale`: the lowest-priority non-stale commit enqueued
+so far. Once that commit is dequeued, every remaining entry is known
+to be STALE and the loop terminates. Specifically, the main loop
+ends when one of the following conditions holds:
+
+ 1. The queue is empty.
+ 2. `max_nonstale` has been dequeued, meaning the queue only contains
+ STALE entries.
+
+Stale entry condition
+~~~~~~~~~~~~~~~~~~~~~
+Once all queued entries are stale, no new merge-base candidates can
+be discovered -- that requires at least one non-stale commit from
+each side meeting. Continuing the walk could still invalidate
+existing candidates by proving one is an ancestor of another, but
+`remove_redundant()` handles that as a post-processing step, so it
+is safe to exit early.
+
+Related documentation
+---------------------
+
+ - `Documentation/technical/commit-graph.adoc` -- generation numbers
+ and the reachability closure property.
diff --git a/commit-reach.c b/commit-reach.c
index 5df471a313..a9483759e0 100644
--- a/commit-reach.c
+++ b/commit-reach.c
@@ -96,7 +96,11 @@ static struct commit *nonstale_queue_get_dedup(struct nonstale_queue *queue)
return commit;
}
-/* all input commits in one and twos[] must have been parsed! */
+/*
+ * See Documentation/technical/paint-down-to-common.adoc
+ *
+ * All input commits in one and twos[] must have been parsed!
+ */
static int paint_down_to_common(struct repository *r,
struct commit *one, int n,
struct commit **twos,
--
gitgitgadget
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox