Skip to content

Commit 49abcd2

Browse files
derrickstoleegitster
authored andcommitted
for-each-ref: add ahead-behind format atom
The previous change implemented the ahead_behind() method, including an algorithm to compute the ahead/behind values for a number of commit tips relative to a number of commit bases. Now, integrate that algorithm as part of 'git for-each-ref' hidden behind a new format atom, ahead-behind. This naturally extends to 'git branch' and 'git tag' builtins, as well. This format allows specifying multiple bases, if so desired, and all matching references are compared against all of those bases. For this reason, failing to read a reference provided from these atoms results in an error. In order to translate the ahead_behind() method information to the format output code in ref-filter.c, we must populate arrays of ahead_behind_count structs. In struct ref_array, we store the full array that will be passed to ahead_behind(). In struct ref_array_item, we store an array of pointers that point to the relvant items within the full array. In this way, we can pull all relevant ahead/behind values directly when formatting output for a specific item. It also ensures the lifetime of the ahead_behind_count structs matches the time that the array is being used. Add specific tests of the ahead/behind counts in t6600-test-reach.sh, as it has an interesting repository shape. In particular, its merging strategy and its use of different commit-graphs would demonstrate over- counting if the ahead_behind() method did not already account for that possibility. Also add tests for the specific for-each-ref, branch, and tag builtins. In the case of 'git tag', there are intersting cases that happen when some of the selected tips are not commits. This requires careful logic around commits_nr in the second loop of filter_ahead_behind(). Also, the test in t7004 is carefully located to avoid being dependent on the GPG prereq. It also avoids using the test_commit helper, as that will add ticks to the time and disrupt the expected timestamps in later tag tests. Also add performance tests in a new p1300-graph-walks.sh script. This will be useful for more uses in the future, but for now compare the ahead-behind counting algorithm in 'git for-each-ref' to the naive implementation by running 'git rev-list --count' processes for each input. For the Git source code repository, the improvement is already obvious: Test this tree --------------------------------------------------------------- 1500.2: ahead-behind counts: git for-each-ref 0.07(0.07+0.00) 1500.3: ahead-behind counts: git branch 0.07(0.06+0.00) 1500.4: ahead-behind counts: git tag 0.07(0.06+0.00) 1500.5: ahead-behind counts: git rev-list 1.32(1.04+0.27) But the standard performance benchmark is the Linux kernel repository, which demosntrates a significant improvement: Test this tree --------------------------------------------------------------- 1500.2: ahead-behind counts: git for-each-ref 0.27(0.24+0.02) 1500.3: ahead-behind counts: git branch 0.27(0.24+0.03) 1500.4: ahead-behind counts: git tag 0.28(0.27+0.01) 1500.5: ahead-behind counts: git rev-list 4.57(4.03+0.54) The 'git rev-list' test exists in this change as a demonstration, but it will be removed in the next change to avoid wasting time on this comparison. Signed-off-by: Derrick Stolee <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent fd67d14 commit 49abcd2

File tree

11 files changed

+295
-1
lines changed

11 files changed

+295
-1
lines changed

Documentation/git-for-each-ref.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,11 @@ worktreepath::
222222
out, if it is checked out in any linked worktree. Empty string
223223
otherwise.
224224

225+
ahead-behind:<committish>::
226+
Two integers, separated by a space, demonstrating the number of
227+
commits ahead and behind, respectively, when comparing the output
228+
ref to the `<committish>` specified in the format.
229+
225230
In addition to the above, for commit and tag objects, the header
226231
field names (`tree`, `parent`, `object`, `type`, and `tag`) can
227232
be used to specify the value in the header field.

builtin/branch.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin
448448
if (verify_ref_format(format))
449449
die(_("unable to parse format string"));
450450

451+
filter_ahead_behind(the_repository, format, &array);
451452
ref_array_sort(sorting, &array);
452453

453454
for (i = 0; i < array.nr; i++) {

builtin/for-each-ref.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "parse-options.h"
77
#include "ref-filter.h"
88
#include "strvec.h"
9+
#include "commit-reach.h"
910

1011
static char const * const for_each_ref_usage[] = {
1112
N_("git for-each-ref [<options>] [<pattern>]"),
@@ -98,6 +99,8 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
9899

99100
filter.match_as_path = 1;
100101
filter_refs(&array, &filter, FILTER_REFS_ALL);
102+
filter_ahead_behind(the_repository, &format, &array);
103+
101104
ref_array_sort(sorting, &array);
102105

103106
if (!maxcount || array.nr < maxcount)

builtin/tag.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ static int list_tags(struct ref_filter *filter, struct ref_sorting *sorting,
6666
die(_("unable to parse format string"));
6767
filter->with_commit_tag_algo = 1;
6868
filter_refs(&array, filter, FILTER_REFS_TAGS);
69+
filter_ahead_behind(the_repository, format, &array);
6970
ref_array_sort(sorting, &array);
7071

7172
for (i = 0; i < array.nr; i++) {

ref-filter.c

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ enum atom_type {
158158
ATOM_THEN,
159159
ATOM_ELSE,
160160
ATOM_REST,
161+
ATOM_AHEADBEHIND,
161162
};
162163

163164
/*
@@ -586,6 +587,22 @@ static int rest_atom_parser(struct ref_format *format, struct used_atom *atom,
586587
return 0;
587588
}
588589

590+
static int ahead_behind_atom_parser(struct ref_format *format, struct used_atom *atom,
591+
const char *arg, struct strbuf *err)
592+
{
593+
struct string_list_item *item;
594+
595+
if (!arg)
596+
return strbuf_addf_ret(err, -1, _("expected format: %%(ahead-behind:<committish>)"));
597+
598+
item = string_list_append(&format->bases, arg);
599+
item->util = lookup_commit_reference_by_name(arg);
600+
if (!item->util)
601+
die("failed to find '%s'", arg);
602+
603+
return 0;
604+
}
605+
589606
static int head_atom_parser(struct ref_format *format, struct used_atom *atom,
590607
const char *arg, struct strbuf *err)
591608
{
@@ -645,6 +662,7 @@ static struct {
645662
[ATOM_THEN] = { "then", SOURCE_NONE },
646663
[ATOM_ELSE] = { "else", SOURCE_NONE },
647664
[ATOM_REST] = { "rest", SOURCE_NONE, FIELD_STR, rest_atom_parser },
665+
[ATOM_AHEADBEHIND] = { "ahead-behind", SOURCE_OTHER, FIELD_STR, ahead_behind_atom_parser },
648666
/*
649667
* Please update $__git_ref_fieldlist in git-completion.bash
650668
* when you add new atoms
@@ -1848,6 +1866,7 @@ static int populate_value(struct ref_array_item *ref, struct strbuf *err)
18481866
struct object *obj;
18491867
int i;
18501868
struct object_info empty = OBJECT_INFO_INIT;
1869+
int ahead_behind_atoms = 0;
18511870

18521871
CALLOC_ARRAY(ref->value, used_atom_cnt);
18531872

@@ -1978,6 +1997,16 @@ static int populate_value(struct ref_array_item *ref, struct strbuf *err)
19781997
else
19791998
v->s = xstrdup("");
19801999
continue;
2000+
} else if (atom_type == ATOM_AHEADBEHIND) {
2001+
if (ref->counts) {
2002+
const struct ahead_behind_count *count;
2003+
count = ref->counts[ahead_behind_atoms++];
2004+
v->s = xstrfmt("%d %d", count->ahead, count->behind);
2005+
} else {
2006+
/* Not a commit. */
2007+
v->s = xstrdup("");
2008+
}
2009+
continue;
19812010
} else
19822011
continue;
19832012

@@ -2328,6 +2357,7 @@ static void free_array_item(struct ref_array_item *item)
23282357
free((char *)item->value[i].s);
23292358
free(item->value);
23302359
}
2360+
free(item->counts);
23312361
free(item);
23322362
}
23332363

@@ -2356,6 +2386,8 @@ void ref_array_clear(struct ref_array *array)
23562386
free_worktrees(ref_to_worktree_map.worktrees);
23572387
ref_to_worktree_map.worktrees = NULL;
23582388
}
2389+
2390+
FREE_AND_NULL(array->counts);
23592391
}
23602392

23612393
#define EXCLUDE_REACHED 0
@@ -2418,6 +2450,47 @@ static void reach_filter(struct ref_array *array,
24182450
free(to_clear);
24192451
}
24202452

2453+
void filter_ahead_behind(struct repository *r,
2454+
struct ref_format *format,
2455+
struct ref_array *array)
2456+
{
2457+
struct commit **commits;
2458+
size_t commits_nr = format->bases.nr + array->nr;
2459+
2460+
if (!format->bases.nr || !array->nr)
2461+
return;
2462+
2463+
ALLOC_ARRAY(commits, commits_nr);
2464+
for (size_t i = 0; i < format->bases.nr; i++)
2465+
commits[i] = format->bases.items[i].util;
2466+
2467+
ALLOC_ARRAY(array->counts, st_mult(format->bases.nr, array->nr));
2468+
2469+
commits_nr = format->bases.nr;
2470+
array->counts_nr = 0;
2471+
for (size_t i = 0; i < array->nr; i++) {
2472+
const char *name = array->items[i]->refname;
2473+
commits[commits_nr] = lookup_commit_reference_by_name(name);
2474+
2475+
if (!commits[commits_nr])
2476+
continue;
2477+
2478+
CALLOC_ARRAY(array->items[i]->counts, format->bases.nr);
2479+
for (size_t j = 0; j < format->bases.nr; j++) {
2480+
struct ahead_behind_count *count;
2481+
count = &array->counts[array->counts_nr++];
2482+
count->tip_index = commits_nr;
2483+
count->base_index = j;
2484+
2485+
array->items[i]->counts[j] = count;
2486+
}
2487+
commits_nr++;
2488+
}
2489+
2490+
ahead_behind(r, commits, commits_nr, array->counts, array->counts_nr);
2491+
free(commits);
2492+
}
2493+
24212494
/*
24222495
* API for filtering a set of refs. Based on the type of refs the user
24232496
* has requested, we iterate through those refs and apply filters

ref-filter.h

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "refs.h"
66
#include "commit.h"
77
#include "parse-options.h"
8+
#include "string-list.h"
89

910
/* Quoting styles */
1011
#define QUOTE_NONE 0
@@ -24,6 +25,7 @@
2425

2526
struct atom_value;
2627
struct ref_sorting;
28+
struct ahead_behind_count;
2729

2830
enum ref_sorting_order {
2931
REF_SORTING_REVERSE = 1<<0,
@@ -40,13 +42,18 @@ struct ref_array_item {
4042
const char *symref;
4143
struct commit *commit;
4244
struct atom_value *value;
45+
struct ahead_behind_count **counts;
46+
4347
char refname[FLEX_ARRAY];
4448
};
4549

4650
struct ref_array {
4751
int nr, alloc;
4852
struct ref_array_item **items;
4953
struct rev_info *revs;
54+
55+
struct ahead_behind_count *counts;
56+
size_t counts_nr;
5057
};
5158

5259
struct ref_filter {
@@ -80,9 +87,15 @@ struct ref_format {
8087

8188
/* Internal state to ref-filter */
8289
int need_color_reset_at_eol;
90+
91+
/* List of bases for ahead-behind counts. */
92+
struct string_list bases;
8393
};
8494

85-
#define REF_FORMAT_INIT { .use_color = -1 }
95+
#define REF_FORMAT_INIT { \
96+
.use_color = -1, \
97+
.bases = STRING_LIST_INIT_DUP, \
98+
}
8699

87100
/* Macros for checking --merged and --no-merged options */
88101
#define _OPT_MERGED_NO_MERGED(option, filter, h) \
@@ -143,4 +156,15 @@ struct ref_array_item *ref_array_push(struct ref_array *array,
143156
const char *refname,
144157
const struct object_id *oid);
145158

159+
/*
160+
* If the provided format includes ahead-behind atoms, then compute the
161+
* ahead-behind values for the array of filtered references. Must be
162+
* called after filter_refs() but before outputting the formatted refs.
163+
*
164+
* If this is not called, then any ahead-behind atoms will be blank.
165+
*/
166+
void filter_ahead_behind(struct repository *r,
167+
struct ref_format *format,
168+
struct ref_array *array);
169+
146170
#endif /* REF_FILTER_H */

t/perf/p1500-graph-walks.sh

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/bin/sh
2+
3+
test_description='Commit walk performance tests'
4+
. ./perf-lib.sh
5+
6+
test_perf_large_repo
7+
8+
test_expect_success 'setup' '
9+
git for-each-ref --format="%(refname)" "refs/heads/*" "refs/tags/*" >allrefs &&
10+
sort -r allrefs | head -n 50 >refs &&
11+
for ref in $(cat refs)
12+
do
13+
git branch -f ref-$ref $ref &&
14+
echo ref-$ref ||
15+
return 1
16+
done >branches &&
17+
for ref in $(cat refs)
18+
do
19+
git tag -f tag-$ref $ref &&
20+
echo tag-$ref ||
21+
return 1
22+
done >tags &&
23+
git commit-graph write --reachable
24+
'
25+
26+
test_perf 'ahead-behind counts: git for-each-ref' '
27+
git for-each-ref --format="%(ahead-behind:HEAD)" --stdin <refs
28+
'
29+
30+
test_perf 'ahead-behind counts: git branch' '
31+
xargs git branch -l --format="%(ahead-behind:HEAD)" <branches
32+
'
33+
34+
test_perf 'ahead-behind counts: git tag' '
35+
xargs git tag -l --format="%(ahead-behind:HEAD)" <tags
36+
'
37+
38+
test_perf 'ahead-behind counts: git rev-list' '
39+
for r in $(cat refs)
40+
do
41+
git rev-list --count "HEAD..$r" || return 1
42+
done
43+
'
44+
45+
test_done

t/t3203-branch-output.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,20 @@ test_expect_success 'git branch --format option' '
337337
test_cmp expect actual
338338
'
339339

340+
test_expect_success 'git branch --format with ahead-behind' '
341+
cat >expect <<-\EOF &&
342+
(HEAD detached from fromtag) 0 0
343+
refs/heads/ambiguous 0 0
344+
refs/heads/branch-one 1 0
345+
refs/heads/branch-two 0 0
346+
refs/heads/main 1 0
347+
refs/heads/ref-to-branch 1 0
348+
refs/heads/ref-to-remote 1 0
349+
EOF
350+
git branch --format="%(refname) %(ahead-behind:HEAD)" >actual &&
351+
test_cmp expect actual
352+
'
353+
340354
test_expect_success 'git branch with --format=%(rest) must fail' '
341355
test_must_fail git branch --format="%(rest)" >actual
342356
'

t/t6301-for-each-ref-errors.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,18 @@ test_expect_success 'Missing objects are reported correctly' '
5454
test_must_be_empty brief-err
5555
'
5656

57+
test_expect_success 'ahead-behind requires an argument' '
58+
test_must_fail git for-each-ref \
59+
--format="%(ahead-behind)" 2>err &&
60+
echo "fatal: expected format: %(ahead-behind:<committish>)" >expect &&
61+
test_cmp expect err
62+
'
63+
64+
test_expect_success 'missing ahead-behind base' '
65+
test_must_fail git for-each-ref \
66+
--format="%(ahead-behind:refs/heads/missing)" 2>err &&
67+
echo "fatal: failed to find '\''refs/heads/missing'\''" >expect &&
68+
test_cmp expect err
69+
'
70+
5771
test_done

0 commit comments

Comments
 (0)