From 83f0412f3fff3b4108e7f05c30f3e861d148f5f2 Mon Sep 17 00:00:00 2001 From: Kevin Bracey Date: Thu, 16 May 2013 18:32:27 +0300 Subject: decorate.c: compact table when growing When growing the table, take the opportunity to "compact" it by removing entries with NULL decoration. Users may have "removed" decorations by passing NULL to insert_decoration. An object's table entry can't actually be removed during normal operation, as it would break the linear hash collision search. But we can remove NULL decoration entries when rebuilding the table. Signed-off-by: Kevin Bracey Signed-off-by: Junio C Hamano --- decorate.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decorate.c b/decorate.c index 2f8a63e388..7cb5d29a89 100644 --- a/decorate.c +++ b/decorate.c @@ -49,7 +49,7 @@ static void grow_decoration(struct decoration *n) const struct object *base = old_hash[i].base; void *decoration = old_hash[i].decoration; - if (!base) + if (!decoration) continue; insert_decoration(n, base, decoration); } -- cgit v1.2.3 From c72424b1b5d511a346742dba62bec93a719ef1c8 Mon Sep 17 00:00:00 2001 From: Kevin Bracey Date: Thu, 16 May 2013 18:32:28 +0300 Subject: t6019: test file dropped in -s ours merge In preparation for upcoming TREESAME work, check the result for G.t, which is dropped in "-s ours" merge L. The default rev-list is empty, as expected - it follows the first parent path where it never existed. Unfortunately, --ancestry-path is also empty. Merges H J and L are all TREESAME to 1 parent, so are treated as TREESAME and not shown. This is clearly undesirable in the case of merge L, which dropped our G.t by taking the non-ancestry-path version. Document this as a known failure, and expect "H J L", the 3 merges along the path that had to chose G.t versions. Signed-off-by: Kevin Bracey Signed-off-by: Junio C Hamano --- t/t6019-rev-list-ancestry-path.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/t/t6019-rev-list-ancestry-path.sh b/t/t6019-rev-list-ancestry-path.sh index dd5b0e55d2..c3bc2e73ef 100755 --- a/t/t6019-rev-list-ancestry-path.sh +++ b/t/t6019-rev-list-ancestry-path.sh @@ -16,6 +16,9 @@ test_description='--ancestry-path' # # F...I == F G H I # --ancestry-path F...I == F H I +# +# G..M -- G.t == [nothing - was dropped in "-s ours" merge L] +# --ancestry-path G..M -- G.t == H J L . ./test-lib.sh @@ -89,6 +92,22 @@ test_expect_success 'rev-list --ancestry-path F...I' ' test_cmp expect actual ' +# G.t is dropped in an "-s ours" merge +test_expect_success 'rev-list G..M -- G.t' ' + >expect && + git rev-list --format=%s G..M -- G.t | + sed -e "/^commit /d" >actual && + test_cmp expect actual +' + +test_expect_failure 'rev-list --ancestry-path G..M -- G.t' ' + for c in H J L; do echo $c; done >expect && + git rev-list --ancestry-path --format=%s G..M -- G.t | + sed -e "/^commit /d" | + sort >actual && + test_cmp expect actual +' + # b---bc # / \ / # a X -- cgit v1.2.3 From abdea96efdade975246cce0674fa2cfb8a7cc148 Mon Sep 17 00:00:00 2001 From: Kevin Bracey Date: Thu, 16 May 2013 18:32:29 +0300 Subject: t6111: new TREESAME test set Some side branching and odd merging to illustrate various flaws in revision list scans, particularly when limiting the list. Many expected failures, which will be gone by the end of the "history traversal refinements" series. Signed-off-by: Kevin Bracey Signed-off-by: Junio C Hamano --- t/t6111-rev-list-treesame.sh | 184 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100755 t/t6111-rev-list-treesame.sh diff --git a/t/t6111-rev-list-treesame.sh b/t/t6111-rev-list-treesame.sh new file mode 100755 index 0000000000..b2bca771cc --- /dev/null +++ b/t/t6111-rev-list-treesame.sh @@ -0,0 +1,184 @@ +#!/bin/sh +# +# ,---E--. *H----------. * marks !TREESAME parent paths +# / \ / \* +# *A--*B---D--*F-*G---------K-*L-*M +# \ /* \ / +# `-C-' `-*I-*J +# +# A creates "file", B and F change it. +# Odd merge G takes the old version from B. +# I changes it, but J reverts it, so K is TREESAME to both parents. +# H and L both change "file", and M merges those changes. + +test_description='TREESAME and limiting' + +. ./test-lib.sh + +note () { + git tag "$1" +} + +unnote () { + git name-rev --tags --stdin | sed -e "s|$_x40 (tags/\([^)]*\)) |\1 |g" +} + +test_expect_success setup ' + test_commit "Initial file" file "Hi there" A && + git branch other-branch && + + test_commit "file=Hello" file "Hello" B && + git branch third-branch && + + git checkout other-branch && + test_commit "Added other" other "Hello" C && + + git checkout master && + test_merge D other-branch && + + git checkout third-branch && + test_commit "Third file" third "Nothing" E && + + git checkout master && + test_commit "file=Blah" file "Blah" F && + + test_tick && git merge --no-commit third-branch && + git checkout third-branch file && + git commit && + note G && + git branch fiddler-branch && + + git checkout -b part2-branch && + test_commit "file=Part 2" file "Part 2" H && + + git checkout fiddler-branch && + test_commit "Bad commit" file "Silly" I && + + test_tick && git revert I && note J && + + git checkout master && + test_tick && git merge --no-ff fiddler-branch && + note K + + test_commit "file=Part 1" file "Part 1" L && + + test_tick && test_must_fail git merge part2-branch && + test_commit M file "Parts 1+2" +' + +FMT='tformat:%P %H | %s' + +# could we soup this up to optionally check parents? So "(BA)C" would check +# that C is shown and has parents B A. +check_outcome () { + outcome=$1 + shift + for c in $1 + do + echo "$c" + done >expect && + shift && + param="$*" && + test_expect_$outcome "log $param" ' + git log --format="$FMT" $param | + unnote >actual && + sed -e "s/^.* \([^ ]*\) .*/\1/" >check Date: Thu, 16 May 2013 18:32:30 +0300 Subject: t6111: allow checking the parents as well Signed-off-by: Junio C Hamano --- t/t6111-rev-list-treesame.sh | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/t/t6111-rev-list-treesame.sh b/t/t6111-rev-list-treesame.sh index b2bca771cc..1e4a550025 100755 --- a/t/t6111-rev-list-treesame.sh +++ b/t/t6111-rev-list-treesame.sh @@ -20,7 +20,7 @@ note () { } unnote () { - git name-rev --tags --stdin | sed -e "s|$_x40 (tags/\([^)]*\)) |\1 |g" + git name-rev --tags --stdin | sed -e "s|$_x40 (tags/\([^)]*\))\([ ]\)|\1\2|g" } test_expect_success setup ' @@ -66,23 +66,34 @@ test_expect_success setup ' test_commit M file "Parts 1+2" ' -FMT='tformat:%P %H | %s' - # could we soup this up to optionally check parents? So "(BA)C" would check # that C is shown and has parents B A. check_outcome () { outcome=$1 shift - for c in $1 - do - echo "$c" - done >expect && - shift && + + case "$1" in + *"("*) + FMT="%P %H | %s" + munge_actual=" + s/^\([^ ]*\) \([^ ]*\) .*/(\1)\2/ + s/ //g + s/()// + " + ;; + *) + FMT="%H | %s" + munge_actual="s/^\([^ ]*\) .*/\1/" + ;; + esac && + printf "%s\n" $1 >expect && + shift + param="$*" && test_expect_$outcome "log $param" ' git log --format="$FMT" $param | unnote >actual && - sed -e "s/^.* \([^ ]*\) .*/\1/" >check check && test_cmp expect check || { cat actual false @@ -99,6 +110,7 @@ check_result () { # shown in normal full-history, as we can't distinguish unless we do a # simplification pass. After simplification, D is dropped but G remains. check_result 'M L K J I H G F E D C B A' +check_result '(LH)M (K)L (GJ)K (I)J (G)I (G)H (FE)G (D)F (B)E (BC)D (A)C (A)B A' check_result 'M H L K J I G E F D C B A' --topo-order check_result 'M L H B A' -- file check_result 'M L H B A' --parents -- file -- cgit v1.2.3 From 53e38358c8ad5bef1a71e3dac4331014120165cc Mon Sep 17 00:00:00 2001 From: Kevin Bracey Date: Thu, 16 May 2013 18:32:31 +0300 Subject: t6111: add parents to tests Signed-off-by: Kevin Bracey Signed-off-by: Junio C Hamano --- t/t6111-rev-list-treesame.sh | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/t/t6111-rev-list-treesame.sh b/t/t6111-rev-list-treesame.sh index 1e4a550025..4d74d3cfdf 100755 --- a/t/t6111-rev-list-treesame.sh +++ b/t/t6111-rev-list-treesame.sh @@ -66,8 +66,6 @@ test_expect_success setup ' test_commit M file "Parts 1+2" ' -# could we soup this up to optionally check parents? So "(BA)C" would check -# that C is shown and has parents B A. check_outcome () { outcome=$1 shift @@ -109,14 +107,16 @@ check_result () { # except the most basic list. Achieving this means normal merge D will also be # shown in normal full-history, as we can't distinguish unless we do a # simplification pass. After simplification, D is dropped but G remains. +# Also, merge simplification of G should not drop the parent B that the default +# simple history follows. check_result 'M L K J I H G F E D C B A' check_result '(LH)M (K)L (GJ)K (I)J (G)I (G)H (FE)G (D)F (B)E (BC)D (A)C (A)B A' check_result 'M H L K J I G E F D C B A' --topo-order check_result 'M L H B A' -- file -check_result 'M L H B A' --parents -- file +check_result '(LH)M (B)L (B)H (A)B A' --parents -- file check_outcome failure 'M L J I H G F D B A' --full-history -- file # drops G -check_result 'M L K J I H G F D B A' --full-history --parents -- file -check_outcome failure 'M H L J I G F B A' --simplify-merges -- file # drops G +check_result '(LH)M (K)L (GJ)K (I)J (G)I (G)H (FB)G (D)F (BA)D (A)B A' --full-history --parents -- file +check_outcome failure '(LH)M (G)H (J)L (I)J (G)I (FB)G (B)F (A)B A' --simplify-merges -- file # drops G check_result 'M L K G F D B A' --first-parent check_result 'M L G F B A' --first-parent -- file @@ -124,14 +124,14 @@ check_result 'M L G F B A' --first-parent -- file check_result 'M L K J I H G E' F..M check_result 'M H L K J I G E' F..M --topo-order check_result 'M L H' F..M -- file -check_result 'M L H' F..M --parents -- file # L+H's parents rewritten to B, so more useful than it may seem +check_result '(LH)M (B)L (B)H' --parents F..M -- file check_outcome failure 'M L J I H G' F..M --full-history -- file # drops G -check_result 'M L K J I H G' F..M --full-history --parents -- file -check_outcome failure 'M H L J I G' F..M --simplify-merges -- file # drops G +check_result '(LH)M (K)L (GJ)K (I)J (G)I (G)H (FB)G' F..M --full-history --parents -- file +check_outcome failure '(LH)M (G)H (J)L (I)J (G)I (FB)G' F..M --simplify-merges -- file # drops G check_result 'M L K J I H G' F..M --ancestry-path check_outcome failure 'M L J I H G' F..M --ancestry-path -- file # drops G -check_result 'M L K J I H G' F..M --ancestry-path --parents -- file -check_result 'M H L J I G' F..M --ancestry-path --simplify-merges -- file +check_result '(LH)M (K)L (GJ)K (I)J (G)I (G)H (FE)G' F..M --ancestry-path --parents -- file +check_result '(LH)M (G)H (J)L (I)J (G)I (FE)G' F..M --ancestry-path --simplify-merges -- file check_result 'M L K G' F..M --first-parent check_result 'M L G' F..M --first-parent -- file @@ -139,15 +139,15 @@ check_result 'M L G' F..M --first-parent -- file # If we want history since E, then we're quite happy to ignore G that took E. check_result 'M L K J I H G' E..M --ancestry-path check_result 'M L J I H' E..M --ancestry-path -- file -check_outcome failure 'M L K J I H' E..M --ancestry-path --parents -- file # includes G -check_outcome failure 'M H L J I' E..M --ancestry-path --simplify-merges -- file # includes G +check_outcome failure '(LH)M (K)L (EJ)K (I)J (E)I (E)H' E..M --ancestry-path --parents -- file # includes G +check_outcome failure '(LH)M (E)H (J)L (I)J (E)I' E..M --ancestry-path --simplify-merges -- file # includes G # Should still be able to ignore I-J branch in simple log, despite limiting # to G. check_result 'M L K J I H' G..M check_result 'M H L K J I' G..M --topo-order check_outcome failure 'M L H' G..M -- file # includes J I -check_outcome failure 'M L H' G..M --parents -- file # includes J I +check_outcome failure '(LH)M (G)L (G)H' G..M --parents -- file # includes J I check_result 'M L J I H' G..M --full-history -- file check_result 'M L K J I H' G..M --full-history --parents -- file check_result 'M H L J I' G..M --simplify-merges -- file @@ -162,10 +162,10 @@ check_result 'M H L J I' G..M --ancestry-path --simplify-merges -- file # we can't decide if the merge from INTERESTING commit C was sensible. check_result 'F D C' B..F check_result 'F' B..F -- file -check_outcome failure 'F' B..F --parents -- file # includes D +check_outcome failure '(B)F' B..F --parents -- file # includes D check_outcome failure 'F D' B..F --full-history -- file # drops D prematurely -check_result 'F D' B..F --full-history --parents -- file -check_result 'F' B..F --simplify-merges -- file +check_result '(D)F (BA)D' B..F --full-history --parents -- file +check_result '(B)F' B..F --simplify-merges -- file check_result 'F D' B..F --ancestry-path check_result 'F' B..F --ancestry-path -- file check_outcome failure 'F' B..F --ancestry-path --parents -- file # includes D @@ -181,10 +181,10 @@ check_result 'F' E...F -- file # and it differs from it. check_result 'F D B' C..F check_result 'F B' C..F -- file -check_result 'F B' C..F --parents -- file +check_result '(B)F (A)B' C..F --parents -- file check_outcome failure 'F D B' C..F --full-history -- file # drops D -check_result 'F D B' C..F --full-history --parents -- file -check_result 'F D B' C..F --simplify-merges -- file +check_result '(D)F (BC)D (A)B' C..F --full-history --parents -- file +check_result '(D)F (BC)D (A)B' C..F --simplify-merges -- file check_result 'F D' C..F --ancestry-path check_outcome failure 'F D' C..F --ancestry-path -- file # drops D check_result 'F D' C..F --ancestry-path --parents -- file -- cgit v1.2.3 From 617f50cbfded66e2b314fc5ae550bf94854bc55e Mon Sep 17 00:00:00 2001 From: Kevin Bracey Date: Thu, 16 May 2013 18:32:32 +0300 Subject: rev-list-options.txt: correct TREESAME for P In the example given, P is not TREESAME to E. This doesn't affect the current result, but it will matter when we change behaviour. Signed-off-by: Kevin Bracey Signed-off-by: Junio C Hamano --- Documentation/rev-list-options.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Documentation/rev-list-options.txt b/Documentation/rev-list-options.txt index 3bdbf5e856..50bbff7f0a 100644 --- a/Documentation/rev-list-options.txt +++ b/Documentation/rev-list-options.txt @@ -367,8 +367,7 @@ each merge. The commits are: `N` and `D` to "foobarbaz"; i.e., it is not TREESAME to any parent. * `E` changes `quux` to "xyzzy", and its merge `P` combines the - strings to "quux xyzzy". Despite appearing interesting, `P` is - TREESAME to all parents. + strings to "quux xyzzy". `P` is TREESAME to `O`, but not to `E`. 'rev-list' walks backwards through history, including or excluding commits based on whether '\--full-history' and/or parent rewriting -- cgit v1.2.3 From e32db66d7a6b2ba1569ef5e316d869c40a8fac3b Mon Sep 17 00:00:00 2001 From: Kevin Bracey Date: Thu, 16 May 2013 18:32:33 +0300 Subject: Documentation: avoid "uninteresting" The documentation of --boundary uses the term "uninteresting", which is not used or defined anywhere else in the documentation. This is unhelpful and confusing to anyone who hasn't seen the UNINTERESTING flag in the source code. Change to use "excluded", as per revisions.txt. Signed-off-by: Kevin Bracey Signed-off-by: Junio C Hamano --- Documentation/rev-list-options.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/rev-list-options.txt b/Documentation/rev-list-options.txt index 50bbff7f0a..55ddf33e8e 100644 --- a/Documentation/rev-list-options.txt +++ b/Documentation/rev-list-options.txt @@ -271,8 +271,8 @@ See also linkgit:git-reflog[1]. --boundary:: - Output uninteresting commits at the boundary, which are usually - not shown. + Output excluded boundary commits. Boundary commits are + prefixed with `-`. -- -- cgit v1.2.3 From d0af663e42abfcd5be6f7c3db21a29e521aa4ca2 Mon Sep 17 00:00:00 2001 From: Kevin Bracey Date: Thu, 16 May 2013 18:32:34 +0300 Subject: revision.c: Make --full-history consider more merges History simplification previously always treated merges as TREESAME if they were TREESAME to any parent. While this was consistent with the default behaviour, this could be extremely unhelpful when searching detailed history, and could not be overridden. For example, if a merge had ignored a change, as if by "-s ours", then: git log -m -p --full-history -Schange file would successfully locate "change"'s addition but would not locate the merge that resolved against it. Futher, simplify_merges could drop the actual parent that a commit was TREESAME to, leaving it as a normal commit marked TREESAME that isn't actually TREESAME to its remaining parent. Now redefine a commit's TREESAME flag to be true only if a commit is TREESAME to _all_ of its parents. This doesn't affect either the default simplify_history behaviour (because partially TREESAME merges are turned into normal commits), or full-history with parent rewriting (because all merges are output). But it does affect other modes. The clearest difference is that --full-history will show more merges - sufficient to ensure that -m -p --full-history log searches can really explain every change to the file, including those changes' ultimate fate in merges. Also modify simplify_merges to recalculate TREESAME after removing a parent. This is achieved by storing per-parent TREESAME flags on the initial scan, so the combined flag can be easily recomputed. This fixes some t6111 failures, but creates a couple of new ones - we are now showing some merges that don't need to be shown. Signed-off-by: Kevin Bracey Signed-off-by: Junio C Hamano --- Documentation/rev-list-options.txt | 6 +- revision.c | 241 ++++++++++++++++++++++++++++++++----- revision.h | 1 + t/t6019-rev-list-ancestry-path.sh | 2 +- t/t6111-rev-list-treesame.sh | 26 ++-- 5 files changed, 229 insertions(+), 47 deletions(-) diff --git a/Documentation/rev-list-options.txt b/Documentation/rev-list-options.txt index 55ddf33e8e..d166384ff2 100644 --- a/Documentation/rev-list-options.txt +++ b/Documentation/rev-list-options.txt @@ -409,10 +409,10 @@ parent lines. the example, we get + ----------------------------------------------------------------------- - I A B N D O + I A B N D O P ----------------------------------------------------------------------- + -`P` and `M` were excluded because they are TREESAME to a parent. `E`, +`M` was excluded because it is TREESAME to both parents. `E`, `C` and `B` were all walked, but only `B` was !TREESAME, so the others do not appear. + @@ -440,7 +440,7 @@ themselves. This results in Compare to '\--full-history' without rewriting above. Note that `E` was pruned away because it is TREESAME, but the parent list of P was rewritten to contain `E`'s parent `I`. The same happened for `C` and -`N`. Note also that `P` was included despite being TREESAME. +`N`. In addition to the above settings, you can change whether TREESAME affects inclusion: diff --git a/revision.c b/revision.c index 7f7a8ab7cb..64b86ae91e 100644 --- a/revision.c +++ b/revision.c @@ -429,10 +429,100 @@ static int rev_same_tree_as_empty(struct rev_info *revs, struct commit *commit) return retval >= 0 && (tree_difference == REV_TREE_SAME); } +struct treesame_state { + unsigned int nparents; + unsigned char treesame[FLEX_ARRAY]; +}; + +static struct treesame_state *initialise_treesame(struct rev_info *revs, struct commit *commit) +{ + unsigned n = commit_list_count(commit->parents); + struct treesame_state *st = xcalloc(1, sizeof(*st) + n); + st->nparents = n; + add_decoration(&revs->treesame, &commit->object, st); + return st; +} + +/* + * Must be called immediately after removing the nth_parent from a commit's + * parent list, if we are maintaining the per-parent treesame[] decoration. + * This does not recalculate the master TREESAME flag - update_treesame() + * should be called to update it after a sequence of treesame[] modifications + * that may have affected it. + */ +static int compact_treesame(struct rev_info *revs, struct commit *commit, unsigned nth_parent) +{ + struct treesame_state *st; + int old_same; + + if (!commit->parents) { + /* + * Have just removed the only parent from a non-merge. + * Different handling, as we lack decoration. + */ + if (nth_parent != 0) + die("compact_treesame %u", nth_parent); + old_same = !!(commit->object.flags & TREESAME); + if (rev_same_tree_as_empty(revs, commit)) + commit->object.flags |= TREESAME; + else + commit->object.flags &= ~TREESAME; + return old_same; + } + + st = lookup_decoration(&revs->treesame, &commit->object); + if (!st || nth_parent >= st->nparents) + die("compact_treesame %u", nth_parent); + + old_same = st->treesame[nth_parent]; + memmove(st->treesame + nth_parent, + st->treesame + nth_parent + 1, + st->nparents - nth_parent - 1); + + /* + * If we've just become a non-merge commit, update TREESAME + * immediately, and remove the no-longer-needed decoration. + * If still a merge, defer update until update_treesame(). + */ + if (--st->nparents == 1) { + if (commit->parents->next) + die("compact_treesame parents mismatch"); + if (st->treesame[0] && revs->dense) + commit->object.flags |= TREESAME; + else + commit->object.flags &= ~TREESAME; + free(add_decoration(&revs->treesame, &commit->object, NULL)); + } + + return old_same; +} + +static unsigned update_treesame(struct rev_info *revs, struct commit *commit) +{ + if (commit->parents && commit->parents->next) { + unsigned n; + struct treesame_state *st; + + st = lookup_decoration(&revs->treesame, &commit->object); + if (!st) + die("update_treesame %s", sha1_to_hex(commit->object.sha1)); + commit->object.flags |= TREESAME; + for (n = 0; n < st->nparents; n++) { + if (!st->treesame[n]) { + commit->object.flags &= ~TREESAME; + break; + } + } + } + + return commit->object.flags & TREESAME; +} + static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit) { struct commit_list **pp, *parent; - int tree_changed = 0, tree_same = 0, nth_parent = 0; + struct treesame_state *ts = NULL; + int tree_changed = 0, nth_parent; /* * If we don't do pruning, everything is interesting @@ -456,25 +546,43 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit) if (!revs->dense && !commit->parents->next) return; - pp = &commit->parents; - while ((parent = *pp) != NULL) { + for (pp = &commit->parents, nth_parent = 0; + (parent = *pp) != NULL; + pp = &parent->next, nth_parent++) { struct commit *p = parent->item; - /* - * Do not compare with later parents when we care only about - * the first parent chain, in order to avoid derailing the - * traversal to follow a side branch that brought everything - * in the path we are limited to by the pathspec. - */ - if (revs->first_parent_only && nth_parent++) - break; + if (nth_parent == 1) { + /* + * This our second loop iteration - so we now know + * we're dealing with a merge. + * + * Do not compare with later parents when we care only about + * the first parent chain, in order to avoid derailing the + * traversal to follow a side branch that brought everything + * in the path we are limited to by the pathspec. + */ + if (revs->first_parent_only) + break; + /* + * If this will remain a potentially-simplifiable + * merge, remember per-parent treesame if needed. + * Initialise the array with the comparison from our + * first iteration. + */ + if (revs->treesame.name && + !revs->simplify_history && + !(commit->object.flags & UNINTERESTING)) { + ts = initialise_treesame(revs, commit); + if (!tree_changed) + ts->treesame[0] = 1; + } + } if (parse_commit(p) < 0) die("cannot simplify commit %s (because of %s)", sha1_to_hex(commit->object.sha1), sha1_to_hex(p->object.sha1)); switch (rev_compare_tree(revs, p, commit)) { case REV_TREE_SAME: - tree_same = 1; if (!revs->simplify_history || (p->object.flags & UNINTERESTING)) { /* Even if a merge with an uninteresting * side branch brought the entire change @@ -482,7 +590,8 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit) * to lose the other branches of this * merge, so we just keep going. */ - pp = &parent->next; + if (ts) + ts->treesame[nth_parent] = 1; continue; } parent->next = NULL; @@ -511,12 +620,11 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit) case REV_TREE_OLD: case REV_TREE_DIFFERENT: tree_changed = 1; - pp = &parent->next; continue; } die("bad tree compare for commit %s", sha1_to_hex(commit->object.sha1)); } - if (tree_changed && !tree_same) + if (tree_changed) return; commit->object.flags |= TREESAME; } @@ -1947,28 +2055,32 @@ static void add_child(struct rev_info *revs, struct commit *parent, struct commi l->next = add_decoration(&revs->children, &parent->object, l); } -static int remove_duplicate_parents(struct commit *commit) +static int remove_duplicate_parents(struct rev_info *revs, struct commit *commit) { + struct treesame_state *ts = lookup_decoration(&revs->treesame, &commit->object); struct commit_list **pp, *p; int surviving_parents; /* Examine existing parents while marking ones we have seen... */ pp = &commit->parents; + surviving_parents = 0; while ((p = *pp) != NULL) { struct commit *parent = p->item; if (parent->object.flags & TMP_MARK) { *pp = p->next; + if (ts) + compact_treesame(revs, commit, surviving_parents); continue; } parent->object.flags |= TMP_MARK; + surviving_parents++; pp = &p->next; } - /* count them while clearing the temporary mark */ - surviving_parents = 0; + /* clear the temporary mark */ for (p = commit->parents; p; p = p->next) { p->item->object.flags &= ~TMP_MARK; - surviving_parents++; } + /* no update_treesame() - removing duplicates can't affect TREESAME */ return surviving_parents; } @@ -1988,6 +2100,70 @@ static struct merge_simplify_state *locate_simplify_state(struct rev_info *revs, return st; } +static int mark_redundant_parents(struct rev_info *revs, struct commit *commit) +{ + struct commit_list *h = reduce_heads(commit->parents); + int i = 0, marked = 0; + struct commit_list *po, *pn; + + /* Want these for sanity-checking only */ + int orig_cnt = commit_list_count(commit->parents); + int cnt = commit_list_count(h); + + /* + * Not ready to remove items yet, just mark them for now, based + * on the output of reduce_heads(). reduce_heads outputs the reduced + * set in its original order, so this isn't too hard. + */ + po = commit->parents; + pn = h; + while (po) { + if (pn && po->item == pn->item) { + pn = pn->next; + i++; + } else { + po->item->object.flags |= TMP_MARK; + marked++; + } + po=po->next; + } + + if (i != cnt || cnt+marked != orig_cnt) + die("mark_redundant_parents %d %d %d %d", orig_cnt, cnt, i, marked); + + free_commit_list(h); + + return marked; +} + +static int remove_marked_parents(struct rev_info *revs, struct commit *commit) +{ + struct commit_list **pp, *p; + int nth_parent, removed = 0; + + pp = &commit->parents; + nth_parent = 0; + while ((p = *pp) != NULL) { + struct commit *parent = p->item; + if (parent->object.flags & TMP_MARK) { + parent->object.flags &= ~TMP_MARK; + *pp = p->next; + free(p); + removed++; + compact_treesame(revs, commit, nth_parent); + continue; + } + pp = &p->next; + nth_parent++; + } + + /* Removing parents can only increase TREESAMEness */ + if (removed && !(commit->object.flags & TREESAME)) + update_treesame(revs, commit); + + return nth_parent; +} + static struct commit_list **simplify_one(struct rev_info *revs, struct commit *commit, struct commit_list **tail) { struct commit_list *p; @@ -2032,7 +2208,9 @@ static struct commit_list **simplify_one(struct rev_info *revs, struct commit *c } /* - * Rewrite our list of parents. + * Rewrite our list of parents. Note that this cannot + * affect our TREESAME flags in any way - a commit is + * always TREESAME to its simplification. */ for (p = commit->parents; p; p = p->next) { pst = locate_simplify_state(revs, p->item); @@ -2044,31 +2222,30 @@ static struct commit_list **simplify_one(struct rev_info *revs, struct commit *c if (revs->first_parent_only) cnt = 1; else - cnt = remove_duplicate_parents(commit); + cnt = remove_duplicate_parents(revs, commit); /* * It is possible that we are a merge and one side branch * does not have any commit that touches the given paths; - * in such a case, the immediate parents will be rewritten - * to different commits. + * in such a case, the immediate parent from that branch + * will be rewritten to be the merge base. * * o----X X: the commit we are looking at; * / / o: a commit that touches the paths; * ---o----' * - * Further reduce the parents by removing redundant parents. + * Detect and simplify this case. */ if (1 < cnt) { - struct commit_list *h = reduce_heads(commit->parents); - cnt = commit_list_count(h); - free_commit_list(commit->parents); - commit->parents = h; + int marked = mark_redundant_parents(revs, commit); + if (marked) + cnt = remove_marked_parents(revs, commit); } /* * A commit simplifies to itself if it is a root, if it is * UNINTERESTING, if it touches the given paths, or if it is a - * merge and its parents simplifies to more than one commits + * merge and its parents simplify to more than one commit * (the first two cases are already handled at the beginning of * this function). * @@ -2176,6 +2353,10 @@ int prepare_revision_walk(struct rev_info *revs) if (!revs->leak_pending) free(list); + /* Signal whether we need per-parent treesame decoration */ + if (revs->simplify_merges) + revs->treesame.name = "treesame"; + if (revs->no_walk != REVISION_WALK_NO_WALK_UNSORTED) commit_list_sort_by_date(&revs->commits); if (revs->no_walk) @@ -2235,7 +2416,7 @@ static int rewrite_parents(struct rev_info *revs, struct commit *commit) } pp = &parent->next; } - remove_duplicate_parents(commit); + remove_duplicate_parents(revs, commit); return 0; } diff --git a/revision.h b/revision.h index 878a5556fd..7d5763b86c 100644 --- a/revision.h +++ b/revision.h @@ -168,6 +168,7 @@ struct rev_info { struct reflog_walk_info *reflog_info; struct decoration children; struct decoration merge_simplification; + struct decoration treesame; /* notes-specific options: which refs to show */ struct display_notes_opt notes_opt; diff --git a/t/t6019-rev-list-ancestry-path.sh b/t/t6019-rev-list-ancestry-path.sh index c3bc2e73ef..5ad96e3320 100755 --- a/t/t6019-rev-list-ancestry-path.sh +++ b/t/t6019-rev-list-ancestry-path.sh @@ -100,7 +100,7 @@ test_expect_success 'rev-list G..M -- G.t' ' test_cmp expect actual ' -test_expect_failure 'rev-list --ancestry-path G..M -- G.t' ' +test_expect_success 'rev-list --ancestry-path G..M -- G.t' ' for c in H J L; do echo $c; done >expect && git rev-list --ancestry-path --format=%s G..M -- G.t | sed -e "/^commit /d" | diff --git a/t/t6111-rev-list-treesame.sh b/t/t6111-rev-list-treesame.sh index 4d74d3cfdf..0047b541fd 100755 --- a/t/t6111-rev-list-treesame.sh +++ b/t/t6111-rev-list-treesame.sh @@ -114,9 +114,9 @@ check_result '(LH)M (K)L (GJ)K (I)J (G)I (G)H (FE)G (D)F (B)E (BC)D (A)C (A)B A' check_result 'M H L K J I G E F D C B A' --topo-order check_result 'M L H B A' -- file check_result '(LH)M (B)L (B)H (A)B A' --parents -- file -check_outcome failure 'M L J I H G F D B A' --full-history -- file # drops G +check_result 'M L J I H G F D B A' --full-history -- file check_result '(LH)M (K)L (GJ)K (I)J (G)I (G)H (FB)G (D)F (BA)D (A)B A' --full-history --parents -- file -check_outcome failure '(LH)M (G)H (J)L (I)J (G)I (FB)G (B)F (A)B A' --simplify-merges -- file # drops G +check_outcome failure '(LH)M (G)H (J)L (I)J (G)I (FB)G (B)F (A)B A' --simplify-merges -- file # drops parent B from G check_result 'M L K G F D B A' --first-parent check_result 'M L G F B A' --first-parent -- file @@ -125,11 +125,11 @@ check_result 'M L K J I H G E' F..M check_result 'M H L K J I G E' F..M --topo-order check_result 'M L H' F..M -- file check_result '(LH)M (B)L (B)H' --parents F..M -- file -check_outcome failure 'M L J I H G' F..M --full-history -- file # drops G +check_result 'M L J I H G' F..M --full-history -- file check_result '(LH)M (K)L (GJ)K (I)J (G)I (G)H (FB)G' F..M --full-history --parents -- file -check_outcome failure '(LH)M (G)H (J)L (I)J (G)I (FB)G' F..M --simplify-merges -- file # drops G +check_outcome failure '(LH)M (G)H (J)L (I)J (G)I (FB)G' F..M --simplify-merges -- file # drops parent B from G check_result 'M L K J I H G' F..M --ancestry-path -check_outcome failure 'M L J I H G' F..M --ancestry-path -- file # drops G +check_result 'M L J I H G' F..M --ancestry-path -- file check_result '(LH)M (K)L (GJ)K (I)J (G)I (G)H (FE)G' F..M --ancestry-path --parents -- file check_result '(LH)M (G)H (J)L (I)J (G)I (FE)G' F..M --ancestry-path --simplify-merges -- file check_result 'M L K G' F..M --first-parent @@ -138,7 +138,7 @@ check_result 'M L G' F..M --first-parent -- file # Note that G is pruned when E is the bottom, even if it's the same commit list # If we want history since E, then we're quite happy to ignore G that took E. check_result 'M L K J I H G' E..M --ancestry-path -check_result 'M L J I H' E..M --ancestry-path -- file +check_outcome failure 'M L J I H' E..M --ancestry-path -- file # includes G check_outcome failure '(LH)M (K)L (EJ)K (I)J (E)I (E)H' E..M --ancestry-path --parents -- file # includes G check_outcome failure '(LH)M (E)H (J)L (I)J (E)I' E..M --ancestry-path --simplify-merges -- file # includes G @@ -161,32 +161,32 @@ check_result 'M H L J I' G..M --ancestry-path --simplify-merges -- file # But --full-history shouldn't drop D on its own - without simplification, # we can't decide if the merge from INTERESTING commit C was sensible. check_result 'F D C' B..F -check_result 'F' B..F -- file +check_outcome failure 'F' B..F -- file # includes D check_outcome failure '(B)F' B..F --parents -- file # includes D -check_outcome failure 'F D' B..F --full-history -- file # drops D prematurely +check_result 'F D' B..F --full-history -- file check_result '(D)F (BA)D' B..F --full-history --parents -- file check_result '(B)F' B..F --simplify-merges -- file check_result 'F D' B..F --ancestry-path -check_result 'F' B..F --ancestry-path -- file +check_outcome failure 'F' B..F --ancestry-path -- file # includes D check_outcome failure 'F' B..F --ancestry-path --parents -- file # includes D check_outcome failure 'F' B..F --ancestry-path --simplify-merges -- file # includes D check_result 'F D' B..F --first-parent check_result 'F' B..F --first-parent -- file # E...F should be equivalent to E F ^B, and be able to drop D as above. -check_result 'F' E F ^B -- file -check_result 'F' E...F -- file +check_outcome failure 'F' E F ^B -- file # includes D +check_outcome failure 'F' E...F -- file # includes D # Any sort of full history of C..F should show D, as it's the connection to C, # and it differs from it. check_result 'F D B' C..F check_result 'F B' C..F -- file check_result '(B)F (A)B' C..F --parents -- file -check_outcome failure 'F D B' C..F --full-history -- file # drops D +check_result 'F D B' C..F --full-history -- file check_result '(D)F (BC)D (A)B' C..F --full-history --parents -- file check_result '(D)F (BC)D (A)B' C..F --simplify-merges -- file check_result 'F D' C..F --ancestry-path -check_outcome failure 'F D' C..F --ancestry-path -- file # drops D +check_result 'F D' C..F --ancestry-path -- file check_result 'F D' C..F --ancestry-path --parents -- file check_result 'F D' C..F --ancestry-path --simplify-merges -- file check_result 'F D B' C..F --first-parent -- cgit v1.2.3 From d5d2fc8b1a2afdd086b194b2a834e896367d6713 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 16 May 2013 18:32:35 +0300 Subject: t6012: update test for tweaked full-history traversal Signed-off-by: Junio C Hamano --- t/t6012-rev-list-simplify.sh | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/t/t6012-rev-list-simplify.sh b/t/t6012-rev-list-simplify.sh index dd6dc844e7..4e55872b1d 100755 --- a/t/t6012-rev-list-simplify.sh +++ b/t/t6012-rev-list-simplify.sh @@ -14,21 +14,24 @@ unnote () { test_expect_success setup ' echo "Hi there" >file && - git add file && - test_tick && git commit -m "Initial file" && + echo "initial" >lost && + git add file lost && + test_tick && git commit -m "Initial file and lost" && note A && git branch other-branch && echo "Hello" >file && - git add file && - test_tick && git commit -m "Modified file" && + echo "second" >lost && + git add file lost && + test_tick && git commit -m "Modified file and lost" && note B && git checkout other-branch && echo "Hello" >file && - git add file && + >lost && + git add file lost && test_tick && git commit -m "Modified the file identically" && note C && @@ -37,7 +40,9 @@ test_expect_success setup ' test_tick && git commit -m "Add another file" && note D && - test_tick && git merge -m "merge" master && + test_tick && + test_must_fail git merge -m "merge" master && + >lost && git commit -a -m "merge" && note E && echo "Yet another" >elif && @@ -110,4 +115,16 @@ check_result 'I B A' -- file check_result 'I B A' --topo-order -- file check_result 'H' --first-parent -- another-file +check_result 'E C B A' --full-history E -- lost +test_expect_success 'full history simplification without parent' ' + printf "%s\n" E C B A >expect && + git log --pretty="$FMT" --full-history E -- lost | + unnote >actual && + sed -e "s/^.* \([^ ]*\) .*/\1/" >check Date: Thu, 16 May 2013 18:32:36 +0300 Subject: simplify-merges: never remove all TREESAME parents When simplifying an odd merge, such as one that used "-s ours", we may find ourselves TREESAME to apparently redundant parents. Prevent simplify_merges() from removing every TREESAME parent; if this would happen reinstate the first TREESAME parent - the one that the default log would have followed. This avoids producing a totally disjoint history from the default log when the default log is a better explanation of the end result, and aids visualisation of odd merges. Signed-off-by: Kevin Bracey Signed-off-by: Junio C Hamano --- Documentation/rev-list-options.txt | 3 +- revision.c | 69 ++++++++++++++++++++++++++++++++++++++ t/t6111-rev-list-treesame.sh | 4 +-- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/Documentation/rev-list-options.txt b/Documentation/rev-list-options.txt index d166384ff2..f41e86545b 100644 --- a/Documentation/rev-list-options.txt +++ b/Documentation/rev-list-options.txt @@ -471,7 +471,8 @@ history according to the following rules: + * Replace each parent `P` of `C'` with its simplification `P'`. In the process, drop parents that are ancestors of other parents, and - remove duplicates. + remove duplicates, but take care to never drop all parents that + we are TREESAME to. + * If after this parent rewriting, `C'` is a root or merge commit (has zero or >1 parents), a boundary commit, or !TREESAME, it remains. diff --git a/revision.c b/revision.c index 64b86ae91e..62f399c22f 100644 --- a/revision.c +++ b/revision.c @@ -2136,6 +2136,73 @@ static int mark_redundant_parents(struct rev_info *revs, struct commit *commit) return marked; } +/* + * Awkward naming - this means one parent we are TREESAME to. + * cf mark_treesame_root_parents: root parents that are TREESAME (to an + * empty tree). Better name suggestions? + */ +static int leave_one_treesame_to_parent(struct rev_info *revs, struct commit *commit) +{ + struct treesame_state *ts = lookup_decoration(&revs->treesame, &commit->object); + struct commit *unmarked = NULL, *marked = NULL; + struct commit_list *p; + unsigned n; + + for (p = commit->parents, n = 0; p; p = p->next, n++) { + if (ts->treesame[n]) { + if (p->item->object.flags & TMP_MARK) { + if (!marked) + marked = p->item; + } else { + if (!unmarked) { + unmarked = p->item; + break; + } + } + } + } + + /* + * If we are TREESAME to a marked-for-deletion parent, but not to any + * unmarked parents, unmark the first TREESAME parent. This is the + * parent that the default simplify_history==1 scan would have followed, + * and it doesn't make sense to omit that path when asking for a + * simplified full history. Retaining it improves the chances of + * understanding odd missed merges that took an old version of a file. + * + * Example: + * + * I--------*X A modified the file, but mainline merge X used + * \ / "-s ours", so took the version from I. X is + * `-*A--' TREESAME to I and !TREESAME to A. + * + * Default log from X would produce "I". Without this check, + * --full-history --simplify-merges would produce "I-A-X", showing + * the merge commit X and that it changed A, but not making clear that + * it had just taken the I version. With this check, the topology above + * is retained. + * + * Note that it is possible that the simplification chooses a different + * TREESAME parent from the default, in which case this test doesn't + * activate, and we _do_ drop the default parent. Example: + * + * I------X A modified the file, but it was reverted in B, + * \ / meaning mainline merge X is TREESAME to both + * *A-*B parents. + * + * Default log would produce "I" by following the first parent; + * --full-history --simplify-merges will produce "I-A-B". But this is a + * reasonable result - it presents a logical full history leading from + * I to X, and X is not an important merge. + */ + if (!unmarked && marked) { + marked->object.flags &= ~TMP_MARK; + return 1; + } + + return 0; +} + static int remove_marked_parents(struct rev_info *revs, struct commit *commit) { struct commit_list **pp, *p; @@ -2238,6 +2305,8 @@ static struct commit_list **simplify_one(struct rev_info *revs, struct commit *c */ if (1 < cnt) { int marked = mark_redundant_parents(revs, commit); + if (marked) + marked -= leave_one_treesame_to_parent(revs, commit); if (marked) cnt = remove_marked_parents(revs, commit); } diff --git a/t/t6111-rev-list-treesame.sh b/t/t6111-rev-list-treesame.sh index 0047b541fd..484b69bb2d 100755 --- a/t/t6111-rev-list-treesame.sh +++ b/t/t6111-rev-list-treesame.sh @@ -116,7 +116,7 @@ check_result 'M L H B A' -- file check_result '(LH)M (B)L (B)H (A)B A' --parents -- file check_result 'M L J I H G F D B A' --full-history -- file check_result '(LH)M (K)L (GJ)K (I)J (G)I (G)H (FB)G (D)F (BA)D (A)B A' --full-history --parents -- file -check_outcome failure '(LH)M (G)H (J)L (I)J (G)I (FB)G (B)F (A)B A' --simplify-merges -- file # drops parent B from G +check_result '(LH)M (G)H (J)L (I)J (G)I (FB)G (B)F (A)B A' --simplify-merges -- file check_result 'M L K G F D B A' --first-parent check_result 'M L G F B A' --first-parent -- file @@ -127,7 +127,7 @@ check_result 'M L H' F..M -- file check_result '(LH)M (B)L (B)H' --parents F..M -- file check_result 'M L J I H G' F..M --full-history -- file check_result '(LH)M (K)L (GJ)K (I)J (G)I (G)H (FB)G' F..M --full-history --parents -- file -check_outcome failure '(LH)M (G)H (J)L (I)J (G)I (FB)G' F..M --simplify-merges -- file # drops parent B from G +check_result '(LH)M (G)H (J)L (I)J (G)I (FB)G' F..M --simplify-merges -- file check_result 'M L K J I H G' F..M --ancestry-path check_result 'M L J I H G' F..M --ancestry-path -- file check_result '(LH)M (K)L (GJ)K (I)J (G)I (G)H (FE)G' F..M --ancestry-path --parents -- file -- cgit v1.2.3 From 143f1eafdb0d326b1a8e3d15bf59a497b9440bf1 Mon Sep 17 00:00:00 2001 From: Kevin Bracey Date: Thu, 16 May 2013 18:32:37 +0300 Subject: simplify-merges: drop merge from irrelevant side branch Reimplement commit 4b7f53da on top of the new simplify-merges infrastructure, tightening the condition to only consider root parents; the original version incorrectly dropped parents that were TREESAME to anything. Original log message follows. The merge simplification rule stated in 6546b59 (revision traversal: show full history with merge simplification, 2008-07-31) still treated merge commits too specially. Namely, in a history with this shape: ---o---o---M / x---x---x where three 'x' were on a history completely unrelated to the main history 'o' and do not touch any of the paths we are following, we still said that after simplifying all of the parents of M, 'x' (which is the leftmost 'x' that rightmost 'x simplifies down to) and 'o' (which would be the last commit on the main history that touches the paths we are following) are independent from each other, and both need to be kept. That is incorrect; when the side branch 'x' never touches the paths, it should be removed to allow M to simplify down to the last commit on the main history that touches the paths. Suggested-by: Junio C Hamano Signed-off-by: Kevin Bracey Signed-off-by: Junio C Hamano --- Documentation/rev-list-options.txt | 34 +++++++++++++++++++++------------- revision.c | 26 +++++++++++++++++++++++++- t/t6012-rev-list-simplify.sh | 2 +- 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/Documentation/rev-list-options.txt b/Documentation/rev-list-options.txt index f41e86545b..b462f17f62 100644 --- a/Documentation/rev-list-options.txt +++ b/Documentation/rev-list-options.txt @@ -342,13 +342,13 @@ In the following, we will always refer to the same example history to illustrate the differences between simplification settings. We assume that you are filtering for a file `foo` in this commit graph: ----------------------------------------------------------------------- - .-A---M---N---O---P - / / / / / - I B C D E - \ / / / / - `-------------' + .-A---M---N---O---P---Q + / / / / / / + I B C D E Y + \ / / / / / + `-------------' X ----------------------------------------------------------------------- -The horizontal line of history A---P is taken to be the first parent of +The horizontal line of history A---Q is taken to be the first parent of each merge. The commits are: * `I` is the initial commit, in which `foo` exists with contents @@ -369,6 +369,10 @@ each merge. The commits are: * `E` changes `quux` to "xyzzy", and its merge `P` combines the strings to "quux xyzzy". `P` is TREESAME to `O`, but not to `E`. +* `X` is an indpendent root commit that added a new file `side`, and `Y` + modified it. `Y` is TREESAME to `X`. Its merge `Q` added `side` to `P`, and + `Q` is TREESAME to `P`, but not to `Y`. + 'rev-list' walks backwards through history, including or excluding commits based on whether '\--full-history' and/or parent rewriting (via '\--parents' or '\--children') are used. The following settings @@ -409,7 +413,7 @@ parent lines. the example, we get + ----------------------------------------------------------------------- - I A B N D O P + I A B N D O P Q ----------------------------------------------------------------------- + `M` was excluded because it is TREESAME to both parents. `E`, @@ -430,7 +434,7 @@ Along each parent, prune away commits that are not included themselves. This results in + ----------------------------------------------------------------------- - .-A---M---N---O---P + .-A---M---N---O---P---Q / / / / / I B / D / \ / / / / @@ -440,7 +444,7 @@ themselves. This results in Compare to '\--full-history' without rewriting above. Note that `E` was pruned away because it is TREESAME, but the parent list of P was rewritten to contain `E`'s parent `I`. The same happened for `C` and -`N`. +`N`, and `X`, `Y` and `Q`. In addition to the above settings, you can change whether TREESAME affects inclusion: @@ -470,9 +474,9 @@ history according to the following rules: * Set `C'` to `C`. + * Replace each parent `P` of `C'` with its simplification `P'`. In - the process, drop parents that are ancestors of other parents, and - remove duplicates, but take care to never drop all parents that - we are TREESAME to. + the process, drop parents that are ancestors of other parents or that are + root commits TREESAME to an empty tree, and remove duplicates, but take care + to never drop all parents that we are TREESAME to. + * If after this parent rewriting, `C'` is a root or merge commit (has zero or >1 parents), a boundary commit, or !TREESAME, it remains. @@ -490,7 +494,7 @@ The effect of this is best shown by way of comparing to `---------' ----------------------------------------------------------------------- + -Note the major differences in `N` and `P` over '--full-history': +Note the major differences in `N`, `P` and `Q` over '--full-history': + -- * `N`'s parent list had `I` removed, because it is an ancestor of the @@ -498,6 +502,10 @@ Note the major differences in `N` and `P` over '--full-history': + * `P`'s parent list similarly had `I` removed. `P` was then removed completely, because it had one parent and is TREESAME. ++ +* `Q`'s parent list had `Y` simplified to `X`. `X` was then removed, because it + was a TREESAME root. `Q` was then removed completely, because it had one + parent and is TREESAME. -- Finally, there is a fifth simplification mode available: diff --git a/revision.c b/revision.c index 62f399c22f..4f7446c359 100644 --- a/revision.c +++ b/revision.c @@ -2136,6 +2136,22 @@ static int mark_redundant_parents(struct rev_info *revs, struct commit *commit) return marked; } +static int mark_treesame_root_parents(struct rev_info *revs, struct commit *commit) +{ + struct commit_list *p; + int marked = 0; + + for (p = commit->parents; p; p = p->next) { + struct commit *parent = p->item; + if (!parent->parents && (parent->object.flags & TREESAME)) { + parent->object.flags |= TMP_MARK; + marked++; + } + } + + return marked; +} + /* * Awkward naming - this means one parent we are TREESAME to. * cf mark_treesame_root_parents: root parents that are TREESAME (to an @@ -2301,10 +2317,18 @@ static struct commit_list **simplify_one(struct rev_info *revs, struct commit *c * / / o: a commit that touches the paths; * ---o----' * - * Detect and simplify this case. + * Further, a merge of an independent branch that doesn't + * touch the path will reduce to a treesame root parent: + * + * ----o----X X: the commit we are looking at; + * / o: a commit that touches the paths; + * r r: a root commit not touching the paths + * + * Detect and simplify both cases. */ if (1 < cnt) { int marked = mark_redundant_parents(revs, commit); + marked += mark_treesame_root_parents(revs, commit); if (marked) marked -= leave_one_treesame_to_parent(revs, commit); if (marked) diff --git a/t/t6012-rev-list-simplify.sh b/t/t6012-rev-list-simplify.sh index 4e55872b1d..57ce2395d6 100755 --- a/t/t6012-rev-list-simplify.sh +++ b/t/t6012-rev-list-simplify.sh @@ -110,7 +110,7 @@ check_result 'L K J I H G F E D C B A' --full-history check_result 'K I H E C B A' --full-history -- file check_result 'K I H E C B A' --full-history --topo-order -- file check_result 'K I H E C B A' --full-history --date-order -- file -check_outcome failure 'I E C B A' --simplify-merges -- file +check_result 'I E C B A' --simplify-merges -- file check_result 'I B A' -- file check_result 'I B A' --topo-order -- file check_result 'H' --first-parent -- another-file -- cgit v1.2.3 From 7f34a46ff57121910c9ad697c2809a2d01ed1673 Mon Sep 17 00:00:00 2001 From: Kevin Bracey Date: Thu, 16 May 2013 18:32:38 +0300 Subject: revision.c: add BOTTOM flag for commits When performing edge-based operations on the revision graph, it can be useful to be able to identify the INTERESTING graph's connection(s) to the bottom commit(s) specified by the user. Conceptually when the user specifies "A..B" (== B ^A), they are asking for the history from A to B. The first connection from A onto the INTERESTING graph is part of that history, and should be considered. If we consider only INTERESTING nodes and their connections, then we're really only considering the history from A's immediate descendants to B. This patch does not change behaviour, but adds a new BOTTOM flag to indicate the bottom commits specified by the user, ready to be used by following patches. We immediately use the BOTTOM flag to return collect_bottom_commits() to its original approach of examining the pending commit list rather than the command line. This will ensure alignment of the definition of "bottom" with future patches. Signed-off-by: Kevin Bracey Signed-off-by: Junio C Hamano --- revision.c | 34 ++++++++++++++++------------------ revision.h | 3 ++- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/revision.c b/revision.c index 4f7446c359..6607dabcc1 100644 --- a/revision.c +++ b/revision.c @@ -909,16 +909,12 @@ static void limit_to_ancestry(struct commit_list *bottom, struct commit_list *li * to filter the result of "A..B" further to the ones that can actually * reach A. */ -static struct commit_list *collect_bottom_commits(struct rev_info *revs) +static struct commit_list *collect_bottom_commits(struct commit_list *list) { - struct commit_list *bottom = NULL; - int i; - for (i = 0; i < revs->cmdline.nr; i++) { - struct rev_cmdline_entry *elem = &revs->cmdline.rev[i]; - if ((elem->flags & UNINTERESTING) && - elem->item->type == OBJ_COMMIT) - commit_list_insert((struct commit *)elem->item, &bottom); - } + struct commit_list *elem, *bottom = NULL; + for (elem = list; elem; elem = elem->next) + if (elem->item->object.flags & BOTTOM) + commit_list_insert(elem->item, &bottom); return bottom; } @@ -949,7 +945,7 @@ static int limit_list(struct rev_info *revs) struct commit_list *bottom = NULL; if (revs->ancestry_path) { - bottom = collect_bottom_commits(revs); + bottom = collect_bottom_commits(list); if (!bottom) die("--ancestry-path given but there are no bottom commits"); } @@ -1121,7 +1117,7 @@ static int add_parents_only(struct rev_info *revs, const char *arg_, int flags) const char *arg = arg_; if (*arg == '^') { - flags ^= UNINTERESTING; + flags ^= UNINTERESTING | BOTTOM; arg++; } if (get_sha1_committish(arg, sha1)) @@ -1213,8 +1209,8 @@ static void prepare_show_merge(struct rev_info *revs) add_pending_object(revs, &head->object, "HEAD"); add_pending_object(revs, &other->object, "MERGE_HEAD"); bases = get_merge_bases(head, other, 1); - add_rev_cmdline_list(revs, bases, REV_CMD_MERGE_BASE, UNINTERESTING); - add_pending_commit_list(revs, bases, UNINTERESTING); + add_rev_cmdline_list(revs, bases, REV_CMD_MERGE_BASE, UNINTERESTING | BOTTOM); + add_pending_commit_list(revs, bases, UNINTERESTING | BOTTOM); free_commit_list(bases); head->object.flags |= SYMMETRIC_LEFT; @@ -1250,13 +1246,15 @@ int handle_revision_arg(const char *arg_, struct rev_info *revs, int flags, unsi int cant_be_filename = revarg_opt & REVARG_CANNOT_BE_FILENAME; unsigned get_sha1_flags = 0; + flags = flags & UNINTERESTING ? flags | BOTTOM : flags & ~BOTTOM; + dotdot = strstr(arg, ".."); if (dotdot) { unsigned char from_sha1[20]; const char *next = dotdot + 2; const char *this = arg; int symmetric = *next == '.'; - unsigned int flags_exclude = flags ^ UNINTERESTING; + unsigned int flags_exclude = flags ^ (UNINTERESTING | BOTTOM); static const char head_by_default[] = "HEAD"; unsigned int a_flags; @@ -1332,13 +1330,13 @@ int handle_revision_arg(const char *arg_, struct rev_info *revs, int flags, unsi dotdot = strstr(arg, "^!"); if (dotdot && !dotdot[2]) { *dotdot = 0; - if (!add_parents_only(revs, arg, flags ^ UNINTERESTING)) + if (!add_parents_only(revs, arg, flags ^ (UNINTERESTING | BOTTOM))) *dotdot = '^'; } local_flags = 0; if (*arg == '^') { - local_flags = UNINTERESTING; + local_flags = UNINTERESTING | BOTTOM; arg++; } @@ -1815,7 +1813,7 @@ static int handle_revision_pseudo_opt(const char *submodule, handle_refs(submodule, revs, *flags, for_each_branch_ref_submodule); } else if (!strcmp(arg, "--bisect")) { handle_refs(submodule, revs, *flags, for_each_bad_bisect_ref); - handle_refs(submodule, revs, *flags ^ UNINTERESTING, for_each_good_bisect_ref); + handle_refs(submodule, revs, *flags ^ (UNINTERESTING | BOTTOM), for_each_good_bisect_ref); revs->bisect = 1; } else if (!strcmp(arg, "--tags")) { handle_refs(submodule, revs, *flags, for_each_tag_ref_submodule); @@ -1841,7 +1839,7 @@ static int handle_revision_pseudo_opt(const char *submodule, } else if (!strcmp(arg, "--reflog")) { handle_reflog(revs, *flags); } else if (!strcmp(arg, "--not")) { - *flags ^= UNINTERESTING; + *flags ^= UNINTERESTING | BOTTOM; } else if (!strcmp(arg, "--no-walk")) { revs->no_walk = REVISION_WALK_NO_WALK_SORTED; } else if (!prefixcmp(arg, "--no-walk=")) { diff --git a/revision.h b/revision.h index 7d5763b86c..1e2c95cc83 100644 --- a/revision.h +++ b/revision.h @@ -15,7 +15,8 @@ #define ADDED (1u<<7) /* Parents already parsed and added? */ #define SYMMETRIC_LEFT (1u<<8) #define PATCHSAME (1u<<9) -#define ALL_REV_FLAGS ((1u<<10)-1) +#define BOTTOM (1u<<10) +#define ALL_REV_FLAGS ((1u<<11)-1) #define DECORATE_SHORT_REFS 1 #define DECORATE_FULL_REFS 2 -- cgit v1.2.3 From 4d826608e9610851d29440ca290e54b701921608 Mon Sep 17 00:00:00 2001 From: Kevin Bracey Date: Thu, 16 May 2013 18:32:39 +0300 Subject: revision.c: discount side branches when computing TREESAME Use the BOTTOM flag to define relevance for pruning. Relevant commits are those that are !UNINTERESTING or BOTTOM, and this allows us to identify irrelevant side branches (UNINTERESTING && !BOTTOM). If a merge has relevant parents, and it is TREESAME to them, then do not let irrelevant parents cause the merge to be treated as !TREESAME. When considering simplification, don't always include all merges - merges with exactly one relevant parent can be simplified, if TREESAME according to the above rule. These two changes greatly increase simplification in limited, pruned revision lists. Signed-off-by: Kevin Bracey Signed-off-by: Junio C Hamano --- revision.c | 171 +++++++++++++++++++++++++++++++++----- t/t6019-rev-list-ancestry-path.sh | 12 ++- t/t6111-rev-list-treesame.sh | 8 +- 3 files changed, 164 insertions(+), 27 deletions(-) diff --git a/revision.c b/revision.c index 6607dabcc1..1c750705d8 100644 --- a/revision.c +++ b/revision.c @@ -332,6 +332,80 @@ static int everybody_uninteresting(struct commit_list *orig) return 1; } +/* + * A definition of "relevant" commit that we can use to simplify limited graphs + * by eliminating side branches. + * + * A "relevant" commit is one that is !UNINTERESTING (ie we are including it + * in our list), or that is a specified BOTTOM commit. Then after computing + * a limited list, during processing we can generally ignore boundary merges + * coming from outside the graph, (ie from irrelevant parents), and treat + * those merges as if they were single-parent. TREESAME is defined to consider + * only relevant parents, if any. If we are TREESAME to our on-graph parents, + * we don't care if we were !TREESAME to non-graph parents. + * + * Treating bottom commits as relevant ensures that a limited graph's + * connection to the actual bottom commit is not viewed as a side branch, but + * treated as part of the graph. For example: + * + * ....Z...A---X---o---o---B + * . / + * W---Y + * + * When computing "A..B", the A-X connection is at least as important as + * Y-X, despite A being flagged UNINTERESTING. + * + * And when computing --ancestry-path "A..B", the A-X connection is more + * important than Y-X, despite both A and Y being flagged UNINTERESTING. + */ +static inline int relevant_commit(struct commit *commit) +{ + return (commit->object.flags & (UNINTERESTING | BOTTOM)) != UNINTERESTING; +} + +/* + * Return a single relevant commit from a parent list. If we are a TREESAME + * commit, and this selects one of our parents, then we can safely simplify to + * that parent. + */ +static struct commit *one_relevant_parent(const struct rev_info *revs, + struct commit_list *orig) +{ + struct commit_list *list = orig; + struct commit *relevant = NULL; + + if (!orig) + return NULL; + + /* + * For 1-parent commits, or if first-parent-only, then return that + * first parent (even if not "relevant" by the above definition). + * TREESAME will have been set purely on that parent. + */ + if (revs->first_parent_only || !orig->next) + return orig->item; + + /* + * For multi-parent commits, identify a sole relevant parent, if any. + * If we have only one relevant parent, then TREESAME will be set purely + * with regard to that parent, and we can simplify accordingly. + * + * If we have more than one relevant parent, or no relevant parents + * (and multiple irrelevant ones), then we can't select a parent here + * and return NULL. + */ + while (list) { + struct commit *commit = list->item; + list = list->next; + if (relevant_commit(commit)) { + if (relevant) + return NULL; + relevant = commit; + } + } + return relevant; +} + /* * The goal is to get REV_TREE_NEW as the result only if the * diff consists of all '+' (and no other changes), REV_TREE_OLD @@ -502,27 +576,52 @@ static unsigned update_treesame(struct rev_info *revs, struct commit *commit) if (commit->parents && commit->parents->next) { unsigned n; struct treesame_state *st; + struct commit_list *p; + unsigned relevant_parents; + unsigned relevant_change, irrelevant_change; st = lookup_decoration(&revs->treesame, &commit->object); if (!st) die("update_treesame %s", sha1_to_hex(commit->object.sha1)); - commit->object.flags |= TREESAME; - for (n = 0; n < st->nparents; n++) { - if (!st->treesame[n]) { - commit->object.flags &= ~TREESAME; - break; - } + relevant_parents = 0; + relevant_change = irrelevant_change = 0; + for (p = commit->parents, n = 0; p; n++, p = p->next) { + if (relevant_commit(p->item)) { + relevant_change |= !st->treesame[n]; + relevant_parents++; + } else + irrelevant_change |= !st->treesame[n]; } + if (relevant_parents ? relevant_change : irrelevant_change) + commit->object.flags &= ~TREESAME; + else + commit->object.flags |= TREESAME; } return commit->object.flags & TREESAME; } +static inline int limiting_can_increase_treesame(const struct rev_info *revs) +{ + /* + * TREESAME is irrelevant unless prune && dense; + * if simplify_history is set, we can't have a mixture of TREESAME and + * !TREESAME INTERESTING parents (and we don't have treesame[] + * decoration anyway); + * if first_parent_only is set, then the TREESAME flag is locked + * against the first parent (and again we lack treesame[] decoration). + */ + return revs->prune && revs->dense && + !revs->simplify_history && + !revs->first_parent_only; +} + static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit) { struct commit_list **pp, *parent; struct treesame_state *ts = NULL; - int tree_changed = 0, nth_parent; + int relevant_change = 0, irrelevant_change = 0; + int relevant_parents, nth_parent; /* * If we don't do pruning, everything is interesting @@ -546,10 +645,12 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit) if (!revs->dense && !commit->parents->next) return; - for (pp = &commit->parents, nth_parent = 0; + for (pp = &commit->parents, nth_parent = 0, relevant_parents = 0; (parent = *pp) != NULL; pp = &parent->next, nth_parent++) { struct commit *p = parent->item; + if (relevant_commit(p)) + relevant_parents++; if (nth_parent == 1) { /* @@ -573,7 +674,7 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit) !revs->simplify_history && !(commit->object.flags & UNINTERESTING)) { ts = initialise_treesame(revs, commit); - if (!tree_changed) + if (!(irrelevant_change || relevant_change)) ts->treesame[0] = 1; } } @@ -619,14 +720,27 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit) /* fallthrough */ case REV_TREE_OLD: case REV_TREE_DIFFERENT: - tree_changed = 1; + if (relevant_commit(p)) + relevant_change = 1; + else + irrelevant_change = 1; continue; } die("bad tree compare for commit %s", sha1_to_hex(commit->object.sha1)); } - if (tree_changed) - return; - commit->object.flags |= TREESAME; + + /* + * TREESAME is straightforward for single-parent commits. For merge + * commits, it is most useful to define it so that "irrelevant" + * parents cannot make us !TREESAME - if we have any relevant + * parents, then we only consider TREESAMEness with respect to them, + * allowing irrelevant merges from uninteresting branches to be + * simplified away. Only if we have only irrelevant parents do we + * base TREESAME on them. Note that this logic is replicated in + * update_treesame, which should be kept in sync. + */ + if (relevant_parents ? !relevant_change : !irrelevant_change) + commit->object.flags |= TREESAME; } static void commit_list_insert_by_date_cached(struct commit *p, struct commit_list **head, @@ -998,6 +1112,18 @@ static int limit_list(struct rev_info *revs) free_commit_list(bottom); } + /* + * Check if any commits have become TREESAME by some of their parents + * becoming UNINTERESTING. + */ + if (limiting_can_increase_treesame(revs)) + for (list = newlist; list; list = list->next) { + struct commit *c = list->item; + if (c->object.flags & (UNINTERESTING | TREESAME)) + continue; + update_treesame(revs, c); + } + revs->commits = newlist; return 0; } @@ -2248,6 +2374,7 @@ static int remove_marked_parents(struct rev_info *revs, struct commit *commit) static struct commit_list **simplify_one(struct rev_info *revs, struct commit *commit, struct commit_list **tail) { struct commit_list *p; + struct commit *parent; struct merge_simplify_state *st, *pst; int cnt; @@ -2336,19 +2463,20 @@ static struct commit_list **simplify_one(struct rev_info *revs, struct commit *c /* * A commit simplifies to itself if it is a root, if it is * UNINTERESTING, if it touches the given paths, or if it is a - * merge and its parents simplify to more than one commit + * merge and its parents don't simplify to one relevant commit * (the first two cases are already handled at the beginning of * this function). * - * Otherwise, it simplifies to what its sole parent simplifies to. + * Otherwise, it simplifies to what its sole relevant parent + * simplifies to. */ if (!cnt || (commit->object.flags & UNINTERESTING) || !(commit->object.flags & TREESAME) || - (1 < cnt)) + (parent = one_relevant_parent(revs, commit->parents)) == NULL) st->simplified = commit; else { - pst = locate_simplify_state(revs, commit->parents->item); + pst = locate_simplify_state(revs, parent); st->simplified = pst->simplified; } return tail; @@ -2445,7 +2573,8 @@ int prepare_revision_walk(struct rev_info *revs) free(list); /* Signal whether we need per-parent treesame decoration */ - if (revs->simplify_merges) + if (revs->simplify_merges || + (revs->limited && limiting_can_increase_treesame(revs))) revs->treesame.name = "treesame"; if (revs->no_walk != REVISION_WALK_NO_WALK_UNSORTED) @@ -2479,15 +2608,15 @@ static enum rewrite_result rewrite_one(struct rev_info *revs, struct commit **pp if (!revs->limited) if (add_parents_to_list(revs, p, &revs->commits, &cache) < 0) return rewrite_one_error; - if (p->parents && p->parents->next) - return rewrite_one_ok; if (p->object.flags & UNINTERESTING) return rewrite_one_ok; if (!(p->object.flags & TREESAME)) return rewrite_one_ok; if (!p->parents) return rewrite_one_noparents; - *pp = p->parents->item; + if ((p = one_relevant_parent(revs, p->parents)) == NULL) + return rewrite_one_ok; + *pp = p; } } diff --git a/t/t6019-rev-list-ancestry-path.sh b/t/t6019-rev-list-ancestry-path.sh index 5ad96e3320..dabebaee0b 100755 --- a/t/t6019-rev-list-ancestry-path.sh +++ b/t/t6019-rev-list-ancestry-path.sh @@ -18,7 +18,8 @@ test_description='--ancestry-path' # --ancestry-path F...I == F H I # # G..M -- G.t == [nothing - was dropped in "-s ours" merge L] -# --ancestry-path G..M -- G.t == H J L +# --ancestry-path G..M -- G.t == L +# --ancestry-path --simplify-merges G^..M -- G.t == G L . ./test-lib.sh @@ -101,8 +102,15 @@ test_expect_success 'rev-list G..M -- G.t' ' ' test_expect_success 'rev-list --ancestry-path G..M -- G.t' ' - for c in H J L; do echo $c; done >expect && + echo L >expect && git rev-list --ancestry-path --format=%s G..M -- G.t | + sed -e "/^commit /d" >actual && + test_cmp expect actual +' + +test_expect_success 'rev-list --ancestry-path --simplify-merges G^..M -- G.t' ' + for c in G L; do echo $c; done >expect && + git rev-list --ancestry-path --simplify-merges --format=%s G^..M -- G.t | sed -e "/^commit /d" | sort >actual && test_cmp expect actual diff --git a/t/t6111-rev-list-treesame.sh b/t/t6111-rev-list-treesame.sh index 484b69bb2d..e32b37306f 100755 --- a/t/t6111-rev-list-treesame.sh +++ b/t/t6111-rev-list-treesame.sh @@ -138,9 +138,9 @@ check_result 'M L G' F..M --first-parent -- file # Note that G is pruned when E is the bottom, even if it's the same commit list # If we want history since E, then we're quite happy to ignore G that took E. check_result 'M L K J I H G' E..M --ancestry-path -check_outcome failure 'M L J I H' E..M --ancestry-path -- file # includes G +check_result 'M L J I H' E..M --ancestry-path -- file check_outcome failure '(LH)M (K)L (EJ)K (I)J (E)I (E)H' E..M --ancestry-path --parents -- file # includes G -check_outcome failure '(LH)M (E)H (J)L (I)J (E)I' E..M --ancestry-path --simplify-merges -- file # includes G +check_result '(LH)M (E)H (J)L (I)J (E)I' E..M --ancestry-path --simplify-merges -- file # Should still be able to ignore I-J branch in simple log, despite limiting # to G. @@ -167,9 +167,9 @@ check_result 'F D' B..F --full-history -- file check_result '(D)F (BA)D' B..F --full-history --parents -- file check_result '(B)F' B..F --simplify-merges -- file check_result 'F D' B..F --ancestry-path -check_outcome failure 'F' B..F --ancestry-path -- file # includes D +check_result 'F' B..F --ancestry-path -- file check_outcome failure 'F' B..F --ancestry-path --parents -- file # includes D -check_outcome failure 'F' B..F --ancestry-path --simplify-merges -- file # includes D +check_result 'F' B..F --ancestry-path --simplify-merges -- file check_result 'F D' B..F --first-parent check_result 'F' B..F --first-parent -- file -- cgit v1.2.3 From bf3418b08bac89902a5f6c70f8695f148df99828 Mon Sep 17 00:00:00 2001 From: Kevin Bracey Date: Thu, 16 May 2013 18:32:40 +0300 Subject: revision.c: don't show all merges for --parents When using --parents or --children, get_commit_action() previously showed all merges, even if TREESAME to both parents. This was intended to tie together the topology of the rewritten parents, but it was excessive - in fact we only need to show merges that have two or more relevant parents. Merges at the boundary do not necessarily need to be shown. Signed-off-by: Kevin Bracey Signed-off-by: Junio C Hamano --- revision.c | 22 +++++++++++++++------- t/t6111-rev-list-treesame.sh | 4 ++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/revision.c b/revision.c index 1c750705d8..edb7e1c4d1 100644 --- a/revision.c +++ b/revision.c @@ -2760,10 +2760,7 @@ enum commit_action get_commit_action(struct rev_info *revs, struct commit *commi if (revs->min_age != -1 && (commit->date > revs->min_age)) return commit_ignore; if (revs->min_parents || (revs->max_parents >= 0)) { - int n = 0; - struct commit_list *p; - for (p = commit->parents; p; p = p->next) - n++; + int n = commit_list_count(commit->parents); if ((n < revs->min_parents) || ((revs->max_parents >= 0) && (n > revs->max_parents))) return commit_ignore; @@ -2773,12 +2770,23 @@ enum commit_action get_commit_action(struct rev_info *revs, struct commit *commi if (revs->prune && revs->dense) { /* Commit without changes? */ if (commit->object.flags & TREESAME) { + int n; + struct commit_list *p; /* drop merges unless we want parenthood */ if (!want_ancestry(revs)) return commit_ignore; - /* non-merge - always ignore it */ - if (!commit->parents || !commit->parents->next) - return commit_ignore; + /* + * If we want ancestry, then need to keep any merges + * between relevant commits to tie together topology. + * For consistency with TREESAME and simplification + * use "relevant" here rather than just INTERESTING, + * to treat bottom commit(s) as part of the topology. + */ + for (n = 0, p = commit->parents; p; p = p->next) + if (relevant_commit(p->item)) + if (++n >= 2) + return commit_show; + return commit_ignore; } } return commit_show; diff --git a/t/t6111-rev-list-treesame.sh b/t/t6111-rev-list-treesame.sh index e32b37306f..25cc8ad0e8 100755 --- a/t/t6111-rev-list-treesame.sh +++ b/t/t6111-rev-list-treesame.sh @@ -139,7 +139,7 @@ check_result 'M L G' F..M --first-parent -- file # If we want history since E, then we're quite happy to ignore G that took E. check_result 'M L K J I H G' E..M --ancestry-path check_result 'M L J I H' E..M --ancestry-path -- file -check_outcome failure '(LH)M (K)L (EJ)K (I)J (E)I (E)H' E..M --ancestry-path --parents -- file # includes G +check_result '(LH)M (K)L (EJ)K (I)J (E)I (E)H' E..M --ancestry-path --parents -- file check_result '(LH)M (E)H (J)L (I)J (E)I' E..M --ancestry-path --simplify-merges -- file # Should still be able to ignore I-J branch in simple log, despite limiting @@ -168,7 +168,7 @@ check_result '(D)F (BA)D' B..F --full-history --parents -- file check_result '(B)F' B..F --simplify-merges -- file check_result 'F D' B..F --ancestry-path check_result 'F' B..F --ancestry-path -- file -check_outcome failure 'F' B..F --ancestry-path --parents -- file # includes D +check_result 'F' B..F --ancestry-path --parents -- file check_result 'F' B..F --ancestry-path --simplify-merges -- file check_result 'F D' B..F --first-parent check_result 'F' B..F --first-parent -- file -- cgit v1.2.3 From 141efdba57b1769fc60ff9a3925afbc6af398faf Mon Sep 17 00:00:00 2001 From: Kevin Bracey Date: Thu, 16 May 2013 18:32:41 +0300 Subject: revision.c: make default history consider bottom commits Previously, the default history treated bottom commits the same as any other UNINTERESTING commit, which could force it down side branches. Consider the following history: *A--*B---D--*F * marks !TREESAME parent paths \ /* `-C-' When requesting "B..F", B is UNINTERESTING but TREESAME to D. C is !UNINTERESTING. So default following would go from D into the irrelevant side branch C to A, rather than to B. Note also that if there had been an extra !UNINTERESTING commit B1 between B and D, it wouldn't have gone down C. Change the default following to test relevant_commit() instead of !UNINTERESTING, so it can proceed straight from D to B, thus finishing the traversal of that path. Signed-off-by: Kevin Bracey Signed-off-by: Junio C Hamano --- revision.c | 2 +- t/t6111-rev-list-treesame.sh | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/revision.c b/revision.c index edb7e1c4d1..914ac78328 100644 --- a/revision.c +++ b/revision.c @@ -684,7 +684,7 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit) sha1_to_hex(p->object.sha1)); switch (rev_compare_tree(revs, p, commit)) { case REV_TREE_SAME: - if (!revs->simplify_history || (p->object.flags & UNINTERESTING)) { + if (!revs->simplify_history || !relevant_commit(p)) { /* Even if a merge with an uninteresting * side branch brought the entire change * we are interested in, we do not want diff --git a/t/t6111-rev-list-treesame.sh b/t/t6111-rev-list-treesame.sh index 25cc8ad0e8..88b84dfa73 100755 --- a/t/t6111-rev-list-treesame.sh +++ b/t/t6111-rev-list-treesame.sh @@ -146,8 +146,8 @@ check_result '(LH)M (E)H (J)L (I)J (E)I' E..M --ancestry-path --simplify-merges # to G. check_result 'M L K J I H' G..M check_result 'M H L K J I' G..M --topo-order -check_outcome failure 'M L H' G..M -- file # includes J I -check_outcome failure '(LH)M (G)L (G)H' G..M --parents -- file # includes J I +check_result 'M L H' G..M -- file +check_result '(LH)M (G)L (G)H' G..M --parents -- file check_result 'M L J I H' G..M --full-history -- file check_result 'M L K J I H' G..M --full-history --parents -- file check_result 'M H L J I' G..M --simplify-merges -- file @@ -161,8 +161,8 @@ check_result 'M H L J I' G..M --ancestry-path --simplify-merges -- file # But --full-history shouldn't drop D on its own - without simplification, # we can't decide if the merge from INTERESTING commit C was sensible. check_result 'F D C' B..F -check_outcome failure 'F' B..F -- file # includes D -check_outcome failure '(B)F' B..F --parents -- file # includes D +check_result 'F' B..F -- file +check_result '(B)F' B..F --parents -- file check_result 'F D' B..F --full-history -- file check_result '(D)F (BA)D' B..F --full-history --parents -- file check_result '(B)F' B..F --simplify-merges -- file @@ -174,8 +174,8 @@ check_result 'F D' B..F --first-parent check_result 'F' B..F --first-parent -- file # E...F should be equivalent to E F ^B, and be able to drop D as above. -check_outcome failure 'F' E F ^B -- file # includes D -check_outcome failure 'F' E...F -- file # includes D +check_result 'F' E F ^B -- file # includes D +check_result 'F' E...F -- file # includes D # Any sort of full history of C..F should show D, as it's the connection to C, # and it differs from it. -- cgit v1.2.3