From 27b62421283dbc05f17c53220f9938814dda15a6 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 10 Jun 2026 11:54:36 +0700 Subject: [PATCH] Fix first push for new worktrees --- packages/app/src/git/policy.test.ts | 19 +++++++++++++++++++ packages/app/src/git/policy.ts | 29 ++++++++++++++++++++++------- packages/app/src/git/use-actions.ts | 12 ++++++------ 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/packages/app/src/git/policy.test.ts b/packages/app/src/git/policy.test.ts index a9365e5ca..3d8e34c8e 100644 --- a/packages/app/src/git/policy.test.ts +++ b/packages/app/src/git/policy.test.ts @@ -166,6 +166,25 @@ describe("git-actions-policy", () => { }); }); + it("keeps push available for a no-upstream Paseo worktree with local commits", () => { + const actions = buildGitActions( + createInput({ + hasRemote: true, + isPaseoOwnedWorktree: true, + isOnBaseBranch: false, + aheadCount: 1, + aheadOfOrigin: null, + behindOfOrigin: null, + }), + ); + const pushAction = actions.secondary.find((action) => action.id === "push"); + + expect(pushAction).toMatchObject({ + disabled: false, + unavailableMessage: undefined, + }); + }); + it("shows update-from-base only on feature branches that are behind the base branch", () => { const actions = buildGitActions( createInput({ diff --git a/packages/app/src/git/policy.ts b/packages/app/src/git/policy.ts index 36e91c02d..936f31e0e 100644 --- a/packages/app/src/git/policy.ts +++ b/packages/app/src/git/policy.ts @@ -70,8 +70,8 @@ export interface BuildGitActionsInput { baseRefLabel: string; aheadCount: number; behindBaseCount: number; - aheadOfOrigin: number; - behindOfOrigin: number; + aheadOfOrigin: number | null; + behindOfOrigin: number | null; shouldPromoteArchive: boolean; shipDefault: "merge" | "pr"; runtime: Record; @@ -489,11 +489,20 @@ function buildDisablePullRequestAutoMergeAction(input: BuildGitActionsInput): Gi } function canPull(input: BuildGitActionsInput): boolean { - return input.hasRemote && !input.hasUncommittedChanges && input.behindOfOrigin > 0; + return input.hasRemote && !input.hasUncommittedChanges && (input.behindOfOrigin ?? 0) > 0; } function canPush(input: BuildGitActionsInput): boolean { - return input.hasRemote && input.aheadOfOrigin > 0 && input.behindOfOrigin === 0; + return input.hasRemote && hasPushableCommits(input) && (input.behindOfOrigin ?? 0) === 0; +} + +function hasPushableCommits(input: BuildGitActionsInput): boolean { + if ((input.aheadOfOrigin ?? 0) > 0) { + return true; + } + // No-upstream Paseo worktrees are first-pushable: the daemon push sets upstream with `git push -u`. + // Do not fold this into aheadOfOrigin; null also covers deleted/pruned upstream branches. + return input.isPaseoOwnedWorktree && input.aheadOfOrigin === null && input.aheadCount > 0; } function canMergeFromBase(input: BuildGitActionsInput): boolean { @@ -587,6 +596,9 @@ function getPullUnavailableMessage(input: BuildGitActionsInput): string | undefi if (input.hasUncommittedChanges) { return "Pull isn't available while you have local changes so commit or stash them first"; } + if (input.behindOfOrigin === null) { + return "Pull isn't available here because this branch is not connected to a remote yet"; + } if (input.behindOfOrigin === 0) { return "Pull isn't available because this branch is already up to date"; } @@ -597,10 +609,10 @@ function getPushUnavailableMessage(input: BuildGitActionsInput): string | undefi if (!input.hasRemote) { return "Push isn't available here because this branch is not connected to a remote yet"; } - if (input.behindOfOrigin > 0) { + if ((input.behindOfOrigin ?? 0) > 0) { return "Push isn't available yet because there are newer changes to bring in first"; } - if (input.aheadOfOrigin === 0) { + if (!hasPushableCommits(input)) { return "Push isn't available because there is nothing new to send"; } return undefined; @@ -613,13 +625,16 @@ function getPullAndPushUnavailableMessage(input: BuildGitActionsInput): string | if (input.hasUncommittedChanges) { return "Pull and push isn't available while you have local changes so commit or stash them first"; } + if (input.behindOfOrigin === null) { + return "Pull and push isn't available because there are no incoming changes to pull first"; + } if (input.behindOfOrigin === 0 && input.aheadOfOrigin === 0) { return "Pull and push isn't available because this branch is already in sync"; } if (input.behindOfOrigin === 0) { return "Pull and push isn't available because there are no incoming changes to pull first"; } - if (input.aheadOfOrigin === 0) { + if ((input.aheadOfOrigin ?? 0) === 0) { return "Pull and push isn't available because there is nothing new to send after pulling"; } return undefined; diff --git a/packages/app/src/git/use-actions.ts b/packages/app/src/git/use-actions.ts index c1ab3f087..db4d795c3 100644 --- a/packages/app/src/git/use-actions.ts +++ b/packages/app/src/git/use-actions.ts @@ -59,8 +59,8 @@ interface DerivedGitActionsState { actionsDisabled: boolean; aheadCount: number; behindBaseCount: number; - aheadOfOrigin: number; - behindOfOrigin: number; + aheadOfOrigin: number | null; + behindOfOrigin: number | null; hasPullRequest: boolean; hasRemote: boolean; isPaseoOwnedWorktree: boolean; @@ -71,16 +71,16 @@ interface DerivedGitActionsState { interface GitCommitCounts { aheadCount: number; behindBaseCount: number; - aheadOfOrigin: number; - behindOfOrigin: number; + aheadOfOrigin: number | null; + behindOfOrigin: number | null; } function extractGitCommitCounts(gitStatus: CheckoutStatusPayload | null): GitCommitCounts { return { aheadCount: gitStatus?.aheadBehind?.ahead ?? 0, behindBaseCount: gitStatus?.aheadBehind?.behind ?? 0, - aheadOfOrigin: gitStatus?.aheadOfOrigin ?? 0, - behindOfOrigin: gitStatus?.behindOfOrigin ?? 0, + aheadOfOrigin: gitStatus?.aheadOfOrigin ?? null, + behindOfOrigin: gitStatus?.behindOfOrigin ?? null, }; }