Skip to content

feat(hm-common): process module — capture + pathbin - #171

Merged
markovejnovic merged 19 commits into
mainfrom
feat/subprocess-capture
Jul 26, 2026
Merged

feat(hm-common): process module — capture + pathbin#171
markovejnovic merged 19 commits into
mainfrom
feat/subprocess-capture

Conversation

@markovejnovic

@markovejnovic markovejnovic commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@markovejnovic markovejnovic changed the title feat(hm-common): process capture helper feat(hm-common): process module — capture + pathbin Jul 25, 2026
Add hm_common::process: a `.captured()` extension on std and tokio
Command that spawns, captures stdout/stderr + exit status, and returns
a typed Captured. `Captured::success()` splits a zero exit (CapturedOk)
from a failure (CapturedError, a std::error::Error whose Display names
the program, status, and a trimmed stderr snippet). Output is read via
the sealed CapturedStreams trait: byte-first `stdout()`, strict
`stdout_str`/`stdout_string`, panicking `_unwrap` variants, and lossy
`stdout_lossy` (stderr mirrors). Covered by rstest cases + a tokio test.

Replace the four hand-rolled spawn/status/utf8 blocks:
- hm-dsl-engine python_engine: async `.captured().await?.success()?`,
  dropping the manual Stdio piping and the bail! error construction.
- hm run git_metadata / git_remote_url / git_default_branch: one
  `git_at()` builder + `.captured()`, collapsing the duplicated
  Command/output/filter/from_utf8_lossy chains.
Move process.rs to process/capture.rs (git-tracked rename) and add
process/mod.rs, which re-exports the capture API so the public path
`hm_common::process::{CommandExt, CapturedStreams, ...}` is unchanged.
Add an empty process/which.rs as the home for the forthcoming
require_on_path helper.
Implement pathbin in process/which.rs: resolve a program to its
absolute path on PATH, returning a typed ExecutableNotFound (naming the
program) on miss. Re-export it (and the error) at process::. A presence
check is pathbin(x).is_ok() — no separate helper.

Replace the last which::which sites:
- hm-dsl-engine python_engine: pathbin("python3"), with the install
  hint moved to a .context() at the call site.
- the six python3 test skip-guards across hm and hm-dsl-engine tests.

With no which::which callers left outside hm-common, drop the now-unused
`which` dependency from hm and hm-dsl-engine and hm-common's redundant
dev-dependency copy.
@markovejnovic
markovejnovic force-pushed the feat/subprocess-capture branch from a29b1df to 2e727ce Compare July 25, 2026 22:58
Add git()/python3() aliases over pathbin, and SystemBins — the external
executables Harmont shells out to, resolved from PATH via resolve().
python3 is required; git resolves to Option since git metadata is
best-effort. Adopt the python3() alias in the DSL engine.

Also restore the # Errors docs on capture's success()/captured() that a
prior module-doc edit dropped.
git is a hard dependency for a developer tool, so SystemBins.git is a
PathBuf (not Option) and resolve() fails fast when git is absent from
PATH. Simplifies git() to return &Path.
Add a process-global runtime context, gated behind the opt-in
`app-runtime` feature. AppRuntime::init() resolves SystemBins and
captures the absolute cwd once, installing them in a OnceLock; the
associated bins()/cwd() accessors read the global from anywhere with no
threading, panicking only if used before init. init() fails fast with a
typed InitError when a required executable is missing.
Remove the git()/python3() convenience wrappers; callers and
SystemBins::resolve use pathbin("git")/pathbin("python3") directly.
Move AppRuntime::init() out of `hm run` and into run() in main, so the
toolchain (git + python3) is resolved once for every command and a
missing tool fails fast with a clear error. It runs after arg parsing,
so --help/--version and parse errors are not gated on it.
Add hm_common::git: a typed handle hierarchy over the git CLI —
Git(bin) → GitRepo(repo) → GitBranch / GitRemote — driven through
process::capture. GitRepo::repo() validates the work tree
(InvalidRepoError); operations are best-effort (Option). Raw git output
is exposed as bstr BString/BStr. Every method that shells out is
#[instrument]-ed.

Refactor hm run to use it: git_metadata / git_remote_url /
git_default_branch / git_remote_repo_name now take a Git built from
AppRuntime::bins().git() (finally consuming the resolved git path), and
the git_at helper plus the repo-name / default-branch parsers (with
their tests) move into the git module.

Also instrument process::capture::captured (sync + async) and
which::pathbin, per the observability goal. Adds bstr to hm-common and
hm.
@markovejnovic
markovejnovic force-pushed the feat/subprocess-capture branch from b88e7c0 to 42a996e Compare July 26, 2026 00:45
Expose the system git as a bound Git handle, so callers use
AppRuntime::git() instead of Git::new(AppRuntime::bins().git()). Returns
Git by value (it is Copy; a stored &Git would be self-referential). Adopt
it in hm run.
Replace git_metadata, git_remote_url, git_default_branch, and
git_remote_repo_name with inline GitRepo accessor calls at their sole
call sites in the run-request assembly. The branch/commit/repo-name read
now shares a single borrow of repo_root in one block; the default-branch
and remote-url reads inline directly into the cloud-target and
auto-create paths. Removes the git_metadata unit test (its target is
gone).
The inlined accessors re-opened the worktree repo and its origin remote
up to three more times on the cloud paths (remoteless registration and
auto-create each re-shelled to git). Open repo + origin once in step 7,
read branch/commit/repo_name/remote_url/default_branch from that single
pair of handles into owned strings, and have the cloud paths reuse those
values. Net fewer git subprocesses on cloud runs; local runs pay one
extra symbolic-ref for default_branch, which they don't consume.
A git-style runner for python3: `Python` (interpreter handle, from
`AppRuntime::python()`), `PyCommand` (derefs to tokio::process::Command,
adds pythonpath + a run terminal), and `PyOutput` (derefs to CapturedOk,
adds JSON decoding). Failures surface as `PyError` carrying stderr;
stdout decoding is the caller's choice on the output.

Replaces the ad-hoc Command dance in SubprocessPythonEngine with
`AppRuntime::python()`-shaped calls; the harmont-py bundling stays in
hm-dsl-engine. Covered by rstest against a real python3.
Remove the SystemBins struct; AppRuntime now holds the resolved git and
python3 paths directly and resolves them in ::init via pathbin. Drop the
AppRuntime::bins() accessor (git()/python() read the fields).

SubprocessPythonEngine loses its python_bin field and fallible new() —
it is a unit struct that runs python3 through AppRuntime::python(), so
the DSL engine now depends on AppRuntime being initialized (feature +
init-or-skip in the integration tests; the hm binary already inits in
main). Mark AppRuntime with a TODO to thread an explicit handle later.
…Engine::new()

The free function was a bare shorthand for the constructor; callers now
use SubprocessPythonEngine::new() directly.
@markovejnovic
markovejnovic merged commit 621668a into main Jul 26, 2026
15 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant