// std/worktree — isolated git worktree helpers built on std/runtime process exec.
//
// Every git subprocess these helpers spawn runs non-interactive by
// default: `GIT_TERMINAL_PROMPT=0` plus an empty askpass and an ssh
// `BatchMode=yes` transport. Harn runs worktree git from TTY-less
// contexts (`harn serve`, `@job`, CI); without this guard a clone/fetch
// that needs a credential or host-key decision would block on an
// interactive prompt and hang the runtime. The guard only disables
// *prompting* — credentials supplied via env/helper/agent still work —
// and is merged (`env_mode: "merge"`) so inherited PATH/HOME survive.
// Callers that genuinely want interactive git can override by passing
// their own `env` / `env_mode` in `options`.
//
// Import: import "std/worktree"
import { git_env_remove, git_noninteractive_env } from "std/git"
import { process_run, process_shell } from "std/runtime"
/**
* worktree_noninteractive_env returns the default prompt guard env applied
* to git subprocesses so credential/host-key prompts fail fast. Alias of
* `git_noninteractive_env` — the single source of truth for the guard.
*
* @effects: []
* @errors: []
*/
pub fn worktree_noninteractive_env() -> dict {
return git_noninteractive_env()
}
/**
* __worktree_git_options builds process.exec options for a git command in
* `cwd`: non-interactive guard env merged with the inherited environment,
* the std/git ambient-env strip, and any caller overrides from `options`.
*/
fn __worktree_git_options(cwd, options) -> dict {
let opts = options ?? {}
// Caller `env` overrides individual guard keys; `env_mode` defaults to
// "merge" so inherited PATH/HOME/credentials survive.
let env = git_noninteractive_env() + opts?.env ?? {}
var out = {
cwd: cwd,
env: env,
env_mode: opts?.env_mode ?? "merge",
env_remove: opts?.env_remove ?? git_env_remove(),
}
for key in ["timeout_ms", "stdin", "capture", "max_inline_bytes", "policy_context"] {
if opts[key] != nil {
out = out + {[key]: opts[key]}
}
}
return out
}
/**
* __worktree_git runs one argv-mode git command in `cwd` with the
* non-interactive guard applied. `args` omits the leading "git".
*/
fn __worktree_git(cwd, args, options) {
return process_run(["git"] + args, __worktree_git_options(cwd, options))
}
/**
* worktree_default_path.
*
* @effects: []
* @errors: []
*/
pub fn worktree_default_path(repo, name) {
return repo + "/.harn/worktrees/" + name
}
/**
* worktree_create.
*
* @effects: [host]
* @errors: []
*/
pub fn worktree_create(repo, name, base_ref, path, options = {}) {
let target = if path == nil || path == "" {
worktree_default_path(repo, name)
} else {
path
}
harness.fs.mkdir(repo + "/.harn")
harness.fs.mkdir(repo + "/.harn/worktrees")
let result = __worktree_git(repo, ["worktree", "add", "-B", name, target, base_ref], options)
return {repo: repo, name: name, path: target, base_ref: base_ref, result: result, success: result?.success}
}
/**
* worktree_remove.
*
* @effects: [host]
* @errors: []
*/
pub fn worktree_remove(repo, path, force, options = {}) {
if force {
return __worktree_git(repo, ["worktree", "remove", "--force", path], options)
}
return __worktree_git(repo, ["worktree", "remove", path], options)
}
/**
* worktree_status.
*
* @effects: [host]
* @errors: []
*/
pub fn worktree_status(path, options = {}) {
return __worktree_git(path, ["status", "--short", "--branch"], options)
}
/**
* worktree_diff.
*
* @effects: [host]
* @errors: []
*/
pub fn worktree_diff(path, base_ref, options = {}) {
if base_ref == nil || base_ref == "" {
return __worktree_git(path, ["diff", "--stat"], options)
}
return __worktree_git(path, ["diff", base_ref + "...HEAD"], options)
}
/**
* worktree_shell.
*
* @effects: [host]
* @errors: []
*/
pub fn worktree_shell(path, script, options = {}) {
return process_shell(script, __worktree_git_options(path, options))
}