core_blame.py file-level

at sha256:c · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:c docs(KD-STAGING): sync governance after KD-6b DONE Mirror workspace go… · aaronrene · Jul 10, 2026
1 """``muse blame`` (core VCS) — line-level attribution for any text file.
2
3 Shows which commit last modified each line of a tracked text file — the
4 universal, domain-agnostic counterpart to ``muse code blame`` (symbol-level)
5 and ``muse midi note-blame`` (bar-level).
6
7 Usage::
8
9 muse blame README.md
10 muse blame --ref v1.0.0 docs/design.md
11 muse blame --porcelain state/config.toml # machine-readable output
12
13 Output format (default)::
14
15 <sha12> (<author> <date>) 1 line content here
16
17 The ``--porcelain`` flag emits one JSON object per line for pipeline use.
18
19 Note: this command lives as ``muse blame`` at the Tier 2 porcelain level.
20 Domain-specific blame (symbols, notes) lives under ``muse code`` and
21 ``muse midi`` respectively.
22 """
23
24 from __future__ import annotations
25
26 import argparse
27 import json
28 import logging
29 import pathlib
30 import sys
31
32
33 from muse.core.blame import blame_file
34 from muse.core.errors import ExitCode
35 from muse.core.repo import read_repo_id, require_repo
36 from muse.core.store import get_head_commit_id, read_current_branch, resolve_commit_ref
37 from muse.core.validation import clamp_int, sanitize_display
38
39 logger = logging.getLogger(__name__)
40
41
42 def _read_branch(root: pathlib.Path) -> str:
43 return read_current_branch(root)
44
45
46
47 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
48 """Register the blame subcommand."""
49 parser = subparsers.add_parser(
50 "blame",
51 help="Show which commit last modified each line of a text file.",
52 description=__doc__,
53 formatter_class=argparse.RawDescriptionHelpFormatter,
54 )
55 parser.add_argument(
56 "file",
57 help="Workspace-relative path to the file to blame.",
58 )
59 parser.add_argument(
60 "--ref", default=None,
61 help="Commit ref (SHA, branch, tag) to blame at (default: HEAD).",
62 )
63 parser.add_argument(
64 "--porcelain", action="store_true",
65 help="Emit one JSON object per line (machine-readable).",
66 )
67 parser.add_argument(
68 "--short", type=int, default=12, metavar="N",
69 help="Number of characters to show for each commit SHA (default: 12).",
70 )
71 parser.set_defaults(func=run)
72
73
74 def run(args: argparse.Namespace) -> None:
75 """Show which commit last modified each line of a text file.
76
77 Walks the commit history backwards and attributes each line to the
78 oldest commit that introduced or last changed it.
79
80 Examples::
81
82 muse blame README.md
83 muse blame --ref v1.0.0 src/main.py
84 muse blame --porcelain config.toml | jq '.commit_id'
85 """
86 file: str = args.file
87 ref: str | None = args.ref
88 porcelain: bool = args.porcelain
89 short: int = clamp_int(args.short, 1, 1000, 'short')
90
91 root = require_repo()
92 branch = _read_branch(root)
93 repo_id = read_repo_id(root)
94
95 if ref is None:
96 commit_id = get_head_commit_id(root, branch)
97 if not commit_id:
98 print("❌ No commits yet on this branch.")
99 raise SystemExit(ExitCode.USER_ERROR)
100 else:
101 commit = resolve_commit_ref(root, repo_id, branch, ref)
102 if commit is None:
103 print(f"❌ Ref '{sanitize_display(ref)}' not found.")
104 raise SystemExit(ExitCode.USER_ERROR)
105 commit_id = commit.commit_id
106
107 lines = blame_file(root, file, commit_id)
108 if lines is None:
109 print(f"❌ File '{sanitize_display(file)}' not found at {commit_id[:short]}.")
110 raise SystemExit(ExitCode.USER_ERROR)
111
112 if not lines:
113 print(f"(empty file '{sanitize_display(file)}')")
114 return
115
116 if porcelain:
117 for bl in lines:
118 print(json.dumps({
119 "lineno": bl.lineno,
120 "commit_id": bl.commit_id,
121 "author": bl.author,
122 "committed_at": bl.committed_at,
123 "message": bl.message,
124 "content": bl.content,
125 }))
126 return
127
128 # Human-readable output: align columns.
129 sha_w = short
130 author_w = min(20, max((len(bl.author) for bl in lines), default=0))
131 date_w = 10
132 lineno_w = len(str(len(lines)))
133
134 for bl in lines:
135 sha = bl.commit_id[:sha_w]
136 author = bl.author[:author_w].ljust(author_w)
137 date = bl.committed_at[:date_w]
138 lineno = str(bl.lineno).rjust(lineno_w)
139 content = sanitize_display(bl.content)
140 print(f"{sha} ({sanitize_display(author)} {date}) {lineno} {sanitize_display(content)}")