Fix first push for new worktrees

This commit is contained in:
Mohamed Boudra
2026-06-10 11:54:36 +07:00
parent 51d1d007ce
commit 27b6242128
3 changed files with 47 additions and 13 deletions
+19
View File
@@ -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", () => { it("shows update-from-base only on feature branches that are behind the base branch", () => {
const actions = buildGitActions( const actions = buildGitActions(
createInput({ createInput({
+22 -7
View File
@@ -70,8 +70,8 @@ export interface BuildGitActionsInput {
baseRefLabel: string; baseRefLabel: string;
aheadCount: number; aheadCount: number;
behindBaseCount: number; behindBaseCount: number;
aheadOfOrigin: number; aheadOfOrigin: number | null;
behindOfOrigin: number; behindOfOrigin: number | null;
shouldPromoteArchive: boolean; shouldPromoteArchive: boolean;
shipDefault: "merge" | "pr"; shipDefault: "merge" | "pr";
runtime: Record<GitActionId, GitActionRuntimeState>; runtime: Record<GitActionId, GitActionRuntimeState>;
@@ -489,11 +489,20 @@ function buildDisablePullRequestAutoMergeAction(input: BuildGitActionsInput): Gi
} }
function canPull(input: BuildGitActionsInput): boolean { 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 { 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 { function canMergeFromBase(input: BuildGitActionsInput): boolean {
@@ -587,6 +596,9 @@ function getPullUnavailableMessage(input: BuildGitActionsInput): string | undefi
if (input.hasUncommittedChanges) { if (input.hasUncommittedChanges) {
return "Pull isn't available while you have local changes so commit or stash them first"; 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) { if (input.behindOfOrigin === 0) {
return "Pull isn't available because this branch is already up to date"; 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) { if (!input.hasRemote) {
return "Push isn't available here because this branch is not connected to a remote yet"; 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"; 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 "Push isn't available because there is nothing new to send";
} }
return undefined; return undefined;
@@ -613,13 +625,16 @@ function getPullAndPushUnavailableMessage(input: BuildGitActionsInput): string |
if (input.hasUncommittedChanges) { if (input.hasUncommittedChanges) {
return "Pull and push isn't available while you have local changes so commit or stash them first"; 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) { if (input.behindOfOrigin === 0 && input.aheadOfOrigin === 0) {
return "Pull and push isn't available because this branch is already in sync"; return "Pull and push isn't available because this branch is already in sync";
} }
if (input.behindOfOrigin === 0) { if (input.behindOfOrigin === 0) {
return "Pull and push isn't available because there are no incoming changes to pull first"; 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 "Pull and push isn't available because there is nothing new to send after pulling";
} }
return undefined; return undefined;
+6 -6
View File
@@ -59,8 +59,8 @@ interface DerivedGitActionsState {
actionsDisabled: boolean; actionsDisabled: boolean;
aheadCount: number; aheadCount: number;
behindBaseCount: number; behindBaseCount: number;
aheadOfOrigin: number; aheadOfOrigin: number | null;
behindOfOrigin: number; behindOfOrigin: number | null;
hasPullRequest: boolean; hasPullRequest: boolean;
hasRemote: boolean; hasRemote: boolean;
isPaseoOwnedWorktree: boolean; isPaseoOwnedWorktree: boolean;
@@ -71,16 +71,16 @@ interface DerivedGitActionsState {
interface GitCommitCounts { interface GitCommitCounts {
aheadCount: number; aheadCount: number;
behindBaseCount: number; behindBaseCount: number;
aheadOfOrigin: number; aheadOfOrigin: number | null;
behindOfOrigin: number; behindOfOrigin: number | null;
} }
function extractGitCommitCounts(gitStatus: CheckoutStatusPayload | null): GitCommitCounts { function extractGitCommitCounts(gitStatus: CheckoutStatusPayload | null): GitCommitCounts {
return { return {
aheadCount: gitStatus?.aheadBehind?.ahead ?? 0, aheadCount: gitStatus?.aheadBehind?.ahead ?? 0,
behindBaseCount: gitStatus?.aheadBehind?.behind ?? 0, behindBaseCount: gitStatus?.aheadBehind?.behind ?? 0,
aheadOfOrigin: gitStatus?.aheadOfOrigin ?? 0, aheadOfOrigin: gitStatus?.aheadOfOrigin ?? null,
behindOfOrigin: gitStatus?.behindOfOrigin ?? 0, behindOfOrigin: gitStatus?.behindOfOrigin ?? null,
}; };
} }