From aef843026b710be32af1b22128868e49843f11c0 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Tue, 1 Sep 2026 22:24:37 +0000 Subject: [PATCH 01/20] commit: clarify FROM_REBASE_PICK and is_from_rebase() names Commit 430b75f7209c (commit: give correct advice for empty commit during a rebase, 2019-12-06) introduced a FROM_REBASE_PICK enum value and an is_from_rebase() function. Those names failed to convey that they were specifically about hitting a commit that becomes empty when rebasing. Clarify their names now. While at it, change `whence == FROM_REBASE_NOW_EMPTY` to use `is_from_rebase_now_empty(whence)`. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/commit.c | 14 +++++++------- sequencer.c | 2 +- wt-status.h | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/builtin/commit.c b/builtin/commit.c index 28f61745034506..17cc27e53e0c13 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -520,7 +520,7 @@ static const char *prepare_index(const char **argv, const char *prefix, die(_("cannot do a partial commit during a merge.")); else if (is_from_cherry_pick(whence)) die(_("cannot do a partial commit during a cherry-pick.")); - else if (is_from_rebase(whence)) + else if (is_from_rebase_now_empty(whence)) die(_("cannot do a partial commit during a rebase.")); } @@ -893,7 +893,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix, */ else if (whence == FROM_MERGE) hook_arg1 = "merge"; - else if (is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) { + else if (is_from_cherry_pick(whence) || is_from_rebase_now_empty(whence)) { hook_arg1 = "commit"; hook_arg2 = "CHERRY_PICK_HEAD"; } @@ -1086,7 +1086,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix, if (amend) fputs(_(empty_amend_advice), stderr); else if (is_from_cherry_pick(whence) || - whence == FROM_REBASE_PICK) { + is_from_rebase_now_empty(whence)) { fputs(_(empty_cherry_pick_advice), stderr); if (whence == FROM_CHERRY_PICK_SINGLE) fputs(_(empty_cherry_pick_advice_single), stderr); @@ -1333,7 +1333,7 @@ static int parse_and_validate_options(int argc, const char *argv[], die(_("You are in the middle of a merge -- cannot amend.")); else if (is_from_cherry_pick(whence)) die(_("You are in the middle of a cherry-pick -- cannot amend.")); - else if (whence == FROM_REBASE_PICK) + else if (is_from_rebase_now_empty(whence)) die(_("You are in the middle of a rebase -- cannot amend.")); } if (fixup_message && squash_message) @@ -1353,7 +1353,7 @@ static int parse_and_validate_options(int argc, const char *argv[], if (amend && !use_message && !fixup_message) use_message = "HEAD"; if (!use_message && !is_from_cherry_pick(whence) && - !is_from_rebase(whence) && renew_authorship) + !is_from_rebase_now_empty(whence) && renew_authorship) die(_("--reset-author can be used only with -C, -c or --amend.")); if (use_message) { use_message_buffer = read_commit_message(use_message); @@ -1362,7 +1362,7 @@ static int parse_and_validate_options(int argc, const char *argv[], author_message_buffer = use_message_buffer; } } - if ((is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) && + if ((is_from_cherry_pick(whence) || is_from_rebase_now_empty(whence)) && !renew_authorship) { author_message = "CHERRY_PICK_HEAD"; author_message_buffer = read_commit_message(author_message); @@ -1887,7 +1887,7 @@ int cmd_commit(int argc, if (!reflog_msg) reflog_msg = is_from_cherry_pick(whence) ? "commit (cherry-pick)" - : is_from_rebase(whence) + : is_from_rebase_now_empty(whence) ? "commit (rebase)" : "commit"; commit_list_insert(current_head, &parents); diff --git a/sequencer.c b/sequencer.c index 57855b0066ac98..5ebcd7ecd543e2 100644 --- a/sequencer.c +++ b/sequencer.c @@ -6855,7 +6855,7 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence) !repo_get_oid(r, "REBASE_HEAD", &rebase_head) && !repo_get_oid(r, "CHERRY_PICK_HEAD", &cherry_pick_head) && oideq(&rebase_head, &cherry_pick_head)) - *whence = FROM_REBASE_PICK; + *whence = FROM_REBASE_NOW_EMPTY; else *whence = FROM_CHERRY_PICK_SINGLE; diff --git a/wt-status.h b/wt-status.h index e9fe32e98cc18c..2143f50b49208c 100644 --- a/wt-status.h +++ b/wt-status.h @@ -41,7 +41,7 @@ enum commit_whence { FROM_MERGE, /* commit came from merge */ FROM_CHERRY_PICK_SINGLE, /* commit came from cherry-pick */ FROM_CHERRY_PICK_MULTI, /* commit came from a sequence of cherry-picks */ - FROM_REBASE_PICK /* commit came from a pick/reword/edit */ + FROM_REBASE_NOW_EMPTY /* rebase applied a pick that became empty */ }; static inline int is_from_cherry_pick(enum commit_whence whence) @@ -50,9 +50,9 @@ static inline int is_from_cherry_pick(enum commit_whence whence) whence == FROM_CHERRY_PICK_MULTI; } -static inline int is_from_rebase(enum commit_whence whence) +static inline int is_from_rebase_now_empty(enum commit_whence whence) { - return whence == FROM_REBASE_PICK; + return whence == FROM_REBASE_NOW_EMPTY; } struct wt_status_change_data { From d692305326bf24ded5185f1c76fa82b93731db01 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Tue, 1 Sep 2026 22:24:38 +0000 Subject: [PATCH 02/20] commit: allow a partial commit when a rebase pick becomes empty For years, we disallowed partial commits during merges or cherry-picks. In commit 430b75f7209c (commit: give correct advice for empty commit during a rebase, 2019-12-06) it was noted that the "cannot do a partial commit during a cherry-pick" message was also printed when rebasing a commit that became empty, and rather than drop the check in that case, that commit opted to make the message print the actual operation that was in progress. Since a commit that has become empty comes without conflicts, a new partial commit poses no problems; remove the error in that case. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/commit.c | 2 -- t/t3404-rebase-interactive.sh | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/builtin/commit.c b/builtin/commit.c index 17cc27e53e0c13..01b79185e7f91b 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -520,8 +520,6 @@ static const char *prepare_index(const char **argv, const char *prefix, die(_("cannot do a partial commit during a merge.")); else if (is_from_cherry_pick(whence)) die(_("cannot do a partial commit during a cherry-pick.")); - else if (is_from_rebase_now_empty(whence)) - die(_("cannot do a partial commit during a rebase.")); } if (list_paths(&partial, !current_head ? NULL : "HEAD", &pathspec)) diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 58b3bb0c271aae..17b30b7825ae05 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1829,7 +1829,7 @@ test_expect_success 'post-commit hook is called' ' test_cmp expect actual ' -test_expect_success 'correct error message for partial commit after empty pick' ' +test_expect_success 'partial commit is allowed when a rebase pick becomes empty' ' test_when_finished "git rebase --abort" && ( set_fake_editor && @@ -1838,8 +1838,7 @@ test_expect_success 'correct error message for partial commit after empty pick' test_must_fail git rebase -i A D ) && echo x >file1 && - test_must_fail git commit file1 2>err && - test_grep "cannot do a partial commit during a rebase." err + git commit file1 ' test_expect_success 'correct error message for commit --amend after empty pick' ' From a3837282fe8415620f1da7e333643d734b03fe07 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Tue, 1 Sep 2026 22:24:39 +0000 Subject: [PATCH 03/20] commit: reword the empty-commit rebase amend error When a rebase applies a commit that becomes empty, it stops and asks the user to decide whether to keep it or drop it. HEAD still points at the previously-applied commit at that point, so amending is refused, with: You are in the middle of a rebase -- cannot amend. That message would suggest that amending is not allowed during an 'edit' or 'break' stop, which is misleading, plus it lacks the specificity that might help the user know why their particular case is a problem: the commit they intended to amend became empty and was dropped, so amending would affect the wrong commit. Reword the error accordingly. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/commit.c | 2 +- t/t3404-rebase-interactive.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/commit.c b/builtin/commit.c index 01b79185e7f91b..9b6eaa3c72e0f8 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1332,7 +1332,7 @@ static int parse_and_validate_options(int argc, const char *argv[], else if (is_from_cherry_pick(whence)) die(_("You are in the middle of a cherry-pick -- cannot amend.")); else if (is_from_rebase_now_empty(whence)) - die(_("You are in the middle of a rebase -- cannot amend.")); + die(_("The now-empty commit has been dropped -- cannot amend.")); } if (fixup_message && squash_message) die(_("options '%s' and '%s' cannot be used together"), "--squash", "--fixup"); diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 17b30b7825ae05..5a0aa93b1023a2 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1851,7 +1851,7 @@ test_expect_success 'correct error message for commit --amend after empty pick' ) && echo x>file1 && test_must_fail git commit -a --amend 2>err && - test_grep "middle of a rebase -- cannot amend." err + test_grep "now-empty commit has been dropped -- cannot amend." err ' test_expect_success 'todo has correct onto hash' ' From 6257588252ec0196c3d4567bec2921ed4e68d43e Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Tue, 1 Sep 2026 22:24:40 +0000 Subject: [PATCH 04/20] commit: refuse to amend during conflict resolution Running `git commit --amend` during conflict resolution is an ugly foot-gun. For many years, we have rejected amending during conflict resolution in the middle of - a merge - a cherry-pick However, this was never extended to other operations that can also produce conflicts: - an `am` operation - a revert - a rebase Extend it to handle these other cases now. Extending to `am`, revert, and the apply backend of rebase are fairly straightforward. However, with the merge backend of rebase we have to be more careful, since it powers interactive rebases and - the interactive machinery internally uses `git commit --amend` for `squash` and `reword` directives - users are expected to `git commit --amend` after hitting an `edit` or `break` directive So, we need to be careful with rebase to only reject amending when doing conflict resolution. A few files under the rebase-merge/ directory provide us the necessary information: - stopped-sha is written only when the rebase stops and hands control back to the user, so its presence marks a genuine stop -- as opposed to the sequencer's own internal `git commit --amend` while applying a squash, fixup, or reword, during which no stopped-sha exists. - amend is written only when the rebase stops with HEAD already pointing at the commit the user is meant to amend: a clean `edit`, or a fast-forward `reword`. Its absence at a stop therefore means the commit did not apply, so HEAD is the previously-applied commit rather than the one being rebased -- exactly the case we refuse. So for the merge backend we die when stopped-sha exists and amend does not. This covers a plain conflicted pick as well as a conflicted `edit` (both leave HEAD on the previously-applied commit), while still allowing a clean `edit` or `reword` stop and a `break` stop (no stopped-sha). stopped-sha is unlinked at the start of the resume loop, so a resumed squash's internal amend is unaffected. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/commit.c | 27 +++++++--- sequencer.c | 57 +++++++++++++++++++++ sequencer.h | 23 +++++++++ t/t3404-rebase-interactive.sh | 87 +++++++++++++++++++++++++++++++++ t/t3507-cherry-pick-conflict.sh | 11 +++++ t/t4151-am-abort.sh | 11 +++++ 6 files changed, 210 insertions(+), 6 deletions(-) diff --git a/builtin/commit.c b/builtin/commit.c index 9b6eaa3c72e0f8..284fc7fdc6e9e4 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1324,15 +1324,30 @@ static int parse_and_validate_options(int argc, const char *argv[], use_editor = 0; /* Sanity check options */ - if (amend && !current_head) - die(_("You have nothing to amend.")); - if (amend && whence != FROM_COMMIT) { - if (whence == FROM_MERGE) + if (amend) { + if (!current_head) + die(_("You have nothing to amend.")); + /* + * Refuse to amend in the middle of any operation that is + * meant to record its result as a new commit on top of HEAD + * rather than by rewriting HEAD. + */ + switch (sequencer_ongoing_operation(s->repo, whence)) { + case ONGOING_NONE: + break; + case ONGOING_MERGE: die(_("You are in the middle of a merge -- cannot amend.")); - else if (is_from_cherry_pick(whence)) + case ONGOING_CHERRY_PICK: die(_("You are in the middle of a cherry-pick -- cannot amend.")); - else if (is_from_rebase_now_empty(whence)) + case ONGOING_REBASE_NOW_EMPTY: die(_("The now-empty commit has been dropped -- cannot amend.")); + case ONGOING_REVERT: + die(_("You are in the middle of a revert -- cannot amend.")); + case ONGOING_AM: + die(_("You are in the middle of an am session -- cannot amend.")); + case ONGOING_REBASE_CONFLICT: + die(_("You are resolving conflicts during a rebase -- cannot amend.")); + } } if (fixup_message && squash_message) die(_("options '%s' and '%s' cannot be used together"), "--squash", "--fixup"); diff --git a/sequencer.c b/sequencer.c index 5ebcd7ecd543e2..83bb2f2f18f510 100644 --- a/sequencer.c +++ b/sequencer.c @@ -142,6 +142,13 @@ static GIT_PATH_FUNC(rebase_path_author_script, "rebase-merge/author-script") * command is processed, this file is deleted. */ static GIT_PATH_FUNC(rebase_path_amend, "rebase-merge/amend") +/* + * The apply ("am") backend keeps its state in the rebase-apply directory; + * the "applying" file within it marks a plain `git am` (as opposed to an + * apply-based rebase). + */ +static GIT_PATH_FUNC(apply_dir, "rebase-apply") +static GIT_PATH_FUNC(apply_path_applying, "rebase-apply/applying") /* * When we stop at a given patch via the "edit" command, this file contains * the commit object name of the corresponding patch. @@ -6865,6 +6872,56 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence) return 0; } +enum ongoing_operation sequencer_ongoing_operation(struct repository *r, + enum commit_whence whence) +{ + /* + * The merge, cherry-pick, and (empty) rebase-pick stops are already + * distinguished by 'whence'. + */ + switch (whence) { + case FROM_MERGE: + return ONGOING_MERGE; + case FROM_CHERRY_PICK_SINGLE: + case FROM_CHERRY_PICK_MULTI: + return ONGOING_CHERRY_PICK; + case FROM_REBASE_NOW_EMPTY: + return ONGOING_REBASE_NOW_EMPTY; + case FROM_COMMIT: + break; + } + + /* + * 'whence' is FROM_COMMIT, but we may still be in the middle of an + * operation that records its result on top of HEAD; detect those + * from their on-disk state. + */ + + /* In the middle of a revert? */ + if (refs_ref_exists(get_main_ref_store(r), "REVERT_HEAD")) + return ONGOING_REVERT; + + /* In the middle of an `am`? */ + if (file_exists(apply_path_applying())) + return ONGOING_AM; + + /* + * In the middle of a rebase that stopped for conflict resolution? + * The apply backend only ever stops for conflicts, so the presence + * of its state directory is enough. The merge backend writes + * stopped-sha whenever it hands control back to the user, but omits + * `amend` unless it stopped with HEAD already pointing at the commit + * to be amended (a clean edit/reword stop); its absence therefore + * marks a conflicted stop. + */ + if (file_exists(apply_dir()) || + (file_exists(rebase_path_stopped_sha()) && + !file_exists(rebase_path_amend()))) + return ONGOING_REBASE_CONFLICT; + + return ONGOING_NONE; +} + int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs) { diff --git a/sequencer.h b/sequencer.h index 3164bd437d6a22..854a16e486fcdc 100644 --- a/sequencer.h +++ b/sequencer.h @@ -269,6 +269,29 @@ int sequencer_get_last_command(struct repository* r, enum replay_action *action); int sequencer_determine_whence(struct repository *r, enum commit_whence *whence); +/* + * An in-progress operation that records its result (often a conflict + * resolution) as a new commit on top of HEAD, during which amending + * HEAD via "git commit --amend" is almost always a mistake. + */ +enum ongoing_operation { + ONGOING_NONE = 0, + ONGOING_MERGE, + ONGOING_CHERRY_PICK, + ONGOING_REBASE_NOW_EMPTY, + ONGOING_REVERT, + ONGOING_AM, + ONGOING_REBASE_CONFLICT +}; + +/* + * Return which in-progress operation, if any, is underway; see enum + * ongoing_operation. 'whence' is the origin already computed for the + * pending commit. + */ +enum ongoing_operation sequencer_ongoing_operation(struct repository *r, + enum commit_whence whence); + /** * Append the set of ref-OID pairs that are currently stored for the 'git * rebase --update-refs' feature if such a rebase is currently happening. diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 5a0aa93b1023a2..c63f5201919412 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1854,6 +1854,93 @@ test_expect_success 'correct error message for commit --amend after empty pick' test_grep "now-empty commit has been dropped -- cannot amend." err ' +test_expect_success 'commit --amend is refused at a rebase conflict stop' ' + test_when_finished "git rebase --abort" && + git checkout --detach conflict-branch && + ( + set_fake_editor && + FAKE_LINES="1 3" && + export FAKE_LINES && + test_must_fail git rebase -i A + ) && + test_path_is_file .git/rebase-merge/patch && + test_path_is_missing .git/rebase-merge/amend && + echo resolved >conflict && + git add conflict && + test_must_fail git commit --amend --no-edit 2>err && + test_grep "You are resolving conflicts during a rebase -- cannot amend" err +' + +test_expect_success 'commit --amend is refused when an "edit" pick conflicts' ' + test_when_finished "git rebase --abort" && + git checkout --detach conflict-branch && + ( + set_fake_editor && + FAKE_LINES="1 edit 3" && + export FAKE_LINES && + test_must_fail git rebase -i A + ) && + test_path_is_file .git/rebase-merge/patch && + test_path_is_missing .git/rebase-merge/amend && + echo resolved >conflict && + git add conflict && + test_must_fail git commit --amend --no-edit 2>err && + test_grep "You are resolving conflicts during a rebase -- cannot amend" err +' + +test_expect_success 'commit --amend is allowed at a rebase edit stop' ' + test_when_finished "git rebase --abort" && + git checkout --detach no-conflict-branch && + ( + set_fake_editor && + FAKE_LINES="edit 1 2 3 4" && + export FAKE_LINES && + git rebase -i A + ) && + test_path_is_file .git/rebase-merge/amend && + echo tweak >fileJ && + git add fileJ && + git commit --amend --no-edit +' + +test_expect_success 'commit --amend is allowed at a rebase break stop' ' + test_when_finished "git rebase --abort" && + git checkout --detach no-conflict-branch && + ( + set_fake_editor && + FAKE_LINES="break 1 2 3 4" && + export FAKE_LINES && + git rebase -i A + ) && + test_must_fail git rev-parse --verify REBASE_HEAD && + echo tweak >fileJ && + git add fileJ && + git commit --amend --no-edit +' + +test_expect_success 'commit --amend is refused at an apply-backend conflict stop' ' + test_when_finished "rm -rf apply-backend" && + test_create_repo apply-backend && + ( + cd apply-backend && + test_commit base file && + git branch -M mainline && + test_commit upstream file upstream && + git checkout -b side mainline~1 && + test_commit conflicting file side && + test_commit unrelated other && + test_must_fail git rebase --apply mainline && + # the apply backend only ever stops for conflicts, and + # leaves HEAD on the previously-applied commit + test_path_is_dir .git/rebase-apply && + test_path_is_missing .git/rebase-apply/applying && + echo resolved >file && + git add file && + test_must_fail git commit --amend --no-edit 2>err && + test_grep "You are resolving conflicts during a rebase -- cannot amend" err + ) +' + test_expect_success 'todo has correct onto hash' ' GIT_SEQUENCE_EDITOR=cat git rebase -i no-conflict-branch~4 no-conflict-branch >actual && onto=$(git rev-parse --short HEAD~4) && diff --git a/t/t3507-cherry-pick-conflict.sh b/t/t3507-cherry-pick-conflict.sh index 44596cb1e8036c..42de398f76edfa 100755 --- a/t/t3507-cherry-pick-conflict.sh +++ b/t/t3507-cherry-pick-conflict.sh @@ -364,6 +364,17 @@ test_expect_success 'failed revert sets REVERT_HEAD' ' test_cmp_rev picked REVERT_HEAD ' +test_expect_success 'commit --amend of revert fails' ' + pristine_detach initial && + + test_must_fail git revert picked && + echo resolved >foo && + git add foo && + test_must_fail git commit --amend 2>err && + + test_grep "in the middle of a revert -- cannot amend." err +' + test_expect_success 'successful revert does not set REVERT_HEAD' ' pristine_detach base && git revert base && diff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh index 8e1ecf8a68546c..9313a074b21e0e 100755 --- a/t/t4151-am-abort.sh +++ b/t/t4151-am-abort.sh @@ -63,6 +63,17 @@ do done +test_expect_success 'commit --amend during a failed am fails' ' + git reset --hard initial && + cp file-2-expect file-2 && + test_must_fail git am 000[1245]-*.patch && + echo resolved >file-1 && + git add file-1 && + test_must_fail git commit --amend 2>err && + test_grep "in the middle of an am session -- cannot amend." err && + git am --abort +' + test_expect_success 'am -3 --skip removes otherfile-4' ' git reset --hard initial && test_must_fail git am -3 0003-*.patch && From cc499d40e5b1523a66271639746a1c05fa7771c9 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Tue, 1 Sep 2026 22:24:41 +0000 Subject: [PATCH 05/20] commit: refuse partial commits during conflict resolution Similar to the previous commit, just as `git commit --amend` is a foot-gun during conflict resolution, so is a partial commit (`git commit `). Recording a conflict resolution is about capturing the state of the entire tree on top of HEAD, not a subset of paths. For many years we have rejected partial commits in the middle of - a merge - a cherry-pick but, just like amending, this was never extended to the other operations that can also leave conflicts to resolve: - an `am` operation - a revert - a rebase that stopped for conflict resolution Reuse sequencer_ongoing_operation(), introduced for the analogous `--amend` check, to detect these and refuse the partial commit. A rebase that stopped because a pick became empty is not conflict resolution and, as an earlier patch established, is deliberately left permitted. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/commit.c | 24 ++++++++++++++++++----- sequencer.h | 5 +++-- t/t3404-rebase-interactive.sh | 34 +++++++++++++++++++++++++++++++++ t/t3507-cherry-pick-conflict.sh | 11 +++++++++++ t/t4151-am-abort.sh | 11 +++++++++++ 5 files changed, 78 insertions(+), 7 deletions(-) diff --git a/builtin/commit.c b/builtin/commit.c index 284fc7fdc6e9e4..4e0fd58f0a4631 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -515,11 +515,25 @@ static const char *prepare_index(const char **argv, const char *prefix, */ commit_style = COMMIT_PARTIAL; - if (whence != FROM_COMMIT) { - if (whence == FROM_MERGE) - die(_("cannot do a partial commit during a merge.")); - else if (is_from_cherry_pick(whence)) - die(_("cannot do a partial commit during a cherry-pick.")); + switch (sequencer_ongoing_operation(the_repository, whence)) { + case ONGOING_NONE: + break; + case ONGOING_MERGE: + die(_("cannot do a partial commit during a merge.")); + case ONGOING_CHERRY_PICK: + die(_("cannot do a partial commit during a cherry-pick.")); + case ONGOING_REBASE_NOW_EMPTY: + /* + * A pick that became empty is not a conflict, and creating + * a new commit (partial or not) poses no problem. + */ + break; + case ONGOING_REVERT: + die(_("cannot do a partial commit during a revert.")); + case ONGOING_AM: + die(_("cannot do a partial commit during an am session.")); + case ONGOING_REBASE_CONFLICT: + die(_("cannot do a partial commit while resolving conflicts during a rebase.")); } if (list_paths(&partial, !current_head ? NULL : "HEAD", &pathspec)) diff --git a/sequencer.h b/sequencer.h index 854a16e486fcdc..a34a049dc26095 100644 --- a/sequencer.h +++ b/sequencer.h @@ -271,8 +271,9 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence) /* * An in-progress operation that records its result (often a conflict - * resolution) as a new commit on top of HEAD, during which amending - * HEAD via "git commit --amend" is almost always a mistake. + * resolution) as a new commit on top of HEAD. Some ways of invoking + * "git commit" -- amending HEAD, or a partial commit -- are almost + * always a mistake during such an operation. */ enum ongoing_operation { ONGOING_NONE = 0, diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index c63f5201919412..61b5a99650aa94 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1941,6 +1941,40 @@ test_expect_success 'commit --amend is refused at an apply-backend conflict stop ) ' +test_expect_success 'partial commit is refused at a rebase conflict stop' ' + test_when_finished "git rebase --abort" && + git checkout --detach conflict-branch && + ( + set_fake_editor && + FAKE_LINES="1 3" && + export FAKE_LINES && + test_must_fail git rebase -i A + ) && + echo resolved >conflict && + git add conflict && + test_must_fail git commit conflict 2>err && + test_grep "cannot do a partial commit while resolving conflicts during a rebase." err +' + +test_expect_success 'partial commit is refused at an apply-backend conflict stop' ' + test_when_finished "rm -rf apply-backend" && + test_create_repo apply-backend && + ( + cd apply-backend && + test_commit base file && + git branch -M mainline && + test_commit upstream file upstream && + git checkout -b side mainline~1 && + test_commit conflicting file side && + test_commit unrelated other && + test_must_fail git rebase --apply mainline && + echo resolved >file && + git add file && + test_must_fail git commit file 2>err && + test_grep "cannot do a partial commit while resolving conflicts during a rebase." err + ) +' + test_expect_success 'todo has correct onto hash' ' GIT_SEQUENCE_EDITOR=cat git rebase -i no-conflict-branch~4 no-conflict-branch >actual && onto=$(git rev-parse --short HEAD~4) && diff --git a/t/t3507-cherry-pick-conflict.sh b/t/t3507-cherry-pick-conflict.sh index 42de398f76edfa..c3d024c97f19f4 100755 --- a/t/t3507-cherry-pick-conflict.sh +++ b/t/t3507-cherry-pick-conflict.sh @@ -375,6 +375,17 @@ test_expect_success 'commit --amend of revert fails' ' test_grep "in the middle of a revert -- cannot amend." err ' +test_expect_success 'partial commit during a revert fails' ' + pristine_detach initial && + + test_must_fail git revert picked && + echo resolved >foo && + git add foo && + test_must_fail git commit foo 2>err && + + test_grep "cannot do a partial commit during a revert." err +' + test_expect_success 'successful revert does not set REVERT_HEAD' ' pristine_detach base && git revert base && diff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh index 9313a074b21e0e..c80269e015d81b 100755 --- a/t/t4151-am-abort.sh +++ b/t/t4151-am-abort.sh @@ -74,6 +74,17 @@ test_expect_success 'commit --amend during a failed am fails' ' git am --abort ' +test_expect_success 'partial commit during a failed am fails' ' + git reset --hard initial && + cp file-2-expect file-2 && + test_must_fail git am 000[1245]-*.patch && + echo resolved >file-1 && + git add file-1 && + test_must_fail git commit file-1 2>err && + test_grep "cannot do a partial commit during an am session." err && + git am --abort +' + test_expect_success 'am -3 --skip removes otherfile-4' ' git reset --hard initial && test_must_fail git am -3 0003-*.patch && From 95d48952cc66b299554ebd90aada5c3fbc2a6e7d Mon Sep 17 00:00:00 2001 From: Jeff King Date: Wed, 2 Sep 2026 01:55:26 -0400 Subject: [PATCH 06/20] repository: make repo_clear() idempotent Calling repo_clear() twice in a row will segfault because the second call will invoke parse_object_pool_clear() on a NULL pointer. This is not usually a big deal, but we can make some error cleanup a little simpler if callers do not need to worry about invoking it twice. We can fix it by catching the NULL case. The rest of repo_clear() appears to be idempotent. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- repository.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/repository.c b/repository.c index 187dd471c4e607..bb8252e0264107 100644 --- a/repository.c +++ b/repository.c @@ -384,7 +384,8 @@ void repo_clear(struct repository *repo) odb_free(repo->objects); repo->objects = NULL; - parsed_object_pool_clear(repo->parsed_objects); + if (repo->parsed_objects) + parsed_object_pool_clear(repo->parsed_objects); FREE_AND_NULL(repo->parsed_objects); repo_settings_clear(repo); From 2c03739705a593930d2614a3fbd42500701f37a2 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Wed, 2 Sep 2026 01:57:30 -0400 Subject: [PATCH 07/20] submodule--helper: free URL when repository setup fails If repo setup fails, we'll return an error without freeing the allocated url string, leaking the memory. The test suite does trigger this error, but never with the leak. We only allocate a url if submodule_from_path() returned something, but our tests use other situations, like totally nonexistent submodules. We can cover this case by asking about a submodule that exists but which has not been initialized. The new test fails with SANITIZE=leak. The smallest fix would just be a call to free(url), but I think it's a little nicer to set up a dedicated out-path for cleanup here. The previous commit made it safe to call repo_clear() even if repo_submodule_init() fails. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- builtin/submodule--helper.c | 10 +++++++--- t/t7426-submodule-get-default-remote.sh | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index 1cc82a134db22e..bc6c8a71eab3c0 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -80,6 +80,7 @@ static int get_default_remote_submodule(const char *module_path, char **default_ struct repository subrepo; const char *remote_name = NULL; char *url = NULL; + int ret = 0; sub = submodule_from_path(the_repository, null_oid(the_hash_algo), module_path); if (sub && sub->url) { @@ -96,9 +97,11 @@ static int get_default_remote_submodule(const char *module_path, char **default_ } if (repo_submodule_init(&subrepo, the_repository, module_path, - null_oid(the_hash_algo)) < 0) - return die_message(_("could not get a repository handle for submodule '%s'"), + null_oid(the_hash_algo)) < 0) { + ret = die_message(_("could not get a repository handle for submodule '%s'"), module_path); + goto out; + } /* Look up by URL first */ if (url) @@ -108,10 +111,11 @@ static int get_default_remote_submodule(const char *module_path, char **default_ *default_remote = xstrdup(remote_name); +out: repo_clear(&subrepo); free(url); - return 0; + return ret; } static int module_get_default_remote(int argc, const char **argv, const char *prefix, diff --git a/t/t7426-submodule-get-default-remote.sh b/t/t7426-submodule-get-default-remote.sh index b842af9a2d26ff..0379c9f04489f3 100755 --- a/t/t7426-submodule-get-default-remote.sh +++ b/t/t7426-submodule-get-default-remote.sh @@ -60,6 +60,23 @@ test_expect_success 'get-default-remote fails with non-submodule path' ' ) ' +test_expect_success 'get-default-remote fails with uninitialized submodule' ' + test_when_finished " + git -C super config -f .gitmodules --remove-section submodule.uninitialized && + git -C super update-index --force-remove uninitialized + " && + ( + cd super && + git config -f .gitmodules submodule.uninitialized.path uninitialized && + git config -f .gitmodules submodule.uninitialized.url ../sub && + head=$(git -C ../sub rev-parse HEAD) && + git update-index --add --cacheinfo 160000,$head,uninitialized && + test_must_fail git submodule--helper get-default-remote \ + uninitialized 2>err && + test_grep "could not get a repository handle" err + ) +' + test_expect_success 'get-default-remote fails without path argument' ' ( cd super && From 76621488e867bab3a8319098f09a395aea2a8b4a Mon Sep 17 00:00:00 2001 From: Jinbao Chen Date: Thu, 3 Sep 2026 14:36:57 +0800 Subject: [PATCH 08/20] history: do not dereference NULL when parent tree is missing write_ondisk_index() dereferences the return value of repo_parse_tree_indirect() unconditionally. If the parent commit's tree object is missing from the object store (corrupt repository, object removed by tooling, or incomplete restore), the function returns NULL and "git history split" crashes with a SIGSEGV. Guard the parse result and error out gracefully, following the codebase convention for objects that cannot be loaded. Signed-off-by: Jinbao Chen Acked-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/history.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/builtin/history.c b/builtin/history.c index 091465a59e2f96..3bc72fc8c45482 100644 --- a/builtin/history.c +++ b/builtin/history.c @@ -755,6 +755,10 @@ static int write_ondisk_index(struct repository *repo, opts.dst_index = &index; tree = repo_parse_tree_indirect(repo, oid); + if (!tree) { + ret = error(_("unable to parse tree %s"), oid_to_hex(oid)); + goto out; + } init_tree_desc(&tree_desc, &tree->object.oid, tree->buffer, tree->size); if (unpack_trees(1, &tree_desc, &opts)) { From 88d06b1c911e8491029ef24e96e3f412ce24c2ae Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 3 Sep 2026 11:13:29 -0700 Subject: [PATCH 09/20] pathspec: match and original in pathspec_item are const No existing code modifies these two strings in pathspec elements after they are created via these two pointers. Declare them as "const char *" to stress on this fact and cast away constness from the code that frees these two strings. Signed-off-by: Junio C Hamano --- pathspec.c | 4 ++-- pathspec.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pathspec.c b/pathspec.c index 5993c4afa0eb37..8105df8a927179 100644 --- a/pathspec.c +++ b/pathspec.c @@ -749,8 +749,8 @@ void clear_pathspec(struct pathspec *pathspec) int i, j; for (i = 0; i < pathspec->nr; i++) { - free(pathspec->items[i].match); - free(pathspec->items[i].original); + free((void *)pathspec->items[i].match); + free((void *)pathspec->items[i].original); for (j = 0; j < pathspec->items[i].attr_match_nr; j++) free(pathspec->items[i].attr_match[j].value); diff --git a/pathspec.h b/pathspec.h index 5e3a6f1fe7b7c5..fc1b9465ad7c2b 100644 --- a/pathspec.h +++ b/pathspec.h @@ -35,8 +35,8 @@ struct pathspec { unsigned magic; int max_depth; struct pathspec_item { - char *match; - char *original; + const char *match; + const char *original; unsigned magic; int len, prefix; int nowildcard_len; From c655855559a056365b38df831c8481cc3e471c3d Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Sat, 5 Sep 2026 19:28:12 +0200 Subject: [PATCH 10/20] doc: git: list gitdatamodel(7) as a concept guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dee80940 (doc: add an explanation of Git's data model, 2025-11-12) added gitdatamodel(7), documenting Git’s data model. But it is not mentioned anywhere. Let’s start by listing it under Guides in git(1) and with `git help --guides`. Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- command-list.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/command-list.txt b/command-list.txt index 21b802c42026b3..63ae2a67c94055 100644 --- a/command-list.txt +++ b/command-list.txt @@ -217,6 +217,7 @@ gitcli userinterfaces gitcore-tutorial guide gitcredentials guide gitcvs-migration guide +gitdatamodel guide gitdiffcore guide giteveryday guide gitfaq guide From 5745353ddf22db3543e021c931faec2e3b283174 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Sat, 5 Sep 2026 19:28:13 +0200 Subject: [PATCH 11/20] doc: git: link to the gitdatamodel(7) tutorial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit added the first mention of gitdatamodel(7) here, under Guides. But there are also other sections where a mention is relevant. Let’s mention it: • under Description, since it is as useful as the other tutorials already mentioned there for those who are interested; • under Terminology, since it complements gitglossary(7) as a pedagogical rather than reference source for the core terms;[1] and • under See Also, since the other tutorials (plus the user manual) are mentioned there. We don’t need to mention it under Further Documentation since we now mention it under Description. † 1: See dee80940 (doc: add an explanation of Git's data model, 2025-11-12): `gitglossary`. This makes a good effort, but it's an alphabetically ordered dictionary and a dictionary is not a good way to learn concepts. You have to jump around too much and it's not possible to present the concepts in the order that they should be explained. Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/git.adoc | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Documentation/git.adoc b/Documentation/git.adoc index 8a5cdd3b3d22c5..6f0075f9188774 100644 --- a/Documentation/git.adoc +++ b/Documentation/git.adoc @@ -25,7 +25,8 @@ and full access to internals. See linkgit:gittutorial[7] to get started, then see linkgit:giteveryday[7] for a useful minimum set of commands. The link:user-manual.html[Git User's Manual] has a more -in-depth introduction. +in-depth introduction. See linkgit:gitdatamodel[7] if you want to +learn about the data model and important terminology. After you mastered the basic concepts, you can come back to this page to learn what commands Git offers. You can learn more about @@ -469,7 +470,9 @@ Higher level SCMs may provide and manage additional information in the Terminology ----------- -Please see linkgit:gitglossary[7]. +Please see linkgit:gitglossary[7]. See linkgit:gitdatamodel[7] for a +discussion of the core data model, which includes important terminology +used throughout the documentation. Environment Variables @@ -1199,8 +1202,9 @@ SEE ALSO -------- linkgit:gittutorial[7], linkgit:gittutorial-2[7], linkgit:giteveryday[7], linkgit:gitcvs-migration[7], -linkgit:gitglossary[7], linkgit:gitcore-tutorial[7], -linkgit:gitcli[7], link:user-manual.html[The Git User's Manual], +linkgit:gitglossary[7], linkgit:gitdatamodel[7], +linkgit:gitcore-tutorial[7], linkgit:gitcli[7], +link:user-manual.html[The Git User's Manual], linkgit:gitworkflows[7] GIT From 5720c0918163c598ace846e045b9b036e89172c6 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Sat, 5 Sep 2026 19:28:14 +0200 Subject: [PATCH 12/20] doc: glossary: link four of the terms to gitdatamodel(7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four of the terms in the glossary are discussed in gitdatamodel(7). Let’s link to the data model page from the glossary. The phrasing needs to be tweaked based on what gitdatamodel(7) offers for each term compared to the glossary, or even other pages (see the git-reflog(1) mention). For instance, the ref/reference discussion can be called a “see also” since the glossary here already goes into detail. On the other hand, gitdatamodel(7) offers more details on the subject of “the index”. Let’s also add gitdatamodel(7) to See Also. It is at least as relevant as the other tutorial pages that are already mentioned. Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/gitglossary.adoc | 1 + Documentation/glossary-content.adoc | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Documentation/gitglossary.adoc b/Documentation/gitglossary.adoc index 0e85be48470ea0..b046d9cb293977 100644 --- a/Documentation/gitglossary.adoc +++ b/Documentation/gitglossary.adoc @@ -16,6 +16,7 @@ include::glossary-content.adoc[] SEE ALSO -------- +linkgit:gitdatamodel[7], linkgit:gittutorial[7], linkgit:gittutorial-2[7], linkgit:gitcvs-migration[7], diff --git a/Documentation/glossary-content.adoc b/Documentation/glossary-content.adoc index 8c4e9dd3beede1..52301a56ddaa60 100644 --- a/Documentation/glossary-content.adoc +++ b/Documentation/glossary-content.adoc @@ -251,7 +251,8 @@ for a more flexible and robust system to do the same thing. as objects. The index is a stored version of your <>. Truth be told, it can also contain a second, and even a third version of a working tree, which are used - when <>. + when <>. See "THE INDEX" in + linkgit:gitdatamodel[7] for details. [[def_index_entry]]index entry:: The information regarding a particular file, stored in the @@ -290,7 +291,8 @@ This commit is referred to as a "merge commit", or sometimes just a [[def_object]]object:: The unit of storage in Git. It is uniquely identified by the <> of its contents. Consequently, an - object cannot be changed. + object cannot be changed. See "OBJECTS" in + linkgit:gitdatamodel[7] for details. [[def_object_database]]object database:: Stores a set of "objects", and an individual <> is @@ -587,12 +589,17 @@ extended in the future: Different subhierarchies are used for different purposes. For example, the `refs/heads/` hierarchy is used to represent local branches whereas the `refs/tags/` hierarchy is used to represent local tags.. ++ +See also "REFERENCES" in linkgit:gitdatamodel[7]. [[def_reflog]]reflog:: A reflog shows the local "history" of a ref. In other words, it can tell you what the 3rd last revision in _this_ repository was, and what was the current state in _this_ repository, - yesterday 9:14pm. See linkgit:git-reflog[1] for details. + yesterday 9:14pm. ++ +See "REFLOGS" in linkgit:gitdatamodel[7] for a short explanation of the +format. See linkgit:git-reflog[1] for details. [[def_refspec]]refspec:: A "refspec" is used by <> and From ec602484bd9f265a6eb2dd211d066e1116d61cfc Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Sat, 5 Sep 2026 19:28:15 +0200 Subject: [PATCH 13/20] doc: datamodel: link to the glossary We linked from the glossary to the data model page in the last commit. It can also be useful to link the other way for readers who might want to reference more terminology. Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/gitdatamodel.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Documentation/gitdatamodel.adoc b/Documentation/gitdatamodel.adoc index dcfdff0346f669..56b7635c1997af 100644 --- a/Documentation/gitdatamodel.adoc +++ b/Documentation/gitdatamodel.adoc @@ -300,6 +300,10 @@ $ git reflog main --date=iso --no-decorate 4ccb6d7 main@{2025-09-29 15:16:48 -0400}: commit (initial): Initial commit ---- +SEE ALSO +-------- +linkgit:gitglossary[7] + GIT --- Part of the linkgit:git[1] suite From 3e4574885f7c0c9e4a3a47dcd3373fa91ab17547 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Sat, 5 Sep 2026 20:13:31 +0300 Subject: [PATCH 14/20] t3507: check no CHERRY_PICK_HEAD after conflicting --no-commit Whether CHERRY_PICK_HEAD is written depends on the command, on whether the merge started, and on --no-commit, all in one condition in do_pick_commit(). The suite checks the clean --no-commit pick; nothing checks the conflicting one. The test that already runs a conflicting --no-commit pick compares the advice the command prints, which is what tells us it stopped on a conflict. Assert the ref is missing there too. Signed-off-by: Aleksei Sviridkin Signed-off-by: Junio C Hamano --- t/t3507-cherry-pick-conflict.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/t/t3507-cherry-pick-conflict.sh b/t/t3507-cherry-pick-conflict.sh index 44596cb1e8036c..aa004d929b1840 100755 --- a/t/t3507-cherry-pick-conflict.sh +++ b/t/t3507-cherry-pick-conflict.sh @@ -79,7 +79,8 @@ test_expect_success 'advice from failed cherry-pick --no-commit' " EOF test_must_fail git cherry-pick --no-commit picked 2>actual && - test_cmp expected actual + test_cmp expected actual && + test_ref_missing CHERRY_PICK_HEAD " test_expect_success 'failed cherry-pick sets CHERRY_PICK_HEAD' ' From 81c1e0b397da27b8763bae83939c1e8c56258b49 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Sat, 5 Sep 2026 20:13:32 +0300 Subject: [PATCH 15/20] doc: cherry-pick: note --no-commit skips CHERRY_PICK_HEAD The list of what happens when a change is hard to apply states without qualification that CHERRY_PICK_HEAD is set. Under --no-commit it is not: d7e5c0cbfb (Introduce CHERRY_PICK_HEAD, 2011-02-19) skips the ref on purpose there, presuming the user intends to further edit the result and possibly pick more commits on top. The option's own description says nothing about the ref or about authorship. "git commit" reads the author of a cherry-pick from CHERRY_PICK_HEAD, so without it a plain commit records you as the author. Say so where the option is described, and say that this is the point of the option rather than a wrinkle: what is being built is the user's own work, not a reproduction of the original commit. Signed-off-by: Aleksei Sviridkin Signed-off-by: Junio C Hamano --- Documentation/git-cherry-pick.adoc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Documentation/git-cherry-pick.adoc b/Documentation/git-cherry-pick.adoc index 42b41923d5f0bc..f4cd8b9db7a954 100644 --- a/Documentation/git-cherry-pick.adoc +++ b/Documentation/git-cherry-pick.adoc @@ -25,7 +25,8 @@ happens: 1. The current branch and `HEAD` pointer stay at the last commit successfully made. 2. The `CHERRY_PICK_HEAD` ref is set to point at the commit that - introduced the change that is difficult to apply. + introduced the change that is difficult to apply, unless the + `--no-commit` option was given. 3. Paths in which the change applied cleanly are updated both in the index file and in your working tree. 4. For conflicting paths, the index file records up to three @@ -101,6 +102,11 @@ OPTIONS + This is useful when cherry-picking more than one commits' effect to your index in a row. ++ +This option does not record `CHERRY_PICK_HEAD`, so a plain `git commit` +afterwards records you as the author. That is by design: what you are +building is your own work, which you keep changing before committing, +rather than a reproduction of the original commit. -s:: --signoff:: From 7129d77f1c0c465aca2c1ca5eef4e42b6adf7427 Mon Sep 17 00:00:00 2001 From: Ted Nyman Date: Sun, 26 Jul 2026 01:33:11 -0700 Subject: [PATCH 16/20] fetch-pack: trace packfile URI downloads When a protocol v2 fetch includes packfile URIs, the client downloads each advertised pack in a separate http-fetch process. Existing Trace2 regions cover negotiation, but not the time spent downloading these packs or the number of advertised URIs. Add a Trace2 region around the packfile URI download loop and record the number of URIs. This makes the cost of downloading external packs visible without emitting an event for each pack. Extend the existing packfile URI test to verify the region and count. Signed-off-by: Ted Nyman Signed-off-by: Junio C Hamano --- fetch-pack.c | 12 ++++++++++++ t/t5702-protocol-v2.sh | 7 ++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/fetch-pack.c b/fetch-pack.c index 29c41132ee0495..701a23f808cc6f 100644 --- a/fetch-pack.c +++ b/fetch-pack.c @@ -1886,6 +1886,13 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args, } } + if (packfile_uris.nr) { + trace2_region_enter("fetch-pack", "packfile-uris", + the_repository); + trace2_data_intmax("fetch-pack", the_repository, + "packfile-uris/count", packfile_uris.nr); + } + for (i = 0; i < packfile_uris.nr; i++) { int j; struct child_process cmd = CHILD_PROCESS_INIT; @@ -1936,6 +1943,11 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args, repo_get_object_directory(the_repository), packname)); } + + if (packfile_uris.nr) + trace2_region_leave("fetch-pack", "packfile-uris", + the_repository); + string_list_clear(&packfile_uris, 0); strvec_clear(&index_pack_args); diff --git a/t/t5702-protocol-v2.sh b/t/t5702-protocol-v2.sh index 74a2b7730bf3da..537deff7b3b4c0 100755 --- a/t/t5702-protocol-v2.sh +++ b/t/t5702-protocol-v2.sh @@ -1223,7 +1223,7 @@ configure_exclusion () { test_expect_success 'part of packfile response provided as URI' ' P="$HTTPD_DOCUMENT_ROOT_PATH/http_parent" && - rm -rf "$P" http_child log && + rm -rf "$P" http_child log trace2 && git init "$P" && git -C "$P" config "uploadpack.allowsidebandall" "true" && @@ -1238,10 +1238,15 @@ test_expect_success 'part of packfile response provided as URI' ' configure_exclusion "$P" other-blob >h2 && GIT_TRACE=1 GIT_TRACE_PACKET="$(pwd)/log" GIT_TEST_SIDEBAND_ALL=1 \ + GIT_TRACE2_EVENT="$(pwd)/trace2" \ git -c protocol.version=2 \ -c fetch.uriprotocols=http,https \ clone "$HTTPD_URL/smart/http_parent" http_child && + test_grep \"event\":\"region_enter\".*\"label\":\"packfile-uris\" trace2 && + test_grep \"key\":\"packfile-uris/count\",\"value\":\"2\" trace2 && + test_grep \"event\":\"region_leave\".*\"label\":\"packfile-uris\" trace2 && + # Ensure that my-blob and other-blob are in separate packfiles. for idx in http_child/.git/objects/pack/*.idx do From 8a631963a95b0357fdb2a4c61e5df898028c4641 Mon Sep 17 00:00:00 2001 From: Tuomas Ahola Date: Mon, 7 Sep 2026 21:48:58 +0300 Subject: [PATCH 17/20] lint-gitlink: don't use empty lower bound in .{0,8} The regex quantifier {,n} was added in Perl 5.34.0 as a shorthand for {0,n}. That makes it too new an introduction for Git which targets Perl 5.26.0. Even though Documentation/lint-gitlink.perl is a development helper script, let's stick to the general Perl version requirement for consistency, and use an explicit zero in .{0,8}. Signed-off-by: Tuomas Ahola Signed-off-by: Junio C Hamano --- Documentation/lint-gitlink.perl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/lint-gitlink.perl b/Documentation/lint-gitlink.perl index a92e887b4c75f2..6a87633dd4aa7c 100755 --- a/Documentation/lint-gitlink.perl +++ b/Documentation/lint-gitlink.perl @@ -42,7 +42,7 @@ sub report { while (<>) { my $line = $_; next if $line =~ /^\s*(ifn?def|endif)::/; - while ($line =~ m/(.{,8})((git[-a-z]+|scalar)\[(\d)*\])/g) { + while ($line =~ m/(.{0,8})((git[-a-z]+|scalar)\[(\d)*\])/g) { my $pos = pos $line; my ($macro, $target, $page, $section) = ($1, $2, $3, $4); if ( $macro ne "linkgit:" ) { From 609ea2363a9a65aa1bf775de40de29329abd85fc Mon Sep 17 00:00:00 2001 From: Brigham Campbell Date: Thu, 10 Sep 2026 00:52:31 -0600 Subject: [PATCH 18/20] doc: fix conjoined maintenance strategies in git-config(1) Fix conjoined bullet items for the maintenance strategies in git-config(1). Reportedly, asciidoctor renders this document correctly both before and after this patch. asciidoc renders it correctly only _after_ this patch. Signed-off-by: Brigham Campbell Acked-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/config/maintenance.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc index da8be9f812c68d..c778ab09ace9bc 100644 --- a/Documentation/config/maintenance.adoc +++ b/Documentation/config/maintenance.adoc @@ -41,6 +41,7 @@ The possible strategies are: This repacking strategy is a full replacement for the `gc` strategy and is recommended for large repositories. This is the default strategy for manual maintenance. ++ * `incremental`: This setting optimizes for performing small maintenance activities that do not delete any data. This does not schedule the `gc` task, but runs the `prefetch` and `commit-graph` tasks hourly, the From 7792e407939f0a27850a60a5b438916564b1a5d2 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Wed, 2 Sep 2026 03:16:13 -0400 Subject: [PATCH 19/20] ci: use system asciidoctor Our CI Documentation builds have pinned asciidoctor since 615a6c37e1 (ci: stick with Asciidoctor v1.5.8 for now, 2019-03-29). Back then a few changes were needed to build with the then-new v2.0.0. We've since made those changes, in f6461b82b9 (Documentation: fix build with Asciidoctor 2, 2019-09-15) and other commits. So it is not only safe to use newer versions of asciidoctor, but preferable: it's what people are likely to use for actually building the documentation in practice! I don't think there's any need to pin to a specific version. We can just use what ships in our Ubuntu image, which should give us an arbitrary representative version (and if it changes and something breaks, we'd want to know). Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- ci/install-dependencies.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/install-dependencies.sh b/ci/install-dependencies.sh index 2f61fbb07c577d..d57dce56630173 100755 --- a/ci/install-dependencies.sh +++ b/ci/install-dependencies.sh @@ -164,9 +164,9 @@ sparse) libexpat-dev gettext zlib1g-dev sparse ;; Documentation) - sudo apt-get -q -y install asciidoc xmlto docbook-xsl-ns make ruby + sudo apt-get -q -y install asciidoc xmlto docbook-xsl-ns make ruby \ + asciidoctor - sudo gem install --version 1.5.8 asciidoctor sudo gem install concurrent-ruby ;; esac From 3699d22b59a6ea467ce13edb81b6bdea0398c803 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 13 Sep 2026 21:53:15 -0700 Subject: [PATCH 20/20] 2nd batch for -rc1 Signed-off-by: Junio C Hamano --- Documentation/RelNotes/2.56.0.adoc | 40 +++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/Documentation/RelNotes/2.56.0.adoc b/Documentation/RelNotes/2.56.0.adoc index 28f555da03abf7..9c816018fe83d0 100644 --- a/Documentation/RelNotes/2.56.0.adoc +++ b/Documentation/RelNotes/2.56.0.adoc @@ -148,6 +148,11 @@ UI, Workflows & Features drop merge commits and linearize the replayed history, mimicking 'git rebase --no-rebase-merges'. + * The documentation for 'git cherry-pick' has been updated to clarify + that the '--no-commit' option intentionally skips setting the + 'CHERRY_PICK_HEAD' ref. A test has also been added to ensure this + behavior holds even when the operation stops for conflicts. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -518,6 +523,11 @@ Performance, Internal Implementation, Development Support etc. the main 'git.exe' to be used directly without the extra wrapper process on Windows. + * The process of downloading packfile URIs in protocol v2 has been + instrumented with a Trace2 region. This visibility allows tracking + the cumulative time spent downloading external packs and the number + of advertised URIs without emitting a separate event per pack. + Fixes since v2.55 ----------------- @@ -819,7 +829,35 @@ Fixes since v2.55 package manager instead of pinning to an older version via gem. Additionally, an obsolete variable used for retired Azure Pipelines environments has been removed. - (merge 1c1eed13bd jk/ci-use-system-asciidoctor later to maint). + + * Teach 'am', 'revert', and 'rebase' that running 'commit --amend' or a + partial 'commit ' makes no sense during operations that stop + and return control to the user to resolve conflicts left in the + working tree, just like 'cherry-pick' and 'merge' do. + (merge cc499d40e5 en/no-amend-during-conflicts later to maint). + + * The error path in 'git submodule--helper' has been updated to plug a + memory leak when a repository handle could not be obtained, + leveraging an updated idempotent repo_clear(). + (merge 2c03739705 jk/submodule-error-leak later to maint). + + * Two members in "struct pathspec_item" were of type "char *", but + nobody updated the string through these pointers. They have been + made "const char *" instead. + (merge 88d06b1c91 jc/pathspec-match-const later to maint). + + * The gitdatamodel documentation page has been linked from a handful + of key documentaiton pages. + (merge ec602484bd kh/doc-datamodel later to maint). + + * Running "git history" in a corrupt repository can (unsurprisingly) + segfault when a necessary tree object is not found. + (merge 76621488e8 jc/history-missing-tree-errorfix later to maint). + + * The development helper script to lint gitlink references in the + documentation has been updated to avoid a newer Perl regular + expression syntax that breaks on older Perl versions. + (merge 8a631963a9 ta/lint-gitlink-older-perl-fix later to maint). * Other code cleanup, docfix, build fix, etc. (merge 026636128f ss/submittingpatches-typofix later to maint).