Patchwork revset: introduce feature revset for tracking in-progress work (issue4968)

login
register
mail settings
Submitter Andrew Halberstadt
Date Nov. 26, 2015, 10:31 p.m.
Message ID <3545b0234e4884f57dac.1448577083@localhost.localdomain>
Download mbox | patch
Permalink /patch/11680/
State Deferred
Headers show

Comments

Andrew Halberstadt - Nov. 26, 2015, 10:31 p.m.
# HG changeset patch
# User Andrew Halberstadt <ahalberstadt@mozilla.com>
# Date 1448490626 18000
#      Wed Nov 25 17:30:26 2015 -0500
# Node ID 3545b0234e4884f57dac44fd4e443deac5b9d673
# Parent  61fbf5dc12b23e7a2a30cf04ebd9f096c42a1f61
revset: introduce feature revset for tracking in-progress work (issue4968)

The revset "only(<rev>) and not public()" is often used to track wip features.
But this approach is simplistic and doesn't take things like merges,
obsolescence and bookmarks into account. This change formalizes a 'feature()'
revset that can turn a set of revision specifications into all the commits
within their associated feature branches.

A commit C is in the feature ending at revision R if all of the following
conditions are true:

1. C is R or C is an ancestor of R
2. C is not public
3. C is not a merge commit
4. C is not obsolete
5. no bookmarks exist in [C, R) for C != R
6. all commits in (C, R) are also within R for C != R
Gregory Szorc - Nov. 30, 2015, 7:25 p.m.
On Thu, Nov 26, 2015 at 2:31 PM, Andrew Halberstadt <halbersa@gmail.com>
wrote:

> # HG changeset patch
> # User Andrew Halberstadt <ahalberstadt@mozilla.com>
> # Date 1448490626 18000
> #      Wed Nov 25 17:30:26 2015 -0500
> # Node ID 3545b0234e4884f57dac44fd4e443deac5b9d673
> # Parent  61fbf5dc12b23e7a2a30cf04ebd9f096c42a1f61
> revset: introduce feature revset for tracking in-progress work (issue4968)
>
>
Related to the bikeshed this patch will likely trigger, my recollection is
mpm gave approval at the London sprint for a `hg smartlog` or `hg wip` type
command to be in core. For those not familiar, this would be a command that
basically shows a "not public()" centric view of changesets that haven't
been landed/finalized/published yet, probably with the visual graph shown,
possibly with a more condensed template to make output more readable.
timeless - Nov. 30, 2015, 9:53 p.m.
On Mon, Nov 30, 2015 at 2:25 PM, Gregory Szorc <gregory.szorc@gmail.com> wrote:
> On Thu, Nov 26, 2015 at 2:31 PM, Andrew Halberstadt <halbersa@gmail.com>
> wrote:
>>
>> # HG changeset patch
>> # User Andrew Halberstadt <ahalberstadt@mozilla.com>
>> # Date 1448490626 18000
>> #      Wed Nov 25 17:30:26 2015 -0500
>> # Node ID 3545b0234e4884f57dac44fd4e443deac5b9d673
>> # Parent  61fbf5dc12b23e7a2a30cf04ebd9f096c42a1f61
>> revset: introduce feature revset for tracking in-progress work (issue4968)
>>
>
> Related to the bikeshed this patch will likely trigger, my recollection is
> mpm gave approval at the London sprint for a `hg smartlog` or `hg wip` type
> command to be in core. For those not familiar, this would be a command that
> basically shows a "not public()" centric view of changesets that haven't
> been landed/finalized/published yet, probably with the visual graph shown,
> possibly with a more condensed template to make output more readable.


I'd prefer `wip()` over `feature()` for this bikeshed.

I think it does a better job of identifying what's in the shed.
Augie Fackler - Nov. 30, 2015, 10:31 p.m.
On Thu, Nov 26, 2015 at 05:31:23PM -0500, Andrew Halberstadt wrote:
> # HG changeset patch
> # User Andrew Halberstadt <ahalberstadt@mozilla.com>
> # Date 1448490626 18000
> #      Wed Nov 25 17:30:26 2015 -0500
> # Node ID 3545b0234e4884f57dac44fd4e443deac5b9d673
> # Parent  61fbf5dc12b23e7a2a30cf04ebd9f096c42a1f61
> revset: introduce feature revset for tracking in-progress work (issue4968)

Interesting
work. https://www.mercurial-scm.org/wiki/FeatureBranchesStruggle would
probably like a brief summary of what you've done here.

Pierre-yves, Sean (both cc'ed), and I have been getting frustrated
about this feature gap in hg for a while. We're hoping to put some
spit and polish on our "topics" concept in the near future, would you
have interest in testing that and seeing how it behaves compared with
what you've got in bookbinder?

(Bookbinder for those not reading the bug is
https://bitbucket.org/halbersa/bookbinder, which is an interesting
notion of how to "assign" a changeset to be "part" of a bookmark.)

>
> The revset "only(<rev>) and not public()" is often used to track wip features.
> But this approach is simplistic and doesn't take things like merges,
> obsolescence and bookmarks into account. This change formalizes a 'feature()'
> revset that can turn a set of revision specifications into all the commits
> within their associated feature branches.
>
> A commit C is in the feature ending at revision R if all of the following
> conditions are true:
>
> 1. C is R or C is an ancestor of R
> 2. C is not public
> 3. C is not a merge commit
> 4. C is not obsolete
> 5. no bookmarks exist in [C, R) for C != R
> 6. all commits in (C, R) are also within R for C != R
>
> diff --git a/mercurial/revset.py b/mercurial/revset.py
> --- a/mercurial/revset.py
> +++ b/mercurial/revset.py
> @@ -3,16 +3,17 @@
>  # Copyright 2010 Matt Mackall <mpm@selenic.com>
>  #
>  # This software may be used and distributed according to the terms of the
>  # GNU General Public License version 2 or any later version.
>
>  from __future__ import absolute_import
>
>  import heapq
> +import itertools
>  import re
>
>  from .i18n import _
>  from . import (
>      destutil,
>      encoding,
>      error,
>      hbisect,
> @@ -926,16 +927,51 @@ def extra(repo, subset, x):
>          kind, value, matcher = util.stringmatcher(value)
>
>      def _matchvalue(r):
>          extra = repo[r].extra()
>          return label in extra and (value is None or matcher(extra[label]))
>
>      return subset.filter(lambda r: _matchvalue(r))
>
> +def feature(repo, subset, x):
> +    """``feature(set)``
> +    Changesets that are in a feature of a revision specification in set.
> +
> +    Changeset X is in Y's feature if X is an ancestor of Y and they don't have
> +    any merge commits, public changesets or changesets with a bookmark in
> +    between them.
> +    """
> +    args = getargs(x, 1, 1, _("feature takes one argument"))
> +    specs = getset(repo, fullreposet(repo), args[0])
> +    specs = [formatspec('rev(%d)', s) if isinstance(s, int) else s
> +             for s in specs]
> +
> +    m = matchany(repo.ui, specs, repo)
> +    l = m(repo)
> +
> +    feature = set()
> +    for i, root in enumerate(l):
> +        nodes = itertools.chain([root], repo[root].ancestors())
> +        for node in nodes:
> +            ctx = repo[node]
> +            # if public, a merge or obsolete, not in feature
> +            if ctx.phase() == 0 or len(ctx.parents()) == 2 or ctx.obsolete():
> +                break
> +
> +            # if a bookmark other than specs[i] exists, not in feature
> +            marks = set(ctx.bookmarks()) - set(repo[root].bookmarks())
> +            if specs[i] in marks:
> +                marks.remove(specs[i])
> +            if len(marks) > 0:
> +                break
> +            feature.add(ctx.rev())
> +
> +    return subset & feature
> +
>  def filelog(repo, subset, x):
>      """``filelog(pattern)``
>      Changesets connected to the specified filelog.
>
>      For performance reasons, visits only revisions mentioned in the file-level
>      filelog, rather than filtering through all changesets (much faster, but
>      doesn't include deletes or duplicate changes). For a slower, more accurate
>      result, use ``file()``.
> @@ -2123,16 +2159,17 @@ symbols = {
>      "desc": desc,
>      "descendants": descendants,
>      "_firstdescendants": _firstdescendants,
>      "destination": destination,
>      "divergent": divergent,
>      "draft": draft,
>      "extinct": extinct,
>      "extra": extra,
> +    "feature": feature,
>      "file": hasfile,
>      "filelog": filelog,
>      "first": first,
>      "follow": follow,
>      "_followfirst": _followfirst,
>      "grep": grep,
>      "head": head,
>      "heads": heads,
> @@ -2200,16 +2237,17 @@ safesymbols = set([
>      "desc",
>      "descendants",
>      "_firstdescendants",
>      "destination",
>      "divergent",
>      "draft",
>      "extinct",
>      "extra",
> +    "feature",
>      "file",
>      "filelog",
>      "first",
>      "follow",
>      "_followfirst",
>      "head",
>      "heads",
>      "hidden",
> diff --git a/tests/test-revset.t b/tests/test-revset.t
> --- a/tests/test-revset.t
> +++ b/tests/test-revset.t
> @@ -2006,16 +2006,38 @@ test or-ed indirect predicates (issue377
>    0
>    1
>    2
>    3
>    4
>    5
>    6
>
> +test feature
> +  $ log 'feature(7)'
> +  7
> +  $ log 'feature(only)'
> +  0
> +  1
> +  2
> +  4
> +  8
> +  9
> +  $ hg phase -f -p 8
> +  $ log 'feature(only)'
> +  9
> +  $ log 'feature(8)'
> +  $ hg phase -f -d 8
> +  $ hg bookmark -r 8 other
> +  $ log 'feature(only)'
> +  9
> +  $ log 'feature(only | 7)'
> +  7
> +  9
> +
>  tests for 'remote()' predicate:
>  #.  (csets in remote) (id)            (remote)
>  1.  less than local   current branch  "default"
>  2.  same with local   specified       "default"
>  3.  more than local   specified       specified
>
>    $ hg clone --quiet -U . ../remote3
>    $ cd ../remote3
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@selenic.com
> https://selenic.com/mailman/listinfo/mercurial-devel
Augie Fackler - Nov. 30, 2015, 10:36 p.m.
On Mon, Nov 30, 2015 at 04:53:36PM -0500, timeless wrote:
> On Mon, Nov 30, 2015 at 2:25 PM, Gregory Szorc <gregory.szorc@gmail.com> wrote:
> > On Thu, Nov 26, 2015 at 2:31 PM, Andrew Halberstadt <halbersa@gmail.com>
> > wrote:
> >>
> >> # HG changeset patch
> >> # User Andrew Halberstadt <ahalberstadt@mozilla.com>
> >> # Date 1448490626 18000
> >> #      Wed Nov 25 17:30:26 2015 -0500
> >> # Node ID 3545b0234e4884f57dac44fd4e443deac5b9d673
> >> # Parent  61fbf5dc12b23e7a2a30cf04ebd9f096c42a1f61
> >> revset: introduce feature revset for tracking in-progress work (issue4968)
> >>
> >
> > Related to the bikeshed this patch will likely trigger, my recollection is
> > mpm gave approval at the London sprint for a `hg smartlog` or `hg wip` type
> > command to be in core. For those not familiar, this would be a command that
> > basically shows a "not public()" centric view of changesets that haven't
> > been landed/finalized/published yet, probably with the visual graph shown,
> > possibly with a more condensed template to make output more readable.
>
>
> I'd prefer `wip()` over `feature()` for this bikeshed.
>
> I think it does a better job of identifying what's in the shed.

The issue is that the output probably wants to include things that
aren't in progress as context information. My smartlog is pretty
complex, but basically it is along these lines:

heads(public()) + (not public()) + (parents(not public()))

I'm in favor of smartlog (with a default alias of sl also being around
probably) over wip, simply because it's less opaque ("it's like log,
but tries to be smart and show you things of interest").
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@selenic.com
> https://selenic.com/mailman/listinfo/mercurial-devel
Andrew Halberstadt - Nov. 30, 2015, 10:51 p.m.
Sure, I can add a blurb to that wiki page. I'd also be happy trying out 
this "topic" concept (though I don't know anything about it). Is there 
an extension I can install?

-Andrew

On 30/11/15 05:31 PM, Augie Fackler wrote:
> On Thu, Nov 26, 2015 at 05:31:23PM -0500, Andrew Halberstadt wrote:
>> # HG changeset patch
>> # User Andrew Halberstadt <ahalberstadt@mozilla.com>
>> # Date 1448490626 18000
>> #      Wed Nov 25 17:30:26 2015 -0500
>> # Node ID 3545b0234e4884f57dac44fd4e443deac5b9d673
>> # Parent  61fbf5dc12b23e7a2a30cf04ebd9f096c42a1f61
>> revset: introduce feature revset for tracking in-progress work (issue4968)
> Interesting
> work. https://www.mercurial-scm.org/wiki/FeatureBranchesStruggle would
> probably like a brief summary of what you've done here.
>
> Pierre-yves, Sean (both cc'ed), and I have been getting frustrated
> about this feature gap in hg for a while. We're hoping to put some
> spit and polish on our "topics" concept in the near future, would you
> have interest in testing that and seeing how it behaves compared with
> what you've got in bookbinder?
>
> (Bookbinder for those not reading the bug is
> https://bitbucket.org/halbersa/bookbinder, which is an interesting
> notion of how to "assign" a changeset to be "part" of a bookmark.)
Andrew Halberstadt - Nov. 30, 2015, 11:01 p.m.
On 30/11/15 05:36 PM, Augie Fackler wrote:
> On Mon, Nov 30, 2015 at 04:53:36PM -0500, timeless wrote:
>> On Mon, Nov 30, 2015 at 2:25 PM, Gregory Szorc <gregory.szorc@gmail.com> wrote:
>>> On Thu, Nov 26, 2015 at 2:31 PM, Andrew Halberstadt <halbersa@gmail.com>
>>> wrote:
>>>> # HG changeset patch
>>>> # User Andrew Halberstadt <ahalberstadt@mozilla.com>
>>>> # Date 1448490626 18000
>>>> #      Wed Nov 25 17:30:26 2015 -0500
>>>> # Node ID 3545b0234e4884f57dac44fd4e443deac5b9d673
>>>> # Parent  61fbf5dc12b23e7a2a30cf04ebd9f096c42a1f61
>>>> revset: introduce feature revset for tracking in-progress work (issue4968)
>>>>
>>> Related to the bikeshed this patch will likely trigger, my recollection is
>>> mpm gave approval at the London sprint for a `hg smartlog` or `hg wip` type
>>> command to be in core. For those not familiar, this would be a command that
>>> basically shows a "not public()" centric view of changesets that haven't
>>> been landed/finalized/published yet, probably with the visual graph shown,
>>> possibly with a more condensed template to make output more readable.
>>
>> I'd prefer `wip()` over `feature()` for this bikeshed.
>>
>> I think it does a better job of identifying what's in the shed.
> The issue is that the output probably wants to include things that
> aren't in progress as context information. My smartlog is pretty
> complex, but basically it is along these lines:
>
> heads(public()) + (not public()) + (parents(not public()))
>
> I'm in favor of smartlog (with a default alias of sl also being around
> probably) over wip, simply because it's less opaque ("it's like log,
> but tries to be smart and show you things of interest").

Fwiw, I don't really like the name 'feature()' either. I just didn't have any better ideas at the time. I'll gladly change it to whatever the consensus is.
Augie Fackler - Nov. 30, 2015, 11:02 p.m.
On Mon, Nov 30, 2015 at 05:51:47PM -0500, Andrew Halberstadt wrote:
> Sure, I can add a blurb to that wiki page. I'd also be happy trying out this
> "topic" concept (though I don't know anything about it). Is there an
> extension I can install?

We've got some known rough edges on topics that can probably be fixed
with only a small chunk of effort. I'll try and report back on this
thread when we get those dealt with.

>
> -Andrew
>
> On 30/11/15 05:31 PM, Augie Fackler wrote:
> >On Thu, Nov 26, 2015 at 05:31:23PM -0500, Andrew Halberstadt wrote:
> >># HG changeset patch
> >># User Andrew Halberstadt <ahalberstadt@mozilla.com>
> >># Date 1448490626 18000
> >>#      Wed Nov 25 17:30:26 2015 -0500
> >># Node ID 3545b0234e4884f57dac44fd4e443deac5b9d673
> >># Parent  61fbf5dc12b23e7a2a30cf04ebd9f096c42a1f61
> >>revset: introduce feature revset for tracking in-progress work (issue4968)
> >Interesting
> >work. https://www.mercurial-scm.org/wiki/FeatureBranchesStruggle would
> >probably like a brief summary of what you've done here.
> >
> >Pierre-yves, Sean (both cc'ed), and I have been getting frustrated
> >about this feature gap in hg for a while. We're hoping to put some
> >spit and polish on our "topics" concept in the near future, would you
> >have interest in testing that and seeing how it behaves compared with
> >what you've got in bookbinder?
> >
> >(Bookbinder for those not reading the bug is
> >https://bitbucket.org/halbersa/bookbinder, which is an interesting
> >notion of how to "assign" a changeset to be "part" of a bookmark.)
Durham Goode - Nov. 30, 2015, 11:39 p.m.
On 11/26/15 5:31 PM, Andrew Halberstadt wrote:
> # HG changeset patch
> # User Andrew Halberstadt <ahalberstadt@mozilla.com>
> # Date 1448490626 18000
> #      Wed Nov 25 17:30:26 2015 -0500
> # Node ID 3545b0234e4884f57dac44fd4e443deac5b9d673
> # Parent  61fbf5dc12b23e7a2a30cf04ebd9f096c42a1f61
> revset: introduce feature revset for tracking in-progress work (issue4968)
>
> The revset "only(<rev>) and not public()" is often used to track wip features.
> But this approach is simplistic and doesn't take things like merges,
> obsolescence and bookmarks into account. This change formalizes a 'feature()'
> revset that can turn a set of revision specifications into all the commits
> within their associated feature branches.
>
> A commit C is in the feature ending at revision R if all of the following
> conditions are true:
>
> 1. C is R or C is an ancestor of R
> 2. C is not public
> 3. C is not a merge commit
> 4. C is not obsolete
> 5. no bookmarks exist in [C, R) for C != R
> 6. all commits in (C, R) are also within R for C != R
>
This is awfully specific. What are the use cases that you want to use 
this revset for?

#3 why exclude merge commits if they met all the other criteria?
#5 this implies a certain usage of bookmarks (depending on what your use 
cases for feature() are)
timeless - Nov. 30, 2015, 11:51 p.m.
>> 1. C is R or C is an ancestor of R
>> 2. C is not public
>> 3. C is not a merge commit
>> 4. C is not obsolete
>> 5. no bookmarks exist in [C, R) for C != R
>> 6. all commits in (C, R) are also within R for C != R
>>
> This is awfully specific. What are the use cases that you want to use this
> revset for?
>
> #3 why exclude merge commits if they met all the other criteria?

I assume the UC is:

o - wip() - tip
|
m
| \
| o - public() -- excluded by class 2
|
o - wip()

The merge w/ public isn't interesting to someone reviewing their work

I suppose a more interesting question is:

o - wip() - tip
|
m - significant merge
| \
| o - wip()
|  |
o | - wip()
| /
o - wip(origin)
|
o - public()

where you diverged from yourself in making a and b and you also made a
meaningful merge at the merge point due to conflicts.

He's assuming that this wouldn't happen. Namely, people shouldn't be
adding features in merges, just resolving differences, which aren't
interesting when they're reviewing what they've done.

> #5 this implies a certain usage of bookmarks (depending on what your use
> cases for feature() are)

It implies if you're interested in a different thing, you'd look at
wip(otherbookmark).

It's not an unreasonable methodology. It does mean that a bookmark
ends work on one thing and begins work on another, but I don't see
that as particularly unreasonable.
Durham Goode - Nov. 30, 2015, 11:59 p.m.
On 11/30/15 6:51 PM, timeless wrote:
>>> 1. C is R or C is an ancestor of R
>>> 2. C is not public
>>> 3. C is not a merge commit
>>> 4. C is not obsolete
>>> 5. no bookmarks exist in [C, R) for C != R
>>> 6. all commits in (C, R) are also within R for C != R
>>>
>> This is awfully specific. What are the use cases that you want to use this
>> revset for?
>>
>> #3 why exclude merge commits if they met all the other criteria?
> I assume the UC is:
By use case, I meant more of: is this for making rebases easier? 
histedit?  'hg log -r'?  If it's not producing contiguous spans of 
commits, it's not so useful for histedit or rebase -r, so the workflows 
this is meant to work with kind of influences what criteria are chosen.

>
> o - wip() - tip
> |
> m
> | \
> | o - public() -- excluded by class 2
> |
> o - wip()
>
> The merge w/ public isn't interesting to someone reviewing their work
>
> I suppose a more interesting question is:
>
> o - wip() - tip
> |
> m - significant merge
> | \
> | o - wip()
> |  |
> o | - wip()
> | /
> o - wip(origin)
> |
> o - public()
>
> where you diverged from yourself in making a and b and you also made a
> meaningful merge at the merge point due to conflicts.
>
> He's assuming that this wouldn't happen. Namely, people shouldn't be
> adding features in merges, just resolving differences, which aren't
> interesting when they're reviewing what they've done.
>
>> #5 this implies a certain usage of bookmarks (depending on what your use
>> cases for feature() are)
> It implies if you're interested in a different thing, you'd look at
> wip(otherbookmark).
>
> It's not an unreasonable methodology. It does mean that a bookmark
> ends work on one thing and begins work on another, but I don't see
> that as particularly unreasonable.
Andrew Halberstadt - Dec. 1, 2015, 12:02 a.m.
On 30/11/15 06:39 PM, Durham Goode wrote:
> On 11/26/15 5:31 PM, Andrew Halberstadt wrote:
>> # HG changeset patch
>> # User Andrew Halberstadt <ahalberstadt@mozilla.com>
>> # Date 1448490626 18000
>> #      Wed Nov 25 17:30:26 2015 -0500
>> # Node ID 3545b0234e4884f57dac44fd4e443deac5b9d673
>> # Parent  61fbf5dc12b23e7a2a30cf04ebd9f096c42a1f61
>> revset: introduce feature revset for tracking in-progress work 
>> (issue4968)
>>
>> The revset "only(<rev>) and not public()" is often used to track wip 
>> features.
>> But this approach is simplistic and doesn't take things like merges,
>> obsolescence and bookmarks into account. This change formalizes a 
>> 'feature()'
>> revset that can turn a set of revision specifications into all the 
>> commits
>> within their associated feature branches.
>>
>> A commit C is in the feature ending at revision R if all of the 
>> following
>> conditions are true:
>>
>> 1. C is R or C is an ancestor of R
>> 2. C is not public
>> 3. C is not a merge commit
>> 4. C is not obsolete
>> 5. no bookmarks exist in [C, R) for C != R
>> 6. all commits in (C, R) are also within R for C != R
>>
> This is awfully specific. What are the use cases that you want to use 
> this revset for?
>
> #3 why exclude merge commits if they met all the other criteria?
> #5 this implies a certain usage of bookmarks (depending on what your 
> use cases for feature() are)

Yeah, timeless is pretty much dead on with my reasoning. You're also 
right about #5, it was designed to be used specifically with an 
extension [1] I wrote for enabling a particular bookmark based workflow. 
I understand if there's a desire to make the definition in core a little 
less strict, e.g by dropping the bookmarks/merge bits. Though for me 
personally, the bookmarks requirement is also the most useful part.

See the README of bookbinder for examples, but I use this regularly for 
log, rebase, graft, incoming/outgoing + all the evolve commands.

[1] https://bitbucket.org/halbersa/bookbinder

Patch

diff --git a/mercurial/revset.py b/mercurial/revset.py
--- a/mercurial/revset.py
+++ b/mercurial/revset.py
@@ -3,16 +3,17 @@ 
 # Copyright 2010 Matt Mackall <mpm@selenic.com>
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
 from __future__ import absolute_import
 
 import heapq
+import itertools
 import re
 
 from .i18n import _
 from . import (
     destutil,
     encoding,
     error,
     hbisect,
@@ -926,16 +927,51 @@  def extra(repo, subset, x):
         kind, value, matcher = util.stringmatcher(value)
 
     def _matchvalue(r):
         extra = repo[r].extra()
         return label in extra and (value is None or matcher(extra[label]))
 
     return subset.filter(lambda r: _matchvalue(r))
 
+def feature(repo, subset, x):
+    """``feature(set)``
+    Changesets that are in a feature of a revision specification in set.
+
+    Changeset X is in Y's feature if X is an ancestor of Y and they don't have
+    any merge commits, public changesets or changesets with a bookmark in
+    between them.
+    """
+    args = getargs(x, 1, 1, _("feature takes one argument"))
+    specs = getset(repo, fullreposet(repo), args[0])
+    specs = [formatspec('rev(%d)', s) if isinstance(s, int) else s
+             for s in specs]
+
+    m = matchany(repo.ui, specs, repo)
+    l = m(repo)
+
+    feature = set()
+    for i, root in enumerate(l):
+        nodes = itertools.chain([root], repo[root].ancestors())
+        for node in nodes:
+            ctx = repo[node]
+            # if public, a merge or obsolete, not in feature
+            if ctx.phase() == 0 or len(ctx.parents()) == 2 or ctx.obsolete():
+                break
+
+            # if a bookmark other than specs[i] exists, not in feature
+            marks = set(ctx.bookmarks()) - set(repo[root].bookmarks())
+            if specs[i] in marks:
+                marks.remove(specs[i])
+            if len(marks) > 0:
+                break
+            feature.add(ctx.rev())
+
+    return subset & feature
+
 def filelog(repo, subset, x):
     """``filelog(pattern)``
     Changesets connected to the specified filelog.
 
     For performance reasons, visits only revisions mentioned in the file-level
     filelog, rather than filtering through all changesets (much faster, but
     doesn't include deletes or duplicate changes). For a slower, more accurate
     result, use ``file()``.
@@ -2123,16 +2159,17 @@  symbols = {
     "desc": desc,
     "descendants": descendants,
     "_firstdescendants": _firstdescendants,
     "destination": destination,
     "divergent": divergent,
     "draft": draft,
     "extinct": extinct,
     "extra": extra,
+    "feature": feature,
     "file": hasfile,
     "filelog": filelog,
     "first": first,
     "follow": follow,
     "_followfirst": _followfirst,
     "grep": grep,
     "head": head,
     "heads": heads,
@@ -2200,16 +2237,17 @@  safesymbols = set([
     "desc",
     "descendants",
     "_firstdescendants",
     "destination",
     "divergent",
     "draft",
     "extinct",
     "extra",
+    "feature",
     "file",
     "filelog",
     "first",
     "follow",
     "_followfirst",
     "head",
     "heads",
     "hidden",
diff --git a/tests/test-revset.t b/tests/test-revset.t
--- a/tests/test-revset.t
+++ b/tests/test-revset.t
@@ -2006,16 +2006,38 @@  test or-ed indirect predicates (issue377
   0
   1
   2
   3
   4
   5
   6
 
+test feature
+  $ log 'feature(7)'
+  7
+  $ log 'feature(only)'
+  0
+  1
+  2
+  4
+  8
+  9
+  $ hg phase -f -p 8
+  $ log 'feature(only)'
+  9
+  $ log 'feature(8)'
+  $ hg phase -f -d 8
+  $ hg bookmark -r 8 other
+  $ log 'feature(only)'
+  9
+  $ log 'feature(only | 7)'
+  7
+  9
+
 tests for 'remote()' predicate:
 #.  (csets in remote) (id)            (remote)
 1.  less than local   current branch  "default"
 2.  same with local   specified       "default"
 3.  more than local   specified       specified
 
   $ hg clone --quiet -U . ../remote3
   $ cd ../remote3