diff --git a/.changeset/manual-release-a6fb057f.md b/.changeset/manual-release-a6fb057f.md new file mode 100644 index 00000000..64baf196 --- /dev/null +++ b/.changeset/manual-release-a6fb057f.md @@ -0,0 +1,5 @@ +--- +'@link-assistant/hive-mind': patch +--- + +Fix branch checkout failure when PR is from fork with different naming convention diff --git a/docs/case-studies/issue-1217/README.md b/docs/case-studies/issue-1217/README.md new file mode 100644 index 00000000..1b8f1b01 --- /dev/null +++ b/docs/case-studies/issue-1217/README.md @@ -0,0 +1,253 @@ +# Case Study: BRANCH CHECKOUT FAILED - Fork PR Branch Resolution Bug + +**Issue:** [#1217](https://github.com/link-assistant/hive-mind/issues/1217) +**Related PR:** [objectionary/eo2js#154](https://github.com/objectionary/eo2js/pull/154) +**Status:** Analysis Complete +**Date:** 2026-02-05 + +## Executive Summary + +The hive-mind `solve` command fails to checkout a PR branch when: + +1. The PR comes from another user's fork (e.g., `skulidropek/eo2js`) +2. The current user has their own fork with a different naming convention (e.g., `konard/objectionary-eo2js`) +3. The `--fork` flag is used in continue mode + +The bug occurs because the code incorrectly constructs the `pr-fork` remote URL using a naming convention that may not match the actual fork repository name. + +## Timeline of Events + +### 2026-02-05T09:18:29Z - Solution Draft Initiated + +- solve.mjs v1.15.1 started processing PR https://github.com/objectionary/eo2js/pull/154 + +### 2026-02-05T09:18:36Z - PR Analysis + +- PR #154 detected as fork PR from `skulidropek/eo2js` +- Branch name: `issues/117` +- Fork owner: `skulidropek` + +### 2026-02-05T09:18:38Z - Fork Conflict Check + +- Verified no fork conflict between `konard/objectionary-eo2js` and `objectionary/eo2js` +- Fork exists: `konard/objectionary-eo2js` +- Fork parent validated: `objectionary/eo2js` + +### 2026-02-05T09:18:43Z - PR Fork Remote Setup Fails + +- Code attempted to add `pr-fork` remote +- **BUG:** Constructed URL as `skulidropek/objectionary-eo2js` (non-existent) +- **Actual repository:** `skulidropek/eo2js` +- Error: `remote: Repository not found.` + +### 2026-02-05T09:18:44Z - Branch Checkout Fails + +- Branch `issues/117` not found in any accessible remote +- Git error: `fatal: 'origin/issues/117' is not a commit and a branch 'issues/117' cannot be created from it` + +## Root Cause Analysis + +### Primary Bug Location + +**File:** `src/solve.repository.lib.mjs` +**Function:** `setupPrForkRemote()` +**Lines:** 1183-1196 + +### The Bug + +```javascript +// Line 1183-1187: Incorrectly constructs fork repo name +let prForkRepoName = repo; +if (owner && argv.prefixForkNameWithOwnerName) { + // When prefix option is enabled, try prefixed name first + prForkRepoName = `${owner}-${repo}`; // This creates "objectionary-eo2js" +} +``` + +The problem: + +1. `argv.prefixForkNameWithOwnerName` applies to the **current user's** fork naming preference +2. This flag is being incorrectly applied to **another user's** fork (`skulidropek`) +3. Another user's fork name is independent of the current user's naming preferences + +### Why This Happens + +When `--fork` flag is used and the PR is from another user's fork: + +1. The code correctly identifies that `skulidropek` is the PR fork owner +2. It uses `--prefix-fork-name-with-owner-name` (or auto-fork behavior) creating `konard/objectionary-eo2js` +3. When setting up `pr-fork` remote, it **incorrectly assumes** `skulidropek` also used the same naming convention +4. It constructs `skulidropek/objectionary-eo2js` which doesn't exist (actual: `skulidropek/eo2js`) + +### Reproduction Steps + +1. Have a fork with prefixed name: `{your-user}/{owner}-{repo}` (e.g., `konard/objectionary-eo2js`) +2. Try to continue working on a PR from another fork: `skulidropek/eo2js#154` +3. Use the `--fork` flag + +```bash +./solve.mjs "https://github.com/objectionary/eo2js/pull/154" --fork +``` + +## Impact Assessment + +### Severity: Medium + +- **Workaround available:** Users can avoid using `--fork` flag for PRs from other forks +- **Limited scope:** Only affects continue mode with `--fork` flag on PRs from other users' forks +- **No data loss:** Fails early with clear error message + +### Affected Workflows + +1. AI-powered solution drafts continuing work on external PRs +2. Collaborative PR reviews using fork-based workflows +3. Any automation using `solve` to continue PRs from different fork owners + +## Proposed Solutions + +### Solution 1: Query GitHub API for Actual Fork Name (Recommended) + +Instead of guessing the fork name based on current user's preferences, query the GitHub API to get the actual repository name: + +```javascript +export const setupPrForkRemote = async (tempDir, argv, prForkOwner, repo, isContinueMode, owner = null) => { + // ... existing validation code ... + + // NEW: Query GitHub API to find actual fork name + const forkSearchResult = await $`gh api repos/${owner}/${repo}/forks --paginate --jq '.[] | select(.owner.login == "${prForkOwner}") | .name'`; + + let prForkRepoName = repo; // Default to standard name + if (forkSearchResult.code === 0 && forkSearchResult.stdout) { + prForkRepoName = forkSearchResult.stdout.toString().trim() || repo; + } + + // Verify the fork exists before adding remote + const forkVerifyResult = await $`gh repo view ${prForkOwner}/${prForkRepoName} --json name 2>/dev/null`; + if (forkVerifyResult.code !== 0) { + // Try alternative: search for any fork owned by prForkOwner + const searchResult = await $`gh api search/repositories --jq '.items[0].name' -f q="fork:true user:${prForkOwner} ${repo}"`; + if (searchResult.code === 0 && searchResult.stdout) { + prForkRepoName = searchResult.stdout.toString().trim(); + } + } + + // ... rest of the function ... +}; +``` + +### Solution 2: Direct PR Reference Checkout + +Use GitHub's special PR refs to checkout directly without needing the fork: + +```javascript +// Fetch the PR directly using GitHub's special refs +await $({ cwd: tempDir })`git fetch origin pull/${prNumber}/head:${branchName}`; +await $({ cwd: tempDir })`git checkout ${branchName}`; +``` + +This approach: + +- Works regardless of fork naming +- Doesn't require access to the contributor's fork +- Is the standard way to checkout PR branches + +### Solution 3: Try Multiple Fork Naming Conventions + +```javascript +const possibleForkNames = [ + repo, // Standard: "eo2js" + `${owner}-${repo}`, // Prefixed: "objectionary-eo2js" + repo.replace(/-/g, ''), // No dashes variant +]; + +let actualForkName = null; +for (const candidate of possibleForkNames) { + const result = await $`gh repo view ${prForkOwner}/${candidate} --json name 2>/dev/null`; + if (result.code === 0) { + actualForkName = candidate; + break; + } +} +``` + +## Existing Components/Libraries + +### 1. GitHub CLI (`gh`) + +- `gh api repos/{owner}/{repo}/forks` - List forks to find actual fork name +- `gh pr checkout {number}` - Direct PR checkout (handles fork branches automatically) +- `gh repo view` - Verify repository existence + +### 2. Git Pull Request Refs + +GitHub provides special refs for all PRs: + +- `refs/pull/{number}/head` - The PR branch +- `refs/pull/{number}/merge` - Merge commit preview + +These can be fetched directly without knowing the fork name. + +### 3. Similar Issue References + +- [GitHub Community: Checkout a branch from a fork](https://github.com/orgs/community/discussions/23445) +- [GitHub Docs: Checking out pull requests locally](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally) + +## Recommendations + +### Immediate Fix (Solution 2) + +Use Git's PR refs for checkout - simplest, most reliable solution: + +```javascript +// In checkoutPrBranch() for PRs from other forks +if (prNumber) { + await $({ cwd: tempDir })`git fetch origin pull/${prNumber}/head:${branchName}`; + return await $({ cwd: tempDir })`git checkout ${branchName}`; +} +``` + +### Long-term Improvement (Solution 1) + +Implement proper fork discovery via GitHub API to handle all edge cases. + +### Testing + +Add test cases for: + +1. Standard fork name (`user/repo`) +2. Prefixed fork name (`user/owner-repo`) +3. Custom fork name (renamed after creation) +4. Fork doesn't exist (deleted) + +## Files to Modify + +1. **`src/solve.repository.lib.mjs`** + - `setupPrForkRemote()` - Fix fork name construction + - `checkoutPrBranch()` - Add PR ref checkout as fallback + +2. **`src/solve.branch.lib.mjs`** + - `createOrCheckoutBranch()` - Pass prNumber for PR ref checkout + +3. **Tests** + - Add unit tests for fork name resolution + - Add integration tests for cross-fork PR checkout + +## Appendix + +### Full Error Log + +See [./failure-log.md](./failure-log.md) for the complete failure log from the original incident. + +### Repository Structure + +``` +objectionary/eo2js (upstream) +├── skulidropek/eo2js (PR author's fork) - ACTUAL +│ └── branch: issues/117 (PR #154) +└── konard/objectionary-eo2js (current user's fork) - DIFFERENT NAMING +``` + +### Related Issues + +- This case study follows the pattern established in issue #967 (fork parent mismatch detection) +- The solution should be consistent with existing fork handling in `validateForkParent()` diff --git a/docs/case-studies/issue-1217/failure-log.md b/docs/case-studies/issue-1217/failure-log.md new file mode 100644 index 00000000..aeef9505 --- /dev/null +++ b/docs/case-studies/issue-1217/failure-log.md @@ -0,0 +1,194 @@ +# Failure Log - solve.mjs Branch Checkout Error + +**Source:** [PR Comment #3852172323](https://github.com/objectionary/eo2js/pull/154#issuecomment-3852172323) +**Date:** 2026-02-05T09:18:29.306Z +**Version:** solve v1.15.1 + +## Full Log + +``` +# Solve.mjs Log - 2026-02-05T09:18:29.306Z + +[2026-02-05T09:18:29.307Z] [INFO] 📁 Log file: /home/hive/solve-2026-02-05T09-18-29-306Z.log +[2026-02-05T09:18:29.308Z] [INFO] (All output will be logged here) +[2026-02-05T09:18:29.975Z] [INFO] +[2026-02-05T09:18:29.976Z] [INFO] 🚀 solve v1.15.1 +[2026-02-05T09:18:29.976Z] [INFO] 🔧 Raw command executed: +[2026-02-05T09:18:29.977Z] [INFO] /home/hive/.nvm/versions/node/v20.20.0/bin/node /home/hive/.bun/bin/solve https://github.com/objectionary/eo2js/pull/154 --model sonnet --attach-logs --verbose --no-tool-check --auto-resume-on-limit-reset --tokens-budget-stats +[2026-02-05T09:18:29.977Z] [INFO] +[2026-02-05T09:18:29.997Z] [INFO] +[2026-02-05T09:18:29.998Z] [WARNING] ⚠️ SECURITY WARNING: --attach-logs is ENABLED +[2026-02-05T09:18:29.998Z] [INFO] +[2026-02-05T09:18:29.998Z] [INFO] This option will upload the complete solution draft log file to the Pull Request. +[2026-02-05T09:18:29.999Z] [INFO] The log may contain sensitive information such as: +[2026-02-05T09:18:29.999Z] [INFO] • API keys, tokens, or secrets +[2026-02-05T09:18:29.999Z] [INFO] • File paths and directory structures +[2026-02-05T09:18:29.999Z] [INFO] • Command outputs and error messages +[2026-02-05T09:18:29.999Z] [INFO] • Internal system information +[2026-02-05T09:18:29.999Z] [INFO] +[2026-02-05T09:18:29.999Z] [INFO] ⚠️ DO NOT use this option with public repositories or if the log +[2026-02-05T09:18:30.000Z] [INFO] might contain sensitive data that should not be shared publicly. +[2026-02-05T09:18:30.000Z] [INFO] +[2026-02-05T09:18:30.000Z] [INFO] Continuing in 5 seconds... (Press Ctrl+C to abort) +[2026-02-05T09:18:30.000Z] [INFO] +[2026-02-05T09:18:35.007Z] [INFO] +[2026-02-05T09:18:35.035Z] [INFO] 💾 Disk space check: 51431MB available (2048MB required) ✅ +[2026-02-05T09:18:35.037Z] [INFO] 🧠 Memory check: 10770MB available, swap: 4095MB (0MB used), total: 14865MB (256MB required) ✅ +[2026-02-05T09:18:35.056Z] [INFO] ⏩ Skipping tool connection validation (dry-run mode or skip-tool-connection-check enabled) +[2026-02-05T09:18:35.057Z] [INFO] ⏩ Skipping GitHub authentication check (dry-run mode or skip-tool-connection-check enabled) +[2026-02-05T09:18:35.057Z] [INFO] 📋 URL validation: +[2026-02-05T09:18:35.058Z] [INFO] Input URL: https://github.com/objectionary/eo2js/pull/154 +[2026-02-05T09:18:35.058Z] [INFO] Is Issue URL: false +[2026-02-05T09:18:35.058Z] [INFO] Is PR URL: true +[2026-02-05T09:18:35.059Z] [INFO] 🔍 Checking repository access for auto-fork... +[2026-02-05T09:18:35.832Z] [INFO] Repository visibility: public +[2026-02-05T09:18:35.832Z] [INFO] ✅ Auto-fork: No write access detected, enabling fork mode +[2026-02-05T09:18:35.833Z] [INFO] ✅ Repository access check: Skipped (fork mode enabled) +[2026-02-05T09:18:36.265Z] [INFO] Repository visibility: public +[2026-02-05T09:18:36.266Z] [INFO] Auto-cleanup default: false (repository is public) +[2026-02-05T09:18:36.267Z] [INFO] 🔄 Continue mode: Working with PR #154 +[2026-02-05T09:18:36.267Z] [INFO] Continue mode activated: PR URL provided directly +[2026-02-05T09:18:36.267Z] [INFO] PR Number set to: 154 +[2026-02-05T09:18:36.267Z] [INFO] Will fetch PR details and linked issue +[2026-02-05T09:18:36.711Z] [INFO] 🍴 Detected fork PR from skulidropek/eo2js +[2026-02-05T09:18:36.711Z] [INFO] Fork owner: skulidropek +[2026-02-05T09:18:36.712Z] [INFO] Will clone fork repository for continue mode +[2026-02-05T09:18:36.712Z] [INFO] 📝 PR branch: issues/117 +[2026-02-05T09:18:36.713Z] [WARNING] ⚠️ Warning: No linked issue found in PR body +[2026-02-05T09:18:36.715Z] [WARNING] The PR should contain "Fixes #123" or similar to link an issue +[2026-02-05T09:18:36.716Z] [INFO] +Creating temporary directory: /tmp/gh-issue-solver-1770283116716 +[2026-02-05T09:18:36.719Z] [INFO] +🍴 Fork mode: ENABLED +[2026-02-05T09:18:36.719Z] [INFO] Checking fork status... + +[2026-02-05T09:18:37.035Z] [INFO] 🔍 Detecting fork conflicts... +[2026-02-05T09:18:38.332Z] [INFO] ✅ No fork conflict: Safe to proceed +[2026-02-05T09:18:38.675Z] [INFO] ✅ Fork exists: konard/objectionary-eo2js +[2026-02-05T09:18:38.676Z] [INFO] 🔍 Validating fork parent... +[2026-02-05T09:18:39.131Z] [INFO] ✅ Fork parent validated: objectionary/eo2js +[2026-02-05T09:18:39.132Z] [INFO] +📥 Cloning repository: konard/objectionary-eo2js +[2026-02-05T09:18:40.774Z] [INFO] ✅ Cloned to: /tmp/gh-issue-solver-1770283116716 +[2026-02-05T09:18:40.812Z] [INFO] 🔗 Setting upstream: objectionary/eo2js +[2026-02-05T09:18:40.854Z] [INFO] ℹ️ Upstream exists: Using existing upstream remote +[2026-02-05T09:18:40.854Z] [INFO] 🔄 Fetching upstream... +[2026-02-05T09:18:41.276Z] [INFO] ✅ Upstream fetched: Successfully +[2026-02-05T09:18:41.276Z] [INFO] 🔄 Syncing default branch... +[2026-02-05T09:18:41.776Z] [INFO] ℹ️ Default branch: master +[2026-02-05T09:18:41.828Z] [INFO] ✅ Default branch synced: with upstream/master +[2026-02-05T09:18:41.829Z] [INFO] 🔄 Pushing to fork: master branch +[2026-02-05T09:18:42.734Z] [INFO] ✅ Fork updated: Default branch pushed to fork +[2026-02-05T09:18:42.736Z] [INFO] +🔍 Checking PR fork: Determining if branch is in another fork... +[2026-02-05T09:18:43.041Z] [INFO] 🔗 Setting up pr-fork: Branch exists in another user's fork +[2026-02-05T09:18:43.041Z] [INFO] PR fork owner: skulidropek +[2026-02-05T09:18:43.041Z] [INFO] Current user: konard +[2026-02-05T09:18:43.042Z] [INFO] Action: Adding skulidropek/objectionary-eo2js as pr-fork remote +[2026-02-05T09:18:43.079Z] [INFO] ✅ Remote added: pr-fork +[2026-02-05T09:18:43.080Z] [INFO] 📥 Fetching branches: From pr-fork remote... +[2026-02-05T09:18:43.423Z] [INFO] ❌ Error: Failed to fetch from pr-fork +[2026-02-05T09:18:43.424Z] [INFO] Details: remote: Repository not found. +fatal: repository 'https://github.com/skulidropek/objectionary-eo2js.git/' not found +[2026-02-05T09:18:43.424Z] [INFO] Suggestion: Check if you have access to the fork +[2026-02-05T09:18:43.538Z] [INFO] +📌 Default branch: master +[2026-02-05T09:18:43.583Z] [INFO] +🔄 Checking out PR branch: issues/117 +[2026-02-05T09:18:43.584Z] [INFO] 📥 Fetching branches: From remote... +[2026-02-05T09:18:43.970Z] [INFO] 🔄 Branch not in origin: Checking upstream remote... +[2026-02-05T09:18:44.010Z] [INFO] 📥 Fetching from upstream: Looking for PR branch... +[2026-02-05T09:18:44.370Z] [WARNING] ⚠️ Branch not found: Not in origin or upstream remotes +[2026-02-05T09:18:44.371Z] [INFO] +[2026-02-05T09:18:45.770Z] [ERROR] ❌ BRANCH CHECKOUT FAILED +[2026-02-05T09:18:45.770Z] [INFO] +[2026-02-05T09:18:45.771Z] [INFO] 🔍 What happened: +[2026-02-05T09:18:45.771Z] [INFO] Failed to checkout the branch 'issues/117' for PR #154. +[2026-02-05T09:18:45.771Z] [INFO] Repository: https://github.com/objectionary/eo2js +[2026-02-05T09:18:45.772Z] [INFO] Pull Request: https://github.com/objectionary/eo2js/pull/154 +[2026-02-05T09:18:45.772Z] [INFO] The branch doesn't exist in the main repository (https://github.com/objectionary/eo2js). +[2026-02-05T09:18:45.772Z] [INFO] +[2026-02-05T09:18:45.772Z] [INFO] 📦 Git error details: +[2026-02-05T09:18:45.773Z] [INFO] fatal: 'origin/issues/117' is not a commit and a branch 'issues/117' cannot be created from it +[2026-02-05T09:18:45.773Z] [INFO] +[2026-02-05T09:18:45.773Z] [INFO] 💡 Why this happened: +[2026-02-05T09:18:45.773Z] [INFO] The PR branch 'issues/117' exists in the fork repository: +[2026-02-05T09:18:45.773Z] [INFO] https://github.com/skulidropek/eo2js +[2026-02-05T09:18:45.773Z] [INFO] but you're trying to access it from the main repository: +[2026-02-05T09:18:45.773Z] [INFO] https://github.com/objectionary/eo2js +[2026-02-05T09:18:45.773Z] [INFO] This branch does NOT exist in the main repository. +[2026-02-05T09:18:45.774Z] [INFO] This is a common issue with pull requests from forks. +[2026-02-05T09:18:45.774Z] [INFO] +[2026-02-05T09:18:45.774Z] [INFO] 🔧 How to fix this: +[2026-02-05T09:18:45.774Z] [INFO] +[2026-02-05T09:18:45.774Z] [INFO] ┌──────────────────────────────────────────────────────────┐ +[2026-02-05T09:18:45.774Z] [INFO] │ RECOMMENDED: Use the --fork option │ +[2026-02-05T09:18:45.774Z] [INFO] └──────────────────────────────────────────────────────────┘ +[2026-02-05T09:18:45.774Z] [INFO] +[2026-02-05T09:18:45.775Z] [INFO] Run this command: +[2026-02-05T09:18:45.776Z] [INFO] ./solve.mjs "https://github.com/objectionary/eo2js/pull/154" --fork +[2026-02-05T09:18:45.776Z] [INFO] +[2026-02-05T09:18:45.776Z] [INFO] This will automatically: +[2026-02-05T09:18:45.776Z] [INFO] ✓ Use your existing fork (skulidropek/eo2js) +[2026-02-05T09:18:45.776Z] [INFO] ✓ Set up the correct remotes and branches +[2026-02-05T09:18:45.776Z] [INFO] ✓ Allow you to work on the PR without permission issues +[2026-02-05T09:18:45.776Z] [INFO] +[2026-02-05T09:18:45.776Z] [INFO] ───────────────────────────────────────────────────────── +[2026-02-05T09:18:45.776Z] [INFO] +[2026-02-05T09:18:45.776Z] [INFO] Alternative options: +[2026-02-05T09:18:45.777Z] [INFO] • Verify PR details: gh pr view 154 --repo objectionary/eo2js +[2026-02-05T09:18:45.777Z] [INFO] • Check your local setup: cd /tmp/gh-issue-solver-1770283116716 && git remote -v +[2026-02-05T09:18:45.777Z] [INFO] +[2026-02-05T09:18:45.777Z] [INFO] 📂 Working directory: /tmp/gh-issue-solver-1770283116716 +[2026-02-05T09:18:45.782Z] [INFO] Error executing command: +[2026-02-05T09:18:45.785Z] [INFO] Stack trace: Error: Branch operation failed + at createOrCheckoutBranch (file:///home/hive/.bun/install/global/node_modules/@link-assistant/hive-mind/src/solve.branch.lib.mjs:166:11) + at async file:///home/hive/.bun/install/global/node_modules/@link-assistant/hive-mind/src/solve.mjs:549:22 +[2026-02-05T09:18:45.786Z] [ERROR] 📁 Full log file: /home/hive/solve-2026-02-05T09-18-29-306Z.log +[2026-02-05T09:18:46.067Z] [WARNING] ⚠️ Could not determine GitHub user. Cannot create error report issue. +[2026-02-05T09:18:46.067Z] [INFO] +📄 Attempting to attach failure logs... +``` + +## Key Error Points + +### 1. Incorrect Fork Name Construction (T+13.423s) + +``` +[INFO] Action: Adding skulidropek/objectionary-eo2js as pr-fork remote +``` + +The code incorrectly tried to add `skulidropek/objectionary-eo2js` but the actual fork is `skulidropek/eo2js`. + +### 2. Repository Not Found (T+13.806s) + +``` +[INFO] ❌ Error: Failed to fetch from pr-fork +[INFO] Details: remote: Repository not found. +fatal: repository 'https://github.com/skulidropek/objectionary-eo2js.git/' not found +``` + +### 3. Branch Checkout Failure (T+15.753s) + +``` +[ERROR] ❌ BRANCH CHECKOUT FAILED +[INFO] fatal: 'origin/issues/117' is not a commit and a branch 'issues/117' cannot be created from it +``` + +## Repository Verification + +| Repository | Exists | Notes | +| -------------------------------- | ------ | -------------------------- | +| `objectionary/eo2js` | ✅ Yes | Upstream repository | +| `skulidropek/eo2js` | ✅ Yes | PR author's fork (correct) | +| `skulidropek/objectionary-eo2js` | ❌ No | Incorrectly guessed name | +| `konard/objectionary-eo2js` | ✅ Yes | Current user's fork | + +## Branch Verification + +| Branch | Location | Exists | +| ------------ | --------------------------- | ------ | +| `issues/117` | `skulidropek/eo2js` | ✅ Yes | +| `issues/117` | `objectionary/eo2js` | ❌ No | +| `issues/117` | `konard/objectionary-eo2js` | ❌ No | +| `master` | All | ✅ Yes | diff --git a/src/solve.branch.lib.mjs b/src/solve.branch.lib.mjs index 92b776d3..c4bf1a87 100644 --- a/src/solve.branch.lib.mjs +++ b/src/solve.branch.lib.mjs @@ -110,7 +110,8 @@ export async function createOrCheckoutBranch({ isContinueMode, prBranch, issueNu branchName = prBranch; const repository = await import('./solve.repository.lib.mjs'); const { checkoutPrBranch } = repository; - checkoutResult = await checkoutPrBranch(tempDir, branchName, null, null); // prForkRemote and prForkOwner not needed here + // Pass prNumber to enable PR refs fallback (refs/pull/{number}/head) when fork checkout fails + checkoutResult = await checkoutPrBranch(tempDir, branchName, null, null, prNumber); } else { // Traditional mode: create new branch for issue const randomHex = crypto.randomBytes(6).toString('hex'); diff --git a/src/solve.repository.lib.mjs b/src/solve.repository.lib.mjs index bddeb74a..02ef48e0 100644 --- a/src/solve.repository.lib.mjs +++ b/src/solve.repository.lib.mjs @@ -1178,17 +1178,59 @@ export const setupPrForkRemote = async (tempDir, argv, prForkOwner, repo, isCont } // This is someone else's fork - add it as pr-fork remote - // Determine the fork repository name (might be prefixed if --prefix-fork-name-with-owner-name was used) - // Try both standard and prefixed names - let prForkRepoName = repo; - if (owner && argv.prefixForkNameWithOwnerName) { - // When prefix option is enabled, try prefixed name first - prForkRepoName = `${owner}-${repo}`; - } + // IMPORTANT: The fork owner's repository name is independent of our naming preferences + // We need to discover the actual fork name, not assume it matches our convention + // This fixes issue #1217 where incorrect fork name was used await log(`${formatAligned('🔗', 'Setting up pr-fork:', "Branch exists in another user's fork")}`); await log(`${formatAligned('', 'PR fork owner:', prForkOwner)}`); await log(`${formatAligned('', 'Current user:', currentUser)}`); + + // Discover the actual fork repository name by querying GitHub API + // The fork could have any name (standard, prefixed, or custom renamed) + let prForkRepoName = null; + + // Strategy 1: Query the upstream repo's forks to find this user's fork + if (owner) { + await log(`${formatAligned('🔍', 'Discovering fork name:', `Searching ${owner}/${repo}/forks for ${prForkOwner}'s fork...`)}`); + const forksResult = await $`gh api repos/${owner}/${repo}/forks --paginate --jq '.[] | select(.owner.login == "${prForkOwner}") | .name'`; + if (forksResult.code === 0 && forksResult.stdout) { + const forkName = forksResult.stdout.toString().trim().split('\n')[0]; // Take first match + if (forkName) { + prForkRepoName = forkName; + await log(`${formatAligned('✅', 'Found fork:', `${prForkOwner}/${prForkRepoName}`)}`); + } + } + } + + // Strategy 2: If not found in forks list, try common naming patterns + if (!prForkRepoName) { + const possibleNames = [ + repo, // Standard name: "eo2js" + owner ? `${owner}-${repo}` : null, // Prefixed name: "objectionary-eo2js" + ].filter(Boolean); + + await log(`${formatAligned('🔍', 'Trying common names:', possibleNames.join(', '))}`); + + for (const candidateName of possibleNames) { + const checkResult = await $`gh repo view ${prForkOwner}/${candidateName} --json name 2>/dev/null`; + if (checkResult.code === 0) { + prForkRepoName = candidateName; + await log(`${formatAligned('✅', 'Found fork:', `${prForkOwner}/${prForkRepoName}`)}`); + break; + } + } + } + + // If still not found, we cannot proceed + if (!prForkRepoName) { + await log(`${formatAligned('❌', 'Error:', `Could not find ${prForkOwner}'s fork of ${owner}/${repo}`)}`); + await log(`${formatAligned('', 'Checked:', `${prForkOwner}/${repo} and ${prForkOwner}/${owner}-${repo}`)}`); + await log(`${formatAligned('', 'Suggestion:', 'The fork may have been deleted or renamed')}`); + await log(`${formatAligned('', 'Workaround:', 'Remove --fork flag to continue work in the original fork')}`); + return null; + } + await log(`${formatAligned('', 'Action:', `Adding ${prForkOwner}/${prForkRepoName} as pr-fork remote`)}`); const addRemoteResult = await $({ @@ -1224,7 +1266,8 @@ export const setupPrForkRemote = async (tempDir, argv, prForkOwner, repo, isCont }; // Checkout branch for continue mode (PR branch from remote) -export const checkoutPrBranch = async (tempDir, branchName, prForkRemote, prForkOwner) => { +// prNumber is optional - when provided, enables PR refs fallback (refs/pull/{number}/head) +export const checkoutPrBranch = async (tempDir, branchName, prForkRemote, prForkOwner, prNumber = null) => { await log(`\n${formatAligned('🔄', 'Checking out PR branch:', branchName)}`); // Determine which remote to use for branch checkout @@ -1287,6 +1330,32 @@ export const checkoutPrBranch = async (tempDir, branchName, prForkRemote, prFork } } } + + // FALLBACK: If all remote checks failed and we have a PR number, + // use GitHub's special PR refs (refs/pull/{number}/head) + // This works regardless of fork naming conventions and doesn't require fork access + // See: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally + if (checkoutResult.code !== 0 && prNumber) { + await log(`${formatAligned('🔄', 'Trying PR refs fallback:', `Fetching refs/pull/${prNumber}/head...`)}`); + + // Fetch the PR head using GitHub's special refs + const prRefFetchResult = await $({ cwd: tempDir })`git fetch origin pull/${prNumber}/head:${branchName}`; + + if (prRefFetchResult.code === 0) { + await log(`${formatAligned('✅', 'Fetched PR ref:', `refs/pull/${prNumber}/head`)}`); + checkoutResult = await $({ cwd: tempDir })`git checkout ${branchName}`; + + if (checkoutResult.code === 0) { + await log(`${formatAligned('ℹ️', 'Note:', 'Checked out using GitHub PR refs (fork access not required)')}`); + await log(`${formatAligned('', '', 'This is a read-only checkout - you may need to push to a different branch')}`); + } + } else { + await log(`${formatAligned('⚠️', 'PR refs fallback failed:', 'Could not fetch PR head')}`); + if (prRefFetchResult.stderr) { + await log(`${formatAligned('', 'Details:', prRefFetchResult.stderr.toString().trim())}`); + } + } + } } return checkoutResult;