From 1dbf1f8e101cec674c15ab218ede2dbb60e91e3b Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Tue, 8 Sep 2026 20:41:52 +0000 Subject: [PATCH 001/155] history: extract helper for a commit's parent tree Three places resolve the tree of a commit's first parent, falling back to the empty tree for a root commit, each repeating the same parse and oidcpy dance. Extract a first_parent_tree_oid() helper and route the existing callers through it. No change in behavior. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- builtin/history.c | 58 +++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/builtin/history.c b/builtin/history.c index 000155ad9c24d5..c22715d848f203 100644 --- a/builtin/history.c +++ b/builtin/history.c @@ -164,6 +164,25 @@ static int commit_tree_ext(struct repository *repo, return ret; } +static int first_parent_tree_oid(struct repository *repo, + struct commit *commit, + struct object_id *out) +{ + struct commit *parent = commit->parents ? commit->parents->item : NULL; + + if (!parent) { + oidcpy(out, repo->hash_algo->empty_tree); + return 0; + } + + if (repo_parse_commit(repo, parent)) + return error(_("unable to parse parent commit %s"), + oid_to_hex(&parent->object.oid)); + + oidcpy(out, &repo_get_commit_tree(repo, parent)->object.oid); + return 0; +} + static int commit_tree_with_edited_message(struct repository *repo, const char *action, struct commit *original, @@ -171,21 +190,11 @@ static int commit_tree_with_edited_message(struct repository *repo, { struct object_id parent_tree_oid; const struct object_id *tree_oid; - struct commit *parent; tree_oid = &repo_get_commit_tree(repo, original)->object.oid; - parent = original->parents ? original->parents->item : NULL; - if (parent) { - if (repo_parse_commit(repo, parent)) { - return error(_("unable to parse parent commit %s"), - oid_to_hex(&parent->object.oid)); - } - - parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid; - } else { - oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree); - } + if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0) + return -1; return commit_tree_ext(repo, action, original, original->parents, &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE); @@ -475,18 +484,10 @@ static int commit_became_empty(struct repository *repo, struct commit *original, struct tree *result) { - struct commit *parent = original->parents ? original->parents->item : NULL; struct object_id parent_tree_oid; - if (parent) { - if (repo_parse_commit(repo, parent)) - return error(_("unable to parse parent of %s"), - oid_to_hex(&original->object.oid)); - - parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid; - } else { - oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree); - } + if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0) + return -1; return oideq(&result->object.oid, &parent_tree_oid); } @@ -830,16 +831,9 @@ static int split_commit(struct repository *repo, struct tree *split_tree; int ret; - if (original->parents) { - if (repo_parse_commit(repo, original->parents->item)) { - ret = error(_("unable to parse parent commit %s"), - oid_to_hex(&original->parents->item->object.oid)); - goto out; - } - - parent_tree_oid = *get_commit_tree_oid(original->parents->item); - } else { - oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree); + if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0) { + ret = -1; + goto out; } original_commit_tree_oid = get_commit_tree_oid(original); From 28626dfa66a44c0f5e03e90bb4c3b9c424197ac1 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Tue, 8 Sep 2026 20:41:53 +0000 Subject: [PATCH 002/155] history: give commit_tree_ext a message template commit_tree_ext() reuses the message of the commit it is handed. A caller that folds several commits together wants to seed the message from more than that single commit, so add an optional message_template parameter. When NULL, the behavior is unchanged. Pass NULL from the existing fixup and split callers. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- builtin/history.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/builtin/history.c b/builtin/history.c index c22715d848f203..a60e8dbcd02a67 100644 --- a/builtin/history.c +++ b/builtin/history.c @@ -108,6 +108,7 @@ enum commit_tree_flags { static int commit_tree_ext(struct repository *repo, const char *action, struct commit *commit_with_message, + const char *message_template, const struct commit_list *parents, const struct object_id *old_tree, const struct object_id *new_tree, @@ -137,13 +138,16 @@ static int commit_tree_ext(struct repository *repo, original_author = xmemdupz(ptr, len); find_commit_subject(original_message, &original_body); + if (!message_template) + message_template = original_body; + if (flags & COMMIT_TREE_EDIT_MESSAGE) { ret = fill_commit_message(repo, old_tree, new_tree, - original_body, action, &commit_message); + message_template, action, &commit_message); if (ret < 0) goto out; } else { - strbuf_addstr(&commit_message, original_body); + strbuf_addstr(&commit_message, message_template); } original_extra_headers = read_commit_extra_headers(commit_with_message, @@ -196,7 +200,7 @@ static int commit_tree_with_edited_message(struct repository *repo, if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0) return -1; - return commit_tree_ext(repo, action, original, original->parents, + return commit_tree_ext(repo, action, original, NULL, original->parents, &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE); } @@ -675,7 +679,7 @@ static int cmd_history_fixup(int argc, goto out; if (!skip_commit) { - ret = commit_tree_ext(repo, "fixup", original, original->parents, + ret = commit_tree_ext(repo, "fixup", original, NULL, original->parents, &original_tree->object.oid, &merge_result.tree->object.oid, &rewritten, flags); if (ret < 0) { @@ -886,7 +890,7 @@ static int split_commit(struct repository *repo, * The first commit is constructed from the split-out tree. The base * that shall be diffed against is the parent of the original commit. */ - ret = commit_tree_ext(repo, "split-out", original, original->parents, &parent_tree_oid, + ret = commit_tree_ext(repo, "split-out", original, NULL, original->parents, &parent_tree_oid, &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE); if (ret < 0) { ret = error(_("failed writing first commit")); @@ -903,7 +907,7 @@ static int split_commit(struct repository *repo, old_tree_oid = &repo_get_commit_tree(repo, first_commit)->object.oid; new_tree_oid = &repo_get_commit_tree(repo, original)->object.oid; - ret = commit_tree_ext(repo, "split-out", original, parents, old_tree_oid, + ret = commit_tree_ext(repo, "split-out", original, NULL, parents, old_tree_oid, new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE); if (ret < 0) { ret = error(_("failed writing second commit")); From e887fe0b668bfd99fa5ff8b4d82b86ce888a28ea Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Tue, 8 Sep 2026 20:41:54 +0000 Subject: [PATCH 003/155] sequencer: share the squash message marker helpers and flags When "git rebase -i" squashes commits it builds an editor template with a "This is a combination of N commits." banner, a "This is the 1st/Nth commit message:" header above each kept message (or a "will be skipped" header for a dropped one), and a commented-out subject for any fixup!, squash! or amend! commit. The banner, the headers and the subject-commenting all live in static helpers in sequencer.c wired to the rebase state, so no other command can present a squash the same way. Pull the three pieces out into add_squash_combination_header(), add_squash_message_header() (which takes a flag for the "will be skipped" variant) and squash_subject_comment_len(), and use them from update_squash_messages() and append_squash_message(). Also move the todo_item_flags enum to the header, so a caller reading the output of todo_list_rearrange_squash() can tell an amend! (TODO_REPLACE_FIXUP_MSG) from a plain fixup!. A later change reuses all of this to give "git history squash" the same template. No change in behavior. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- sequencer.c | 70 +++++++++++++++++++++++++++++------------------------ sequencer.h | 30 +++++++++++++++++++++++ 2 files changed, 69 insertions(+), 31 deletions(-) diff --git a/sequencer.c b/sequencer.c index 65afd100d98e61..557149d62f1667 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1881,18 +1881,38 @@ static int is_pick_or_similar(enum todo_command command) } } -enum todo_item_flags { - TODO_EDIT_MERGE_MSG = (1 << 0), - TODO_REPLACE_FIXUP_MSG = (1 << 1), - TODO_EDIT_FIXUP_MSG = (1 << 2), -}; - static const char first_commit_msg_str[] = N_("This is the 1st commit message:"); static const char nth_commit_msg_fmt[] = N_("This is the commit message #%d:"); static const char skip_first_commit_msg_str[] = N_("The 1st commit message will be skipped:"); static const char skip_nth_commit_msg_fmt[] = N_("The commit message #%d will be skipped:"); static const char combined_commit_msg_fmt[] = N_("This is a combination of %d commits."); +void add_squash_combination_header(struct strbuf *buf, int n) +{ + strbuf_addf(buf, "%s ", comment_line_str); + strbuf_addf(buf, _(combined_commit_msg_fmt), n); +} + +void add_squash_message_header(struct strbuf *buf, int n, int skip) +{ + strbuf_addf(buf, "%s ", comment_line_str); + if (n == 1) + strbuf_addstr(buf, skip ? _(skip_first_commit_msg_str) : + _(first_commit_msg_str)); + else + strbuf_addf(buf, skip ? _(skip_nth_commit_msg_fmt) : + _(nth_commit_msg_fmt), n); +} + +size_t squash_subject_comment_len(const char *body, int squashing) +{ + if (starts_with(body, "amend!") || + (squashing && (starts_with(body, "squash!") || + starts_with(body, "fixup!")))) + return commit_subject_length(body); + return 0; +} + static int is_fixup_flag(enum todo_command command, unsigned flag) { return command == TODO_FIXUP && ((flag & TODO_REPLACE_FIXUP_MSG) || @@ -2013,20 +2033,13 @@ static int append_squash_message(struct strbuf *buf, const char *body, { struct replay_ctx *ctx = opts->ctx; const char *fixup_msg; - size_t commented_len = 0, fixup_off; - /* - * amend is non-interactive and not normally used with fixup! - * or squash! commits, so only comment out those subjects when - * squashing commit messages. - */ - if (starts_with(body, "amend!") || - ((command == TODO_SQUASH || seen_squash(ctx)) && - (starts_with(body, "squash!") || starts_with(body, "fixup!")))) - commented_len = commit_subject_length(body); + size_t commented_len, fixup_off; + + commented_len = squash_subject_comment_len(body, + command == TODO_SQUASH || seen_squash(ctx)); - strbuf_addf(buf, "\n%s ", comment_line_str); - strbuf_addf(buf, _(nth_commit_msg_fmt), - ++ctx->current_fixup_count + 1); + strbuf_addch(buf, '\n'); + add_squash_message_header(buf, ++ctx->current_fixup_count + 1, 0); strbuf_addstr(buf, "\n\n"); strbuf_add_commented_lines(buf, body, commented_len, comment_line_str); /* buf->buf may be reallocated so store an offset into the buffer */ @@ -2091,9 +2104,8 @@ static int update_squash_messages(struct repository *r, eol = !starts_with(buf.buf, comment_line_str) ? buf.buf : strchrnul(buf.buf, '\n'); - strbuf_addf(&header, "%s ", comment_line_str); - strbuf_addf(&header, _(combined_commit_msg_fmt), - ctx->current_fixup_count + 2); + add_squash_combination_header(&header, + ctx->current_fixup_count + 2); strbuf_splice(&buf, 0, eol - buf.buf, header.buf, header.len); strbuf_release(&header); if (is_fixup_flag(command, flag) && !seen_squash(ctx)) @@ -2117,12 +2129,9 @@ static int update_squash_messages(struct repository *r, repo_unuse_commit_buffer(r, head_commit, head_message); return error(_("cannot write '%s'"), rebase_path_fixup_msg()); } - strbuf_addf(&buf, "%s ", comment_line_str); - strbuf_addf(&buf, _(combined_commit_msg_fmt), 2); - strbuf_addf(&buf, "\n%s ", comment_line_str); - strbuf_addstr(&buf, is_fixup_flag(command, flag) ? - _(skip_first_commit_msg_str) : - _(first_commit_msg_str)); + add_squash_combination_header(&buf, 2); + strbuf_addch(&buf, '\n'); + add_squash_message_header(&buf, 1, is_fixup_flag(command, flag)); strbuf_addstr(&buf, "\n\n"); if (is_fixup_flag(command, flag)) strbuf_add_commented_lines(&buf, body, strlen(body), @@ -2141,9 +2150,8 @@ static int update_squash_messages(struct repository *r, if (command == TODO_SQUASH || is_fixup_flag(command, flag)) { res = append_squash_message(&buf, body, command, opts, flag); } else if (command == TODO_FIXUP) { - strbuf_addf(&buf, "\n%s ", comment_line_str); - strbuf_addf(&buf, _(skip_nth_commit_msg_fmt), - ++ctx->current_fixup_count + 1); + strbuf_addch(&buf, '\n'); + add_squash_message_header(&buf, ++ctx->current_fixup_count + 1, 1); strbuf_addstr(&buf, "\n\n"); strbuf_add_commented_lines(&buf, body, strlen(body), comment_line_str); diff --git a/sequencer.h b/sequencer.h index 64a9c7fb1beccd..b01f8970201fc3 100644 --- a/sequencer.h +++ b/sequencer.h @@ -119,6 +119,13 @@ enum todo_command { TODO_COMMENT }; +/* Bits for the "flags" member of struct todo_item */ +enum todo_item_flags { + TODO_EDIT_MERGE_MSG = (1 << 0), + TODO_REPLACE_FIXUP_MSG = (1 << 1), + TODO_EDIT_FIXUP_MSG = (1 << 2), +}; + struct todo_item { enum todo_command command; struct commit *commit; @@ -208,6 +215,29 @@ int todo_list_rearrange_squash(struct todo_list *todo_list); */ void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag); +/* + * Append the "This is a combination of N commits." banner that "git rebase + * -i" writes at the top of a squashed commit's message, commented out with + * the comment character. + */ +void add_squash_combination_header(struct strbuf *buf, int n); + +/* + * Append the header (1-based N) that "git rebase -i" writes above each message + * when squashing, commented out with the comment character. With SKIP it reads + * "The ... commit message will be skipped" for a message that is dropped (a + * fixup), otherwise "This is the ... commit message". + */ +void add_squash_message_header(struct strbuf *buf, int n, int skip); + +/* + * Return the length of the leading subject of BODY when it should be commented + * out in a squash message, or 0 otherwise. An "amend!" subject always + * qualifies; "squash!" and "fixup!" subjects only when SQUASHING, since a + * plain fixup chain keeps them. + */ +size_t squash_subject_comment_len(const char *body, int squashing); + void append_conflicts_hint(struct index_state *istate, struct strbuf *msgbuf, enum commit_msg_cleanup_mode cleanup_mode); enum commit_msg_cleanup_mode get_cleanup_mode(const char *cleanup_arg, From 6fd039c98475a4b7c5c2da077d25536661385089 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Tue, 8 Sep 2026 20:41:55 +0000 Subject: [PATCH 004/155] history: add skeleton for squash subcommand Add the entry point and option parsing for "git history squash". Pass the remaining arguments through setup_revisions() so the command accepts revision ranges and rev-list options, while restoring the ordering and simplification settings required by the fold if an option changes them. Require at least one BOTTOM revision. The squashed commit needs a commit outside the selected range to serve as its base, so a single positive revision is not a sufficient range. Keep this step limited to defining the revision input contract so graph validation and the rewrite can be added independently. Signed-off-by: Phillip Wood Helped-by: Phillip Wood Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- Documentation/git-history.adoc | 1 + builtin/history.c | 94 ++++++++++++++++++++++++++++++++++ t/t9902-completion.sh | 1 + 3 files changed, 96 insertions(+) diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc index 28b477cd378150..b660baf94db033 100644 --- a/Documentation/git-history.adoc +++ b/Documentation/git-history.adoc @@ -12,6 +12,7 @@ git history drop [--dry-run] [--update-refs=(branches|head)] [--empty=( git history fixup [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] git history reword [--dry-run] [--update-refs=(branches|head)] git history split [--dry-run] [--update-refs=(branches|head)] [--] [...] +git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] DESCRIPTION ----------- diff --git a/builtin/history.c b/builtin/history.c index a60e8dbcd02a67..4f5a7a46ce6fb5 100644 --- a/builtin/history.c +++ b/builtin/history.c @@ -34,6 +34,8 @@ N_("git history reword [--dry-run] [--update-refs=(branches|head)]") #define GIT_HISTORY_SPLIT_USAGE \ N_("git history split [--dry-run] [--update-refs=(branches|head)] [--] [...]") +#define GIT_HISTORY_SQUASH_USAGE \ + N_("git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] ") static void change_data_free(void *util, const char *str UNUSED) { @@ -1004,6 +1006,96 @@ static int cmd_history_split(int argc, return ret; } +static int setup_squash_revisions(struct repository *repo, + int argc, const char **argv, + struct rev_info *revs) +{ + repo_init_revisions(repo, revs, NULL); + revs->reverse = 1; + revs->topo_order = 1; + revs->sort_order = REV_SORT_IN_GRAPH_ORDER; + revs->simplify_history = 0; + revs->ancestry_path = 1; + revs->limited = 1; + revs->ancestry_path_implicit_bottoms = 1; + + argc = setup_revisions(argc, argv, revs, NULL); + if (argc > 1) + return error(_("unrecognized argument: %s"), argv[1]); + + if (revs->reverse != 1 || revs->topo_order != 1 || + revs->sort_order != REV_SORT_IN_GRAPH_ORDER || + revs->simplify_history != 0 || revs->boundary == 1 || + revs->ancestry_path != 1 || revs->limited != 1 || + revs->ancestry_path_implicit_bottoms != 1) { + warning(_("ignoring rev-list options that would change how the " + "range is walked")); + revs->reverse = 1; + revs->topo_order = 1; + revs->sort_order = REV_SORT_IN_GRAPH_ORDER; + revs->simplify_history = 0; + revs->boundary = 0; + revs->ancestry_path = 1; + revs->limited = 1; + revs->ancestry_path_implicit_bottoms = 1; + } + + /* + * A squash needs a base to reparent onto, so the range has to exclude + * something, as in "..". A revision range with no such + * bottom commit cannot be squashed. + */ + for (size_t i = 0; i < revs->cmdline.nr; i++) + if (revs->cmdline.rev[i].flags & BOTTOM) + return 0; + + return error(_("not a '..' revision range")); +} + +static int cmd_history_squash(int argc, + const char **argv, + const char *prefix, + struct repository *repo) +{ + const char * const usage[] = { + GIT_HISTORY_SQUASH_USAGE, + NULL, + }; + enum ref_action action = REF_ACTION_DEFAULT; + int dry_run = 0; + int edit = 1; + struct option options[] = { + OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)", + N_("control which refs should be updated"), + PARSE_OPT_NONEG, parse_ref_action), + OPT_BOOL('n', "dry-run", &dry_run, + N_("perform a dry-run without updating any refs")), + OPT_BOOL('e', "edit", &edit, + N_("edit the commit message")), + OPT_END(), + }; + struct rev_info revs = { 0 }; + int ret; + + argc = parse_options(argc, argv, prefix, options, usage, + PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_ARGV0); + if (argc < 2) { + ret = error(_("command expects a revision range")); + goto out; + } + repo_config(repo, git_default_config, NULL); + + ret = setup_squash_revisions(repo, argc, argv, &revs); + if (ret < 0) + goto out; + + ret = error(_("squashing commits is not implemented yet")); + +out: + release_revisions(&revs); + return ret; +} + static int update_worktree(struct repository *repo, const struct commit *old_head, const struct commit *new_head, @@ -1192,6 +1284,7 @@ int cmd_history(int argc, GIT_HISTORY_FIXUP_USAGE, GIT_HISTORY_REWORD_USAGE, GIT_HISTORY_SPLIT_USAGE, + GIT_HISTORY_SQUASH_USAGE, NULL, }; parse_opt_subcommand_fn *fn = NULL; @@ -1200,6 +1293,7 @@ int cmd_history(int argc, OPT_SUBCOMMAND("fixup", &fn, cmd_history_fixup), OPT_SUBCOMMAND("reword", &fn, cmd_history_reword), OPT_SUBCOMMAND("split", &fn, cmd_history_split), + OPT_SUBCOMMAND("squash", &fn, cmd_history_squash), OPT_END(), }; diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh index 38cf22171829a1..978c42c629b511 100755 --- a/t/t9902-completion.sh +++ b/t/t9902-completion.sh @@ -3227,6 +3227,7 @@ test_expect_success 'git history subcommands' ' fixup Z reword Z split Z + squash Z EOF test_completion "git history --" "" ' From fb70711b903352c2ec300a6f9ec825b0e2cdf198 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Tue, 8 Sep 2026 20:41:56 +0000 Subject: [PATCH 005/155] history: validate squash revision ranges Walk the selected commits in topological order from oldest to newest and mark each one as it is seen. Every parent after the oldest commit must already be selected or also be a parent of the oldest commit. This accepts merges contained by the range while rejecting a merge arm that entered it from elsewhere. Track the remaining graph tips during the same walk and require exactly one. Also reject empty and single-commit ranges and any selection that reaches a root commit. These checks identify the oldest commit whose parents will be preserved and the single tip whose tree will be used by the rewrite. Signed-off-by: Phillip Wood Helped-by: Phillip Wood Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- builtin/history.c | 119 ++++++++++++++++++++++++++++++++++---- object.h | 1 + t/meson.build | 1 + t/t3455-history-squash.sh | 65 +++++++++++++++++++++ 4 files changed, 174 insertions(+), 12 deletions(-) create mode 100755 t/t3455-history-squash.sh diff --git a/builtin/history.c b/builtin/history.c index 4f5a7a46ce6fb5..6541a397e80277 100644 --- a/builtin/history.c +++ b/builtin/history.c @@ -1006,6 +1006,9 @@ static int cmd_history_split(int argc, return ret; } +#define SQUASH_SEEN (1u << 11) +#define SQUASH_TIP (1u << 12) + static int setup_squash_revisions(struct repository *repo, int argc, const char **argv, struct rev_info *revs) @@ -1052,6 +1055,104 @@ static int setup_squash_revisions(struct repository *repo, return error(_("not a '..' revision range")); } +/* + * Resolve a revision range into its oldest commit and single tip. Every + * parent after the oldest commit must either be selected or also be a parent + * of the oldest commit. + */ +static int resolve_squash_range(struct repository *repo, + int argc, const char **argv, + struct commit **oldest_out, + struct commit **tip_out) +{ + struct rev_info revs; + struct commit *commit, *oldest = NULL, *tip = NULL; + int ret, tip_count = 0; + bool walk_started = false; + + ret = setup_squash_revisions(repo, argc, argv, &revs); + if (ret < 0) + goto out; + + if (prepare_revision_walk(&revs) < 0) { + ret = error(_("error preparing revisions")); + goto out; + } + walk_started = true; + while ((commit = get_revision(&revs))) { + struct commit_list *p; + + if (!commit->parents) { + ret = error(_("cannot squash down to root commit")); + goto out; + } + for (p = commit->parents; oldest && p; p = p->next) { + struct commit_list *q; + struct object *o; + bool seen; + + if (repo_parse_commit(repo, p->item)) { + ret = error(_("cannot parse commit")); + goto out; + } + o = &p->item->object; + seen = o->flags & SQUASH_SEEN; + /* + * Allow parents that match the parents of the + * squashed commit. + */ + for (q = oldest->parents; !seen && q; q = q->next) + if (p->item == q->item) + seen = true; + if (!seen) { + ret = error(_("parent %s of commit %s is " + "outside the revision range"), + repo_find_unique_abbrev(repo, &o->oid, + DEFAULT_ABBREV), + repo_find_unique_abbrev(repo, + &commit->object.oid, + DEFAULT_ABBREV)); + goto out; + } + if (o->flags & SQUASH_TIP) { + tip_count--; + o->flags &= ~SQUASH_TIP; + } + } + if (!oldest) + oldest = commit; + tip = commit; + tip->object.flags |= SQUASH_SEEN | SQUASH_TIP; + tip_count++; + } + + if (!tip_count) { + ret = error(_("the revision range is empty")); + goto out; + } else if (tip_count != 1) { + ret = error(_("the revision range contains more than one tip " + "commit")); + goto out; + } else if (oldest == tip) { + ret = error(_("the revision range holds a single commit; " + "nothing to squash")); + goto out; + } else if (!oldest->parents) { + BUG("an in-range commit must have a parent"); + } + + *oldest_out = oldest; + *tip_out = tip; + ret = 0; + +out: + clear_object_flags(repo, SQUASH_SEEN | SQUASH_TIP); + if (walk_started) + reset_revision_walk(); + release_revisions(&revs); + return ret; +} + static int cmd_history_squash(int argc, const char **argv, const char *prefix, @@ -1074,26 +1175,20 @@ static int cmd_history_squash(int argc, N_("edit the commit message")), OPT_END(), }; - struct rev_info revs = { 0 }; + struct commit *oldest, *tip; int ret; argc = parse_options(argc, argv, prefix, options, usage, PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_ARGV0); - if (argc < 2) { - ret = error(_("command expects a revision range")); - goto out; - } + if (argc < 2) + return error(_("command expects a revision range")); repo_config(repo, git_default_config, NULL); - ret = setup_squash_revisions(repo, argc, argv, &revs); + ret = resolve_squash_range(repo, argc, argv, &oldest, &tip); if (ret < 0) - goto out; - - ret = error(_("squashing commits is not implemented yet")); + return ret; -out: - release_revisions(&revs); - return ret; + return error(_("squashing commits is not implemented yet")); } static int update_worktree(struct repository *repo, diff --git a/object.h b/object.h index 8fb03ff90a3e56..dcf30156ca6786 100644 --- a/object.h +++ b/object.h @@ -74,6 +74,7 @@ void object_array_init(struct object_array *array); * bisect.c: 16 * bundle.c: 16 * http-push.c: 11-----14 + * builtin/history.c: 1112 * commit-graph.c: 15 * commit-reach.c: 16-------20 * builtin/last-modified.c: 1617 diff --git a/t/meson.build b/t/meson.build index 7f53cca7d1f891..ba6f33cdffa184 100644 --- a/t/meson.build +++ b/t/meson.build @@ -407,6 +407,7 @@ integration_tests = [ 't3452-history-split.sh', 't3453-history-fixup.sh', 't3454-history-drop.sh', + 't3455-history-squash.sh', 't3500-cherry.sh', 't3501-revert-cherry-pick.sh', 't3502-cherry-pick-merge.sh', diff --git a/t/t3455-history-squash.sh b/t/t3455-history-squash.sh new file mode 100755 index 00000000000000..df92aa4f6c9e6c --- /dev/null +++ b/t/t3455-history-squash.sh @@ -0,0 +1,65 @@ +#!/bin/sh + +test_description='tests for git-history squash subcommand' + +. ./test-lib.sh + +test_expect_success 'setup linear history' ' + test_commit base file a start && + test_commit one file b && + test_commit two file c && + test_commit three file d +' + +test_expect_success 'errors on missing range argument' ' + test_must_fail git history squash 2>err && + test_grep "expects a revision range" err +' + +test_expect_success 'errors on an empty range' ' + test_must_fail git history squash HEAD..HEAD 2>err && + test_grep "the revision range is empty" err +' + +test_expect_success 'errors on a single revision that is not a range' ' + test_must_fail git history squash HEAD 2>err && + test_grep "not a .*range" err && + test_must_fail git history squash HEAD~1 2>err && + test_grep "not a .*range" err +' + +test_expect_success 'errors on a range holding a single commit' ' + test_must_fail git history squash "HEAD^!" 2>err && + test_grep "single commit; nothing to squash" err +' + +test_expect_success 'rejects a root commit' ' + oid=$(git commit-tree -m root three^{tree}) && + test_must_fail git history squash \ + --ancestry-path=start "$oid..three" 2>err && + test_grep "cannot squash down to root commit" err +' + +test_expect_success 'rejects multiple tips' ' + oid=$(git commit-tree -m tip -p start^0 three^{tree}) && + test_must_fail git history squash ^start "$oid" three~1 2>err && + test_grep "revision range contains more than one tip" err +' + +test_expect_success 'rejects a merge parent outside the range' ' + git reset --hard start && + main=$(git symbolic-ref --short HEAD) && + git checkout -b outside-parent && + test_commit --no-tag outside-parent outside x && + git checkout "$main" && + test_commit --no-tag outside-main file b && + base=$(git rev-parse HEAD) && + test_commit --no-tag outside-mid file c && + git merge --no-ff -m "merge outside-parent" outside-parent && + git branch -D outside-parent && + + test_must_fail git history squash "$base.." 2>err && + test_grep "parent .* of commit .* is outside the revision range" err +' + +test_done From 25cfa7c69aef48203f91751f4a83ed0324bea3e3 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Tue, 8 Sep 2026 20:41:57 +0000 Subject: [PATCH 006/155] history: protect branches when squashing a range A local branch that descends from the selected graph without containing its tip cannot be replayed as a descendant of the squashed commit. Find those branches with ref-filter before creating any replacement objects and refuse the operation unless --update-refs=head was requested. Limit this protection to local branches, matching the refs that the default history rewrite mode updates; tags and remote-tracking refs remain untouched. Sort the blocking refs and print their short branch names so the user can decide whether to move them or leave them behind. Add advice.historyUpdateRefs for the hint that points to --update-refs=head. Signed-off-by: Phillip Wood Helped-by: Phillip Wood Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- Documentation/config/advice.adoc | 4 ++ advice.c | 1 + advice.h | 1 + builtin/history.c | 70 ++++++++++++++++++++++++++++++-- t/t3455-history-squash.sh | 39 ++++++++++++++++++ 5 files changed, 111 insertions(+), 4 deletions(-) diff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc index 81f80a92745123..3d91c90eda72f9 100644 --- a/Documentation/config/advice.adoc +++ b/Documentation/config/advice.adoc @@ -59,6 +59,10 @@ all advice messages. forceDeleteBranch:: Shown when the user tries to delete a not fully merged branch without the force option set. + historyUpdateRefs:: + Shown when `git history squash` refuses because a local branch + cannot be rewritten as a descendant of the squashed commit, to + tell the user about `--update-refs=head`. ignoredHook:: Shown when a hook is ignored because the hook is not set as executable. diff --git a/advice.c b/advice.c index 63bf8b0c5f0481..401d0473917cd9 100644 --- a/advice.c +++ b/advice.c @@ -58,6 +58,7 @@ static struct { [ADVICE_FETCH_SHOW_FORCED_UPDATES] = { "fetchShowForcedUpdates" }, [ADVICE_FORCE_DELETE_BRANCH] = { "forceDeleteBranch" }, [ADVICE_GRAFT_FILE_DEPRECATED] = { "graftFileDeprecated" }, + [ADVICE_HISTORY_UPDATE_REFS] = { "historyUpdateRefs" }, [ADVICE_IGNORED_HOOK] = { "ignoredHook" }, [ADVICE_IMPLICIT_IDENTITY] = { "implicitIdentity" }, [ADVICE_MERGE_CONFLICT] = { "mergeConflict" }, diff --git a/advice.h b/advice.h index 66f6cd6a772d8c..3f0b4f0485c7fe 100644 --- a/advice.h +++ b/advice.h @@ -25,6 +25,7 @@ enum advice_type { ADVICE_FETCH_SHOW_FORCED_UPDATES, ADVICE_FORCE_DELETE_BRANCH, ADVICE_GRAFT_FILE_DEPRECATED, + ADVICE_HISTORY_UPDATE_REFS, ADVICE_IGNORED_HOOK, ADVICE_IMPLICIT_IDENTITY, ADVICE_MERGE_CONFLICT, diff --git a/builtin/history.c b/builtin/history.c index 6541a397e80277..e65b76b59f8222 100644 --- a/builtin/history.c +++ b/builtin/history.c @@ -1,6 +1,7 @@ #define USE_THE_REPOSITORY_VARIABLE #include "builtin.h" +#include "advice.h" #include "cache-tree.h" #include "commit.h" #include "commit-reach.h" @@ -16,10 +17,12 @@ #include "path.h" #include "read-cache.h" #include "refs.h" +#include "ref-filter.h" #include "replay.h" #include "reset.h" #include "revision.h" #include "sequencer.h" +#include "string-list.h" #include "strvec.h" #include "tree.h" #include "tree-walk.h" @@ -1061,6 +1064,7 @@ static int setup_squash_revisions(struct repository *repo, * of the oldest commit. */ static int resolve_squash_range(struct repository *repo, + bool update_branches, int argc, const char **argv, struct commit **oldest_out, struct commit **tip_out) @@ -1069,6 +1073,8 @@ static int resolve_squash_range(struct repository *repo, struct commit *commit, *oldest = NULL, *tip = NULL; int ret, tip_count = 0; bool walk_started = false; + struct ref_filter filter = REF_FILTER_INIT; + struct ref_array refs = { 0 }; ret = setup_squash_revisions(repo, argc, argv, &revs); if (ret < 0) @@ -1101,9 +1107,12 @@ static int resolve_squash_range(struct repository *repo, * Allow parents that match the parents of the * squashed commit. */ - for (q = oldest->parents; !seen && q; q = q->next) - if (p->item == q->item) + for (q = oldest->parents; !seen && q; q = q->next) { + if (p->item == q->item) { seen = true; + commit_list_insert(commit, &filter.with_commit); + } + } if (!seen) { ret = error(_("parent %s of commit %s is " "outside the revision range"), @@ -1119,12 +1128,17 @@ static int resolve_squash_range(struct repository *repo, o->flags &= ~SQUASH_TIP; } } - if (!oldest) + if (!oldest) { + commit_list_insert(commit, &filter.with_commit); oldest = commit; + } tip = commit; tip->object.flags |= SQUASH_SEEN | SQUASH_TIP; tip_count++; } + clear_object_flags(repo, SQUASH_SEEN | SQUASH_TIP); + reset_revision_walk(); + walk_started = false; if (!tip_count) { ret = error(_("the revision range is empty")); @@ -1141,6 +1155,49 @@ static int resolve_squash_range(struct repository *repo, BUG("an in-range commit must have a parent"); } + commit_list_insert(tip, &filter.no_commit); + filter.kind = FILTER_REFS_BRANCHES; + if (update_branches && + filter_refs(&refs, &filter, filter.kind)) { + ret = error(_("could not filter refs")); + goto out; + } + if (refs.nr) { + struct ref_format format = REF_FORMAT_INIT; + struct ref_sorting *sorting; + struct string_list sorting_options = STRING_LIST_INIT_DUP; + struct strbuf branches = STRBUF_INIT; + struct strbuf err = STRBUF_INIT; + + format.format = "%(refname:short)"; + if (verify_ref_format(&format)) + BUG("invalid branch format"); + string_list_append(&sorting_options, "refname"); + sorting = ref_sorting_options(&sorting_options); + ref_array_sort(sorting, &refs); + for (int i = 0; i < refs.nr; i++) { + strbuf_reset(&err); + strbuf_addstr(&branches, "\n "); + if (format_ref_array_item(refs.items[i], &format, + &branches, &err)) + BUG("could not format branch name: %s", err.buf); + } + /* + * TODO: also check HEADS from other worktrees. + */ + ret = error(_("the following branches cannot be rewritten as " + "descendants of the squashed commit:%s"), branches.buf); + advise_if_enabled(ADVICE_HISTORY_UPDATE_REFS, + _("Use --update-refs=head to rewrite only " + "the current branch and leave such branches " + "untouched.")); + strbuf_release(&err); + strbuf_release(&branches); + ref_sorting_release(sorting); + string_list_clear(&sorting_options, 0); + goto out; + } + *oldest_out = oldest; *tip_out = tip; ret = 0; @@ -1150,6 +1207,8 @@ static int resolve_squash_range(struct repository *repo, if (walk_started) reset_revision_walk(); release_revisions(&revs); + ref_filter_clear(&filter); + ref_array_clear(&refs); return ret; } @@ -1183,8 +1242,11 @@ static int cmd_history_squash(int argc, if (argc < 2) return error(_("command expects a revision range")); repo_config(repo, git_default_config, NULL); + if (action == REF_ACTION_DEFAULT) + action = REF_ACTION_BRANCHES; - ret = resolve_squash_range(repo, argc, argv, &oldest, &tip); + ret = resolve_squash_range(repo, action == REF_ACTION_BRANCHES, + argc, argv, &oldest, &tip); if (ret < 0) return ret; diff --git a/t/t3455-history-squash.sh b/t/t3455-history-squash.sh index df92aa4f6c9e6c..b1f65de5f507ad 100755 --- a/t/t3455-history-squash.sh +++ b/t/t3455-history-squash.sh @@ -62,4 +62,43 @@ test_expect_success 'rejects a merge parent outside the range' ' test_grep "parent .* of commit .* is outside the revision range" err ' +test_expect_success 'prints branches that cannot follow the squash' ' + test_when_finished \ + "git switch -f $GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME; \ + git branch -D feature" && + git checkout -f -b feature start && + test_commit C1 && + test_commit C2 && + git checkout -b topic-1 start && + test_commit C3 && + test_commit C4 && + git checkout C3 && + test_commit C5 && + git checkout feature && + git merge C5 && + test_commit C6 && + git checkout -b topic-2 C2 && + test_commit C7 && + git checkout feature && + + test_must_fail git history squash start.. 2>err && + test_grep "^error: the following branches cannot be rewritten" err && + test_grep "^ topic-1$" err && + test_grep "^ topic-2$" err && + test_grep "^hint: .* --update-refs=head" err +' + +test_expect_success 'advice.historyUpdateRefs silences the hint' ' + git reset --hard three && + git branch -f mid HEAD~1 && + + test_must_fail git -c advice.historyUpdateRefs=false \ + history squash start.. 2>err && + test_grep "^error: the following branches cannot be rewritten" err && + test_grep "^ mid$" err && + test_grep ! "hint:" err && + + git branch -D mid +' + test_done From 8c80498582bffc7b755406dbe3660e8b6098affe Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Tue, 8 Sep 2026 20:41:58 +0000 Subject: [PATCH 007/155] history: create squashed commits without editing Create one replacement commit from the resolved range when --no-edit is selected. Preserve the authorship and all parents of the oldest commit, use the tip tree, and replay descendants through the existing history rewrite machinery. Record the complete revision expression in the reflog and retain dry-run and update-refs behavior. Resolve fixup!, squash! and amend! subjects while walking the range. Reject markers whose targets are not selected and refuse any no-edit fold that would discard a squash! or amend! message. A range made entirely from related markers can still be consolidated, with the last applicable amend! body supplying the message. Inspired-by: Sergey Chernov Signed-off-by: Phillip Wood Helped-by: Phillip Wood Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- Documentation/git-history.adoc | 44 ++- builtin/history.c | 324 ++++++++++++++++- object.h | 2 +- t/t3455-history-squash.sh | 633 +++++++++++++++++++++++++++++++-- 4 files changed, 968 insertions(+), 35 deletions(-) diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc index b660baf94db033..fb04a6768513f0 100644 --- a/Documentation/git-history.adoc +++ b/Documentation/git-history.adoc @@ -44,8 +44,11 @@ at once. LIMITATIONS ----------- -This command does not (yet) work with histories that contain merges. You -should use linkgit:git-rebase[1] with the `--rebase-merges` flag instead. +This command does not (yet) replay merge commits onto the rewritten +history: if a commit that would be replayed is a merge, the operation is +rejected, and you should use linkgit:git-rebase[1] with the +`--rebase-merges` flag instead. The `squash` subcommand can still fold merges +that lie inside the selected range, subject to the restrictions below. Furthermore, the command does not support operations that can result in merge conflicts. This limitation is by design as history rewrites are not intended to @@ -114,6 +117,43 @@ linkgit:gitglossary[7]. It is invalid to select either all or no hunks, as that would lead to one of the commits becoming empty. +`squash `:: + Fold all commits in __ into the oldest commit of that + range. The resulting commit keeps the oldest commit's authorship and + takes the tree of the range's newest commit, so the whole range + collapses into a single commit. Commits above the range are replayed + on top of the result. ++ +The range is given in the usual `..` form, where __ is +the commit just below the oldest commit to squash. For example, `git +history squash HEAD~3..HEAD` folds the three most recent commits into +one, and `git history squash HEAD~5..HEAD~2` squashes an interior range +while leaving the two newest commits in place. Several revisions may be +given, for example `HEAD~3..HEAD ^topic` to additionally exclude what is +already on `topic`. Rev-list options may also be given, but any that would +change how the range is walked are overridden with a warning. ++ +With `--no-edit`, the oldest commit's message is preserved, except that an +`amend!` commit targeting it replaces its message. ++ +The selected commits must form a connected graph with a single tip and must +not include a root commit. Every parent of a commit after the oldest one must +either be selected or also be a parent of the oldest commit. When the oldest +commit is a merge, all of its parents are preserved in the squashed commit. ++ +A `fixup!`, `squash!`, or `amend!` commit is refused unless the commit it +targets is also in the range, so the fold does not silently absorb a +marker meant for a commit outside it. As an exception, a range made up entirely +of markers for one target is combined into a single commit. With `--no-edit`, +the last `amend!` message is used if there is one; a `squash!` or `amend!` is +otherwise refused if folding it would discard its message. ++ +A local branch descended from a selected commit but not from the range tip +cannot be rewritten as a descendant of the result, so with the default +`--update-refs=branches` the command refuses. Rerun with `--update-refs=head` +to rewrite only the current branch and leave such branches unchanged. Tags +and remote-tracking refs are always left unchanged. + OPTIONS ------- diff --git a/builtin/history.c b/builtin/history.c index e65b76b59f8222..8176f6794e8292 100644 --- a/builtin/history.c +++ b/builtin/history.c @@ -1011,6 +1011,260 @@ static int cmd_history_split(int argc, #define SQUASH_SEEN (1u << 11) #define SQUASH_TIP (1u << 12) +#define SQUASH_AMEND_TARGET (1u << 13) + +static bool is_autosquash_subject(const char *s) +{ + return starts_with(s, "amend!") || starts_with(s, "fixup!") || + starts_with(s, "squash!"); +} + +static bool skip_one_autosquash_prefix(const char *s, const char **out) +{ + if (skip_prefix(s, "amend!", out) || skip_prefix(s, "fixup!", out) || + skip_prefix(s, "squash!", out)) { + while (**out == ' ') + (*out)++; + return true; + } + return false; +} + +static void truncate_message_to_subject(struct strbuf *msg) +{ + const char *eos = strstr(msg->buf, "\n\n"); + + if (eos) + strbuf_setlen(msg, eos - msg->buf + 1); +} + +struct subject_data { + struct strintmap subjects; + struct strbuf subject; + struct strbuf squash_message; + const char *message; + bool edit_message; +}; + +#define SUBJECT_DATA_INIT { \ + .subjects = STRINTMAP_INIT, \ + .subject = STRBUF_INIT, \ + .squash_message = STRBUF_INIT, \ +} + +static void subject_data_clear(struct subject_data *data) +{ + strintmap_clear(&data->subjects); + strbuf_release(&data->subject); + strbuf_release(&data->squash_message); +} + +static int squash_amend_message(struct repository *repo, + struct commit *commit, + struct subject_data *data, + unsigned flags) +{ + const char *body = data->message + data->subject.len; + + while (isspace(*body)) + body++; + + if (!*body) { + warning(_("ignoring %s (%s): message body is empty"), + repo_find_unique_abbrev(repo, &commit->object.oid, + DEFAULT_ABBREV), + data->subject.buf); + return 0; + } + + if (data->edit_message) { + return 0; + } else if (flags & SQUASH_AMEND_TARGET) { + if (starts_with(data->squash_message.buf, "squash!")) + return error(_("squashing %s (%s) would overwrite " + "'squash!' message, please combine them " + "using '--edit'"), + repo_find_unique_abbrev(repo, + &commit->object.oid, + DEFAULT_ABBREV), + data->subject.buf); + if (starts_with(data->squash_message.buf, "fixup!")) + strbuf_splice(&data->squash_message, 0, 5, "amend!", 5); + if (starts_with(data->squash_message.buf, "amend!")) { + truncate_message_to_subject(&data->squash_message); + strbuf_addch(&data->squash_message, '\n'); + } else { + strbuf_reset(&data->squash_message); + } + strbuf_addstr(&data->squash_message, body); + strbuf_complete_line(&data->squash_message); + } else { + return error(_("cannot squash %s (%s) that does not target " + "base commit without '--edit'"), + repo_find_unique_abbrev(repo, &commit->object.oid, + DEFAULT_ABBREV), + data->subject.buf); + } + return 0; +} + +static int squash_squash_message(struct repository *repo, + struct commit *commit, + struct subject_data *data, + unsigned flags) +{ + const char *body = data->message + data->subject.len; + + while (isspace(*body)) + body++; + + if (data->edit_message) { + return 0; + } else if (flags & SQUASH_AMEND_TARGET) { + if (starts_with(data->squash_message.buf, "fixup!")) { + truncate_message_to_subject(&data->squash_message); + strbuf_splice(&data->squash_message, 0, 5, "squash", 6); + } + if (starts_with(data->squash_message.buf, "squash!")) { + strbuf_addch(&data->squash_message, '\n'); + strbuf_addstr(&data->squash_message, body); + strbuf_complete_line(&data->squash_message); + } else { + return error(_("squashing %s (%s) would discard its " + "message, please combine them using " + "'--edit'"), + repo_find_unique_abbrev(repo, + &commit->object.oid, + DEFAULT_ABBREV), + data->subject.buf); + } + } else { + return error(_("cannot squash %s (%s) that does not target " + "base commit without '--edit'"), + repo_find_unique_abbrev(repo, &commit->object.oid, + DEFAULT_ABBREV), + data->subject.buf); + } + return 0; +} + +static int squash_check_can_autosquash(struct repository *repo, + struct commit *commit, + struct subject_data *data, + unsigned flags) +{ + commit->object.flags |= flags & SQUASH_AMEND_TARGET; + if (starts_with(data->subject.buf, "amend!")) + return squash_amend_message(repo, commit, data, flags); + else if (starts_with(data->subject.buf, "squash!")) + return squash_squash_message(repo, commit, data, flags); + + return 0; +} + +static int squash_check_autosquash_subject(struct repository *repo, + struct commit *commit, + struct subject_data *data) +{ + const char* s = data->subject.buf; + struct commit *target; + struct hashmap_iter iter; + struct strmap_entry *entry; + /* Try skipping autosquash prefixes one at a time to allow + * squashing + * a commit + * fixup! fixup! a commit + * + * where we may have started with + * a commit + * fixup! a commit + * fixup! fixup! a commit + * + * and squashed the first fixup separately from the second + */ + while (skip_one_autosquash_prefix(s, &s)) { + unsigned flags = strintmap_get(&data->subjects, s); + if (flags) + return squash_check_can_autosquash(repo, commit, data, flags); + } + /* + * Allow "fixup! ", but not "fixup! HEAD^" or + * "fixup! main". If the target is not being squashed check the subject + * to allow "fixup! abc123" and "fixup! " to be + * squashed together. + */ + target = lookup_commit_reference_by_name(s); + if (target && starts_with(oid_to_hex(&target->object.oid), s)) { + unsigned flags = + target->object.flags & (SQUASH_SEEN | SQUASH_AMEND_TARGET); + if (!flags) { + const char *subject_start; + const char *buffer = repo_logmsg_reencode(repo, target, + NULL, NULL); + size_t subject_len = find_commit_subject(buffer, + &subject_start); + char *subject = xmemdupz(subject_start, subject_len); + + flags = strintmap_get(&data->subjects, subject); + free(subject); + repo_unuse_commit_buffer(repo, target, buffer); + } + if (flags) + return squash_check_can_autosquash(repo, commit, + data, flags); + } + /* Try subject prefix matches */ + strintmap_for_each_entry(&data->subjects, &iter, entry) { + s = data->subject.buf; + while(skip_one_autosquash_prefix(s, &s)) { + if (starts_with(entry->key, s)) { + unsigned value = (intptr_t)entry->value; + + return squash_check_can_autosquash(repo, commit, + data, value); + } + } + } + return error(_("cannot squash %s (%s): its target is not being " + "squashed"), + repo_find_unique_abbrev(repo, &commit->object.oid, + DEFAULT_ABBREV), + data->subject.buf); +} + +static int squash_check_subject(struct repository *repo, + struct commit *commit, + struct subject_data *data) +{ + int ret = 0; + const char *buf = repo_logmsg_reencode(repo, commit, NULL, NULL); + size_t subject_len = find_commit_subject(buf, &data->message); + + strbuf_reset(&data->subject); + strbuf_add(&data->subject, data->message, subject_len); + + if (!strintmap_get_size(&data->subjects)) { + const char *s; + + strbuf_addstr(&data->squash_message, data->message); + strbuf_complete_line(&data->squash_message); + /* + * Strip a single autosquash prefix to allow squashing + * fixup! base + * amend! base + */ + s = data->subject.buf; + skip_one_autosquash_prefix(s, &s); + strintmap_set(&data->subjects, s, SQUASH_AMEND_TARGET | SQUASH_SEEN); + commit->object.flags |= SQUASH_AMEND_TARGET; + } else if (is_autosquash_subject(data->subject.buf)) { + ret = squash_check_autosquash_subject(repo, commit, data); + } else { + strintmap_set(&data->subjects, data->subject.buf, SQUASH_SEEN); + } + repo_unuse_commit_buffer(repo, commit, buf); + return ret; +} static int setup_squash_revisions(struct repository *repo, int argc, const char **argv, @@ -1067,9 +1321,11 @@ static int resolve_squash_range(struct repository *repo, bool update_branches, int argc, const char **argv, struct commit **oldest_out, - struct commit **tip_out) + struct commit **tip_out, + char **message_out) { struct rev_info revs; + struct subject_data subject_data = SUBJECT_DATA_INIT; struct commit *commit, *oldest = NULL, *tip = NULL; int ret, tip_count = 0; bool walk_started = false; @@ -1132,6 +1388,10 @@ static int resolve_squash_range(struct repository *repo, commit_list_insert(commit, &filter.with_commit); oldest = commit; } + if (squash_check_subject(repo, commit, &subject_data)) { + ret = -1; + goto out; + } tip = commit; tip->object.flags |= SQUASH_SEEN | SQUASH_TIP; tip_count++; @@ -1200,12 +1460,15 @@ static int resolve_squash_range(struct repository *repo, *oldest_out = oldest; *tip_out = tip; + *message_out = strbuf_detach(&subject_data.squash_message, NULL); ret = 0; out: - clear_object_flags(repo, SQUASH_SEEN | SQUASH_TIP); + clear_object_flags(repo, SQUASH_SEEN | SQUASH_TIP | + SQUASH_AMEND_TARGET); if (walk_started) reset_revision_walk(); + subject_data_clear(&subject_data); release_revisions(&revs); ref_filter_clear(&filter); ref_array_clear(&refs); @@ -1234,23 +1497,68 @@ static int cmd_history_squash(int argc, N_("edit the commit message")), OPT_END(), }; - struct commit *oldest, *tip; + struct strbuf reflog_msg = STRBUF_INIT; + struct commit *oldest, *tip, *rewritten; + const struct object_id *base_tree_oid, *tip_tree_oid; + char *message_template = NULL; + struct rev_info revs = { 0 }; int ret; argc = parse_options(argc, argv, prefix, options, usage, PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_ARGV0); - if (argc < 2) - return error(_("command expects a revision range")); + if (argc < 2) { + ret = error(_("command expects a revision range")); + goto out; + } repo_config(repo, git_default_config, NULL); + if (action == REF_ACTION_DEFAULT) action = REF_ACTION_BRANCHES; + strbuf_addstr(&reflog_msg, "squash: updating "); + strbuf_join_argv(&reflog_msg, argc - 1, argv + 1, ' '); + ret = resolve_squash_range(repo, action == REF_ACTION_BRANCHES, - argc, argv, &oldest, &tip); + argc, argv, &oldest, &tip, + &message_template); if (ret < 0) - return ret; + goto out; + if (edit) { + ret = error(_("message editing is not supported yet; use '--no-edit'")); + goto out; + } + + ret = setup_revwalk(repo, action, tip, &revs); + if (ret < 0) + goto out; + + base_tree_oid = &repo_get_commit_tree(repo, + oldest->parents->item)->object.oid; + tip_tree_oid = &repo_get_commit_tree(repo, tip)->object.oid; + + ret = commit_tree_ext(repo, "squash", oldest, message_template, + oldest->parents, base_tree_oid, tip_tree_oid, + &rewritten, 0); + if (ret < 0) { + ret = error(_("failed writing squashed commit")); + goto out; + } + + ret = handle_reference_updates(&revs, action, tip, rewritten, + reflog_msg.buf, dry_run, + REPLAY_EMPTY_COMMIT_ABORT); + if (ret < 0) { + ret = error(_("failed replaying descendants")); + goto out; + } - return error(_("squashing commits is not implemented yet")); + ret = 0; + +out: + strbuf_release(&reflog_msg); + release_revisions(&revs); + free(message_template); + return ret; } static int update_worktree(struct repository *repo, diff --git a/object.h b/object.h index dcf30156ca6786..46cade33fb18fb 100644 --- a/object.h +++ b/object.h @@ -74,7 +74,7 @@ void object_array_init(struct object_array *array); * bisect.c: 16 * bundle.c: 16 * http-push.c: 11-----14 - * builtin/history.c: 1112 + * builtin/history.c: 11---13 * commit-graph.c: 15 * commit-reach.c: 16-------20 * builtin/last-modified.c: 1617 diff --git a/t/t3455-history-squash.sh b/t/t3455-history-squash.sh index b1f65de5f507ad..fb06637aafd0e1 100755 --- a/t/t3455-history-squash.sh +++ b/t/t3455-history-squash.sh @@ -4,11 +4,50 @@ test_description='tests for git-history squash subcommand' . ./test-lib.sh -test_expect_success 'setup linear history' ' +stage_file () { + printf "%s\n" "$1" >file && + git add file +} + +commit_with_message () { + printf "%b" "$1" >msg && + git commit --allow-empty -qF msg +} + +check_commit_count () { + git rev-list --count "$1" >actual && + echo "$2" >expect && + test_cmp expect actual +} + +check_log_subjects () { + git log --format="%s" "$1" >actual && + cat >expect && + test_cmp expect actual +} + +check_log_messages () { + git log --format="%B" "$1" >actual && + cat >expect && + test_cmp expect actual +} + +# Checks that the author data of two commits matches +# Usage: check_commit_author +check_commit_author () { + git show -s --format="%an <%ae> %ad" "$1" >expect && + git show -s --format="%an <%ae> %ad" "$2" >actual && + test_cmp expect actual +} + +test_expect_success 'setup linear history touching two files' ' test_commit base file a start && - test_commit one file b && - test_commit two file c && - test_commit three file d + GIT_AUTHOR_NAME=One GIT_AUTHOR_EMAIL=one@example.com \ + test_commit one other x && + GIT_AUTHOR_NAME=Two GIT_AUTHOR_EMAIL=two@example.com \ + test_commit two file c && + GIT_AUTHOR_NAME=Three GIT_AUTHOR_EMAIL=three@example.com \ + test_commit three file d ' test_expect_success 'errors on missing range argument' ' @@ -29,40 +68,331 @@ test_expect_success 'errors on a single revision that is not a range' ' ' test_expect_success 'errors on a range holding a single commit' ' + git reset --hard three && + head_before=$(git rev-parse HEAD) && + test_must_fail git history squash "HEAD^!" 2>err && - test_grep "single commit; nothing to squash" err + test_grep "single commit; nothing to squash" err && + test_cmp_rev "$head_before" HEAD ' -test_expect_success 'rejects a root commit' ' +test_expect_success 'rejects root commit' ' + # create a disconnected root commit oid=$(git commit-tree -m root three^{tree}) && - test_must_fail git history squash \ - --ancestry-path=start "$oid..three" 2>err && - test_grep "cannot squash down to root commit" err + # because we pass --ancestry-path when calling setup_revs() it the + # revision walk will only include commits decended from $oid so + # we need to give it another --ancestry-path commit to actually walk + # any commits. + test_must_fail git history squash --ancestry-path=start $oid..three 2>err && + echo "error: cannot squash down to root commit" >expect && + test_cmp expect err ' test_expect_success 'rejects multiple tips' ' oid=$(git commit-tree -m tip -p start^0 three^{tree}) && - test_must_fail git history squash ^start "$oid" three~1 2>err && - test_grep "revision range contains more than one tip" err + test_must_fail git history squash ^start $oid three~1 2>err && + echo "error: the revision range contains more than one tip commit" >expect && + test_cmp expect err && + + git reset --hard three && + git history squash --no-edit ^start three~1 three && + test_cmp_rev HEAD~1 start^0 && + test_cmp_rev HEAD^{tree} three^{tree} ' -test_expect_success 'rejects a merge parent outside the range' ' - git reset --hard start && +test_expect_success 'accepts multiple revision arguments with an exclusion' ' + git reset --hard three && + git branch -f keep HEAD~2 && + tip_tree=$(git rev-parse HEAD^{tree}) && + + git history squash --no-edit start..HEAD ^keep && + + git reflog -1 --format=%gs >actual && + echo "squash: updating start..HEAD ^keep" >expect && + test_cmp expect actual && + + check_log_subjects start..HEAD <<-\EOF && + two + one + EOF + test_cmp_rev keep HEAD~1 && + test "$tip_tree" = "$(git rev-parse HEAD^{tree})" && + + git branch -D keep +' + +test_expect_success 'squashes a branch the current branch is not on' ' + git reset --hard three && main=$(git symbolic-ref --short HEAD) && - git checkout -b outside-parent && - test_commit --no-tag outside-parent outside x && + head_before=$(git rev-parse HEAD) && + git checkout -b off-history start && + test_commit --no-tag off-one off a && + test_commit --no-tag off-two off b && git checkout "$main" && - test_commit --no-tag outside-main file b && - base=$(git rev-parse HEAD) && - test_commit --no-tag outside-mid file c && - git merge --no-ff -m "merge outside-parent" outside-parent && - git branch -D outside-parent && - test_must_fail git history squash "$base.." 2>err && - test_grep "parent .* of commit .* is outside the revision range" err + git history squash --no-edit start..off-history && + + check_commit_count start..off-history 1 && + test_cmp_rev "$head_before" HEAD && + + git branch -D off-history +' + +test_expect_success 'squashes a range into a single commit without changing the tree' ' + git reset --hard three && + head_before=$(git rev-parse HEAD) && + tip_tree=$(git rev-parse HEAD^{tree}) && + + git history squash --no-edit --dry-run start.. >out && + predicted=$(awk "/^update refs\/heads\// {print \$3}" out) && + test_cmp_rev "$head_before" HEAD && + + git history squash --no-edit start.. && + + test "$predicted" = "$(git rev-parse HEAD)" && + check_commit_count start..HEAD 1 && + test_cmp_rev start HEAD^ && + test "$tip_tree" = "$(git rev-parse HEAD^{tree})" && + check_log_subjects -1 <<-\EOF && + one + EOF + git reflog >reflog && + test_grep "squash: updating" reflog +' + +test_expect_success 'sanitizes rev-list walk options, before and after --' ' + git reset --hard three && + tip_tree=$(git rev-parse HEAD^{tree}) && + + git history squash --no-edit --date-order start.. 2>err && + test_grep "ignoring rev-list options" err && + test_cmp_rev start HEAD^ && + test "$tip_tree" = "$(git rev-parse HEAD^{tree})" && + + git reset --hard three && + git history squash --no-edit -- --reverse start.. 2>err && + test_grep "ignoring rev-list options" err && + test_cmp_rev start HEAD^ && + test "$tip_tree" = "$(git rev-parse HEAD^{tree})" +' + +test_expect_success 'squashes an interior range and replays descendants verbatim' ' + git reset --hard three && + final_tree=$(git rev-parse HEAD^{tree}) && + + git history squash --no-edit start..@~1 && + + check_log_subjects start..HEAD <<-\EOF && + three + one + EOF + + test_cmp_rev start HEAD~2 && + test "$final_tree" = "$(git rev-parse HEAD^{tree})" +' + +test_expect_success 'squashes when the base is the root commit' ' + git reset --hard three && + root=$(git rev-list --max-parents=0 HEAD) && + tip_tree=$(git rev-parse HEAD^{tree}) && + + git history squash --no-edit "$root.." && + + check_commit_count "$root..HEAD" 1 && + test_cmp_rev "$root" HEAD^ && + test "$tip_tree" = "$(git rev-parse HEAD^{tree})" +' + +test_expect_success 'squashing a mix of fixups' ' + git reset --hard three && + echo fix >file && + git commit --fixup=two -a && + echo really fix >file && + git commit --fixup=one -a && + echo really really fix >file && + git commit --fixup=HEAD~1 -a && # fixup! two + echo really really really fix >file && + git commit --fixup=HEAD~1 -a && # fixup! one + + # squashing fixup! with a target that is not being squashed fails + test_must_fail git history squash one.. 2>err && + test_grep "^error: cannot squash .* (fixup! one): its target is not being squashed" err && + + # squashing fixup! into fixup! with a different target fails + test_must_fail git history squash HEAD~4.. 2>err && # HEAD~4 is fixup! two + test_grep "^error: cannot squash .* (fixup! one): its target is not being squashed" err && + + # squashing a sequence of fixup! commits into their targets + git history squash --no-edit start..HEAD~1 && + test_cmp_rev start HEAD~2 && + check_commit_author one HEAD~1 && + test_commit_message HEAD~1 -m one && + + # squashing "fixup! fixup! " into "" + git history squash --no-edit start.. && + test_cmp_rev start HEAD~1 && + check_commit_author one HEAD && + test_commit_message HEAD -m one +' + +test_expect_success 'squashing "squash!" messages' ' + git reset --hard two && + echo fix >file && + git commit --fixup=HEAD -a && + oldest=$(git rev-parse HEAD) && + echo better fix >file && + git commit -a -F - <<-EOF && + squash! $(git rev-parse two) + + Append this + EOF + + echo an even better fix >file && + git commit -a -F - <<-EOF && + squash! squash! two + + Append this as well + EOF + + # must edit when squashing "squash!" into its target + test_must_fail git history squash --no-edit two^.. 2>err && + test_grep "^error: squashing .* (squash! [a-f0-9]*) would discard its message" err && + + # squashing "squash!" into "fixup!" appends messages and changes + # subject prefix + git history squash --no-edit two.. && + test_cmp_rev HEAD^ two && + test_commit_message HEAD <<-\EOF && + squash! two + + Append this + + Append this as well + EOF + check_commit_author "$oldest" HEAD && + + git commit --allow-empty -F - <<-\EOF && + amend! two + + A new message + EOF + + # "amend!" does not replace "squash!" + test_must_fail git history squash --no-edit HEAD~2.. 2>err && + test_grep "^error: squashing .* (amend! two) would overwrite .squash!. message" err +' + +test_expect_success '--no-edit uses last "amend!" message without an editor' ' + git reset --hard three && + write_script editor <<-\EOF && + exit 1 + EOF + test_set_editor "$(pwd)/editor" && + echo fix >file && + git commit --author="Fix Me " --fixup=HEAD -a && + git commit --allow-empty -F - <<-EOF && + amend! $(git rev-parse --short HEAD) + + The first reword + + More detail + EOF + + git commit --allow-empty -F - <<-\EOF && + amend! three + + The second reword + + Extra detail + EOF + + test_commit WIP && + + cat >msg <<-EOF && + amend! $(git rev-parse --short HEAD^) + + The third reword + + Excruciating detail + EOF + + git commit --author="Someone Else " --allow-empty \ + -F msg && + + # squashing amend! updates the commit message + git history squash --no-edit three^.. && + sed -e 1,2d msg | test_commit_message HEAD && + check_commit_author three HEAD && + test_cmp_rev HEAD^ three^ && + + # squashing amend! into fixup! updates subject prefix + git reset --hard HEAD@{1} && + git history squash --no-edit three.. && + sed "1s/.*/amend! three/" msg | test_commit_message HEAD && + check_commit_author HEAD@{1}~4 HEAD && + test_cmp_rev HEAD^ three && + + # squashing amend! into amend! keeps original subject line + git reset --hard HEAD@{1} && + git history squash --no-edit HEAD~3.. && + sed "1s/.*/amend! three/" msg | test_commit_message HEAD && + test_cmp_rev HEAD~3 three && + + # all amend! messages must target the first commit + git reset --hard HEAD@{1} && + git commit --allow-empty -F - <<-\EOF && + amend! WIP + + The real message + EOF + + test_must_fail git history squash --no-edit HEAD~4.. 2>err && + test_grep "^error: cannot squash .* that does not target" err && + + # amend! message that targets commit that is not in range is rejected + test_must_fail git history squash --no-edit HEAD~3.. 2>err && + test_grep "^error: cannot squash .* target is not being squashed" err && + test_set_editor : ' -test_expect_success 'prints branches that cannot follow the squash' ' +test_expect_success 'squashing fixups into a merge' ' + test_when_finished \ + "git switch -f $GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME; \ + git branch -D feature" && + git checkout -f start && + test_commit F1 && + git checkout -b feature start && + test_commit F2 && + git merge F1 && + echo fixed >F1.t && + cat >msg <<-EOF && + amend! $(git rev-parse HEAD) + + merge F1 and F2 + + reworded + EOF + + git commit -a -F msg && + git history squash --no-edit HEAD^^! HEAD && + test_cmp_rev HEAD^1 F2 && + test_cmp_rev HEAD^2 F1 && + test_cmp_rev HEAD@{1}^{tree} HEAD^{tree} && + sed 1,2d msg | test_commit_message HEAD +' + +test_expect_success '--update-refs=head only moves HEAD' ' + git reset --hard three && + git branch -f other HEAD && + other_before=$(git rev-parse other) && + + git history squash --no-edit --update-refs=head start.. && + + check_commit_count start..HEAD 1 && + test_cmp_rev "$other_before" other +' + +test_expect_success 'refuses to fold a range a branch points into' ' test_when_finished \ "git switch -f $GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME; \ git branch -D feature" && @@ -85,20 +415,275 @@ test_expect_success 'prints branches that cannot follow the squash' ' test_grep "^error: the following branches cannot be rewritten" err && test_grep "^ topic-1$" err && test_grep "^ topic-2$" err && - test_grep "^hint: .* --update-refs=head" err + test_grep "^hint: .* --update-refs=head" err && + test_cmp_rev C6 HEAD && + + # squash succeeds with --update-refs=head + git history squash --no-edit --update-refs=head start.. && + test_cmp_rev start HEAD^ && + test_cmp_rev C6^{tree} HEAD^{tree} && + test_cmp_rev C6 HEAD@{1} ' test_expect_success 'advice.historyUpdateRefs silences the hint' ' git reset --hard three && git branch -f mid HEAD~1 && + head_before=$(git rev-parse HEAD) && test_must_fail git -c advice.historyUpdateRefs=false \ history squash start.. 2>err && test_grep "^error: the following branches cannot be rewritten" err && test_grep "^ mid$" err && test_grep ! "hint:" err && + test_cmp_rev "$head_before" HEAD && git branch -D mid ' +test_expect_success 'leaves tags and remote-tracking refs unchanged' ' + git reset --hard three && + git tag -f mark HEAD~1 && + git update-ref refs/remotes/origin/mark HEAD~1 && + mark_before=$(git rev-parse mark) && + + git history squash --no-edit start.. && + + test_cmp_rev "$mark_before" mark && + test_cmp_rev "$mark_before" refs/remotes/origin/mark && + + git tag -d mark && + git update-ref -d refs/remotes/origin/mark +' + +test_expect_success 'squashes a range whose internal merge has a single base' ' + git reset --hard start && + main=$(git symbolic-ref --short HEAD) && + test_commit --no-tag before-side file b && + git checkout -b inner-side && + test_commit --no-tag on-inner-side inner x && + git checkout "$main" && + test_commit --no-tag after-side file c && + git merge --no-ff -m merge inner-side && + git branch -D inner-side && + test_commit --no-tag after-merge file d && + tip_tree=$(git rev-parse HEAD^{tree}) && + + git history squash --no-edit start.. && + + check_commit_count start..HEAD 1 && + check_log_subjects -1 <<-\EOF && + before-side + EOF + test "$tip_tree" = "$(git rev-parse HEAD^{tree})" && + test_path_is_file inner +' + +test_expect_success 'folds a merge of a branch that forked at the base' ' + git reset --hard start && + main=$(git symbolic-ref --short HEAD) && + git checkout -b base-fork-side && + test_commit --no-tag base-fork-side side x && + git checkout "$main" && + test_commit --no-tag base-fork-main file b && + git merge --no-ff -m "merge base-fork-side" base-fork-side && + git branch -D base-fork-side && + test_commit --no-tag base-fork-tail file c && + tip_tree=$(git rev-parse HEAD^{tree}) && + + git history squash --no-edit start.. && + + check_commit_count start..HEAD 1 && + test_cmp_rev start HEAD^ && + test "$tip_tree" = "$(git rev-parse HEAD^{tree})" && + test_path_is_file side +' + +test_expect_success 'refuses a merge whose other parent is outside the range' ' + git reset --hard start && + main=$(git symbolic-ref --short HEAD) && + git checkout -b outside-parent && + test_commit --no-tag outside-parent outside x && + git checkout "$main" && + test_commit --no-tag outside-main file b && + base=$(git rev-parse HEAD) && + test_commit --no-tag outside-mid file c && + git merge --no-ff -m "merge outside-parent" outside-parent && + git branch -D outside-parent && + merged=$(git rev-parse HEAD) && + + test_must_fail git history squash "$base.." 2>err && + test_grep "parent .* of commit .* is outside the revision range" err && + test_cmp_rev "$merged" HEAD +' + +test_expect_success 'folds a range whose tip is a merge commit' ' + git reset --hard start && + main=$(git symbolic-ref --short HEAD) && + test_commit --no-tag tipmerge-base file b && + git checkout -b tipmerge-side && + test_commit --no-tag tipmerge-side side x && + git checkout "$main" && + test_commit --no-tag tipmerge-main file c && + git merge --no-ff -m "merge tipmerge-side" tipmerge-side && + git branch -D tipmerge-side && + tip_tree=$(git rev-parse HEAD^{tree}) && + + git history squash --no-edit start.. && + + check_commit_count start..HEAD 1 && + test "$tip_tree" = "$(git rev-parse HEAD^{tree})" && + test_path_is_file side +' + +test_expect_success 'folds a range whose base is a merge commit' ' + git reset --hard start && + main=$(git symbolic-ref --short HEAD) && + git checkout -b basemerge-side && + test_commit --no-tag basemerge-side side x && + git checkout "$main" && + test_commit --no-tag basemerge-main file b && + git merge --no-ff -m "merge basemerge-side" basemerge-side && + git branch -D basemerge-side && + base=$(git rev-parse HEAD) && + test_commit --no-tag basemerge-one file c && + test_commit --no-tag basemerge-two file d && + tip_tree=$(git rev-parse HEAD^{tree}) && + + git history squash --no-edit "$base.." && + + check_commit_count "$base..HEAD" 1 && + test_cmp_rev "$base" HEAD^ && + test "$tip_tree" = "$(git rev-parse HEAD^{tree})" +' + +test_expect_success 'folds a range with two interior merges' ' + git reset --hard start && + main=$(git symbolic-ref --short HEAD) && + test_commit --no-tag two-merge-a file a1 && + git checkout -b two-merge-s1 && + test_commit --no-tag two-merge-s1 s1 x && + git checkout "$main" && + git merge --no-ff -m "merge s1" two-merge-s1 && + test_commit --no-tag two-merge-b file b1 && + git checkout -b two-merge-s2 && + test_commit --no-tag two-merge-s2 s2 y && + git checkout "$main" && + git merge --no-ff -m "merge s2" two-merge-s2 && + git branch -D two-merge-s1 two-merge-s2 && + tip_tree=$(git rev-parse HEAD^{tree}) && + + git history squash --no-edit start.. && + + check_commit_count start..HEAD 1 && + test "$tip_tree" = "$(git rev-parse HEAD^{tree})" && + test_path_is_file s1 && + test_path_is_file s2 +' + +test_expect_success 'folds a range with a nested merge' ' + git reset --hard start && + main=$(git symbolic-ref --short HEAD) && + git checkout -b nested-outer && + test_commit --no-tag nested-outer outer x && + git checkout -b nested-inner && + test_commit --no-tag nested-inner inner y && + git checkout nested-outer && + git merge --no-ff -m "merge inner" nested-inner && + git checkout "$main" && + test_commit --no-tag nested-main file b1 && + git merge --no-ff -m "merge outer" nested-outer && + git branch -D nested-outer nested-inner && + tip_tree=$(git rev-parse HEAD^{tree}) && + + git history squash --no-edit start.. && + + check_commit_count start..HEAD 1 && + test "$tip_tree" = "$(git rev-parse HEAD^{tree})" && + test_path_is_file outer && + test_path_is_file inner +' + +test_expect_success 'folds a range with an octopus merge' ' + git reset --hard start && + main=$(git symbolic-ref --short HEAD) && + test_commit --no-tag octo-base file a1 && + git checkout -b octo-1 && + test_commit --no-tag octo-1 o1 x && + git checkout "$main" && + git checkout -b octo-2 && + test_commit --no-tag octo-2 o2 y && + git checkout "$main" && + git merge --no-ff -m octopus octo-1 octo-2 && + git branch -D octo-1 octo-2 && + tip_tree=$(git rev-parse HEAD^{tree}) && + + git history squash --no-edit start.. && + + check_commit_count start..HEAD 1 && + test "$tip_tree" = "$(git rev-parse HEAD^{tree})" && + test_path_is_file o1 && + test_path_is_file o2 +' + +test_expect_success 'refuses an octopus merge with an arm forked before the base' ' + git reset --hard start && + main=$(git symbolic-ref --short HEAD) && + git checkout -b octo-pre && + test_commit octo-pre-side pside x && + git checkout "$main" && + test_commit octo-pre-main file b1 && + octo_base=$(git rev-parse HEAD) && + git checkout -b octo-within && + test_commit --no-tag octo-within wside y && + git checkout "$main" && + git merge --no-ff -m octopus octo-pre octo-within && + merged=$(git rev-parse HEAD) && + git branch -D octo-pre octo-within && + + test_must_fail git history squash "$octo_base.." 2>err && + test_grep "parent .* of commit .* is outside the revision range" err && + test_cmp_rev "$merged" HEAD +' + +test_expect_success 'refuses when a descendant above the range is a merge' ' + git reset --hard start && + main=$(git symbolic-ref --short HEAD) && + test_commit --no-tag desc-one file b && + test_commit --no-tag desc-two file c && + git tag desc-tip && + git checkout -b desc-above && + test_commit --no-tag desc-above above x && + git checkout "$main" && + test_commit --no-tag desc-main file d && + git merge --no-ff -m "merge desc-above" desc-above && + git branch -D desc-above && + head_before=$(git rev-parse HEAD) && + + test_must_fail git history squash --no-edit start..desc-tip 2>err && + test_grep "merge commits is not supported" err && + test_cmp_rev "$head_before" HEAD +' + +test_expect_success 'refuses to fold a range a ref points into at a merge' ' + git reset --hard start && + main=$(git symbolic-ref --short HEAD) && + test_commit --no-tag refmerge-base file b && + git checkout -b refmerge-side && + test_commit --no-tag refmerge-side side x && + git checkout "$main" && + test_commit --no-tag refmerge-main file c && + git merge --no-ff -m "interior merge" refmerge-side && + git branch -D refmerge-side && + git branch at-merge HEAD && + test_commit --no-tag refmerge-tail file d && + head_before=$(git rev-parse HEAD) && + + test_must_fail git history squash start.. 2>err && + test_grep "^error: the following branches cannot be rewritten" err && + test_grep "^ at-merge$" err && + test_cmp_rev "$head_before" HEAD && + + git branch -D at-merge +' + test_done From 991dbca43e3c6110ac5e37b53efc972ef2429e4e Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Tue, 8 Sep 2026 20:41:59 +0000 Subject: [PATCH 008/155] history: support editing squashed commit messages Open the editor by default when squashing and provide --no-edit as the opt-out. Record the exact commits selected by the revision walk, rearrange that todo list with the sequencer's autosquash machinery, and build the message template from the resulting order. Match interactive rebase's treatment of marker messages: comment out fixup! messages, retain squash! bodies, and let amend! replace its target unless a preceding squash! requires both bodies. This keeps message editing aligned with the marker validation used by the no-edit path. Signed-off-by: Phillip Wood Helped-by: Phillip Wood Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- Documentation/git-history.adoc | 18 ++- builtin/history.c | 97 +++++++++++++++- t/t3455-history-squash.sh | 197 +++++++++++++++++++++++++++++++++ 3 files changed, 305 insertions(+), 7 deletions(-) diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc index fb04a6768513f0..2e2e31f521bee9 100644 --- a/Documentation/git-history.adoc +++ b/Documentation/git-history.adoc @@ -133,8 +133,9 @@ given, for example `HEAD~3..HEAD ^topic` to additionally exclude what is already on `topic`. Rev-list options may also be given, but any that would change how the range is walked are overridden with a warning. + -With `--no-edit`, the oldest commit's message is preserved, except that an -`amend!` commit targeting it replaces its message. +An editor opens pre-filled with the messages of all the folded commits so you +can combine them. With `--no-edit`, the oldest commit's message is preserved +instead, except that an `amend!` commit targeting it replaces its message. + The selected commits must form a connected graph with a single tip and must not include a root commit. Every parent of a commit after the oldest one must @@ -148,6 +149,13 @@ of markers for one target is combined into a single commit. With `--no-edit`, the last `amend!` message is used if there is one; a `squash!` or `amend!` is otherwise refused if folding it would discard its message. + +The editor template mirrors `git rebase -i --autosquash`: each `fixup!`, +`squash!`, or `amend!` is grouped under the commit it targets rather than +shown in commit order. A `fixup!` message is dropped (commented out in full), +a `squash!` keeps its body with only the marker subject commented, and an +`amend!` replaces its target's message, unless a `squash!` folded into that +target first, in which case it keeps its body like a `squash!`. ++ A local branch descended from a selected commit but not from the range tip cannot be rewritten as a descendant of the result, so with the default `--update-refs=branches` the command refuses. Rerun with `--update-refs=head` @@ -163,6 +171,12 @@ OPTIONS objects will be written into the repository, so applying these printed ref updates is generally safe. +`--edit`:: +`--no-edit`:: + For `squash`, open an editor to combine the messages of the folded commits. + This is the default; use `--no-edit` to keep the selected message without + opening an editor. + `--reedit-message`:: Open an editor to modify the target commit's message. diff --git a/builtin/history.c b/builtin/history.c index 8176f6794e8292..2d88bc43529fd8 100644 --- a/builtin/history.c +++ b/builtin/history.c @@ -1266,6 +1266,10 @@ static int squash_check_subject(struct repository *repo, return ret; } +static int build_squash_message(struct repository *repo, + const struct strbuf *todo_buf, + struct strbuf *out); + static int setup_squash_revisions(struct repository *repo, int argc, const char **argv, struct rev_info *revs) @@ -1319,6 +1323,7 @@ static int setup_squash_revisions(struct repository *repo, */ static int resolve_squash_range(struct repository *repo, bool update_branches, + bool edit_message, int argc, const char **argv, struct commit **oldest_out, struct commit **tip_out, @@ -1327,11 +1332,13 @@ static int resolve_squash_range(struct repository *repo, struct rev_info revs; struct subject_data subject_data = SUBJECT_DATA_INIT; struct commit *commit, *oldest = NULL, *tip = NULL; + struct strbuf todo_buf = STRBUF_INIT; int ret, tip_count = 0; bool walk_started = false; struct ref_filter filter = REF_FILTER_INIT; struct ref_array refs = { 0 }; + subject_data.edit_message = edit_message; ret = setup_squash_revisions(repo, argc, argv, &revs); if (ret < 0) goto out; @@ -1344,6 +1351,10 @@ static int resolve_squash_range(struct repository *repo, while ((commit = get_revision(&revs))) { struct commit_list *p; + if (edit_message) + strbuf_addf(&todo_buf, "pick %s\n", + oid_to_hex(&commit->object.oid)); + if (!commit->parents) { ret = error(_("cannot squash down to root commit")); goto out; @@ -1457,6 +1468,13 @@ static int resolve_squash_range(struct repository *repo, string_list_clear(&sorting_options, 0); goto out; } + if (edit_message) { + strbuf_reset(&subject_data.squash_message); + ret = build_squash_message(repo, &todo_buf, + &subject_data.squash_message); + if (ret < 0) + goto out; + } *oldest_out = oldest; *tip_out = tip; @@ -1464,6 +1482,7 @@ static int resolve_squash_range(struct repository *repo, ret = 0; out: + strbuf_release(&todo_buf); clear_object_flags(repo, SQUASH_SEEN | SQUASH_TIP | SQUASH_AMEND_TARGET); if (walk_started) @@ -1475,6 +1494,76 @@ static int resolve_squash_range(struct repository *repo, return ret; } +static bool amend_replaces_target(struct todo_list *todo, int target) +{ + for (int i = target + 1; i < todo->nr && + todo->items[i].command != TODO_PICK; i++) { + if (todo->items[i].command == TODO_SQUASH) + return false; + if (todo->items[i].flags & TODO_REPLACE_FIXUP_MSG) + return true; + } + return false; +} + +static int build_squash_message(struct repository *repo, + const struct strbuf *todo_buf, + struct strbuf *out) +{ + struct todo_list todo = TODO_LIST_INIT; + struct replay_opts opts = REPLAY_OPTS_INIT; + int nr_commits, ret; + + if (todo_list_parse_insn_buffer(repo, &opts, todo_buf->buf, &todo) < 0 || + todo_list_rearrange_squash(&todo) < 0) { + ret = error(_("could not prepare the squash message")); + goto out; + } + + nr_commits = todo.nr; + for (int i = 0; i < nr_commits; i++) { + struct todo_item *item = &todo.items[i]; + const char *message, *body; + size_t commented_len; + bool skip, squashing; + + squashing = item->command == TODO_SQUASH || + (item->flags & TODO_REPLACE_FIXUP_MSG); + if (item->command == TODO_PICK) + skip = amend_replaces_target(&todo, i); + else + skip = !squashing; + + message = repo_logmsg_reencode(repo, item->commit, NULL, NULL); + find_commit_subject(message, &body); + + if (skip) + commented_len = strlen(body); + else if (squashing) + commented_len = squash_subject_comment_len(body, 1); + else + commented_len = 0; + + if (!i) + add_squash_combination_header(out, nr_commits); + strbuf_addch(out, '\n'); + add_squash_message_header(out, i + 1, skip); + strbuf_addstr(out, "\n\n"); + strbuf_add_commented_lines(out, body, commented_len, comment_line_str); + strbuf_addstr(out, body + commented_len); + strbuf_complete_line(out); + + repo_unuse_commit_buffer(repo, item->commit, message); + } + + ret = 0; + +out: + todo_list_release(&todo); + replay_opts_release(&opts); + return ret; +} + static int cmd_history_squash(int argc, const char **argv, const char *prefix, @@ -1519,14 +1608,11 @@ static int cmd_history_squash(int argc, strbuf_join_argv(&reflog_msg, argc - 1, argv + 1, ' '); ret = resolve_squash_range(repo, action == REF_ACTION_BRANCHES, + edit, argc, argv, &oldest, &tip, &message_template); if (ret < 0) goto out; - if (edit) { - ret = error(_("message editing is not supported yet; use '--no-edit'")); - goto out; - } ret = setup_revwalk(repo, action, tip, &revs); if (ret < 0) @@ -1538,7 +1624,8 @@ static int cmd_history_squash(int argc, ret = commit_tree_ext(repo, "squash", oldest, message_template, oldest->parents, base_tree_oid, tip_tree_oid, - &rewritten, 0); + &rewritten, + edit ? COMMIT_TREE_EDIT_MESSAGE : 0); if (ret < 0) { ret = error(_("failed writing squashed commit")); goto out; diff --git a/t/t3455-history-squash.sh b/t/t3455-history-squash.sh index fb06637aafd0e1..d21e9d9fc468fd 100755 --- a/t/t3455-history-squash.sh +++ b/t/t3455-history-squash.sh @@ -381,6 +381,203 @@ test_expect_success 'squashing fixups into a merge' ' sed 1,2d msg | test_commit_message HEAD ' +test_expect_success 'edits every message and aborts on an empty result' ' + git reset --hard start && + stage_file b && + git commit -m "re-one subject" -m "re-one body line" && + test_commit --no-tag re-two file c && + test_commit re-three file d && + head_before=$(git rev-parse HEAD) && + + write_script empty-editor <<-\EOF && + >"$1" + EOF + test_set_editor "$(pwd)/empty-editor" && + test_must_fail git history squash start.. 2>err && + test_grep "Aborting commit due to empty commit message" err && + test_cmp_rev "$head_before" HEAD && + + write_script editor <<-\EOF && + cat "$1" >edited && + echo combined >"$1" + EOF + test_set_editor "$(pwd)/editor" && + git history squash start.. && + + cat >expect <<-EOF && + # This is a combination of 3 commits. + # This is the 1st commit message: + + re-one subject + + re-one body line + + # This is the commit message #2: + + re-two + + # This is the commit message #3: + + re-three + + # Please enter the commit message for the squash changes. Lines starting + # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit. + # Changes to be committed: + # modified: file + # + EOF + test_cmp expect edited && + check_log_subjects -1 <<-\EOF + combined + EOF +' + +test_expect_success 'handles fixup!, squash! and amend! like rebase' ' + git reset --hard start && + test_commit --no-tag mark-base file b && + stage_file c && + commit_with_message "fixup! mark-base\n\nfixup body\n" && + stage_file d && + commit_with_message "squash! mark-base\n\nsquash remark\n" && + stage_file e && + commit_with_message "amend! mark-base\n\namended message\n" && + + write_script editor <<-\EOF && + cat "$1" >edited + EOF + test_set_editor "$(pwd)/editor" && + git history squash start.. && + + cat >expect <<-EOF && + # This is a combination of 4 commits. + # This is the 1st commit message: + + mark-base + + # The commit message #2 will be skipped: + + # fixup! mark-base + # + # fixup body + + # This is the commit message #3: + + # squash! mark-base + + squash remark + + # This is the commit message #4: + + # amend! mark-base + + amended message + + # Please enter the commit message for the squash changes. Lines starting + # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit. + # Changes to be committed: + # modified: file + # + EOF + test_cmp expect edited && + check_log_messages -1 <<-\EOF + mark-base + + squash remark + + amended message + + EOF +' + +test_expect_success 'groups fixups under their targets in the editor' ' + git reset --hard start && + test_commit --no-tag alpha file a1 && + test_commit --no-tag beta file b1 && + stage_file a2 && + commit_with_message "fixup! alpha\n" && + stage_file b2 && + commit_with_message "fixup! beta\n" && + + write_script editor <<-\EOF && + cat "$1" >edited + EOF + test_set_editor "$(pwd)/editor" && + git history squash start.. && + + cat >expect <<-EOF && + # This is a combination of 4 commits. + # This is the 1st commit message: + + alpha + + # The commit message #2 will be skipped: + + # fixup! alpha + + # This is the commit message #3: + + beta + + # The commit message #4 will be skipped: + + # fixup! beta + + # Please enter the commit message for the squash changes. Lines starting + # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit. + # Changes to be committed: + # modified: file + # + EOF + test_cmp expect edited +' + +test_expect_success 'lets amend! replace its target message in the editor' ' + git reset --hard start && + test_commit --no-tag mark-base file b && + stage_file c && + commit_with_message "amend! mark-base\n\namended message\n" && + stage_file d && + commit_with_message "squash! mark-base\n\nsquash remark\n" && + + write_script editor <<-\EOF && + cat "$1" >edited + EOF + test_set_editor "$(pwd)/editor" && + git history squash start.. && + + cat >expect <<-EOF && + # This is a combination of 3 commits. + # The 1st commit message will be skipped: + + # mark-base + + # This is the commit message #2: + + # amend! mark-base + + amended message + + # This is the commit message #3: + + # squash! mark-base + + squash remark + + # Please enter the commit message for the squash changes. Lines starting + # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit. + # Changes to be committed: + # modified: file + # + EOF + test_cmp expect edited && + check_log_messages -1 <<-\EOF + amended message + + squash remark + + EOF +' + test_expect_success '--update-refs=head only moves HEAD' ' git reset --hard three && git branch -f other HEAD && From 59b551cba76ed49d297541059a08697db734a07c Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 9 Sep 2026 13:12:47 +0200 Subject: [PATCH 009/155] parse-options: allow for hidden aliases The `OPT_ALIAS()` option can be used to create an exact alias that maps one option name to the same semantics as another option name. This option type is especially useful when deprecating an old name in favor of a new one. But curiously enough, we don't have the infrastructure in place to properly support this use case because we don't expose the ability to hide the alias via `PARSE_OPT_HIDDEN`. Introduce a new `OPT_ALIAS_F()` function that allows the user to pass flags and propagate these flags when rewriting aliases to match their respective source options. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- parse-options.c | 4 +++- parse-options.h | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/parse-options.c b/parse-options.c index 4519ead9dc77b8..51a49792d14367 100644 --- a/parse-options.c +++ b/parse-options.c @@ -925,6 +925,7 @@ static struct option *preprocess_options(struct parse_opt_ctx_t *ctx, const char *long_name; const char *source; struct strbuf help = STRBUF_INIT; + enum parse_opt_option_flags flags; int j; if (newopt[i].type != OPTION_ALIAS) @@ -933,6 +934,7 @@ static struct option *preprocess_options(struct parse_opt_ctx_t *ctx, short_name = newopt[i].short_name; long_name = newopt[i].long_name; source = newopt[i].value; + flags = newopt[i].flags; if (!long_name) BUG("An alias must have long option name"); @@ -951,7 +953,7 @@ static struct option *preprocess_options(struct parse_opt_ctx_t *ctx, newopt[i].short_name = short_name; newopt[i].long_name = long_name; newopt[i].help = strbuf_detach(&help, NULL); - newopt[i].flags |= PARSE_OPT_FROM_ALIAS; + newopt[i].flags |= flags | PARSE_OPT_FROM_ALIAS; break; } diff --git a/parse-options.h b/parse-options.h index d7f896a9337024..a0b30f3c045692 100644 --- a/parse-options.h +++ b/parse-options.h @@ -386,13 +386,16 @@ static char *parse_options_noop_ignored_value MAYBE_UNUSED; .callback = parse_opt_noop_cb, \ } -#define OPT_ALIAS(s, l, source_long_name) { \ +#define OPT_ALIAS_F(s, l, source_long_name, f) { \ .type = OPTION_ALIAS, \ .short_name = (s), \ .long_name = (l), \ .value = (char *)(source_long_name), \ + .flags = (f), \ } +#define OPT_ALIAS(s, l, source_long_name) OPT_ALIAS_F(s, l, source_long_name, 0) + #define OPT_SUBCOMMAND_F(l, v, fn, f) { \ .type = OPTION_SUBCOMMAND, \ .long_name = (l), \ From 9e7865bd142528df950acb687b86695e9968ddb6 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 9 Sep 2026 13:12:48 +0200 Subject: [PATCH 010/155] builtin/init: rename "--ref-format=" to "--ref-storage-format=" Back when we gained support for reftables we of course introduced the ability to control the reference storage format that is used by newly created repositories. This infrastructure has grown over time, and unfortunately without consistency: - The command line parameter to specify the ref storage format is called "--ref-format=", while the corresponding repository extension is called "refStorage". - In most cases we refer to the "ref storage format" in our docs, so calling it "--ref-format=" is being inconsistent with them. - It is possible to override the ref storage format via an environment variable that is called "GIT_REFERENCE_BACKEND", which is not even remotely consistent with anything else. - There is also an "object format", but that format does not control how we store objects but rather whether we use SHA1 or SHA256. So in summary, it's a huge mess. This problem is about to become even worse though, as we're soon going to introduce an object storage extension. This extension is the equivalent to the ref storage extension, and of course we also want users to be able to control which object storage format new repositories are using. But we cannot properly name that parameter without creating even more inconsistencies: - "--object-format=" would match "--ref-format=", but that parameter name is already taken to specify the hash function. - "--object-storage=" would be a good fit, but be inconsistent with "--ref-format=". Asking the user to execute `git init --ref-format= --object-storage=` just feels extremely awkward. Instead, this and subsequent patches will fix the mess by consistently referring to the ref storage format as such throughout all options, environment variables and config settings. This new name much more closely indicates that it is about how we store data and finally brings consistency into this area. We will keep the old names working of course for the sake of backwards compatibility. Start with git-init(1). Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/config/init.adoc | 2 +- Documentation/git-init.adoc | 7 +++-- Documentation/git.adoc | 2 +- builtin/init-db.c | 15 ++++++----- t/perf/p1401-ref-store-tombstones.sh | 4 +-- t/perf/perf-lib.sh | 2 +- t/t0001-init.sh | 24 ++++++++--------- t/t0610-reftable-basics.sh | 14 +++++----- t/t0611-reftable-httpd.sh | 2 +- t/t1400-update-ref.sh | 2 +- t/t1423-ref-backend.sh | 6 ++--- t/t1460-refs-migrate.sh | 36 +++++++++++++------------- t/t1900-repo-info.sh | 6 ++--- t/t5510-fetch.sh | 8 +++--- t/t7424-submodule-mixed-ref-formats.sh | 2 +- 15 files changed, 68 insertions(+), 64 deletions(-) diff --git a/Documentation/config/init.adoc b/Documentation/config/init.adoc index 7b4abdaf8ba29b..a048f0bddcac0b 100644 --- a/Documentation/config/init.adoc +++ b/Documentation/config/init.adoc @@ -15,7 +15,7 @@ endif::[] this config. `init.defaultRefFormat`:: Allows overriding the default ref storage format for new repositories. - See `--ref-format=` in linkgit:git-init[1]. Both the command line + See `--ref-storage-format=` in linkgit:git-init[1]. Both the command line option and the `GIT_DEFAULT_REF_FORMAT` environment variable take precedence over this config. diff --git a/Documentation/git-init.adoc b/Documentation/git-init.adoc index bab99b9b477ba9..73e1f787cb2cad 100644 --- a/Documentation/git-init.adoc +++ b/Documentation/git-init.adoc @@ -11,7 +11,7 @@ SYNOPSIS [synopsis] git init [-q | --quiet] [--bare] [--template=] [--separate-git-dir ] [--object-format=] - [--ref-format=] + [--ref-storage-format=] [-b | --initial-branch=] [--shared[=]] [] @@ -57,11 +57,14 @@ values are `sha1` and (if enabled) `sha256`. `sha1` is the default. + include::object-format-disclaimer.adoc[] -`--ref-format=`:: +`--ref-storage-format=`:: Specify the given ref storage __ for the repository. The valid values are: + include::ref-storage-format.adoc[] +`--ref-format=`:: +Deprecated alias of `--ref-storage-format=`. + `--template=`:: Specify the directory from which templates will be used. (See the "TEMPLATE DIRECTORY" section below.) diff --git a/Documentation/git.adoc b/Documentation/git.adoc index 8a5cdd3b3d22c5..23ba65656e6965 100644 --- a/Documentation/git.adoc +++ b/Documentation/git.adoc @@ -593,7 +593,7 @@ double-quotes and respecting backslash escapes. E.g., the value `GIT_DEFAULT_REF_FORMAT`:: If this variable is set, the default reference backend format for new repositories will be set to this value. The default is "files". - See `--ref-format` in linkgit:git-init[1]. + See `--ref-storage-format` in linkgit:git-init[1]. `GIT_REFERENCE_BACKEND`:: Specify which reference backend to be used along with its URI. diff --git a/builtin/init-db.c b/builtin/init-db.c index e96b1283b7010d..1612413af0dc95 100644 --- a/builtin/init-db.c +++ b/builtin/init-db.c @@ -57,7 +57,7 @@ static int shared_callback(const struct option *opt, const char *arg, int unset) static const char *const init_db_usage[] = { N_("git init [-q | --quiet] [--bare] [--template=]\n" " [--separate-git-dir ] [--object-format=]\n" - " [--ref-format=]\n" + " [--ref-storage-format=]\n" " [-b | --initial-branch=]\n" " [--shared[=]] []"), NULL @@ -83,7 +83,7 @@ int cmd_init_db(int argc, unsigned int flags = 0; int bare = startup_info->force_bare_repository ? 1 : -1; const char *object_format = NULL; - const char *ref_format = NULL; + const char *ref_storage_format_str = NULL; const char *initial_branch = NULL; int hash_algo = GIT_HASH_UNKNOWN; enum ref_storage_format ref_storage_format = REF_STORAGE_FORMAT_UNKNOWN; @@ -109,8 +109,9 @@ int cmd_init_db(int argc, N_("override the name of the initial branch")), OPT_STRING(0, "object-format", &object_format, N_("hash"), N_("specify the hash algorithm to use")), - OPT_STRING(0, "ref-format", &ref_format, N_("format"), - N_("specify the reference format to use")), + OPT_STRING(0, "ref-storage-format", &ref_storage_format_str, N_("format"), + N_("specify the reference storage format to use")), + OPT_ALIAS_F(0, "ref-format", "ref-storage-format", PARSE_OPT_HIDDEN), OPT_END() }; int ret; @@ -173,10 +174,10 @@ int cmd_init_db(int argc, die(_("unknown hash algorithm '%s'"), object_format); } - if (ref_format) { - ref_storage_format = ref_storage_format_by_name(ref_format); + if (ref_storage_format_str) { + ref_storage_format = ref_storage_format_by_name(ref_storage_format_str); if (ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN) - die(_("unknown ref storage format '%s'"), ref_format); + die(_("unknown ref storage format '%s'"), ref_storage_format_str); } if (init_shared_repository != -1) diff --git a/t/perf/p1401-ref-store-tombstones.sh b/t/perf/p1401-ref-store-tombstones.sh index 9e3d8031aa1e04..37ffe0c3a7d31b 100755 --- a/t/perf/p1401-ref-store-tombstones.sh +++ b/t/perf/p1401-ref-store-tombstones.sh @@ -5,7 +5,7 @@ test_description="Tests performance of ref operations with many tombstones" . ./perf-lib.sh test_expect_success "setup" ' - git init --ref-format=reftable repo && + git init --ref-storage-format=reftable repo && blob=$(echo foo | git -C repo hash-object -w --stdin) && for i in $(test_seq 8000) do @@ -24,7 +24,7 @@ test_perf "recreate refs after mass delete" ' ' test_expect_success "setup asymmetric" ' - git init --ref-format=reftable repo2 && + git init --ref-storage-format=reftable repo2 && blob=$(echo foo | git -C repo2 hash-object -w --stdin) && for i in $(test_seq 8000) do diff --git a/t/perf/perf-lib.sh b/t/perf/perf-lib.sh index 2ac007888e806a..3ce49fd4236223 100644 --- a/t/perf/perf-lib.sh +++ b/t/perf/perf-lib.sh @@ -153,7 +153,7 @@ test_perf_create_repo_from () { ) && ( cd "$repo" && - "$MODERN_GIT" init -q --ref-format="$refformat" --object-format="$objectformat" && + "$MODERN_GIT" init -q --ref-storage-format="$refformat" --object-format="$objectformat" && test_perf_do_repo_symlink_config_ && mv .git/hooks .git/hooks-disabled 2>/dev/null && if test -f .git/index.lock diff --git a/t/t0001-init.sh b/t/t0001-init.sh index 5cf2e5a35aa1f7..df9a2ff2da0a0d 100755 --- a/t/t0001-init.sh +++ b/t/t0001-init.sh @@ -696,9 +696,9 @@ do test_cmp expect actual ' - test_expect_success "init with --ref-format=$format" ' + test_expect_success "init with --ref-storage-format=$format" ' test_when_finished "rm -rf refformat" && - git init --ref-format=$format refformat && + git init --ref-storage-format=$format refformat && echo $format >expect && git -C refformat rev-parse --show-ref-format >actual && test_cmp expect actual @@ -717,9 +717,9 @@ do test_cmp expect actual ' - test_expect_success "--ref-format=$format overrides GIT_DEFAULT_REF_FORMAT" ' + test_expect_success "--ref-storage-format=$format overrides GIT_DEFAULT_REF_FORMAT" ' test_when_finished "rm -rf refformat" && - GIT_DEFAULT_REF_FORMAT=garbage git init --ref-format=$format refformat && + GIT_DEFAULT_REF_FORMAT=garbage git init --ref-storage-format=$format refformat && echo $format >expect && git -C refformat rev-parse --show-ref-format >actual && test_cmp expect actual @@ -735,9 +735,9 @@ do ' done -test_expect_success "--ref-format= overrides GIT_DEFAULT_REF_FORMAT" ' +test_expect_success "--ref-storage-format= overrides GIT_DEFAULT_REF_FORMAT" ' test_when_finished "rm -rf refformat" && - GIT_DEFAULT_REF_FORMAT=files git init --ref-format=reftable refformat && + GIT_DEFAULT_REF_FORMAT=files git init --ref-storage-format=reftable refformat && echo reftable >expect && git -C refformat rev-parse --show-ref-format >actual && test_cmp expect actual @@ -791,8 +791,8 @@ for from_format in $backends do test_expect_success "re-init with same format ($from_format)" ' test_when_finished "rm -rf refformat" && - git init --ref-format=$from_format refformat && - git init --ref-format=$from_format refformat && + git init --ref-storage-format=$from_format refformat && + git init --ref-storage-format=$from_format refformat && echo $from_format >expect && git -C refformat rev-parse --show-ref-format >actual && test_cmp expect actual @@ -807,11 +807,11 @@ do test_expect_success "re-init with different format fails ($from_format -> $to_format)" ' test_when_finished "rm -rf refformat" && - git init --ref-format=$from_format refformat && + git init --ref-storage-format=$from_format refformat && cat >expect <<-EOF && fatal: attempt to reinitialize repository with different reference storage format EOF - test_must_fail git init --ref-format=$to_format refformat 2>err && + test_must_fail git init --ref-storage-format=$to_format refformat 2>err && test_cmp expect err && echo $from_format >expect && git -C refformat rev-parse --show-ref-format >actual && @@ -820,12 +820,12 @@ do done done -test_expect_success 'init with --ref-format=garbage' ' +test_expect_success 'init with --ref-storage-format=garbage' ' test_when_finished "rm -rf refformat" && cat >expect <<-EOF && fatal: unknown ref storage format ${SQ}garbage${SQ} EOF - test_must_fail git init --ref-format=garbage refformat 2>err && + test_must_fail git init --ref-storage-format=garbage refformat 2>err && test_cmp expect err ' diff --git a/t/t0610-reftable-basics.sh b/t/t0610-reftable-basics.sh index 35e98b43dba035..1cf96ce2c50f72 100755 --- a/t/t0610-reftable-basics.sh +++ b/t/t0610-reftable-basics.sh @@ -59,28 +59,28 @@ test_expect_success 'init: reinitializing reftable backend succeeds' ' test_commit -C repo A && git -C repo for-each-ref >expect && - git init --ref-format=reftable repo && + git init --ref-storage-format=reftable repo && git -C repo for-each-ref >actual && test_cmp expect actual ' test_expect_success 'init: reinitializing files with reftable backend fails' ' test_when_finished "rm -rf repo" && - git init --ref-format=files repo && + git init --ref-storage-format=files repo && test_commit -C repo file && cp repo/.git/HEAD expect && - test_must_fail git init --ref-format=reftable repo && + test_must_fail git init --ref-storage-format=reftable repo && test_cmp expect repo/.git/HEAD ' test_expect_success 'init: reinitializing reftable with files backend fails' ' test_when_finished "rm -rf repo" && - git init --ref-format=reftable repo && + git init --ref-storage-format=reftable repo && test_commit -C repo file && cp repo/.git/HEAD expect && - test_must_fail git init --ref-format=files repo && + test_must_fail git init --ref-storage-format=files repo && test_cmp expect repo/.git/HEAD ' @@ -163,7 +163,7 @@ test_expect_success 'clone: can clone reftable repository' ' test_expect_success 'clone: can clone reffiles into reftable repository' ' test_when_finished "rm -rf reffiles reftable" && - git init --ref-format=files reffiles && + git init --ref-storage-format=files reffiles && test_commit -C reffiles A && git clone --ref-format=reftable ./reffiles reftable && @@ -182,7 +182,7 @@ test_expect_success 'clone: can clone reffiles into reftable repository' ' test_expect_success 'clone: can clone reftable into reffiles repository' ' test_when_finished "rm -rf reffiles reftable" && - git init --ref-format=reftable reftable && + git init --ref-storage-format=reftable reftable && test_commit -C reftable A && git clone --ref-format=files ./reftable reffiles && diff --git a/t/t0611-reftable-httpd.sh b/t/t0611-reftable-httpd.sh index 5e05b9c1f2ac73..b030814b9fe972 100755 --- a/t/t0611-reftable-httpd.sh +++ b/t/t0611-reftable-httpd.sh @@ -10,7 +10,7 @@ start_httpd REPO="$HTTPD_DOCUMENT_ROOT_PATH/repo" test_expect_success 'serving ls-remote' ' - git init --ref-format=reftable -b main "$REPO" && + git init --ref-storage-format=reftable -b main "$REPO" && cd "$REPO" && test_commit m1 && >.git/git-daemon-export-ok && diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh index 269fdaa3ed71aa..1a164c96f96ca2 100755 --- a/t/t1400-update-ref.sh +++ b/t/t1400-update-ref.sh @@ -2361,7 +2361,7 @@ do ' test_expect_success CASE_INSENSITIVE_FS "stdin $type batch-updates existing reference" ' - git init --ref-format=reftable repo && + git init --ref-storage-format=reftable repo && test_when_finished "rm -fr repo" && ( cd repo && diff --git a/t/t1423-ref-backend.sh b/t/t1423-ref-backend.sh index fd47d77e8e2904..9ae295cf3dec68 100755 --- a/t/t1423-ref-backend.sh +++ b/t/t1423-ref-backend.sh @@ -137,7 +137,7 @@ do test_expect_success "$method: read from $to_format backend, $dir dir" ' test_when_finished "rm -rf repo" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && ( cd repo && test_commit 1 && @@ -152,7 +152,7 @@ do test_expect_success "$method: write to $to_format backend, $dir dir" ' test_when_finished "rm -rf repo" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && ( cd repo && test_commit 1 && @@ -179,7 +179,7 @@ do test_expect_success "$method: with worktree and $to_format backend, $dir dir" ' test_when_finished "rm -rf repo wt" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && ( cd repo && test_commit 1 && diff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh index 8f426971435309..bb2507f571f256 100755 --- a/t/t1460-refs-migrate.sh +++ b/t/t1460-refs-migrate.sh @@ -105,7 +105,7 @@ do test_expect_success "$from_format: migration to same format fails" ' test_when_finished "rm -rf repo" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && test_must_fail git -C repo refs migrate \ --ref-format=$from_format 2>err && cat >expect <<-EOF && @@ -116,7 +116,7 @@ do test_expect_success "$from_format -> $to_format: migration with worktree fails" ' test_when_finished "rm -rf repo" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && git -C repo worktree add wt && test_must_fail git -C repo refs migrate \ --ref-format=$to_format 2>err && @@ -128,20 +128,20 @@ do test_expect_success "$from_format -> $to_format: unborn HEAD" ' test_when_finished "rm -rf repo" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && test_migration repo "$to_format" ' test_expect_success "$from_format -> $to_format: single ref" ' test_when_finished "rm -rf repo" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && test_commit -C repo initial && test_migration repo "$to_format" ' test_expect_success "$from_format -> $to_format: bare repository" ' test_when_finished "rm -rf repo repo.git" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && test_commit -C repo initial && git clone --ref-format=$from_format --mirror repo repo.git && test_migration repo.git "$to_format" @@ -149,7 +149,7 @@ do test_expect_success "$from_format -> $to_format: dangling symref" ' test_when_finished "rm -rf repo" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && test_commit -C repo initial && git -C repo symbolic-ref BROKEN_HEAD refs/heads/nonexistent && test_migration repo "$to_format" && @@ -160,7 +160,7 @@ do test_expect_success "$from_format -> $to_format: broken ref" ' test_when_finished "rm -rf repo" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && test_commit -C repo initial && test-tool -C repo ref-store main update-ref "" refs/heads/broken \ "$(test_oid 001)" "$ZERO_OID" REF_SKIP_CREATE_REFLOG,REF_SKIP_OID_VERIFICATION && @@ -172,7 +172,7 @@ do test_expect_success "$from_format -> $to_format: pseudo-refs" ' test_when_finished "rm -rf repo" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && test_commit -C repo initial && git -C repo update-ref FOO_HEAD HEAD && test_migration repo "$to_format" @@ -180,7 +180,7 @@ do test_expect_success "$from_format -> $to_format: special refs are left alone" ' test_when_finished "rm -rf repo" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && test_commit -C repo initial && git -C repo rev-parse HEAD >repo/.git/MERGE_HEAD && git -C repo rev-parse MERGE_HEAD && @@ -190,7 +190,7 @@ do test_expect_success "$from_format -> $to_format: a bunch of refs" ' test_when_finished "rm -rf repo" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && test_commit -C repo initial && cat >input <<-EOF && @@ -208,7 +208,7 @@ do test_expect_success "$from_format -> $to_format: dry-run migration does not modify repository" ' test_when_finished "rm -rf repo" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && test_commit -C repo initial && git -C repo refs migrate --dry-run \ --ref-format=$to_format >output && @@ -221,7 +221,7 @@ do test_expect_success "$from_format -> $to_format: reflogs of symrefs with target deleted" ' test_when_finished "rm -rf repo" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && test_commit -C repo initial && git -C repo branch branch-1 HEAD && git -C repo symbolic-ref refs/heads/symref refs/heads/branch-1 && @@ -234,7 +234,7 @@ do test_expect_success "$from_format -> $to_format: reflogs order is retained" ' test_when_finished "rm -rf repo" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && test_commit --date "100005000 +0700" --no-tag -C repo initial && test_commit --date "100003000 +0700" --no-tag -C repo second && test_migration repo "$to_format" @@ -242,7 +242,7 @@ do test_expect_success "$from_format -> $to_format: stash is retained" ' test_when_finished "rm -rf repo" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && ( cd repo && test_commit initial A && @@ -259,7 +259,7 @@ do test_expect_success "$from_format -> $to_format: skip reflog with --skip-reflog" ' test_when_finished "rm -rf repo" && - git init --ref-format=$from_format repo && + git init --ref-storage-format=$from_format repo && test_commit -C repo initial && # we see that the repository contains reflogs. git -C repo reflog --all >reflogs && @@ -274,7 +274,7 @@ done test_expect_success 'multiple reftable blocks with multiple entries' ' test_when_finished "rm -rf repo" && - git init --ref-format=files repo && + git init --ref-storage-format=files repo && test_commit -C repo first && test_seq -f "create refs/heads/ref-%d HEAD" 5000 | git -C repo update-ref --stdin && @@ -286,7 +286,7 @@ test_expect_success 'multiple reftable blocks with multiple entries' ' test_expect_success 'migrating from files format deletes backend files' ' test_when_finished "rm -rf repo" && - git init --ref-format=files repo && + git init --ref-storage-format=files repo && test_commit -C repo first && git -C repo pack-refs --all && test_commit -C repo second && @@ -313,7 +313,7 @@ test_expect_success 'migrating from files format deletes backend files' ' test_expect_success 'migrating from reftable format deletes backend files' ' test_when_finished "rm -rf repo" && - git init --ref-format=reftable repo && + git init --ref-storage-format=reftable repo && test_commit -C repo first && test_path_is_dir repo/.git/reftable && diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh index c85d390f438e80..d115d2d9f98e64 100755 --- a/t/t1900-repo-info.sh +++ b/t/t1900-repo-info.sh @@ -39,10 +39,10 @@ test_repo_info () { } test_repo_info 'ref format files is retrieved correctly' \ - 'git init --ref-format=files' 'format-files' 'references.format' 'files' + 'git init --ref-storage-format=files' 'format-files' 'references.format' 'files' test_repo_info 'ref format reftable is retrieved correctly' \ - 'git init --ref-format=reftable' 'format-reftable' 'references.format' 'reftable' + 'git init --ref-storage-format=reftable' 'format-reftable' 'references.format' 'reftable' test_repo_info 'bare repository = false is retrieved correctly' \ 'git init' 'nonbare' 'layout.bare' 'false' @@ -75,7 +75,7 @@ test_expect_success 'values returned in order requested' ' references.format=files layout.bare=false EOF - git init --ref-format=files ordered && + git init --ref-storage-format=files ordered && git -C ordered repo info layout.bare references.format layout.bare >actual && test_cmp expect actual ' diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index a8d38d9176fec1..359c3cf99b09b4 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -1803,7 +1803,7 @@ test_expect_success CASE_INSENSITIVE_FS,REFFILES 'existing references in a case test_expect_success REFFILES 'existing reference lock in repo' ' test_when_finished rm -rf base repo && ( - git init --ref-format=reftable base && + git init --ref-storage-format=reftable base && cd base && echo >file update && git add . && @@ -1814,7 +1814,7 @@ test_expect_success REFFILES 'existing reference lock in repo' ' git update-ref refs/heads/branch @ && cd .. && - git init --ref-format=files --bare repo && + git init --ref-storage-format=files --bare repo && cd repo && git remote add origin ../base && touch refs/heads/foo.lock && @@ -1857,7 +1857,7 @@ test_expect_success CASE_INSENSITIVE_FS,REFFILES 'D/F conflict on case insensiti test_expect_success REFFILES 'D/F conflict on case sensitive filesystem with lock' ' test_when_finished rm -rf base repo && ( - git init --ref-format=reftable base && + git init --ref-storage-format=reftable base && cd base && echo >file update && git add . && @@ -1868,7 +1868,7 @@ test_expect_success REFFILES 'D/F conflict on case sensitive filesystem with loc git update-ref refs/heads/branch @ && cd .. && - git init --ref-format=files --bare repo && + git init --ref-storage-format=files --bare repo && cd repo && git remote add origin ../base && mkdir refs/heads/foo && diff --git a/t/t7424-submodule-mixed-ref-formats.sh b/t/t7424-submodule-mixed-ref-formats.sh index 559713b60773a4..1ca245c732bcaa 100755 --- a/t/t7424-submodule-mixed-ref-formats.sh +++ b/t/t7424-submodule-mixed-ref-formats.sh @@ -31,7 +31,7 @@ test_expect_success 'add existing repository with different ref storage format' ( cd parent && test_commit parent && - git init --ref-format=$OTHER_FORMAT submodule && + git init --ref-storage-format=$OTHER_FORMAT submodule && test_commit -C submodule submodule && git submodule add ./submodule ) From 9bba3ad909375ff200eba25ac89429b83df1fc5f Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 9 Sep 2026 13:12:49 +0200 Subject: [PATCH 011/155] builtin/clone: rename "--ref-format=" to "--ref-storage-format=" With the same reasoning as for git-init(1), rename "--ref-format=" to "--ref-storage-format=" and keep the old name as an alias. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/git-clone.adoc | 5 ++++- builtin/clone.c | 13 +++++++------ t/t0610-reftable-basics.sh | 4 ++-- t/t1460-refs-migrate.sh | 2 +- t/t5510-fetch.sh | 6 +++--- t/t5601-clone.sh | 4 ++-- t/t7424-submodule-mixed-ref-formats.sh | 4 ++-- 7 files changed, 21 insertions(+), 17 deletions(-) diff --git a/Documentation/git-clone.adoc b/Documentation/git-clone.adoc index b6e1f8ada22813..27e4d1394217a1 100644 --- a/Documentation/git-clone.adoc +++ b/Documentation/git-clone.adoc @@ -348,12 +348,15 @@ or `--mirror` is given) The result is Git repository can be separated from working tree. -`--ref-format=`:: +`--ref-storage-format=`:: Specify the given ref storage format for the repository. The valid values are: + include::ref-storage-format.adoc[] +`--ref-format=`:: +Deprecated alias of `--ref-storage-format=`. + `-j`:: `--jobs=`:: The number of submodules fetched at the same time. diff --git a/builtin/clone.c b/builtin/clone.c index 5b25cca5102956..42cae9e0f6e851 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -901,7 +901,7 @@ int cmd_clone(int argc, char *option_origin = NULL; struct string_list option_not = STRING_LIST_INIT_NODUP; const char *real_git_dir = NULL; - const char *ref_format = NULL; + const char *ref_storage_format_str = NULL; const char *option_upload_pack = "git-upload-pack"; int option_progress = -1; int option_sparse_checkout = 0; @@ -981,8 +981,9 @@ int cmd_clone(int argc, N_("any cloned submodules will be shallow")), OPT_STRING(0, "separate-git-dir", &real_git_dir, N_("gitdir"), N_("separate git dir from working tree")), - OPT_STRING(0, "ref-format", &ref_format, N_("format"), - N_("specify the reference format to use")), + OPT_STRING(0, "ref-storage-format", &ref_storage_format_str, N_("format"), + N_("specify the reference storage format to use")), + OPT_ALIAS_F(0, "ref-format", "ref-storage-format", PARSE_OPT_HIDDEN), OPT_STRING_LIST('c', "config", &option_config, N_("key=value"), N_("set config inside the new repository")), OPT_STRING_LIST(0, "server-option", &server_options, @@ -1027,10 +1028,10 @@ int cmd_clone(int argc, if (option_single_branch == -1) option_single_branch = deepen ? 1 : 0; - if (ref_format) { - ref_storage_format = ref_storage_format_by_name(ref_format); + if (ref_storage_format_str) { + ref_storage_format = ref_storage_format_by_name(ref_storage_format_str); if (ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN) - die(_("unknown ref storage format '%s'"), ref_format); + die(_("unknown ref storage format '%s'"), ref_storage_format_str); } if (option_mirror) { diff --git a/t/t0610-reftable-basics.sh b/t/t0610-reftable-basics.sh index 1cf96ce2c50f72..d325f17a143c03 100755 --- a/t/t0610-reftable-basics.sh +++ b/t/t0610-reftable-basics.sh @@ -165,7 +165,7 @@ test_expect_success 'clone: can clone reffiles into reftable repository' ' test_when_finished "rm -rf reffiles reftable" && git init --ref-storage-format=files reffiles && test_commit -C reffiles A && - git clone --ref-format=reftable ./reffiles reftable && + git clone --ref-storage-format=reftable ./reffiles reftable && git -C reffiles rev-parse HEAD >expect && git -C reftable rev-parse HEAD >actual && @@ -184,7 +184,7 @@ test_expect_success 'clone: can clone reftable into reffiles repository' ' test_when_finished "rm -rf reffiles reftable" && git init --ref-storage-format=reftable reftable && test_commit -C reftable A && - git clone --ref-format=files ./reftable reffiles && + git clone --ref-storage-format=files ./reftable reffiles && git -C reftable rev-parse HEAD >expect && git -C reffiles rev-parse HEAD >actual && diff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh index bb2507f571f256..8aded6597efe2f 100755 --- a/t/t1460-refs-migrate.sh +++ b/t/t1460-refs-migrate.sh @@ -143,7 +143,7 @@ do test_when_finished "rm -rf repo repo.git" && git init --ref-storage-format=$from_format repo && test_commit -C repo initial && - git clone --ref-format=$from_format --mirror repo repo.git && + git clone --ref-storage-format=$from_format --mirror repo repo.git && test_migration repo.git "$to_format" ' diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index 359c3cf99b09b4..300bd5396df95e 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -46,19 +46,19 @@ test_expect_success "clone and setup child repos" ' ) && git clone . bundle && git clone . seven && - git clone --ref-format=reftable . case_sensitive && + git clone --ref-storage-format=reftable . case_sensitive && ( cd case_sensitive && git branch branch1 && git branch bRanch1 ) && - git clone --ref-format=reftable . case_sensitive_fd && + git clone --ref-storage-format=reftable . case_sensitive_fd && ( cd case_sensitive_fd && git branch foo/bar && git branch Foo ) && - git clone --ref-format=reftable . case_sensitive_df && + git clone --ref-storage-format=reftable . case_sensitive_df && ( cd case_sensitive_df && git branch Foo/bar && diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh index b6167582a185c0..202d86bc839c64 100755 --- a/t/t5601-clone.sh +++ b/t/t5601-clone.sh @@ -166,7 +166,7 @@ test_expect_success 'clone --mirror does not repeat tags' ' test_expect_success 'clone with files ref format' ' test_when_finished "rm -rf ref-storage" && - git clone --ref-format=files --mirror src ref-storage && + git clone --ref-storage-format=files --mirror src ref-storage && echo files >expect && git -C ref-storage rev-parse --show-ref-format >actual && test_cmp expect actual @@ -176,7 +176,7 @@ test_expect_success 'clone with garbage ref format' ' cat >expect <<-EOF && fatal: unknown ref storage format ${SQ}garbage${SQ} EOF - test_must_fail git clone --ref-format=garbage --mirror src ref-storage 2>err && + test_must_fail git clone --ref-storage-format=garbage --mirror src ref-storage 2>err && test_cmp expect err && test_path_is_missing ref-storage ' diff --git a/t/t7424-submodule-mixed-ref-formats.sh b/t/t7424-submodule-mixed-ref-formats.sh index 1ca245c732bcaa..5f31d233e9a84d 100755 --- a/t/t7424-submodule-mixed-ref-formats.sh +++ b/t/t7424-submodule-mixed-ref-formats.sh @@ -63,9 +63,9 @@ test_expect_success 'recursive clone propagates ref storage format' ' test_ref_format upstream/submodule "$GIT_DEFAULT_REF_FORMAT" && # The cloned repositories should use the other ref format that we have - # specified via `--ref-format`. The option should propagate to cloned + # specified via `--ref-storage-format`. The option should propagate to cloned # submodules. - git clone --ref-format=$OTHER_FORMAT --recurse-submodules \ + git clone --ref-storage-format=$OTHER_FORMAT --recurse-submodules \ upstream downstream && test_ref_format downstream "$OTHER_FORMAT" && test_ref_format downstream/submodule "$OTHER_FORMAT" From 82fbd1c537d32b7aa0b4f24205d7404e80e20737 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 9 Sep 2026 13:12:50 +0200 Subject: [PATCH 012/155] builtin/refs: rename "--ref-format=" to "--ref-storage-format=" With the same reasoning as for git-init(1), rename "--ref-format=" to "--ref-storage-format=" and keep the old name as an alias. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/git-refs.adoc | 9 ++++++--- builtin/fetch.c | 2 +- builtin/refs.c | 9 +++++---- t/t1423-ref-backend.sh | 8 ++++---- t/t1460-refs-migrate.sh | 12 ++++++------ t/t7424-submodule-mixed-ref-formats.sh | 2 +- 6 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc index 9063892651e478..ae722c60e681e9 100644 --- a/Documentation/git-refs.adoc +++ b/Documentation/git-refs.adoc @@ -9,7 +9,7 @@ git-refs - Low-level access to refs SYNOPSIS -------- [synopsis] -git refs migrate --ref-format= [--no-reflog] [--dry-run] +git refs migrate --ref-storage-format= [--no-reflog] [--dry-run] git refs verify [--strict] [--verbose] git refs list [--count=] [--shell|--perl|--python|--tcl] [(--sort=)...] [--format=] @@ -97,11 +97,14 @@ OPTIONS The following options are specific to `git refs migrate`: -`--ref-format=`:: - The ref format to migrate the ref store to. Can be one of: +`--ref-storage-format=`:: + The ref storage format to migrate the ref store to. Can be one of: + include::ref-storage-format.adoc[] +`--ref-format=`:: +Deprecated alias of `--ref-storage-format=`. + `--dry-run`:: Perform the migration, but do not modify the repository. The migrated refs will be written into a separate directory that can be inspected diff --git a/builtin/fetch.c b/builtin/fetch.c index ab7db2be06d14c..687909c0c4cd5b 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -1848,7 +1848,7 @@ static void ref_transaction_rejection_handler(const char *refname, "can either accept this as-is, in which case you won't be able to\n" "store all remote references on disk. Or you can alternatively\n" "migrate your repository to use the 'reftable' backend with the\n" - "following command:\n\n git refs migrate --ref-format=reftable\n\n" + "following command:\n\n git refs migrate --ref-storage-format=reftable\n\n" "Please keep in mind that not all implementations of Git support this\n" "new format yet. So if you use tools other than Git to access this\n" "repository it may not be an option to migrate to reftables.\n")); diff --git a/builtin/refs.c b/builtin/refs.c index 5cd21c25fe53dc..31aafb99745a7c 100644 --- a/builtin/refs.c +++ b/builtin/refs.c @@ -10,7 +10,7 @@ #include "refs/refs-internal.h" #define REFS_MIGRATE_USAGE \ - N_("git refs migrate --ref-format= [--no-reflog] [--dry-run]") + N_("git refs migrate --ref-storage-format= [--no-reflog] [--dry-run]") #define REFS_VERIFY_USAGE \ N_("git refs verify [--strict] [--verbose]") @@ -44,9 +44,10 @@ static int cmd_refs_migrate(int argc, const char **argv, const char *prefix, enum ref_storage_format format; unsigned int flags = 0; struct option options[] = { - OPT_STRING_F(0, "ref-format", &format_str, N_("format"), - N_("specify the reference format to convert to"), + OPT_STRING_F(0, "ref-storage-format", &format_str, N_("format"), + N_("specify the reference storage format to convert to"), PARSE_OPT_NONEG), + OPT_ALIAS_F(0, "ref-format", "ref-storage-format", PARSE_OPT_HIDDEN), OPT_BIT(0, "dry-run", &flags, N_("perform a non-destructive dry-run"), REPO_MIGRATE_REF_STORAGE_FORMAT_DRYRUN), @@ -62,7 +63,7 @@ static int cmd_refs_migrate(int argc, const char **argv, const char *prefix, if (argc) usage(_("too many arguments")); if (!format_str) - usage(_("missing --ref-format=")); + usage(_("missing --ref-storage-format=")); format = ref_storage_format_by_name(format_str); if (format == REF_STORAGE_FORMAT_UNKNOWN) { diff --git a/t/t1423-ref-backend.sh b/t/t1423-ref-backend.sh index 9ae295cf3dec68..ab119a6568bd98 100755 --- a/t/t1423-ref-backend.sh +++ b/t/t1423-ref-backend.sh @@ -144,7 +144,7 @@ do test_commit 2 && test_commit 3 && - git refs migrate --dry-run --ref-format=$to_format >out && + git refs migrate --dry-run --ref-storage-format=$to_format >out && BACKEND_PATH="$dir/$(sed "s/.* ${SQ}.git\/\(.*\)${SQ}/\1/" out)" && test_refs_backend . $from_format "$to_format://$BACKEND_PATH" "$method" ) @@ -159,7 +159,7 @@ do test_commit 2 && test_commit 3 && - git refs migrate --dry-run --ref-format=$to_format >out && + git refs migrate --dry-run --ref-storage-format=$to_format >out && BACKEND_PATH="$dir/$(sed "s/.* ${SQ}.git\/\(.*\)${SQ}/\1/" out)" && test_refs_backend . $from_format "$to_format://$BACKEND_PATH" "$method" && @@ -186,7 +186,7 @@ do test_commit 2 && test_commit 3 && - git refs migrate --dry-run --ref-format=$to_format >out && + git refs migrate --dry-run --ref-storage-format=$to_format >out && BACKEND_PATH="$dir/$(sed "s/.* ${SQ}.git\/\(.*\)${SQ}/\1/" out)" && run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \ @@ -218,7 +218,7 @@ do test_commit 2 && test_commit 3 && - git refs migrate --ref-format=$to_format && + git refs migrate --ref-storage-format=$to_format && git refs list >out && test_grep "refs/tags/1" out && test_grep "refs/tags/2" out && diff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh index 8aded6597efe2f..204dd79b412da9 100755 --- a/t/t1460-refs-migrate.sh +++ b/t/t1460-refs-migrate.sh @@ -42,7 +42,7 @@ test_migration () { print_all_reflog_entries "$repo" >expect_logs fi && - git -C "$repo" refs migrate --ref-format="$format" "$@" && + git -C "$repo" refs migrate --ref-storage-format="$format" "$@" && git -C "$repo" for-each-ref --include-root-refs \ --format='%(refname) %(objectname) %(symref)' >actual && @@ -77,7 +77,7 @@ test_expect_success "missing ref storage format" ' git init repo && test_must_fail git -C repo refs migrate 2>err && cat >expect <<-EOF && - usage: missing --ref-format= + usage: missing --ref-storage-format= EOF test_cmp expect err ' @@ -86,7 +86,7 @@ test_expect_success "unknown ref storage format" ' test_when_finished "rm -rf repo" && git init repo && test_must_fail git -C repo refs migrate \ - --ref-format=unknown 2>err && + --ref-storage-format=unknown 2>err && cat >expect <<-EOF && error: unknown ref storage format ${SQ}unknown${SQ} EOF @@ -107,7 +107,7 @@ do test_when_finished "rm -rf repo" && git init --ref-storage-format=$from_format repo && test_must_fail git -C repo refs migrate \ - --ref-format=$from_format 2>err && + --ref-storage-format=$from_format 2>err && cat >expect <<-EOF && error: repository already uses ${SQ}$from_format${SQ} format EOF @@ -119,7 +119,7 @@ do git init --ref-storage-format=$from_format repo && git -C repo worktree add wt && test_must_fail git -C repo refs migrate \ - --ref-format=$to_format 2>err && + --ref-storage-format=$to_format 2>err && cat >expect <<-EOF && error: migrating repositories with worktrees is not supported yet EOF @@ -211,7 +211,7 @@ do git init --ref-storage-format=$from_format repo && test_commit -C repo initial && git -C repo refs migrate --dry-run \ - --ref-format=$to_format >output && + --ref-storage-format=$to_format >output && test_grep "Finished dry-run migration of refs" output && test_path_is_dir repo/.git/ref_migration.* && echo $from_format >expect && diff --git a/t/t7424-submodule-mixed-ref-formats.sh b/t/t7424-submodule-mixed-ref-formats.sh index 5f31d233e9a84d..e61389af058b2f 100755 --- a/t/t7424-submodule-mixed-ref-formats.sh +++ b/t/t7424-submodule-mixed-ref-formats.sh @@ -94,7 +94,7 @@ test_expect_success 'status with mixed submodule ref storages' ' git init main && git -C main submodule add "file://$(pwd)/submodule" && git -C main commit -m "add submodule" && - git -C main/submodule refs migrate --ref-format=$OTHER_FORMAT && + git -C main/submodule refs migrate --ref-storage-format=$OTHER_FORMAT && # The main repository should use the default ref format now, whereas # the submodule should use the other format. From da01f4939f2f90fb27d7f47c2e32ffed59cc931f Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 9 Sep 2026 13:12:51 +0200 Subject: [PATCH 013/155] builtin/submodule: rename "--ref-format=" to "--ref-storage-format=" With the same reasoning as for git-init(1), rename "--ref-format=" to "--ref-storage-format=" and keep the old name as an alias. Note that this commit is a bit more complex compared to the others as we also need to adapt the submodule helper for consistency. But overall, the changes are straight-forward and in the same spirit. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/git-submodule.adoc | 14 ++++++++------ builtin/clone.c | 2 +- builtin/submodule--helper.c | 21 ++++++++++++--------- git-submodule.sh | 20 ++++++++++---------- t/t7424-submodule-mixed-ref-formats.sh | 6 +++--- 5 files changed, 34 insertions(+), 29 deletions(-) diff --git a/Documentation/git-submodule.adoc b/Documentation/git-submodule.adoc index 722d827908e2ab..e9c81a7170f899 100644 --- a/Documentation/git-submodule.adoc +++ b/Documentation/git-submodule.adoc @@ -34,7 +34,7 @@ COMMANDS With no arguments, shows the status of existing submodules. Several subcommands are available to perform operations on the submodules. -`add [-b ] [-f | --force] [--name ] [--reference ] [--ref-format ] [--depth ] [--] []`:: +`add [-b ] [-f | --force] [--name ] [--reference ] [--ref-storage-format ] [--depth ] [--] []`:: Add the given repository as a submodule at the given path to the changeset to be committed next to the current project: the current project is termed the "superproject". @@ -72,8 +72,9 @@ location, and only the superproject's URL needs to be provided. git-submodule will correctly locate the submodule using the relative URL in `.gitmodules`. + -If `--ref-format ` is specified, the ref storage format of newly -cloned submodules will be set accordingly. +If `--ref-storage-format ` or its deprecated alias `--ref-format +` is specified, the ref storage format of the newly cloned submodules +will be set accordingly. `status [--cached] [--recursive] [--] [...]`:: Show the status of the submodules. This will print the SHA-1 of the @@ -139,7 +140,7 @@ If you really want to remove a submodule from the repository and commit that use linkgit:git-rm[1] instead. See linkgit:gitsubmodules[7] for removal options. -`update [--init] [--remote] [-N | --no-fetch] [--[no-]recommend-shallow] [-f | --force] [--checkout | --rebase | --merge] [--reference=] [--ref-format=] [--depth=] [--recursive] [--jobs ] [--[no-]single-branch] [--filter=] [--] [...]`:: +`update [--init] [--remote] [-N | --no-fetch] [--[no-]recommend-shallow] [-f | --force] [--checkout | --rebase | --merge] [--reference=] [--ref-storage-format=] [--depth=] [--recursive] [--jobs ] [--[no-]single-branch] [--filter=] [--] [...]`:: + -- Update the registered submodules to match what the superproject @@ -188,8 +189,9 @@ submodule with the `--init` option. If `--recursive` is specified, this command will recurse into the registered submodules, and update any nested submodules within. -If `--ref-format ` is specified, the ref storage format of newly -cloned submodules will be set accordingly. +If `--ref-storage-format ` or its deprecated alias `--ref-format +` is specified, the ref storage format of the newly cloned submodules +will be set accordingly. If `--filter ` is specified, the given partial clone filter will be applied to the submodule. See linkgit:git-rev-list[1] for details on filter diff --git a/builtin/clone.c b/builtin/clone.c index 42cae9e0f6e851..dd722e4de179d6 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -725,7 +725,7 @@ static int checkout(int submodule_progress, } if (ref_storage_format != REF_STORAGE_FORMAT_UNKNOWN) - strvec_pushf(&cmd.args, "--ref-format=%s", + strvec_pushf(&cmd.args, "--ref-storage-format=%s", ref_storage_format_to_name(ref_storage_format)); if (filter_submodules && filter_options->choice) diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index e7cd3225fa84c4..41883af2ac0a88 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -1941,7 +1941,7 @@ static int clone_submodule(const struct module_clone_data *clone_data, item->string, NULL); } if (clone_data->ref_storage_format != REF_STORAGE_FORMAT_UNKNOWN) - strvec_pushf(&cp.args, "--ref-format=%s", + strvec_pushf(&cp.args, "--ref-storage-format=%s", ref_storage_format_to_name(clone_data->ref_storage_format)); if (clone_data->dissociate) strvec_push(&cp.args, "--dissociate"); @@ -2057,8 +2057,9 @@ static int module_clone(int argc, const char **argv, const char *prefix, OPT_STRING_LIST(0, "reference", &reference, N_("repo"), N_("reference repository")), - OPT_STRING(0, "ref-format", &ref_storage_format, N_("format"), - N_("specify the reference format to use")), + OPT_STRING(0, "ref-storage-format", &ref_storage_format, N_("format"), + N_("specify the reference storage format to use")), + OPT_ALIAS_F(0, "ref-format", "ref-storage-format", PARSE_OPT_HIDDEN), OPT_BOOL(0, "dissociate", &dissociate, N_("use --reference only while cloning")), OPT_INTEGER(0, "depth", &clone_data.depth, @@ -2357,7 +2358,7 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce, if (suc->update_data->require_init) strvec_push(&child->args, "--require-init"); if (suc->update_data->ref_storage_format != REF_STORAGE_FORMAT_UNKNOWN) - strvec_pushf(&child->args, "--ref-format=%s", + strvec_pushf(&child->args, "--ref-storage-format=%s", ref_storage_format_to_name(suc->update_data->ref_storage_format)); strvec_pushl(&child->args, "--path", sub->path, NULL); strvec_pushl(&child->args, "--name", sub->name, NULL); @@ -2801,7 +2802,7 @@ static void update_data_to_args(const struct update_data *update_data, strvec_pushl(args, "--reference", item->string, NULL); } if (update_data->ref_storage_format != REF_STORAGE_FORMAT_UNKNOWN) - strvec_pushf(args, "--ref-format=%s", + strvec_pushf(args, "--ref-storage-format=%s", ref_storage_format_to_name(update_data->ref_storage_format)); if (update_data->filter_options && update_data->filter_options->choice) strvec_pushf(args, "--filter=%s", @@ -3010,8 +3011,9 @@ static int module_update(int argc, const char **argv, const char *prefix, SM_UPDATE_REBASE), OPT_STRING_LIST(0, "reference", &opt.references, N_("repo"), N_("reference repository")), - OPT_STRING(0, "ref-format", &ref_storage_format, N_("format"), - N_("specify the reference format to use")), + OPT_STRING(0, "ref-storage-format", &ref_storage_format, N_("format"), + N_("specify the reference storage format to use")), + OPT_ALIAS_F(0, "ref-format", "ref-storage-format", PARSE_OPT_HIDDEN), OPT_BOOL(0, "dissociate", &opt.dissociate, N_("use --reference only while cloning")), OPT_INTEGER(0, "depth", &opt.depth, @@ -3659,8 +3661,9 @@ static int module_add(int argc, const char **argv, const char *prefix, OPT_BOOL(0, "progress", &progress, N_("force cloning progress")), OPT_STRING(0, "reference", &add_data.reference_path, N_("repository"), N_("reference repository")), - OPT_STRING(0, "ref-format", &ref_storage_format, N_("format"), - N_("specify the reference format to use")), + OPT_STRING(0, "ref-storage-format", &ref_storage_format, N_("format"), + N_("specify the reference storage format to use")), + OPT_ALIAS_F(0, "ref-format", "ref-storage-format", PARSE_OPT_HIDDEN), OPT_BOOL(0, "dissociate", &dissociate, N_("borrow the objects from reference repositories")), OPT_STRING(0, "name", &add_data.sm_name, N_("name"), N_("sets the submodule's name to the given string " diff --git a/git-submodule.sh b/git-submodule.sh index 2999b31fad3e97..863219413803c3 100755 --- a/git-submodule.sh +++ b/git-submodule.sh @@ -95,13 +95,13 @@ cmd_add() --reference=*) reference="$1" ;; - --ref-format) + --ref-format|--ref-storage-format) case "$2" in '') usage ;; esac - ref_format="--ref-format=$2" + ref_storage_format="--ref-storage-format=$2" shift ;; - --ref-format=*) - ref_format="$1" + --ref-format=*|--ref-storage-format=*) + ref_storage_format="$1" ;; --dissociate) dissociate=$1 @@ -147,7 +147,7 @@ cmd_add() $progress \ ${branch:+"$branch"} \ ${reference:+"$reference"} \ - ${ref_format:+"$ref_format"} \ + ${ref_storage_format:+"$ref_storage_format"} \ $dissociate \ ${name:+"$name"} \ ${depth:+"$depth"} \ @@ -302,13 +302,13 @@ cmd_update() -r|--rebase) rebase=$1 ;; - --ref-format) + --ref-format|--ref-storage-format) case "$2" in '') usage ;; esac - ref_format="--ref-format=$2" + ref_storage_format="--ref-storage-format=$2" shift ;; - --ref-format=*) - ref_format="$1" + --ref-format=*|--ref-storage-format=*) + ref_storage_format="$1" ;; --reference) case "$2" in '') usage ;; esac @@ -385,7 +385,7 @@ cmd_update() $rebase \ $merge \ $checkout \ - ${ref_format:+"$ref_format"} \ + ${ref_storage_format:+"$ref_storage_format"} \ ${reference:+"$reference"} \ $dissociate \ ${depth:+"$depth"} \ diff --git a/t/t7424-submodule-mixed-ref-formats.sh b/t/t7424-submodule-mixed-ref-formats.sh index e61389af058b2f..2ef85289b39855 100755 --- a/t/t7424-submodule-mixed-ref-formats.sh +++ b/t/t7424-submodule-mixed-ref-formats.sh @@ -44,7 +44,7 @@ test_expect_success 'add submodules with different ref storage format' ' test_commit -C submodule submodule-initial && git init upstream && test_ref_format upstream "$GIT_DEFAULT_REF_FORMAT" && - git -C upstream submodule add --ref-format="$OTHER_FORMAT" "file://$(pwd)/submodule" && + git -C upstream submodule add --ref-storage-format="$OTHER_FORMAT" "file://$(pwd)/submodule" && test_ref_format upstream/submodule "$OTHER_FORMAT" ' @@ -82,7 +82,7 @@ test_expect_success 'clone submodules with different ref storage format' ' git clone --no-recurse-submodules "file://$(pwd)/upstream" downstream && test_ref_format downstream "$GIT_DEFAULT_REF_FORMAT" && - git -C downstream submodule update --init --ref-format=$OTHER_FORMAT && + git -C downstream submodule update --init --ref-storage-format=$OTHER_FORMAT && test_ref_format downstream/submodule "$OTHER_FORMAT" ' @@ -122,7 +122,7 @@ test_expect_success 'recursive pull with mixed formats' ' # Clone the upstream repository such that the main repo and its # submodules have different formats. git clone --no-recurse-submodules "file://$(pwd)/upstream" downstream && - git -C downstream submodule update --init --ref-format=$OTHER_FORMAT && + git -C downstream submodule update --init --ref-storage-format=$OTHER_FORMAT && test_ref_format downstream "$GIT_DEFAULT_REF_FORMAT" && test_ref_format downstream/submodule "$OTHER_FORMAT" && From 4ebec1971154a6b7abf9b20952e18e85542e20d2 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 9 Sep 2026 13:12:52 +0200 Subject: [PATCH 014/155] builtin/rev-parse: rename "--show-ref-format" to "--show-ref-storage-format" With the same reasoning as for git-init(1), rename "--show-ref-format" to "--show-ref-storage-format" and keep the old name as an alias. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/git-rev-parse.adoc | 5 +++- builtin/rev-parse.c | 2 +- contrib/completion/git-prompt.sh | 2 +- t/perf/perf-lib.sh | 4 +-- t/t0001-init.sh | 36 +++++++++++++------------- t/t0610-reftable-basics.sh | 16 ++++++------ t/t1460-refs-migrate.sh | 4 +-- t/t1500-rev-parse.sh | 8 +++--- t/t5601-clone.sh | 2 +- t/t7424-submodule-mixed-ref-formats.sh | 2 +- 10 files changed, 42 insertions(+), 39 deletions(-) diff --git a/Documentation/git-rev-parse.adoc b/Documentation/git-rev-parse.adoc index 5398691f3f15f7..14462ce7d99cef 100644 --- a/Documentation/git-rev-parse.adoc +++ b/Documentation/git-rev-parse.adoc @@ -331,9 +331,12 @@ The following options are unaffected by `--path-format`: requested and no compatibility algorithm is enabled, prints an empty line. If not specified, the default is "storage". ---show-ref-format:: +--show-ref-storage-format:: Show the reference storage format used for the repository. +--show-ref-format:: + Deprecated alias of `--show-ref-storage-format`. + Other Options ~~~~~~~~~~~~~ diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c index 43693454d53f0c..ec33c19bdfc9c8 100644 --- a/builtin/rev-parse.c +++ b/builtin/rev-parse.c @@ -1134,7 +1134,7 @@ int cmd_rev_parse(int argc, } continue; } - if (!strcmp(arg, "--show-ref-format")) { + if (!strcmp(arg, "--show-ref-format") || !strcmp(arg, "--show-ref-storage-format")) { puts(ref_storage_format_to_name(the_repository->ref_storage_format)); continue; } diff --git a/contrib/completion/git-prompt.sh b/contrib/completion/git-prompt.sh index 6186c474ba71e1..754a5edc9aa990 100644 --- a/contrib/completion/git-prompt.sh +++ b/contrib/completion/git-prompt.sh @@ -466,7 +466,7 @@ __git_ps1 () local repo_info rev_parse_exit_code repo_info="$(git rev-parse --git-dir --is-inside-git-dir \ - --is-bare-repository --is-inside-work-tree --show-ref-format \ + --is-bare-repository --is-inside-work-tree --show-ref-storage-format \ --short HEAD 2>/dev/null)" rev_parse_exit_code="$?" diff --git a/t/perf/perf-lib.sh b/t/perf/perf-lib.sh index 3ce49fd4236223..846ecc35c3b49b 100644 --- a/t/perf/perf-lib.sh +++ b/t/perf/perf-lib.sh @@ -135,7 +135,7 @@ test_perf_create_repo_from () { source_git="$("$MODERN_GIT" -C "$source" rev-parse --git-dir)" objects_dir="$("$MODERN_GIT" -C "$source" rev-parse --git-path objects)" common_dir="$("$MODERN_GIT" -C "$source" rev-parse --git-common-dir)" - refformat="$("$MODERN_GIT" -C "$source" rev-parse --show-ref-format)" + ref_storage_format="$("$MODERN_GIT" -C "$source" rev-parse --show-ref-storage-format)" objectformat="$("$MODERN_GIT" -C "$source" rev-parse --show-object-format)" mkdir -p "$repo/.git" ( @@ -153,7 +153,7 @@ test_perf_create_repo_from () { ) && ( cd "$repo" && - "$MODERN_GIT" init -q --ref-storage-format="$refformat" --object-format="$objectformat" && + "$MODERN_GIT" init -q --ref-storage-format="$ref_storage_format" --object-format="$objectformat" && test_perf_do_repo_symlink_config_ && mv .git/hooks .git/hooks-disabled 2>/dev/null && if test -f .git/index.lock diff --git a/t/t0001-init.sh b/t/t0001-init.sh index df9a2ff2da0a0d..b4f19d8077b429 100755 --- a/t/t0001-init.sh +++ b/t/t0001-init.sh @@ -657,7 +657,7 @@ test_expect_success 'init warns about invalid init.defaultRefFormat' ' git init repo 2>err && test_cmp expect err && - git -C repo rev-parse --show-ref-format >actual && + git -C repo rev-parse --show-ref-storage-format >actual && echo $GIT_DEFAULT_REF_FORMAT >expected && test_cmp expected actual ' @@ -669,7 +669,7 @@ test_expect_success 'default ref format' ' git init refformat ) && git version --build-options | sed -ne "s/^default-ref-format: //p" >expect && - git -C refformat rev-parse --show-ref-format >actual && + git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' @@ -692,7 +692,7 @@ do test_cmp expect actual && echo $format >expect && - git -C refformat rev-parse --show-ref-format >actual && + git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' @@ -700,7 +700,7 @@ do test_when_finished "rm -rf refformat" && git init --ref-storage-format=$format refformat && echo $format >expect && - git -C refformat rev-parse --show-ref-format >actual && + git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' @@ -713,7 +713,7 @@ do ) && echo $format >expect && - git -C refformat rev-parse --show-ref-format >actual && + git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' @@ -721,16 +721,16 @@ do test_when_finished "rm -rf refformat" && GIT_DEFAULT_REF_FORMAT=garbage git init --ref-storage-format=$format refformat && echo $format >expect && - git -C refformat rev-parse --show-ref-format >actual && + git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' test_expect_success "reinit repository with GIT_DEFAULT_REF_FORMAT=$format does not change format" ' test_when_finished "rm -rf refformat" && git init refformat && - git -C refformat rev-parse --show-ref-format >expect && + git -C refformat rev-parse --show-ref-storage-format >expect && GIT_DEFAULT_REF_FORMAT=$format git init refformat && - git -C refformat rev-parse --show-ref-format >actual && + git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' done @@ -739,7 +739,7 @@ test_expect_success "--ref-storage-format= overrides GIT_DEFAULT_REF_FORMAT" ' test_when_finished "rm -rf refformat" && GIT_DEFAULT_REF_FORMAT=files git init --ref-storage-format=reftable refformat && echo reftable >expect && - git -C refformat rev-parse --show-ref-format >actual && + git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' @@ -749,7 +749,7 @@ test_expect_success "GIT_DEFAULT_REF_FORMAT= overrides init.defaultRefFormat" ' GIT_DEFAULT_REF_FORMAT=reftable git init refformat && echo reftable >expect && - git -C refformat rev-parse --show-ref-format >actual && + git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' @@ -761,7 +761,7 @@ test_expect_success "init with feature.experimental=true" ' git init refformat ) && echo reftable >expect && - git -C refformat rev-parse --show-ref-format >actual && + git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' @@ -774,7 +774,7 @@ test_expect_success "init.defaultRefFormat overrides feature.experimental=true" git init refformat ) && echo files >expect && - git -C refformat rev-parse --show-ref-format >actual && + git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' @@ -783,7 +783,7 @@ test_expect_success "GIT_DEFAULT_REF_FORMAT= overrides feature.experimental=true test_config_global feature.experimental true && GIT_DEFAULT_REF_FORMAT=files git init refformat && echo files >expect && - git -C refformat rev-parse --show-ref-format >actual && + git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' @@ -794,7 +794,7 @@ do git init --ref-storage-format=$from_format refformat && git init --ref-storage-format=$from_format refformat && echo $from_format >expect && - git -C refformat rev-parse --show-ref-format >actual && + git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' @@ -814,7 +814,7 @@ do test_must_fail git init --ref-storage-format=$to_format refformat 2>err && test_cmp expect err && echo $from_format >expect && - git -C refformat rev-parse --show-ref-format >actual && + git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' done @@ -933,7 +933,7 @@ test_expect_success 'init with includeIf.onbranch condition' ' test_when_finished "rm -rf repo" && git -c includeIf.onbranch:main.path=nonexistent init repo && echo $GIT_DEFAULT_REF_FORMAT >expect && - git -C repo rev-parse --show-ref-format >actual && + git -C repo rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' @@ -942,7 +942,7 @@ test_expect_success 'init with includeIf.onbranch condition with existing direct mkdir repo && git -c includeIf.onbranch:nonexistent.path=/does/not/exist init repo && echo $GIT_DEFAULT_REF_FORMAT >expect && - git -C repo rev-parse --show-ref-format >actual && + git -C repo rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' @@ -951,7 +951,7 @@ test_expect_success 're-init with includeIf.onbranch condition' ' git init repo && git -c includeIf.onbranch:nonexistent.path=/does/not/exist init repo && echo $GIT_DEFAULT_REF_FORMAT >expect && - git -C repo rev-parse --show-ref-format >actual && + git -C repo rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' diff --git a/t/t0610-reftable-basics.sh b/t/t0610-reftable-basics.sh index d325f17a143c03..b6db54430ad0c2 100755 --- a/t/t0610-reftable-basics.sh +++ b/t/t0610-reftable-basics.sh @@ -27,7 +27,7 @@ test_expect_success 'init: creates basic reftable structures' ' test_path_is_dir repo/.git/reftable && test_path_is_file repo/.git/reftable/tables.list && echo reftable >expect && - git -C repo rev-parse --show-ref-format >actual && + git -C repo rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' @@ -38,7 +38,7 @@ test_expect_success 'init: sha256 object format via environment variable' ' sha256 reftable EOF - git -C repo rev-parse --show-object-format --show-ref-format >actual && + git -C repo rev-parse --show-object-format --show-ref-storage-format >actual && test_cmp expect actual ' @@ -49,7 +49,7 @@ test_expect_success 'init: sha256 object format via option' ' sha256 reftable EOF - git -C repo rev-parse --show-object-format --show-ref-format >actual && + git -C repo rev-parse --show-object-format --show-ref-storage-format >actual && test_cmp expect actual ' @@ -156,7 +156,7 @@ test_expect_success 'clone: can clone reftable repository' ' git clone repo cloned && echo reftable >expect && - git -C cloned rev-parse --show-ref-format >actual && + git -C cloned rev-parse --show-ref-storage-format >actual && test_cmp expect actual && test_path_is_file cloned/file1 ' @@ -171,11 +171,11 @@ test_expect_success 'clone: can clone reffiles into reftable repository' ' git -C reftable rev-parse HEAD >actual && test_cmp expect actual && - git -C reftable rev-parse --show-ref-format >actual && + git -C reftable rev-parse --show-ref-storage-format >actual && echo reftable >expect && test_cmp expect actual && - git -C reffiles rev-parse --show-ref-format >actual && + git -C reffiles rev-parse --show-ref-storage-format >actual && echo files >expect && test_cmp expect actual ' @@ -190,11 +190,11 @@ test_expect_success 'clone: can clone reftable into reffiles repository' ' git -C reffiles rev-parse HEAD >actual && test_cmp expect actual && - git -C reftable rev-parse --show-ref-format >actual && + git -C reftable rev-parse --show-ref-storage-format >actual && echo reftable >expect && test_cmp expect actual && - git -C reffiles rev-parse --show-ref-format >actual && + git -C reffiles rev-parse --show-ref-storage-format >actual && echo files >expect && test_cmp expect actual ' diff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh index 204dd79b412da9..ecf6411288145e 100755 --- a/t/t1460-refs-migrate.sh +++ b/t/t1460-refs-migrate.sh @@ -53,7 +53,7 @@ test_migration () { test_cmp expect_logs actual_logs fi && - git -C "$repo" rev-parse --show-ref-format >actual && + git -C "$repo" rev-parse --show-ref-storage-format >actual && echo "$format" >expect && test_cmp expect actual } @@ -215,7 +215,7 @@ do test_grep "Finished dry-run migration of refs" output && test_path_is_dir repo/.git/ref_migration.* && echo $from_format >expect && - git -C repo rev-parse --show-ref-format >actual && + git -C repo rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' diff --git a/t/t1500-rev-parse.sh b/t/t1500-rev-parse.sh index 4174ca40c342a9..30bf8a4d0a0be7 100755 --- a/t/t1500-rev-parse.sh +++ b/t/t1500-rev-parse.sh @@ -241,19 +241,19 @@ test_expect_success RUST 'rev-parse --show-object-format in repo with compat mod ) ' -test_expect_success 'rev-parse --show-ref-format' ' +test_expect_success 'rev-parse --show-ref-storage-format' ' test_detect_ref_format >expect && - git rev-parse --show-ref-format >actual && + git rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' -test_expect_success 'rev-parse --show-ref-format with invalid storage' ' +test_expect_success 'rev-parse --show-ref-storage-format with invalid storage' ' test_when_finished "rm -rf repo" && git init repo && ( cd repo && git config extensions.refstorage broken && - test_must_fail git rev-parse --show-ref-format 2>err && + test_must_fail git rev-parse --show-ref-storage-format 2>err && test_grep "error: invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}broken${SQ}" err ) ' diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh index 202d86bc839c64..06f6121f82e526 100755 --- a/t/t5601-clone.sh +++ b/t/t5601-clone.sh @@ -168,7 +168,7 @@ test_expect_success 'clone with files ref format' ' test_when_finished "rm -rf ref-storage" && git clone --ref-storage-format=files --mirror src ref-storage && echo files >expect && - git -C ref-storage rev-parse --show-ref-format >actual && + git -C ref-storage rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' diff --git a/t/t7424-submodule-mixed-ref-formats.sh b/t/t7424-submodule-mixed-ref-formats.sh index 2ef85289b39855..9707744644855b 100755 --- a/t/t7424-submodule-mixed-ref-formats.sh +++ b/t/t7424-submodule-mixed-ref-formats.sh @@ -6,7 +6,7 @@ test_description='submodules handle mixed ref storage formats' test_ref_format () { echo "$2" >expect && - git -C "$1" rev-parse --show-ref-format >actual && + git -C "$1" rev-parse --show-ref-storage-format >actual && test_cmp expect actual } From 022e59bc6b2d0db08529bfed10dce58b107d5a53 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 9 Sep 2026 13:12:53 +0200 Subject: [PATCH 015/155] help: rename "default-ref-format" to "default-ref-storage-format" When printing information about how specifically Git was built and what defaults it has we also print the default ref storage format used when initializing new repositories. This is to prepare for Git 3.0, where the default storage format will change from the "files" backend to the "reftable" backend. In preceding commits we have adapted "ref-format" parameters to be called "ref-storage-format" instead to resolve some conceptual mismatches. The build information is now the only place where we still refer to it as "default-ref-format". Rename the field to "default-ref-storage-format" instead. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- help.c | 2 +- t/t0001-init.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/help.c b/help.c index 46241492cee117..1dd8391eebd39c 100644 --- a/help.c +++ b/help.c @@ -824,7 +824,7 @@ void get_version_info(struct strbuf *buf, int show_build_options) SHA1_UNSAFE_BACKEND); #endif strbuf_addf(buf, "SHA-256: %s\n", SHA256_BACKEND); - strbuf_addf(buf, "default-ref-format: %s\n", + strbuf_addf(buf, "default-ref-storage-format: %s\n", ref_storage_format_to_name(REF_STORAGE_FORMAT_DEFAULT)); strbuf_addf(buf, "default-hash: %s\n", hash_algos[GIT_HASH_DEFAULT].name); } diff --git a/t/t0001-init.sh b/t/t0001-init.sh index b4f19d8077b429..6f4431bed7e76e 100755 --- a/t/t0001-init.sh +++ b/t/t0001-init.sh @@ -668,7 +668,7 @@ test_expect_success 'default ref format' ' sane_unset GIT_DEFAULT_REF_FORMAT && git init refformat ) && - git version --build-options | sed -ne "s/^default-ref-format: //p" >expect && + git version --build-options | sed -ne "s/^default-ref-storage-format: //p" >expect && git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' From 0af0e959c819e195ffdeef7c7c76c28c23e46576 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 9 Sep 2026 13:12:54 +0200 Subject: [PATCH 016/155] refs: expose function to parse reference URIs In the next commit we're about to add more sites that want to parse a reference backends URI into a format and payload. Expose a new function `ref_storage_format_by_uri()` that enables this. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- refs.c | 23 +++++++++++++++++++++++ refs.h | 4 ++++ setup.c | 48 ++++++++++++------------------------------------ 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/refs.c b/refs.c index 92d5df5b71fa4b..951db56113d536 100644 --- a/refs.c +++ b/refs.c @@ -54,6 +54,29 @@ enum ref_storage_format ref_storage_format_by_name(const char *name) return REF_STORAGE_FORMAT_UNKNOWN; } +enum ref_storage_format ref_storage_format_by_uri(const char *uri, + char **payload) +{ + enum ref_storage_format format; + const char *schema_end; + char *name; + + schema_end = strstr(uri, "://"); + if (!schema_end) { + name = xstrdup(uri); + if (payload) + *payload = NULL; + } else { + name = xstrndup(uri, schema_end - uri); + if (payload) + *payload = xstrdup(schema_end + 3); + } + + format = ref_storage_format_by_name(name); + free(name); + return format; +} + const char *ref_storage_format_to_name(enum ref_storage_format ref_storage_format) { const struct ref_storage_be *be = find_ref_storage_backend(ref_storage_format); diff --git a/refs.h b/refs.h index 9979446d15fd3b..ee3b8a62ef87bd 100644 --- a/refs.h +++ b/refs.h @@ -17,6 +17,10 @@ struct worktree; enum ref_storage_format ref_storage_format_by_name(const char *name); const char *ref_storage_format_to_name(enum ref_storage_format ref_storage_format); +/* Parse a reference storage URI in the format "[://]". */ +enum ref_storage_format ref_storage_format_by_uri(const char *uri, + char **payload); + enum ref_transaction_error { /* Default error code */ REF_TRANSACTION_ERROR_GENERIC = -1, diff --git a/setup.c b/setup.c index dfe05d9a03acb9..3be7dac452989c 100644 --- a/setup.c +++ b/setup.c @@ -632,21 +632,6 @@ static enum extension_result handle_extension_v0(const char *var, return EXTENSION_UNKNOWN; } -static void parse_reference_uri(const char *value, char **format, - char **payload) -{ - const char *schema_end; - - schema_end = strstr(value, "://"); - if (!schema_end) { - *format = xstrdup(value); - *payload = NULL; - } else { - *format = xstrndup(value, schema_end - value); - *payload = xstrdup_or_null(schema_end + 3); - } -} - /* * Record any new extensions in this function. */ @@ -689,16 +674,13 @@ static enum extension_result handle_extension(const char *var, return EXTENSION_OK; } else if (!strcmp(ext, "refstorage")) { unsigned int format; - char *format_str; if (!value) return config_error_nonbool(var); - parse_reference_uri(value, &format_str, - &data->ref_storage_payload); - - format = ref_storage_format_by_name(format_str); - free(format_str); + FREE_AND_NULL(data->ref_storage_payload); + format = ref_storage_format_by_uri(value, + &data->ref_storage_payload); if (format == REF_STORAGE_FORMAT_UNKNOWN) return error(_("invalid value for '%s': '%s'"), @@ -2069,16 +2051,12 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok) */ ref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT); if (ref_backend_uri) { - char *format; - - free(discovery.format.ref_storage_payload); - - parse_reference_uri(ref_backend_uri, &format, &discovery.format.ref_storage_payload); - discovery.format.ref_storage_format = ref_storage_format_by_name(format); + FREE_AND_NULL(discovery.format.ref_storage_payload); + discovery.format.ref_storage_format = + ref_storage_format_by_uri(ref_backend_uri, + &discovery.format.ref_storage_payload); if (discovery.format.ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN) - die(_("unknown ref storage format: '%s'"), format); - - free(format); + die(_("unknown ref storage format: '%s'"), ref_backend_uri); } if (apply_repository_format(repo, &discovery.format, @@ -2806,18 +2784,16 @@ static void repository_format_configure(struct repository_format *repo_fmt, ref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT); if (ref_backend_uri) { - char *backend, *payload; enum ref_storage_format format; + char *payload; - parse_reference_uri(ref_backend_uri, &backend, &payload); - format = ref_storage_format_by_name(backend); + format = ref_storage_format_by_uri(ref_backend_uri, &payload); if (format == REF_STORAGE_FORMAT_UNKNOWN) - die(_("unknown ref storage format: '%s'"), backend); + die(_("unknown ref storage format: '%s'"), ref_backend_uri); repo_fmt->ref_storage_format = format; + free(repo_fmt->ref_storage_payload); repo_fmt->ref_storage_payload = payload; - - free(backend); } } From 78c0b3565059e6e235bb70f3bd225a8056a600ea Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 9 Sep 2026 13:12:55 +0200 Subject: [PATCH 017/155] setup: refactor how we configure the ref storage format When (re)initializing a repository we need to figure out the ref storage format that the repository ought to use. This logic is surprisingly complex, as we have grown a lot of different mechanisms over time to configure the format. Unfortunately, as a result of this organic growth, the logic that configures the storage format has grown very complex. The biggest culprit here is that we're mixing the logic that determines the desired storage format with the logic that validates whether the end result is sane. This leads to some repetitive code, and makes it very easy to forget validation for some of the branches. In fact, the way we handle GIT_REFERENCE_BACKEND shows exactly one such edge case where we don't properly validate. When initializing a repository with one storage format and then reinitializing it with the environment variable set to a different format then we'd corrupt the repository because we silently change the format: $ git init repo $ git -C repo commit --allow-empty -m message $ GIT_REFERENCE_BACKEND=reftable git -C repo init fatal: could not open '.../refs/heads' for writing: Is a directory $ git -C repo log fatal: your current branch appears to be broken Refactor the code so that we clearly distinguish between these two different concerns. This lets us clearly spell out the precedence order and makes the whole logic significantly easier to extend going forward. Note that the new logic intentionally changes the precedence order so that "GIT_REFERENCE_BACKEND" is now overridden by the "--ref-storage-format=" command line option. This matches our usual precedence order, where explicit command line arguments override environment variables. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- setup.c | 103 ++++++++++++++++++++++++++++++------------------ t/t0001-init.sh | 12 +++++- 2 files changed, 75 insertions(+), 40 deletions(-) diff --git a/setup.c b/setup.c index 3be7dac452989c..38fa5e854ca610 100644 --- a/setup.c +++ b/setup.c @@ -2674,7 +2674,7 @@ static void separate_git_dir(struct repository *repo, struct default_format_config { int hash; - enum ref_storage_format ref_format; + enum ref_storage_format ref_storage_format; }; static int read_default_format_config(const char *key, const char *value, @@ -2699,8 +2699,8 @@ static int read_default_format_config(const char *key, const char *value, ret = git_config_string(&str, key, value); if (ret) goto out; - cfg->ref_format = ref_storage_format_by_name(str); - if (cfg->ref_format == REF_STORAGE_FORMAT_UNKNOWN) + cfg->ref_storage_format = ref_storage_format_by_name(str); + if (cfg->ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN) warning(_("unknown ref storage format '%s'"), str); goto out; } @@ -2710,9 +2710,9 @@ static int read_default_format_config(const char *key, const char *value, * "init.defaultRefFormat" takes precedence over this setting. */ if (!strcmp(key, "feature.experimental") && - cfg->ref_format == REF_STORAGE_FORMAT_UNKNOWN && + cfg->ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN && git_config_bool(key, value)) { - cfg->ref_format = REF_STORAGE_FORMAT_REFTABLE; + cfg->ref_storage_format = REF_STORAGE_FORMAT_REFTABLE; ret = 0; goto out; } @@ -2724,18 +2724,18 @@ static int read_default_format_config(const char *key, const char *value, } static void repository_format_configure(struct repository_format *repo_fmt, - int hash, enum ref_storage_format ref_format) + int hash, enum ref_storage_format ref_storage_format) { struct default_format_config cfg = { .hash = GIT_HASH_UNKNOWN, - .ref_format = REF_STORAGE_FORMAT_UNKNOWN, + .ref_storage_format = REF_STORAGE_FORMAT_UNKNOWN, }; struct config_options opts = { .respect_includes = 1, .ignore_repo = 1, .ignore_worktree = 1, }; - const char *ref_backend_uri; + char *ref_storage_payload = NULL; const char *env; config_with_options(read_default_format_config, &cfg, NULL, NULL, &opts); @@ -2761,40 +2761,65 @@ static void repository_format_configure(struct repository_format *repo_fmt, repo_fmt->hash_algo = cfg.hash; } - env = getenv("GIT_DEFAULT_REF_FORMAT"); - if (repo_fmt->version >= 0 && - ref_format != REF_STORAGE_FORMAT_UNKNOWN && - ref_format != repo_fmt->ref_storage_format) { - die(_("attempt to reinitialize repository with different reference storage format")); - } else if (ref_format != REF_STORAGE_FORMAT_UNKNOWN) { - repo_fmt->ref_storage_format = ref_format; - } else if (env) { - ref_format = ref_storage_format_by_name(env); - if (ref_format == REF_STORAGE_FORMAT_UNKNOWN) - die(_("unknown ref storage format '%s'"), env); - if (repo_fmt->version < 0 || - repo_fmt->ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN) - repo_fmt->ref_storage_format = ref_format; - } else if (cfg.ref_format != REF_STORAGE_FORMAT_UNKNOWN) { - repo_fmt->ref_storage_format = cfg.ref_format; + /* + * We have the following order of preference when configuring the ref + * storage format: + * + * 1. Explicit override via the command line, like in `git init + * --ref-storage-format=`. + * + * 2. Explicit override via the environment with + * GIT_REFERENCE_BACKEND. + * + * 3. Existing repository format. All the subsequent sources only + * kick in when there is no repository yet. + * + * 4. The default ref storage format for new repositories as + * configured via "GIT_DEFAULT_REF_FORMAT". + * + * 5. The default ref storage format for new repositories as + * configured via "init.defaultRefFormat" + * + * 6. Otherwise, we fall back to the default ref storage format + * compiled into Git. + */ + if (ref_storage_format != REF_STORAGE_FORMAT_UNKNOWN) { + /* nothing to do */ + } else if ((env = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT))) { + ref_storage_format = ref_storage_format_by_uri(env, &ref_storage_payload); + if (ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN) + die(_("unknown ref storage format specified via %s: '%s'"), + GIT_REFERENCE_BACKEND_ENVIRONMENT, env); + } else if (repo_fmt->version >= 0) { + ref_storage_format = repo_fmt->ref_storage_format; + ref_storage_payload = xstrdup_or_null(repo_fmt->ref_storage_payload); + } else if ((env = getenv("GIT_DEFAULT_REF_FORMAT"))) { + ref_storage_format = ref_storage_format_by_name(env); + if (ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN) + die(_("unknown ref storage format specified via %s: '%s'"), + "GIT_DEFAULT_REF_FORMAT", env); + } else if (cfg.ref_storage_format != REF_STORAGE_FORMAT_UNKNOWN) { + ref_storage_format = cfg.ref_storage_format; } else { - repo_fmt->ref_storage_format = REF_STORAGE_FORMAT_DEFAULT; + ref_storage_format = REF_STORAGE_FORMAT_DEFAULT; } - - ref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT); - if (ref_backend_uri) { - enum ref_storage_format format; - char *payload; - - format = ref_storage_format_by_uri(ref_backend_uri, &payload); - if (format == REF_STORAGE_FORMAT_UNKNOWN) - die(_("unknown ref storage format: '%s'"), ref_backend_uri); - - repo_fmt->ref_storage_format = format; - free(repo_fmt->ref_storage_payload); - repo_fmt->ref_storage_payload = payload; - } + /* + * If we have a preexisting repository we need to verify that its + * current ref storage format does not change. + */ + if (repo_fmt->version >= 0) { + if (ref_storage_format != repo_fmt->ref_storage_format) + die(_("attempt to reinitialize repository with different reference storage format")); + if ((ref_storage_payload || repo_fmt->ref_storage_payload) && + strcmp(ref_storage_payload ? ref_storage_payload : "", + repo_fmt->ref_storage_payload ? repo_fmt->ref_storage_payload : "")) + die(_("attempt to reinitialize repository with different reference storage payload")); + } + + free(repo_fmt->ref_storage_payload); + repo_fmt->ref_storage_format = ref_storage_format; + repo_fmt->ref_storage_payload = ref_storage_payload; } int init_db(struct repository *repo, diff --git a/t/t0001-init.sh b/t/t0001-init.sh index 6f4431bed7e76e..ca44dfc1ce5b46 100755 --- a/t/t0001-init.sh +++ b/t/t0001-init.sh @@ -643,12 +643,22 @@ test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage with unknown back test_expect_success 'init with GIT_DEFAULT_REF_FORMAT=garbage' ' test_when_finished "rm -rf refformat" && cat >expect <<-EOF && - fatal: unknown ref storage format ${SQ}garbage${SQ} + fatal: unknown ref storage format specified via GIT_DEFAULT_REF_FORMAT: ${SQ}garbage${SQ} EOF test_must_fail env GIT_DEFAULT_REF_FORMAT=garbage git init refformat 2>err && test_cmp expect err ' +test_expect_success 'GIT_REFERENCE_BACKEND refuses to reinitialize with different storage format' ' + test_when_finished "rm -rf refbackend" && + git init --ref-storage-format=files refbackend && + cat >expect <<-EOF && + fatal: attempt to reinitialize repository with different reference storage format + EOF + test_must_fail env GIT_REFERENCE_BACKEND=reftable git init refbackend 2>err && + test_cmp expect err +' + test_expect_success 'init warns about invalid init.defaultRefFormat' ' test_when_finished "rm -rf repo" && test_config_global init.defaultRefFormat garbage && From 768ec528646c79d98a511b6f55909405441b70d0 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 9 Sep 2026 13:12:56 +0200 Subject: [PATCH 018/155] setup: rename ref storage format environment variables With the same reasoning as for git-init(1), rename the environment variables GIT_REFERENCE_BACKEND and GIT_DEFAULT_REF_FORMAT to GIT_REF_STORAGE_FORMAT and GIT_DEFAULT_REF_STORAGE_FORMAT, respectively. The old names are kept as an alias to retain compatibility. While at it, fix indentation for `GIT_REF_STORAGE_FORMAT` docs to use tabs instead of spaces. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/config/init.adoc | 2 +- Documentation/git.adoc | 16 ++++++--- environment.h | 1 + setup.c | 28 +++++++++++---- t/t0001-init.sh | 50 +++++++++++++------------- t/t1419-exclude-refs.sh | 16 ++++----- t/t1423-ref-backend.sh | 18 +++++----- t/t7424-submodule-mixed-ref-formats.sh | 14 ++++---- t/test-lib.sh | 8 ++--- 9 files changed, 88 insertions(+), 65 deletions(-) diff --git a/Documentation/config/init.adoc b/Documentation/config/init.adoc index a048f0bddcac0b..f82fcf25e63efd 100644 --- a/Documentation/config/init.adoc +++ b/Documentation/config/init.adoc @@ -16,7 +16,7 @@ endif::[] `init.defaultRefFormat`:: Allows overriding the default ref storage format for new repositories. See `--ref-storage-format=` in linkgit:git-init[1]. Both the command line - option and the `GIT_DEFAULT_REF_FORMAT` environment variable take + option and the `GIT_DEFAULT_REF_STORAGE_FORMAT` environment variable take precedence over this config. init.defaultSubmodulePathConfig:: diff --git a/Documentation/git.adoc b/Documentation/git.adoc index 23ba65656e6965..cee0da28db028d 100644 --- a/Documentation/git.adoc +++ b/Documentation/git.adoc @@ -590,15 +590,21 @@ double-quotes and respecting backslash escapes. E.g., the value is always used. The default is "sha1". See `--object-format` in linkgit:git-init[1]. -`GIT_DEFAULT_REF_FORMAT`:: - If this variable is set, the default reference backend format for new +`GIT_DEFAULT_REF_STORAGE_FORMAT`:: + If this variable is set, the default ref storage format for new repositories will be set to this value. The default is "files". See `--ref-storage-format` in linkgit:git-init[1]. +`GIT_DEFAULT_REF_FORMAT`:: + Deprecated alias of `GIT_DEFAULT_REF_STORAGE_FORMAT`. + +`GIT_REF_STORAGE_FORMAT`:: + Specify which ref storage format to use along with its URI. + See `extensions.refStorage` option in linkgit:git-config[1] for more + details. Overrides the config variable when used. + `GIT_REFERENCE_BACKEND`:: - Specify which reference backend to be used along with its URI. - See `extensions.refStorage` option in linkgit:git-config[1] for more - details. Overrides the config variable when used. + Deprecated alias of `GIT_REF_STORAGE_FORMAT`. Git Commits ~~~~~~~~~~~ diff --git a/environment.h b/environment.h index e7ec5b0437342d..4bd8f08dee23f9 100644 --- a/environment.h +++ b/environment.h @@ -44,6 +44,7 @@ #define GIT_TEXT_DOMAIN_DIR_ENVIRONMENT "GIT_TEXTDOMAINDIR" #define GIT_ATTR_SOURCE_ENVIRONMENT "GIT_ATTR_SOURCE" #define GIT_REFERENCE_BACKEND_ENVIRONMENT "GIT_REFERENCE_BACKEND" +#define GIT_REF_STORAGE_FORMAT_ENVIRONMENT "GIT_REF_STORAGE_FORMAT" /* * Environment variable used to propagate the --no-advice global option to the diff --git a/setup.c b/setup.c index 38fa5e854ca610..621cf87c9cf24c 100644 --- a/setup.c +++ b/setup.c @@ -2049,7 +2049,9 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok) * The env variable should override the repository config * for 'extensions.refStorage'. */ - ref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT); + ref_backend_uri = getenv(GIT_REF_STORAGE_FORMAT_ENVIRONMENT); + if (!ref_backend_uri) + ref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT); if (ref_backend_uri) { FREE_AND_NULL(discovery.format.ref_storage_payload); discovery.format.ref_storage_format = @@ -2769,22 +2771,31 @@ static void repository_format_configure(struct repository_format *repo_fmt, * --ref-storage-format=`. * * 2. Explicit override via the environment with - * GIT_REFERENCE_BACKEND. + * "GIT_REF_STORAGE_FORMAT". * - * 3. Existing repository format. All the subsequent sources only + * 3. Its deprecated equivalent "GIT_REFERENCE_BACKEND". + * + * 4. Existing repository format. All the subsequent sources only * kick in when there is no repository yet. * * 4. The default ref storage format for new repositories as - * configured via "GIT_DEFAULT_REF_FORMAT". + * configured via "GIT_DEFAULT_REF_STORAGE_FORMAT". + * + * 5. Its deprecated equivalent "GIT_DEFAULT_REF_FORMAT". * - * 5. The default ref storage format for new repositories as + * 6. The default ref storage format for new repositories as * configured via "init.defaultRefFormat" * - * 6. Otherwise, we fall back to the default ref storage format + * 7. Otherwise, we fall back to the default ref storage format * compiled into Git. */ if (ref_storage_format != REF_STORAGE_FORMAT_UNKNOWN) { /* nothing to do */ + } else if ((env = getenv(GIT_REF_STORAGE_FORMAT_ENVIRONMENT))) { + ref_storage_format = ref_storage_format_by_uri(env, &ref_storage_payload); + if (ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN) + die(_("unknown ref storage format specified via %s: '%s'"), + GIT_REF_STORAGE_FORMAT_ENVIRONMENT, env); } else if ((env = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT))) { ref_storage_format = ref_storage_format_by_uri(env, &ref_storage_payload); if (ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN) @@ -2793,6 +2804,11 @@ static void repository_format_configure(struct repository_format *repo_fmt, } else if (repo_fmt->version >= 0) { ref_storage_format = repo_fmt->ref_storage_format; ref_storage_payload = xstrdup_or_null(repo_fmt->ref_storage_payload); + } else if ((env = getenv("GIT_DEFAULT_REF_STORAGE_FORMAT"))) { + ref_storage_format = ref_storage_format_by_name(env); + if (ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN) + die(_("unknown ref storage format specified via %s: '%s'"), + "GIT_DEFAULT_REF_STORAGE_FORMAT", env); } else if ((env = getenv("GIT_DEFAULT_REF_FORMAT"))) { ref_storage_format = ref_storage_format_by_name(env); if (ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN) diff --git a/t/t0001-init.sh b/t/t0001-init.sh index ca44dfc1ce5b46..af9a7ba958db93 100755 --- a/t/t0001-init.sh +++ b/t/t0001-init.sh @@ -640,22 +640,22 @@ test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage with unknown back test_grep "invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}garbage${SQ}" err ' -test_expect_success 'init with GIT_DEFAULT_REF_FORMAT=garbage' ' +test_expect_success 'init with GIT_DEFAULT_REF_STORAGE_FORMAT=garbage' ' test_when_finished "rm -rf refformat" && cat >expect <<-EOF && - fatal: unknown ref storage format specified via GIT_DEFAULT_REF_FORMAT: ${SQ}garbage${SQ} + fatal: unknown ref storage format specified via GIT_DEFAULT_REF_STORAGE_FORMAT: ${SQ}garbage${SQ} EOF - test_must_fail env GIT_DEFAULT_REF_FORMAT=garbage git init refformat 2>err && + test_must_fail env GIT_DEFAULT_REF_STORAGE_FORMAT=garbage git init refformat 2>err && test_cmp expect err ' -test_expect_success 'GIT_REFERENCE_BACKEND refuses to reinitialize with different storage format' ' +test_expect_success 'GIT_REF_STORAGE_FORMAT refuses to reinitialize with different storage format' ' test_when_finished "rm -rf refbackend" && git init --ref-storage-format=files refbackend && cat >expect <<-EOF && fatal: attempt to reinitialize repository with different reference storage format EOF - test_must_fail env GIT_REFERENCE_BACKEND=reftable git init refbackend 2>err && + test_must_fail env GIT_REF_STORAGE_FORMAT=reftable git init refbackend 2>err && test_cmp expect err ' @@ -668,14 +668,14 @@ test_expect_success 'init warns about invalid init.defaultRefFormat' ' test_cmp expect err && git -C repo rev-parse --show-ref-storage-format >actual && - echo $GIT_DEFAULT_REF_FORMAT >expected && + echo $GIT_DEFAULT_REF_STORAGE_FORMAT >expected && test_cmp expected actual ' test_expect_success 'default ref format' ' test_when_finished "rm -rf refformat" && ( - sane_unset GIT_DEFAULT_REF_FORMAT && + sane_unset GIT_DEFAULT_REF_STORAGE_FORMAT && git init refformat ) && git version --build-options | sed -ne "s/^default-ref-storage-format: //p" >expect && @@ -686,9 +686,9 @@ test_expect_success 'default ref format' ' backends="files reftable" for format in $backends do - test_expect_success DEFAULT_REPO_FORMAT "init with GIT_DEFAULT_REF_FORMAT=$format" ' + test_expect_success DEFAULT_REPO_FORMAT "init with GIT_DEFAULT_REF_STORAGE_FORMAT=$format" ' test_when_finished "rm -rf refformat" && - GIT_DEFAULT_REF_FORMAT=$format git init refformat && + GIT_DEFAULT_REF_STORAGE_FORMAT=$format git init refformat && if test $format = files then @@ -718,7 +718,7 @@ do test_when_finished "rm -rf refformat" && test_config_global init.defaultRefFormat $format && ( - sane_unset GIT_DEFAULT_REF_FORMAT && + sane_unset GIT_DEFAULT_REF_STORAGE_FORMAT && git init refformat ) && @@ -727,37 +727,37 @@ do test_cmp expect actual ' - test_expect_success "--ref-storage-format=$format overrides GIT_DEFAULT_REF_FORMAT" ' + test_expect_success "--ref-storage-format=$format overrides GIT_DEFAULT_REF_STORAGE_FORMAT" ' test_when_finished "rm -rf refformat" && - GIT_DEFAULT_REF_FORMAT=garbage git init --ref-storage-format=$format refformat && + GIT_DEFAULT_REF_STORAGE_FORMAT=garbage git init --ref-storage-format=$format refformat && echo $format >expect && git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' - test_expect_success "reinit repository with GIT_DEFAULT_REF_FORMAT=$format does not change format" ' + test_expect_success "reinit repository with GIT_DEFAULT_REF_STORAGE_FORMAT=$format does not change format" ' test_when_finished "rm -rf refformat" && git init refformat && git -C refformat rev-parse --show-ref-storage-format >expect && - GIT_DEFAULT_REF_FORMAT=$format git init refformat && + GIT_DEFAULT_REF_STORAGE_FORMAT=$format git init refformat && git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' done -test_expect_success "--ref-storage-format= overrides GIT_DEFAULT_REF_FORMAT" ' +test_expect_success "--ref-storage-format= overrides GIT_DEFAULT_REF_STORAGE_FOMAT" ' test_when_finished "rm -rf refformat" && - GIT_DEFAULT_REF_FORMAT=files git init --ref-storage-format=reftable refformat && + GIT_DEFAULT_REF_STORAGE_FORMAT=files git init --ref-storage-format=reftable refformat && echo reftable >expect && git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' -test_expect_success "GIT_DEFAULT_REF_FORMAT= overrides init.defaultRefFormat" ' +test_expect_success "GIT_DEFAULT_REF_STORAGE_FORMAT= overrides init.defaultRefFormat" ' test_when_finished "rm -rf refformat" && test_config_global init.defaultRefFormat files && - GIT_DEFAULT_REF_FORMAT=reftable git init refformat && + GIT_DEFAULT_REF_STORAGE_FORMAT=reftable git init refformat && echo reftable >expect && git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual @@ -767,7 +767,7 @@ test_expect_success "init with feature.experimental=true" ' test_when_finished "rm -rf refformat" && test_config_global feature.experimental true && ( - sane_unset GIT_DEFAULT_REF_FORMAT && + sane_unset GIT_DEFAULT_REF_STORAGE_FORMAT && git init refformat ) && echo reftable >expect && @@ -780,7 +780,7 @@ test_expect_success "init.defaultRefFormat overrides feature.experimental=true" test_config_global feature.experimental true && test_config_global init.defaultRefFormat files && ( - sane_unset GIT_DEFAULT_REF_FORMAT && + sane_unset GIT_DEFAULT_REF_STORAGE_FORMAT && git init refformat ) && echo files >expect && @@ -788,10 +788,10 @@ test_expect_success "init.defaultRefFormat overrides feature.experimental=true" test_cmp expect actual ' -test_expect_success "GIT_DEFAULT_REF_FORMAT= overrides feature.experimental=true" ' +test_expect_success "GIT_DEFAULT_REF_STORAGE_FORMAT= overrides feature.experimental=true" ' test_when_finished "rm -rf refformat" && test_config_global feature.experimental true && - GIT_DEFAULT_REF_FORMAT=files git init refformat && + GIT_DEFAULT_REF_STORAGE_FORMAT=files git init refformat && echo files >expect && git -C refformat rev-parse --show-ref-storage-format >actual && test_cmp expect actual @@ -942,7 +942,7 @@ test_expect_success 'branch -m with the initial branch' ' test_expect_success 'init with includeIf.onbranch condition' ' test_when_finished "rm -rf repo" && git -c includeIf.onbranch:main.path=nonexistent init repo && - echo $GIT_DEFAULT_REF_FORMAT >expect && + echo $GIT_DEFAULT_REF_STORAGE_FORMAT >expect && git -C repo rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' @@ -951,7 +951,7 @@ test_expect_success 'init with includeIf.onbranch condition with existing direct test_when_finished "rm -rf repo" && mkdir repo && git -c includeIf.onbranch:nonexistent.path=/does/not/exist init repo && - echo $GIT_DEFAULT_REF_FORMAT >expect && + echo $GIT_DEFAULT_REF_STORAGE_FORMAT >expect && git -C repo rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' @@ -960,7 +960,7 @@ test_expect_success 're-init with includeIf.onbranch condition' ' test_when_finished "rm -rf repo" && git init repo && git -c includeIf.onbranch:nonexistent.path=/does/not/exist init repo && - echo $GIT_DEFAULT_REF_FORMAT >expect && + echo $GIT_DEFAULT_REF_STORAGE_FORMAT >expect && git -C repo rev-parse --show-ref-storage-format >actual && test_cmp expect actual ' diff --git a/t/t1419-exclude-refs.sh b/t/t1419-exclude-refs.sh index 04797aee59e03c..50237a5aeb0064 100755 --- a/t/t1419-exclude-refs.sh +++ b/t/t1419-exclude-refs.sh @@ -21,13 +21,13 @@ assert_jumps () { local nr="$1" local trace="$2" - case "$GIT_DEFAULT_REF_FORMAT" in + case "$GIT_DEFAULT_REF_STORAGE_FORMAT" in files) grep -q "name:jumps_made value:$nr$" $trace;; reftable) grep -q "name:reseeks_made value:$nr$" $trace;; *) - BUG "unhandled ref format $GIT_DEFAULT_REF_FORMAT";; + BUG "unhandled ref storage format $GIT_DEFAULT_REF_STORAGE_FORMAT";; esac } @@ -93,13 +93,13 @@ test_expect_success 'adjacent, non-overlapping excluded regions' ' for_each_ref refs/heads/foo refs/heads/quux >expect && test_cmp expect actual && - case "$GIT_DEFAULT_REF_FORMAT" in + case "$GIT_DEFAULT_REF_STORAGE_FORMAT" in files) assert_jumps 1 perf;; reftable) assert_jumps 2 perf;; *) - BUG "unhandled ref format $GIT_DEFAULT_REF_FORMAT";; + BUG "unhandled ref storage format $GIT_DEFAULT_REF_STORAGE_FORMAT";; esac ' @@ -125,13 +125,13 @@ test_expect_success 'several overlapping excluded regions' ' for_each_ref refs/heads/quux >expect && test_cmp expect actual && - case "$GIT_DEFAULT_REF_FORMAT" in + case "$GIT_DEFAULT_REF_STORAGE_FORMAT" in files) assert_jumps 1 perf;; reftable) assert_jumps 3 perf;; *) - BUG "unhandled ref format $GIT_DEFAULT_REF_FORMAT";; + BUG "unhandled ref storage format $GIT_DEFAULT_REF_STORAGE_FORMAT";; esac ' @@ -141,13 +141,13 @@ test_expect_success 'unordered excludes' ' for_each_ref refs/heads/bar refs/heads/quux >expect && test_cmp expect actual && - case "$GIT_DEFAULT_REF_FORMAT" in + case "$GIT_DEFAULT_REF_STORAGE_FORMAT" in files) assert_jumps 1 perf;; reftable) assert_jumps 2 perf;; *) - BUG "unhandled ref format $GIT_DEFAULT_REF_FORMAT";; + BUG "unhandled ref storage format $GIT_DEFAULT_REF_STORAGE_FORMAT";; esac ' diff --git a/t/t1423-ref-backend.sh b/t/t1423-ref-backend.sh index ab119a6568bd98..525b2a19b4bca6 100755 --- a/t/t1423-ref-backend.sh +++ b/t/t1423-ref-backend.sh @@ -12,7 +12,7 @@ test_description='Test reference backend URIs' # is the new URI to be set for the ref storage. # is the git subcommand to be run in the repository. # if 'config', set the backend via the 'extensions.refStorage' config. -# if 'env', set the backend via the 'GIT_REFERENCE_BACKEND' env. +# if 'env', set the backend via the 'GIT_REF_STORAGE_FORMAT' env. run_with_uri () { repo=$1 && backend=$2 && @@ -23,7 +23,7 @@ run_with_uri () { git -C "$repo" config set core.repositoryformatversion 1 && if test "$via" = "env" then - test_env GIT_REFERENCE_BACKEND="$uri" git -C "$repo" $cmd + test_env GIT_REF_STORAGE_FORMAT="$uri" git -C "$repo" $cmd elif test "$via" = "config" then git -C "$repo" config set extensions.refStorage "$uri" && @@ -40,7 +40,7 @@ run_with_uri () { # is the original ref storage of the repo. # is the new URI to be set for the ref storage. # if 'config', set the backend via the 'extensions.refStorage' config. -# if 'env', set the backend via the 'GIT_REFERENCE_BACKEND' env. +# if 'env', set the backend via the 'GIT_REF_STORAGE_FORMAT' env. # (optional) if set, check if 'git-refs(1)' failed with the provided msg. test_refs_backend () { repo=$1 && @@ -54,7 +54,7 @@ test_refs_backend () { then if test "$via" = "env" then - test_env GIT_REFERENCE_BACKEND="$uri" test_must_fail git -C "$repo" refs list 2>err + test_env GIT_REF_STORAGE_FORMAT="$uri" test_must_fail git -C "$repo" refs list 2>err elif test "$via" = "config" then git -C "$repo" config set extensions.refStorage "$uri" && @@ -83,7 +83,7 @@ verify_files_exist () { test_cmp expect $gitdir/HEAD # verify that backend specific files exist. - case "$GIT_DEFAULT_REF_FORMAT" in + case "$GIT_DEFAULT_REF_STORAGE_FORMAT" in files) test_path_is_dir $refdir/refs/heads && test_path_is_file $refdir/HEAD;; @@ -91,7 +91,7 @@ verify_files_exist () { test_path_is_dir $refdir/reftable && test_path_is_file $refdir/reftable/tables.list;; *) - BUG "unhandled ref format $GIT_DEFAULT_REF_FORMAT";; + BUG "unhandled ref storage format $GIT_DEFAULT_REF_STORAGE_FORMAT";; esac } @@ -210,7 +210,7 @@ do test_expect_success "migrating repository to $to_format with alternate refs directory" ' test_when_finished "rm -rf repo refdir" && mkdir refdir && - GIT_REFERENCE_BACKEND="${from_format}://$(pwd)/refdir" git init repo && + GIT_REF_STORAGE_FORMAT="${from_format}://$(pwd)/refdir" git init repo && ( cd repo && @@ -235,7 +235,7 @@ test_expect_success 'initializing repository with alt ref directory' ' test_when_finished "rm -rf repo refdir" && mkdir refdir && BACKEND="$(test_detect_ref_format)://$(pwd)/refdir" && - GIT_REFERENCE_BACKEND=$BACKEND git init repo && + GIT_REF_STORAGE_FORMAT=$BACKEND git init repo && verify_files_exist repo/.git refdir && ( cd repo && @@ -264,7 +264,7 @@ test_expect_success 'cloning repository with alt ref directory' ' test_commit -C source 3 && BACKEND="$(test_detect_ref_format)://$(pwd)/refdir" && - GIT_REFERENCE_BACKEND=$BACKEND git clone source repo && + GIT_REF_STORAGE_FORMAT=$BACKEND git clone source repo && git -C repo config get extensions.refstorage >actual && echo $BACKEND >expect && diff --git a/t/t7424-submodule-mixed-ref-formats.sh b/t/t7424-submodule-mixed-ref-formats.sh index 9707744644855b..197c02bcf02d5c 100755 --- a/t/t7424-submodule-mixed-ref-formats.sh +++ b/t/t7424-submodule-mixed-ref-formats.sh @@ -12,7 +12,7 @@ test_ref_format () { for OTHER_FORMAT in files reftable do - if test "$OTHER_FORMAT" = "$GIT_DEFAULT_REF_FORMAT" + if test "$OTHER_FORMAT" = "$GIT_DEFAULT_REF_STORAGE_FORMAT" then continue fi @@ -43,7 +43,7 @@ test_expect_success 'add submodules with different ref storage format' ' git init submodule && test_commit -C submodule submodule-initial && git init upstream && - test_ref_format upstream "$GIT_DEFAULT_REF_FORMAT" && + test_ref_format upstream "$GIT_DEFAULT_REF_STORAGE_FORMAT" && git -C upstream submodule add --ref-storage-format="$OTHER_FORMAT" "file://$(pwd)/submodule" && test_ref_format upstream/submodule "$OTHER_FORMAT" ' @@ -59,8 +59,8 @@ test_expect_success 'recursive clone propagates ref storage format' ' # The upstream repository and its submodule should be using the default # ref format. - test_ref_format upstream "$GIT_DEFAULT_REF_FORMAT" && - test_ref_format upstream/submodule "$GIT_DEFAULT_REF_FORMAT" && + test_ref_format upstream "$GIT_DEFAULT_REF_STORAGE_FORMAT" && + test_ref_format upstream/submodule "$GIT_DEFAULT_REF_STORAGE_FORMAT" && # The cloned repositories should use the other ref format that we have # specified via `--ref-storage-format`. The option should propagate to cloned @@ -81,7 +81,7 @@ test_expect_success 'clone submodules with different ref storage format' ' git -C upstream commit -m "upstream submodule" && git clone --no-recurse-submodules "file://$(pwd)/upstream" downstream && - test_ref_format downstream "$GIT_DEFAULT_REF_FORMAT" && + test_ref_format downstream "$GIT_DEFAULT_REF_STORAGE_FORMAT" && git -C downstream submodule update --init --ref-storage-format=$OTHER_FORMAT && test_ref_format downstream/submodule "$OTHER_FORMAT" ' @@ -98,7 +98,7 @@ test_expect_success 'status with mixed submodule ref storages' ' # The main repository should use the default ref format now, whereas # the submodule should use the other format. - test_ref_format main "$GIT_DEFAULT_REF_FORMAT" && + test_ref_format main "$GIT_DEFAULT_REF_STORAGE_FORMAT" && test_ref_format main/submodule "$OTHER_FORMAT" && cat >expect <<-EOF && @@ -123,7 +123,7 @@ test_expect_success 'recursive pull with mixed formats' ' # submodules have different formats. git clone --no-recurse-submodules "file://$(pwd)/upstream" downstream && git -C downstream submodule update --init --ref-storage-format=$OTHER_FORMAT && - test_ref_format downstream "$GIT_DEFAULT_REF_FORMAT" && + test_ref_format downstream "$GIT_DEFAULT_REF_STORAGE_FORMAT" && test_ref_format downstream/submodule "$OTHER_FORMAT" && # Update the upstream submodule as well as the owning repository such diff --git a/t/test-lib.sh b/t/test-lib.sh index 1f0505e412d8ff..60a2179a0a3bc4 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -575,8 +575,8 @@ export EDITOR GIT_TEST_BUILTIN_HASH=$("$GIT_BINARY" version --build-options | sed -ne 's/^default-hash: //p') GIT_DEFAULT_HASH="${GIT_TEST_DEFAULT_HASH:-$GIT_TEST_BUILTIN_HASH}" export GIT_DEFAULT_HASH -GIT_DEFAULT_REF_FORMAT="${GIT_TEST_DEFAULT_REF_FORMAT:-files}" -export GIT_DEFAULT_REF_FORMAT +GIT_DEFAULT_REF_STORAGE_FORMAT="${GIT_TEST_DEFAULT_REF_FORMAT:-files}" +export GIT_DEFAULT_REF_STORAGE_FORMAT # Tests using GIT_TRACE typically don't want : output GIT_TRACE_BARE=1 @@ -1752,13 +1752,13 @@ parisc* | hppa*) ;; esac -case "$GIT_DEFAULT_REF_FORMAT" in +case "$GIT_DEFAULT_REF_STORAGE_FORMAT" in files) test_set_prereq REFFILES;; reftable) test_set_prereq REFTABLE;; *) - echo 2>&1 "error: unknown ref format $GIT_DEFAULT_REF_FORMAT" + echo 2>&1 "error: unknown ref storage format $GIT_DEFAULT_REF_STORAGE_FORMAT" exit 1 ;; esac From 3a58de10ad2c43b6c60e35c0d8601fdf807ee235 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 9 Sep 2026 13:12:57 +0200 Subject: [PATCH 019/155] t: rename GIT_TEST_DEFAULT_REF_FORMAT With the same reasoning as for all the other environment variables, rename the GIT_TEST_DEFAULT_REF_FORMAT environment variable to GIT_TEST_DEFAULT_REF_STORAGE_FORMAT. Note that in this case, the old name is not retained for backwards compatibility as this is only running in the context of tests anyway. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- ci/run-build-and-tests.sh | 2 +- t/README | 4 ++-- t/t0600-reffiles-backend.sh | 4 ++-- t/t0601-reffiles-pack-refs.sh | 4 ++-- t/t0602-reffiles-fsck.sh | 4 ++-- t/t0610-reftable-basics.sh | 4 ++-- t/t0612-reftable-jgit-compatibility.sh | 4 ++-- t/t0613-reftable-write-options.sh | 4 ++-- t/t0614-reftable-fsck.sh | 4 ++-- t/t1463-refs-optimize.sh | 4 ++-- t/test-lib-functions.sh | 2 +- t/test-lib.sh | 2 +- 12 files changed, 21 insertions(+), 21 deletions(-) diff --git a/ci/run-build-and-tests.sh b/ci/run-build-and-tests.sh index 1d9a0a736d255b..6a3b43b366048d 100755 --- a/ci/run-build-and-tests.sh +++ b/ci/run-build-and-tests.sh @@ -45,7 +45,7 @@ linux-sha256) export GIT_TEST_DEFAULT_HASH=sha256 ;; linux-reftable|linux-reftable-leaks|osx-reftable) - export GIT_TEST_DEFAULT_REF_FORMAT=reftable + export GIT_TEST_DEFAULT_REF_STORAGE_FORMAT=reftable ;; esac diff --git a/t/README b/t/README index 9a9daaf2afe5e2..42716bb14ab0e6 100644 --- a/t/README +++ b/t/README @@ -448,8 +448,8 @@ GIT_TEST_DEFAULT_HASH= specifies which hash algorithm to use in the test scripts. Recognized values for are "sha1" and "sha256". -GIT_TEST_DEFAULT_REF_FORMAT= specifies which ref storage format to use -in the test scripts. Recognized values for are "files" and +GIT_TEST_DEFAULT_REF_STORAGE_FORMAT= specifies which ref storage format +to use in the test scripts. Recognized values for are "files" and "reftable". GIT_TEST_NO_WRITE_REV_INDEX=, when true disables the diff --git a/t/t0600-reffiles-backend.sh b/t/t0600-reffiles-backend.sh index bbbf6fa4228cd7..23e38a16958440 100755 --- a/t/t0600-reffiles-backend.sh +++ b/t/t0600-reffiles-backend.sh @@ -4,8 +4,8 @@ test_description='Test reffiles backend' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME -GIT_TEST_DEFAULT_REF_FORMAT=files -export GIT_TEST_DEFAULT_REF_FORMAT +GIT_TEST_DEFAULT_REF_STORAGE_FORMAT=files +export GIT_TEST_DEFAULT_REF_STORAGE_FORMAT . ./test-lib.sh diff --git a/t/t0601-reffiles-pack-refs.sh b/t/t0601-reffiles-pack-refs.sh index 3c706978efc219..17283f8beacbb7 100755 --- a/t/t0601-reffiles-pack-refs.sh +++ b/t/t0601-reffiles-pack-refs.sh @@ -12,8 +12,8 @@ semantic is still the same. GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME -GIT_TEST_DEFAULT_REF_FORMAT=files -export GIT_TEST_DEFAULT_REF_FORMAT +GIT_TEST_DEFAULT_REF_STORAGE_FORMAT=files +export GIT_TEST_DEFAULT_REF_STORAGE_FORMAT . ./test-lib.sh diff --git a/t/t0602-reffiles-fsck.sh b/t/t0602-reffiles-fsck.sh index 13259821a02ef1..10781417fd739b 100755 --- a/t/t0602-reffiles-fsck.sh +++ b/t/t0602-reffiles-fsck.sh @@ -4,8 +4,8 @@ test_description='Test reffiles backend consistency check' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME -GIT_TEST_DEFAULT_REF_FORMAT=files -export GIT_TEST_DEFAULT_REF_FORMAT +GIT_TEST_DEFAULT_REF_STORAGE_FORMAT=files +export GIT_TEST_DEFAULT_REF_STORAGE_FORMAT . ./test-lib.sh diff --git a/t/t0610-reftable-basics.sh b/t/t0610-reftable-basics.sh index b6db54430ad0c2..065d6a69b45e10 100755 --- a/t/t0610-reftable-basics.sh +++ b/t/t0610-reftable-basics.sh @@ -7,8 +7,8 @@ test_description='reftable basics' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME -GIT_TEST_DEFAULT_REF_FORMAT=reftable -export GIT_TEST_DEFAULT_REF_FORMAT +GIT_TEST_DEFAULT_REF_STORAGE_FORMAT=reftable +export GIT_TEST_DEFAULT_REF_STORAGE_FORMAT . ./test-lib.sh diff --git a/t/t0612-reftable-jgit-compatibility.sh b/t/t0612-reftable-jgit-compatibility.sh index 7df2ad5817e745..eddfd9a1524bb8 100755 --- a/t/t0612-reftable-jgit-compatibility.sh +++ b/t/t0612-reftable-jgit-compatibility.sh @@ -4,8 +4,8 @@ test_description='reftables are compatible with JGit' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME -GIT_TEST_DEFAULT_REF_FORMAT=reftable -export GIT_TEST_DEFAULT_REF_FORMAT +GIT_TEST_DEFAULT_REF_STORAGE_FORMAT=reftable +export GIT_TEST_DEFAULT_REF_STORAGE_FORMAT # JGit does not support the 'link' DIRC extension. GIT_TEST_SPLIT_INDEX=0 diff --git a/t/t0613-reftable-write-options.sh b/t/t0613-reftable-write-options.sh index a65960d0488640..0b27d75fe3c08e 100755 --- a/t/t0613-reftable-write-options.sh +++ b/t/t0613-reftable-write-options.sh @@ -2,8 +2,8 @@ test_description='reftable write options' -GIT_TEST_DEFAULT_REF_FORMAT=reftable -export GIT_TEST_DEFAULT_REF_FORMAT +GIT_TEST_DEFAULT_REF_STORAGE_FORMAT=reftable +export GIT_TEST_DEFAULT_REF_STORAGE_FORMAT # Disable auto-compaction for all tests as we explicitly control repacking of # refs. GIT_TEST_REFTABLE_AUTOCOMPACTION=false diff --git a/t/t0614-reftable-fsck.sh b/t/t0614-reftable-fsck.sh index d24b87f9611975..9576a8247836ad 100755 --- a/t/t0614-reftable-fsck.sh +++ b/t/t0614-reftable-fsck.sh @@ -2,8 +2,8 @@ test_description='Test reftable backend consistency check' -GIT_TEST_DEFAULT_REF_FORMAT=reftable -export GIT_TEST_DEFAULT_REF_FORMAT +GIT_TEST_DEFAULT_REF_STORAGE_FORMAT=reftable +export GIT_TEST_DEFAULT_REF_STORAGE_FORMAT . ./test-lib.sh diff --git a/t/t1463-refs-optimize.sh b/t/t1463-refs-optimize.sh index 9afe3c1ed7e33a..b4b97358283306 100755 --- a/t/t1463-refs-optimize.sh +++ b/t/t1463-refs-optimize.sh @@ -8,8 +8,8 @@ semantic is still the same. GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME -GIT_TEST_DEFAULT_REF_FORMAT=files -export GIT_TEST_DEFAULT_REF_FORMAT +GIT_TEST_DEFAULT_REF_STORAGE_FORMAT=files +export GIT_TEST_DEFAULT_REF_STORAGE_FORMAT . ./test-lib.sh diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh index ed9779b0f73310..09a41d51adcd22 100644 --- a/t/test-lib-functions.sh +++ b/t/test-lib-functions.sh @@ -1737,7 +1737,7 @@ test_detect_hash () { # Detect the ref format in use. test_detect_ref_format () { - echo "${GIT_TEST_DEFAULT_REF_FORMAT:-files}" + echo "${GIT_TEST_DEFAULT_REF_STORAGE_FORMAT:-files}" } # Load common hash metadata and common placeholder object IDs for use with diff --git a/t/test-lib.sh b/t/test-lib.sh index 60a2179a0a3bc4..321c2ba33944ed 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -575,7 +575,7 @@ export EDITOR GIT_TEST_BUILTIN_HASH=$("$GIT_BINARY" version --build-options | sed -ne 's/^default-hash: //p') GIT_DEFAULT_HASH="${GIT_TEST_DEFAULT_HASH:-$GIT_TEST_BUILTIN_HASH}" export GIT_DEFAULT_HASH -GIT_DEFAULT_REF_STORAGE_FORMAT="${GIT_TEST_DEFAULT_REF_FORMAT:-files}" +GIT_DEFAULT_REF_STORAGE_FORMAT="${GIT_TEST_DEFAULT_REF_STORAGE_FORMAT:-files}" export GIT_DEFAULT_REF_STORAGE_FORMAT # Tests using GIT_TRACE typically don't want : output From 53d995c53fb100920464c0709389f4fa55c6f215 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 9 Sep 2026 13:12:58 +0200 Subject: [PATCH 020/155] setup: rename "init.defaultRefFormat" to "init.defaultRefStorageFormat" With the same reasoning as for git-init(1), rename the "init.defaultRefFormat" config option to "init.defaultRefStorageFormat" and keep the old name as an alias. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/BreakingChanges.adoc | 2 +- Documentation/config/feature.adoc | 2 +- Documentation/config/init.adoc | 4 +++- setup.c | 8 +++++--- t/t0001-init.sh | 16 ++++++++-------- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Documentation/BreakingChanges.adoc b/Documentation/BreakingChanges.adoc index 73bb939359c72e..c2cef3f528fcac 100644 --- a/Documentation/BreakingChanges.adoc +++ b/Documentation/BreakingChanges.adoc @@ -156,7 +156,7 @@ Cf. <2f5de416-04ba-c23d-1e0b-83bb655829a7@zombino.com>, "packed-refs" file. + Users that get immediate benefit from the "reftable" backend could continue to -opt-in to the "reftable" format manually by setting the "init.defaultRefFormat" +opt-in to the "reftable" format manually by setting the "init.defaultRefStorageFormat" config. But defaults matter, and we think that overall users will have a better experience with less platform-specific quirks when they use the new backend by default. diff --git a/Documentation/config/feature.adoc b/Documentation/config/feature.adoc index 924f5ff4e3caaa..e25877059162e0 100644 --- a/Documentation/config/feature.adoc +++ b/Documentation/config/feature.adoc @@ -25,7 +25,7 @@ reusing objects from multiple packs instead of just one. significantly smaller in the presence of certain filename collisions with Git's default name-hash. + -* `init.defaultRefFormat=reftable` causes newly initialized repositories to use +* `init.defaultRefStorageFormat=reftable` causes newly initialized repositories to use the reftable format for storing references. This new format solves issues with case-insensitive filesystems, compresses better and performs significantly better with many use cases. Refer to Documentation/technical/reftable.adoc for diff --git a/Documentation/config/init.adoc b/Documentation/config/init.adoc index f82fcf25e63efd..e45bd692dbc91e 100644 --- a/Documentation/config/init.adoc +++ b/Documentation/config/init.adoc @@ -13,11 +13,13 @@ endif::[] `--object-format=` in linkgit:git-init[1]. Both the command line option and the `GIT_DEFAULT_HASH` environment variable take precedence over this config. -`init.defaultRefFormat`:: +`init.defaultRefStorageFormat`:: Allows overriding the default ref storage format for new repositories. See `--ref-storage-format=` in linkgit:git-init[1]. Both the command line option and the `GIT_DEFAULT_REF_STORAGE_FORMAT` environment variable take precedence over this config. +`init.defaultRefFormat`:: + Deprecated alias of `init.defaultRefStorageFormat`. init.defaultSubmodulePathConfig:: A boolean that specifies if `git init` and `git clone` should diff --git a/setup.c b/setup.c index 621cf87c9cf24c..df8cbf196d9161 100644 --- a/setup.c +++ b/setup.c @@ -2697,7 +2697,8 @@ static int read_default_format_config(const char *key, const char *value, goto out; } - if (!strcmp(key, "init.defaultrefformat")) { + if (!strcmp(key, "init.defaultrefstorageformat") || + !strcmp(key, "init.defaultrefformat")) { ret = git_config_string(&str, key, value); if (ret) goto out; @@ -2709,7 +2710,7 @@ static int read_default_format_config(const char *key, const char *value, /* * Enable the reftable format when "features.experimental" is enabled. - * "init.defaultRefFormat" takes precedence over this setting. + * "init.defaultRefStorageFormat" takes precedence over this setting. */ if (!strcmp(key, "feature.experimental") && cfg->ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN && @@ -2784,7 +2785,8 @@ static void repository_format_configure(struct repository_format *repo_fmt, * 5. Its deprecated equivalent "GIT_DEFAULT_REF_FORMAT". * * 6. The default ref storage format for new repositories as - * configured via "init.defaultRefFormat" + * configured via "init.defaultRefStorageFormat" or its deprecated + * equivalent "init.defaultRefFormat". * * 7. Otherwise, we fall back to the default ref storage format * compiled into Git. diff --git a/t/t0001-init.sh b/t/t0001-init.sh index af9a7ba958db93..fe5bc3d3e17a3e 100755 --- a/t/t0001-init.sh +++ b/t/t0001-init.sh @@ -659,9 +659,9 @@ test_expect_success 'GIT_REF_STORAGE_FORMAT refuses to reinitialize with differe test_cmp expect err ' -test_expect_success 'init warns about invalid init.defaultRefFormat' ' +test_expect_success 'init warns about invalid init.defaultRefStorageFormat' ' test_when_finished "rm -rf repo" && - test_config_global init.defaultRefFormat garbage && + test_config_global init.defaultRefStorageFormat garbage && echo "warning: unknown ref storage format ${SQ}garbage${SQ}" >expect && git init repo 2>err && @@ -714,9 +714,9 @@ do test_cmp expect actual ' - test_expect_success "init with init.defaultRefFormat=$format" ' + test_expect_success "init with init.defaultRefStorageFormat=$format" ' test_when_finished "rm -rf refformat" && - test_config_global init.defaultRefFormat $format && + test_config_global init.defaultRefStorageFormat $format && ( sane_unset GIT_DEFAULT_REF_STORAGE_FORMAT && git init refformat @@ -753,9 +753,9 @@ test_expect_success "--ref-storage-format= overrides GIT_DEFAULT_REF_STORAGE_FOM test_cmp expect actual ' -test_expect_success "GIT_DEFAULT_REF_STORAGE_FORMAT= overrides init.defaultRefFormat" ' +test_expect_success "GIT_DEFAULT_REF_STORAGE_FORMAT= overrides init.defaultRefStorageFormat" ' test_when_finished "rm -rf refformat" && - test_config_global init.defaultRefFormat files && + test_config_global init.defaultRefStorageFormat files && GIT_DEFAULT_REF_STORAGE_FORMAT=reftable git init refformat && echo reftable >expect && @@ -775,10 +775,10 @@ test_expect_success "init with feature.experimental=true" ' test_cmp expect actual ' -test_expect_success "init.defaultRefFormat overrides feature.experimental=true" ' +test_expect_success "init.defaultRefStorageFormat overrides feature.experimental=true" ' test_when_finished "rm -rf refformat" && test_config_global feature.experimental true && - test_config_global init.defaultRefFormat files && + test_config_global init.defaultRefStorageFormat files && ( sane_unset GIT_DEFAULT_REF_STORAGE_FORMAT && git init refformat From dd907a4172749499de863faf97e47eadf7d7e1de Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 9 Sep 2026 13:12:59 +0200 Subject: [PATCH 021/155] setup: allow "--ref-storage-format=" to specify a payload Reference storage backends can be configured with a payload via the "extensions.refStorage" config key and the "GIT_REF_STORAGE_FORMAT" environment variable, both of which accept a URI in the format "://". The payload may contain backend-specific information, for example an alternate refs directory or which database references should be stored in. The `--ref-storage-format=` option of git-init(1) and git-clone(1) does not know about payloads though: its value is parsed as a plain format name, so backends that require a payload cannot be conveniently set up at initialization time via the command line. Teach the option to accept the same URI syntax. Also, document the optional payloads for both the "files" and "reftable" backends. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/git-clone.adoc | 5 ++++- Documentation/git-init.adoc | 5 ++++- builtin/clone.c | 12 ++++++------ builtin/init-db.c | 15 ++++----------- setup.c | 16 +++++++++++----- setup.h | 2 +- t/t0001-init.sh | 2 +- t/t1423-ref-backend.sh | 30 ++++++++++++++++++++++++++++++ 8 files changed, 61 insertions(+), 26 deletions(-) diff --git a/Documentation/git-clone.adoc b/Documentation/git-clone.adoc index 27e4d1394217a1..07f7cf1a98dd3f 100644 --- a/Documentation/git-clone.adoc +++ b/Documentation/git-clone.adoc @@ -350,7 +350,10 @@ or `--mirror` is given) `--ref-storage-format=`:: -Specify the given ref storage format for the repository. The valid values are: +Specify the given ref storage __ for the repository. Backends that +require additional configuration accept a payload in the form +`://`, for example a connection string identifying the +database that shall store the references. The valid values are: + include::ref-storage-format.adoc[] diff --git a/Documentation/git-init.adoc b/Documentation/git-init.adoc index 73e1f787cb2cad..b02af22ad11e3e 100644 --- a/Documentation/git-init.adoc +++ b/Documentation/git-init.adoc @@ -58,7 +58,10 @@ values are `sha1` and (if enabled) `sha256`. `sha1` is the default. include::object-format-disclaimer.adoc[] `--ref-storage-format=`:: -Specify the given ref storage __ for the repository. The valid values are: +Specify the given ref storage __ for the repository. Backends that +require additional configuration accept a payload in the form +`://`, for example a connection string identifying the +database that shall store the references. The valid values are: + include::ref-storage-format.adoc[] diff --git a/builtin/clone.c b/builtin/clone.c index dd722e4de179d6..9812aaec8f842c 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -901,7 +901,7 @@ int cmd_clone(int argc, char *option_origin = NULL; struct string_list option_not = STRING_LIST_INIT_NODUP; const char *real_git_dir = NULL; - const char *ref_storage_format_str = NULL; + const char *ref_storage_format_uri = NULL; const char *option_upload_pack = "git-upload-pack"; int option_progress = -1; int option_sparse_checkout = 0; @@ -981,7 +981,7 @@ int cmd_clone(int argc, N_("any cloned submodules will be shallow")), OPT_STRING(0, "separate-git-dir", &real_git_dir, N_("gitdir"), N_("separate git dir from working tree")), - OPT_STRING(0, "ref-storage-format", &ref_storage_format_str, N_("format"), + OPT_STRING(0, "ref-storage-format", &ref_storage_format_uri, N_("format"), N_("specify the reference storage format to use")), OPT_ALIAS_F(0, "ref-format", "ref-storage-format", PARSE_OPT_HIDDEN), OPT_STRING_LIST('c', "config", &option_config, N_("key=value"), @@ -1028,10 +1028,10 @@ int cmd_clone(int argc, if (option_single_branch == -1) option_single_branch = deepen ? 1 : 0; - if (ref_storage_format_str) { - ref_storage_format = ref_storage_format_by_name(ref_storage_format_str); + if (ref_storage_format_uri) { + ref_storage_format = ref_storage_format_by_uri(ref_storage_format_uri, NULL); if (ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN) - die(_("unknown ref storage format '%s'"), ref_storage_format_str); + die(_("unknown ref storage format '%s'"), ref_storage_format_uri); } if (option_mirror) { @@ -1187,7 +1187,7 @@ int cmd_clone(int argc, * their on-disk data structures. */ init_db(the_repository, git_dir, real_git_dir, work_tree, option_template, - GIT_HASH_UNKNOWN, ref_storage_format, NULL, + GIT_HASH_UNKNOWN, ref_storage_format_uri, NULL, do_not_override_repo_unix_permissions, INIT_DB_QUIET | INIT_DB_SKIP_REFDB); diff --git a/builtin/init-db.c b/builtin/init-db.c index 1612413af0dc95..7a65d1673b97e8 100644 --- a/builtin/init-db.c +++ b/builtin/init-db.c @@ -83,10 +83,9 @@ int cmd_init_db(int argc, unsigned int flags = 0; int bare = startup_info->force_bare_repository ? 1 : -1; const char *object_format = NULL; - const char *ref_storage_format_str = NULL; + const char *ref_storage_format_uri = NULL; const char *initial_branch = NULL; int hash_algo = GIT_HASH_UNKNOWN; - enum ref_storage_format ref_storage_format = REF_STORAGE_FORMAT_UNKNOWN; int init_shared_repository = -1; const struct option init_db_options[] = { OPT_STRING(0, "template", &template_dir, N_("template-directory"), @@ -109,7 +108,7 @@ int cmd_init_db(int argc, N_("override the name of the initial branch")), OPT_STRING(0, "object-format", &object_format, N_("hash"), N_("specify the hash algorithm to use")), - OPT_STRING(0, "ref-storage-format", &ref_storage_format_str, N_("format"), + OPT_STRING(0, "ref-storage-format", &ref_storage_format_uri, N_("format"), N_("specify the reference storage format to use")), OPT_ALIAS_F(0, "ref-format", "ref-storage-format", PARSE_OPT_HIDDEN), OPT_END() @@ -174,12 +173,6 @@ int cmd_init_db(int argc, die(_("unknown hash algorithm '%s'"), object_format); } - if (ref_storage_format_str) { - ref_storage_format = ref_storage_format_by_name(ref_storage_format_str); - if (ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN) - die(_("unknown ref storage format '%s'"), ref_storage_format_str); - } - if (init_shared_repository != -1) repo_settings_set_shared_repository(the_repository, init_shared_repository); @@ -250,8 +243,8 @@ int cmd_init_db(int argc, flags |= INIT_DB_EXIST_OK; ret = init_db(the_repository, git_dir, real_git_dir, work_tree, - template_dir, hash_algo, ref_storage_format, initial_branch, - init_shared_repository, flags); + template_dir, hash_algo, ref_storage_format_uri, + initial_branch, init_shared_repository, flags); free(template_dir_to_free); free(real_git_dir_to_free); diff --git a/setup.c b/setup.c index df8cbf196d9161..13f4a0aba99d6f 100644 --- a/setup.c +++ b/setup.c @@ -2727,7 +2727,8 @@ static int read_default_format_config(const char *key, const char *value, } static void repository_format_configure(struct repository_format *repo_fmt, - int hash, enum ref_storage_format ref_storage_format) + int hash, + const char *ref_storage_format_uri) { struct default_format_config cfg = { .hash = GIT_HASH_UNKNOWN, @@ -2738,6 +2739,7 @@ static void repository_format_configure(struct repository_format *repo_fmt, .ignore_repo = 1, .ignore_worktree = 1, }; + enum ref_storage_format ref_storage_format; char *ref_storage_payload = NULL; const char *env; @@ -2791,8 +2793,12 @@ static void repository_format_configure(struct repository_format *repo_fmt, * 7. Otherwise, we fall back to the default ref storage format * compiled into Git. */ - if (ref_storage_format != REF_STORAGE_FORMAT_UNKNOWN) { - /* nothing to do */ + if (ref_storage_format_uri) { + ref_storage_format = ref_storage_format_by_uri(ref_storage_format_uri, + &ref_storage_payload); + if (ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN) + die(_("unknown ref storage format specified via command line: '%s'"), + ref_storage_format_uri); } else if ((env = getenv(GIT_REF_STORAGE_FORMAT_ENVIRONMENT))) { ref_storage_format = ref_storage_format_by_uri(env, &ref_storage_payload); if (ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN) @@ -2845,7 +2851,7 @@ int init_db(struct repository *repo, const char *real_git_dir, const char *worktree, const char *template_dir, int hash, - enum ref_storage_format ref_storage_format, + const char *ref_storage_format_uri, const char *initial_branch, int init_shared_repository, unsigned int flags) { @@ -2882,7 +2888,7 @@ int init_db(struct repository *repo, * is an attempt to reinitialize new repository with an old tool. */ read_and_verify_repository_format(&repo_fmt, repo_get_git_dir(repo), NULL); - repository_format_configure(&repo_fmt, hash, ref_storage_format); + repository_format_configure(&repo_fmt, hash, ref_storage_format_uri); if (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0) die("%s", err.buf); diff --git a/setup.h b/setup.h index 763fd384e86c28..f04d2984b4ad66 100644 --- a/setup.h +++ b/setup.h @@ -265,7 +265,7 @@ int init_db(struct repository *repo, const char *real_git_dir, const char *worktree, const char *template_dir, int hash_algo, - enum ref_storage_format ref_storage_format, + const char *ref_storage_format_uri, const char *initial_branch, int init_shared_repository, unsigned int flags); void initialize_repository_version(struct repository *repo, diff --git a/t/t0001-init.sh b/t/t0001-init.sh index fe5bc3d3e17a3e..f1d227f271677c 100755 --- a/t/t0001-init.sh +++ b/t/t0001-init.sh @@ -833,7 +833,7 @@ done test_expect_success 'init with --ref-storage-format=garbage' ' test_when_finished "rm -rf refformat" && cat >expect <<-EOF && - fatal: unknown ref storage format ${SQ}garbage${SQ} + fatal: unknown ref storage format specified via command line: ${SQ}garbage${SQ} EOF test_must_fail git init --ref-storage-format=garbage refformat 2>err && test_cmp expect err diff --git a/t/t1423-ref-backend.sh b/t/t1423-ref-backend.sh index 525b2a19b4bca6..ee2bb66b99c1f8 100755 --- a/t/t1423-ref-backend.sh +++ b/t/t1423-ref-backend.sh @@ -254,6 +254,36 @@ test_expect_success 'initializing repository with alt ref directory' ' ) ' +test_expect_success 'initializing repository with --ref-storage-format and payload' ' + test_when_finished "rm -rf repo refdir" && + mkdir refdir && + BACKEND="$(test_detect_ref_format)://$(pwd)/refdir" && + git init --ref-storage-format="$BACKEND" repo && + verify_files_exist repo/.git refdir && + + git -C repo config get extensions.refstorage >actual && + echo $BACKEND >expect && + test_cmp expect actual && + + test_commit -C repo 1 && + git -C repo refs list >out && + test_grep "refs/tags/1" out && + + # Reinitializing the repository is fine when not specifying any format. + git -C repo init && + # Reinitializing with the same backend is fine, too. + git -C repo init --ref-storage-format="$BACKEND" && + # Reinitializing without a payload should fail. + test_must_fail git -C repo init --ref-storage-format="$(test_detect_ref_format)" 2>err && + test_grep "attempt to reinitialize repository with different reference storage payload" err && + # Reinitializing with a different payload should fail, too. + test_must_fail git -C repo init --ref-storage-format="$(test_detect_ref_format)://$(pwd)/other" 2>err && + test_grep "attempt to reinitialize repository with different reference storage payload" err && + + git -C repo config get extensions.refstorage >actual && + test_cmp expect actual +' + test_expect_success 'cloning repository with alt ref directory' ' test_when_finished "rm -rf source repo refdir" && mkdir refdir && From 20e38cba3926a5b42ae1a83c7d4776285bc5c841 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 10 Sep 2026 17:09:11 +0200 Subject: [PATCH 022/155] setup: split up concerns of `init_db()` The function `init_db()` is responsible for creating the on-disk directory structure required for a Git repository. It is used by both git-init(1) and git-clone(1), and because their expected behaviour is different we support a couple of flags: - The `QUIET` flag controls whether the command is quiet or not. For git-init(1) this is user-controllable, whereas for git-clone(1) we're always quiet. - The `EXIST_OK` flag controls whether a preexisting repository is okay or not. For git-init(1) it is, for git-clone(1) it's not. - The `SKIP_REFDB` flag controls whether the reference database should already be created or not. For git-init(1) we do, but for git-clone(1) we don't because it does not yet know about the default branch and about the remote object hash. Furthermore, we're about to add another divergence in behaviour, where we have to also skip creation of the object database in git-clone(1). This is becoming quite cumbersome though. Instead of introducing another flag, start to split up concerns of the function so that we never create the reference or object database. This becomes the responsibility of the caller, which is thus free to defer their creation to a later point in time. This lets us get rid of most of the divergent behaviour: - We don't need the `SKIP_REFDB` and a potential `SKIP_ODB` flags anymore. - We don't need the `QUIET` flag anymore, as nothing prints output except for the final status message that tells the user that the repository has been (re)initialized. But as this message is specific to git-init(1), we can easily move it there. The only piece of information we still have to convey is whether or not reinitialization of a preexisting repository is okay. This is handled by a new `reinit_ok` pointer that, if non-`NULL`, indicates that it is okay to reinitialize the repository. Furthermore, the pointer will be written to to indicate whether the repository was reinitialized or not, which we need in git-init(1) to print the correct initialization message. With these refactorings, `init_db()` is named quite misleadingly though, as we don't create any of the reference or object databases anymore. Rename it to `create_repository()`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/clone.c | 9 ++++---- builtin/init-db.c | 32 +++++++++++++++++++++------- setup.c | 54 +++++++++++++++-------------------------------- setup.h | 45 ++++++++++++++++++++++++++++----------- 4 files changed, 79 insertions(+), 61 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index 5b25cca5102956..904d2d859f7f4a 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -1185,10 +1185,10 @@ int cmd_clone(int argc, * repository, and reference backends may persist that information into * their on-disk data structures. */ - init_db(the_repository, git_dir, real_git_dir, work_tree, option_template, - GIT_HASH_UNKNOWN, ref_storage_format, NULL, - do_not_override_repo_unix_permissions, - INIT_DB_QUIET | INIT_DB_SKIP_REFDB); + create_repository(the_repository, git_dir, real_git_dir, work_tree, + option_template, GIT_HASH_UNKNOWN, ref_storage_format, + do_not_override_repo_unix_permissions, NULL); + create_object_database(the_repository); if (real_git_dir) { free((char *)git_dir); @@ -1445,6 +1445,7 @@ int cmd_clone(int argc, initialize_repository_version(the_repository, hash_algo, the_repository->ref_storage_format, 1); repo_set_hash_algo(the_repository, hash_algo); create_reference_database(the_repository, NULL, 1); + startup_info->have_repository = 1; /* * Before fetching from the remote, download and install bundle diff --git a/builtin/init-db.c b/builtin/init-db.c index e96b1283b7010d..f2c7e3be6d2458 100644 --- a/builtin/init-db.c +++ b/builtin/init-db.c @@ -80,7 +80,7 @@ int cmd_init_db(int argc, char *work_tree = NULL; const char *template_dir = NULL; char *template_dir_to_free = NULL; - unsigned int flags = 0; + int quiet = 0; int bare = startup_info->force_bare_repository ? 1 : -1; const char *object_format = NULL; const char *ref_format = NULL; @@ -102,7 +102,7 @@ int cmd_init_db(int argc, .flags = PARSE_OPT_OPTARG | PARSE_OPT_NONEG, .callback = shared_callback }, - OPT_BIT('q', "quiet", &flags, N_("be quiet"), INIT_DB_QUIET), + OPT_BOOL('q', "quiet", &quiet, N_("be quiet")), OPT_STRING(0, "separate-git-dir", &real_git_dir, N_("gitdir"), N_("separate git dir from working tree")), OPT_STRING('b', "initial-branch", &initial_branch, N_("name"), @@ -113,7 +113,7 @@ int cmd_init_db(int argc, N_("specify the reference format to use")), OPT_END() }; - int ret; + int reinit; argc = parse_options(argc, argv, prefix, init_db_options, init_db_usage, 0); @@ -247,14 +247,30 @@ int cmd_init_db(int argc, die(_("--separate-git-dir incompatible with bare repository")); } - flags |= INIT_DB_EXIST_OK; - ret = init_db(the_repository, git_dir, real_git_dir, work_tree, - template_dir, hash_algo, ref_storage_format, initial_branch, - init_shared_repository, flags); + create_repository(the_repository, git_dir, real_git_dir, work_tree, + template_dir, hash_algo, ref_storage_format, + init_shared_repository, &reinit); + create_reference_database(the_repository, initial_branch, quiet); + create_object_database(the_repository); + + if (!quiet) { + int len = strlen(git_dir); + + if (reinit) + printf(repo_settings_get_shared_repository(the_repository) + ? _("Reinitialized existing shared Git repository in %s%s\n") + : _("Reinitialized existing Git repository in %s%s\n"), + git_dir, len && git_dir[len-1] != '/' ? "/" : ""); + else + printf(repo_settings_get_shared_repository(the_repository) + ? _("Initialized empty shared Git repository in %s%s\n") + : _("Initialized empty Git repository in %s%s\n"), + git_dir, len && git_dir[len-1] != '/' ? "/" : ""); + } free(template_dir_to_free); free(real_git_dir_to_free); free(work_tree); free(git_dir); - return ret; + return 0; } diff --git a/setup.c b/setup.c index d90654f5842deb..8c7b97f92e32dd 100644 --- a/setup.c +++ b/setup.c @@ -2647,7 +2647,7 @@ static int create_default_files(struct repository *repo, return reinit; } -static void create_object_database(struct repository *repo) +void create_object_database(struct repository *repo) { /* * Create the "objects" directory in the common directory. This is done @@ -2822,17 +2822,17 @@ static void repository_format_configure(struct repository_format *repo_fmt, } } -int init_db(struct repository *repo, - const char *git_dir, - const char *real_git_dir, - const char *worktree, - const char *template_dir, int hash, - enum ref_storage_format ref_storage_format, - const char *initial_branch, - int init_shared_repository, unsigned int flags) +void create_repository(struct repository *repo, + const char *git_dir, + const char *real_git_dir, + const char *worktree, + const char *template_dir, + int hash, + enum ref_storage_format ref_storage_format, + int init_shared_repository, + int *reinit_ok) { - int reinit; - int exist_ok = flags & INIT_DB_EXIST_OK; + int reinit_ignored; char *original_git_dir = real_pathdup(git_dir, 1); struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT; struct strbuf err = STRBUF_INIT; @@ -2840,10 +2840,10 @@ int init_db(struct repository *repo, if (real_git_dir) { struct stat st; - if (!exist_ok && !stat(git_dir, &st)) + if (!reinit_ok && !stat(git_dir, &st)) die(_("%s already exists"), git_dir); - if (!exist_ok && !stat(real_git_dir, &st)) + if (!reinit_ok && !stat(real_git_dir, &st)) die(_("%s already exists"), real_git_dir); apply_and_export_relative_gitdir(repo, real_git_dir, 1); @@ -2877,8 +2877,10 @@ int init_db(struct repository *repo, safe_create_dir(repo, git_dir, 0); - reinit = create_default_files(repo, template_dir, original_git_dir, - &repo_fmt, init_shared_repository); + if (!reinit_ok) + reinit_ok = &reinit_ignored; + *reinit_ok = create_default_files(repo, template_dir, original_git_dir, + &repo_fmt, init_shared_repository); if (repo_settings_get_shared_repository(repo)) { char buf[10]; @@ -2901,29 +2903,7 @@ int init_db(struct repository *repo, repo_config_set(repo, "receive.denyNonFastforwards", "true"); } - if (!(flags & INIT_DB_SKIP_REFDB)) - create_reference_database(repo, initial_branch, flags & INIT_DB_QUIET); - create_object_database(repo); - - startup_info->have_repository = 1; - - if (!(flags & INIT_DB_QUIET)) { - int len = strlen(git_dir); - - if (reinit) - printf(repo_settings_get_shared_repository(repo) - ? _("Reinitialized existing shared Git repository in %s%s\n") - : _("Reinitialized existing Git repository in %s%s\n"), - git_dir, len && git_dir[len-1] != '/' ? "/" : ""); - else - printf(repo_settings_get_shared_repository(repo) - ? _("Initialized empty shared Git repository in %s%s\n") - : _("Initialized empty Git repository in %s%s\n"), - git_dir, len && git_dir[len-1] != '/' ? "/" : ""); - } - clear_repository_format(&repo_fmt); strbuf_release(&err); free(original_git_dir); - return 0; } diff --git a/setup.h b/setup.h index 763fd384e86c28..f1c1ed65fb1480 100644 --- a/setup.h +++ b/setup.h @@ -256,24 +256,45 @@ int apply_repository_format(struct repository *repo, const char *get_template_dir(const char *option_template); -#define INIT_DB_QUIET (1 << 0) -#define INIT_DB_EXIST_OK (1 << 1) -#define INIT_DB_SKIP_REFDB (1 << 2) - -int init_db(struct repository *repo, - const char *git_dir, - const char *real_git_dir, - const char *worktree, - const char *template_dir, int hash_algo, - enum ref_storage_format ref_storage_format, - const char *initial_branch, int init_shared_repository, - unsigned int flags); +/* + * Create the repository by creating the necessary directory structures, + * setting up the configuration and configuring the repository's format. If + * `template_dir` is set, copy over templates from that directory. Furthermore, + * if and only if `reinit_ok` is a non-NULL pointer, then the function may + * reinitialize a preexisting repository. In that case, the pointer will be set + * to `1` in case the repo was reinitialized and `0` if it didn't exist yet. + * + * Note that this function does not create the reference and object databases. + */ +void create_repository(struct repository *repo, + const char *git_dir, + const char *real_git_dir, + const char *worktree, + const char *template_dir, + int hash_algo, + enum ref_storage_format ref_storage_format, + int init_shared_repository, + int *reinit_ok); + void initialize_repository_version(struct repository *repo, int hash_algo, enum ref_storage_format ref_storage_format, int reinit); + +/* + * Create the reference database for the repository. The repository and its ref + * storage format must have already been configured properly before calling + * this function. When set, `initial_branch` overrides the default branch that + * HEAD will point to. + */ void create_reference_database(struct repository *repo, const char *initial_branch, int quiet); +/* + * Create the object database for the repository. The repository must have + * already been configured properly before calling this function. + */ +void create_object_database(struct repository *repo); + /* * NOTE NOTE NOTE!! * From dc33728d9d6249892c68060e2c21a211560e960c Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 10 Sep 2026 17:09:12 +0200 Subject: [PATCH 023/155] builtin/clone: defer setup of the object database When cloning a repository we defer initialization of the reference database. This is because we don't yet know all details required for us to initialize the refdb in the first place. Most importantly, what we are missing is information about the object hash. We don't do the same thing for the object database yet, but here we essentially have the same problem. While the "files" database does not need any information about the object format at creation time, alternate backends are likely to require that information so that they can properly set up their data structures. Besides this forward-looking future proofing though, we also have a second use case for deferring initialization of the object database, namely alternates. When initializing the object database we do not yet know whether we'll need alternates or not because this depends on the repository we're about to clone from. If it is a local repository and the user has passed "--reference{,-if-able}", then we will end up writing alternates into the object database. The ugly part though is that we cannot determine where the repository is getting cloned from before it has been initialized. While we of course already have access to the user-provided URI, that URI can be very well rewritten via "url..insteadOf". We can of course read the global- and system-level configuration to resolve it. But we explicitly resolve the URI a second time after we have initialized the repository because it can happen that we copy a ".git/config" over from our templates, and that file may cause us to rewrite the path. In a subsequent commit though we'll start to write alternates as part of the repository initialization, so we'll need to have the URI properly resolved before we can initialize the object database. This is ugly, but as mentioned above it makes sense for us to defer its initialization anyway so that we also know about the object hash already. Defer creation of the object database until after we have resolved the URI. Note that this also requires us to defer the call to `setup_reference()` until after we have created the object database. While you might think that this function has something to do with references ("refs/*"), it is in fact responsible for setting up alternates. Consequently, we can only call it after we have created the object database already. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/clone.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index 904d2d859f7f4a..bdcbd7aa1b73a9 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -1188,7 +1188,6 @@ int cmd_clone(int argc, create_repository(the_repository, git_dir, real_git_dir, work_tree, option_template, GIT_HASH_UNKNOWN, ref_storage_format, do_not_override_repo_unix_permissions, NULL); - create_object_database(the_repository); if (real_git_dir) { free((char *)git_dir); @@ -1311,9 +1310,6 @@ int cmd_clone(int argc, strbuf_reset(&key); } - if (option_required_reference.nr || option_optional_reference.nr) - setup_reference(); - remote = remote_get_early(remote_name); if (!option_rev) @@ -1342,6 +1338,10 @@ int cmd_clone(int argc, if (option_local > 0 && !is_local) warning(_("--local is ignored")); + create_object_database(the_repository); + if (option_required_reference.nr || option_optional_reference.nr) + setup_reference(); + transport = transport_get(remote, path ? path : remote->url.v[0]); transport_set_verbosity(transport, option_verbosity, option_progress); transport->family = family; From f7aeeae6682881263adabc3d176ef1ce83205e79 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 10 Sep 2026 17:09:13 +0200 Subject: [PATCH 024/155] builtin/clone: move around `setup_reference()` In a subsequent commit, `setup_reference()` will start to call `copy_alternates()`. Prepare for this by moving the function further down so that we can avoid adding a declaration. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/clone.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index bdcbd7aa1b73a9..ac5843d7b99778 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -179,16 +179,6 @@ static int add_one_reference(struct string_list_item *item, void *cb_data) return 0; } -static void setup_reference(void) -{ - int required = 1; - for_each_string_list(&option_required_reference, - add_one_reference, &required); - required = 0; - for_each_string_list(&option_optional_reference, - add_one_reference, &required); -} - static void copy_alternates(struct strbuf *src, const char *src_repo) { /* @@ -228,6 +218,16 @@ static void copy_alternates(struct strbuf *src, const char *src_repo) fclose(in); } +static void setup_reference(void) +{ + int required = 1; + for_each_string_list(&option_required_reference, + add_one_reference, &required); + required = 0; + for_each_string_list(&option_optional_reference, + add_one_reference, &required); +} + static void mkdir_if_missing(const char *pathname, mode_t mode) { struct stat st; From 501548160c6bd13a622e78ad393baf49f9a6eed8 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 10 Sep 2026 17:09:14 +0200 Subject: [PATCH 025/155] builtin/clone: refactor handling of "--reference{,-if-able}" Users can pass "--reference{,-if-able}" to git-clone(1) to instruct it to set up alternates for the newly created repository. This allows it to reuse objects from the source repository so that in the best case we don't have to clone all objects over. Those options are handled by the confusingly named `setup_reference()` function -- without the above context, one might rightfully believe that it was about refs, not about alternates. The function itself is rather simple: we loop through all provided alternate paths and then, if such an alternate is valid, we write it to the object database. In subsequent commits we're about to consolidate the complete setup of alternates into this function, and furthermore we'll refactor the setup of the object database to handle doing this for us instead of writing the alternates into it one by one. Prepare for this refactoring by collecting the alternates into a strvec. Rename the function to `collect_alternates()` to clarify its scope, as it does not set up the references itself anymore. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/clone.c | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index ac5843d7b99778..08d913d3062236 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -154,24 +154,26 @@ static char *get_repo_path(const char *repo, int *is_bundle) return canon; } -static int add_one_reference(struct string_list_item *item, void *cb_data) +struct collect_alternates_data { + struct strvec *alternates; + bool required; +}; + +static int collect_one_alternate(struct string_list_item *item, void *cb_data) { + struct collect_alternates_data *data = cb_data; struct strbuf err = STRBUF_INIT; - int *required = cb_data; char *ref_git = compute_alternate_path(item->string, &err); if (!ref_git) { - if (*required) + if (data->required) die("%s", err.buf); else fprintf(stderr, _("info: Could not add alternate for '%s': %s\n"), item->string, err.buf); } else { - struct strbuf sb = STRBUF_INIT; - strbuf_addf(&sb, "%s/objects", ref_git); - odb_add_to_alternates_file(the_repository->objects, sb.buf); - strbuf_release(&sb); + strvec_pushf(data->alternates, "%s/objects", ref_git); } strbuf_release(&err); @@ -218,14 +220,20 @@ static void copy_alternates(struct strbuf *src, const char *src_repo) fclose(in); } -static void setup_reference(void) +static void collect_alternates(struct strvec *alternates) { - int required = 1; - for_each_string_list(&option_required_reference, - add_one_reference, &required); - required = 0; - for_each_string_list(&option_optional_reference, - add_one_reference, &required); + if (option_required_reference.nr || option_optional_reference.nr) { + struct collect_alternates_data data = { + .alternates = alternates, + .required = true, + }; + + for_each_string_list(&option_required_reference, + collect_one_alternate, &data); + data.required = false; + for_each_string_list(&option_optional_reference, + collect_one_alternate, &data); + } } static void mkdir_if_missing(const char *pathname, mode_t mode) @@ -999,6 +1007,7 @@ int cmd_clone(int argc, N_("uri"), N_("a URI for downloading bundles before fetching from origin remote")), OPT_END() }; + struct strvec alternates = STRVEC_INIT; const char * const builtin_clone_usage[] = { N_("git clone [] [--] []"), @@ -1339,8 +1348,10 @@ int cmd_clone(int argc, warning(_("--local is ignored")); create_object_database(the_repository); - if (option_required_reference.nr || option_optional_reference.nr) - setup_reference(); + collect_alternates(&alternates); + + for (size_t i = 0; i < alternates.nr; i++) + odb_add_to_alternates_file(the_repository->objects, alternates.v[i]); transport = transport_get(remote, path ? path : remote->url.v[0]); transport_set_verbosity(transport, option_verbosity, option_progress); @@ -1638,6 +1649,7 @@ int cmd_clone(int argc, string_list_clear(&option_not, 0); string_list_clear(&option_config, 0); string_list_clear(&server_options, 0); + strvec_clear(&alternates); free(remote_name); strbuf_release(&reflog_msg); From 87e10a91e2396507451387d3cf9f351d2cf7d7e5 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 10 Sep 2026 17:09:15 +0200 Subject: [PATCH 026/155] builtin/clone: move setup of alternates for shared local clones When cloning a local repository with "--shared" we add that repository to the new repository's alternates. This is done in `clone_local()`, which is responsible for performing local clones. Move the logic into `collect_alternates()` to unify our setup of alternates. Furthermore, this will allow us to set up alternates right at creation time of the object database. Note that the logic for cloning a local repository with "--no-shared" is not yet part of `collect_alternates()`. This will be handled in the next commit, but means that at this step, we may compute `commondir` without it being used. It will become used in the next step though. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/clone.c | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index 08d913d3062236..d397fd36b27f0a 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -220,7 +220,8 @@ static void copy_alternates(struct strbuf *src, const char *src_repo) fclose(in); } -static void collect_alternates(struct strvec *alternates) +static void collect_alternates(struct strvec *alternates, + const char *local_source_repo) { if (option_required_reference.nr || option_optional_reference.nr) { struct collect_alternates_data data = { @@ -234,6 +235,16 @@ static void collect_alternates(struct strvec *alternates) for_each_string_list(&option_optional_reference, collect_one_alternate, &data); } + + if (local_source_repo) { + struct strbuf commondir = STRBUF_INIT; + + get_common_dir(&commondir, local_source_repo); + if (option_shared) + strvec_pushf(alternates, "%s/objects", commondir.buf); + + strbuf_release(&commondir); + } } static void mkdir_if_missing(const char *pathname, mode_t mode) @@ -357,13 +368,7 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest, static void clone_local(const char *src_repo, const char *dest_repo) { - if (option_shared) { - struct strbuf alt = STRBUF_INIT; - get_common_dir(&alt, src_repo); - strbuf_addstr(&alt, "/objects"); - odb_add_to_alternates_file(the_repository->objects, alt.buf); - strbuf_release(&alt); - } else { + if (!option_shared) { struct strbuf src = STRBUF_INIT; struct strbuf dest = STRBUF_INIT; get_common_dir(&src, src_repo); @@ -1348,7 +1353,7 @@ int cmd_clone(int argc, warning(_("--local is ignored")); create_object_database(the_repository); - collect_alternates(&alternates); + collect_alternates(&alternates, is_local ? path : NULL); for (size_t i = 0; i < alternates.nr; i++) odb_add_to_alternates_file(the_repository->objects, alternates.v[i]); From 002a7cfc731b9a4b77b580e82cba9aeea5b692cf Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 10 Sep 2026 17:09:16 +0200 Subject: [PATCH 027/155] builtin/clone: move setup of alternates for non-shared local clones Similar as in the preceding commit, move the setup of alternates for local clones with "--no-shared" into `collect_alternates()`. With this step, the complete setup of alternates is now handled by that function. Note that besides moving stuff around, it also fixes a bug: previously, we did not know to resolve the referenced repository's common directory. Consequently, when referencing a worktree we failed to resolve alternates. But as `collect_alternates()` already knows to resolve the commondir for "--local" we can simply reuse this resolved path for our purpose. Add two tests, the first one of which exercises this bug to avoid future regressions. The second test ensures that we properly handle relative alternates for a referenced worktree. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/clone.c | 34 +++++++++++++++++++++++----------- t/t5604-clone-reference.sh | 25 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index d397fd36b27f0a..17353a8e1fc5e3 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -181,7 +181,7 @@ static int collect_one_alternate(struct string_list_item *item, void *cb_data) return 0; } -static void copy_alternates(struct strbuf *src, const char *src_repo) +static void read_alternates(struct strvec *alternates, const char *src_repo) { /* * Read from the source objects/info/alternates file @@ -195,29 +195,41 @@ static void copy_alternates(struct strbuf *src, const char *src_repo) * to turn entries with paths relative to the original * absolute, so that they can be used in the new repository. */ - FILE *in = xfopen(src->buf, "r"); + FILE *in; + struct strbuf path = STRBUF_INIT; struct strbuf line = STRBUF_INIT; + strbuf_addf(&path, "%s/objects/info/alternates", src_repo); + + in = fopen(path.buf, "r"); + if (!in) { + if (errno == ENOENT) + goto out; + die_errno("could not read alternates file '%s'", path.buf); + } + while (strbuf_getline(&line, in) != EOF) { char *abs_path; if (!line.len || line.buf[0] == '#') continue; if (is_absolute_path(line.buf)) { - odb_add_to_alternates_file(the_repository->objects, - line.buf); + strvec_push(alternates, line.buf); continue; } abs_path = mkpathdup("%s/objects/%s", src_repo, line.buf); if (!normalize_path_copy(abs_path, abs_path)) - odb_add_to_alternates_file(the_repository->objects, - abs_path); + strvec_push(alternates, abs_path); else warning("skipping invalid relative alternate: %s/%s", src_repo, line.buf); free(abs_path); } + +out: + strbuf_release(&path); strbuf_release(&line); - fclose(in); + if (in) + fclose(in); } static void collect_alternates(struct strvec *alternates, @@ -242,6 +254,8 @@ static void collect_alternates(struct strvec *alternates, get_common_dir(&commondir, local_source_repo); if (option_shared) strvec_pushf(alternates, "%s/objects", commondir.buf); + else + read_alternates(alternates, commondir.buf); strbuf_release(&commondir); } @@ -320,11 +334,9 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest, continue; } - /* Files that cannot be copied bit-for-bit... */ - if (!fspathcmp(iter->relative_path, "info/alternates")) { - copy_alternates(src, src_repo); + /* Alternates were already handled earlier. */ + if (!fspathcmp(iter->relative_path, "info/alternates")) continue; - } if (unlink(dest->buf) && errno != ENOENT) die_errno(_("failed to unlink '%s'"), dest->buf); diff --git a/t/t5604-clone-reference.sh b/t/t5604-clone-reference.sh index 39a0c318dfb2d4..9e4b98fdb82597 100755 --- a/t/t5604-clone-reference.sh +++ b/t/t5604-clone-reference.sh @@ -383,4 +383,29 @@ test_expect_success 'dissociate from repo with commit graph' ' git clone --no-local --reference graph.git --dissociate orig clone ' +test_expect_success 'local clone from linked worktree carries over alternates' ' + rm -fr base derived derived-wt dst expect && + git init base && + test_commit -C base one && + git clone --shared base derived && + git -C derived worktree add ../derived-wt && + git clone derived-wt dst && + echo "$(pwd)/base/.git/objects" >expect && + test_cmp expect dst/.git/objects/info/alternates && + git -C dst fsck +' + +test_expect_success 'local clone from linked worktree resolves relative alternates' ' + rm -fr base derived derived-wt dst expect && + git init base && + test_commit -C base one && + git clone --shared base derived && + echo "../../../base/.git/objects" >derived/.git/objects/info/alternates && + git -C derived worktree add ../derived-wt && + git clone derived-wt dst && + echo "$(pwd)/base/.git/objects" >expect && + test_cmp expect dst/.git/objects/info/alternates && + git -C dst fsck +' + test_done From d13e56771dd8ff8002c9ec31f17789df00d9199b Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 10 Sep 2026 17:09:17 +0200 Subject: [PATCH 028/155] odb/source: support writing alternates when creating the database Add the ability to write alternates when creating the object database. This change allows us to remove the `write_alternates()` callback in a subsequent patch. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-files.c | 76 ++++++++++++++++++++++++++++++++++++++++++++-- odb/source.h | 17 +++++++++-- setup.c | 4 ++- 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/odb/source-files.c b/odb/source-files.c index b7b3a297bb79d7..8fe65d91f8eb6b 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -18,6 +18,7 @@ #include "run-command.h" #include "strbuf.h" #include "string-list.h" +#include "strmap.h" #include "strvec.h" #include "tree.h" #include "write-or-die.h" @@ -51,9 +52,14 @@ static void odb_source_files_close(struct odb_source *source) odb_source_close(&files->packed->base); } -static int odb_source_files_create_on_disk(struct odb_source *source) +static int odb_source_files_create_on_disk(struct odb_source *source, + const struct odb_create_on_disk_options *opts) { + struct lock_file alternates_lock = LOCK_INIT; struct strbuf path = STRBUF_INIT; + struct strset seen = STRSET_INIT; + struct strbuf line = STRBUF_INIT; + int ret; safe_create_dir(source->odb->repo, source->path, 1); @@ -64,8 +70,74 @@ static int odb_source_files_create_on_disk(struct odb_source *source) strbuf_addf(&path, "%s/info", source->path); safe_create_dir(source->odb->repo, path.buf, 1); + if (opts->alternates && opts->alternates->nr) { + FILE *alternates, *orig; + + strbuf_reset(&path); + strbuf_addf(&path, "%s/info/alternates", source->path); + + repo_hold_lock_file_for_update(source->odb->repo, &alternates_lock, + path.buf, LOCK_DIE_ON_ERROR); + + alternates = fdopen_lock_file(&alternates_lock, "w"); + if (!alternates) { + ret = error_errno(_("unable to fdopen alternates lockfile")); + goto out; + } + + /* + * The alternates file may already exist, e.g. when it has been + * seeded from a template directory. Read any preexisting + * entries so that we don't end up writing duplicates. + */ + orig = fopen(path.buf, "r"); + if (orig) { + while (strbuf_getline(&line, orig) != EOF) { + strset_add(&seen, line.buf); + fprintf(alternates, "%s\n", line.buf); + } + + if (ferror(orig)) { + ret = error_errno(_("unable to read alternates file")); + fclose(orig); + goto out; + } + + fclose(orig); + } else if (errno != ENOENT) { + ret = error_errno(_("unable to read alternates file")); + goto out; + } + + for (size_t i = 0; i < opts->alternates->nr; i++) { + const char *alternate = opts->alternates->v[i]; + if (!strset_add(&seen, alternate)) + continue; + fprintf(alternates, "%s\n", alternate); + } + + if (ferror(alternates)) { + ret = error_errno(_("unable to write alternates file")); + goto out; + } + + if (commit_lock_file(&alternates_lock)) { + ret = error_errno(_("unable to commit alternates file")); + goto out; + } + } + + /* Reprepare the object database to activate alternates. */ + odb_reprepare(source->odb); + + ret = 0; + +out: + rollback_lock_file(&alternates_lock); + strbuf_release(&line); strbuf_release(&path); - return 0; + strset_clear(&seen); + return ret; } static void odb_source_files_prepare(struct odb_source *source, diff --git a/odb/source.h b/odb/source.h index ea8675247e45f7..63f1c0c531f9a2 100644 --- a/odb/source.h +++ b/odb/source.h @@ -36,6 +36,15 @@ struct object_id; struct odb_stream; struct strvec; +struct odb_create_on_disk_options { + /* + * Alternates that shall be written into the newly created object + * database. Whether or not this option can be handled is specific to + * the backend. + */ + const struct strvec *alternates; +}; + /* * The source is the part of the object database that stores the actual * objects. It thus encapsulates the logic to read and write the specific @@ -106,7 +115,8 @@ struct odb_source { * This callback may be NULL in case the source does not need any * on-disk setup. */ - int (*create_on_disk)(struct odb_source *source); + int (*create_on_disk)(struct odb_source *source, + const struct odb_create_on_disk_options *opts); /* * This callback is expected to prepare the source so that it becomes @@ -356,11 +366,12 @@ static inline void odb_source_close(struct odb_source *source) * Create on-disk data structures that are required for this source to operate * correctly. Returns 0 on success, a negative error code otherwise. */ -static inline int odb_source_create_on_disk(struct odb_source *source) +static inline int odb_source_create_on_disk(struct odb_source *source, + const struct odb_create_on_disk_options *opts) { if (!source->create_on_disk) return 0; - return source->create_on_disk(source); + return source->create_on_disk(source, opts); } /* diff --git a/setup.c b/setup.c index 8c7b97f92e32dd..37a8e6f1241a3e 100644 --- a/setup.c +++ b/setup.c @@ -2649,6 +2649,8 @@ static int create_default_files(struct repository *repo, void create_object_database(struct repository *repo) { + struct odb_create_on_disk_options opts = { 0 }; + /* * Create the "objects" directory in the common directory. This is done * so that the repository can be discovered regardless of the backend @@ -2668,7 +2670,7 @@ void create_object_database(struct repository *repo) repo->objects = odb_new(repo, ODB_NEW_HONOR_ENV); - if (odb_source_create_on_disk(repo->objects->sources) < 0) + if (odb_source_create_on_disk(repo->objects->sources, &opts) < 0) die(_("failed creating object database")); } From a9c1a15debaf5dfc6e84bf86d24cc18a1333dfb1 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 10 Sep 2026 17:09:18 +0200 Subject: [PATCH 029/155] builtin/clone: write alternates via `odb_create_on_disk()` When creating a repository with alternates we first initialize the object database and then write alternates to it in a separate step. This is unfortunate due to a couple of reasons: - It requires us to have a `write_alternates()` callback, which is unfortunate as we never even write alternates to an object database after it has been created. - We're about to make alternates an implementation detail of the object database's backend in a future patch series, so having this callback is suboptimal there. - The backend has more flexibility with how exactly alternates are configured when it itself is in full control over their setup at the time where it creates the object database itself. We have thus introduced the ability to write alternates right at creation time in the preceding commits, and we have unified setup of alternates into a single location. All that's left to do for us now is to wire up alternates as an option for the database creation. Do so. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/clone.c | 5 +---- builtin/init-db.c | 2 +- setup.c | 7 +++++-- setup.h | 6 ++++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index 17353a8e1fc5e3..b14264c33a0f04 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -1364,11 +1364,8 @@ int cmd_clone(int argc, if (option_local > 0 && !is_local) warning(_("--local is ignored")); - create_object_database(the_repository); collect_alternates(&alternates, is_local ? path : NULL); - - for (size_t i = 0; i < alternates.nr; i++) - odb_add_to_alternates_file(the_repository->objects, alternates.v[i]); + create_object_database(the_repository, &alternates); transport = transport_get(remote, path ? path : remote->url.v[0]); transport_set_verbosity(transport, option_verbosity, option_progress); diff --git a/builtin/init-db.c b/builtin/init-db.c index f2c7e3be6d2458..5c22eae2f3f2d1 100644 --- a/builtin/init-db.c +++ b/builtin/init-db.c @@ -251,7 +251,7 @@ int cmd_init_db(int argc, template_dir, hash_algo, ref_storage_format, init_shared_repository, &reinit); create_reference_database(the_repository, initial_branch, quiet); - create_object_database(the_repository); + create_object_database(the_repository, NULL); if (!quiet) { int len = strlen(git_dir); diff --git a/setup.c b/setup.c index 37a8e6f1241a3e..17d0d259730358 100644 --- a/setup.c +++ b/setup.c @@ -2647,9 +2647,12 @@ static int create_default_files(struct repository *repo, return reinit; } -void create_object_database(struct repository *repo) +void create_object_database(struct repository *repo, + const struct strvec *alternates) { - struct odb_create_on_disk_options opts = { 0 }; + struct odb_create_on_disk_options opts = { + .alternates = alternates, + }; /* * Create the "objects" directory in the common directory. This is done diff --git a/setup.h b/setup.h index f1c1ed65fb1480..27b249237393f4 100644 --- a/setup.h +++ b/setup.h @@ -291,9 +291,11 @@ void create_reference_database(struct repository *repo, const char *initial_bran /* * Create the object database for the repository. The repository must have - * already been configured properly before calling this function. + * already been configured properly before calling this function. When set, + * `alternates` is the list of alternates that should be written into the + * object database. */ -void create_object_database(struct repository *repo); +void create_object_database(struct repository *repo, const struct strvec *alternates); /* * NOTE NOTE NOTE!! From d1019ac8941cd0d50e71c6b03e171faf80843c8d Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 10 Sep 2026 17:09:19 +0200 Subject: [PATCH 030/155] odb/source: remove the ability to write alternates There are no users of `odb_source_write_alternates()` in our tree anymore. Remove that function and its supporting infrastructure. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb.c | 9 -------- odb.h | 7 ------ odb/source-files.c | 54 ------------------------------------------- odb/source-inmemory.c | 7 ------ odb/source-loose.c | 7 ------ odb/source-packed.c | 7 ------ odb/source.h | 26 --------------------- 7 files changed, 117 deletions(-) diff --git a/odb.c b/odb.c index 67d98d64fcfdd9..b531cf8fb31902 100644 --- a/odb.c +++ b/odb.c @@ -239,15 +239,6 @@ static struct odb_source *odb_add_alternate_recursively(struct object_database * return alternate; } -void odb_add_to_alternates_file(struct object_database *odb, - const char *dir) -{ - int ret = odb_source_write_alternate(odb->sources, dir); - if (ret < 0) - die(NULL); - odb_add_alternate_recursively(odb, dir, 0); -} - struct odb_source *odb_add_to_alternates_memory(struct object_database *odb, const char *dir) { diff --git a/odb.h b/odb.h index b9e0db56ec4f5a..2d002461f89928 100644 --- a/odb.h +++ b/odb.h @@ -270,13 +270,6 @@ int odb_mkstemp(struct object_database *odb, */ int odb_has_alternates(struct object_database *odb); -/* - * Add the directory to the on-disk alternates file; the new entry will also - * take effect in the current process. - */ -void odb_add_to_alternates_file(struct object_database *odb, - const char *dir); - /* * Add the directory to the in-memory list of alternate sources (along with any * recursive alternates it points to), but do not modify the on-disk alternates diff --git a/odb/source-files.c b/odb/source-files.c index 8fe65d91f8eb6b..b3f340dff8ae3c 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -306,59 +306,6 @@ static int odb_source_files_read_alternates(struct odb_source *source, return 0; } -static int odb_source_files_write_alternate(struct odb_source *source, - const char *alternate) -{ - struct lock_file lock = LOCK_INIT; - char *path = xstrfmt("%s/%s", source->path, "info/alternates"); - FILE *in, *out; - int found = 0; - int ret; - - repo_hold_lock_file_for_update(source->odb->repo, &lock, path, - LOCK_DIE_ON_ERROR); - out = fdopen_lock_file(&lock, "w"); - if (!out) { - ret = error_errno(_("unable to fdopen alternates lockfile")); - goto out; - } - - in = fopen(path, "r"); - if (in) { - struct strbuf line = STRBUF_INIT; - - while (strbuf_getline(&line, in) != EOF) { - if (!strcmp(alternate, line.buf)) { - found = 1; - break; - } - fprintf_or_die(out, "%s\n", line.buf); - } - - strbuf_release(&line); - fclose(in); - } else if (errno != ENOENT) { - ret = error_errno(_("unable to read alternates file")); - goto out; - } - - if (found) { - rollback_lock_file(&lock); - } else { - fprintf_or_die(out, "%s\n", alternate); - if (commit_lock_file(&lock)) { - ret = error_errno(_("unable to move new alternates file into place")); - goto out; - } - } - - ret = 0; - -out: - free(path); - return ret; -} - static int too_many_loose_objects(struct odb_source_files *files, int limit) { unsigned long loose_count; @@ -842,7 +789,6 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb, files->base.write_object_stream = odb_source_files_write_object_stream; files->base.begin_transaction = odb_source_files_begin_transaction; files->base.read_alternates = odb_source_files_read_alternates; - files->base.write_alternate = odb_source_files_write_alternate; files->base.optimize = odb_source_files_optimize; files->base.optimize_required = odb_source_files_optimize_required; diff --git a/odb/source-inmemory.c b/odb/source-inmemory.c index 795672adf255c6..b00248dfb2210d 100644 --- a/odb/source-inmemory.c +++ b/odb/source-inmemory.c @@ -326,12 +326,6 @@ static int odb_source_inmemory_read_alternates(struct odb_source *source UNUSED, return 0; } -static int odb_source_inmemory_write_alternate(struct odb_source *source UNUSED, - const char *alternate UNUSED) -{ - return error("in-memory source does not support alternates"); -} - static void odb_source_inmemory_close(struct odb_source *source UNUSED) { } @@ -388,7 +382,6 @@ struct odb_source_inmemory *odb_source_inmemory_new(struct object_database *odb) source->base.freshen_object = odb_source_inmemory_freshen_object; source->base.begin_transaction = odb_source_inmemory_begin_transaction; source->base.read_alternates = odb_source_inmemory_read_alternates; - source->base.write_alternate = odb_source_inmemory_write_alternate; return source; } diff --git a/odb/source-loose.c b/odb/source-loose.c index bb3455dfbd3334..0f9b30bac176f1 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -982,12 +982,6 @@ static int odb_source_loose_read_alternates(struct odb_source *source UNUSED, return 0; } -static int odb_source_loose_write_alternate(struct odb_source *source UNUSED, - const char *alternate UNUSED) -{ - return error("loose source does not support alternates"); -} - static void odb_source_loose_clear_cache(struct odb_source_loose *loose) { oidtree_clear(loose->cache); @@ -1053,7 +1047,6 @@ struct odb_source_loose *odb_source_loose_new(struct object_database *odb, loose->base.write_object_stream = odb_source_loose_write_object_stream; loose->base.begin_transaction = odb_source_loose_begin_transaction; loose->base.read_alternates = odb_source_loose_read_alternates; - loose->base.write_alternate = odb_source_loose_write_alternate; if (!is_absolute_path(loose->base.path)) chdir_notify_register(NULL, odb_source_loose_reparent, loose); diff --git a/odb/source-packed.c b/odb/source-packed.c index 630d9555856d7c..c2d253759c97d1 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -628,12 +628,6 @@ static int odb_source_packed_read_alternates(struct odb_source *source UNUSED, return 0; } -static int odb_source_packed_write_alternate(struct odb_source *source UNUSED, - const char *alternate UNUSED) -{ - return error("packed backend cannot write alternates"); -} - void (*report_garbage)(unsigned seen_bits, const char *path); static void report_helper(const struct string_list *list, @@ -849,7 +843,6 @@ struct odb_source_packed *odb_source_packed_new(struct object_database *odb, packed->base.write_object_stream = odb_source_packed_write_object_stream; packed->base.begin_transaction = odb_source_packed_begin_transaction; packed->base.read_alternates = odb_source_packed_read_alternates; - packed->base.write_alternate = odb_source_packed_write_alternate; if (!is_absolute_path(path)) chdir_notify_register(NULL, odb_source_packed_reparent, packed); diff --git a/odb/source.h b/odb/source.h index 63f1c0c531f9a2..693a9fc6049acf 100644 --- a/odb/source.h +++ b/odb/source.h @@ -286,19 +286,6 @@ struct odb_source { int (*read_alternates)(struct odb_source *source, struct strvec *out); - /* - * This callback is expected to persist the singular alternate passed - * to it into its list of alternates. Any pre-existing alternates are - * expected to remain active. Subsequent calls to `read_alternates` are - * thus expected to yield the pre-existing list of alternates plus the - * newly added alternate appended to its end. - * - * The callback is expected to return 0 on success, a negative error - * code otherwise. - */ - int (*write_alternate)(struct odb_source *source, - const char *alternate); - /* * This callback is expected to optimize the object database source. * Returns 0 on success, a negative error code otherwise. @@ -518,19 +505,6 @@ static inline int odb_source_read_alternates(struct odb_source *source, return source->read_alternates(source, out); } -/* - * Write and persist a new alternate object database source for the given - * source. Any preexisting alternates are expected to stay valid, and the new - * alternate shall be appended to the end of the list. - * - * Returns 0 on success, a negative error code otherwise. - */ -static inline int odb_source_write_alternate(struct odb_source *source, - const char *alternate) -{ - return source->write_alternate(source, alternate); -} - /* * Create a new transaction that can be used to write objects into a temporary * staging area. The objects will only be persisted when the transaction is From bc53d6c795e8e763a07baadcafdf7723f93dbe29 Mon Sep 17 00:00:00 2001 From: "D. Ben Knoble" Date: Fri, 11 Sep 2026 08:32:27 -0400 Subject: [PATCH 031/155] meson: expose knob for xmlto relative links in manuals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makefile-based builds have had this knob for most of the project's life, since a479a564dc (Documentation/Makefile: allow man.base.url.for.relative.link to be set from Make, 2009-12-03). Meson, however, hard-codes the equivalent of $prefix/$mandir, which is not really where all the HTML docs are stored in most distro builds. Plus, this value is missing a trailing slash, so links come out broken, like this in git.1: 1. Git User’s Manual /usr/share/manuser-manual.html Of course we can do better: 1. Change the default to match Make: use file://$(htmldir)/ (with trailing slash!) to form a local URL pointing at the HTML docs. This is safe because all current uses of link: point at HTML docs: git grep 'link:[[:alnum:]]' Documentation | grep -ve html -e http produces only a single result (Documentation/howto/howto-index.sh) which can be ignored. Since nothing else [*] in the normal build sets MAN_BASE_URL, this seems like the right default. 2. Provide a configurable knob, just like the Makefile, so distributions that build with Meson (like Gentoo) can decide where to make the links if they need to. Those that set htmldir probably won't need to tweak this any further, though. [*]: Well, Git's todo branch has a script dodoc.sh to build and archive docs for kernel.org; these docs are pulled by Homebrew installations, for example. It sets MAN_BASE_URL to "git_htmldocs", so the equivalent note on macOS + Homebrew is 1. Git User’s Manual git-htmldocs/user-manual.html which is not functional either, but that's a problem for downstream. In any case, users can recover the right path with "git --html-path". Signed-off-by: D. Ben Knoble Signed-off-by: Junio C Hamano --- Documentation/meson.build | 7 ++++++- meson_options.txt | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Documentation/meson.build b/Documentation/meson.build index f4854f802d455f..cfa9c67609987c 100644 --- a/Documentation/meson.build +++ b/Documentation/meson.build @@ -379,13 +379,18 @@ foreach manpage, category : manpages output: fs.stem(manpage) + '.xml', ) + man_base_url = 'file://' + htmldir + '/' + if get_option('man_base_url') != '' + man_base_url = get_option('man_base_url') + endif + doc_targets += custom_target( command: [ xmlto, '-m', '@INPUT0@', '-m', '@INPUT1@', '--stringparam', - 'man.base.url.for.relative.links=' + get_option('prefix') / get_option('mandir'), + 'man.base.url.for.relative.links=' + man_base_url, 'man', manpage_xml_target, '-o', diff --git a/meson_options.txt b/meson_options.txt index dc88f130d754a1..d590c21648ca42 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -111,6 +111,8 @@ option('default_help_format', type: 'combo', choices: ['man', 'html', 'platform' description: 'Default format used when executing git-help(1).') option('docs_backend', type: 'combo', choices: ['asciidoc', 'asciidoctor', 'auto'], value: 'auto', description: 'Which backend to use to generate documentation.') +option('man_base_url', type: 'string', value: '', + description: 'The base URL to use for relative links in manuals') # Testing. option('benchmarks', type: 'feature', value: 'auto', From d08b8448f381d8238e2cbb22c404c5765f5131c6 Mon Sep 17 00:00:00 2001 From: "D. Ben Knoble" Date: Fri, 11 Sep 2026 08:32:28 -0400 Subject: [PATCH 032/155] environment: align repo_config_values_init with struct declaration The order of assignments in repo_config_values_init is chaotic and hard to follow, especially with the definition of 'struct repo_config_values' to ensure all members are initialized. As new members will be added in the future, make it easier to validate changes by aligning the two. Refactor assignment order with no behavioral changes. Signed-off-by: D. Ben Knoble Signed-off-by: Junio C Hamano --- environment.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/environment.c b/environment.c index 76ee65e62b4823..6676e6f5ae5e03 100644 --- a/environment.c +++ b/environment.c @@ -745,6 +745,7 @@ int git_default_config(const char *var, const char *value, void repo_config_values_init(struct repo_config_values *cfg) { + /* section "core" config values */ cfg->attributes_file = NULL; cfg->excludes_file = NULL; cfg->editor_program = NULL; @@ -756,20 +757,24 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->autorebase = AUTOREBASE_NEVER; cfg->object_creation_mode = OBJECT_CREATION_MODE; cfg->apply_sparse_checkout = 0; - cfg->protect_hfs = PROTECT_HFS_DEFAULT; - cfg->protect_ntfs = PROTECT_NTFS_DEFAULT; - cfg->ignore_case = 0; - cfg->trust_executable_bit = 1; - cfg->has_symlinks = platform_has_symlinks(); - cfg->branch_track = BRANCH_TRACK_REMOTE; cfg->trust_ctime = 1; cfg->check_stat = 1; cfg->zlib_compression_level = Z_BEST_SPEED; cfg->pack_compression_level = Z_DEFAULT_COMPRESSION; cfg->precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */ cfg->core_sparse_checkout_cone = 0; - cfg->sparse_expect_files_outside_of_patterns = 0; cfg->warn_on_object_refname_ambiguity = 1; + cfg->protect_hfs = PROTECT_HFS_DEFAULT; + cfg->protect_ntfs = PROTECT_NTFS_DEFAULT; + cfg->ignore_case = 0; + cfg->trust_executable_bit = 1; + cfg->has_symlinks = platform_has_symlinks(); + + /* section "sparse" config values */ + cfg->sparse_expect_files_outside_of_patterns = 0; + + /* section "branch" config values */ + cfg->branch_track = BRANCH_TRACK_REMOTE; } void repo_config_values_clear(struct repo_config_values *cfg) From 3a21fe8cf9cbc3a2ffb019d33d46626a3b9d4bf0 Mon Sep 17 00:00:00 2001 From: "D. Ben Knoble" Date: Fri, 11 Sep 2026 08:32:29 -0400 Subject: [PATCH 033/155] core: convert build-time USE_NSEC into runtime core.useNanosec Racy Git problems persist today, manifesting themselves in the performance of commands like "git diff" in new worktrees [1]. We have long had a build knob "USE_NSEC" to tell Git to use in-core nanosecond precision when available, which mitigates most if not all racy issues, but most builds we know about don't use it. In part, that's because someone distributing Git can't safely enable it at compile-time if they don't know exactly what platforms their distribution will be used on. [1]: https://lore.kernel.org/git/CALnO6CADMJSixqYvL1Yo8qKX5rWhKQ+2OoSEuPUh-yoeK9TseQ@mail.gmail.com These days, most platforms are likely to be safe for the USE_NSEC code. Regardless, we want to give users the ability to benefit from it. This requires exposing the compile-time gated code as a runtime option. In addition, update the Racy Git documentation and other mentions of USE_NSEC in the code. Due to the conversion from #ifdef to runtime check, using the flag "--ignore-space-change" may be particularly helpful when viewing changes from this patch. Signed-off-by: D. Ben Knoble Signed-off-by: Junio C Hamano --- Documentation/config/core.adoc | 7 +++++++ Documentation/technical/racy-git.adoc | 11 ++++++----- Makefile | 12 +----------- builtin/update-index.c | 2 +- compat/posix.h | 1 - configure.ac | 6 ------ environment.c | 8 ++++++++ environment.h | 1 + read-cache.c | 15 ++++++--------- statinfo.c | 14 +++++++------- 10 files changed, 37 insertions(+), 40 deletions(-) diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc index 340329edc38143..0b697f53f10111 100644 --- a/Documentation/config/core.adoc +++ b/Documentation/config/core.adoc @@ -118,6 +118,13 @@ core.trustctime:: crawlers and some backup systems). See linkgit:git-update-index[1]. True by default. +core.useNanosec:: + If true, use nanosecond precision for ctime and mtime + comparisons between the index and the working tree (if Git + was compiled to respect this option). + This is unsafe on some platforms; + see link:technical/racy-git.html[Racy Git]. False by default. + core.splitIndex:: If true, the split-index feature of the index will be used. See linkgit:git-update-index[1]. False by default. diff --git a/Documentation/technical/racy-git.adoc b/Documentation/technical/racy-git.adoc index 59bea66c0fc6ef..499231585b00c1 100644 --- a/Documentation/technical/racy-git.adoc +++ b/Documentation/technical/racy-git.adoc @@ -39,8 +39,8 @@ files) from `st_mode` member, `st_mtime` and `st_ctime` timestamps, `st_uid`, `st_gid`, `st_ino`, and `st_size` members. With a `USE_STDEV` compile-time option, `st_dev` is also compared, but this is not enabled by default because this member -is not stable on network filesystems. With `USE_NSEC` -compile-time option, `st_mtim.tv_nsec` and `st_ctim.tv_nsec` +is not stable on network filesystems. With 'core.useNanosec' +config setting, `st_mtim.tv_nsec` and `st_ctim.tv_nsec` members are also compared. On Linux, this is not enabled by default because in-core timestamps can have finer granularity than on-disk timestamps, resulting in meaningless changes when an @@ -49,9 +49,10 @@ of git://git.kernel.org/pub/scm/linux/kernel/git/tglx/history.git ([PATCH] Sync in core time granularity with filesystems, 2005-01-04). This patch is included in kernel 2.6.11 and newer, but only fixes the issue for file systems with exactly 1 ns or 1 s -resolution. Other file systems are still broken in current Linux -kernels (e.g. CEPH, CIFS, NTFS, UDF), see -https://lore.kernel.org/lkml/5577240D.7020309@gmail.com/ +resolution. As of kernel 4.3, other file systems (CEPH, CIFS, NTFS, UFS, FUSE) +were fixed; see https://public-inbox.org/git/5605D88A.20104%40gmail.com/. FAT +has been fixed since 2015. The usual suspects (ext2, ext4, XFS) are known to +work, too. Racy Git -------- diff --git a/Makefile b/Makefile index fac3e8879c8377..b4ebcb9e835290 100644 --- a/Makefile +++ b/Makefile @@ -197,18 +197,11 @@ include shared.mak # Define NO_NORETURN if using buggy versions of gcc 4.6+ and profile feedback, # as the compiler can crash (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=49299) # -# Define USE_NSEC below if you want git to care about sub-second file mtimes -# and ctimes. Note that you need recent glibc (at least 2.2.4) for this. On -# Linux, kernel 2.6.11 or newer is required for reliable sub-second file times -# on file systems with exactly 1 ns or 1 s resolution. If you intend to use Git -# on other file systems (e.g. CEPH, CIFS, NTFS, UDF), don't enable USE_NSEC. See -# Documentation/technical/racy-git.adoc for details. -# # Define USE_ST_TIMESPEC if your "struct stat" uses "st_ctimespec" instead of # "st_ctim" # # Define NO_NSEC if your "struct stat" does not have "st_ctim.tv_nsec" -# available. This automatically turns USE_NSEC off. +# available. # # Define USE_STDEV below if you want git to care about the underlying device # change being considered an inode change from the update-index perspective. @@ -1935,9 +1928,6 @@ endif ifdef NO_ST_BLOCKS_IN_STRUCT_STAT BASIC_CFLAGS += -DNO_ST_BLOCKS_IN_STRUCT_STAT endif -ifdef USE_NSEC - BASIC_CFLAGS += -DUSE_NSEC -endif ifdef USE_ST_TIMESPEC BASIC_CFLAGS += -DUSE_ST_TIMESPEC endif diff --git a/builtin/update-index.c b/builtin/update-index.c index 241abd4332dcf9..a10c07b6362989 100644 --- a/builtin/update-index.c +++ b/builtin/update-index.c @@ -130,7 +130,7 @@ static void xrmdir(const char *path) static void avoid_racy(void) { /* - * not use if we could usleep(10) if USE_NSEC is defined. The + * not use if we could usleep(10) if core.useNanosec is enabled. The * field nsec could be there, but the OS could choose to * ignore it? */ diff --git a/compat/posix.h b/compat/posix.h index e2e794cad7d419..51ee03233bbe82 100644 --- a/compat/posix.h +++ b/compat/posix.h @@ -487,7 +487,6 @@ int git_qsort_s(void *base, size_t nmemb, size_t size, } while (0) #ifdef NO_NSEC -#undef USE_NSEC #define ST_CTIME_NSEC(st) 0 #define ST_MTIME_NSEC(st) 0 #else diff --git a/configure.ac b/configure.ac index cfb50112bf81ff..fc956776ab0445 100644 --- a/configure.ac +++ b/configure.ac @@ -351,12 +351,6 @@ GIT_PARSE_WITH(iconv)) ## --enable-FEATURE[=ARG] and --disable-FEATURE # -# Define USE_NSEC below if you want git to care about sub-second file mtimes -# and ctimes. Note that you need recent glibc (at least 2.2.4) for this, and -# it will BREAK YOUR LOCAL DIFFS! show-diff and anything using it will likely -# randomly break unless your underlying filesystem supports those sub-second -# times (my ext3 doesn't). -# # Define USE_STDEV below if you want git to care about the underlying device # change being considered an inode change from the update-index perspective. diff --git a/environment.c b/environment.c index 6676e6f5ae5e03..c83cf448394168 100644 --- a/environment.c +++ b/environment.c @@ -571,6 +571,13 @@ int git_default_core_config(const char *var, const char *value, return 0; } +#ifndef NO_NSEC + if (!strcmp(var, "core.usenanosec")) { + cfg->use_nanosec = git_config_bool(var, value); + return 0; + } +#endif + /* Add other config variables here and to Documentation/config.adoc. */ return platform_core_config(var, value, ctx, cb); } @@ -769,6 +776,7 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->ignore_case = 0; cfg->trust_executable_bit = 1; cfg->has_symlinks = platform_has_symlinks(); + cfg->use_nanosec = 0; /* section "sparse" config values */ cfg->sparse_expect_files_outside_of_patterns = 0; diff --git a/environment.h b/environment.h index e7ec5b0437342d..a35534afe5c974 100644 --- a/environment.h +++ b/environment.h @@ -139,6 +139,7 @@ struct repo_config_values { int ignore_case; int trust_executable_bit; int has_symlinks; + int use_nanosec; /* section "sparse" config values */ int sparse_expect_files_outside_of_patterns; diff --git a/read-cache.c b/read-cache.c index 6c449f393d8d4d..b32cfd0ef13ab3 100644 --- a/read-cache.c +++ b/read-cache.c @@ -354,15 +354,12 @@ static int is_racy_stat(const struct index_state *istate, const struct stat_data *sd) { return (istate->timestamp.sec && -#ifdef USE_NSEC - /* nanosecond timestamped files can also be racy! */ - (istate->timestamp.sec < sd->sd_mtime.sec || - (istate->timestamp.sec == sd->sd_mtime.sec && - istate->timestamp.nsec <= sd->sd_mtime.nsec)) -#else - istate->timestamp.sec <= sd->sd_mtime.sec -#endif - ); + /* nanosecond timestamped files can also be racy! */ + (repo_config_values(istate->repo)->use_nanosec + ? (istate->timestamp.sec < sd->sd_mtime.sec || + (istate->timestamp.sec == sd->sd_mtime.sec && + istate->timestamp.nsec <= sd->sd_mtime.nsec)) + : istate->timestamp.sec <= sd->sd_mtime.sec)); } int is_racy_timestamp(const struct index_state *istate, diff --git a/statinfo.c b/statinfo.c index 5e00af127d657d..d9ddcf93820825 100644 --- a/statinfo.c +++ b/statinfo.c @@ -72,13 +72,13 @@ int match_stat_data(const struct stat_data *sd, struct stat *st) sd->sd_ctime.sec != (unsigned int)st->st_ctime) changed |= CTIME_CHANGED; -#ifdef USE_NSEC - if (cfg->check_stat && sd->sd_mtime.nsec != ST_MTIME_NSEC(*st)) - changed |= MTIME_CHANGED; - if (cfg->trust_ctime && cfg->check_stat && - sd->sd_ctime.nsec != ST_CTIME_NSEC(*st)) - changed |= CTIME_CHANGED; -#endif + if (cfg->use_nanosec) { + if (cfg->check_stat && sd->sd_mtime.nsec != ST_MTIME_NSEC(*st)) + changed |= MTIME_CHANGED; + if (cfg->trust_ctime && cfg->check_stat && + sd->sd_ctime.nsec != ST_CTIME_NSEC(*st)) + changed |= CTIME_CHANGED; + } if (cfg->check_stat) { if (sd->sd_uid != (unsigned int) st->st_uid || From 87fcb886b4711e1c41ce444423af82d7b69adb4b Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Thu, 10 Sep 2026 23:54:06 +0200 Subject: [PATCH 034/155] doc: add proc-receive hook info in 'git-receive-pack.adoc' The manpage of git-receive-pack(1) documents hooks invoked when receiving a push. The manpage does not mention the 'proc-receive' hook though, which is also invoked as part of that process. Add a paragraph about this hook to plug that gap. Helped-by: Patrick Steinhardt Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- Documentation/git-receive-pack.adoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Documentation/git-receive-pack.adoc b/Documentation/git-receive-pack.adoc index 0956086d6117d3..5806792ba79965 100644 --- a/Documentation/git-receive-pack.adoc +++ b/Documentation/git-receive-pack.adoc @@ -236,6 +236,14 @@ if the repository is packed and is served via a dumb transport. exec git update-server-info ---- +PROC-RECEIVE HOOK +----------------- +This hook is invoked by linkgit:git-receive-pack[1]. If the server has +set the multi-valued config variable `receive.procReceiveRefs`, and the +commands sent to 'receive-pack' have matching reference names, these +commands will be executed by this hook, instead of by the internal +`execute_commands()` function. This hook is responsible for updating +the relevant references and reporting the results back to 'receive-pack'. QUARANTINE ENVIRONMENT ---------------------- From 57c09985947224ffdee464111d0a3d0750207341 Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Thu, 10 Sep 2026 23:54:07 +0200 Subject: [PATCH 035/155] receive-pack: drop static variables to track report status version In 'git-receive-pack(1)', to track the report status version, we use the static variables `report_status` and `report_status_v2`. As the report status version is mutually exclusive, using an enum better suits the requirement. switch to using a new `enum report_status_version`, while also dropping the static variable to make the flow easier to understand. Helped-by: Junio C Hamano Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- builtin/receive-pack.c | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index e6e54ba55f7a8c..75a1788fa508c5 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -53,6 +53,12 @@ enum deny_action { DENY_UPDATE_INSTEAD }; +enum report_status_version { + REPORT_STATUS_UNKNOWN = 0, + REPORT_STATUS_V0, + REPORT_STATUS_V2, +}; + static int deny_deletes; static int deny_non_fast_forwards; static enum deny_action deny_current_branch = DENY_UNCONFIGURED; @@ -64,8 +70,6 @@ static int advertise_atomic_push = 1; static int advertise_push_options; static int advertise_sid; static off_t max_input_size; -static int report_status; -static int report_status_v2; static int use_sideband; static int use_atomic; static int use_push_options; @@ -2191,7 +2195,8 @@ static void queue_commands_from_cert(struct command **tail, } static struct command *read_head_info(struct packet_reader *reader, - struct oid_array *shallow) + struct oid_array *shallow, + enum report_status_version *version) { struct command *commands = NULL; struct command **p = &commands; @@ -2217,9 +2222,9 @@ static struct command *read_head_info(struct packet_reader *reader, const char *client_sid; size_t len = 0; if (parse_feature_request(feature_list, "report-status")) - report_status = 1; + *version = REPORT_STATUS_V0; if (parse_feature_request(feature_list, "report-status-v2")) - report_status_v2 = 1; + *version = REPORT_STATUS_V2; if (parse_feature_request(feature_list, "side-band-64k")) use_sideband = LARGE_PACKET_MAX; if (parse_feature_request(feature_list, "quiet")) @@ -2500,6 +2505,7 @@ int cmd_receive_pack(int argc, struct shallow_info si; struct packet_reader reader; struct odb_transaction *transaction = NULL; + enum report_status_version version = REPORT_STATUS_UNKNOWN; struct option options[] = { OPT__QUIET(&quiet, N_("quiet")), @@ -2563,7 +2569,7 @@ int cmd_receive_pack(int argc, PACKET_READ_CHOMP_NEWLINE | PACKET_READ_DIE_ON_ERR_PACKET); - if ((commands = read_head_info(&reader, &shallow))) { + if ((commands = read_head_info(&reader, &shallow, &version))) { struct string_list push_options = STRING_LIST_INIT_DUP; struct strbuf unpack_status = STRBUF_INIT; @@ -2596,10 +2602,18 @@ int cmd_receive_pack(int argc, &push_options); odb_transaction_finalize(transaction); sigchain_push(SIGPIPE, SIG_IGN); - if (report_status_v2) + + switch (version) { + case REPORT_STATUS_V2: report_v2(commands, &unpack_status); - else if (report_status) + break; + case REPORT_STATUS_V0: report(commands, &unpack_status); + break; + case REPORT_STATUS_UNKNOWN: + break; + } + sigchain_pop(SIGPIPE); run_receive_hook(commands, "post-receive", 1, NULL, &push_options); From f81e34b3f7de23f1250b11e38a6f84decf8abb13 Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Thu, 10 Sep 2026 23:54:08 +0200 Subject: [PATCH 036/155] receive-pack: move message generation to separate function After git-receive-pack(1) has committed the reference updates, we call either `report()` or `report_v2()` to report to the client which of the references we have updated successfully and which updates have failed. The only difference between those two functions is that the latter also knows to provide a more detailed report about how exactly a given reference was updated. With this, also drop `report_v2()` as both report functions now are similar in structure with only the `report_status_version` differentiating them. In the next commit we're about to add another site that wants to generate these reports. Refactor the logic into a shared function that can easily be reused. Helped-by: Patrick Steinhardt Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- builtin/receive-pack.c | 75 ++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 43 deletions(-) diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 75a1788fa508c5..8b1ae4f7f31055 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -2414,67 +2414,58 @@ static void update_shallow_info(struct command *commands, free(ref_status); } -static void report(struct command *commands, const struct strbuf *unpack_status) +/* + * Generate the response to be sent to the client invoking 'git-receive-pack(1)'. + */ +static void generate_report(struct strbuf *buf, struct command *commands, + const struct strbuf *unpack_status, + enum report_status_version version) { struct command *cmd; - struct strbuf buf = STRBUF_INIT; - packet_buf_write(&buf, "unpack %s\n", + packet_buf_write(buf, "unpack %s\n", unpack_status->len ? unpack_status->buf : "ok"); - for (cmd = commands; cmd; cmd = cmd->next) { - if (!cmd->error_string) - packet_buf_write(&buf, "ok %s\n", - cmd->ref_name); - else - packet_buf_write(&buf, "ng %s %s\n", - cmd->ref_name, cmd->error_string); - } - packet_buf_flush(&buf); - - if (use_sideband) - send_sideband(1, 1, buf.buf, buf.len, use_sideband); - else - write_or_die(1, buf.buf, buf.len); - strbuf_release(&buf); -} - -static void report_v2(struct command *commands, const struct strbuf *unpack_status) -{ - struct command *cmd; - struct strbuf buf = STRBUF_INIT; - struct ref_push_report *report; - packet_buf_write(&buf, "unpack %s\n", - unpack_status->len ? unpack_status->buf : "ok"); for (cmd = commands; cmd; cmd = cmd->next) { + struct ref_push_report *report; int count = 0; - if (cmd->error_string) { - packet_buf_write(&buf, "ng %s %s\n", - cmd->ref_name, - cmd->error_string); + if (cmd->error_string) + packet_buf_write(buf, "ng %s %s\n", + cmd->ref_name, cmd->error_string); + else + packet_buf_write(buf, "ok %s\n", cmd->ref_name); + + if (version != REPORT_STATUS_V2 || cmd->error_string) continue; - } - packet_buf_write(&buf, "ok %s\n", - cmd->ref_name); + for (report = cmd->report; report; report = report->next) { if (count++ > 0) - packet_buf_write(&buf, "ok %s\n", + packet_buf_write(buf, "ok %s\n", cmd->ref_name); if (report->ref_name) - packet_buf_write(&buf, "option refname %s\n", + packet_buf_write(buf, "option refname %s\n", report->ref_name); if (report->old_oid) - packet_buf_write(&buf, "option old-oid %s\n", + packet_buf_write(buf, "option old-oid %s\n", oid_to_hex(report->old_oid)); if (report->new_oid) - packet_buf_write(&buf, "option new-oid %s\n", + packet_buf_write(buf, "option new-oid %s\n", oid_to_hex(report->new_oid)); if (report->forced_update) - packet_buf_write(&buf, "option forced-update\n"); + packet_buf_write(buf, "option forced-update\n"); } } - packet_buf_flush(&buf); + + packet_buf_flush(buf); +} + +static void report(struct command *commands, const struct strbuf *unpack_status, + enum report_status_version version) +{ + struct strbuf buf = STRBUF_INIT; + + generate_report(&buf, commands, unpack_status, version); if (use_sideband) send_sideband(1, 1, buf.buf, buf.len, use_sideband); @@ -2605,10 +2596,8 @@ int cmd_receive_pack(int argc, switch (version) { case REPORT_STATUS_V2: - report_v2(commands, &unpack_status); - break; case REPORT_STATUS_V0: - report(commands, &unpack_status); + report(commands, &unpack_status, version); break; case REPORT_STATUS_UNKNOWN: break; From 3bbb0864a27bcbc0d31539431ac65916ba197d1d Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Thu, 10 Sep 2026 23:54:09 +0200 Subject: [PATCH 037/155] hook: introduce the receive-report hook When running 'git-receive-pack(1)', there is no way for the server to intercept and modify the status report before it is sent back to the client. Servers with custom logic may need to transform or gate the report based on the outcome of external logic post reference updates. This is specially needed for our usecase at GitLab where we have custom MVCC logic on top of Git which creates a new version for each push operation. The new version is only committed when certain external operations post reference transaction succeed. So reporting the correct message based on the outcome of these operations is important. The outcome of these operations is only known after `execute_commands()` has returned and before the report is written. There is no point in receive-pack where the server can act on that. We cannot use any of the existing hooks as: - The pre-receive hook runs too early, as we haven't updated references at that point yet and we need to have the full view of all resulting updates (both objects and references). - The update hook is too inefficient as it runs once per reference, and we cannot trivially determine the last update. - The reference-transaction hook is not suited for this. It fires from within `ref_transaction_commit()`, which is before the outcome we need to report is known, so there is no phase at which it could give us the answer. It also does not contain any knowledge regarding the push and cannot communicate with the clients. - The proc-receive hook replaces execute_commands() for references matching 'receive.procReceiveRefs'. We need to gate the report for the push as a whole. - The post-receive and post-update hooks cannot be used as they run too late, at the point where we have already reported success to the client. Introduce a new 'receive-report' hook. The hook receives the complete pkt-line encoded status report on standard input, after all ref updates have been applied to the repository by execute_commands() but before the report is sent to the client. See linkgit:gitprotocol-pack[5] details on the protocol structure. The hook's stdout fully replaces the report sent to the client. receive-pack fully buffers the hook's stdout before acting on the exit status, so the exit code is known before the client receives anything. This gives two distinct behaviors depending on exit status: - Exit 0: the hook's stdout is used as the report. The hook can rewrite 'ok' lines to 'ng' lines to signal per-ref rejection to the client while receive-pack itself exits cleanly. The client marks rejected refs as '[remote rejected]' and exits with a non-zero status if any ref is 'ng'. - Non-zero exit: the hook's stdout is discarded, receive-pack modifies all references to be rejected with a 'receive-report hook failed' error. In both cases, any output the hook writes to standard error is forwarded to the client over the sideband channel and appears as 'remote:' lines on the client terminal. Writing to stderr alone does not affect the push outcome. Reference updates applied by execute_commands() are not rolled back in either failure mode. The hook can cause the client to perceive the push as failed, but cannot undo server-side changes. This creates a divergence that the server cannot resolve: the client leaves its remote-tracking reference at the old value while the update is in fact applied, and a later fetch may reveal the update that the push reported as rejected. The hook is therefore only appropriate for servers which can guarantee that a rejected update is not observable by any reader. In our case the transaction committed by execute_commands() produces a candidate version which is not visible to other readers and is only published once the subsequent operations succeed, so a report of 'ng' corresponds to a version that is discarded rather than published. On a repository where a committed reference update is immediately visible, rejecting a push from this hook would instead leave the pusher with a view that does not match the server. This hook does not use the config-based hook infrastructure, which supports running multiple scripts per hook event. This hook is a bidirectional filter: it receives the report on stdin and writes a modified version to stdout. Running multiple such scripts sequentially would require piping the output of one into the input of the next, which the current hook infrastructure does not support. A single-script design is therefore a natural fit, and is consistent with how 'proc-receive' is structured for the same reason. Helped-by: Patrick Steinhardt Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- Documentation/git-receive-pack.adoc | 9 + Documentation/githooks.adoc | 61 +++++++ builtin/receive-pack.c | 50 ++++++ t/meson.build | 1 + t/t5412-receive-report-hook.sh | 257 ++++++++++++++++++++++++++++ 5 files changed, 378 insertions(+) create mode 100755 t/t5412-receive-report-hook.sh diff --git a/Documentation/git-receive-pack.adoc b/Documentation/git-receive-pack.adoc index 5806792ba79965..ab668ffa0c06c5 100644 --- a/Documentation/git-receive-pack.adoc +++ b/Documentation/git-receive-pack.adoc @@ -245,6 +245,15 @@ commands will be executed by this hook, instead of by the internal `execute_commands()` function. This hook is responsible for updating the relevant references and reporting the results back to 'receive-pack'. +RECEIVE-REPORT HOOK +------------------- +This hook is invoked by 'git-receive-pack' after all the ref updates +have been applied but before the report is sent to the client. The hook +receives the complete report in pkt-line format on stdin and its stdout +replaces the report sent to the client, which allows the hook to rewrite +the outcomes or abort the push completely. See linkgit:githooks[5] for +the full protocol description. + QUARANTINE ENVIRONMENT ---------------------- diff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc index ed045940d18ba5..145642bf05437d 100644 --- a/Documentation/githooks.adoc +++ b/Documentation/githooks.adoc @@ -527,6 +527,67 @@ The exit status of the hook is ignored for any state except for the status will cause the transaction to be aborted. The hook will not be called with "aborted" state in that case. +receive-report +~~~~~~~~~~~~~~ + +This hook is invoked by linkgit:git-receive-pack[1] when it reacts to +`git push` and updates references in its repository. It executes on +the repository once after all refs have been updated and after all +accepted ref changes are applied to the repository, but before the +pkt-line encoded status report is sent back to the client. + +The hook receives the complete pkt-line encoded status report on +standard input, see linkgit:gitprotocol-pack[5] for details on the +structure. The hook's standard output entirely replaces the report +that is sent to the client. The hook must write a valid pkt-line +encoded report in the same format it received. The hook's stdout is +fully buffered by `receive-pack` before any data is sent to the client, +so the hook's exit status is known before the client receives anything. + +There are three distinct ways the hook can affect the push outcome: + +* To reject the push, modify the unpack status from `ok` to the required + error message. While `git-push` will fail, individual references may + still show success messages unless modified. + +* To reject individual ref updates while keeping `receive-pack` alive, + rewrite the corresponding `ok ` lines to + `ng [ ]` lines in the output and exit with status 0. + The client will then mark those specific refs as rejected while + treating any `ok` refs as successful. The push as a whole is + considered failed if any ref is `ng`, and `git push` will exit with + a non-zero status on the client side. + +* To abort the entire push unconditionally, exit with a non-zero + status. In this case the hook's stdout is discarded, `receive-pack` + modifies all references to be rejected with a 'receive-report hook + failed' error. + +Any output written to standard error is forwarded to the client over +the sideband channel and will appear as `remote:` lines on clients +using 'git-push(1)', regardless of the hook's exit status. Writing to +standard error alone does not affect the push outcome. + +Note that by the time this hook runs, all ref updates have already been +applied to the repository. Neither a non-zero exit nor rewriting refs +to `ng` rolls back any ref changes that were already committed +server-side. The hook can cause the client to perceive the push as +failed, but cannot undo the server-side updates. + +This means that reporting a reference as `ng` makes the client believe +the update did not happen while the server has in fact applied it. The +client leaves its remote-tracking reference at its old value, and a +later `git fetch` may reveal the very update that the push reported as +rejected. Neither Git nor the server can reconcile this; only the user, +by fetching again, will find out. + +This hook is therefore only appropriate for servers which can guarantee +that a rejected update is not observable by any reader, for example +because the committed transaction produces a candidate state that is +discarded rather than published. On a repository where a committed +reference update is immediately visible, using this hook to reject a +push will leave the pusher with a view that does not match the server. + push-to-checkout ~~~~~~~~~~~~~~~~ diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 8b1ae4f7f31055..9ac7717096714e 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -992,6 +992,41 @@ static int run_update_hook(struct command *cmd) return code; } +static int run_receive_report_hook(struct strbuf *report) +{ + struct child_process proc = CHILD_PROCESS_INIT; + struct async sideband_async; + int sideband_async_started = 0; + int saved_stderr = -1; + struct strbuf out = STRBUF_INIT; + const char *hook_path; + int ret; + + hook_path = find_hook(the_repository, "receive-report"); + if (!hook_path) + return 0; + + strvec_push(&proc.args, hook_path); + proc.trace2_hook_name = "receive-report"; + + prepare_sideband_async(&sideband_async, &saved_stderr, + &sideband_async_started); + + sigchain_push(SIGPIPE, SIG_IGN); + ret = pipe_command(&proc, report->buf, report->len, &out, + report->len, NULL, 0); + sigchain_pop(SIGPIPE); + + finish_sideband_async(&sideband_async, saved_stderr, + sideband_async_started); + + if (!ret) + strbuf_swap(&out, report); + + strbuf_release(&out); + return ret; +} + static struct command *find_command_by_refname(struct command *list, const char *refname) { @@ -2414,6 +2449,15 @@ static void update_shallow_info(struct command *commands, free(ref_status); } +static void override_cmds_error(struct command *commands, const char *err) +{ + for (struct command *cmd = commands; cmd; cmd = cmd->next) { + if (cmd->error_string_owned) + FREE_AND_NULL(cmd->error_string_owned); + cmd->error_string = err; + } +} + /* * Generate the response to be sent to the client invoking 'git-receive-pack(1)'. */ @@ -2467,6 +2511,12 @@ static void report(struct command *commands, const struct strbuf *unpack_status, generate_report(&buf, commands, unpack_status, version); + if (run_receive_report_hook(&buf)) { + strbuf_reset(&buf); + override_cmds_error(commands, "receive-report hook failed"); + generate_report(&buf, commands, unpack_status, version); + } + if (use_sideband) send_sideband(1, 1, buf.buf, buf.len, use_sideband); else diff --git a/t/meson.build b/t/meson.build index 7f53cca7d1f891..692e6011c5a12c 100644 --- a/t/meson.build +++ b/t/meson.build @@ -652,6 +652,7 @@ integration_tests = [ 't5409-colorize-remote-messages.sh', 't5410-receive-pack.sh', 't5411-proc-receive-hook.sh', + 't5412-receive-report-hook.sh', 't5500-fetch-pack.sh', 't5501-fetch-push-alternates.sh', 't5502-quickfetch.sh', diff --git a/t/t5412-receive-report-hook.sh b/t/t5412-receive-report-hook.sh new file mode 100755 index 00000000000000..2f6515b5f06b27 --- /dev/null +++ b/t/t5412-receive-report-hook.sh @@ -0,0 +1,257 @@ +#!/bin/sh + +test_description='test receive-report hook' + +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +. ./test-lib.sh + +. "$TEST_DIRECTORY"/t5411/common-functions.sh + +URL_PREFIX="\.\." + +test_expect_success "setup workbench" ' + git init workbench && + create_commits_in workbench A B +' + +test_expect_success "no report hook, push succeeds" ' + test_when_finished "rm -rf upstream" && + test_when_finished "git -C workbench remote remove origin" && + git init --bare upstream && + + git -C workbench remote add origin ../upstream && + git -C workbench push origin $A:refs/heads/main && + git -C workbench push origin $B:refs/heads/main >out 2>&1 && + + make_user_friendly_and_stable_output actual && + cat >expect <<-\EOF && + To ../upstream + .. -> main + EOF + test_cmp expect actual +' + +test_expect_success "passthrough does not alter report" ' + test_when_finished "rm -rf upstream" && + test_when_finished "git -C workbench remote remove origin" && + git init --bare upstream && + + test_hook -C upstream --setup receive-report <<-\EOF && + cat + EOF + + git -C workbench remote add origin ../upstream && + git -C workbench push origin $A:refs/heads/main && + git -C workbench push origin $B:refs/heads/main >out 2>&1 && + + make_user_friendly_and_stable_output actual && + cat >expect <<-\EOF && + To ../upstream + .. -> main + EOF + test_cmp expect actual +' + +test_expect_success "non-zero exit reports as hook failed" ' + test_when_finished "rm -rf upstream" && + test_when_finished "git -C workbench remote remove origin" && + + git init --bare upstream && + git -C workbench remote add origin ../upstream && + git -C workbench push origin $A:refs/heads/main && + + test_hook -C upstream --setup receive-report <<-\EOF && + exit 1 + EOF + + test_must_fail git -C workbench push origin $B:refs/heads/main >out 2>&1 && + make_user_friendly_and_stable_output actual && + cat >expect <<-\EOF && + To ../upstream + ! [remote rejected] -> main (receive-report hook failed) + EOF + test_cmp expect actual +' + +test_expect_success "hook is invoked and receives report on stdin" ' + test_when_finished "rm -rf upstream" && + test_when_finished "git -C workbench remote remove origin" && + + git init --bare upstream && + test_hook -C upstream --setup receive-report <<-EOF && + tee raw + EOF + + git -C workbench remote add origin ../upstream && + git -C workbench push origin $A:refs/heads/main && + git -C workbench push origin $B:refs/heads/main >out 2>&1 && + + make_user_friendly_and_stable_output actual && + cat >expect <<-EOF && + To ../upstream + .. -> main + EOF + test_cmp expect actual && + + test-tool pkt-line unpack actual-report && + cat >expect-report <<-EOF && + unpack ok + ok refs/heads/main + 0000 + EOF + test_cmp expect-report actual-report +' + +test_expect_success "hook can modify the report sent to client" ' + test_when_finished "rm -rf upstream" && + test_when_finished "git -C workbench remote remove origin" && + + git init --bare upstream && + git -C workbench remote add origin ../upstream && + git -C workbench push origin $A:refs/heads/main && + + test_hook -C upstream --setup receive-report <<-\EOF && + test-tool pkt-line unpack | + sed "s/^ok /ng /" | + test-tool pkt-line pack + EOF + + test_must_fail git -C workbench push origin $B:refs/heads/main >out 2>&1 && + make_user_friendly_and_stable_output actual && + cat >expect <<-\EOF && + To ../upstream + ! [remote rejected] -> main (failed) + EOF + test_cmp expect actual +' + +test_expect_success "hook can modify the unpack status" ' + test_when_finished "rm -rf upstream" && + test_when_finished "git -C workbench remote remove origin" && + + git init --bare upstream && + git -C workbench remote add origin ../upstream && + git -C workbench push origin $A:refs/heads/main && + + test_hook -C upstream --setup receive-report <<-\EOF && + test-tool pkt-line unpack | + sed "s/^unpack ok$/unpack push failed due to server error/" | + test-tool pkt-line pack + EOF + + test_must_fail git -C workbench push origin $B:refs/heads/main >out 2>&1 && + test_grep "error: remote unpack failed: push failed due to server error" out && + make_user_friendly_and_stable_output actual && + cat >expect <<-\EOF && + To ../upstream + .. -> main + EOF + test_cmp expect actual +' + +test_expect_success "hook can report a custom failure message" ' + test_when_finished "rm -rf upstream" && + test_when_finished "git -C workbench remote remove origin" && + + git init --bare upstream && + git -C workbench remote add origin ../upstream && + git -C workbench push origin $A:refs/heads/main && + + test_hook -C upstream --setup receive-report <<-\EOF && + echo "push rejected: service X is down" >&2 + test-tool pkt-line unpack | + sed "s/^ok \(.*\)/ng \1 service-x-is-down/" | + test-tool pkt-line pack | + tee raw + EOF + + test_must_fail git -C workbench push origin $B:refs/heads/main >out 2>&1 && + test_grep "push rejected: service X is down" out && + + test-tool pkt-line unpack actual-report && + cat >expect-report <<-\EOF && + unpack ok + ng refs/heads/main service-x-is-down + 0000 + EOF + test_cmp expect-report actual-report +' + +test_expect_success "hook stderr with zero exit status code" ' + test_when_finished "rm -rf upstream" && + test_when_finished "git -C workbench remote remove origin" && + + git init --bare upstream && + git -C workbench remote add origin ../upstream && + git -C workbench push origin $A:refs/heads/main && + + test_hook -C upstream --setup receive-report <<-\EOF && + echo "push rejected: service X is down" >&2 + tee raw + EOF + + git -C workbench push origin $B:refs/heads/main >out 2>&1 && + test_grep "push rejected: service X is down" out && + + test-tool pkt-line unpack actual-report && + cat >expect-report <<-\EOF && + unpack ok + ok refs/heads/main + 0000 + EOF + test_cmp expect-report actual-report +' + +test_expect_success "non-zero exit with pre-existing ng from proc-receive" ' + test_when_finished "rm -rf upstream" && + test_when_finished "git -C workbench remote remove origin" && + + git init --bare upstream && + git -C upstream config receive.procReceiveRefs refs/for && + git -C workbench remote add origin ../upstream && + git -C workbench push origin $A:refs/heads/main && + + # Use a proc-receive hook to generate a dynamic error string. + # This is used to capture any leaks stemming from overriding the + # error message via the receive-report. + test_hook -C upstream --setup proc-receive <<-\EOF && + test-tool proc-receive -r "ng refs/for/main/topic push-rejected-by-service-x" + EOF + + test_hook -C upstream --setup receive-report <<-\EOF && + tee raw + exit 1 + EOF + + test_must_fail git -C workbench push origin HEAD:refs/for/main/topic >out 2>&1 && + test_grep "receive-report hook failed" out && + + test-tool pkt-line unpack actual-report && + cat >expect-report <<-\EOF && + unpack ok + ng refs/for/main/topic push-rejected-by-service-x + 0000 + EOF + test_cmp expect-report actual-report +' + +test_expect_success "hook stderr is relayed to client via sideband" ' + test_when_finished "rm -rf upstream" && + test_when_finished "git -C workbench remote remove origin" && + + git init --bare upstream && + git -C workbench remote add origin ../upstream && + git -C workbench push origin $A:refs/heads/main && + + test_hook -C upstream --setup receive-report <<-\EOF && + echo "hook-stderr-message" >&2 + exit 1 + EOF + + test_must_fail git -C workbench push origin $B:refs/heads/main >out 2>&1 && + test_grep "remote: hook-stderr-message" out +' + +test_done From d870565117a948be0b8a6ac48b7a862628ed18f1 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 11 Sep 2026 15:09:47 -0700 Subject: [PATCH 038/155] cocci: remove risky "if (!E) free(E)" conversion The current cocci patches try to convert if (!E) free(E); into an unconditional call to free(E), with the rationale cocci: detect useless free(3) calls Add a semantic patch for removing checks that cause free(3) to only be called with a NULL pointer, as that must be a programming mistake. which came from ec6cd14c7a (cocci: detect useless free(3) calls, 2017-02-11). Leaving _something_ in ALL.patch output to draw programmers' attention is a good thing, but this changes a piece of code that is originally a no-op to do something else, which may be even worse. We could change it to if (!E) BUG("free(E) is certainly not what we meant to write"); to force programmers to think. But it probably is safer to just rewrite one form of no-op into a simpler form of no-op. Signed-off-by: Junio C Hamano --- tools/coccinelle/free.cocci | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tools/coccinelle/free.cocci b/tools/coccinelle/free.cocci index 03799e190828fa..3dfaae9dd86f97 100644 --- a/tools/coccinelle/free.cocci +++ b/tools/coccinelle/free.cocci @@ -8,16 +8,6 @@ expression E; commit_list_free(E); ) -@@ -expression E; -@@ -- if (!E) -( - free(E); -| - commit_list_free(E); -) - @@ expression E; @@ From 32814b69d05714c42def93b44a6ff8f1a242e8fe Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 11 Sep 2026 15:21:35 -0700 Subject: [PATCH 039/155] cocci: FREE_AND_NULL(E) is safe to call on NULL Just like we allow calling free(E) without checking if E is not NULL, it is safe to call FREE_AND_NULL(E) unconditionally. Signed-off-by: Junio C Hamano --- tools/coccinelle/free.cocci | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/coccinelle/free.cocci b/tools/coccinelle/free.cocci index 3dfaae9dd86f97..f2af140cfb0d75 100644 --- a/tools/coccinelle/free.cocci +++ b/tools/coccinelle/free.cocci @@ -6,6 +6,8 @@ expression E; free(E); | commit_list_free(E); +| + FREE_AND_NULL(E); ) @@ From d8e458582219ef786545ce7c083595c5ec91d5d3 Mon Sep 17 00:00:00 2001 From: Ariel Keselman Date: Fri, 11 Sep 2026 18:46:09 -0700 Subject: [PATCH 040/155] refs/files: avoid packed-refs lock for root ref deletion Deleting a root ref queues a packed-ref transaction in the files backend, even though root refs cannot be packed. For example, holding .git/packed-refs.lock makes "git update-ref --no-deref -d AUTO_MERGE" fail, whether or not AUTO_MERGE exists. This also affects post-commit cleanup, which deletes AUTO_MERGE after updating HEAD. In a linked worktree with read-only shared metadata, commit succeeds but cleanup reports a packed-refs.lock error. Deleting CHERRY_PICK_HEAD and REVERT_HEAD is affected as well. Skip the packed transaction for root-ref deletions. Keep loose-ref locking and packed-ref deletion for other refs unchanged. Signed-off-by: Ariel Keselman Signed-off-by: Junio C Hamano --- refs/files-backend.c | 8 +++++--- t/t0600-reffiles-backend.sh | 40 +++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/refs/files-backend.c b/refs/files-backend.c index a4c7858787127d..0333d5d355dc3e 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -2981,10 +2981,12 @@ static int files_transaction_prepare(struct ref_store *ref_store, if (update->flags & REF_DELETING && !(update->flags & REF_LOG_ONLY) && - !(update->flags & REF_IS_PRUNING)) { + !(update->flags & REF_IS_PRUNING) && + !is_root_ref(update->refname)) { /* - * This reference has to be deleted from - * packed-refs if it exists there. + * This reference has to be deleted from packed-refs if it + * exists there. Root refs are never packed, so we do not + * have to delete them from packed-refs. */ if (!packed_transaction) { packed_transaction = ref_store_transaction_begin( diff --git a/t/t0600-reffiles-backend.sh b/t/t0600-reffiles-backend.sh index 74bfa2e9ba060d..560caf56b088f8 100755 --- a/t/t0600-reffiles-backend.sh +++ b/t/t0600-reffiles-backend.sh @@ -519,4 +519,44 @@ test_expect_success 'symref transaction supports false symlink config' ' test_cmp expect actual ' +test_expect_success 'deleting a root ref does not lock packed-refs' ' + test_when_finished "rm -rf root-ref" && + git init root-ref && + ( + cd root-ref && + test_commit initial && + git update-ref AUTO_MERGE HEAD && + git pack-refs --all && + test_path_is_file .git/AUTO_MERGE && + cp .git/packed-refs expect && + : >.git/packed-refs.lock && + git update-ref --no-deref -d AUTO_MERGE && + test_path_is_missing .git/AUTO_MERGE && + test_path_is_file .git/packed-refs.lock && + test_cmp expect .git/packed-refs + ) +' + +test_expect_success 'deleting root and packed refs in one transaction requires packed-refs lock' ' + test_when_finished "rm -rf root-ref" && + git init root-ref && + ( + cd root-ref && + test_commit initial && + git update-ref refs/heads/packed-branch HEAD && + git pack-refs --all && + test_path_is_missing .git/refs/heads/packed-branch && + git update-ref AUTO_MERGE HEAD && + git refs list --include-root-refs >expect && + : >.git/packed-refs.lock && + test_must_fail git update-ref --no-deref --stdin 2>err <<-EOF && + delete AUTO_MERGE + delete refs/heads/packed-branch + EOF + test_grep "Unable to create .*packed-refs.lock" err && + git refs list --include-root-refs >actual && + test_cmp expect actual + ) +' + test_done From c090a2363c8209998fcd0f72e85dab5c289f2382 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 14 Sep 2026 15:36:05 -0700 Subject: [PATCH 041/155] receive-pack: coccinelle fix Let's not check the nullness of cmd->error_string_owned before calling FREE_AND_NULL(cmd->error_string_owned). It is cheap and safe to call FREE_AND_NULL(variable) for a variable that has NULL in it. Signed-off-by: Junio C Hamano --- builtin/receive-pack.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 9ac7717096714e..1d5b050beb4e72 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -2452,8 +2452,7 @@ static void update_shallow_info(struct command *commands, static void override_cmds_error(struct command *commands, const char *err) { for (struct command *cmd = commands; cmd; cmd = cmd->next) { - if (cmd->error_string_owned) - FREE_AND_NULL(cmd->error_string_owned); + FREE_AND_NULL(cmd->error_string_owned); cmd->error_string = err; } } From 8f2f2656222298285cbc1439d500642a060824fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SZEDER=20G=C3=A1bor?= Date: Tue, 15 Sep 2026 08:09:49 +0200 Subject: [PATCH 042/155] Makefile: remove XDIFF_OBJS initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Object files under 'xlib/' used to be listed in the XDIFF_OBJS Makefile variable so we could build a static library from them. This static library was removed in cf680cdb95 (make: delete XDIFF_LIB, add xdiff to LIB_OBJS, 2025-10-02), along with filling XDIFF_OBJS with object files. But the initial empty initalization of XDIFF_OBJS remained, so remove it now. Signed-off-by: SZEDER Gábor Signed-off-by: Junio C Hamano --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index d4b775953d3842..7d9ac15c74738a 100644 --- a/Makefile +++ b/Makefile @@ -695,7 +695,6 @@ BUILTIN_OBJS = BUILT_INS = COMPAT_CFLAGS = COMPAT_OBJS = -XDIFF_OBJS = GENERATED_H = EXTRA_CPPFLAGS = FUZZ_OBJS = From 986cd1cd9e2c10bbcccd510be699a68fdf2ceaa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SZEDER=20G=C3=A1bor?= Date: Tue, 15 Sep 2026 08:09:50 +0200 Subject: [PATCH 043/155] cmake: remove any "$(*_OBJS)" variables when parsing Makefile for sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To get various lists of files, CMake parses our Makefile looking for lines matching e.g. "list_var += ...". In case of LIB_OBJS this picks up the line "LIB_OBJS += $(COMPAT_OBJS)" as well, so the parsing macro has a specific instruction to remove "$(COMPAT_OBJS)" from the resulting list. Currently this is the only such Makefile variable to be removed from the list, but the next patches will (re)introduce more variables containing lists of object files, so let's generalize that removing instruction to remove any "$(*_OBJS)" Makefile variable as well. Note that we can't make the pattern matching the Makefile variable too general, e.g. to match any "$(VARIABLE)", because some lines of our Makefile do contain variables as directory prefixes, e.g. "UNIT_TEST_OBJS += $(UNIT_TEST_DIR)/test-lib.o", and we must definitely keep those. Signed-off-by: SZEDER Gábor Signed-off-by: Junio C Hamano --- contrib/buildsystems/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt index e76bd19b65642b..42244b3c0a93cd 100644 --- a/contrib/buildsystems/CMakeLists.txt +++ b/contrib/buildsystems/CMakeLists.txt @@ -102,7 +102,7 @@ project(git macro(parse_makefile_for_sources list_var makefile regex) file(STRINGS ${makefile} ${list_var} REGEX "^${regex} \\+=(.*)") string(REPLACE "${regex} +=" "" ${list_var} ${${list_var}}) - string(REPLACE "$(COMPAT_OBJS)" "" ${list_var} ${${list_var}}) #remove "$(COMPAT_OBJS)" This is only for libgit. + string(REGEX REPLACE "\\$\\([^)]*_OBJS\\)" "" ${list_var} ${${list_var}}) # remove any "$(*_OBJS)" variables string(STRIP ${${list_var}} ${list_var}) #remove trailing/leading whitespaces string(REPLACE ".o" ".c;" ${list_var} ${${list_var}}) #change .o to .c, ; is for converting the string into a list list(TRANSFORM ${list_var} STRIP) #remove trailing/leading whitespaces for each element in list From af36bb91f925a545c5ee157a912b63140316d08b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SZEDER=20G=C3=A1bor?= Date: Tue, 15 Sep 2026 08:09:51 +0200 Subject: [PATCH 044/155] Makefile: reintroduce REFTABLE_OBJS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the next commit we are about to precompile "git-compat-util.h" with "make" to reduce build times. But using the precompiled header should not change what actually gets compiled, therefore a source file can only be compiled using the precompiled header if the first included header file is "git-compat-util.h". The reftable source files are kind of special, because the reftable implementation is supposed to be easily includable in other projects. Therefore, the reftable source files don't include "git-compat-util.h", with the sole exception of the purposefully project-specific "reftable/system.c". Consequently, they shouldn't be compiled with our precompiled header. List object files under "reftable" in the REFTABLE_OBJS Makefile variable, so in the next commit we'll be able to easily filter them out and keep building them the old way, without the precompiled header. Note that object files under "reftable/" used to be listed in REFTABLE_OBJS so we could build a static library from them. This static library was removed in f3b4c89d59 (make: delete REFTABLE_LIB, add reftable to LIB_OBJS, 2025-10-02), along with filling REFTALBE_OBJS with object files. This change essentially reverts the removal of REFTABLE_OBJS, but not the building of that static library. Signed-off-by: SZEDER Gábor Signed-off-by: Junio C Hamano --- Makefile | 32 ++++++++++++++++------------- contrib/buildsystems/CMakeLists.txt | 4 ++++ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 7d9ac15c74738a..2c4d6f4d4ae846 100644 --- a/Makefile +++ b/Makefile @@ -707,6 +707,7 @@ OBJECTS = OTHER_PROGRAMS = PROGRAM_OBJS = PROGRAMS = +REFTABLE_OBJS = RUST_SOURCES = EXCLUDED_PROGRAMS = SCRIPT_PERL = @@ -1284,20 +1285,6 @@ LIB_OBJS += refs/iterator.o LIB_OBJS += refs/packed-backend.o LIB_OBJS += refs/ref-cache.o LIB_OBJS += refspec.o -LIB_OBJS += reftable/basics.o -LIB_OBJS += reftable/block.o -LIB_OBJS += reftable/blocksource.o -LIB_OBJS += reftable/error.o -LIB_OBJS += reftable/fsck.o -LIB_OBJS += reftable/iter.o -LIB_OBJS += reftable/merged.o -LIB_OBJS += reftable/pq.o -LIB_OBJS += reftable/record.o -LIB_OBJS += reftable/stack.o -LIB_OBJS += reftable/system.o -LIB_OBJS += reftable/table.o -LIB_OBJS += reftable/tree.o -LIB_OBJS += reftable/writer.o LIB_OBJS += remote.o LIB_OBJS += repack.o LIB_OBJS += repack-cruft.o @@ -1386,6 +1373,23 @@ LIB_OBJS += xdiff/xpatience.o LIB_OBJS += xdiff/xprepare.o LIB_OBJS += xdiff/xutils.o +REFTABLE_OBJS += reftable/basics.o +REFTABLE_OBJS += reftable/block.o +REFTABLE_OBJS += reftable/blocksource.o +REFTABLE_OBJS += reftable/error.o +REFTABLE_OBJS += reftable/fsck.o +REFTABLE_OBJS += reftable/iter.o +REFTABLE_OBJS += reftable/merged.o +REFTABLE_OBJS += reftable/pq.o +REFTABLE_OBJS += reftable/record.o +REFTABLE_OBJS += reftable/stack.o +REFTABLE_OBJS += reftable/system.o +REFTABLE_OBJS += reftable/table.o +REFTABLE_OBJS += reftable/tree.o +REFTABLE_OBJS += reftable/writer.o + +LIB_OBJS += $(REFTABLE_OBJS) + BUILTIN_OBJS += builtin/add.o BUILTIN_OBJS += builtin/am.o BUILTIN_OBJS += builtin/annotate.o diff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt index 42244b3c0a93cd..e58403c801410c 100644 --- a/contrib/buildsystems/CMakeLists.txt +++ b/contrib/buildsystems/CMakeLists.txt @@ -675,6 +675,10 @@ include_directories(${CMAKE_BINARY_DIR}) #libgit parse_makefile_for_sources(libgit_SOURCES ${CMAKE_SOURCE_DIR}/Makefile "LIB_OBJS") +#reftable +parse_makefile_for_sources(reftable_SOURCES ${CMAKE_SOURCE_DIR}/Makefile "REFTABLE_OBJS") +list(APPEND libgit_SOURCES ${reftable_SOURCES}) + list(TRANSFORM libgit_SOURCES PREPEND "${CMAKE_SOURCE_DIR}/") list(TRANSFORM compat_SOURCES PREPEND "${CMAKE_SOURCE_DIR}/") From 8d3a6731471521eb1ee7e3dd73d15b123f7ba0b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SZEDER=20G=C3=A1bor?= Date: Tue, 15 Sep 2026 08:09:52 +0200 Subject: [PATCH 045/155] Makefile: precompile "git-compat-util.h" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch follows the idea of 671df48df8 (meson: precompile "git-compat-util.h", 2026-03-19) to make it faster to build Git using "make". The notable differences are the boilerplate needed to wire up the precompiled header with "make", and the selection of object files that are built using the precompiled header: - Add a new rule to precompile "tools/precompiled.h" into "tools/precompiled.h.gch". This rule and its dependencies are basically the same as our existing rule and its dependencies for compiling object files from our source files and, except that in this rule we don't use the EXTRA_CPPFLAGS target-specific variable. This is partly because any compiler flags in that target-specific variable can't possibly make sense for the project-wide "git-compat-util.h", and partly because it could run afoul of "make"'s quirks with target-specific variables and GCC's "-Winvalid-pch" compiler option [1]. Our Makefile always writes object files next to the source files they were compiled from. Since a precompiled header is in many ways similar to an object file, let's follow suit and write it next to the header file it was created from as well. 671df48df8 doesn't mention why "precompiled.h" was placed in "tools/". Use the ".gch" suffix for the precompiled header file, because that's the only suffix GCC looks for when searching for a precompiled header. Clang apparently looks for both ".pch" and ".gch" suffixes. If we ever encounter a compiler which only supports a different precompiled header suffix, then we might make the suffix configurable via a Makefile knob; but until then it remains hard-coded for the sake of simplicity. - Declare the precompiled header's dependencies, so it gets rebuilt when any of our header files it includes is modified. When using computed header dependencies, then the compiler can generate the Makefile snippet containing the header files inlcuded by the precompiled header, just like when compiling a C source file into an object file. When not using computed header dependencies, then make the precompiled header depend on the same header files that our object files depend on, i.e. those listed in LIB_H and GENERATED_H. This is not ideal, because the precompiled header will be rebuilt even when a header file it doesn't actually depend on changes; but in that case all object files will be rebuilt as well anyway. - List the object files that are built using the precompiled header in the PRECOMPILED_HEADER_USERS variable: - The precompiled header should not change what actually gets compiled. Therefore, use the precompiled header only when compiling source files that start with including "git-compat-util.h" (directly or indirectly, e.g. via "builtin.h"), or its inclusion is only preceeded by #define directives that don't influence "git-compat-util.h" between its include guards [2] (currently DISABLE_SIGN_COMPARE_WARNINGS, USE_THE_REPOSITORY_VARIABLE or GIT_TEST_PROGRESS_ONLY). [3] Several (but not all) object or source files listed in the COMPAT_OBJS, REFTABLE_OBJS and THIRD_PARTY_SOURCES variables don't include "git-compat-util.h", therefore, for the sake of simplicity, none of the files listed in these variables are built with the precompiled header. [4] Since 671df48df8 the Meson build uses the precompiled header even when compiling those reftable source files that don't include "git-compat-util.h" at all, although this change is not mentioned in the commit message. - But other than that, use the precompiled header when compiling all other source files, including e.g. source files for standalone executables (e.g. "daemon.c" for "git-daemon"), or those in the directories "t/helper/" and "t/unit-tests/" as well. The Meson build, however, only uses the precompiled header for compiling objects in "libgit_sources" and "builtin_sources", despite 671df48df8 giving the impression that the precompiled header is included in all compilation units. - In short, PRECOMPILED_HEADER_USERS contains all object files listed in OBJECTS, except those that are listed in COMPAT_OBJS, REFTABLE_OBJS or THIRD_PARTY_SOURCES as well. - Add a new rule to build object files listed in PRECOMPILED_HEADER_USERS with the precompiled header. This rule and its dependencies are basically the same as our existing rule and its dependencies compiling object files from our source files, except: - This rule depends on the precompiled header as well, to make sure that it's built before it is used. - Use the "-include tools/precompiled.h" option to make the precompiled header the first header in the compilation unit, because only then is it used to speed things up. - Use the "-Winvalid-pch" option to catch any issues when the precompiled header is present but can't be used for whatever reason. While being unable to use the precompiled header would normally only result in a slower build, it is probably better that developers are made aware that simething is not quite right. Meson uses this option as well when compiling a source file using the precompiled header. Note, that in case of such an issue with "-Winvalid-pch" we would get a warning for each object file that is build using the precompiled header, but together with "-Werror" (e.g. with DEVELOPER=1) it would fail the build. Object files not listed in PRECOMPILED_HEADER_USERS are built with the existing (and unchanged) rule for object files. - This way the precompiled header is compiled only once during the whole build process. The Meson build, however, currently compiles the precompiled header twice: once for "libgit_sources" and once for "builtin_sources", despite 671df48df8 giving the impression that it's compiled only once. And, by the looks of it, it would have to be compiled once more for each new library or executable where we declare the use of the precompiled header. - Add the Makefile knob NO_PRECOMPILED_HEADER to make it possible to build without using the precompiled header for testing purposes, or for any compiler that might be out there that doesn't support the "-Winvalid-pch" option. Note, that when this knob is set then we merely leave PRECOMPILED_HEADER_USERS empty: this way nothing is built with the precompiled header, but "make clean NO_PRECOMPILED_HEADER=1" would still remove a leftover precompiled header file. With this patch series on top of v2.55.0 I got the following build time improvement: Benchmark 1: make -j12 (rev = v2.55.0) Time (mean ± σ): 29.414 s ± 0.031 s [User: 254.786 s, System: 47.447 s] Range (min … max): 29.370 s … 29.470 s 10 runs Benchmark 2: make -j12 (rev = precompile) Time (mean ± σ): 21.725 s ± 0.047 s [User: 186.047 s, System: 35.109 s] Range (min … max): 21.643 s … 21.768 s 10 runs Summary 'make -j12 (rev = precompile)' ran 1.35 ± 0.00 times faster than 'make -j12 (rev = v2.55.0)' [1] "make" applies target-specific variables to the first target's dependencies as well. So, if the rule creating the precompiled header included $(EXTRA_CPPFLAGS) as well, then we could get this: $ make -s clean $ make V=1 git gcc -o tools/precompiled.h.gch -c -MF [...] -DSHELL_PATH='"/bin/sh"' '-DGIT_HTML_PATH="share/doc/git-doc"' '-DGIT_MAN_PATH="share/man"' '-DGIT_INFO_PATH="share/info"' tools/precompiled.h gcc -o git.o -c -include tools/precompiled.h -Winvalid-pch -MF [...] -DSHELL_PATH='"/bin/sh"' '-DGIT_HTML_PATH="share/doc/git-doc"' '-DGIT_MAN_PATH="share/man"' '-DGIT_INFO_PATH="share/info"' git.c gcc -o builtin/add.o -c -include tools/precompiled.h -Winvalid-pch -MF [...] -DSHELL_PATH='"/bin/sh"' builtin/add.c cc1: error: ./tools/precompiled.h.gch: not used because `GIT_MAN_PATH' not defined [-Werror=invalid-pch] cc1: all warnings being treated as errors make: *** [Makefile:2940: builtin/add.o] Error 1 So in this case "git.o" is the first object file to be build, and since it has a target-specific EXTRA_CPPFLAGS and since it depends on the precompiled header, the precompiled header is built with the same EXTRA_CPPFLAGS as well. But then "builtin/add.o" is built with empty EXTRA_CPPFLAGS, and GCC's sanity checks for the -Winvalid-pch option complain. Clang apparently doesn't consider this to be an issue, and builds "git" just fine. Note, however, that if the first object file to be built is not "git.o" (or one of the other object files with target-specific EXTRA_CPPFLAGS), which is usually the case with e.g. "make all", then the precompiled header is built with empty EXTRA_CPPFLAGS, and then all is well. Weird. We already had issues with "make" and target-specific variables in the past, see a673cfede6 (Makefile: Fix occasional GIT-CFLAGS breakage, 2010-03-19). The Meson build doesn't use target-specific variables, and the whole project needs to be rebuilt when e.g. "mandir" is changed, although this is not mentioned in 904339edbd (Introduce support for the Meson build system, 2024-12-06). [2] See baa61e46da (git-compat-util.h: move warning infra to prepare for PCHs, 2026-03-19). [3] This can be verified by simply scanning through the output of: { printf '%s\n' 'print-%:' printf '\t%s\n' '@printf "%s\n" $($*)' } >/tmp/printvars.mak for f in $(make -f /tmp/printvars.mak -f Makefile print-PRECOMPILED_HEADER_USERS) do f="${f%o}c" sed -n -E -e "s%# *(define|include).*%& $f%p" -e '/# *include/q' "$f" done | sort [4] "make"'s flexibility would allow us to separately list those object files that don't include "git-compat-util.h", but then on my (I assume fairly typical) Linux box the number of object files built using the precompiled header increases only by 9 (from 536 to 545), which reduces the build time by about 1% (0.2s). Therefore, I don't think it's worth the churn. Signed-off-by: SZEDER Gábor Signed-off-by: Junio C Hamano --- .gitignore | 1 + Makefile | 26 +++++++++++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 4da58c6754899e..0209bd16f209c2 100644 --- a/.gitignore +++ b/.gitignore @@ -233,6 +233,7 @@ /.vscode/ /tags /TAGS +/tools/precompiled.h.gch /cscope* /compile_commands.json /.cache/ diff --git a/Makefile b/Makefile index 2c4d6f4d4ae846..02162719db1eef 100644 --- a/Makefile +++ b/Makefile @@ -321,6 +321,9 @@ include shared.mak # dependency rules. The default is "auto", which means to use computed header # dependencies if your compiler is detected to support it. # +# Define NO_PRECOMPILED_HEADER if you want to build Git without precompiling +# "git-compat-util.h". +# # Define NATIVE_CRLF if your platform uses CRLF for line endings. # # Define GIT_USER_AGENT if you want to change how git identifies itself during @@ -2905,8 +2908,15 @@ endif .PHONY: objects objects: $(OBJECTS) -dep_files := $(foreach f,$(OBJECTS),$(dir $f).depend/$(notdir $f).d) -dep_dirs := $(addsuffix .depend,$(sort $(dir $(OBJECTS)))) +PRECOMPILED_HEADER := tools/precompiled.h +PRECOMPILED_HEADER_GCH := $(addsuffix .gch,$(PRECOMPILED_HEADER)) + +ifndef NO_PRECOMPILED_HEADER +PRECOMPILED_HEADER_USERS := $(filter-out $(COMPAT_OBJS) $(REFTABLE_OBJS) $(patsubst %.c,%.o,$(THIRD_PARTY_SOURCES)),$(OBJECTS)) +endif + +dep_files := $(foreach f,$(OBJECTS) $(PRECOMPILED_HEADER_GCH),$(dir $f).depend/$(notdir $f).d) +dep_dirs := $(addsuffix .depend,$(sort $(dir $(OBJECTS) $(PRECOMPILED_HEADER_GCH)))) ifeq ($(uname_S),Darwin) dep_dirs += $(addsuffix .depend,$(sort $(dir contrib/credential/osxkeychain/git-credential-osxkeychain.o))) @@ -2940,7 +2950,13 @@ missing_compdb_dir = compdb_args = endif -$(OBJECTS): %.o: %.c GIT-CFLAGS $(missing_dep_dirs) $(missing_compdb_dir) +$(PRECOMPILED_HEADER_GCH): %.gch: % GIT-CFLAGS $(missing_dep_dirs) $(missing_compdb_dir) + $(QUIET_CC)$(CC) -o $@ -c $(dep_args) $(compdb_args) $(ALL_CFLAGS) $< + +$(PRECOMPILED_HEADER_USERS): %.o: %.c $(PRECOMPILED_HEADER_GCH) GIT-CFLAGS $(missing_dep_dirs) $(missing_compdb_dir) + $(QUIET_CC)$(CC) -o $*.o -c -include $(PRECOMPILED_HEADER) -Winvalid-pch $(dep_args) $(compdb_args) $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $< + +$(filter-out $(PRECOMPILED_HEADER_USERS),$(OBJECTS)): %.o: %.c GIT-CFLAGS $(missing_dep_dirs) $(missing_compdb_dir) $(QUIET_CC)$(CC) -o $*.o -c $(dep_args) $(compdb_args) $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $< %.s: %.c GIT-CFLAGS FORCE @@ -2954,7 +2970,7 @@ ifneq ($(dep_files_present),) include $(dep_files_present) endif else -$(OBJECTS): $(LIB_H) $(GENERATED_H) +$(OBJECTS) $(PRECOMPILED_HEADER_GCH): $(LIB_H) $(GENERATED_H) endif ifeq ($(GENERATE_COMPILATION_DATABASE),yes) @@ -3908,7 +3924,7 @@ clean: profile-clean coverage-clean cocciclean $(RM) GIT-TEST-SUITES $(RM) po/git.pot po/git-core.pot $(RM) git.rc git.res - $(RM) $(OBJECTS) + $(RM) $(OBJECTS) $(PRECOMPILED_HEADER_GCH) $(RM) headless-git.o $(RM) $(LIB_FILE) $(RM) $(ALL_PROGRAMS) $(SCRIPT_LIB) $(BUILT_INS) $(OTHER_PROGRAMS) From 700f7b74de75ba9bc6c463e2ded8c0144390f9b8 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Wed, 16 Sep 2026 15:46:31 +0200 Subject: [PATCH 046/155] commit-reach: parse commits in the given repository `can_all_from_reach()` and `can_all_from_reach_with_flag()` parse the commits they walk in `the_repository`, even though their caller may be working in a different repository. `repo_is_descendant_of()` is such a caller: it is told which repository to work in, but as soon as generation numbers are enabled it hands the commits over to `can_all_from_reach()`, which then parses them elsewhere. This breaks merging a superproject whose submodule pointer advanced on both sides. merge-ort resolves it by calling `repo_in_merge_bases()` on the submodule, and with a commit-graph in both the superproject and the submodule the merge dies: $ git merge side fatal: invalid commit position. commit-graph is likely corrupt `merge_submodule()` looks the submodule commits up in the submodule, so walking their ancestry pulls in parents whose commit-graph position was recorded while reading the submodule's commit-graph. The walk then parses those parents in `the_repository`, where the recorded position indexes the superproject's commit-graph instead: `fill_commit_graph_info()` dies when the position is out of bounds, and quietly returns another commit's date, generation and parents when it is not. The latter used to be the only symptom. Before bb5da75d61 (commit: use commit graph in `lookup_commit_reference_gently()`, 2026-02-16) the initial lookup did not record commit-graph positions, so the walk simply failed to find the submodule commits in the superproject: error: Could not read Failed to merge submodule sub (commits don't follow merge-base) Pass the repository into both functions. git-fetch-pack(1) and git-upload-pack(1) keep passing `the_repository`. Reported-by: Florian Schmidt Signed-off-by: Orestis Floros Signed-off-by: Junio C Hamano --- commit-reach.c | 22 ++++++++++----------- commit-reach.h | 7 ++++--- fetch-pack.c | 4 ++-- t/helper/test-reach.c | 5 +++-- t/t6437-submodule-merge.sh | 39 ++++++++++++++++++++++++++++++++++++++ upload-pack.c | 6 +++--- 6 files changed, 62 insertions(+), 21 deletions(-) diff --git a/commit-reach.c b/commit-reach.c index 5df471a313cf6b..3d579d8f7f18ae 100644 --- a/commit-reach.c +++ b/commit-reach.c @@ -572,7 +572,7 @@ int repo_is_descendant_of(struct repository *r, struct commit_list *from_list = NULL; int result; commit_list_insert(commit, &from_list); - result = can_all_from_reach(from_list, with_commit, 0); + result = can_all_from_reach(r, from_list, with_commit, 0); commit_list_free(from_list); return result; } else { @@ -853,7 +853,8 @@ int commit_contains(struct ref_filter *filter, struct commit *commit, return repo_is_descendant_of(the_repository, commit, list); } -int can_all_from_reach_with_flag(struct object_array *from, +int can_all_from_reach_with_flag(struct repository *r, + struct object_array *from, unsigned int with_flag, unsigned int assign_flag, timestamp_t min_commit_date, @@ -872,8 +873,7 @@ int can_all_from_reach_with_flag(struct object_array *from, if (!from_one || from_one->flags & assign_flag) continue; - from_one = deref_tag(the_repository, from_one, - "a from object", 0); + from_one = deref_tag(r, from_one, "a from object", 0); if (!from_one || from_one->type != OBJ_COMMIT) { /* * no way to tell if this is reachable by @@ -886,7 +886,7 @@ int can_all_from_reach_with_flag(struct object_array *from, } list[nr_commits] = (struct commit *)from_one; - if (repo_parse_commit(the_repository, list[nr_commits]) || + if (repo_parse_commit(r, list[nr_commits]) || commit_graph_generation(list[nr_commits]) < min_generation) { result = 0; goto cleanup; @@ -921,7 +921,7 @@ int can_all_from_reach_with_flag(struct object_array *from, if (!(parent->item->object.flags & assign_flag)) { parent->item->object.flags |= assign_flag; - if (repo_parse_commit(the_repository, parent->item) || + if (repo_parse_commit(r, parent->item) || parent->item->date < min_commit_date || commit_graph_generation(parent->item) < min_generation) continue; @@ -955,8 +955,8 @@ int can_all_from_reach_with_flag(struct object_array *from, return result; } -int can_all_from_reach(struct commit_list *from, struct commit_list *to, - int cutoff_by_min_date) +int can_all_from_reach(struct repository *r, struct commit_list *from, + struct commit_list *to, int cutoff_by_min_date) { struct object_array from_objs = OBJECT_ARRAY_INIT; struct commit_list *from_iter = from, *to_iter = to; @@ -967,7 +967,7 @@ int can_all_from_reach(struct commit_list *from, struct commit_list *to, while (from_iter) { add_object_array(&from_iter->item->object, NULL, &from_objs); - if (!repo_parse_commit(the_repository, from_iter->item)) { + if (!repo_parse_commit(r, from_iter->item)) { timestamp_t generation; if (from_iter->item->date < min_commit_date) min_commit_date = from_iter->item->date; @@ -981,7 +981,7 @@ int can_all_from_reach(struct commit_list *from, struct commit_list *to, } while (to_iter) { - if (!repo_parse_commit(the_repository, to_iter->item)) { + if (!repo_parse_commit(r, to_iter->item)) { timestamp_t generation; if (to_iter->item->date < min_commit_date) min_commit_date = to_iter->item->date; @@ -996,7 +996,7 @@ int can_all_from_reach(struct commit_list *from, struct commit_list *to, to_iter = to_iter->next; } - result = can_all_from_reach_with_flag(&from_objs, PARENT2, PARENT1, + result = can_all_from_reach_with_flag(r, &from_objs, PARENT2, PARENT1, min_commit_date, min_generation); while (from) { diff --git a/commit-reach.h b/commit-reach.h index 3f3a563d8a5dd1..5e2b82655b2f96 100644 --- a/commit-reach.h +++ b/commit-reach.h @@ -88,13 +88,14 @@ int commit_contains(struct ref_filter *filter, struct commit *commit, * commits with date below 'min_commit_date' or generation below * 'min_generation'. */ -int can_all_from_reach_with_flag(struct object_array *from, +int can_all_from_reach_with_flag(struct repository *r, + struct object_array *from, unsigned int with_flag, unsigned int assign_flag, timestamp_t min_commit_date, timestamp_t min_generation); -int can_all_from_reach(struct commit_list *from, struct commit_list *to, - int commit_date_cutoff); +int can_all_from_reach(struct repository *r, struct commit_list *from, + struct commit_list *to, int commit_date_cutoff); /* diff --git a/fetch-pack.c b/fetch-pack.c index 120e01f3cf2674..26dbbdc4f68458 100644 --- a/fetch-pack.c +++ b/fetch-pack.c @@ -2330,8 +2330,8 @@ void negotiate_using_fetch(const struct oid_array *negotiation_restrict_tips, die(_("unexpected 'ready' from remote")); else do_check_stateless_delimiter(stateless_rpc, &reader); - if (can_all_from_reach_with_flag(&nt_object_array, COMMON, - REACH_SCRATCH, 0, + if (can_all_from_reach_with_flag(the_repository, &nt_object_array, + COMMON, REACH_SCRATCH, 0, min_generation)) last_iteration = 1; trace2_region_leave_printf("negotiation", "round", diff --git a/t/helper/test-reach.c b/t/helper/test-reach.c index 5d86a96c17e4e5..8ea77a782c1201 100644 --- a/t/helper/test-reach.c +++ b/t/helper/test-reach.c @@ -127,7 +127,7 @@ int cmd__reach(int ac, const char **av) print_sorted_commit_ids(list); commit_list_free(list); } else if (!strcmp(av[1], "can_all_from_reach")) { - printf("%s(X,Y):%d\n", av[1], can_all_from_reach(X, Y, 1)); + printf("%s(X,Y):%d\n", av[1], can_all_from_reach(r, X, Y, 1)); } else if (!strcmp(av[1], "can_all_from_reach_with_flag")) { struct commit_list *iter = Y; @@ -136,7 +136,8 @@ int cmd__reach(int ac, const char **av) iter = iter->next; } - printf("%s(X,_,_,0,0):%d\n", av[1], can_all_from_reach_with_flag(&X_obj, 2, 4, 0, 0)); + printf("%s(X,_,_,0,0):%d\n", av[1], + can_all_from_reach_with_flag(r, &X_obj, 2, 4, 0, 0)); } else if (!strcmp(av[1], "commit_contains")) { struct ref_filter filter = REF_FILTER_INIT; struct contains_cache cache; diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh index a564758f528c93..afb484b9636395 100755 --- a/t/t6437-submodule-merge.sh +++ b/t/t6437-submodule-merge.sh @@ -517,4 +517,43 @@ test_expect_success 'merging should fail with no merge base' ' ) ' +test_expect_success 'setup for commit-graphs in superproject and submodule' ' + git init commit-graph && + (cd commit-graph && + git init sub && + for i in 1 2 3 4 5 6 7 8 9 10 + do + test_commit -C sub "sub-$i" || return 1 + done && + + git -C sub checkout --detach sub-1 && + git add sub && + git commit -m base && + + # Write the commit-graph of the superproject while it only + # contains a single commit. Any commit-graph position of the + # submodule is thus out of bounds for the superproject. + git commit-graph write --reachable && + + git checkout -b side && + git -C sub checkout --detach sub-5 && + git add sub && + git commit -m side && + + git checkout main && + git -C sub checkout --detach sub-10 && + git add sub && + git commit -m main && + + git -C sub commit-graph write --reachable) +' + +test_expect_success 'merge does not mix up superproject and submodule commit-graphs' ' + (cd commit-graph && + git merge side && + git rev-parse HEAD:sub >actual && + git -C sub rev-parse sub-10^{commit} >expect && + test_cmp expect actual) +' + test_done diff --git a/upload-pack.c b/upload-pack.c index a52856d869891d..550fea67639cdf 100644 --- a/upload-pack.c +++ b/upload-pack.c @@ -565,9 +565,9 @@ static int ok_to_give_up(struct upload_pack_data *data) if (!data->have_obj.nr) return 0; - return can_all_from_reach_with_flag(&data->want_obj, THEY_HAVE, - COMMON_KNOWN, data->oldest_have, - min_generation); + return can_all_from_reach_with_flag(the_repository, &data->want_obj, + THEY_HAVE, COMMON_KNOWN, + data->oldest_have, min_generation); } static int get_common_commits(struct upload_pack_data *data, From 191740edaa4a17813b8e1595e3ef15068f88c8ba Mon Sep 17 00:00:00 2001 From: Royce Remer Date: Wed, 16 Sep 2026 13:32:21 -0700 Subject: [PATCH 047/155] upload-pack: swap wanted-ref/shallow-info responses When a server enables uploadpack.allowRefInWant, upload_pack_v2() sends wanted-ref info before shallow-info. The fetch-pack client expects shallow-info first; receiving them out of order causes it to exit: fatal: expected 'packfile', received 'shallow-info' This error condition only applies to protocol v2 clients performs a shallow fetch (--depth) against servers with allowRefInWant configured. Swap the send order so that upload_pack_v2() sends shallow-info before wanted-ref info. This is a server-side-only change and is compatible with all existing client versions. Signed-off-by: Royce Remer Signed-off-by: Junio C Hamano --- t/t5703-upload-pack-ref-in-want.sh | 18 ++++++++++++++++++ upload-pack.c | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/t/t5703-upload-pack-ref-in-want.sh b/t/t5703-upload-pack-ref-in-want.sh index 249137b46734e0..9e2a090c9e5f3d 100755 --- a/t/t5703-upload-pack-ref-in-want.sh +++ b/t/t5703-upload-pack-ref-in-want.sh @@ -295,6 +295,24 @@ test_expect_success 'fetching with wildcard that matches multiple refs' ' grep "want-ref refs/heads/o/bar" log ' +test_expect_success 'shallow clone with ref-in-want' ' + rm -rf local && + GIT_TEST_PROTOCOL_VERSION=2 git clone --depth=1 "file://$REPO" local && + git -C "$REPO" rev-parse main >expected && + git -C local rev-parse refs/remotes/origin/main >actual && + test_cmp expected actual && + git -C local log --oneline refs/remotes/origin/main >log && + test_line_count = 1 log +' + +test_expect_success 'incremental shallow fetch with ref-in-want' ' + rm -rf local && + GIT_TEST_PROTOCOL_VERSION=2 git clone --depth=1 "file://$REPO" local && + GIT_TEST_PROTOCOL_VERSION=2 git -C local fetch --depth=2 origin main && + git -C local log --oneline refs/remotes/origin/main >log && + test_line_count = 2 log +' + REPO="$(pwd)/repo-ns" test_expect_success 'setup namespaced repo' ' diff --git a/upload-pack.c b/upload-pack.c index 9f6d6fe48c8c58..cc958c4c2da5a4 100644 --- a/upload-pack.c +++ b/upload-pack.c @@ -1811,8 +1811,8 @@ int upload_pack_v2(struct repository *r, struct packet_reader *request) state = UPLOAD_DONE; break; case UPLOAD_SEND_PACK: - send_wanted_ref_info(&data); send_shallow_info(&data); + send_wanted_ref_info(&data); if (data.uri_protocols.nr) { create_pack_file(&data, &data.uri_protocols); From 528bb4f0195ded898a648cad8f6d9fd7595105f4 Mon Sep 17 00:00:00 2001 From: Thomas Bachem Date: Thu, 17 Sep 2026 18:42:12 +0000 Subject: [PATCH 048/155] config: add git_config_append_parameter() "git -c" passes its settings to the commands it spawns through GIT_CONFIG_PARAMETERS, a list of quoted 'key'='value' pairs. The only place that formats such an entry is git_config_push_split_parameter(), which writes straight into our own environment. Split the formatting out into git_config_append_parameter(), which appends one entry to a strbuf, so that a caller can build the value for a child's environment. The sequencer will use it in a later commit. Assisted-by: Claude Fable 5.1 Signed-off-by: Thomas Bachem Signed-off-by: Junio C Hamano --- config.c | 20 +++++++++++++------- config.h | 12 ++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/config.c b/config.c index d9019e7e6c34b0..e0bb29b53d3ca3 100644 --- a/config.c +++ b/config.c @@ -450,18 +450,24 @@ static int git_config_include(const char *var, const char *value, return ret; } +void git_config_append_parameter(struct strbuf *env, const char *key, + const char *value) +{ + if (env->len) + strbuf_addch(env, ' '); + sq_quote_buf(env, key); + strbuf_addch(env, '='); + if (value) + sq_quote_buf(env, value); +} + static void git_config_push_split_parameter(const char *key, const char *value) { struct strbuf env = STRBUF_INIT; const char *old = getenv(CONFIG_DATA_ENVIRONMENT); - if (old && *old) { + if (old && *old) strbuf_addstr(&env, old); - strbuf_addch(&env, ' '); - } - sq_quote_buf(&env, key); - strbuf_addch(&env, '='); - if (value) - sq_quote_buf(&env, value); + git_config_append_parameter(&env, key, value); setenv(CONFIG_DATA_ENVIRONMENT, env.buf, 1); strbuf_release(&env); } diff --git a/config.h b/config.h index b66dd08007c97a..b048f635711429 100644 --- a/config.h +++ b/config.h @@ -22,6 +22,7 @@ */ struct object_id; +struct strbuf; /* git_config_parse_key() returns these negated: */ #define CONFIG_INVALID_KEY 1 @@ -186,6 +187,17 @@ int git_config_from_blob_oid(config_fn_t fn, const char *name, enum config_scope scope); void git_config_push_parameter(const char *text); void git_config_push_env(const char *spec); + +/* + * Append a config option to the buffer that can be exported via the + * GIT_CONFIG_PARAMETERS environment variable, which allows us to + * propagate configuration across Git processes. The format of the + * variable is a space-separated list of quoted "''=''" + * pairs. With a NULL `value`, only 'key'= is appended, which git reads + * back as a boolean true, like "-c key" on the command line. + */ +void git_config_append_parameter(struct strbuf *env, const char *key, + const char *value); int git_config_from_parameters(config_fn_t fn, void *data); /* From 2d1fa0323f434c1211cefbb98615ff57650bf0e2 Mon Sep 17 00:00:00 2001 From: Thomas Bachem Date: Thu, 17 Sep 2026 18:42:13 +0000 Subject: [PATCH 049/155] rebase, cherry-pick, revert: run auto maintenance when done Commands that use the sequencer with the "merge" backend, like git-cherry-pick(1) or git-rebase(1) with "--merge", create their commits in-process. Consequently, these commands typically don't execute auto maintenance at all. Only the commands they spawn on the way run it, like git-commit(1) for a resolved conflict or an edited message. In contrast to that, the "apply" backend of git-rebase(1) _does_ run auto maintenance after it has processed the sequence of commits. And this is a sensible thing to do: after all, we may just have written lots of objects, so chances are high that we have something to clean up now. Adapt users of the "merge" backend to do the same. The sequencer has no single exit where a call to run_auto_maintenance() could go. A sequence ends in pick_commits(), but a single pick never gets there: it returns from sequencer_pick_revisions() via single_pick(), its "--continue" from sequencer_continue() via continue_single_pick(), and its "--skip" from sequencer_skip(). So call it from the sequencer's two callers instead: run_specific_rebase() for the merge backend, once its state directory is gone, and run_sequencer() in builtin/revert.c after a successful pick, "--continue" or "--skip". Assisted-by: Claude Fable 5.1 Signed-off-by: Thomas Bachem Signed-off-by: Junio C Hamano --- builtin/rebase.c | 13 ++++++++++--- builtin/revert.c | 19 ++++++++++++------- t/t3418-rebase-continue.sh | 14 ++++++++++++++ t/t3510-cherry-pick-sequence.sh | 25 +++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 10 deletions(-) diff --git a/builtin/rebase.c b/builtin/rebase.c index 10a306310cd439..535db601818128 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -762,9 +762,16 @@ static int run_specific_rebase(struct rebase_options *opts) if (opts->dont_finish_rebase) ; /* do nothing */ - else if (opts->type == REBASE_MERGE) - ; /* merge backend cleans up after itself */ - else if (status == 0) { + else if (opts->type == REBASE_MERGE) { + int quiet = !(opts->flags & (REBASE_NO_QUIET|REBASE_VERBOSE)); + + /* + * The sequencer cleans up after itself. Its state directory + * is gone once it is done, and stays while it is stopped. + */ + if (status == 0 && !is_directory(opts->state_dir)) + run_auto_maintenance(the_repository, quiet); + } else if (status == 0) { if (!file_exists(state_dir_path("stopped-sha", opts))) finish_rebase(opts); } else if (status == 2) { diff --git a/builtin/revert.c b/builtin/revert.c index bedc40f368eccc..52100a20cba596 100644 --- a/builtin/revert.c +++ b/builtin/revert.c @@ -8,6 +8,7 @@ #include "gettext.h" #include "revision.h" #include "rerere.h" +#include "run-command.h" #include "sequencer.h" #include "branch.h" @@ -116,7 +117,7 @@ static int run_sequencer(int argc, const char **argv, const char *prefix, const char *strategy = &sentinel_value; const char *gpg_sign = &sentinel_value; enum empty_action empty_opt = EMPTY_COMMIT_UNSPECIFIED; - int cmd = 0; + int cmd = 0, ret; struct option base_options[] = { OPT_CMDMODE(0, "quit", &cmd, N_("end revert or cherry-pick sequence"), 'q'), OPT_CMDMODE(0, "continue", &cmd, N_("resume revert or cherry-pick sequence"), 'c'), @@ -264,18 +265,22 @@ static int run_sequencer(int argc, const char **argv, const char *prefix, free(options); if (cmd == 'q') { - int ret = sequencer_remove_state(opts); + ret = sequencer_remove_state(opts); if (!ret) remove_branch_state(the_repository, 0); return ret; } - if (cmd == 'c') - return sequencer_continue(the_repository, opts); if (cmd == 'a') return sequencer_rollback(the_repository, opts); - if (cmd == 's') - return sequencer_skip(the_repository, opts); - return sequencer_pick_revisions(the_repository, opts); + if (cmd == 'c') + ret = sequencer_continue(the_repository, opts); + else if (cmd == 's') + ret = sequencer_skip(the_repository, opts); + else + ret = sequencer_pick_revisions(the_repository, opts); + if (!ret) + run_auto_maintenance(the_repository, opts->quiet); + return ret; } int cmd_revert(int argc, diff --git a/t/t3418-rebase-continue.sh b/t/t3418-rebase-continue.sh index cb5c3a1cb5bc6f..8056b3995530e9 100755 --- a/t/t3418-rebase-continue.sh +++ b/t/t3418-rebase-continue.sh @@ -395,4 +395,18 @@ test_orig_head () { test_orig_head --apply test_orig_head --merge +test_expect_success 'rebase runs auto maintenance once it is done' ' + # topic and main both add F2, so the pick conflicts and the rebase + # stops before the exec runs, and once more when the exec fails + git checkout -b auto-maintenance topic && + test_must_fail env GIT_TRACE2_EVENT="$(pwd)/stop.txt" \ + git rebase -x false main && + test_subcommand_flex ! git maintenance run --auto F2 && + git add F2 && + test_must_fail git rebase --continue && + GIT_TRACE2_EVENT="$(pwd)/end.txt" git rebase --continue && + test_subcommand_flex git maintenance run --auto maintenance && + test_line_count = 1 maintenance +' + +test_expect_success 'cherry-pick runs auto maintenance once a stopped sequence is done' ' + # both picked and anotherpick conflict on foo, so "--continue" stops + # once more before "--skip" ends the sequence + pristine_detach initial && + test_must_fail env GIT_TRACE2_EVENT="$(pwd)/stop.txt" \ + git cherry-pick base..anotherpick && + test_subcommand_flex ! git maintenance run --auto foo && + git add foo && + test_must_fail git cherry-pick --continue && + GIT_TRACE2_EVENT="$(pwd)/end.txt" git cherry-pick --skip && + test_subcommand_flex git maintenance run --auto Date: Thu, 17 Sep 2026 18:42:14 +0000 Subject: [PATCH 050/155] sequencer: disable auto maintenance in spawned commands When the sequencer spawns "git commit" or "git merge", those commands run "git maintenance run --auto" in the background, which can interfere with the sequencer (e.g. 'rerere gc' holding MERGE_RR.lock or repacks deleting active packs). Pass maintenance.auto=false via GIT_CONFIG_PARAMETERS to the spawned commit, merge and exec commands. Appending it after the user's own settings ensures it wins, and the environment reaches whatever they spawn in turn. Auto maintenance now runs exactly once when the sequence completes. Commands run manually by the user while stopped are unaffected and continue to run auto maintenance normally. Assisted-by: Claude Fable 5.1 Signed-off-by: Thomas Bachem Signed-off-by: Junio C Hamano --- sequencer.c | 38 ++++++++++++++++++++++++++++++--- t/t3418-rebase-continue.sh | 14 ++++++++---- t/t3510-cherry-pick-sequence.sh | 14 +++++++++--- 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/sequencer.c b/sequencer.c index 65afd100d98e61..e99ef09f02697f 100644 --- a/sequencer.c +++ b/sequencer.c @@ -234,6 +234,11 @@ struct replay_ctx { * Whether message contains a commit message. */ unsigned have_message :1; + /* + * GIT_CONFIG_PARAMETERS for the commands we spawn, with auto + * maintenance turned off. Built on first use. + */ + char *config_parameters; }; struct replay_ctx* replay_ctx_new(void) @@ -407,6 +412,7 @@ static void replay_ctx_release(struct replay_ctx *ctx) { strbuf_release(&ctx->current_fixups); strbuf_release(&ctx->message); + free(ctx->config_parameters); } void replay_opts_release(struct replay_opts *opts) @@ -1107,6 +1113,27 @@ static int run_command_silent_on_success(struct child_process *cmd) return rc; } +/* + * Don't let the commands we spawn run auto maintenance. It would race + * us for MERGE_RR.lock or delete packs we still have open. Our caller + * runs it once the sequence is done. + */ +static void disable_auto_maintenance(struct replay_opts *opts, + struct child_process *cmd) +{ + if (!opts->ctx->config_parameters) { + const char *old = getenv(CONFIG_DATA_ENVIRONMENT); + struct strbuf buf = STRBUF_INIT; + + if (old && *old) + strbuf_addstr(&buf, old); + git_config_append_parameter(&buf, "maintenance.auto", "false"); + opts->ctx->config_parameters = strbuf_detach(&buf, NULL); + } + strvec_pushf(&cmd->env, "%s=%s", CONFIG_DATA_ENVIRONMENT, + opts->ctx->config_parameters); +} + /* * If we are cherry-pick, and if the merge did not result in * hand-editing, we will hit this commit and inherit the original @@ -1148,6 +1175,7 @@ static int run_git_commit(const char *defmsg, author_date_from_env(&cmd.env)); if (opts->ignore_date) strvec_push(&cmd.env, "GIT_AUTHOR_DATE="); + disable_auto_maintenance(opts, &cmd); strvec_push(&cmd.args, "commit"); @@ -3924,16 +3952,18 @@ static int error_failed_squash(struct repository *r, return error_with_patch(r, commit, subject, subject_len, opts, 1, 1); } -static int do_exec(struct repository *r, const char *command_line, int quiet) +static int do_exec(struct repository *r, const char *command_line, + struct replay_opts *opts) { struct child_process cmd = CHILD_PROCESS_INIT; int dirty, status; - if (!quiet) + if (!opts->quiet) fprintf(stderr, _("Executing: %s\n"), command_line); cmd.use_shell = 1; strvec_push(&cmd.args, command_line); strvec_push(&cmd.env, "GIT_CHERRY_PICK_HELP"); + disable_auto_maintenance(opts, &cmd); status = run_command(&cmd); /* force re-reading of the cache */ @@ -4342,6 +4372,7 @@ static int do_merge(struct repository *r, author_date_from_env(&cmd.env)); if (opts->ignore_date) strvec_push(&cmd.env, "GIT_AUTHOR_DATE="); + disable_auto_maintenance(opts, &cmd); cmd.git_cmd = 1; strvec_push(&cmd.args, "merge"); @@ -5158,7 +5189,7 @@ static int pick_commits(struct repository *r, if (!opts->verbose) term_clear_line(); *end_of_arg = '\0'; - res = do_exec(r, arg, opts->quiet); + res = do_exec(r, arg, opts); *end_of_arg = saved; if (res) { @@ -5329,6 +5360,7 @@ static int continue_single_pick(struct repository *r, struct replay_opts *opts) return error(_("no cherry-pick or revert in progress")); cmd.git_cmd = 1; + disable_auto_maintenance(opts, &cmd); strvec_push(&cmd.args, "commit"); /* diff --git a/t/t3418-rebase-continue.sh b/t/t3418-rebase-continue.sh index 8056b3995530e9..8c43ef64fce3d4 100755 --- a/t/t3418-rebase-continue.sh +++ b/t/t3418-rebase-continue.sh @@ -397,16 +397,22 @@ test_orig_head --merge test_expect_success 'rebase runs auto maintenance once it is done' ' # topic and main both add F2, so the pick conflicts and the rebase - # stops before the exec runs, and once more when the exec fails + # stops before the exec runs. "--continue" commits the resolution + # first, then runs the exec, which fails and stops it again. git checkout -b auto-maintenance topic && test_must_fail env GIT_TRACE2_EVENT="$(pwd)/stop.txt" \ - git rebase -x false main && + git rebase -x "git commit --allow-empty -m exec && false" main && test_subcommand_flex ! git maintenance run --auto F2 && git add F2 && - test_must_fail git rebase --continue && + test_must_fail env GIT_TRACE2_EVENT="$(pwd)/mid.txt" \ + git rebase --continue && + test_subcommand_flex git commit maintenance && + test_line_count = 1 maintenance ' test_done diff --git a/t/t3510-cherry-pick-sequence.sh b/t/t3510-cherry-pick-sequence.sh index 1dbc42e7688d23..9400ddfc45b475 100755 --- a/t/t3510-cherry-pick-sequence.sh +++ b/t/t3510-cherry-pick-sequence.sh @@ -723,8 +723,11 @@ test_expect_success 'commit descriptions in insn sheet are optional' ' test_expect_success 'cherry-pick runs auto maintenance once it is done' ' pristine_detach base && - GIT_TRACE2_EVENT="$(pwd)/single.txt" git cherry-pick picked && + GIT_TRACE2_EVENT="$(pwd)/single.txt" git cherry-pick --edit picked && + test_subcommand_flex git commit maintenance && + test_line_count = 1 maintenance && GIT_TRACE2_EVENT="$(pwd)/sequence.txt" \ git cherry-pick anotherpick yetanotherpick && test_subcommand_flex git maintenance run --auto foo && git add foo && - test_must_fail git cherry-pick --continue && + test_must_fail env GIT_TRACE2_EVENT="$(pwd)/mid.txt" \ + git cherry-pick --continue && + test_subcommand_flex git commit maintenance && + test_line_count = 1 maintenance ' test_done From 506a21b3123cfd7d78ec520e431c68393e254812 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 17 Sep 2026 17:52:30 +0000 Subject: [PATCH 051/155] wrapper: guard writev_in_full() against signed overflow As Git for Windows' Coverity run after merging v2.56.0-rc0 reported, `writev_in_full()` keeps its cumulative successful output in an `ssize_t`. Although `xwritev()` limits each individual write to a syscall-sized amount, repeated successful writes can still exceed `SSIZE_MAX`. The unchecked accumulation was introduced by d70eb7f3600d (wrapper: introduce writev(3p) wrappers, 2026-08-07). Treat an aggregate that would overflow the signed total as an I/O failure. Assisted-by: GPT-5.6 Luna Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- wrapper.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wrapper.c b/wrapper.c index 561f9ee9c99fc1..05a9cd369c5971 100644 --- a/wrapper.c +++ b/wrapper.c @@ -376,6 +376,10 @@ ssize_t writev_in_full(int fd, struct iovec *iov, int iovcnt) return -1; } + if (signed_add_overflows(total_written, bytes_written)) { + errno = EOVERFLOW; + return -1; + } total_written += bytes_written; /* From 723dd6ca3e38509c62e396f43b9303de100c49e0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 17 Sep 2026 17:52:31 +0000 Subject: [PATCH 052/155] gpg-interface: make signature-prefix matching length-aware After merging v2.56.0-rc0 into Git for Windows, its Coverity run reported the following issue: The `parse_signed_buffer()` function accepts object buffers with an explicit size, while `get_format_by_sig()` uses `starts_with()`, i.e. it expects a NUL-terminated buffer. A tag object with a non-NUL-terminated payload ending in a partial signature prefix, such as a final '-' byte, could therefore cause an invalid read past the object buffer. The observable consequences are limited to reading past the allocation. In practice it can crash Git if the read enters an unmapped page. It can also misplace the payload/signature split, corrupting the compat-hash object being written. The older unbounded matcher predates this path, but c8762c30df5b (object-file-convert: convert tag objects when writing, 2023-10-01) exposed the defect by passing exact-sized converted tag buffers to `parse_signed_buffer()`. That commit first shipped in v2.45.0, so the defect has been latent in every release since. This pattern was noticed on the mailing list in February 2024. Reviewing a patch for a very similar issue in commit.c's find_header_mem(), Jeff King observed in https://lore.kernel.org/git/20240208214137.GB1090198@coredump.intra.peff.net/: But more interestingly: even though we pass a buf/len pair to parse_signed_buffer(), it then calls get_format_by_sig() which takes only a NUL-terminated string. [...] That raises the question of whether parse_signed_buffer() has a similar walk-too-far problem. ;) The answer is no, because we feed it from a strbuf. But it's not a great pattern overall. That reasoning surveyed the callers that existed at the time and missed c8762c30df5b (object-file-convert: convert tag objects when writing, 2023-10-01), which was four months old at that time, and does not feed from a strbuf; `convert_tag_object()` hands `parse_signed_buffer()` an exact-sized `xmalloc()` buffer, and the concern flagged and dismissed in that thread is exactly the defect Coverity now reports. Jeff went on to add `starts_with_mem()` a month later, in https://lore.kernel.org/git/20240307092638.GK2080210@coredump.intra.peff.net/, precisely for "cases where the buffer is not NUL-terminated (and we instead have an explicit size or end pointer)", so the tool for this fix has been in the tree since v2.45.0. Even though the issue had been latent, it most likely surfaced via Coverity because of 215d305f450f (odb: compute compat object ID in `odb_write_object_ext()`, 2026-07-17), which moved `convert_object_file()` out of the `source->write_object` function pointer into a direct call in `odb_write_object_ext()`. Preserve the existing NUL-terminated behavior for callers that provide strings while making signature-prefix matching honor the known buffer lengths, via the `starts_with_mem()` helper. This keeps reads within the object data without implying exploitability beyond the observed invalid read. Assisted-by: GPT-5.6 Luna Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- gpg-interface.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gpg-interface.c b/gpg-interface.c index 95abf1ef4e1a0c..60c315fba9732a 100644 --- a/gpg-interface.c +++ b/gpg-interface.c @@ -133,20 +133,20 @@ static struct gpg_format *get_format_by_name(const char *str) return NULL; } -static struct gpg_format *get_format_by_sig(const char *sig) +static struct gpg_format *get_format_by_sig(const char *sig, size_t len) { int j; for (size_t i = 0; i < ARRAY_SIZE(gpg_format); i++) for (j = 0; gpg_format[i].sigs[j]; j++) - if (starts_with(sig, gpg_format[i].sigs[j])) + if (starts_with_mem(sig, len, gpg_format[i].sigs[j])) return gpg_format + i; return NULL; } const char *get_signature_format(const char *buf) { - struct gpg_format *format = get_format_by_sig(buf); + struct gpg_format *format = get_format_by_sig(buf, strlen(buf)); return format ? format->name : "unknown"; } @@ -669,7 +669,7 @@ int check_signature(struct signature_check *sigc, sigc->result = 'N'; sigc->trust_level = TRUST_UNDEFINED; - fmt = get_format_by_sig(signature); + fmt = get_format_by_sig(signature, slen); if (!fmt) die(_("bad/incompatible signature '%s'"), signature); @@ -706,7 +706,7 @@ size_t parse_signed_buffer(const char *buf, size_t size) while (len < size) { const char *eol; - if (get_format_by_sig(buf + len)) + if (get_format_by_sig(buf + len, size - len)) match = len; eol = memchr(buf + len, '\n', size - len); From c3a6be23337192248d7bd694992030433fd17441 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 17 Sep 2026 17:52:32 +0000 Subject: [PATCH 053/155] midx: validate incremental MIDX pack IDs Incremental MIDX support made object-offset pack IDs local to each layer and then converted them to chain-global IDs by adding `num_packs_in_base`. The conversion was introduced by 19419821bac5 (midx: teach `nth_midxed_pack_int_id()` about incremental MIDXs, 2024-08-06). Chain-aware pack preparation followed in 1820bd878c62 (midx: teach `prepare_midx_pack()` about incremental MIDXs, 2024-08-06), but the final `midx_fill_entry()` lookup remained tied to the original layer. Only with 8f909ff4e9e8 (packfile: recover when a multi-pack-index names a removed pack, 2026-08-29) did Coverity point out this issue: a local ID such as `UINT32_MAX` could wrap when the base-pack count was added, producing a plausible but incorrect global ID. After `prepare_midx_pack()` resolved the chain, `midx_fill_entry()` could then underflow or address the wrong layer while indexing the current layer's pack array, causing an invalid memory access and crashing Git. Validate each local pack ID against its layer's pack count before adding the base count, and obtain the final pack through `nth_midxed_pack()`, which resolves the correct MIDX layer. This prevents an invalid local ID from wrapping during conversion and ensures that the lookup uses the layer identified by the resolved chain-global ID. Assisted-by: GPT-5.6 Luna Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- midx.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/midx.c b/midx.c index 6d1c548e3dae8e..6968fc1c005085 100644 --- a/midx.c +++ b/midx.c @@ -583,10 +583,16 @@ off_t nth_midxed_offset(struct multi_pack_index *m, uint32_t pos) uint32_t nth_midxed_pack_int_id(struct multi_pack_index *m, uint32_t pos) { + uint32_t pack_int_id; + pos = midx_for_object(&m, pos); + pack_int_id = get_be32(m->chunk_object_offsets + + (off_t)pos * MIDX_CHUNK_OFFSET_WIDTH); + if (pack_int_id >= m->num_packs) + die(_("bad pack-int-id: %"PRIu32" (%"PRIu32" total packs)"), + pack_int_id, m->num_packs); - return m->num_packs_in_base + get_be32(m->chunk_object_offsets + - (off_t)pos * MIDX_CHUNK_OFFSET_WIDTH); + return m->num_packs_in_base + pack_int_id; } enum midx_fill_result midx_fill_entry(struct multi_pack_index *m, @@ -606,7 +612,7 @@ enum midx_fill_result midx_fill_entry(struct multi_pack_index *m, if (prepare_midx_pack(m, pack_int_id)) return MIDX_FILL_OWNER_UNAVAILABLE; - p = m->packs[pack_int_id - m->num_packs_in_base]; + p = nth_midxed_pack(m, pack_int_id); /* * We are about to tell the caller where they can locate the From 742d8eb897b0521bb42cb7ebfff6cdacd5a9f6dc Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 17 Sep 2026 17:52:33 +0000 Subject: [PATCH 054/155] rerere: do not record failed conflict resolution data `rerere` can mark a conflict variant as resolved even when writing its preimage or postimage fails. A later invocation may then replay incomplete data from the cache, turning a local filesystem failure into an incorrect working-tree change. 629716d256a7 (rerere: do use multiple variants, 2015-07-30) introduced the code paths without checks for those I/O results. Treat such failures as failures, report them, and leave the rerere status unchanged unless the corresponding data was recorded successfully. The defect has been latent since 2015. Git for Windows' Coverity run only reported it after merging v2.56.0-rc0, for reasons that could not be figured out in a reasonable amount of time. Assisted-by: GPT-5.6 Luna Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- rerere.c | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/rerere.c b/rerere.c index 1c3745d9e3279a..45bbe6ab2c5439 100644 --- a/rerere.c +++ b/rerere.c @@ -476,8 +476,11 @@ static int handle_file(struct index_state *istate, unlink_or_warn(output); return error(_("could not parse conflict hunks in '%s'"), path); } - if (io.io.wrerror) + if (io.io.wrerror) { + if (output) + unlink_or_warn(output); return -1; + } return has_conflicts; } @@ -729,8 +732,25 @@ static void do_rerere_one_path(struct index_state *istate, /* Has the user resolved it already? */ if (variant >= 0) { - if (!handle_file(istate, path, NULL, NULL)) { - copy_file(the_repository, rerere_path(&buf, id, "postimage"), path, 0666); + int ret = handle_file(istate, path, NULL, NULL); + + if (ret < 0) + goto out; + if (!ret) { + const int had_postimage = + id->collection->status[variant] & RR_HAS_POSTIMAGE; + const char *postimage = + rerere_path(&buf, id, "postimage"); + + if (copy_file(the_repository, + postimage, + path, 0666)) { + if (!had_postimage) + unlink_or_warn(postimage); + error_errno(_("could not copy resolution for '%s'"), + path); + goto out; + } id->collection->status[variant] |= RR_HAS_POSTIMAGE; fprintf_ln(stderr, _("Recorded resolution for '%s'."), path); free_rerere_id(rr_item); @@ -778,7 +798,9 @@ static void do_rerere_one_path(struct index_state *istate, assign_variant(id); variant = id->variant; - handle_file(istate, path, NULL, rerere_path(&buf, id, "preimage")); + if (handle_file(istate, path, NULL, + rerere_path(&buf, id, "preimage")) < 0) + goto out; if (id->collection->status[variant] & RR_HAS_POSTIMAGE) { const char *path = rerere_path(&buf, id, "postimage"); if (unlink(path)) From 11ab0ca4e3dae17c124b93ef08277b9a89cb5b38 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 17 Sep 2026 17:52:34 +0000 Subject: [PATCH 055/155] t/unit-tests: check reftable iterator initialization Coverity pointed out that the `test_reftable_table__seek_invalid_log_offset()` test, which was introduced by a1c085df8dcb (reftable/table: fix NULL pointer access when seeking to bogus offsets, 2026-07-03), ignores the result of `reftable_table_init_log_iterator()` and proceeds to `reftable_iterator_seek_log()`, although initialization can return `REFTABLE_OUT_OF_MEMORY_ERROR` without installing an ops table. Under allocation failure, the test then dereferences a NULL function table. Assert successful iterator initialization before seeking. Assisted-by: GPT-5.6 Luna Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/unit-tests/u-reftable-table.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/unit-tests/u-reftable-table.c b/t/unit-tests/u-reftable-table.c index bd04b477a3eff9..1e4378b2eb1b1d 100644 --- a/t/unit-tests/u-reftable-table.c +++ b/t/unit-tests/u-reftable-table.c @@ -257,7 +257,7 @@ void test_reftable_table__seek_invalid_log_offset(void) * know that the table is corrupt, so the seek must report a format * error instead of pretending that the section is empty. */ - reftable_table_init_log_iterator(table, &it); + cl_assert_equal_i(reftable_table_init_log_iterator(table, &it), 0); cl_assert_equal_i(reftable_iterator_seek_log(&it, ""), REFTABLE_FORMAT_ERROR); From 237edbc58d77729588ed657ffe5d0f6908268e59 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 17 Sep 2026 17:52:35 +0000 Subject: [PATCH 056/155] oss-fuzz: handle reftable iterator initialization failures The reftable fuzzer introduced by adf45165e65b (oss-fuzz: add fuzzer for parsing reftables, 2026-07-03) ignored failures from `reftable_table_init_ref_iterator()` and `reftable_table_init_log_iterator()`. Coverity reported that under allocation failure, either constructor can return `REFTABLE_OUT_OF_MEMORY_ERROR` without installing an ops table, allowing a subsequent seek to dereference NULL. Treat iterator initialization failure as a reason to skip the corresponding seek and iteration while retaining safe destruction for an uninitialized iterator. Assisted-by: GPT-5.6 Luna Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- oss-fuzz/fuzz-reftable.c | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/oss-fuzz/fuzz-reftable.c b/oss-fuzz/fuzz-reftable.c index c46eac2c6bce62..75b8ad0c3d9aef 100644 --- a/oss-fuzz/fuzz-reftable.c +++ b/oss-fuzz/fuzz-reftable.c @@ -33,10 +33,11 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) struct reftable_ref_record ref = { 0 }; struct reftable_iterator it = { 0 }; - reftable_table_init_ref_iterator(table, &it); - if (!reftable_iterator_seek_ref(&it, "")) - while (!reftable_iterator_next_ref(&it, &ref)) - ; + if (!reftable_table_init_ref_iterator(table, &it)) { + if (!reftable_iterator_seek_ref(&it, "")) + while (!reftable_iterator_next_ref(&it, &ref)) + ; + } reftable_ref_record_release(&ref); reftable_iterator_destroy(&it); @@ -46,10 +47,11 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) struct reftable_log_record log = { 0 }; struct reftable_iterator it = { 0 }; - reftable_table_init_log_iterator(table, &it); - if (!reftable_iterator_seek_log(&it, "")) - while (!reftable_iterator_next_log(&it, &log)) - ; + if (!reftable_table_init_log_iterator(table, &it)) { + if (!reftable_iterator_seek_log(&it, "")) + while (!reftable_iterator_next_log(&it, &log)) + ; + } reftable_log_record_release(&log); reftable_iterator_destroy(&it); From c88341b7e1a5ca3c384c2a3aeb62b72fffbd8c00 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 17 Sep 2026 17:52:36 +0000 Subject: [PATCH 057/155] test-read-midx: check midx_fill_entry() result The `--show-objects` mode of `read_midx_file()` uses the output of `midx_fill_entry()` without checking whether the lookup succeeded. A failed lookup or unavailable pack can leave that output unusable, allowing malformed or concurrently changed MIDX data to make this test helper crash instead of reporting a controlled error. Reject the entry unless `midx_fill_entry()` returns `MIDX_FILL_HIT`. The unchecked call was introduced by 86d174b7246b (t/helper/test-read-midx.c: add '--show-objects', 2021-03-30); later incremental-MIDX changes expanded the possible failure modes, but this remains a test-helper robustness issue, not a production Git attack surface or an arbitrary-code-execution vulnerability. It is unclear why Coverity reports this issue in Git for Windows only after merging v2.56.0-rc0; The issue was not reported before. Assisted-by: GPT-5.6 Luna Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/helper/test-read-midx.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/t/helper/test-read-midx.c b/t/helper/test-read-midx.c index 83b07c6236b167..412089563f5a7c 100644 --- a/t/helper/test-read-midx.c +++ b/t/helper/test-read-midx.c @@ -90,7 +90,11 @@ static int read_midx_file(const char *object_dir, const char *checksum, for (i = 0; i < m->num_objects; i++) { nth_midxed_object_oid(&oid, m, i + m->num_objects_in_base); - midx_fill_entry(m, &oid, &e, NULL); + if (midx_fill_entry(m, &oid, &e, NULL) != + MIDX_FILL_HIT) { + ret = error(_("failed to load pack entry")); + goto out; + } printf("%s %"PRIu64"\t%s\n", oid_to_hex(&oid), e.offset, e.p->pack_name); From 9a13b3742b935aca4c686bfdf2f4e76dbab77e13 Mon Sep 17 00:00:00 2001 From: Haokai Ding Date: Fri, 18 Sep 2026 07:19:06 +0000 Subject: [PATCH 058/155] diff --no-index: fix -R with file/directory conflicts When a path is a file on one side and a directory on the other, queue_diff() queues the file separately before recursing into the directory. This early queue entry bypasses the reverse_diff handling used for ordinary files. As a result, comparing directories d and e where d/sub is a file and e/sub/file is another file reports both paths as deleted with -R. Reversing the operands reports both paths as added instead. Swap the filespecs of the early queue entry when reverse_diff is set, so that -R reverses the file change as well as the directory contents. Add regression tests for both directions. Signed-off-by: Haokai Ding Reviewed-by: Jacob Keller Signed-off-by: Junio C Hamano --- diff-no-index.c | 2 ++ t/t4053-diff-no-index.sh | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/diff-no-index.c b/diff-no-index.c index f320424f05fe0f..a9151cc27a0448 100644 --- a/diff-no-index.c +++ b/diff-no-index.c @@ -188,6 +188,8 @@ static int queue_diff(struct diff_options *o, const struct git_hash_algo *algop, mode1 = 0; } /* emit that file */ + if (o->flags.reverse_diff) + SWAP(d1, d2); diff_queue(&diff_queued_diff, d1, d2); /* and then let the entire directory be created or deleted */ diff --git a/t/t4053-diff-no-index.sh b/t/t4053-diff-no-index.sh index 15076dfe0dcb07..06741670e7b94c 100755 --- a/t/t4053-diff-no-index.sh +++ b/t/t4053-diff-no-index.sh @@ -120,6 +120,24 @@ test_expect_success 'turning a file into a directory' ' ) ' +test_expect_success 'reverse diff when turning a file into a directory' ' + ( + cd non/git && + printf "A\td/sub\nD\te/sub/file\n" >expect && + test_expect_code 1 git diff --no-index -R --name-status d e >actual && + test_cmp expect actual + ) +' + +test_expect_success 'reverse diff when turning a directory into a file' ' + ( + cd non/git && + printf "D\td/sub\nA\te/sub/file\n" >expect && + test_expect_code 1 git diff --no-index -R --name-status e d >actual && + test_cmp expect actual + ) +' + test_expect_success 'diff from repo subdir shows real paths (explicit)' ' echo "diff --git a/../../non/git/a b/../../non/git/b" >expect && test_expect_code 1 \ From 79ac6eff39af19b8a1f63a5ff9320dc9d7579fcf Mon Sep 17 00:00:00 2001 From: Yongqiang Tian Date: Tue, 22 Sep 2026 09:47:56 +1000 Subject: [PATCH 059/155] compat/winansi: fix die_lasterr() argument formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit During WinANSI initialization, duplicate_handle() reports the handle when DuplicateHandle() fails. die_lasterr() collects the formatting arguments in a va_list, but passes that va_list to die_errno() as an ordinary variadic argument. die_errno() consequently formats part of the va_list representation instead of the supplied handle, producing an incorrect fatal message. The helper also converts GetLastError() to errno, losing the exact Windows error code. Remove die_lasterr() and report GetLastError() directly at its four call sites, following the existing Windows diagnostic style. This passes the handle to the formatter correctly and preserves the Windows error code. Keep the existing %li representation of the handle. Helped-by: Johannes Sixt Helped-by: René Scharfe Signed-off-by: Yongqiang Tian Signed-off-by: Junio C Hamano --- compat/winansi.c | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/compat/winansi.c b/compat/winansi.c index 3ce190093901b4..088734a1dfa8f7 100644 --- a/compat/winansi.c +++ b/compat/winansi.c @@ -436,15 +436,6 @@ static void winansi_exit(void) CloseHandle(hthread); } -static void die_lasterr(const char *fmt, ...) -{ - va_list params; - va_start(params, fmt); - errno = err_win_to_posix(GetLastError()); - die_errno(fmt, params); - va_end(params); -} - #undef dup2 int winansi_dup2(int oldfd, int newfd) { @@ -462,8 +453,8 @@ static HANDLE duplicate_handle(HANDLE hnd) HANDLE hresult, hproc = GetCurrentProcess(); if (!DuplicateHandle(hproc, hnd, hproc, &hresult, 0, TRUE, DUPLICATE_SAME_ACCESS)) - die_lasterr("DuplicateHandle(%li) failed", - (long) (intptr_t) hnd); + die("DuplicateHandle(%li) failed: %lu", + (long) (intptr_t) hnd, GetLastError()); return hresult; } @@ -609,16 +600,16 @@ void winansi_init(void) hwrite = CreateNamedPipeW(name, PIPE_ACCESS_OUTBOUND, PIPE_TYPE_BYTE | PIPE_WAIT, 1, BUFFER_SIZE, 0, 0, NULL); if (hwrite == INVALID_HANDLE_VALUE) - die_lasterr("CreateNamedPipe failed"); + die("CreateNamedPipe failed: %lu", GetLastError()); hread = CreateFileW(name, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); if (hread == INVALID_HANDLE_VALUE) - die_lasterr("CreateFile for named pipe failed"); + die("CreateFile for named pipe failed: %lu", GetLastError()); /* start console spool thread on the pipe's read end */ hthread = CreateThread(NULL, 0, console_thread, NULL, 0, NULL); if (!hthread) - die_lasterr("CreateThread(console_thread) failed"); + die("CreateThread(console_thread) failed: %lu", GetLastError()); /* schedule cleanup routine */ if (atexit(winansi_exit)) From 38bdda17dd33e42193f1b3b7e91797d5c03145d4 Mon Sep 17 00:00:00 2001 From: Jiri Kuncar Date: Mon, 21 Sep 2026 14:32:46 +0000 Subject: [PATCH 060/155] pull: avoid segfault when commit lookup fails get_can_ff() and already_up_to_date() pass the result of lookup_commit_reference() straight to commit_list_insert() and repo_is_descendant_of() without checking it. When the object behind HEAD or one of the merge heads cannot be parsed, e.g. because a loose object was left truncated by a fetch or gc racing on the same repository, lookup_commit_reference() returns NULL and "git pull" segfaults instead of reporting the corruption. Treat a failed lookup as "cannot fast-forward" and "not up to date", so that the caller falls through to the normal merge path, which already diagnoses the broken object and fails cleanly. An alternative would be to report the breakage at each lookup site, which could give a more precise diagnosis. The minimal guards are preferred because they do no more than is needed to avoid the crash, and will be easy to drop once "git pull" is reworked to resolve object names into commit objects early and pass those around, at which point there will not be multiple lookups of the same object name to guard in the first place. The test corrupts the loose object in place rather than removing it: a missing object that is still recorded in the commit-graph is caught by the consistency check in fetch-pack before "git pull" reaches the fast-forward check, so removing it would not exercise the crash. Signed-off-by: Jiri Kuncar Signed-off-by: Junio C Hamano --- builtin/pull.c | 10 +++++++++- t/t5520-pull.sh | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/builtin/pull.c b/builtin/pull.c index d49b09114ae7d8..4336088178d4ce 100644 --- a/builtin/pull.c +++ b/builtin/pull.c @@ -800,8 +800,12 @@ static int get_can_ff(struct object_id *orig_head, orig_merge_head = &merge_heads->oid[0]; head = lookup_commit_reference(the_repository, orig_head); - commit_list_insert(head, &list); + if (!head) + return 0; merge_head = lookup_commit_reference(the_repository, orig_merge_head); + if (!merge_head) + return 0; + commit_list_insert(head, &list); ret = repo_is_descendant_of(the_repository, merge_head, list); commit_list_free(list); if (ret < 0) @@ -820,12 +824,16 @@ static int already_up_to_date(struct object_id *orig_head, struct commit *ours; ours = lookup_commit_reference(the_repository, orig_head); + if (!ours) + return 0; for (size_t i = 0; i < merge_heads->nr; i++) { struct commit_list *list = NULL; struct commit *theirs; int ok; theirs = lookup_commit_reference(the_repository, &merge_heads->oid[i]); + if (!theirs) + return 0; commit_list_insert(theirs, &list); ok = repo_is_descendant_of(the_repository, ours, list); commit_list_free(list); diff --git a/t/t5520-pull.sh b/t/t5520-pull.sh index 0e0019347e85e9..5d018dbec36d9c 100755 --- a/t/t5520-pull.sh +++ b/t/t5520-pull.sh @@ -888,4 +888,31 @@ test_expect_success 'git pull --rebase against local branch' ' test_cmp expect file2 ' +test_expect_success 'pull does not crash when a merge head does not resolve' ' + test_when_finished "rm -rf up dn" && + git init up && + ( + cd up && + test_commit base && + git switch -c sideA && + test_commit a && + git switch -c sideB base && + test_commit b + ) && + git clone up dn && + ( + cd dn && + git commit-graph write --reachable && + oid=$(git rev-parse refs/remotes/origin/sideA) && + obj=.git/objects/$(test_oid_to_path "$oid") && + + # Corrupt the object instead of removing it: a missing + # object that is still in the commit-graph is caught by + # fetch before pull ever reaches the fast-forward check. + rm -f "$obj" && + echo garbage >"$obj" && + test_must_fail git pull --no-rebase origin sideA sideB + ) +' + test_done From 7d4fbc64ce234d2def89cca8f6ed65f684cddb24 Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Wed, 23 Sep 2026 17:03:14 -0500 Subject: [PATCH 061/155] object-file: lift ODB reprepare out of packfile flush When flushing a packfile via `flush_packfile_transaction()`, `odb_reprepare()` is invoked so the written packfile becomes visible in the current process. In a subsequent commit, flushing the packfile is performed earlier when committing a "files" ODB transaction, but the ODB reprepare needs to remain the last step. Lift ODB reprepare out of `flush_packfile_transaction()` and instead require callers to explicitly invoke `odb_reprepare()` if required. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- object-file.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/object-file.c b/object-file.c index a4cbf8b081df1e..0f123b79fad123 100644 --- a/object-file.c +++ b/object-file.c @@ -857,8 +857,6 @@ static void flush_packfile_transaction(struct odb_transaction_files *transaction memset(state, 0, sizeof(*state)); strbuf_release(&packname); - /* Make objects we just wrote available to ourselves */ - odb_reprepare(repo->objects); } /* @@ -909,8 +907,10 @@ static int odb_transaction_files_write_object_stream(struct odb_transaction *bas * to zlib compression and is sufficient for this check. */ if (state->nr_written && pack_size_limit_cfg && - pack_size_limit_cfg < state->offset + stream->size) + pack_size_limit_cfg < state->offset + stream->size) { flush_packfile_transaction(transaction); + odb_reprepare(transaction->base.source->odb); + } CALLOC_ARRAY(idx, 1); prepare_packfile_transaction(transaction); @@ -1260,6 +1260,7 @@ static int odb_transaction_files_commit(struct odb_transaction *base) { struct odb_transaction_files *transaction = container_of(base, struct odb_transaction_files, base); + int have_packfile = !!transaction->packfile.f; if (transaction->objdir) { struct strbuf temp_path = STRBUF_INIT; @@ -1293,6 +1294,9 @@ static int odb_transaction_files_commit(struct odb_transaction *base) flush_packfile_transaction(transaction); + if (have_packfile) + odb_reprepare(transaction->base.source->odb); + return 0; } From ef7faa6350fc2ee4151584bd912dfd61606be98c Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Wed, 23 Sep 2026 17:03:15 -0500 Subject: [PATCH 062/155] object-file: flush transaction packfile before migrating objects A "files" ODB transaction creates a temporary directory to stage newly written objects in when configured to batch fsync loose objects. Once the temporary directory is created, it is configured as the primary ODB and all object are written to it accordingly. This also includes packfiles containing blobs that exceed `core.bigFileThreshold` written via `odb_transaction_files_write_object_stream()`. If a "large" blob packfile is written to the ODB transaction temporary directory after other loose objects, the ODB transaction fails to commit as a result of the temporary directory being migrated prior to the packfile being flushed. Fix this bug by always flushing the packfile transaction before objects are migrated to the main ODB. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- object-file.c | 4 ++-- t/t1050-large.sh | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/object-file.c b/object-file.c index 0f123b79fad123..210984f825326e 100644 --- a/object-file.c +++ b/object-file.c @@ -1262,6 +1262,8 @@ static int odb_transaction_files_commit(struct odb_transaction *base) container_of(base, struct odb_transaction_files, base); int have_packfile = !!transaction->packfile.f; + flush_packfile_transaction(transaction); + if (transaction->objdir) { struct strbuf temp_path = STRBUF_INIT; struct tempfile *temp; @@ -1292,8 +1294,6 @@ static int odb_transaction_files_commit(struct odb_transaction *base) transaction->objdir = NULL; } - flush_packfile_transaction(transaction); - if (have_packfile) odb_reprepare(transaction->base.source->odb); diff --git a/t/t1050-large.sh b/t/t1050-large.sh index d295c265c75c0e..95233458b459a6 100755 --- a/t/t1050-large.sh +++ b/t/t1050-large.sh @@ -87,6 +87,23 @@ test_expect_success 'add a large file or two' ' test $count = 1 ' +test_expect_success 'add large file with loose object in batch fsync' ' + test_when_finished "rm -rf batch" && + git init batch && + ( + cd batch && + git config core.bigFileThreshold 5 && + echo foo >1-small && + echo foobar >2-large && + + git -c core.fsync=loose-object -c core.fsyncMethod=batch \ + add 1-small 2-large && + + git cat-file -e :1-small && + git cat-file -e :2-large + ) +' + test_expect_success 'checkout a large file' ' large1=$(git rev-parse :large1) && git update-index --add --cacheinfo 100644 $large1 another && From 60cf5005515d6df30bfc3c1dd13475e13b12f445 Mon Sep 17 00:00:00 2001 From: Pushkar Singh Date: Thu, 24 Sep 2026 17:58:44 +0000 Subject: [PATCH 063/155] reflog: fix default expiry periods The default reflog expiry periods were swapped when they were moved to REFLOG_EXPIRE_OPTIONS_INIT() by 85658275702b (builtin/reflog: stop storing default reflog expiry dates globally). This caused reachable entries to expire after 30 days instead of 90 days, and unreachable entries after 90 days instead of 30 days. Reported-by: r.norouzi Signed-off-by: Pushkar Singh Signed-off-by: Junio C Hamano --- reflog.h | 4 +-- t/t1410-reflog.sh | 66 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/reflog.h b/reflog.h index b996712c00f65e..3bdd1ca8a5c25d 100644 --- a/reflog.h +++ b/reflog.h @@ -23,8 +23,8 @@ struct reflog_expire_options { int recno; }; #define REFLOG_EXPIRE_OPTIONS_INIT(now) { \ - .default_expire_total = now - 30 * 24 * 3600, \ - .default_expire_unreachable = now - 90 * 24 * 3600, \ + .default_expire_total = now - 90 * 24 * 3600, \ + .default_expire_unreachable = now - 30 * 24 * 3600, \ } /* diff --git a/t/t1410-reflog.sh b/t/t1410-reflog.sh index ce71f9a30ae1ee..0f27deb3d60c14 100755 --- a/t/t1410-reflog.sh +++ b/t/t1410-reflog.sh @@ -153,6 +153,72 @@ test_expect_success 'reflog expire should not barf on an annotated tag' ' test_grep ! "error: [Oo]bject .* not a commit" err ' +test_expect_success 'reflog expire keeps reachable entries for 90 days' ' + test_when_finished "rm -rf reachable-keep" && + git init reachable-keep && + ( + cd reachable-keep && + timestamp=$(test-tool date timestamp "60.days.ago") && + timestamp=${timestamp#* -> } && + test_commit --no-tag --date "$timestamp +0000" old && + git reflog expire --all && + test_stdout_line_count = 1 git reflog refs/heads/main + ) +' + +test_expect_success 'reflog expire removes reachable entries after 90 days' ' + test_when_finished "rm -rf reachable-expire" && + git init reachable-expire && + ( + cd reachable-expire && + timestamp=$(test-tool date timestamp "100.days.ago") && + timestamp=${timestamp#* -> } && + test_commit --no-tag --date "$timestamp +0000" old && + git reflog expire --all && + test_stdout_line_count = 0 git reflog refs/heads/main + ) +' + +test_expect_success 'reflog expire keeps unreachable entries for 30 days' ' + test_when_finished "rm -rf unreachable-keep" && + git init unreachable-keep && + ( + cd unreachable-keep && + test_commit --no-tag base && + base=$(git rev-parse HEAD) && + timestamp=$(test-tool date timestamp "20.days.ago") && + timestamp=${timestamp#* -> } && + test_commit --no-tag --date "$timestamp +0000" old && + old=$(git rev-parse HEAD) && + git update-ref refs/heads/main "$base" && + git rev-list --all --objects >reachable && + test_grep ! "$old" reachable && + git reflog expire --all && + git reflog --format='%H' refs/heads/main >actual && + test_grep "$old" actual + ) +' + +test_expect_success 'reflog expire removes unreachable entries after 30 days' ' + test_when_finished "rm -rf unreachable-expire" && + git init unreachable-expire && + ( + cd unreachable-expire && + test_commit --no-tag base && + base=$(git rev-parse HEAD) && + timestamp=$(test-tool date timestamp "40.days.ago") && + timestamp=${timestamp#* -> } && + test_commit --no-tag --date "$timestamp +0000" old && + old=$(git rev-parse HEAD) && + git update-ref refs/heads/main "$base" && + git rev-list --all --objects >reachable && + test_grep ! "$old" reachable && + git reflog expire --all && + git reflog --format='%H' refs/heads/main >actual && + test_grep ! "$old" actual + ) +' + test_expect_success 'corrupt and check' ' corrupt $F && From d8e1ba5c4d6f178d9322d8007130eb88b2d35c45 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 24 Sep 2026 16:09:10 +0200 Subject: [PATCH 064/155] meson: avoid recompiling HTTP sources several times MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We only link curl into a subset of our subcommands. Consequently, as both "http.c" and "http-walker.c" depend on curl, we don't compile these into "libgit.a" but instead only link those into the commands that depend on curl. In Meson, we wire these dependencies into the target executables by using the `sources:` keyword. But this has the consequence that we're recompiling those multiple several times, once for every different command they are linked into. In fact, each of these sources is compiled seven times, which of course has an impact on compilation speed. Fix this issue by instead linking these into a static library so that they only need to be compiled once. This gives us an almost 10% speedup in a clean build: Benchmark 1: meson compile (version = HEAD~) Time (mean ± σ): 6.781 s ± 0.052 s [User: 100.775 s, System: 22.954 s] Range (min … max): 6.709 s … 6.867 s 10 runs Benchmark 2: meson compile (version = HEAD) Time (mean ± σ): 6.274 s ± 0.021 s [User: 91.882 s, System: 22.092 s] Range (min … max): 6.242 s … 6.306 s 10 runs Summary meson compile (version = HEAD) ran 1.08 ± 0.01 times faster than meson compile (version = HEAD~) Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- meson.build | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/meson.build b/meson.build index 0a95d90d211fd6..4fdb4c5405e32b 100644 --- a/meson.build +++ b/meson.build @@ -1925,10 +1925,13 @@ bin_wrappers += executable('scalar', if curl.found() libgit_curl = declare_dependency( - sources: [ - 'http.c', - 'http-walker.c', - ], + link_with: static_library('git-curl', + sources: [ + 'http.c', + 'http-walker.c', + ], + dependencies: [libgit_commonmain, curl], + ), dependencies: [libgit_commonmain, curl], ) From cc839e97a3dc15cbe280303d397f3c471801eebe Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 24 Sep 2026 16:09:11 +0200 Subject: [PATCH 065/155] meson: don't recompile git-remote-http(1) multiple times for tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When running our tests, we expect git-remote-http(1) and a couple of other binaries to be available to the test suite. In our Makefile, we achieve this by simply hardlinking the file into place in our source directory. We cannot easily do that in Meson though because there is no available command to create such a hardlink. We could of course create a custom target that uses a script for that, but that feels quite awkward. Instead, we build the executable several times, which is of course less efficient. Even worse though, similar as in the preceding commit, we're building "remote-curl.c" once for each of these targets, which makes this even more expensive. Fix this by reusing the already-compiled objects from git-remote-http(1) so that we only have to perform the linking step several times. This leads to a mild speedup: Benchmark 1: meson compile (version = HEAD~) Time (mean ± σ): 6.250 s ± 0.040 s [User: 90.881 s, System: 21.912 s] Range (min … max): 6.197 s … 6.344 s 10 runs Benchmark 2: meson compile (version = HEAD) Time (mean ± σ): 6.218 s ± 0.029 s [User: 90.633 s, System: 22.022 s] Range (min … max): 6.166 s … 6.262 s 10 runs Summary meson compile (version = HEAD) ran 1.01 ± 0.01 times faster than meson compile (version = HEAD~) Honestly, a 1% speedup isn't really worth it. But the change makes sense anyway, as we're doing the same when we build git-receive-pack(1) et al. So while the speed improvement is negligible, it brings more consistency into our build instructions. For the record: I also had a look at using a custom target that hardlinks the files into place. But the improvement it had on our build times were not that mindblowing either, saving roundabout ~100ms in wall time. So sticking with the status quo felt like the better solution as it is native to Meson. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- meson.build | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index 4fdb4c5405e32b..fa104a3efda7df 100644 --- a/meson.build +++ b/meson.build @@ -1935,12 +1935,13 @@ if curl.found() dependencies: [libgit_commonmain, curl], ) - test_dependencies += executable('git-remote-http', + git_remote_http = executable('git-remote-http', sources: 'remote-curl.c', dependencies: [libgit_curl], install: true, install_dir: git_exec_path, ) + test_dependencies += git_remote_http test_dependencies += executable('git-http-fetch', sources: 'http-fetch.c', @@ -1960,7 +1961,7 @@ if curl.found() foreach alias : [ 'git-remote-https', 'git-remote-ftp', 'git-remote-ftps' ] test_dependencies += executable(alias, - sources: 'remote-curl.c', + objects: git_remote_http.extract_all_objects(recursive: false), dependencies: [libgit_curl], ) From ae24379dd46b92734043ddcec47079f26afe3d3a Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 24 Sep 2026 16:09:12 +0200 Subject: [PATCH 066/155] meson: use precompiled headers for our test-helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In 671df48df8 (meson: precompile "git-compat-util.h", 2026-03-19) we have introduced support for precompiled headers into Meson. At that time though we only converted "libgit.a" to make use of those. Nowadays though, our test-helper also consists of a bunch of code files, and all of these include "git-compat-util.h" via "test-tool.h" as the first header. So they're a natural target to also use precompiled headers. Adapt the test-tool executable to make use of them, which results in a surprisingly large speedup for clean builds: Benchmark 1: meson compile (version = HEAD~) Time (mean ± σ): 6.363 s ± 0.033 s [User: 92.858 s, System: 22.500 s] Range (min … max): 6.311 s … 6.418 s 10 runs Benchmark 2: meson compile (version = HEAD) Time (mean ± σ): 5.327 s ± 0.021 s [User: 75.135 s, System: 20.373 s] Range (min … max): 5.299 s … 5.362 s 10 runs Summary meson compile (version = HEAD) ran 1.19 ± 0.01 times faster than meson compile (version = HEAD~) Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/helper/meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/t/helper/meson.build b/t/helper/meson.build index 3235f10ab8aae1..ae513b4cdcd275 100644 --- a/t/helper/meson.build +++ b/t/helper/meson.build @@ -83,6 +83,7 @@ test_tool_sources = [ test_tool = executable('test-tool', sources: test_tool_sources, + c_pch: '../../tools/precompiled.h', dependencies: [libgit_commonmain], ) bin_wrappers += test_tool From 3d7418e351a2c50bcf804b8d57a4a442757fb8a5 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 24 Sep 2026 16:09:13 +0200 Subject: [PATCH 067/155] meson: use precompiled headers for unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same as in the preceding commit, our unit tests don't use precompiled headers yet. In this case though it's a tiny bit more complicated to make use of them, as we do not want to include "git-compat-util.h" for "clar.c", as that code file is a third-party implementation that is independent of the Git codebase. But there's an easy workaround: instead of linking that file into the executable directly, we can easily adapt it to be built into a static library first. Like that we can trivially have separate build flags for that one file. Do so and adapt the remaining sources to use precompiled headers. This results in a small but noticeable build speedup: Benchmark 1: meson compile (version = HEAD~) Time (mean ± σ): 5.343 s ± 0.019 s [User: 75.478 s, System: 20.382 s] Range (min … max): 5.308 s … 5.376 s 10 runs Benchmark 2: meson compile (version = HEAD) Time (mean ± σ): 5.077 s ± 0.017 s [User: 70.557 s, System: 19.999 s] Range (min … max): 5.047 s … 5.103 s 10 runs Summary meson compile (version = HEAD) ran 1.05 ± 0.01 times faster than meson compile (version = HEAD~) Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/meson.build | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/t/meson.build b/t/meson.build index 3ca7b271040a62..9f1ee9ad597a8c 100644 --- a/t/meson.build +++ b/t/meson.build @@ -30,7 +30,6 @@ clar_test_suites = [ ] clar_sources = [ - 'unit-tests/clar/clar.c', 'unit-tests/unit-test.c', 'unit-tests/lib-oid.c', 'unit-tests/lib-reftable.c' @@ -49,7 +48,7 @@ clar_decls_h = custom_target( ) clar_sources += clar_decls_h -clar_sources += custom_target( +clar_suite_h = custom_target( input: clar_decls_h, output: 'clar.suite', command : [ @@ -66,6 +65,13 @@ clar_unit_tests = executable('unit-tests', c_args: [ '-DGIT_CLAR_DECLS_H="' + clar_decls_h.full_path() + '"', ], + c_pch: '../tools/precompiled.h', + link_with: static_library('clar', + sources: [ + 'unit-tests/clar/clar.c', + clar_suite_h, + ], + ), dependencies: [libgit_commonmain], ) test('unit-tests', clar_unit_tests, kwargs: test_kwargs) From 47780704699b83a54caa23e0e69c1e3af84c49d2 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 24 Sep 2026 16:09:14 +0200 Subject: [PATCH 068/155] meson: fix outdated completion helpers When using Meson 1.3.0 or newer, we use `fs.copyfile()` to put our completion helpers into the expected location so that our test suite can find these scripts. Naturally, we thus also add these scripts to our test dependencies so that we know to build them before executing tests. But there's an issue here: we include the "contrib/completion" subdir after we have already wired up our tests, so any dependencies we add here are not being honored correctly. This has the consequence that we don't know to copy around these completion helpers when we execute tests, and one has to manually `meson compile` beforehand. The interesting part here is that the code path we use with older versions of Meson don't suffer from the same problem as they use `configure_file()`, and that function will always run whenever the source file changes. It's conceptually correct to use `fs.copyfile()` instead, but given that it's mostly creating problems for us it does not really seem sensible to continue using it. Adapt the build instructions to unconditionally use `configure_file()` to fix this issue. A better fix would arguably be to promote our shell completion helpers out of "contrib/" -- they are an important part of Git nowadays, and these helpers get installed on lots of platforms. If so, we could also fix the order of subdir includes so that test dependencies are properly honored. But that feels like a bigger change, so that's left for a future patch series. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- contrib/completion/meson.build | 38 ++++++++++++++-------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/contrib/completion/meson.build b/contrib/completion/meson.build index 576125b083d308..4483c5be3eb498 100644 --- a/contrib/completion/meson.build +++ b/contrib/completion/meson.build @@ -4,31 +4,23 @@ foreach script : [ 'git-completion.zsh', 'git-prompt.sh' ] - if meson.version().version_compare('>=1.3.0') - test_dependencies += fs.copyfile(script) - else - configure_file( - input: script, - output: script, - copy: true, - ) - endif + # Note that we intentionally don't use `fs.copyfile()` here because we'd have + # to add it to our test dependencies in that case, but that creates a + # chicken-and-egg situation between including "t/" or "contrib/" first. + configure_file( + input: script, + output: script, + copy: true, + ) endforeach # We have to discern between the test dependency and the installed file. Our # tests assume the completion scripts to have the same name as the in-tree # files, but the installed filenames need to match the executable's basename. -if meson.version().version_compare('>=1.3.0') - fs.copyfile('git-completion.bash', 'git', - install: true, - install_dir: get_option('datadir') / 'bash-completion/completions', - ) -else - configure_file( - input: 'git-completion.bash', - output: 'git', - copy: true, - install: true, - install_dir: get_option('datadir') / 'bash-completion/completions', - ) -endif +configure_file( + input: 'git-completion.bash', + output: 'git', + copy: true, + install: true, + install_dir: get_option('datadir') / 'bash-completion/completions', +) From e492974847e182abc111506c0d2c1efd454e9686 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 24 Sep 2026 16:09:15 +0200 Subject: [PATCH 069/155] meson: update wrappers Our subproject wrappers are used on platforms that do not have the respective dependencies available. Most importantly, this can be used on Windows to have an almost-dependency-free build of Git. Update these wrappers via `meson wrap update`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- subprojects/curl.wrap | 19 ++++++++++--------- subprojects/expat.wrap | 21 +++++++++++---------- subprojects/openssl.wrap | 23 +++++++++++------------ subprojects/pcre2.wrap | 24 +++++++++++------------- subprojects/zlib.wrap | 21 +++++++++++---------- 5 files changed, 54 insertions(+), 54 deletions(-) diff --git a/subprojects/curl.wrap b/subprojects/curl.wrap index f7e384b85cf5ee..d73b88b75e9ad9 100644 --- a/subprojects/curl.wrap +++ b/subprojects/curl.wrap @@ -1,13 +1,14 @@ [wrap-file] -directory = curl-8.10.1 -source_url = https://github.com/curl/curl/releases/download/curl-8_10_1/curl-8.10.1.tar.xz -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/curl_8.10.1-1/curl-8.10.1.tar.xz -source_filename = curl-8.10.1.tar.xz -source_hash = 73a4b0e99596a09fa5924a4fb7e4b995a85fda0d18a2c02ab9cf134bebce04ee -patch_filename = curl_8.10.1-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/curl_8.10.1-1/get_patch -patch_hash = 707c28f35fc9b0e8d68c0c2800712007612f922a31da9637ce706a2159f3ddd8 -wrapdb_version = 8.10.1-1 +directory = curl-8.12.1 +source_url = https://github.com/curl/curl/releases/download/curl-8_12_1/curl-8.12.1.tar.xz +source_fallback_url = https://wrapdb.mesonbuild.com/v2/curl_8.12.1-2/get_source/curl-8.12.1.tar.xz +source_filename = curl-8.12.1.tar.xz +source_hash = 0341f1ed97a26c811abaebd37d62b833956792b7607ea3f15d001613c76de202 +patch_filename = curl_8.12.1-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/curl_8.12.1-2/get_patch +patch_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/curl_8.12.1-2/curl_8.12.1-2_patch.zip +patch_hash = bfd8886cc76ccfab52b1b0472e6c682cdf5374a4c30be2427326f2f495de4eba +wrapdb_version = 8.12.1-2 [provide] dependency_names = libcurl diff --git a/subprojects/expat.wrap b/subprojects/expat.wrap index 0e9292f97bfb89..67c4ae872fb10a 100644 --- a/subprojects/expat.wrap +++ b/subprojects/expat.wrap @@ -1,13 +1,14 @@ [wrap-file] -directory = expat-2.7.1 -source_url = https://github.com/libexpat/libexpat/releases/download/R_2_7_1/expat-2.7.1.tar.xz -source_filename = expat-2.7.1.tar.bz2 -source_hash = 354552544b8f99012e5062f7d570ec77f14b412a3ff5c7d8d0dae62c0d217c30 -patch_filename = expat_2.7.1-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/expat_2.7.1-1/get_patch -patch_hash = fe28cbbc427a7c9787d08b969ad54d19f59d8dd18294b4a18651cecfc789d4ef -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/expat_2.7.1-1/expat-2.7.1.tar.bz2 -wrapdb_version = 2.7.1-1 +directory = expat-2.8.4 +source_url = https://github.com/libexpat/libexpat/releases/download/R_2_8_4/expat-2.8.4.tar.xz +source_filename = expat-2.8.4.tar.xz +source_hash = 656ae1cc8da3b4ea513bb4e254f33e6243938084c0ec6239da873376b09985a7 +source_fallback_url = https://wrapdb.mesonbuild.com/v2/expat_2.8.4-1/get_source/expat-2.8.4.tar.xz +patch_filename = expat_2.8.4-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/expat_2.8.4-1/get_patch +patch_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/expat_2.8.4-1/expat_2.8.4-1_patch.zip +patch_hash = 221c537a6cfd8d55ea2d41600ee7f32d8c0659cf6e41511e9e7a2f294b21cb92 +wrapdb_version = 2.8.4-1 [provide] -expat = expat_dep +dependency_names = expat diff --git a/subprojects/openssl.wrap b/subprojects/openssl.wrap index 873d55106e9b83..e775bb104f0eb7 100644 --- a/subprojects/openssl.wrap +++ b/subprojects/openssl.wrap @@ -1,15 +1,14 @@ [wrap-file] -directory = openssl-3.0.8 -source_url = https://www.openssl.org/source/openssl-3.0.8.tar.gz -source_filename = openssl-3.0.8.tar.gz -source_hash = 6c13d2bf38fdf31eac3ce2a347073673f5d63263398f1f69d0df4a41253e4b3e -patch_filename = openssl_3.0.8-3_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/openssl_3.0.8-3/get_patch -patch_hash = 300da189e106942347d61a4a4295aa2edbcf06184f8d13b4cee0bed9fb936963 -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/openssl_3.0.8-3/openssl-3.0.8.tar.gz -wrapdb_version = 3.0.8-3 +directory = openssl-3.0.10 +source_url = https://www.openssl.org/source/openssl-3.0.10.tar.gz +source_filename = openssl-3.0.10.tar.gz +source_hash = 1761d4f5b13a1028b9b6f3d4b8e17feb0cedc9370f6afe61d7193d2cdce83323 +source_fallback_url = https://wrapdb.mesonbuild.com/v2/openssl_3.0.10-1/get_source/openssl-3.0.10.tar.gz +patch_filename = openssl_3.0.10-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/openssl_3.0.10-1/get_patch +patch_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/openssl_3.0.10-1/openssl_3.0.10-1_patch.zip +patch_hash = 2d142b7e3b1ac092cf67cb4891594c4a2d044aa92624c617a8dcbfe4f056d907 +wrapdb_version = 3.0.10-1 [provide] -libcrypto = libcrypto_dep -libssl = libssl_dep -openssl = openssl_dep +dependency_names = libcrypto, libssl, openssl diff --git a/subprojects/pcre2.wrap b/subprojects/pcre2.wrap index f45c968e2f3667..a2ff6268dc3b16 100644 --- a/subprojects/pcre2.wrap +++ b/subprojects/pcre2.wrap @@ -1,16 +1,14 @@ [wrap-file] -directory = pcre2-10.45 -source_url = https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.45/pcre2-10.45.tar.bz2 -source_filename = pcre2-10.45.tar.bz2 -source_hash = 21547f3516120c75597e5b30a992e27a592a31950b5140e7b8bfde3f192033c4 -patch_filename = pcre2_10.45-2_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/pcre2_10.45-2/get_patch -patch_hash = 7c6f34b703708652a404f9dc2769c67658c437b6043573295fa3428a9b7a6807 -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/pcre2_10.45-2/pcre2-10.45.tar.bz2 -wrapdb_version = 10.45-2 +directory = pcre2-10.48 +source_url = https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.48/pcre2-10.48.tar.bz2 +source_filename = pcre2-10.48.tar.bz2 +source_hash = b6c68fdf6f3ac31388b50aa89ff0fc49c00c987c16e7b5146491d12003f2c8ed +source_fallback_url = https://wrapdb.mesonbuild.com/v2/pcre2_10.48-1/get_source/pcre2-10.48.tar.bz2 +patch_filename = pcre2_10.48-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/pcre2_10.48-1/get_patch +patch_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/pcre2_10.48-1/pcre2_10.48-1_patch.zip +patch_hash = fbcc964804a921b02ea78fc0ff1558d5999b224db8361087828e30012023525c +wrapdb_version = 10.48-1 [provide] -libpcre2-8 = libpcre2_8 -libpcre2-16 = libpcre2_16 -libpcre2-32 = libpcre2_32 -libpcre2-posix = libpcre2_posix +dependency_names = libpcre2-8, libpcre2-16, libpcre2-32, libpcre2-posix diff --git a/subprojects/zlib.wrap b/subprojects/zlib.wrap index aa14de17740728..0626401ac2559b 100644 --- a/subprojects/zlib.wrap +++ b/subprojects/zlib.wrap @@ -1,13 +1,14 @@ [wrap-file] -directory = zlib-1.3.1 -source_url = http://zlib.net/fossils/zlib-1.3.1.tar.gz -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/zlib_1.3.1-1/zlib-1.3.1.tar.gz -source_filename = zlib-1.3.1.tar.gz -source_hash = 9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23 -patch_filename = zlib_1.3.1-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/zlib_1.3.1-1/get_patch -patch_hash = e79b98eb24a75392009cec6f99ca5cdca9881ff20bfa174e8b8926d5c7a47095 -wrapdb_version = 1.3.1-1 +directory = zlib-1.3.2 +source_url = https://zlib.net/zlib-1.3.2.tar.xz +source_fallback_url = https://wrapdb.mesonbuild.com/v2/zlib_1.3.2-1/get_source/zlib-1.3.2.tar.xz +source_filename = zlib-1.3.2.tar.xz +source_hash = d7a0654783a4da529d1bb793b7ad9c3318020af77667bcae35f95d0e42a792f3 +patch_filename = zlib_1.3.2-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/zlib_1.3.2-1/get_patch +patch_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/zlib_1.3.2-1/zlib_1.3.2-1_patch.zip +patch_hash = 5ae7a2e92f823df118cfb8c1b23d94e3117864392b3446581d669049b2fba6dd +wrapdb_version = 1.3.2-1 [provide] -zlib = zlib_dep +dependency_names = zlib From ce4a600322fa2c302e354755f9331f04d4242edd Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 24 Sep 2026 16:09:16 +0200 Subject: [PATCH 070/155] gitlab-ci: fix hanging MSVC jobs Starting with GitLab Runner 19.x, the runner executes `git credential reject` in its cleanup stage. This has bad interactions with our build environment because we install our own version of PortableGit, and the runner picks up that version of Git. The consequence is that we invoke PortableGit's default credential manager, which is Git Credential Manager for Windows. GCM then tries to use Windows Credential Manager, but it cannot and thus the job hangs in its cleanup phase forever. Fix this hang by unsetting the credential helper after installing PortableGit. This means that `git credential reject` becomes a no-op, and thus the cleanup succeeds again. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- ci/install-dependencies.ps1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ci/install-dependencies.ps1 b/ci/install-dependencies.ps1 index e3b367fa54d042..2ceb5dd99ac16d 100755 --- a/ci/install-dependencies.ps1 +++ b/ci/install-dependencies.ps1 @@ -53,3 +53,11 @@ Invoke-Installer msiexec.exe @('/i', $mesonMsi, 'INSTALLDIR=C:\Meson', '/quiet', $rustMsi = Get-Installer "rust.msi" ` "https://static.rust-lang.org/dist/rust-$RustVersion-x86_64-pc-windows-msvc.msi" Invoke-Installer msiexec.exe @('/i', $rustMsi, 'INSTALLDIR=C:\Rust', 'ADDLOCAL=Rustc,Cargo,Std', '/quiet', '/norestart') + +# Disable Git Credential Manager, which is auto-configured by PortableGit. +# GitLab's runner picks up this Git in its cleanup stage and runs `git +# credential reject`, which hangs in GCM and makes the job time out. +& "C:\Program Files\Git\bin\git.exe" config unset --all --system credential.helper +if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 5) { + throw "Failed to unset credential.helper with exit code $LASTEXITCODE" +} From 933247f61c536b0c5e87d45d2a45f3c56e4752c0 Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Thu, 24 Sep 2026 15:09:18 +0200 Subject: [PATCH 071/155] ci: fix unit tests not running on windows Since 3141df7ec4 (ci: don't skip smallest test slice in GitLab, 2026-02-19) both our CI workflows at GitLab and GitHub use one-indexed slices to run windows tests. Modify the check for running unit tests on Windows to also be one-indexed as otherwise, the unit tests are never run. Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- ci/run-test-slice.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/run-test-slice.sh b/ci/run-test-slice.sh index ff948e397fcb70..d0063efc424a58 100755 --- a/ci/run-test-slice.sh +++ b/ci/run-test-slice.sh @@ -10,8 +10,8 @@ TESTS=$(cd t && ./helper/test-tool path-utils slice-tests "$1" "$2" t[0-9]*.sh) group "Run tests" make --quiet -C t T="$(echo "$TESTS" | tr '\n' ' ')" || handle_failed_tests -# We only have one unit test at the moment, so run it in the first slice -if [ "$1" == "0" ] ; then +# We only have one unit test at the moment, so run it in the first slice. +if [ "$1" == "1" ] ; then group "Run unit tests" make --quiet -C t unit-tests-test-tool fi From 661f415b9d3eb1853bd43454f688f24f8c31fda0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 24 Sep 2026 20:01:50 +0000 Subject: [PATCH 072/155] ci(gitlab,windows): provision GNU Rust for SDK-based MinGW builds The minimal Git for Windows SDK already supplies Git and GCC. The MinGW Makefile build needs the Rust toolchain that targets GCC (as opposed to the more common MSVC one), not another Git installation or Meson. Prepare the `install-dependencies.ps1` script to be able to install the GCC-targeting Rust toolchain. Assisted-by: GPT-6 Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- ci/install-dependencies.ps1 | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/ci/install-dependencies.ps1 b/ci/install-dependencies.ps1 index e3b367fa54d042..8c68fb0cfc8662 100755 --- a/ci/install-dependencies.ps1 +++ b/ci/install-dependencies.ps1 @@ -1,5 +1,6 @@ param( - [string]$DownloadDirectory = '.dependencies' + [string]$DownloadDirectory = '.dependencies', + [switch]$Mingw ) $ErrorActionPreference = 'Stop' @@ -41,6 +42,17 @@ function Invoke-Installer { } } +$rustTarget = if ($Mingw) { 'gnu' } else { 'msvc' } +$rustMsi = Get-Installer "rust-$rustTarget.msi" ( + "https://static.rust-lang.org/dist/" + + "rust-$RustVersion-x86_64-pc-windows-$rustTarget.msi") +Invoke-Installer msiexec.exe @('/i', $rustMsi, 'INSTALLDIR=C:\Rust', + 'ADDLOCAL=Rustc,Cargo,Std', '/quiet', '/norestart') + +if ($Mingw) { + return +} + $gitAssetVersion = $GitVersion -replace '\.windows\.\d+$', '' $gitInstaller = Get-Installer "Git-Installer.exe" ` "https://github.com/git-for-windows/git/releases/download/v$GitVersion/PortableGit-$gitAssetVersion-64-bit.7z.exe" @@ -49,7 +61,3 @@ Invoke-Installer $gitInstaller @('-y', '-o"C:\Program Files\Git"') $mesonMsi = Get-Installer "meson.msi" ` "https://github.com/mesonbuild/meson/releases/download/$MesonVersion/meson-$MesonVersion-64.msi" Invoke-Installer msiexec.exe @('/i', $mesonMsi, 'INSTALLDIR=C:\Meson', '/quiet', '/norestart') - -$rustMsi = Get-Installer "rust.msi" ` - "https://static.rust-lang.org/dist/rust-$RustVersion-x86_64-pc-windows-msvc.msi" -Invoke-Installer msiexec.exe @('/i', $rustMsi, 'INSTALLDIR=C:\Rust', 'ADDLOCAL=Rustc,Cargo,Std', '/quiet', '/norestart') From 0af2d7af63a8402d2b906efb2c9dcf3d6111f27e Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 24 Sep 2026 20:01:51 +0000 Subject: [PATCH 073/155] ci(gitlab,windows): preserve exclusions during dependency setup Creating .git/info/exclude as a file with `New-Item` and `-Force` truncates existing contents. When install-dependencies.ps1 follows install-sdk.ps1, this discards the latter's /git-sdk exclusion and causes ci/lib.sh to reject SDK files as unignored build artifacts. Assisted-by: GPT-6 Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- ci/install-dependencies.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ci/install-dependencies.ps1 b/ci/install-dependencies.ps1 index 8c68fb0cfc8662..f6868dc670d3f6 100755 --- a/ci/install-dependencies.ps1 +++ b/ci/install-dependencies.ps1 @@ -12,7 +12,9 @@ $RustVersion = '1.96.0' New-Item -Path $DownloadDirectory -ItemType Directory -Force | Out-Null New-Item -Path .git/info -ItemType Directory -Force | Out-Null -New-Item -Path .git/info/exclude -ItemType File -Force | Out-Null +if (-not (Test-Path .git/info/exclude)) { + New-Item -Path .git/info/exclude -ItemType File | Out-Null +} Add-Content -Path .git/info/exclude -Value "/$DownloadDirectory" function Get-Installer { From e302a9189f0b47e5f4dae6add7baf14b6b5783e9 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 24 Sep 2026 20:01:52 +0000 Subject: [PATCH 074/155] ci(gitlab,windows): fix Rust setup for GitLab's MinGW build GitLab's MinGW job fails with "cargo: command not found": https://gitlab.com/git-scm/git/-/jobs/16576450182 86909a94db5d (ci(windows): build with Rust, 2026-09-13) enabled Rust in the shared CI configuration, but added the necessary setup only for GitHub Actions. The build needs Cargo to be reachable after the minimal SDK's login profile replaces PATH. Installing the toolchain alone is not enough. Assisted-by: GPT-6 Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- .gitlab-ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1a8e90932cc292..95b2e8beb1744d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -138,8 +138,11 @@ build:mingw64: before_script: - *windows_before_script - ./ci/install-sdk.ps1 -directory "git-sdk" + - ./ci/install-dependencies.ps1 -Mingw script: - - git-sdk/usr/bin/bash.exe -l -c 'ci/make-test-artifacts.sh artifacts' + # The minimal SDK's profile resets PATH. + - git-sdk/usr/bin/bash.exe -l -c + 'PATH=$PATH:/c/Rust/bin ci/make-test-artifacts.sh artifacts' artifacts: paths: - artifacts From 1e5f522ef1d19117376a73a7d7730ba9593ddc51 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 24 Sep 2026 20:01:53 +0000 Subject: [PATCH 075/155] ci(gitlab,windows): provide GNU Rust's host-linker support GitLab's MinGW job cannot find `x86_64-w64-mingw32-gcc` when linking gitcore's build script: https://gitlab.com/dscho/git1/-/jobs/16593470275 Although gitcore is a static library, Cargo first links `build.rs` as a host executable. We omitted the Rust MSI's `Gcc` feature, which supplies the required linker and platform libraries: https://github.com/rust-lang/rust/blob/1.96.0/src/etc/installer/msi/rust.wxs Assisted-by: GPT-6 Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- ci/install-dependencies.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/install-dependencies.ps1 b/ci/install-dependencies.ps1 index f6868dc670d3f6..9b833b93700d69 100755 --- a/ci/install-dependencies.ps1 +++ b/ci/install-dependencies.ps1 @@ -49,7 +49,7 @@ $rustMsi = Get-Installer "rust-$rustTarget.msi" ( "https://static.rust-lang.org/dist/" + "rust-$RustVersion-x86_64-pc-windows-$rustTarget.msi") Invoke-Installer msiexec.exe @('/i', $rustMsi, 'INSTALLDIR=C:\Rust', - 'ADDLOCAL=Rustc,Cargo,Std', '/quiet', '/norestart') + "ADDLOCAL=Rustc,Cargo,Std$(if ($Mingw) { ',Gcc' })", '/quiet', '/norestart') if ($Mingw) { return From 64172cf117bd00f0caee5e6e0ab7de30aab587f4 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 24 Sep 2026 20:53:49 +0000 Subject: [PATCH 076/155] ci: work around Debian 12's HTTP/2 authentication failures Since 00fa8502354 (ci: bump debian-11 job to debian-12, 2026-09-05), the `debian-12` job has intermittently failed t5559's half-auth clone with: curl 92 Stream error in the HTTP/2 framing layer Anonymous discovery succeeds, but the upload-pack POST requires authentication. Apache can return an early 401 and close the HTTP/2 stream before libcurl finishes sending the request body. Debian 12's curl 7.88.1 treats that closure as a transport error instead of allowing an authentication retry. Curl fixed this handling in 331b89a319d0 (http2: polish things around POST), included in 8.3.0: https://github.com/curl/curl/pull/11756 This did not happen before switching to Debian 12 because Debian 11 ships with libcurl 7.74.0-1.3+deb11u16, which does not have that bug, it was only introduced in cURL 7.88.0. Replacing the packaged libcurl with a modern build would defeat this job's purpose of testing older supported distributions. So let's simply skip the flaky test cases when a buggy libcurl version is detected. Assisted-by: GPT-6 Astra, GPT-6 Sol Helped-by: Jeff King Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t5551-http-fetch-smart.sh | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh index dcff0bc7d46421..f66d7ce7ac1791 100755 --- a/t/t5551-http-fetch-smart.sh +++ b/t/t5551-http-fetch-smart.sh @@ -17,6 +17,19 @@ fi test "$HTTP_PROTO" = "HTTP/2" && enable_http2 start_httpd +# The cURL version which Debian 12 ships (v7.88.1) can fail to retry +# authentication after an early HTTP/2 response. This bug was introduced +# in cURL v7.88.0 (8c762f5998 (http2: minor buffer and error path fixes, +# 2023-02-08)) and fixed in v8.3.0 (https://github.com/curl/curl/pull/11756). +test_lazy_prereq HAVE_CURL_HTTP2_BUG " + test_have_prereq HTTP2 && + build_option libcurl | + awk -F. ' + ($1 == 7 && $2 >= 88) || ($1 == 8 && $2 < 3) { broken = 1 } + END { exit !broken } + ' +" + test_expect_success HTTP2 'enable client-side http/2' ' git config --global http.version HTTP/2 ' @@ -224,7 +237,8 @@ test_expect_success 'clone from auth-only-for-push repository' ' test_cmp expect actual ' -test_expect_success 'clone from auth-only-for-objects repository' ' +test_expect_success !HAVE_CURL_HTTP2_BUG \ + 'clone from auth-only-for-objects repository' ' echo two >expect && set_askpass user@host pass@host && git clone --bare "$HTTPD_URL/auth-fetch/smart/repo.git" half-auth && @@ -233,7 +247,8 @@ test_expect_success 'clone from auth-only-for-objects repository' ' test_cmp expect actual ' -test_expect_success 'no-op half-auth fetch does not require a password' ' +test_expect_success !HAVE_CURL_HTTP2_BUG \ + 'no-op half-auth fetch does not require a password' ' set_askpass wrong && # NEEDSWORK: When using HTTP(S), protocol v0 supports a "half-auth" From 2fc11e33820f1032ad6f253f386717746a8dce97 Mon Sep 17 00:00:00 2001 From: Julia Evans Date: Fri, 25 Sep 2026 00:52:26 +0000 Subject: [PATCH 077/155] doc: add more AsciiDoc cross-references Instead of saying "see EXAMPLES below", say "see <> below" to make the man pages easier to navigate on the web. The reason for using the more verbose <> (instead of <>) is that in some cases, <> is rendered as `the section called "EXAMPLES"` or `[EXAMPLES]`. <> is rendered as `EXAMPLES`, which gives us more control over the output. This also changes some of the HTML IDs of the headings from `_examples` to `EXAMPLES`, which has the potential to break some links. Signed-off-by: Julia Evans Acked-by: Jeff King Signed-off-by: Junio C Hamano --- Documentation/fetch-options.adoc | 4 +- Documentation/git-add.adoc | 3 +- Documentation/git-bundle.adoc | 7 +- Documentation/git-cat-file.adoc | 12 ++-- Documentation/git-checkout.adoc | 11 +-- Documentation/git-credential-cache.adoc | 3 +- Documentation/git-credential-store.adoc | 3 +- Documentation/git-fast-export.adoc | 5 +- Documentation/git-fast-import.adoc | 7 +- Documentation/git-fetch.adoc | 1 + Documentation/git-filter-branch.adoc | 3 +- Documentation/git-for-each-ref.adoc | 4 +- Documentation/git-format-patch.adoc | 4 +- Documentation/git-gc.adoc | 12 ++-- Documentation/git-grep.adoc | 9 ++- Documentation/git-http-backend.adoc | 6 +- Documentation/git-ls-files.adoc | 8 ++- Documentation/git-ls-tree.adoc | 3 +- Documentation/git-maintenance.adoc | 3 +- Documentation/git-merge-tree.adoc | 2 +- Documentation/git-notes.adoc | 14 ++-- Documentation/git-p4.adoc | 12 ++-- Documentation/git-pack-objects.adoc | 5 +- Documentation/git-prune.adoc | 3 +- Documentation/git-push.adoc | 10 +-- Documentation/git-rebase.adoc | 69 +++++++++++-------- Documentation/git-replay.adoc | 4 +- Documentation/git-repo.adoc | 5 +- Documentation/git-rev-parse.adoc | 7 +- Documentation/git-send-email.adoc | 5 +- Documentation/git-stash.adoc | 3 +- Documentation/git-svn.adoc | 10 +-- Documentation/git-worktree.adoc | 5 +- Documentation/gitremote-helpers.adoc | 15 ++-- Documentation/gitsubmodules.adoc | 9 ++- Documentation/gitworkflows.adoc | 6 +- .../howto/revert-a-faulty-merge.adoc | 5 +- Documentation/revisions.adoc | 4 +- 38 files changed, 190 insertions(+), 111 deletions(-) diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc index 035f780e583cee..47dea1de8e5328 100644 --- a/Documentation/fetch-options.adoc +++ b/Documentation/fetch-options.adoc @@ -199,7 +199,7 @@ endif::git-pull[] providing the tag refspec. ifndef::git-pull[] + -See the PRUNING section below for more details. +See the <> section below for more details. `-P`:: `--prune-tags`:: @@ -210,7 +210,7 @@ See the PRUNING section below for more details. a shorthand for providing the explicit tag refspec along with `--prune`, see the discussion about that in its documentation. + -See the PRUNING section below for more details. +See the <> section below for more details. endif::git-pull[] diff --git a/Documentation/git-add.adoc b/Documentation/git-add.adoc index 16b06e38e185d1..906db7ccf30789 100644 --- a/Documentation/git-add.adoc +++ b/Documentation/git-add.adoc @@ -117,7 +117,7 @@ The intent of this option is to pick and choose lines of the patch to apply, or even to modify the contents of lines to be staged. This can be quicker and more flexible than using the interactive hunk selector. However, it is easy to confuse oneself and create a patch that does not -apply to the index. See EDITING PATCHES below. +apply to the index. See <> below. `-u`:: `--update`:: @@ -375,6 +375,7 @@ diff:: `HEAD` and index). +[[EDITING_PATCHES]] EDITING PATCHES --------------- diff --git a/Documentation/git-bundle.adoc b/Documentation/git-bundle.adoc index 03cd36fe8d2f7a..cd722bd674f034 100644 --- a/Documentation/git-bundle.adoc +++ b/Documentation/git-bundle.adoc @@ -43,7 +43,7 @@ header indicating what references are contained within the bundle. Like the packed archive format itself bundles can either be self-contained, or be created using exclusions. -See the "OBJECT PREREQUISITES" section below. +See the <> section below. Bundles created using revision exclusions are "thin packs" created using the `--thin` option to linkgit:git-pack-objects[1], and @@ -94,7 +94,8 @@ unbundle :: :: A list of arguments, acceptable to 'git rev-parse' and - 'git rev-list' (and containing a named ref, see SPECIFYING REFERENCES + 'git rev-list' (and containing a named ref, see + <> below), that specifies the specific objects and references to transport. For example, `master~10..master` causes the current master reference to be packaged along with all objects @@ -127,6 +128,7 @@ unbundle :: This flag makes the command not to report its progress on the standard error stream. +[[SPECIFYING_REFERENCES]] SPECIFYING REFERENCES --------------------- @@ -169,6 +171,7 @@ $ git bundle create master-yesterday.bundle master~10..master~5 fatal: Refusing to create empty bundle. ---------------- +[[OBJECT_PREREQUISITES]] OBJECT PREREQUISITES -------------------- diff --git a/Documentation/git-cat-file.adoc b/Documentation/git-cat-file.adoc index 514bfc00328caf..c4ea2524cf18a6 100644 --- a/Documentation/git-cat-file.adoc +++ b/Documentation/git-cat-file.adoc @@ -115,7 +115,7 @@ are not of the requested type. -- * When used with `--textconv` or `--filters`, the input lines must specify the path, separated by whitespace. See the section - `BATCH OUTPUT` below for details. + <> below for details. * When used with `--use-mailmap`, for commit and tag objects, the contents part of the output shows the identities replaced using the @@ -133,7 +133,7 @@ are not of the requested type. -- * When used with `--textconv` or `--filters`, the input lines must specify the path, separated by whitespace. See the section - `BATCH OUTPUT` below for details. + <> below for details. * When used with `--use-mailmap`, for commit and tag objects, the printed object information shows the size of the object as if the @@ -149,7 +149,7 @@ are not of the requested type. -- * When used with `--textconv` or `--filters`, the input lines must specify the path, separated by whitespace. See the section - `BATCH OUTPUT` below for details. + <> below for details. * When used with `--use-mailmap`, for commit and tag objects, the `contents` command shows the identities replaced using the @@ -295,6 +295,7 @@ If `-p` is specified, the contents of `` are pretty-printed. If `` is specified, the raw (though uncompressed) contents of the `` will be returned. +[[BATCH_OUTPUT]] BATCH OUTPUT ------------ @@ -333,12 +334,12 @@ newline. The available atoms are: `objectsize:disk`:: The size, in bytes, that the object takes up on disk. See the - note about on-disk sizes in the `CAVEATS` section below. + note about on-disk sizes in the <> section below. `deltabase`:: If the object is stored as a delta on-disk, this expands to the full hex representation of the delta base object name. - Otherwise, expands to the null OID (all zeroes). See `CAVEATS` + Otherwise, expands to the null OID (all zeroes). See <> below. `rest`:: @@ -447,6 +448,7 @@ are replaced with NUL terminators. This ensures that output will be parsable if the output itself would contain a linefeed and is thus recommended for scripting purposes. +[[CAVEATS]] CAVEATS ------- diff --git a/Documentation/git-checkout.adoc b/Documentation/git-checkout.adoc index a8b3b8c2e238bf..2aefea02287456 100644 --- a/Documentation/git-checkout.adoc +++ b/Documentation/git-checkout.adoc @@ -27,7 +27,8 @@ DESCRIPTION 2. **Restore a different version of a file**, for example with `git checkout ` or `git checkout ` -See ARGUMENT DISAMBIGUATION below for how Git decides which one to do. +See <> below +for how Git decides which one to do. `git checkout []`:: Switch to __. This sets the current branch to __ and @@ -68,7 +69,7 @@ uncommitted changes. The same as `git checkout `, except that instead of pointing `HEAD` at the branch, it points `HEAD` at the commit ID. - See the "DETACHED HEAD" section below for more. + See the <> section below for more. + Omitting __ detaches `HEAD` at the tip of the current branch. @@ -210,8 +211,8 @@ variable. Rather than checking out a branch to work on it, check out a commit for inspection and discardable experiments. This is the default behavior of `git checkout ` when - __ is not a branch name. See the "DETACHED HEAD" section - below for details. + __ is not a branch name. See the + <> section below for details. `--orphan `:: Create a new unborn branch, named __, started from @@ -372,6 +373,7 @@ leave out at most one of __ and __, in which case it defaults to ` + For more details, see the 'pathspec' entry in linkgit:gitglossary[7]. +[[DETACHED_HEAD]] DETACHED HEAD ------------- `HEAD` normally refers to a named branch (e.g. `master`). Meanwhile, each @@ -504,6 +506,7 @@ $ git reflog -2 HEAD # or $ git log -g -2 HEAD ------------ +[[ARGUMENT_DISAMBIGUATION]] ARGUMENT DISAMBIGUATION ----------------------- diff --git a/Documentation/git-credential-cache.adoc b/Documentation/git-credential-cache.adoc index 54fa7a27e199f0..2f6395937d002e 100644 --- a/Documentation/git-credential-cache.adoc +++ b/Documentation/git-credential-cache.adoc @@ -24,7 +24,7 @@ user by filesystem permissions. You probably don't want to invoke this command directly; it is meant to be used as a credential helper by other parts of Git. See -linkgit:gitcredentials[7] or `EXAMPLES` below. +linkgit:gitcredentials[7] or <> below. OPTIONS ------- @@ -54,6 +54,7 @@ credentials before their timeout, you can issue an `exit` action: git credential-cache exit -------------------------------------- +[[EXAMPLES]] EXAMPLES -------- diff --git a/Documentation/git-credential-store.adoc b/Documentation/git-credential-store.adoc index 71864a872642e8..3f8a426f93259f 100644 --- a/Documentation/git-credential-store.adoc +++ b/Documentation/git-credential-store.adoc @@ -24,7 +24,7 @@ Git programs. You probably don't want to invoke this command directly; it is meant to be used as a credential helper by other parts of git. See -linkgit:gitcredentials[7] or `EXAMPLES` below. +linkgit:gitcredentials[7] or <> below. OPTIONS ------- @@ -67,6 +67,7 @@ written to. When erasing credentials, matching credentials will be erased from all files. +[[EXAMPLES]] EXAMPLES -------- diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc index 719aeca244d534..0c2ce385c45cc5 100644 --- a/Documentation/git-fast-export.adoc +++ b/Documentation/git-fast-export.adoc @@ -148,12 +148,12 @@ by keeping the marks the same across runs. --anonymize:: Anonymize the contents of the repository while still retaining the shape of the history and stored tree. See the section on - `ANONYMIZING` below. + <> below. --anonymize-map=[:]:: Convert token `` to `` in the anonymized output. If `` is omitted, map `` to itself (i.e., do not - anonymize it). See the section on `ANONYMIZING` below. + anonymize it). See the section on <> below. --reference-excluded-parents:: By default, running a command such as `git fast-export @@ -219,6 +219,7 @@ referenced by that revision range contains the string 'refs/heads/master'. +[[ANONYMIZING]] ANONYMIZING ----------- diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc index fd165e11d2d259..c5e1cec1a50495 100644 --- a/Documentation/git-fast-import.adoc +++ b/Documentation/git-fast-import.adoc @@ -31,6 +31,7 @@ imports are supported from a particular foreign source depends on the frontend program in use. +[[OPTIONS]] OPTIONS ------- @@ -456,7 +457,7 @@ and control the current import process. More detailed discussion supports the specified feature, and aborts if it does not. `option`:: - Specify any of the options listed under OPTIONS that do not + Specify any of the options listed under <> that do not change stream semantic to suit the frontend's needs. This command is optional and is not needed to perform an import. @@ -1242,7 +1243,7 @@ no-relative-marks:: force:: Act as though the corresponding command-line option with a leading `--` was passed on the command line - (see OPTIONS, above). + (see <>, above). import-marks:: import-marks-if-exists:: @@ -1291,7 +1292,7 @@ options the user may specify to git fast-import itself. .... The `