threads / discuss / 23537

Getting 'git log' (or something else) to show me the relevant sub-graph?

Subject: Getting 'git log' (or something else) to show me the relevant sub-graph?

## tl;dr

11 messages between Apr 20, 2010 and Apr 21, 2010.

replies: 10people: 4as markdown or json

Johan Herland· Apr 20, 2010, 14:49 UTC · lore
Hi,
Consider the following (simplified) history:
---O---O---O---O---O---O---O---O---O---O---O---O---M2--H  <-- mainline
    \                                             /
     O---O---O---O---O---O---O---M1--D---E---F---G  <-- dev-branch
              \                 /
               O---O---A---B---C  <-- topic-branch

Now, assume that I have bisected my way through to 'A', and found that it introduces some bug. Now, I'm interested in visualizing the path that this bug "travelled" to get into "mainline", i.e. the following sub-graph:

                          --M2--H  <-- mainline
                           /
        --M1--D---E---F---G  <-- dev-branch
         /
A---B---C  <-- topic-branch
In other words, I'm interested in the following log (with decorations):

H (mainline) M2 G (dev-branch) F E D M1 C (topic-branch) B A

I have unsuccessfully dug through the 'git log' documentation to figure out if it can produce this log, so I'm now throwing the question to the almighty knowledge of the mailing list...

Here are some of my closest attempts, so far:
- git branch --contains A
	gives me "topic-branch", "dev-branch" and "mainline", which is
	relevant, but incomplete.
- git log --oneline --decorate --graph A^..mainline
	gives me a log/graph where I can search for A and then use the graph
	to trace the way back up to "mainline", but it still displays a lot of
	uninteresting commits (ancestors of M1 and M2) that I have to
	disregard. Although this is ok once in a while, the problem is common
	enough (and the real-world graphs complicated enough), that I'd like a
	better solution, if possible.

I guess what I'm looking for is something similar to --first-parent, except instead of the _first_ parent, it should follow the _relevant_ parent, as far as the relationship between A and "mainline" is concerned.

In set-theory terms I guess what I want is "that which is both an ancestor of H, and a descendant of A (inclusive)", but I don't know how to explain this to 'git log'.

Thanks for any help,
...Johan
-- 
Johan Herland, <johan@herland.net>
www.herland.net
Michael J Gruber· Apr 20, 2010, 15:18 UTC · re: Johan Herland · lore

Re: Getting 'git log' (or something else) to show me the relevant sub-graph?

Johan Herland venit, vidit, dixit 20.04.2010 16:49:
Show 60 quoted lines
> Hi,
> 
> Consider the following (simplified) history:
> 
> ---O---O---O---O---O---O---O---O---O---O---O---O---M2--H  <-- mainline
>     \                                             /
>      O---O---O---O---O---O---O---M1--D---E---F---G  <-- dev-branch
>               \                 /
>                O---O---A---B---C  <-- topic-branch
> 
> Now, assume that I have bisected my way through to 'A', and found that 
> it introduces some bug. Now, I'm interested in visualizing the path 
> that this bug "travelled" to get into "mainline", i.e. the following 
> sub-graph:
> 
>                           --M2--H  <-- mainline
>                            /
>         --M1--D---E---F---G  <-- dev-branch
>          /
> A---B---C  <-- topic-branch
> 
> In other words, I'm interested in the following log (with decorations):
> 
> H (mainline)
> M2
> G (dev-branch)
> F
> E
> D
> M1
> C (topic-branch)
> B
> A
> 
> I have unsuccessfully dug through the 'git log' documentation to figure 
> out if it can produce this log, so I'm now throwing the question to the 
> almighty knowledge of the mailing list...
> 
> Here are some of my closest attempts, so far:
> 
> - git branch --contains A
> 	gives me "topic-branch", "dev-branch" and "mainline", which is
> 	relevant, but incomplete.
> 
> - git log --oneline --decorate --graph A^..mainline
> 	gives me a log/graph where I can search for A and then use the graph
> 	to trace the way back up to "mainline", but it still displays a lot of
> 	uninteresting commits (ancestors of M1 and M2) that I have to
> 	disregard. Although this is ok once in a while, the problem is common
> 	enough (and the real-world graphs complicated enough), that I'd like a
> 	better solution, if possible.
> 
> I guess what I'm looking for is something similar to --first-parent, 
> except instead of the _first_ parent, it should follow the _relevant_ 
> parent, as far as the relationship between A and "mainline" is 
> concerned.
> 
> In set-theory terms I guess what I want is "that which is both an 
> ancestor of H, and a descendant of A (inclusive)", but I don't know how 
> to explain this to 'git log'.

This would require a reverse tree walker for the first half of that set intersection. It would come in handy in many situations, but we don't have it.

Mathematically speaking it is trivial to associate to a directed graph the graph with opposite orientation. Then we could walk as usual...

But, in fact, we do not "walk then intersect", but the walker implements the intersection, so having that other dag would not even help.

Maybe we can teach the walker to mark commits as uninteresting which do not have A in their ancestry?

Michael
Junio C Hamano· Apr 20, 2010, 20:45 UTC · re: Johan Herland · lore

Re: Getting 'git log' (or something else) to show me the relevant sub-graph?

Johan Herland <johan@herland.net> writes:
Show 10 quoted lines
> Now, assume that I have bisected my way through to 'A', and found that 
> it introduces some bug. Now, I'm interested in visualizing the path 
> that this bug "travelled" to get into "mainline", i.e. the following 
> sub-graph:
>
>                           --M2--H  <-- mainline
>                            /
>         --M1--D---E---F---G  <-- dev-branch
>          /
> A---B---C  <-- topic-branch
Interesting.  I am looking into it ;-)
Junio C Hamano· Apr 20, 2010, 22:10 UTC · re: Junio C Hamano · lore

[PATCH] revision: --ancestry-path

This is somewhat a rough-cut, as I haven't thought through possible interactions this new option may have with "rev-list --parents" and "rev-list --graph" (see "NEEDSWORK" in revision.c), but I'll leave that to people who are more interested in this topic than myself ;-).

-- >8 -- "rev-list A..H" computes the set of commits that are ancestors of H, but excludes the ones that are ancestors of A. This is useful to see what happened to the history leading to H since A, in the sense that "what does H have that did not exist in A" (e.g. when you have a choice to update to H from A).

	       x---x---A---B---C  <-- topic
	      /			\
     x---x---x---o---o---o---o---M---D---E---F---G  <-- dev
    /						  \
   x---o---o---o---o---o---o---o---o---o---o---o---N---H  <-- master

The result in the above example would be the commits marked with caps letters (except for A itself, of course), and the ones marked with 'o'.

When you want to find out what commits in H are contaminated with the bug introduced by A and need fixing, however, you might want to view only the subset of "A..B" that are actually descendants of A, i.e. excluding the ones marked with 'o'. Introduce a new option --ancestry-path to compute this set with "rev-list --ancestry-path A..B".

Note that in practice, you would build a fix immediately on top of A and "git branch --contains A" will give the names of branches that you would need to merge the fix into (i.e. topic, dev and master), so this may not be worth paying the extra cost of postprocessing.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 revision.c                        |  106 +++++++++++++++++++++++++++++++++++++
 revision.h                        |    1 +
 t/t6019-rev-list-ancestry-path.sh |   56 +++++++++++++++++++
 3 files changed, 163 insertions(+), 0 deletions(-)
 create mode 100755 t/t6019-rev-list-ancestry-path.sh
diff --git a/revision.c b/revision.c
index f4b8b38..cb91094 100644
--- a/revision.c
+++ b/revision.c
@@ -646,6 +646,99 @@ static int still_interesting(struct commit_list *src, unsigned long date, int sl
 	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;
+
+	/*
+	 * Clear TMP_MARK and reverse the list so that it will be likely
+	 * that we would process parents before children.
+	 */
+	for (p = list; p; p = p->next) {
+		p->item->object.flags &= ~TMP_MARK;
+		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) {
+			c->object.flags &= ~TMP_MARK;
+			continue;
+		}
+		c->object.flags |= UNINTERESTING;
+	}
+
+	/* Release the bottom list */
+	while (bottom) {
+		p = bottom->next;
+		bottom->item->object.flags &= ~TMP_MARK;
+		free(bottom);
+		bottom = p;
+	}
+	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)
 {
 	int slop = SLOP;
@@ -653,6 +746,13 @@ static int limit_list(struct rev_info *revs)
 	struct commit_list *list = revs->commits;
 	struct commit_list *newlist = NULL;
 	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 is no bottom commits");
+	}
 
 	while (list) {
 		struct commit_list *entry = list;
@@ -694,6 +794,9 @@ static int limit_list(struct rev_info *revs)
 	if (revs->cherry_pick)
 		cherry_pick_list(newlist, revs);
 
+	if (bottom)
+		limit_to_ancestry(bottom, newlist);
+
 	revs->commits = newlist;
 	return 0;
 }
@@ -1089,6 +1192,9 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
 		revs->min_age = approxidate(arg + 8);
 	} else if (!strcmp(arg, "--first-parent")) {
 		revs->first_parent_only = 1;
+	} else if (!strcmp(arg, "--ancestry-path")) {
+		revs->ancestry_path = 1;
+		revs->limited = 1;
 	} else if (!strcmp(arg, "-g") || !strcmp(arg, "--walk-reflogs")) {
 		init_reflog_walk(&revs->reflog_info);
 	} else if (!strcmp(arg, "--default")) {
diff --git a/revision.h b/revision.h
index 568f1c9..855464f 100644
--- a/revision.h
+++ b/revision.h
@@ -66,6 +66,7 @@ struct rev_info {
 			reverse_output_stage:1,
 			cherry_pick:1,
 			bisect:1,
+			ancestry_path:1,
 			first_parent_only:1;
 
 	/* Diff flags */
diff --git a/t/t6019-rev-list-ancestry-path.sh b/t/t6019-rev-list-ancestry-path.sh
new file mode 100755
index 0000000..0230724
--- /dev/null
+++ b/t/t6019-rev-list-ancestry-path.sh
@@ -0,0 +1,56 @@
+#!/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
+
+. ./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_done
-- 
1.7.1.rc2.265.g8743f
Johan Herland· Apr 21, 2010, 07:34 UTC · re: Junio C Hamano · lore

Re: [PATCH] revision: --ancestry-path

On Wednesday 21 April 2010, Junio C Hamano wrote:
> This is somewhat a rough-cut, as I haven't thought through possible
> interactions this new option may have with "rev-list --parents" and
> "rev-list --graph" (see "NEEDSWORK" in revision.c), but I'll leave that
> to people who are more interested in this topic than myself ;-).
Thanks! :)

I can confirm that this patch works with my original (more complicated) scenario. I've also played around with combinations of --ancestry-path, -- graph and --parents (and even --boundary), and AFAICS, the new option does not clobber the other options, and (IMHO) it all behaves correctly in the scenarios I've tested. Consider it:

Tested-by: Johan Herland <johan@herland.net>

I've also reviewed the patch, and it all looks good to me (i.e. "Acked-by", except I'm not super-familiar in the relevant area of the code).

What more should I do to push this forwards towards 'next'?
...Johan
-- 
Johan Herland, <johan@herland.net>
www.herland.net
Johannes Sixt· Apr 21, 2010, 07:47 UTC · re: Johan Herland · lore

Re: [PATCH] revision: --ancestry-path

Am 4/21/2010 9:34, schrieb Johan Herland:
Show 5 quoted lines
> I can confirm that this patch works with my original (more complicated) 
> scenario. I've also played around with combinations of --ancestry-path, --
> graph and --parents (and even --boundary), and AFAICS, the new option does 
> not clobber the other options, and (IMHO) it all behaves correctly in the 
> scenarios I've tested.

How about possible interactions with --reverse; --date-order/--topo-order, --parents (important for gitk); --simplify-by-decoration (useful for your problem that triggered this); --full-history and --simplify-merges with path limiting?

-- Hannes
Junio C Hamano· Apr 21, 2010, 08:01 UTC · re: Johannes Sixt · lore

Re: [PATCH] revision: --ancestry-path

Johannes Sixt <j.sixt@viscovery.net> writes:
Show 11 quoted lines
> Am 4/21/2010 9:34, schrieb Johan Herland:
>> I can confirm that this patch works with my original (more complicated) 
>> scenario. I've also played around with combinations of --ancestry-path, --
>> graph and --parents (and even --boundary), and AFAICS, the new option does 
>> not clobber the other options, and (IMHO) it all behaves correctly in the 
>> scenarios I've tested.
>
> How about possible interactions with --reverse; --date-order/--topo-order,
> --parents (important for gitk); --simplify-by-decoration (useful for your
> problem that triggered this); --full-history and --simplify-merges with
> path limiting?
These are all good points.

I am reasonably sure that parents (specifically, "rewrite_parents") is broken. The new function should cull parents that do not appear on the ancestry path from merges (that is what "NEEDSWORK" is about). It may or may not break gitk, though---these off-path parents are shown as parents of an on-path merge but will be marked as UNINTERESTING.

I do not think reverse/date-order/topo-order would be affected by this change, as they only change the presentation order of what limit_list() returns;

Also simplify-merges and full-history should be Ok, as they control what is done in the current logic in limit_list(), which is an input to the new logic, meaning that the new logic will work on already simplified history.

This is not a new problem, but I strongly suspect that cherry-pick is broken the same way wrt "rewrite_parents".

Junio C Hamano· Apr 21, 2010, 08:19 UTC · re: Junio C Hamano · lore

[PATCH v2] revision: --ancestry-path

"rev-list A..H" computes the set of commits that are ancestors of H, but excludes the ones that are ancestors of A. This is useful to see what happened to the history leading to H since A, in the sense that "what does H have that did not exist in A" (e.g. when you have a choice to update to H from A).

	       x---x---A---B---C  <-- topic
	      /			\
     x---x---x---o---o---o---o---M---D---E---F---G  <-- dev
    /						  \
   x---o---o---o---o---o---o---o---o---o---o---o---N---H  <-- master

The result in the above example would be the commits marked with caps letters (except for A itself, of course), and the ones marked with 'o'.

When you want to find out what commits in H are contaminated with the bug introduced by A and need fixing, however, you might want to view only the subset of "A..B" that are actually descendants of A, i.e. excluding the ones marked with 'o'. Introduce a new option --ancestry-path to compute this set with "rev-list --ancestry-path A..B".

Note that in practice, you would build a fix immediately on top of A and "git branch --contains A" will give the names of branches that you would need to merge the fix into (i.e. topic, dev and master), so this may not be worth paying the extra cost of postprocessing.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 * This simplifies the handling of TMP_MARK (the contract among the
   functions is that this is used only locally and the user is responsible
   for cleaning them up after use, so there is no need to clear the mark
   upfront), and also makes it responsibility of the caller to free the
   "bottom" list.
   Interdiff since v1 looks like this.
    diff -u b/revision.c b/revision.c
    --- b/revision.c
    +++ b/revision.c
    @@ -660,13 +660,11 @@
     	int made_progress;
     
     	/*
    -	 * Clear TMP_MARK and reverse the list so that it will be likely
    -	 * that we would process parents before children.
    +	 * Reverse the list so that it will be likely that we would
    +	 * process parents before children.
     	 */
    -	for (p = list; p; p = p->next) {
    -		p->item->object.flags &= ~TMP_MARK;
    +	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;
    @@ -705,20 +703,16 @@
     	 */
     	for (p = list; p; p = p->next) {
     		struct commit *c = p->item;
    -		if (c->object.flags & TMP_MARK) {
    -			c->object.flags &= ~TMP_MARK;
    +		if (c->object.flags & TMP_MARK)
     			continue;
    -		}
     		c->object.flags |= UNINTERESTING;
     	}
     
    -	/* Release the bottom list */
    -	while (bottom) {
    -		p = bottom->next;
    -		bottom->item->object.flags &= ~TMP_MARK;
    -		free(bottom);
    -		bottom = p;
    -	}
    +	/* 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);
     }
     
    @@ -794,8 +788,10 @@
     	if (revs->cherry_pick)
     		cherry_pick_list(newlist, revs);
     
    -	if (bottom)
    +	if (bottom) {
     		limit_to_ancestry(bottom, newlist);
    +		free_commit_list(bottom);
    +	}
     
     	revs->commits = newlist;
     	return 0;
 revision.c                        |  102 +++++++++++++++++++++++++++++++++++++
 revision.h                        |    1 +
 t/t6019-rev-list-ancestry-path.sh |   56 ++++++++++++++++++++
 3 files changed, 159 insertions(+), 0 deletions(-)
 create mode 100755 t/t6019-rev-list-ancestry-path.sh
diff --git a/revision.c b/revision.c
index f4b8b38..71fec3c 100644
--- a/revision.c
+++ b/revision.c
@@ -646,6 +646,93 @@ static int still_interesting(struct commit_list *src, unsigned long date, int sl
 	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)
 {
 	int slop = SLOP;
@@ -653,6 +740,13 @@ static int limit_list(struct rev_info *revs)
 	struct commit_list *list = revs->commits;
 	struct commit_list *newlist = NULL;
 	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 is no bottom commits");
+	}
 
 	while (list) {
 		struct commit_list *entry = list;
@@ -694,6 +788,11 @@ static int limit_list(struct rev_info *revs)
 	if (revs->cherry_pick)
 		cherry_pick_list(newlist, revs);
 
+	if (bottom) {
+		limit_to_ancestry(bottom, newlist);
+		free_commit_list(bottom);
+	}
+
 	revs->commits = newlist;
 	return 0;
 }
@@ -1089,6 +1188,9 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
 		revs->min_age = approxidate(arg + 8);
 	} else if (!strcmp(arg, "--first-parent")) {
 		revs->first_parent_only = 1;
+	} else if (!strcmp(arg, "--ancestry-path")) {
+		revs->ancestry_path = 1;
+		revs->limited = 1;
 	} else if (!strcmp(arg, "-g") || !strcmp(arg, "--walk-reflogs")) {
 		init_reflog_walk(&revs->reflog_info);
 	} else if (!strcmp(arg, "--default")) {
diff --git a/revision.h b/revision.h
index 568f1c9..855464f 100644
--- a/revision.h
+++ b/revision.h
@@ -66,6 +66,7 @@ struct rev_info {
 			reverse_output_stage:1,
 			cherry_pick:1,
 			bisect:1,
+			ancestry_path:1,
 			first_parent_only:1;
 
 	/* Diff flags */
diff --git a/t/t6019-rev-list-ancestry-path.sh b/t/t6019-rev-list-ancestry-path.sh
new file mode 100755
index 0000000..0230724
--- /dev/null
+++ b/t/t6019-rev-list-ancestry-path.sh
@@ -0,0 +1,56 @@
+#!/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
+
+. ./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_done
-- 
1.7.1.rc2.265.g8743f
Johan Herland· Apr 21, 2010, 08:46 UTC · re: Junio C Hamano · lore

Re: [PATCH] revision: --ancestry-path

On Wednesday 21 April 2010, Junio C Hamano wrote:
Show 20 quoted lines
> Johannes Sixt <j.sixt@viscovery.net> writes:
> > Am 4/21/2010 9:34, schrieb Johan Herland:
> >> I can confirm that this patch works with my original (more
> >> complicated) scenario. I've also played around with combinations of
> >> --ancestry-path, -- graph and --parents (and even --boundary), and
> >> AFAICS, the new option does not clobber the other options, and (IMHO)
> >> it all behaves correctly in the scenarios I've tested.
> > 
> > How about possible interactions with --reverse;
> > --date-order/--topo-order, --parents (important for gitk);
> > --simplify-by-decoration (useful for your problem that triggered
> > this); --full-history and --simplify-merges with path limiting?
> 
> These are all good points.
> 
> I am reasonably sure that parents (specifically, "rewrite_parents") is
> broken.  The new function should cull parents that do not appear on the
> ancestry path from merges (that is what "NEEDSWORK" is about).  It may or
> may not break gitk, though---these off-path parents are shown as parents
> of an on-path merge but will be marked as UNINTERESTING.

FWIW, I added the following patch to 'gitk', and then ran it against the t6019 test repo as follows:

	gitk --ancestry-path D..M

...and the resulting graph is exactly what I'd expect; showing the uninteresting parents (D, G and K) as "hollow" nodes.

diff --git a/gitk-git/gitk b/gitk-git/gitk
index 1b0e09a..7749d2a 100644
--- a/gitk-git/gitk
+++ b/gitk-git/gitk
@@ -190,7 +190,7 @@ proc parseviewargs {n arglist} {
            "--author=*" - "--committer=*" - "--grep=*" - "-[iE]" -
            "--remove-empty" - "--first-parent" - "--cherry-pick" -
            "-S*" - "--pickaxe-all" - "--pickaxe-regex" -
-           "--simplify-by-decoration" {
+           "--simplify-by-decoration" - "--ancestry-path" {
                # These mean that we get a subset of the commits
                set filtered 1
                lappend glflags $arg


> I do not think reverse/date-order/topo-order would be affected by this
> change, as they only change the presentation order of what limit_list()
> returns;
> 
> Also simplify-merges and full-history should be Ok, as they control what
> is done in the current logic in limit_list(), which is an input to the
> new logic, meaning that the new logic will work on already simplified
> history.
> 
> This is not a new problem, but I strongly suspect that cherry-pick is
> broken the same way wrt "rewrite_parents".

I'm not very familiar with "rewrite_parents", nor do I know exactly how it 
should affect/interoperate with --ancestry-path in all cases, but running

	git rev-list D..M -- M.t

produces one commit (M), whereas

	git rev-list --ancestry-path D..M -- M.t

produces nothing, so I suspect there is something not quite right here.


...Johan
-- 
Johan Herland, <johan@herland.net>
www.herland.net
Junio C Hamano· Apr 21, 2010, 09:04 UTC · re: Johan Herland · lore

Re: [PATCH] revision: --ancestry-path

Johan Herland <johan@herland.net> writes:
Show 10 quoted lines
> I'm not very familiar with "rewrite_parents", nor do I know exactly how it 
> should affect/interoperate with --ancestry-path in all cases, but running
>
> 	git rev-list D..M -- M.t
>
> produces one commit (M), whereas
>
> 	git rev-list --ancestry-path D..M -- M.t
>
> produces nothing, so I suspect there is something not quite right here.
Merge simplification gets in our way big time.
          D---E-------F
         /     \       \
    B---C---G---H---I---J
   /                     \
  A-------K---------------L--M

While traversing the history from M, we notice that M's parent is L, and then further we notice that neither the transition between J to L nor between K to L change the named path M.t, so we simplify the merge L to have K as its sole parent. This makes M appear disconnected from D's decendant chain, and causes the TMP_MARK reverse traversal not to work as intended.

Junio C Hamano· Apr 21, 2010, 08:49 UTC · re: Junio C Hamano · lore

Re: [PATCH] revision: --ancestry-path

Junio C Hamano <gitster@pobox.com> writes:
Show 5 quoted lines
> I am reasonably sure that parents (specifically, "rewrite_parents") is
> broken.  The new function should cull parents that do not appear on the
> ancestry path from merges (that is what "NEEDSWORK" is about).  It may or
> may not break gitk, though---these off-path parents are shown as parents
> of an on-path merge but will be marked as UNINTERESTING.

Thinking about it a bit more, I think this is Ok, as all the ancestors on a side branch that is off-path are marked uninteresting, and we do show uninteresting parents in the output of an interesting commit. In fact, if you do "rev-list --parents HEAD^..HEAD -- .", on a non-merge non-empty commit, you will see uninteresting HEAD^ shown as the parent of HEAD.

> This is not a new problem, but I strongly suspect that cherry-pick is
> broken the same way wrt "rewrite_parents".

This is a different and a real issue. By marking ones that are duplicate of commits from the other side as SHOWN, we will spit out a disconnected history. It should rewrite the parent list so that children of a commit that is removed due to being a duplicate from the other side point at an ancestor of that removed commit to keep the history connected.

← back to recent threads