293 lines
7.5 KiB
Bash
Executable File
293 lines
7.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Create one jj workspace per issue, optionally create bookmarks, and optionally open zellij tabs running pi.
|
|
|
|
Usage:
|
|
jj-workspace-fanout.sh [options] --issue "KEY=Title" [--issue "KEY=Title" ...]
|
|
|
|
Options:
|
|
--repo PATH Repo root (default: current directory)
|
|
--base REV Base revision/bookmark (default: master)
|
|
--remote NAME Git remote to fetch from (default: origin)
|
|
--issue KEY=TITLE Issue key and title (repeatable)
|
|
--session NAME Zellij session name (defaults to ZELLIJ_SESSION_NAME if set)
|
|
--open-pi Open a zellij tab per workspace and launch pi
|
|
--no-fetch Skip jj git fetch
|
|
--no-bookmarks Do not create feature/<issue> bookmarks
|
|
--keep-existing Skip creation for existing workspaces instead of failing
|
|
--reset-existing Forget and delete existing workspaces before recreating them
|
|
--prompt-suffix TEXT Extra text appended to each pi prompt
|
|
--pi-cmd CMD pi command to launch (default: pi)
|
|
--dry-run Print planned actions without making changes
|
|
--help Show this help
|
|
|
|
Examples:
|
|
jj-workspace-fanout.sh \
|
|
--repo /path/to/Phoenix \
|
|
--base master \
|
|
--issue "SPA-748=Wrap text in credits line items" \
|
|
--issue "SPA-754=Resize seat count picker"
|
|
|
|
jj-workspace-fanout.sh \
|
|
--repo /path/to/Phoenix \
|
|
--base master \
|
|
--session attio \
|
|
--open-pi \
|
|
--issue "SPA-748=Wrap text in credits line items"
|
|
EOF
|
|
}
|
|
|
|
require_cmd() {
|
|
if ! command -v "$1" >/dev/null 2>&1; then
|
|
echo "error: missing required command: $1" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
shell_escape() {
|
|
printf '%q' "$1"
|
|
}
|
|
|
|
log() {
|
|
printf '[jj-issue-workspaces] %s\n' "$*"
|
|
}
|
|
|
|
run() {
|
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
printf '[dry-run] '
|
|
printf '%q ' "$@"
|
|
printf '\n'
|
|
else
|
|
"$@"
|
|
fi
|
|
}
|
|
|
|
workspace_exists() {
|
|
local workspace_name="$1"
|
|
jj -R "$REPO" workspace list | awk -F: '{print $1}' | grep -Fxq "$workspace_name"
|
|
}
|
|
|
|
bookmark_exists() {
|
|
local workspace_dir="$1"
|
|
local bookmark_name="$2"
|
|
jj -R "$workspace_dir" bookmark list "$bookmark_name" 2>/dev/null | grep -Eq "^${bookmark_name}:"
|
|
}
|
|
|
|
close_tab_if_exists() {
|
|
local session_name="$1"
|
|
local tab_name="$2"
|
|
local tabs
|
|
|
|
tabs=$(zellij --session "$session_name" action query-tab-names 2>/dev/null || true)
|
|
if printf '%s\n' "$tabs" | grep -Fxq "$tab_name"; then
|
|
log "closing existing zellij tab $tab_name"
|
|
run zellij --session "$session_name" action go-to-tab-name "$tab_name"
|
|
run zellij --session "$session_name" action close-tab
|
|
fi
|
|
}
|
|
|
|
launch_pi_tab() {
|
|
local session_name="$1"
|
|
local tab_name="$2"
|
|
local workspace_dir="$3"
|
|
local prompt="$4"
|
|
local cmd
|
|
|
|
cmd="cd $(shell_escape "$workspace_dir") && pwd && $PI_CMD $(shell_escape "$prompt")"
|
|
|
|
close_tab_if_exists "$session_name" "$tab_name"
|
|
run zellij --session "$session_name" action new-tab --name "$tab_name"
|
|
run zellij --session "$session_name" action write-chars "$cmd"
|
|
run zellij --session "$session_name" action write 10
|
|
}
|
|
|
|
REPO="$(pwd)"
|
|
BASE="master"
|
|
REMOTE="origin"
|
|
SESSION="${ZELLIJ_SESSION_NAME:-}"
|
|
OPEN_PI=0
|
|
FETCH=1
|
|
CREATE_BOOKMARKS=1
|
|
KEEP_EXISTING=0
|
|
RESET_EXISTING=0
|
|
DRY_RUN=0
|
|
PROMPT_SUFFIX=""
|
|
PI_CMD="pi"
|
|
declare -a ISSUES=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--repo)
|
|
REPO="$2"
|
|
shift 2
|
|
;;
|
|
--base)
|
|
BASE="$2"
|
|
shift 2
|
|
;;
|
|
--remote)
|
|
REMOTE="$2"
|
|
shift 2
|
|
;;
|
|
--issue)
|
|
ISSUES+=("$2")
|
|
shift 2
|
|
;;
|
|
--session)
|
|
SESSION="$2"
|
|
shift 2
|
|
;;
|
|
--open-pi)
|
|
OPEN_PI=1
|
|
shift
|
|
;;
|
|
--no-fetch)
|
|
FETCH=0
|
|
shift
|
|
;;
|
|
--no-bookmarks)
|
|
CREATE_BOOKMARKS=0
|
|
shift
|
|
;;
|
|
--keep-existing)
|
|
KEEP_EXISTING=1
|
|
shift
|
|
;;
|
|
--reset-existing)
|
|
RESET_EXISTING=1
|
|
shift
|
|
;;
|
|
--prompt-suffix)
|
|
PROMPT_SUFFIX="$2"
|
|
shift 2
|
|
;;
|
|
--pi-cmd)
|
|
PI_CMD="$2"
|
|
shift 2
|
|
;;
|
|
--dry-run)
|
|
DRY_RUN=1
|
|
shift
|
|
;;
|
|
--help|-h)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "error: unknown argument: $1" >&2
|
|
usage >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ ${#ISSUES[@]} -eq 0 ]]; then
|
|
echo "error: at least one --issue KEY=TITLE is required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$KEEP_EXISTING" -eq 1 && "$RESET_EXISTING" -eq 1 ]]; then
|
|
echo "error: --keep-existing and --reset-existing cannot be combined" >&2
|
|
exit 1
|
|
fi
|
|
|
|
REPO="$(cd "$REPO" && pwd)"
|
|
PARENT_DIR="$(dirname "$REPO")"
|
|
REPO_BASENAME="$(basename "$REPO")"
|
|
|
|
require_cmd jj
|
|
if [[ "$OPEN_PI" -eq 1 ]]; then
|
|
require_cmd zellij
|
|
if [[ -z "$SESSION" ]]; then
|
|
echo "error: --open-pi requires --session <name> or ZELLIJ_SESSION_NAME" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if [[ ! -d "$REPO/.jj" ]]; then
|
|
echo "error: repo is not a jj repository: $REPO" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$FETCH" -eq 1 ]]; then
|
|
log "fetching $BASE from $REMOTE"
|
|
run jj -R "$REPO" git fetch --remote "$REMOTE" --branch "$BASE"
|
|
fi
|
|
|
|
log "validating base revision $BASE"
|
|
run jj -R "$REPO" log -r "$BASE" --no-pager
|
|
|
|
created_workspaces=()
|
|
|
|
for issue in "${ISSUES[@]}"; do
|
|
if [[ "$issue" != *=* ]]; then
|
|
echo "error: issue must be formatted as KEY=TITLE: $issue" >&2
|
|
exit 1
|
|
fi
|
|
|
|
issue_key="${issue%%=*}"
|
|
issue_title="${issue#*=}"
|
|
issue_slug="$(printf '%s' "$issue_key" | tr '[:upper:]' '[:lower:]')"
|
|
workspace_name="$issue_slug"
|
|
workspace_dir="$PARENT_DIR/${REPO_BASENAME}-${issue_slug}"
|
|
bookmark_name="feature/$issue_slug"
|
|
prompt="Work on ${issue_key}: ${issue_title}. You are in the dedicated jj workspace for this issue. First inspect the relevant code, identify the main components involved, and propose a short plan before editing."
|
|
|
|
if [[ -n "$PROMPT_SUFFIX" ]]; then
|
|
prompt+=" ${PROMPT_SUFFIX}"
|
|
fi
|
|
|
|
if workspace_exists "$workspace_name" || [[ -e "$workspace_dir" ]]; then
|
|
if [[ "$RESET_EXISTING" -eq 1 ]]; then
|
|
log "resetting existing workspace $workspace_name"
|
|
if workspace_exists "$workspace_name"; then
|
|
run jj -R "$REPO" workspace forget "$workspace_name"
|
|
fi
|
|
run rm -rf "$workspace_dir"
|
|
elif [[ "$KEEP_EXISTING" -eq 1 ]]; then
|
|
log "keeping existing workspace $workspace_name at $workspace_dir"
|
|
else
|
|
echo "error: workspace already exists: $workspace_name ($workspace_dir). Use --keep-existing or --reset-existing." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if ! workspace_exists "$workspace_name"; then
|
|
log "creating workspace $workspace_name at $workspace_dir"
|
|
run jj -R "$REPO" workspace add --name "$workspace_name" -r "$BASE" "$workspace_dir"
|
|
fi
|
|
|
|
if [[ "$CREATE_BOOKMARKS" -eq 1 ]]; then
|
|
log "ensuring bookmark $bookmark_name exists"
|
|
if bookmark_exists "$workspace_dir" "$bookmark_name"; then
|
|
run jj -R "$workspace_dir" bookmark set "$bookmark_name" -r @
|
|
else
|
|
run jj -R "$workspace_dir" bookmark create "$bookmark_name"
|
|
fi
|
|
fi
|
|
|
|
if [[ "$OPEN_PI" -eq 1 ]]; then
|
|
log "opening zellij tab $workspace_name in session $SESSION"
|
|
run launch_pi_tab "$SESSION" "$workspace_name" "$workspace_dir" "$prompt"
|
|
fi
|
|
|
|
created_workspaces+=("$workspace_name:$workspace_dir:$bookmark_name")
|
|
done
|
|
|
|
printf '\nCreated/updated workspaces:\n'
|
|
for item in "${created_workspaces[@]}"; do
|
|
IFS=':' read -r workspace_name workspace_dir bookmark_name <<<"$item"
|
|
printf ' - %s -> %s' "$workspace_name" "$workspace_dir"
|
|
if [[ "$CREATE_BOOKMARKS" -eq 1 ]]; then
|
|
printf ' [%s]' "$bookmark_name"
|
|
fi
|
|
printf '\n'
|
|
done
|
|
|
|
if [[ "$OPEN_PI" -eq 1 ]]; then
|
|
printf '\nZellij session: %s\n' "$SESSION"
|
|
fi
|