392 """Show git status with Option C compliance annotations."""
393 func =
"git_status_cmd"
394 root = get_project_root(ctx)
397 f
"root={root} json={json_mode} no_check={no_check} scope={scope!r}",
401 from oct.core.git
import (
409 if not is_git_repo(root):
410 click.echo(
"Not a git repository.", err=
True)
418 repo_root = get_repo_root(root)
419 _dbg(
"GIT", func, f
"repo_root={repo_root}", 3)
427 f
"workspace_mode={in_workspace_mode} "
428 f
"subprojects={(len(workspace.subprojects) if workspace else 0)}",
435 if isinstance(octrc, dict):
436 git_cfg = octrc.get(
"git")
437 if isinstance(git_cfg, dict):
438 raw = git_cfg.get(
"status_default_scope")
439 if isinstance(raw, str)
and raw
in (
"project",
"repo",
"auto"):
444 root_resolved = root.resolve()
445 repo_root_resolved = repo_root.resolve()
448 effective_scope = scope
451 "project" if root_resolved == repo_root_resolved
else "repo"
453 _dbg(
"GIT", func, f
"effective_scope={effective_scope}", 3)
455 branch = current_branch(repo_root)
456 detached = is_detached_head(repo_root)
458 staged = git_staged_files(repo_root)
464 def _partition(files: list[Path]) -> tuple[list[Path], list[Path]]:
465 in_proj: list[Path] = []
466 out_proj: list[Path] = []
472 return in_proj, out_proj
474 staged_in, staged_out = _partition(staged)
475 modified_in, modified_out = _partition(modified)
476 untracked_in, untracked_out = _partition(untracked)
482 staged_in_set = {p.resolve()
for p
in staged_in}
483 modified_in = [p
for p
in modified_in
if p.resolve()
not in staged_in_set]
484 staged_out_set = {p.resolve()
for p
in staged_out}
485 modified_out = [p
for p
in modified_out
if p.resolve()
not in staged_out_set]
488 show_out_of_project = effective_scope ==
"repo"
491 def annotate(files: list[Path]) -> list[dict]:
494 rel = to_project_relative(path, root)
495 if path.suffix !=
".py":
497 "path": rel,
"status":
"non_python",
"issues": [],
501 "path": rel,
"status":
"unchecked",
"issues": [],
507 "status":
"ok" if compliant
else "violations",
512 def annotate_out(files: list[Path]) -> list[dict]:
513 """Out-of-project files: render with repo-relative path, unchecked."""
517 rel = path.resolve().relative_to(repo_root_resolved).as_posix()
519 rel = path.as_posix()
520 out.append({
"path": rel,
"status":
"unchecked",
"issues": []})
523 staged_ann = annotate(staged_in)
524 modified_ann = annotate(modified_in)
526 {
"path": to_project_relative(p, root),
527 "status":
"untracked",
"issues": []}
528 for p
in untracked_in
531 out_of_project_ann: list[dict] = []
532 if show_out_of_project:
533 for entry
in annotate_out(staged_out):
534 entry[
"section"] =
"staged"
535 out_of_project_ann.append(entry)
536 for entry
in annotate_out(modified_out):
537 entry[
"section"] =
"modified"
538 out_of_project_ann.append(entry)
539 for path
in untracked_out:
541 rel = path.resolve().relative_to(repo_root_resolved).as_posix()
543 rel = path.as_posix()
544 out_of_project_ann.append({
545 "path": rel,
"status":
"untracked",
546 "issues": [],
"section":
"untracked",
550 workspace_view: dict |
None =
None
551 if in_workspace_mode
and workspace
is not None and workspace.subprojects:
555 modified=modified_in,
556 untracked=untracked_in,
561 record = ctx.obj.get(
"_audit_record",
AuditRecord())
562 record.timestamp = record.timestamp
or _utc_now_iso()
563 record.command =
"oct git status"
564 record.branch = branch
if not detached
else None
565 record.staged_files = len(staged_in)
566 record.checks_passed =
True
568 ctx.obj[
"_audit_record"] = record
573 "branch": branch
if not detached
else None,
574 "detached": detached,
577 "scope": effective_scope,
578 "staged": staged_ann,
579 "modified": modified_ann,
580 "untracked": untracked_ann,
581 "out_of_project": out_of_project_ann,
582 "workspace_view": workspace_view,
584 click.echo(json.dumps(output, indent=2, ensure_ascii=
False))
589 click.echo(
"HEAD detached")
591 click.echo(f
"On branch {branch}")
592 if ahead > 0
or behind > 0:
595 parts.append(f
"ahead by {ahead}")
597 parts.append(f
"behind by {behind}")
598 click.echo(f
" {', '.join(parts)}")
601 def _render_section(label: str, items: list[dict]):
604 click.echo(f
"{label} ({len(items)} file{'s' if len(items) != 1 else ''}):")
606 status = item[
"status"]
610 elif status ==
"violations":
611 issues_str =
" (" +
", ".join(item[
"issues"]) +
")" if item[
"issues"]
else ""
612 click.echo(f
" [!!] {path}{issues_str}")
614 elif status ==
"non_python":
616 elif status ==
"unchecked":
620 click.echo(f
"{badge}{path}")
623 if workspace_view
is not None:
627 *(sp.name
for sp
in workspace.subprojects),
630 bucket = workspace_view.get(bucket_name, {})
631 staged_b = bucket.get(
"staged", [])
632 modified_b = bucket.get(
"modified", [])
633 untracked_b = bucket.get(
"untracked", [])
634 if not (staged_b
or modified_b
or untracked_b):
636 click.echo(f
"=== {bucket_name} ===")
637 _render_section(
"Staged", staged_b)
638 _render_section(
"Modified", modified_b)
639 _render_section(
"Untracked", untracked_b)
641 _render_section(
"Staged", staged_ann)
642 _render_section(
"Modified", modified_ann)
643 _render_section(
"Untracked", untracked_ann)
644 if show_out_of_project
and out_of_project_ann:
646 f
"Other repo files ({len(out_of_project_ann)} "
647 f
"file{'s' if len(out_of_project_ann) != 1 else ''}, "
648 f
"outside OCT project):"
650 for item
in out_of_project_ann:
651 section = item.get(
"section",
"?")
652 click.echo(f
" [{section[:3]}] {item['path']}")
661@git.command(name="check")
663 "--staged-only", is_flag=
True,
664 help=
"Check only staged (index) files.",
667 "--all",
"check_all", is_flag=
True,
668 help=
"Check every Python file in the project.",
671 "--include-tests", is_flag=
True,
672 help=
"Also run pytest (exit code 4 on failure).",
675 "--fix", is_flag=
True,
676 help=
"Attempt auto-fix of lint/format violations, then re-check.",
680 type=click.Choice([
"proto",
"compact",
"strict"], case_sensitive=
False),
682 help=
"Override the lint profile.",
684@click.option("--json", "json_mode", is_flag=True, help="Emit JSON output.")
686 "--sandbox", is_flag=
True, default=
False,
687 help=
"OI-517: run pytest (when --include-tests) under the MCP sandbox "
688 "environment so PYTHONPATH and secret-named env vars are stripped.",
691@audited("oct git check")
692def git_check_cmd(ctx, staged_only, check_all, include_tests, fix, profile, json_mode, sandbox):
693 """Run the unified quality gate: lint + format + secrets."""
694 func =
"git_check_cmd"
695 root = get_project_root(ctx)
696 _dbg(
"GIT", func, f
"root={root}", 2)
698 from oct.core.git
import current_branch, is_git_repo
699 from oct.git.policy import apply_profile_policy, resolve_effective_profile
702 if not is_git_repo(root):
703 click.echo(
"Not a git repository.", err=
True)
716 branch = current_branch(root)
718 effective_profile = resolve_effective_profile(branch, octrc, profile)
721 f
"scope={scope} profile={effective_profile} fix={fix}",
726 result = run_quality_gate(
729 profile=effective_profile,
730 include_tests=include_tests,
736 exit_code = apply_profile_policy(effective_profile, result)
737 result.exit_code = exit_code
740 record = ctx.obj.get(
"_audit_record",
AuditRecord())
741 record.timestamp = record.timestamp
or _utc_now_iso()
742 record.command =
"oct git check"
743 record.branch = branch
744 record.staged_files = result.files_checked
745 record.checks_passed = (exit_code == 0)
746 record.secrets_detected = bool(result.secrets_findings)
747 record.lint_violations = result.lint_violations
748 record.format_violations = result.format_violations
749 record.exit_code = exit_code
750 ctx.obj[
"_audit_record"] = record
756 "profile": effective_profile,
757 "files_checked": result.files_checked,
758 "lint_violations": result.lint_violations,
759 "format_violations": result.format_violations,
760 "secrets_findings": [
761 {
"path": s[0],
"line": s[1],
"reason": s[2]}
762 for s
in result.secrets_findings
764 "test_failures": result.test_failures,
765 "exit_code": exit_code,
766 "duration_ms": result.duration_ms,
768 click.echo(json.dumps(output, indent=2, ensure_ascii=
False))
773 click.echo(f
"Quality gate: scope={scope}, profile={effective_profile}")
774 click.echo(f
" Files checked: {result.files_checked}")
775 click.echo(f
" Lint violations: {result.lint_violations}")
776 click.echo(f
" Format issues: {result.format_violations}")
777 click.echo(f
" Secrets findings: {len(result.secrets_findings)}")
779 click.echo(f
" Test failures: {result.test_failures}")
780 click.echo(f
" Duration: {result.duration_ms} ms")
783 if result.secrets_findings:
784 click.echo(
"SECRETS DETECTED (always blocking):", err=
True)
785 for path_str, line, reason
in result.secrets_findings:
786 loc = f
"{path_str}:{line}" if line
else path_str
787 click.echo(f
" {loc} — {reason}", err=
True)
791 click.echo(
"All checks passed.")
794 1:
"Lint violations",
795 2:
"Format violations",
796 3:
"Lint + format violations",
798 5:
"Secrets detected",
801 f
"FAILED (exit {exit_code}): {labels.get(exit_code, 'unknown')}",
1062 ctx, messages, force_commit, no_verify, profile, json_mode,
1063 ignore_unstaged_mutation, extra_args,
1065 """Commit staged changes with quality gate enforcement."""
1066 func =
"git_commit_cmd"
1067 root = get_project_root(ctx)
1070 message =
"\n\n".join(m
for m
in messages
if m
is not None)
1071 _dbg(
"GIT", func, f
"root={root} force={force_commit} -m count={len(messages)}", 2)
1073 from oct.core.git
import current_branch, git_run, is_git_repo
1075 analyse_diagnostics_impact,
1076 format_validation_errors,
1081 if not is_git_repo(root):
1082 click.echo(
"Not a git repository.", err=
True)
1086 branch = current_branch(root)
1090 pre = pre_commit_checks(
1094 force_commit=force_commit,
1098 effective_profile = pre.effective_profile
1100 click.echo(pre.pb_advisory, err=
True)
1106 root, branch, message, ctx, json_mode,
1108 if auto_branch_name
is None:
1112 branch = auto_branch_name
1114 pre = pre_commit_checks(
1115 project_root=root, branch=branch, message=message,
1116 force_commit=force_commit, profile=profile, octrc=octrc,
1118 effective_profile = pre.effective_profile
1123 ctx, exit_code=pre.exit_code, message=message,
1124 branch=branch, profile=effective_profile,
1127 elif pre.exit_code == 3:
1128 click.echo(format_validation_errors(pre.errors), err=
True)
1129 elif pre.exit_code == 4:
1130 click.echo(
"Nothing to commit (no staged files).", err=
True)
1133 ctx, branch=branch, exit_code=pre.exit_code,
1134 checks_passed=
False, staged_count=len(pre.staged_files),
1136 ctx.exit(pre.exit_code)
1139 staged = pre.staged_files
1143 pre_snapshot: dict[str, str] = {}
1144 if observe_mode !=
"off":
1146 from oct.core.git
import get_repo_root
1148 repo_root_for_obs = get_repo_root(root)
1150 str(p.resolve().relative_to(repo_root_for_obs.resolve()))
1155 pre_snapshot = snapshot_unstaged_tracked(
1156 repo_root_for_obs, staged_set=staged_rel,
1158 except Exception
as exc:
1161 f
"observer snapshot failed: {exc!r}; continuing without",
1165 observe_mode =
"off"
1174 per_sp_results: dict[str,
"QualityGateResult"] = {}
1175 per_sp_meta: dict[str, dict] = {}
1177 commit_in_workspace_mode
1178 and commit_workspace
is not None
1179 and bool(commit_workspace.subprojects)
1185 for sp
in commit_workspace.subprojects:
1186 sp_files = groups.get(sp.name, [])
1191 sp, sp_octrc, profile, branch,
1193 per_sp_results[sp.name] = run_quality_gate_on_files(
1194 project_root=sp.path, files=sp_files,
1195 profile=sp_profile, include_tests=
False, fix=
False,
1197 per_sp_meta[sp.name] = {
1198 "profile": sp_profile,
"files": len(sp_files),
1200 ws_files = groups.get(
"workspace", [])
1203 profile
or commit_workspace.workspace_profile
1204 or effective_profile
1206 per_sp_results[
"workspace"] = run_quality_gate_on_files(
1207 project_root=commit_workspace.root, files=ws_files,
1208 profile=ws_profile, include_tests=
False, fix=
False,
1210 per_sp_meta[
"workspace"] = {
1211 "profile": ws_profile,
"files": len(ws_files),
1216 if not force_commit:
1219 apply_profile_policy(per_sp_meta[name][
"profile"], r)
1220 for name, r
in per_sp_results.items()
1226 elif not force_commit:
1227 result = run_quality_gate(
1230 profile=effective_profile,
1231 include_tests=
False,
1234 gate_exit = apply_profile_policy(effective_profile, result)
1237 result = run_quality_gate(
1240 profile=effective_profile,
1241 include_tests=
False,
1247 if result.secrets_findings:
1250 ctx, exit_code=2, message=message, branch=branch,
1251 profile=effective_profile,
1252 errors=[f
"Secrets detected in {len(result.secrets_findings)} location(s)."],
1253 secrets=result.secrets_findings,
1256 click.echo(
"SECRETS DETECTED (non-bypassable):", err=
True)
1257 for path_str, line, reason
in result.secrets_findings:
1258 loc = f
"{path_str}:{line}" if line
else path_str
1259 click.echo(f
" {loc} — {reason}", err=
True)
1261 ctx, branch=branch, exit_code=2,
1262 checks_passed=
False, staged_count=len(staged),
1263 secrets=
True, lint_v=result.lint_violations,
1264 format_v=result.format_violations,
1270 if gate_exit != 0
and not force_commit:
1273 ctx, exit_code=1, message=message, branch=branch,
1274 profile=effective_profile,
1275 errors=[f
"Quality gate failed (exit {gate_exit})."],
1279 f
"Quality gate failed (exit {gate_exit}). "
1280 f
"Use --force to skip lint/format checks.",
1284 ctx, branch=branch, exit_code=1,
1285 checks_passed=
False, staged_count=len(staged),
1286 lint_v=result.lint_violations,
1287 format_v=result.format_violations,
1292 if force_commit
and gate_exit != 0:
1294 f
"Warning: quality gate would have failed (exit {gate_exit}), "
1295 f
"bypassed with --force.",
1301 for rel, size
in oversized:
1302 mb = size / (1024 * 1024)
1304 f
"Warning: {rel} is {mb:.1f} MB. Consider .gitignore or Git LFS.",
1314 "Skipping git hooks (--no-verify). "
1315 "Note: oct git commit still runs its own quality checks.",
1320 if observe_mode !=
"off" and pre_snapshot:
1322 root, pre_snapshot, staged,
1323 stage_label=
"quality gate",
1325 if gate_mutations
and observe_mode ==
"block":
1327 ctx, mutations=gate_mutations,
1328 message=message, branch=branch,
1329 profile=effective_profile, json_mode=json_mode,
1330 staged_count=len(staged),
1335 git_args = [
"commit",
"-m", message]
1337 git_args.append(
"--no-verify")
1338 git_args.extend(extra_args)
1341 git_run(git_args, cwd=root)
1342 except Exception
as exc:
1346 ctx, exit_code=4, message=message, branch=branch,
1347 profile=effective_profile,
1348 errors=[f
"git commit failed: {err_msg}"],
1351 click.echo(f
"git commit failed: {err_msg}", err=
True)
1353 ctx, branch=branch, exit_code=4,
1354 checks_passed=
False, staged_count=len(staged),
1360 if observe_mode !=
"off" and pre_snapshot:
1362 root, pre_snapshot, staged,
1363 stage_label=
"git-hook",
1365 if hook_mutations
and observe_mode ==
"block":
1367 ctx, mutations=hook_mutations,
1368 message=message, branch=branch,
1369 profile=effective_profile, json_mode=json_mode,
1370 staged_count=len(staged),
1376 from oct.core.git
import git_run
as _gr
1378 [
"diff",
"HEAD~1..HEAD"], cwd=root, check=
False,
1380 impact = analyse_diagnostics_impact(diff_proc.stdout)
1382 click.echo(
"Diagnostics Impact:", err=
True)
1384 click.echo(f
" {line}", err=
True)
1391 ctx, exit_code=0, message=message, branch=branch,
1392 profile=effective_profile, errors=[],
1395 click.echo(
"Commit successful.")
1399 if use_fanout
else None
1402 ctx, branch=branch, exit_code=0,
1403 checks_passed=
True, staged_count=len(staged),
1404 per_subproject_results=psr_audit,
1746 """Manage branches with Option C naming conventions."""
1747 func =
"git_branch_cmd"
1748 root = get_project_root(ctx)
1749 _dbg(
"GIT", func, f
"root={root} create={create_name!r} switch={switch_name!r}", 2)
1751 from oct.core.git
import (
1754 has_uncommitted_changes,
1760 if not is_git_repo(root):
1761 click.echo(
"Not a git repository.", err=
True)
1769 if create_name
and switch_name:
1770 msg =
"Error: --create and --switch are mutually exclusive."
1771 click.echo(msg, err=
True)
1778 if not create_name
and not switch_name:
1779 msg =
"Use --create NAME or --switch NAME. Branch listing coming soon."
1780 click.echo(msg, err=
True)
1789 valid, val_msg = validate_branch_name(create_name, octrc)
1791 click.echo(f
"Error: {val_msg}", err=
True)
1794 operation=
"create", branch=create_name,
1795 exit_code=1, errors=[val_msg],
1801 if branch_exists(root, create_name):
1802 msg = f
"Error: branch '{create_name}' already exists."
1803 click.echo(msg, err=
True)
1806 operation=
"create", branch=create_name,
1807 exit_code=1, errors=[msg],
1814 create_branch(root, create_name)
1815 except Exception
as exc:
1816 msg = f
"Git error: {exc}"
1817 click.echo(msg, err=
True)
1820 operation=
"create", branch=create_name,
1821 exit_code=2, errors=[msg],
1829 operation=
"create", branch=create_name, exit_code=0,
1832 click.echo(f
"Created branch '{create_name}' (not switched to it).")
1838 if not branch_exists(root, switch_name):
1839 msg = f
"Error: branch '{switch_name}' does not exist."
1840 click.echo(msg, err=
True)
1843 operation=
"switch", branch=switch_name,
1844 exit_code=1, errors=[msg],
1850 if has_uncommitted_changes(root):
1852 "Error: uncommitted changes would be lost. "
1853 "Commit or stash them first."
1855 click.echo(msg, err=
True)
1858 operation=
"switch", branch=switch_name,
1859 exit_code=1, errors=[msg],
1866 switch_branch(root, switch_name)
1867 except Exception
as exc:
1868 msg = f
"Git error: {exc}"
1869 click.echo(msg, err=
True)
1872 operation=
"switch", branch=switch_name,
1873 exit_code=2, errors=[msg],
1881 operation=
"switch", branch=switch_name, exit_code=0,
1884 click.echo(f
"Switched to branch '{switch_name}'.")
2032 ctx, paths, patch_mode, update_mode, all_mode, dry_run, json_mode,
2034 """Stage files for commit (whole-file or interactive hunk)."""
2035 func =
"git_add_cmd"
2036 root = get_project_root(ctx)
2039 f
"root={root} paths={paths} patch={patch_mode} "
2040 f
"update={update_mode} all={all_mode} dry_run={dry_run}",
2044 from oct.core.git
import (
2048 stage_paths_interactive,
2051 if not is_git_repo(root):
2052 click.echo(
"Not a git repository.", err=
True)
2057 repo_root = get_repo_root(root)
2061 if patch_mode
and (update_mode
or all_mode):
2062 msg =
"Error: --patch cannot be combined with --update or --all."
2063 click.echo(msg, err=
True)
2066 operation=
"patch", paths_staged=[], paths_skipped=[],
2067 post_check=
None, exit_code=1, errors=[msg],
2073 if update_mode
and all_mode:
2074 msg =
"Error: --update and --all are mutually exclusive."
2075 click.echo(msg, err=
True)
2078 operation=
"add", paths_staged=[], paths_skipped=[],
2079 post_check=
None, exit_code=1, errors=[msg],
2086 root_resolved = root.resolve()
2087 skipped: list[dict] = []
2088 resolved_paths: list[Path] = []
2090 candidate = (Path.cwd() / raw).resolve() \
2091 if not Path(raw).is_absolute()
else Path(raw).resolve()
2094 "path": str(raw),
"reason":
"outside project scope",
2097 resolved_paths.append(candidate)
2099 if skipped
and not json_mode:
2100 for entry
in skipped:
2102 f
"Skipping {entry['path']} ({entry['reason']}).",
2107 if not resolved_paths
and not (update_mode
or all_mode
or patch_mode):
2109 "Error: no paths to stage. Provide PATHS, "
2110 "or use --update / --all / --patch."
2112 click.echo(msg, err=
True)
2115 operation=
"add", paths_staged=[], paths_skipped=skipped,
2116 post_check=
None, exit_code=1, errors=[msg],
2124 operation =
"dry-run"
2126 to_project_relative(p, root)
for p
in resolved_paths
2128 if update_mode
or all_mode:
2130 from oct.core.git
import git_run
as _git_run
2131 argv = [
"status",
"--porcelain"]
2132 proc = _git_run(argv, cwd=repo_root)
2133 for line
in proc.stdout.splitlines():
2138 rel = line[3:].strip()
2139 if update_mode
and xy[1]
in (
" ",
""):
2142 if update_mode
and xy[1] ==
"?":
2144 full = (repo_root / rel).resolve()
2147 rel_strings.append(to_project_relative(full, root))
2150 operation=operation, paths_staged=rel_strings,
2151 paths_skipped=skipped, post_check=
None, exit_code=0,
2154 click.echo(
"Would stage:")
2155 for s
in rel_strings:
2157 _add_audit(ctx, exit_code=0, staged_count=len(rel_strings))
2164 "Note: --patch is interactive; --json output is limited.",
2168 rc = stage_paths_interactive(repo_root, resolved_paths)
2169 except Exception
as exc:
2170 msg = f
"Git error: {exc}"
2171 click.echo(msg, err=
True)
2174 operation=
"patch", paths_staged=[],
2175 paths_skipped=skipped, post_check=
None,
2176 exit_code=2, errors=[msg],
2188 operation=
"patch", paths_staged=[],
2189 paths_skipped=skipped, post_check=post, exit_code=0,
2203 from oct.core.git
import git_staged_files
2204 pre_index = {p.resolve()
for p
in git_staged_files(repo_root)}
2206 git_warning: str |
None =
None
2209 repo_root, resolved_paths,
2210 update=update_mode, all_=all_mode,
2212 except Exception
as exc:
2216 post_index = {p.resolve()
for p
in git_staged_files(repo_root)}
2217 newly_staged = post_index - pre_index
2219 git_warning = str(exc)
2221 f
"Note: git emitted a warning during staging but "
2222 f
"{len(newly_staged)} file(s) were staged successfully:\n"
2227 msg = f
"Git error: {exc}"
2228 click.echo(msg, err=
True)
2231 operation=
"add", paths_staged=[],
2232 paths_skipped=skipped,
2233 post_check=
None, exit_code=2, errors=[msg],
2240 staged_now = git_staged_files(repo_root)
2244 staged_strings = [to_project_relative(p, root)
for p
in staged_within]
2246 operation =
"all" if all_mode
else "update" if update_mode
else "add"
2251 operation=operation, paths_staged=staged_strings,
2252 paths_skipped=skipped, post_check=post, exit_code=0,
2256 for p
in resolved_paths:
2257 click.echo(f
"Staged {to_project_relative(p, root)}")
2260 f
"Staged {len(staged_strings)} tracked modified file(s).",
2264 f
"Staged {len(staged_strings)} file(s) (including untracked).",
2268 _add_audit(ctx, exit_code=0, staged_count=len(staged_strings))
2438 ctx, paths, staged_mode, all_mode, skip_prompt, json_mode,
2440 """Restore working-tree edits or unstage paths.
2442 Without ``--staged``: discards unstaged working-tree edits in
2443 *paths*, reverting them to the index (i.e. ``git restore <paths>``).
2445 With ``--staged``: removes *paths* from the index without touching
2446 the working tree (i.e. ``git restore --staged <paths>``).
2448 With ``--all``: scope is every modified file inside the OCT
2449 project root. ``--staged --all`` requires confirmation (irreversible
2450 destruction of all staged content); pass ``--yes`` to skip.
2452 func =
"git_restore_cmd"
2453 root = get_project_root(ctx)
2456 f
"root={root} paths={paths} staged={staged_mode} "
2457 f
"all={all_mode} json={json_mode}",
2461 from oct.core.git
import (
2462 get_repo_root, is_git_repo, restore_paths,
2465 if not is_git_repo(root):
2466 click.echo(
"Not a git repository.", err=
True)
2471 repo_root = get_repo_root(root)
2472 root_resolved = root.resolve()
2474 if not paths
and not all_mode:
2475 msg =
"Error: provide PATHS or use --all."
2476 click.echo(msg, err=
True)
2478 click.echo(json.dumps({
2479 "operation":
"restore",
2480 "paths_restored": [],
"exit_code": 1,
"errors": [msg],
2487 skipped: list[dict] = []
2488 resolved_paths: list[Path] = []
2490 candidate = Path(raw)
2491 if not candidate.is_absolute():
2492 candidate = (Path.cwd() / candidate).resolve()
2494 candidate = candidate.resolve()
2497 {
"path": str(raw),
"reason":
"outside project scope"},
2500 resolved_paths.append(candidate)
2502 if skipped
and not json_mode:
2503 for entry
in skipped:
2505 f
"Skipping {entry['path']} ({entry['reason']}).",
2511 from oct.core.git
import git_run
as _gr
2513 argv = [
"diff",
"--cached",
"--name-only"]
2515 argv = [
"diff",
"--name-only"]
2516 proc = _gr(argv, cwd=repo_root)
2517 for line
in proc.stdout.splitlines():
2521 full = (repo_root / rel).resolve()
2523 resolved_paths.append(full)
2525 if not resolved_paths:
2527 "Nothing to restore (no in-scope paths matched)."
2529 "Nothing to restore."
2532 click.echo(json.dumps({
2533 "operation":
"restore",
2534 "paths_restored": [],
"exit_code": 0,
2535 "skipped": skipped,
"errors": [],
2544 if staged_mode
and all_mode
and not skip_prompt
and not json_mode:
2546 f
"About to UNSTAGE {len(resolved_paths)} file(s). This "
2547 f
"discards every staged change in the project scope and "
2548 f
"cannot be undone. Continue? [y/N] ",
2552 answer = click.get_text_stream(
"stdin").readline().strip().lower()
2553 except (KeyboardInterrupt, OSError):
2555 if answer
not in (
"y",
"yes"):
2556 click.echo(
"Aborted.", err=
True)
2562 restore_paths(repo_root, resolved_paths, staged=staged_mode)
2563 except Exception
as exc:
2564 msg = f
"Git error: {exc}"
2565 click.echo(msg, err=
True)
2567 click.echo(json.dumps({
2568 "operation":
"restore",
2569 "paths_restored": [],
"exit_code": 2,
2576 rel_strings = [to_project_relative(p, root)
for p
in resolved_paths]
2579 click.echo(json.dumps({
2580 "operation":
"restore",
2581 "staged": staged_mode,
2582 "paths_restored": rel_strings,
2588 verb =
"Unstaged" if staged_mode
else "Restored"
2589 click.echo(f
"{verb} {len(rel_strings)} file(s):")
2590 for rel
in rel_strings:
2591 click.echo(f
" {rel}")
2595@git.command(name="ignore")
2596@click.argument("patterns", nargs=-1)
2598 "--remove",
"remove_pattern", default=
None,
2599 help=
"Remove a pattern from the OCT-managed block.",
2602 "--list",
"list_mode", is_flag=
True,
2603 help=
"List currently active ignore patterns.",
2607 type=click.Choice([
"workspace",
"project",
"nearest"]),
2610 "Which .gitignore to target: 'workspace' (workspace-root file), "
2611 "'project' (subproject-root file), 'nearest' (workspace if at "
2612 "workspace root, subproject otherwise; default)."
2616 "--json",
"json_mode", is_flag=
True, help=
"Emit JSON output.",
2619@audited("oct git ignore")
2621 ctx, patterns, remove_pattern, list_mode, scope, json_mode,
2623 """Manage .gitignore patterns within the OCT-managed block."""
2624 func =
"git_ignore_cmd"
2625 root = get_project_root(ctx)
2628 f
"root={root} patterns={patterns} "
2629 f
"remove={remove_pattern!r} list={list_mode} scope={scope!r}",
2633 from oct.core.git
import get_repo_root, is_git_repo
2635 add_ignore_patterns,
2636 list_ignore_patterns,
2637 remove_ignore_pattern,
2638 validate_ignore_pattern,
2641 if not is_git_repo(root):
2642 click.echo(
"Not a git repository.", err=
True)
2647 repo_root = get_repo_root(root)
2654 repo_root=repo_root,
2655 scope=effective_scope,
2657 if scope_err
is not None:
2658 click.echo(f
"Error: {scope_err}", err=
True)
2661 operation=
"ignore", exit_code=1, errors=[scope_err],
2668 f
"effective_scope={effective_scope} target_dir={target_dir}",
2673 op_count = sum([bool(patterns), bool(remove_pattern), bool(list_mode)])
2675 msg =
"Error: PATTERNS, --remove, and --list are mutually exclusive."
2676 click.echo(msg, err=
True)
2679 operation=
"ignore", exit_code=1, errors=[msg],
2686 "Use PATTERNS to add, --remove PATTERN to remove, "
2687 "or --list to show current patterns."
2689 click.echo(msg, err=
True)
2692 operation=
"ignore", exit_code=1, errors=[msg],
2700 oct_managed, other = list_ignore_patterns(target_dir)
2703 operation=
"list", oct_managed=oct_managed,
2704 other=other, exit_code=0,
2708 f
"OCT-managed patterns ({len(oct_managed)}):"
2710 for p
in oct_managed:
2714 f
"Other patterns ({len(other)}, read-only via oct):"
2723 valid, val_msg = validate_ignore_pattern(remove_pattern)
2725 click.echo(f
"Error: {val_msg}", err=
True)
2728 operation=
"remove", exit_code=1, errors=[val_msg],
2734 removed, msg = remove_ignore_pattern(target_dir, remove_pattern)
2735 except OSError
as exc:
2736 err = f
"IO error: {exc}"
2737 click.echo(err, err=
True)
2740 operation=
"remove", exit_code=2, errors=[err],
2746 click.echo(f
"Error: {msg}", err=
True)
2749 operation=
"remove", patterns_removed=[],
2750 exit_code=1, errors=[msg],
2758 patterns_removed=[remove_pattern], exit_code=0,
2761 click.echo(f
"Removed pattern '{remove_pattern}' from .gitignore.")
2766 validated: list[str] = []
2767 errors: list[str] = []
2768 for pat
in patterns:
2769 valid, val_msg = validate_ignore_pattern(pat)
2771 errors.append(f
"{pat!r}: {val_msg}")
2773 validated.append(pat)
2777 click.echo(f
"Error: {e}", err=
True)
2780 operation=
"add", patterns_added=[],
2781 exit_code=1, errors=errors,
2788 added = add_ignore_patterns(target_dir, validated)
2789 except OSError
as exc:
2790 err = f
"IO error: {exc}"
2791 click.echo(err, err=
True)
2794 operation=
"add", exit_code=2, errors=[err],
2802 operation=
"add", patterns_added=added, exit_code=0,
2807 f
"Added {len(added)} pattern(s) to .gitignore "
2808 f
"(OCT-managed block):"
2813 click.echo(
"All patterns already present in .gitignore.")
2822@git.command(name="commit-msg")
2825 type=click.Path(exists=
True, dir_okay=
False, path_type=Path),
2827@click.option("--json", "json_mode", is_flag=True, help="Emit JSON result.")