# 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 [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 [--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 }