fix(PullRequestSection): normalize and resolve base/remote branches (#388)
This commit is contained in:
@@ -995,19 +995,58 @@ export const GitView: React.FC<GitViewProps> = ({ mode = 'full' }) => {
|
|||||||
}, [remotes, remoteBranches, remoteUrl, status?.tracking]);
|
}, [remotes, remoteBranches, remoteUrl, status?.tracking]);
|
||||||
|
|
||||||
const baseBranch = React.useMemo(() => {
|
const baseBranch = React.useMemo(() => {
|
||||||
const fromMeta = typeof worktreeMetadata?.createdFromBranch === 'string'
|
const remoteNames = new Set(effectiveRemotes.map((remote) => remote.name));
|
||||||
? worktreeMetadata.createdFromBranch.trim()
|
const normalizeBaseCandidate = (value: string): string => {
|
||||||
: '';
|
if (!value) {
|
||||||
if (fromMeta && fromMeta !== 'HEAD') return fromMeta;
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
const fromHint = typeof rootBranchHint === 'string' ? rootBranchHint.trim() : '';
|
let normalized = value.trim();
|
||||||
if (fromHint && fromHint !== 'HEAD') return fromHint;
|
if (!normalized || normalized === 'HEAD') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localBranches.includes(normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.startsWith('refs/heads/')) {
|
||||||
|
normalized = normalized.slice('refs/heads/'.length);
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('heads/')) {
|
||||||
|
normalized = normalized.slice('heads/'.length);
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('remotes/')) {
|
||||||
|
normalized = normalized.slice('remotes/'.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const slashIndex = normalized.indexOf('/');
|
||||||
|
if (slashIndex > 0) {
|
||||||
|
const maybeRemote = normalized.slice(0, slashIndex);
|
||||||
|
if (remoteNames.has(maybeRemote)) {
|
||||||
|
const withoutRemote = normalized.slice(slashIndex + 1).trim();
|
||||||
|
if (withoutRemote) {
|
||||||
|
normalized = withoutRemote;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromMeta = normalizeBaseCandidate(
|
||||||
|
typeof worktreeMetadata?.createdFromBranch === 'string' ? worktreeMetadata.createdFromBranch : ''
|
||||||
|
);
|
||||||
|
if (fromMeta) return fromMeta;
|
||||||
|
|
||||||
|
const fromHint = normalizeBaseCandidate(typeof rootBranchHint === 'string' ? rootBranchHint : '');
|
||||||
|
if (fromHint) return fromHint;
|
||||||
|
|
||||||
if (localBranches.includes('main')) return 'main';
|
if (localBranches.includes('main')) return 'main';
|
||||||
if (localBranches.includes('master')) return 'master';
|
if (localBranches.includes('master')) return 'master';
|
||||||
if (localBranches.includes('develop')) return 'develop';
|
if (localBranches.includes('develop')) return 'develop';
|
||||||
return 'main';
|
return 'main';
|
||||||
}, [localBranches, rootBranchHint, worktreeMetadata?.createdFromBranch]);
|
}, [effectiveRemotes, localBranches, rootBranchHint, worktreeMetadata?.createdFromBranch]);
|
||||||
|
|
||||||
const availableIdentities = React.useMemo(() => {
|
const availableIdentities = React.useMemo(() => {
|
||||||
const unique = new Map<string, GitIdentityProfile>();
|
const unique = new Map<string, GitIdentityProfile>();
|
||||||
@@ -1755,6 +1794,7 @@ export const GitView: React.FC<GitViewProps> = ({ mode = 'full' }) => {
|
|||||||
branch={pullRequestProps.branch}
|
branch={pullRequestProps.branch}
|
||||||
baseBranch={baseBranch}
|
baseBranch={baseBranch}
|
||||||
remotes={remotes}
|
remotes={remotes}
|
||||||
|
remoteBranches={remoteBranches}
|
||||||
onGeneratedDescription={scrollActionPanelToBottom}
|
onGeneratedDescription={scrollActionPanelToBottom}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -112,6 +112,44 @@ const branchToTitle = (branch: string): string => {
|
|||||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeBranchRef = (value: string): string => {
|
||||||
|
let normalized = value.trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('refs/heads/')) {
|
||||||
|
normalized = normalized.slice('refs/heads/'.length);
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('heads/')) {
|
||||||
|
normalized = normalized.slice('heads/'.length);
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('remotes/')) {
|
||||||
|
normalized = normalized.slice('remotes/'.length);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
|
||||||
|
const remoteBranchToName = (value: string, remoteName: string | null): string => {
|
||||||
|
const normalized = normalizeBranchRef(value);
|
||||||
|
if (!normalized || normalized.includes('->')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteName) {
|
||||||
|
const prefix = `${remoteName}/`;
|
||||||
|
if (normalized.startsWith(prefix)) {
|
||||||
|
return normalized.slice(prefix.length).trim();
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const slashIndex = normalized.indexOf('/');
|
||||||
|
if (slashIndex > 0) {
|
||||||
|
return normalized.slice(slashIndex + 1).trim();
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
|
||||||
const getPullRequestSnapshotKey = (directory: string, branch: string): string => `${directory}::${branch}`;
|
const getPullRequestSnapshotKey = (directory: string, branch: string): string => `${directory}::${branch}`;
|
||||||
|
|
||||||
type PullRequestDraftSnapshot = {
|
type PullRequestDraftSnapshot = {
|
||||||
@@ -119,6 +157,7 @@ type PullRequestDraftSnapshot = {
|
|||||||
body: string;
|
body: string;
|
||||||
draft: boolean;
|
draft: boolean;
|
||||||
additionalContext: string;
|
additionalContext: string;
|
||||||
|
targetBaseBranch?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TimelineCommentItem = {
|
type TimelineCommentItem = {
|
||||||
@@ -175,9 +214,10 @@ export const PullRequestSection: React.FC<{
|
|||||||
branch: string;
|
branch: string;
|
||||||
baseBranch: string;
|
baseBranch: string;
|
||||||
remotes?: GitRemote[];
|
remotes?: GitRemote[];
|
||||||
|
remoteBranches?: string[];
|
||||||
variant?: 'framed' | 'plain';
|
variant?: 'framed' | 'plain';
|
||||||
onGeneratedDescription?: () => void;
|
onGeneratedDescription?: () => void;
|
||||||
}> = ({ directory, branch, baseBranch, remotes = [], variant = 'framed', onGeneratedDescription }) => {
|
}> = ({ directory, branch, baseBranch, remotes = [], remoteBranches = [], variant = 'framed', onGeneratedDescription }) => {
|
||||||
const { github } = useRuntimeAPIs();
|
const { github } = useRuntimeAPIs();
|
||||||
const githubAuthStatus = useGitHubAuthStore((state) => state.status);
|
const githubAuthStatus = useGitHubAuthStore((state) => state.status);
|
||||||
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
|
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
|
||||||
@@ -211,6 +251,15 @@ export const PullRequestSection: React.FC<{
|
|||||||
const [body, setBody] = React.useState(() => initialSnapshot?.body ?? '');
|
const [body, setBody] = React.useState(() => initialSnapshot?.body ?? '');
|
||||||
const [draft, setDraft] = React.useState(() => initialSnapshot?.draft ?? false);
|
const [draft, setDraft] = React.useState(() => initialSnapshot?.draft ?? false);
|
||||||
const [additionalContext, setAdditionalContext] = React.useState(() => initialSnapshot?.additionalContext ?? '');
|
const [additionalContext, setAdditionalContext] = React.useState(() => initialSnapshot?.additionalContext ?? '');
|
||||||
|
const [targetBaseBranch, setTargetBaseBranch] = React.useState(() => {
|
||||||
|
const fromSnapshot = typeof initialSnapshot?.targetBaseBranch === 'string'
|
||||||
|
? normalizeBranchRef(initialSnapshot.targetBaseBranch)
|
||||||
|
: '';
|
||||||
|
if (fromSnapshot) {
|
||||||
|
return fromSnapshot;
|
||||||
|
}
|
||||||
|
return normalizeBranchRef(baseBranch);
|
||||||
|
});
|
||||||
const [mergeMethod, setMergeMethod] = React.useState<MergeMethod>('squash');
|
const [mergeMethod, setMergeMethod] = React.useState<MergeMethod>('squash');
|
||||||
|
|
||||||
const [isGenerating, setIsGenerating] = React.useState(false);
|
const [isGenerating, setIsGenerating] = React.useState(false);
|
||||||
@@ -227,6 +276,31 @@ export const PullRequestSection: React.FC<{
|
|||||||
const [isContextSheetOpen, setIsContextSheetOpen] = React.useState(false);
|
const [isContextSheetOpen, setIsContextSheetOpen] = React.useState(false);
|
||||||
const [selectedRemote, setSelectedRemote] = React.useState<GitRemote | null>(() => remotes[0] ?? null);
|
const [selectedRemote, setSelectedRemote] = React.useState<GitRemote | null>(() => remotes[0] ?? null);
|
||||||
|
|
||||||
|
const availableBaseBranches = React.useMemo(() => {
|
||||||
|
const selectedRemoteName = selectedRemote?.name?.trim() || null;
|
||||||
|
const unique = new Set<string>();
|
||||||
|
|
||||||
|
for (const remoteBranch of remoteBranches) {
|
||||||
|
const branchName = remoteBranchToName(remoteBranch, selectedRemoteName);
|
||||||
|
if (!branchName || branchName === 'HEAD') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
unique.add(branchName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultBase = normalizeBranchRef(baseBranch);
|
||||||
|
if (defaultBase && defaultBase !== 'HEAD') {
|
||||||
|
unique.add(defaultBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTarget = normalizeBranchRef(targetBaseBranch);
|
||||||
|
if (currentTarget && currentTarget !== 'HEAD') {
|
||||||
|
unique.add(currentTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(unique).sort((a, b) => a.localeCompare(b));
|
||||||
|
}, [baseBranch, remoteBranches, selectedRemote?.name, targetBaseBranch]);
|
||||||
|
|
||||||
const hasMultipleRemotes = remotes.length > 1;
|
const hasMultipleRemotes = remotes.length > 1;
|
||||||
|
|
||||||
// Update selected remote when remotes change
|
// Update selected remote when remotes change
|
||||||
@@ -236,6 +310,27 @@ export const PullRequestSection: React.FC<{
|
|||||||
}
|
}
|
||||||
}, [remotes, selectedRemote]);
|
}, [remotes, selectedRemote]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const normalizedBase = normalizeBranchRef(baseBranch);
|
||||||
|
if (!targetBaseBranch && normalizedBase) {
|
||||||
|
setTargetBaseBranch(normalizedBase);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availableBaseBranches.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!availableBaseBranches.includes(targetBaseBranch)) {
|
||||||
|
const fallback = availableBaseBranches.includes(normalizedBase)
|
||||||
|
? normalizedBase
|
||||||
|
: availableBaseBranches[0];
|
||||||
|
if (fallback) {
|
||||||
|
setTargetBaseBranch(fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [availableBaseBranches, baseBranch, targetBaseBranch]);
|
||||||
|
|
||||||
const [checksDialogOpen, setChecksDialogOpen] = React.useState(false);
|
const [checksDialogOpen, setChecksDialogOpen] = React.useState(false);
|
||||||
const [checkDetails, setCheckDetails] = React.useState<GitHubPullRequestContextResult | null>(null);
|
const [checkDetails, setCheckDetails] = React.useState<GitHubPullRequestContextResult | null>(null);
|
||||||
const [isLoadingCheckDetails, setIsLoadingCheckDetails] = React.useState(false);
|
const [isLoadingCheckDetails, setIsLoadingCheckDetails] = React.useState(false);
|
||||||
@@ -863,11 +958,12 @@ export const PullRequestSection: React.FC<{
|
|||||||
setTitle(snapshot?.title ?? branchToTitle(branch));
|
setTitle(snapshot?.title ?? branchToTitle(branch));
|
||||||
setBody(snapshot?.body ?? '');
|
setBody(snapshot?.body ?? '');
|
||||||
setDraft(snapshot?.draft ?? false);
|
setDraft(snapshot?.draft ?? false);
|
||||||
|
setTargetBaseBranch(snapshot?.targetBaseBranch ? normalizeBranchRef(snapshot.targetBaseBranch) : normalizeBranchRef(baseBranch));
|
||||||
setStatus(statusSnapshot);
|
setStatus(statusSnapshot);
|
||||||
setError(null);
|
setError(null);
|
||||||
setIsInitialStatusResolved(Boolean(statusSnapshot));
|
setIsInitialStatusResolved(Boolean(statusSnapshot));
|
||||||
void refresh({ force: true, markInitialResolved: true });
|
void refresh({ force: true, markInitialResolved: true });
|
||||||
}, [branch, refresh, snapshotKey]);
|
}, [baseBranch, branch, refresh, snapshotKey]);
|
||||||
|
|
||||||
// Refetch when selected remote changes
|
// Refetch when selected remote changes
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -936,8 +1032,9 @@ export const PullRequestSection: React.FC<{
|
|||||||
body,
|
body,
|
||||||
draft,
|
draft,
|
||||||
additionalContext,
|
additionalContext,
|
||||||
|
targetBaseBranch,
|
||||||
});
|
});
|
||||||
}, [snapshotKey, title, body, draft, additionalContext, directory, branch]);
|
}, [snapshotKey, title, body, draft, additionalContext, targetBaseBranch, directory, branch]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!status) {
|
if (!status) {
|
||||||
@@ -953,7 +1050,7 @@ export const PullRequestSection: React.FC<{
|
|||||||
try {
|
try {
|
||||||
const zenModel = useConfigStore.getState().settingsZenModel;
|
const zenModel = useConfigStore.getState().settingsZenModel;
|
||||||
const generated = await generatePullRequestDescription(directory, {
|
const generated = await generatePullRequestDescription(directory, {
|
||||||
base: baseBranch,
|
base: targetBaseBranch,
|
||||||
head: branch,
|
head: branch,
|
||||||
context: additionalContext,
|
context: additionalContext,
|
||||||
...(zenModel ? { zenModel } : {}),
|
...(zenModel ? { zenModel } : {}),
|
||||||
@@ -972,7 +1069,7 @@ export const PullRequestSection: React.FC<{
|
|||||||
} finally {
|
} finally {
|
||||||
setIsGenerating(false);
|
setIsGenerating(false);
|
||||||
}
|
}
|
||||||
}, [baseBranch, branch, directory, isGenerating, additionalContext, onGeneratedDescription]);
|
}, [branch, directory, isGenerating, additionalContext, onGeneratedDescription, targetBaseBranch]);
|
||||||
|
|
||||||
const createPr = React.useCallback(async () => {
|
const createPr = React.useCallback(async () => {
|
||||||
if (!github?.prCreate) {
|
if (!github?.prCreate) {
|
||||||
@@ -985,6 +1082,16 @@ export const PullRequestSection: React.FC<{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trimmedBase = targetBaseBranch.trim();
|
||||||
|
if (!trimmedBase) {
|
||||||
|
toast.error('Base branch is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (trimmedBase === branch) {
|
||||||
|
toast.error('Base branch must differ from head branch');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
try {
|
try {
|
||||||
// Let the server determine the head source from tracking info
|
// Let the server determine the head source from tracking info
|
||||||
@@ -993,7 +1100,7 @@ export const PullRequestSection: React.FC<{
|
|||||||
directory,
|
directory,
|
||||||
title: trimmedTitle,
|
title: trimmedTitle,
|
||||||
head: branch,
|
head: branch,
|
||||||
base: baseBranch,
|
base: trimmedBase,
|
||||||
...(body.trim() ? { body } : {}),
|
...(body.trim() ? { body } : {}),
|
||||||
draft,
|
draft,
|
||||||
...(selectedRemote ? { remote: selectedRemote.name } : {}),
|
...(selectedRemote ? { remote: selectedRemote.name } : {}),
|
||||||
@@ -1007,7 +1114,7 @@ export const PullRequestSection: React.FC<{
|
|||||||
} finally {
|
} finally {
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
}
|
}
|
||||||
}, [baseBranch, body, branch, directory, draft, github, refresh, selectedRemote, title]);
|
}, [body, branch, directory, draft, github, refresh, selectedRemote, targetBaseBranch, title]);
|
||||||
|
|
||||||
const mergePr = React.useCallback(async (pr: GitHubPullRequest) => {
|
const mergePr = React.useCallback(async (pr: GitHubPullRequest) => {
|
||||||
if (!github?.prMerge) {
|
if (!github?.prMerge) {
|
||||||
@@ -1469,7 +1576,7 @@ export const PullRequestSection: React.FC<{
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="typography-ui-label text-foreground">Create PR</div>
|
<div className="typography-ui-label text-foreground">Create PR</div>
|
||||||
<div className="typography-micro text-muted-foreground truncate">
|
<div className="typography-micro text-muted-foreground truncate">
|
||||||
{branch} → {baseBranch}
|
{branch} → {targetBaseBranch}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{repoUrl ? (
|
{repoUrl ? (
|
||||||
@@ -1494,6 +1601,28 @@ export const PullRequestSection: React.FC<{
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label className="space-y-1">
|
||||||
|
<div className="typography-micro text-muted-foreground">Base branch</div>
|
||||||
|
{availableBaseBranches.length > 0 ? (
|
||||||
|
<Select value={targetBaseBranch} onValueChange={setTargetBaseBranch}>
|
||||||
|
<SelectTrigger className="h-9">
|
||||||
|
<SelectValue placeholder="Select base branch" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableBaseBranches.map((candidate) => (
|
||||||
|
<SelectItem key={candidate} value={candidate}>{candidate}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
value={targetBaseBranch}
|
||||||
|
onChange={(e) => setTargetBaseBranch(e.target.value)}
|
||||||
|
placeholder="main"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
|
||||||
<label className="space-y-1">
|
<label className="space-y-1">
|
||||||
<div className="typography-micro text-muted-foreground">Description</div>
|
<div className="typography-micro text-muted-foreground">Description</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -1632,7 +1761,7 @@ export const PullRequestSection: React.FC<{
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="min-w-[7.5rem] justify-center gap-2"
|
className="min-w-[7.5rem] justify-center gap-2"
|
||||||
onClick={createPr}
|
onClick={createPr}
|
||||||
disabled={isCreating || !isConnected}
|
disabled={isCreating || !isConnected || !targetBaseBranch.trim() || targetBaseBranch.trim() === branch}
|
||||||
>
|
>
|
||||||
<span className="inline-flex size-4 items-center justify-center">
|
<span className="inline-flex size-4 items-center justify-center">
|
||||||
{isCreating ? <RiLoader4Line className="size-4 animate-spin" /> : <RiGitPullRequestLine className="size-4" />}
|
{isCreating ? <RiLoader4Line className="size-4 animate-spin" /> : <RiGitPullRequestLine className="size-4" />}
|
||||||
|
|||||||
@@ -7591,14 +7591,14 @@ async function main(options = {}) {
|
|||||||
const directory = typeof req.body?.directory === 'string' ? req.body.directory.trim() : '';
|
const directory = typeof req.body?.directory === 'string' ? req.body.directory.trim() : '';
|
||||||
const title = typeof req.body?.title === 'string' ? req.body.title.trim() : '';
|
const title = typeof req.body?.title === 'string' ? req.body.title.trim() : '';
|
||||||
const head = typeof req.body?.head === 'string' ? req.body.head.trim() : '';
|
const head = typeof req.body?.head === 'string' ? req.body.head.trim() : '';
|
||||||
const base = typeof req.body?.base === 'string' ? req.body.base.trim() : '';
|
const requestedBase = typeof req.body?.base === 'string' ? req.body.base.trim() : '';
|
||||||
const body = typeof req.body?.body === 'string' ? req.body.body : undefined;
|
const body = typeof req.body?.body === 'string' ? req.body.body : undefined;
|
||||||
const draft = typeof req.body?.draft === 'boolean' ? req.body.draft : undefined;
|
const draft = typeof req.body?.draft === 'boolean' ? req.body.draft : undefined;
|
||||||
// remote = target repo (where PR is created, e.g., 'upstream' for forks)
|
// remote = target repo (where PR is created, e.g., 'upstream' for forks)
|
||||||
const remote = typeof req.body?.remote === 'string' ? req.body.remote.trim() : 'origin';
|
const remote = typeof req.body?.remote === 'string' ? req.body.remote.trim() : 'origin';
|
||||||
// headRemote = source repo (where head branch lives, e.g., 'origin' for forks)
|
// headRemote = source repo (where head branch lives, e.g., 'origin' for forks)
|
||||||
const headRemote = typeof req.body?.headRemote === 'string' ? req.body.headRemote.trim() : '';
|
const headRemote = typeof req.body?.headRemote === 'string' ? req.body.headRemote.trim() : '';
|
||||||
if (!directory || !title || !head || !base) {
|
if (!directory || !title || !head || !requestedBase) {
|
||||||
return res.status(400).json({ error: 'directory, title, head, base are required' });
|
return res.status(400).json({ error: 'directory, title, head, base are required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7614,13 +7614,42 @@ async function main(options = {}) {
|
|||||||
return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' });
|
return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeBranchRef = (value, remoteNames = new Set()) => {
|
||||||
|
if (!value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
let normalized = value.trim();
|
||||||
|
if (normalized.startsWith('refs/heads/')) {
|
||||||
|
normalized = normalized.substring('refs/heads/'.length);
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('heads/')) {
|
||||||
|
normalized = normalized.substring('heads/'.length);
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('remotes/')) {
|
||||||
|
normalized = normalized.substring('remotes/'.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const slashIndex = normalized.indexOf('/');
|
||||||
|
if (slashIndex > 0) {
|
||||||
|
const maybeRemote = normalized.slice(0, slashIndex);
|
||||||
|
if (remoteNames.has(maybeRemote)) {
|
||||||
|
const withoutRemotePrefix = normalized.slice(slashIndex + 1).trim();
|
||||||
|
if (withoutRemotePrefix) {
|
||||||
|
normalized = withoutRemotePrefix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
|
||||||
// Determine the source remote for the head branch
|
// Determine the source remote for the head branch
|
||||||
// Priority: 1) explicit headRemote, 2) tracking branch remote, 3) 'origin' if targeting non-origin
|
// Priority: 1) explicit headRemote, 2) tracking branch remote, 3) 'origin' if targeting non-origin
|
||||||
let sourceRemote = headRemote;
|
let sourceRemote = headRemote;
|
||||||
|
const { getStatus, getRemotes } = await import('./lib/git-service.js');
|
||||||
|
|
||||||
// If no explicit headRemote, check the branch's tracking info
|
// If no explicit headRemote, check the branch's tracking info
|
||||||
if (!sourceRemote) {
|
if (!sourceRemote) {
|
||||||
const { getStatus } = await import('./lib/git-service.js');
|
|
||||||
const status = await getStatus(directory).catch(() => null);
|
const status = await getStatus(directory).catch(() => null);
|
||||||
if (status?.tracking) {
|
if (status?.tracking) {
|
||||||
// tracking is like "gsxdsm/fix/multi-remote-branch-creation" or "origin/main"
|
// tracking is like "gsxdsm/fix/multi-remote-branch-creation" or "origin/main"
|
||||||
@@ -7636,6 +7665,22 @@ async function main(options = {}) {
|
|||||||
sourceRemote = 'origin';
|
sourceRemote = 'origin';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const remoteNames = new Set([remote]);
|
||||||
|
const remotes = await getRemotes(directory).catch(() => []);
|
||||||
|
for (const item of remotes) {
|
||||||
|
if (item?.name) {
|
||||||
|
remoteNames.add(item.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sourceRemote) {
|
||||||
|
remoteNames.add(sourceRemote);
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = normalizeBranchRef(requestedBase, remoteNames);
|
||||||
|
if (!base) {
|
||||||
|
return res.status(400).json({ error: 'Invalid base branch name' });
|
||||||
|
}
|
||||||
|
|
||||||
// For fork workflows: we need to determine the correct head reference
|
// For fork workflows: we need to determine the correct head reference
|
||||||
let headRef = head;
|
let headRef = head;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user