changeset 136:fcd34011b0f0

Fix hg prompt functionality for Python 3.8 Includes Mathieu Clabaut's Python 3.8 patch (https://bitbucket.org/sjl/hg-prompt/issues/45/python-38-port) and reverts 0850e9a3 where the author stripped incoming/outgoing functionality (because I use it)
author IBBoard <dev@ibboard.co.uk>
date Sat, 18 Jan 2020 20:00:20 +0000
parents 298fa524406a
children f71524147eb4
files prompt.py
diffstat 1 files changed, 231 insertions(+), 137 deletions(-) [+]
line wrap: on
line diff
--- a/prompt.py	Sat Jan 04 20:09:53 2020 +0000
+++ b/prompt.py	Sat Jan 18 20:00:20 2020 +0000
@@ -6,28 +6,26 @@
 
 Useful mostly for putting information about the current repository into
 a shell prompt.
-
-https://hg.sr.ht/~sjl/hg-prompt/browse/default/prompt.py @ 9f0b70dfcf52
 '''
 
 from __future__ import with_statement
 
 import re
 import os
+import platform
 import subprocess
 from datetime import datetime, timedelta
-from contextlib import closing
 from os import path
 from mercurial import extensions, commands, cmdutil, help
 from mercurial.i18n import _
 from mercurial.node import hex, short
 
-# command registration moved into `registrar` module in v4.3.
+# command registration moved into `registrar` module in v3.7.
 cmdtable = {}
 try:
     from mercurial import registrar
     command = registrar.command(cmdtable)
-except (ImportError, AttributeError) as e:
+except ImportError:
     command = cmdutil.command(cmdtable)
 
 # `revrange' has been moved into module `scmutil' since v1.9.
@@ -37,8 +35,21 @@
 except :
     revrange = cmdutil.revrange
 
-FILTER_ARG = re.compile(r'\|.+\((.*)\)')
+CACHE_PATH = b".hg/prompt/cache"
+CACHE_TIMEOUT = timedelta(minutes=15)
+
+FILTER_ARG = re.compile(rb'\|.+\((.*)\)')
 
+def _cache_remote(repo, kind):
+    cache = path.join(repo.root, CACHE_PATH, kind)
+    c_tmp = cache + b'.temp'
+
+    # This is kind of a hack and I feel a little bit dirty for doing it.
+    IGNORE = open('NUL:','w') if platform.system() == 'Windows' else open('/dev/null','w')
+
+    subprocess.call(['hg', kind, '--quiet'], stdout=open(c_tmp, 'w'), stderr=IGNORE)
+    os.rename(c_tmp, cache)
+    return
 
 def _with_groups(groups, out):
     out_groups = [groups[0]] + [groups[-1]]
@@ -46,13 +57,13 @@
     if any(out_groups) and not all(out_groups):
         print('Error parsing prompt string.  Mismatched braces?')
 
-    out = out.replace('%', '%%')
-    return ("%s" + out + "%s") % (out_groups[0][:-1] if out_groups[0] else '',
-                                  out_groups[1][1:] if out_groups[1] else '')
+    out = out.replace(b'%', b'%%')
+    return (b"%b" + out + b"%b") % (out_groups[0][:-1] if out_groups[0] else b'',
+                                  out_groups[1][1:] if out_groups[1] else b'')
 
 def _get_filter(name, g):
     '''Return the filter with the given name, or None if it was not used.'''
-    matching_filters = filter(lambda s: s and s.startswith('|%s' % name), g)
+    matching_filters = list(filter(lambda s: s and s.startswith(b'|%b' % name), g))
     if not matching_filters:
         return None
 
@@ -72,7 +83,9 @@
         return None
 
 @command(b'prompt',
-         [(b'', b'angle-brackets', None, b'use angle brackets (<>) for keywords')],
+         [(b'', b'angle-brackets', None, b'use angle brackets (<>) for keywords'),
+          (b'', b'cache-incoming', None, b'used internally by hg-prompt'),
+          (b'', b'cache-outgoing', None, b'used internally by hg-prompt')],
          b'hg prompt STRING')
 def prompt(ui, repo, fs='', **opts):
     '''get repository information for use in a shell prompt
@@ -100,21 +113,14 @@
         currently at my-bookmark
 
     See 'hg help prompt-keywords' for a list of available keywords.
-
-    The format string may also be defined in an hgrc file::
-
-        [prompt]
-        template = {currently at {bookmark}}
-
-    This is used when no format string is passed on the command line.
     '''
 
     def _basename(m):
-        return _with_groups(m.groups(), path.basename(repo.root)) if repo.root else ''
+        return _with_groups(m.groups(), path.basename(repo.root)) if repo.root else b''
 
     def _bookmark(m):
         try:
-            book = extensions.find('bookmarks').current(repo)
+            book = extensions.find(b'bookmarks').current(repo)
         except AttributeError:
             book = getattr(repo, '_bookmarkcurrent', None)
         except KeyError:
@@ -126,110 +132,110 @@
             if repo._bookmarks[book] == cur:
                 return _with_groups(m.groups(), book)
         else:
-            return ''
+            return b''
 
     def _branch(m):
         g = m.groups()
 
         branch = repo.dirstate.branch()
-        quiet = _get_filter('quiet', g)
+        quiet = _get_filter(b'quiet', g)
 
-        out = branch if (not quiet) or (branch != 'default') else ''
+        out = branch if (not quiet) or (branch != b'default') else b''
 
-        return _with_groups(g, out) if out else ''
+        return _with_groups(g, out) if out else b''
 
     def _closed(m):
         g = m.groups()
 
-        quiet = _get_filter('quiet', g)
+        quiet = _get_filter(b'quiet', g)
 
         p = repo[None].parents()[0]
         pn = p.node()
         branch = repo.dirstate.branch()
-        closed = (p.extra().get('close')
+        closed = (p.extra().get(b'close')
                   and pn in repo.branchheads(branch, closed=True))
-        out = 'X' if (not quiet) and closed else ''
+        out = b'X' if (not quiet) and closed else b''
 
-        return _with_groups(g, out) if out else ''
+        return _with_groups(g, out) if out else b'b'
 
     def _count(m):
         g = m.groups()
-        query = [g[1][1:]] if g[1] else ['all()']
+        query = [g[1][1:]] if g[1] else [b'all()']
         return _with_groups(g, str(len(revrange(repo, query))))
 
     def _node(m):
         g = m.groups()
 
         parents = repo[None].parents()
-        p = 0 if '|merge' not in g else 1
+        p = 0 if b'|merge' not in g else 1
         p = p if len(parents) > p else None
 
-        format = short if '|short' in g else hex
+        format = short if b'|short' in g else hex
 
         node = format(parents[p].node()) if p is not None else None
-        return _with_groups(g, str(node)) if node else ''
+        return _with_groups(g, str(node)) if node else b''
 
     def _patch(m):
         g = m.groups()
 
         try:
-            extensions.find('mq')
+            extensions.find(b'mq')
         except KeyError:
-            return ''
+            return b''
 
         q = repo.mq
 
-        if _get_filter('quiet', g) and not len(q.series):
-            return ''
+        if _get_filter(b'quiet', g) and not len(q.series):
+            return b''
 
-        if _get_filter('topindex', g):
+        if _get_filter(b'topindex', g):
             if len(q.applied):
                 out = str(len(q.applied) - 1)
             else:
-                out = ''
-        elif _get_filter('applied', g):
+                out = b''
+        elif _get_filter(b'applied', g):
             out = str(len(q.applied))
-        elif _get_filter('unapplied', g):
+        elif _get_filter(b'unapplied', g):
             out = str(len(q.unapplied(repo)))
-        elif _get_filter('count', g):
+        elif _get_filter(b'count', g):
             out = str(len(q.series))
         else:
-            out = q.applied[-1].name if q.applied else ''
+            out = q.applied[-1].name if q.applied else b''
 
-        return _with_groups(g, out) if out else ''
+        return _with_groups(g, out) if out else b''
 
     def _patches(m):
         g = m.groups()
 
         try:
-            extensions.find('mq')
+            extensions.find(b'mq')
         except KeyError:
-            return ''
+            return b''
 
-        join_filter = _get_filter('join', g)
+        join_filter = _get_filter(b'join', g)
         join_filter_arg = _get_filter_arg(join_filter)
-        sep = join_filter_arg if join_filter else ' -> '
+        sep = join_filter_arg if join_filter else b' -> '
 
         patches = repo.mq.series
         applied = [p.name for p in repo.mq.applied]
         unapplied = filter(lambda p: p not in applied, patches)
 
-        if _get_filter('hide_applied', g):
-            patches = filter(lambda p: p not in applied, patches)
-        if _get_filter('hide_unapplied', g):
-            patches = filter(lambda p: p not in unapplied, patches)
+        if _get_filter(b'hide_applied', g):
+            patches = list(filter(lambda p: p not in applied, patches))
+        if _get_filter(b'hide_unapplied', g):
+            patches = list(filter(lambda p: p not in unapplied, patches))
 
-        if _get_filter('reverse', g):
+        if _get_filter(b'reverse', g):
             patches = reversed(patches)
 
-        pre_applied_filter = _get_filter('pre_applied', g)
+        pre_applied_filter = _get_filter(b'pre_applied', g)
         pre_applied_filter_arg = _get_filter_arg(pre_applied_filter)
-        post_applied_filter = _get_filter('post_applied', g)
+        post_applied_filter = _get_filter(b'post_applied', g)
         post_applied_filter_arg = _get_filter_arg(post_applied_filter)
 
-        pre_unapplied_filter = _get_filter('pre_unapplied', g)
+        pre_unapplied_filter = _get_filter(b'pre_unapplied', g)
         pre_unapplied_filter_arg = _get_filter_arg(pre_unapplied_filter)
-        post_unapplied_filter = _get_filter('post_unapplied', g)
+        post_unapplied_filter = _get_filter(b'post_unapplied', g)
         post_unapplied_filter_arg = _get_filter_arg(post_unapplied_filter)
 
         for n, patch in enumerate(patches):
@@ -244,39 +250,67 @@
                 if post_unapplied_filter:
                     patches[n] = patches[n] + post_unapplied_filter_arg
 
-        return _with_groups(g, sep.join(patches)) if patches else ''
+        return _with_groups(g, sep.join(patches)) if patches else b''
 
     def _queue(m):
         g = m.groups()
 
         try:
-            extensions.find('mq')
+            extensions.find(b'mq')
         except KeyError:
-            return ''
+            return b''
 
         q = repo.mq
 
         out = os.path.basename(q.path)
-        if out == 'patches' and not os.path.isdir(q.path):
-            out = ''
-        elif out.startswith('patches-'):
+        if out == b'patches' and not os.path.isdir(q.path):
+            out = b''
+        elif out.startswith(b'patches-'):
             out = out[8:]
 
-        return _with_groups(g, out) if out else ''
+        return _with_groups(g, out) if out else b''
+
+    def _remote(kind):
+        def _r(m):
+            g = m.groups()
+
+            cache_dir = path.join(repo.root, CACHE_PATH)
+            cache = path.join(cache_dir, kind)
+            if not path.isdir(cache_dir):
+                os.makedirs(cache_dir)
+
+            cache_exists = path.isfile(cache)
 
+            cache_time = (datetime.fromtimestamp(os.stat(cache).st_mtime)
+                          if cache_exists else None)
+            if not cache_exists or cache_time < datetime.now() - CACHE_TIMEOUT:
+                if not cache_exists:
+                    open(cache, 'w').close()
+                subprocess.Popen(['hg', 'prompt', '--cache-%s' % kind.decode('ascii')])
+
+            if cache_exists:
+                with open(cache) as c:
+                    count = len(c.readlines())
+                    if g[1]:
+                        return _with_groups(g, str(count).encode()) if count else b''
+                    else:
+                        return _with_groups(g, b'') if count else b''
+            else:
+                return b''
+        return _r
 
     def _rev(m):
         g = m.groups()
 
         parents = repo[None].parents()
-        parent = 0 if '|merge' not in g else 1
+        parent = 0 if b'|merge' not in g else 1
         parent = parent if len(parents) > parent else None
 
         rev = parents[parent].rev() if parent is not None else -1
-        return _with_groups(g, str(rev)) if rev >= 0 else ''
+        return _with_groups(g, str(rev)) if rev >= 0 else b''
 
     def _root(m):
-        return _with_groups(m.groups(), repo.root) if repo.root else ''
+        return _with_groups(m.groups(), repo.root) if repo.root else b''
 
     def _status(m):
         g = m.groups()
@@ -285,46 +319,46 @@
         modified = any(st[:4])
         unknown = len(st[-1]) > 0
 
-        flag = ''
-        if '|modified' not in g and '|unknown' not in g:
-            flag = '!' if modified else '?' if unknown else ''
+        flag = b''
+        if b'|modified' not in g and b'|unknown' not in g:
+            flag = b'!' if modified else b'?' if unknown else b''
         else:
-            if '|modified' in g:
-                flag += '!' if modified else ''
-            if '|unknown' in g:
-                flag += '?' if unknown else ''
+            if b'|modified' in g:
+                flag += b'!' if modified else b''
+            if b'|unknown' in g:
+                flag += b'?' if unknown else b''
 
-        return _with_groups(g, flag) if flag else ''
+        return _with_groups(g, flag) if flag else b''
 
     def _tags(m):
         g = m.groups()
 
-        sep = g[2][1:] if g[2] else ' '
+        sep = g[2][1:] if g[2] else b' '
         tags = repo[None].tags()
 
-        quiet = _get_filter('quiet', g)
+        quiet = _get_filter(b'quiet', g)
         if quiet:
-            tags = filter(lambda tag: tag != 'tip', tags)
+            tags = list(filter(lambda tag: tag != b'tip', tags))
 
-        return _with_groups(g, sep.join(tags)) if tags else ''
+        return _with_groups(g, sep.join(tags)) if tags else b''
 
     def _task(m):
         try:
-            task = extensions.find('tasks').current(repo)
-            return _with_groups(m.groups(), task) if task else ''
+            task = extensions.find(b'tasks').current(repo)
+            return _with_groups(m.groups(), task) if task else b''
         except KeyError:
-            return ''
+            return b''
 
     def _tip(m):
         g = m.groups()
 
-        format = short if '|short' in g else hex
+        format = short if b'|short' in g else hex
 
         tip = repo[len(repo) - 1]
         rev = tip.rev()
-        tip = format(tip.node()) if '|node' in g else tip.rev()
+        tip = format(tip.node()) if b'|node' in g else tip.rev()
 
-        return _with_groups(g, str(tip)) if rev >= 0 else ''
+        return _with_groups(g, str(tip)) if rev >= 0 else b''
 
     def _update(m):
         current_rev = repo[None].parents()[0]
@@ -336,77 +370,109 @@
         except (KeyError, IndexError):
             # We are in an empty repository.
 
-            return ''
+            return b''
 
         for head in reversed(heads):
             if not repo[head].closesbranch():
                 tip = head
                 break
 
-        return _with_groups(m.groups(), '^') if current_rev.children() else ''
+        return _with_groups(m.groups(), b'^') if current_rev != repo[tip] else b''
+
 
     if opts.get("angle_brackets"):
-        tag_start = r'\<([^><]*?\<)?'
-        tag_end = r'(\>[^><]*?)?>'
-        brackets = '<>'
+        tag_start = rb'\<([^><]*?\<)?'
+        tag_end = rb'(\>[^><]*?)?>'
+        brackets = b'<>'
     else:
-        tag_start = r'\{([^{}]*?\{)?'
-        tag_end = r'(\}[^{}]*?)?\}'
-        brackets = '{}'
+        tag_start = rb'\{([^{}]*?\{)?'
+        tag_end = rb'(\}[^{}]*?)?\}'
+        brackets = b'{}'
 
     patterns = {
-        'bookmark': _bookmark,
-        'branch(\|quiet)?': _branch,
-        'closed(\|quiet)?': _closed,
-        'count(\|[^%s]*?)?' % brackets[-1]: _count,
-        'node(?:'
-            '(\|short)'
-            '|(\|merge)'
-            ')*': _node,
-        'patch(?:'
-            '(\|topindex)'
-            '|(\|applied)'
-            '|(\|unapplied)'
-            '|(\|count)'
-            '|(\|quiet)'
-            ')*': _patch,
-        'patches(?:' +
-            '(\|join\([^%s]*?\))' % brackets[-1] +
-            '|(\|reverse)' +
-            '|(\|hide_applied)' +
-            '|(\|hide_unapplied)' +
-            '|(\|pre_applied\([^%s]*?\))' % brackets[-1] +
-            '|(\|post_applied\([^%s]*?\))' % brackets[-1] +
-            '|(\|pre_unapplied\([^%s]*?\))' % brackets[-1] +
-            '|(\|post_unapplied\([^%s]*?\))' % brackets[-1] +
-            ')*': _patches,
-        'queue': _queue,
-        'rev(\|merge)?': _rev,
-        'root': _root,
-        'root\|basename': _basename,
-        'status(?:'
-            '(\|modified)'
-            '|(\|unknown)'
-            ')*': _status,
-        'tags(?:' +
-            '(\|quiet)' +
-            '|(\|[^%s]*?)' % brackets[-1] +
-            ')*': _tags,
-        'task': _task,
-        'tip(?:'
-            '(\|node)'
-            '|(\|short)'
-            ')*': _tip,
-        'update': _update
+        b'bookmark': _bookmark,
+        b'branch(\|quiet)?': _branch,
+        b'closed(\|quiet)?': _closed,
+        b'count(\|[^%b]*?)?' % brackets[-1:]: _count,
+        b'node(?:'
+            b'(\|short)'
+            b'|(\|merge)'
+            b')*': _node,
+        b'patch(?:'
+            b'(\|topindex)'
+            b'|(\|applied)'
+            b'|(\|unapplied)'
+            b'|(\|count)'
+            b'|(\|quiet)'
+            b')*': _patch,
+        b'patches(?:' +
+            b'(\|join\([^%b]*?\))' % brackets[-1:] +
+            b'|(\|reverse)' +
+            b'|(\|hide_applied)' +
+            b'|(\|hide_unapplied)' +
+            b'|(\|pre_applied\([^%b]*?\))' % brackets[-1:] +
+            b'|(\|post_applied\([^%b]*?\))' % brackets[-1:] +
+            b'|(\|pre_unapplied\([^%b]*?\))' % brackets[-1:] +
+            b'|(\|post_unapplied\([^%b]*?\))' % brackets[-1:] +
+            b')*': _patches,
+        b'queue': _queue,
+        b'rev(\|merge)?': _rev,
+        b'root': _root,
+        b'root\|basename': _basename,
+        b'status(?:'
+            b'(\|modified)'
+            b'|(\|unknown)'
+            b')*': _status,
+        b'tags(?:' +
+            b'(\|quiet)' +
+            b'|(\|[^%b]*?)' % brackets[-1:] +
+            b')*': _tags,
+        b'task': _task,
+        b'tip(?:'
+            b'(\|node)'
+            b'|(\|short)'
+            b')*': _tip,
+        b'update': _update,
+
+        b'incoming(\|count)?': _remote(b'incoming'),
+        b'outgoing(\|count)?': _remote(b'outgoing'),
     }
 
-    if not fs:
-        fs = repo.ui.config("prompt", "template", "")
+    if opts.get("cache_incoming"):
+        _cache_remote(repo, b'incoming')
+
+    if opts.get("cache_outgoing"):
+        _cache_remote(repo, b'outgoing')
 
     for tag, repl in patterns.items():
         fs = re.sub(tag_start + tag + tag_end, repl, fs)
+
     ui.status(fs)
 
+def _pull_with_cache(orig, ui, repo, *args, **opts):
+    """Wrap the pull command to delete the incoming cache as well."""
+    res = orig(ui, repo, *args, **opts)
+    cache = path.join(repo.root, CACHE_PATH, 'incoming')
+    if path.isfile(cache):
+        os.remove(cache)
+    return res
+
+def _push_with_cache(orig, ui, repo, *args, **opts):
+    """Wrap the push command to delete the outgoing cache as well."""
+    res = orig(ui, repo, *args, **opts)
+    cache = path.join(repo.root, CACHE_PATH, 'outgoing')
+    if path.isfile(cache):
+        os.remove(cache)
+    return res
+
+def uisetup(ui):
+    extensions.wrapcommand(commands.table, b'pull', _pull_with_cache)
+    extensions.wrapcommand(commands.table, b'push', _push_with_cache)
+    try:
+        extensions.wrapcommand(extensions.find(b"fetch").cmdtable, b'fetch', _pull_with_cache)
+    except KeyError:
+        pass
+
 help.helptable += (
     (['prompt-keywords'], _('Keywords supported by hg-prompt'),
      lambda _: r'''hg-prompt currently supports a number of keywords.
@@ -436,6 +502,20 @@
      |REVSET
          The revset to count.
 
+incoming
+     Display nothing, but if the default path contains incoming changesets the
+     extra text will be expanded.
+
+     For example: `{incoming changes{incoming}}` will expand to
+     `incoming changes` if there are changes, otherwise nothing.
+
+     Checking for incoming changesets is an expensive operation, so `hg-prompt`
+     will cache the results in `.hg/prompt/cache/` and refresh them every 15
+     minutes.
+
+     |count
+         Display the number of incoming changesets (if greater than 0).
+
 node
      Display the (full) changeset hash of the current parent.
 
@@ -445,6 +525,20 @@
      |merge
          Display the hash of the changeset you're merging with.
 
+outgoing
+     Display nothing, but if the current repository contains outgoing
+     changesets (to default) the extra text will be expanded.
+
+     For example: `{outgoing changes{outgoing}}` will expand to
+     `outgoing changes` if there are changes, otherwise nothing.
+
+     Checking for outgoing changesets is an expensive operation, so `hg-prompt`
+     will cache the results in `.hg/prompt/cache/` and refresh them every 15
+     minutes.
+
+     |count
+         Display the number of outgoing changesets (if greater than 0).
+
 patch
      Display the topmost currently-applied patch (requires the mq
      extension).