feat(hm-common): process module — capture + pathbin#171
Merged
Conversation
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
force-pushed
the
feat/subprocess-capture
branch
from
July 25, 2026 22:58
a29b1df to
2e727ce
Compare
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
force-pushed
the
feat/subprocess-capture
branch
from
July 26, 2026 00:45
b88e7c0 to
42a996e
Compare
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.
# Conflicts: # Cargo.lock
…Engine::new() The free function was a bare shorthand for the constructor; callers now use SubprocessPythonEngine::new() directly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.