diff --git a/powerline/colorschemes/default.json b/powerline/colorschemes/default.json index a199a09f..134ad076 100644 --- a/powerline/colorschemes/default.json +++ b/powerline/colorschemes/default.json @@ -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"] }, diff --git a/powerline/ext/vim/segments/core.py b/powerline/ext/vim/segments/core.py index ef1df96b..59300330 100644 --- a/powerline/ext/vim/segments/core.py +++ b/powerline/ext/vim/segments/core.py @@ -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 diff --git a/powerline/lib/vcs/__init__.py b/powerline/lib/vcs/__init__.py new file mode 100644 index 00000000..16786fe7 --- /dev/null +++ b/powerline/lib/vcs/__init__.py @@ -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 diff --git a/powerline/lib/vcs/git.py b/powerline/lib/vcs/git.py new file mode 100644 index 00000000..1ccb17fe --- /dev/null +++ b/powerline/lib/vcs/git.py @@ -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 diff --git a/powerline/lib/vcs/mercurial.py b/powerline/lib/vcs/mercurial.py new file mode 100644 index 00000000..80b676d9 --- /dev/null +++ b/powerline/lib/vcs/mercurial.py @@ -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() diff --git a/powerline/themes/vim/default.json b/powerline/themes/vim/default.json index c9ce2ca0..32747eb7 100644 --- a/powerline/themes/vim/default.json +++ b/powerline/themes/vim/default.json @@ -31,6 +31,11 @@ "name": "file_name", "draw_divider": false }, + { + "name": "file_vcs_status", + "draw_divider": false, + "before": " " + }, { "name": "modified_indicator", "args": { "text": "+" },