diff options
Diffstat (limited to 'powerline_gitstatus/segments.py')
-rw-r--r-- | powerline_gitstatus/segments.py | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/powerline_gitstatus/segments.py b/powerline_gitstatus/segments.py new file mode 100644 index 0000000..cdd0a01 --- /dev/null +++ b/powerline_gitstatus/segments.py @@ -0,0 +1,199 @@ +# vim:fileencoding=utf-8:noet + +from powerline.segments import Segment, with_docstring +from powerline.theme import requires_segment_info +from subprocess import PIPE, Popen +import os, re, string + + +@requires_segment_info +class GitStatusSegment(Segment): + + def execute(self, pl, command): + pl.debug('Executing command: %s' % ' '.join(command)) + + git_env = os.environ.copy() + git_env['LC_ALL'] = 'C' + + proc = Popen(command, stdout=PIPE, stderr=PIPE, env=git_env) + out, err = [item.decode('utf-8') for item in proc.communicate()] + + if out: + pl.debug('Command output: %s' % out.strip(string.whitespace)) + if err: + pl.debug('Command errors: %s' % err.strip(string.whitespace)) + + return (out.splitlines(), err.splitlines()) + + def get_base_command(self, cwd, use_dash_c): + if use_dash_c: + return ['git', '-c', 'core.fsmonitor=', '-C', cwd] + + while cwd and cwd != os.sep: + gitdir = os.path.join(cwd, '.git') + + if os.path.isdir(gitdir): + return ['git', '-c', 'core.fsmonitor=', '--git-dir=%s' % gitdir, '--work-tree=%s' % cwd] + + cwd = os.path.dirname(cwd) + + return None + + def parse_branch(self, line): + if not line: + return ('', False, 0, 0) + + if line.startswith('## '): + line = line[3:] + + match = re.search('^Initial commit on (.+)$', line) + if match is not None: + return (match.group(1), False, 0, 0) + + match = re.search('^(.+) \(no branch\)$', line) + if match is not None: + return (match.group(1), True, 0, 0) + + match = re.search('^(.+?)\.\.\.', line) + if match is not None: + branch = match.group(1) + + match = re.search('\[ahead (\d+), behind (\d+)\]$', line) + if match is not None: + return (branch, False, int(match.group(2)), int(match.group(1))) + match = re.search('\[ahead (\d+)\]$', line) + if match is not None: + return (branch, False, 0, int(match.group(1))) + match = re.search('\[behind (\d+)\]$', line) + if match is not None: + return (branch, False, int(match.group(1)), 0) + + return (branch, False, 0, 0) + + return (line, False, 0, 0) + + def parse_status(self, lines): + staged = len([True for l in lines if l[0] in 'MRC' or (l[0] == 'D' and l[1] != 'D') or (l[0] == 'A' and l[1] != 'A')]) + unmerged = len([True for l in lines if l[0] == 'U' or l[1] == 'U' or (l[0] == 'A' and l[1] == 'A') or (l[0] == 'D' and l[1] == 'D')]) + changed = len([True for l in lines if l[1] == 'M' or (l[1] == 'D' and l[0] != 'D')]) + untracked = len([True for l in lines if l[0] == '?']) + + return (staged, unmerged, changed, untracked) + + def build_segments(self, formats, branch, detached, tag, behind, ahead, staged, unmerged, changed, untracked, stashed, untracked_not_dirty): + if detached: + branch_group = 'gitstatus_branch_detached' + elif staged or unmerged or changed or (untracked and not untracked_not_dirty): + branch_group = 'gitstatus_branch_dirty' + else: + branch_group = 'gitstatus_branch_clean' + + segments = [ + {'contents': formats.get('branch', u'\ue0a0 {}').format(branch), 'highlight_groups': [branch_group, 'gitstatus_branch', 'gitstatus'], 'divider_highlight_group': 'gitstatus:divider'} + ] + + if tag: + segments.append({'contents': formats.get('tag', u' \u2605 {}').format(tag), 'highlight_groups': ['gitstatus_tag', 'gitstatus'], 'divider_highlight_group': 'gitstatus:divider'}) + if behind: + segments.append({'contents': formats.get('behind', ' ↓ {}').format(behind), 'highlight_groups': ['gitstatus_behind', 'gitstatus'], 'divider_highlight_group': 'gitstatus:divider'}) + if ahead: + segments.append({'contents': formats.get('ahead', ' ↑ {}').format(ahead), 'highlight_groups': ['gitstatus_ahead', 'gitstatus'], 'divider_highlight_group': 'gitstatus:divider'}) + if staged: + segments.append({'contents': formats.get('staged', ' ● {}').format(staged), 'highlight_groups': ['gitstatus_staged', 'gitstatus'], 'divider_highlight_group': 'gitstatus:divider'}) + if unmerged: + segments.append({'contents': formats.get('unmerged', ' ✖ {}').format(unmerged), 'highlight_groups': ['gitstatus_unmerged', 'gitstatus'], 'divider_highlight_group': 'gitstatus:divider'}) + if changed: + segments.append({'contents': formats.get('changed', ' ✚ {}').format(changed), 'highlight_groups': ['gitstatus_changed', 'gitstatus'], 'divider_highlight_group': 'gitstatus:divider'}) + if untracked: + segments.append({'contents': formats.get('untracked', ' … {}').format(untracked), 'highlight_groups': ['gitstatus_untracked', 'gitstatus'], 'divider_highlight_group': 'gitstatus:divider'}) + if stashed: + segments.append({'contents': formats.get('stashed', ' ⚑ {}').format(stashed), 'highlight_groups': ['gitstatus_stashed', 'gitstatus'], 'divider_highlight_group': 'gitstatus:divider'}) + + return segments + + def __call__(self, pl, segment_info, use_dash_c=True, show_tag=False, formats={}, detached_head_style='revision', untracked_not_dirty=False): + pl.debug('Running gitstatus %s -C' % ('with' if use_dash_c else 'without')) + + cwd = segment_info['getcwd']() + + if not cwd: + return + + base = self.get_base_command(cwd, use_dash_c) + + if not base: + return + + status, err = self.execute(pl, base + ['status', '--branch', '--porcelain']) + + if err and ('error' in err[0] or 'fatal' in err[0]): + return + + branch, detached, behind, ahead = self.parse_branch(status.pop(0)) + + if not branch: + return + + if branch == 'HEAD': + if detached_head_style == 'revision': + branch = self.execute(pl, base + ['rev-parse', '--short', 'HEAD'])[0][0] + elif detached_head_style == 'ref': + branch = self.execute(pl, base + ['describe', '--contains', '--all'])[0][0] + + staged, unmerged, changed, untracked = self.parse_status(status) + + stashed = len(self.execute(pl, base + ['stash', 'list', '--no-decorate'])[0]) + + if not show_tag: + tag, err = [''], False + elif show_tag == 'contains': + tag, err = self.execute(pl, base + ['describe', '--contains']) + elif show_tag == 'last': + tag, err = self.execute(pl, base + ['describe', '--tags']) + elif show_tag == 'annotated': + tag, err = self.execute(pl, base + ['describe']) + else: + tag, err = self.execute(pl, base + ['describe', '--tags', '--exact-match', '--abbrev=0']) + + if err and ('error' in err[0] or 'fatal' in err[0] or 'Could not get sha1 for HEAD' in err[0]): + tag = '' + else: + tag = tag[0] + + return self.build_segments(formats, branch, detached, tag, behind, ahead, staged, unmerged, changed, untracked, stashed, untracked_not_dirty) + + +gitstatus = with_docstring(GitStatusSegment(), +'''Return the status of a Git working copy. + +It will show the branch-name, or the commit hash if in detached head state. + +It will also show the number of commits behind, commits ahead, staged files, +unmerged files (conflicts), changed files, untracked files and stashed files +if that number is greater than zero. + +:param bool use_dash_c: + Call git with ``-C``, which is more performant and accurate, but requires git 1.8.5 or higher. + Otherwise it will traverse the current working directory up towards the root until it finds a ``.git`` directory, then use ``--git-dir`` and ``--work-tree``. + True by default. + +:param bool show_tag: + Show tag description. Valid options are``contains``, ``last``, ``annotated`` and ``exact``. A value of True behaves the same as ``exact``, which only displays a tag when it's assigned to the currently checked-out revision. + False by default, because it needs to execute git an additional time. + +:param dict formats: + A string-to-string dictionary for customizing Git status formats. Valid keys include ``branch``, ``tag``, ``ahead``, ``behind``, ``staged``, ``unmerged``, ``changes``, ``untracked``, and ``stashed``. + Empty dictionary by default, which means the default formats are used. + +:param detached_head_style: + Display style when in detached HEAD state. Valid values are ``revision``, which shows the current revision id, and ``ref``, which shows the closest reachable ref object. + The default is ``revision``. + +:param untracked_not_dirty: + Untracked files alone will not mark the git branch status as dirty. + False by default. + +Divider highlight group used: ``gitstatus:divider``. + +Highlight groups used: ``gitstatus_branch_detached``, ``gitstatus_branch_dirty``, ``gitstatus_branch_clean``, ``gitstatus_branch``, ``gitstatus_tag``, ``gitstatus_behind``, ``gitstatus_ahead``, ``gitstatus_staged``, ``gitstatus_unmerged``, ``gitstatus_changed``, ``gitstatus_untracked``, ``gitstatus_stashed``, ``gitstatus``. +''') |