From ecc2f465a1537bee21b91e29739bca77b923473d Mon Sep 17 00:00:00 2001 From: Avi Fenesh Date: Tue, 24 Feb 2026 08:24:55 +0200 Subject: [PATCH 1/4] fix: handle gh pr merge in worktree context (ship#3) When /ship runs from a git worktree, commands like `git checkout`, `gh pr merge --delete-branch`, and `git branch -D` fail because worktrees cannot freely switch branches. This adds worktree detection (checking if .git is a file rather than a directory) and adapts: - Phase 6 merge: use remote-only strategy, fetch instead of checkout - Phase 11 cleanup: skip local branch ops in worktree mode - Cancel/cleanup: close PR and delete remote branch separately - Deployment (Phase 9): block with clear error, require main repo - Rollback: block with clear error, require main repo --- commands/ship-deployment.md | 20 +++++++++++++++++ commands/ship-error-handling.md | 20 ++++++++++++----- commands/ship.md | 39 ++++++++++++++++++++++++--------- 3 files changed, 64 insertions(+), 15 deletions(-) diff --git a/commands/ship-deployment.md b/commands/ship-deployment.md index 2de54c4..44b2f2e 100644 --- a/commands/ship-deployment.md +++ b/commands/ship-deployment.md @@ -186,6 +186,18 @@ Proceeding to production... ```bash echo "Merging $MAIN_BRANCH → $PROD_BRANCH..." +# Worktree check - multi-branch deployment requires branch checkout +IS_WORKTREE="false" +if [ -f "$(git rev-parse --show-toplevel)/.git" ]; then + IS_WORKTREE="true" +fi + +if [ "$IS_WORKTREE" = "true" ]; then + echo "[ERROR] Multi-branch deployment is not supported from a worktree" + echo "Run deployment from the main repo directory instead" + exit 1 +fi + git checkout $PROD_BRANCH git pull origin $PROD_BRANCH @@ -286,6 +298,14 @@ rollback_production() { echo "========================================" echo "ROLLBACK INITIATED" echo "========================================" + + # Worktree check + if [ -f "$(git rev-parse --show-toplevel)/.git" ]; then + echo "[ERROR] Rollback is not supported from a worktree" + echo "Run rollback from the main repo directory instead" + exit 1 + fi + echo "WARNING: Force pushing to $PROD_BRANCH to revert" git checkout $PROD_BRANCH diff --git a/commands/ship-error-handling.md b/commands/ship-error-handling.md index dc7cbe4..1f0f2f6 100644 --- a/commands/ship-error-handling.md +++ b/commands/ship-error-handling.md @@ -227,12 +227,22 @@ git push ### Cancel and Cleanup ```bash -# If you need to abandon the PR -gh pr close $PR_NUMBER --delete-branch +# Detect worktree +IS_WORKTREE="false" +if [ -f "$(git rev-parse --show-toplevel)/.git" ]; then + IS_WORKTREE="true" +fi -# Clean up local -git checkout $MAIN_BRANCH -git branch -D $CURRENT_BRANCH +if [ "$IS_WORKTREE" = "true" ]; then + # In worktree: close PR and delete remote branch separately + gh pr close $PR_NUMBER + git push origin --delete "$CURRENT_BRANCH" 2>/dev/null || true + echo "[OK] PR closed (worktree mode - local cleanup deferred)" +else + gh pr close $PR_NUMBER --delete-branch + git checkout $MAIN_BRANCH + git branch -D $CURRENT_BRANCH +fi ``` ## Exit Codes diff --git a/commands/ship.md b/commands/ship.md index c03a768..94daf05 100644 --- a/commands/ship.md +++ b/commands/ship.md @@ -348,17 +348,33 @@ fi echo "[OK] All comments resolved" -# 3. Merge with strategy (default: squash) -STRATEGY=${STRATEGY:-squash} -gh pr merge $PR_NUMBER --$STRATEGY --delete-branch +# 3. Detect if running from a worktree +IS_WORKTREE="false" +if [ -f "$(git rev-parse --show-toplevel)/.git" ]; then + IS_WORKTREE="true" + echo "[INFO] Running from worktree - using remote-only merge strategy" +fi -# Update local -git checkout $MAIN_BRANCH -git pull origin $MAIN_BRANCH +# 4. Merge with strategy (default: squash) +STRATEGY=${STRATEGY:-squash} +if [ "$IS_WORKTREE" = "true" ]; then + # In worktree: merge without --delete-branch (it tries to checkout main locally) + gh pr merge $PR_NUMBER --$STRATEGY --repo "$OWNER/$REPO" + # Delete remote branch separately + git push origin --delete "$CURRENT_BRANCH" 2>/dev/null || true + # Fetch to get merge SHA without checking out main + git fetch origin $MAIN_BRANCH + MERGE_SHA=$(git rev-parse origin/$MAIN_BRANCH) +else + gh pr merge $PR_NUMBER --$STRATEGY --delete-branch + # Update local + git checkout $MAIN_BRANCH + git pull origin $MAIN_BRANCH + MERGE_SHA=$(git rev-parse HEAD) +fi # Update repo-map if it exists (non-blocking) node -e "const { getPluginRoot } = require('@agentsys/lib/cross-platform'); const pluginRoot = getPluginRoot('ship'); if (!pluginRoot) { console.log('Plugin root not found, skipping repo-map'); process.exit(0); } const repoMap = require(\`\${pluginRoot}/lib/repo-map\`); if (repoMap.exists(process.cwd())) { repoMap.update(process.cwd(), {}).then(() => console.log('[OK] Repo-map updated')).catch((e) => console.log('[WARN] Repo-map update failed: ' + e.message)); } else { console.log('Repo-map not found, skipping'); }" || true -MERGE_SHA=$(git rev-parse HEAD) echo "[OK] Merged PR #$PR_NUMBER at $MERGE_SHA" ``` @@ -438,9 +454,12 @@ if (workflowState) { ### Local Branch Cleanup ```bash -git checkout $MAIN_BRANCH -# Feature branch already deleted by --delete-branch -git branch -D $CURRENT_BRANCH 2>/dev/null || true +if [ "$IS_WORKTREE" = "true" ]; then + echo "[OK] Skipping local branch cleanup (worktree mode)" +else + git checkout $MAIN_BRANCH + git branch -D $CURRENT_BRANCH 2>/dev/null || true +fi ``` ## Phase 12: Completion Report From 7d13d1d12117c135e50985795c23d4c06af2419a Mon Sep 17 00:00:00 2001 From: Avi Fenesh Date: Tue, 24 Feb 2026 08:30:07 +0200 Subject: [PATCH 2/4] fix: address review findings for worktree merge handling - Get merge SHA from PR metadata instead of fetch (avoids race condition) - Add OWNER/REPO validation before merge command - Move worktree checks before output in deployment/rollback - Add fallback worktree detection in Phase 11 cleanup - Surface branch deletion warnings instead of silent suppression - Add 2>/dev/null to git rev-parse in error paths - Include main repo path in deployment error messages --- commands/ship-deployment.md | 31 ++++++++++++++----------------- commands/ship-error-handling.md | 6 +++--- commands/ship.md | 22 +++++++++++++++++----- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/commands/ship-deployment.md b/commands/ship-deployment.md index 44b2f2e..9429111 100644 --- a/commands/ship-deployment.md +++ b/commands/ship-deployment.md @@ -184,20 +184,16 @@ Proceeding to production... ### Merge to Production Branch ```bash -echo "Merging $MAIN_BRANCH → $PROD_BRANCH..." - -# Worktree check - multi-branch deployment requires branch checkout -IS_WORKTREE="false" -if [ -f "$(git rev-parse --show-toplevel)/.git" ]; then - IS_WORKTREE="true" -fi - -if [ "$IS_WORKTREE" = "true" ]; then +# Worktree check must come first - multi-branch deployment requires branch checkout +if [ -f "$(git rev-parse --show-toplevel 2>/dev/null)/.git" ]; then + MAIN_REPO=$(git rev-parse --git-common-dir 2>/dev/null | sed 's|/\.git.*|.|') echo "[ERROR] Multi-branch deployment is not supported from a worktree" - echo "Run deployment from the main repo directory instead" + echo "Run from the main repo: cd $MAIN_REPO" exit 1 fi +echo "Merging $MAIN_BRANCH → $PROD_BRANCH..." + git checkout $PROD_BRANCH git pull origin $PROD_BRANCH @@ -295,17 +291,18 @@ fi ```bash rollback_production() { - echo "========================================" - echo "ROLLBACK INITIATED" - echo "========================================" - - # Worktree check - if [ -f "$(git rev-parse --show-toplevel)/.git" ]; then + # Worktree check must come first + if [ -f "$(git rev-parse --show-toplevel 2>/dev/null)/.git" ]; then + MAIN_REPO=$(git rev-parse --git-common-dir 2>/dev/null | sed 's|/\.git.*|.|') echo "[ERROR] Rollback is not supported from a worktree" - echo "Run rollback from the main repo directory instead" + echo "Run from the main repo: cd $MAIN_REPO" exit 1 fi + echo "========================================" + echo "ROLLBACK INITIATED" + echo "========================================" + echo "WARNING: Force pushing to $PROD_BRANCH to revert" git checkout $PROD_BRANCH diff --git a/commands/ship-error-handling.md b/commands/ship-error-handling.md index 1f0f2f6..534cc2d 100644 --- a/commands/ship-error-handling.md +++ b/commands/ship-error-handling.md @@ -229,15 +229,15 @@ git push ```bash # Detect worktree IS_WORKTREE="false" -if [ -f "$(git rev-parse --show-toplevel)/.git" ]; then +if [ -f "$(git rev-parse --show-toplevel 2>/dev/null)/.git" ]; then IS_WORKTREE="true" fi if [ "$IS_WORKTREE" = "true" ]; then # In worktree: close PR and delete remote branch separately gh pr close $PR_NUMBER - git push origin --delete "$CURRENT_BRANCH" 2>/dev/null || true - echo "[OK] PR closed (worktree mode - local cleanup deferred)" + git push origin --delete "$CURRENT_BRANCH" 2>&1 || echo "[WARN] Remote branch deletion failed - may need manual cleanup" + echo "[OK] PR closed (worktree mode - local cleanup deferred to worktree removal)" else gh pr close $PR_NUMBER --delete-branch git checkout $MAIN_BRANCH diff --git a/commands/ship.md b/commands/ship.md index 94daf05..5748fd3 100644 --- a/commands/ship.md +++ b/commands/ship.md @@ -357,14 +357,18 @@ fi # 4. Merge with strategy (default: squash) STRATEGY=${STRATEGY:-squash} +if [ -z "$OWNER" ] || [ -z "$REPO" ]; then + echo "[ERROR] Failed to extract repo owner/name" + exit 1 +fi + if [ "$IS_WORKTREE" = "true" ]; then # In worktree: merge without --delete-branch (it tries to checkout main locally) gh pr merge $PR_NUMBER --$STRATEGY --repo "$OWNER/$REPO" # Delete remote branch separately - git push origin --delete "$CURRENT_BRANCH" 2>/dev/null || true - # Fetch to get merge SHA without checking out main - git fetch origin $MAIN_BRANCH - MERGE_SHA=$(git rev-parse origin/$MAIN_BRANCH) + git push origin --delete "$CURRENT_BRANCH" 2>&1 || echo "[WARN] Remote branch deletion failed - may need manual cleanup" + # Get merge SHA from the PR metadata (avoids race condition with fetch) + MERGE_SHA=$(gh pr view $PR_NUMBER --repo "$OWNER/$REPO" --json mergeCommit --jq '.mergeCommit.oid') else gh pr merge $PR_NUMBER --$STRATEGY --delete-branch # Update local @@ -454,8 +458,16 @@ if (workflowState) { ### Local Branch Cleanup ```bash +# Re-detect worktree in case Phase 6 was skipped or run separately +if [ -z "$IS_WORKTREE" ]; then + IS_WORKTREE="false" + if [ -f "$(git rev-parse --show-toplevel)/.git" ]; then + IS_WORKTREE="true" + fi +fi + if [ "$IS_WORKTREE" = "true" ]; then - echo "[OK] Skipping local branch cleanup (worktree mode)" + echo "[OK] Local branch cleanup deferred (worktree mode - cleaned when worktree is removed)" else git checkout $MAIN_BRANCH git branch -D $CURRENT_BRANCH 2>/dev/null || true From 2ef96a1b82c29ce0729526d84422fcfac9f2a29e Mon Sep 17 00:00:00 2001 From: Avi Fenesh Date: Tue, 24 Feb 2026 08:31:54 +0200 Subject: [PATCH 3/4] fix: add 2>/dev/null to git rev-parse calls, use dirname for path extraction --- commands/ship-deployment.md | 4 ++-- commands/ship.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/commands/ship-deployment.md b/commands/ship-deployment.md index 9429111..51357ca 100644 --- a/commands/ship-deployment.md +++ b/commands/ship-deployment.md @@ -186,7 +186,7 @@ Proceeding to production... ```bash # Worktree check must come first - multi-branch deployment requires branch checkout if [ -f "$(git rev-parse --show-toplevel 2>/dev/null)/.git" ]; then - MAIN_REPO=$(git rev-parse --git-common-dir 2>/dev/null | sed 's|/\.git.*|.|') + MAIN_REPO=$(dirname "$(git rev-parse --git-common-dir 2>/dev/null)") echo "[ERROR] Multi-branch deployment is not supported from a worktree" echo "Run from the main repo: cd $MAIN_REPO" exit 1 @@ -293,7 +293,7 @@ fi rollback_production() { # Worktree check must come first if [ -f "$(git rev-parse --show-toplevel 2>/dev/null)/.git" ]; then - MAIN_REPO=$(git rev-parse --git-common-dir 2>/dev/null | sed 's|/\.git.*|.|') + MAIN_REPO=$(dirname "$(git rev-parse --git-common-dir 2>/dev/null)") echo "[ERROR] Rollback is not supported from a worktree" echo "Run from the main repo: cd $MAIN_REPO" exit 1 diff --git a/commands/ship.md b/commands/ship.md index 5748fd3..a4ecb50 100644 --- a/commands/ship.md +++ b/commands/ship.md @@ -350,7 +350,7 @@ echo "[OK] All comments resolved" # 3. Detect if running from a worktree IS_WORKTREE="false" -if [ -f "$(git rev-parse --show-toplevel)/.git" ]; then +if [ -f "$(git rev-parse --show-toplevel 2>/dev/null)/.git" ]; then IS_WORKTREE="true" echo "[INFO] Running from worktree - using remote-only merge strategy" fi @@ -461,7 +461,7 @@ if (workflowState) { # Re-detect worktree in case Phase 6 was skipped or run separately if [ -z "$IS_WORKTREE" ]; then IS_WORKTREE="false" - if [ -f "$(git rev-parse --show-toplevel)/.git" ]; then + if [ -f "$(git rev-parse --show-toplevel 2>/dev/null)/.git" ]; then IS_WORKTREE="true" fi fi From 79df24d4e6cd60ce4388b61cb80eb87be4e0ab35 Mon Sep 17 00:00:00 2001 From: Avi Fenesh Date: Tue, 24 Feb 2026 08:39:51 +0200 Subject: [PATCH 4/4] docs: add changelog entry for worktree merge fix --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 893769a..0cf2d43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [Unreleased] + +- fix: handle `gh pr merge` in worktree context - detect worktree and use remote-only merge strategy to avoid `fatal: 'main' is already used by worktree` error (#3) + ## [1.0.0] - 2026-02-21 Initial release. Extracted from [agentsys](https://github.com/agent-sh/agentsys) monorepo.