changeset 64:39b07c5f8785

Add Git and Mercurial support to command prompt
author IBBoard <dev@ibboard.co.uk>
date Tue, 25 Apr 2017 11:42:15 +0100
parents 327c85286b54
children dc844728d1d4
files bashrc hgrc prompt.py setup.sh
diffstat 4 files changed, 711 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/bashrc	Tue Apr 25 11:17:38 2017 +0100
+++ b/bashrc	Tue Apr 25 11:42:15 2017 +0100
@@ -38,10 +38,25 @@
 export GTK_IM_MODULE=ibus
 export LS_OPTIONS="$LS_OPTIONS -p"
 
+function vcs_status {
+	GIT_PS1_SHOWDIRTYSTATE=1
+	GIT_PS1_SHOWUPSTREAM=verbose
+	. /etc/bash_completion.d/git-prompt.sh
+	VCS_EXTRA=$(__git_ps1 "%s")
+
+	if [[ "x$VCS_EXTRA" == "x" ]]; then
+		#FIXME: Currently shows "-" even when no incoming changes
+		VCS_EXTRA=$(hg prompt "{branch} {-{incoming|count}}{+{outgoing|count}}")
+	fi
+	if [[ "x$VCS_EXTRA" != "x" ]]; then
+		echo -ne ' \033[01;38;5;247m'"($VCS_EXTRA)\033[39m"
+	fi
+}
+
 # Bold the command prompt and standardise on SUSE format
 #export PS1='\[\033[01m\]\u@\h\[\033[00m\]:\[\033[01m\]\w\[\033[00m\]> '
 # With colour!
-export PS1='\[\033[01;38;5;34m\]\u\[\033[01;38;5;28m\]@\[\033[01;38;5;34m\]\h\[\033[39m\]:\[\033[01;38;5;26m\]\w$(test $PWD != / && echo -n /)\[\033[39m\]>\[\033[00m\] '
+export PS1='\[\033[01;38;5;34m\]\u\[\033[01;38;5;28m\]@\[\033[01;38;5;34m\]\h\[\033[39m\]:\[\033[01;38;5;26m\]\w$(test $PWD != / && echo -n /)\[\033[39m\]$(vcs_status)>\[\033[00m\] '
 
 # Fix "less" highlighting
 export LESS_TERMCAP_so=$'\033[30;48;5;247m'
--- a/hgrc	Tue Apr 25 11:17:38 2017 +0100
+++ b/hgrc	Tue Apr 25 11:42:15 2017 +0100
@@ -20,6 +20,7 @@
 pager =
 progress =
 mercurial_keyring = 
+prompt = ~/.hg-prompt.py
 
 [extdiff]
 cmd.meld = meld
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/prompt.py	Tue Apr 25 11:42:15 2017 +0100
@@ -0,0 +1,693 @@
+#!/usr/bin/env python
+
+'''get repository information for use in a shell prompt
+
+Take a string, parse any special variables inside, and output the result.
+
+Useful mostly for putting information about the current repository into
+a shell prompt.
+
+
+
+Taken from https://bitbucket.org/sjl/hg-prompt/src @ 5334581
+'''
+
+from __future__ import with_statement
+
+import re
+import os
+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
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+
+# `revrange' has been moved into module `scmutil' since v1.9.
+try :
+    from mercurial import scmutil
+    revrange = scmutil.revrange
+except :
+    revrange = cmdutil.revrange
+
+CACHE_PATH = ".hg/prompt/cache"
+CACHE_TIMEOUT = timedelta(minutes=15)
+
+FILTER_ARG = re.compile(r'\|.+\((.*)\)')
+
+
+def _cache_remote(repo, kind):
+    cache = path.join(repo.root, CACHE_PATH, kind)
+    c_tmp = cache + '.temp'
+
+    popenargs = ['hg', kind, '--quiet']
+    remote_path = repo.ui.config('prompt', 'remote')
+    if remote_path is not None:
+        popenargs.append(remote_path)
+
+    null_path = 'NUL:' if subprocess.mswindows else '/dev/null'
+    with open(null_path, 'w') as null_fp:
+        with open(c_tmp, 'w') as stdout_fp:
+            exit_code = subprocess.call(popenargs, stdout=stdout_fp, stderr=null_fp)
+
+    if exit_code not in (0, 1): # (changesets_found, changesets_not_found)
+        msg = "hg-prompt error: "
+        if remote_path: # Failure likely due to bad remote. Is 255 a valid check?
+            msg += "Can't access remote '%s'" % remote_path
+        else:
+            msg += "Error attempting 'hg %s'" % kind
+        print msg
+
+    os.rename(c_tmp, cache)
+    return
+
+
+def _with_groups(groups, out):
+    out_groups = [groups[0]] + [groups[-1]]
+
+    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 '')
+
+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)
+    if not matching_filters:
+        return None
+
+    # Later filters will override earlier ones, for now.
+    f = matching_filters[-1]
+
+    return f
+
+def _get_filter_arg(f):
+    if not f:
+        return None
+
+    args = FILTER_ARG.match(f).groups()
+    if args:
+        return args[0]
+    else:
+        return None
+
+@command('prompt',
+         [('', 'angle-brackets', None, 'use angle brackets (<>) for keywords'),
+          ('', 'cache-incoming', None, 'used internally by hg-prompt'),
+          ('', 'cache-outgoing', None, 'used internally by hg-prompt')],
+         'hg prompt STRING')
+def prompt(ui, repo, fs='', **opts):
+    '''get repository information for use in a shell prompt
+
+    Take a string and output it for use in a shell prompt. You can use
+    keywords in curly braces::
+
+        $ hg prompt "currently on {branch}"
+        currently on default
+
+    You can also use an extended form of any keyword::
+
+        {optional text here{keyword}more optional text}
+
+    This will expand the inner {keyword} and output it along with the extra
+    text only if the {keyword} expands successfully.  This is useful if you
+    have a keyword that may not always apply to the current state and you
+    have some text that you would like to see only if it is appropriate::
+
+        $ hg prompt "currently at {bookmark}"
+        currently at
+        $ hg prompt "{currently at {bookmark}}"
+        $ hg bookmark my-bookmark
+        $ hg prompt "{currently at {bookmark}}"
+        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 ''
+
+    def _bookmark(m):
+        try:
+            book = extensions.find('bookmarks').current(repo)
+        except AttributeError:
+            book = getattr(repo, '_bookmarkcurrent', None)
+        except KeyError:
+            book = getattr(repo, '_bookmarkcurrent', None)
+        if book is None:
+            book = getattr(repo, '_activebookmark', None)
+        if book:
+            cur = repo['.'].node()
+            if repo._bookmarks[book] == cur:
+                return _with_groups(m.groups(), book)
+        else:
+            return ''
+
+    def _branch(m):
+        g = m.groups()
+
+        branch = repo.dirstate.branch()
+        quiet = _get_filter('quiet', g)
+
+        out = branch if (not quiet) or (branch != 'default') else ''
+
+        return _with_groups(g, out) if out else ''
+
+    def _closed(m):
+        g = m.groups()
+
+        quiet = _get_filter('quiet', g)
+
+        p = repo[None].parents()[0]
+        pn = p.node()
+        branch = repo.dirstate.branch()
+        closed = (p.extra().get('close')
+                  and pn in repo.branchheads(branch, closed=True))
+        out = 'X' if (not quiet) and closed else ''
+
+        return _with_groups(g, out) if out else ''
+
+    def _count(m):
+        g = m.groups()
+        query = [g[1][1:]] if g[1] else ['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 = p if len(parents) > p else None
+
+        format = short if '|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 ''
+
+    def _patch(m):
+        g = m.groups()
+
+        try:
+            extensions.find('mq')
+        except KeyError:
+            return ''
+
+        q = repo.mq
+
+        if _get_filter('quiet', g) and not len(q.series):
+            return ''
+
+        if _get_filter('topindex', g):
+            if len(q.applied):
+                out = str(len(q.applied) - 1)
+            else:
+                out = ''
+        elif _get_filter('applied', g):
+            out = str(len(q.applied))
+        elif _get_filter('unapplied', g):
+            out = str(len(q.unapplied(repo)))
+        elif _get_filter('count', g):
+            out = str(len(q.series))
+        else:
+            out = q.applied[-1].name if q.applied else ''
+
+        return _with_groups(g, out) if out else ''
+
+    def _patches(m):
+        g = m.groups()
+
+        try:
+            extensions.find('mq')
+        except KeyError:
+            return ''
+
+        join_filter = _get_filter('join', g)
+        join_filter_arg = _get_filter_arg(join_filter)
+        sep = join_filter_arg if join_filter else ' -> '
+
+        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('reverse', g):
+            patches = reversed(patches)
+
+        pre_applied_filter = _get_filter('pre_applied', g)
+        pre_applied_filter_arg = _get_filter_arg(pre_applied_filter)
+        post_applied_filter = _get_filter('post_applied', g)
+        post_applied_filter_arg = _get_filter_arg(post_applied_filter)
+
+        pre_unapplied_filter = _get_filter('pre_unapplied', g)
+        pre_unapplied_filter_arg = _get_filter_arg(pre_unapplied_filter)
+        post_unapplied_filter = _get_filter('post_unapplied', g)
+        post_unapplied_filter_arg = _get_filter_arg(post_unapplied_filter)
+
+        for n, patch in enumerate(patches):
+            if patch in applied:
+                if pre_applied_filter:
+                    patches[n] = pre_applied_filter_arg + patches[n]
+                if post_applied_filter:
+                    patches[n] = patches[n] + post_applied_filter_arg
+            elif patch in unapplied:
+                if pre_unapplied_filter:
+                    patches[n] = pre_unapplied_filter_arg + patches[n]
+                if post_unapplied_filter:
+                    patches[n] = patches[n] + post_unapplied_filter_arg
+
+        return _with_groups(g, sep.join(patches)) if patches else ''
+
+    def _queue(m):
+        g = m.groups()
+
+        try:
+            extensions.find('mq')
+        except KeyError:
+            return ''
+
+        q = repo.mq
+
+        out = os.path.basename(q.path)
+        if out == 'patches' and not os.path.isdir(q.path):
+            out = ''
+        elif out.startswith('patches-'):
+            out = out[8:]
+
+        return _with_groups(g, out) if out else ''
+
+    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])
+
+            if cache_exists:
+                with open(cache) as c:
+                    count = len(c.readlines())
+                    if g[1] and count > 0:
+                        return _with_groups(g, str(count))
+                    elif g[2]:
+                        return _with_groups(g, '0') if not count else ''
+                    else:
+                        return _with_groups(g, '')
+            else:
+                return ''
+        return _r
+
+    def _rev(m):
+        g = m.groups()
+
+        parents = repo[None].parents()
+        parent = 0 if '|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 ''
+
+    def _root(m):
+        return _with_groups(m.groups(), repo.root) if repo.root else ''
+
+    def _status(m):
+        g = m.groups()
+
+        st = repo.status(unknown=True)[:5]
+        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 ''
+        else:
+            if '|modified' in g:
+                flag += '!' if modified else ''
+            if '|unknown' in g:
+                flag += '?' if unknown else ''
+
+        return _with_groups(g, flag) if flag else ''
+
+    def _tags(m):
+        g = m.groups()
+
+        sep = g[2][1:] if g[2] else ' '
+        tags = repo[None].tags()
+
+        quiet = _get_filter('quiet', g)
+        if quiet:
+            tags = filter(lambda tag: tag != 'tip', tags)
+
+        return _with_groups(g, sep.join(tags)) if tags else ''
+
+    def _task(m):
+        try:
+            task = extensions.find('tasks').current(repo)
+            return _with_groups(m.groups(), task) if task else ''
+        except KeyError:
+            return ''
+
+    def _tip(m):
+        g = m.groups()
+
+        format = short if '|short' in g else hex
+
+        tip = repo[len(repo) - 1]
+        rev = tip.rev()
+        tip = format(tip.node()) if '|node' in g else tip.rev()
+
+        return _with_groups(g, str(tip)) if rev >= 0 else ''
+
+    def _update(m):
+        current_rev = repo[None].parents()[0]
+
+        # Get the tip of the branch for the current branch
+        try:
+            heads = repo.branchmap()[current_rev.branch()]
+            tip = heads[-1]
+        except (KeyError, IndexError):
+            # We are in an empty repository.
+
+            return ''
+
+        for head in reversed(heads):
+            if not repo[head].closesbranch():
+                tip = head
+                break
+
+        return _with_groups(m.groups(), '^') if current_rev.children() else ''
+
+    if opts.get("angle_brackets"):
+        tag_start = r'\<([^><]*?\<)?'
+        tag_end = r'(\>[^><]*?)?>'
+        brackets = '<>'
+    else:
+        tag_start = r'\{([^{}]*?\{)?'
+        tag_end = r'(\}[^{}]*?)?\}'
+        brackets = '{}'
+
+    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,
+
+        'incoming(?:'
+            '(\|count)'
+            '|(\|zero)'
+            ')*': _remote('incoming'),
+        'outgoing(?:'
+            '(\|count)'
+            '|(\|zero)'
+            ')*': _remote('outgoing')
+    }
+
+    if opts.get("cache_incoming"):
+        _cache_remote(repo, 'incoming')
+
+    if opts.get("cache_outgoing"):
+        _cache_remote(repo, 'outgoing')
+
+    if not fs:
+        fs = repo.ui.config("prompt", "template", "")
+
+    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, 'pull', _pull_with_cache)
+    extensions.wrapcommand(commands.table, 'push', _push_with_cache)
+    try:
+        extensions.wrapcommand(extensions.find("fetch").cmdtable, '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.
+
+Some keywords support filters.  Filters can be chained when it makes
+sense to do so.  When in doubt, try it!
+
+bookmark
+     Display the current bookmark (requires the bookmarks extension).
+
+branch
+     Display the current branch.
+
+     |quiet
+         Display the current branch only if it is not the default branch.
+
+closed
+     Display `X` if working on a closed branch (i.e. committing now would reopen
+     the branch).
+
+count
+     Display the number of revisions in the given revset (the revset `all()`
+     will be used if none is given).
+
+     See `hg help revsets` for more information.
+
+     |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).
+     |zero
+         Display 0 if there are no incoming changesets.
+
+node
+     Display the (full) changeset hash of the current parent.
+
+     |short
+         Display the hash as the short, 12-character form.
+
+     |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).
+     |zero
+         Display 0 if there are no incoming changesets.
+
+patch
+     Display the topmost currently-applied patch (requires the mq
+     extension).
+
+     |count
+         Display the number of patches in the queue.
+
+     |topindex
+         Display (zero-based) index of the topmost applied patch in the series
+         list (as displayed by :hg:`qtop -v`), or the empty string if no patch
+         is applied.
+
+     |applied
+         Display the number of currently applied patches in the queue.
+
+     |unapplied
+         Display the number of currently unapplied patches in the queue.
+
+     |quiet
+         Display a number only if there are any patches in the queue.
+
+patches
+     Display a list of the current patches in the queue.  It will look like
+     this:
+
+         :::console
+         $ hg prompt '{patches}'
+         bottom-patch -> middle-patch -> top-patch
+
+     |reverse
+         Display the patches in reverse order (i.e. topmost first).
+
+     |hide_applied
+         Do not display applied patches.
+
+     |hide_unapplied
+         Do not display unapplied patches.
+
+     |join(SEP)
+         Display SEP between each patch, instead of the default ` -> `.
+
+     |pre_applied(STRING)
+         Display STRING immediately before each applied patch.  Useful for
+         adding color codes.
+
+     |post_applied(STRING)
+         Display STRING immediately after each applied patch.  Useful for
+         resetting color codes.
+
+     |pre_unapplied(STRING)
+         Display STRING immediately before each unapplied patch.  Useful for
+         adding color codes.
+
+     |post_unapplied(STRING)
+         Display STRING immediately after each unapplied patch.  Useful for
+         resetting color codes.
+
+queue
+     Display the name of the current MQ queue.
+
+rev
+     Display the repository-local changeset number of the current parent.
+
+     |merge
+         Display the repository-local changeset number of the changeset you're
+         merging with.
+
+root
+     Display the full path to the root of the current repository, without a
+     trailing slash.
+
+     |basename
+         Display the directory name of the root of the current repository. For
+         example, if the repository is in `/home/u/myrepo` then this keyword
+         would expand to `myrepo`.
+
+status
+     Display `!` if the repository has any changed/added/removed files,
+     otherwise `?` if it has any untracked (but not ignored) files, otherwise
+     nothing.
+
+     |modified
+         Display `!` if the current repository contains files that have been
+         modified, added, removed, or deleted, otherwise nothing.
+
+     |unknown
+         Display `?` if the current repository contains untracked files,
+         otherwise nothing.
+
+tags
+     Display the tags of the current parent, separated by a space.
+
+     |quiet
+         Display the tags of the current parent, excluding the tag "tip".
+
+     |SEP
+         Display the tags of the current parent, separated by `SEP`.
+
+task
+     Display the current task (requires the tasks extension).
+
+tip
+     Display the repository-local changeset number of the current tip.
+
+     |node
+         Display the (full) changeset hash of the current tip.
+
+     |short
+         Display a short form of the changeset hash of the current tip (must be
+         used with the **|node** filter)
+
+update
+     Display `^` if the current parent is not the tip of the current branch,
+     otherwise nothing.  In effect, this lets you see if running `hg update`
+     would do something.
+'''),
+)
--- a/setup.sh	Tue Apr 25 11:17:38 2017 +0100
+++ b/setup.sh	Tue Apr 25 11:42:15 2017 +0100
@@ -27,6 +27,7 @@
 
 create_symlink tmux.conf ~/.tmux.conf
 create_symlink hgrc ~/.hgrc
+create_symlink prompt.py ~/.hg-prompt.py
 create_symlink bashrc ~/.bashrc
 create_symlink profile ~/.profile
 create_symlink inputrc ~/.inputrc