mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
feat: allow custom worktree branch names (#6454)
* feat: allow custom worktree branch names * refactor(ui): render custom branch name field unconditionally under advanced container * fix(composer): hide manual branch field when a work-item source drives the branch A tracked PR/issue/MR/Linear source derives the branch itself, and a linked GitHub PR re-resolves the branch name at submit — so an override typed in the Advanced branch field was silently ignored. Only render the field for the typed-name and base-branch flows, where the manual override is honored. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
co-authored by
Orca
Wolfgang Schoenberger
Neil
parent
9f31826e79
commit
8796261eed
@@ -16,7 +16,7 @@ describe('classifyWorkspaceCreateError', () => {
|
||||
})
|
||||
|
||||
it('buckets a branch-already-exists throw as path_collision', () => {
|
||||
const err = new Error('Branch "feature/foo" already exists. Pick a different worktree name.')
|
||||
const err = new Error('Branch "feature/foo" already exists. Pick a different branch name.')
|
||||
expect(classifyWorkspaceCreateError(err)).toBe('path_collision')
|
||||
})
|
||||
|
||||
@@ -29,15 +29,13 @@ describe('classifyWorkspaceCreateError', () => {
|
||||
|
||||
it('buckets a branch-already-exists-locally throw as path_collision', () => {
|
||||
const err = new Error(
|
||||
'Branch "feature/foo" already exists locally. Pick a different worktree name.'
|
||||
'Branch "feature/foo" already exists locally. Pick a different branch name.'
|
||||
)
|
||||
expect(classifyWorkspaceCreateError(err)).toBe('path_collision')
|
||||
})
|
||||
|
||||
it('buckets an existing-PR collision throw as path_collision', () => {
|
||||
const err = new Error(
|
||||
'Branch "feature/foo" already has PR #42. Pick a different worktree name.'
|
||||
)
|
||||
const err = new Error('Branch "feature/foo" already has PR #42. Pick a different branch name.')
|
||||
expect(classifyWorkspaceCreateError(err)).toBe('path_collision')
|
||||
})
|
||||
|
||||
|
||||
@@ -1452,6 +1452,7 @@ export async function createRemoteWorktree(
|
||||
// commit author identity rather than hosted-account usernames.
|
||||
const username = await getSshGitUsername(provider, repo.path)
|
||||
|
||||
const branchConflictSubject = args.branchNameOverride ? 'branch name' : 'worktree name'
|
||||
// Determine base branch
|
||||
// Why: previously fell back to a hardcoded 'origin/main' when
|
||||
// symbolic-ref failed. That silently handed addWorktree a ref that may
|
||||
@@ -1531,7 +1532,7 @@ export async function createRemoteWorktree(
|
||||
if (!remotePathResolved) {
|
||||
if (lastBranchConflictKind) {
|
||||
throw new Error(
|
||||
`Branch "${branchName}" already exists ${lastBranchConflictKind === 'local' ? 'locally' : 'on a remote'}. Pick a different worktree name.`
|
||||
`Branch "${branchName}" already exists ${lastBranchConflictKind === 'local' ? 'locally' : 'on a remote'}. Pick a different ${branchConflictSubject}.`
|
||||
)
|
||||
}
|
||||
throw new Error(
|
||||
@@ -1989,6 +1990,7 @@ export async function createLocalWorktree(
|
||||
let branchName = ''
|
||||
let worktreePath = ''
|
||||
|
||||
const branchConflictSubject = args.branchNameOverride ? 'branch name' : 'worktree name'
|
||||
let resolved = false
|
||||
let checkoutExistingBranch = false
|
||||
let selectedExistingLocalBranchName: string | null = null
|
||||
@@ -2111,12 +2113,12 @@ export async function createLocalWorktree(
|
||||
// failed instead of a generic error or (worse) an infinite spinner.
|
||||
if (lastExistingReviewNumber !== null) {
|
||||
throw new Error(
|
||||
`Branch "${branchName}" already has PR #${lastExistingReviewNumber}. Pick a different worktree name.`
|
||||
`Branch "${branchName}" already has PR #${lastExistingReviewNumber}. Pick a different ${branchConflictSubject}.`
|
||||
)
|
||||
}
|
||||
if (lastBranchConflictKind) {
|
||||
throw new Error(
|
||||
`Branch "${branchName}" already exists ${lastBranchConflictKind === 'local' ? 'locally' : 'on a remote'}. Pick a different worktree name.`
|
||||
`Branch "${branchName}" already exists ${lastBranchConflictKind === 'local' ? 'locally' : 'on a remote'}. Pick a different ${branchConflictSubject}.`
|
||||
)
|
||||
}
|
||||
throw new Error(
|
||||
|
||||
@@ -135,6 +135,8 @@ function renderCard(
|
||||
canReuseSelectedBranch={false}
|
||||
reuseSelectedBranch={false}
|
||||
onReuseSelectedBranchChange={() => {}}
|
||||
branchNameOverride=""
|
||||
onBranchNameOverrideChange={() => {}}
|
||||
forkPushWarning={null}
|
||||
detectedAgentIds={null}
|
||||
onOpenAgentSettings={() => {}}
|
||||
@@ -174,6 +176,25 @@ function renderCard(
|
||||
return { container, root }
|
||||
}
|
||||
|
||||
function findInputByLabel(container: HTMLElement, labelText: string): HTMLInputElement | null {
|
||||
const label = [...container.querySelectorAll('label')].find(
|
||||
(candidate) => candidate.textContent?.trim() === labelText
|
||||
)
|
||||
const labelledId = label?.getAttribute('for')
|
||||
if (labelledId) {
|
||||
return document.getElementById(labelledId) as HTMLInputElement | null
|
||||
}
|
||||
return label?.parentElement?.querySelector<HTMLInputElement>('input') ?? null
|
||||
}
|
||||
|
||||
function changeInputValue(input: HTMLInputElement, value: string): void {
|
||||
const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
|
||||
act(() => {
|
||||
valueSetter?.call(input, value)
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
})
|
||||
}
|
||||
|
||||
let current: { container: HTMLDivElement; root: Root } | null = null
|
||||
|
||||
describe('NewWorkspaceComposerCard folder task source mode', () => {
|
||||
@@ -320,6 +341,64 @@ describe('NewWorkspaceComposerCard folder task source mode', () => {
|
||||
expect(changes).toEqual(['wait-for-setup'])
|
||||
})
|
||||
|
||||
it('shows a git-only branch name field in Advanced and emits manual edits', () => {
|
||||
const changes: (string | undefined)[] = []
|
||||
current = renderCard({
|
||||
advancedOpen: false,
|
||||
branchesEnabled: true,
|
||||
branchNameOverride: 'feature/initial',
|
||||
onBranchNameOverrideChange: (next) => changes.push(next)
|
||||
})
|
||||
|
||||
const branchInput = findInputByLabel(current.container, 'Branch name')
|
||||
expect(branchInput).toBeTruthy()
|
||||
expect(branchInput?.value).toBe('feature/initial')
|
||||
|
||||
changeInputValue(branchInput as HTMLInputElement, 'feature/manual')
|
||||
|
||||
expect(changes).toEqual(['feature/manual'])
|
||||
})
|
||||
|
||||
it('omits the branch name field for non-git projects', () => {
|
||||
current = renderCard({
|
||||
advancedOpen: true,
|
||||
branchesEnabled: true,
|
||||
selectedRepoIsGit: false,
|
||||
branchNameOverride: 'feature/manual',
|
||||
onBranchNameOverrideChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(findInputByLabel(current.container, 'Branch name')).toBeNull()
|
||||
})
|
||||
|
||||
it('omits the branch name field when a tracked work item is the source', () => {
|
||||
// Why: a PR/issue/MR/Linear source derives the branch itself (and a linked
|
||||
// GitHub PR re-resolves it at submit), so a manual override would be a
|
||||
// silently ignored control — the field is only for typed-name/base-branch.
|
||||
current = renderCard({
|
||||
advancedOpen: true,
|
||||
branchesEnabled: true,
|
||||
branchNameOverride: 'feature/manual',
|
||||
smartNameSelection: { kind: 'github-pr', label: '#42 Fix', url: 'https://example.com/pr/42' },
|
||||
onBranchNameOverrideChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(findInputByLabel(current.container, 'Branch name')).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the branch name field when creating from a base branch', () => {
|
||||
// Why: choosing a base branch still lets the user name their new branch.
|
||||
current = renderCard({
|
||||
advancedOpen: true,
|
||||
branchesEnabled: true,
|
||||
branchNameOverride: 'feature/manual',
|
||||
smartNameSelection: { kind: 'branch', label: 'main' },
|
||||
onBranchNameOverrideChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(findInputByLabel(current.container, 'Branch name')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('does not disable folder workspace creation when only source lookup needs SSH', () => {
|
||||
current = renderCard({
|
||||
eligibleRepos: [
|
||||
|
||||
@@ -102,6 +102,8 @@ type NewWorkspaceComposerCardProps = {
|
||||
showAddProjectButton?: boolean
|
||||
name: string
|
||||
onNameValueChange: (value: string) => void
|
||||
branchNameOverride: string | undefined
|
||||
onBranchNameOverrideChange: (value: string | undefined) => void
|
||||
onSmartGitHubItemSelect: (item: GitHubWorkItem) => void
|
||||
onSmartGitLabItemSelect: (item: GitLabWorkItem) => void
|
||||
onSmartBranchSelect: (refName: string, localBranchName: string) => void
|
||||
@@ -564,6 +566,8 @@ export default function NewWorkspaceComposerCard({
|
||||
showAddProjectButton = true,
|
||||
name,
|
||||
onNameValueChange,
|
||||
branchNameOverride,
|
||||
onBranchNameOverrideChange,
|
||||
onSmartGitHubItemSelect,
|
||||
onSmartGitLabItemSelect,
|
||||
onSmartBranchSelect,
|
||||
@@ -620,6 +624,7 @@ export default function NewWorkspaceComposerCard({
|
||||
const disabledTuiAgents = useAppStore((s) => s.settings?.disabledTuiAgents ?? [])
|
||||
const updateSettings = useAppStore((s) => s.updateSettings)
|
||||
const nameInputFocusFrameRef = React.useRef<number | null>(null)
|
||||
const branchNameInputId = React.useId()
|
||||
const submitShortcutModifierLabel = getScreenSubmitModifierLabel()
|
||||
const selectedRepoName = React.useMemo(() => {
|
||||
const repo = eligibleRepos.find((candidate) => candidate.id === repoId)
|
||||
@@ -1136,6 +1141,38 @@ export default function NewWorkspaceComposerCard({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Why: only offer a manual branch name when creating from a
|
||||
typed name or a base branch. When a tracked work item (PR/
|
||||
issue/MR/Linear) is the source, the branch is derived from
|
||||
that item — a linked GitHub PR even re-resolves it at submit —
|
||||
so an override typed here would be silently ignored. */}
|
||||
{selectedRepoIsGit &&
|
||||
branchesEnabled &&
|
||||
(!smartNameSelection || smartNameSelection.kind === 'branch') ? (
|
||||
<div className="space-y-1">
|
||||
<label
|
||||
htmlFor={branchNameInputId}
|
||||
className="text-xs font-medium text-muted-foreground"
|
||||
>
|
||||
{translate(
|
||||
'auto.components.NewWorkspaceComposerCard.branchName',
|
||||
'Branch name'
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
id={branchNameInputId}
|
||||
type="text"
|
||||
value={branchNameOverride ?? ''}
|
||||
onChange={(event) => onBranchNameOverrideChange(event.target.value)}
|
||||
placeholder={translate(
|
||||
'auto.components.NewWorkspaceComposerCard.branchNamePlaceholder',
|
||||
'feature/my-branch'
|
||||
)}
|
||||
className="w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{translate('auto.components.NewWorkspaceComposerCard.f8728aa4f9', 'Note')}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
resolveComposerBranchNameOverrideForCreate,
|
||||
resolveComposerBranchReuse,
|
||||
resolveComposerBranchSelection,
|
||||
resolveComposerManualBranchNameChange,
|
||||
resolveComposerReuseOverride
|
||||
} from './composer-branch-selection'
|
||||
|
||||
@@ -57,15 +58,28 @@ describe('resolveComposerBranchSelection', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps resolver-provided PR branch overrides when the workspace name changes', () => {
|
||||
it('keeps manual branch-name overrides when preserving workspace edits', () => {
|
||||
expect(
|
||||
resolveComposerBranchNameOverrideForCreate({
|
||||
branchNameOverride: 'feature/fix',
|
||||
branchAutoName: '',
|
||||
branchNameOverride: 'feature/manual',
|
||||
branchAutoName: 'feature/auto',
|
||||
workspaceName: 'edited display name',
|
||||
preserveWorkspaceNameEdits: true
|
||||
})
|
||||
).toBe('feature/fix')
|
||||
).toBe('feature/manual')
|
||||
})
|
||||
|
||||
it('drops empty or missing branch-name overrides even when preserving workspace edits', () => {
|
||||
for (const branchNameOverride of [undefined, '']) {
|
||||
expect(
|
||||
resolveComposerBranchNameOverrideForCreate({
|
||||
branchNameOverride,
|
||||
branchAutoName: 'feature/fix',
|
||||
workspaceName: 'edited display name',
|
||||
preserveWorkspaceNameEdits: true
|
||||
})
|
||||
).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps existing branch picker override behavior tied to the auto-name', () => {
|
||||
@@ -80,6 +94,58 @@ describe('resolveComposerBranchSelection', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveComposerManualBranchNameChange', () => {
|
||||
const forkPushTarget = {
|
||||
remoteName: 'contributor',
|
||||
branchName: 'feature/from-pr',
|
||||
remoteUrl: 'https://example.com/contributor/repo.git'
|
||||
}
|
||||
|
||||
it('clears a PR-derived push target when the manual branch changes to a different branch', () => {
|
||||
expect(
|
||||
resolveComposerManualBranchNameChange({
|
||||
value: 'feature/manual',
|
||||
pushTarget: forkPushTarget,
|
||||
forkPushWarning: 'Cannot push to fork'
|
||||
})
|
||||
).toEqual({
|
||||
branchNameOverride: 'feature/manual',
|
||||
pushTarget: undefined,
|
||||
forkPushWarning: null
|
||||
})
|
||||
})
|
||||
|
||||
it('clears a PR-derived push target when manual branch input is empty or whitespace', () => {
|
||||
for (const value of ['', ' ']) {
|
||||
expect(
|
||||
resolveComposerManualBranchNameChange({
|
||||
value,
|
||||
pushTarget: forkPushTarget,
|
||||
forkPushWarning: 'Cannot push to fork'
|
||||
})
|
||||
).toEqual({
|
||||
branchNameOverride: undefined,
|
||||
pushTarget: undefined,
|
||||
forkPushWarning: null
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves a PR-derived push target when the manual branch exactly matches its branch', () => {
|
||||
expect(
|
||||
resolveComposerManualBranchNameChange({
|
||||
value: 'feature/from-pr',
|
||||
pushTarget: forkPushTarget,
|
||||
forkPushWarning: 'Cannot push to fork'
|
||||
})
|
||||
).toEqual({
|
||||
branchNameOverride: 'feature/from-pr',
|
||||
pushTarget: forkPushTarget,
|
||||
forkPushWarning: 'Cannot push to fork'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('isBranchCheckedOutInWorktrees', () => {
|
||||
it('matches a branch against both refs/heads-qualified and short worktree refs', () => {
|
||||
expect(
|
||||
|
||||
@@ -3,6 +3,7 @@ export {
|
||||
resolveComposerBranchNameOverrideForCreate,
|
||||
resolveComposerBranchReuse,
|
||||
resolveComposerBranchSelection,
|
||||
resolveComposerManualBranchNameChange,
|
||||
resolveComposerReuseOverride,
|
||||
type ComposerBranchSelection
|
||||
} from '../../../shared/composer-branch-selection'
|
||||
|
||||
@@ -339,6 +339,19 @@ describe('useComposerState host-context boundaries', () => {
|
||||
expect(section).not.toContain('folderSourceRequiresConnection')
|
||||
})
|
||||
|
||||
it('clears branch reuse state when manually editing the branch name', () => {
|
||||
const section = sourceBetween(
|
||||
HOOK_SOURCE,
|
||||
'const handleBranchNameOverrideChange = useCallback',
|
||||
'const addComposerAttachments = useCallback'
|
||||
)
|
||||
|
||||
expect(section).toContain('resolveComposerManualBranchNameChange')
|
||||
expect(section).toContain('setReuseEligibleBranch(null)')
|
||||
expect(section).toContain('setReuseSelectedBranch(false)')
|
||||
expect(section).toContain("branchAutoNameRef.current = ''")
|
||||
})
|
||||
|
||||
it('forces repo-scoped source reset when returning from folder target to a repo with the same id', () => {
|
||||
const handleRepoChange = sourceBetween(
|
||||
HOOK_SOURCE,
|
||||
|
||||
@@ -163,6 +163,7 @@ import {
|
||||
resolveComposerBranchNameOverrideForCreate,
|
||||
resolveComposerBranchReuse,
|
||||
resolveComposerBranchSelection,
|
||||
resolveComposerManualBranchNameChange,
|
||||
resolveComposerReuseOverride
|
||||
} from './composer-branch-selection'
|
||||
import { isCurrentComposerDropOwner } from './composer-drop-owner'
|
||||
@@ -255,6 +256,8 @@ export type ComposerCardProps = {
|
||||
smartNameRepoSwitchTarget?: 'project' | 'task-source'
|
||||
name: string
|
||||
onNameValueChange: (value: string) => void
|
||||
branchNameOverride: string | undefined
|
||||
onBranchNameOverrideChange: (value: string | undefined) => void
|
||||
onSmartGitHubItemSelect: (item: GitHubWorkItem) => void
|
||||
onSmartGitLabItemSelect: (item: GitLabWorkItem) => void
|
||||
onSmartBranchSelect: (refName: string, localBranchName: string) => void
|
||||
@@ -2275,6 +2278,23 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
||||
},
|
||||
[branchNameOverride, branchNameOverridePreservesNameEdits, name]
|
||||
)
|
||||
const handleBranchNameOverrideChange = useCallback(
|
||||
(value: string | undefined): void => {
|
||||
const next = resolveComposerManualBranchNameChange({
|
||||
value,
|
||||
pushTarget,
|
||||
forkPushWarning
|
||||
})
|
||||
setBranchNameOverride(next.branchNameOverride)
|
||||
setBranchNameOverridePreservesNameEdits(Boolean(next.branchNameOverride))
|
||||
setPushTarget(next.pushTarget)
|
||||
setForkPushWarning(next.forkPushWarning)
|
||||
setReuseEligibleBranch(null)
|
||||
setReuseSelectedBranch(false)
|
||||
branchAutoNameRef.current = ''
|
||||
},
|
||||
[forkPushWarning, pushTarget]
|
||||
)
|
||||
|
||||
const addComposerAttachments = useCallback((paths: string[]): void => {
|
||||
if (paths.length === 0) {
|
||||
@@ -4166,6 +4186,8 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
||||
smartNameRepoSwitchTarget: isProjectGroupTarget ? 'task-source' : 'project',
|
||||
name,
|
||||
onNameValueChange: handleNameValueChange,
|
||||
branchNameOverride: isProjectGroupTarget ? undefined : branchNameOverride,
|
||||
onBranchNameOverrideChange: isProjectGroupTarget ? () => {} : handleBranchNameOverrideChange,
|
||||
onSmartGitHubItemSelect: handleSmartGitHubItemSelect,
|
||||
onSmartGitLabItemSelect: handleSmartGitLabItemSelect,
|
||||
onSmartBranchSelect: isProjectGroupTarget ? () => {} : handleSmartBranchSelect,
|
||||
|
||||
@@ -1170,7 +1170,9 @@
|
||||
"ephemeralVm": "Per-Workspace Environment",
|
||||
"chooseRunTarget": "Choose target",
|
||||
"noRunTargets": "No run targets are ready for this project.",
|
||||
"perWorkspaceEnvHint": "Provision an on-demand environment from a recipe"
|
||||
"perWorkspaceEnvHint": "Provision an on-demand environment from a recipe",
|
||||
"branchName": "Branch name",
|
||||
"branchNamePlaceholder": "feature/my-branch"
|
||||
},
|
||||
"NewWorkspaceComposerModal": {
|
||||
"createWorktree": "Create worktree",
|
||||
|
||||
@@ -1170,7 +1170,9 @@
|
||||
"ephemeralVm": "Entorno por espacio de trabajo",
|
||||
"chooseRunTarget": "Elige objetivo",
|
||||
"noRunTargets": "No hay objetivos de ejecución listos para este proyecto.",
|
||||
"perWorkspaceEnvHint": "Proporcione un entorno bajo demanda a partir de una receta"
|
||||
"perWorkspaceEnvHint": "Proporcione un entorno bajo demanda a partir de una receta",
|
||||
"branchName": "Nombre de rama",
|
||||
"branchNamePlaceholder": "feature/my-branch"
|
||||
},
|
||||
"NewWorkspaceComposerModal": {
|
||||
"createWorktree": "Crear árbol de trabajo",
|
||||
|
||||
@@ -1170,7 +1170,9 @@
|
||||
"ephemeralVm": "ワークスペースごとの環境",
|
||||
"chooseRunTarget": "ターゲットを選択",
|
||||
"noRunTargets": "このプロジェクトには実行ターゲットが準備されていません。",
|
||||
"perWorkspaceEnvHint": "レシピからオンデマンド環境をプロビジョニングする"
|
||||
"perWorkspaceEnvHint": "レシピからオンデマンド環境をプロビジョニングする",
|
||||
"branchName": "ブランチ名",
|
||||
"branchNamePlaceholder": "feature/my-branch"
|
||||
},
|
||||
"NewWorkspaceComposerModal": {
|
||||
"createWorktree": "ワークツリーを作成する",
|
||||
|
||||
@@ -1170,7 +1170,9 @@
|
||||
"ephemeralVm": "워크스페이스별 환경",
|
||||
"chooseRunTarget": "대상 선택",
|
||||
"noRunTargets": "이 프로젝트에는 실행 대상이 준비되어 있지 않습니다.",
|
||||
"perWorkspaceEnvHint": "레시피에서 온디맨드 환경 프로비저닝"
|
||||
"perWorkspaceEnvHint": "레시피에서 온디맨드 환경 프로비저닝",
|
||||
"branchName": "브랜치 이름",
|
||||
"branchNamePlaceholder": "feature/my-branch"
|
||||
},
|
||||
"NewWorkspaceComposerModal": {
|
||||
"createWorktree": "작업 트리 만들기",
|
||||
|
||||
@@ -1170,7 +1170,9 @@
|
||||
"ephemeralVm": "工作区专属环境",
|
||||
"chooseRunTarget": "选择目标",
|
||||
"noRunTargets": "此项目还没有可用的运行目标。",
|
||||
"perWorkspaceEnvHint": "根据配方按需创建环境"
|
||||
"perWorkspaceEnvHint": "根据配方按需创建环境",
|
||||
"branchName": "分支名称",
|
||||
"branchNamePlaceholder": "feature/my-branch"
|
||||
},
|
||||
"NewWorkspaceComposerModal": {
|
||||
"createWorktree": "创建工作树",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { GitPushTarget } from './types'
|
||||
|
||||
export type ComposerBranchSelection = {
|
||||
baseBranch: string
|
||||
branchNameOverride: string | undefined
|
||||
@@ -111,3 +113,27 @@ export function resolveComposerBranchNameOverrideForCreate(args: {
|
||||
}
|
||||
return args.workspaceName === args.branchAutoName ? args.branchNameOverride : undefined
|
||||
}
|
||||
|
||||
export function resolveComposerManualBranchNameChange(args: {
|
||||
value: string | undefined
|
||||
pushTarget: GitPushTarget | undefined
|
||||
forkPushWarning: string | null
|
||||
}): {
|
||||
branchNameOverride: string | undefined
|
||||
pushTarget: GitPushTarget | undefined
|
||||
forkPushWarning: string | null
|
||||
} {
|
||||
const branchNameOverride = args.value?.trim() || undefined
|
||||
if (args.pushTarget && args.pushTarget.branchName !== branchNameOverride) {
|
||||
return {
|
||||
branchNameOverride,
|
||||
pushTarget: undefined,
|
||||
forkPushWarning: null
|
||||
}
|
||||
}
|
||||
return {
|
||||
branchNameOverride,
|
||||
pushTarget: args.pushTarget,
|
||||
forkPushWarning: args.forkPushWarning
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user