Merge branch 'ps/rerere-subcommands' into seen

The subcommand parsing of 'git rerere' has been converted to use
the standard 'OPT_SUBCOMMAND()' API, removing custom ad-hoc parsing
logic and bringing it in line with other Git commands.

* ps/rerere-subcommands:
  builtin/rerere: introduce proper usage strings for subcommands
  builtin/rerere: use `OPT_SUBCOMMAND()`
  builtin/rerere: turn "gc" into a proper subcommand
  builtin/rerere: turn "diff" into a proper subcommand
  builtin/rerere: turn "remaining" into a proper subcommand
  builtin/rerere: turn "status" into a proper subcommand
  builtin/rerere: turn "clear" into a proper subcommand
  builtin/rerere: turn "forget" into a proper subcommand
  builtin/rerere: stop passing flags to subcommands that ignore them
  builtin/rerere: drop `the_repository`
  rerere: convert flags into an enum
  rerere: rename `repo_rerere()`
This commit is contained in:
Junio C Hamano committed 2026-11-05 20:45:46 -08:00
commit cf5b1ecb2a
11 files changed
+293 -108

No files matched your search

+7 -1
View File
@@ -8,7 +8,13 @@ git-rerere - Reuse recorded resolution of conflicted merges
SYNOPSIS
--------
[verse]
'git rerere' [clear | forget <pathspec>... | diff | status | remaining | gc]
'git rerere' [--[no-]rerere-autoupdate]
'git rerere' clear
'git rerere' diff
'git rerere' forget <pathspec>...
'git rerere' gc
'git rerere' remaining
'git rerere' status
DESCRIPTION
-----------
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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);
+1 -1
View File
@@ -1957,7 +1957,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);
+1 -1
View File
@@ -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;
+246 -77
View File
@@ -1,5 +1,3 @@
#define USE_THE_REPOSITORY_VARIABLE
#include "builtin.h"
#include "config.h"
#include "gettext.h"
@@ -7,14 +5,18 @@
#include "rerere.h"
#include "strbuf.h"
#include "string-list.h"
#include "strvec.h"
#include "xdiff/xdiff.h"
#include "xdiff-interface.h"
#include "pathspec.h"
static const char * const rerere_usage[] = {
N_("git rerere [clear | forget <pathspec>... | 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 <pathspec>...")
#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)
{
@@ -54,98 +56,265 @@ static int diff_two(const char *file1, const char *label1,
return ret;
}
int cmd_rerere(int argc,
const char **argv,
const char *prefix,
struct repository *repo UNUSED)
static int cmd_rerere_clear(int argc,
const char **argv,
const char *prefix,
struct repository *repo)
{
struct option options[] = {
OPT_END(),
};
const char * const rerere_clear_usage[] = {
RERERE_CLEAR_USAGE,
NULL,
};
struct string_list merge_rr = STRING_LIST_INIT_DUP;
int autoupdate = -1, skip_locked = 0, flags = 0;
argc = parse_options(argc, argv, prefix, options, rerere_clear_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_diff(int argc,
const char **argv,
const char *prefix,
struct repository *repo)
{
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_diff_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,
struct repository *repo)
{
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_forget_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;
}
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(),
};
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_gc_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_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_HIDDEN_BOOL(0, "skip-locked", &skip_locked,
N_("skip gc while another process holds the lock")),
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);
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);
if (argc)
usage(_("too many arguments"));
if (autoupdate == 1)
flags = RERERE_AUTOUPDATE;
if (autoupdate == 0)
flags = RERERE_NOAUTOUPDATE;
if (argc < 1)
return repo_rerere(the_repository, flags);
return rerere(repo, flags);
}
if (!strcmp(argv[0], "forget")) {
struct pathspec pathspec;
int ret;
static int cmd_rerere_remaining(int argc,
const char **argv,
const char *prefix,
struct repository *repo)
{
struct option options[] = {
OPT_END(),
};
const char * const rerere_remaining_usage[] = {
RERERE_REMAINING_USAGE,
NULL,
};
struct string_list merge_rr = STRING_LIST_INIT_DUP;
if (argc < 2)
warning(_("'git rerere forget' without paths is deprecated"));
parse_pathspec(&pathspec, 0, PATHSPEC_PREFER_CWD,
prefix, argv + 1);
argc = parse_options(argc, argv, prefix, options, rerere_remaining_usage, 0);
if (argc)
usage(_("too many arguments"));
ret = rerere_forget(the_repository, &pathspec);
clear_pathspec(&pathspec);
return ret;
}
if (!strcmp(argv[0], "clear")) {
rerere_clear(the_repository, &merge_rr);
} else if (!strcmp(argv[0], "gc"))
rerere_gc(the_repository, &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,
flags | RERERE_READONLY) < 0)
return 0;
for (size_t i = 0; i < merge_rr.nr; i++)
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);
rerere_cache_release(&cache);
} else if (!strcmp(argv[0], "remaining")) {
rerere_remaining(the_repository, &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")) {
struct strbuf buf = STRBUF_INIT;
struct rerere_cache cache;
if (rerere_cache_setup(&cache, the_repository, &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"),
path, path, path))
die(_("unable to generate diff for '%s'"),
rerere_path(the_repository, &buf, id, NULL));
}
rerere_cache_release(&cache);
strbuf_release(&buf);
} else
usage_with_options(rerere_usage, options);
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,
struct repository *repo)
{
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_status_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,
struct repository *repo)
{
parse_opt_subcommand_fn *fn = NULL;
struct option options[] = {
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(),
};
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);
repo_config(repo, git_xmerge_config, NULL);
if (!fn) {
struct strvec args = STRVEC_INIT;
const char **args_copy;
int ret;
strvec_push(&args, "perform");
strvec_pushv(&args, argv);
/*
* 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);
ret = cmd_rerere_perform(args.nr, args_copy, prefix, repo);
strvec_clear(&args);
free(args_copy);
return ret;
}
return fn(argc, argv, prefix, repo);
}
+1 -1
View File
@@ -680,7 +680,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."));
+4 -8
View File
@@ -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,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, enum rerere_flags flags)
{
struct string_list merge_rr = STRING_LIST_INIT_DUP;
struct rerere_cache cache;
@@ -1341,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;
+28 -9
View File
@@ -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,13 +33,37 @@ 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);
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, enum rerere_flags flags);
/*
* Given the conflict ID and the name of a "file" used for replaying
* the recorded resolution (e.g. "preimage", "postimage"), return the
@@ -58,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
+2 -2
View File
@@ -2560,7 +2560,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;
}
@@ -4492,7 +4492,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
-5
View File
@@ -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 &&