Add file_vcs_status segment, using powerline.lib.vcs instead of fugitive

Using pure-python implementation allows to use the same code in the
terminal segments (not implemented currently). It also should be more
effective. Git module is able to use pygit2 instead of command-line git
which is much faster. Mercurial module assumes that mercurial is always
accessible as a python module.

Code for repository (not file) status is untested. It exists solely for
terminal segments.

Closes #26.
This commit is contained in:
ZyX 2013-01-03 03:29:38 +04:00 committed by Kim Silkebækken
parent ca1225b864
commit 102e749093
6 changed files with 234 additions and 20 deletions

View File

@ -58,6 +58,7 @@
"modified_indicator": { "fg": "brightyellow", "bg": "gray4", "attr": ["bold"] },
"paste_indicator": { "fg": "white", "bg": "mediumorange", "attr": ["bold"] },
"readonly_indicator": { "fg": "brightestred", "bg": "gray4" },
"file_vcs_status": { "fg": "brightestred", "bg": "gray4" },
"branch": { "fg": "gray9", "bg": "gray4" },
"file_directory": { "fg": "gray9", "bg": "gray4" },
"file_name": { "fg": "white", "bg": "gray4", "attr": ["bold"] },

View File

@ -5,6 +5,7 @@ import vim
from powerline.ext.vim.bindings import vim_get_func
from powerline.lib.memoize import memoize
from powerline.lib.vcs import guess
vim_funcs = {
'col': vim_get_func('col', rettype=int),
@ -12,9 +13,6 @@ vim_funcs = {
'expand': vim_get_func('expand'),
'line': vim_get_func('line', rettype=int),
'mode': vim_get_func('mode'),
'vcs': {
'fugitive': vim_get_func('fugitive#head'),
},
}
vim_modes = {
@ -77,23 +75,6 @@ def readonly_indicator(text=u''):
return text if int(vim.eval('&readonly')) else None
@memoize(2)
def branch():
'''Return VCS branch.
TODO: Expand this function to handle several VCS plugins.
'''
branch = None
try:
branch = vim_funcs['vcs']['fugitive'](5)
except vim.error:
vim_funcs['vcs']['fugitive'] = None
except TypeError:
pass
return branch if branch else None
def file_directory():
'''Return file directory (head component of the file path).
'''
@ -171,3 +152,21 @@ def col_current(virtcol=True):
characters ignored (default), else returns byte offset.
'''
return vim_funcs['virtcol' if virtcol else 'col']('.')
@memoize(2)
def branch():
repo = guess(os.path.abspath(vim.current.buffer.name or os.getcwd()))
if repo:
return repo.branch()
return None
# TODO Drop cache on BufWrite event
@memoize(2)
def file_vcs_status():
if vim.current.buffer.name and not vim.eval('&buftype'):
repo = guess(os.path.abspath(vim.current.buffer.name))
if repo:
return repo.status(os.path.relpath(vim.current.buffer.name, repo.directory))
return None

View File

@ -0,0 +1,27 @@
import importlib
import os
from powerline.lib.memoize import memoize
def generate_directories(path):
yield path
while True:
old_path = path
path = os.path.dirname(path)
if path == old_path:
break
yield path
@memoize(100)
def guess(path):
for directory in generate_directories(path):
for vcs, vcs_dir in (('git', '.git'), ('mercurial', '.hg')):
if os.path.isdir(os.path.join(directory, vcs_dir)):
try:
if vcs not in globals():
globals()[vcs] = importlib.import_module('powerline.lib.vcs.' + vcs)
return globals()[vcs].Repository(directory)
except:
pass
return None

132
powerline/lib/vcs/git.py Normal file
View File

@ -0,0 +1,132 @@
try:
import pygit2 as git
class Repository(object):
__slots__ = ('repo', 'directory')
def __init__(self, directory):
self.directory = directory
self.repo = git.Repository(directory)
def status(self, path=None):
'''Return status of repository or file.
Without file argument: returns status of the repository:
:First column: working directory status (D: dirty / space)
:Second column: index status (I: index dirty / space)
:Third column: presense of untracked files (U: untracked files / space)
:None: repository clean
With file argument: returns status of this file. Output is
equivalent to the first two columns of "git status --porcelain"
(except for merge statuses as they are not supported by libgit2).
'''
if path:
status = self.repo.status_file(path)
if status == git.GIT_STATUS_CURRENT:
return None
else:
if status & git.GIT_STATUS_WT_NEW:
return '??'
if status & git.GIT_STATUS_IGNORED:
return '!!'
if status & git.GIT_STATUS_INDEX_NEW:
index_status = 'A'
elif status & git.GIT_STATUS_INDEX_DELETED:
index_status = 'D'
elif status & git.GIT_STATUS_INDEX_MODIFIED:
index_status = 'M'
else:
index_status = ' '
if status & git.GIT_STATUS_WT_DELETED:
wt_status = 'D'
elif status & git.GIT_STATUS_WT_MODIFIED:
wt_status = 'M'
else:
wt_status = ' '
return index_status + wt_status
else:
wt_column = ' '
index_column = ' '
untracked_column = ' '
for status in self.repo.status():
if status & (git.GIT_STATUS_WT_DELETED
| git.GIT_STATUS_WT_MODIFIED):
wt_column = 'D'
elif status & (git.GIT_STATUS_INDEX_NEW
| git.GIT_STATUS_INDEX_MODIFIED
| git.GIT_STATUS_INDEX_DELETED):
index_column = 'I'
elif status & git.GIT_STATUS_WT_NEW:
untracked_column = 'U'
return wt_column + index_column + untracked_column
def branch(self):
try:
ref = self.repo.lookup_reference('HEAD')
except KeyError:
return None
try:
target = ref.target
except ValueError:
return '[DETACHED HEAD]'
if target.startswith('refs/heads/'):
return target[11:]
else:
return '[DETACHED HEAD]'
except ImportError:
from subprocess import Popen, PIPE
def readlines(cmd, cwd):
p = Popen(cmd, shell=False, stdout=PIPE, stderr=PIPE, cwd=cwd)
p.stderr.close()
for line in p.stdout:
yield line[:-1]
class Repository(object):
__slots__ = ('directory',)
def __init__(self, directory):
self.directory = directory
def _gitcmd(self, *args):
return readlines(('git',) + args, self.directory)
def status(self, path=None):
if path:
try:
return self._gitcmd('status', '--porcelain', '--', path).next()[:2]
except StopIteration:
try:
self._gitcmd('ls-files', '--ignored', '--exclude-standard', '--others', '--', path).next()
return '!!'
except StopIteration:
return None
else:
wt_column = ' '
index_column = ' '
untracked_column = ' '
for line in self._gitcmd('status', '--porcelain'):
if line[0] == '?':
untracked_column = 'U'
elif line[0] == '!':
pass
elif line[0] != ' ':
index_column = 'I'
elif line[1] != ' ':
wt_column = 'D'
r = wt_column + index_column + untracked_column
return None if r == ' ' else r
def branch(self):
for line in self._gitcmd('branch', '-l'):
if line[0] == '*':
return line[2:]
return None

View File

@ -0,0 +1,50 @@
from __future__ import absolute_import
from mercurial import hg, ui, match
class Repository(object):
__slots__ = ('directory', 'ui')
statuses = 'MARDUI'
repo_statuses = (1, 1, 1, 1, 2)
repo_statuses_str = (None, 'D ', ' U', 'DU')
def __init__(self, directory):
self.directory = directory
self.ui = ui.ui()
def _repo(self):
# Cannot create this object once and use always: when repository updates
# functions emit invalid results
return hg.repository(self.ui, self.directory)
def status(self, path=None):
'''Return status of repository or file.
Without file argument: returns status of the repository:
:"D?": dirty (tracked modified files: added, removed, deleted, modified),
:"?U": untracked-dirty (added, but not tracked files)
:None: clean (status is empty)
With file argument: returns status of this file: "M"odified, "A"dded,
"R"emoved, "D"eleted (removed from filesystem, but still tracked),
"U"nknown, "I"gnored, (None)Clean.
'''
repo = self._repo()
if path:
m = match.match(None, None, [path], exact=True)
statuses = repo.status(match=m, unknown=True, ignored=True)
for status, paths in zip(self.statuses, statuses):
if paths:
return status
return None
else:
resulting_status = 0
for status, paths in zip(self.repo_statuses, repo.status(unknown=True)):
resulting_status |= status
return self.repo_statuses_str[resulting_status]
def branch(self):
return self._repo().dirstate.branch()

View File

@ -31,6 +31,11 @@
"name": "file_name",
"draw_divider": false
},
{
"name": "file_vcs_status",
"draw_divider": false,
"before": " "
},
{
"name": "modified_indicator",
"args": { "text": "+" },