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
This commit is contained in:
@ -384,6 +384,14 @@ Default mode::
|
|||||||
merges from the resulting history, as there are no selected
|
merges from the resulting history, as there are no selected
|
||||||
commits contributing to this merge.
|
commits contributing to this merge.
|
||||||
|
|
||||||
|
--ancestry-path::
|
||||||
|
|
||||||
|
When given a range of commits to display (e.g. 'commit1..commit2'
|
||||||
|
or 'commit2 {caret}commit1'), only display commits that exist
|
||||||
|
directly on the ancestry chain between the 'commit1' and
|
||||||
|
'commit2', i.e. commits that are both descendants of 'commit1',
|
||||||
|
and ancestors of 'commit2'.
|
||||||
|
|
||||||
A more detailed explanation follows.
|
A more detailed explanation follows.
|
||||||
|
|
||||||
Suppose you specified `foo` as the <paths>. We shall call commits
|
Suppose you specified `foo` as the <paths>. We shall call commits
|
||||||
@ -440,7 +448,7 @@ This results in:
|
|||||||
+
|
+
|
||||||
-----------------------------------------------------------------------
|
-----------------------------------------------------------------------
|
||||||
.-A---N---O
|
.-A---N---O
|
||||||
/ /
|
/ / /
|
||||||
I---------D
|
I---------D
|
||||||
-----------------------------------------------------------------------
|
-----------------------------------------------------------------------
|
||||||
+
|
+
|
||||||
@ -511,8 +519,6 @@ Note that without '\--full-history', this still simplifies merges: if
|
|||||||
one of the parents is TREESAME, we follow only that one, so the other
|
one of the parents is TREESAME, we follow only that one, so the other
|
||||||
sides of the merge are never walked.
|
sides of the merge are never walked.
|
||||||
|
|
||||||
Finally, there is a fourth simplification mode available:
|
|
||||||
|
|
||||||
--simplify-merges::
|
--simplify-merges::
|
||||||
|
|
||||||
First, build a history graph in the same way that
|
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':
|
|||||||
removed completely, because it had one parent and is TREESAME.
|
removed completely, because it had one parent and is TREESAME.
|
||||||
--
|
--
|
||||||
|
|
||||||
|
Finally, there is a fifth simplification mode available:
|
||||||
|
|
||||||
|
--ancestry-path::
|
||||||
|
|
||||||
|
Limit the displayed commits to those directly on the ancestry
|
||||||
|
chain between the "from" and "to" commits in the given commit
|
||||||
|
range. I.e. only display commits that are ancestor of the "to"
|
||||||
|
commit, and descendants of the "from" commit.
|
||||||
|
+
|
||||||
|
As an example use case, consider the following commit history:
|
||||||
|
+
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
D---E-------F
|
||||||
|
/ \ \
|
||||||
|
B---C---G---H---I---J
|
||||||
|
/ \
|
||||||
|
A-------K---------------L--M
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
+
|
||||||
|
A regular 'D..M' computes the set of commits that are ancestors of `M`,
|
||||||
|
but excludes the ones that are ancestors of `D`. This is useful to see
|
||||||
|
what happened to the history leading to `M` since `D`, in the sense
|
||||||
|
that "what does `M` have that did not exist in `D`". The result in this
|
||||||
|
example would be all the commits, except `A` and `B` (and `D` itself,
|
||||||
|
of course).
|
||||||
|
+
|
||||||
|
When we want to find out what commits in `M` are contaminated with the
|
||||||
|
bug introduced by `D` and need fixing, however, we might want to view
|
||||||
|
only the subset of 'D..M' that are actually descendants of `D`, i.e.
|
||||||
|
excluding `C` and `K`. This is exactly what the '\--ancestry-path'
|
||||||
|
option does. Applied to the 'D..M' range, it results in:
|
||||||
|
+
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
E-------F
|
||||||
|
\ \
|
||||||
|
G---H---I---J
|
||||||
|
\
|
||||||
|
L--M
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
|
||||||
The '\--simplify-by-decoration' option allows you to view only the
|
The '\--simplify-by-decoration' option allows you to view only the
|
||||||
big picture of the topology of the history, by omitting commits
|
big picture of the topology of the history, by omitting commits
|
||||||
that are not referenced by tags. Commits are marked as !TREESAME
|
that are not referenced by tags. Commits are marked as !TREESAME
|
||||||
|
103
revision.c
103
revision.c
@ -646,6 +646,93 @@ static int still_interesting(struct commit_list *src, unsigned long date, int sl
|
|||||||
return slop-1;
|
return slop-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* "rev-list --ancestry-path A..B" computes commits that are ancestors
|
||||||
|
* of B but not ancestors of A but further limits the result to those
|
||||||
|
* that are descendants of A. This takes the list of bottom commits and
|
||||||
|
* the result of "A..B" without --ancestry-path, and limits the latter
|
||||||
|
* further to the ones that can reach one of the commits in "bottom".
|
||||||
|
*/
|
||||||
|
static void limit_to_ancestry(struct commit_list *bottom, struct commit_list *list)
|
||||||
|
{
|
||||||
|
struct commit_list *p;
|
||||||
|
struct commit_list *rlist = NULL;
|
||||||
|
int made_progress;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Reverse the list so that it will be likely that we would
|
||||||
|
* process parents before children.
|
||||||
|
*/
|
||||||
|
for (p = list; p; p = p->next)
|
||||||
|
commit_list_insert(p->item, &rlist);
|
||||||
|
|
||||||
|
for (p = bottom; p; p = p->next)
|
||||||
|
p->item->object.flags |= TMP_MARK;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Mark the ones that can reach bottom commits in "list",
|
||||||
|
* in a bottom-up fashion.
|
||||||
|
*/
|
||||||
|
do {
|
||||||
|
made_progress = 0;
|
||||||
|
for (p = rlist; p; p = p->next) {
|
||||||
|
struct commit *c = p->item;
|
||||||
|
struct commit_list *parents;
|
||||||
|
if (c->object.flags & (TMP_MARK | UNINTERESTING))
|
||||||
|
continue;
|
||||||
|
for (parents = c->parents;
|
||||||
|
parents;
|
||||||
|
parents = parents->next) {
|
||||||
|
if (!(parents->item->object.flags & TMP_MARK))
|
||||||
|
continue;
|
||||||
|
c->object.flags |= TMP_MARK;
|
||||||
|
made_progress = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (made_progress);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NEEDSWORK: decide if we want to remove parents that are
|
||||||
|
* not marked with TMP_MARK from commit->parents for commits
|
||||||
|
* in the resulting list. We may not want to do that, though.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The ones that are not marked with TMP_MARK are uninteresting
|
||||||
|
*/
|
||||||
|
for (p = list; p; p = p->next) {
|
||||||
|
struct commit *c = p->item;
|
||||||
|
if (c->object.flags & TMP_MARK)
|
||||||
|
continue;
|
||||||
|
c->object.flags |= UNINTERESTING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We are done with the TMP_MARK */
|
||||||
|
for (p = list; p; p = p->next)
|
||||||
|
p->item->object.flags &= ~TMP_MARK;
|
||||||
|
for (p = bottom; p; p = p->next)
|
||||||
|
p->item->object.flags &= ~TMP_MARK;
|
||||||
|
free_commit_list(rlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Before walking the history, keep the set of "negative" refs the
|
||||||
|
* caller has asked to exclude.
|
||||||
|
*
|
||||||
|
* This is used to compute "rev-list --ancestry-path A..B", as we need
|
||||||
|
* to filter the result of "A..B" further to the ones that can actually
|
||||||
|
* reach A.
|
||||||
|
*/
|
||||||
|
static struct commit_list *collect_bottom_commits(struct commit_list *list)
|
||||||
|
{
|
||||||
|
struct commit_list *elem, *bottom = NULL;
|
||||||
|
for (elem = list; elem; elem = elem->next)
|
||||||
|
if (elem->item->object.flags & UNINTERESTING)
|
||||||
|
commit_list_insert(elem->item, &bottom);
|
||||||
|
return bottom;
|
||||||
|
}
|
||||||
|
|
||||||
static int limit_list(struct rev_info *revs)
|
static int limit_list(struct rev_info *revs)
|
||||||
{
|
{
|
||||||
int slop = SLOP;
|
int slop = SLOP;
|
||||||
@ -653,6 +740,13 @@ static int limit_list(struct rev_info *revs)
|
|||||||
struct commit_list *list = revs->commits;
|
struct commit_list *list = revs->commits;
|
||||||
struct commit_list *newlist = NULL;
|
struct commit_list *newlist = NULL;
|
||||||
struct commit_list **p = &newlist;
|
struct commit_list **p = &newlist;
|
||||||
|
struct commit_list *bottom = NULL;
|
||||||
|
|
||||||
|
if (revs->ancestry_path) {
|
||||||
|
bottom = collect_bottom_commits(list);
|
||||||
|
if (!bottom)
|
||||||
|
die("--ancestry-path given but there are no bottom commits");
|
||||||
|
}
|
||||||
|
|
||||||
while (list) {
|
while (list) {
|
||||||
struct commit_list *entry = list;
|
struct commit_list *entry = list;
|
||||||
@ -694,6 +788,11 @@ static int limit_list(struct rev_info *revs)
|
|||||||
if (revs->cherry_pick)
|
if (revs->cherry_pick)
|
||||||
cherry_pick_list(newlist, revs);
|
cherry_pick_list(newlist, revs);
|
||||||
|
|
||||||
|
if (bottom) {
|
||||||
|
limit_to_ancestry(bottom, newlist);
|
||||||
|
free_commit_list(bottom);
|
||||||
|
}
|
||||||
|
|
||||||
revs->commits = newlist;
|
revs->commits = newlist;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -1089,6 +1188,10 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
|
|||||||
revs->min_age = approxidate(arg + 8);
|
revs->min_age = approxidate(arg + 8);
|
||||||
} else if (!strcmp(arg, "--first-parent")) {
|
} else if (!strcmp(arg, "--first-parent")) {
|
||||||
revs->first_parent_only = 1;
|
revs->first_parent_only = 1;
|
||||||
|
} else if (!strcmp(arg, "--ancestry-path")) {
|
||||||
|
revs->ancestry_path = 1;
|
||||||
|
revs->simplify_history = 0;
|
||||||
|
revs->limited = 1;
|
||||||
} else if (!strcmp(arg, "-g") || !strcmp(arg, "--walk-reflogs")) {
|
} else if (!strcmp(arg, "-g") || !strcmp(arg, "--walk-reflogs")) {
|
||||||
init_reflog_walk(&revs->reflog_info);
|
init_reflog_walk(&revs->reflog_info);
|
||||||
} else if (!strcmp(arg, "--default")) {
|
} else if (!strcmp(arg, "--default")) {
|
||||||
|
@ -66,6 +66,7 @@ struct rev_info {
|
|||||||
reverse_output_stage:1,
|
reverse_output_stage:1,
|
||||||
cherry_pick:1,
|
cherry_pick:1,
|
||||||
bisect:1,
|
bisect:1,
|
||||||
|
ancestry_path:1,
|
||||||
first_parent_only:1;
|
first_parent_only:1;
|
||||||
|
|
||||||
/* Diff flags */
|
/* Diff flags */
|
||||||
|
73
t/t6019-rev-list-ancestry-path.sh
Executable file
73
t/t6019-rev-list-ancestry-path.sh
Executable file
@ -0,0 +1,73 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
test_description='--ancestry-path'
|
||||||
|
|
||||||
|
# D---E-------F
|
||||||
|
# / \ \
|
||||||
|
# B---C---G---H---I---J
|
||||||
|
# / \
|
||||||
|
# A-------K---------------L--M
|
||||||
|
#
|
||||||
|
# D..M == E F G H I J K L M
|
||||||
|
# --ancestry-path D..M == E F H I J L M
|
||||||
|
#
|
||||||
|
# D..M -- M.t == M
|
||||||
|
# --ancestry-path D..M -- M.t == M
|
||||||
|
|
||||||
|
. ./test-lib.sh
|
||||||
|
|
||||||
|
test_merge () {
|
||||||
|
test_tick &&
|
||||||
|
git merge -s ours -m "$2" "$1" &&
|
||||||
|
git tag "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_expect_success setup '
|
||||||
|
test_commit A &&
|
||||||
|
test_commit B &&
|
||||||
|
test_commit C &&
|
||||||
|
test_commit D &&
|
||||||
|
test_commit E &&
|
||||||
|
test_commit F &&
|
||||||
|
git reset --hard C &&
|
||||||
|
test_commit G &&
|
||||||
|
test_merge E H &&
|
||||||
|
test_commit I &&
|
||||||
|
test_merge F J &&
|
||||||
|
git reset --hard A &&
|
||||||
|
test_commit K &&
|
||||||
|
test_merge J L &&
|
||||||
|
test_commit M
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'rev-list D..M' '
|
||||||
|
for c in E F G H I J K L M; do echo $c; done >expect &&
|
||||||
|
git rev-list --format=%s D..M |
|
||||||
|
sed -e "/^commit /d" |
|
||||||
|
sort >actual &&
|
||||||
|
test_cmp expect actual
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'rev-list --ancestry-path D..M' '
|
||||||
|
for c in E F H I J L M; do echo $c; done >expect &&
|
||||||
|
git rev-list --ancestry-path --format=%s D..M |
|
||||||
|
sed -e "/^commit /d" |
|
||||||
|
sort >actual &&
|
||||||
|
test_cmp expect actual
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'rev-list D..M -- M.t' '
|
||||||
|
echo M >expect &&
|
||||||
|
git rev-list --format=%s D..M -- M.t |
|
||||||
|
sed -e "/^commit /d" >actual &&
|
||||||
|
test_cmp expect actual
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'rev-list --ancestry-patch D..M -- M.t' '
|
||||||
|
echo M >expect &&
|
||||||
|
git rev-list --ancestry-path --format=%s D..M -- M.t |
|
||||||
|
sed -e "/^commit /d" >actual &&
|
||||||
|
test_cmp expect actual
|
||||||
|
'
|
||||||
|
|
||||||
|
test_done
|
Reference in New Issue
Block a user