Merge branch 'vd/for-each-ref-unsorted-optimization'

"git for-each-ref --no-sort" still sorted the refs alphabetically
which paid non-trivial cost.  It has been redefined to show output
in an unspecified order, to allow certain optimizations to take
advantage of.

* vd/for-each-ref-unsorted-optimization:
  t/perf: add perf tests for for-each-ref
  ref-filter.c: use peeled tag for '*' format fields
  for-each-ref: clean up documentation of --format
  ref-filter.c: filter & format refs in the same callback
  ref-filter.c: refactor to create common helper functions
  ref-filter.c: rename 'ref_filter_handler()' to 'filter_one()'
  ref-filter.h: add functions for filter/format & format-only
  ref-filter.h: move contains caches into filter
  ref-filter.h: add max_count and omit_empty to ref_format
  ref-filter.c: really don't sort when using --no-sort
maint
Junio C Hamano 2023-12-09 16:37:50 -08:00
commit 98d0a1f93e
12 changed files with 523 additions and 180 deletions

View File

@ -51,17 +51,14 @@ OPTIONS
key.

--format=<format>::
A string that interpolates `%(fieldname)` from a ref being shown
and the object it points at. If `fieldname`
is prefixed with an asterisk (`*`) and the ref points
at a tag object, use the value for the field in the object
which the tag object refers to (instead of the field in the tag object).
When unspecified, `<format>` defaults to
`%(objectname) SPC %(objecttype) TAB %(refname)`.
It also interpolates `%%` to `%`, and `%xx` where `xx`
are hex digits interpolates to character with hex code
`xx`; for example `%00` interpolates to `\0` (NUL),
`%09` to `\t` (TAB) and `%0a` to `\n` (LF).
A string that interpolates `%(fieldname)` from a ref being shown and
the object it points at. In addition, the string literal `%%`
renders as `%` and `%xx` - where `xx` are hex digits - renders as
the character with hex code `xx`. For example, `%00` interpolates to
`\0` (NUL), `%09` to `\t` (TAB), and `%0a` to `\n` (LF).
+
When unspecified, `<format>` defaults to `%(objectname) SPC %(objecttype)
TAB %(refname)`.

--color[=<when>]::
Respect any colors specified in the `--format` option. The
@ -298,6 +295,10 @@ fields will correspond to the appropriate date or name-email-date tuple
from the `committer` or `tagger` fields depending on the object type.
These are intended for working on a mix of annotated and lightweight tags.

For tag objects, a `fieldname` prefixed with an asterisk (`*`) expands to
the `fieldname` value of the peeled object, rather than that of the tag
object itself.

Fields that have name-email-date tuple as its value (`author`,
`committer`, and `tagger`) can be suffixed with `name`, `email`,
and `date` to extract the named component. For email fields (`authoremail`,

View File

@ -45,7 +45,6 @@ static const char *head;
static struct object_id head_oid;
static int recurse_submodules = 0;
static int submodule_propagate_branches = 0;
static int omit_empty = 0;

static int branch_use_color = -1;
static char branch_colors[][COLOR_MAXLEN] = {
@ -438,8 +437,6 @@ static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin
{
int i;
struct ref_array array;
struct strbuf out = STRBUF_INIT;
struct strbuf err = STRBUF_INIT;
int maxwidth = 0;
const char *remote_prefix = "";
char *to_free = NULL;
@ -469,24 +466,27 @@ static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin
filter_ahead_behind(the_repository, format, &array);
ref_array_sort(sorting, &array);

for (i = 0; i < array.nr; i++) {
strbuf_reset(&err);
strbuf_reset(&out);
if (format_ref_array_item(array.items[i], format, &out, &err))
die("%s", err.buf);
if (column_active(colopts)) {
assert(!filter->verbose && "--column and --verbose are incompatible");
/* format to a string_list to let print_columns() do its job */
if (column_active(colopts)) {
struct strbuf out = STRBUF_INIT, err = STRBUF_INIT;

assert(!filter->verbose && "--column and --verbose are incompatible");

for (i = 0; i < array.nr; i++) {
strbuf_reset(&err);
strbuf_reset(&out);
if (format_ref_array_item(array.items[i], format, &out, &err))
die("%s", err.buf);

/* format to a string_list to let print_columns() do its job */
string_list_append(output, out.buf);
} else {
fwrite(out.buf, 1, out.len, stdout);
if (out.len || !omit_empty)
putchar('\n');
}

strbuf_release(&err);
strbuf_release(&out);
} else {
print_formatted_ref_array(&array, format);
}

strbuf_release(&err);
strbuf_release(&out);
ref_array_clear(&array);
free(to_free);
}
@ -737,7 +737,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
OPT_BIT('D', NULL, &delete, N_("delete branch (even if not merged)"), 2),
OPT_BIT('m', "move", &rename, N_("move/rename a branch and its reflog"), 1),
OPT_BIT('M', NULL, &rename, N_("move/rename a branch, even if target exists"), 2),
OPT_BOOL(0, "omit-empty", &omit_empty,
OPT_BOOL(0, "omit-empty", &format.array_opts.omit_empty,
N_("do not output a newline after empty formatted refs")),
OPT_BIT('c', "copy", &copy, N_("copy a branch and its reflog"), 1),
OPT_BIT('C', NULL, &copy, N_("copy a branch, even if target exists"), 2),
@ -767,7 +767,13 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
if (argc == 2 && !strcmp(argv[1], "-h"))
usage_with_options(builtin_branch_usage, options);

/*
* Try to set sort keys from config. If config does not set any,
* fall back on default (refname) sorting.
*/
git_config(git_branch_config, &sorting_options);
if (!sorting_options.nr)
string_list_append(&sorting_options, "refname");

track = git_branch_track;


View File

@ -19,15 +19,11 @@ static char const * const for_each_ref_usage[] = {

int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
{
int i;
struct ref_sorting *sorting;
struct string_list sorting_options = STRING_LIST_INIT_DUP;
int maxcount = 0, icase = 0, omit_empty = 0;
struct ref_array array;
int icase = 0;
struct ref_filter filter = REF_FILTER_INIT;
struct ref_format format = REF_FORMAT_INIT;
struct strbuf output = STRBUF_INIT;
struct strbuf err = STRBUF_INIT;
int from_stdin = 0;
struct strvec vec = STRVEC_INIT;

@ -40,11 +36,11 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
N_("quote placeholders suitably for python"), QUOTE_PYTHON),
OPT_BIT(0 , "tcl", &format.quote_style,
N_("quote placeholders suitably for Tcl"), QUOTE_TCL),
OPT_BOOL(0, "omit-empty", &omit_empty,
OPT_BOOL(0, "omit-empty", &format.array_opts.omit_empty,
N_("do not output a newline after empty formatted refs")),

OPT_GROUP(""),
OPT_INTEGER( 0 , "count", &maxcount, N_("show only <n> matched refs")),
OPT_INTEGER( 0 , "count", &format.array_opts.max_count, N_("show only <n> matched refs")),
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_REF_FILTER_EXCLUDE(&filter),
@ -61,15 +57,16 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
OPT_END(),
};

memset(&array, 0, sizeof(array));

format.format = "%(objectname) %(objecttype)\t%(refname)";

git_config(git_default_config, NULL);

/* Set default (refname) sorting */
string_list_append(&sorting_options, "refname");

parse_options(argc, argv, prefix, opts, for_each_ref_usage, 0);
if (maxcount < 0) {
error("invalid --count argument: `%d'", maxcount);
if (format.array_opts.max_count < 0) {
error("invalid --count argument: `%d'", format.array_opts.max_count);
usage_with_options(for_each_ref_usage, opts);
}
if (HAS_MULTI_BITS(format.quote_style)) {
@ -101,26 +98,8 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
}

filter.match_as_path = 1;
filter_refs(&array, &filter, FILTER_REFS_ALL);
filter_ahead_behind(the_repository, &format, &array);
filter_and_format_refs(&filter, FILTER_REFS_ALL, sorting, &format);

ref_array_sort(sorting, &array);

if (!maxcount || array.nr < maxcount)
maxcount = array.nr;
for (i = 0; i < maxcount; i++) {
strbuf_reset(&err);
strbuf_reset(&output);
if (format_ref_array_item(array.items[i], &format, &output, &err))
die("%s", err.buf);
fwrite(output.buf, 1, output.len, stdout);
if (output.len || !omit_empty)
putchar('\n');
}

strbuf_release(&err);
strbuf_release(&output);
ref_array_clear(&array);
ref_filter_clear(&filter);
ref_sorting_release(sorting);
strvec_clear(&vec);

View File

@ -58,6 +58,7 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix)
struct transport *transport;
const struct ref *ref;
struct ref_array ref_array;
struct ref_sorting *sorting;
struct string_list sorting_options = STRING_LIST_INIT_DUP;

struct option options[] = {
@ -141,13 +142,8 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix)
item->symref = xstrdup_or_null(ref->symref);
}

if (sorting_options.nr) {
struct ref_sorting *sorting;

sorting = ref_sorting_options(&sorting_options);
ref_array_sort(sorting, &ref_array);
ref_sorting_release(sorting);
}
sorting = ref_sorting_options(&sorting_options);
ref_array_sort(sorting, &ref_array);

for (i = 0; i < ref_array.nr; i++) {
const struct ref_array_item *ref = ref_array.items[i];
@ -157,6 +153,7 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix)
status = 0; /* we found something */
}

ref_sorting_release(sorting);
ref_array_clear(&ref_array);
if (transport_disconnect(transport))
status = 1;

View File

@ -44,18 +44,11 @@ static const char * const git_tag_usage[] = {
static unsigned int colopts;
static int force_sign_annotate;
static int config_sign_tag = -1; /* unspecified */
static int omit_empty = 0;

static int list_tags(struct ref_filter *filter, struct ref_sorting *sorting,
struct ref_format *format)
{
struct ref_array array;
struct strbuf output = STRBUF_INIT;
struct strbuf err = STRBUF_INIT;
char *to_free = NULL;
int i;

memset(&array, 0, sizeof(array));

if (filter->lines == -1)
filter->lines = 0;
@ -73,23 +66,8 @@ static int list_tags(struct ref_filter *filter, struct ref_sorting *sorting,
if (verify_ref_format(format))
die(_("unable to parse format string"));
filter->with_commit_tag_algo = 1;
filter_refs(&array, filter, FILTER_REFS_TAGS);
filter_ahead_behind(the_repository, format, &array);
ref_array_sort(sorting, &array);
filter_and_format_refs(filter, FILTER_REFS_TAGS, sorting, format);

for (i = 0; i < array.nr; i++) {
strbuf_reset(&output);
strbuf_reset(&err);
if (format_ref_array_item(array.items[i], format, &output, &err))
die("%s", err.buf);
fwrite(output.buf, 1, output.len, stdout);
if (output.len || !omit_empty)
putchar('\n');
}

strbuf_release(&err);
strbuf_release(&output);
ref_array_clear(&array);
free(to_free);

return 0;
@ -481,7 +459,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_MERGED(&filter, N_("print only tags that are merged")),
OPT_NO_MERGED(&filter, N_("print only tags that are not merged")),
OPT_BOOL(0, "omit-empty", &omit_empty,
OPT_BOOL(0, "omit-empty", &format.array_opts.omit_empty,
N_("do not output a newline after empty formatted refs")),
OPT_REF_SORT(&sorting_options),
{
@ -501,7 +479,13 @@ int cmd_tag(int argc, const char **argv, const char *prefix)

setup_ref_filter_porcelain_msg();

/*
* Try to set sort keys from config. If config does not set any,
* fall back on default (refname) sorting.
*/
git_config(git_tag_config, &sorting_options);
if (!sorting_options.nr)
string_list_append(&sorting_options, "refname");

memset(&opt, 0, sizeof(opt));
filter.lines = -1;

View File

@ -2508,17 +2508,12 @@ static int populate_value(struct ref_array_item *ref, struct strbuf *err)
return 0;

/*
* If it is a tag object, see if we use a value that derefs
* the object, and if we do grab the object it refers to.
* If it is a tag object, see if we use the peeled value. If we do,
* grab the peeled OID.
*/
oi_deref.oid = *get_tagged_oid((struct tag *)obj);
if (need_tagged && peel_iterated_oid(&obj->oid, &oi_deref.oid))
die("bad tag");

/*
* NEEDSWORK: This derefs tag only once, which
* is good to deal with chains of trust, but
* is not consistent with what deref_tag() does
* which peels the onion to the core.
*/
return get_object(ref, 1, &obj, &oi_deref, err);
}

@ -2716,15 +2711,18 @@ static struct ref_array_item *new_ref_array_item(const char *refname,
return ref;
}

static void ref_array_append(struct ref_array *array, struct ref_array_item *ref)
{
ALLOC_GROW(array->items, array->nr + 1, array->alloc);
array->items[array->nr++] = ref;
}

struct ref_array_item *ref_array_push(struct ref_array *array,
const char *refname,
const struct object_id *oid)
{
struct ref_array_item *ref = new_ref_array_item(refname, oid);

ALLOC_GROW(array->items, array->nr + 1, array->alloc);
array->items[array->nr++] = ref;

ref_array_append(array, ref);
return ref;
}

@ -2761,48 +2759,36 @@ static int filter_ref_kind(struct ref_filter *filter, const char *refname)
return ref_kind_from_refname(refname);
}

struct ref_filter_cbdata {
struct ref_array *array;
struct ref_filter *filter;
struct contains_cache contains_cache;
struct contains_cache no_contains_cache;
};

/*
* A call-back given to for_each_ref(). Filter refs and keep them for
* later object processing.
*/
static int ref_filter_handler(const char *refname, const struct object_id *oid, int flag, void *cb_data)
static struct ref_array_item *apply_ref_filter(const char *refname, const struct object_id *oid,
int flag, struct ref_filter *filter)
{
struct ref_filter_cbdata *ref_cbdata = cb_data;
struct ref_filter *filter = ref_cbdata->filter;
struct ref_array_item *ref;
struct commit *commit = NULL;
unsigned int kind;

if (flag & REF_BAD_NAME) {
warning(_("ignoring ref with broken name %s"), refname);
return 0;
return NULL;
}

if (flag & REF_ISBROKEN) {
warning(_("ignoring broken ref %s"), refname);
return 0;
return NULL;
}

/* Obtain the current ref kind from filter_ref_kind() and ignore unwanted refs. */
kind = filter_ref_kind(filter, refname);
if (!(kind & filter->kind))
return 0;
return NULL;

if (!filter_pattern_match(filter, refname))
return 0;
return NULL;

if (filter_exclude_match(filter, refname))
return 0;
return NULL;

if (filter->points_at.nr && !match_points_at(&filter->points_at, oid, refname))
return 0;
return NULL;

/*
* A merge filter is applied on refs pointing to commits. Hence
@ -2813,15 +2799,15 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid,
filter->with_commit || filter->no_commit || filter->verbose) {
commit = lookup_commit_reference_gently(the_repository, oid, 1);
if (!commit)
return 0;
return NULL;
/* We perform the filtering for the '--contains' option... */
if (filter->with_commit &&
!commit_contains(filter, commit, filter->with_commit, &ref_cbdata->contains_cache))
return 0;
!commit_contains(filter, commit, filter->with_commit, &filter->internal.contains_cache))
return NULL;
/* ...or for the `--no-contains' option */
if (filter->no_commit &&
commit_contains(filter, commit, filter->no_commit, &ref_cbdata->no_contains_cache))
return 0;
commit_contains(filter, commit, filter->no_commit, &filter->internal.no_contains_cache))
return NULL;
}

/*
@ -2829,11 +2815,32 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid,
* to do its job and the resulting list may yet to be pruned
* by maxcount logic.
*/
ref = ref_array_push(ref_cbdata->array, refname, oid);
ref = new_ref_array_item(refname, oid);
ref->commit = commit;
ref->flag = flag;
ref->kind = kind;

return ref;
}

struct ref_filter_cbdata {
struct ref_array *array;
struct ref_filter *filter;
};

/*
* A call-back given to for_each_ref(). Filter refs and keep them for
* later object processing.
*/
static int filter_one(const char *refname, const struct object_id *oid, int flag, void *cb_data)
{
struct ref_filter_cbdata *ref_cbdata = cb_data;
struct ref_array_item *ref;

ref = apply_ref_filter(refname, oid, flag, ref_cbdata->filter);
if (ref)
ref_array_append(ref_cbdata->array, ref);

return 0;
}

@ -2851,6 +2858,49 @@ static void free_array_item(struct ref_array_item *item)
free(item);
}

struct ref_filter_and_format_cbdata {
struct ref_filter *filter;
struct ref_format *format;

struct ref_filter_and_format_internal {
int count;
} internal;
};

static int filter_and_format_one(const char *refname, const struct object_id *oid, int flag, void *cb_data)
{
struct ref_filter_and_format_cbdata *ref_cbdata = cb_data;
struct ref_array_item *ref;
struct strbuf output = STRBUF_INIT, err = STRBUF_INIT;

ref = apply_ref_filter(refname, oid, flag, ref_cbdata->filter);
if (!ref)
return 0;

if (format_ref_array_item(ref, ref_cbdata->format, &output, &err))
die("%s", err.buf);

if (output.len || !ref_cbdata->format->array_opts.omit_empty) {
fwrite(output.buf, 1, output.len, stdout);
putchar('\n');
}

strbuf_release(&output);
strbuf_release(&err);
free_array_item(ref);

/*
* Increment the running count of refs that match the filter. If
* max_count is set and we've reached the max, stop the ref
* iteration by returning a nonzero value.
*/
if (ref_cbdata->format->array_opts.max_count &&
++ref_cbdata->internal.count >= ref_cbdata->format->array_opts.max_count)
return 1;

return 0;
}

/* Free all memory allocated for ref_array */
void ref_array_clear(struct ref_array *array)
{
@ -2969,6 +3019,43 @@ void filter_ahead_behind(struct repository *r,
free(commits);
}

static int do_filter_refs(struct ref_filter *filter, unsigned int type, each_ref_fn fn, void *cb_data)
{
int ret = 0;

filter->kind = type & FILTER_REFS_KIND_MASK;

init_contains_cache(&filter->internal.contains_cache);
init_contains_cache(&filter->internal.no_contains_cache);

/* Simple per-ref filtering */
if (!filter->kind)
die("filter_refs: invalid type");
else {
/*
* For common cases where we need only branches or remotes or tags,
* we only iterate through those refs. If a mix of refs is needed,
* we iterate over all refs and filter out required refs with the help
* of filter_ref_kind().
*/
if (filter->kind == FILTER_REFS_BRANCHES)
ret = for_each_fullref_in("refs/heads/", fn, cb_data);
else if (filter->kind == FILTER_REFS_REMOTES)
ret = for_each_fullref_in("refs/remotes/", fn, cb_data);
else if (filter->kind == FILTER_REFS_TAGS)
ret = for_each_fullref_in("refs/tags/", fn, cb_data);
else if (filter->kind & FILTER_REFS_ALL)
ret = for_each_fullref_in_pattern(filter, fn, cb_data);
if (!ret && (filter->kind & FILTER_REFS_DETACHED_HEAD))
head_ref(fn, cb_data);
}

clear_contains_cache(&filter->internal.contains_cache);
clear_contains_cache(&filter->internal.no_contains_cache);

return ret;
}

/*
* API for filtering a set of refs. Based on the type of refs the user
* has requested, we iterate through those refs and apply filters
@ -2984,38 +3071,10 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int
ref_cbdata.array = array;
ref_cbdata.filter = filter;

filter->kind = type & FILTER_REFS_KIND_MASK;

save_commit_buffer_orig = save_commit_buffer;
save_commit_buffer = 0;

init_contains_cache(&ref_cbdata.contains_cache);
init_contains_cache(&ref_cbdata.no_contains_cache);

/* Simple per-ref filtering */
if (!filter->kind)
die("filter_refs: invalid type");
else {
/*
* For common cases where we need only branches or remotes or tags,
* we only iterate through those refs. If a mix of refs is needed,
* we iterate over all refs and filter out required refs with the help
* of filter_ref_kind().
*/
if (filter->kind == FILTER_REFS_BRANCHES)
ret = for_each_fullref_in("refs/heads/", ref_filter_handler, &ref_cbdata);
else if (filter->kind == FILTER_REFS_REMOTES)
ret = for_each_fullref_in("refs/remotes/", ref_filter_handler, &ref_cbdata);
else if (filter->kind == FILTER_REFS_TAGS)
ret = for_each_fullref_in("refs/tags/", ref_filter_handler, &ref_cbdata);
else if (filter->kind & FILTER_REFS_ALL)
ret = for_each_fullref_in_pattern(filter, ref_filter_handler, &ref_cbdata);
if (!ret && (filter->kind & FILTER_REFS_DETACHED_HEAD))
head_ref(ref_filter_handler, &ref_cbdata);
}

clear_contains_cache(&ref_cbdata.contains_cache);
clear_contains_cache(&ref_cbdata.no_contains_cache);
ret = do_filter_refs(filter, type, filter_one, &ref_cbdata);

/* Filters that need revision walking */
reach_filter(array, &filter->reachable_from, INCLUDE_REACHED);
@ -3025,6 +3084,51 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int
return ret;
}

static inline int can_do_iterative_format(struct ref_filter *filter,
struct ref_sorting *sorting,
struct ref_format *format)
{
/*
* Filtering & formatting results within a single ref iteration
* callback is not compatible with options that require
* post-processing a filtered ref_array. These include:
* - filtering on reachability
* - sorting the filtered results
* - including ahead-behind information in the formatted output
*/
return !(filter->reachable_from ||
filter->unreachable_from ||
sorting ||
format->bases.nr);
}

void filter_and_format_refs(struct ref_filter *filter, unsigned int type,
struct ref_sorting *sorting,
struct ref_format *format)
{
if (can_do_iterative_format(filter, sorting, format)) {
int save_commit_buffer_orig;
struct ref_filter_and_format_cbdata ref_cbdata = {
.filter = filter,
.format = format,
};

save_commit_buffer_orig = save_commit_buffer;
save_commit_buffer = 0;

do_filter_refs(filter, type, filter_and_format_one, &ref_cbdata);

save_commit_buffer = save_commit_buffer_orig;
} else {
struct ref_array array = { 0 };
filter_refs(&array, filter, type);
filter_ahead_behind(the_repository, format, &array);
ref_array_sort(sorting, &array);
print_formatted_ref_array(&array, format);
ref_array_clear(&array);
}
}

static int compare_detached_head(struct ref_array_item *a, struct ref_array_item *b)
{
if (!(a->kind ^ b->kind))
@ -3142,7 +3246,8 @@ void ref_sorting_set_sort_flags_all(struct ref_sorting *sorting,

void ref_array_sort(struct ref_sorting *sorting, struct ref_array *array)
{
QSORT_S(array->items, array->nr, compare_refs, sorting);
if (sorting)
QSORT_S(array->items, array->nr, compare_refs, sorting);
}

static void append_literal(const char *cp, const char *ep, struct ref_formatting_state *state)
@ -3213,6 +3318,29 @@ int format_ref_array_item(struct ref_array_item *info,
return 0;
}

void print_formatted_ref_array(struct ref_array *array, struct ref_format *format)
{
int total;
struct strbuf output = STRBUF_INIT, err = STRBUF_INIT;

total = format->array_opts.max_count;
if (!total || array->nr < total)
total = array->nr;
for (int i = 0; i < total; i++) {
strbuf_reset(&err);
strbuf_reset(&output);
if (format_ref_array_item(array->items[i], format, &output, &err))
die("%s", err.buf);
if (output.len || !format->array_opts.omit_empty) {
fwrite(output.buf, 1, output.len, stdout);
putchar('\n');
}
}

strbuf_release(&err);
strbuf_release(&output);
}

void pretty_print_ref(const char *name, const struct object_id *oid,
struct ref_format *format)
{
@ -3248,18 +3376,6 @@ static int parse_sorting_atom(const char *atom)
return res;
}

/* If no sorting option is given, use refname to sort as default */
static struct ref_sorting *ref_default_sorting(void)
{
static const char cstr_name[] = "refname";

struct ref_sorting *sorting = xcalloc(1, sizeof(*sorting));

sorting->next = NULL;
sorting->atom = parse_sorting_atom(cstr_name);
return sorting;
}

static void parse_ref_sorting(struct ref_sorting **sorting_tail, const char *arg)
{
struct ref_sorting *s;
@ -3283,9 +3399,7 @@ struct ref_sorting *ref_sorting_options(struct string_list *options)
struct string_list_item *item;
struct ref_sorting *sorting = NULL, **tail = &sorting;

if (!options->nr) {
sorting = ref_default_sorting();
} else {
if (options->nr) {
for_each_string_list_item(item, options)
parse_ref_sorting(tail, item->string);
}

View File

@ -7,6 +7,7 @@
#include "commit.h"
#include "string-list.h"
#include "strvec.h"
#include "commit-reach.h"

/* Quoting styles */
#define QUOTE_NONE 0
@ -75,6 +76,11 @@ struct ref_filter {
lines;
int abbrev,
verbose;

struct {
struct contains_cache contains_cache;
struct contains_cache no_contains_cache;
} internal;
};

struct ref_format {
@ -92,6 +98,11 @@ struct ref_format {

/* List of bases for ahead-behind counts. */
struct string_list bases;

struct {
int max_count;
int omit_empty;
} array_opts;
};

#define REF_FILTER_INIT { \
@ -126,6 +137,14 @@ struct ref_format {
* filtered refs in the ref_array structure.
*/
int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int type);
/*
* Filter refs using the given ref_filter and type, sort the contents
* according to the given ref_sorting, format the filtered refs with the
* given ref_format, and print them to stdout.
*/
void filter_and_format_refs(struct ref_filter *filter, unsigned int type,
struct ref_sorting *sorting,
struct ref_format *format);
/* Clear all memory allocated to ref_array */
void ref_array_clear(struct ref_array *array);
/* Used to verify if the given format is correct and to parse out the used atoms */
@ -150,6 +169,12 @@ char *get_head_description(void);
/* Set up translated strings in the output. */
void setup_ref_filter_porcelain_msg(void);

/*
* Print up to maxcount ref_array elements to stdout using the given
* ref_format.
*/
void print_formatted_ref_array(struct ref_array *array, struct ref_format *format);

/*
* Print a single ref, outside of any ref-filter. Note that the
* name must be a fully qualified refname.

87
t/perf/p6300-for-each-ref.sh Executable file
View File

@ -0,0 +1,87 @@
#!/bin/sh

test_description='performance of for-each-ref'
. ./perf-lib.sh

test_perf_fresh_repo

ref_count_per_type=10000
test_iteration_count=10

test_expect_success "setup" '
test_commit_bulk $(( 1 + $ref_count_per_type )) &&

# Create refs
test_seq $ref_count_per_type |
sed "s,.*,update refs/heads/branch_& HEAD~&\nupdate refs/custom/special_& HEAD~&," |
git update-ref --stdin &&

# Create annotated tags
for i in $(test_seq $ref_count_per_type)
do
# Base tags
echo "tag tag_$i" &&
echo "mark :$i" &&
echo "from HEAD~$i" &&
printf "tagger %s <%s> %s\n" \
"$GIT_COMMITTER_NAME" \
"$GIT_COMMITTER_EMAIL" \
"$GIT_COMMITTER_DATE" &&
echo "data <<EOF" &&
echo "tag $i" &&
echo "EOF" &&

# Nested tags
echo "tag nested_$i" &&
echo "from :$i" &&
printf "tagger %s <%s> %s\n" \
"$GIT_COMMITTER_NAME" \
"$GIT_COMMITTER_EMAIL" \
"$GIT_COMMITTER_DATE" &&
echo "data <<EOF" &&
echo "nested tag $i" &&
echo "EOF" || return 1
done | git fast-import
'

test_for_each_ref () {
title="for-each-ref"
if test $# -gt 0; then
title="$title ($1)"
shift
fi
args="$@"

test_perf "$title" "
for i in \$(test_seq $test_iteration_count); do
git for-each-ref $args >/dev/null
done
"
}

run_tests () {
test_for_each_ref "$1"
test_for_each_ref "$1, no sort" --no-sort
test_for_each_ref "$1, --count=1" --count=1
test_for_each_ref "$1, --count=1, no sort" --no-sort --count=1
test_for_each_ref "$1, tags" refs/tags/
test_for_each_ref "$1, tags, no sort" --no-sort refs/tags/
test_for_each_ref "$1, tags, dereferenced" '--format="%(refname) %(objectname) %(*objectname)"' refs/tags/
test_for_each_ref "$1, tags, dereferenced, no sort" --no-sort '--format="%(refname) %(objectname) %(*objectname)"' refs/tags/

test_perf "for-each-ref ($1, tags) + cat-file --batch-check (dereferenced)" "
for i in \$(test_seq $test_iteration_count); do
git for-each-ref --format='%(objectname)^{} %(refname) %(objectname)' refs/tags/ | \
git cat-file --batch-check='%(objectname) %(rest)' >/dev/null
done
"
}

run_tests "loose"

test_expect_success 'pack refs' '
git pack-refs --all
'
run_tests "packed"

test_done

View File

@ -1576,9 +1576,10 @@ test_expect_success 'tracking with unexpected .fetch refspec' '

test_expect_success 'configured committerdate sort' '
git init -b main sort &&
test_config -C sort branch.sort "committerdate" &&

(
cd sort &&
git config branch.sort committerdate &&
test_commit initial &&
git checkout -b a &&
test_commit a &&
@ -1598,9 +1599,10 @@ test_expect_success 'configured committerdate sort' '
'

test_expect_success 'option override configured sort' '
test_config -C sort branch.sort "committerdate" &&

(
cd sort &&
git config branch.sort committerdate &&
git branch --sort=refname >actual &&
cat >expect <<-\EOF &&
a
@ -1612,10 +1614,70 @@ test_expect_success 'option override configured sort' '
)
'

test_expect_success 'invalid sort parameter in configuration' '
test_expect_success '--no-sort cancels config sort keys' '
test_config -C sort branch.sort "-refname" &&

(
cd sort &&

# objecttype is identical for all of them, so sort falls back on
# default (ascending refname)
git branch \
--no-sort \
--sort="objecttype" >actual &&
cat >expect <<-\EOF &&
a
* b
c
main
EOF
test_cmp expect actual
)

'

test_expect_success '--no-sort cancels command line sort keys' '
(
cd sort &&

# objecttype is identical for all of them, so sort falls back on
# default (ascending refname)
git branch \
--sort="-refname" \
--no-sort \
--sort="objecttype" >actual &&
cat >expect <<-\EOF &&
a
* b
c
main
EOF
test_cmp expect actual
)
'

test_expect_success '--no-sort without subsequent --sort prints expected branches' '
(
cd sort &&

# Sort the results with `sort` for a consistent comparison
# against expected
git branch --no-sort | sort >actual &&
cat >expect <<-\EOF &&
a
c
main
* b
EOF
test_cmp expect actual
)
'

test_expect_success 'invalid sort parameter in configuration' '
test_config -C sort branch.sort "v:notvalid" &&

(
cd sort &&
git config branch.sort "v:notvalid" &&

# this works in the "listing" mode, so bad sort key
# is a dying offence.

View File

@ -1335,6 +1335,27 @@ test_expect_success '--no-sort cancels the previous sort keys' '
test_cmp expected actual
'

test_expect_success '--no-sort without subsequent --sort prints expected refs' '
cat >expected <<-\EOF &&
refs/tags/multi-ref1-100000-user1
refs/tags/multi-ref1-100000-user2
refs/tags/multi-ref1-200000-user1
refs/tags/multi-ref1-200000-user2
refs/tags/multi-ref2-100000-user1
refs/tags/multi-ref2-100000-user2
refs/tags/multi-ref2-200000-user1
refs/tags/multi-ref2-200000-user2
EOF

# Sort the results with `sort` for a consistent comparison against
# expected
git for-each-ref \
--format="%(refname)" \
--no-sort \
"refs/tags/multi-*" | sort >actual &&
test_cmp expected actual
'

test_expect_success 'do not dereference NULL upon %(HEAD) on unborn branch' '
test_when_finished "git checkout main" &&
git for-each-ref --format="%(HEAD) %(refname:short)" refs/heads/ >actual &&
@ -1818,6 +1839,28 @@ test_expect_success 'git for-each-ref with non-existing refs' '
test_must_be_empty actual
'

test_expect_success 'git for-each-ref with nested tags' '
git tag -am "Normal tag" nested/base HEAD &&
git tag -am "Nested tag" nested/nest1 refs/tags/nested/base &&
git tag -am "Double nested tag" nested/nest2 refs/tags/nested/nest1 &&

head_oid="$(git rev-parse HEAD)" &&
base_tag_oid="$(git rev-parse refs/tags/nested/base)" &&
nest1_tag_oid="$(git rev-parse refs/tags/nested/nest1)" &&
nest2_tag_oid="$(git rev-parse refs/tags/nested/nest2)" &&

cat >expect <<-EOF &&
refs/tags/nested/base $base_tag_oid tag $head_oid commit
refs/tags/nested/nest1 $nest1_tag_oid tag $head_oid commit
refs/tags/nested/nest2 $nest2_tag_oid tag $head_oid commit
EOF

git for-each-ref \
--format="%(refname) %(objectname) %(objecttype) %(*objectname) %(*objecttype)" \
refs/tags/nested/ >actual &&
test_cmp expect actual
'

GRADE_FORMAT="%(signature:grade)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)"
TRUSTLEVEL_FORMAT="%(signature:trustlevel)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)"


View File

@ -45,8 +45,8 @@ test_expect_success 'check signed tags with --points-at' '
sed -e "s/Z$//" >expect <<-\EOF &&
refs/heads/side Z
refs/tags/annotated-tag four
refs/tags/doubly-annotated-tag An annotated tag
refs/tags/doubly-signed-tag A signed tag
refs/tags/doubly-annotated-tag four
refs/tags/doubly-signed-tag four
refs/tags/four Z
refs/tags/signed-tag four
EOF

View File

@ -1862,6 +1862,51 @@ test_expect_success 'option override configured sort' '
test_cmp expect actual
'

test_expect_success '--no-sort cancels config sort keys' '
test_config tag.sort "-refname" &&

# objecttype is identical for all of them, so sort falls back on
# default (ascending refname)
git tag -l \
--no-sort \
--sort="objecttype" \
"foo*" >actual &&
cat >expect <<-\EOF &&
foo1.10
foo1.3
foo1.6
EOF
test_cmp expect actual
'

test_expect_success '--no-sort cancels command line sort keys' '
# objecttype is identical for all of them, so sort falls back on
# default (ascending refname)
git tag -l \
--sort="-refname" \
--no-sort \
--sort="objecttype" \
"foo*" >actual &&
cat >expect <<-\EOF &&
foo1.10
foo1.3
foo1.6
EOF
test_cmp expect actual
'

test_expect_success '--no-sort without subsequent --sort prints expected tags' '
# Sort the results with `sort` for a consistent comparison against
# expected
git tag -l --no-sort "foo*" | sort >actual &&
cat >expect <<-\EOF &&
foo1.10
foo1.3
foo1.6
EOF
test_cmp expect actual
'

test_expect_success 'invalid sort parameter on command line' '
test_must_fail git tag -l --sort=notvalid "foo*" >actual
'