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:
@@ -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,
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}',
|
||||
|
||||
Reference in New Issue
Block a user