Preserve slash branch names from branch composer (#6748)

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Neil <charlie-eng@stably.ai>
This commit is contained in:
mehmet turac 2026-07-04 03:33:22 +03:00 committed by GitHub
parent ca36072295
commit f7a4100bc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 84 additions and 3 deletions

View File

@ -51,6 +51,7 @@ import SparseCheckoutPresetSelect from '@/components/sparse/SparseCheckoutPreset
import SmartWorkspaceNameField, {
type SmartWorkspaceNameSelection
} from '@/components/new-workspace/SmartWorkspaceNameField'
import type { SmartNameMode } from '@/components/new-workspace/smart-workspace-source-results'
import ProjectCombobox from '@/components/new-workspace/ProjectCombobox'
import type { SetupConfig } from '@/lib/new-workspace'
import type { NewWorkspaceProjectOption } from '@/lib/new-workspace-project-options'
@ -107,6 +108,7 @@ type NewWorkspaceComposerCardProps = {
onSmartGitHubItemSelect: (item: GitHubWorkItem) => void
onSmartGitLabItemSelect: (item: GitLabWorkItem) => void
onSmartBranchSelect: (refName: string, localBranchName: string) => void
onSmartNameModeChange?: (mode: SmartNameMode) => void
onSmartLinearIssueSelect: (issue: LinearIssue) => void
smartNameSelection: SmartWorkspaceNameSelection | null
onClearSmartNameSelection: () => void
@ -571,6 +573,7 @@ export default function NewWorkspaceComposerCard({
onSmartGitHubItemSelect,
onSmartGitLabItemSelect,
onSmartBranchSelect,
onSmartNameModeChange,
onSmartLinearIssueSelect,
smartNameSelection,
onClearSmartNameSelection,
@ -956,6 +959,7 @@ export default function NewWorkspaceComposerCard({
repoBackedSearchRepos={repoBackedSearchRepos}
allowCrossRepoProjectAdd={allowSmartNameAddProject}
crossRepoSwitchTarget={smartNameRepoSwitchTarget}
onActiveSourceModeChange={onSmartNameModeChange}
onPlainEnter={() => {
// Why: Enter on the workspace name advances focus to the next
// field (Agent combobox) rather than submitting, letting the user

View File

@ -92,6 +92,58 @@ describe('resolveComposerBranchSelection', () => {
})
).toBeUndefined()
})
it('uses a slash-containing typed branch name as the create override in branch mode', () => {
expect(
resolveComposerBranchNameOverrideForCreate({
branchNameOverride: undefined,
branchAutoName: '',
workspaceName: 'feature/user-profile',
preserveWorkspaceNameEdits: false,
createBranchFromWorkspaceName: true
})
).toBe('feature/user-profile')
})
it('keeps plain typed branch names on the existing sanitized-name path', () => {
expect(
resolveComposerBranchNameOverrideForCreate({
branchNameOverride: undefined,
branchAutoName: '',
workspaceName: 'feature-user-profile',
preserveWorkspaceNameEdits: false,
createBranchFromWorkspaceName: true
})
).toBeUndefined()
})
it('does not preserve a slash typed name outside branch mode (gate off)', () => {
// Why: only branch mode opts into slash preservation; every other mode keeps
// deriving the branch from the sanitized workspace name.
expect(
resolveComposerBranchNameOverrideForCreate({
branchNameOverride: undefined,
branchAutoName: '',
workspaceName: 'feature/user-profile',
preserveWorkspaceNameEdits: false,
createBranchFromWorkspaceName: false
})
).toBeUndefined()
})
it('keeps a resolver-provided override even in branch mode with a slash name', () => {
// Why: a picked branch (override set) wins over the typed slash name so the
// branch-mode gate never hijacks an explicit branch selection.
expect(
resolveComposerBranchNameOverrideForCreate({
branchNameOverride: 'feature/picked',
branchAutoName: 'feature/picked',
workspaceName: 'feature/user-profile',
preserveWorkspaceNameEdits: true,
createBranchFromWorkspaceName: true
})
).toBe('feature/picked')
})
})
describe('resolveComposerManualBranchNameChange', () => {

View File

@ -141,6 +141,7 @@ import { queueNewWorkspaceTerminalFocus } from '@/lib/new-workspace-terminal-foc
import { getSettingsForRepoRuntimeOwner } from '@/lib/repo-runtime-owner'
import { getSuggestedCreatureName } from '@/components/sidebar/worktree-name-suggestions'
import type { SmartWorkspaceNameSelection } from '@/components/new-workspace/SmartWorkspaceNameField'
import type { SmartNameMode } from '@/components/new-workspace/smart-workspace-source-results'
import { getForkPushWarning } from './fork-push-warning'
import { CONTEXTUAL_TOUR_ENABLE_AUTO_WORKSPACE_NAME_EVENT } from '@/components/contextual-tours/contextual-tour-composer-events'
import { ensureHooksConfirmed } from '@/lib/ensure-hooks-confirmed'
@ -261,6 +262,7 @@ export type ComposerCardProps = {
onSmartGitHubItemSelect: (item: GitHubWorkItem) => void
onSmartGitLabItemSelect: (item: GitLabWorkItem) => void
onSmartBranchSelect: (refName: string, localBranchName: string) => void
onSmartNameModeChange?: (mode: SmartNameMode) => void
onSmartLinearIssueSelect: (issue: LinearIssue) => void
smartNameGitHubSourceContext?: TaskSourceContext | null
/** GitLab parallel of onBaseBranchPrSelect. */
@ -1024,6 +1026,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
const [branchNameOverride, setBranchNameOverride] = useState<string | undefined>(undefined)
const [branchNameOverridePreservesNameEdits, setBranchNameOverridePreservesNameEdits] =
useState(false)
const [smartNameMode, setSmartNameMode] = useState<SmartNameMode>('smart')
// Why (#5181): when the user picks an existing LOCAL branch, let them reuse it
// (check it out) instead of creating a new branch from it. `reuseEligibleBranch`
// is the local branch name eligible for reuse (null = not a reusable local
@ -3476,7 +3479,9 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
branchAutoName: branchAutoNameRef.current,
workspaceName,
preserveWorkspaceNameEdits:
smartGitHubResolution.kind === 'pr-start-point' || branchNameOverridePreservesNameEdits
smartGitHubResolution.kind === 'pr-start-point' || branchNameOverridePreservesNameEdits,
createBranchFromWorkspaceName:
smartGitHubResolution.kind === 'none' && smartNameMode === 'branches'
})
const createDisplayName =
smartGitHubResolution.kind === 'none'
@ -3678,6 +3683,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
settings?.agentDefaultArgs,
settings?.agentDefaultEnv,
settings?.autoRenameBranchFromWork,
smartNameMode,
setSidebarOpen,
setupDecision,
sparseEnabled,
@ -3875,7 +3881,9 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
branchAutoName: branchAutoNameRef.current,
workspaceName,
preserveWorkspaceNameEdits:
smartGitHubResolution.kind === 'pr-start-point' || branchNameOverridePreservesNameEdits
smartGitHubResolution.kind === 'pr-start-point' || branchNameOverridePreservesNameEdits,
createBranchFromWorkspaceName:
smartGitHubResolution.kind === 'none' && smartNameMode === 'branches'
})
const submitBaseBranch = selectedRepoIsGit
? await resolveWorktreeCreateBaseBranch({
@ -4128,6 +4136,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
settings?.agentDefaultArgs,
settings?.agentDefaultEnv,
settings?.autoRenameBranchFromWork,
smartNameMode,
disabledTuiAgents,
setupDecision,
sparseEnabled,
@ -4191,6 +4200,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
onSmartGitHubItemSelect: handleSmartGitHubItemSelect,
onSmartGitLabItemSelect: handleSmartGitLabItemSelect,
onSmartBranchSelect: isProjectGroupTarget ? () => {} : handleSmartBranchSelect,
onSmartNameModeChange: setSmartNameMode,
onSmartLinearIssueSelect: handleSmartLinearIssueSelect,
smartNameGitHubSourceContext: selectedRepoGitHubSourceContext,
smartNameSelection,

View File

@ -99,14 +99,29 @@ export function resolveComposerReuseOverride(args: {
return args.branchNameOverride
}
/**
* The branch-name override to apply when creating a worktree from the composer.
*
* With no resolver-provided override, branch mode (#6721) keeps a
* slash-containing typed name as the git branch validated downstream by
* `git check-ref-format` while the worktree folder name is sanitized
* separately; every other mode leaves the branch to be derived from the
* sanitized name. With an override, keep it verbatim when the workspace name is
* user-edited (`preserveWorkspaceNameEdits`) or still matches the auto-name.
*/
export function resolveComposerBranchNameOverrideForCreate(args: {
branchNameOverride: string | undefined
branchAutoName: string
workspaceName: string
preserveWorkspaceNameEdits: boolean
createBranchFromWorkspaceName?: boolean
}): string | undefined {
if (!args.branchNameOverride) {
return undefined
// Why: branch mode keeps slash-containing git branch names while the
// workspace folder name may still be sanitized separately.
return args.createBranchFromWorkspaceName && args.workspaceName.includes('/')
? args.workspaceName
: undefined
}
if (args.preserveWorkspaceNameEdits) {
return args.branchNameOverride