fix(walkthrough): use remote default branch
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { deriveBaseBranch, hasResolvableBaseBranch } from './baseBranch';
|
||||
|
||||
describe('deriveBaseBranch', () => {
|
||||
test('prefers the remote default branch hint over conventional fallbacks', () => {
|
||||
expect(deriveBaseBranch({
|
||||
remoteNames: new Set(['origin']),
|
||||
localBranches: ['next'],
|
||||
rootBranchHint: 'origin/react',
|
||||
})).toBe('react');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasResolvableBaseBranch', () => {
|
||||
test('rejects the main fallback when it does not exist', () => {
|
||||
expect(hasResolvableBaseBranch({
|
||||
baseBranch: 'main',
|
||||
localBranches: ['next', 'react'],
|
||||
remoteBranches: ['origin/next', 'origin/react'],
|
||||
})).toBe(false);
|
||||
});
|
||||
|
||||
test('accepts a base branch available through a remote-tracking ref', () => {
|
||||
expect(hasResolvableBaseBranch({
|
||||
baseBranch: 'main',
|
||||
localBranches: ['next'],
|
||||
remoteBranches: ['origin/main', 'origin/next'],
|
||||
})).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -62,3 +62,18 @@ export const deriveBaseBranch = (options: {
|
||||
if (localBranches.includes('develop')) return 'develop';
|
||||
return 'main';
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether a base branch can be resolved locally or through one of the active
|
||||
* remote-tracking refs. Callers must not offer comparisons against the `main`
|
||||
* fallback when that ref does not actually exist in the repository.
|
||||
*/
|
||||
export const hasResolvableBaseBranch = (options: {
|
||||
baseBranch: string;
|
||||
localBranches: readonly string[];
|
||||
remoteBranches: readonly string[];
|
||||
}): boolean => {
|
||||
const { baseBranch, localBranches, remoteBranches } = options;
|
||||
return localBranches.includes(baseBranch)
|
||||
|| remoteBranches.some((branch) => branch.endsWith(`/${baseBranch}`));
|
||||
};
|
||||
|
||||
@@ -14,10 +14,10 @@ import { useI18n, type Locale } from '@/lib/i18n';
|
||||
import { buildWalkthroughView } from '@/lib/walkthrough/model';
|
||||
import type { WalkthroughSource, WalkthroughWorkingTreeScope } from '@/lib/walkthrough/types';
|
||||
import { ModelSelector } from '@/components/sections/agents/ModelSelector';
|
||||
import { deriveBaseBranch } from '@/components/views/git/baseBranch';
|
||||
import { deriveBaseBranch, hasResolvableBaseBranch } from '@/components/views/git/baseBranch';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useGitBranches, useGitStatus } from '@/stores/useGitStore';
|
||||
import { useGitBranches, useGitStatus, useGitStore } from '@/stores/useGitStore';
|
||||
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||
import {
|
||||
getFreshestPrStatusForBranch,
|
||||
@@ -152,6 +152,12 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => {
|
||||
|
||||
const status = useGitStatus(directory || null);
|
||||
const branches = useGitBranches(directory || null);
|
||||
const ensureAll = useGitStore((state) => state.ensureAll);
|
||||
const { github, git } = useRuntimeAPIs();
|
||||
|
||||
useEffect(() => {
|
||||
if (directory) void ensureAll(directory, git);
|
||||
}, [directory, ensureAll, git]);
|
||||
|
||||
// The branch source reviews everything on this branch that is not on its
|
||||
// base. Three-dot semantics server-side mean merges from the base are
|
||||
@@ -162,22 +168,28 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => {
|
||||
if (!headRef) return null;
|
||||
const all = branches?.all ?? [];
|
||||
const localBranches = all.filter((name) => !name.startsWith('remotes/'));
|
||||
const remoteBranches = all
|
||||
.filter((name) => name.startsWith('remotes/'))
|
||||
.map((name) => name.slice('remotes/'.length));
|
||||
const remoteNames = new Set(
|
||||
all
|
||||
.filter((name) => name.startsWith('remotes/'))
|
||||
.map((name) => name.slice('remotes/'.length).split('/')[0])
|
||||
remoteBranches
|
||||
.map((name) => name.split('/')[0])
|
||||
.filter(Boolean)
|
||||
);
|
||||
const baseRef = deriveBaseBranch({ remoteNames, localBranches });
|
||||
if (!baseRef || baseRef === headRef) return null;
|
||||
const trackingRemote = status?.tracking?.split('/')[0];
|
||||
const rootBranchHint = (trackingRemote && branches?.defaultBranches?.[trackingRemote])
|
||||
?? branches?.defaultBranches?.origin;
|
||||
const baseRef = deriveBaseBranch({ remoteNames, localBranches, rootBranchHint });
|
||||
if (!baseRef || baseRef === headRef || !hasResolvableBaseBranch({ baseBranch: baseRef, localBranches, remoteBranches })) {
|
||||
return null;
|
||||
}
|
||||
return { kind: 'branch', baseRef, headRef };
|
||||
}, [branches, currentBranch]);
|
||||
}, [branches, currentBranch, status?.tracking]);
|
||||
|
||||
// The pull request for this branch used to appear only after visiting the PR
|
||||
// panel, because nothing else asked GitHub about it. Ask here too: the status
|
||||
// store already dedupes by signature and throttles by TTL, so several panels
|
||||
// wanting the same answer produce one request.
|
||||
const { github } = useRuntimeAPIs();
|
||||
const githubConnected = useGitHubAuthStore((state) => state.status?.connected ?? false);
|
||||
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
|
||||
const ensurePrStatusEntry = useGitHubPrStatusStore((state) => state.ensureEntry);
|
||||
|
||||
@@ -183,6 +183,7 @@ export interface GitBranch {
|
||||
all: string[];
|
||||
current: string;
|
||||
branches: Record<string, GitBranchDetails>;
|
||||
defaultBranches?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface GitCommitSummary {
|
||||
|
||||
Reference in New Issue
Block a user