git-muster 0.4.1__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {git_muster-0.4.1 → git_muster-0.5.0}/PKG-INFO +3 -3
- {git_muster-0.4.1 → git_muster-0.5.0}/README.md +2 -2
- {git_muster-0.4.1 → git_muster-0.5.0}/pyproject.toml +1 -1
- {git_muster-0.4.1 → git_muster-0.5.0}/pyproject.toml.orig +1 -1
- {git_muster-0.4.1 → git_muster-0.5.0}/src/git_muster/cli.py +118 -1
- {git_muster-0.4.1 → git_muster-0.5.0}/LICENSE +0 -0
- {git_muster-0.4.1 → git_muster-0.5.0}/src/git_muster/__init__.py +0 -0
- {git_muster-0.4.1 → git_muster-0.5.0}/src/git_muster/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-muster
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: See every local Git branch and what needs attention
|
|
5
5
|
Keywords: branch,cli,git,github,worktree
|
|
6
6
|
Author: Dan O'Leary
|
|
@@ -123,7 +123,7 @@ git muster --version
|
|
|
123
123
|
|
|
124
124
|
### Color, symbols, and width
|
|
125
125
|
|
|
126
|
-
Color, Unicode symbols, and clickable pull-request numbers appear when the report is written to an interactive terminal that can render them. Git Muster honors the usual overrides:
|
|
126
|
+
Color, Unicode symbols, clickable repository identities, and clickable pull-request numbers appear when the report is written to an interactive terminal that can render them. GitHub repositories appear as `owner / repository`, with the owner and repository linked separately. The title includes `[public]`, `[private]`, or `[internal]` when GitHub reports visibility, `[fork]` when the repository is a fork, and `[archived]` when it is archived. A fork label links to its parent repository. Git Muster resolves the repository from `origin`, or another GitHub remote when `origin` is not hosted there, and honors the usual overrides:
|
|
127
127
|
|
|
128
128
|
| Variable | Effect |
|
|
129
129
|
|---|---|
|
|
@@ -157,7 +157,7 @@ Publication is deliberately separate from Git's configured upstream. A branch is
|
|
|
157
157
|
|
|
158
158
|
When another linked worktree holds a branch, Git Muster shows its directory in the table and lists the full checkout path below it. It does not scan those other worktrees for dirtiness or change them.
|
|
159
159
|
|
|
160
|
-
GitHub integration uses one optional `gh pr list --state all --limit 100` query
|
|
160
|
+
GitHub integration uses one `gh repo view` query for canonical identity, visibility, fork parent, and archive state, plus one optional `gh pr list --state all --limit 100` query for pull requests. A branch whose pull request falls outside the hundred most recent is reported without pull-request state. When several pull requests share a head branch, the newest open, draft, approved, or changes-requested one wins over closed and merged ones. Without an installed and authenticated `gh`, or when no remote points at a known GitHub host, the complete Git report still works, falls back to identity parsed from the remote URL, omits metadata it cannot establish, and names the reason pull-request state is missing.
|
|
161
161
|
|
|
162
162
|
## Safety and scope
|
|
163
163
|
|
|
@@ -99,7 +99,7 @@ git muster --version
|
|
|
99
99
|
|
|
100
100
|
### Color, symbols, and width
|
|
101
101
|
|
|
102
|
-
Color, Unicode symbols, and clickable pull-request numbers appear when the report is written to an interactive terminal that can render them. Git Muster honors the usual overrides:
|
|
102
|
+
Color, Unicode symbols, clickable repository identities, and clickable pull-request numbers appear when the report is written to an interactive terminal that can render them. GitHub repositories appear as `owner / repository`, with the owner and repository linked separately. The title includes `[public]`, `[private]`, or `[internal]` when GitHub reports visibility, `[fork]` when the repository is a fork, and `[archived]` when it is archived. A fork label links to its parent repository. Git Muster resolves the repository from `origin`, or another GitHub remote when `origin` is not hosted there, and honors the usual overrides:
|
|
103
103
|
|
|
104
104
|
| Variable | Effect |
|
|
105
105
|
|---|---|
|
|
@@ -133,7 +133,7 @@ Publication is deliberately separate from Git's configured upstream. A branch is
|
|
|
133
133
|
|
|
134
134
|
When another linked worktree holds a branch, Git Muster shows its directory in the table and lists the full checkout path below it. It does not scan those other worktrees for dirtiness or change them.
|
|
135
135
|
|
|
136
|
-
GitHub integration uses one optional `gh pr list --state all --limit 100` query
|
|
136
|
+
GitHub integration uses one `gh repo view` query for canonical identity, visibility, fork parent, and archive state, plus one optional `gh pr list --state all --limit 100` query for pull requests. A branch whose pull request falls outside the hundred most recent is reported without pull-request state. When several pull requests share a head branch, the newest open, draft, approved, or changes-requested one wins over closed and merged ones. Without an installed and authenticated `gh`, or when no remote points at a known GitHub host, the complete Git report still works, falls back to identity parsed from the remote URL, omits metadata it cannot establish, and names the reason pull-request state is missing.
|
|
137
137
|
|
|
138
138
|
## Safety and scope
|
|
139
139
|
|
|
@@ -12,6 +12,7 @@ from collections.abc import Callable
|
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import Annotated, Any
|
|
15
|
+
from urllib.parse import urlsplit
|
|
15
16
|
|
|
16
17
|
import typer
|
|
17
18
|
from rich.console import Console
|
|
@@ -81,6 +82,7 @@ ASCII_GLYPHS = {
|
|
|
81
82
|
|
|
82
83
|
PR_LIMIT = 100
|
|
83
84
|
PR_FIELDS = "headRefName,number,state,isDraft,reviewDecision,url"
|
|
85
|
+
REPOSITORY_FIELDS = "nameWithOwner,url,visibility,isFork,parent,isArchived"
|
|
84
86
|
ACTIVE_PR_STATES = {"draft", "open", "approved", "changes requested"}
|
|
85
87
|
|
|
86
88
|
PR_NOTICES: dict[str, tuple[str, ...]] = {
|
|
@@ -242,6 +244,19 @@ class PullRequest:
|
|
|
242
244
|
url: str = ""
|
|
243
245
|
|
|
244
246
|
|
|
247
|
+
@dataclass(frozen=True)
|
|
248
|
+
class GitHubRepository:
|
|
249
|
+
"""GitHub identity and presentation metadata for the current repository."""
|
|
250
|
+
|
|
251
|
+
owner: str
|
|
252
|
+
name: str
|
|
253
|
+
url: str
|
|
254
|
+
visibility: str = ""
|
|
255
|
+
is_fork: bool = False
|
|
256
|
+
parent_url: str = ""
|
|
257
|
+
is_archived: bool = False
|
|
258
|
+
|
|
259
|
+
|
|
245
260
|
@dataclass(frozen=True)
|
|
246
261
|
class Branch:
|
|
247
262
|
"""Collected status for one local branch."""
|
|
@@ -268,6 +283,90 @@ def hyperlink(text: str, url: str, *, enabled: bool) -> str:
|
|
|
268
283
|
return f"\033]8;;{url}\033\\{underlined}\033]8;;\033\\"
|
|
269
284
|
|
|
270
285
|
|
|
286
|
+
def github_repository_url(remote_url: str) -> str:
|
|
287
|
+
"""Turn a GitHub clone URL into its canonical HTTPS repository page."""
|
|
288
|
+
scp_match = re.fullmatch(r"(?:[^@/:]+@)?([^/:]+):(.+)", remote_url)
|
|
289
|
+
if scp_match and "://" not in remote_url:
|
|
290
|
+
host, path = scp_match.groups()
|
|
291
|
+
else:
|
|
292
|
+
parsed = urlsplit(remote_url)
|
|
293
|
+
if parsed.scheme not in {"git", "http", "https", "ssh"}:
|
|
294
|
+
return ""
|
|
295
|
+
host, path = parsed.hostname or "", parsed.path
|
|
296
|
+
|
|
297
|
+
if host.lower() != "github.com":
|
|
298
|
+
return ""
|
|
299
|
+
path = path.strip("/")
|
|
300
|
+
if path.endswith(".git"):
|
|
301
|
+
path = path[:-4]
|
|
302
|
+
if not re.fullmatch(r"[-A-Za-z0-9_.]+/[-A-Za-z0-9_.]+", path):
|
|
303
|
+
return ""
|
|
304
|
+
return f"https://github.com/{path}"
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def repository_github_url(remotes: set[str]) -> str:
|
|
308
|
+
"""Find the repository page, preferring the conventional origin remote."""
|
|
309
|
+
for remote in sorted(remotes, key=lambda name: (name != "origin", name)):
|
|
310
|
+
remote_url = command_output("git", "remote", "get-url", remote) or ""
|
|
311
|
+
if url := github_repository_url(remote_url):
|
|
312
|
+
return url
|
|
313
|
+
return ""
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def github_repository(repository_url: str) -> GitHubRepository:
|
|
317
|
+
"""Read repository metadata from GitHub, retaining URL-derived identity on failure."""
|
|
318
|
+
owner, name = urlsplit(repository_url).path.strip("/").split("/", 1)
|
|
319
|
+
fallback = GitHubRepository(owner, name, repository_url)
|
|
320
|
+
result = command_result("gh", "repo", "view", f"{owner}/{name}", "--json", REPOSITORY_FIELDS)
|
|
321
|
+
if result is None or result[0] != 0:
|
|
322
|
+
return fallback
|
|
323
|
+
try:
|
|
324
|
+
payload = json.loads(result[1])
|
|
325
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
326
|
+
return fallback
|
|
327
|
+
if not isinstance(payload, dict):
|
|
328
|
+
return fallback
|
|
329
|
+
|
|
330
|
+
canonical_url = github_repository_url(str(payload.get("url", ""))) or repository_url
|
|
331
|
+
canonical_name = payload.get("nameWithOwner")
|
|
332
|
+
if isinstance(canonical_name, str) and re.fullmatch(
|
|
333
|
+
r"[-A-Za-z0-9_.]+/[-A-Za-z0-9_.]+", canonical_name
|
|
334
|
+
):
|
|
335
|
+
owner, name = canonical_name.split("/", 1)
|
|
336
|
+
canonical_url = f"https://github.com/{canonical_name}"
|
|
337
|
+
else:
|
|
338
|
+
owner, name = urlsplit(canonical_url).path.strip("/").split("/", 1)
|
|
339
|
+
|
|
340
|
+
visibility = payload.get("visibility")
|
|
341
|
+
visibility = (
|
|
342
|
+
visibility.lower()
|
|
343
|
+
if isinstance(visibility, str) and visibility.upper() in {"PUBLIC", "PRIVATE", "INTERNAL"}
|
|
344
|
+
else ""
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
parent_url = ""
|
|
348
|
+
parent = payload.get("parent")
|
|
349
|
+
if isinstance(parent, dict):
|
|
350
|
+
parent_url = github_repository_url(str(parent.get("url", "")))
|
|
351
|
+
parent_name = parent.get("nameWithOwner")
|
|
352
|
+
if (
|
|
353
|
+
not parent_url
|
|
354
|
+
and isinstance(parent_name, str)
|
|
355
|
+
and re.fullmatch(r"[-A-Za-z0-9_.]+/[-A-Za-z0-9_.]+", parent_name)
|
|
356
|
+
):
|
|
357
|
+
parent_url = f"https://github.com/{parent_name}"
|
|
358
|
+
|
|
359
|
+
return GitHubRepository(
|
|
360
|
+
owner=owner,
|
|
361
|
+
name=name,
|
|
362
|
+
url=canonical_url,
|
|
363
|
+
visibility=visibility,
|
|
364
|
+
is_fork=payload.get("isFork") is True,
|
|
365
|
+
parent_url=parent_url,
|
|
366
|
+
is_archived=payload.get("isArchived") is True,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
271
370
|
def pull_request_status(row: dict[str, object]) -> str:
|
|
272
371
|
"""Normalize GitHub's PR and review fields into a compact status."""
|
|
273
372
|
state = str(row.get("state", "")).upper()
|
|
@@ -491,6 +590,8 @@ def run_report(*, no_fetch: bool = False, plain: bool = False) -> int:
|
|
|
491
590
|
if not symref
|
|
492
591
|
}
|
|
493
592
|
remotes = set(git_output("remote").splitlines())
|
|
593
|
+
repo_url = repository_github_url(remotes)
|
|
594
|
+
github_repo = github_repository(repo_url) if repo_url else None
|
|
494
595
|
|
|
495
596
|
raw_branches = git_output(
|
|
496
597
|
"for-each-ref",
|
|
@@ -624,7 +725,23 @@ def run_report(*, no_fetch: bool = False, plain: bool = False) -> int:
|
|
|
624
725
|
behind_count = sum("behind" in branch.state_text for branch in branches)
|
|
625
726
|
|
|
626
727
|
print()
|
|
627
|
-
|
|
728
|
+
if github_repo:
|
|
729
|
+
owner_url = f"https://github.com/{github_repo.owner}"
|
|
730
|
+
rendered_repo = (
|
|
731
|
+
bold(hyperlink(github_repo.owner, owner_url, enabled=use_links))
|
|
732
|
+
+ dim(" / ")
|
|
733
|
+
+ bold(hyperlink(github_repo.name, github_repo.url, enabled=use_links))
|
|
734
|
+
)
|
|
735
|
+
if github_repo.visibility:
|
|
736
|
+
rendered_repo += dim(f" [{github_repo.visibility}]")
|
|
737
|
+
if github_repo.is_fork:
|
|
738
|
+
fork_label = hyperlink("[fork]", github_repo.parent_url, enabled=use_links)
|
|
739
|
+
rendered_repo += " " + dim(fork_label)
|
|
740
|
+
if github_repo.is_archived:
|
|
741
|
+
rendered_repo += " " + yellow("[archived]")
|
|
742
|
+
else:
|
|
743
|
+
rendered_repo = bold(repo_name)
|
|
744
|
+
print(rendered_repo + dim(" :: ") + bold(current or "detached HEAD"))
|
|
628
745
|
separator = glyph["separator"]
|
|
629
746
|
worktree = (
|
|
630
747
|
yellow(f"dirty: {f' {separator} '.join(worktree_parts)}") if dirty_lines else green("clean")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|