From b9fd73a234db1a272f6cbfb528bae0ead9e07bde Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 31 Jul 2025 16:56:49 +0200 Subject: [PATCH 1/6] refs: pass refname when invoking reflog entry callback With `refs_for_each_reflog_ent()` callers can iterate through all the reflog entries for a given reference. The callback that is being invoked for each such entry does not receive the name of the reference that we are currently iterating through. This isn't really a limiting factor, as callers can simply pass the name via the callback data. But this layout sometimes does make for a bit of an awkward calling pattern. One example: when iterating through all reflogs, and for each reflog we iterate through all refnames, we have to do some extra book keeping to track which reference name we are currently yielding reflog entries for. Change the signature of the callback function so that the reference name of the reflog gets passed through to it. Adapt callers accordingly and start using the new parameter in trivial cases. The next commit will refactor the reference migration logic to make use of this parameter so that we can simplify its logic a bit. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/fsck.c | 9 ++++----- builtin/gc.c | 3 ++- builtin/stash.c | 6 ++++-- commit.c | 3 ++- object-name.c | 3 ++- reflog-walk.c | 7 ++++--- reflog.c | 3 ++- reflog.h | 3 ++- refs.c | 20 +++++++++----------- refs.h | 11 +++++++---- refs/debug.c | 5 +++-- refs/files-backend.c | 15 +++++++++------ refs/reftable-backend.c | 2 +- remote.c | 6 ++++-- revision.c | 3 ++- t/helper/test-ref-store.c | 3 ++- wt-status.c | 6 ++++-- 17 files changed, 63 insertions(+), 45 deletions(-) diff --git a/builtin/fsck.c b/builtin/fsck.c index 0084cf7400..67eb5e4fa0 100644 --- a/builtin/fsck.c +++ b/builtin/fsck.c @@ -502,13 +502,12 @@ static void fsck_handle_reflog_oid(const char *refname, struct object_id *oid, } } -static int fsck_handle_reflog_ent(struct object_id *ooid, struct object_id *noid, +static int fsck_handle_reflog_ent(const char *refname, + struct object_id *ooid, struct object_id *noid, const char *email UNUSED, timestamp_t timestamp, int tz UNUSED, - const char *message UNUSED, void *cb_data) + const char *message UNUSED, void *cb_data UNUSED) { - const char *refname = cb_data; - if (verbose) fprintf_ln(stderr, _("Checking reflog %s->%s"), oid_to_hex(ooid), oid_to_hex(noid)); @@ -525,7 +524,7 @@ static int fsck_handle_reflog(const char *logname, void *cb_data) strbuf_worktree_ref(cb_data, &refname, logname); refs_for_each_reflog_ent(get_main_ref_store(the_repository), refname.buf, fsck_handle_reflog_ent, - refname.buf); + NULL); strbuf_release(&refname); return 0; } diff --git a/builtin/gc.c b/builtin/gc.c index fab8f4dd4f..9ae87065d3 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -312,7 +312,8 @@ struct count_reflog_entries_data { size_t limit; }; -static int count_reflog_entries(struct object_id *old_oid, struct object_id *new_oid, +static int count_reflog_entries(const char *refname UNUSED, + struct object_id *old_oid, struct object_id *new_oid, const char *committer, timestamp_t timestamp, int tz, const char *msg, void *cb_data) { diff --git a/builtin/stash.c b/builtin/stash.c index e2f95cc2eb..a1ed67661e 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -738,7 +738,8 @@ cleanup: return ret; } -static int reject_reflog_ent(struct object_id *ooid UNUSED, +static int reject_reflog_ent(const char *refname UNUSED, + struct object_id *ooid UNUSED, struct object_id *noid UNUSED, const char *email UNUSED, timestamp_t timestamp UNUSED, @@ -2173,7 +2174,8 @@ struct stash_entry_data { size_t count; }; -static int collect_stash_entries(struct object_id *old_oid UNUSED, +static int collect_stash_entries(const char *refname UNUSED, + struct object_id *old_oid UNUSED, struct object_id *new_oid, const char *committer UNUSED, timestamp_t timestamp UNUSED, diff --git a/commit.c b/commit.c index 15115125c3..7ebd05f352 100644 --- a/commit.c +++ b/commit.c @@ -1031,7 +1031,8 @@ static void add_one_commit(struct object_id *oid, struct rev_collect *revs) commit->object.flags |= TMP_MARK; } -static int collect_one_reflog_ent(struct object_id *ooid, struct object_id *noid, +static int collect_one_reflog_ent(const char *refname UNUSED, + struct object_id *ooid, struct object_id *noid, const char *ident UNUSED, timestamp_t timestamp UNUSED, int tz UNUSED, const char *message UNUSED, void *cbdata) diff --git a/object-name.c b/object-name.c index ddafe7f9b1..9ec192c373 100644 --- a/object-name.c +++ b/object-name.c @@ -1516,7 +1516,8 @@ struct grab_nth_branch_switch_cbdata { struct strbuf *sb; }; -static int grab_nth_branch_switch(struct object_id *ooid UNUSED, +static int grab_nth_branch_switch(const char *refname UNUSED, + struct object_id *ooid UNUSED, struct object_id *noid UNUSED, const char *email UNUSED, timestamp_t timestamp UNUSED, diff --git a/reflog-walk.c b/reflog-walk.c index c7070b13b0..4f1ce04749 100644 --- a/reflog-walk.c +++ b/reflog-walk.c @@ -22,9 +22,10 @@ struct complete_reflogs { int nr, alloc; }; -static int read_one_reflog(struct object_id *ooid, struct object_id *noid, - const char *email, timestamp_t timestamp, int tz, - const char *message, void *cb_data) +static int read_one_reflog(const char *refname UNUSED, + struct object_id *ooid, struct object_id *noid, + const char *email, timestamp_t timestamp, int tz, + const char *message, void *cb_data) { struct complete_reflogs *array = cb_data; struct reflog_info *item; diff --git a/reflog.c b/reflog.c index 39c205fd26..2264b3bd60 100644 --- a/reflog.c +++ b/reflog.c @@ -492,7 +492,8 @@ void reflog_expiry_cleanup(void *cb_data) free_commit_list(cb->mark_list); } -int count_reflog_ent(struct object_id *ooid UNUSED, +int count_reflog_ent(const char *refname UNUSED, + struct object_id *ooid UNUSED, struct object_id *noid UNUSED, const char *email UNUSED, timestamp_t timestamp, int tz UNUSED, diff --git a/reflog.h b/reflog.h index 63bb56280f..44b306c08a 100644 --- a/reflog.h +++ b/reflog.h @@ -63,7 +63,8 @@ void reflog_expiry_prepare(const char *refname, const struct object_id *oid, int should_expire_reflog_ent(struct object_id *ooid, struct object_id *noid, const char *email, timestamp_t timestamp, int tz, const char *message, void *cb_data); -int count_reflog_ent(struct object_id *ooid, struct object_id *noid, +int count_reflog_ent(const char *refname, + struct object_id *ooid, struct object_id *noid, const char *email, timestamp_t timestamp, int tz, const char *message, void *cb_data); int should_expire_reflog_ent_verbose(struct object_id *ooid, diff --git a/refs.c b/refs.c index 4bd8028705..6ed0cd6ddc 100644 --- a/refs.c +++ b/refs.c @@ -1022,7 +1022,6 @@ int is_branch(const char *refname) } struct read_ref_at_cb { - const char *refname; timestamp_t at_time; int cnt; int reccnt; @@ -1052,7 +1051,8 @@ static void set_read_ref_cutoffs(struct read_ref_at_cb *cb, *cb->cutoff_cnt = cb->reccnt; } -static int read_ref_at_ent(struct object_id *ooid, struct object_id *noid, +static int read_ref_at_ent(const char *refname, + struct object_id *ooid, struct object_id *noid, const char *email UNUSED, timestamp_t timestamp, int tz, const char *message, void *cb_data) @@ -1072,14 +1072,13 @@ static int read_ref_at_ent(struct object_id *ooid, struct object_id *noid, oidcpy(cb->oid, noid); if (!oideq(&cb->ooid, noid)) warning(_("log for ref %s has gap after %s"), - cb->refname, show_date(cb->date, cb->tz, DATE_MODE(RFC2822))); + refname, show_date(cb->date, cb->tz, DATE_MODE(RFC2822))); } else if (cb->date == cb->at_time) oidcpy(cb->oid, noid); else if (!oideq(noid, cb->oid)) warning(_("log for ref %s unexpectedly ended on %s"), - cb->refname, show_date(cb->date, cb->tz, - DATE_MODE(RFC2822))); + refname, show_date(cb->date, cb->tz, DATE_MODE(RFC2822))); cb->reccnt++; oidcpy(&cb->ooid, ooid); oidcpy(&cb->noid, noid); @@ -1094,7 +1093,8 @@ static int read_ref_at_ent(struct object_id *ooid, struct object_id *noid, return 0; } -static int read_ref_at_ent_oldest(struct object_id *ooid, struct object_id *noid, +static int read_ref_at_ent_oldest(const char *refname UNUSED, + struct object_id *ooid, struct object_id *noid, const char *email UNUSED, timestamp_t timestamp, int tz, const char *message, void *cb_data) @@ -1117,7 +1117,6 @@ int read_ref_at(struct ref_store *refs, const char *refname, struct read_ref_at_cb cb; memset(&cb, 0, sizeof(cb)); - cb.refname = refname; cb.at_time = at_time; cb.cnt = cnt; cb.msg = msg; @@ -2976,14 +2975,14 @@ done: struct reflog_migration_data { uint64_t index; - const char *refname; struct ref_store *old_refs; struct ref_transaction *transaction; struct strbuf *errbuf; struct strbuf *sb, *name, *mail; }; -static int migrate_one_reflog_entry(struct object_id *old_oid, +static int migrate_one_reflog_entry(const char *refname, + struct object_id *old_oid, struct object_id *new_oid, const char *committer, timestamp_t timestamp, int tz, @@ -3006,7 +3005,7 @@ static int migrate_one_reflog_entry(struct object_id *old_oid, strbuf_reset(data->sb); strbuf_addstr(data->sb, fmt_ident(data->name->buf, data->mail->buf, WANT_BLANK_IDENT, date, 0)); - ret = ref_transaction_update_reflog(data->transaction, data->refname, + ret = ref_transaction_update_reflog(data->transaction, refname, new_oid, old_oid, data->sb->buf, msg, data->index++, data->errbuf); return ret; @@ -3016,7 +3015,6 @@ static int migrate_one_reflog(const char *refname, void *cb_data) { struct migration_data *migration_data = cb_data; struct reflog_migration_data data = { - .refname = refname, .old_refs = migration_data->old_refs, .transaction = migration_data->transaction, .errbuf = migration_data->errbuf, diff --git a/refs.h b/refs.h index 99b58d0b73..0bf50ce25c 100644 --- a/refs.h +++ b/refs.h @@ -558,10 +558,13 @@ int refs_delete_reflog(struct ref_store *refs, const char *refname); * The cb_data is a caller-supplied pointer given to the iterator * functions. */ -typedef int each_reflog_ent_fn( - struct object_id *old_oid, struct object_id *new_oid, - const char *committer, timestamp_t timestamp, - int tz, const char *msg, void *cb_data); +typedef int each_reflog_ent_fn(const char *refname, + struct object_id *old_oid, + struct object_id *new_oid, + const char *committer, + timestamp_t timestamp, + int tz, const char *msg, + void *cb_data); /* Iterate over reflog entries in the log for `refname`. */ diff --git a/refs/debug.c b/refs/debug.c index 485e3079d7..5e113db307 100644 --- a/refs/debug.c +++ b/refs/debug.c @@ -276,7 +276,8 @@ struct debug_reflog { void *cb_data; }; -static int debug_print_reflog_ent(struct object_id *old_oid, +static int debug_print_reflog_ent(const char *refname, + struct object_id *old_oid, struct object_id *new_oid, const char *committer, timestamp_t timestamp, int tz, const char *msg, void *cb_data) @@ -291,7 +292,7 @@ static int debug_print_reflog_ent(struct object_id *old_oid, if (new_oid) oid_to_hex_r(n, new_oid); - ret = dbg->fn(old_oid, new_oid, committer, timestamp, tz, msg, + ret = dbg->fn(refname, old_oid, new_oid, committer, timestamp, tz, msg, dbg->cb_data); trace_printf_key(&trace_refs, "reflog_ent %s (ret %d): %s -> %s, %s %ld \"%.*s\"\n", diff --git a/refs/files-backend.c b/refs/files-backend.c index f53895cf4b..dff52a583a 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -2115,7 +2115,9 @@ static int files_delete_reflog(struct ref_store *ref_store, return ret; } -static int show_one_reflog_ent(struct files_ref_store *refs, struct strbuf *sb, +static int show_one_reflog_ent(struct files_ref_store *refs, + const char *refname, + struct strbuf *sb, each_reflog_ent_fn fn, void *cb_data) { struct object_id ooid, noid; @@ -2142,7 +2144,7 @@ static int show_one_reflog_ent(struct files_ref_store *refs, struct strbuf *sb, message += 6; else message += 7; - return fn(&ooid, &noid, p, timestamp, tz, message, cb_data); + return fn(refname, &ooid, &noid, p, timestamp, tz, message, cb_data); } static char *find_beginning_of_line(char *bob, char *scan) @@ -2226,7 +2228,7 @@ static int files_for_each_reflog_ent_reverse(struct ref_store *ref_store, strbuf_splice(&sb, 0, 0, bp + 1, endp - (bp + 1)); scanp = bp; endp = bp + 1; - ret = show_one_reflog_ent(refs, &sb, fn, cb_data); + ret = show_one_reflog_ent(refs, refname, &sb, fn, cb_data); strbuf_reset(&sb); if (ret) break; @@ -2238,7 +2240,7 @@ static int files_for_each_reflog_ent_reverse(struct ref_store *ref_store, * Process it, and we can end the loop. */ strbuf_splice(&sb, 0, 0, buf, endp - buf); - ret = show_one_reflog_ent(refs, &sb, fn, cb_data); + ret = show_one_reflog_ent(refs, refname, &sb, fn, cb_data); strbuf_reset(&sb); break; } @@ -2288,7 +2290,7 @@ static int files_for_each_reflog_ent(struct ref_store *ref_store, return -1; while (!ret && !strbuf_getwholeline(&sb, logfp, '\n')) - ret = show_one_reflog_ent(refs, &sb, fn, cb_data); + ret = show_one_reflog_ent(refs, refname, &sb, fn, cb_data); fclose(logfp); strbuf_release(&sb); return ret; @@ -3359,7 +3361,8 @@ struct expire_reflog_cb { dry_run:1; }; -static int expire_reflog_ent(struct object_id *ooid, struct object_id *noid, +static int expire_reflog_ent(const char *refname UNUSED, + struct object_id *ooid, struct object_id *noid, const char *email, timestamp_t timestamp, int tz, const char *message, void *cb_data) { diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index 99fafd75eb..25a1d51618 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -2148,7 +2148,7 @@ static int yield_log_record(struct reftable_ref_store *refs, full_committer = fmt_ident(log->value.update.name, log->value.update.email, WANT_COMMITTER_IDENT, NULL, IDENT_NO_DATE); - return fn(&old_oid, &new_oid, full_committer, + return fn(log->refname, &old_oid, &new_oid, full_committer, log->value.update.time, log->value.update.tz_offset, log->value.update.message, cb_data); } diff --git a/remote.c b/remote.c index e965f022f1..db9eea4fa4 100644 --- a/remote.c +++ b/remote.c @@ -2578,7 +2578,8 @@ struct check_and_collect_until_cb_data { }; /* Get the timestamp of the latest entry. */ -static int peek_reflog(struct object_id *o_oid UNUSED, +static int peek_reflog(const char *refname UNUSED, + struct object_id *o_oid UNUSED, struct object_id *n_oid UNUSED, const char *ident UNUSED, timestamp_t timestamp, int tz UNUSED, @@ -2589,7 +2590,8 @@ static int peek_reflog(struct object_id *o_oid UNUSED, return 1; } -static int check_and_collect_until(struct object_id *o_oid UNUSED, +static int check_and_collect_until(const char *refname UNUSED, + struct object_id *o_oid UNUSED, struct object_id *n_oid, const char *ident UNUSED, timestamp_t timestamp, int tz UNUSED, diff --git a/revision.c b/revision.c index 212ca0de27..0fc1a167a1 100644 --- a/revision.c +++ b/revision.c @@ -1699,7 +1699,8 @@ static void handle_one_reflog_commit(struct object_id *oid, void *cb_data) } } -static int handle_one_reflog_ent(struct object_id *ooid, struct object_id *noid, +static int handle_one_reflog_ent(const char *refname UNUSED, + struct object_id *ooid, struct object_id *noid, const char *email UNUSED, timestamp_t timestamp UNUSED, int tz UNUSED, diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c index 8d9a271845..b2380d57ba 100644 --- a/t/helper/test-ref-store.c +++ b/t/helper/test-ref-store.c @@ -215,7 +215,8 @@ static int cmd_for_each_reflog(struct ref_store *refs, return refs_for_each_reflog(refs, each_reflog, NULL); } -static int each_reflog_ent(struct object_id *old_oid, struct object_id *new_oid, +static int each_reflog_ent(const char *refname UNUSED, + struct object_id *old_oid, struct object_id *new_oid, const char *committer, timestamp_t timestamp, int tz, const char *msg, void *cb_data UNUSED) { diff --git a/wt-status.c b/wt-status.c index 454601afa1..71bd17b610 100644 --- a/wt-status.c +++ b/wt-status.c @@ -972,7 +972,8 @@ static void wt_longstatus_print_changed(struct wt_status *s) wt_longstatus_print_trailer(s); } -static int stash_count_refs(struct object_id *ooid UNUSED, +static int stash_count_refs(const char *refname UNUSED, + struct object_id *ooid UNUSED, struct object_id *noid UNUSED, const char *email UNUSED, timestamp_t timestamp UNUSED, int tz UNUSED, @@ -1664,7 +1665,8 @@ struct grab_1st_switch_cbdata { struct object_id noid; }; -static int grab_1st_switch(struct object_id *ooid UNUSED, +static int grab_1st_switch(const char *refname UNUSED, + struct object_id *ooid UNUSED, struct object_id *noid, const char *email UNUSED, timestamp_t timestamp UNUSED, int tz UNUSED, From 2f530e5d0ac9349ad5884a7d74a60762e4ee05f8 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 31 Jul 2025 16:56:50 +0200 Subject: [PATCH 2/6] refs: simplify logic when migrating reflog entries When migrating reflog entries between two storage formats we have to do so via two callback-driven functions: - `migrate_one_reflog()` gets invoked via `refs_for_each_reflog()` to first list all available reflogs. - `migrate_one_reflog_entry()` gets invoked via `refs_for_each_reflog_ent()` in `migrate_one_reflog()`. Before the preceding commit we didn't have the refname available in `migrate_one_reflog_entry()`, which made it necessary to have a separate structure that we pass to the second callback so that we can propagate the refname. Now that `refs_for_each_reflog_ent()` knows to pass the refname to the callback though that indirection isn't necessary anymore. There's one catch though: we do have an update index that is also stored in the entry-specific callback data. This update index is required so that we can tell the ref backend in which order it should persist the reflog entries to disk. But that purpose can be trivially achieved by just converting it into a global counter that is used for all reflog entries, regardless of which reference they are for. The ordering will remain the same as both the update index and the refname is considered when sorting the entries. Move the index into `struct migration_data` and drop the now-unused `struct reflog_migration_data` to simplify the code a bit. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- refs.c | 36 ++++++++++-------------------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/refs.c b/refs.c index 6ed0cd6ddc..04c9ace793 100644 --- a/refs.c +++ b/refs.c @@ -2941,6 +2941,7 @@ struct migration_data { struct ref_transaction *transaction; struct strbuf *errbuf; struct strbuf sb, name, mail; + uint64_t index; }; static int migrate_one_ref(const char *refname, const char *referent UNUSED, const struct object_id *oid, @@ -2973,14 +2974,6 @@ done: return ret; } -struct reflog_migration_data { - uint64_t index; - struct ref_store *old_refs; - struct ref_transaction *transaction; - struct strbuf *errbuf; - struct strbuf *sb, *name, *mail; -}; - static int migrate_one_reflog_entry(const char *refname, struct object_id *old_oid, struct object_id *new_oid, @@ -2988,7 +2981,7 @@ static int migrate_one_reflog_entry(const char *refname, timestamp_t timestamp, int tz, const char *msg, void *cb_data) { - struct reflog_migration_data *data = cb_data; + struct migration_data *data = cb_data; struct ident_split ident; const char *date; int ret; @@ -2996,17 +2989,17 @@ static int migrate_one_reflog_entry(const char *refname, if (split_ident_line(&ident, committer, strlen(committer)) < 0) return -1; - strbuf_reset(data->name); - strbuf_add(data->name, ident.name_begin, ident.name_end - ident.name_begin); - strbuf_reset(data->mail); - strbuf_add(data->mail, ident.mail_begin, ident.mail_end - ident.mail_begin); + strbuf_reset(&data->name); + strbuf_add(&data->name, ident.name_begin, ident.name_end - ident.name_begin); + strbuf_reset(&data->mail); + strbuf_add(&data->mail, ident.mail_begin, ident.mail_end - ident.mail_begin); date = show_date(timestamp, tz, DATE_MODE(NORMAL)); - strbuf_reset(data->sb); - strbuf_addstr(data->sb, fmt_ident(data->name->buf, data->mail->buf, WANT_BLANK_IDENT, date, 0)); + strbuf_reset(&data->sb); + strbuf_addstr(&data->sb, fmt_ident(data->name.buf, data->mail.buf, WANT_BLANK_IDENT, date, 0)); ret = ref_transaction_update_reflog(data->transaction, refname, - new_oid, old_oid, data->sb->buf, + new_oid, old_oid, data->sb.buf, msg, data->index++, data->errbuf); return ret; } @@ -3014,17 +3007,8 @@ static int migrate_one_reflog_entry(const char *refname, static int migrate_one_reflog(const char *refname, void *cb_data) { struct migration_data *migration_data = cb_data; - struct reflog_migration_data data = { - .old_refs = migration_data->old_refs, - .transaction = migration_data->transaction, - .errbuf = migration_data->errbuf, - .sb = &migration_data->sb, - .name = &migration_data->name, - .mail = &migration_data->mail, - }; - return refs_for_each_reflog_ent(migration_data->old_refs, refname, - migrate_one_reflog_entry, &data); + migrate_one_reflog_entry, migration_data); } static int move_files(const char *from_path, const char *to_path, struct strbuf *errbuf) From 376d7f1a11a52bc3f2f4ce74557536ac2195ce5f Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 31 Jul 2025 16:56:51 +0200 Subject: [PATCH 3/6] builtin/remote: fix sign comparison warnings Fix -Wsign-comparison warnings. All of the warnings we have are about mismatches in signedness for loop counters. These are trivially fixable by using the correct integer type. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/remote.c | 54 +++++++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/builtin/remote.c b/builtin/remote.c index 5dd6cbbaee..f63c5eb888 100644 --- a/builtin/remote.c +++ b/builtin/remote.c @@ -1,5 +1,4 @@ #define USE_THE_REPOSITORY_VARIABLE -#define DISABLE_SIGN_COMPARE_WARNINGS #include "builtin.h" #include "config.h" @@ -182,7 +181,6 @@ static int add(int argc, const char **argv, const char *prefix, struct remote *remote; struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT; const char *name, *url; - int i; int result = 0; struct option options[] = { @@ -233,7 +231,7 @@ static int add(int argc, const char **argv, const char *prefix, strbuf_addf(&buf, "remote.%s.fetch", name); if (track.nr == 0) string_list_append(&track, "*"); - for (i = 0; i < track.nr; i++) { + for (size_t i = 0; i < track.nr; i++) { add_branch(buf.buf, track.items[i].string, name, mirror, &buf2); } @@ -647,18 +645,17 @@ static int read_remote_branches(const char *refname, const char *referent UNUSED static int migrate_file(struct remote *remote) { struct strbuf buf = STRBUF_INIT; - int i; strbuf_addf(&buf, "remote.%s.url", remote->name); - for (i = 0; i < remote->url.nr; i++) + for (size_t i = 0; i < remote->url.nr; i++) git_config_set_multivar(buf.buf, remote->url.v[i], "^$", 0); strbuf_reset(&buf); strbuf_addf(&buf, "remote.%s.push", remote->name); - for (i = 0; i < remote->push.nr; i++) + for (int i = 0; i < remote->push.nr; i++) git_config_set_multivar(buf.buf, remote->push.items[i].raw, "^$", 0); strbuf_reset(&buf); strbuf_addf(&buf, "remote.%s.fetch", remote->name); - for (i = 0; i < remote->fetch.nr; i++) + for (int i = 0; i < remote->fetch.nr; i++) git_config_set_multivar(buf.buf, remote->fetch.items[i].raw, "^$", 0); #ifndef WITH_BREAKING_CHANGES if (remote->origin == REMOTE_REMOTES) @@ -744,7 +741,7 @@ static int mv(int argc, const char **argv, const char *prefix, old_remote_context = STRBUF_INIT; struct string_list remote_branches = STRING_LIST_INIT_DUP; struct rename_info rename; - int i, refs_renamed_nr = 0, refspec_updated = 0; + int refs_renamed_nr = 0, refspec_updated = 0; struct progress *progress = NULL; int result = 0; @@ -790,7 +787,7 @@ static int mv(int argc, const char **argv, const char *prefix, strbuf_addf(&buf, "remote.%s.fetch", rename.new_name); git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE); strbuf_addf(&old_remote_context, ":refs/remotes/%s/", rename.old_name); - for (i = 0; i < oldremote->fetch.nr; i++) { + for (int i = 0; i < oldremote->fetch.nr; i++) { char *ptr; strbuf_reset(&buf2); @@ -813,7 +810,7 @@ static int mv(int argc, const char **argv, const char *prefix, } read_branches(); - for (i = 0; i < branch_list.nr; i++) { + for (size_t i = 0; i < branch_list.nr; i++) { struct string_list_item *item = branch_list.items + i; struct branch_info *info = item->util; if (info->remote_name && !strcmp(info->remote_name, rename.old_name)) { @@ -846,7 +843,7 @@ static int mv(int argc, const char **argv, const char *prefix, _("Renaming remote references"), rename.remote_branches->nr + rename.symrefs_nr); } - for (i = 0; i < remote_branches.nr; i++) { + for (size_t i = 0; i < remote_branches.nr; i++) { struct string_list_item *item = remote_branches.items + i; struct strbuf referent = STRBUF_INIT; @@ -859,7 +856,7 @@ static int mv(int argc, const char **argv, const char *prefix, strbuf_release(&referent); display_progress(progress, ++refs_renamed_nr); } - for (i = 0; i < remote_branches.nr; i++) { + for (size_t i = 0; i < remote_branches.nr; i++) { struct string_list_item *item = remote_branches.items + i; if (item->util) @@ -875,7 +872,7 @@ static int mv(int argc, const char **argv, const char *prefix, die(_("renaming '%s' failed"), item->string); display_progress(progress, ++refs_renamed_nr); } - for (i = 0; i < remote_branches.nr; i++) { + for (size_t i = 0; i < remote_branches.nr; i++) { struct string_list_item *item = remote_branches.items + i; if (!item->util) @@ -920,7 +917,7 @@ static int rm(int argc, const char **argv, const char *prefix, struct string_list branches = STRING_LIST_INIT_DUP; struct string_list skipped = STRING_LIST_INIT_DUP; struct branches_for_remote cb_data; - int i, result; + int result; memset(&cb_data, 0, sizeof(cb_data)); cb_data.branches = &branches; @@ -942,7 +939,7 @@ static int rm(int argc, const char **argv, const char *prefix, for_each_remote(add_known_remote, &known_remotes); read_branches(); - for (i = 0; i < branch_list.nr; i++) { + for (size_t i = 0; i < branch_list.nr; i++) { struct string_list_item *item = branch_list.items + i; struct branch_info *info = item->util; if (info->remote_name && !strcmp(info->remote_name, remote->name)) { @@ -988,7 +985,7 @@ static int rm(int argc, const char **argv, const char *prefix, "Note: Some branches outside the refs/remotes/ hierarchy were not removed;\n" "to delete them, use:", skipped.nr)); - for (i = 0; i < skipped.nr; i++) + for (size_t i = 0; i < skipped.nr; i++) fprintf(stderr, " git branch -d %s\n", skipped.items[i].string); } @@ -1166,7 +1163,6 @@ static int show_local_info_item(struct string_list_item *item, void *cb_data) struct branch_info *branch_info = item->util; struct string_list *merge = &branch_info->merge; int width = show_info->width + 4; - int i; if (branch_info->rebase >= REBASE_TRUE && branch_info->merge.nr > 1) { error(_("invalid branch.%s.merge; cannot rebase onto > 1 branch"), @@ -1192,7 +1188,7 @@ static int show_local_info_item(struct string_list_item *item, void *cb_data) } else { printf_ln(_("merges with remote %s"), merge->items[0].string); } - for (i = 1; i < merge->nr; i++) + for (size_t i = 1; i < merge->nr; i++) printf(_("%-*s and with remote %s\n"), width, "", merge->items[i].string); @@ -1277,7 +1273,6 @@ static int get_one_entry(struct remote *remote, void *priv) struct string_list *list = priv; struct strbuf remote_info_buf = STRBUF_INIT; struct strvec *url; - int i; if (remote->url.nr > 0) { struct strbuf promisor_config = STRBUF_INIT; @@ -1294,8 +1289,7 @@ static int get_one_entry(struct remote *remote, void *priv) } else string_list_append(list, remote->name)->util = NULL; url = push_url_of_remote(remote); - for (i = 0; i < url->nr; i++) - { + for (size_t i = 0; i < url->nr; i++) { strbuf_addf(&remote_info_buf, "%s (push)", url->v[i]); string_list_append(list, remote->name)->util = strbuf_detach(&remote_info_buf, NULL); @@ -1312,10 +1306,8 @@ static int show_all(void) result = for_each_remote(get_one_entry, &list); if (!result) { - int i; - string_list_sort(&list); - for (i = 0; i < list.nr; i++) { + for (size_t i = 0; i < list.nr; i++) { struct string_list_item *item = list.items + i; if (verbose) printf("%s\t%s\n", item->string, @@ -1352,7 +1344,7 @@ static int show(int argc, const char **argv, const char *prefix, query_flag = (GET_REF_STATES | GET_HEAD_NAMES | GET_PUSH_REF_STATES); for (; argc; argc--, argv++) { - int i; + size_t i; struct strvec *url; get_remote_ref_states(*argv, &info.states, query_flag); @@ -1458,7 +1450,7 @@ static void report_set_head_auto(const char *remote, const char *head_name, static int set_head(int argc, const char **argv, const char *prefix, struct repository *repo UNUSED) { - int i, opt_a = 0, opt_d = 0, result = 0, was_detached; + int opt_a = 0, opt_d = 0, result = 0, was_detached; struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT, b_local_head = STRBUF_INIT; char *head_name = NULL; @@ -1489,7 +1481,7 @@ static int set_head(int argc, const char **argv, const char *prefix, else if (states.heads.nr > 1) { result |= error(_("Multiple remote HEAD branches. " "Please choose one explicitly with:")); - for (i = 0; i < states.heads.nr; i++) + for (size_t i = 0; i < states.heads.nr; i++) fprintf(stderr, " git remote set-head %s %s\n", argv[0], states.heads.items[i].string); } else @@ -1714,7 +1706,7 @@ static int set_branches(int argc, const char **argv, const char *prefix, static int get_url(int argc, const char **argv, const char *prefix, struct repository *repo UNUSED) { - int i, push_mode = 0, all_mode = 0; + int push_mode = 0, all_mode = 0; const char *remotename = NULL; struct remote *remote; struct strvec *url; @@ -1742,7 +1734,7 @@ static int get_url(int argc, const char **argv, const char *prefix, url = push_mode ? push_url_of_remote(remote) : &remote->url; if (all_mode) { - for (i = 0; i < url->nr; i++) + for (size_t i = 0; i < url->nr; i++) printf_ln("%s", url->v[i]); } else { printf_ln("%s", url->v[0]); @@ -1754,7 +1746,7 @@ static int get_url(int argc, const char **argv, const char *prefix, static int set_url(int argc, const char **argv, const char *prefix, struct repository *repo UNUSED) { - int i, push_mode = 0, add_mode = 0, delete_mode = 0; + int push_mode = 0, add_mode = 0, delete_mode = 0; int matches = 0, negative_matches = 0; const char *remotename = NULL; const char *newurl = NULL; @@ -1818,7 +1810,7 @@ static int set_url(int argc, const char **argv, const char *prefix, if (regcomp(&old_regex, oldurl, REG_EXTENDED)) die(_("Invalid old URL pattern: %s"), oldurl); - for (i = 0; i < urlset->nr; i++) + for (size_t i = 0; i < urlset->nr; i++) if (!regexec(&old_regex, urlset->v[i], 0, NULL, 0)) matches++; else From 08e6a7add4678662d929718e8aa80d2505352cfd Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 31 Jul 2025 16:56:52 +0200 Subject: [PATCH 4/6] builtin/remote: determine whether refs need renaming early on When renaming a remote we may have to also rename remote refs in case the refspec changes. Pull out this computation into a separate loop. While that seems nonsensical right now, it'll help us in a subsequent commit where we will prepare the reference transaction before we rewrite the configuration. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/remote.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/builtin/remote.c b/builtin/remote.c index f63c5eb888..34ddcaf5f6 100644 --- a/builtin/remote.c +++ b/builtin/remote.c @@ -741,7 +741,7 @@ static int mv(int argc, const char **argv, const char *prefix, old_remote_context = STRBUF_INIT; struct string_list remote_branches = STRING_LIST_INIT_DUP; struct rename_info rename; - int refs_renamed_nr = 0, refspec_updated = 0; + int refs_renamed_nr = 0, refspecs_need_update = 0; struct progress *progress = NULL; int result = 0; @@ -782,11 +782,16 @@ static int mv(int argc, const char **argv, const char *prefix, goto out; } + strbuf_addf(&old_remote_context, ":refs/remotes/%s/", rename.old_name); + + for (int i = 0; i < oldremote->fetch.nr && !refspecs_need_update; i++) + refspecs_need_update = !!strstr(oldremote->fetch.items[i].raw, + old_remote_context.buf); + if (oldremote->fetch.nr) { strbuf_reset(&buf); strbuf_addf(&buf, "remote.%s.fetch", rename.new_name); git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE); - strbuf_addf(&old_remote_context, ":refs/remotes/%s/", rename.old_name); for (int i = 0; i < oldremote->fetch.nr; i++) { char *ptr; @@ -794,7 +799,6 @@ static int mv(int argc, const char **argv, const char *prefix, strbuf_addstr(&buf2, oldremote->fetch.items[i].raw); ptr = strstr(buf2.buf, old_remote_context.buf); if (ptr) { - refspec_updated = 1; strbuf_splice(&buf2, ptr-buf2.buf + strlen(":refs/remotes/"), strlen(rename.old_name), rename.new_name, @@ -825,7 +829,7 @@ static int mv(int argc, const char **argv, const char *prefix, } } - if (!refspec_updated) + if (!refspecs_need_update) goto out; /* From 68d090a6829a46522da0d1b15099efd6d1cdb28c Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 31 Jul 2025 16:56:53 +0200 Subject: [PATCH 5/6] builtin/remote: rework how remote refs get renamed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It was recently reported [1] that renaming a remote that has dangling symrefs is broken. This issue can be trivially reproduced: $ git init repo Initialized empty Git repository in /tmp/repo/.git/ $ cd repo/ $ git remote add origin /dev/null $ git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/master $ git remote rename origin renamed $ git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/master $ git symbolic-ref refs/remotes/renamed/HEAD fatal: ref refs/remotes/renamed/HEAD is not a symbolic ref As one can see, the "HEAD" reference did not get renamed but stays in the same place. There are two issues here: - We use `refs_resolve_ref_unsafe()` to resolve references, but we don't pass the `RESOLVE_REF_NO_RECURSE` flag. Consequently, if the reference does not resolve, the function will fail and we thus ignore this branch. - We use `refs_for_each_ref()` to iterate through the old remote's references, but that function ignores broken references. Both of these issues are easy to fix. But having a closer look at the logic that renames remote references surfaces that it leaves a lot to be desired overall. The problem is that we're using O(|refs| + |symrefs| * 2) many reference transactions to perform the renames. We first delete all symrefs, then individually rename every direct reference and finally we recreate the symrefs. On the one hand this isn't even remotely an atomic operation, so if we hit any error we'll already have deleted all references. But more importantly it is also extremely inefficient. The number of transactions for symrefs doesn't really bother us too much, as there should generally only be a single symref anyway ("HEAD"). But the renames are very expensive: - For the "reftable" backend we perform auto-compaction after every single rename, which does add up. - For the "files" backend we potentially have to rewrite the "packed-refs" file on every single rename in case they are packed. The consequence here is quadratic runtime performance. Renaming a 100k references takes hours to complete. Refactor the code to use a single transaction to perform all the reference updates atomically, which speeds up the transaction quite significantly: Benchmark 1: rename remote (refformat = files, revision = HEAD~) Time (mean ± σ): 238.770 s ± 13.857 s [User: 91.473 s, System: 143.793 s] Range (min … max): 204.863 s … 247.699 s 10 runs Benchmark 2: rename remote (refformat = files, revision = HEAD) Time (mean ± σ): 2.103 s ± 0.036 s [User: 0.360 s, System: 1.313 s] Range (min … max): 2.011 s … 2.141 s 10 runs Summary rename remote (refformat = files, revision = HEAD) ran 113.53 ± 6.87 times faster than rename remote (refformat = files, revision = HEAD~) For the "reftable" backend we see a significant speedup, as well, but given that we don't have quadratic runtime behaviour there it's way less extreme: Benchmark 1: rename remote (refformat = reftable, revision = HEAD~) Time (mean ± σ): 8.604 s ± 0.539 s [User: 4.985 s, System: 2.368 s] Range (min … max): 7.880 s … 9.556 s 10 runs Benchmark 2: rename remote (refformat = reftable, revision = HEAD) Time (mean ± σ): 1.177 s ± 0.103 s [User: 0.446 s, System: 0.270 s] Range (min … max): 1.023 s … 1.410 s 10 runs Summary rename remote (refformat = reftable, revision = HEAD) ran 7.31 ± 0.79 times faster than rename remote (refformat = reftable, revision = HEAD~) There is one issue though with using atomic transactions: when nesting a remote into itself it can happen that renamed references conflict with the old referencse. For example, when we have a reference "refs/remotes/origin/foo" and we rename "origin" to "origin/foo", then we'll end up with an F/D conflict when we try to create the renamed reference "refs/remotes/origin/foo/foo". This situation is overall quite unlikely to happen: people tend to not use nested remotes, and if they do they must at the same time also have a conflicting refname. But the end result would be that the old remote references stay intact whereas all the other parts of the repository have been adjusted for the new remote name. Address this by queueing and preparing the reference update before we touch any other part of the repository. Like this we can make sure that the reference update will go through before rewriting the configuration. Otherwise, if the transaction fails to prepare we can gracefully abort the whole operation without any changes having been performed in the repository yet. Furthermore, we can detect the conflict and print some helpful advice for how the user can resolve this situation. So overall, the tradeoff is that: - Reference transactions are now all-or-nothing. This is a significant improvement over the previous state where we may have ended up with partially-renamed references. - Rewriting references is now significantly faster. - We only rewrite the configuration in case we know that all references can be updated. - But we may refuse to rename a remote in case references conflict. Overall this seems like an acceptable tradeoff. While at it, fix the handling of symbolic/broken references by using `refs_for_each_rawref()`. Add tests that cover both this reported issue and tests that exercise nesting of remotes. One thing to note: with this change we cannot provide a proper progress monitor anymore as we queue the references into the transactions as we iterate through them. Consequently, as we don't know yet how many refs there are in total, we cannot report how many percent of the operation is done anymore. But that's a small price to pay considering that you now shouldn't need the progress monitor in most situations at all anymore. [1]: Reported-by: Han Jiang Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/remote.c | 296 ++++++++++++++++++++++++++++++---------------- t/t5505-remote.sh | 73 ++++++++++++ 2 files changed, 270 insertions(+), 99 deletions(-) diff --git a/builtin/remote.c b/builtin/remote.c index 34ddcaf5f6..db481f39bc 100644 --- a/builtin/remote.c +++ b/builtin/remote.c @@ -1,8 +1,11 @@ #define USE_THE_REPOSITORY_VARIABLE #include "builtin.h" +#include "advice.h" #include "config.h" +#include "date.h" #include "gettext.h" +#include "ident.h" #include "parse-options.h" #include "path.h" #include "transport.h" @@ -610,36 +613,161 @@ static int add_branch_for_removal(const char *refname, struct rename_info { const char *old_name; const char *new_name; - struct string_list *remote_branches; - uint32_t symrefs_nr; + struct ref_transaction *transaction; + struct progress *progress; + struct strbuf *err; + uint32_t progress_nr; + uint64_t index; }; -static int read_remote_branches(const char *refname, const char *referent UNUSED, - const struct object_id *oid UNUSED, - int flags UNUSED, void *cb_data) +static void compute_renamed_ref(struct rename_info *rename, + const char *refname, + struct strbuf *out) +{ + strbuf_reset(out); + strbuf_addstr(out, refname); + strbuf_splice(out, strlen("refs/remotes/"), strlen(rename->old_name), + rename->new_name, strlen(rename->new_name)); +} + +static int rename_one_reflog_entry(const char *old_refname, + struct object_id *old_oid, + struct object_id *new_oid, + const char *committer, + timestamp_t timestamp, int tz, + const char *msg, void *cb_data) { struct rename_info *rename = cb_data; - struct strbuf buf = STRBUF_INIT; - struct string_list_item *item; - int flag; - const char *symref; + struct strbuf new_refname = STRBUF_INIT; + struct strbuf identity = STRBUF_INIT; + struct strbuf name = STRBUF_INIT; + struct strbuf mail = STRBUF_INIT; + struct ident_split ident; + const char *date; + int error; - strbuf_addf(&buf, "refs/remotes/%s/", rename->old_name); - if (starts_with(refname, buf.buf)) { - item = string_list_append(rename->remote_branches, refname); - symref = refs_resolve_ref_unsafe(get_main_ref_store(the_repository), - refname, RESOLVE_REF_READING, - NULL, &flag); - if (symref && (flag & REF_ISSYMREF)) { - item->util = xstrdup(symref); - rename->symrefs_nr++; - } else { - item->util = NULL; - } + compute_renamed_ref(rename, old_refname, &new_refname); + + if (split_ident_line(&ident, committer, strlen(committer)) < 0) { + error = -1; + goto out; } - strbuf_release(&buf); - return 0; + strbuf_add(&name, ident.name_begin, ident.name_end - ident.name_begin); + strbuf_add(&mail, ident.mail_begin, ident.mail_end - ident.mail_begin); + + date = show_date(timestamp, tz, DATE_MODE(NORMAL)); + strbuf_addstr(&identity, fmt_ident(name.buf, mail.buf, + WANT_BLANK_IDENT, date, 0)); + + error = ref_transaction_update_reflog(rename->transaction, new_refname.buf, + new_oid, old_oid, identity.buf, msg, + rename->index++, rename->err); + +out: + strbuf_release(&new_refname); + strbuf_release(&identity); + strbuf_release(&name); + strbuf_release(&mail); + return error; +} + +static int rename_one_reflog(const char *old_refname, + const struct object_id *old_oid, + struct rename_info *rename) +{ + struct strbuf new_refname = STRBUF_INIT; + struct strbuf message = STRBUF_INIT; + int error; + + if (!refs_reflog_exists(get_main_ref_store(the_repository), old_refname)) + return 0; + + error = refs_for_each_reflog_ent(get_main_ref_store(the_repository), + old_refname, rename_one_reflog_entry, rename); + if (error < 0) + goto out; + + compute_renamed_ref(rename, old_refname, &new_refname); + + /* + * Manually write the reflog entry for the now-renamed ref. We cannot + * rely on `rename_one_ref()` to do this for us as that would screw + * over order in which reflog entries are being written. + * + * Furthermore, we only append the entry in case the reference + * resolves. Missing references shouldn't have reflogs anyway. + */ + strbuf_addf(&message, "remote: renamed %s to %s", old_refname, + new_refname.buf); + + error = ref_transaction_update_reflog(rename->transaction, new_refname.buf, + old_oid, old_oid, git_committer_info(0), + message.buf, rename->index++, rename->err); + if (error < 0) + return error; + +out: + strbuf_release(&new_refname); + strbuf_release(&message); + return error; +} + +static int rename_one_ref(const char *old_refname, const char *referent, + const struct object_id *oid, + int flags, void *cb_data) +{ + struct strbuf new_referent = STRBUF_INIT; + struct strbuf new_refname = STRBUF_INIT; + struct rename_info *rename = cb_data; + const char *ptr = old_refname; + int error; + + if (!skip_prefix(ptr, "refs/remotes/", &ptr) || + !skip_prefix(ptr, rename->old_name, &ptr) || + !skip_prefix(ptr, "/", &ptr)) { + error = 0; + goto out; + } + + compute_renamed_ref(rename, old_refname, &new_refname); + + if (flags & REF_ISSYMREF) { + /* + * Stupidly enough `referent` is not pointing to the immediate + * target of a symref, but it's the recursively resolved value. + * So symrefs pointing to symrefs would be misresolved, and + * unborn symrefs don't have any value for the `referent` at all. + */ + referent = refs_resolve_ref_unsafe(get_main_ref_store(the_repository), + old_refname, RESOLVE_REF_NO_RECURSE, + NULL, NULL); + compute_renamed_ref(rename, referent, &new_referent); + oid = NULL; + } + + error = ref_transaction_delete(rename->transaction, old_refname, + oid, referent, REF_NO_DEREF, NULL, rename->err); + if (error < 0) + goto out; + + error = ref_transaction_update(rename->transaction, new_refname.buf, oid, null_oid(the_hash_algo), + (flags & REF_ISSYMREF) ? new_referent.buf : NULL, NULL, + REF_SKIP_CREATE_REFLOG | REF_NO_DEREF | REF_SKIP_OID_VERIFICATION, + NULL, rename->err); + if (error < 0) + goto out; + + error = rename_one_reflog(old_refname, oid, rename); + if (error < 0) + goto out; + + display_progress(rename->progress, ++rename->progress_nr); + +out: + strbuf_release(&new_referent); + strbuf_release(&new_refname); + return error; } static int migrate_file(struct remote *remote) @@ -727,6 +855,14 @@ static void handle_push_default(const char* old_name, const char* new_name) strbuf_release(&push_default.origin); } +static const char conflicting_remote_refs_advice[] = N_( + "The remote you are trying to rename has conflicting references in the\n" + "new target refspec. This is most likely caused by you trying to nest\n" + "a remote into itself, e.g. by renaming 'parent' into 'parent/child'\n" + "or by unnesting a remote, e.g. the other way round.\n" + "\n" + "If that is the case, you can address this by first renaming the\n" + "remote to a different name.\n"); static int mv(int argc, const char **argv, const char *prefix, struct repository *repo UNUSED) @@ -738,11 +874,11 @@ static int mv(int argc, const char **argv, const char *prefix, }; struct remote *oldremote, *newremote; struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT, buf3 = STRBUF_INIT, - old_remote_context = STRBUF_INIT; - struct string_list remote_branches = STRING_LIST_INIT_DUP; - struct rename_info rename; - int refs_renamed_nr = 0, refspecs_need_update = 0; - struct progress *progress = NULL; + old_remote_context = STRBUF_INIT, err = STRBUF_INIT; + struct rename_info rename = { + .err = &err, + }; + int refspecs_need_update = 0; int result = 0; argc = parse_options(argc, argv, prefix, options, @@ -753,8 +889,6 @@ static int mv(int argc, const char **argv, const char *prefix, rename.old_name = argv[0]; rename.new_name = argv[1]; - rename.remote_branches = &remote_branches; - rename.symrefs_nr = 0; oldremote = remote_get(rename.old_name); if (!remote_is_configured(oldremote, 1)) { @@ -788,6 +922,30 @@ static int mv(int argc, const char **argv, const char *prefix, refspecs_need_update = !!strstr(oldremote->fetch.items[i].raw, old_remote_context.buf); + if (refspecs_need_update) { + rename.transaction = ref_store_transaction_begin(get_main_ref_store(the_repository), + 0, &err); + if (!rename.transaction) + goto out; + + if (show_progress) + rename.progress = start_delayed_progress(the_repository, + _("Renaming remote references"), 0); + + result = refs_for_each_rawref(get_main_ref_store(the_repository), + rename_one_ref, &rename); + if (result < 0) + die(_("queueing remote ref renames failed: %s"), rename.err->buf); + + result = ref_transaction_prepare(rename.transaction, &err); + if (result < 0) { + error("renaming remote references failed: %s", err.buf); + if (result == REF_TRANSACTION_ERROR_NAME_CONFLICT) + advise(conflicting_remote_refs_advice); + die(NULL); + } + } + if (oldremote->fetch.nr) { strbuf_reset(&buf); strbuf_addf(&buf, "remote.%s.fetch", rename.new_name); @@ -829,83 +987,23 @@ static int mv(int argc, const char **argv, const char *prefix, } } - if (!refspecs_need_update) - goto out; + if (refspecs_need_update) { + result = ref_transaction_commit(rename.transaction, &err); + if (result < 0) + die(_("renaming remote refs failed: %s"), rename.err->buf); - /* - * First remove symrefs, then rename the rest, finally create - * the new symrefs. - */ - refs_for_each_ref(get_main_ref_store(the_repository), - read_remote_branches, &rename); - if (show_progress) { - /* - * Count symrefs twice, since "renaming" them is done by - * deleting and recreating them in two separate passes. - */ - progress = start_progress(the_repository, - _("Renaming remote references"), - rename.remote_branches->nr + rename.symrefs_nr); + stop_progress(&rename.progress); + + handle_push_default(rename.old_name, rename.new_name); } - for (size_t i = 0; i < remote_branches.nr; i++) { - struct string_list_item *item = remote_branches.items + i; - struct strbuf referent = STRBUF_INIT; - - if (refs_read_symbolic_ref(get_main_ref_store(the_repository), item->string, - &referent)) - continue; - if (refs_delete_ref(get_main_ref_store(the_repository), NULL, item->string, NULL, REF_NO_DEREF)) - die(_("deleting '%s' failed"), item->string); - - strbuf_release(&referent); - display_progress(progress, ++refs_renamed_nr); - } - for (size_t i = 0; i < remote_branches.nr; i++) { - struct string_list_item *item = remote_branches.items + i; - - if (item->util) - continue; - strbuf_reset(&buf); - strbuf_addstr(&buf, item->string); - strbuf_splice(&buf, strlen("refs/remotes/"), strlen(rename.old_name), - rename.new_name, strlen(rename.new_name)); - strbuf_reset(&buf2); - strbuf_addf(&buf2, "remote: renamed %s to %s", - item->string, buf.buf); - if (refs_rename_ref(get_main_ref_store(the_repository), item->string, buf.buf, buf2.buf)) - die(_("renaming '%s' failed"), item->string); - display_progress(progress, ++refs_renamed_nr); - } - for (size_t i = 0; i < remote_branches.nr; i++) { - struct string_list_item *item = remote_branches.items + i; - - if (!item->util) - continue; - strbuf_reset(&buf); - strbuf_addstr(&buf, item->string); - strbuf_splice(&buf, strlen("refs/remotes/"), strlen(rename.old_name), - rename.new_name, strlen(rename.new_name)); - strbuf_reset(&buf2); - strbuf_addstr(&buf2, item->util); - strbuf_splice(&buf2, strlen("refs/remotes/"), strlen(rename.old_name), - rename.new_name, strlen(rename.new_name)); - strbuf_reset(&buf3); - strbuf_addf(&buf3, "remote: renamed %s to %s", - item->string, buf.buf); - if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf)) - die(_("creating '%s' failed"), buf.buf); - display_progress(progress, ++refs_renamed_nr); - } - stop_progress(&progress); - - handle_push_default(rename.old_name, rename.new_name); out: - string_list_clear(&remote_branches, 1); + ref_transaction_free(rename.transaction); strbuf_release(&old_remote_context); strbuf_release(&buf); strbuf_release(&buf2); strbuf_release(&buf3); + strbuf_release(&err); return result; } diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh index 2701eef85e..e592c0bcde 100755 --- a/t/t5505-remote.sh +++ b/t/t5505-remote.sh @@ -1658,4 +1658,77 @@ test_expect_success 'forbid adding superset of existing remote' ' test_grep ".outer. is a superset of existing remote .outer/inner." err ' +test_expect_success 'rename handles unborn HEAD' ' + test_when_finished "git remote remove unborn-renamed" && + git remote add unborn url && + git symbolic-ref refs/remotes/unborn/HEAD refs/remotes/unborn/nonexistent && + git remote rename unborn unborn-renamed && + git symbolic-ref refs/remotes/unborn-renamed/HEAD >actual && + echo refs/remotes/unborn-renamed/nonexistent >expected && + test_cmp expected actual +' + +test_expect_success 'rename can nest a remote into itself' ' + test_commit parent-commit && + COMMIT_ID=$(git rev-parse HEAD) && + test_when_finished "git remote remove parent || true" && + git remote add parent url && + git update-ref refs/remotes/parent/branch $COMMIT_ID && + test_when_finished "git remote remove parent/child" && + git remote rename parent parent/child && + git for-each-ref refs/remotes/ >actual && + printf "$COMMIT_ID commit\trefs/remotes/parent/child/branch\n" >expected && + test_cmp expected actual +' + +test_expect_success 'rename can nest a remote into itself with a conflicting branch name' ' + test_commit parent-conflict && + COMMIT_ID=$(git rev-parse HEAD) && + test_when_finished "git remote remove parent || true" && + git remote add parent url && + git update-ref refs/remotes/parent/child $COMMIT_ID && + test_when_finished "git remote remove parent/child" && + test_must_fail git remote rename parent parent/child 2>err && + test_grep "renaming remote references failed" err && + test_grep "The remote you are trying to rename has conflicting references" err && + git for-each-ref refs/remotes/ >actual && + printf "$COMMIT_ID commit\trefs/remotes/parent/child\n" >expected && + test_cmp expected actual +' + +test_expect_success 'rename can unnest a remote' ' + test_commit parent-child-commit && + COMMIT_ID=$(git rev-parse HEAD) && + test_when_finished "git remote remove parent/child || true" && + git remote add parent/child url && + git update-ref refs/remotes/parent/child/branch $COMMIT_ID && + git remote rename parent/child parent && + git for-each-ref refs/remotes/ >actual && + printf "$COMMIT_ID commit\trefs/remotes/parent/branch\n" >expected && + test_cmp expected actual +' + +test_expect_success 'rename moves around the reflog' ' + test_commit reflog-old && + COMMIT_ID=$(git rev-parse HEAD) && + test_config core.logAllRefUpdates true && + test_when_finished "git remote remove reflog-old || true" && + git remote add reflog-old url && + git update-ref refs/remotes/reflog-old/branch $COMMIT_ID && + test-tool ref-store main for-each-reflog >actual && + test_grep refs/remotes/reflog-old/branch actual && + test-tool ref-store main for-each-reflog-ent refs/remotes/reflog-old/branch >reflog-entries-old && + test_line_count = 1 reflog-entries-old && + git remote rename reflog-old reflog-new && + test-tool ref-store main for-each-reflog >actual && + test_grep ! refs/remotes/reflog-old actual && + test_grep refs/remotes/reflog-new/branch actual && + test-tool ref-store main for-each-reflog-ent refs/remotes/reflog-new/branch >reflog-entries-new && + cat >expect <<-EOF && + $(cat reflog-entries-old) + $COMMIT_ID $COMMIT_ID $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1112912173 -0700 remote: renamed refs/remotes/reflog-old/branch to refs/remotes/reflog-new/branch + EOF + test_cmp expect reflog-entries-new +' + test_done From 16c4fa26b99e6f6c24dc93575ffa884c13b1fe5f Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 31 Jul 2025 16:56:54 +0200 Subject: [PATCH 6/6] builtin/remote: only iterate through refs that are to be renamed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When renaming a remote we also need to rename all references accordingly. But while we only need to rename references that are contained in the "refs/remotes/$OLDNAME/" namespace, we end up using `refs_for_each_rawref()` that iterates through _all_ references. We know to exit early in the callback in case we see an irrelevant reference, but ultimately this is still a waste of compute as we knowingly iterate through references that we won't ever care about. Improve this by using `refs_for_each_rawref_in()`, which knows to only iterate through (potentially broken) references in a given prefix. The following benchmark renames a remote with a single reference in a repository that has 100k unrelated references. This shows a sizeable improvement with the "files" backend: Benchmark 1: rename remote (refformat = files, revision = HEAD~) Time (mean ± σ): 42.6 ms ± 0.9 ms [User: 29.1 ms, System: 8.4 ms] Range (min … max): 40.1 ms … 43.3 ms 10 runs Benchmark 2: rename remote (refformat = files, revision = HEAD) Time (mean ± σ): 31.7 ms ± 4.0 ms [User: 19.6 ms, System: 6.9 ms] Range (min … max): 27.1 ms … 36.0 ms 10 runs Summary rename remote (refformat = files, revision = HEAD) ran 1.35 ± 0.17 times faster than rename remote (refformat = files, revision = HEAD~) The "reftable" backend shows roughly the same absolute improvement, but given that it's already significantly faster than the "files" backend this translates to a much larger relative improvement: Benchmark 1: rename remote (refformat = reftable, revision = HEAD~) Time (mean ± σ): 18.2 ms ± 0.5 ms [User: 12.7 ms, System: 3.0 ms] Range (min … max): 17.3 ms … 21.4 ms 110 runs Benchmark 2: rename remote (refformat = reftable, revision = HEAD) Time (mean ± σ): 8.8 ms ± 0.5 ms [User: 3.8 ms, System: 2.9 ms] Range (min … max): 7.5 ms … 9.9 ms 167 runs Summary rename remote (refformat = reftable, revision = HEAD) ran 2.07 ± 0.12 times faster than rename remote (refformat = reftable, revision = HEAD~) Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/remote.c | 13 ++++--------- refs.c | 8 +++++++- refs.h | 2 ++ 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/builtin/remote.c b/builtin/remote.c index db481f39bc..60e67f1b74 100644 --- a/builtin/remote.c +++ b/builtin/remote.c @@ -720,16 +720,8 @@ static int rename_one_ref(const char *old_refname, const char *referent, struct strbuf new_referent = STRBUF_INIT; struct strbuf new_refname = STRBUF_INIT; struct rename_info *rename = cb_data; - const char *ptr = old_refname; int error; - if (!skip_prefix(ptr, "refs/remotes/", &ptr) || - !skip_prefix(ptr, rename->old_name, &ptr) || - !skip_prefix(ptr, "/", &ptr)) { - error = 0; - goto out; - } - compute_renamed_ref(rename, old_refname, &new_refname); if (flags & REF_ISSYMREF) { @@ -932,7 +924,10 @@ static int mv(int argc, const char **argv, const char *prefix, rename.progress = start_delayed_progress(the_repository, _("Renaming remote references"), 0); - result = refs_for_each_rawref(get_main_ref_store(the_repository), + strbuf_reset(&buf); + strbuf_addf(&buf, "refs/remotes/%s/", rename.old_name); + + result = refs_for_each_rawref_in(get_main_ref_store(the_repository), buf.buf, rename_one_ref, &rename); if (result < 0) die(_("queueing remote ref renames failed: %s"), rename.err->buf); diff --git a/refs.c b/refs.c index 04c9ace793..7e2f02dddf 100644 --- a/refs.c +++ b/refs.c @@ -1839,7 +1839,13 @@ int refs_for_each_namespaced_ref(struct ref_store *refs, int refs_for_each_rawref(struct ref_store *refs, each_ref_fn fn, void *cb_data) { - return do_for_each_ref(refs, "", NULL, fn, 0, + return refs_for_each_rawref_in(refs, "", fn, cb_data); +} + +int refs_for_each_rawref_in(struct ref_store *refs, const char *prefix, + each_ref_fn fn, void *cb_data) +{ + return do_for_each_ref(refs, prefix, NULL, fn, 0, DO_FOR_EACH_INCLUDE_BROKEN, cb_data); } diff --git a/refs.h b/refs.h index 0bf50ce25c..19fb1d924a 100644 --- a/refs.h +++ b/refs.h @@ -428,6 +428,8 @@ int refs_for_each_namespaced_ref(struct ref_store *refs, /* can be used to learn about broken ref and symref */ int refs_for_each_rawref(struct ref_store *refs, each_ref_fn fn, void *cb_data); +int refs_for_each_rawref_in(struct ref_store *refs, const char *prefix, + each_ref_fn fn, void *cb_data); /* * Iterates over all refs including root refs, i.e. pseudorefs and HEAD.