Skip to content

Commit a214afd

Browse files
committed
Merge branch 'jc/rev-list-ancestry-path'
* jc/rev-list-ancestry-path: revision: Turn off history simplification in --ancestry-path mode revision: Fix typo in --ancestry-path error message Documentation/rev-list-options.txt: Explain --ancestry-path Documentation/rev-list-options.txt: Fix missing line in example history graph revision: --ancestry-path
2 parents 13cbf01 + cb7529e commit a214afd

File tree

4 files changed

+226
-3
lines changed

4 files changed

+226
-3
lines changed

Documentation/rev-list-options.txt

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,14 @@ Default mode::
384384
merges from the resulting history, as there are no selected
385385
commits contributing to this merge.
386386

387+
--ancestry-path::
388+
389+
When given a range of commits to display (e.g. 'commit1..commit2'
390+
or 'commit2 {caret}commit1'), only display commits that exist
391+
directly on the ancestry chain between the 'commit1' and
392+
'commit2', i.e. commits that are both descendants of 'commit1',
393+
and ancestors of 'commit2'.
394+
387395
A more detailed explanation follows.
388396

389397
Suppose you specified `foo` as the <paths>. We shall call commits
@@ -440,7 +448,7 @@ This results in:
440448
+
441449
-----------------------------------------------------------------------
442450
.-A---N---O
443-
/ /
451+
/ / /
444452
I---------D
445453
-----------------------------------------------------------------------
446454
+
@@ -511,8 +519,6 @@ Note that without '\--full-history', this still simplifies merges: if
511519
one of the parents is TREESAME, we follow only that one, so the other
512520
sides of the merge are never walked.
513521

514-
Finally, there is a fourth simplification mode available:
515-
516522
--simplify-merges::
517523

518524
First, build a history graph in the same way that
@@ -554,6 +560,46 @@ Note the major differences in `N` and `P` over '\--full-history':
554560
removed completely, because it had one parent and is TREESAME.
555561
--
556562

563+
Finally, there is a fifth simplification mode available:
564+
565+
--ancestry-path::
566+
567+
Limit the displayed commits to those directly on the ancestry
568+
chain between the "from" and "to" commits in the given commit
569+
range. I.e. only display commits that are ancestor of the "to"
570+
commit, and descendants of the "from" commit.
571+
+
572+
As an example use case, consider the following commit history:
573+
+
574+
-----------------------------------------------------------------------
575+
D---E-------F
576+
/ \ \
577+
B---C---G---H---I---J
578+
/ \
579+
A-------K---------------L--M
580+
-----------------------------------------------------------------------
581+
+
582+
A regular 'D..M' computes the set of commits that are ancestors of `M`,
583+
but excludes the ones that are ancestors of `D`. This is useful to see
584+
what happened to the history leading to `M` since `D`, in the sense
585+
that "what does `M` have that did not exist in `D`". The result in this
586+
example would be all the commits, except `A` and `B` (and `D` itself,
587+
of course).
588+
+
589+
When we want to find out what commits in `M` are contaminated with the
590+
bug introduced by `D` and need fixing, however, we might want to view
591+
only the subset of 'D..M' that are actually descendants of `D`, i.e.
592+
excluding `C` and `K`. This is exactly what the '\--ancestry-path'
593+
option does. Applied to the 'D..M' range, it results in:
594+
+
595+
-----------------------------------------------------------------------
596+
E-------F
597+
\ \
598+
G---H---I---J
599+
\
600+
L--M
601+
-----------------------------------------------------------------------
602+
557603
The '\--simplify-by-decoration' option allows you to view only the
558604
big picture of the topology of the history, by omitting commits
559605
that are not referenced by tags. Commits are marked as !TREESAME

revision.c

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,13 +646,107 @@ static int still_interesting(struct commit_list *src, unsigned long date, int sl
646646
return slop-1;
647647
}
648648

649+
/*
650+
* "rev-list --ancestry-path A..B" computes commits that are ancestors
651+
* of B but not ancestors of A but further limits the result to those
652+
* that are descendants of A. This takes the list of bottom commits and
653+
* the result of "A..B" without --ancestry-path, and limits the latter
654+
* further to the ones that can reach one of the commits in "bottom".
655+
*/
656+
static void limit_to_ancestry(struct commit_list *bottom, struct commit_list *list)
657+
{
658+
struct commit_list *p;
659+
struct commit_list *rlist = NULL;
660+
int made_progress;
661+
662+
/*
663+
* Reverse the list so that it will be likely that we would
664+
* process parents before children.
665+
*/
666+
for (p = list; p; p = p->next)
667+
commit_list_insert(p->item, &rlist);
668+
669+
for (p = bottom; p; p = p->next)
670+
p->item->object.flags |= TMP_MARK;
671+
672+
/*
673+
* Mark the ones that can reach bottom commits in "list",
674+
* in a bottom-up fashion.
675+
*/
676+
do {
677+
made_progress = 0;
678+
for (p = rlist; p; p = p->next) {
679+
struct commit *c = p->item;
680+
struct commit_list *parents;
681+
if (c->object.flags & (TMP_MARK | UNINTERESTING))
682+
continue;
683+
for (parents = c->parents;
684+
parents;
685+
parents = parents->next) {
686+
if (!(parents->item->object.flags & TMP_MARK))
687+
continue;
688+
c->object.flags |= TMP_MARK;
689+
made_progress = 1;
690+
break;
691+
}
692+
}
693+
} while (made_progress);
694+
695+
/*
696+
* NEEDSWORK: decide if we want to remove parents that are
697+
* not marked with TMP_MARK from commit->parents for commits
698+
* in the resulting list. We may not want to do that, though.
699+
*/
700+
701+
/*
702+
* The ones that are not marked with TMP_MARK are uninteresting
703+
*/
704+
for (p = list; p; p = p->next) {
705+
struct commit *c = p->item;
706+
if (c->object.flags & TMP_MARK)
707+
continue;
708+
c->object.flags |= UNINTERESTING;
709+
}
710+
711+
/* We are done with the TMP_MARK */
712+
for (p = list; p; p = p->next)
713+
p->item->object.flags &= ~TMP_MARK;
714+
for (p = bottom; p; p = p->next)
715+
p->item->object.flags &= ~TMP_MARK;
716+
free_commit_list(rlist);
717+
}
718+
719+
/*
720+
* Before walking the history, keep the set of "negative" refs the
721+
* caller has asked to exclude.
722+
*
723+
* This is used to compute "rev-list --ancestry-path A..B", as we need
724+
* to filter the result of "A..B" further to the ones that can actually
725+
* reach A.
726+
*/
727+
static struct commit_list *collect_bottom_commits(struct commit_list *list)
728+
{
729+
struct commit_list *elem, *bottom = NULL;
730+
for (elem = list; elem; elem = elem->next)
731+
if (elem->item->object.flags & UNINTERESTING)
732+
commit_list_insert(elem->item, &bottom);
733+
return bottom;
734+
}
735+
649736
static int limit_list(struct rev_info *revs)
650737
{
651738
int slop = SLOP;
652739
unsigned long date = ~0ul;
653740
struct commit_list *list = revs->commits;
654741
struct commit_list *newlist = NULL;
655742
struct commit_list **p = &newlist;
743+
struct commit_list *bottom = NULL;
744+
745+
if (revs->ancestry_path) {
746+
bottom = collect_bottom_commits(list);
747+
if (!bottom)
748+
die("--ancestry-path given but there are no bottom commits");
749+
}
656750

657751
while (list) {
658752
struct commit_list *entry = list;
@@ -694,6 +788,11 @@ static int limit_list(struct rev_info *revs)
694788
if (revs->cherry_pick)
695789
cherry_pick_list(newlist, revs);
696790

791+
if (bottom) {
792+
limit_to_ancestry(bottom, newlist);
793+
free_commit_list(bottom);
794+
}
795+
697796
revs->commits = newlist;
698797
return 0;
699798
}
@@ -1089,6 +1188,10 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
10891188
revs->min_age = approxidate(arg + 8);
10901189
} else if (!strcmp(arg, "--first-parent")) {
10911190
revs->first_parent_only = 1;
1191+
} else if (!strcmp(arg, "--ancestry-path")) {
1192+
revs->ancestry_path = 1;
1193+
revs->simplify_history = 0;
1194+
revs->limited = 1;
10921195
} else if (!strcmp(arg, "-g") || !strcmp(arg, "--walk-reflogs")) {
10931196
init_reflog_walk(&revs->reflog_info);
10941197
} else if (!strcmp(arg, "--default")) {

revision.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ struct rev_info {
6666
reverse_output_stage:1,
6767
cherry_pick:1,
6868
bisect:1,
69+
ancestry_path:1,
6970
first_parent_only:1;
7071

7172
/* Diff flags */

t/t6019-rev-list-ancestry-path.sh

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/bin/sh
2+
3+
test_description='--ancestry-path'
4+
5+
# D---E-------F
6+
# / \ \
7+
# B---C---G---H---I---J
8+
# / \
9+
# A-------K---------------L--M
10+
#
11+
# D..M == E F G H I J K L M
12+
# --ancestry-path D..M == E F H I J L M
13+
#
14+
# D..M -- M.t == M
15+
# --ancestry-path D..M -- M.t == M
16+
17+
. ./test-lib.sh
18+
19+
test_merge () {
20+
test_tick &&
21+
git merge -s ours -m "$2" "$1" &&
22+
git tag "$2"
23+
}
24+
25+
test_expect_success setup '
26+
test_commit A &&
27+
test_commit B &&
28+
test_commit C &&
29+
test_commit D &&
30+
test_commit E &&
31+
test_commit F &&
32+
git reset --hard C &&
33+
test_commit G &&
34+
test_merge E H &&
35+
test_commit I &&
36+
test_merge F J &&
37+
git reset --hard A &&
38+
test_commit K &&
39+
test_merge J L &&
40+
test_commit M
41+
'
42+
43+
test_expect_success 'rev-list D..M' '
44+
for c in E F G H I J K L M; do echo $c; done >expect &&
45+
git rev-list --format=%s D..M |
46+
sed -e "/^commit /d" |
47+
sort >actual &&
48+
test_cmp expect actual
49+
'
50+
51+
test_expect_success 'rev-list --ancestry-path D..M' '
52+
for c in E F H I J L M; do echo $c; done >expect &&
53+
git rev-list --ancestry-path --format=%s D..M |
54+
sed -e "/^commit /d" |
55+
sort >actual &&
56+
test_cmp expect actual
57+
'
58+
59+
test_expect_success 'rev-list D..M -- M.t' '
60+
echo M >expect &&
61+
git rev-list --format=%s D..M -- M.t |
62+
sed -e "/^commit /d" >actual &&
63+
test_cmp expect actual
64+
'
65+
66+
test_expect_success 'rev-list --ancestry-patch D..M -- M.t' '
67+
echo M >expect &&
68+
git rev-list --ancestry-path --format=%s D..M -- M.t |
69+
sed -e "/^commit /d" >actual &&
70+
test_cmp expect actual
71+
'
72+
73+
test_done

0 commit comments

Comments
 (0)