7Mockable git shell-out seam for round-close orchestration.
11Wrap the handful of ``git`` invocations the round-close executors need
12behind one injectable seam so tests never shell out to a real repo.
16- Run git subcommands in a fixed repository directory.
17- Surface conflict state from ``git merge`` without raising.
18- Expose ancestry and fast-forward helpers used by ff_main / consolidate.
24 L2 - lifecycle events (branch create, merge, ff)
25 L3 - command and exit-code details
30- No method mutates the registry database; git is the only side effect.
31- The injected ``runner`` matches the ``subprocess.run`` signature.
32- ``merge`` never raises on conflict; callers inspect ``MergeResult``.
36- oc_diagnostics (structured debug logging via _dbg)
37- subprocess (subprocess.run - the default git runner)
38- pathlib (the repository directory)
40from __future__
import annotations
42from dataclasses
import dataclass, field
43from pathlib
import Path
45from typing
import Callable, Sequence
47from oc_diagnostics
import _dbg
50_dbg(
"OC_CHAT.MAIN",
"module",
"loaded", 4)
52Runner = Callable[...,
"subprocess.CompletedProcess[str]"]
55@dataclass(frozen=True)
57 """Outcome of a single git invocation."""
65 """True when the git command exited zero."""
70@dataclass(frozen=True)
72 """Result of a ``git merge`` attempt."""
76 conflicted: bool =
False
77 conflicted_paths: tuple[str, ...] = field(default_factory=tuple)
78 already_up_to_date: bool =
False
82 """Raised when a git command that must succeed exits non-zero."""
84 def __init__(self, args: Sequence[str], result: GitResult) ->
None:
88 f
"git {' '.join(args)} failed ({result.returncode}): "
89 f
"{result.stderr.strip() or result.stdout.strip()}"
94 """Thin, injectable wrapper over the git CLI for round-close."""
96 def __init__(self, repo_dir: Path, *, runner: Runner |
None =
None) ->
None:
98 self.
_runner: Runner = runner
if runner
is not None else subprocess.run
100 def run(self, *args: str) -> GitResult:
101 """Run ``git <args>`` in the repo dir and capture the result."""
112 returncode=int(getattr(completed,
"returncode", 0)),
113 stdout=str(getattr(completed,
"stdout",
"")
or ""),
114 stderr=str(getattr(completed,
"stderr",
"")
or ""),
116 _dbg(
"OC_CHAT.MAIN", func, f
"git {' '.join(args)} -> {result.returncode}", 3)
121 result = self.
run(*args)
123 _dbg(
"OC_CHAT.MAIN", func, f
"git {' '.join(args)} non-zero", 2)
128 """Return the full SHA for ``ref``."""
131 result = self.
_checked(
"rev-parse", ref)
132 _dbg(
"OC_CHAT.MAIN", func, f
"resolved {ref}", 4)
133 return result.stdout.strip()
136 """Return the current branch name (or empty for detached HEAD)."""
138 func =
"current_branch"
139 result = self.
run(
"rev-parse",
"--abbrev-ref",
"HEAD")
140 _dbg(
"OC_CHAT.MAIN", func,
"queried branch", 4)
141 return result.stdout.strip()
144 """True when a local branch ``name`` exists."""
146 func =
"branch_exists"
147 result = self.
run(
"rev-parse",
"--verify",
"--quiet", f
"refs/heads/{name}")
148 _dbg(
"OC_CHAT.MAIN", func, f
"exists={result.ok} {name}", 4)
152 """Create ``name`` at ``start_point`` and check it out; return HEAD."""
154 func =
"create_branch"
158 self.
_checked(
"checkout",
"-b", name, start_point)
159 _dbg(
"OC_CHAT.MAIN", func, f
"on {name}", 2)
163 """Check out ``ref``."""
167 _dbg(
"OC_CHAT.MAIN", func, f
"checked out {ref}", 3)
174 message: str |
None =
None,
176 """Merge ``branch`` into the current branch; report conflicts."""
181 args.append(
"--no-ff")
182 if message
is not None:
183 args.extend([
"-m", message])
185 result = self.
run(*args)
187 up_to_date =
"Already up to date" in result.stdout
188 _dbg(
"OC_CHAT.MAIN", func, f
"merged {branch} up_to_date={up_to_date}", 2)
192 already_up_to_date=up_to_date,
198 f
"merge {branch} failed conflicts={len(conflicted_paths)}",
204 conflicted=bool(conflicted_paths),
205 conflicted_paths=conflicted_paths,
209 func =
"_conflicted_paths"
210 result = self.
run(
"diff",
"--name-only",
"--diff-filter=U")
211 paths = tuple(p
for p
in result.stdout.splitlines()
if p.strip())
212 _dbg(
"OC_CHAT.MAIN", func, f
"{len(paths)} conflicted", 4)
216 """Abort an in-progress merge."""
219 self.
run(
"merge",
"--abort")
220 _dbg(
"OC_CHAT.MAIN", func,
"merge aborted", 2)
223 """Resolve a single conflicted ``path`` by taking ``side``."""
225 func =
"checkout_path"
226 flag =
"--theirs" if side ==
"current" else "--ours"
227 self.
_checked(
"checkout", flag,
"--", path)
229 _dbg(
"OC_CHAT.MAIN", func, f
"took {side} for {path}", 3)
231 def is_ancestor(self, maybe_ancestor: str, descendant: str) -> bool:
232 """True when ``maybe_ancestor`` is an ancestor of ``descendant``."""
236 "merge-base",
"--is-ancestor", maybe_ancestor, descendant
238 _dbg(
"OC_CHAT.MAIN", func, f
"ancestor={result.ok}", 4)
242 """Return the merge base SHA of two refs."""
245 result = self.
_checked(
"merge-base", a, b)
246 _dbg(
"OC_CHAT.MAIN", func,
"computed merge-base", 4)
247 return result.stdout.strip()
250 """Fast-forward ``branch`` to ``to_sha``; return the new HEAD."""
252 func =
"fast_forward"
254 self.
_checked(
"merge",
"--ff-only", to_sha)
256 _dbg(
"OC_CHAT.MAIN", func, f
"{branch} -> {head}", 2)
260 """Delete a local branch; return True on success."""
262 func =
"branch_delete"
263 flag =
"-D" if force
else "-d"
264 result = self.
run(
"branch", flag, name)
265 _dbg(
"OC_CHAT.MAIN", func, f
"delete {name} ok={result.ok}", 2)
269 """Remove a git worktree; return True on success."""
271 func =
"worktree_remove"
272 args = [
"worktree",
"remove"]
274 args.append(
"--force")
276 result = self.
run(*args)
277 _dbg(
"OC_CHAT.MAIN", func, f
"remove worktree {path} ok={result.ok}", 2)
None __init__(self, Sequence[str] args, GitResult result)
bool worktree_remove(self, str path, *, bool force=False)
GitResult run(self, *str args)
str merge_base(self, str a, str b)
None checkout(self, str ref)
GitResult _checked(self, *str args)
bool is_ancestor(self, str maybe_ancestor, str descendant)
MergeResult merge(self, str branch, *, bool no_ff=True, str|None message=None)
str fast_forward(self, str branch, str to_sha)
None __init__(self, Path repo_dir, *, Runner|None runner=None)
str create_branch(self, str name, str start_point)
None checkout_path(self, str side, str path)
bool branch_delete(self, str name, *, bool force=False)
tuple[str,...] _conflicted_paths(self)
bool branch_exists(self, str name)
str rev_parse(self, str ref="HEAD")