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

login
register
mail settings
Submitter Gregory Szorc
Date Aug. 21, 2016, 10:08 p.m.
Message ID <a9363f9f7e2f2f36e56a.1471817304@ubuntu-vm-main>
Download mbox | patch
Permalink /patch/16376/
State RFC, archived
Headers show

Comments

Gregory Szorc - Aug. 21, 2016, 10:08 p.m.
# HG changeset patch
# User Gregory Szorc <gregory.szorc@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.)
Augie Fackler - Aug. 24, 2016, 2:09 p.m.
On Sun, Aug 21, 2016 at 03:08:24PM -0700, Gregory Szorc wrote:
> # HG changeset patch
> # User Gregory Szorc <gregory.szorc@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

[...]

>  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.)

I like where you're going. I think if it should be a flag under any
command, it might be summary?

Not sure what (if anything) we should do about retaining flexibility
to change the definition of 'underway' going forward, but that seems
like it might be important as our feature branching strategy starts to
materialize.
Jun Wu - Aug. 24, 2016, 2:24 p.m.
For smartlog, I think the direction is to make it just:

  hg log -G -r 'smartlog()'

i.e. no need for a separate "smartlog" command. If the issue is only about
choosing changesets for the graph, is it already solved using the revset
layer? Is a new revset function more flexible?

cc Martijn who may have interest in this topic.

Excerpts from Gregory Szorc's message of 2016-08-21 15:08:24 -0700:
> # HG changeset patch
> # User Gregory Szorc <gregory.szorc@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@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')
> +
> +@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
> +
> +@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
> +

Patch

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@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')
+
+@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
+
+@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
+