Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions e2e/file-explorer-sidebar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,11 @@ test('git sidebar pane renders a nested tree and offers a push menu', async ({
await execFileAsync('git', ['config', 'user.email', 'terminay@example.com'], { cwd: workspace.rootDir })
await execFileAsync('git', ['add', '.'], { cwd: workspace.rootDir })
await execFileAsync('git', ['commit', '-m', 'initial'], { cwd: workspace.rootDir })
const defaultBranch = (
await execFileAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: workspace.rootDir })
).stdout.trim()
const featureBranch = 'feature/push-menu'
await execFileAsync('git', ['checkout', '-b', featureBranch], { cwd: workspace.rootDir })

await workspace.writeText('src/lib/util.ts', 'export const x = 2\n')
await workspace.writeText('src/lib/new.ts', 'export const y = 3\n')
Expand Down Expand Up @@ -390,10 +395,16 @@ test('git sidebar pane renders a nested tree and offers a push menu', async ({
await pushButton.click()

const pushMenu = mainWindow.locator('.context-menu')
const pushMenuHeadings = pushMenu.locator('.context-menu__heading')
await expect(pushMenuHeadings.getByText(`Current Branch (${featureBranch})`, { exact: true })).toBeVisible()
await expect(pushMenu.getByText('Push to current branch', { exact: true })).toBeVisible()
await expect(pushMenu.getByText('Push to current branch + PR')).toBeVisible()
await expect(pushMenu.getByText('Push to current branch + create PR')).toBeVisible()
await expect(pushMenuHeadings.getByText('New Branch', { exact: true })).toBeVisible()
await expect(pushMenu.getByText('Push to new branch', { exact: true })).toBeVisible()
await expect(pushMenu.getByText('Push to new branch + PR')).toBeVisible()
await expect(pushMenu.getByText('Push to new branch + create PR')).toBeVisible()
await expect(pushMenuHeadings.getByText(`Default Branch (${defaultBranch})`, { exact: true })).toBeVisible()
await expect(pushMenu.getByText('Push to default branch')).toBeVisible()
await expect(pushMenu.locator('.context-menu__trailing')).toHaveCount(5)

await mainWindow.keyboard.press('Escape')
await expect(pushMenu).toHaveCount(0)
Expand Down
37 changes: 37 additions & 0 deletions electron/fileViewer/gitDiffService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,15 @@ export class GitDiffService {
return {
gitAvailable: false,
repoRoot: null,
defaultBranch: null,
worktrees: [],
}
}

return {
gitAvailable: true,
repoRoot: null,
defaultBranch: null,
worktrees: [],
}
}
Expand All @@ -167,6 +169,7 @@ export class GitDiffService {
return {
gitAvailable: true,
repoRoot: null,
defaultBranch: null,
worktrees: [],
}
}
Expand All @@ -176,6 +179,7 @@ export class GitDiffService {
})

const worktrees = parseWorktreeList(stdout, repoRoot)
const defaultBranch = await this.resolveDefaultBranch(workingDirectory)

const withEntries = await Promise.all(
worktrees.map(async (worktree): Promise<GitWorktreeStatus> => {
Expand Down Expand Up @@ -234,6 +238,7 @@ export class GitDiffService {
return {
gitAvailable: true,
repoRoot,
defaultBranch,
worktrees: withEntries,
}
}
Expand Down Expand Up @@ -278,6 +283,38 @@ export class GitDiffService {
}
}

private async resolveDefaultBranch(cwd: string): Promise<string | null> {
try {
const result = await execFileAsync('git', ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], {
cwd,
})
const remoteHead = result.stdout.trim()
if (remoteHead.startsWith('origin/')) {
return remoteHead.slice('origin/'.length)
}
if (remoteHead) {
return remoteHead
}
} catch {}

for (const branch of ['main', 'master']) {
try {
await execFileAsync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], { cwd })
return branch
} catch {}
}

try {
const result = await execFileAsync('git', ['branch', '--format=%(refname:short)'], { cwd })
return result.stdout
.split(/\r?\n/)
.map((branch) => branch.trim())
.find((branch) => branch.length > 0) ?? null
} catch {
return null
}
}

private async getAheadOfMainCount(cwd: string): Promise<number | null> {
try {
const result = await execFileAsync('git', ['rev-list', '--count', 'main..HEAD'], { cwd })
Expand Down
52 changes: 50 additions & 2 deletions electron/quickPush/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type PorcelainEntry = {
type QuickPushContext = {
repoRoot: string
branch: string
defaultBranch: string
changedFiles: string[]
statusText: string
diffText: string
Expand Down Expand Up @@ -105,6 +106,36 @@ function actionNeedsPullRequest(action: QuickPushAction): boolean {
return action === 'current-pr' || action === 'new-pr'
}

async function resolveDefaultBranch(cwd: string): Promise<string | null> {
try {
const result = await runGit(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], cwd)
const remoteHead = result.trim()
if (remoteHead.startsWith('origin/')) {
return remoteHead.slice('origin/'.length)
}
if (remoteHead) {
return remoteHead
}
} catch {}

for (const branch of ['main', 'master']) {
try {
await runGit(['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], cwd)
return branch
} catch {}
}

try {
const result = await runGit(['branch', '--format=%(refname:short)'], cwd)
return result
.split(/\r?\n/)
.map((branch) => branch.trim())
.find((branch) => branch.length > 0) ?? null
} catch {
return null
}
}

/**
* Extract the first balanced top-level JSON object from raw model output and
* repair the mistakes models commonly make: code fences, raw control characters
Expand Down Expand Up @@ -287,7 +318,7 @@ export function parseQuickPushPlan(
return { branchName, pullRequest, commits, uncoveredFiles, warnings }
}

function describeAction(action: QuickPushAction, branch: string): string {
function describeAction(action: QuickPushAction, branch: string, defaultBranch: string): string {
switch (action) {
case 'current':
return `Commit all of the changes onto the current branch "${branch}". Do not set "branchName" or "pullRequest".`
Expand All @@ -297,6 +328,8 @@ function describeAction(action: QuickPushAction, branch: string): string {
return `Commit all of the changes onto a new, descriptively named branch. Set "branchName" to a short kebab-case branch name (you may include a "feat/" or "fix/" style prefix). Do not set "pullRequest".`
case 'new-pr':
return `Commit all of the changes onto a new, descriptively named branch, then open a pull request. Set "branchName" to a short kebab-case branch name and set "pullRequest" with a title and body.`
case 'default':
return `Commit all of the changes onto the default branch "${defaultBranch}". Do not set "branchName" or "pullRequest".`
default:
return 'Commit all of the changes.'
}
Expand All @@ -321,7 +354,10 @@ function buildPrompt(context: QuickPushContext, action: QuickPushAction): string
'- Use the file paths EXACTLY as shown under "Changed files" (relative to the repo root).',
'- Group related changes together; split unrelated changes into separate commits.',
'',
`Task: ${describeAction(action, context.branch)}`,
`Task: ${describeAction(action, context.branch, context.defaultBranch)}`,
'',
`Current branch: ${context.branch}`,
`Default branch: ${context.defaultBranch}`,
'',
'Changed files:',
context.changedFiles.length > 0 ? context.changedFiles.map((file) => `- ${file}`).join('\n') : '(none)',
Expand Down Expand Up @@ -400,6 +436,7 @@ async function gatherContext(cwd: string): Promise<QuickPushContext> {
if (!branch || branch === 'HEAD') {
branch = 'the current branch'
}
const defaultBranch = (await resolveDefaultBranch(repoRoot)) ?? 'main'

const porcelain = await runGit(
['status', '--porcelain=v1', '-z', '--untracked-files=all', '--ignored=no'],
Expand Down Expand Up @@ -432,6 +469,7 @@ async function gatherContext(cwd: string): Promise<QuickPushContext> {
return {
repoRoot,
branch,
defaultBranch,
changedFiles,
statusText: statusTextRaw,
diffText,
Expand Down Expand Up @@ -608,6 +646,16 @@ export class QuickPushService {
}
await run(`Create branch ${branchName}`, ['checkout', '-b', branchName])
branch = branchName
} else if (request.action === 'default') {
const defaultBranch = await resolveDefaultBranch(repoRoot)
if (!defaultBranch) {
throw new Error('Unable to resolve the repository default branch.')
}
const currentBranch = (await runGit(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot)).trim()
if (currentBranch !== defaultBranch) {
await run(`Checkout default branch ${defaultBranch}`, ['checkout', defaultBranch])
}
branch = defaultBranch
} else {
branch = (await runGit(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot)).trim()
}
Expand Down
10 changes: 10 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -2855,6 +2855,16 @@
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}

.context-menu__heading {
padding: 8px 10px 4px;
color: #8d96a6;
font-size: 11px;
font-weight: 600;
line-height: 1.2;
text-transform: uppercase;
white-space: nowrap;
}

.context-menu__item {
all: unset;
display: flex;
Expand Down
Loading
Loading