for-each-ref: delay parsing of --sort=<atom> options

The for-each-ref family of commands invoke parsers immediately when
it sees each --sort=<atom> option, and die before even seeing the
other options on the command line when the <atom> is unrecognised.

Instead, accumulate them in a string list, and have them parsed into
a ref_sorting structure after the command line parsing is done.  As
a consequence, "git branch --sort=bogus -h" used to fail to give the
brief help, which arguably may have been a feature, now does so,
which is more consistent with how other options work.

The patch is smaller than the actual extent of the "damage" to the
codebase, thanks to the fact that the original code consistently
used OPT_REF_SORT() macro to handle command line options.  We only
needed to replace the variable used for the list, and implementation
of the callback function used in the macro.

The old rule was for the users of the API to:

 - Declare ref_sorting and ref_sorting_tail variables;

 - OPT_REF_SORT() macro will instantiate ref_sorting instance (which
   may barf and die) and append it to the tail;

 - Append to the tail each ref_sorting read from the configuration
   by parsing in the config callback (which may barf and die);

 - See if ref_sorting is null and use ref_sorting_default() instead.

Now the rule is not all that different but is simpler:

 - Declare ref_sorting_options string list.

 - OPT_REF_SORT() macro will append it to the string list;

 - Append to the string list the sort key read from the
   configuration;

 - call ref_sorting_options() to turn the string list to ref_sorting
   structure (which also deals with the default value).

As side effects, this change also cleans up a few issues:

 - 95be717c (parse_opt_ref_sorting: always use with NONEG flag,
   2019-03-20) muses that "git for-each-ref --no-sort" should simply
   clear the sort keys accumulated so far; it now does.

 - The implementation detail of "struct ref_sorting" and the helper
   function parse_ref_sorting() can now be private to the ref-filter
   API implementation.

 - If you set branch.sort to a bogus value, the any "git branch"
   invocation, not only the listing mode, would abort with the
   original code; now it doesn't

Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Junio C Hamano 2021-10-20 12:23:53 -07:00
parent 1a89796e4a
commit 98e7ab6d42
8 changed files with 96 additions and 53 deletions

View File

@ -77,12 +77,11 @@ define_list_config_array(color_branch_slots);
static int git_branch_config(const char *var, const char *value, void *cb) static int git_branch_config(const char *var, const char *value, void *cb)
{ {
const char *slot_name; const char *slot_name;
struct ref_sorting **sorting_tail = (struct ref_sorting **)cb;
if (!strcmp(var, "branch.sort")) { if (!strcmp(var, "branch.sort")) {
if (!value) if (!value)
return config_error_nonbool(var); return config_error_nonbool(var);
parse_ref_sorting(sorting_tail, value); string_list_append(cb, value);
return 0; return 0;
} }
@ -625,7 +624,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
enum branch_track track; enum branch_track track;
struct ref_filter filter; struct ref_filter filter;
int icase = 0; int icase = 0;
static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting; static struct ref_sorting *sorting;
struct string_list sorting_options = STRING_LIST_INIT_DUP;
struct ref_format format = REF_FORMAT_INIT; struct ref_format format = REF_FORMAT_INIT;
struct option options[] = { struct option options[] = {
@ -666,7 +666,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
OPT_MERGED(&filter, N_("print only branches that are merged")), OPT_MERGED(&filter, N_("print only branches that are merged")),
OPT_NO_MERGED(&filter, N_("print only branches that are not merged")), OPT_NO_MERGED(&filter, N_("print only branches that are not merged")),
OPT_COLUMN(0, "column", &colopts, N_("list branches in columns")), OPT_COLUMN(0, "column", &colopts, N_("list branches in columns")),
OPT_REF_SORT(sorting_tail), OPT_REF_SORT(&sorting_options),
OPT_CALLBACK(0, "points-at", &filter.points_at, N_("object"), OPT_CALLBACK(0, "points-at", &filter.points_at, N_("object"),
N_("print only branches of the object"), parse_opt_object_name), N_("print only branches of the object"), parse_opt_object_name),
OPT_BOOL('i', "ignore-case", &icase, N_("sorting and filtering are case insensitive")), OPT_BOOL('i', "ignore-case", &icase, N_("sorting and filtering are case insensitive")),
@ -683,7 +683,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
if (argc == 2 && !strcmp(argv[1], "-h")) if (argc == 2 && !strcmp(argv[1], "-h"))
usage_with_options(builtin_branch_usage, options); usage_with_options(builtin_branch_usage, options);
git_config(git_branch_config, sorting_tail); git_config(git_branch_config, &sorting_options);
track = git_branch_track; track = git_branch_track;
@ -749,8 +749,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
* local branches 'refs/heads/...' and finally remote-tracking * local branches 'refs/heads/...' and finally remote-tracking
* branches 'refs/remotes/...'. * branches 'refs/remotes/...'.
*/ */
if (!sorting) sorting = ref_sorting_options(&sorting_options);
sorting = ref_default_sorting();
ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase); ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase);
ref_sorting_set_sort_flags_all( ref_sorting_set_sort_flags_all(
sorting, REF_SORTING_DETACHED_HEAD_FIRST, 1); sorting, REF_SORTING_DETACHED_HEAD_FIRST, 1);

View File

@ -17,7 +17,8 @@ static char const * const for_each_ref_usage[] = {
int cmd_for_each_ref(int argc, const char **argv, const char *prefix) int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
{ {
int i; int i;
struct ref_sorting *sorting = NULL, **sorting_tail = &sorting; struct ref_sorting *sorting;
struct string_list sorting_options = STRING_LIST_INIT_DUP;
int maxcount = 0, icase = 0; int maxcount = 0, icase = 0;
struct ref_array array; struct ref_array array;
struct ref_filter filter; struct ref_filter filter;
@ -39,7 +40,7 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
OPT_INTEGER( 0 , "count", &maxcount, N_("show only <n> matched refs")), OPT_INTEGER( 0 , "count", &maxcount, N_("show only <n> matched refs")),
OPT_STRING( 0 , "format", &format.format, N_("format"), N_("format to use for the output")), OPT_STRING( 0 , "format", &format.format, N_("format"), N_("format to use for the output")),
OPT__COLOR(&format.use_color, N_("respect format colors")), OPT__COLOR(&format.use_color, N_("respect format colors")),
OPT_REF_SORT(sorting_tail), OPT_REF_SORT(&sorting_options),
OPT_CALLBACK(0, "points-at", &filter.points_at, OPT_CALLBACK(0, "points-at", &filter.points_at,
N_("object"), N_("print only refs which points at the given object"), N_("object"), N_("print only refs which points at the given object"),
parse_opt_object_name), parse_opt_object_name),
@ -70,8 +71,7 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
if (verify_ref_format(&format)) if (verify_ref_format(&format))
usage_with_options(for_each_ref_usage, opts); usage_with_options(for_each_ref_usage, opts);
if (!sorting) sorting = ref_sorting_options(&sorting_options);
sorting = ref_default_sorting();
ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase); ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase);
filter.ignore_case = icase; filter.ignore_case = icase;

View File

@ -54,7 +54,7 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix)
struct transport *transport; struct transport *transport;
const struct ref *ref; const struct ref *ref;
struct ref_array ref_array; struct ref_array ref_array;
static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting; struct string_list sorting_options = STRING_LIST_INIT_DUP;
struct option options[] = { struct option options[] = {
OPT__QUIET(&quiet, N_("do not print remote URL")), OPT__QUIET(&quiet, N_("do not print remote URL")),
@ -68,7 +68,7 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix)
OPT_BIT(0, "refs", &flags, N_("do not show peeled tags"), REF_NORMAL), OPT_BIT(0, "refs", &flags, N_("do not show peeled tags"), REF_NORMAL),
OPT_BOOL(0, "get-url", &get_url, OPT_BOOL(0, "get-url", &get_url,
N_("take url.<base>.insteadOf into account")), N_("take url.<base>.insteadOf into account")),
OPT_REF_SORT(sorting_tail), OPT_REF_SORT(&sorting_options),
OPT_SET_INT_F(0, "exit-code", &status, OPT_SET_INT_F(0, "exit-code", &status,
N_("exit with exit code 2 if no matching refs are found"), N_("exit with exit code 2 if no matching refs are found"),
2, PARSE_OPT_NOCOMPLETE), 2, PARSE_OPT_NOCOMPLETE),
@ -86,8 +86,6 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix)
packet_trace_identity("ls-remote"); packet_trace_identity("ls-remote");
UNLEAK(sorting);
if (argc > 1) { if (argc > 1) {
int i; int i;
CALLOC_ARRAY(pattern, argc); CALLOC_ARRAY(pattern, argc);
@ -139,8 +137,13 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix)
item->symref = xstrdup_or_null(ref->symref); item->symref = xstrdup_or_null(ref->symref);
} }
if (sorting) if (sorting_options.nr) {
struct ref_sorting *sorting;
sorting = ref_sorting_options(&sorting_options);
ref_array_sort(sorting, &ref_array); ref_array_sort(sorting, &ref_array);
ref_sorting_release(sorting);
}
for (i = 0; i < ref_array.nr; i++) { for (i = 0; i < ref_array.nr; i++) {
const struct ref_array_item *ref = ref_array.items[i]; const struct ref_array_item *ref = ref_array.items[i];

View File

@ -178,7 +178,6 @@ static const char tag_template_nocleanup[] =
static int git_tag_config(const char *var, const char *value, void *cb) static int git_tag_config(const char *var, const char *value, void *cb)
{ {
int status; int status;
struct ref_sorting **sorting_tail = (struct ref_sorting **)cb;
if (!strcmp(var, "tag.gpgsign")) { if (!strcmp(var, "tag.gpgsign")) {
config_sign_tag = git_config_bool(var, value); config_sign_tag = git_config_bool(var, value);
@ -188,7 +187,7 @@ static int git_tag_config(const char *var, const char *value, void *cb)
if (!strcmp(var, "tag.sort")) { if (!strcmp(var, "tag.sort")) {
if (!value) if (!value)
return config_error_nonbool(var); return config_error_nonbool(var);
parse_ref_sorting(sorting_tail, value); string_list_append(cb, value);
return 0; return 0;
} }
@ -436,7 +435,8 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
struct ref_transaction *transaction; struct ref_transaction *transaction;
struct strbuf err = STRBUF_INIT; struct strbuf err = STRBUF_INIT;
struct ref_filter filter; struct ref_filter filter;
static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting; struct ref_sorting *sorting;
struct string_list sorting_options = STRING_LIST_INIT_DUP;
struct ref_format format = REF_FORMAT_INIT; struct ref_format format = REF_FORMAT_INIT;
int icase = 0; int icase = 0;
int edit_flag = 0; int edit_flag = 0;
@ -470,7 +470,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
OPT_WITHOUT(&filter.no_commit, N_("print only tags that don't contain the commit")), OPT_WITHOUT(&filter.no_commit, N_("print only tags that don't contain the commit")),
OPT_MERGED(&filter, N_("print only tags that are merged")), OPT_MERGED(&filter, N_("print only tags that are merged")),
OPT_NO_MERGED(&filter, N_("print only tags that are not merged")), OPT_NO_MERGED(&filter, N_("print only tags that are not merged")),
OPT_REF_SORT(sorting_tail), OPT_REF_SORT(&sorting_options),
{ {
OPTION_CALLBACK, 0, "points-at", &filter.points_at, N_("object"), OPTION_CALLBACK, 0, "points-at", &filter.points_at, N_("object"),
N_("print only tags of the object"), PARSE_OPT_LASTARG_DEFAULT, N_("print only tags of the object"), PARSE_OPT_LASTARG_DEFAULT,
@ -486,7 +486,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
setup_ref_filter_porcelain_msg(); setup_ref_filter_porcelain_msg();
git_config(git_tag_config, sorting_tail); git_config(git_tag_config, &sorting_options);
memset(&opt, 0, sizeof(opt)); memset(&opt, 0, sizeof(opt));
memset(&filter, 0, sizeof(filter)); memset(&filter, 0, sizeof(filter));
@ -525,8 +525,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
die(_("--column and -n are incompatible")); die(_("--column and -n are incompatible"));
colopts = 0; colopts = 0;
} }
if (!sorting) sorting = ref_sorting_options(&sorting_options);
sorting = ref_default_sorting();
ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase); ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase);
filter.ignore_case = icase; filter.ignore_case = icase;
if (cmdmode == 'l') { if (cmdmode == 'l') {

View File

@ -2470,6 +2470,12 @@ static int memcasecmp(const void *vs1, const void *vs2, size_t n)
return 0; return 0;
} }
struct ref_sorting {
struct ref_sorting *next;
int atom; /* index into used_atom array (internal) */
enum ref_sorting_order sort_flags;
};
static int cmp_ref_sorting(struct ref_sorting *s, struct ref_array_item *a, struct ref_array_item *b) static int cmp_ref_sorting(struct ref_sorting *s, struct ref_array_item *a, struct ref_array_item *b)
{ {
struct atom_value *va, *vb; struct atom_value *va, *vb;
@ -2663,7 +2669,7 @@ static int parse_sorting_atom(const char *atom)
} }
/* If no sorting option is given, use refname to sort as default */ /* If no sorting option is given, use refname to sort as default */
struct ref_sorting *ref_default_sorting(void) static struct ref_sorting *ref_default_sorting(void)
{ {
static const char cstr_name[] = "refname"; static const char cstr_name[] = "refname";
@ -2674,7 +2680,7 @@ struct ref_sorting *ref_default_sorting(void)
return sorting; return sorting;
} }
void parse_ref_sorting(struct ref_sorting **sorting_tail, const char *arg) static void parse_ref_sorting(struct ref_sorting **sorting_tail, const char *arg)
{ {
struct ref_sorting *s; struct ref_sorting *s;
@ -2692,17 +2698,25 @@ void parse_ref_sorting(struct ref_sorting **sorting_tail, const char *arg)
s->atom = parse_sorting_atom(arg); s->atom = parse_sorting_atom(arg);
} }
int parse_opt_ref_sorting(const struct option *opt, const char *arg, int unset) struct ref_sorting *ref_sorting_options(struct string_list *options)
{ {
/* struct string_list_item *item;
* NEEDSWORK: We should probably clear the list in this case, but we've struct ref_sorting *sorting = NULL, **tail = &sorting;
* already munged the global used_atoms list, which would need to be
* undone.
*/
BUG_ON_OPT_NEG(unset);
parse_ref_sorting(opt->value, arg); if (!options->nr) {
return 0; sorting = ref_default_sorting();
} else {
for_each_string_list_item(item, options)
parse_ref_sorting(tail, item->string);
}
/*
* From here on, the ref_sorting list should be used to talk
* about the sort order used for the output. The caller
* should not touch the string form anymore.
*/
string_list_clear(options, 0);
return sorting;
} }
void ref_sorting_release(struct ref_sorting *sorting) void ref_sorting_release(struct ref_sorting *sorting)

View File

@ -23,16 +23,13 @@
#define FILTER_REFS_KIND_MASK (FILTER_REFS_ALL | FILTER_REFS_DETACHED_HEAD) #define FILTER_REFS_KIND_MASK (FILTER_REFS_ALL | FILTER_REFS_DETACHED_HEAD)
struct atom_value; struct atom_value;
struct ref_sorting;
struct ref_sorting { enum ref_sorting_order {
struct ref_sorting *next; REF_SORTING_REVERSE = 1<<0,
int atom; /* index into used_atom array (internal) */ REF_SORTING_ICASE = 1<<1,
enum { REF_SORTING_VERSION = 1<<2,
REF_SORTING_REVERSE = 1<<0, REF_SORTING_DETACHED_HEAD_FIRST = 1<<3,
REF_SORTING_ICASE = 1<<1,
REF_SORTING_VERSION = 1<<2,
REF_SORTING_DETACHED_HEAD_FIRST = 1<<3,
} sort_flags;
}; };
struct ref_array_item { struct ref_array_item {
@ -97,9 +94,8 @@ struct ref_format {
#define OPT_NO_MERGED(f, h) _OPT_MERGED_NO_MERGED("no-merged", f, h) #define OPT_NO_MERGED(f, h) _OPT_MERGED_NO_MERGED("no-merged", f, h)
#define OPT_REF_SORT(var) \ #define OPT_REF_SORT(var) \
OPT_CALLBACK_F(0, "sort", (var), \ OPT_STRING_LIST(0, "sort", (var), \
N_("key"), N_("field name to sort on"), \ N_("key"), N_("field name to sort on"))
PARSE_OPT_NONEG, parse_opt_ref_sorting)
/* /*
* API for filtering a set of refs. Based on the type of refs the user * API for filtering a set of refs. Based on the type of refs the user
@ -121,14 +117,10 @@ int format_ref_array_item(struct ref_array_item *info,
struct ref_format *format, struct ref_format *format,
struct strbuf *final_buf, struct strbuf *final_buf,
struct strbuf *error_buf); struct strbuf *error_buf);
/* Parse a single sort specifier and add it to the list */
void parse_ref_sorting(struct ref_sorting **sorting_tail, const char *atom);
/* Callback function for parsing the sort option */
int parse_opt_ref_sorting(const struct option *opt, const char *arg, int unset);
/* Default sort option based on refname */
struct ref_sorting *ref_default_sorting(void);
/* Release a "struct ref_sorting" */ /* Release a "struct ref_sorting" */
void ref_sorting_release(struct ref_sorting *); void ref_sorting_release(struct ref_sorting *);
/* Convert list of sort options into ref_sorting */
struct ref_sorting *ref_sorting_options(struct string_list *);
/* Function to parse --merged and --no-merged options */ /* Function to parse --merged and --no-merged options */
int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset); int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset);
/* Get the current HEAD's description */ /* Get the current HEAD's description */

View File

@ -1418,7 +1418,17 @@ test_expect_success 'invalid sort parameter in configuration' '
( (
cd sort && cd sort &&
git config branch.sort "v:notvalid" && git config branch.sort "v:notvalid" &&
test_must_fail git branch
# this works in the "listing" mode, so bad sort key
# is a dying offence.
test_must_fail git branch &&
# these do not need to use sorting, and should all
# succeed
git branch newone main &&
git branch -c newone newerone &&
git branch -m newone newestone &&
git branch -d newerone newestone
) )
' '

View File

@ -419,6 +419,11 @@ test_expect_success 'Verify descending sort' '
test_cmp expected actual test_cmp expected actual
' '
test_expect_success 'Give help even with invalid sort atoms' '
test_expect_code 129 git for-each-ref --sort=bogus -h >actual 2>&1 &&
grep "^usage: git for-each-ref" actual
'
cat >expected <<\EOF cat >expected <<\EOF
refs/tags/testtag refs/tags/testtag
refs/tags/testtag-2 refs/tags/testtag-2
@ -1019,6 +1024,27 @@ test_expect_success 'equivalent sorts fall back on refname' '
test_cmp expected actual test_cmp expected actual
' '
test_expect_success '--no-sort cancels the previous sort keys' '
cat >expected <<-\EOF &&
100000 <user1@example.com> refs/tags/multi-ref1-100000-user1
100000 <user2@example.com> refs/tags/multi-ref1-100000-user2
100000 <user1@example.com> refs/tags/multi-ref2-100000-user1
100000 <user2@example.com> refs/tags/multi-ref2-100000-user2
200000 <user1@example.com> refs/tags/multi-ref1-200000-user1
200000 <user2@example.com> refs/tags/multi-ref1-200000-user2
200000 <user1@example.com> refs/tags/multi-ref2-200000-user1
200000 <user2@example.com> refs/tags/multi-ref2-200000-user2
EOF
git for-each-ref \
--format="%(taggerdate:unix) %(taggeremail) %(refname)" \
--sort=-refname \
--sort=taggeremail \
--no-sort \
--sort=taggerdate \
"refs/tags/multi-*" >actual &&
test_cmp expected actual
'
test_expect_success 'do not dereference NULL upon %(HEAD) on unborn branch' ' test_expect_success 'do not dereference NULL upon %(HEAD) on unborn branch' '
test_when_finished "git checkout main" && test_when_finished "git checkout main" &&
git for-each-ref --format="%(HEAD) %(refname:short)" refs/heads/ >actual && git for-each-ref --format="%(HEAD) %(refname:short)" refs/heads/ >actual &&