feat(ui): head branch selector in the GitHub pull request create form

This commit is contained in:
2026-08-16 16:27:49 +00:00
parent d5dea0c004
commit 7c8c2bba3f
12 changed files with 63 additions and 5 deletions
@@ -367,6 +367,7 @@ export const PullRequestSection: React.FC<{
}
return normalizeBranchRef(baseBranch);
});
const [headBranch, setHeadBranch] = React.useState(branch);
const [mergeMethod, setMergeMethod] = React.useState<MergeMethod>('squash');
const [isGenerating, setIsGenerating] = React.useState(false);
@@ -443,6 +444,37 @@ export const PullRequestSection: React.FC<{
return Array.from(unique).sort((a, b) => a.localeCompare(b));
}, [baseBranch, remoteBranches, selectedRemote?.name, targetBaseBranch, upstreamBranches, useDetectedUpstream]);
const availableHeadBranches = React.useMemo(() => {
const selectedRemoteName = useDetectedUpstream ? null : (selectedRemote?.name?.trim() || null);
const unique = new Set<string>();
// The current local branch must always be offered, even before the branch list resolves.
unique.add(branch);
for (const remoteBranch of remoteBranches) {
const branchName = remoteBranchToName(remoteBranch, selectedRemoteName);
if (!branchName || branchName === 'HEAD') {
continue;
}
unique.add(branchName);
}
// When using detected upstream, include all upstream repo branches
if (useDetectedUpstream) {
for (const b of upstreamBranches) {
if (b && b !== 'HEAD') {
unique.add(b);
}
}
}
const sorted = Array.from(unique).sort((a, b) => a.localeCompare(b));
if (branch && sorted[0] !== branch) {
return [branch, ...sorted.filter((candidate) => candidate !== branch)];
}
return sorted;
}, [branch, remoteBranches, selectedRemote?.name, upstreamBranches, useDetectedUpstream]);
// Update selected remote when remotes change
React.useEffect(() => {
if (remotes.length === 0) {
@@ -1141,6 +1173,7 @@ export const PullRequestSection: React.FC<{
setBody(snapshot?.body ?? '');
setDraft(snapshot?.draft ?? false);
setTargetBaseBranch(snapshot?.targetBaseBranch ? normalizeBranchRef(snapshot.targetBaseBranch) : normalizeBranchRef(baseBranch));
setHeadBranch(branch);
const nextRemote = pickInitialPrRemote(remotes, {
selectedRemoteName: snapshot?.selectedRemoteName,
trackingBranch,
@@ -1278,7 +1311,7 @@ export const PullRequestSection: React.FC<{
toast.error(t('gitView.pr.toast.baseBranchRequired'));
return;
}
if (!useDetectedUpstream && trimmedBase === branch) {
if (!useDetectedUpstream && trimmedBase === headBranch) {
toast.error(t('gitView.pr.toast.baseMustDifferFromHead'));
return;
}
@@ -1292,7 +1325,7 @@ export const PullRequestSection: React.FC<{
const pr = await github.prCreate({
directory,
title: trimmedTitle,
head: branch,
head: headBranch,
base: trimmedBase,
...(body.trim() ? { body } : {}),
draft,
@@ -1315,7 +1348,7 @@ export const PullRequestSection: React.FC<{
} finally {
setIsCreating(false);
}
}, [body, branch, detectedUpstream, directory, draft, github, prStatusKey, refresh, scheduleActionRefresh, selectedRemote, targetBaseBranch, title, trackingBranch, updatePrStatus, useDetectedUpstream, t]);
}, [body, detectedUpstream, directory, draft, github, headBranch, prStatusKey, refresh, scheduleActionRefresh, selectedRemote, targetBaseBranch, title, trackingBranch, updatePrStatus, useDetectedUpstream, t]);
const mergePr = React.useCallback(async (pr: GitHubPullRequest) => {
if (!github?.prMerge) {
@@ -1996,7 +2029,7 @@ export const PullRequestSection: React.FC<{
<div className="min-w-0">
<div className="typography-ui-label text-foreground">{t('gitView.pr.createTitle')}</div>
<div className="typography-micro text-muted-foreground truncate">
{branch} <span className="opacity-60">(local)</span> {targetBaseBranch} <span className="opacity-60">({useDetectedUpstream && detectedUpstream ? 'upstream' : 'remote'})</span>
{headBranch}{headBranch === branch ? <span className="opacity-60">(local)</span> : null} {targetBaseBranch} <span className="opacity-60">({useDetectedUpstream && detectedUpstream ? 'upstream' : 'remote'})</span>
</div>
</div>
{repoUrl ? (
@@ -2021,6 +2054,20 @@ export const PullRequestSection: React.FC<{
/>
</label>
<label className="space-y-1">
<div className="typography-micro text-muted-foreground">{t('gitView.pr.field.headBranch')}</div>
<Select value={headBranch} onValueChange={setHeadBranch}>
<SelectTrigger size="lg">
<SelectValue />
</SelectTrigger>
<SelectContent>
{availableHeadBranches.map((candidate) => (
<SelectItem key={candidate} value={candidate}>{candidate}</SelectItem>
))}
</SelectContent>
</Select>
</label>
<label className="space-y-1">
<div className="typography-micro text-muted-foreground">{t('gitView.pr.field.baseBranch')}</div>
{availableBaseBranches.length > 0 ? (
@@ -2171,7 +2218,7 @@ export const PullRequestSection: React.FC<{
size="sm"
className="min-w-[7.5rem] justify-center gap-2"
onClick={createPr}
disabled={isCreating || !isConnected || !targetBaseBranch.trim() || (!useDetectedUpstream && targetBaseBranch.trim() === branch)}
disabled={isCreating || !isConnected || !targetBaseBranch.trim() || (!useDetectedUpstream && targetBaseBranch.trim() === headBranch)}
>
<span className="inline-flex size-4 items-center justify-center">
{isCreating ? <Icon name="loader-4" className="size-4 animate-spin" /> : <Icon name="git-pull-request" className="size-4" />}
+1
View File
@@ -931,6 +931,7 @@ export const dict = {
'gitView.pr.field.baseBranch': 'Basis-Branch',
'gitView.pr.field.description': 'Beschreibung',
'gitView.pr.field.draft': 'Entwurf',
'gitView.pr.field.headBranch': 'Quellbranch',
'gitView.pr.field.title': 'Titel',
'gitView.pr.githubNotConnected': 'GitHub ist nicht verbunden',
'gitView.pr.history.merged': 'PR #{number} wurde in {base} gemergt.',
+1
View File
@@ -996,6 +996,7 @@ export const dict = {
'gitView.pr.field.baseBranch': 'Base branch',
'gitView.pr.field.description': 'Description',
'gitView.pr.field.draft': 'Draft',
'gitView.pr.field.headBranch': 'Head branch',
'gitView.pr.field.title': 'Title',
'gitView.pr.githubNotConnected': 'GitHub is not connected',
'gitView.pr.history.merged': 'PR #{number} was merged into {base}.',
+1
View File
@@ -997,6 +997,7 @@ export const dict: Record<I18nKey, string> = {
"gitView.pr.field.baseBranch": "Rama base",
"gitView.pr.field.description": "Descripción",
"gitView.pr.field.draft": "Borrador",
"gitView.pr.field.headBranch": "Rama de origen",
"gitView.pr.field.title": "Título",
"gitView.pr.githubNotConnected": "GitHub no está conectado",
"gitView.pr.history.merged": "La PR #{number} se fusionó en {base}.",
+1
View File
@@ -816,6 +816,7 @@ export const dict = {
'gitView.pr.field.baseBranch': 'Branche de base',
'gitView.pr.field.description': 'Description',
'gitView.pr.field.draft': 'Brouillon',
'gitView.pr.field.headBranch': 'Branche source',
'gitView.pr.field.title': 'Titre',
'gitView.pr.githubNotConnected': 'GitHub n\'est pas connecté',
'gitView.pr.history.merged': 'La PR #{number} a été fusionnée dans {base}.',
+1
View File
@@ -993,6 +993,7 @@ export const dict: Record<I18nKey, string> = {
'gitView.pr.field.baseBranch': 'ベースブランチ',
'gitView.pr.field.description': '説明',
'gitView.pr.field.draft': '下書き',
'gitView.pr.field.headBranch': 'ソースブランチ',
'gitView.pr.field.title': 'タイトル',
'gitView.pr.githubNotConnected': 'GitHubが接続されていません',
'gitView.pr.history.merged': 'PR #{number} は {base} にマージされました。',
+1
View File
@@ -997,6 +997,7 @@ export const dict: Record<I18nKey, string> = {
'gitView.pr.field.baseBranch': '기본 브랜치',
'gitView.pr.field.description': '설명',
'gitView.pr.field.draft': '드래프트',
'gitView.pr.field.headBranch': '소스 브랜치',
'gitView.pr.field.title': '제목',
'gitView.pr.githubNotConnected': 'GitHub에 연결되지 않음',
'gitView.pr.history.merged': 'PR #{number}이(가) {base}에 병합되었습니다.',
+1
View File
@@ -2256,6 +2256,7 @@ export const dict: Record<I18nKey, string> = {
'gitView.pr.field.baseBranch': 'Gałąź bazowa',
'gitView.pr.field.description': 'Opis',
'gitView.pr.field.draft': 'Szkic',
'gitView.pr.field.headBranch': 'Gałąź źródłowa',
'gitView.pr.field.title': 'Tytuł',
'gitView.pr.githubNotConnected': 'GitHub nie jest połączony',
'gitView.pr.history.merged': 'PR #{number} został scalony do {base}.',
@@ -997,6 +997,7 @@ export const dict: Record<I18nKey, string> = {
"gitView.pr.field.baseBranch": "Branch base",
"gitView.pr.field.description": "Descrição",
"gitView.pr.field.draft": "Borrador",
"gitView.pr.field.headBranch": "Branch de origem",
"gitView.pr.field.title": "Título",
"gitView.pr.githubNotConnected": "GitHub não está conectado",
"gitView.pr.history.merged": "A PR #{number} foi mesclada em {base}.",
+1
View File
@@ -997,6 +997,7 @@ export const dict: Record<I18nKey, string> = {
"gitView.pr.field.baseBranch": "Базова гілка",
"gitView.pr.field.description": "Опис",
"gitView.pr.field.draft": "Чернетка",
"gitView.pr.field.headBranch": "Вихідна гілка",
"gitView.pr.field.title": "Назва",
"gitView.pr.githubNotConnected": "GitHub не підключено",
"gitView.pr.history.merged": "PR #{number} злито в {base}.",
@@ -997,6 +997,7 @@ export const dict: Record<I18nKey, string> = {
'gitView.pr.field.baseBranch': '基准分支',
'gitView.pr.field.description': '描述',
'gitView.pr.field.draft': '草稿',
'gitView.pr.field.headBranch': '源分支',
'gitView.pr.field.title': '标题',
'gitView.pr.githubNotConnected': 'GitHub 未连接',
'gitView.pr.history.merged': 'PR #{number} 已合并到 {base}。',
@@ -1009,6 +1009,7 @@ export const dict: Record<I18nKey, string> = {
'gitView.pr.field.baseBranch': '基底分支',
'gitView.pr.field.description': '描述',
'gitView.pr.field.draft': '草稿',
'gitView.pr.field.headBranch': '來源分支',
'gitView.pr.field.title': '標題',
'gitView.pr.githubNotConnected': 'GitHub 未連線',
'gitView.pr.history.merged': 'PR #{number} 已合併到 {base}。',