hgext/show.py
changeset 33197 c5a07a3abe7d
parent 33137 99ce2f586cd4
child 33208 9e7efe421395
--- a/hgext/show.py	Wed Jun 28 21:30:46 2017 -0400
+++ b/hgext/show.py	Sat Jul 01 22:38:42 2017 -0700
@@ -32,9 +32,11 @@
 from mercurial import (
     cmdutil,
     commands,
+    destutil,
     error,
     formatter,
     graphmod,
+    phases,
     pycompat,
     registrar,
     revset,
@@ -171,6 +173,166 @@
         fm.data(active=bm == active,
                 longestbookmarklen=longestname)
 
+@showview('stack', csettopic='stack')
+def showstack(ui, repo, displayer):
+    """current line of work"""
+    wdirctx = repo['.']
+    if wdirctx.rev() == nullrev:
+        raise error.Abort(_('stack view only available when there is a '
+                            'working directory'))
+
+    if wdirctx.phase() == phases.public:
+        ui.write(_('(empty stack; working directory is a published '
+                   'changeset)\n'))
+        return
+
+    # TODO extract "find stack" into a function to facilitate
+    # customization and reuse.
+
+    baserev = destutil.stackbase(ui, repo)
+    basectx = None
+
+    if baserev is None:
+        baserev = wdirctx.rev()
+        stackrevs = {wdirctx.rev()}
+    else:
+        stackrevs = set(repo.revs('%d::.', baserev))
+
+    ctx = repo[baserev]
+    if ctx.p1().rev() != nullrev:
+        basectx = ctx.p1()
+
+    # And relevant descendants.
+    branchpointattip = False
+    cl = repo.changelog
+
+    for rev in cl.descendants([wdirctx.rev()]):
+        ctx = repo[rev]
+
+        # Will only happen if . is public.
+        if ctx.phase() == phases.public:
+            break
+
+        stackrevs.add(ctx.rev())
+
+        if len(ctx.children()) > 1:
+            branchpointattip = True
+            break
+
+    stackrevs = list(reversed(sorted(stackrevs)))
+
+    # Find likely target heads for the current stack. These are likely
+    # merge or rebase targets.
+    if basectx:
+        # TODO make this customizable?
+        newheads = set(repo.revs('heads(%d::) - %ld - not public()',
+                                 basectx.rev(), stackrevs))
+    else:
+        newheads = set()
+
+    try:
+        cmdutil.findcmd('rebase', commands.table)
+        haverebase = True
+    except error.UnknownCommand:
+        haverebase = False
+
+    # TODO use templating.
+    # TODO consider using graphmod. But it may not be necessary given
+    # our simplicity and the customizations required.
+    # TODO use proper graph symbols from graphmod
+
+    shortesttmpl = formatter.maketemplater(ui, '{shortest(node, 5)}')
+    def shortest(ctx):
+        return shortesttmpl.render({'ctx': ctx, 'node': ctx.hex()})
+
+    # We write out new heads to aid in DAG awareness and to help with decision
+    # making on how the stack should be reconciled with commits made since the
+    # branch point.
+    if newheads:
+        # Calculate distance from base so we can render the count and so we can
+        # sort display order by commit distance.
+        revdistance = {}
+        for head in newheads:
+            # There is some redundancy in DAG traversal here and therefore
+            # room to optimize.
+            ancestors = cl.ancestors([head], stoprev=basectx.rev())
+            revdistance[head] = len(list(ancestors))
+
+        sourcectx = repo[stackrevs[-1]]
+
+        sortedheads = sorted(newheads, key=lambda x: revdistance[x],
+                             reverse=True)
+
+        for i, rev in enumerate(sortedheads):
+            ctx = repo[rev]
+
+            if i:
+                ui.write(': ')
+            else:
+                ui.write('  ')
+
+            ui.write(('o  '))
+            displayer.show(ctx)
+            displayer.flush(ctx)
+            ui.write('\n')
+
+            if i:
+                ui.write(':/')
+            else:
+                ui.write(' /')
+
+            ui.write('    (')
+            ui.write(_('%d commits ahead') % revdistance[rev],
+                     label='stack.commitdistance')
+
+            if haverebase:
+                # TODO may be able to omit --source in some scenarios
+                ui.write('; ')
+                ui.write(('hg rebase --source %s --dest %s' % (
+                         shortest(sourcectx), shortest(ctx))),
+                         label='stack.rebasehint')
+
+            ui.write(')\n')
+
+        ui.write(':\n:    ')
+        ui.write(_('(stack head)\n'), label='stack.label')
+
+    if branchpointattip:
+        ui.write(' \\ /  ')
+        ui.write(_('(multiple children)\n'), label='stack.label')
+        ui.write('  |\n')
+
+    for rev in stackrevs:
+        ctx = repo[rev]
+        symbol = '@' if rev == wdirctx.rev() else 'o'
+
+        if newheads:
+            ui.write(': ')
+        else:
+            ui.write('  ')
+
+        ui.write(symbol, '  ')
+        displayer.show(ctx)
+        displayer.flush(ctx)
+        ui.write('\n')
+
+    # TODO display histedit hint?
+
+    if basectx:
+        # Vertically and horizontally separate stack base from parent
+        # to reinforce stack boundary.
+        if newheads:
+            ui.write(':/   ')
+        else:
+            ui.write(' /   ')
+
+        ui.write(_('(stack base)'), '\n', label='stack.label')
+        ui.write(('o  '))
+
+        displayer.show(basectx)
+        displayer.flush(basectx)
+        ui.write('\n')
+
 @revsetpredicate('_underway([commitage[, headage]])')
 def underwayrevset(repo, subset, x):
     args = revset.getargsdict(x, 'underway', 'commitage headage')