[PATCH RFC] underway: extension/command for displaying in progress work

Gregory Szorc gregory.szorc at gmail.com
Sun Aug 21 22:08:24 UTC 2016


# HG changeset patch
# User Gregory Szorc <gregory.szorc at gmail.com>
# Date 1471816945 25200
#      Sun Aug 21 15:02:25 2016 -0700
# Node ID a9363f9f7e2f2f36e56a8902291a7a3bf1bb2350
# Parent  997e8cf4d0a29d28759e38659736cb3d1cf9ef3f
underway: extension/command for displaying in progress work

It is common for developers to want to see a snapshot of "in progress"
work in their repository. Commands like `hg bookmarks`, `hg heads`,
`hg branches`, and even `hg qseries` do an OK job of answering this
question. But the output from these commands is overly simple: they
typically only show lists of changesets with no context to note their
position in the DAG (which is often necessary for performing operations
like `hg rebase`), the number of unfinished changesets, etc.

`hg wip` (from
http://jordi.inversethought.com/blog/customising-mercurial-like-a-pro/)
and `hg smartlog` (from
https://bitbucket.org/facebook/hg-experimental/) have both attempted to
solve the problem of "show me a DAG view of in progress work." And,
my experience supporting Mercurial users at Mozilla (where we encourage
the use of `hg wip`) tells me that developers *really* like this
command and functionality. If multiple entities have implemented nearly
the same thing, that's a sign there is a need for a feature in core
Mercurial. FWIW, I recall mpm giving verbal approval for adding such
a feature during a previous sprint.

This commit introduces the "underway" extension and command. The command
is effectively a glorified wrapper around `hg log -G` with a
semi-complicated revset query that shows underway/unfinished/in-progress
changesets and other "important" changesets (namely DAG heads and the
working directory).

I looked at the synonyms listed at
http://www.thesaurus.com/browse/in%20progress?s=t and felt "underway"
was the most appopriate. Here are reasons I ruled out alternatives:

* "wip" is not an intuitive name and therefore has discovery problems,
  especially for non-English speakers.
* "smartlog" is also not intuitive because "smart" isn't descriptive.
  Also, "what makes it 'smart'?' Why can't `hg log` be "smart" by
  default?
* "inprogress" was tempting, but I was worried about the prefix naming
  collision with "incoming."
* "unfinished" was also tempting but I don't like introducing an "un"
  prefixed command without the corresponding command lacking the
  prefix (the presence of the prefix implies that the opposite
  operation/command is possible).

TODO

* More concise template output. IMO one of the big advantages of `hg wip` is
  its concise template that allows you to see a lot about the DAG shape
  without having to excessively scroll. I would like for the command
  to use a new, less verbose, template by default.
* Rename and/or document the options to control revset behavior.
* Should we add an "--underway" flag to `hg log` and let users install
  their own command aliases? (IMO `hg log` already has an argument
  count/complexity problem.)

diff --git a/hgext/underway.py b/hgext/underway.py
new file mode 100644
--- /dev/null
+++ b/hgext/underway.py
@@ -0,0 +1,108 @@
+# underway.py - Show commits that are underway/in-progress
+#
+# Copyright 2016 Gregory Szorc <gregory.szorc at gmail.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
+
+from mercurial.node import nullrev
+from mercurial import (
+    cmdutil,
+    commands,
+    registrar,
+    revset,
+)
+from hgext import (
+    pager,
+)
+
+# Note for extension authors: ONLY specify testedwith = 'internal' for
+# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
+# be specifying the version(s) of Mercurial they are tested with, or
+# leave the attribute unspecified.
+testedwith = 'internal'
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+
+revsetpredicate = registrar.revsetpredicate()
+
+pager.attended.append('underway')
+
+ at revsetpredicate('_underway([commitage[, headage]])')
+def underwayrevset(repo, subset, x):
+    """Changesets that are still mutable and other relevant commits."""
+    args = revset.getargsdict(x, 'underway', 'commitage headage')
+    if 'commitage' not in args:
+        args['commitage'] = None
+    if 'headage' not in args:
+        args['headage'] = None
+
+    # We assume the only caller of this revset adds a topographical sort
+    # on the return. This means there is no benefit to making the revset
+    # lazy since the topographical sort needs to consume all revs.
+
+    # Mutable changesets (non-public) are the most important changesets to
+    # return. ``not public()`` will also pull in obsolete changesets if
+    # there is a non-obsolete changeset with obsolete ancestors. We explicitly
+    # exclude obsolete changesets from this query. The expansion below to
+    # pull in parents of returned changesets will add the first obsolete
+    # ancestor, adding sufficient context to the returned set.
+    rs = 'not public() and not obsolete()'
+    rsargs = []
+    if args['commitage']:
+        rs += ' and date(%s)'
+        rsargs.append(args['commitage'][1])
+    mutable = repo.revs(rs, *rsargs)
+    relevant = mutable
+
+    # Add parents of mutable changesets to provide context.
+    relevant += repo.revs('parents(%ld)', mutable)
+
+    # We also pull in (public) heads if they a) aren't closing a branch b) are
+    # recent.
+    rs = 'head() and not closed()'
+    rsargs = []
+    if args['headage']:
+        rs += ' and date(%s)'
+        rsargs.append(args['headage'][1])
+    relevant += repo.revs(rs, *rsargs)
+
+    # And add the changeset the working directory is based on.
+    wdirrev = repo['.'].rev()
+    if wdirrev != nullrev:
+        relevant += revset.baseset(data=(wdirrev,))
+
+    return subset & relevant
+
+ at command('underway', commands.templateopts)
+def underway(ui, repo, **opts):
+    """show changesets whose development is in progress
+
+    This command will print a graphical view of mutable and "important"
+    changesets. Specifically, it shows changesets that are:
+
+      * mutable
+      * parents of mutable changesets
+      * heads that don't close branches
+      * what the working directory is based on
+
+    The purpose of this command is to give an overview of unfinished work
+    (defined as mutable, non-public changesets) and provide additional
+    context to help finish that work.
+    """
+    commitage = ui.configint('underway', 'maxcommitage', 0)
+    headage = ui.configint('underway', 'maxheadage', 14)
+
+    rsargs = {
+        'headage': '-%d' % headage
+    }
+    if commitage:
+        rsargs['commitage'] = '-%d' % commitage
+
+    args = ', '.join('%s="%s"' % (k, v) for k, v in rsargs.items())
+    rs = 'sort(_underway(%s), topo)' % args
+
+    return cmdutil.graphlog(ui, repo, rev=[rs], **opts)
diff --git a/tests/test-underway.t b/tests/test-underway.t
new file mode 100644
--- /dev/null
+++ b/tests/test-underway.t
@@ -0,0 +1,335 @@
+  $ cat > testmocks.py << EOF
+  > import os
+  > from mercurial import util
+  > origmakedate = util.makedate
+  > def mockmakedate(timestamp=None):
+  >     if 'FAKETIME' in os.environ:
+  >         return int(os.environ['FAKETIME']), 0
+  >     return origmakedate(timestamp)
+  > util.makedate = mockmakedate
+  > EOF
+
+  $ cat >> $HGRCPATH << EOF
+  > [extensions]
+  > underway =
+  > testmocks = $TESTTMP/testmocks.py
+  > EOF
+
+`hg underway` works on an empty repo
+
+  $ hg init repo0
+  $ cd repo0
+  $ hg underway
+
+Single, unpublished changeset
+
+  $ touch file0
+  $ hg -q commit -A -m file0
+  $ hg underway
+  @  changeset:   0:d26a60f4f448
+     tag:         tip
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     file0
+  
+
+Single, unpublished changeset, no working directory
+
+  $ hg -q up -r null
+  $ hg underway
+  o  changeset:   0:d26a60f4f448
+     tag:         tip
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     file0
+  
+
+Single, published changeset, no working directory
+
+  $ hg phase --public -r 0
+  $ hg underway
+
+Single, published changeset, checked out
+
+  $ hg -q up -r 0
+  $ hg underway
+  @  changeset:   0:d26a60f4f448
+     tag:         tip
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     file0
+  
+  $ cd ..
+
+Now test repos with multiple heads
+
+  $ hg init repo1
+  $ cd repo1
+  $ touch file0
+  $ hg -q commit -A -m initial
+  $ touch file1
+  $ hg -q commit -A -m 'head 1 commit 1'
+  $ touch file2
+  $ hg -q commit -A -m 'head 1 commit 2'
+  $ hg -q up -r 0
+  $ touch file3
+  $ hg -q commit -A -m 'head 2 commit 1'
+  $ touch file4
+  $ hg -q commit -A -m 'head 2 commit 2'
+
+All changesets are draft so all should be displayed
+
+  $ hg underway
+  @  changeset:   4:566e972f7665
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     head 2 commit 2
+  |
+  o  changeset:   3:06edf01d2aac
+  |  parent:      0:ab3e79bd1841
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     head 2 commit 1
+  |
+  | o  changeset:   2:e240c80f4191
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     head 1 commit 2
+  | |
+  | o  changeset:   1:b9f0938ca7db
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     head 1 commit 1
+  |
+  o  changeset:   0:ab3e79bd1841
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     initial
+  
+
+Marking the root as public should still show since it is a parent of non-public
+
+  $ hg phase --public -r 0
+  $ hg underway
+  @  changeset:   4:566e972f7665
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     head 2 commit 2
+  |
+  o  changeset:   3:06edf01d2aac
+  |  parent:      0:ab3e79bd1841
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     head 2 commit 1
+  |
+  | o  changeset:   2:e240c80f4191
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     head 1 commit 2
+  | |
+  | o  changeset:   1:b9f0938ca7db
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     head 1 commit 1
+  |
+  o  changeset:   0:ab3e79bd1841
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     initial
+  
+
+Marking the first head as public should hide it since it is old
+(command assumes current time by default)
+
+  $ hg phase --public -r e240c80f4191
+  $ hg underway
+  @  changeset:   4:566e972f7665
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     head 2 commit 2
+  |
+  o  changeset:   3:06edf01d2aac
+  |  parent:      0:ab3e79bd1841
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     head 2 commit 1
+  |
+  o  changeset:   0:ab3e79bd1841
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     initial
+  
+
+Faking the current time should reveal first head since it is within relevance window
+
+  $ FAKETIME=0 hg underway
+  @  changeset:   4:566e972f7665
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     head 2 commit 2
+  |
+  o  changeset:   3:06edf01d2aac
+  |  parent:      0:ab3e79bd1841
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     head 2 commit 1
+  |
+  | o  changeset:   2:e240c80f4191
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     head 1 commit 2
+  |
+  o  changeset:   0:ab3e79bd1841
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     initial
+  
+
+  $ FAKETIME=864000 hg underway
+  @  changeset:   4:566e972f7665
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     head 2 commit 2
+  |
+  o  changeset:   3:06edf01d2aac
+  |  parent:      0:ab3e79bd1841
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     head 2 commit 1
+  |
+  | o  changeset:   2:e240c80f4191
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     head 1 commit 2
+  |
+  o  changeset:   0:ab3e79bd1841
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     initial
+  
+
+Making the first commit of head 2 public should hide root
+
+  $ hg phase --public -r 06edf01d2aac
+  $ hg underway
+  @  changeset:   4:566e972f7665
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     head 2 commit 2
+  |
+  o  changeset:   3:06edf01d2aac
+  |  parent:      0:ab3e79bd1841
+  ~  user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     head 2 commit 1
+  
+Making everything public should only show working directory
+
+  $ hg phase --public -r 566e972f7665
+  $ hg underway
+  @  changeset:   4:566e972f7665
+  |  tag:         tip
+  ~  user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     head 2 commit 2
+  
+
+  $ hg -q up -r e240c80f4191
+  $ hg underway
+  @  changeset:   2:e240c80f4191
+  |  user:        test
+  ~  date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     head 1 commit 2
+  
+If no working directory, nothing is shown
+
+  $ hg -q up -r null
+  $ hg underway
+
+Setting fake time will show the public heads only
+
+  $ FAKETIME=0 hg underway
+  o  changeset:   4:566e972f7665
+  |  tag:         tip
+  ~  user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     head 2 commit 2
+  
+  o  changeset:   2:e240c80f4191
+  |  user:        test
+  ~  date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     head 1 commit 2
+  
+
+Modifying the max head age time works
+
+  $ FAKETIME=864000 hg --config underway.maxheadage=5 underway
+
+  $ cd ..
+
+Now test display when obsolete changesets are involved
+
+  $ hg init obs
+  $ cd obs
+  $ cat >> .hg/hgrc << EOF
+  > [experimental]
+  > evolution = all
+  > EOF
+
+  $ touch file0
+  $ hg -q commit -A -m initial
+  $ touch file1
+  $ hg -q commit -A -m 'head 1 commit 1'
+  $ touch file2
+  $ hg -q commit -A -m 'head 1 commit 2'
+  $ touch file3
+  $ hg -q commit -A -m 'head 1 commit 3'
+  $ hg -q up -r null
+
+  $ hg phase --public -r 0
+  $ hg underway
+  o  changeset:   3:e3e108a93016
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     head 1 commit 3
+  |
+  o  changeset:   2:e240c80f4191
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     head 1 commit 2
+  |
+  o  changeset:   1:b9f0938ca7db
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     head 1 commit 1
+  |
+  o  changeset:   0:ab3e79bd1841
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     initial
+  
+
+Only the first obsolete ancestor should be displayed (commit 2)
+
+  $ hg debugobsolete b9f0938ca7db03fff32d91ca50504c0f54d14d1c
+  $ hg debugobsolete e240c80f41919c6ae8c86643f9661d50cb37201f
+
+  $ hg underway
+  o  changeset:   3:e3e108a93016
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     head 1 commit 3
+  |
+  x  changeset:   2:e240c80f4191
+  |  user:        test
+  ~  date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     head 1 commit 2
+  


More information about the Mercurial-devel mailing list