From f39a29c22ee5deb184517c99890bd749f2114a98 Mon Sep 17 00:00:00 2001 From: Julia Evans Date: Sat, 23 Aug 2025 00:42:58 +0000 Subject: [PATCH 01/13] doc: git-rebase: start with an example - Start with an example that mirrors the example in the `git-merge` man page, to make it easier for folks to understand the difference between a rebase and a merge. - Mention that rebase can combine or reorder commits Signed-off-by: Julia Evans Signed-off-by: Junio C Hamano --- Documentation/git-rebase.adoc | 52 +++++++++++++++++------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/Documentation/git-rebase.adoc b/Documentation/git-rebase.adoc index 956d3048f5a618..bb5a3ff7f82860 100644 --- a/Documentation/git-rebase.adoc +++ b/Documentation/git-rebase.adoc @@ -16,6 +16,32 @@ SYNOPSIS DESCRIPTION ----------- +Transplant a series of commits onto a different starting point. +You can also use `git rebase` to reorder or combine commits: see INTERACTIVE +MODE below for how to do that. + +For example, imagine that you have been working on the `topic` branch in this +history, and you want to "catch up" to the work done on the `master` branch. + +------------ + A---B---C topic + / + D---E---F---G master +------------ + +You want to transplant the commits you made on `topic` since it diverged from +`master` (i.e. A, B, and C), on top of the current `master`. You can do this +by running `git rebase master` while the `topic` branch is checked out. If you +want to rebase `topic` while on another branch, `git rebase master topic` is a +shortcut for `git checkout topic && git rebase master`. + +------------ + A'--B'--C' topic + / + D---E---F---G master +------------ + + If `` is specified, `git rebase` will perform an automatic `git switch ` before doing anything else. Otherwise it remains on the current branch. @@ -58,32 +84,6 @@ that caused the merge failure with `git rebase --skip`. To check out the original `` and remove the `.git/rebase-apply` working files, use the command `git rebase --abort` instead. -Assume the following history exists and the current branch is "topic": - ------------- - A---B---C topic - / - D---E---F---G master ------------- - -From this point, the result of either of the following commands: - - - git rebase master - git rebase master topic - -would be: - ------------- - A'--B'--C' topic - / - D---E---F---G master ------------- - -*NOTE:* The latter form is just a short-hand of `git checkout topic` -followed by `git rebase master`. When rebase exits `topic` will -remain the checked-out branch. - If the upstream branch already contains a change you have made (e.g., because you mailed a patch which was applied upstream), then that commit will be skipped and warnings will be issued (if the 'merge' backend is From af5a099197fca46fd0a255fe036ab03f2d63ffe3 Mon Sep 17 00:00:00 2001 From: Julia Evans Date: Sat, 23 Aug 2025 00:42:59 +0000 Subject: [PATCH 02/13] doc: git rebase: dedup merge conflict discussion Previously there were two explanations, this combines them both into a single explanation. Signed-off-by: Julia Evans Signed-off-by: Junio C Hamano --- Documentation/git-rebase.adoc | 49 ++++++++++++++--------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/Documentation/git-rebase.adoc b/Documentation/git-rebase.adoc index bb5a3ff7f82860..e82ceb9cbfcefa 100644 --- a/Documentation/git-rebase.adoc +++ b/Documentation/git-rebase.adoc @@ -42,6 +42,26 @@ shortcut for `git checkout topic && git rebase master`. ------------ +If there is a merge conflict during this process, `git rebase` will stop at the +first problematic commit and leave conflict markers. If this happens, you can do +one of these things: + +1. Resolve the conflict. You can use `git diff` to find the markers (<<<<<<) + and make edits to resolve the conflict. For each file you edit, you need to + tell Git that the conflict has been resolved. You can mark the conflict as + resolved with `git add `. After resolving all of the conflicts, + you can continue the rebasing process with + + git rebase --continue + +2. Stop the `git rebase` and return your branch to its original state with + + git rebase --abort + +3. Skip the commit that caused the merge conflict with + + git rebase --skip + If `` is specified, `git rebase` will perform an automatic `git switch ` before doing anything else. Otherwise it remains on the current branch. @@ -77,13 +97,6 @@ any commits in `HEAD` which introduce the same textual changes as a commit in `HEAD..` are omitted (i.e., a patch already accepted upstream with a different commit message or timestamp will be skipped). -It is possible that a merge failure will prevent this process from being -completely automatic. You will have to resolve any such merge failure -and run `git rebase --continue`. Another option is to bypass the commit -that caused the merge failure with `git rebase --skip`. To check out the -original `` and remove the `.git/rebase-apply` working files, use -the command `git rebase --abort` instead. - If the upstream branch already contains a change you have made (e.g., because you mailed a patch which was applied upstream), then that commit will be skipped and warnings will be issued (if the 'merge' backend is @@ -186,28 +199,6 @@ This is useful if F and G were flawed in some way, or should not be part of topicA. Note that the argument to `--onto` and the `` parameter can be any valid commit-ish. -In case of conflict, `git rebase` will stop at the first problematic commit -and leave conflict markers in the tree. You can use `git diff` to locate -the markers (<<<<<<) and make edits to resolve the conflict. For each -file you edit, you need to tell Git that the conflict has been resolved, -typically this would be done with - - - git add - - -After resolving the conflict manually and updating the index with the -desired resolution, you can continue the rebasing process with - - - git rebase --continue - - -Alternatively, you can undo the 'git rebase' with - - - git rebase --abort - MODE OPTIONS ------------ From 1469715a9c9dc6e291567fcee16cda9943767138 Mon Sep 17 00:00:00 2001 From: Julia Evans Date: Sat, 23 Aug 2025 00:43:00 +0000 Subject: [PATCH 03/13] doc: git rebase: clarify arguments syntax Remove duplicate explanation of `git rebase ` which is already explained above. Signed-off-by: Julia Evans Signed-off-by: Junio C Hamano --- Documentation/git-rebase.adoc | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Documentation/git-rebase.adoc b/Documentation/git-rebase.adoc index e82ceb9cbfcefa..6d02648a9b3cee 100644 --- a/Documentation/git-rebase.adoc +++ b/Documentation/git-rebase.adoc @@ -62,11 +62,7 @@ one of these things: git rebase --skip -If `` is specified, `git rebase` will perform an automatic -`git switch ` before doing anything else. Otherwise -it remains on the current branch. - -If `` is not specified, the upstream configured in +If you don't specify an `` to rebase onto, the upstream configured in `branch..remote` and `branch..merge` options will be used (see linkgit:git-config[1] for details) and the `--fork-point` option is assumed. If you are currently not on any branch or if the current From 981ce57389af2eafb219a8dc4d6d0f55888c4a14 Mon Sep 17 00:00:00 2001 From: Julia Evans Date: Sat, 23 Aug 2025 00:43:01 +0000 Subject: [PATCH 04/13] doc: git-rebase: move --onto explanation down There's a very clear explanation with examples of using --onto which is currently buried in the very long DESCRIPTION section. This moves it to its own section, so that we can reference the explanation from the `--onto` option by name. Signed-off-by: Julia Evans Signed-off-by: Junio C Hamano --- Documentation/git-rebase.adoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Documentation/git-rebase.adoc b/Documentation/git-rebase.adoc index 6d02648a9b3cee..b3354e0e4f82e9 100644 --- a/Documentation/git-rebase.adoc +++ b/Documentation/git-rebase.adoc @@ -114,6 +114,9 @@ will result in: D---E---A'---F master ------------ +TRANSPLANTING A TOPIC BRANCH WITH --ONTO +---------------------------------------- + Here is how you would transplant a topic branch based on one branch to another, to pretend that you forked the topic branch from the latter branch, using `rebase --onto`. @@ -240,6 +243,8 @@ As a special case, you may use "A\...B" as a shortcut for the merge base of A and B if there is exactly one merge base. You can leave out at most one of A and B, in which case it defaults to HEAD. +See TRANSPLANTING A TOPIC BRANCH WITH --ONTO above for examples. + --keep-base:: Set the starting point at which to create the new commits to the merge base of `` and ``. Running From 3f7f2b0359e38a86db601b406d68e8fb43ae977e Mon Sep 17 00:00:00 2001 From: Julia Evans Date: Sat, 23 Aug 2025 00:43:02 +0000 Subject: [PATCH 05/13] doc: git-rebase: update discussion of internals - make it clearer that we're talking about a multistep process - give a more technically accurate description how rebase works with the merge backend. - condense the explanation of how git rebase skips commits with the same textual changes into a single bullet point and remove the explanatory diagram. Lots of things which are more complicated are already being explained without a diagram. - remove the explanation of how exactly `--fork-point` and `--root` work since that information is in the OPTIONS section - put all discussion of `ORIG_HEAD` inside the note Signed-off-by: Julia Evans Signed-off-by: Junio C Hamano --- Documentation/git-rebase.adoc | 61 +++++++++++------------------------ 1 file changed, 18 insertions(+), 43 deletions(-) diff --git a/Documentation/git-rebase.adoc b/Documentation/git-rebase.adoc index b3354e0e4f82e9..d2f760af68666b 100644 --- a/Documentation/git-rebase.adoc +++ b/Documentation/git-rebase.adoc @@ -68,51 +68,26 @@ linkgit:git-config[1] for details) and the `--fork-point` option is assumed. If you are currently not on any branch or if the current branch does not have a configured upstream, the rebase will abort. -All changes made by commits in the current branch but that are not -in `` are saved to a temporary area. This is the same set -of commits that would be shown by `git log ..HEAD`; or by -`git log 'fork_point'..HEAD`, if `--fork-point` is active (see the -description on `--fork-point` below); or by `git log HEAD`, if the -`--root` option is specified. - -The current branch is reset to `` or `` if the -`--onto` option was supplied. This has the exact same effect as -`git reset --hard ` (or ``). `ORIG_HEAD` is set -to point at the tip of the branch before the reset. +Here is a simplified description of what `git rebase ` does: + +1. Make a list of all commits on your current branch since it branched + off from `` that do not have an equivalent commit in + ``. +2. Check out `` with the equivalent of + `git checkout --detach `. +3. Replay the commits, one by one, in order. This is similar to running + `git cherry-pick ` for each commit. See REBASING MERGES for how merges + are handled. +4. Update your branch to point to the final commit with the equivalent + of `git checkout -B `. [NOTE] -`ORIG_HEAD` is not guaranteed to still point to the previous branch tip -at the end of the rebase if other commands that write that pseudo-ref -(e.g. `git reset`) are used during the rebase. The previous branch tip, -however, is accessible using the reflog of the current branch -(i.e. `@{1}`, see linkgit:gitrevisions[7]). - -The commits that were previously saved into the temporary area are -then reapplied to the current branch, one by one, in order. Note that -any commits in `HEAD` which introduce the same textual changes as a commit -in `HEAD..` are omitted (i.e., a patch already accepted upstream -with a different commit message or timestamp will be skipped). - -If the upstream branch already contains a change you have made (e.g., -because you mailed a patch which was applied upstream), then that commit -will be skipped and warnings will be issued (if the 'merge' backend is -used). For example, running `git rebase master` on the following -history (in which `A'` and `A` introduce the same set of changes, but -have different committer information): - ------------- - A---B---C topic - / - D---E---A'---F master ------------- - -will result in: - ------------- - B'---C' topic - / - D---E---A'---F master ------------- +When starting the rebase, `ORIG_HEAD` is set to point to the commit at the tip +of the to-be-rebased branch. However, `ORIG_HEAD` is not guaranteed to still +point to that commit at the end of the rebase if other commands that change +`ORIG_HEAD` (like `git reset`) are used during the rebase. The previous branch +tip, however, is accessible using the reflog of the current branch (i.e. `@{1}`, +see linkgit:gitrevisions[7]. TRANSPLANTING A TOPIC BRANCH WITH --ONTO ---------------------------------------- From a3540ed20efad4e1aebb71edac2fc74604f2122e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SZEDER=20G=C3=A1bor?= Date: Sun, 24 Aug 2025 21:06:41 +0200 Subject: [PATCH 06/13] line-log: avoid unnecessary tree diffs when processing merge commits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In process_ranges_merge_commit(), the line-level log first creates an array of diff queues by iterating over all parents of a merge commit and computing a tree diff for each. Then in a second loop it iterates over those diff queues, and if it finds that none of the interesting paths were modified in one of them, then it will return early. This means that when none of the interesting paths were modified between a merge and its first parent, then the tree diff between the merge and its second (Nth...) parent was computed in vain. Unify these two loops, so when it iterates over all parents of a merge commit, then it first computes the tree diff between the merge and that particular parent and then processes the resulting diff queue right away. This way we can spare some tree diff computing, thereby speeding up line-level log in repositories with mergy history: # git.git, 25.8% of commits are merges: Benchmark 1: ./git_v2.51.0 -C ~/src/git log -L:'lookup_commit(':commit.c v2.51.0 Time (mean ± σ): 1.001 s ± 0.009 s [User: 0.906 s, System: 0.095 s] Range (min … max): 0.991 s … 1.023 s 10 runs Benchmark 2: ./git -C ~/src/git log -L:'lookup_commit(':commit.c v2.51.0 Time (mean ± σ): 445.5 ms ± 3.4 ms [User: 358.8 ms, System: 84.3 ms] Range (min … max): 440.1 ms … 450.3 ms 10 runs Summary './git -C ~/src/git log -L:'lookup_commit(':commit.c v2.51.0' ran 2.25 ± 0.03 times faster than './git_v2.51.0 -C ~/src/git log -L:'lookup_commit(':commit.c v2.51.0' # linux.git, 7.5% of commits are merges: Benchmark 1: ./git_v2.51.0 -C ~/src/linux.git log -L:build_restore_work_registers:arch/mips/mm/tlbex.c v6.16 Time (mean ± σ): 3.246 s ± 0.007 s [User: 2.835 s, System: 0.409 s] Range (min … max): 3.232 s … 3.255 s 10 runs Benchmark 2: ./git -C ~/src/linux.git log -L:build_restore_work_registers:arch/mips/mm/tlbex.c v6.16 Time (mean ± σ): 2.467 s ± 0.014 s [User: 2.113 s, System: 0.353 s] Range (min … max): 2.455 s … 2.505 s 10 runs Summary './git -C ~/src/linux.git log -L:build_restore_work_registers:arch/mips/mm/tlbex.c v6.16' ran 1.32 ± 0.01 times faster than './git_v2.51.0 -C ~/src/linux.git log -L:build_restore_work_registers:arch/mips/mm/tlbex.c v6.16' And since now each iteration computes a tree diff and processes its result, there is no reason to store the diff queues for each merge parent anymore, so replace that diff queue array with a loop-local diff queue variable. With this change the static free_diffqueues() helper function in 'line-log.c' has no more callers left, remove it. Signed-off-by: SZEDER Gábor Signed-off-by: Junio C Hamano --- line-log.c | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/line-log.c b/line-log.c index 07f2154e84b843..cf30915c942825 100644 --- a/line-log.c +++ b/line-log.c @@ -1087,13 +1087,6 @@ static struct diff_filepair *diff_filepair_dup(struct diff_filepair *pair) return new_filepair; } -static void free_diffqueues(int n, struct diff_queue_struct *dq) -{ - for (int i = 0; i < n; i++) - diff_queue_clear(&dq[i]); - free(dq); -} - static int process_all_files(struct line_log_data **range_out, struct rev_info *rev, struct diff_queue_struct *queue, @@ -1209,7 +1202,6 @@ static int process_ranges_ordinary_commit(struct rev_info *rev, struct commit *c static int process_ranges_merge_commit(struct rev_info *rev, struct commit *commit, struct line_log_data *range) { - struct diff_queue_struct *diffqueues; struct line_log_data **cand; struct commit **parents; struct commit_list *p; @@ -1220,20 +1212,19 @@ static int process_ranges_merge_commit(struct rev_info *rev, struct commit *comm if (nparents > 1 && rev->first_parent_only) nparents = 1; - ALLOC_ARRAY(diffqueues, nparents); CALLOC_ARRAY(cand, nparents); ALLOC_ARRAY(parents, nparents); p = commit->parents; for (i = 0; i < nparents; i++) { + struct diff_queue_struct diffqueue = DIFF_QUEUE_INIT; + int changed; parents[i] = p->item; p = p->next; - queue_diffs(range, &rev->diffopt, &diffqueues[i], commit, parents[i]); - } + queue_diffs(range, &rev->diffopt, &diffqueue, commit, parents[i]); - for (i = 0; i < nparents; i++) { - int changed; - changed = process_all_files(&cand[i], rev, &diffqueues[i], range); + changed = process_all_files(&cand[i], rev, &diffqueue, range); + diff_queue_clear(&diffqueue); if (!changed) { /* * This parent can take all the blame, so we @@ -1267,7 +1258,6 @@ static int process_ranges_merge_commit(struct rev_info *rev, struct commit *comm free(cand[i]); } free(cand); - free_diffqueues(nparents, diffqueues); return ret; /* NEEDSWORK evil merge detection stuff */ From 9df27c258edf89ea8ea0472a0a9c260e026f197f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SZEDER=20G=C3=A1bor?= Date: Sun, 24 Aug 2025 21:06:42 +0200 Subject: [PATCH 07/13] line-log: get rid of the parents array in process_ranges_merge_commit() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We can easily iterate through the parents of a merge commit without turning the list of parents into a dynamically allocated array of parents, so let's do so. This way we can avoid a memory allocation for each processed merge commit, though its effect on runtime seems to be unmeasurable. Signed-off-by: SZEDER Gábor Signed-off-by: Junio C Hamano --- line-log.c | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/line-log.c b/line-log.c index cf30915c942825..b2a31ae956fee9 100644 --- a/line-log.c +++ b/line-log.c @@ -1203,7 +1203,6 @@ static int process_ranges_merge_commit(struct rev_info *rev, struct commit *comm struct line_log_data *range) { struct line_log_data **cand; - struct commit **parents; struct commit_list *p; int i; int nparents = commit_list_count(commit->parents); @@ -1213,15 +1212,15 @@ static int process_ranges_merge_commit(struct rev_info *rev, struct commit *comm nparents = 1; CALLOC_ARRAY(cand, nparents); - ALLOC_ARRAY(parents, nparents); - p = commit->parents; - for (i = 0; i < nparents; i++) { + for (p = commit->parents, i = 0; + p && i < nparents; + p = p->next, i++) { + struct commit *parent = p->item; struct diff_queue_struct diffqueue = DIFF_QUEUE_INIT; int changed; - parents[i] = p->item; - p = p->next; - queue_diffs(range, &rev->diffopt, &diffqueue, commit, parents[i]); + + queue_diffs(range, &rev->diffopt, &diffqueue, commit, parent); changed = process_all_files(&cand[i], rev, &diffqueue, range); diff_queue_clear(&diffqueue); @@ -1230,9 +1229,9 @@ static int process_ranges_merge_commit(struct rev_info *rev, struct commit *comm * This parent can take all the blame, so we * don't follow any other path in history */ - add_line_range(rev, parents[i], cand[i]); + add_line_range(rev, parent, cand[i]); free_commit_list(commit->parents); - commit_list_append(parents[i], &commit->parents); + commit_list_append(parent, &commit->parents); ret = 0; goto out; @@ -1243,14 +1242,15 @@ static int process_ranges_merge_commit(struct rev_info *rev, struct commit *comm * No single parent took the blame. We add the candidates * from the above loop to the parents. */ - for (i = 0; i < nparents; i++) - add_line_range(rev, parents[i], cand[i]); + for (p = commit->parents, i = 0; + p && i < nparents; + p = p->next, i++) + add_line_range(rev, p->item, cand[i]); ret = 1; out: clear_commit_line_range(rev, commit); - free(parents); for (i = 0; i < nparents; i++) { if (!cand[i]) continue; From 62e4ef85fbc5574fd80caababbf41bd33f53a46d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SZEDER=20G=C3=A1bor?= Date: Sun, 24 Aug 2025 21:06:43 +0200 Subject: [PATCH 08/13] line-log: initialize diff queue in process_ranges_ordinary_commit() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit process_ranges_ordinary_commit() uses a local diff queue variable, which it leaves uninitialized before passing its address to queue_diffs(). This is not an issue, because at the end of that function the contents of an other diff queue is moved into it by simply overwriting whatever is in there, i.e. without reading any uninitialized memory. Still, seeing the uninitialized diff queue being passed around scared me more than once, so out of caution let's make sure that it's initialized. Signed-off-by: SZEDER Gábor Signed-off-by: Junio C Hamano --- line-log.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/line-log.c b/line-log.c index b2a31ae956fee9..71fa857ee83a79 100644 --- a/line-log.c +++ b/line-log.c @@ -1182,7 +1182,7 @@ static int process_ranges_ordinary_commit(struct rev_info *rev, struct commit *c struct line_log_data *range) { struct commit *parent = NULL; - struct diff_queue_struct queue; + struct diff_queue_struct queue = DIFF_QUEUE_INIT; struct line_log_data *parent_range; int changed; From 0a15bb634cf005a0266ee1108ac31aa75649a61c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SZEDER=20G=C3=A1bor?= Date: Sun, 24 Aug 2025 21:06:44 +0200 Subject: [PATCH 09/13] line-log: simplify condition checking for merge commits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In process_ranges_arbitrary_commit() the condition deciding whether the given commit is not a merge, i.e. that it doesn't have more than one parent, is head-scratchingly backwards, flip it. Signed-off-by: SZEDER Gábor Signed-off-by: Junio C Hamano --- line-log.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/line-log.c b/line-log.c index 71fa857ee83a79..188d387d40f58d 100644 --- a/line-log.c +++ b/line-log.c @@ -1273,10 +1273,10 @@ int line_log_process_ranges_arbitrary_commit(struct rev_info *rev, struct commit struct line_log_data *prange = line_log_data_copy(range); add_line_range(rev, commit->parents->item, prange); clear_commit_line_range(rev, commit); - } else if (!commit->parents || !commit->parents->next) - changed = process_ranges_ordinary_commit(rev, commit, range); - else + } else if (commit->parents && commit->parents->next) changed = process_ranges_merge_commit(rev, commit, range); + else + changed = process_ranges_ordinary_commit(rev, commit, range); } if (!changed) From 8f32a5a6c050766bfa2827869ec9116cebd0eb9b Mon Sep 17 00:00:00 2001 From: Jeff King Date: Sun, 24 Aug 2025 01:00:40 -0400 Subject: [PATCH 10/13] fetch-pack: re-scan when double-checking graph objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fetch code tries to avoid asking the remote side for an object we already have. It does this by traversing recent commits reachable from our refs looking for matches. Commit 5d4cc78f72 (fetch-pack: die if in commit graph but not obj db, 2024-11-05) introduced an extra check there: if we think we have an object because it's in the commit graph, we double-check that we actually have it in our object database with a call to odb_has_object(). But that call does not pass any flags, and so the function won't call reprepared_packed_git() if it does not find the object. That opens us up to the usual race against some other process repacking the odb: 1. We scan the list of packs in objects/pack but haven't yet opened them. 2. Somebody else packs the object into a new pack (which we don't know about), and deletes the old pack it was in. 3. Our odb_has_object() calls tries to open that old pack, but finds it is gone. We declare that we don't have the object. And this causes us to erroneously complain and abort the fetch, thinking our commit-graph and object database are out of sync. Instead, we should pass HAS_OBJECT_RECHECK_PACKED, which will add a new step: 4. We re-scan the pack directory again, find the new pack, and locate the object. Often the fetch code tries to avoid these kinds of re-scans if it's likely that we won't have the object. If the other side has told us about object X and we want to know if we have it, we'll skip the re-scan (to avoid spending a lot of effort when there are many such objects). We can accept the racy false negative in that case because the worst case is that we ask the other side to send us the object. But this is not one of those cases. These are objects which are accessible from _our_ refs, and which we already found in the commit graph file. We should have them, and if we don't, we'll die() immediately. So the performance impact is negligible, and getting the right answer is important. There's no test here because it's inherently racy. In fact, I had trouble even developing a minimal test. The problem seen in the wild can be produced like this: # Any git.git mirror which supports partial clones; I think this # should work with any repo that contains submodules, but note that # $obj below is specific to this repo url=https://github.com/git/git.git # This is a commit that is not at the tip of any branches (so after # we have it, we'll still have some commits to fetch). obj=cf6f63ea6bf35173e02e18bdc6a4ba41288acff9 git init git fetch --filter=tree:0 $url $obj:refs/heads/foo git checkout foo git commit-graph write --reachable git fetch $url What happens here is that the initial fetch grabs that older commit (and its ancestors) but no trees or blobs, and the subsequent checkout grabs the necessary trees and blobs just for that commit. The final fetch spawns a long sequence of child fetches due to fetch_submodules(), which wants to check whether there have been any gitlink modifications which should trigger a fetch of the related submodule (we'll leave aside the irony that we did not even check out any submodules yet). That series of fetches causes us to accumulate packs, which eventually triggers background maintenance to run. That repacks all-into-one, and the pack containing $obj goes away in favor of a new pack. And then the fetch eventually fails with: fatal: You are attempting to fetch cf6f63ea6bf35173e02e18bdc6a4ba41288acff9, which is in the commit graph file but not in the object database. In the scenario above, the race becomes likely because of the long series of quick fetches. But I _think_ the bug is independent of partial clones entirely, and you could run into the same thing with a single fetch, some other process running "git repack" simultaneously, and a bit of bad luck. I haven't been able to reproduce, though. I'm not sure if that's because there's some mis-analysis above, or if the race window is just small enough that it's hard to trigger. At any rate, re-scanning here seems like an obviously correct thing to do with no downside, and it does fix the partial-clone case shown above. Reported-by: Дилян Палаузов Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- fetch-pack.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fetch-pack.c b/fetch-pack.c index 46c39f85c4ca9e..a9f5e6b5106943 100644 --- a/fetch-pack.c +++ b/fetch-pack.c @@ -143,7 +143,8 @@ static struct commit *deref_without_lazy_fetch(const struct object_id *oid, commit = lookup_commit_in_graph(the_repository, oid); if (commit) { if (mark_tags_complete_and_check_obj_db) { - if (!odb_has_object(the_repository->objects, oid, 0)) + if (!odb_has_object(the_repository->objects, oid, + HAS_OBJECT_RECHECK_PACKED)) die_in_commit_graph_only(oid); } return commit; From 457534d0417d047b943f76a849f256b739894ce9 Mon Sep 17 00:00:00 2001 From: Johannes Sixt Date: Mon, 25 Aug 2025 21:16:12 +0200 Subject: [PATCH 11/13] progress: pay attention to (customized) delay time Using one of the start_delayed_*() functions, clients of the progress API can request that a progress meter is only shown after some time. To do that, the implementation intends to count down the number of seconds stored in struct progress by observing flag progress_update, which the timer interrupt handler sets when a second has elapsed. This works during the first second of the delay. But the code forgets to reset the flag to zero, so that subsequent calls of display_progress() think that another second has elapsed and decrease the count again until zero is reached. Due to the frequency of the calls, this happens without an observable delay in practice, so that the effective delay is always just one second. This bug has been with us since the inception of the feature. Despite having been touched on various occasions, such as 8aade107dd84 (progress: simplify "delayed" progress API), 9c5951cacf5c (progress: drop delay-threshold code), and 44a4693bfcec (progress: create GIT_PROGRESS_DELAY), the short delay went unnoticed. Copy the flag state into a local variable and reset the global flag right away so that we can detect the next clock tick correctly. Since we have not had any complaints that the delay of one second is too short nor that GIT_PROGRESS_DELAY is ignored, people seem to be comfortable with the status quo. Therefore, set the default to 1 to keep the current behavior. Signed-off-by: Johannes Sixt Signed-off-by: Junio C Hamano --- Documentation/git.adoc | 2 +- progress.c | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Documentation/git.adoc b/Documentation/git.adoc index 743b7b00e4d751..03e9e69d257c1d 100644 --- a/Documentation/git.adoc +++ b/Documentation/git.adoc @@ -684,7 +684,7 @@ other `GIT_PROGRESS_DELAY`:: A number controlling how many seconds to delay before showing - optional progress indicators. Defaults to 2. + optional progress indicators. Defaults to 1. `GIT_EDITOR`:: This environment variable overrides `$EDITOR` and `$VISUAL`. diff --git a/progress.c b/progress.c index 8d5ae70f3a9ec7..8315bdc3d4a77a 100644 --- a/progress.c +++ b/progress.c @@ -114,16 +114,19 @@ static void display(struct progress *progress, uint64_t n, const char *done) const char *tp; struct strbuf *counters_sb = &progress->counters_sb; int show_update = 0; + int update = !!progress_update; int last_count_len = counters_sb->len; - if (progress->delay && (!progress_update || --progress->delay)) + progress_update = 0; + + if (progress->delay && (!update || --progress->delay)) return; progress->last_value = n; tp = (progress->throughput) ? progress->throughput->display.buf : ""; if (progress->total) { unsigned percent = n * 100 / progress->total; - if (percent != progress->last_percent || progress_update) { + if (percent != progress->last_percent || update) { progress->last_percent = percent; strbuf_reset(counters_sb); @@ -133,7 +136,7 @@ static void display(struct progress *progress, uint64_t n, const char *done) tp); show_update = 1; } - } else if (progress_update) { + } else if (update) { strbuf_reset(counters_sb); strbuf_addf(counters_sb, "%"PRIuMAX"%s", (uintmax_t)n, tp); show_update = 1; @@ -166,7 +169,6 @@ static void display(struct progress *progress, uint64_t n, const char *done) } fflush(stderr); } - progress_update = 0; } } @@ -281,7 +283,7 @@ static int get_default_delay(void) static int delay_in_secs = -1; if (delay_in_secs < 0) - delay_in_secs = git_env_ulong("GIT_PROGRESS_DELAY", 2); + delay_in_secs = git_env_ulong("GIT_PROGRESS_DELAY", 1); return delay_in_secs; } From 716d905792ebc6fcaf3860d9356fe4dd6ce61715 Mon Sep 17 00:00:00 2001 From: "brian m. carlson" Date: Mon, 25 Aug 2025 22:11:01 +0000 Subject: [PATCH 12/13] docs: note that extensions.compatobjectformat is incomplete The compatibility object format is only implemented for loose objects, not packed objects, so anyone attempting to push or fetch data into a repository with this option will likely not see it work as expected. In addition, the underlying storage of loose object mapping is likely to change because the current format is inefficient and does not handle important mapping information such as that of submodules. It would have been preferable to initially document that this was not yet ready for prime time, but we did not do so. We hinted at the fact that this functionality is incomplete in the description, but did not say so explicitly. Let's do so now: indicate that this feature is incomplete and subject to change and that the option is not designed to be used by end users. Signed-off-by: brian m. carlson Signed-off-by: Junio C Hamano --- Documentation/config/extensions.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc index 9e2f321a6d776f..829f2523fcbca4 100644 --- a/Documentation/config/extensions.adoc +++ b/Documentation/config/extensions.adoc @@ -14,6 +14,10 @@ compatObjectFormat:: compatObjectFormat. As well as being able to use oids encoded in compatObjectFormat in addition to oids encoded with objectFormat to locally specify objects. ++ +Note that the functionality enabled by this extension is incomplete and subject +to change. It currently exists only to allow development and testing of +the underlying feature and is not designed to be enabled by end users. noop:: This extension does not change git's behavior at all. It is useful only From 42bc22449512d0a5ce43155d48ee6adf278adcda Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 28 Aug 2025 11:28:26 -0700 Subject: [PATCH 13/13] The fourth batch Signed-off-by: Junio C Hamano --- Documentation/RelNotes/2.52.0.adoc | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Documentation/RelNotes/2.52.0.adoc b/Documentation/RelNotes/2.52.0.adoc index 9f8607a75240ad..3625db515ef963 100644 --- a/Documentation/RelNotes/2.52.0.adoc +++ b/Documentation/RelNotes/2.52.0.adoc @@ -86,6 +86,30 @@ including security updates, are included in this release. ignored") did not work well with "--name-only" and friends. (merge b55e6d36eb ly/diff-name-only-with-diff-from-content later to maint). + * Documentation for "git rebase" has been updated. + (merge 3f7f2b0359 je/doc-rebase later to maint). + + * The start_delayed_progress() function in the progress eye-candy API + did not clear its internal state, making an initial delay value + larger than 1 second ineffective, which has been corrected. + (merge 457534d041 js/progress-delay-fix later to maint). + + * The compatObjectFormat extension is used to hide an incomplete + feature that is not yet usable for any purpose other than + developing the feature further. Document it as such to discourage + its use by mere mortals. + (merge 716d905792 bc/doc-compat-object-format-not-working later to maint). + + * "git log -L..." compared trees of multiple parents with the tree of the + merge result in an unnecessarily inefficient way. + (merge 0a15bb634c sg/line-log-merge-optim later to maint). + + * Under a race against another process that is repacking the + repository, especially a partially cloned one, "git fetch" may + mistakenly think some objects we do have are missing, which has + been corrected. + (merge 8f32a5a6c0 jk/fetch-check-graph-objects-fix later to maint). + * Other code cleanup, docfix, build fix, etc. (merge 823d537fa7 kh/doc-git-log-markup-fix later to maint). (merge cf7efa4f33 rj/t6137-cygwin-fix later to maint).