444 """Show git status with Option C compliance annotations."""
445 func =
"git_status_cmd"
446 root = get_project_root(ctx)
449 f
"root={root} json={json_mode} no_check={no_check} scope={scope!r}",
453 from oct.core.git
import (
461 if not is_git_repo(root):
462 click.echo(
"Not a git repository.", err=
True)
470 repo_root = get_repo_root(root)
471 _dbg(
"GIT", func, f
"repo_root={repo_root}", 3)
479 f
"workspace_mode={in_workspace_mode} "
480 f
"subprojects={(len(workspace.subprojects) if workspace else 0)}",
486 octrc = load_octrc_safe(root)
487 if isinstance(octrc, dict):
488 git_cfg = octrc.get(
"git")
489 if isinstance(git_cfg, dict):
490 raw = git_cfg.get(
"status_default_scope")
491 if isinstance(raw, str)
and raw
in (
"project",
"repo",
"auto"):
496 root_resolved = root.resolve()
497 repo_root_resolved = repo_root.resolve()
500 effective_scope = scope
503 "project" if root_resolved == repo_root_resolved
else "repo"
505 _dbg(
"GIT", func, f
"effective_scope={effective_scope}", 3)
507 branch = current_branch(repo_root)
508 detached = is_detached_head(repo_root)
510 staged = git_staged_files(repo_root)
516 def _partition(files: list[Path]) -> tuple[list[Path], list[Path]]:
517 in_proj: list[Path] = []
518 out_proj: list[Path] = []
524 return in_proj, out_proj
526 staged_in, staged_out = _partition(staged)
527 modified_in, modified_out = _partition(modified)
528 untracked_in, untracked_out = _partition(untracked)
534 staged_in_set = {p.resolve()
for p
in staged_in}
535 modified_in = [p
for p
in modified_in
if p.resolve()
not in staged_in_set]
536 staged_out_set = {p.resolve()
for p
in staged_out}
537 modified_out = [p
for p
in modified_out
if p.resolve()
not in staged_out_set]
540 show_out_of_project = effective_scope ==
"repo"
543 def annotate(files: list[Path]) -> list[dict]:
546 rel = to_project_relative(path, root)
547 if path.suffix !=
".py":
549 "path": rel,
"status":
"non_python",
"issues": [],
553 "path": rel,
"status":
"unchecked",
"issues": [],
559 "status":
"ok" if compliant
else "violations",
564 def annotate_out(files: list[Path]) -> list[dict]:
565 """Out-of-project files: render with repo-relative path, unchecked."""
569 rel = path.resolve().relative_to(repo_root_resolved).as_posix()
571 rel = path.as_posix()
572 out.append({
"path": rel,
"status":
"unchecked",
"issues": []})
575 staged_ann = annotate(staged_in)
576 modified_ann = annotate(modified_in)
578 {
"path": to_project_relative(p, root),
579 "status":
"untracked",
"issues": []}
580 for p
in untracked_in
583 out_of_project_ann: list[dict] = []
584 if show_out_of_project:
585 for entry
in annotate_out(staged_out):
586 entry[
"section"] =
"staged"
587 out_of_project_ann.append(entry)
588 for entry
in annotate_out(modified_out):
589 entry[
"section"] =
"modified"
590 out_of_project_ann.append(entry)
591 for path
in untracked_out:
593 rel = path.resolve().relative_to(repo_root_resolved).as_posix()
595 rel = path.as_posix()
596 out_of_project_ann.append({
597 "path": rel,
"status":
"untracked",
598 "issues": [],
"section":
"untracked",
602 workspace_view: dict |
None =
None
603 if in_workspace_mode
and workspace
is not None and workspace.subprojects:
607 modified=modified_in,
608 untracked=untracked_in,
613 record = ctx.obj.get(
"_audit_record",
AuditRecord())
614 record.timestamp = record.timestamp
or _utc_now_iso()
615 record.command =
"oct git status"
616 record.branch = branch
if not detached
else None
617 record.staged_files = len(staged_in)
618 record.checks_passed =
True
620 ctx.obj[
"_audit_record"] = record
625 "branch": branch
if not detached
else None,
626 "detached": detached,
629 "scope": effective_scope,
630 "staged": staged_ann,
631 "modified": modified_ann,
632 "untracked": untracked_ann,
633 "out_of_project": out_of_project_ann,
634 "workspace_view": workspace_view,
636 click.echo(json.dumps(output, indent=2, ensure_ascii=
False))
641 click.echo(
"HEAD detached")
643 click.echo(f
"On branch {branch}")
644 if ahead > 0
or behind > 0:
647 parts.append(f
"ahead by {ahead}")
649 parts.append(f
"behind by {behind}")
650 click.echo(f
" {', '.join(parts)}")
653 def _render_section(label: str, items: list[dict]):
656 click.echo(f
"{label} ({len(items)} file{'s' if len(items) != 1 else ''}):")
658 status = item[
"status"]
662 elif status ==
"violations":
663 issues_str =
" (" +
", ".join(item[
"issues"]) +
")" if item[
"issues"]
else ""
664 click.echo(f
" [!!] {path}{issues_str}")
666 elif status ==
"non_python":
668 elif status ==
"unchecked":
672 click.echo(f
"{badge}{path}")
675 if workspace_view
is not None:
679 *(sp.name
for sp
in workspace.subprojects),
683 bucket = workspace_view.get(bucket_name, {})
684 staged_b = bucket.get(
"staged", [])
685 modified_b = bucket.get(
"modified", [])
686 untracked_b = bucket.get(
"untracked", [])
687 if not (staged_b
or modified_b
or untracked_b):
689 click.echo(f
"=== {bucket_name} ===")
690 _render_section(
"Staged", staged_b)
691 _render_section(
"Modified", modified_b)
692 _render_section(
"Untracked", untracked_b)
694 _render_section(
"Staged", staged_ann)
695 _render_section(
"Modified", modified_ann)
696 _render_section(
"Untracked", untracked_ann)
697 if show_out_of_project
and out_of_project_ann:
699 f
"Other repo files ({len(out_of_project_ann)} "
700 f
"file{'s' if len(out_of_project_ann) != 1 else ''}, "
701 f
"outside OCT project):"
703 for item
in out_of_project_ann:
704 section = item.get(
"section",
"?")
705 click.echo(f
" [{section[:3]}] {item['path']}")
950 """Run the unified quality gate: lint + format + secrets."""
951 func =
"git_check_cmd"
952 root = get_project_root(ctx)
953 _dbg(
"GIT", func, f
"root={root}", 2)
955 from oct.core.git
import current_branch, is_git_repo
957 apply_profile_policy,
958 resolve_profile_for_project,
962 if not is_git_repo(root):
963 click.echo(
"Not a git repository.", err=
True)
976 branch = current_branch(root)
977 octrc = load_octrc_safe(root)
980 effective_profile = resolve_profile_for_project(
981 root, branch, octrc, profile,
985 f
"scope={scope} profile={effective_profile} fix={fix}",
1001 check_in_workspace_mode
1002 and check_workspace
is not None
1003 and bool(check_workspace.subprojects)
1009 workspace=check_workspace,
1011 cli_profile=profile,
1012 fallback_profile=effective_profile,
1014 include_tests=include_tests,
1016 json_mode=json_mode,
1022 result = run_quality_gate(
1025 profile=effective_profile,
1026 include_tests=include_tests,
1032 exit_code = apply_profile_policy(effective_profile, result)
1033 result.exit_code = exit_code
1036 record = ctx.obj.get(
"_audit_record",
AuditRecord())
1037 record.timestamp = record.timestamp
or _utc_now_iso()
1038 record.command =
"oct git check"
1039 record.branch = branch
1040 record.staged_files = result.files_checked
1041 record.checks_passed = (exit_code == 0)
1042 record.secrets_detected = bool(result.secrets_findings)
1043 record.lint_violations = result.lint_violations
1044 record.format_violations = result.format_violations
1045 record.exit_code = exit_code
1046 ctx.obj[
"_audit_record"] = record
1052 "profile": effective_profile,
1053 "files_checked": result.files_checked,
1054 "lint_violations": result.lint_violations,
1055 "format_violations": result.format_violations,
1056 "secrets_findings": [
1057 {
"path": s[0],
"line": s[1],
"reason": s[2]}
1058 for s
in result.secrets_findings
1060 "test_failures": result.test_failures,
1061 "exit_code": exit_code,
1062 "duration_ms": result.duration_ms,
1064 click.echo(json.dumps(output, indent=2, ensure_ascii=
False))
1069 click.echo(f
"Quality gate: scope={scope}, profile={effective_profile}")
1070 click.echo(f
" Files checked: {result.files_checked}")
1071 click.echo(f
" Lint violations: {result.lint_violations}")
1072 click.echo(f
" Format issues: {result.format_violations}")
1073 click.echo(f
" Secrets findings: {len(result.secrets_findings)}")
1075 click.echo(f
" Test failures: {result.test_failures}")
1076 click.echo(f
" Duration: {result.duration_ms} ms")
1079 if result.secrets_findings:
1080 click.echo(
"SECRETS DETECTED (always blocking):", err=
True)
1081 for path_str, line, reason
in result.secrets_findings:
1082 loc = f
"{path_str}:{line}" if line
else path_str
1083 click.echo(f
" {loc} - {reason}", err=
True)
1087 click.echo(
"All checks passed.")
1090 1:
"Lint violations",
1091 2:
"Format violations",
1092 3:
"Lint + format violations",
1094 5:
"Secrets detected",
1097 f
"FAILED (exit {exit_code}): {labels.get(exit_code, 'unknown')}",
1364 messages: tuple[str, ...],
1367 profile: str |
None,
1369 ignore_unstaged_mutation: bool,
1370 extra_args: tuple[str, ...],
1372 """Commit staged changes with quality gate enforcement."""
1373 func =
"git_commit_cmd"
1374 root = get_project_root(ctx)
1377 message =
"\n\n".join(m
for m
in messages
if m
is not None)
1378 _dbg(
"GIT", func, f
"root={root} force={force_commit} -m count={len(messages)}", 2)
1380 from oct.core.git
import current_branch, git_run, is_git_repo
1382 analyse_diagnostics_impact,
1383 format_validation_errors,
1388 if not is_git_repo(root):
1389 click.echo(
"Not a git repository.", err=
True)
1393 branch = current_branch(root)
1394 octrc = load_octrc_safe(root)
1397 pre = pre_commit_checks(
1401 force_commit=force_commit,
1405 effective_profile = pre.effective_profile
1407 click.echo(pre.pb_advisory, err=
True)
1413 root, branch, message, ctx, json_mode,
1415 if auto_branch_name
is None:
1419 branch = auto_branch_name
1421 pre = pre_commit_checks(
1422 project_root=root, branch=branch, message=message,
1423 force_commit=force_commit, profile=profile, octrc=octrc,
1425 effective_profile = pre.effective_profile
1430 ctx, exit_code=pre.exit_code, message=message,
1431 branch=branch, profile=effective_profile,
1434 elif pre.exit_code == 3:
1435 click.echo(format_validation_errors(pre.errors), err=
True)
1436 elif pre.exit_code == 4:
1437 click.echo(
"Nothing to commit (no staged files).", err=
True)
1440 ctx, branch=branch, exit_code=pre.exit_code,
1441 checks_passed=
False, staged_count=len(pre.staged_files),
1443 ctx.exit(pre.exit_code)
1446 staged = pre.staged_files
1455 except ValueError
as exc:
1456 click.echo(str(exc), err=
True)
1459 ctx, exit_code=2, message=message, branch=branch,
1460 profile=effective_profile, errors=[str(exc)],
1463 ctx, branch=branch, exit_code=2,
1464 checks_passed=
False, staged_count=len(staged),
1468 pre_snapshot: dict[str, str] = {}
1469 if observe_mode !=
"off":
1471 from oct.core.git
import get_repo_root
1473 repo_root_for_obs = get_repo_root(root)
1475 str(p.resolve().relative_to(repo_root_for_obs.resolve()))
1480 pre_snapshot = snapshot_unstaged_tracked(
1481 repo_root_for_obs, staged_set=staged_rel,
1482 method=observe_method,
1484 except Exception
as exc:
1487 f
"observer snapshot failed: {exc!r}; continuing without",
1491 observe_mode =
"off"
1500 per_sp_results: dict[str,
"QualityGateResult"] = {}
1501 per_sp_meta: dict[str, dict] = {}
1503 commit_in_workspace_mode
1504 and commit_workspace
is not None
1505 and bool(commit_workspace.subprojects)
1514 commit_workspace, staged,
1515 cli_profile=profile, branch=branch,
1516 fallback_profile=effective_profile,
1517 apply_policy=
not force_commit,
1519 unbucketed = outcome.unbucketed
1522 to_project_relative(p, root)
for p
in unbucketed
1526 f
"partition invariant violated; {len(unbucketed)} staged "
1527 f
"file(s) reached no bucket: {names}",
1532 ctx, exit_code=2, message=message, branch=branch,
1533 profile=effective_profile,
1535 "Refusing to commit: "
1536 f
"{len(unbucketed)} staged file(s) could not be "
1537 "assigned to any quality-gate scope and would be "
1538 f
"committed unscanned: {names}."
1543 "Refusing to commit: staged file(s) could not be "
1544 "assigned to any quality-gate scope and would be "
1545 f
"committed unscanned:\n {names}",
1550 per_sp_results = outcome.per_bucket_results
1551 per_sp_meta = outcome.per_bucket_meta
1552 result = outcome.result
1553 gate_exit = outcome.gate_exit
1554 elif not force_commit:
1555 result = run_quality_gate(
1558 profile=effective_profile,
1559 include_tests=
False,
1562 gate_exit = apply_profile_policy(effective_profile, result)
1565 result = run_quality_gate(
1568 profile=effective_profile,
1569 include_tests=
False,
1584 committed_secrets = result.secrets_findings
1587 from oct.core.git
import get_repo_root, git_staged_files
1589 commit_repo_root = get_repo_root(root)
1590 full_index = git_staged_files(commit_repo_root)
1597 committed_secrets = run_secrets_scan_on_files(commit_repo_root, full_index)
1598 except Exception
as exc:
1603 f
"full-index secrets scan failed ({exc!r}); refusing commit",
1608 ctx, exit_code=2, message=message, branch=branch,
1609 profile=effective_profile,
1611 "Refusing to commit: could not scan the full committed "
1612 f
"index for secrets ({type(exc).__name__})."
1617 "Refusing to commit: could not scan the full committed "
1618 f
"index for secrets ({type(exc).__name__}).",
1625 if committed_secrets:
1628 ctx, exit_code=2, message=message, branch=branch,
1629 profile=effective_profile,
1630 errors=[f
"Secrets detected in {len(committed_secrets)} location(s)."],
1631 secrets=committed_secrets,
1634 click.echo(
"SECRETS DETECTED (non-bypassable):", err=
True)
1635 for path_str, line, reason
in committed_secrets:
1636 loc = f
"{path_str}:{line}" if line
else path_str
1637 click.echo(f
" {loc} - {reason}", err=
True)
1639 ctx, branch=branch, exit_code=2,
1640 checks_passed=
False, staged_count=len(staged),
1641 secrets=
True, lint_v=result.lint_violations,
1642 format_v=result.format_violations,
1648 if gate_exit != 0
and not force_commit:
1651 ctx, exit_code=1, message=message, branch=branch,
1652 profile=effective_profile,
1653 errors=[f
"Quality gate failed (exit {gate_exit})."],
1657 f
"Quality gate failed (exit {gate_exit}). "
1658 f
"Use --force to skip lint/format checks.",
1662 ctx, branch=branch, exit_code=1,
1663 checks_passed=
False, staged_count=len(staged),
1664 lint_v=result.lint_violations,
1665 format_v=result.format_violations,
1670 if force_commit
and gate_exit != 0:
1672 f
"Warning: quality gate would have failed (exit {gate_exit}), "
1673 f
"bypassed with --force.",
1679 for rel, size
in oversized:
1680 mb = size / (1024 * 1024)
1682 f
"Warning: {rel} is {mb:.1f} MB. Consider .gitignore or Git LFS.",
1692 "Skipping git hooks (--no-verify). "
1693 "Note: oct git commit still runs its own quality checks.",
1698 if observe_mode !=
"off" and pre_snapshot:
1700 root, pre_snapshot, staged,
1701 stage_label=
"quality gate",
1702 method=observe_method,
1704 if gate_mutations
and observe_mode ==
"block":
1706 ctx, mutations=gate_mutations,
1707 message=message, branch=branch,
1708 profile=effective_profile, json_mode=json_mode,
1709 staged_count=len(staged),
1714 git_args = [
"commit",
"-m", message]
1716 git_args.append(
"--no-verify")
1717 git_args.extend(extra_args)
1720 git_run(git_args, cwd=root)
1721 except Exception
as exc:
1725 ctx, exit_code=4, message=message, branch=branch,
1726 profile=effective_profile,
1727 errors=[f
"git commit failed: {err_msg}"],
1730 click.echo(f
"git commit failed: {err_msg}", err=
True)
1732 ctx, branch=branch, exit_code=4,
1733 checks_passed=
False, staged_count=len(staged),
1739 if observe_mode !=
"off" and pre_snapshot:
1741 root, pre_snapshot, staged,
1742 stage_label=
"git-hook",
1743 method=observe_method,
1745 if hook_mutations
and observe_mode ==
"block":
1747 ctx, mutations=hook_mutations,
1748 message=message, branch=branch,
1749 profile=effective_profile, json_mode=json_mode,
1750 staged_count=len(staged),
1756 from oct.core.git
import git_run
as _gr
1758 [
"diff",
"HEAD~1..HEAD"], cwd=root, check=
False,
1760 impact = analyse_diagnostics_impact(diff_proc.stdout)
1762 click.echo(
"Diagnostics Impact:", err=
True)
1764 click.echo(f
" {line}", err=
True)
1771 ctx, exit_code=0, message=message, branch=branch,
1772 profile=effective_profile, errors=[],
1775 click.echo(
"Commit successful.")
1779 if use_fanout
else None
1782 ctx, branch=branch, exit_code=0,
1783 checks_passed=
True, staged_count=len(staged),
1784 per_subproject_results=psr_audit,
2368 create_name: str |
None,
2369 switch_name: str |
None,
2372 """Manage branches with Option C naming conventions."""
2373 func =
"git_branch_cmd"
2374 root = get_project_root(ctx)
2375 _dbg(
"GIT", func, f
"root={root} create={create_name!r} switch={switch_name!r}", 2)
2377 from oct.core.git
import (
2380 has_uncommitted_changes,
2386 if not is_git_repo(root):
2387 click.echo(
"Not a git repository.", err=
True)
2392 octrc = load_octrc_safe(root)
2395 if create_name
and switch_name:
2396 msg =
"Error: --create and --switch are mutually exclusive."
2397 click.echo(msg, err=
True)
2404 if not create_name
and not switch_name:
2405 msg =
"Use --create NAME or --switch NAME. Branch listing coming soon."
2406 click.echo(msg, err=
True)
2415 valid, val_msg = validate_branch_name(create_name, octrc)
2417 click.echo(f
"Error: {val_msg}", err=
True)
2420 operation=
"create", branch=create_name,
2421 exit_code=1, errors=[val_msg],
2427 if branch_exists(root, create_name):
2428 msg = f
"Error: branch '{create_name}' already exists."
2429 click.echo(msg, err=
True)
2432 operation=
"create", branch=create_name,
2433 exit_code=1, errors=[msg],
2440 create_branch(root, create_name)
2441 except Exception
as exc:
2442 msg = f
"Git error: {exc}"
2443 click.echo(msg, err=
True)
2446 operation=
"create", branch=create_name,
2447 exit_code=2, errors=[msg],
2455 operation=
"create", branch=create_name, exit_code=0,
2458 click.echo(f
"Created branch '{create_name}' (not switched to it).")
2464 if not branch_exists(root, switch_name):
2465 msg = f
"Error: branch '{switch_name}' does not exist."
2466 click.echo(msg, err=
True)
2469 operation=
"switch", branch=switch_name,
2470 exit_code=1, errors=[msg],
2476 if has_uncommitted_changes(root):
2478 "Error: uncommitted changes would be lost. "
2479 "Commit or stash them first."
2481 click.echo(msg, err=
True)
2484 operation=
"switch", branch=switch_name,
2485 exit_code=1, errors=[msg],
2492 switch_branch(root, switch_name)
2493 except Exception
as exc:
2494 msg = f
"Git error: {exc}"
2495 click.echo(msg, err=
True)
2498 operation=
"switch", branch=switch_name,
2499 exit_code=2, errors=[msg],
2507 operation=
"switch", branch=switch_name, exit_code=0,
2510 click.echo(f
"Switched to branch '{switch_name}'.")
2749 paths: tuple[str, ...],
2756 """Stage files for commit (whole-file or interactive hunk)."""
2757 func =
"git_add_cmd"
2758 root = get_project_root(ctx)
2761 f
"root={root} paths={paths} patch={patch_mode} "
2762 f
"update={update_mode} all={all_mode} dry_run={dry_run}",
2766 from oct.core.git
import (
2770 stage_paths_interactive,
2773 if not is_git_repo(root):
2774 click.echo(
"Not a git repository.", err=
True)
2779 repo_root = get_repo_root(root)
2780 octrc = load_octrc_safe(root)
2783 if patch_mode
and (update_mode
or all_mode):
2784 msg =
"Error: --patch cannot be combined with --update or --all."
2785 click.echo(msg, err=
True)
2788 operation=
"patch", paths_staged=[], paths_skipped=[],
2789 post_check=
None, exit_code=1, errors=[msg],
2795 if update_mode
and all_mode:
2796 msg =
"Error: --update and --all are mutually exclusive."
2797 click.echo(msg, err=
True)
2800 operation=
"add", paths_staged=[], paths_skipped=[],
2801 post_check=
None, exit_code=1, errors=[msg],
2808 root_resolved = root.resolve()
2809 skipped: list[dict] = []
2810 resolved_paths: list[Path] = []
2812 candidate = (Path.cwd() / raw).resolve() \
2813 if not Path(raw).is_absolute()
else Path(raw).resolve()
2816 "path": str(raw),
"reason":
"outside project scope",
2819 resolved_paths.append(candidate)
2821 if skipped
and not json_mode:
2822 for entry
in skipped:
2824 f
"Skipping {entry['path']} ({entry['reason']}).",
2829 if not resolved_paths
and not (update_mode
or all_mode
or patch_mode):
2831 "Error: no paths to stage. Provide PATHS, "
2832 "or use --update / --all / --patch."
2834 click.echo(msg, err=
True)
2837 operation=
"add", paths_staged=[], paths_skipped=skipped,
2838 post_check=
None, exit_code=1, errors=[msg],
2846 operation =
"dry-run"
2848 to_project_relative(p, root)
for p
in resolved_paths
2850 if update_mode
or all_mode:
2852 from oct.core.git
import git_run
as _git_run
2853 argv = [
"status",
"--porcelain"]
2854 proc = _git_run(argv, cwd=repo_root)
2855 for line
in proc.stdout.splitlines():
2860 rel = line[3:].strip()
2861 if update_mode
and xy[1]
in (
" ",
""):
2864 if update_mode
and xy[1] ==
"?":
2866 full = (repo_root / rel).resolve()
2869 rel_strings.append(to_project_relative(full, root))
2872 operation=operation, paths_staged=rel_strings,
2873 paths_skipped=skipped, post_check=
None, exit_code=0,
2876 click.echo(
"Would stage:")
2877 for s
in rel_strings:
2879 _add_audit(ctx, exit_code=0, staged_count=len(rel_strings))
2886 "Note: --patch is interactive; --json output is limited.",
2890 rc = stage_paths_interactive(repo_root, resolved_paths)
2891 except Exception
as exc:
2892 msg = f
"Git error: {exc}"
2893 click.echo(msg, err=
True)
2896 operation=
"patch", paths_staged=[],
2897 paths_skipped=skipped, post_check=
None,
2898 exit_code=2, errors=[msg],
2910 operation=
"patch", paths_staged=[],
2911 paths_skipped=skipped, post_check=post, exit_code=0,
2925 from oct.core.git
import git_staged_files
2926 pre_index = {p.resolve()
for p
in git_staged_files(repo_root)}
2928 git_warning: str |
None =
None
2931 repo_root, resolved_paths,
2932 update=update_mode, all_=all_mode,
2934 except Exception
as exc:
2938 post_index = {p.resolve()
for p
in git_staged_files(repo_root)}
2939 newly_staged = post_index - pre_index
2941 git_warning = str(exc)
2943 f
"Note: git emitted a warning during staging but "
2944 f
"{len(newly_staged)} file(s) were staged successfully:\n"
2949 msg = f
"Git error: {exc}"
2950 click.echo(msg, err=
True)
2953 operation=
"add", paths_staged=[],
2954 paths_skipped=skipped,
2955 post_check=
None, exit_code=2, errors=[msg],
2962 staged_now = git_staged_files(repo_root)
2966 staged_strings = [to_project_relative(p, root)
for p
in staged_within]
2968 operation =
"all" if all_mode
else "update" if update_mode
else "add"
2973 operation=operation, paths_staged=staged_strings,
2974 paths_skipped=skipped, post_check=post, exit_code=0,
2978 for p
in resolved_paths:
2979 click.echo(f
"Staged {to_project_relative(p, root)}")
2982 f
"Staged {len(staged_strings)} tracked modified file(s).",
2986 f
"Staged {len(staged_strings)} file(s) (including untracked).",
2990 _add_audit(ctx, exit_code=0, staged_count=len(staged_strings))
3388 patterns: tuple[str, ...],
3389 remove_pattern: str |
None,
3394 """Manage .gitignore patterns within the OCT-managed block."""
3395 func =
"git_ignore_cmd"
3396 root = get_project_root(ctx)
3399 f
"root={root} patterns={patterns} "
3400 f
"remove={remove_pattern!r} list={list_mode} scope={scope!r}",
3404 from oct.core.git
import get_repo_root, is_git_repo
3406 add_ignore_patterns,
3407 list_ignore_patterns,
3408 remove_ignore_pattern,
3409 validate_ignore_pattern,
3412 if not is_git_repo(root):
3413 click.echo(
"Not a git repository.", err=
True)
3418 repo_root = get_repo_root(root)
3421 octrc = load_octrc_safe(root)
3425 repo_root=repo_root,
3426 scope=effective_scope,
3428 if scope_err
is not None:
3429 click.echo(f
"Error: {scope_err}", err=
True)
3432 operation=
"ignore", exit_code=1, errors=[scope_err],
3439 f
"effective_scope={effective_scope} target_dir={target_dir}",
3444 op_count = sum([bool(patterns), bool(remove_pattern), bool(list_mode)])
3446 msg =
"Error: PATTERNS, --remove, and --list are mutually exclusive."
3447 click.echo(msg, err=
True)
3450 operation=
"ignore", exit_code=1, errors=[msg],
3457 "Use PATTERNS to add, --remove PATTERN to remove, "
3458 "or --list to show current patterns."
3460 click.echo(msg, err=
True)
3463 operation=
"ignore", exit_code=1, errors=[msg],
3471 oct_managed, other = list_ignore_patterns(target_dir)
3474 operation=
"list", oct_managed=oct_managed,
3475 other=other, exit_code=0,
3479 f
"OCT-managed patterns ({len(oct_managed)}):"
3481 for p
in oct_managed:
3485 f
"Other patterns ({len(other)}, read-only via oct):"
3494 valid, val_msg = validate_ignore_pattern(remove_pattern)
3496 click.echo(f
"Error: {val_msg}", err=
True)
3499 operation=
"remove", exit_code=1, errors=[val_msg],
3505 removed, msg = remove_ignore_pattern(target_dir, remove_pattern)
3506 except OSError
as exc:
3507 err = f
"IO error: {exc}"
3508 click.echo(err, err=
True)
3511 operation=
"remove", exit_code=2, errors=[err],
3517 click.echo(f
"Error: {msg}", err=
True)
3520 operation=
"remove", patterns_removed=[],
3521 exit_code=1, errors=[msg],
3529 patterns_removed=[remove_pattern], exit_code=0,
3532 click.echo(f
"Removed pattern '{remove_pattern}' from .gitignore.")
3537 validated: list[str] = []
3538 errors: list[str] = []
3539 for pat
in patterns:
3540 valid, val_msg = validate_ignore_pattern(pat)
3542 errors.append(f
"{pat!r}: {val_msg}")
3544 validated.append(pat)
3548 click.echo(f
"Error: {e}", err=
True)
3551 operation=
"add", patterns_added=[],
3552 exit_code=1, errors=errors,
3559 added = add_ignore_patterns(target_dir, validated)
3560 except OSError
as exc:
3561 err = f
"IO error: {exc}"
3562 click.echo(err, err=
True)
3565 operation=
"add", exit_code=2, errors=[err],
3573 operation=
"add", patterns_added=added, exit_code=0,
3578 f
"Added {len(added)} pattern(s) to .gitignore "
3579 f
"(OCT-managed block):"
3584 click.echo(
"All patterns already present in .gitignore.")