From 6b79aee7de1ac7142707326699c3f4aa5b18cb28 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Wed, 15 Apr 2026 23:58:00 +0000 Subject: [PATCH 01/16] backfill: reject rev-list arguments that do not make sense Some rev-list options accepted by setup_revisions() are silently ignored or actively counterproductive when used with 'git backfill', because the path-walk API has its own tree-walking logic that bypasses the mechanisms these options rely on: * -S/-G (pickaxe) and --diff-filter work by computing per-commit diffs in get_revision_1() and filtering commits whose diffs don't match. Since backfill's goal is to download all blobs reachable from commits in the range, filtering out commits based on diff content would silently skip blobs -- the opposite of what users want. * --follow disables path pruning (revs->prune) and only makes sense for tracking a single file through renames in log output. It has no useful interaction with backfill. * -L (line-log) computes line-level diffs to track the evolution of a function or line range. Like pickaxe, it filters commits based on diff content, which would cause blobs to be silently skipped. * --diff-merges controls how merge commit diffs are displayed. The path-walk API walks trees directly and never computes per-commit diffs, so this option would be silently ignored. * --filter (object filtering, e.g. --filter=blob:none) is used by the list-objects traversal but is completely ignored by the path-walk API, so it would silently do nothing. Rather than letting users think these options are being honored, reject them with a clear error message. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/backfill.c | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/builtin/backfill.c b/builtin/backfill.c index d794dd842f65ce..a9edddcb7edfa3 100644 --- a/builtin/backfill.c +++ b/builtin/backfill.c @@ -78,6 +78,28 @@ static int fill_missing_blobs(const char *path UNUSED, return 0; } +static void reject_unsupported_rev_list_options(struct rev_info *revs) +{ + if (revs->diffopt.pickaxe) + die(_("'%s' cannot be used with 'git backfill'"), + (revs->diffopt.pickaxe_opts & DIFF_PICKAXE_REGEX) ? "-G" : "-S"); + if (revs->diffopt.filter || revs->diffopt.filter_not) + die(_("'%s' cannot be used with 'git backfill'"), + "--diff-filter"); + if (revs->diffopt.flags.follow_renames) + die(_("'%s' cannot be used with 'git backfill'"), + "--follow"); + if (revs->line_level_traverse) + die(_("'%s' cannot be used with 'git backfill'"), + "-L"); + if (revs->explicit_diff_merges) + die(_("'%s' cannot be used with 'git backfill'"), + "--diff-merges"); + if (revs->filter.choice) + die(_("'%s' cannot be used with 'git backfill'"), + "--filter"); +} + static int do_backfill(struct backfill_context *ctx) { struct path_walk_info info = PATH_WALK_INFO_INIT; @@ -144,6 +166,7 @@ int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit if (argc > 1) die(_("unrecognized argument: %s"), argv[1]); + reject_unsupported_rev_list_options(&ctx.revs); repo_config(repo, git_default_config, NULL); From ef6d3c94746462ec560893ed35c83df485a6344d Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Wed, 15 Apr 2026 23:58:01 +0000 Subject: [PATCH 02/16] backfill: document acceptance of revision-range in more standard manner 302aff09223f (backfill: accept revision arguments, 2026-03-26) added support for passing revision arguments to 'git backfill' but documented them only with a prose sentence: You may also specify the commit limiting options from git-rev-list(1). No other command that accepts revision arguments documents them this way. Commands like log, shortlog, and replay define a formal entry and include rev-list-options.adoc. Commands like bundle, fast-export, and filter-branch, which pass arguments through to the revision machinery without including the full options file, still define a formal entry explaining what is accepted. Add a formal entry in the synopsis and OPTIONS section, following the convention used by other commands, and mention that commit-limiting options from git-rev-list(1) are also accepted. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- Documentation/git-backfill.adoc | 15 ++++++++++++--- builtin/backfill.c | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Documentation/git-backfill.adoc b/Documentation/git-backfill.adoc index 246ab417c24a10..bf26d7694f402d 100644 --- a/Documentation/git-backfill.adoc +++ b/Documentation/git-backfill.adoc @@ -9,7 +9,7 @@ git-backfill - Download missing objects in a partial clone SYNOPSIS -------- [synopsis] -git backfill [--min-batch-size=] [--[no-]sparse] +git backfill [--min-batch-size=] [--[no-]sparse] [] DESCRIPTION ----------- @@ -43,7 +43,7 @@ smaller network calls than downloading the entire repository at clone time. By default, `git backfill` downloads all blobs reachable from the `HEAD` -commit. This set can be restricted or expanded using various options. +commit. This set can be restricted or expanded using various options below. THIS COMMAND IS EXPERIMENTAL. ITS BEHAVIOR MAY CHANGE IN THE FUTURE. @@ -63,7 +63,16 @@ OPTIONS current sparse-checkout. If the sparse-checkout feature is enabled, then `--sparse` is assumed and can be disabled with `--no-sparse`. -You may also specify the commit limiting options from linkgit:git-rev-list[1]. +``:: + Backfill only blobs reachable from commits in the specified + revision range. When no __ is specified, it + defaults to `HEAD` (i.e. the whole history leading to the + current commit). For a complete list of ways to spell + __, see the "Specifying Ranges" section of + linkgit:gitrevisions[7]. ++ +You may also use commit-limiting options understood by +linkgit:git-rev-list[1] such as `--first-parent`, `--since`, or pathspecs. SEE ALSO -------- diff --git a/builtin/backfill.c b/builtin/backfill.c index a9edddcb7edfa3..e934d360fdd9d0 100644 --- a/builtin/backfill.c +++ b/builtin/backfill.c @@ -26,7 +26,7 @@ #include "path-walk.h" static const char * const builtin_backfill_usage[] = { - N_("git backfill [--min-batch-size=] [--[no-]sparse]"), + N_("git backfill [--min-batch-size=] [--[no-]sparse] []"), NULL }; From a1ad4a0fca14cdeb55ab9fb065551b15cafa8a4f Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Wed, 15 Apr 2026 23:58:02 +0000 Subject: [PATCH 03/16] backfill: default to grabbing edge blobs too Commit 302aff09223f (backfill: accept revision arguments, 2026-03-26) added support for accepting revision arguments to backfill. This allows users to do things like git backfill --remotes ^v2.3.0 and then run many commands without triggering on-demand downloads of blobs. However, if they have topics based on v2.3.0, they will likely still trigger on-demand downloads. Consider, for example, the command git log -p v2.3.0..topic This would still trigger on-demand blob loadings after the backfill command above, because the commit(s) with A as a parent will need to diff against the blobs in A. In fact, multiple commands need blobs from the lower boundary of the revision range: * git log -p A..B # After backfill A..B * git replay --onto TARGET A..B # After backfill TARGET^! A..B * git checkout A && git merge B # After backfill A...B Add an extra --[no-]include-edges flag to allow grabbing blobs from edge commits. Since the point of backfill is to prevent on-demand blob loading and these are common commands, default to --include-edges. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- Documentation/git-backfill.adoc | 9 ++- builtin/backfill.c | 8 ++- t/t5620-backfill.sh | 110 ++++++++++++++++++++++++++++++-- 3 files changed, 119 insertions(+), 8 deletions(-) diff --git a/Documentation/git-backfill.adoc b/Documentation/git-backfill.adoc index bf26d7694f402d..c0a3b80615e034 100644 --- a/Documentation/git-backfill.adoc +++ b/Documentation/git-backfill.adoc @@ -9,7 +9,7 @@ git-backfill - Download missing objects in a partial clone SYNOPSIS -------- [synopsis] -git backfill [--min-batch-size=] [--[no-]sparse] [] +git backfill [--min-batch-size=] [--[no-]sparse] [--[no-]include-edges] [] DESCRIPTION ----------- @@ -63,6 +63,13 @@ OPTIONS current sparse-checkout. If the sparse-checkout feature is enabled, then `--sparse` is assumed and can be disabled with `--no-sparse`. +`--include-edges`:: +`--no-include-edges`:: + Include blobs from boundary commits in the backfill. Useful in + preparation for commands like `git log -p A..B` or `git replay + --onto TARGET A..B`, where A..B normally excludes A but you need + the blobs from A as well. `--include-edges` is the default. + ``:: Backfill only blobs reachable from commits in the specified revision range. When no __ is specified, it diff --git a/builtin/backfill.c b/builtin/backfill.c index e934d360fdd9d0..7ffab2ea74f5cc 100644 --- a/builtin/backfill.c +++ b/builtin/backfill.c @@ -26,7 +26,7 @@ #include "path-walk.h" static const char * const builtin_backfill_usage[] = { - N_("git backfill [--min-batch-size=] [--[no-]sparse] []"), + N_("git backfill [--min-batch-size=] [--[no-]sparse] [--[no-]include-edges] []"), NULL }; @@ -35,6 +35,7 @@ struct backfill_context { struct oid_array current_batch; size_t min_batch_size; int sparse; + int include_edges; struct rev_info revs; }; @@ -116,6 +117,8 @@ static int do_backfill(struct backfill_context *ctx) /* Walk from HEAD if otherwise unspecified. */ if (!ctx->revs.pending.nr) add_head_to_pending(&ctx->revs); + if (ctx->include_edges) + ctx->revs.edge_hint = 1; info.blobs = 1; info.tags = info.commits = info.trees = 0; @@ -143,12 +146,15 @@ int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit .min_batch_size = 50000, .sparse = -1, .revs = REV_INFO_INIT, + .include_edges = 1, }; struct option options[] = { OPT_UNSIGNED(0, "min-batch-size", &ctx.min_batch_size, N_("Minimum number of objects to request at a time")), OPT_BOOL(0, "sparse", &ctx.sparse, N_("Restrict the missing objects to the current sparse-checkout")), + OPT_BOOL(0, "include-edges", &ctx.include_edges, + N_("Include blobs from boundary commits in the backfill")), OPT_END(), }; struct repo_config_values *cfg = repo_config_values(the_repository); diff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh index f3b5e39493677b..94f35ce1901671 100755 --- a/t/t5620-backfill.sh +++ b/t/t5620-backfill.sh @@ -257,11 +257,12 @@ test_expect_success 'backfill with revision range' ' git -C backfill-revs rev-list --quiet --objects --missing=print HEAD >missing && test_line_count = 48 missing && - git -C backfill-revs backfill HEAD~2..HEAD && + GIT_TRACE2_EVENT="$(pwd)/backfill-trace" git -C backfill-revs backfill HEAD~2..HEAD && - # 30 objects downloaded. + # 36 objects downloaded, 12 still missing + test_trace2_data promisor fetch_count 36 missing && - test_line_count = 18 missing + test_line_count = 12 missing ' test_expect_success 'backfill with revisions over stdin' ' @@ -279,11 +280,12 @@ test_expect_success 'backfill with revisions over stdin' ' ^HEAD~2 EOF - git -C backfill-revs backfill --stdin missing && - test_line_count = 18 missing + test_line_count = 12 missing ' test_expect_success 'backfill with prefix pathspec' ' @@ -398,6 +400,102 @@ test_expect_success 'backfill with --since' ' test_line_count = 6 missing ' +test_expect_success 'backfill range with include-edges enables fetch-free git-log' ' + git clone --no-checkout --filter=blob:none \ + --single-branch --branch=main \ + "file://$(pwd)/srv.bare" backfill-log && + + # Backfill the range with default include edges. + git -C backfill-log backfill HEAD~2..HEAD && + + # git log -p needs edge blobs for the "before" side of + # diffs. With edge inclusion, all needed blobs are local. + GIT_TRACE2_EVENT="$(pwd)/log-trace" git \ + -C backfill-log log -p HEAD~2..HEAD >log-output && + + # No promisor fetches should have been needed. + ! grep "fetch_count" log-trace +' + +test_expect_success 'backfill range without include edges causes on-demand fetches in git-log' ' + git clone --no-checkout --filter=blob:none \ + --single-branch --branch=main \ + "file://$(pwd)/srv.bare" backfill-log-no-bdy && + + # Backfill WITHOUT include edges -- file.3 v1 blobs are missing. + git -C backfill-log-no-bdy backfill --no-include-edges HEAD~2..HEAD && + + # git log -p HEAD~2..HEAD computes diff of commit 7 against + # commit 6. It needs file.3 v1 (the "before" side), which was + # not backfilled. This triggers on-demand promisor fetches. + GIT_TRACE2_EVENT="$(pwd)/log-no-bdy-trace" git \ + -C backfill-log-no-bdy log -p HEAD~2..HEAD >log-output && + + grep "fetch_count" log-no-bdy-trace +' + +test_expect_success 'backfill range enables fetch-free replay' ' + # Create a repo with a branch to replay. + git init replay-src && + ( + cd replay-src && + git config uploadpack.allowfilter 1 && + git config uploadpack.allowanysha1inwant 1 && + test_commit base && + git checkout -b topic && + test_commit topic-change && + git checkout main && + test_commit main-change + ) && + git clone --bare --filter=blob:none \ + "file://$(pwd)/replay-src" replay-dest.git && + + # Backfill the replay range: --onto main, replaying topic~1..topic. + # For replay, we need TARGET^! plus the range. + main_oid=$(git -C replay-dest.git rev-parse main) && + topic_oid=$(git -C replay-dest.git rev-parse topic) && + base_oid=$(git -C replay-dest.git rev-parse topic~1) && + git -C replay-dest.git backfill \ + "$main_oid^!" "$base_oid..$topic_oid" && + + # Now replay should complete without any promisor fetches. + GIT_TRACE2_EVENT="$(pwd)/replay-trace" git -C replay-dest.git \ + replay --onto main topic~1..topic >replay-out && + + ! grep "fetch_count" replay-trace +' + +test_expect_success 'backfill enables fetch-free merge' ' + # Create a repo with two branches to merge. + git init merge-src && + ( + cd merge-src && + git config uploadpack.allowfilter 1 && + git config uploadpack.allowanysha1inwant 1 && + test_commit merge-base && + git checkout -b side && + test_commit side-change && + git checkout main && + test_commit main-side-change + ) && + git clone --filter=blob:none \ + "file://$(pwd)/merge-src" merge-dest && + + # The clone checked out main, fetching its blobs. + # Backfill the three endpoint commits needed for merge. + main_oid=$(git -C merge-dest rev-parse origin/main) && + side_oid=$(git -C merge-dest rev-parse origin/side) && + mbase=$(git -C merge-dest merge-base origin/main origin/side) && + git -C merge-dest backfill --no-include-edges \ + "$main_oid^!" "$side_oid^!" "$mbase^!" && + + # Merge should complete without promisor fetches. + GIT_TRACE2_EVENT="$(pwd)/merge-trace" git -C merge-dest \ + merge origin/side -m "test merge" && + + ! grep "fetch_count" merge-trace +' + . "$TEST_DIRECTORY"/lib-httpd.sh start_httpd From 7002d6cd16047c0ed0b6befc22b5a7d54d4d6fde Mon Sep 17 00:00:00 2001 From: Siddharth Shrimali Date: Tue, 21 Apr 2026 11:03:32 +0530 Subject: [PATCH 04/16] t7004: drop hardcoded tag count for state verification The test 'trying to create a tag with a non-valid name should fail', checked that exactly one tag existed in the repository before and after attempting to create invalid tags. As pointed out by Junio, this makes the test brittle by relying on a specific global tag count. If future tests are added or removed before this test, the expected state changes and this test would break for completely unrelated reasons. Modernize the test by taking a snapshot of the existing tags before the failure attempts and comparing it to a snapshot taken after. This provides a "belt-and-suspenders" approach: we verify that 'git tag' both exits with the expected error code and leaves the repository state untouched, without being brittle to the specific number of tags present. This replaces the hardcoded 'test_line_count = 1' checks with 'test_cmp' to ensure the tag list remains identical. Suggested-by: Junio C Hamano Signed-off-by: Siddharth Shrimali Signed-off-by: Junio C Hamano --- t/t7004-tag.sh | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh index faf7d97fc487d3..77a7a9777d1989 100755 --- a/t/t7004-tag.sh +++ b/t/t7004-tag.sh @@ -191,15 +191,14 @@ test_expect_success 'trying to create a tag with the name of one existing should ' test_expect_success 'trying to create a tag with a non-valid name should fail' ' - git tag -l >actual && - test_line_count = 1 actual && + git tag -l >tags-before && test_must_fail git tag "" && test_must_fail git tag .othertag && test_must_fail git tag "other tag" && test_must_fail git tag "othertag^" && test_must_fail git tag "other~tag" && - git tag -l >actual && - test_line_count = 1 actual + git tag -l >tags-after && + test_cmp tags-before tags-after ' test_expect_success 'creating a tag using HEAD directly should succeed' ' From e3253255d3e1c007e80742c304ddde9421dca9ca Mon Sep 17 00:00:00 2001 From: Siddharth Shrimali Date: Tue, 21 Apr 2026 11:03:33 +0530 Subject: [PATCH 05/16] t7004: dynamically grab expected state in tests The tests for 'Multiple -l or --list options' and 'trying to delete tags without params', hardcodes that exactly one or two specific tags ('myhead', 'mytag') exist in the repository. If other tests are added, modified, or removed earlier in the script, this expected global state will change, resulting in these tests to fail for completely unrelated reasons. Instead of hardcoding the expected tags, dynamically grab the state of the repository before running the commands under test ('git tag -l' and 'git tag -d'), and verify that the output matches or remains unchanged afterward. This keeps the tests independent from the script's overall state. Signed-off-by: Siddharth Shrimali Signed-off-by: Junio C Hamano --- t/t7004-tag.sh | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh index 77a7a9777d1989..bef7618da24433 100755 --- a/t/t7004-tag.sh +++ b/t/t7004-tag.sh @@ -145,9 +145,7 @@ test_expect_success 'listing all tags if one exists should succeed' ' ' test_expect_success 'Multiple -l or --list options are equivalent to one -l option' ' - cat >expect <<-\EOF && - mytag - EOF + git tag -l >expect && git tag -l -l >actual && test_cmp expect actual && git tag --list --list >actual && @@ -226,12 +224,7 @@ test_expect_success 'trying to delete an unknown tag should fail' ' ' test_expect_success 'trying to delete tags without params should succeed and do nothing' ' - cat >expect <<-\EOF && - myhead - mytag - EOF - git tag -l >actual && - test_cmp expect actual && + git tag -l >expect && git tag -d && git tag -l >actual && test_cmp expect actual From ef85286e511b4cebfdce0c4bffc7c8985274f142 Mon Sep 17 00:00:00 2001 From: Siddharth Shrimali Date: Tue, 21 Apr 2026 11:03:34 +0530 Subject: [PATCH 06/16] t7004: avoid subshells to capture git exit codes Several tests in t7004 use the 'test$(git ...) = ...' or the '! (git ...)' subshell pattern. This swallows git's exit code. If git crashes (e.g. segmentation fault) the crash would go undetected, and the test would fail due to a mismatch or an inverted exit code. Modernize these tests by directly writing output to files(actual) and verifying them with 'test_cmp' or 'test_grep'. Replace subshell negations with 'test_must_fail'. This way, if git crashes, the test fails immediately and clearly instead of hiding the error behind a string mismatch. Signed-off-by: Siddharth Shrimali Signed-off-by: Junio C Hamano --- t/t7004-tag.sh | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh index bef7618da24433..d918005dd9859a 100755 --- a/t/t7004-tag.sh +++ b/t/t7004-tag.sh @@ -155,8 +155,10 @@ test_expect_success 'Multiple -l or --list options are equivalent to one -l opti ' test_expect_success 'listing all tags if one exists should output that tag' ' - test $(git tag -l) = mytag && - test $(git tag) = mytag + git tag -l >actual && + test_grep "^mytag$" actual && + git tag >actual && + test_grep "^mytag$" actual ' # pattern matching: @@ -166,11 +168,15 @@ test_expect_success 'listing a tag using a matching pattern should succeed' ' ' test_expect_success 'listing a tag with --ignore-case' ' - test $(git tag -l --ignore-case MYTAG) = mytag + echo mytag >expect && + git tag -l --ignore-case MYTAG >actual && + test_cmp expect actual ' test_expect_success 'listing a tag using a matching pattern should output that tag' ' - test $(git tag -l mytag) = mytag + echo mytag >expect && + git tag -l mytag >actual && + test_cmp expect actual ' test_expect_success 'listing tags using a non-matching pattern should succeed' ' @@ -430,8 +436,12 @@ test_expect_success 'listing tags -n in column with column.ui ignored' ' test_expect_success 'a non-annotated tag created without parameters should point to HEAD' ' git tag non-annotated-tag && - test $(git cat-file -t non-annotated-tag) = commit && - test $(git rev-parse non-annotated-tag) = $(git rev-parse HEAD) + echo commit >expect && + git cat-file -t non-annotated-tag >actual && + test_cmp expect actual && + git rev-parse HEAD >expect && + git rev-parse non-annotated-tag >actual && + test_cmp expect actual ' test_expect_success 'trying to verify an unknown tag should fail' ' @@ -1520,11 +1530,11 @@ test_expect_success GPG 'verify signed tag fails when public key is not present' ' test_expect_success 'git tag -a fails if tag annotation is empty' ' - ! (GIT_EDITOR=cat git tag -a initial-comment) + test_must_fail env GIT_EDITOR=cat git tag -a initial-comment ' test_expect_success 'message in editor has initial comment' ' - ! (GIT_EDITOR=cat git tag -a initial-comment >actual) + test_must_fail env GIT_EDITOR=cat git tag -a initial-comment >actual ' test_expect_success 'message in editor has initial comment: first line' ' From 7584d10bc289011cf005d453591a4c009d8b6508 Mon Sep 17 00:00:00 2001 From: Mirko Faina Date: Wed, 22 Apr 2026 23:45:17 +0200 Subject: [PATCH 07/16] Fix docs for format.commitListFormat When renaming the option --cover-letter-format to --commit-list-format we forgot to rename the opton in the section too. Fix it. Signed-off-by: Mirko Faina Signed-off-by: Junio C Hamano --- Documentation/config/format.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/config/format.adoc b/Documentation/config/format.adoc index dbd186290b501f..95d19134b26ea0 100644 --- a/Documentation/config/format.adoc +++ b/Documentation/config/format.adoc @@ -102,7 +102,7 @@ format.coverLetter:: Default is false. format.commitListFormat:: - When the `--cover-letter-format` option is not given, `format-patch` + When the `--commit-list-format` option is not given, `format-patch` uses the value of this variable to decide how to format the entry of each commit. Defaults to `shortlog`. From b2eec6663f10a53dead5e7a4e5e9b91220bf9473 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Mon, 20 Apr 2026 22:30:14 +0000 Subject: [PATCH 08/16] merge-ort: handle cached rename & trivial resolution interaction better Back in commit a562d90a350d (merge-ort: fix failing merges in special corner case, 2025-11-03), we hit a rename assertion due to a trivial directory resolution affecting the parent of a cached rename. Since the path didn't need to be considered, we side-stepped it with if (!newinfo) continue; in process_renames(). We have since run into a case in production where a trivial resolution of a file affects the direct target of a cached rename rather than a parent directory of it. Add a testcase demonstrating this additional case. Now, if we were to follow the lead of commit a562d90a350d, we could resolve this alternate case with an extra condition on the above if: if (!newinfo || newinfo->merged.clean) continue; However, if we had done that earlier, we would have made 979ee83e8a90 (merge-ort: fix corner case recursive submodule/directory conflict handling, 2025-12-29) harder to find and fix, and this particular position for this condition isn't actually at the root of the issue but downstream from it. Instead, let's rip out this if-check from a562d90a350d and put in an alternative that more directly addresses trivially resolved paths that happen to be cached renames or parent directories thereof, which is a better fix for the original testcase and which also solves the newly added testcase as well. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-ort.c | 48 +++++++++---------- t/t6429-merge-sequence-rename-caching.sh | 60 ++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 26 deletions(-) diff --git a/merge-ort.c b/merge-ort.c index 00923ce3cd749b..544be9e466c9b5 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -2953,32 +2953,6 @@ static int process_renames(struct merge_options *opt, if (!oldinfo || oldinfo->merged.clean) continue; - /* - * Rename caching from a previous commit might give us an - * irrelevant rename for the current commit. - * - * Imagine: - * foo/A -> bar/A - * was a cached rename for the upstream side from the - * previous commit (without the directories being renamed), - * but the next commit being replayed - * * does NOT add or delete files - * * does NOT have directory renames - * * does NOT modify any files under bar/ - * * does NOT modify foo/A - * * DOES modify other files under foo/ (otherwise the - * !oldinfo check above would have already exited for - * us) - * In such a case, our trivial directory resolution will - * have already merged bar/, and our attempt to process - * the cached - * foo/A -> bar/A - * would be counterproductive, and lack the necessary - * information anyway. Skip such renames. - */ - if (!newinfo) - continue; - /* * diff_filepairs have copies of pathnames, thus we have to * use standard 'strcmp()' (negated) instead of '=='. @@ -3329,6 +3303,28 @@ static void use_cached_pairs(struct merge_options *opt, if (!new_name) new_name = old_name; + /* + * If this is a rename and the target path is either + * absent from opt->priv->paths (because a parent + * directory was trivially resolved) or already cleanly + * resolved (e.g. all three sides agree on its content), + * the cached rename is irrelevant for this commit. + * Skip it here rather than in process_renames() to + * preserve VERIFY_CI(newinfo)'s ability to catch bugs + * for non-cached renames (see 979ee83e8a90 (merge-ort: + * fix corner case recursive submodule/directory conflict + * handling, 2025-12-29) for an example of a bug that + * assertion caught). The rename remains in cached_pairs + * for use in subsequent commits. + */ + if (entry->value) { + struct merged_info *mi; + + mi = strmap_get(&opt->priv->paths, new_name); + if (!mi || mi->clean) + continue; + } + /* * cached_pairs has *copies* of old_name and new_name, * because it has to persist across merges. Since diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh index 15dd2d94b75f0a..56ee9689898276 100755 --- a/t/t6429-merge-sequence-rename-caching.sh +++ b/t/t6429-merge-sequence-rename-caching.sh @@ -846,4 +846,64 @@ test_expect_success 'rename a file, use it on first pick, but irrelevant on seco ) ' +# +# In the following testcase: +# Base: subdir/file_1 +# Upstream: file_1 (renamed from subdir/file) +# Topic_1: subdir/file_2 (modified subdir/file) +# Topic_2: subdir/file_2, file_2 (added another "file" with same contents) +# Topic_3: file_2 (deleted subdir/file) +# +# +# This testcase presents no problems for git traditionally, but the fact that +# subdir/file -> file +# gets cached after the first pick presents a problem for the third commit +# to be replayed, because file has contents file_2 on all three sides and +# is thus trivially resolved early. The point of renames is to allow us to +# three-way merge contents across multiple filenames, but if the target is +# already resolved, we risk throwing an assertion. Verify that the code +# correctly drops the irrelevant rename in order to avoid hitting that +# assertion. +# +test_expect_success 'cached rename does not assert on trivially clean target' ' + git init cached-rename-trivially-clean-target && + ( + cd cached-rename-trivially-clean-target && + + mkdir subdir && + printf "%s\n" 1 2 3 >subdir/file && + git add subdir/file && + git commit -m orig && + + git branch upstream && + git branch topic && + + git switch upstream && + git mv subdir/file file && + git commit -m "rename subdir/file to file" && + + git switch topic && + + echo 4 >>subdir/file && + git add subdir/file && + git commit -m "modify subdir/file" && + + cp subdir/file file && + git add file && + git commit -m "copy subdir/file to file" && + + git rm subdir/file && + git commit -m "delete subdir/file" && + + git switch upstream && + git replay --onto HEAD upstream..topic && + git checkout topic && + + git ls-files >tracked-files && + test_line_count = 1 tracked-files && + printf "%s\n" 1 2 3 4 >expect && + test_cmp expect file + ) +' + test_done From 9ff4b5ab1b3b66d454a6c09e92d608c9be15a7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Fri, 24 Apr 2026 23:04:27 +0200 Subject: [PATCH 09/16] grep: fix --column --only-match for 2nd and later matches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "git grep --column --only-match" shows the 1-based column number of the first match on each line, but confusing numbers for further matches. Example: $ echo 123456789012345678901234567890 >file $ for d in 1 2 3 4 5 6 7 8 9 0 do git grep --no-index --column --only-matching $d file | awk -v FS=: -v l=$d: '{l = l sprintf("%3s", $2)} END {print l}' done 1: 1 2 12 2: 2 4 14 3: 3 6 16 4: 4 8 18 5: 5 10 20 6: 6 12 22 7: 7 14 24 8: 8 16 26 9: 9 18 28 0: 10 20 30 Report the column number of each match instead: $ for d in 1 2 3 4 5 6 7 8 9 0 do ./git grep --no-index --column --only-matching $d file | awk -v FS=: -v l=$d: '{l = l sprintf("%3s", $2)} END {print l}' done 1: 1 11 21 2: 2 12 22 3: 3 13 23 4: 4 14 24 5: 5 15 25 6: 6 16 26 7: 7 17 27 8: 8 18 28 9: 9 19 29 0: 10 20 30 We need to adjust the test in t7810 as well. The file it uses has the following five lines; I add a line highlighting the matches and a ruler at the bottom here, to make it easier to see that the second "mmap" indeed starts at column 14: foo mmap bar foo_mmap bar foo_mmap bar mmap foo mmap bar_mmap foo_mmap bar mmap baz ==== ==== 123456789 123456789 1 Reported-by: Brandon Chinn Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- grep.c | 3 ++- t/t7810-grep.sh | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/grep.c b/grep.c index c7e1dc1e0ee4fe..a54e5d86a96cfd 100644 --- a/grep.c +++ b/grep.c @@ -1267,6 +1267,7 @@ static void show_line(struct grep_opt *opt, regmatch_t match; enum grep_context ctx = GREP_CONTEXT_BODY; int eflags = 0; + const char *start = bol; if (want_color(opt->color)) { if (sign == ':') @@ -1285,6 +1286,7 @@ static void show_line(struct grep_opt *opt, if (match.rm_so == match.rm_eo) break; + cno = bol - start + match.rm_so + 1; if (opt->only_matching) show_line_header(opt, name, lno, cno, sign); else @@ -1294,7 +1296,6 @@ static void show_line(struct grep_opt *opt, if (opt->only_matching) opt->output(opt, "\n", 1); bol += match.rm_eo; - cno += match.rm_eo; rest -= match.rm_eo; eflags = REG_NOTBOL; } diff --git a/t/t7810-grep.sh b/t/t7810-grep.sh index 64ac4f04ee97ad..bd439563d6f149 100755 --- a/t/t7810-grep.sh +++ b/t/t7810-grep.sh @@ -322,11 +322,11 @@ do ${HC}file:1:5:mmap ${HC}file:2:5:mmap ${HC}file:3:5:mmap - ${HC}file:3:13:mmap + ${HC}file:3:14:mmap ${HC}file:4:5:mmap - ${HC}file:4:13:mmap + ${HC}file:4:14:mmap ${HC}file:5:5:mmap - ${HC}file:5:13:mmap + ${HC}file:5:14:mmap EOF git grep --column -n -o -e mmap $H >actual && test_cmp expected actual From 13817db2746d48b6d3cfe68e3bf10a1be67865a9 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Tue, 28 Apr 2026 18:39:08 +0000 Subject: [PATCH 10/16] stash: add --label-ours, --label-theirs, --label-base for apply Allow callers of "git stash apply" to pass custom labels for conflict markers instead of the default "Updated upstream" and "Stashed changes". Document the new options and add a test. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- Documentation/git-stash.adoc | 11 ++++++++++- builtin/stash.c | 28 ++++++++++++++++++++-------- t/t3903-stash.sh | 24 ++++++++++++++++++++++++ xdiff/xmerge.c | 6 +++--- 4 files changed, 57 insertions(+), 12 deletions(-) diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc index b05c990ecd8759..50bb89f48362a4 100644 --- a/Documentation/git-stash.adoc +++ b/Documentation/git-stash.adoc @@ -12,7 +12,7 @@ git stash list [] git stash show [-u | --include-untracked | --only-untracked] [] [] git stash drop [-q | --quiet] [] git stash pop [--index] [-q | --quiet] [] -git stash apply [--index] [-q | --quiet] [] +git stash apply [--index] [-q | --quiet] [--label-ours=