[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