From 78c418fadebbd6a28e9487364a6922e8690f1f9a Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 29 Oct 2026 10:57:10 +0100 Subject: [PATCH 01/12] rerere: rename `repo_rerere()` The `repo_rerere()` function is the only function in "rerere.h" that does not have a `rerere_` prefix. And while it does indeed receive a repository as first parameter, that's not a good-enough reason to give it a `repo_` prefix. Rename the function to just `rerere()`. Move around and extend documentation of this function to better explain what it does. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- apply.c | 2 +- builtin/am.c | 4 ++-- builtin/commit.c | 2 +- builtin/merge.c | 2 +- builtin/rerere.c | 2 +- builtin/stash.c | 2 +- rerere.c | 7 +------ rerere.h | 9 ++++++++- sequencer.c | 4 ++-- 9 files changed, 18 insertions(+), 16 deletions(-) diff --git a/apply.c b/apply.c index a0b7119c63..646fabeafa 100644 --- a/apply.c +++ b/apply.c @@ -4866,7 +4866,7 @@ static int write_out_results(struct apply_state *state, struct patch *list) * tree with conflict markers, but that isn't written with --cached. */ if (!state->cached) - repo_rerere(state->repo, 0); + rerere(state->repo, 0); } return errs; diff --git a/builtin/am.c b/builtin/am.c index e9623b8307..4a5b37bee0 100644 --- a/builtin/am.c +++ b/builtin/am.c @@ -1649,7 +1649,7 @@ static int fall_back_threeway(const struct am_state *state, const char *index_pa o.verbosity = 0; if (merge_ort_generic(&o, &our_tree, &their_tree, 1, bases, &result)) { - repo_rerere(the_repository, state->allow_rerere_autoupdate); + rerere(the_repository, state->allow_rerere_autoupdate); free(their_tree_name); return error(_("Failed to merge in the changes.")); } @@ -1979,7 +1979,7 @@ static void am_resolve(struct am_state *state, int allow_empty) goto next; } - repo_rerere(the_repository, 0); + rerere(the_repository, 0); do_commit(state); diff --git a/builtin/commit.c b/builtin/commit.c index 8ffd42681b..3a4b94ad62 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1962,7 +1962,7 @@ int cmd_commit(int argc, git_test_write_commit_graph_or_die(the_repository); - repo_rerere(the_repository, 0); + rerere(the_repository, 0); run_auto_maintenance(the_repository, quiet); run_commit_hook(use_editor, repo_get_index_file(the_repository), NULL, "post-commit", NULL); diff --git a/builtin/merge.c b/builtin/merge.c index d7bf209114..089e36e857 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -1061,7 +1061,7 @@ static int suggest_conflicts(void) fputs(msgbuf.buf, fp); strbuf_release(&msgbuf); fclose(fp); - repo_rerere(the_repository, allow_rerere_auto); + rerere(the_repository, allow_rerere_auto); printf(_("Automatic merge failed; " "fix conflicts and then commit the result.\n")); return 1; diff --git a/builtin/rerere.c b/builtin/rerere.c index 2c17f02f56..a499e7b915 100644 --- a/builtin/rerere.c +++ b/builtin/rerere.c @@ -83,7 +83,7 @@ int cmd_rerere(int argc, flags = RERERE_NOAUTOUPDATE; if (argc < 1) - return repo_rerere(the_repository, flags); + return rerere(the_repository, flags); if (!strcmp(argv[0], "forget")) { struct pathspec pathspec; diff --git a/builtin/stash.c b/builtin/stash.c index e1a3a76bd0..91870a255f 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -688,7 +688,7 @@ static enum stash_apply_result do_apply_stash(const char *prefix, ret = error(_("could not write index")); if (ret) { - repo_rerere(the_repository, 0); + rerere(the_repository, 0); if (index) fprintf_ln(stderr, _("Index was not unstashed.")); diff --git a/rerere.c b/rerere.c index e089761208..47a4a325bf 100644 --- a/rerere.c +++ b/rerere.c @@ -951,12 +951,7 @@ int rerere_cache_setup(struct rerere_cache *cache, return fd; } -/* - * The main entry point that is called internally from codepaths that - * perform mergy operations, possibly leaving conflicted index entries - * and working tree files. - */ -int repo_rerere(struct repository *r, int flags) +int rerere(struct repository *r, int flags) { struct string_list merge_rr = STRING_LIST_INIT_DUP; struct rerere_cache cache; diff --git a/rerere.h b/rerere.h index d3cbd10e13..dd9da03d6d 100644 --- a/rerere.h +++ b/rerere.h @@ -45,7 +45,14 @@ int rerere_cache_setup(struct rerere_cache *cache, int flags); void rerere_cache_release(struct rerere_cache *cache); -int repo_rerere(struct repository *, int); +/* + * The main entry point that is called internally from codepaths that + * perform mergy operations, possibly leaving conflicted index entries + * and working tree files. It records (and optionally updates) resolved + * conflicts and reuses recorded resolutions for unresolved conflicts. + */ +int rerere(struct repository *r, int flags); + /* * Given the conflict ID and the name of a "file" used for replaying * the recorded resolution (e.g. "preimage", "postimage"), return the diff --git a/sequencer.c b/sequencer.c index 508ec71792..ace1cfac1c 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2556,7 +2556,7 @@ static enum pick_result do_pick_commit(struct repository *r, : _("could not apply %s... %s"), short_commit_name(r, commit), msg.subject); print_advice(r, res == 1, opts); - repo_rerere(r, opts->allow_rerere_auto); + rerere(r, opts->allow_rerere_auto); goto leave; } @@ -4488,7 +4488,7 @@ static int do_merge(struct repository *r, rollback_lock_file(&lock); if (ret) - repo_rerere(r, opts->allow_rerere_auto); + rerere(r, opts->allow_rerere_auto); else /* * In case of problems, we now want to return a positive From ebaccb230c8f191a7f972a5ab096a0b3b73c94a4 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 29 Oct 2026 10:57:11 +0100 Subject: [PATCH 02/12] rerere: convert flags into an enum Our modern coding guidelines say that flags should typically be passed around as enums so that it becomes trivial to see what kind of flags are expected. Adapt the rerere flags accordingly and document them. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- rerere.c | 7 ++++--- rerere.h | 30 +++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/rerere.c b/rerere.c index 47a4a325bf..3652b7ac2d 100644 --- a/rerere.c +++ b/rerere.c @@ -908,7 +908,7 @@ out: int rerere_cache_setup(struct rerere_cache *cache, struct repository *r, struct string_list *merge_rr, - int flags) + enum rerere_flags flags) { int lock_timeout_ms = 1000; int fd; @@ -951,7 +951,7 @@ int rerere_cache_setup(struct rerere_cache *cache, return fd; } -int rerere(struct repository *r, int flags) +int rerere(struct repository *r, enum rerere_flags flags) { struct string_list merge_rr = STRING_LIST_INIT_DUP; struct rerere_cache cache; @@ -1336,7 +1336,8 @@ out: return needed; } -void rerere_gc(struct repository *r, struct string_list *rr, int flags) +void rerere_gc(struct repository *r, struct string_list *rr, + enum rerere_flags flags) { struct rerere_cache cache; struct string_list to_remove = STRING_LIST_INIT_DUP; diff --git a/rerere.h b/rerere.h index dd9da03d6d..f13790c5dc 100644 --- a/rerere.h +++ b/rerere.h @@ -9,12 +9,6 @@ struct pathspec; struct repository; -#define RERERE_AUTOUPDATE 01 -#define RERERE_NOAUTOUPDATE 02 -#define RERERE_READONLY 04 -/* If MERGE_RR.lock is taken, return -1 as if rerere were disabled */ -#define RERERE_NOWAIT 010 - /* * Marks paths that have been hand-resolved and added to the * index. Set in the util field of such paths after calling @@ -39,10 +33,27 @@ struct rerere_id { int variant; }; +enum rerere_flags { + /* + * Auto-update the index with previously recorded resolutions. This can + * be used to explicitly override "rerere.autoUpdate". + */ + RERERE_AUTOUPDATE = (1 << 0), + + /* Reverse of RERERE_AUTOUPDATE. */ + RERERE_NOAUTOUPDATE = (1 << 1), + + /* Skip acquiring the MERGE_RR.lock for a read-only operation. */ + RERERE_READONLY = (1 << 2), + + /* If MERGE_RR.lock is taken, return -1 as if rerere were disabled. */ + RERERE_NOWAIT = (1 << 3), +}; + int rerere_cache_setup(struct rerere_cache *cache, struct repository *r, struct string_list *merge_rr, - int flags); + enum rerere_flags flags); void rerere_cache_release(struct rerere_cache *cache); /* @@ -51,7 +62,7 @@ void rerere_cache_release(struct rerere_cache *cache); * and working tree files. It records (and optionally updates) resolved * conflicts and reuses recorded resolutions for unresolved conflicts. */ -int rerere(struct repository *r, int flags); +int rerere(struct repository *r, enum rerere_flags flags); /* * Given the conflict ID and the name of a "file" used for replaying @@ -65,7 +76,8 @@ const char *rerere_path(struct repository *, int rerere_forget(struct repository *, struct pathspec *); int rerere_remaining(struct repository *, struct string_list *); void rerere_clear(struct repository *, struct string_list *); -void rerere_gc(struct repository *, struct string_list *, int); +void rerere_gc(struct repository *, struct string_list *, + enum rerere_flags flags); /* * Check whether garbage collection for rerere entries is needed, which is From 419552376f52aa527857ef1adaeb1c66147af2a9 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 29 Oct 2026 10:57:12 +0100 Subject: [PATCH 03/12] builtin/rerere: drop `the_repository` There are a couple of references to `the_repository` in git-rerere(1). As that command cannot run outside of a repository these references can be trivially replaced by the repository that's passed in as parameter to `cmd_rerere()`. We don't have to worry about the repository being `NULL` when called with "-h", either, as there are no references to the repo before we call `parse_options()`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/rerere.c | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/builtin/rerere.c b/builtin/rerere.c index a499e7b915..83fb3783c6 100644 --- a/builtin/rerere.c +++ b/builtin/rerere.c @@ -1,5 +1,3 @@ -#define USE_THE_REPOSITORY_VARIABLE - #include "builtin.h" #include "config.h" #include "gettext.h" @@ -57,7 +55,7 @@ static int diff_two(const char *file1, const char *label1, int cmd_rerere(int argc, const char **argv, const char *prefix, - struct repository *repo UNUSED) + struct repository *repo) { struct string_list merge_rr = STRING_LIST_INIT_DUP; int autoupdate = -1, skip_locked = 0, flags = 0; @@ -75,7 +73,7 @@ int cmd_rerere(int argc, if (skip_locked && (argc < 1 || strcmp(argv[0], "gc"))) die(_("the option '%s' requires '%s'"), "--skip-locked", "gc"); - repo_config(the_repository, git_xmerge_config, NULL); + repo_config(repo, git_xmerge_config, NULL); if (autoupdate == 1) flags = RERERE_AUTOUPDATE; @@ -83,7 +81,7 @@ int cmd_rerere(int argc, flags = RERERE_NOAUTOUPDATE; if (argc < 1) - return rerere(the_repository, flags); + return rerere(repo, flags); if (!strcmp(argv[0], "forget")) { struct pathspec pathspec; @@ -94,21 +92,21 @@ int cmd_rerere(int argc, parse_pathspec(&pathspec, 0, PATHSPEC_PREFER_CWD, prefix, argv + 1); - ret = rerere_forget(the_repository, &pathspec); + ret = rerere_forget(repo, &pathspec); clear_pathspec(&pathspec); return ret; } if (!strcmp(argv[0], "clear")) { - rerere_clear(the_repository, &merge_rr); + rerere_clear(repo, &merge_rr); } else if (!strcmp(argv[0], "gc")) - rerere_gc(the_repository, &merge_rr, + rerere_gc(repo, &merge_rr, skip_locked ? RERERE_NOWAIT : 0); else if (!strcmp(argv[0], "status")) { struct rerere_cache cache; - if (rerere_cache_setup(&cache, the_repository, &merge_rr, + if (rerere_cache_setup(&cache, repo, &merge_rr, flags | RERERE_READONLY) < 0) return 0; for (size_t i = 0; i < merge_rr.nr; i++) @@ -116,7 +114,7 @@ int cmd_rerere(int argc, rerere_cache_release(&cache); } else if (!strcmp(argv[0], "remaining")) { - rerere_remaining(the_repository, &merge_rr); + rerere_remaining(repo, &merge_rr); for (size_t i = 0; i < merge_rr.nr; i++) { if (merge_rr.items[i].util != RERERE_RESOLVED) printf("%s\n", merge_rr.items[i].string); @@ -129,16 +127,16 @@ int cmd_rerere(int argc, struct strbuf buf = STRBUF_INIT; struct rerere_cache cache; - if (rerere_cache_setup(&cache, the_repository, &merge_rr, + if (rerere_cache_setup(&cache, repo, &merge_rr, flags | RERERE_READONLY) < 0) return 0; for (size_t i = 0; i < merge_rr.nr; i++) { const char *path = merge_rr.items[i].string; const struct rerere_id *id = merge_rr.items[i].util; - if (diff_two(rerere_path(the_repository, &buf, id, "preimage"), + if (diff_two(rerere_path(repo, &buf, id, "preimage"), path, path, path)) die(_("unable to generate diff for '%s'"), - rerere_path(the_repository, &buf, id, NULL)); + rerere_path(repo, &buf, id, NULL)); } rerere_cache_release(&cache); From 62932a42fe506ea37c148a3f33d239b608820f5f Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 29 Oct 2026 10:57:13 +0100 Subject: [PATCH 04/12] builtin/rerere: stop passing flags to subcommands that ignore them The git-rerere(1) command has a "--[no-]rerere-autoupdate" flag. If passed, we explicitly enable or disable recording clean merge resolutions that are staged in the index. This flag only matters to git-rerere(1) when not executed with any subcommand, but we also forward the flag to `git rerere status` and `git rerere diff`. This is mostly harmless as we would ignore those flags anyway, but it makes subsequent refactorings harder to understand. Prepare for these refactorings by not using the flag in those subcommands anymore. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/rerere.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/rerere.c b/builtin/rerere.c index 83fb3783c6..405276cc94 100644 --- a/builtin/rerere.c +++ b/builtin/rerere.c @@ -107,7 +107,7 @@ int cmd_rerere(int argc, struct rerere_cache cache; if (rerere_cache_setup(&cache, repo, &merge_rr, - flags | RERERE_READONLY) < 0) + RERERE_READONLY) < 0) return 0; for (size_t i = 0; i < merge_rr.nr; i++) printf("%s\n", merge_rr.items[i].string); @@ -128,7 +128,7 @@ int cmd_rerere(int argc, struct rerere_cache cache; if (rerere_cache_setup(&cache, repo, &merge_rr, - flags | RERERE_READONLY) < 0) + RERERE_READONLY) < 0) return 0; for (size_t i = 0; i < merge_rr.nr; i++) { const char *path = merge_rr.items[i].string; From 9b1940400632e0e4eca0af8eac0c83ec6b5a0eb9 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 29 Oct 2026 10:57:14 +0100 Subject: [PATCH 05/12] builtin/rerere: turn "forget" into a proper subcommand The git-rerere(1) command has its own, home-grown implementation of subcommands. Of course this implementation is inconsistent with how subcommands typically work: - Options from other subcommands apply to each other. - We don't have proper usage strings. - Running `git rerere -h` doesn't print the options that are supported by that subcommand, but instead it just prints the help of git-rerere(1) itself. A subsequent commit will thus introduce proper subcommands as supported by our parse-options subsystem. Refactor the "forget" subcommand to use parse-options as a preparatory step. The other commands will follow in subsequent commits until all of them have been converted, and then we switch over to `OPT_SUBCOMMAND()`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/rerere.c | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/builtin/rerere.c b/builtin/rerere.c index 405276cc94..5d2ae154ff 100644 --- a/builtin/rerere.c +++ b/builtin/rerere.c @@ -52,6 +52,28 @@ static int diff_two(const char *file1, const char *label1, return ret; } +static int cmd_rerere_forget(int argc, + const char **argv, + const char *prefix, + struct repository *repo) +{ + struct option options[] = { + OPT_END(), + }; + struct pathspec pathspec; + int ret; + + argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); + if (!argc) + warning(_("'git rerere forget' without paths is deprecated")); + parse_pathspec(&pathspec, 0, PATHSPEC_PREFER_CWD, prefix, argv); + + ret = rerere_forget(repo, &pathspec); + + clear_pathspec(&pathspec); + return ret; +} + int cmd_rerere(int argc, const char **argv, const char *prefix, @@ -83,20 +105,8 @@ int cmd_rerere(int argc, if (argc < 1) return rerere(repo, flags); - if (!strcmp(argv[0], "forget")) { - struct pathspec pathspec; - int ret; - - if (argc < 2) - warning(_("'git rerere forget' without paths is deprecated")); - parse_pathspec(&pathspec, 0, PATHSPEC_PREFER_CWD, - prefix, argv + 1); - - ret = rerere_forget(repo, &pathspec); - - clear_pathspec(&pathspec); - return ret; - } + if (!strcmp(argv[0], "forget")) + return cmd_rerere_forget(argc, argv, prefix, repo); if (!strcmp(argv[0], "clear")) { rerere_clear(repo, &merge_rr); From 3bcf76b89e0a155a6749ddade13413902b885d47 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 29 Oct 2026 10:57:15 +0100 Subject: [PATCH 06/12] builtin/rerere: turn "clear" into a proper subcommand As explained in a preceding commit, a subsequent commit will introduce proper subcommands as supported by our parse-options subsystem. Refactor the "clear" subcommand to use parse-options as a preparatory step. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/rerere.c | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/builtin/rerere.c b/builtin/rerere.c index 5d2ae154ff..62f69b488b 100644 --- a/builtin/rerere.c +++ b/builtin/rerere.c @@ -52,6 +52,26 @@ static int diff_two(const char *file1, const char *label1, return ret; } +static int cmd_rerere_clear(int argc, + const char **argv, + const char *prefix, + struct repository *repo) +{ + struct option options[] = { + OPT_END(), + }; + struct string_list merge_rr = STRING_LIST_INIT_DUP; + + argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); + if (argc) + usage(_("too many arguments")); + + rerere_clear(repo, &merge_rr); + + string_list_clear(&merge_rr, 1); + return 0; +} + static int cmd_rerere_forget(int argc, const char **argv, const char *prefix, @@ -107,9 +127,8 @@ int cmd_rerere(int argc, if (!strcmp(argv[0], "forget")) return cmd_rerere_forget(argc, argv, prefix, repo); - - if (!strcmp(argv[0], "clear")) { - rerere_clear(repo, &merge_rr); + else if (!strcmp(argv[0], "clear")) { + return cmd_rerere_clear(argc, argv, prefix, repo); } else if (!strcmp(argv[0], "gc")) rerere_gc(repo, &merge_rr, skip_locked ? RERERE_NOWAIT : 0); From e4c7113d8bfee07721fb296eba0446f3dbe382b3 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 29 Oct 2026 10:57:16 +0100 Subject: [PATCH 07/12] builtin/rerere: turn "status" into a proper subcommand As explained in a preceding commit, a subsequent commit will introduce proper subcommands as supported by our parse-options subsystem. Refactor the "status" subcommand to use parse-options as a preparatory step. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/rerere.c | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/builtin/rerere.c b/builtin/rerere.c index 62f69b488b..2bcd270a1f 100644 --- a/builtin/rerere.c +++ b/builtin/rerere.c @@ -94,6 +94,32 @@ static int cmd_rerere_forget(int argc, return ret; } +static int cmd_rerere_status(int argc, + const char **argv, + const char *prefix, + struct repository *repo) +{ + struct option options[] = { + OPT_END(), + }; + struct string_list merge_rr = STRING_LIST_INIT_DUP; + struct rerere_cache cache; + + argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); + if (argc) + usage(_("too many arguments")); + + if (rerere_cache_setup(&cache, repo, &merge_rr, + RERERE_READONLY) < 0) + return 0; + for (size_t i = 0; i < merge_rr.nr; i++) + printf("%s\n", merge_rr.items[i].string); + + string_list_clear(&merge_rr, 1); + rerere_cache_release(&cache); + return 0; +} + int cmd_rerere(int argc, const char **argv, const char *prefix, @@ -132,17 +158,9 @@ int cmd_rerere(int argc, } else if (!strcmp(argv[0], "gc")) rerere_gc(repo, &merge_rr, skip_locked ? RERERE_NOWAIT : 0); - else if (!strcmp(argv[0], "status")) { - struct rerere_cache cache; - - if (rerere_cache_setup(&cache, repo, &merge_rr, - RERERE_READONLY) < 0) - return 0; - for (size_t i = 0; i < merge_rr.nr; i++) - printf("%s\n", merge_rr.items[i].string); - - rerere_cache_release(&cache); - } else if (!strcmp(argv[0], "remaining")) { + else if (!strcmp(argv[0], "status")) + return cmd_rerere_status(argc, argv, prefix, repo); + else if (!strcmp(argv[0], "remaining")) { rerere_remaining(repo, &merge_rr); for (size_t i = 0; i < merge_rr.nr; i++) { if (merge_rr.items[i].util != RERERE_RESOLVED) From 951de3f4628b2b3436dacb8b57e3a9dc5a00973b Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 29 Oct 2026 10:57:17 +0100 Subject: [PATCH 08/12] builtin/rerere: turn "remaining" into a proper subcommand As explained in a preceding commit, a subsequent commit will introduce proper subcommands as supported by our parse-options subsystem. Refactor the "remaining" subcommand to use parse-options as a preparatory step. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/rerere.c | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/builtin/rerere.c b/builtin/rerere.c index 2bcd270a1f..3f82bcbeb1 100644 --- a/builtin/rerere.c +++ b/builtin/rerere.c @@ -94,6 +94,34 @@ static int cmd_rerere_forget(int argc, return ret; } +static int cmd_rerere_remaining(int argc, + const char **argv, + const char *prefix, + struct repository *repo) +{ + struct option options[] = { + OPT_END(), + }; + struct string_list merge_rr = STRING_LIST_INIT_DUP; + + argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); + if (argc) + usage(_("too many arguments")); + + rerere_remaining(repo, &merge_rr); + for (size_t i = 0; i < merge_rr.nr; i++) { + if (merge_rr.items[i].util != RERERE_RESOLVED) + printf("%s\n", merge_rr.items[i].string); + else + /* prepare for later call to + * string_list_clear() */ + merge_rr.items[i].util = NULL; + } + + string_list_clear(&merge_rr, 1); + return 0; +} + static int cmd_rerere_status(int argc, const char **argv, const char *prefix, @@ -160,17 +188,9 @@ int cmd_rerere(int argc, skip_locked ? RERERE_NOWAIT : 0); else if (!strcmp(argv[0], "status")) return cmd_rerere_status(argc, argv, prefix, repo); - else if (!strcmp(argv[0], "remaining")) { - rerere_remaining(repo, &merge_rr); - for (size_t i = 0; i < merge_rr.nr; i++) { - if (merge_rr.items[i].util != RERERE_RESOLVED) - printf("%s\n", merge_rr.items[i].string); - else - /* prepare for later call to - * string_list_clear() */ - merge_rr.items[i].util = NULL; - } - } else if (!strcmp(argv[0], "diff")) { + else if (!strcmp(argv[0], "remaining")) + return cmd_rerere_remaining(argc, argv, prefix, repo); + else if (!strcmp(argv[0], "diff")) { struct strbuf buf = STRBUF_INIT; struct rerere_cache cache; From b24052eb3740b6623b7c68056b6a859de8d06554 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 29 Oct 2026 10:57:18 +0100 Subject: [PATCH 09/12] builtin/rerere: turn "diff" into a proper subcommand As explained in a preceding commit, a subsequent commit will introduce proper subcommands as supported by our parse-options subsystem. Refactor the "diff" subcommand to use parse-options as a preparatory step. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/rerere.c | 56 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/builtin/rerere.c b/builtin/rerere.c index 3f82bcbeb1..40bae5cb9c 100644 --- a/builtin/rerere.c +++ b/builtin/rerere.c @@ -72,6 +72,40 @@ static int cmd_rerere_clear(int argc, return 0; } +static int cmd_rerere_diff(int argc, + const char **argv, + const char *prefix, + struct repository *repo) +{ + struct option options[] = { + OPT_END(), + }; + struct string_list merge_rr = STRING_LIST_INIT_DUP; + struct strbuf buf = STRBUF_INIT; + struct rerere_cache cache; + + argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); + if (argc) + usage(_("too many arguments")); + + if (rerere_cache_setup(&cache, repo, &merge_rr, + RERERE_READONLY) < 0) + return 0; + + for (size_t i = 0; i < merge_rr.nr; i++) { + const char *path = merge_rr.items[i].string; + const struct rerere_id *id = merge_rr.items[i].util; + if (diff_two(rerere_path(repo, &buf, id, "preimage"), path, path, path)) + die(_("unable to generate diff for '%s'"), + rerere_path(repo, &buf, id, NULL)); + } + + string_list_clear(&merge_rr, 1); + rerere_cache_release(&cache); + strbuf_release(&buf); + return 0; +} + static int cmd_rerere_forget(int argc, const char **argv, const char *prefix, @@ -190,25 +224,9 @@ int cmd_rerere(int argc, return cmd_rerere_status(argc, argv, prefix, repo); else if (!strcmp(argv[0], "remaining")) return cmd_rerere_remaining(argc, argv, prefix, repo); - else if (!strcmp(argv[0], "diff")) { - struct strbuf buf = STRBUF_INIT; - struct rerere_cache cache; - - if (rerere_cache_setup(&cache, repo, &merge_rr, - RERERE_READONLY) < 0) - return 0; - for (size_t i = 0; i < merge_rr.nr; i++) { - const char *path = merge_rr.items[i].string; - const struct rerere_id *id = merge_rr.items[i].util; - if (diff_two(rerere_path(repo, &buf, id, "preimage"), - path, path, path)) - die(_("unable to generate diff for '%s'"), - rerere_path(repo, &buf, id, NULL)); - } - - rerere_cache_release(&cache); - strbuf_release(&buf); - } else + else if (!strcmp(argv[0], "diff")) + return cmd_rerere_diff(argc, argv, prefix, repo); + else usage_with_options(rerere_usage, options); string_list_clear(&merge_rr, 1); From 9359378e87981c4eaf5c8b3431eddc987ac7f75d Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 29 Oct 2026 10:57:19 +0100 Subject: [PATCH 10/12] builtin/rerere: turn "gc" into a proper subcommand As explained in a preceding commit, a subsequent commit will introduce proper subcommands as supported by our parse-options subsystem. Refactor the "gc" subcommand to use parse-options as a preparatory step. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/rerere.c | 43 ++++++++++++++++++++++++++++++------------- t/t4200-rerere.sh | 5 ----- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/builtin/rerere.c b/builtin/rerere.c index 40bae5cb9c..3162b074ec 100644 --- a/builtin/rerere.c +++ b/builtin/rerere.c @@ -128,6 +128,30 @@ static int cmd_rerere_forget(int argc, return ret; } +static int cmd_rerere_gc(int argc, + const char **argv, + const char *prefix, + struct repository *repo) +{ + int skip_locked = 0; + struct option options[] = { + OPT_HIDDEN_BOOL(0, "skip-locked", &skip_locked, + N_("skip gc while another process holds the lock")), + OPT_END(), + }; + struct string_list merge_rr = STRING_LIST_INIT_DUP; + + argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); + if (argc) + usage(_("too many arguments")); + + rerere_gc(repo, &merge_rr, + skip_locked ? RERERE_NOWAIT : 0); + + string_list_clear(&merge_rr, 1); + return 0; +} + static int cmd_rerere_remaining(int argc, const char **argv, const char *prefix, @@ -187,21 +211,16 @@ int cmd_rerere(int argc, const char *prefix, struct repository *repo) { - struct string_list merge_rr = STRING_LIST_INIT_DUP; - int autoupdate = -1, skip_locked = 0, flags = 0; + int autoupdate = -1, flags = 0; struct option options[] = { OPT_SET_INT(0, "rerere-autoupdate", &autoupdate, N_("register clean resolutions in index"), 1), - OPT_HIDDEN_BOOL(0, "skip-locked", &skip_locked, - N_("skip gc while another process holds the lock")), OPT_END(), }; - argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); - - if (skip_locked && (argc < 1 || strcmp(argv[0], "gc"))) - die(_("the option '%s' requires '%s'"), "--skip-locked", "gc"); + argc = parse_options(argc, argv, prefix, options, rerere_usage, + PARSE_OPT_KEEP_UNKNOWN_OPT); repo_config(repo, git_xmerge_config, NULL); @@ -215,11 +234,10 @@ int cmd_rerere(int argc, if (!strcmp(argv[0], "forget")) return cmd_rerere_forget(argc, argv, prefix, repo); - else if (!strcmp(argv[0], "clear")) { + else if (!strcmp(argv[0], "clear")) return cmd_rerere_clear(argc, argv, prefix, repo); - } else if (!strcmp(argv[0], "gc")) - rerere_gc(repo, &merge_rr, - skip_locked ? RERERE_NOWAIT : 0); + else if (!strcmp(argv[0], "gc")) + return cmd_rerere_gc(argc, argv, prefix, repo); else if (!strcmp(argv[0], "status")) return cmd_rerere_status(argc, argv, prefix, repo); else if (!strcmp(argv[0], "remaining")) @@ -229,6 +247,5 @@ int cmd_rerere(int argc, else usage_with_options(rerere_usage, options); - string_list_clear(&merge_rr, 1); return 0; } diff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh index ab412ecc6e..0d4cbf5114 100755 --- a/t/t4200-rerere.sh +++ b/t/t4200-rerere.sh @@ -263,11 +263,6 @@ test_expect_success 'gc --skip-locked prunes while MERGE_RR is not locked' ' test_path_is_missing $rr2/preimage ' -test_expect_success '--skip-locked is only accepted by gc' ' - test_must_fail git rerere --skip-locked clear 2>err && - test_grep "option .--skip-locked. requires .gc." err -' - test_expect_success 'a held lock is waited out within rerere.lockTimeout' ' git reset --hard && rm -rf $rr && From 6f0881eeee5cbb4ce330b958d866afaaf9fe5cc4 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 29 Oct 2026 10:57:20 +0100 Subject: [PATCH 11/12] builtin/rerere: use `OPT_SUBCOMMAND()` In git-rerere(1) we have our own custom logic to parse subcommands. In preceding commits we have refactored all of those subcommands so that they are ready to be used as `OPT_SUBCOMMAND()`s. Switch over to use these so that we get rid of our custom subcommand parsing. Unfortunately this means that we have a backwards-incompatible change because we no longer accept the "--[no-]rerere-autoupdate" flag for all subcommands. But this was arguably broken and misleading as we ignore this flag for any other subcommand aynway. So we cheat a bit and simply call it a bug that those flags were accepted in the first place. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/rerere.c | 82 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 27 deletions(-) diff --git a/builtin/rerere.c b/builtin/rerere.c index 3162b074ec..97c791e420 100644 --- a/builtin/rerere.c +++ b/builtin/rerere.c @@ -5,6 +5,7 @@ #include "rerere.h" #include "strbuf.h" #include "string-list.h" +#include "strvec.h" #include "xdiff/xdiff.h" #include "xdiff-interface.h" #include "pathspec.h" @@ -152,6 +153,31 @@ static int cmd_rerere_gc(int argc, return 0; } +static int cmd_rerere_perform(int argc, + const char **argv, + const char *prefix, + struct repository *repo) +{ + int autoupdate = -1; + struct option options[] = { + OPT_SET_INT(0, "rerere-autoupdate", &autoupdate, + N_("register clean resolutions in index"), 1), + OPT_END(), + }; + enum rerere_flags flags = 0; + + argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); + if (argc) + usage(_("too many arguments")); + + if (autoupdate == 1) + flags = RERERE_AUTOUPDATE; + if (autoupdate == 0) + flags = RERERE_NOAUTOUPDATE; + + return rerere(repo, flags); +} + static int cmd_rerere_remaining(int argc, const char **argv, const char *prefix, @@ -211,41 +237,43 @@ int cmd_rerere(int argc, const char *prefix, struct repository *repo) { - int autoupdate = -1, flags = 0; - + parse_opt_subcommand_fn *fn = NULL; struct option options[] = { - OPT_SET_INT(0, "rerere-autoupdate", &autoupdate, - N_("register clean resolutions in index"), 1), + OPT_SUBCOMMAND("clear", &fn, cmd_rerere_clear), + OPT_SUBCOMMAND("diff", &fn, cmd_rerere_diff), + OPT_SUBCOMMAND("forget", &fn, cmd_rerere_forget), + OPT_SUBCOMMAND("gc", &fn, cmd_rerere_gc), + OPT_SUBCOMMAND("remaining", &fn, cmd_rerere_remaining), + OPT_SUBCOMMAND("status", &fn, cmd_rerere_status), OPT_END(), }; argc = parse_options(argc, argv, prefix, options, rerere_usage, - PARSE_OPT_KEEP_UNKNOWN_OPT); - + PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_SUBCOMMAND_OPTIONAL); repo_config(repo, git_xmerge_config, NULL); - if (autoupdate == 1) - flags = RERERE_AUTOUPDATE; - if (autoupdate == 0) - flags = RERERE_NOAUTOUPDATE; + if (!fn) { + struct strvec args = STRVEC_INIT; + const char **args_copy; + int ret; - if (argc < 1) - return rerere(repo, flags); + strvec_push(&args, "perform"); + strvec_pushv(&args, argv); - if (!strcmp(argv[0], "forget")) - return cmd_rerere_forget(argc, argv, prefix, repo); - else if (!strcmp(argv[0], "clear")) - return cmd_rerere_clear(argc, argv, prefix, repo); - else if (!strcmp(argv[0], "gc")) - return cmd_rerere_gc(argc, argv, prefix, repo); - else if (!strcmp(argv[0], "status")) - return cmd_rerere_status(argc, argv, prefix, repo); - else if (!strcmp(argv[0], "remaining")) - return cmd_rerere_remaining(argc, argv, prefix, repo); - else if (!strcmp(argv[0], "diff")) - return cmd_rerere_diff(argc, argv, prefix, repo); - else - usage_with_options(rerere_usage, options); + /* + * We need to copy the array as it will be modified by + * `parse_options()`, and if so we leak the strings + * referenced by `args`. + */ + CALLOC_ARRAY(args_copy, args.nr); + COPY_ARRAY(args_copy, args.v, args.nr); - return 0; + ret = cmd_rerere_perform(args.nr, args_copy, prefix, repo); + + strvec_clear(&args); + free(args_copy); + return ret; + } + + return fn(argc, argv, prefix, repo); } From fba7572ebcff692d5b8b0d82de2beeb0aa7c8e91 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 29 Oct 2026 10:57:21 +0100 Subject: [PATCH 12/12] builtin/rerere: introduce proper usage strings for subcommands Adapt git-rerere(1) to have proper usage strings for each of the subcommands. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/git-rerere.adoc | 8 ++++- builtin/rerere.c | 61 +++++++++++++++++++++++++++++------ 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/Documentation/git-rerere.adoc b/Documentation/git-rerere.adoc index 4e6ab9a27c..e04e9ef3ef 100644 --- a/Documentation/git-rerere.adoc +++ b/Documentation/git-rerere.adoc @@ -8,7 +8,13 @@ git-rerere - Reuse recorded resolution of conflicted merges SYNOPSIS -------- [verse] -'git rerere' [clear | forget ... | diff | status | remaining | gc] +'git rerere' [--[no-]rerere-autoupdate] +'git rerere' clear +'git rerere' diff +'git rerere' forget ... +'git rerere' gc +'git rerere' remaining +'git rerere' status DESCRIPTION ----------- diff --git a/builtin/rerere.c b/builtin/rerere.c index 97c791e420..051e50838c 100644 --- a/builtin/rerere.c +++ b/builtin/rerere.c @@ -10,10 +10,13 @@ #include "xdiff-interface.h" #include "pathspec.h" -static const char * const rerere_usage[] = { - N_("git rerere [clear | forget ... | diff | status | remaining | gc]"), - NULL, -}; +#define RERERE_USAGE N_("git rerere [--[no-]rerere-autoupdate]") +#define RERERE_CLEAR_USAGE N_("git rerere clear") +#define RERERE_DIFF_USAGE N_("git rerere diff") +#define RERERE_FORGET_USAGE N_("git rerere forget ...") +#define RERERE_GC_USAGE N_("git rerere gc") +#define RERERE_REMAINING_USAGE N_("git rerere remaining") +#define RERERE_STATUS_USAGE N_("git rerere status") static int outf(void *dummy UNUSED, mmfile_t *ptr, int nbuf) { @@ -61,9 +64,13 @@ static int cmd_rerere_clear(int argc, struct option options[] = { OPT_END(), }; + const char * const rerere_clear_usage[] = { + RERERE_CLEAR_USAGE, + NULL, + }; struct string_list merge_rr = STRING_LIST_INIT_DUP; - argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); + argc = parse_options(argc, argv, prefix, options, rerere_clear_usage, 0); if (argc) usage(_("too many arguments")); @@ -81,11 +88,15 @@ static int cmd_rerere_diff(int argc, struct option options[] = { OPT_END(), }; + const char * const rerere_diff_usage[] = { + RERERE_DIFF_USAGE, + NULL, + }; struct string_list merge_rr = STRING_LIST_INIT_DUP; struct strbuf buf = STRBUF_INIT; struct rerere_cache cache; - argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); + argc = parse_options(argc, argv, prefix, options, rerere_diff_usage, 0); if (argc) usage(_("too many arguments")); @@ -115,10 +126,14 @@ static int cmd_rerere_forget(int argc, struct option options[] = { OPT_END(), }; + const char * const rerere_forget_usage[] = { + RERERE_FORGET_USAGE, + NULL, + }; struct pathspec pathspec; int ret; - argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); + argc = parse_options(argc, argv, prefix, options, rerere_forget_usage, 0); if (!argc) warning(_("'git rerere forget' without paths is deprecated")); parse_pathspec(&pathspec, 0, PATHSPEC_PREFER_CWD, prefix, argv); @@ -140,9 +155,13 @@ static int cmd_rerere_gc(int argc, N_("skip gc while another process holds the lock")), OPT_END(), }; + const char * const rerere_gc_usage[] = { + RERERE_GC_USAGE, + NULL, + }; struct string_list merge_rr = STRING_LIST_INIT_DUP; - argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); + argc = parse_options(argc, argv, prefix, options, rerere_gc_usage, 0); if (argc) usage(_("too many arguments")); @@ -164,6 +183,10 @@ static int cmd_rerere_perform(int argc, N_("register clean resolutions in index"), 1), OPT_END(), }; + const char * const rerere_usage[] = { + RERERE_USAGE, + NULL, + }; enum rerere_flags flags = 0; argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); @@ -186,9 +209,13 @@ static int cmd_rerere_remaining(int argc, struct option options[] = { OPT_END(), }; + const char * const rerere_remaining_usage[] = { + RERERE_REMAINING_USAGE, + NULL, + }; struct string_list merge_rr = STRING_LIST_INIT_DUP; - argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); + argc = parse_options(argc, argv, prefix, options, rerere_remaining_usage, 0); if (argc) usage(_("too many arguments")); @@ -214,10 +241,14 @@ static int cmd_rerere_status(int argc, struct option options[] = { OPT_END(), }; + const char * const rerere_status_usage[] = { + RERERE_STATUS_USAGE, + NULL, + }; struct string_list merge_rr = STRING_LIST_INIT_DUP; struct rerere_cache cache; - argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); + argc = parse_options(argc, argv, prefix, options, rerere_status_usage, 0); if (argc) usage(_("too many arguments")); @@ -247,6 +278,16 @@ int cmd_rerere(int argc, OPT_SUBCOMMAND("status", &fn, cmd_rerere_status), OPT_END(), }; + const char * const rerere_usage[] = { + RERERE_USAGE, + RERERE_CLEAR_USAGE, + RERERE_DIFF_USAGE, + RERERE_FORGET_USAGE, + RERERE_GC_USAGE, + RERERE_REMAINING_USAGE, + RERERE_STATUS_USAGE, + NULL, + }; argc = parse_options(argc, argv, prefix, options, rerere_usage, PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_SUBCOMMAND_OPTIONAL);