129 lines
3.2 KiB
Python
Executable File
129 lines
3.2 KiB
Python
Executable File
#!/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()
|