mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-01-24 05:20:04 +08:00
226 lines
7.2 KiB
Python
226 lines
7.2 KiB
Python
# ============================================================================
|
|
# FILE: git.py
|
|
# AUTHOR: Kristijan Husak <husakkristijan at gmail.com>
|
|
# License: MIT license
|
|
# ============================================================================
|
|
|
|
import typing
|
|
import subprocess
|
|
from defx.base.column import Base
|
|
from defx.context import Context
|
|
from defx.view import View
|
|
from neovim import Nvim
|
|
from functools import cmp_to_key
|
|
from pathlib import PurePath
|
|
|
|
|
|
class Column(Base):
|
|
|
|
def __init__(self, vim: Nvim) -> None:
|
|
super().__init__(vim)
|
|
|
|
self.name = 'git'
|
|
self.vars = {
|
|
'indicators': {
|
|
'Modified': '✹',
|
|
'Staged': '✚',
|
|
'Untracked': '✭',
|
|
'Renamed': '➜',
|
|
'Unmerged': '═',
|
|
'Ignored': '☒',
|
|
'Deleted': '✖',
|
|
'Unknown': '?'
|
|
},
|
|
'column_length': 1,
|
|
'show_ignored': False,
|
|
'raw_mode': False,
|
|
'max_indicator_width': None
|
|
}
|
|
|
|
custom_opts = ['indicators', 'column_length', 'show_ignored',
|
|
'raw_mode', 'max_indicator_width']
|
|
|
|
for opt in custom_opts:
|
|
if 'defx_git#' + opt in self.vim.vars:
|
|
self.vars[opt] = self.vim.vars['defx_git#' + opt]
|
|
|
|
self.cache: typing.List[str] = []
|
|
self.git_root = ''
|
|
self.colors = {
|
|
'Modified': {
|
|
'color': 'guifg=#fabd2f ctermfg=214',
|
|
'match': ' M'
|
|
},
|
|
'Staged': {
|
|
'color': 'guifg=#b8bb26 ctermfg=142',
|
|
'match': '\(M\|A\|C\).'
|
|
},
|
|
'Renamed': {
|
|
'color': 'guifg=#fabd2f ctermfg=214',
|
|
'match': 'R.'
|
|
},
|
|
'Unmerged': {
|
|
'color': 'guifg=#fb4934 ctermfg=167',
|
|
'match': '\(UU\|AA\|DD\)'
|
|
},
|
|
'Deleted': {
|
|
'color': 'guifg=#fb4934 ctermfg=167',
|
|
'match': ' D'
|
|
},
|
|
'Untracked': {
|
|
'color': 'guifg=NONE guibg=NONE ctermfg=NONE ctermbg=NONE',
|
|
'match': '??'
|
|
},
|
|
'Ignored': {
|
|
'color': 'guifg=NONE guibg=NONE ctermfg=NONE ctermbg=NONE',
|
|
'match': '!!'
|
|
},
|
|
'Unknown': {
|
|
'color': 'guifg=NONE guibg=NONE ctermfg=NONE ctermbg=NONE',
|
|
'match': 'X '
|
|
}
|
|
}
|
|
min_column_length = 2 if self.vars['raw_mode'] else 1
|
|
self.column_length = max(min_column_length, self.vars['column_length'])
|
|
|
|
def on_init(self, view: View, context: Context) -> None:
|
|
# Set vim global variable for search mappings matching indicators
|
|
self.vim.vars['defx_git_indicators'] = self.vars['indicators']
|
|
|
|
if not self.vars.get('max_indicator_width'):
|
|
# Find longest indicator
|
|
self.vars['max_indicator_width'] = len(
|
|
max(self.vars['indicators'].values(), key=len))
|
|
|
|
def get(self, context: Context, candidate: dict) -> str:
|
|
default = self.format('').ljust(
|
|
self.column_length + self.vars['max_indicator_width'] - 1)
|
|
if candidate.get('is_root', False):
|
|
self.cache_status(candidate['action__path'])
|
|
return default
|
|
|
|
if not self.cache:
|
|
return default
|
|
|
|
entry = self.find_in_cache(candidate)
|
|
|
|
if not entry:
|
|
return default
|
|
|
|
return self.get_indicator(entry)
|
|
|
|
def get_indicator(self, entry: str) -> str:
|
|
if self.vars['raw_mode']:
|
|
return self.format(entry[:2])
|
|
|
|
state = self.get_indicator_name(entry[0], entry[1])
|
|
return self.format(
|
|
self.vars['indicators'][state]
|
|
)
|
|
|
|
def length(self, context: Context) -> int:
|
|
return self.column_length
|
|
|
|
def syntaxes(self) -> typing.List[str]:
|
|
return [
|
|
self.syntax_name + '_' + name for name in self.vars['indicators']]
|
|
|
|
def highlight_commands(self) -> typing.List[str]:
|
|
commands: typing.List[str] = []
|
|
for name, icon in self.vars['indicators'].items():
|
|
if self.vars['raw_mode']:
|
|
commands.append((
|
|
'syntax match {0}_{1} /{2}/ contained containedin={0}'
|
|
).format(self.syntax_name, name, self.colors[name]['match']))
|
|
else:
|
|
commands.append((
|
|
'syntax match {0}_{1} /[{2}]/ contained containedin={0}'
|
|
).format(self.syntax_name, name, icon))
|
|
|
|
commands.append('highlight default {0}_{1} {2}'.format(
|
|
self.syntax_name, name, self.colors[name]['color']
|
|
))
|
|
return commands
|
|
|
|
def find_in_cache(self, candidate: dict) -> str:
|
|
action_path = PurePath(candidate['action__path']).as_posix()
|
|
path = str(action_path).replace(f'{self.git_root}/', '')
|
|
path += '/' if candidate['is_directory'] else ''
|
|
for item in self.cache:
|
|
item_path = item[3:]
|
|
if item[0] == 'R':
|
|
item_path = item_path.split(' -> ')[1]
|
|
|
|
if item_path.startswith(path):
|
|
return item
|
|
|
|
return ''
|
|
|
|
def cache_status(self, path: str) -> None:
|
|
self.cache = []
|
|
|
|
if not self.git_root or not str(path).startswith(self.git_root):
|
|
self.git_root = PurePath(self.run_cmd(
|
|
['git', 'rev-parse', '--show-toplevel'], path
|
|
)).as_posix()
|
|
|
|
if not self.git_root:
|
|
return None
|
|
|
|
cmd = ['git', 'status', '--porcelain', '-u']
|
|
if self.vars['show_ignored']:
|
|
cmd += ['--ignored']
|
|
|
|
status = self.run_cmd(cmd, self.git_root)
|
|
results = [line for line in status.split('\n') if line != '']
|
|
self.cache = sorted(results, key=cmp_to_key(self.sort))
|
|
|
|
def sort(self, a, b) -> int:
|
|
if a[0] == 'U' or a[1] == 'U':
|
|
return -1
|
|
|
|
if (a[0] == 'M' or a[1] == 'M') and not (b[0] == 'U' or b[1] == 'U'):
|
|
return -1
|
|
|
|
if ((a[0] == '?' and a[1] == '?') and not
|
|
(b[0] in ['M', 'U'] or b[1] in ['M', 'U'])):
|
|
return -1
|
|
|
|
return 1
|
|
|
|
def format(self, column: str) -> str:
|
|
return format(column, f'<{self.column_length}')
|
|
|
|
def get_indicator_name(self, us: str, them: str) -> str:
|
|
if us == '?' and them == '?':
|
|
return 'Untracked'
|
|
elif us == ' ' and them == 'M':
|
|
return 'Modified'
|
|
elif us in ['M', 'A', 'C']:
|
|
return 'Staged'
|
|
elif us == 'R':
|
|
return 'Renamed'
|
|
elif us == '!':
|
|
return 'Ignored'
|
|
elif (us == 'U' or them == 'U' or us == 'A' and them == 'A'
|
|
or us == 'D' and them == 'D'):
|
|
return 'Unmerged'
|
|
elif them == 'D':
|
|
return 'Deleted'
|
|
else:
|
|
return 'Unknown'
|
|
|
|
def run_cmd(self, cmd: typing.List[str], cwd=None) -> str:
|
|
try:
|
|
p = subprocess.run(cmd, stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL, cwd=cwd)
|
|
except:
|
|
return ''
|
|
|
|
decoded = p.stdout.decode('utf-8')
|
|
|
|
if not decoded:
|
|
return ''
|
|
|
|
return decoded.strip('\n')
|