Merge pull request #1724 from laerus/gitstatus

vc_branch prompt
This commit is contained in:
Anthony Scopatz 2016-09-24 17:42:16 -04:00 committed by GitHub
commit 75eada8412
4 changed files with 116 additions and 100 deletions

16
news/vc_branch.rst Normal file
View file

@ -0,0 +1,16 @@
**Added:** None
**Changed:**
* ``xonsh.prompt.gitstatus.gitstatus`` now returns a namedtuple
* implementation of ``xonsh.prompt.vc_branch.get_git_branch`` and
``xonsh.prompt.vc_branch.git_dirty_working_directory`` to be faster
**Deprecated:** None
**Removed:** None
**Fixed:** None
**Security:** None

View file

@ -24,22 +24,24 @@ from xonsh.prompt.vc_branch import (
from xonsh.prompt.gitstatus import gitstatus_prompt from xonsh.prompt.gitstatus import gitstatus_prompt
FORMATTER_DICT = xl.LazyObject(lambda: dict( @xl.lazyobject
user=os.environ.get('USERNAME' if xp.ON_WINDOWS else 'USER', '<user>'), def FORMATTER_DICT():
prompt_end='#' if xt.is_superuser() else '$', return dict(
hostname=socket.gethostname().split('.', 1)[0], user=os.environ.get('USERNAME' if xp.ON_WINDOWS else 'USER', '<user>'),
cwd=_dynamically_collapsed_pwd, prompt_end='#' if xt.is_superuser() else '$',
cwd_dir=lambda: os.path.dirname(_replace_home_cwd()), hostname=socket.gethostname().split('.', 1)[0],
cwd_base=lambda: os.path.basename(_replace_home_cwd()), cwd=_dynamically_collapsed_pwd,
short_cwd=_collapsed_pwd, cwd_dir=lambda: os.path.dirname(_replace_home_cwd()),
curr_branch=current_branch, cwd_base=lambda: os.path.basename(_replace_home_cwd()),
branch_color=branch_color, short_cwd=_collapsed_pwd,
branch_bg_color=branch_bg_color, curr_branch=current_branch,
current_job=_current_job, branch_color=branch_color,
env_name=env_name, branch_bg_color=branch_bg_color,
vte_new_tab_cwd=vte_new_tab_cwd, current_job=_current_job,
gitstatus=gitstatus_prompt, env_name=env_name,
), globals(), 'FORMATTER_DICT') vte_new_tab_cwd=vte_new_tab_cwd,
gitstatus=gitstatus_prompt,
)
@xl.lazyobject @xl.lazyobject

View file

@ -1,13 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Informative git status prompt formatter""" """Informative git status prompt formatter"""
import os
import builtins import builtins
import collections
import os
import subprocess import subprocess
import xonsh.lazyasd as xl import xonsh.lazyasd as xl
GitStatus = collections.namedtuple('GitStatus',
['branch', 'num_ahead', 'num_behind',
'untracked', 'changed', 'conflicts',
'staged', 'stashed', 'operations'])
def _check_output(*args, **kwargs): def _check_output(*args, **kwargs):
kwargs.update(dict(env=builtins.__xonsh_env__.detype(), kwargs.update(dict(env=builtins.__xonsh_env__.detype(),
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
@ -70,9 +76,10 @@ def _gitoperation(gitdir):
def gitstatus(): def gitstatus():
"""Return (branch name, number of ahead commit, number of behind commit, """Return namedtuple with fields:
untracked number, changed number, conflicts number, branch name, number of ahead commit, number of behind commit,
staged number, stashed number, operation)""" untracked number, changed number, conflicts number,
staged number, stashed number, operation."""
status = _check_output(['git', 'status', '--porcelain', '--branch']) status = _check_output(['git', 'status', '--porcelain', '--branch'])
branch = '' branch = ''
num_ahead, num_behind = 0, 0 num_ahead, num_behind = 0, 0
@ -111,7 +118,7 @@ def gitstatus():
stashed = _get_stash(gitdir) stashed = _get_stash(gitdir)
operations = _gitoperation(gitdir) operations = _gitoperation(gitdir)
return (branch, num_ahead, num_behind, return GitStatus(branch, num_ahead, num_behind,
untracked, changed, conflicts, staged, stashed, untracked, changed, conflicts, staged, stashed,
operations) operations)
@ -119,31 +126,29 @@ def gitstatus():
def gitstatus_prompt(): def gitstatus_prompt():
"""Return str `BRANCH|OPERATOR|numbers`""" """Return str `BRANCH|OPERATOR|numbers`"""
try: try:
(branch, num_ahead, num_behind, s = gitstatus()
untracked, changed, conflicts, staged, stashed,
operations) = gitstatus()
except subprocess.SubprocessError: except subprocess.SubprocessError:
return None return None
ret = _get_def('BRANCH') + branch ret = _get_def('BRANCH') + s.branch
if num_ahead > 0: if s.num_ahead > 0:
ret += _get_def('AHEAD') + str(num_ahead) ret += _get_def('AHEAD') + str(s.num_ahead)
if num_behind > 0: if s.num_behind > 0:
ret += _get_def('BEHIND') + str(num_behind) ret += _get_def('BEHIND') + str(s.num_behind)
if operations: if s.operations:
ret += _get_def('OPERATION') + '|' + '|'.join(operations) ret += _get_def('OPERATION') + '|' + '|'.join(s.operations)
ret += '|' ret += '|'
if staged > 0: if s.staged > 0:
ret += _get_def('STAGED') + str(staged) + '{NO_COLOR}' ret += _get_def('STAGED') + str(s.staged) + '{NO_COLOR}'
if conflicts > 0: if s.conflicts > 0:
ret += _get_def('CONFLICTS') + str(conflicts) + '{NO_COLOR}' ret += _get_def('CONFLICTS') + str(s.conflicts) + '{NO_COLOR}'
if changed > 0: if s.changed > 0:
ret += _get_def('CHANGED') + str(changed) + '{NO_COLOR}' ret += _get_def('CHANGED') + str(s.changed) + '{NO_COLOR}'
if untracked > 0: if s.untracked > 0:
ret += _get_def('UNTRACKED') + str(untracked) + '{NO_COLOR}' ret += _get_def('UNTRACKED') + str(s.untracked) + '{NO_COLOR}'
if stashed > 0: if s.stashed > 0:
ret += _get_def('STASHED') + str(stashed) + '{NO_COLOR}' ret += _get_def('STASHED') + str(s.stashed) + '{NO_COLOR}'
if staged + conflicts + changed + untracked + stashed == 0: if s.staged + s.conflicts + s.changed + s.untracked + s.stashed == 0:
ret += _get_def('CLEAN') + '{NO_COLOR}' ret += _get_def('CLEAN') + '{NO_COLOR}'
ret += '{NO_COLOR}' ret += '{NO_COLOR}'

View file

@ -9,49 +9,40 @@ import warnings
import subprocess import subprocess
import xonsh.platform as xp import xonsh.platform as xp
import xonsh.prompt
import threading, queue
def _get_git_branch(q):
try:
status = subprocess.check_output(['git', 'status'],
stderr=subprocess.DEVNULL)
except (subprocess.CalledProcessError, OSError):
q.put(None)
else:
status = status.decode().split()
if status[2] == 'at':
q.put(status[3])
else:
q.put(status[2])
def get_git_branch(): def get_git_branch():
"""Attempts to find the current git branch. If no branch is found, then """Attempts to find the current git branch. If this could not
an empty string is returned. If a timeout occured, the timeout exception be determined (timeout, not in a git repo, etc.) then this returns None.
(subprocess.TimeoutExpired) is returned.
""" """
branch = None branch = None
env = builtins.__xonsh_env__ timeout = builtins.__xonsh_env__.get('VC_BRANCH_TIMEOUT')
cwd = env['PWD'] q = queue.Queue()
denv = env.detype()
vcbt = env['VC_BRANCH_TIMEOUT'] t = threading.Thread(target=_get_git_branch, args=(q,))
if not xp.ON_WINDOWS: t.start()
prompt_scripts = ['/usr/lib/git-core/git-sh-prompt', t.join(timeout=timeout)
'/usr/local/etc/bash_completion.d/git-prompt.sh'] try:
for script in prompt_scripts: branch = q.get_nowait()
# note that this is about 10x faster than bash -i "__git_ps1" except queue.Empty:
inp = 'source {}; __git_ps1 "${{1:-%s}}"'.format(script) branch = None
try:
branch = subprocess.check_output(['bash'], cwd=cwd, input=inp,
stderr=subprocess.PIPE, timeout=vcbt, env=denv,
universal_newlines=True)
break
except subprocess.TimeoutExpired as e:
branch = e
break
except (subprocess.CalledProcessError, FileNotFoundError):
continue
# fall back to using the git binary if the above failed
if branch is None:
cmd = ['git', 'rev-parse', '--abbrev-ref', 'HEAD']
try:
s = subprocess.check_output(cmd, cwd=cwd, timeout=vcbt, env=denv,
stderr=subprocess.PIPE, universal_newlines=True)
if xp.ON_WINDOWS and len(s) == 0:
# Workaround for a bug in ConEMU/cmder, retry without redirection
s = subprocess.check_output(cmd, cwd=cwd, timeout=vcbt,
env=denv, universal_newlines=True)
branch = s.strip()
except subprocess.TimeoutExpired as e:
branch = e
except (subprocess.CalledProcessError, FileNotFoundError):
branch = None
return branch return branch
@ -137,30 +128,32 @@ def current_branch(pad=NotImplemented):
return branch or None return branch or None
def git_dirty_working_directory(cwd=None, include_untracked=False): def _git_dirty_working_directory(q):
status = None
try:
status = subprocess.check_output(['git', 'status'],
stderr=subprocess.DEVNULL)
except (subprocess.CalledProcessError, OSError):
q.put(None)
if status is not None:
if b'nothing to commit' in status:
return q.put(False)
else:
return q.put(True)
def git_dirty_working_directory():
"""Returns whether or not the git directory is dirty. If this could not """Returns whether or not the git directory is dirty. If this could not
be determined (timeout, file not sound, etc.) then this returns None. be determined (timeout, file not sound, etc.) then this returns None.
""" """
cmd = ['git', 'status', '--porcelain'] timeout = builtins.__xonsh_env__.get("VC_BRANCH_TIMEOUT")
if include_untracked: q = queue.Queue()
cmd.append('--untracked-files=normal') t = threading.Thread(target=_git_dirty_working_directory, args=(q,))
else: t.start()
cmd.append('--untracked-files=no') t.join(timeout=timeout)
env = builtins.__xonsh_env__
cwd = env['PWD']
denv = env.detype()
vcbt = env['VC_BRANCH_TIMEOUT']
try: try:
s = subprocess.check_output(cmd, stderr=subprocess.PIPE, cwd=cwd, return q.get_nowait()
timeout=vcbt, universal_newlines=True, except queue.Empty:
env=denv)
if xp.ON_WINDOWS and len(s) == 0:
# Workaround for a bug in ConEMU/cmder, retry without redirection
s = subprocess.check_output(cmd, cwd=cwd, timeout=vcbt,
env=denv, universal_newlines=True)
return bool(s)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired,
FileNotFoundError):
return None return None