Patchwork D7631: RFC: absorb: allowing committed changes to be absorbed into their ancestors

login
register
mail settings
Submitter phabricator
Date Dec. 13, 2019, 5:56 a.m.
Message ID <differential-rev-PHID-DREV-qbcvl27i2il44an6en4b-req@mercurial-scm.org>
Download mbox | patch
Permalink /patch/43776/
State New
Headers show

Comments

phabricator - Dec. 13, 2019, 5:56 a.m.
rdamazio created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  This can also be combined with --interactive to absorb just part of a
  commit into its parents.
  
  This initial implementation has the shortcoming that it does not do anything
  with the absorbed commit:
  
  - With obsmarkers, this means the user still needs to evolve/rebase manually
  - Without obsmarkers, the old commits would not be stripped at all because their child was not replaced

REPOSITORY
  rHG Mercurial

BRANCH
  default

REVISION DETAIL
  https://phab.mercurial-scm.org/D7631

AFFECTED FILES
  hgext/absorb.py
  tests/test-absorb-rev.t
  tests/test-absorb.t

CHANGE DETAILS




To: rdamazio, #hg-reviewers
Cc: mercurial-devel
phabricator - Dec. 13, 2019, 6:21 p.m.
quark added a comment.


  `--rev` seems ambiguous since there might be different kinds of revisions to specify - target and revisions to edit. Maybe something like `--source`, `--from`, `--target`?

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7631/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7631

To: rdamazio, #hg-reviewers
Cc: quark, mercurial-devel
phabricator - Dec. 14, 2019, 6:02 a.m.
rdamazio added a comment.


  In D7631#112414 <https://phab.mercurial-scm.org/D7631#112414>, @quark wrote:
  
  > `--rev` seems ambiguous since there might be different kinds of revisions to specify - target and revisions to edit. Maybe something like `--source`, `--from`, `--target`?
  
  Done. Used `--source` to match `rebase`.
  
  I'm assuming no fundamental objections then? Removing the "RFC" part so it gets a proper review then.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7631/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7631

To: rdamazio, #hg-reviewers
Cc: quark, mercurial-devel
phabricator - Dec. 17, 2019, 12:29 p.m.
pulkit added a comment.


  Can you describe the feature a bit in the commit message. Specific things which I feel are missing:
  
  - what happens to wdir changes
  - what happens if source is a commit is not a head
  
  I understand them but after reading the tests, so might be worth to add them to description.
  
  Also, it will be nice if you add an entry in releasenotes.

INLINE COMMENTS

> absorb.py:993
>              raise error.Abort(_(b'cannot absorb into a merge'))
> +        if len(targetctx.parents()) > 1:
> +            raise error.Abort(_(b'cannot absorb a merge'))

need to do more checks here about being public commit etc. `rewriteutil.precheck` should help.

> test-absorb-rev.t:73
> +
> +  $ grep 6 a
> +  6

`hg status|diff` should be a better command here.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7631/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7631

To: rdamazio, #hg-reviewers
Cc: pulkit, quark, mercurial-devel
phabricator - Dec. 18, 2019, 4:42 a.m.
rdamazio added a comment.


  > Can you describe the feature a bit in the commit message. Specific things which I feel are missing:
  
  Done
  
  > Also, it will be nice if you add an entry in releasenotes.
  
  Done

INLINE COMMENTS

> pulkit wrote in absorb.py:993
> need to do more checks here about being public commit etc. `rewriteutil.precheck` should help.

Done. Notice that *technically* the user could do such an absorb while in the middle of a merge, but it sounds like a bad idea and inviting troubles, so I'm letting it also disallow that case. I'll be surprised if anyone even notices.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7631/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7631

To: rdamazio, #hg-reviewers
Cc: pulkit, quark, mercurial-devel
phabricator - Dec. 18, 2019, 7:55 a.m.
pulkit added inline comments.

INLINE COMMENTS

> absorb.py:1113
> +    absorb analyzes each change in your working directory (or the revision given
> +    to `--rev`, if one is specified) and attempts to amend the changed lines
> +    into the changesets in your stack that first introduced those lines.

s/--rev/--source

> next:5
> +   the working directory changes, which continues to be the default unless
> +   `--rev`/`-r` is specified.
>  

s/--rev/--source

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7631/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7631

To: rdamazio, #hg-reviewers
Cc: pulkit, quark, mercurial-devel
phabricator - Jan. 2, 2020, 8:21 p.m.
mharbison72 added a comment.
mharbison72 added subscribers: martinvonz, mharbison72.


  In D7631#112604 <https://phab.mercurial-scm.org/D7631#112604>, @rdamazio wrote:
  
  > In D7631#112414 <https://phab.mercurial-scm.org/D7631#112414>, @quark wrote:
  >
  >> `--rev` seems ambiguous since there might be different kinds of revisions to specify - target and revisions to edit. Maybe something like `--source`, `--from`, `--target`?
  >
  > Done. Used `--source` to match `rebase`.
  
  Is `--exact` from `hg fold` a better model?  I don't feel strongly; I only mention it because `hg rebase -s` will take that revision and its descendants, so it's more like "stack" in my mind.  I'm not sure how many other commands have `-s` off the top of my head, but @martinvonz 
  mentioned adding that to `hg fix` (probably in IRC), and I think mentioned the word "stack" in that context.  So I might not be the only one to get slightly tripped up by that.
  
  > I'm assuming no fundamental objections then? Removing the "RFC" part so it gets a proper review then.
  
  I like it.

INLINE COMMENTS

> absorb.py:1141
> +        # default to working copy
> +        ctx = scmutil.revsingle(repo, source, default=None)
> +

Should it abort if multiple revisions are given, instead of picking the latest?

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7631/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7631

To: rdamazio, #hg-reviewers
Cc: mharbison72, martinvonz, pulkit, quark, mercurial-devel
phabricator - Jan. 7, 2020, 12:46 a.m.
rdamazio added a comment.


  In D7631#114393 <https://phab.mercurial-scm.org/D7631#114393>, @mharbison72 wrote:
  
  > In D7631#112604 <https://phab.mercurial-scm.org/D7631#112604>, @rdamazio wrote:
  >
  >> In D7631#112414 <https://phab.mercurial-scm.org/D7631#112414>, @quark wrote:
  >>
  >>> `--rev` seems ambiguous since there might be different kinds of revisions to specify - target and revisions to edit. Maybe something like `--source`, `--from`, `--target`?
  >>
  >> Done. Used `--source` to match `rebase`.
  >
  > Is `--exact` from `hg fold` a better model?  I don't feel strongly; I only mention it because `hg rebase -s` will take that revision and its descendants, so it's more like "stack" in my mind.  I'm not sure how many other commands have `-s` off the top of my head, but @martinvonz 
  > mentioned adding that to `hg fix` (probably in IRC), and I think mentioned the word "stack" in that context.  So I might not be the only one to get slightly tripped up by that.
  
  IMHO no, needing `--exact` is actually confusing to almost every user we've talked to, and they'd instead expect that to be the default behavior, with "fold up to this commit" being the one that needs a specific flag.
  
  >> I'm assuming no fundamental objections then? Removing the "RFC" part so it gets a proper review then.
  >
  > I like it.
  
  Thanks

INLINE COMMENTS

> mharbison72 wrote in absorb.py:1141
> Should it abort if multiple revisions are given, instead of picking the latest?

I suspect other places may want something similar (e.g. it'd make sense in `rebase --dest`, so I changed revsingle to add the behavior.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7631/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7631

To: rdamazio, #hg-reviewers
Cc: mharbison72, martinvonz, pulkit, quark, mercurial-devel
phabricator - Jan. 13, 2020, 4:05 p.m.
pulkit added inline comments.

INLINE COMMENTS

> rdamazio wrote in absorb.py:993
> Done. Notice that *technically* the user could do such an absorb while in the middle of a merge, but it sounds like a bad idea and inviting troubles, so I'm letting it also disallow that case. I'll be surprised if anyone even notices.

Sorry, I misunderstood the patch earlier. `rewriteutil.precheck` on target rev is not very helpful as we are not obsolete-ing that in this rev, but we are re-writing it's ancestors. So, if target-rev is a head, and `evolution.alloworphans=False` is set, it will still create orphans.

Not sure what's the best way forward, maybe we should do `rewriteutil.precheck` for the parent instead until we start obsoleting this rev.

> test-absorb-rev.t:72
>  Run absorb:
>  
> +  $ hg absorb --apply-changes -s .

I locally added some `hg log --graph` calls before and after absorb call to understand what happens. It will be nice to add them as it will make things easier for others to understand.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7631/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7631

To: rdamazio, #hg-reviewers
Cc: mharbison72, martinvonz, pulkit, quark, mercurial-devel
phabricator - Jan. 13, 2020, 5:31 p.m.
martinvonz added a comment.


  In D7631#112604 <https://phab.mercurial-scm.org/D7631#112604>, @rdamazio wrote:
  
  > In D7631#112414 <https://phab.mercurial-scm.org/D7631#112414>, @quark wrote:
  >
  >> `--rev` seems ambiguous since there might be different kinds of revisions to specify - target and revisions to edit. Maybe something like `--source`, `--from`, `--target`?
  >
  > Done. Used `--source` to match `rebase`.
  
  Sorry I didn't notice until now, but `--source` makes me think it will behave like `hg rebase --source` and absorb from the given commit and all its descendants. I would have preferred `--from` (and maybe a `--into` for choosing which commits to absorb into in the future).

INLINE COMMENTS

> pulkit wrote in absorb.py:993
> Sorry, I misunderstood the patch earlier. `rewriteutil.precheck` on target rev is not very helpful as we are not obsolete-ing that in this rev, but we are re-writing it's ancestors. So, if target-rev is a head, and `evolution.alloworphans=False` is set, it will still create orphans.
> 
> Not sure what's the best way forward, maybe we should do `rewriteutil.precheck` for the parent instead until we start obsoleting this rev.

Maybe I'm also misunderstanding what this patch does in that case. `hg absorb -r A` will not obsolete A? I would think it definitely should do that. Perhaps the successors or the absorbed commit should be all the nodes absorbed into as well as any potential leftovers (which were not absorbed).

> test-absorb-rev.t:63
> +
> +  $ hg absorb --apply-changes -s .^+.
> +  abort: revision set matched multiple revisions

nit: I think I've heard that `^` needs to be quoted on Windows, so maybe `-s '.^+.'`

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7631/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7631

To: rdamazio, #hg-reviewers
Cc: mharbison72, martinvonz, pulkit, quark, mercurial-devel
phabricator - Jan. 16, 2020, 4:49 p.m.
mharbison72 added inline comments.

INLINE COMMENTS

> martinvonz wrote in test-absorb-rev.t:63
> nit: I think I've heard that `^` needs to be quoted on Windows, so maybe `-s '.^+.'`

That's true in cmd.exe, but msys doesn't seem to care.  (That said, I thought check-code enforced quoting around `^`.)

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7631/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7631

To: rdamazio, #hg-reviewers
Cc: mharbison72, martinvonz, pulkit, quark, mercurial-devel

Patch

diff --git a/tests/test-absorb.t b/tests/test-absorb.t
--- a/tests/test-absorb.t
+++ b/tests/test-absorb.t
@@ -143,7 +143,7 @@ 
   nothing applied
   [1]
 
-Insertaions:
+Insertions:
 
   $ cat > a << EOF
   > insert before 2b
diff --git a/tests/test-absorb.t b/tests/test-absorb-rev.t
copy from tests/test-absorb.t
copy to tests/test-absorb-rev.t
--- a/tests/test-absorb.t
+++ b/tests/test-absorb-rev.t
@@ -1,26 +1,14 @@ 
   $ cat >> $HGRCPATH << EOF
   > [extensions]
   > absorb=
+  > rebase=
+  > [experimental]
+  > evolution=createmarkers
   > EOF
 
-  $ sedi() { # workaround check-code
-  > pattern="$1"
-  > shift
-  > for i in "$@"; do
-  >     sed "$pattern" "$i" > "$i".tmp
-  >     mv "$i".tmp "$i"
-  > done
-  > }
-
   $ hg init repo1
   $ cd repo1
 
-Do not crash with empty repo:
-
-  $ hg absorb
-  abort: no mutable changeset to change
-  [255]
-
 Make some commits:
 
   $ for i in 1 2 3 4 5; do
@@ -45,9 +33,13 @@ 
   > 5e
   > EOF
 
+Commit that, too.
+
+  $ hg commit -qm "commit to absorb"
+
 Preview absorb changes:
 
-  $ hg absorb --print-changes --dry-run
+  $ hg absorb --print-changes --dry-run -r .
   showing changes for a
           @@ -0,2 +0,2 @@
   4ec16f8 -1
@@ -66,462 +58,111 @@ 
   5c5f952 commit 2
   4ec16f8 commit 1
 
+Add an uncommitted working directory change:
+
+  $ echo 6 >> a
+
 Run absorb:
 
-  $ hg absorb --apply-changes
-  saved backup bundle to * (glob)
+  $ hg absorb --apply-changes -r .
+  1 new orphan changesets
   2 of 2 chunk(s) applied
-  $ hg annotate a
-  0: 1a
-  1: 2b
-  2: 3
-  3: 4d
-  4: 5e
+
+Check that the pending wdir change was left alone:
+
+  $ grep 6 a
+  6
+  $ hg update -Cq .
+
+Rebase the absorbed revision on top of the destination (as evolve would):
+TODO: the evolve-type operation should happen automatically for the changeset
+being absorbed, and even through that the pending wdir change should be left
+alone.
+
+  $ hg rebase -d tip -r .
+  rebasing 5:1631091f9648 "commit to absorb"
+  note: not rebasing 5:1631091f9648 "commit to absorb", its destination already has all its changes
 
-Delete a few lines and related commits will be removed if they will be empty:
+  $ hg log -G -T '{node|short} {desc} {instabilities}'
+  @  2f7ba78d6abc commit 5
+  |
+  o  04c8ba6df782 commit 4
+  |
+  o  484c6ac0cea3 commit 3
+  |
+  o  9b19176bb127 commit 2
+  |
+  o  241ace8326d0 commit 1
+  
+  $ hg annotate -c a
+  241ace8326d0: 1a
+  9b19176bb127: 2b
+  484c6ac0cea3: 3
+  04c8ba6df782: 4d
+  2f7ba78d6abc: 5e
+
+Do it again, but this time with an unrelated commit checked out (plus working
+directory changes on top):
 
   $ cat > a <<EOF
+  > 1a
   > 2b
-  > 4d
+  > 3
+  > 4f
+  > 5g
   > EOF
-  $ echo y | hg absorb --config ui.interactive=1
+  $ hg commit -qm "commit to absorb 2"
+  $ TOABSORB=$(hg id -i)
+
+  $ hg update -q 241ace8326d0
+  $ echo committed unrelated >> a
+  $ hg commit -qm "unrelated commit"
+  $ echo pending wdir change >> a
+
+  $ hg absorb --apply-changes --print-changes -r ${TOABSORB}
   showing changes for a
-          @@ -0,1 +0,0 @@
-  f548282 -1a
-          @@ -2,1 +1,0 @@
-  ff5d556 -3
-          @@ -4,1 +2,0 @@
-  84e5416 -5e
+          @@ -3,2 +3,2 @@
+  04c8ba6 -4d
+  2f7ba78 -5e
+  04c8ba6 +4f
+  2f7ba78 +5g
   
-  3 changesets affected
-  84e5416 commit 5
-  ff5d556 commit 3
-  f548282 commit 1
-  apply changes (yn)?  y
-  saved backup bundle to * (glob)
-  3 of 3 chunk(s) applied
-  $ hg annotate a
-  1: 2b
-  2: 4d
-  $ hg log -T '{rev} {desc}\n' -Gp
-  @  2 commit 4
-  |  diff -r 1cae118c7ed8 -r 58a62bade1c6 a
-  |  --- a/a	Thu Jan 01 00:00:00 1970 +0000
-  |  +++ b/a	Thu Jan 01 00:00:00 1970 +0000
-  |  @@ -1,1 +1,2 @@
-  |   2b
-  |  +4d
+  2 changesets affected
+  2f7ba78 commit 5
+  04c8ba6 commit 4
+  1 new orphan changesets
+  1 of 1 chunk(s) applied
+
+  $ hg annotate -c a -r 'wdir()'
+  241ace8326d0 : 1a
+  dbce69d9fe03 : committed unrelated
+  dbce69d9fe03+: pending wdir change
+
+
+  $ hg update -Cq .
+
+  $ hg rebase -d tip -r ${TOABSORB}
+  rebasing \d+:[0-9a-f]+ "commit to absorb 2" (re)
+  note: not rebasing \d+:[0-9a-f]+ "commit to absorb 2", its destination already has all its changes (re)
+
+  $ hg log -G -T '{node|short} {desc} {instabilities}'
+  o  789b01face13 commit 5
   |
-  o  1 commit 2
-  |  diff -r 84add69aeac0 -r 1cae118c7ed8 a
-  |  --- a/a	Thu Jan 01 00:00:00 1970 +0000
-  |  +++ b/a	Thu Jan 01 00:00:00 1970 +0000
-  |  @@ -0,0 +1,1 @@
-  |  +2b
+  o  9c83c60f49f2 commit 4
   |
-  o  0 commit 1
+  | @  dbce69d9fe03 unrelated commit
+  | |
+  o |  484c6ac0cea3 commit 3
+  | |
+  o |  9b19176bb127 commit 2
+  |/
+  o  241ace8326d0 commit 1
   
 
-Non 1:1 map changes will be ignored:
-
-  $ echo 1 > a
-  $ hg absorb --apply-changes
-  nothing applied
-  [1]
-
-The prompt is not given if there are no changes to be applied, even if there
-are some changes that won't be applied:
-
-  $ hg absorb
-  showing changes for a
-          @@ -0,2 +0,1 @@
-          -2b
-          -4d
-          +1
-  
-  0 changesets affected
-  nothing applied
-  [1]
-
-Insertaions:
-
-  $ cat > a << EOF
-  > insert before 2b
-  > 2b
-  > 4d
-  > insert aftert 4d
-  > EOF
-  $ hg absorb -q --apply-changes
-  $ hg status
-  $ hg annotate a
-  1: insert before 2b
-  1: 2b
-  2: 4d
-  2: insert aftert 4d
-
-Bookmarks are moved:
-
-  $ hg bookmark -r 1 b1
-  $ hg bookmark -r 2 b2
-  $ hg bookmark ba
-  $ hg bookmarks
-     b1                        1:b35060a57a50
-     b2                        2:946e4bc87915
-   * ba                        2:946e4bc87915
-  $ sedi 's/insert/INSERT/' a
-  $ hg absorb -q --apply-changes
-  $ hg status
-  $ hg bookmarks
-     b1                        1:a4183e9b3d31
-     b2                        2:c9b20c925790
-   * ba                        2:c9b20c925790
-
-Non-modified files are ignored:
-
-  $ touch b
-  $ hg commit -A b -m b
-  $ touch c
-  $ hg add c
-  $ hg rm b
-  $ hg absorb --apply-changes
-  nothing applied
-  [1]
-  $ sedi 's/INSERT/Insert/' a
-  $ hg absorb --apply-changes
-  saved backup bundle to * (glob)
-  2 of 2 chunk(s) applied
-  $ hg status
-  A c
-  R b
-
-Public commits will not be changed:
-
-  $ hg phase -p 1
-  $ sedi 's/Insert/insert/' a
-  $ hg absorb -pn
-  showing changes for a
-          @@ -0,1 +0,1 @@
-          -Insert before 2b
-          +insert before 2b
-          @@ -3,1 +3,1 @@
-  85b4e0e -Insert aftert 4d
-  85b4e0e +insert aftert 4d
-  
-  1 changesets affected
-  85b4e0e commit 4
-  $ hg absorb --apply-changes
-  saved backup bundle to * (glob)
-  1 of 2 chunk(s) applied
-  $ hg diff -U 0
-  diff -r 1c8eadede62a a
-  --- a/a	Thu Jan 01 00:00:00 1970 +0000
-  +++ b/a	* (glob)
-  @@ -1,1 +1,1 @@
-  -Insert before 2b
-  +insert before 2b
-  $ hg annotate a
-  1: Insert before 2b
-  1: 2b
-  2: 4d
-  2: insert aftert 4d
-
-  $ hg co -qC 1
-  $ sedi 's/Insert/insert/' a
-  $ hg absorb --apply-changes
-  abort: no mutable changeset to change
-  [255]
-
-Make working copy clean:
-
-  $ hg co -qC ba
-  $ rm c
-  $ hg status
-
-Merge commit will not be changed:
-
-  $ echo 1 > m1
-  $ hg commit -A m1 -m m1
-  $ hg bookmark -q -i m1
-  $ hg update -q '.^'
-  $ echo 2 > m2
-  $ hg commit -q -A m2 -m m2
-  $ hg merge -q m1
-  $ hg commit -m merge
-  $ hg bookmark -d m1
-  $ hg log -G -T '{rev} {desc} {phase}\n'
-  @    6 merge draft
-  |\
-  | o  5 m2 draft
-  | |
-  o |  4 m1 draft
-  |/
-  o  3 b draft
-  |
-  o  2 commit 4 draft
-  |
-  o  1 commit 2 public
-  |
-  o  0 commit 1 public
-  
-  $ echo 2 >> m1
-  $ echo 2 >> m2
-  $ hg absorb --apply-changes
-  abort: cannot absorb into a merge
-  [255]
-  $ hg revert -q -C m1 m2
-
-Use a new repo:
-
-  $ cd ..
-  $ hg init repo2
-  $ cd repo2
-
-Make some commits to multiple files:
-
-  $ for f in a b; do
-  >   for i in 1 2; do
-  >     echo $f line $i >> $f
-  >     hg commit -A $f -m "commit $f $i" -q
-  >   done
-  > done
-
-Use pattern to select files to be fixed up:
-
-  $ sedi 's/line/Line/' a b
-  $ hg status
-  M a
-  M b
-  $ hg absorb --apply-changes a
-  saved backup bundle to * (glob)
-  1 of 1 chunk(s) applied
-  $ hg status
-  M b
-  $ hg absorb --apply-changes --exclude b
-  nothing applied
-  [1]
-  $ hg absorb --apply-changes b
-  saved backup bundle to * (glob)
-  1 of 1 chunk(s) applied
-  $ hg status
-  $ cat a b
-  a Line 1
-  a Line 2
-  b Line 1
-  b Line 2
-
-Test config option absorb.max-stack-size:
+  $ hg annotate -c a -r tip
+  241ace8326d0: 1a
+  9b19176bb127: 2b
+  484c6ac0cea3: 3
+  9c83c60f49f2: 4f
+  789b01face13: 5g
 
-  $ sedi 's/Line/line/' a b
-  $ hg log -T '{rev}:{node} {desc}\n'
-  3:712d16a8f445834e36145408eabc1d29df05ec09 commit b 2
-  2:74cfa6294160149d60adbf7582b99ce37a4597ec commit b 1
-  1:28f10dcf96158f84985358a2e5d5b3505ca69c22 commit a 2
-  0:f9a81da8dc53380ed91902e5b82c1b36255a4bd0 commit a 1
-  $ hg --config absorb.max-stack-size=1 absorb -pn
-  absorb: only the recent 1 changesets will be analysed
-  showing changes for a
-          @@ -0,2 +0,2 @@
-          -a Line 1
-          -a Line 2
-          +a line 1
-          +a line 2
-  showing changes for b
-          @@ -0,2 +0,2 @@
-          -b Line 1
-  712d16a -b Line 2
-          +b line 1
-  712d16a +b line 2
-  
-  1 changesets affected
-  712d16a commit b 2
-
-Test obsolete markers creation:
-
-  $ cat >> $HGRCPATH << EOF
-  > [experimental]
-  > evolution=createmarkers
-  > [absorb]
-  > add-noise=1
-  > EOF
-
-  $ hg --config absorb.max-stack-size=3 absorb -a
-  absorb: only the recent 3 changesets will be analysed
-  2 of 2 chunk(s) applied
-  $ hg log -T '{rev}:{node|short} {desc} {get(extras, "absorb_source")}\n'
-  6:3dfde4199b46 commit b 2 712d16a8f445834e36145408eabc1d29df05ec09
-  5:99cfab7da5ff commit b 1 74cfa6294160149d60adbf7582b99ce37a4597ec
-  4:fec2b3bd9e08 commit a 2 28f10dcf96158f84985358a2e5d5b3505ca69c22
-  0:f9a81da8dc53 commit a 1 
-  $ hg absorb --apply-changes
-  1 of 1 chunk(s) applied
-  $ hg log -T '{rev}:{node|short} {desc} {get(extras, "absorb_source")}\n'
-  10:e1c8c1e030a4 commit b 2 3dfde4199b4610ea6e3c6fa9f5bdad8939d69524
-  9:816c30955758 commit b 1 99cfab7da5ffdaf3b9fc6643b14333e194d87f46
-  8:5867d584106b commit a 2 fec2b3bd9e0834b7cb6a564348a0058171aed811
-  7:8c76602baf10 commit a 1 f9a81da8dc53380ed91902e5b82c1b36255a4bd0
-
-Executable files:
-
-  $ cat >> $HGRCPATH << EOF
-  > [diff]
-  > git=True
-  > EOF
-  $ cd ..
-  $ hg init repo3
-  $ cd repo3
-
-#if execbit
-  $ echo > foo.py
-  $ chmod +x foo.py
-  $ hg add foo.py
-  $ hg commit -mfoo
-#else
-  $ hg import -q --bypass - <<EOF
-  > # HG changeset patch
-  > foo
-  > 
-  > diff --git a/foo.py b/foo.py
-  > new file mode 100755
-  > --- /dev/null
-  > +++ b/foo.py
-  > @@ -0,0 +1,1 @@
-  > +
-  > EOF
-  $ hg up -q
-#endif
-
-  $ echo bla > foo.py
-  $ hg absorb --dry-run --print-changes
-  showing changes for foo.py
-          @@ -0,1 +0,1 @@
-  99b4ae7 -
-  99b4ae7 +bla
-  
-  1 changesets affected
-  99b4ae7 foo
-  $ hg absorb --dry-run --interactive --print-changes
-  diff -r 99b4ae712f84 foo.py
-  1 hunks, 1 lines changed
-  examine changes to 'foo.py'?
-  (enter ? for help) [Ynesfdaq?] y
-  
-  @@ -1,1 +1,1 @@
-  -
-  +bla
-  record this change to 'foo.py'?
-  (enter ? for help) [Ynesfdaq?] y
-  
-  showing changes for foo.py
-          @@ -0,1 +0,1 @@
-  99b4ae7 -
-  99b4ae7 +bla
-  
-  1 changesets affected
-  99b4ae7 foo
-  $ hg absorb --apply-changes
-  1 of 1 chunk(s) applied
-  $ hg diff -c .
-  diff --git a/foo.py b/foo.py
-  new file mode 100755
-  --- /dev/null
-  +++ b/foo.py
-  @@ -0,0 +1,1 @@
-  +bla
-  $ hg diff
-
-Remove lines may delete changesets:
-
-  $ cd ..
-  $ hg init repo4
-  $ cd repo4
-  $ cat > a <<EOF
-  > 1
-  > 2
-  > EOF
-  $ hg commit -m a12 -A a
-  $ cat > b <<EOF
-  > 1
-  > 2
-  > EOF
-  $ hg commit -m b12 -A b
-  $ echo 3 >> b
-  $ hg commit -m b3
-  $ echo 4 >> b
-  $ hg commit -m b4
-  $ echo 1 > b
-  $ echo 3 >> a
-  $ hg absorb -pn
-  showing changes for a
-          @@ -2,0 +2,1 @@
-  bfafb49 +3
-  showing changes for b
-          @@ -1,3 +1,0 @@
-  1154859 -2
-  30970db -3
-  a393a58 -4
-  
-  4 changesets affected
-  a393a58 b4
-  30970db b3
-  1154859 b12
-  bfafb49 a12
-  $ hg absorb -av | grep became
-  0:bfafb49242db: 1 file(s) changed, became 4:1a2de97fc652
-  1:115485984805: 2 file(s) changed, became 5:0c930dfab74c
-  2:30970dbf7b40: became empty and was dropped
-  3:a393a58b9a85: became empty and was dropped
-  $ hg log -T '{rev} {desc}\n' -Gp
-  @  5 b12
-  |  diff --git a/b b/b
-  |  new file mode 100644
-  |  --- /dev/null
-  |  +++ b/b
-  |  @@ -0,0 +1,1 @@
-  |  +1
-  |
-  o  4 a12
-     diff --git a/a b/a
-     new file mode 100644
-     --- /dev/null
-     +++ b/a
-     @@ -0,0 +1,3 @@
-     +1
-     +2
-     +3
-  
-
-Use revert to make the current change and its parent disappear.
-This should move us to the non-obsolete ancestor.
-
-  $ cd ..
-  $ hg init repo5
-  $ cd repo5
-  $ cat > a <<EOF
-  > 1
-  > 2
-  > EOF
-  $ hg commit -m a12 -A a
-  $ hg id
-  bfafb49242db tip
-  $ echo 3 >> a
-  $ hg commit -m a123 a
-  $ echo 4 >> a
-  $ hg commit -m a1234 a
-  $ hg id
-  82dbe7fd19f0 tip
-  $ hg revert -r 0 a
-  $ hg absorb -pn
-  showing changes for a
-          @@ -2,2 +2,0 @@
-  f1c23dd -3
-  82dbe7f -4
-  
-  2 changesets affected
-  82dbe7f a1234
-  f1c23dd a123
-  $ hg absorb --apply-changes --verbose
-  1:f1c23dd5d08d: became empty and was dropped
-  2:82dbe7fd19f0: became empty and was dropped
-  a: 1 of 1 chunk(s) applied
-  $ hg id
-  bfafb49242db tip
diff --git a/hgext/absorb.py b/hgext/absorb.py
--- a/hgext/absorb.py
+++ b/hgext/absorb.py
@@ -979,18 +979,20 @@ 
     return overlaycontext(memworkingcopy, ctx)
 
 
-def absorb(ui, repo, stack=None, targetctx=None, pats=None, opts=None):
+def absorb(ui, repo, targetctx, stack=None, pats=None, opts=None):
     """pick fixup chunks from targetctx, apply them to stack.
 
-    if targetctx is None, the working copy context will be used.
     if stack is None, the current draft stack will be used.
     return fixupstate.
     """
     if stack is None:
         limit = ui.configint(b'absorb', b'max-stack-size')
-        headctx = repo[b'.']
+        headctx = targetctx.p1()
         if len(headctx.parents()) > 1:
             raise error.Abort(_(b'cannot absorb into a merge'))
+        if len(targetctx.parents()) > 1:
+            raise error.Abort(_(b'cannot absorb a merge'))
+
         stack = getdraftstack(headctx, limit)
         if limit and len(stack) >= limit:
             ui.warn(
@@ -1002,8 +1004,6 @@ 
             )
     if not stack:
         raise error.Abort(_(b'no mutable changeset to change'))
-    if targetctx is None:  # default to working copy
-        targetctx = repo[None]
     if pats is None:
         pats = ()
     if opts is None:
@@ -1088,6 +1088,13 @@ 
                 b'(EXPERIMENTAL)'
             ),
         ),
+        (
+            b'r',
+            b'rev',
+            b'',
+            _(b'the revision to absorb changes from, if not the working '
+              b'directory'),
+        ),
     ]
     + commands.dryrunopts
     + commands.templateopts
@@ -1099,9 +1106,9 @@ 
 def absorbcmd(ui, repo, *pats, **opts):
     """incorporate corrections into the stack of draft changesets
 
-    absorb analyzes each change in your working directory and attempts to
-    amend the changed lines into the changesets in your stack that first
-    introduced those lines.
+    absorb analyzes each change in your working directory (or the revision given
+    to `--rev`, if one is specified) and attempts to amend the changed lines
+    into the changesets in your stack that first introduced those lines.
 
     If absorb cannot find an unambiguous changeset to amend for a change,
     that change will be left in the working directory, untouched. They can be
@@ -1126,6 +1133,10 @@ 
         if not opts[b'dry_run']:
             cmdutil.checkunfinished(repo)
 
-        state = absorb(ui, repo, pats=pats, opts=opts)
+        rev = opts[b'rev']
+        # default to working copy
+        ctx = scmutil.revsingle(repo, rev, default=None)
+
+        state = absorb(ui, repo, ctx, pats=pats, opts=opts)
         if sum(s[0] for s in state.chunkstats.values()) == 0:
             return 1