git-muster 0.4.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-muster
3
- Version: 0.4.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
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Classifier: Programming Language :: Python :: 3.14
15
15
  Classifier: Topic :: Software Development :: Version Control :: Git
16
- Requires-Dist: rich>=14,<15
16
+ Requires-Dist: rich>=14,<16
17
17
  Requires-Dist: typer>=0.27,<0.28
18
18
  Requires-Python: >=3.13, <3.15
19
19
  Project-URL: Homepage, https://github.com/olearydj/git-muster
@@ -57,7 +57,7 @@ BRANCH (4 local) REMOTE STATE WORKTREE UPDATED PULL REQUES
57
57
  - Conditional linked-worktree names and checkout paths.
58
58
  - Draft, open, approved, changes-requested, merged, and closed GitHub pull requests.
59
59
  - Clickable, underlined PR numbers in supported interactive terminals.
60
- - Responsive terminal layout plus stable ASCII output for logs and pipes.
60
+ - Responsive terminal layout plus stable plain output with ASCII interface glyphs for logs and pipes.
61
61
  - Read-only operation with `--no-fetch`; no branch switching, deletion, merging, rebasing, or pushing.
62
62
 
63
63
  ## Requirements
@@ -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
  |---|---|
@@ -131,7 +131,7 @@ Color, Unicode symbols, and clickable pull-request numbers appear when the repor
131
131
  | `FORCE_COLOR`, `CLICOLOR_FORCE` | Keep color when output is redirected to a file or pipe. |
132
132
  | `COLUMNS` | Overrides the detected terminal width used to fit columns. |
133
133
 
134
- `--plain` overrides all of them and emits pure ASCII, without color or terminal hyperlinks. Git Muster also falls back to those ASCII glyphs on its own whenever the destination stream cannot encode its symbols, so redirected output never fails to encode.
134
+ `--plain` overrides all of them and uses ASCII interface glyphs without color or terminal hyperlinks. Repository-provided text, including branch, remote, and worktree names, is preserved and may contain Unicode. Git Muster also falls back to the ASCII glyph set on its own whenever the destination stream cannot encode its symbols, so its built-in decorations never cause an encoding failure.
135
135
 
136
136
  ## Reading the report
137
137
 
@@ -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, so 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 and names the reason pull-request state is missing.
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
 
@@ -33,7 +33,7 @@ BRANCH (4 local) REMOTE STATE WORKTREE UPDATED PULL REQUES
33
33
  - Conditional linked-worktree names and checkout paths.
34
34
  - Draft, open, approved, changes-requested, merged, and closed GitHub pull requests.
35
35
  - Clickable, underlined PR numbers in supported interactive terminals.
36
- - Responsive terminal layout plus stable ASCII output for logs and pipes.
36
+ - Responsive terminal layout plus stable plain output with ASCII interface glyphs for logs and pipes.
37
37
  - Read-only operation with `--no-fetch`; no branch switching, deletion, merging, rebasing, or pushing.
38
38
 
39
39
  ## Requirements
@@ -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
  |---|---|
@@ -107,7 +107,7 @@ Color, Unicode symbols, and clickable pull-request numbers appear when the repor
107
107
  | `FORCE_COLOR`, `CLICOLOR_FORCE` | Keep color when output is redirected to a file or pipe. |
108
108
  | `COLUMNS` | Overrides the detected terminal width used to fit columns. |
109
109
 
110
- `--plain` overrides all of them and emits pure ASCII, without color or terminal hyperlinks. Git Muster also falls back to those ASCII glyphs on its own whenever the destination stream cannot encode its symbols, so redirected output never fails to encode.
110
+ `--plain` overrides all of them and uses ASCII interface glyphs without color or terminal hyperlinks. Repository-provided text, including branch, remote, and worktree names, is preserved and may contain Unicode. Git Muster also falls back to the ASCII glyph set on its own whenever the destination stream cannot encode its symbols, so its built-in decorations never cause an encoding failure.
111
111
 
112
112
  ## Reading the report
113
113
 
@@ -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, so 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 and names the reason pull-request state is missing.
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
 
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "git-muster"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  description = "See every local Git branch and what needs attention"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.15"
@@ -27,7 +27,7 @@ classifiers = [
27
27
  "Topic :: Software Development :: Version Control :: Git",
28
28
  ]
29
29
  dependencies = [
30
- "rich>=14,<15",
30
+ "rich>=14,<16",
31
31
  "typer>=0.27,<0.28",
32
32
  ]
33
33
 
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "git-muster"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  description = "See every local Git branch and what needs attention"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.15"
@@ -21,7 +21,7 @@ classifiers = [
21
21
  "Programming Language :: Python :: 3.14",
22
22
  "Topic :: Software Development :: Version Control :: Git",
23
23
  ]
24
- dependencies = ["rich>=14,<15", "typer>=0.27,<0.28"]
24
+ dependencies = ["rich>=14,<16", "typer>=0.27,<0.28"]
25
25
 
26
26
  [project.scripts]
27
27
  git-muster = "git_muster.cli:entrypoint"
@@ -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
@@ -38,7 +39,7 @@ EPILOG = """
38
39
  Use the remote-tracking data already on disk.
39
40
 
40
41
  [cyan]git muster --plain[/cyan]
41
- Emit stable ASCII output without color for logs and pipes.
42
+ Emit stable output with ASCII decorations and no color for logs and pipes.
42
43
 
43
44
  [bold]Linked worktrees[/bold]
44
45
 
@@ -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()
@@ -315,7 +414,10 @@ def github_pull_requests() -> tuple[list[dict[str, object]], str | None]:
315
414
  return [], "unusable"
316
415
  if not isinstance(parsed, list):
317
416
  return [], "unusable"
318
- return [row for row in parsed if isinstance(row, dict)], None
417
+ rows = [row for row in parsed if isinstance(row, dict)]
418
+ if parsed and not rows:
419
+ return [], "unusable"
420
+ return rows, None
319
421
 
320
422
 
321
423
  def parse_track(track: str) -> tuple[int, int] | None:
@@ -488,6 +590,8 @@ def run_report(*, no_fetch: bool = False, plain: bool = False) -> int:
488
590
  if not symref
489
591
  }
490
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
491
595
 
492
596
  raw_branches = git_output(
493
597
  "for-each-ref",
@@ -621,7 +725,23 @@ def run_report(*, no_fetch: bool = False, plain: bool = False) -> int:
621
725
  behind_count = sum("behind" in branch.state_text for branch in branches)
622
726
 
623
727
  print()
624
- print(bold(repo_name) + dim(" :: ") + bold(current or "detached HEAD"))
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"))
625
745
  separator = glyph["separator"]
626
746
  worktree = (
627
747
  yellow(f"dirty: {f' {separator} '.join(worktree_parts)}") if dirty_lines else green("clean")
@@ -747,7 +867,10 @@ def main(
747
867
  bool,
748
868
  typer.Option(
749
869
  "--plain",
750
- help="Disable color and Unicode symbols for stable logs and pipes.",
870
+ help=(
871
+ "Disable color, hyperlinks, and Unicode interface symbols for stable logs and "
872
+ "pipes. Repository-provided text is preserved."
873
+ ),
751
874
  rich_help_panel="Report options",
752
875
  ),
753
876
  ] = False,
File without changes