From 4eec748022750f7954cd4a785b43897dc7efdb51 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 21 Jun 2025 14:36:38 +0200 Subject: [PATCH] Adding upstream version 1.3.3. Signed-off-by: Daniel Baumann --- .gitignore | 5 + LICENSE | 21 ++++ README.md | 199 +++++++++++++++++++++++++++++ powerline_gitstatus/__init__.py | 1 + powerline_gitstatus/segments.py | 213 ++++++++++++++++++++++++++++++++ pyproject.toml | 28 +++++ screenshot.png | Bin 0 -> 13238 bytes 7 files changed, 467 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 powerline_gitstatus/__init__.py create mode 100644 powerline_gitstatus/segments.py create mode 100644 pyproject.toml create mode 100644 screenshot.png 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..15f7065 --- /dev/null +++ b/README.md @@ -0,0 +1,199 @@ +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 + } +} +``` + +It's strongly recommended to define the `trusted_paths` argument. This will +restrict the locations where git commands will be invoked, limiting the +exposure to remote code execution via malicious repositories. Navigating the +shell to repositories outside these trusted paths will not display the segment. + +```json +"gitstatus": { + "args": { + "trusted_paths": [ + "/home/foo/code", + "/home/foo/projects" + ] + } +} +``` + +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..034e50b --- /dev/null +++ b/powerline_gitstatus/segments.py @@ -0,0 +1,213 @@ +# vim:fileencoding=utf-8:noet + +from powerline.segments import Segment, with_docstring +from powerline.theme import requires_segment_info +from subprocess import PIPE, Popen +from pathlib import PurePath +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(r'^Initial commit on (.+)$', line) + if match is not None: + return (match.group(1), False, 0, 0) + + match = re.search(r'^(.+) \(no branch\)$', line) + if match is not None: + return (match.group(1), True, 0, 0) + + match = re.search(r'^(.+?)\.\.\.', line) + if match is not None: + branch = match.group(1) + + match = re.search(r'\[ahead (\d+), behind (\d+)\]$', line) + if match is not None: + return (branch, False, int(match.group(2)), int(match.group(1))) + match = re.search(r'\[ahead (\d+)\]$', line) + if match is not None: + return (branch, False, 0, int(match.group(1))) + match = re.search(r'\[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 path_is_trusted(self, cwd, trusted_paths, pl): + for trusted_path in trusted_paths: + cwd_path = PurePath(cwd) + try: + cwd_path.relative_to(trusted_path) + return True + except ValueError: + pass + return False + + def __call__(self, pl, segment_info, use_dash_c=True, show_tag=False, formats={}, detached_head_style='revision', untracked_not_dirty=False, trusted_paths=[]): + cwd = segment_info['getcwd']() + + if not cwd: + return + + if trusted_paths and not self.path_is_trusted(cwd, trusted_paths, pl): + pl.debug("cwd not in trusted paths") + return + + base = self.get_base_command(cwd, use_dash_c) + + if not base: + return + + pl.debug('Running gitstatus %s -C' % ('with' if use_dash_c else 'without')) + 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/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..173a01b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "powerline-gitstatus" +description = "A Powerline segment for showing the status of a Git working copy" +version = "1.3.3" +keywords = ["powerline git status prompt"] +license = {text = "MIT License"} +authors = [ + {name = "Jasper N. Brouwer", email = "jasper@nerdsweide.nl"}, +] +maintainers = [ + {name ="Jérôme Charaoui", email = "jerome@riseup.net"}, +] +classifiers = [ + "Development Status :: 6 - Mature", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Topic :: Terminals" +] + +[project.urls] +Homepage = "https://github.com/jaspernbrouwer/powerline-gitstatus" +Repository = "https://github.com/jaspernbrouwer/powerline-gitstatus.git" +Issues = "https://github.com/jaspernbrouwer/powerline-gitstatus" diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..1e44f517c4e86c48e88c5f40cc8be617d276db87 GIT binary patch literal 13238 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 literal 0 HcmV?d00001