From 7557526117e795f9a600f4033e053821a0be0489 Mon Sep 17 00:00:00 2001 From: Tris Forster Date: Sun, 14 Sep 2025 21:03:05 +1000 Subject: [PATCH] Initial commit (0.1.1) --- .gitignore | 2 + README.md | 16 ++++++ git_overview.py | 128 ++++++++++++++++++++++++++++++++++++++++++++++++ poetry.lock | 7 +++ pyproject.toml | 19 +++++++ 5 files changed, 172 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 git_overview.py create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac8d7b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +dist diff --git a/README.md b/README.md new file mode 100644 index 0000000..0bc0b36 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Git Overview - Work In Progress + +Displays a brief summary for multiple git work directories. + +## Usage + +``` +$ git-overview -a ~/Projects +Workdir Branch Status +\033[93mnoted main changed [1M]\03[0m +git-overview main untracked [6U] +xmon code-revie untracked [3U] +polyphonic-forms native-for changed [7M, 1D, 3U] +polyphonic master changed [1M, 1U] +django-byostorage master clean +``` diff --git a/git_overview.py b/git_overview.py new file mode 100755 index 0000000..fce11c4 --- /dev/null +++ b/git_overview.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 + +import os +import logging +import subprocess +import dataclasses + +logger = logging.getLogger(__name__) + +STATUS_COLORS = { + "untracked": 93, + "changed": 31, + "clean": 32, + "unknown": 94, +} + +FORMAT_STRINGS = { + "default": "\033[{color}m{name:20.20s} {branch:10.10s} {status:10.10s} {changes}\033[0m" +} + + +@dataclasses.dataclass +class GitStatus: + name: str + branch: str = "" + status: str = "untracked" + modified: int = 0 + deleted: int = 0 + untracked: int = 0 + + def info(self, fmt): + changes = [] + if self.modified: + changes.append(f"{self.modified}M") + if self.deleted: + changes.append(f"{self.deleted}D") + if self.untracked: + changes.append(f"{self.untracked}U") + + changes = f" [{', '.join(changes)}]" if changes else "" + color = STATUS_COLORS.get(self.status, STATUS_COLORS["unknown"]) + fmt = fmt or FORMAT_STRINGS["default"] + + return fmt.format(changes=changes, color=color, **dataclasses.asdict(self)) + + +def multi_repo_overview(directory: str, all: bool = False): + result = [] + for f in os.scandir(directory): + if not f.is_dir(): + continue + + try: + result.append(repo_overview(f.path)) + except FileNotFoundError: + if all: + result.append(GitStatus(name=f.name, status="-")) + + return result + + +def repo_overview(repo): + if not os.path.isdir(os.path.join(repo, ".git")): + raise FileNotFoundError(f"{repo} is not a git repo") + + p = subprocess.run(("git", "-C", repo, "status"), capture_output=True) + lines = p.stdout.decode().split("\n") + + info = GitStatus(name=os.path.basename(repo)) + for line in lines: + if line.startswith("On branch"): + info.branch = line[10:] + + if line.startswith("Changes "): + info.status = "changed" + + if line.startswith("Your branch is up to date"): + info.status = "clean" + + if line.startswith("\tmodified:"): + info.modified += 1 + elif line.startswith("\tdeleted:"): + info.deleted += 1 + elif line.startswith("\t"): + info.untracked += 1 + + return info + + +def cmd(): + import argparse + + parser = argparse.ArgumentParser( + description="Check the work trees in a directory for their status" + ) + parser.add_argument("--log-level", "-l", default="info") + parser.add_argument( + "--all", + "-a", + action="store_true", + help="Include directories without a git folder", + ) + parser.add_argument("--format", "-f", default="default") + parser.add_argument("directory", nargs="?", default=".") + options = parser.parse_args() + + logging.basicConfig(level=getattr(logging, options.log_level.upper(), logging.INFO)) + logger.debug(options) + + repos = multi_repo_overview(options.directory, options.all) + + fmt = FORMAT_STRINGS.get(options.format, FORMAT_STRINGS["default"]) + + print( + fmt.format( + color=0, + name="Workdir", + branch="Branch", + status="Status", + changes="", + ) + ) + for repo in repos: + print(repo.info(fmt)) + + +if __name__ == "__main__": + cmd() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..d62b44d --- /dev/null +++ b/poetry.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +package = [] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.10" +content-hash = "b08227dc5674045d1a6b5ab20f461c6a08d2d9bdfd724891aceb1a0c2b3614b1" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0508e8a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "git-overview" +version = "0.1.1" +description = "Show overview of multiple git working directories\"" +authors = [ + {name = "Tris Forster",email = "tris@tfconsulting.com.au"} +] +readme = "README.md" +requires-python = ">=3.0" +dependencies = [ +] + +[project.scripts] +git-overview = "git_overview:cmd" + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api"