Patchwork [4,of,5,v3] rebase: refactor computerebase to case-by-case handling

login
register
mail settings
Submitter Mads Kiilerich
Date Dec. 4, 2014, 3:12 a.m.
Message ID <742988c7a05cb1bd49f9.1417662768@localhost.localdomain>
Download mbox | patch
Permalink /patch/6991/
State Changes Requested
Headers show

Comments

Mads Kiilerich - Dec. 4, 2014, 3:12 a.m.
# HG changeset patch
# User Mads Kiilerich <madski@unity3d.com>
# Date 1416364731 -3600
#      Wed Nov 19 03:38:51 2014 +0100
# Node ID 742988c7a05cb1bd49f9be0f057a9c8ef4e20d95
# Parent  e031c5201577b359af981396957a0b9a9a5f5300
rebase: refactor computerebase to case-by-case handling

This makes it simpler to review and tweak handling of different cases.

This implementation was created using the old implementation as black box. It
started out with how I think it should be, and was tweaked until it matched how
it actually is. Further tweaking can take us close to where we want to be, step
by step.

The only case where the test suite coverage gives different result from
computerebase is in test-rebase-obsolete.t in a case where None as ancestor
would give the same ancestor as the one previously explicitly specified.
Pierre-Yves David - Dec. 5, 2014, 12:26 a.m.
On 12/03/2014 07:12 PM, Mads Kiilerich wrote:
> # HG changeset patch
> # User Mads Kiilerich <madski@unity3d.com>
> # Date 1416364731 -3600
> #      Wed Nov 19 03:38:51 2014 +0100
> # Node ID 742988c7a05cb1bd49f9be0f057a9c8ef4e20d95
> # Parent  e031c5201577b359af981396957a0b9a9a5f5300
> rebase: refactor computerebase to case-by-case handling
>
> This makes it simpler to review and tweak handling of different cases.

What was wrong with the previous implementation, what makes yours better?

> This implementation was created using the old implementation as black box. It
> started out with how I think it should be, and was tweaked until it matched how
> it actually is. Further tweaking can take us close to where we want to be, step
> by step.

Now that we have the full list of case, are we sure they are all 
properly tested? I would makes me more comfortable in such refactoring.

> The only case where the test suite coverage gives different result from
> computerebase is in test-rebase-obsolete.t in a case where None as ancestor
> would give the same ancestor as the one previously explicitly specified.

Why does it change? What does it mean? Is that bad? Why don't we get it 
right in the first place?

>
> diff --git a/hgext/rebase.py b/hgext/rebase.py
> --- a/hgext/rebase.py
> +++ b/hgext/rebase.py
> @@ -25,6 +25,7 @@ import os, errno
>
>   nullmerge = -2
>   revignored = -3
> +outside = -4 # in computerebase: not in state, thus outside rebase set
>
>   cmdtable = {}
>   command = cmdutil.command(cmdtable)
> @@ -586,88 +587,93 @@ def computerebase(repo, rev, target, sta
>       '''Return the merge revisions and new parent relationship of rev rebased
>       to target:
>           merge parent 1, merge parent 2, ancestor, new parent 1, new parent 2
> +
> +    The following table shows how it is - not necessarily how it should be.
> +
> +    Rebasing of rev with parents p1 and p2 onto target as rev' is written:
> +        m1,m2|a
> +        p1',p2'
> +
> +        \  p2     null         ancestor        rebased        outside
> +      p1 \
> +    null       target,rev|  target,rev|None  p2',rev|p2    target,rev|p2
> +                 target         target           p2'          target
> +
> +    ancestor target,rev|Non target,rev|None  p2',rev|p2   target,rev|None
> +                 target         target           p2'         target,p2
> +
> +    rebased    p1',rev|p1     p1',rev|p1    target,rev|p1   p1',rev|p1
> +                   p1'            p1'          p1',p2         p1',p2
> +
> +    outside   target,rev|p1 target,rev|None  p2',rev|p2       abort
> +                 target        target,p1       p2',p1       3 parents

This table is fairly hard to read can we more to a version with:

1) clearer boundary
2) fixed size, aligned value. There is various option for this I think 
my currently preferred is using T (target), R (rev), 1 (p1), 2 (p2) and 
- (no value, applies for None)

> -        # In case of merge, we need to pick the right parent as merge base.
> -        #
> -        # Imagine we have:
> -        # - M: currently rebase revision in this step
> -        # - A: one parent of M
> -        # - B: second parent of M
> -        # - D: destination of this merge step (p1 var)
> -        #
> -        # If we are rebasing on D, D is the successors of A or B. The right
> -        # merge base is the one D succeed to. We pretend it is B for the rest
> -        # of this comment
> -        #
> -        # If we pick B as the base, the merge involves:
> -        # - changes from B to M (actual changeset payload)
> -        # - changes from B to D (induced by rebase) as D is a rebased
> -        #   version of B)
> -        # Which exactly represent the rebase operation.
> -        #
> -        # If we pick the A as the base, the merge involves
> -        # - changes from A to M (actual changeset payload)
> -        # - changes from A to D (with include changes between unrelated A and B
> -        #   plus changes induced by rebase)
> -        # Which does not represent anything sensible and creates a lot of
> -        # conflicts.

This comment is very useful at explaining the rebase semantic and 
internal. It should not be dropped.

> -        for p in repo[rev].parents():
> -            if state.get(p.rev()) == p1:
> -                base = p.rev()
> -                break
> -        else: # fallback when base not found
> -            base = None
> -
> -            # Raise because this function is called wrong (see issue 4106)
> -            raise AssertionError('no base found to rebase on '
> -                                 '(computerebase called wrong)')
> -    return p1, rev, base, p1, p2
> +    if p1 == nullrev:
> +        if p2 == nullrev:
> +            # Currently not possible because 'nothing to rebase from ...'
> +            repo.ui.warn(' p1=nullrev, p2=nullrev - untested\n')
> +            return target, rev, nullrev, target, nullrev

-1 for using inline returns in such hairy code. Lets make the logic flow 
explicit with if/elif/else usage.

I'd trouble to compare the results of all the cases to the table value 
(mostly because the table is hard to read). I'm a bit concerned by the 
lack of factorization and mistake that can emerge from it, but lets wait 
for the next version.
Mads Kiilerich - Dec. 5, 2014, 1:20 a.m.
On 12/05/2014 01:26 AM, Pierre-Yves David wrote:
>
>
> On 12/03/2014 07:12 PM, Mads Kiilerich wrote:
>> # HG changeset patch
>> # User Mads Kiilerich <madski@unity3d.com>
>> # Date 1416364731 -3600
>> #      Wed Nov 19 03:38:51 2014 +0100
>> # Node ID 742988c7a05cb1bd49f9be0f057a9c8ef4e20d95
>> # Parent  e031c5201577b359af981396957a0b9a9a5f5300
>> rebase: refactor computerebase to case-by-case handling
>>
>> This makes it simpler to review and tweak handling of different cases.
>
> What was wrong with the previous implementation, what makes yours better?

You saw that in v1 of the patch series ... and complained about it.

>
>> This implementation was created using the old implementation as black 
>> box. It
>> started out with how I think it should be, and was tweaked until it 
>> matched how
>> it actually is. Further tweaking can take us close to where we want 
>> to be, step
>> by step.
>
> Now that we have the full list of case, are we sure they are all 
> properly tested? I would makes me more comfortable in such refactoring.

The cases that not are tested have a "untested" warning and a comment 
explaining why.

>
>> The only case where the test suite coverage gives different result from
>> computerebase is in test-rebase-obsolete.t in a case where None as 
>> ancestor
>> would give the same ancestor as the one previously explicitly specified.
>
> Why does it change? What does it mean? Is that bad? Why don't we get 
> it right in the first place?

The old algorithm was IMO a mess that tried to cover all the very 
different scenarios with the same algorithm. I gave up untangling it. 
Apparently, sometimes it would use None as ancestor (and thus 
automatically finding one in the merge algorithm) when rebasing an 
ancestor child, and sometimes it would explicitly use the parent as 
ancestor. In the end it would use the same ancestor and give the same 
result. In the next change we make it use the explicit ancestor in all 
cases.


>
>>
>> diff --git a/hgext/rebase.py b/hgext/rebase.py
>> --- a/hgext/rebase.py
>> +++ b/hgext/rebase.py
>> @@ -25,6 +25,7 @@ import os, errno
>>
>>   nullmerge = -2
>>   revignored = -3
>> +outside = -4 # in computerebase: not in state, thus outside rebase set
>>
>>   cmdtable = {}
>>   command = cmdutil.command(cmdtable)
>> @@ -586,88 +587,93 @@ def computerebase(repo, rev, target, sta
>>       '''Return the merge revisions and new parent relationship of 
>> rev rebased
>>       to target:
>>           merge parent 1, merge parent 2, ancestor, new parent 1, new 
>> parent 2
>> +
>> +    The following table shows how it is - not necessarily how it 
>> should be.
>> +
>> +    Rebasing of rev with parents p1 and p2 onto target as rev' is 
>> written:
>> +        m1,m2|a
>> +        p1',p2'
>> +
>> +        \  p2     null         ancestor        rebased outside
>> +      p1 \
>> +    null       target,rev|  target,rev|None  p2',rev|p2 target,rev|p2
>> +                 target         target           p2' target
>> +
>> +    ancestor target,rev|Non target,rev|None  p2',rev|p2 target,rev|None
>> +                 target         target           p2' target,p2
>> +
>> +    rebased    p1',rev|p1     p1',rev|p1    target,rev|p1 p1',rev|p1
>> +                   p1'            p1'          p1',p2 p1',p2
>> +
>> +    outside   target,rev|p1 target,rev|None  p2',rev|p2 abort
>> +                 target        target,p1       p2',p1       3 parents
>
> This table is fairly hard to read can we more to a version with:

It contains a lot of information.

> 1) clearer boundary
> 2) fixed size, aligned value. There is various option for this I think 
> my currently preferred is using T (target), R (rev), 1 (p1), 2 (p2) 
> and - (no value, applies for None)

I don't understand what you mean.

>
>> -        # In case of merge, we need to pick the right parent as 
>> merge base.
>> -        #
>> -        # Imagine we have:
>> -        # - M: currently rebase revision in this step
>> -        # - A: one parent of M
>> -        # - B: second parent of M
>> -        # - D: destination of this merge step (p1 var)
>> -        #
>> -        # If we are rebasing on D, D is the successors of A or B. 
>> The right
>> -        # merge base is the one D succeed to. We pretend it is B for 
>> the rest
>> -        # of this comment
>> -        #
>> -        # If we pick B as the base, the merge involves:
>> -        # - changes from B to M (actual changeset payload)
>> -        # - changes from B to D (induced by rebase) as D is a rebased
>> -        #   version of B)
>> -        # Which exactly represent the rebase operation.
>> -        #
>> -        # If we pick the A as the base, the merge involves
>> -        # - changes from A to M (actual changeset payload)
>> -        # - changes from A to D (with include changes between 
>> unrelated A and B
>> -        #   plus changes induced by rebase)
>> -        # Which does not represent anything sensible and creates a 
>> lot of
>> -        # conflicts.
>
> This comment is very useful at explaining the rebase semantic and 
> internal. It should not be dropped.

I do not understand the old comment. AFAICS it is trying to justify an 
approach that just not is a good fit for the problem - thus the "need" 
for a long explanation.

But arguably, you can make such a long explanation for every field in my 
table. I do however not find that justified when the cases can be 
considered one by one.

>> +    if p1 == nullrev:
>> +        if p2 == nullrev:
>> +            # Currently not possible because 'nothing to rebase from 
>> ...'
>> +            repo.ui.warn(' p1=nullrev, p2=nullrev - untested\n')
>> +            return target, rev, nullrev, target, nullrev
>
> -1 for using inline returns in such hairy code. Lets make the logic 
> flow explicit with if/elif/else usage.

I disagree. It has value to say "now we are done with this case".

> I'd trouble to compare the results of all the cases to the table value 
> (mostly because the table is hard to read). 

Because it carries a lot of information.

> I'm a bit concerned by the lack of factorization and mistake that can 
> emerge from it, but lets wait for the next version.

Factorization?

/Mads
Pierre-Yves David - Dec. 5, 2014, 1:49 a.m.
On 12/04/2014 05:20 PM, Mads Kiilerich wrote:
> On 12/05/2014 01:26 AM, Pierre-Yves David wrote:
>>
>>
>> On 12/03/2014 07:12 PM, Mads Kiilerich wrote:
>>> # HG changeset patch
>>> # User Mads Kiilerich <madski@unity3d.com>
>>> # Date 1416364731 -3600
>>> #      Wed Nov 19 03:38:51 2014 +0100
>>> # Node ID 742988c7a05cb1bd49f9be0f057a9c8ef4e20d95
>>> # Parent  e031c5201577b359af981396957a0b9a9a5f5300
>>> rebase: refactor computerebase to case-by-case handling
>>>
>>> This makes it simpler to review and tweak handling of different cases.
>>
>> What was wrong with the previous implementation, what makes yours better?
>
> You saw that in v1 of the patch series ... and complained about it.

Information relevant to a changesets should be in the changesets 
description…

>>> This implementation was created using the old implementation as black
>>> box. It
>>> started out with how I think it should be, and was tweaked until it
>>> matched how
>>> it actually is. Further tweaking can take us close to where we want
>>> to be, step
>>> by step.
>>
>> Now that we have the full list of case, are we sure they are all
>> properly tested? I would makes me more comfortable in such refactoring.
>
> The cases that not are tested have a "untested" warning and a comment
> explaining why.

Ok, probably worth mentioning that your check that all testable case 
were tested.

The untestable (because mercurial does not not use them) should probably 
just fails for now. This would simplify your table a little.

>>> The only case where the test suite coverage gives different result from
>>> computerebase is in test-rebase-obsolete.t in a case where None as
>>> ancestor
>>> would give the same ancestor as the one previously explicitly specified.
>>
>> Why does it change? What does it mean? Is that bad? Why don't we get
>> it right in the first place?
>
> The old algorithm was IMO a mess that tried to cover all the very
> different scenarios with the same algorithm. I gave up untangling it.
> Apparently, sometimes it would use None as ancestor (and thus
> automatically finding one in the merge algorithm) when rebasing an
> ancestor child, and sometimes it would explicitly use the parent as
> ancestor. In the end it would use the same ancestor and give the same
> result. In the next change we make it use the explicit ancestor in all
> cases.

Please update your description with these details.

I'm still wondering why you are not making it right from the start.


>>> diff --git a/hgext/rebase.py b/hgext/rebase.py
>>> --- a/hgext/rebase.py
>>> +++ b/hgext/rebase.py
>>> @@ -25,6 +25,7 @@ import os, errno
>>>
>>>   nullmerge = -2
>>>   revignored = -3
>>> +outside = -4 # in computerebase: not in state, thus outside rebase set
>>>
>>>   cmdtable = {}
>>>   command = cmdutil.command(cmdtable)
>>> @@ -586,88 +587,93 @@ def computerebase(repo, rev, target, sta
>>>       '''Return the merge revisions and new parent relationship of
>>> rev rebased
>>>       to target:
>>>           merge parent 1, merge parent 2, ancestor, new parent 1, new
>>> parent 2
>>> +
>>> +    The following table shows how it is - not necessarily how it
>>> should be.
>>> +
>>> +    Rebasing of rev with parents p1 and p2 onto target as rev' is
>>> written:
>>> +        m1,m2|a
>>> +        p1',p2'
>>> +
>>> +        \  p2     null         ancestor        rebased outside
>>> +      p1 \
>>> +    null       target,rev|  target,rev|None  p2',rev|p2 target,rev|p2
>>> +                 target         target           p2' target
>>> +
>>> +    ancestor target,rev|Non target,rev|None  p2',rev|p2 target,rev|None
>>> +                 target         target           p2' target,p2
>>> +
>>> +    rebased    p1',rev|p1     p1',rev|p1    target,rev|p1 p1',rev|p1
>>> +                   p1'            p1'          p1',p2 p1',p2
>>> +
>>> +    outside   target,rev|p1 target,rev|None  p2',rev|p2 abort
>>> +                 target        target,p1       p2',p1       3 parents
>>
>> This table is fairly hard to read can we more to a version with:
>
> It contains a lot of information.
>
>> 1) clearer boundary
>> 2) fixed size, aligned value. There is various option for this I think
>> my currently preferred is using T (target), R (rev), 1 (p1), 2 (p2)
>> and - (no value, applies for None)
>
> I don't understand what you mean.

You table is too hard to read. It needs vertically align content easily 
comparable.

eg: (half convincing one)

   ancestors | Ta Re -- Ta -- | Ta Re -- Ta -- |
             |                |                |
   rebased   | 1' Re p1 1' -- | 1' Re p1 1' -- |
Pierre-Yves David - Dec. 5, 2014, 3:52 a.m.
grmlm, forgot the last two comments.

On 12/04/2014 05:20 PM, Mads Kiilerich wrote:
> On 12/05/2014 01:26 AM, Pierre-Yves David wrote:
>>
>>
>> On 12/03/2014 07:12 PM, Mads Kiilerich wrote:
[…]
>>> -        # In case of merge, we need to pick the right parent as
>>> merge base.
>>> -        #
>>> -        # Imagine we have:
>>> -        # - M: currently rebase revision in this step
>>> -        # - A: one parent of M
>>> -        # - B: second parent of M
>>> -        # - D: destination of this merge step (p1 var)
>>> -        #
>>> -        # If we are rebasing on D, D is the successors of A or B.
>>> The right
>>> -        # merge base is the one D succeed to. We pretend it is B for
>>> the rest
>>> -        # of this comment
>>> -        #
>>> -        # If we pick B as the base, the merge involves:
>>> -        # - changes from B to M (actual changeset payload)
>>> -        # - changes from B to D (induced by rebase) as D is a rebased
>>> -        #   version of B)
>>> -        # Which exactly represent the rebase operation.
>>> -        #
>>> -        # If we pick the A as the base, the merge involves
>>> -        # - changes from A to M (actual changeset payload)
>>> -        # - changes from A to D (with include changes between
>>> unrelated A and B
>>> -        #   plus changes induced by rebase)
>>> -        # Which does not represent anything sensible and creates a
>>> lot of
>>> -        # conflicts.
>>
>> This comment is very useful at explaining the rebase semantic and
>> internal. It should not be dropped.
>
> I do not understand the old comment. AFAICS it is trying to justify an
> approach that just not is a good fit for the problem - thus the "need"
> for a long explanation.
 >
> But arguably, you can make such a long explanation for every field in my
> table. I do however not find that justified when the cases can be
> considered one by one.

You do not needs a such explanation for each cases. But this would be a 
valuable insight about the problem we are trying to solve and how we 
solves it.

I insist. this comment contains valuable explanation about what a rebase 
means, how different parents affect its result and how we pick stuff. If 
should remains around one way or another for next people sanity.

Simply discard this bookmark makes you loose points on the "I have and 
idea about what I'm doing on rebase code" front.

>>> +    if p1 == nullrev:
>>> +        if p2 == nullrev:
>>> +            # Currently not possible because 'nothing to rebase from
>>> ...'
>>> +            repo.ui.warn(' p1=nullrev, p2=nullrev - untested\n')
>>> +            return target, rev, nullrev, target, nullrev
>>
>> -1 for using inline returns in such hairy code. Lets make the logic
>> flow explicit with if/elif/else usage.
>
> I disagree. It has value to say "now we are done with this case".

The issue with this return based style is that I've to dig into level 
clause to check if they terminal or not. If you were properly using 
if/elif/else, I could have a look at higher level first knowing they are 
mutually exclusive. I find it to helps readability a lot.

It usually also helps when time comes to do post processing on the value.

But this is a style nits, I'll not fight to death over it.

>> I'd trouble to compare the results of all the cases to the table value
>> (mostly because the table is hard to read).
>
> Because it carries a lot of information.

Mostly because the table is not presented in an easy to read way. 
impairing my understanding of it.

>> I'm a bit concerned by the lack of factorization and mistake that can
>> emerge from it, but lets wait for the next version.
>
> Factorization?

Similar cases whose code has to be duplicated because they are on 
different branches. This is usually a good way to have different 
implementation when someone update them.

But I'll come back to checking this part when the table get more 
readable to me.
Matt Mackall - Dec. 5, 2014, 8:39 p.m.
On Thu, 2014-12-04 at 04:12 +0100, Mads Kiilerich wrote:
> # HG changeset patch
> # User Mads Kiilerich <madski@unity3d.com>
> # Date 1416364731 -3600
> #      Wed Nov 19 03:38:51 2014 +0100
> # Node ID 742988c7a05cb1bd49f9be0f057a9c8ef4e20d95
> # Parent  e031c5201577b359af981396957a0b9a9a5f5300
> rebase: refactor computerebase to case-by-case handling
> 
> This makes it simpler to review and tweak handling of different cases.
> 
> This implementation was created using the old implementation as black box. It
> started out with how I think it should be, and was tweaked until it matched how
> it actually is. Further tweaking can take us close to where we want to be, step
> by step.
> 
> The only case where the test suite coverage gives different result from
> computerebase is in test-rebase-obsolete.t in a case where None as ancestor
> would give the same ancestor as the one previously explicitly specified.
> 
> diff --git a/hgext/rebase.py b/hgext/rebase.py
> --- a/hgext/rebase.py
> +++ b/hgext/rebase.py
> @@ -25,6 +25,7 @@ import os, errno
>  
>  nullmerge = -2
>  revignored = -3
> +outside = -4 # in computerebase: not in state, thus outside rebase set
>  
>  cmdtable = {}
>  command = cmdutil.command(cmdtable)
> @@ -586,88 +587,93 @@ def computerebase(repo, rev, target, sta
>      '''Return the merge revisions and new parent relationship of rev rebased
>      to target:
>          merge parent 1, merge parent 2, ancestor, new parent 1, new parent 2
> +
> +    The following table shows how it is - not necessarily how it should be.
> +
> +    Rebasing of rev with parents p1 and p2 onto target as rev' is written:
> +        m1,m2|a
> +        p1',p2'
> +
> +        \  p2     null         ancestor        rebased        outside
> +      p1 \
> +    null       target,rev|  target,rev|None  p2',rev|p2    target,rev|p2
> +                 target         target           p2'          target
> +
> +    ancestor target,rev|Non target,rev|None  p2',rev|p2   target,rev|None
> +                 target         target           p2'         target,p2
> +
> +    rebased    p1',rev|p1     p1',rev|p1    target,rev|p1   p1',rev|p1
> +                   p1'            p1'          p1',p2         p1',p2
> +
> +    outside   target,rev|p1 target,rev|None  p2',rev|p2       abort
> +                 target        target,p1       p2',p1       3 parents

Woo, a table! If I read your commit message right, this table matches
the behavior of both the old and new code, right? If so, I'd like to get
some form of this table (and your docstring) in place as a first step.
Last time I looked at this code, I really struggled to figure out what
the args to this function meant or even what types they were.

I think we need a key:

p1' = already rebased rev of p1
p2' = already rebased rev of p2
ancestor = rev is in targetancestors
rebased = rev has been rebased
outside = other

Is there a difference between null(rev) and None?

Patch

diff --git a/hgext/rebase.py b/hgext/rebase.py
--- a/hgext/rebase.py
+++ b/hgext/rebase.py
@@ -25,6 +25,7 @@  import os, errno
 
 nullmerge = -2
 revignored = -3
+outside = -4 # in computerebase: not in state, thus outside rebase set
 
 cmdtable = {}
 command = cmdutil.command(cmdtable)
@@ -586,88 +587,93 @@  def computerebase(repo, rev, target, sta
     '''Return the merge revisions and new parent relationship of rev rebased
     to target:
         merge parent 1, merge parent 2, ancestor, new parent 1, new parent 2
+
+    The following table shows how it is - not necessarily how it should be.
+
+    Rebasing of rev with parents p1 and p2 onto target as rev' is written:
+        m1,m2|a
+        p1',p2'
+
+        \  p2     null         ancestor        rebased        outside
+      p1 \
+    null       target,rev|  target,rev|None  p2',rev|p2    target,rev|p2
+                 target         target           p2'          target
+
+    ancestor target,rev|Non target,rev|None  p2',rev|p2   target,rev|None
+                 target         target           p2'         target,p2
+
+    rebased    p1',rev|p1     p1',rev|p1    target,rev|p1   p1',rev|p1
+                   p1'            p1'          p1',p2         p1',p2
+
+    outside   target,rev|p1 target,rev|None  p2',rev|p2       abort
+                 target        target,p1       p2',p1       3 parents
     '''
-    parents = repo[rev].parents()
-    p1 = p2 = nullrev
+    ctx = repo[rev]
 
-    p1n = parents[0].rev()
-    if p1n in targetancestors:
-        p1 = target
-    elif p1n in state:
-        if state[p1n] == nullmerge:
-            p1 = target
-        elif state[p1n] == revignored:
-            p1 = nearestrebased(repo, p1n, state)
-            if p1 is None:
-                p1 = target
-        else:
-            p1 = state[p1n]
-    else: # p1n external
-        p1 = target
-        p2 = p1n
+    p1 = ctx.p1().rev()
+    p1_ = state.get(p1, outside)
+    if p1_ == revignored:
+        p1_ = nearestrebased(repo, p1, state)
+        if p1_ is None:
+            p1_ = target
 
-    if len(parents) == 2 and parents[1].rev() not in targetancestors:
-        p2n = parents[1].rev()
-        # interesting second parent
-        if p2n in state:
-            if p1 == target: # p1n in targetancestors or external
-                p1 = state[p2n]
-            elif state[p2n] == revignored:
-                p2 = nearestrebased(repo, p2n, state)
-                if p2 is None:
-                    # no ancestors rebased yet, detach
-                    p2 = target
-            else:
-                p2 = state[p2n]
-        else: # p2n external
-            if p2 != nullrev: # p1n external too => rev is a merged revision
-                raise util.Abort(_('cannot use revision %d as base, result '
-                        'would have 3 parents') % rev)
-            p2 = p2n
+    p2 = ctx.p2().rev()
+    p2_ = state.get(p2, outside)
+    if p2_ == revignored:
+        p2_ = nearestrebased(repo, p2, state)
+        if p2_ is None:
+            p2_ = target
 
-    if rev == min(state):
-        # Case (1) initial changeset of a non-detaching rebase.
-        # Let the merge mechanism find the base itself.
-        base = None
-    elif not repo[rev].p2():
-        # Case (2) detaching the node with a single parent, use this parent
-        base = repo[rev].p1().rev()
-    else:
-        # In case of merge, we need to pick the right parent as merge base.
-        #
-        # Imagine we have:
-        # - M: currently rebase revision in this step
-        # - A: one parent of M
-        # - B: second parent of M
-        # - D: destination of this merge step (p1 var)
-        #
-        # If we are rebasing on D, D is the successors of A or B. The right
-        # merge base is the one D succeed to. We pretend it is B for the rest
-        # of this comment
-        #
-        # If we pick B as the base, the merge involves:
-        # - changes from B to M (actual changeset payload)
-        # - changes from B to D (induced by rebase) as D is a rebased
-        #   version of B)
-        # Which exactly represent the rebase operation.
-        #
-        # If we pick the A as the base, the merge involves
-        # - changes from A to M (actual changeset payload)
-        # - changes from A to D (with include changes between unrelated A and B
-        #   plus changes induced by rebase)
-        # Which does not represent anything sensible and creates a lot of
-        # conflicts.
-        for p in repo[rev].parents():
-            if state.get(p.rev()) == p1:
-                base = p.rev()
-                break
-        else: # fallback when base not found
-            base = None
-
-            # Raise because this function is called wrong (see issue 4106)
-            raise AssertionError('no base found to rebase on '
-                                 '(computerebase called wrong)')
-    return p1, rev, base, p1, p2
+    if p1 == nullrev:
+        if p2 == nullrev:
+            # Currently not possible because 'nothing to rebase from ...'
+            repo.ui.warn(' p1=nullrev, p2=nullrev - untested\n')
+            return target, rev, nullrev, target, nullrev
+        # These can't happen with normal hg but might happen anyway:
+        if p2 in targetancestors:
+            repo.ui.warn(' p1=nullrev, p2 ancestor - untested\n')
+            return target, rev, None, target, nullrev
+        if p2_ >= nullrev:
+            repo.ui.warn(' p1=nullrev, p2 rebased - untested\n')
+            return p2_, rev, p2, p2_, nullrev
+        repo.ui.warn(' p1=nullrev, p2 outside - untested\n')
+        return target, rev, p2, target, nullrev
+    if p1 in targetancestors:
+        if p2 == nullrev:
+            repo.ui.debug(' p1 ancestor, p2=nullrev\n')
+            return target, rev, None, target, nullrev
+        if p2 in targetancestors:
+            repo.ui.debug(' p1 ancestor, p2 ancestor\n')
+            return target, rev, None, target, nullrev
+        if p2_ >= nullrev:
+            repo.ui.debug(' p1 ancestor, p2 rebased\n')
+            return p2_, rev, p2, p2_, nullrev
+        repo.ui.debug(' p1 ancestor, p2 outside\n')
+        return target, rev, None, target, p2
+    if p1_ >= nullrev:
+        if p2 == nullrev:
+            repo.ui.debug(' p1 rebased, p2=nullrev\n')
+            return p1_, rev, p1, p1_, nullrev
+        if p2 in targetancestors:
+            repo.ui.debug(' p1 rebased, p2 ancestor\n')
+            return p1_, rev, p1, p1_, nullrev
+        if p2_ >= nullrev:
+            repo.ui.debug(' p1 rebased, p2 rebased\n')
+            return target, rev, p1, target, nullrev
+        repo.ui.debug(' p1 rebased, p2 outside\n')
+        return p1_, rev, p1, p1_, p2
+    if p2 == nullrev:
+        repo.ui.debug(' p1 outside, p2=nullrev\n')
+        return target, rev, p1, target, nullrev
+    if p2 in targetancestors:
+        repo.ui.debug(' p1 outside, p2 ancestor\n')
+        return target, rev, None, target, p1
+    if p2_ >= nullrev:
+        repo.ui.debug(' p1 outside, p2 rebased\n')
+        return p2_, rev, p2, p2_, p1
+    repo.ui.debug(' p1 outside, p2 outside\n')
+    raise util.Abort(_('cannot use revision %d as base, result '
+                       'would have 3 parents') % rev)
 
 def isagitpatch(repo, patchname):
     'Return true if the given patch is in git format'
diff --git a/tests/test-rebase-conflicts.t b/tests/test-rebase-conflicts.t
--- a/tests/test-rebase-conflicts.t
+++ b/tests/test-rebase-conflicts.t
@@ -213,6 +213,7 @@  Check that the right ancestors is used w
   $ hg rebase -s9 -d2 --debug # use debug to really check merge base used
   rebase onto 2 starting from e31216eec445
   rebasing: 9:e31216eec445 5/6 changesets (83.33%)
+   p1 outside, p2=nullrev
    merging 2 and 9 using ancestor 8, setting parents 2 and -1
   rebase status stored
    update to 2:4bc80088dc6b
@@ -236,6 +237,7 @@  Check that the right ancestors is used w
   updating: f1.txt 1/1 files (100.00%)
   f1.txt
   rebasing: 10:2f2496ddf49d 6/6 changesets (100.00%)
+   p1 outside, p2 rebased
    merging 11 and 10 using ancestor 9, setting parents 11 and 7
   rebase status stored
    already in target