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

Jun Wu quark at fb.com
Wed Aug 24 10:24:42 EDT 2016


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