summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--LICENSE21
-rw-r--r--README.md183
-rw-r--r--powerline_gitstatus/__init__.py1
-rw-r--r--powerline_gitstatus/segments.py199
-rw-r--r--screenshot.pngbin0 -> 13238 bytes
-rw-r--r--setup.py21
7 files changed, 430 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bcdae01
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+*.egg-info
+*.pyc
+build/*
+dist/*
+build_howto.md
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b3e5af9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 - 2018 Jasper N. Brouwer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5ecaf16
--- /dev/null
+++ b/README.md
@@ -0,0 +1,183 @@
+Powerline Gitstatus
+===================
+
+A [Powerline][1] segment for showing the status of a Git working copy.
+
+By [Jasper N. Brouwer][2].
+
+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.
+
+![screenshot][4]
+
+Glossary
+--------
+- ``: branch name or commit hash
+- `★`: most recent tag (if enabled)
+- `↓`: n commits behind
+- `↑`: n commits ahead
+- `●`: n staged files
+- `✖`: n unmerged files (conflicts)
+- `✚`: n changed files
+- `…`: n untracked files
+- `⚑`: n stashed files
+
+Requirements
+------------
+
+The Gitstatus segment requires [git][5]! Preferably, but not limited to, version 1.8.5 or higher.
+
+Version 1.8.5 will enable the usage of the `-C` parameter, which is more performant and accurate.
+
+Installation
+------------
+
+### On Debian/Ubuntu
+
+On a recent enough Debian (at least Stretch with backports enabled) or Ubuntu (at least 18.10) there is an official package available.
+
+```txt
+apt install powerline-gitstatus
+```
+
+This command will also instruct your package manager to install Powerline, if it's not already available.
+
+Powerline will be automatically configured to use the Gitstatus highlight groups and add the segment to the default
+shell theme.
+
+### Using pip
+
+```txt
+pip install powerline-gitstatus
+```
+
+Configuration
+-------------
+
+The Gitstatus segment uses a couple of custom highlight groups. You'll need to define those groups in your colorscheme,
+for example in `.config/powerline/colorschemes/default.json`:
+
+```json
+{
+ "groups": {
+ "gitstatus": { "fg": "gray8", "bg": "gray2", "attrs": [] },
+ "gitstatus_branch": { "fg": "gray8", "bg": "gray2", "attrs": [] },
+ "gitstatus_branch_clean": { "fg": "green", "bg": "gray2", "attrs": [] },
+ "gitstatus_branch_dirty": { "fg": "gray8", "bg": "gray2", "attrs": [] },
+ "gitstatus_branch_detached": { "fg": "mediumpurple", "bg": "gray2", "attrs": [] },
+ "gitstatus_tag": { "fg": "darkcyan", "bg": "gray2", "attrs": [] },
+ "gitstatus_behind": { "fg": "gray10", "bg": "gray2", "attrs": [] },
+ "gitstatus_ahead": { "fg": "gray10", "bg": "gray2", "attrs": [] },
+ "gitstatus_staged": { "fg": "green", "bg": "gray2", "attrs": [] },
+ "gitstatus_unmerged": { "fg": "brightred", "bg": "gray2", "attrs": [] },
+ "gitstatus_changed": { "fg": "mediumorange", "bg": "gray2", "attrs": [] },
+ "gitstatus_untracked": { "fg": "brightestorange", "bg": "gray2", "attrs": [] },
+ "gitstatus_stashed": { "fg": "darkblue", "bg": "gray2", "attrs": [] },
+ "gitstatus:divider": { "fg": "gray8", "bg": "gray2", "attrs": [] }
+ }
+}
+```
+
+Then you can activate the Gitstatus segment by adding it to your segment configuration,
+for example in `.config/powerline/themes/shell/default.json`:
+
+```json
+{
+ "function": "powerline_gitstatus.gitstatus",
+ "priority": 40
+}
+```
+
+The Gitstatus segment will use the `-C` argument by default, but this requires git 1.8.5 or higher.
+
+If you cannot meet that requirement, you'll have to disable the usage of `-C`.
+Do this by passing `false` to the `use_dash_c` argument, for example in `.config/powerline/themes/shell/__main__.json`:
+
+```json
+"gitstatus": {
+ "args": {
+ "use_dash_c": false
+ }
+}
+```
+
+Optionally, a tag description for the current branch may be displayed using the `show_tag` option. Valid values for this
+argument are:
+
+ * `last` : shows the most recent tag
+ * `annotated` : shows the most recent annotated tag
+ * `contains` : shows the closest tag that comes after the current commit
+ * `exact` : shows a tag only if it matches the current commit
+
+You can enable this by passing one of these to the `show_tag` argument, for example in `.config/powerline/themes/shell/__main__.json`:
+
+```json
+"gitstatus": {
+ "args": {
+ "show_tag": "exact"
+ }
+}
+```
+Git is executed an additional time to find this tag, so it is disabled by default.
+
+Note: before v1.3.0, the behavior when the value is `True` was `last`. As of v1.3.0 onwards, `True` behaves as `exact`.
+
+Optionally the format in which Gitstatus shows information can be customized.
+This allows to use a different symbol or remove a fragment if desired. You can
+customize string formats for _branch_, _tag_, _behind_, _ahead_, _staged_, _unmerged_,
+_changed_, _untracked_ and _stash_ fragments with the following arguments in a
+theme configuration file, for example `.config/powerline/themes/shell/__main__.json`:
+
+```json
+"gitstatus": {
+ "args": {
+ "formats": {
+ "branch": "\ue0a0 {}",
+ "tag": " ★ {}",
+ "behind": " ↓ {}",
+ "ahead": " ↑ {}",
+ "staged": " ● {}",
+ "unmerged": " ✖ {}",
+ "changed": " ✚ {}",
+ "untracked": " … {}",
+ "stashed": " ⚑ {}"
+ }
+ }
+}
+```
+
+By default, when in detached head state (current revision is not a branch tip), Gitstatus shows a short commit hash in
+place of the branch name. This can be replaced with a description of the closest reachable ref using the
+`detached_head_style` argument, for example in `.config/powerline/themes/shell/__main__.json`:
+
+```json
+"gitstatus": {
+ "args": {
+ "detached_head_style": "ref"
+ }
+}
+```
+
+By default, if your local branch has untracked files but no other changes, the branch status will be highlighted as dirty in the segment. You can disable this behavior by setting the `untracked_not_dirty` argument to `true`, for example in `.config/powerline/themes/shell/__main__.json`:
+
+```json
+"gitstatus": {
+ "args": {
+ "untracked_not_dirty": true
+ }
+}
+```
+
+License
+-------
+
+Licensed under [the MIT License][3].
+
+[1]: https://powerline.readthedocs.org/en/master/
+[2]: https://github.com/jaspernbrouwer
+[3]: https://github.com/jaspernbrouwer/powerline-gitstatus/blob/master/LICENSE
+[4]: https://github.com/jaspernbrouwer/powerline-gitstatus/blob/master/screenshot.png
+[5]: https://git-scm.com/
diff --git a/powerline_gitstatus/__init__.py b/powerline_gitstatus/__init__.py
new file mode 100644
index 0000000..9dd7283
--- /dev/null
+++ b/powerline_gitstatus/__init__.py
@@ -0,0 +1 @@
+from .segments import gitstatus
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``.
+''')
diff --git a/screenshot.png b/screenshot.png
new file mode 100644
index 0000000..1e44f51
--- /dev/null
+++ b/screenshot.png
Binary files differ
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..b4e10d2
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,21 @@
+# vim:fileencoding=utf-8:noet
+
+from setuptools import setup
+
+setup(
+ name = 'powerline-gitstatus',
+ description = 'A Powerline segment for showing the status of a Git working copy',
+ version = '1.3.1',
+ keywords = 'powerline git status prompt',
+ license = 'MIT',
+ author = 'Jasper N. Brouwer',
+ author_email = 'jasper@nerdsweide.nl',
+ url = 'https://github.com/jaspernbrouwer/powerline-gitstatus',
+ packages = ['powerline_gitstatus'],
+ classifiers = [
+ 'Environment :: Console',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Topic :: Terminals'
+ ]
+)