fix: allow PR worktrees from existing local branches

Use existing local PR branches instead of blocking worktree creation
Only block PR branches already checked out in another worktree
Clarify local branch validation messages across locales
This commit is contained in:
Bohdan Triapitsyn
2026-05-08 16:59:31 +03:00
parent a591f63ed2
commit 1a0f3bea8e
9 changed files with 37 additions and 9 deletions
@@ -195,13 +195,15 @@ export function GitHubIntegrationDialog({
worktreeName: branchName,
});
const isBlocked = result.errors.some(
(entry) => entry.code === 'branch_in_use' || entry.code === 'branch_exists'
);
const blockingError = result.errors.find((entry) => entry.code === 'branch_in_use');
setValidations(prev => new Map(prev).set(branchName, {
isValid: !isBlocked,
error: isBlocked ? t('session.githubIntegration.validation.branchAlreadyCheckedOut') : null,
isValid: !blockingError,
error: blockingError
? t(blockingError.code === 'branch_exists'
? 'session.githubIntegration.validation.branchAlreadyExists'
: 'session.githubIntegration.validation.branchAlreadyCheckedOut')
: null,
}));
} catch {
setValidations(prev => new Map(prev).set(branchName, {
@@ -129,12 +129,24 @@ const sanitizeRemoteName = (value: string): string => {
return normalized || 'pr-head';
};
const resolvePrWorktreeConfig = (pr: GitHubPullRequestSummary, remoteBranches: string[]) => {
const resolvePrWorktreeConfig = (pr: GitHubPullRequestSummary, localBranches: string[], remoteBranches: string[]) => {
const headBranch = normalizeBranchName(pr.head || '');
if (!headBranch) {
throw new Error('PR head branch is missing');
}
if (localBranches.includes(headBranch)) {
return {
existingBranch: headBranch,
setUpstream: undefined,
upstreamRemote: undefined,
upstreamBranch: undefined,
ensureRemoteName: undefined,
ensureRemoteUrl: undefined,
sourceLabel: headBranch,
};
}
const availableRemoteBranch = remoteBranches.find((remoteBranch) => {
const slashIndex = remoteBranch.indexOf('/');
if (slashIndex <= 0 || slashIndex >= remoteBranch.length - 1) {
@@ -734,11 +746,15 @@ export function NewWorktreeDialog({
// Only run server validation if we have values
if (normalizedBranch && normalizedWorktree) {
const linkedPr = mode === 'new-branch' ? newBranchState.linkedPr : null;
const prConfig = linkedPr ? resolvePrWorktreeConfig(linkedPr, localBranches, remoteBranches) : null;
const result = await validateWorktreeCreate(projectRef, {
mode: mode === 'existing-branch' ? 'existing' : 'new',
mode: mode === 'existing-branch' || prConfig ? 'existing' : 'new',
branchName: normalizedBranch,
worktreeName: normalizedWorktree,
existingBranch: mode === 'existing-branch' ? normalizedBranch : undefined,
existingBranch: prConfig?.existingBranch ?? (mode === 'existing-branch' ? normalizedBranch : undefined),
...(prConfig?.ensureRemoteName ? { ensureRemoteName: prConfig.ensureRemoteName } : {}),
...(prConfig?.ensureRemoteUrl ? { ensureRemoteUrl: prConfig.ensureRemoteUrl } : {}),
});
if (abortController.signal.aborted) return;
@@ -777,8 +793,11 @@ export function NewWorktreeDialog({
projectRef,
mode,
newBranchState.branchName,
newBranchState.linkedPr,
existingBranchState.selectedBranch,
currentState.worktreeName,
localBranches,
remoteBranches,
validation.touched,
validationAbortController,
isCreating,
@@ -846,7 +865,7 @@ export function NewWorktreeDialog({
let sourceLabel = '';
const args = (() => {
if (linkedPr) {
const prConfig = resolvePrWorktreeConfig(linkedPr, remoteBranches);
const prConfig = resolvePrWorktreeConfig(linkedPr, localBranches, remoteBranches);
sourceLabel = prConfig.sourceLabel;
return {
preferredName: normalizedBranch || normalizedWorktree,
+1
View File
@@ -1323,6 +1323,7 @@ export const dict = {
'session.githubIntegration.error.notConnected': 'GitHub not connected',
'session.githubIntegration.error.loadDataFailed': 'Failed to load data',
'session.githubIntegration.validation.branchAlreadyCheckedOut': 'Branch is already checked out in a worktree',
'session.githubIntegration.validation.branchAlreadyExists': 'Branch already exists locally',
'session.githubIntegration.validation.failed': 'Validation failed',
'chat.fileAttachment.toast.attachFailed': 'Failed to attach file',
'chat.fileAttachment.toast.someFilesSkipped': 'Some files were skipped:\n{summary}',
+1
View File
@@ -1289,6 +1289,7 @@ export const dict: Record<I18nKey, string> = {
"session.githubIntegration.error.notConnected": "GitHub no está conectado",
"session.githubIntegration.error.loadDataFailed": "No se pudieron cargar los datos",
"session.githubIntegration.validation.branchAlreadyCheckedOut": "La rama ya está en uso en un worktree",
"session.githubIntegration.validation.branchAlreadyExists": "La rama ya existe localmente",
"session.githubIntegration.validation.failed": "No se pudo validar",
"chat.fileAttachment.toast.attachFailed": "No se pudo adjuntar el archivo",
"chat.fileAttachment.toast.someFilesSkipped": "Algunos archivos se omitieron:\n{summary}",
+1
View File
@@ -1325,6 +1325,7 @@ export const dict: Record<I18nKey, string> = {
'session.githubIntegration.error.notConnected': 'GitHub에 연결되어 있지 않습니다',
'session.githubIntegration.error.loadDataFailed': '데이터를 불러오지 못했습니다',
'session.githubIntegration.validation.branchAlreadyCheckedOut': '브랜치가 이미 워크트리에 체크아웃되어 있습니다',
'session.githubIntegration.validation.branchAlreadyExists': '브랜치가 이미 로컬에 있습니다',
'session.githubIntegration.validation.failed': '유효성 검사에 실패했습니다',
'chat.fileAttachment.toast.attachFailed': '첨부 파일 실패',
'chat.fileAttachment.toast.someFilesSkipped': '일부 파일을 건너뛰었습니다:\n{summary}',
+1
View File
@@ -390,6 +390,7 @@ export const dict: Record<I18nKey, string> = {
'session.githubIntegration.error.notConnected': 'GitHub nie jest połączony',
'session.githubIntegration.error.loadDataFailed': 'Nie udało się załadować danych',
'session.githubIntegration.validation.branchAlreadyCheckedOut': 'Gałąź jest już wyewidencjonowana w drzewie pracy',
'session.githubIntegration.validation.branchAlreadyExists': 'Gałąź już istnieje lokalnie',
'session.githubIntegration.validation.failed': 'Walidacja nie powiodła się',
'chat.fileAttachment.toast.attachFailed': 'Nie udało się dołączyć pliku',
'chat.fileAttachment.toast.someFilesSkipped': 'Niektóre pliki zostały pominięte:\n{summary}',
@@ -1289,6 +1289,7 @@ export const dict: Record<I18nKey, string> = {
"session.githubIntegration.error.notConnected": "GitHub não está conectado",
"session.githubIntegration.error.loadDataFailed": "Não foi possível carregar os dados",
"session.githubIntegration.validation.branchAlreadyCheckedOut": "A branch já está em uso em um worktree",
"session.githubIntegration.validation.branchAlreadyExists": "A branch já existe localmente",
"session.githubIntegration.validation.failed": "Não foi possível validar",
"chat.fileAttachment.toast.attachFailed": "Não foi possível anexar o arquivo",
"chat.fileAttachment.toast.someFilesSkipped": "Alguns arquivos foram omitidos:\n{summary}",
+1
View File
@@ -1289,6 +1289,7 @@ export const dict: Record<I18nKey, string> = {
"session.githubIntegration.error.notConnected": "GitHub не підключено",
"session.githubIntegration.error.loadDataFailed": "Не вдалося завантажити дані",
"session.githubIntegration.validation.branchAlreadyCheckedOut": "Гілку вже відкрито в worktree",
"session.githubIntegration.validation.branchAlreadyExists": "Гілка вже існує локально",
"session.githubIntegration.validation.failed": "Помилка перевірки",
"chat.fileAttachment.toast.attachFailed": "Не вдалося прикріпити файл",
"chat.fileAttachment.toast.someFilesSkipped": "Деякі файли були пропущені:\n{summary}",
@@ -1289,6 +1289,7 @@ export const dict: Record<I18nKey, string> = {
'session.githubIntegration.error.notConnected': 'GitHub 未连接',
'session.githubIntegration.error.loadDataFailed': '加载数据失败',
'session.githubIntegration.validation.branchAlreadyCheckedOut': '该分支已在某个 worktree 中检出',
'session.githubIntegration.validation.branchAlreadyExists': '该分支已在本地存在',
'session.githubIntegration.validation.failed': '校验失败',
'chat.fileAttachment.toast.attachFailed': '附加文件失败',
'chat.fileAttachment.toast.someFilesSkipped': '以下文件被跳过:\n{summary}',