Files
opencode-sandbox/lib/worktree.sh
T
k.denisov 727feb6be3 Update agent instructions; check a worktree is clean before its pre-rm hook
- AGENTS.md and the skill: only the user's browser is off (xdg-open); browser tools run
  in the sandbox, headless or in a Wayland window; host-run project files are read-only
- wt rm: refuse a dirty worktree before wt-pre-rm runs, so its stack is not stopped for
  nothing
2026-10-30 01:41:08 -07:00

92 lines
3.5 KiB
Bash

# shellcheck shell=bash
# Worktrees for parallel agent sessions. Generic part here; project specifics (ports, .env,
# starting a stack, seeding) go into hooks:
# .opencode-sandbox/hooks/wt-post-add after `git worktree add`, cwd = the new worktree
# .opencode-sandbox/hooks/wt-pre-rm before removal (stop the stack), cwd = the worktree
# Hooks get WT_DIR, WT_BRANCH, WT_SLUG, MAIN_ROOT in the environment and the extra
# arguments of `wt add` / `wt rm` as their arguments. A failing hook aborts the command.
ocsb_wt_main_root() {
git -C "${OCSB_PROJECT_DIR}" rev-parse --path-format=absolute --git-common-dir | sed 's|/\.git$||'
}
ocsb_wt_slug() {
local branch=$1 slug=""
if [[ -n ${OCSB_WT_SLUG_REGEX} && ${branch} =~ ${OCSB_WT_SLUG_REGEX} ]]; then
slug=${BASH_REMATCH[1]}
fi
[[ -n ${slug} ]] || slug=$(printf '%s' "${branch}" | tr '/' '-' | tr -c 'a-zA-Z0-9._-' '-')
echo "${slug}"
}
ocsb_wt_hook() {
local name=$1 dir=$2
shift 2
local hook=${OCSB_PROJECT_DIR}/.opencode-sandbox/hooks/${name}
[[ -x ${hook} ]] || return 0
ocsb_info "hook ${name}"
(cd "${dir}" && "${hook}" "$@")
}
ocsb_wt_add() {
[[ $# -ge 1 ]] || ocsb_die "usage: opencode-sandbox wt add <branch> [hook args]"
local branch=$1 main slug dir base
shift
git check-ref-format --branch "${branch}" >/dev/null 2>&1 || ocsb_die "invalid branch name: ${branch}"
main=$(ocsb_wt_main_root)
slug=$(ocsb_wt_slug "${branch}")
dir=${OCSB_WORKTREES_DIR}/${OCSB_WT_PREFIX}${slug}
[[ -e ${dir} ]] && ocsb_die "already exists: ${dir}"
base=${OCSB_WT_BASE:-HEAD}
if [[ ${base} != HEAD ]] && git -C "${main}" rev-parse -q --verify "origin/${base}" >/dev/null; then
echo " ${base} is $(git -C "${main}" rev-list --count "${base}..origin/${base}") commit(s) behind origin/${base} (git fetch if needed)"
fi
mkdir -p "${OCSB_WORKTREES_DIR}"
git -C "${main}" worktree add -b "${branch}" "${dir}" "${base}"
export WT_DIR=${dir} WT_BRANCH=${branch} WT_SLUG=${slug} MAIN_ROOT=${main}
ocsb_wt_hook wt-post-add "${dir}" "$@"
echo "✓ worktree ${dir} (branch ${branch}); remove: opencode-sandbox wt rm ${slug}"
}
ocsb_wt_rm() {
[[ $# -ge 1 ]] || ocsb_die "usage: opencode-sandbox wt rm <name|path> [--force]"
local name=$1 force=0 main dir branch status a extra=()
shift
for a in "$@"; do
if [[ ${a} == --force ]]; then force=1; else extra+=("${a}"); fi
done
main=$(ocsb_wt_main_root)
case "${name}" in
/*) dir=${name} ;;
*) dir=${OCSB_WORKTREES_DIR}/${OCSB_WT_PREFIX}${name} ;;
esac
[[ -d ${dir} ]] || ocsb_die "no such worktree: ${dir}"
[[ $(realpath "${dir}") != "$(realpath "${main}")" ]] || ocsb_die "refusing to remove the main worktree"
branch=$(git -C "${dir}" rev-parse --abbrev-ref HEAD 2>/dev/null || true)
export WT_DIR=${dir} WT_BRANCH=${branch} WT_SLUG=${name##*/} MAIN_ROOT=${main}
status=$(git -C "${dir}" status --porcelain)
if [[ -n ${status} && ${force} -ne 1 ]]; then
echo "${status}" >&2
ocsb_die "worktree has uncommitted changes — commit them or pass --force (they will be lost)"
fi
ocsb_wt_hook wt-pre-rm "${dir}" "${extra[@]}"
if [[ ${force} -eq 1 ]]; then
git -C "${main}" worktree remove --force "${dir}"
else
git -C "${main}" worktree remove "${dir}"
fi
git -C "${main}" worktree prune
echo "✓ removed ${dir} (branch ${branch:-?} kept)"
}
ocsb_wt() {
local sub=${1:-list}
[[ $# -gt 0 ]] && shift
case "${sub}" in
add) ocsb_wt_add "$@" ;;
rm) ocsb_wt_rm "$@" ;;
list) git -C "${OCSB_PROJECT_DIR}" worktree list ;;
*) ocsb_die "wt: unknown subcommand ${sub} (add|rm|list)" ;;
esac
}