feat(ui): head branch selector in the GitHub pull request create form
This commit is contained in:
@@ -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" />}
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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}.',
|
||||
|
||||
@@ -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}.",
|
||||
|
||||
@@ -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}.',
|
||||
|
||||
@@ -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} にマージされました。',
|
||||
|
||||
@@ -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}에 병합되었습니다.',
|
||||
|
||||
@@ -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}.",
|
||||
|
||||
@@ -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}。',
|
||||
|
||||
Reference in New Issue
Block a user