[PATCH] revset: introduce feature revset for tracking in-progress work (issue4968)

Andrew Halberstadt halbersa at gmail.com
Thu Nov 26 22:31:23 UTC 2015


# HG changeset patch
# User Andrew Halberstadt <ahalberstadt at 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

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 at 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


More information about the Mercurial-devel mailing list