|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | +# worktree-sync-from-main.sh — Sync a worktree branch with the latest main |
| 4 | +# |
| 5 | +# Fetches origin/main and merges it into the current worktree branch, then |
| 6 | +# fetches the latest ticket state from origin/tickets so the worktree's |
| 7 | +# .tickets-tracker/ is current. |
| 8 | +# |
| 9 | +# Referenced from WORKTREE-GUIDE.md and sprint/SKILL.md Step 3. |
| 10 | +# Run before launching sub-agent batches to ensure ticket state and code are current. |
| 11 | +# |
| 12 | +# Usage: |
| 13 | +# worktree-sync-from-main.sh [--skip-tickets] [--skip-code] |
| 14 | +# |
| 15 | +# Options: |
| 16 | +# --skip-tickets Skip syncing the tickets branch |
| 17 | +# --skip-code Skip merging origin/main into the worktree branch |
| 18 | +# --help Print this usage message and exit |
| 19 | +# |
| 20 | +# Exit codes: |
| 21 | +# 0 — Sync complete (or no-op if already up to date) |
| 22 | +# 1 — Fatal error (merge conflict that could not be auto-resolved; not in worktree) |
| 23 | +# |
| 24 | +# Notes: |
| 25 | +# - Must be run from inside a worktree (i.e., .git is a file, not a directory) |
| 26 | +# - Non-ticket merge conflicts are reported and exit 1 so the caller can |
| 27 | +# invoke /dso:resolve-conflicts before continuing |
| 28 | +# - Ticket sync failures are non-fatal (stale ticket state < blocked batch) |
| 29 | + |
| 30 | +set -euo pipefail |
| 31 | + |
| 32 | +# ── Argument parsing ───────────────────────────────────────────────────────── |
| 33 | + |
| 34 | +SKIP_TICKETS=0 |
| 35 | +SKIP_CODE=0 |
| 36 | + |
| 37 | +for arg in "$@"; do |
| 38 | + case "$arg" in |
| 39 | + --skip-tickets) SKIP_TICKETS=1 ;; |
| 40 | + --skip-code) SKIP_CODE=1 ;; |
| 41 | + --help) |
| 42 | + cat <<'USAGE' |
| 43 | +Usage: worktree-sync-from-main.sh [--skip-tickets] [--skip-code] |
| 44 | +
|
| 45 | + --skip-tickets Skip syncing the tickets branch |
| 46 | + --skip-code Skip merging origin/main into the worktree branch |
| 47 | + --help Print this usage message and exit |
| 48 | +
|
| 49 | +Syncs the current worktree branch with the latest origin/main and pulls the |
| 50 | +latest ticket state from origin/tickets into .tickets-tracker/. |
| 51 | +USAGE |
| 52 | + exit 0 |
| 53 | + ;; |
| 54 | + *) |
| 55 | + echo "ERROR: Unknown option: $arg" >&2 |
| 56 | + echo "Run '$0 --help' for usage." >&2 |
| 57 | + exit 1 |
| 58 | + ;; |
| 59 | + esac |
| 60 | +done |
| 61 | + |
| 62 | +# ── Locate repo root ───────────────────────────────────────────────────────── |
| 63 | + |
| 64 | +REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "") |
| 65 | +if [ -z "$REPO_ROOT" ]; then |
| 66 | + echo "ERROR: Not inside a git repository." >&2 |
| 67 | + exit 1 |
| 68 | +fi |
| 69 | + |
| 70 | +# Must be called from a worktree (.git is a file, not a directory) |
| 71 | +if [ -d "$REPO_ROOT/.git" ]; then |
| 72 | + echo "ERROR: Not inside a worktree. Run from a worktree session, not the main repo." >&2 |
| 73 | + exit 1 |
| 74 | +fi |
| 75 | + |
| 76 | +CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "") |
| 77 | +if [ -z "$CURRENT_BRANCH" ]; then |
| 78 | + echo "ERROR: Could not determine current branch (detached HEAD?)." >&2 |
| 79 | + exit 1 |
| 80 | +fi |
| 81 | + |
| 82 | +echo "Syncing worktree '$CURRENT_BRANCH' from main..." >&2 |
| 83 | + |
| 84 | +# ── 1) Merge origin/main into the worktree branch ──────────────────────────── |
| 85 | + |
| 86 | +if [ "$SKIP_CODE" -eq 0 ]; then |
| 87 | + echo " Fetching origin/main..." >&2 |
| 88 | + if ! git fetch origin main --quiet 2>/dev/null; then |
| 89 | + echo " WARNING: git fetch origin main failed — skipping code sync." >&2 |
| 90 | + else |
| 91 | + # Check if already up to date |
| 92 | + LOCAL_SHA=$(git rev-parse HEAD) |
| 93 | + ORIGIN_MAIN_SHA=$(git rev-parse origin/main 2>/dev/null || echo "") |
| 94 | + MERGE_BASE=$(git merge-base HEAD origin/main 2>/dev/null || echo "") |
| 95 | + |
| 96 | + if [ "$MERGE_BASE" = "$ORIGIN_MAIN_SHA" ]; then |
| 97 | + echo " OK: Worktree branch is already up to date with origin/main." >&2 |
| 98 | + else |
| 99 | + echo " Merging origin/main into $CURRENT_BRANCH..." >&2 |
| 100 | + if ! git merge origin/main --no-edit -q 2>&1; then |
| 101 | + echo "ERROR: Merge of origin/main failed. Resolve conflicts then re-run." >&2 |
| 102 | + echo " Use /dso:resolve-conflicts for guided conflict resolution." >&2 |
| 103 | + exit 1 |
| 104 | + fi |
| 105 | + echo " OK: Merged origin/main into $CURRENT_BRANCH." >&2 |
| 106 | + fi |
| 107 | + fi |
| 108 | +else |
| 109 | + echo " Skipping code sync (--skip-code)." >&2 |
| 110 | +fi |
| 111 | + |
| 112 | +# ── 2) Sync ticket state from origin/tickets ───────────────────────────────── |
| 113 | + |
| 114 | +if [ "$SKIP_TICKETS" -eq 0 ]; then |
| 115 | + # Locate .tickets-tracker/ — it is a git worktree on the tickets branch |
| 116 | + # mounted relative to the main (non-worktree) checkout. From a worktree, |
| 117 | + # git rev-parse --git-common-dir points to the main repo's .git directory. |
| 118 | + MAIN_GIT_DIR=$(git rev-parse --git-common-dir 2>/dev/null || echo "") |
| 119 | + MAIN_REPO="" |
| 120 | + if [ -n "$MAIN_GIT_DIR" ]; then |
| 121 | + MAIN_REPO=$(dirname "$MAIN_GIT_DIR") |
| 122 | + fi |
| 123 | + |
| 124 | + TRACKER_DIR="" |
| 125 | + if [ -n "$MAIN_REPO" ] && [ -d "$MAIN_REPO/.tickets-tracker" ]; then |
| 126 | + TRACKER_DIR="$MAIN_REPO/.tickets-tracker" |
| 127 | + elif [ -d "$REPO_ROOT/.tickets-tracker" ]; then |
| 128 | + TRACKER_DIR="$REPO_ROOT/.tickets-tracker" |
| 129 | + fi |
| 130 | + |
| 131 | + if [ -z "$TRACKER_DIR" ]; then |
| 132 | + echo " INFO: .tickets-tracker/ not found — skipping ticket sync." >&2 |
| 133 | + elif ! git -C "$TRACKER_DIR" rev-parse --verify tickets &>/dev/null; then |
| 134 | + echo " INFO: tickets branch not found in .tickets-tracker/ — skipping ticket sync." >&2 |
| 135 | + else |
| 136 | + echo " Syncing tickets branch from origin..." >&2 |
| 137 | + if git -C "$TRACKER_DIR" fetch origin tickets --quiet 2>/dev/null; then |
| 138 | + REMOTE_EXISTS=$(git -C "$TRACKER_DIR" rev-parse --verify origin/tickets 2>/dev/null || echo "") |
| 139 | + if [ -n "$REMOTE_EXISTS" ]; then |
| 140 | + if git -C "$TRACKER_DIR" pull --rebase origin tickets --quiet 2>/dev/null; then |
| 141 | + echo " OK: Ticket state synced from origin/tickets." >&2 |
| 142 | + else |
| 143 | + echo " WARNING: Ticket rebase from origin/tickets failed — continuing with local state." >&2 |
| 144 | + fi |
| 145 | + else |
| 146 | + echo " INFO: origin/tickets not available — using local ticket state." >&2 |
| 147 | + fi |
| 148 | + else |
| 149 | + echo " WARNING: git fetch origin tickets failed — using local ticket state." >&2 |
| 150 | + fi |
| 151 | + fi |
| 152 | +else |
| 153 | + echo " Skipping ticket sync (--skip-tickets)." >&2 |
| 154 | +fi |
| 155 | + |
| 156 | +echo "Sync complete." >&2 |
0 commit comments