# ============================================================================ # FILE: gitstatus.py # AUTHOR: Qiming Zhao <chemzqm@gmail.com> # License: MIT license # ============================================================================ # pylint: disable=E0401,C0411 import os import re import subprocess import shlex from itertools import filterfalse from .base import Base from denite import util from ..kind.file import Kind as File EMPTY_LINE = re.compile(r"^\s*$") STATUS_MAP = { ' ': ' ', 'M': '~', 'T': '~', 'A': '+', 'D': '-', 'R': '→', 'C': 'C', 'U': 'U', '?': '?'} def _parse_line(line, root, winnr): path = os.path.join(root, line[3:]) index_symbol = STATUS_MAP[line[0]] tree_symbol = STATUS_MAP[line[1]] word = "{0}{1} {2}".format(index_symbol, tree_symbol, line[3:]) return { 'word': word, 'action__path': path, 'source__root': root, 'Source__winnr': winnr, 'source__staged': index_symbol not in [' ', '?'], 'source__tree': tree_symbol not in [' ', '?'] } def run_command(commands, cwd, encoding='utf-8'): try: p = subprocess.run(commands, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) except subprocess.CalledProcessError: return [] return p.stdout.decode(encoding).split('\n') class Source(Base): def __init__(self, vim): super().__init__(vim) self.name = 'gitstatus' self.kind = Kind(vim) self.is_public_context = True def on_init(self, context): winnr = self.vim.call('winnr') gitdir = self.vim.call('denite#git#gitdir') context['__root'] = '' if not gitdir else os.path.dirname(gitdir) context['__winnr'] = winnr def highlight(self): self.vim.command('highlight deniteGitStatusAdd guifg=#009900 ctermfg=2') self.vim.command('highlight deniteGitStatusChange guifg=#bbbb00 ctermfg=3') self.vim.command('highlight deniteGitStatusDelete guifg=#ff2222 ctermfg=1') self.vim.command('highlight deniteGitStatusUnknown guifg=#5f5f5f ctermfg=59') def define_syntax(self): self.vim.command(r'syntax match deniteGitStatusHeader /^.*$/ ' + r'containedin=' + self.syntax_name) self.vim.command(r'syntax match deniteGitStatusSymbol /^\s*\zs\S\+/ ' + r'contained containedin=deniteGitStatusHeader') self.vim.command(r'syntax match deniteGitStatusAdd /+/ ' + r'contained containedin=deniteGitStatusSymbol') self.vim.command(r'syntax match deniteGitStatusDelete /-/ ' + r'contained containedin=deniteGitStatusSymbol') self.vim.command(r'syntax match deniteGitStatusChange /\~/ ' + r'contained containedin=deniteGitStatusSymbol') self.vim.command(r'syntax match deniteGitStatusUnknown /?/ ' + r'contained containedin=deniteGitStatusSymbol') def gather_candidates(self, context): root = context['__root'] winnr = context['__winnr'] if not root: return [] args = ['git', 'status', '--porcelain', '-uall'] self.print_message(context, ' '.join(args)) lines = run_command(args, root) candidates = [] for line in lines: if EMPTY_LINE.fullmatch(line): continue candidates.append(_parse_line(line, root, winnr)) return candidates class Kind(File): def __init__(self, vim): super().__init__(vim) self.persist_actions += ['reset', 'add', 'delete'] # pylint: disable=E1101 self.redraw_actions += ['reset', 'add', 'commit'] # pylint: disable=E1101 self.name = 'gitstatus' self._previewed_target = None val = self.vim.call('exists', ':Rm') if val == 2: self.remove = 'rm' elif self.vim.call('executable', 'rmtrash'): self.remove = 'rmtrash' else: self.remove = 'delete' def action_patch(self, context): args = [] root = context['targets'][0]['source__root'] for target in context['targets']: filepath = target['action__path'] args.append(os.path.relpath(filepath, root)) self.vim.command('terminal git add ' + ' '.join(args) + ' --patch') def action_add(self, context): args = ['git', 'add'] root = context['targets'][0]['source__root'] for target in context['targets']: filepath = target['action__path'] args.append(os.path.relpath(filepath, root)) run_command(args, root) def __get_preview_window(self): return next(filterfalse(lambda x: not x.options['previewwindow'], self.vim.windows), None) # diff action def action_delete(self, context): target = context['targets'][0] root = target['source__root'] winnr = target['Source__winnr'] gitdir = os.path.join(target['source__root'], '.git') preview_window = self.__get_preview_window() if preview_window: self.vim.command('pclose!') if self._previewed_target == target: return relpath = os.path.relpath(target['action__path'], root) prefix = '' if target['source__staged']: if target['source__tree']: confirmed = str(self.vim.call('denite#util#input', 'Diff cached?[y/n]', 'y', '')) == 'y' if confirmed == 'y': prefix = '--cached ' else: prefix = '--cached ' prev_id = self.vim.call('win_getid') self.vim.command(str(winnr) + 'wincmd w') self.vim.call('denite#git#diffPreview', prefix, relpath, gitdir) self.vim.call('win_gotoid', prev_id) self._previewed_target = target def action_reset(self, context): cwd = os.path.normpath(self.vim.eval('expand("%:p:h")')) for target in context['targets']: filepath = target['action__path'] root = target['source__root'] path = os.path.relpath(filepath, root) if target['source__tree'] and target['source__staged']: res = str(self.vim.call('denite#util#input', 'Select action reset or checkout [r/c]', '', '')) if res == 'c': args = 'git checkout -- ' + path run_command(shlex.split(args), root) elif res == 'r': args = 'git reset HEAD -- ' + path run_command(shlex.split(args), root) elif target['source__tree']: args = 'git checkout -- ' + path run_command(shlex.split(args), root) elif target['source__staged']: args = 'git reset HEAD -- ' + path run_command(shlex.split(args), root) else: if self.remove == 'rm': self.vim.command('Rm ' + os.path.relpath(filepath, cwd)) elif self.remove == 'rmtrash': run_command(['rmtrash', filepath], root) else: self.vim.call('delete', filepath) self.vim.command('checktime') def action_commit(self, context): root = context['targets'][0]['source__root'] files = [] for target in context['targets']: filepath = target['action__path'] files.append(os.path.relpath(filepath, root)) self.vim.call('denite#git#commit', '-v', files)