Add header Git branch dropdown#121
Conversation
Review Summary by QodoAdd header Git branch dropdown with commit checkout
WalkthroughsDescription• Replace simple header branch action with searchable Git dropdown component • Add detached HEAD state display with commit subject, SHA, and date metadata • Implement branch commit expansion showing recent commits with checkout capability • Add backend endpoints for branch commits and detached commit checkout with worktree validation • Extend GitBranchState type with HEAD metadata and dirty/detached status tracking • Apply dark theme styling to new dropdown component and related elements Diagramflowchart LR
A["Git Branch State API"] -->|headSha, headSubject, headDate, detached, dirty| B["HeaderGitBranchDropdown Component"]
C["Branch Commits Endpoint"] -->|GitCommitOption list| B
D["Checkout Commit Endpoint"] -->|POST with SHA| B
B -->|Display current branch or detached HEAD| E["Header Trigger"]
B -->|Search & filter branches| F["Branch List"]
B -->|Expand & show commits| G["Commit List"]
E -->|Toggle Review pane| H["Review State"]
F -->|Checkout branch| I["Git Checkout"]
G -->|Checkout commit| J["Git Detached Checkout"]
File Changes1. src/api/codexGateway.ts
|
Code Review by Qodo
1. Unborn repo breaks header state
|
| const gitRoot = await runCommandCapture('git', ['rev-parse', '--show-toplevel'], { cwd }) | ||
| const currentBranchRaw = await runCommandCapture('git', ['branch', '--show-current'], { cwd: gitRoot }) | ||
| const currentBranch = currentBranchRaw.trim() || null | ||
| const headShaRaw = await runCommandCapture('git', ['rev-parse', '--short=12', 'HEAD'], { cwd: gitRoot }) | ||
| const headCommitRaw = await runCommandCapture('git', ['show', '-s', '--date=short', '--format=%cd%x09%s', 'HEAD'], { cwd: gitRoot }) | ||
| const [headDate = '', ...headSubjectParts] = headCommitRaw.split('\t') | ||
| const statusRaw = await runCommandCapture('git', ['status', '--porcelain'], { cwd: gitRoot }) |
There was a problem hiding this comment.
1. Unborn repo breaks header state 🐞 Bug ☼ Reliability
readGitHeaderState() unconditionally runs git commands against HEAD (rev-parse/show), which fail for repositories with no initial commit, causing /codex-api/git/branches and /codex-api/git/checkout to return 500 instead of a usable state.
Agent Prompt
### Issue description
`readGitHeaderState()` assumes `HEAD` resolves to a commit and fails in an unborn repository (no commits). This turns valid repos into 500s for `/codex-api/git/branches` and for successful checkouts that still have no commits.
### Issue Context
`isMissingHeadError()` already exists and is used elsewhere to handle missing-HEAD cases; the header state path should do the same.
### Fix Focus Areas
- src/server/codexAppServerBridge.ts[2501-2525]
- src/server/codexAppServerBridge.ts[2439-2445]
### Implementation notes
- Wrap `rev-parse ... HEAD` and `git show ... HEAD` in `try/catch`.
- If the error matches `isMissingHeadError`, return `headSha/headSubject/headDate = null` (and keep `currentBranch` from `git branch --show-current`, plus `dirty` from `git status`).
- Only rethrow for non-missing-HEAD errors.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if (req.method === 'GET' && url.pathname === '/codex-api/git/branch-commits') { | ||
| const rawCwd = (url.searchParams.get('cwd') ?? '').trim() | ||
| const branch = (url.searchParams.get('branch') ?? '').trim() | ||
| if (!rawCwd) { | ||
| setJson(res, 400, { error: 'Missing cwd' }) | ||
| return | ||
| } | ||
| if (!branch) { | ||
| setJson(res, 400, { error: 'Missing branch' }) | ||
| return | ||
| } | ||
| const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd) | ||
| try { | ||
| const gitRoot = await runCommandCapture('git', ['rev-parse', '--show-toplevel'], { cwd }) | ||
| await runCommandCapture('git', ['rev-parse', '--verify', `${branch}^{commit}`], { cwd: gitRoot }) | ||
| const output = await runCommandCapture( | ||
| 'git', | ||
| ['log', '-n', '12', '--date=short', '--format=%H%x09%h%x09%cd%x09%s', branch], | ||
| { cwd: gitRoot }, | ||
| ) | ||
| const commits = output.split('\n').flatMap((line) => { | ||
| const [sha = '', shortSha = '', date = '', ...subjectParts] = line.split('\t') | ||
| const subject = subjectParts.join('\t').trim() | ||
| return sha.trim() && shortSha.trim() | ||
| ? [{ sha: sha.trim(), shortSha: shortSha.trim(), date: date.trim(), subject: subject || shortSha.trim() }] | ||
| : [] | ||
| }) | ||
| setJson(res, 200, { data: commits }) | ||
| } catch (error) { | ||
| setJson(res, 500, { error: getErrorMessage(error, 'Failed to load branch commits') }) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| if (req.method === 'POST' && url.pathname === '/codex-api/git/checkout-commit') { | ||
| const payload = await readJsonBody(req) | ||
| const record = asRecord(payload) | ||
| if (!record) { | ||
| setJson(res, 400, { error: 'Invalid body: expected object' }) | ||
| return | ||
| } | ||
| const rawCwd = readNonEmptyString(record.cwd) | ||
| const sha = readNonEmptyString(record.sha) | ||
| if (!rawCwd) { | ||
| setJson(res, 400, { error: 'Missing cwd' }) | ||
| return | ||
| } | ||
| if (!sha) { | ||
| setJson(res, 400, { error: 'Missing commit' }) | ||
| return | ||
| } | ||
| const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd) | ||
| try { | ||
| const gitRoot = await runCommandCapture('git', ['rev-parse', '--show-toplevel'], { cwd }) | ||
| await assertCleanGitWorktree(gitRoot) | ||
| const targetSha = await runCommandCapture('git', ['rev-parse', '--verify', `${sha}^{commit}`], { cwd: gitRoot }) | ||
| await runCommand('git', ['checkout', '--detach', targetSha.trim()], { cwd: gitRoot }) | ||
| setJson(res, 200, { data: await readGitHeaderState(gitRoot) }) |
There was a problem hiding this comment.
2. Git option injection via refs 🐞 Bug ⛨ Security
The new /codex-api/git/branch-commits and /codex-api/git/checkout-commit endpoints pass user-controlled branch/sha into git argv without validating that they are safe refs/hex SHAs; values starting with '-' can be parsed as git options, leading to unintended command behavior.
Agent Prompt
### Issue description
`branch` and `sha` are user-controlled and are passed directly to git commands. Even without a shell, this enables *option injection* into git (args beginning with `-`) and can trigger unexpected behavior.
### Issue Context
This affects new endpoints:
- `GET /codex-api/git/branch-commits`
- `POST /codex-api/git/checkout-commit`
### Fix Focus Areas
- src/server/codexAppServerBridge.ts[6027-6058]
- src/server/codexAppServerBridge.ts[6061-6089]
### Implementation notes
- Reject `branch` values starting with `-` or containing whitespace/control chars.
- Prefer mapping `branch` to a *full ref* and verifying via `git show-ref --verify --quiet`:
- Try `refs/heads/${branch}` and `refs/remotes/${branch}`; require exactly one to exist.
- Use the verified full ref when running `git log` and `git rev-parse`.
- For `sha`, require a hex pattern (e.g. `/^[0-9a-fA-F]{7,40}$/`) before `rev-parse --verify`.
- Consider returning 400 for invalid inputs instead of 500.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Summary
Tests