# HG changeset patch # User IBBoard # Date 1579377620 0 # Node ID fcd34011b0f0740cb81a4ef2cceeb89d7df38d61 # Parent 298fa524406ad2614988b9ba0fdb2cda0b434d6a 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) diff -r 298fa524406a -r fcd34011b0f0 prompt.py --- 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).