Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const bridgeMock = vi.hoisted(() => ({
vi.fn<
() => Promise<{ sourceBranch: string | null; commitsAhead: number; sourceAhead: number }>
>(),
ghGetPrForBranch: vi.fn<() => Promise<null>>(),
generateCommitMessage: vi.fn<() => Promise<{ message: string }>>(),
}));

Expand Down Expand Up @@ -239,6 +240,7 @@ describe("GitReviewSidebar", () => {
bridgeMock.gitCommit.mockResolvedValue(undefined);
bridgeMock.gitFetch.mockResolvedValue(undefined);
bridgeMock.gitGetWorktreeSourceBranch.mockImplementation(() => new Promise(() => {}));
bridgeMock.ghGetPrForBranch.mockResolvedValue(null);
bridgeMock.generateCommitMessage.mockResolvedValue({ message: "generated" });
useGitStore.setState({
ghAvailable: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ export function GitReviewSidebar(props: {
project,
effectiveBranch,
effectivePrKey,
worktreePath,
isGitHub,
ghAvailable,
preferredSourceBranch: prBaseBranch,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,11 @@ export function useGitReviewActions(args: UseGitReviewActionsArgs) {
// callers that chain further work — e.g. the combined "Commit & Create PR"
// action — only proceed when there's actually something pushed to open a PR
// against.
async function handleCommit(addAll: boolean, pushAfter = false): Promise<boolean> {
async function handleCommit(
addAll: boolean,
pushAfter = false,
skipDelayedRefresh = false,
): Promise<boolean> {
setIsCommitting(true);
try {
let message = commitMessage.trim();
Expand Down Expand Up @@ -300,7 +304,12 @@ export function useGitReviewActions(args: UseGitReviewActionsArgs) {
// GitHub takes a beat to register the new commits — refreshing
// immediately fetches a stale PR snapshot. Delay so the post-push
// refresh picks up fresh state.
setTimeout(() => onRefresh(), 1500);
// When the caller chains a PR creation (skipDelayedRefresh), the
// delayed refresh would race with handleCreatePr's setPrData and
// overwrite it with a stale null from ghGetPrForBranch.
if (!skipDelayedRefresh) {
setTimeout(() => onRefresh(), 2000);
}
} finally {
setIsSyncing(false);
}
Expand Down Expand Up @@ -390,7 +399,7 @@ export function useGitReviewActions(args: UseGitReviewActionsArgs) {
// GitHub takes a beat to register the new commits — refreshing
// immediately fetches a stale PR snapshot (mergeable/checks not
// yet updated). Delay so the post-push refresh picks up fresh state.
setTimeout(() => onRefresh(), 1500);
setTimeout(() => onRefresh(), 2000);
} else {
captureProductEvent("git.sync_action", {
action: "sync",
Expand Down Expand Up @@ -425,7 +434,7 @@ export function useGitReviewActions(args: UseGitReviewActionsArgs) {
if (key === "push") {
refreshPrAfterPush();
applyStatusOptimistic((s) => ({ ...s, ahead: 0 }));
setTimeout(() => onRefresh(), 1500);
setTimeout(() => onRefresh(), 2000);
return;
}
onRefresh();
Expand Down Expand Up @@ -642,6 +651,7 @@ export function useGitReviewActions(args: UseGitReviewActionsArgs) {
useGitStore.getState().setPrData(effectivePrKey, pr);
}
patch(storeKey, { prTitle: "", prBody: "", prGen: null });
onRefresh();
} catch (err) {
console.error("[git] create PR failed", err);
toast.danger(friendlyError(err));
Expand All @@ -655,9 +665,10 @@ export function useGitReviewActions(args: UseGitReviewActionsArgs) {
// opens the dialog), so this stays a single uninterrupted action regardless
// of the prCreateMode setting.
async function handleCommitAndCreatePr(addAll: boolean): Promise<void> {
const committed = await handleCommit(addAll, true);
const committed = await handleCommit(addAll, true, true);
if (!committed) return;
await handleCreatePr(false);
onRefresh();
}

async function handleGeneratePrSummary(): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export function useSourceBranchData(params: {
project: { location: ProjectLocation };
effectiveBranch: string | undefined;
effectivePrKey: string | undefined;
worktreePath: string | undefined;
isGitHub: boolean;
ghAvailable: boolean;
/** When set (e.g. PR base), overrides the inferred source branch. */
Expand All @@ -19,7 +18,6 @@ export function useSourceBranchData(params: {
project,
effectiveBranch,
effectivePrKey,
worktreePath,
isGitHub,
ghAvailable,
preferredSourceBranch,
Expand Down Expand Up @@ -92,9 +90,13 @@ export function useSourceBranchData(params: {
refreshKey,
]);

// Fetch PR data for non-worktree mode (worktree PR data is polled elsewhere)
// Fetch PR data on mount / refreshKey change for both worktree and
// non-worktree modes. Worktree PR data is also polled by the periodic
// refreshGitProject cycle, but that cycle may not have run yet when the
// panel is first opened — leaving the PR section blank after a "commit &
// create PR" action or when reopening the panel.
useEffect(() => {
if (worktreePath || !isGitHub || !ghAvailable || !effectiveBranch || !effectivePrKey) return;
if (!isGitHub || !ghAvailable || !effectiveBranch || !effectivePrKey) return;
let isActive = true;
readBridge()
.ghGetPrForBranch({ projectLocation: project.location, branch: effectiveBranch })
Expand All @@ -106,15 +108,7 @@ export function useSourceBranchData(params: {
return () => {
isActive = false;
};
}, [
worktreePath,
isGitHub,
ghAvailable,
effectiveBranch,
effectivePrKey,
project.location,
refreshKey,
]);
}, [isGitHub, ghAvailable, effectiveBranch, effectivePrKey, project.location, refreshKey]);

return { sourceBranchLoading };
}