Merge branch 'ds/ahead-behind'

"git for-each-ref" learns '%(ahead-behind:<base>)' that computes the
distances from a single reference point in the history with bunch
of commits in bulk.

* ds/ahead-behind:
  commit-reach: add tips_reachable_from_bases()
  for-each-ref: add ahead-behind format atom
  commit-reach: implement ahead_behind() logic
  commit-graph: introduce `ensure_generations_valid()`
  commit-graph: return generation from memory
  commit-graph: simplify compute_generation_numbers()
  commit-graph: refactor compute_topological_levels()
  for-each-ref: explicitly test no matches
  for-each-ref: add --stdin option
This commit is contained in:
Junio C Hamano 2023-04-06 13:38:21 -07:00
commit 7727da99df
17 changed files with 866 additions and 91 deletions

View File

@ -9,7 +9,8 @@ SYNOPSIS
--------
[verse]
'git for-each-ref' [--count=<count>] [--shell|--perl|--python|--tcl]
[(--sort=<key>)...] [--format=<format>] [<pattern>...]
[(--sort=<key>)...] [--format=<format>]
[ --stdin | <pattern>... ]
[--points-at=<object>]
[--merged[=<object>]] [--no-merged[=<object>]]
[--contains[=<object>]] [--no-contains[=<object>]]
@ -32,6 +33,10 @@ OPTIONS
literally, in the latter case matching completely or from the
beginning up to a slash.
--stdin::
If `--stdin` is supplied, then the list of patterns is read from
standard input instead of from the argument list.
--count=<count>::
By default the command shows all refs that match
`<pattern>`. This option makes it stop after showing
@ -217,6 +222,11 @@ worktreepath::
out, if it is checked out in any linked worktree. Empty string
otherwise.
ahead-behind:<committish>::
Two integers, separated by a space, demonstrating the number of
commits ahead and behind, respectively, when comparing the output
ref to the `<committish>` specified in the format.
In addition to the above, for commit and tag objects, the header
field names (`tree`, `parent`, `object`, `type`, and `tag`) can
be used to specify the value in the header field.

View File

@ -448,6 +448,7 @@ static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin
if (verify_ref_format(format))
die(_("unable to parse format string"));
filter_ahead_behind(the_repository, format, &array);
ref_array_sort(sorting, &array);
for (i = 0; i < array.nr; i++) {

View File

@ -5,6 +5,8 @@
#include "object.h"
#include "parse-options.h"
#include "ref-filter.h"
#include "strvec.h"
#include "commit-reach.h"
static char const * const for_each_ref_usage[] = {
N_("git for-each-ref [<options>] [<pattern>]"),
@ -25,6 +27,8 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
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;
struct option opts[] = {
OPT_BIT('s', "shell", &format.quote_style,
@ -49,6 +53,7 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
OPT_CONTAINS(&filter.with_commit, N_("print only refs which contain the commit")),
OPT_NO_CONTAINS(&filter.no_commit, N_("print only refs which don't contain the commit")),
OPT_BOOL(0, "ignore-case", &icase, N_("sorting and filtering are case insensitive")),
OPT_BOOL(0, "stdin", &from_stdin, N_("read reference patterns from stdin")),
OPT_END(),
};
@ -75,9 +80,27 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase);
filter.ignore_case = icase;
filter.name_patterns = argv;
if (from_stdin) {
struct strbuf line = STRBUF_INIT;
if (argv[0])
die(_("unknown arguments supplied with --stdin"));
while (strbuf_getline(&line, stdin) != EOF)
strvec_push(&vec, line.buf);
strbuf_release(&line);
/* vec.v is NULL-terminated, just like 'argv'. */
filter.name_patterns = vec.v;
} else {
filter.name_patterns = argv;
}
filter.match_as_path = 1;
filter_refs(&array, &filter, FILTER_REFS_ALL);
filter_ahead_behind(the_repository, &format, &array);
ref_array_sort(sorting, &array);
if (!maxcount || array.nr < maxcount)
@ -97,5 +120,6 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
free_commit_list(filter.with_commit);
free_commit_list(filter.no_commit);
ref_sorting_release(sorting);
strvec_clear(&vec);
return 0;
}

View File

@ -67,6 +67,7 @@ static int list_tags(struct ref_filter *filter, struct ref_sorting *sorting,
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);
for (i = 0; i < array.nr; i++) {

View File

@ -117,12 +117,10 @@ timestamp_t commit_graph_generation(const struct commit *c)
struct commit_graph_data *data =
commit_graph_data_slab_peek(&commit_graph_data_slab, c);
if (!data)
return GENERATION_NUMBER_INFINITY;
else if (data->graph_pos == COMMIT_NOT_FROM_GRAPH)
return GENERATION_NUMBER_INFINITY;
if (data && data->generation)
return data->generation;
return data->generation;
return GENERATION_NUMBER_INFINITY;
}
static struct commit_graph_data *commit_graph_data_at(const struct commit *c)
@ -1447,24 +1445,52 @@ static void close_reachable(struct write_commit_graph_context *ctx)
stop_progress(&ctx->progress);
}
static void compute_topological_levels(struct write_commit_graph_context *ctx)
struct compute_generation_info {
struct repository *r;
struct packed_commit_list *commits;
struct progress *progress;
int progress_cnt;
timestamp_t (*get_generation)(struct commit *c, void *data);
void (*set_generation)(struct commit *c, timestamp_t gen, void *data);
void *data;
};
static timestamp_t compute_generation_from_max(struct commit *c,
timestamp_t max_gen,
int generation_version)
{
switch (generation_version) {
case 1: /* topological levels */
if (max_gen > GENERATION_NUMBER_V1_MAX - 1)
max_gen = GENERATION_NUMBER_V1_MAX - 1;
return max_gen + 1;
case 2: /* corrected commit date */
if (c->date && c->date > max_gen)
max_gen = c->date - 1;
return max_gen + 1;
default:
BUG("attempting unimplemented version");
}
}
static void compute_reachable_generation_numbers(
struct compute_generation_info *info,
int generation_version)
{
int i;
struct commit_list *list = NULL;
if (ctx->report_progress)
ctx->progress = start_delayed_progress(
_("Computing commit graph topological levels"),
ctx->commits.nr);
for (i = 0; i < ctx->commits.nr; i++) {
struct commit *c = ctx->commits.list[i];
uint32_t level;
for (i = 0; i < info->commits->nr; i++) {
struct commit *c = info->commits->list[i];
timestamp_t gen;
repo_parse_commit(info->r, c);
gen = info->get_generation(c, info->data);
display_progress(info->progress, info->progress_cnt + 1);
repo_parse_commit(ctx->r, c);
level = *topo_level_slab_at(ctx->topo_levels, c);
display_progress(ctx->progress, i + 1);
if (level != GENERATION_NUMBER_ZERO)
if (gen != GENERATION_NUMBER_ZERO && gen != GENERATION_NUMBER_INFINITY)
continue;
commit_list_insert(c, &list);
@ -1472,41 +1498,91 @@ static void compute_topological_levels(struct write_commit_graph_context *ctx)
struct commit *current = list->item;
struct commit_list *parent;
int all_parents_computed = 1;
uint32_t max_level = 0;
uint32_t max_gen = 0;
for (parent = current->parents; parent; parent = parent->next) {
repo_parse_commit(ctx->r, parent->item);
level = *topo_level_slab_at(ctx->topo_levels, parent->item);
repo_parse_commit(info->r, parent->item);
gen = info->get_generation(parent->item, info->data);
if (level == GENERATION_NUMBER_ZERO) {
if (gen == GENERATION_NUMBER_ZERO) {
all_parents_computed = 0;
commit_list_insert(parent->item, &list);
break;
}
if (level > max_level)
max_level = level;
if (gen > max_gen)
max_gen = gen;
}
if (all_parents_computed) {
pop_commit(&list);
if (max_level > GENERATION_NUMBER_V1_MAX - 1)
max_level = GENERATION_NUMBER_V1_MAX - 1;
*topo_level_slab_at(ctx->topo_levels, current) = max_level + 1;
gen = compute_generation_from_max(
current, max_gen,
generation_version);
info->set_generation(current, gen, info->data);
}
}
}
}
static timestamp_t get_topo_level(struct commit *c, void *data)
{
struct write_commit_graph_context *ctx = data;
return *topo_level_slab_at(ctx->topo_levels, c);
}
static void set_topo_level(struct commit *c, timestamp_t t, void *data)
{
struct write_commit_graph_context *ctx = data;
*topo_level_slab_at(ctx->topo_levels, c) = (uint32_t)t;
}
static void compute_topological_levels(struct write_commit_graph_context *ctx)
{
struct compute_generation_info info = {
.r = ctx->r,
.commits = &ctx->commits,
.get_generation = get_topo_level,
.set_generation = set_topo_level,
.data = ctx,
};
if (ctx->report_progress)
info.progress = ctx->progress
= start_delayed_progress(
_("Computing commit graph topological levels"),
ctx->commits.nr);
compute_reachable_generation_numbers(&info, 1);
stop_progress(&ctx->progress);
}
static timestamp_t get_generation_from_graph_data(struct commit *c, void *data)
{
return commit_graph_data_at(c)->generation;
}
static void set_generation_v2(struct commit *c, timestamp_t t, void *data)
{
struct commit_graph_data *g = commit_graph_data_at(c);
g->generation = (uint32_t)t;
}
static void compute_generation_numbers(struct write_commit_graph_context *ctx)
{
int i;
struct commit_list *list = NULL;
struct compute_generation_info info = {
.r = ctx->r,
.commits = &ctx->commits,
.get_generation = get_generation_from_graph_data,
.set_generation = set_generation_v2,
.data = ctx,
};
if (ctx->report_progress)
ctx->progress = start_delayed_progress(
info.progress = ctx->progress
= start_delayed_progress(
_("Computing commit graph generation numbers"),
ctx->commits.nr);
@ -1518,47 +1594,7 @@ static void compute_generation_numbers(struct write_commit_graph_context *ctx)
}
}
for (i = 0; i < ctx->commits.nr; i++) {
struct commit *c = ctx->commits.list[i];
timestamp_t corrected_commit_date;
repo_parse_commit(ctx->r, c);
corrected_commit_date = commit_graph_data_at(c)->generation;
display_progress(ctx->progress, i + 1);
if (corrected_commit_date != GENERATION_NUMBER_ZERO)
continue;
commit_list_insert(c, &list);
while (list) {
struct commit *current = list->item;
struct commit_list *parent;
int all_parents_computed = 1;
timestamp_t max_corrected_commit_date = 0;
for (parent = current->parents; parent; parent = parent->next) {
repo_parse_commit(ctx->r, parent->item);
corrected_commit_date = commit_graph_data_at(parent->item)->generation;
if (corrected_commit_date == GENERATION_NUMBER_ZERO) {
all_parents_computed = 0;
commit_list_insert(parent->item, &list);
break;
}
if (corrected_commit_date > max_corrected_commit_date)
max_corrected_commit_date = corrected_commit_date;
}
if (all_parents_computed) {
pop_commit(&list);
if (current->date && current->date > max_corrected_commit_date)
max_corrected_commit_date = current->date - 1;
commit_graph_data_at(current)->generation = max_corrected_commit_date + 1;
}
}
}
compute_reachable_generation_numbers(&info, 2);
for (i = 0; i < ctx->commits.nr; i++) {
struct commit *c = ctx->commits.list[i];
@ -1569,6 +1605,35 @@ static void compute_generation_numbers(struct write_commit_graph_context *ctx)
stop_progress(&ctx->progress);
}
static void set_generation_in_graph_data(struct commit *c, timestamp_t t,
void *data)
{
commit_graph_data_at(c)->generation = t;
}
/*
* After this method, all commits reachable from those in the given
* list will have non-zero, non-infinite generation numbers.
*/
void ensure_generations_valid(struct repository *r,
struct commit **commits, size_t nr)
{
int generation_version = get_configured_generation_version(r);
struct packed_commit_list list = {
.list = commits,
.alloc = nr,
.nr = nr,
};
struct compute_generation_info info = {
.r = r,
.commits = &list,
.get_generation = get_generation_from_graph_data,
.set_generation = set_generation_in_graph_data,
};
compute_reachable_generation_numbers(&info, generation_version);
}
static void trace2_bloom_filter_write_statistics(struct write_commit_graph_context *ctx)
{
trace2_data_intmax("commit-graph", ctx->r, "filter-computed",

View File

@ -189,4 +189,12 @@ struct commit_graph_data {
*/
timestamp_t commit_graph_generation(const struct commit *);
uint32_t commit_graph_position(const struct commit *);
/*
* After this method, all commits reachable from those in the given
* list will have non-zero, non-infinite generation numbers.
*/
void ensure_generations_valid(struct repository *r,
struct commit **commits, size_t nr);
#endif

View File

@ -10,6 +10,7 @@
#include "revision.h"
#include "tag.h"
#include "commit-reach.h"
#include "ewah/ewok.h"
/* Remember to update object flag allocation in object.h */
#define PARENT1 (1u<<16)
@ -947,3 +948,218 @@ struct commit_list *get_reachable_subset(struct commit **from, int nr_from,
return found_commits;
}
define_commit_slab(bit_arrays, struct bitmap *);
static struct bit_arrays bit_arrays;
static void insert_no_dup(struct prio_queue *queue, struct commit *c)
{
if (c->object.flags & PARENT2)
return;
prio_queue_put(queue, c);
c->object.flags |= PARENT2;
}
static struct bitmap *get_bit_array(struct commit *c, int width)
{
struct bitmap **bitmap = bit_arrays_at(&bit_arrays, c);
if (!*bitmap)
*bitmap = bitmap_word_alloc(width);
return *bitmap;
}
static void free_bit_array(struct commit *c)
{
struct bitmap **bitmap = bit_arrays_at(&bit_arrays, c);
if (!*bitmap)
return;
bitmap_free(*bitmap);
*bitmap = NULL;
}
void ahead_behind(struct repository *r,
struct commit **commits, size_t commits_nr,
struct ahead_behind_count *counts, size_t counts_nr)
{
struct prio_queue queue = { .compare = compare_commits_by_gen_then_commit_date };
size_t width = DIV_ROUND_UP(commits_nr, BITS_IN_EWORD);
if (!commits_nr || !counts_nr)
return;
for (size_t i = 0; i < counts_nr; i++) {
counts[i].ahead = 0;
counts[i].behind = 0;
}
ensure_generations_valid(r, commits, commits_nr);
init_bit_arrays(&bit_arrays);
for (size_t i = 0; i < commits_nr; i++) {
struct commit *c = commits[i];
struct bitmap *bitmap = get_bit_array(c, width);
bitmap_set(bitmap, i);
insert_no_dup(&queue, c);
}
while (queue_has_nonstale(&queue)) {
struct commit *c = prio_queue_get(&queue);
struct commit_list *p;
struct bitmap *bitmap_c = get_bit_array(c, width);
for (size_t i = 0; i < counts_nr; i++) {
int reach_from_tip = !!bitmap_get(bitmap_c, counts[i].tip_index);
int reach_from_base = !!bitmap_get(bitmap_c, counts[i].base_index);
if (reach_from_tip ^ reach_from_base) {
if (reach_from_base)
counts[i].behind++;
else
counts[i].ahead++;
}
}
for (p = c->parents; p; p = p->next) {
struct bitmap *bitmap_p;
repo_parse_commit(r, p->item);
bitmap_p = get_bit_array(p->item, width);
bitmap_or(bitmap_p, bitmap_c);
/*
* If this parent is reachable from every starting
* commit, then none of its ancestors can contribute
* to the ahead/behind count. Mark it as STALE, so
* we can stop the walk when every commit in the
* queue is STALE.
*/
if (bitmap_popcount(bitmap_p) == commits_nr)
p->item->object.flags |= STALE;
insert_no_dup(&queue, p->item);
}
free_bit_array(c);
}
/* STALE is used here, PARENT2 is used by insert_no_dup(). */
repo_clear_commit_marks(r, PARENT2 | STALE);
clear_bit_arrays(&bit_arrays);
clear_prio_queue(&queue);
}
struct commit_and_index {
struct commit *commit;
unsigned int index;
timestamp_t generation;
};
static int compare_commit_and_index_by_generation(const void *va, const void *vb)
{
const struct commit_and_index *a = (const struct commit_and_index *)va;
const struct commit_and_index *b = (const struct commit_and_index *)vb;
if (a->generation > b->generation)
return 1;
if (a->generation < b->generation)
return -1;
return 0;
}
void tips_reachable_from_bases(struct repository *r,
struct commit_list *bases,
struct commit **tips, size_t tips_nr,
int mark)
{
struct commit_and_index *commits;
size_t min_generation_index = 0;
timestamp_t min_generation;
struct commit_list *stack = NULL;
if (!bases || !tips || !tips_nr)
return;
/*
* Do a depth-first search starting at 'bases' to search for the
* tips. Stop at the lowest (un-found) generation number. When
* finding the lowest commit, increase the minimum generation
* number to the next lowest (un-found) generation number.
*/
CALLOC_ARRAY(commits, tips_nr);
for (size_t i = 0; i < tips_nr; i++) {
commits[i].commit = tips[i];
commits[i].index = i;
commits[i].generation = commit_graph_generation(tips[i]);
}
/* Sort with generation number ascending. */
QSORT(commits, tips_nr, compare_commit_and_index_by_generation);
min_generation = commits[0].generation;
while (bases) {
repo_parse_commit(r, bases->item);
commit_list_insert(bases->item, &stack);
bases = bases->next;
}
while (stack) {
int explored_all_parents = 1;
struct commit_list *p;
struct commit *c = stack->item;
timestamp_t c_gen = commit_graph_generation(c);
/* Does it match any of our tips? */
for (size_t j = min_generation_index; j < tips_nr; j++) {
if (c_gen < commits[j].generation)
break;
if (commits[j].commit == c) {
tips[commits[j].index]->object.flags |= mark;
if (j == min_generation_index) {
unsigned int k = j + 1;
while (k < tips_nr &&
(tips[commits[k].index]->object.flags & mark))
k++;
/* Terminate early if all found. */
if (k >= tips_nr)
goto done;
min_generation_index = k;
min_generation = commits[k].generation;
}
}
}
for (p = c->parents; p; p = p->next) {
repo_parse_commit(r, p->item);
/* Have we already explored this parent? */
if (p->item->object.flags & SEEN)
continue;
/* Is it below the current minimum generation? */
if (commit_graph_generation(p->item) < min_generation)
continue;
/* Ok, we will explore from here on. */
p->item->object.flags |= SEEN;
explored_all_parents = 0;
commit_list_insert(p->item, &stack);
break;
}
if (explored_all_parents)
pop_commit(&stack);
}
done:
free(commits);
repo_clear_commit_marks(r, SEEN);
}

View File

@ -104,4 +104,44 @@ struct commit_list *get_reachable_subset(struct commit **from, int nr_from,
struct commit **to, int nr_to,
unsigned int reachable_flag);
struct ahead_behind_count {
/**
* As input, the *_index members indicate which positions in
* the 'tips' array correspond to the tip and base of this
* comparison.
*/
size_t tip_index;
size_t base_index;
/**
* These values store the computed counts for each side of the
* symmetric difference:
*
* 'ahead' stores the number of commits reachable from the tip
* and not reachable from the base.
*
* 'behind' stores the number of commits reachable from the base
* and not reachable from the tip.
*/
unsigned int ahead;
unsigned int behind;
};
/*
* Given an array of commits and an array of ahead_behind_count pairs,
* compute the ahead/behind counts for each pair.
*/
void ahead_behind(struct repository *r,
struct commit **commits, size_t commits_nr,
struct ahead_behind_count *counts, size_t counts_nr);
/*
* For all tip commits, add 'mark' to their flags if and only if they
* are reachable from one of the commits in 'bases'.
*/
void tips_reachable_from_bases(struct repository *r,
struct commit_list *bases,
struct commit **tips, size_t tips_nr,
int mark);
#endif

View File

@ -158,6 +158,7 @@ enum atom_type {
ATOM_THEN,
ATOM_ELSE,
ATOM_REST,
ATOM_AHEADBEHIND,
};
/*
@ -600,6 +601,22 @@ static int rest_atom_parser(struct ref_format *format,
return 0;
}
static int ahead_behind_atom_parser(struct ref_format *format, struct used_atom *atom,
const char *arg, struct strbuf *err)
{
struct string_list_item *item;
if (!arg)
return strbuf_addf_ret(err, -1, _("expected format: %%(ahead-behind:<committish>)"));
item = string_list_append(&format->bases, arg);
item->util = lookup_commit_reference_by_name(arg);
if (!item->util)
die("failed to find '%s'", arg);
return 0;
}
static int head_atom_parser(struct ref_format *format UNUSED,
struct used_atom *atom,
const char *arg, struct strbuf *err)
@ -660,6 +677,7 @@ static struct {
[ATOM_THEN] = { "then", SOURCE_NONE },
[ATOM_ELSE] = { "else", SOURCE_NONE },
[ATOM_REST] = { "rest", SOURCE_NONE, FIELD_STR, rest_atom_parser },
[ATOM_AHEADBEHIND] = { "ahead-behind", SOURCE_OTHER, FIELD_STR, ahead_behind_atom_parser },
/*
* Please update $__git_ref_fieldlist in git-completion.bash
* when you add new atoms
@ -1866,6 +1884,7 @@ static int populate_value(struct ref_array_item *ref, struct strbuf *err)
struct object *obj;
int i;
struct object_info empty = OBJECT_INFO_INIT;
int ahead_behind_atoms = 0;
CALLOC_ARRAY(ref->value, used_atom_cnt);
@ -1996,6 +2015,16 @@ static int populate_value(struct ref_array_item *ref, struct strbuf *err)
else
v->s = xstrdup("");
continue;
} else if (atom_type == ATOM_AHEADBEHIND) {
if (ref->counts) {
const struct ahead_behind_count *count;
count = ref->counts[ahead_behind_atoms++];
v->s = xstrfmt("%d %d", count->ahead, count->behind);
} else {
/* Not a commit. */
v->s = xstrdup("");
}
continue;
} else
continue;
@ -2346,6 +2375,7 @@ static void free_array_item(struct ref_array_item *item)
free((char *)item->value[i].s);
free(item->value);
}
free(item->counts);
free(item);
}
@ -2374,6 +2404,8 @@ void ref_array_clear(struct ref_array *array)
free_worktrees(ref_to_worktree_map.worktrees);
ref_to_worktree_map.worktrees = NULL;
}
FREE_AND_NULL(array->counts);
}
#define EXCLUDE_REACHED 0
@ -2382,33 +2414,22 @@ static void reach_filter(struct ref_array *array,
struct commit_list *check_reachable,
int include_reached)
{
struct rev_info revs;
int i, old_nr;
struct commit **to_clear;
struct commit_list *cr;
if (!check_reachable)
return;
CALLOC_ARRAY(to_clear, array->nr);
repo_init_revisions(the_repository, &revs, NULL);
for (i = 0; i < array->nr; i++) {
struct ref_array_item *item = array->items[i];
add_pending_object(&revs, &item->commit->object, item->refname);
to_clear[i] = item->commit;
}
for (cr = check_reachable; cr; cr = cr->next) {
struct commit *merge_commit = cr->item;
merge_commit->object.flags |= UNINTERESTING;
add_pending_object(&revs, &merge_commit->object, "");
}
revs.limited = 1;
if (prepare_revision_walk(&revs))
die(_("revision walk setup failed"));
tips_reachable_from_bases(the_repository,
check_reachable,
to_clear, array->nr,
UNINTERESTING);
old_nr = array->nr;
array->nr = 0;
@ -2432,10 +2453,50 @@ static void reach_filter(struct ref_array *array,
clear_commit_marks(merge_commit, ALL_REV_FLAGS);
}
release_revisions(&revs);
free(to_clear);
}
void filter_ahead_behind(struct repository *r,
struct ref_format *format,
struct ref_array *array)
{
struct commit **commits;
size_t commits_nr = format->bases.nr + array->nr;
if (!format->bases.nr || !array->nr)
return;
ALLOC_ARRAY(commits, commits_nr);
for (size_t i = 0; i < format->bases.nr; i++)
commits[i] = format->bases.items[i].util;
ALLOC_ARRAY(array->counts, st_mult(format->bases.nr, array->nr));
commits_nr = format->bases.nr;
array->counts_nr = 0;
for (size_t i = 0; i < array->nr; i++) {
const char *name = array->items[i]->refname;
commits[commits_nr] = lookup_commit_reference_by_name(name);
if (!commits[commits_nr])
continue;
CALLOC_ARRAY(array->items[i]->counts, format->bases.nr);
for (size_t j = 0; j < format->bases.nr; j++) {
struct ahead_behind_count *count;
count = &array->counts[array->counts_nr++];
count->tip_index = commits_nr;
count->base_index = j;
array->items[i]->counts[j] = count;
}
commits_nr++;
}
ahead_behind(r, commits, commits_nr, array->counts, array->counts_nr);
free(commits);
}
/*
* 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

View File

@ -4,6 +4,7 @@
#include "oid-array.h"
#include "refs.h"
#include "commit.h"
#include "string-list.h"
/* Quoting styles */
#define QUOTE_NONE 0
@ -23,6 +24,7 @@
struct atom_value;
struct ref_sorting;
struct ahead_behind_count;
struct option;
enum ref_sorting_order {
@ -40,6 +42,8 @@ struct ref_array_item {
const char *symref;
struct commit *commit;
struct atom_value *value;
struct ahead_behind_count **counts;
char refname[FLEX_ARRAY];
};
@ -47,6 +51,9 @@ struct ref_array {
int nr, alloc;
struct ref_array_item **items;
struct rev_info *revs;
struct ahead_behind_count *counts;
size_t counts_nr;
};
struct ref_filter {
@ -80,9 +87,15 @@ struct ref_format {
/* Internal state to ref-filter */
int need_color_reset_at_eol;
/* List of bases for ahead-behind counts. */
struct string_list bases;
};
#define REF_FORMAT_INIT { .use_color = -1 }
#define REF_FORMAT_INIT { \
.use_color = -1, \
.bases = STRING_LIST_INIT_DUP, \
}
/* Macros for checking --merged and --no-merged options */
#define _OPT_MERGED_NO_MERGED(option, filter, h) \
@ -143,4 +156,15 @@ struct ref_array_item *ref_array_push(struct ref_array *array,
const char *refname,
const struct object_id *oid);
/*
* If the provided format includes ahead-behind atoms, then compute the
* ahead-behind values for the array of filtered references. Must be
* called after filter_refs() but before outputting the formatted refs.
*
* If this is not called, then any ahead-behind atoms will be blank.
*/
void filter_ahead_behind(struct repository *r,
struct ref_format *format,
struct ref_array *array);
#endif /* REF_FILTER_H */

50
t/perf/p1500-graph-walks.sh Executable file
View File

@ -0,0 +1,50 @@
#!/bin/sh
test_description='Commit walk performance tests'
. ./perf-lib.sh
test_perf_large_repo
test_expect_success 'setup' '
git for-each-ref --format="%(refname)" "refs/heads/*" "refs/tags/*" >allrefs &&
sort -r allrefs | head -n 50 >refs &&
for ref in $(cat refs)
do
git branch -f ref-$ref $ref &&
echo ref-$ref ||
return 1
done >branches &&
for ref in $(cat refs)
do
git tag -f tag-$ref $ref &&
echo tag-$ref ||
return 1
done >tags &&
git commit-graph write --reachable
'
test_perf 'ahead-behind counts: git for-each-ref' '
git for-each-ref --format="%(ahead-behind:HEAD)" --stdin <refs
'
test_perf 'ahead-behind counts: git branch' '
xargs git branch -l --format="%(ahead-behind:HEAD)" <branches
'
test_perf 'ahead-behind counts: git tag' '
xargs git tag -l --format="%(ahead-behind:HEAD)" <tags
'
test_perf 'contains: git for-each-ref --merged' '
git for-each-ref --merged=HEAD --stdin <refs
'
test_perf 'contains: git branch --merged' '
xargs git branch --merged=HEAD <branches
'
test_perf 'contains: git tag --merged' '
xargs git tag --merged=HEAD <tags
'
test_done

View File

@ -337,6 +337,20 @@ test_expect_success 'git branch --format option' '
test_cmp expect actual
'
test_expect_success 'git branch --format with ahead-behind' '
cat >expect <<-\EOF &&
(HEAD detached from fromtag) 0 0
refs/heads/ambiguous 0 0
refs/heads/branch-one 1 0
refs/heads/branch-two 0 0
refs/heads/main 1 0
refs/heads/ref-to-branch 1 0
refs/heads/ref-to-remote 1 0
EOF
git branch --format="%(refname) %(ahead-behind:HEAD)" >actual &&
test_cmp expect actual
'
test_expect_success 'git branch with --format=%(rest) must fail' '
test_must_fail git branch --format="%(rest)" >actual
'

View File

@ -630,7 +630,7 @@ test_expect_success 'detect incorrect generation number' '
test_expect_success 'detect incorrect generation number' '
corrupt_graph_and_verify $GRAPH_BYTE_COMMIT_GENERATION "\01" \
"non-zero generation number"
"commit-graph generation for commit"
'
test_expect_success 'detect incorrect commit date' '

View File

@ -1464,4 +1464,54 @@ sig_crlf="$(printf "%s" "$sig" | append_cr; echo dummy)"
sig_crlf=${sig_crlf%dummy}
test_atom refs/tags/fake-sig-crlf contents:signature "$sig_crlf"
test_expect_success 'git for-each-ref --stdin: empty' '
>in &&
git for-each-ref --format="%(refname)" --stdin <in >actual &&
git for-each-ref --format="%(refname)" >expect &&
test_cmp expect actual
'
test_expect_success 'git for-each-ref --stdin: fails if extra args' '
>in &&
test_must_fail git for-each-ref --format="%(refname)" \
--stdin refs/heads/extra <in 2>err &&
grep "unknown arguments supplied with --stdin" err
'
test_expect_success 'git for-each-ref --stdin: matches' '
cat >in <<-EOF &&
refs/tags/multi*
refs/heads/amb*
EOF
cat >expect <<-EOF &&
refs/heads/ambiguous
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
refs/tags/multiline
EOF
git for-each-ref --format="%(refname)" --stdin <in >actual &&
test_cmp expect actual
'
test_expect_success 'git for-each-ref with non-existing refs' '
cat >in <<-EOF &&
refs/heads/this-ref-does-not-exist
refs/tags/bogus
EOF
git for-each-ref --format="%(refname)" --stdin <in >actual &&
test_must_be_empty actual &&
xargs git for-each-ref --format="%(refname)" <in >actual &&
test_must_be_empty actual
'
test_done

View File

@ -54,4 +54,18 @@ test_expect_success 'Missing objects are reported correctly' '
test_must_be_empty brief-err
'
test_expect_success 'ahead-behind requires an argument' '
test_must_fail git for-each-ref \
--format="%(ahead-behind)" 2>err &&
echo "fatal: expected format: %(ahead-behind:<committish>)" >expect &&
test_cmp expect err
'
test_expect_success 'missing ahead-behind base' '
test_must_fail git for-each-ref \
--format="%(ahead-behind:refs/heads/missing)" 2>err &&
echo "fatal: failed to find '\''refs/heads/missing'\''" >expect &&
test_cmp expect err
'
test_done

View File

@ -443,4 +443,173 @@ test_expect_success 'get_reachable_subset:none' '
test_all_modes get_reachable_subset
'
test_expect_success 'for-each-ref ahead-behind:linear' '
cat >input <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-1-3
refs/heads/commit-1-5
refs/heads/commit-1-8
EOF
cat >expect <<-\EOF &&
refs/heads/commit-1-1 0 8
refs/heads/commit-1-3 0 6
refs/heads/commit-1-5 0 4
refs/heads/commit-1-8 0 1
EOF
run_all_modes git for-each-ref \
--format="%(refname) %(ahead-behind:commit-1-9)" --stdin
'
test_expect_success 'for-each-ref ahead-behind:all' '
cat >input <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-2-4
refs/heads/commit-4-2
refs/heads/commit-4-4
EOF
cat >expect <<-\EOF &&
refs/heads/commit-1-1 0 24
refs/heads/commit-2-4 0 17
refs/heads/commit-4-2 0 17
refs/heads/commit-4-4 0 9
EOF
run_all_modes git for-each-ref \
--format="%(refname) %(ahead-behind:commit-5-5)" --stdin
'
test_expect_success 'for-each-ref ahead-behind:some' '
cat >input <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-5-3
refs/heads/commit-4-8
refs/heads/commit-9-9
EOF
cat >expect <<-\EOF &&
refs/heads/commit-1-1 0 53
refs/heads/commit-4-8 8 30
refs/heads/commit-5-3 0 39
refs/heads/commit-9-9 27 0
EOF
run_all_modes git for-each-ref \
--format="%(refname) %(ahead-behind:commit-9-6)" --stdin
'
test_expect_success 'for-each-ref ahead-behind:some, multibase' '
cat >input <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-5-3
refs/heads/commit-7-8
refs/heads/commit-4-8
refs/heads/commit-9-9
EOF
cat >expect <<-\EOF &&
refs/heads/commit-1-1 0 53 0 53
refs/heads/commit-4-8 8 30 0 22
refs/heads/commit-5-3 0 39 0 39
refs/heads/commit-7-8 14 12 8 6
refs/heads/commit-9-9 27 0 27 0
EOF
run_all_modes git for-each-ref \
--format="%(refname) %(ahead-behind:commit-9-6) %(ahead-behind:commit-6-9)" \
--stdin
'
test_expect_success 'for-each-ref ahead-behind:none' '
cat >input <<-\EOF &&
refs/heads/commit-7-5
refs/heads/commit-4-8
refs/heads/commit-9-9
EOF
cat >expect <<-\EOF &&
refs/heads/commit-4-8 16 16
refs/heads/commit-7-5 7 4
refs/heads/commit-9-9 49 0
EOF
run_all_modes git for-each-ref \
--format="%(refname) %(ahead-behind:commit-8-4)" --stdin
'
test_expect_success 'for-each-ref merged:linear' '
cat >input <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-1-3
refs/heads/commit-1-5
refs/heads/commit-1-8
refs/heads/commit-2-1
refs/heads/commit-5-1
refs/heads/commit-9-1
EOF
cat >expect <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-1-3
refs/heads/commit-1-5
refs/heads/commit-1-8
EOF
run_all_modes git for-each-ref --merged=commit-1-9 \
--format="%(refname)" --stdin
'
test_expect_success 'for-each-ref merged:all' '
cat >input <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-2-4
refs/heads/commit-4-2
refs/heads/commit-4-4
EOF
cat >expect <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-2-4
refs/heads/commit-4-2
refs/heads/commit-4-4
EOF
run_all_modes git for-each-ref --merged=commit-5-5 \
--format="%(refname)" --stdin
'
test_expect_success 'for-each-ref ahead-behind:some' '
cat >input <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-5-3
refs/heads/commit-4-8
refs/heads/commit-9-9
EOF
cat >expect <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-5-3
EOF
run_all_modes git for-each-ref --merged=commit-9-6 \
--format="%(refname)" --stdin
'
test_expect_success 'for-each-ref merged:some, multibase' '
cat >input <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-5-3
refs/heads/commit-7-8
refs/heads/commit-4-8
refs/heads/commit-9-9
EOF
cat >expect <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-4-8
refs/heads/commit-5-3
EOF
run_all_modes git for-each-ref \
--merged=commit-5-8 \
--merged=commit-8-5 \
--format="%(refname)" \
--stdin
'
test_expect_success 'for-each-ref merged:none' '
cat >input <<-\EOF &&
refs/heads/commit-7-5
refs/heads/commit-4-8
refs/heads/commit-9-9
EOF
>expect &&
run_all_modes git for-each-ref --merged=commit-8-4 \
--format="%(refname)" --stdin
'
test_done

View File

@ -792,6 +792,34 @@ test_expect_success 'annotations for blobs are empty' '
test_cmp expect actual
'
# Run this before doing any signing, so the test has the same results
# regardless of the GPG prereq.
test_expect_success 'git tag --format with ahead-behind' '
test_when_finished git reset --hard tag-one-line &&
git commit --allow-empty -m "left" &&
git tag -a -m left tag-left &&
git reset --hard HEAD~1 &&
git commit --allow-empty -m "right" &&
git tag -a -m left tag-right &&
# Use " !" at the end to demonstrate whitespace
# around empty ahead-behind token for tag-blob.
cat >expect <<-EOF &&
refs/tags/tag-blob !
refs/tags/tag-left 1 1 !
refs/tags/tag-lines 0 1 !
refs/tags/tag-one-line 0 1 !
refs/tags/tag-right 0 0 !
refs/tags/tag-zero-lines 0 1 !
EOF
git tag -l --format="%(refname) %(ahead-behind:HEAD) !" >actual 2>err &&
grep "refs/tags/tag" actual >actual.focus &&
test_cmp expect actual.focus &&
# Error reported for tags that point to non-commits.
grep "error: object [0-9a-f]* is a blob, not a commit" err
'
# trying to verify annotated non-signed tags:
test_expect_success GPG \