feat(app): add pull-and-push git action (#627)
Combines `git pull` followed by `git push` behind a single action in the git actions split-button. Push is skipped if pull fails, and errors are surfaced through the same toast pathway as the existing pull and push actions.
This commit is contained in:
@@ -36,6 +36,11 @@ function createInput(overrides: Partial<BuildGitActionsInput> = {}): BuildGitAct
|
|||||||
status: "idle",
|
status: "idle",
|
||||||
handler: () => undefined,
|
handler: () => undefined,
|
||||||
},
|
},
|
||||||
|
"pull-and-push": {
|
||||||
|
disabled: false,
|
||||||
|
status: "idle",
|
||||||
|
handler: () => undefined,
|
||||||
|
},
|
||||||
pr: {
|
pr: {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
status: "idle",
|
status: "idle",
|
||||||
@@ -65,7 +70,7 @@ describe("git-actions-policy", () => {
|
|||||||
it("shows only remote sync actions on the base branch", () => {
|
it("shows only remote sync actions on the base branch", () => {
|
||||||
const actions = buildGitActions(createInput({ hasRemote: true }));
|
const actions = buildGitActions(createInput({ hasRemote: true }));
|
||||||
|
|
||||||
expect(actions.secondary.map((action) => action.id)).toEqual(["pull", "push"]);
|
expect(actions.secondary.map((action) => action.id)).toEqual(["pull", "push", "pull-and-push"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prioritizes pull when the branch is behind origin", () => {
|
it("prioritizes pull when the branch is behind origin", () => {
|
||||||
@@ -149,6 +154,7 @@ describe("git-actions-policy", () => {
|
|||||||
expect(actions.secondary.map((action) => action.id)).toEqual([
|
expect(actions.secondary.map((action) => action.id)).toEqual([
|
||||||
"pull",
|
"pull",
|
||||||
"push",
|
"push",
|
||||||
|
"pull-and-push",
|
||||||
"merge-from-base",
|
"merge-from-base",
|
||||||
"merge-branch",
|
"merge-branch",
|
||||||
"pr",
|
"pr",
|
||||||
@@ -158,6 +164,48 @@ describe("git-actions-policy", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("enables pull-and-push when the branch has both incoming and outgoing commits", () => {
|
||||||
|
const actions = buildGitActions(
|
||||||
|
createInput({
|
||||||
|
hasRemote: true,
|
||||||
|
aheadOfOrigin: 2,
|
||||||
|
behindOfOrigin: 3,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const action = actions.secondary.find((entry) => entry.id === "pull-and-push");
|
||||||
|
|
||||||
|
expect(action).toMatchObject({
|
||||||
|
label: "Pull and push",
|
||||||
|
disabled: false,
|
||||||
|
unavailableMessage: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("explains why pull-and-push is unavailable when the branch is in sync", () => {
|
||||||
|
const actions = buildGitActions(createInput({ hasRemote: true }));
|
||||||
|
const action = actions.secondary.find((entry) => entry.id === "pull-and-push");
|
||||||
|
|
||||||
|
expect(action).toMatchObject({
|
||||||
|
disabled: false,
|
||||||
|
unavailableMessage: "Pull and push isn't available because this branch is already in sync",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("explains why pull-and-push is unavailable when there are uncommitted changes", () => {
|
||||||
|
const actions = buildGitActions(
|
||||||
|
createInput({
|
||||||
|
hasRemote: true,
|
||||||
|
hasUncommittedChanges: true,
|
||||||
|
aheadOfOrigin: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const action = actions.secondary.find((entry) => entry.id === "pull-and-push");
|
||||||
|
|
||||||
|
expect(action?.unavailableMessage).toBe(
|
||||||
|
"Pull and push isn't available while you have local changes so commit or stash them first",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("only shows archive worktree for paseo worktrees", () => {
|
it("only shows archive worktree for paseo worktrees", () => {
|
||||||
const hidden = buildGitActions(createInput());
|
const hidden = buildGitActions(createInput());
|
||||||
const shown = buildGitActions(createInput({ isPaseoOwnedWorktree: true }));
|
const shown = buildGitActions(createInput({ isPaseoOwnedWorktree: true }));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type GitActionId =
|
|||||||
| "commit"
|
| "commit"
|
||||||
| "pull"
|
| "pull"
|
||||||
| "push"
|
| "push"
|
||||||
|
| "pull-and-push"
|
||||||
| "pr"
|
| "pr"
|
||||||
| "merge-branch"
|
| "merge-branch"
|
||||||
| "merge-from-base"
|
| "merge-from-base"
|
||||||
@@ -56,7 +57,7 @@ export interface BuildGitActionsInput {
|
|||||||
runtime: Record<GitActionId, GitActionRuntimeState>;
|
runtime: Record<GitActionId, GitActionRuntimeState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const REMOTE_ACTION_IDS: GitActionId[] = ["pull", "push"];
|
const REMOTE_ACTION_IDS: GitActionId[] = ["pull", "push", "pull-and-push"];
|
||||||
const FEATURE_ACTION_IDS: GitActionId[] = ["merge-from-base", "merge-branch", "pr"];
|
const FEATURE_ACTION_IDS: GitActionId[] = ["merge-from-base", "merge-branch", "pr"];
|
||||||
|
|
||||||
export function buildGitActions(input: BuildGitActionsInput): GitActions {
|
export function buildGitActions(input: BuildGitActionsInput): GitActions {
|
||||||
@@ -101,6 +102,20 @@ export function buildGitActions(input: BuildGitActionsInput): GitActions {
|
|||||||
handler: input.runtime.push.handler,
|
handler: input.runtime.push.handler,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
allActions.set("pull-and-push", {
|
||||||
|
id: "pull-and-push",
|
||||||
|
label: "Pull and push",
|
||||||
|
pendingLabel: "Pulling and pushing...",
|
||||||
|
successLabel: "Pulled and pushed",
|
||||||
|
disabled: input.runtime["pull-and-push"].disabled,
|
||||||
|
status: input.runtime["pull-and-push"].status,
|
||||||
|
unavailableMessage: input.runtime["pull-and-push"].disabled
|
||||||
|
? undefined
|
||||||
|
: getPullAndPushUnavailableMessage(input),
|
||||||
|
icon: input.runtime["pull-and-push"].icon,
|
||||||
|
handler: input.runtime["pull-and-push"].handler,
|
||||||
|
});
|
||||||
|
|
||||||
allActions.set("pr", buildPrAction(input));
|
allActions.set("pr", buildPrAction(input));
|
||||||
|
|
||||||
allActions.set("merge-branch", {
|
allActions.set("merge-branch", {
|
||||||
@@ -265,6 +280,19 @@ function getPushUnavailableMessage(input: BuildGitActionsInput): string | undefi
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPullAndPushUnavailableMessage(input: BuildGitActionsInput): string | undefined {
|
||||||
|
if (!input.hasRemote) {
|
||||||
|
return "Pull and push isn't available here because this branch is not connected to a remote yet";
|
||||||
|
}
|
||||||
|
if (input.hasUncommittedChanges) {
|
||||||
|
return "Pull and push isn't available while you have local changes so commit or stash them first";
|
||||||
|
}
|
||||||
|
if (input.behindOfOrigin === 0 && input.aheadOfOrigin === 0) {
|
||||||
|
return "Pull and push isn't available because this branch is already in sync";
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function getCreatePrUnavailableMessage(input: BuildGitActionsInput): string | undefined {
|
function getCreatePrUnavailableMessage(input: BuildGitActionsInput): string | undefined {
|
||||||
if (!input.githubFeaturesEnabled) {
|
if (!input.githubFeaturesEnabled) {
|
||||||
return "Create PR isn't available right now because GitHub isn't connected";
|
return "Create PR isn't available right now because GitHub isn't connected";
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { useIsCompactFormFactor } from "@/constants/layout";
|
|||||||
import {
|
import {
|
||||||
AlignJustify,
|
AlignJustify,
|
||||||
Archive,
|
Archive,
|
||||||
|
ArrowDownUp,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Columns2,
|
Columns2,
|
||||||
Download,
|
Download,
|
||||||
@@ -1071,6 +1072,7 @@ interface GitActionRunners {
|
|||||||
runCommit: (args: { serverId: string; cwd: string }) => Promise<void>;
|
runCommit: (args: { serverId: string; cwd: string }) => Promise<void>;
|
||||||
runPull: (args: { serverId: string; cwd: string }) => Promise<void>;
|
runPull: (args: { serverId: string; cwd: string }) => Promise<void>;
|
||||||
runPush: (args: { serverId: string; cwd: string }) => Promise<void>;
|
runPush: (args: { serverId: string; cwd: string }) => Promise<void>;
|
||||||
|
runPullAndPush: (args: { serverId: string; cwd: string }) => Promise<void>;
|
||||||
runCreatePr: (args: { serverId: string; cwd: string }) => Promise<void>;
|
runCreatePr: (args: { serverId: string; cwd: string }) => Promise<void>;
|
||||||
runMergeBranch: (args: { serverId: string; cwd: string; baseRef: string }) => Promise<void>;
|
runMergeBranch: (args: { serverId: string; cwd: string; baseRef: string }) => Promise<void>;
|
||||||
runMergeFromBase: (args: { serverId: string; cwd: string; baseRef: string }) => Promise<void>;
|
runMergeFromBase: (args: { serverId: string; cwd: string; baseRef: string }) => Promise<void>;
|
||||||
@@ -1099,6 +1101,7 @@ interface GitActionHandlers {
|
|||||||
handleCommit: () => void;
|
handleCommit: () => void;
|
||||||
handlePull: () => void;
|
handlePull: () => void;
|
||||||
handlePush: () => void;
|
handlePush: () => void;
|
||||||
|
handlePullAndPush: () => void;
|
||||||
handleCreatePr: () => void;
|
handleCreatePr: () => void;
|
||||||
handleMergeBranch: () => void;
|
handleMergeBranch: () => void;
|
||||||
handleMergeFromBase: () => void;
|
handleMergeFromBase: () => void;
|
||||||
@@ -1154,6 +1157,18 @@ function useGitActionHandlers({
|
|||||||
});
|
});
|
||||||
}, [cwd, runners, serverId, toastActionError, toastActionSuccess]);
|
}, [cwd, runners, serverId, toastActionError, toastActionSuccess]);
|
||||||
|
|
||||||
|
const handlePullAndPush = useCallback(() => {
|
||||||
|
void runners
|
||||||
|
.runPullAndPush({ serverId, cwd })
|
||||||
|
.then(() => {
|
||||||
|
toastActionSuccess("Pulled and pushed");
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toastActionError(err, "Failed to pull and push");
|
||||||
|
});
|
||||||
|
}, [cwd, runners, serverId, toastActionError, toastActionSuccess]);
|
||||||
|
|
||||||
const handleCreatePr = useCallback(() => {
|
const handleCreatePr = useCallback(() => {
|
||||||
void persistShipDefault("pr");
|
void persistShipDefault("pr");
|
||||||
void runners
|
void runners
|
||||||
@@ -1233,6 +1248,7 @@ function useGitActionHandlers({
|
|||||||
handleCommit,
|
handleCommit,
|
||||||
handlePull,
|
handlePull,
|
||||||
handlePush,
|
handlePush,
|
||||||
|
handlePullAndPush,
|
||||||
handleCreatePr,
|
handleCreatePr,
|
||||||
handleMergeBranch,
|
handleMergeBranch,
|
||||||
handleMergeFromBase,
|
handleMergeFromBase,
|
||||||
@@ -1350,6 +1366,7 @@ function computeDisabledStates(
|
|||||||
commitDisabled: actionsDisabled || statuses.commitStatus === pending,
|
commitDisabled: actionsDisabled || statuses.commitStatus === pending,
|
||||||
pullDisabled: actionsDisabled || statuses.pullStatus === pending,
|
pullDisabled: actionsDisabled || statuses.pullStatus === pending,
|
||||||
pushDisabled: actionsDisabled || statuses.pushStatus === pending,
|
pushDisabled: actionsDisabled || statuses.pushStatus === pending,
|
||||||
|
pullAndPushDisabled: actionsDisabled || statuses.pullAndPushStatus === pending,
|
||||||
prDisabled: actionsDisabled || statuses.prCreateStatus === pending,
|
prDisabled: actionsDisabled || statuses.prCreateStatus === pending,
|
||||||
mergeDisabled: actionsDisabled || statuses.mergeStatus === pending,
|
mergeDisabled: actionsDisabled || statuses.mergeStatus === pending,
|
||||||
mergeFromBaseDisabled: actionsDisabled || statuses.mergeFromBaseStatus === pending,
|
mergeFromBaseDisabled: actionsDisabled || statuses.mergeFromBaseStatus === pending,
|
||||||
@@ -1393,6 +1410,7 @@ interface GitActionsStatusInputs {
|
|||||||
commitStatus: CheckoutGitActionStatus;
|
commitStatus: CheckoutGitActionStatus;
|
||||||
pullStatus: CheckoutGitActionStatus;
|
pullStatus: CheckoutGitActionStatus;
|
||||||
pushStatus: CheckoutGitActionStatus;
|
pushStatus: CheckoutGitActionStatus;
|
||||||
|
pullAndPushStatus: CheckoutGitActionStatus;
|
||||||
prCreateStatus: CheckoutGitActionStatus;
|
prCreateStatus: CheckoutGitActionStatus;
|
||||||
mergeStatus: CheckoutGitActionStatus;
|
mergeStatus: CheckoutGitActionStatus;
|
||||||
mergeFromBaseStatus: CheckoutGitActionStatus;
|
mergeFromBaseStatus: CheckoutGitActionStatus;
|
||||||
@@ -1403,6 +1421,7 @@ interface GitActionsDisabledInputs {
|
|||||||
commitDisabled: boolean;
|
commitDisabled: boolean;
|
||||||
pullDisabled: boolean;
|
pullDisabled: boolean;
|
||||||
pushDisabled: boolean;
|
pushDisabled: boolean;
|
||||||
|
pullAndPushDisabled: boolean;
|
||||||
prDisabled: boolean;
|
prDisabled: boolean;
|
||||||
mergeDisabled: boolean;
|
mergeDisabled: boolean;
|
||||||
mergeFromBaseDisabled: boolean;
|
mergeFromBaseDisabled: boolean;
|
||||||
@@ -1462,6 +1481,12 @@ function buildGitActionsForPane({
|
|||||||
icon: <Upload size={16} color={iconColor} />,
|
icon: <Upload size={16} color={iconColor} />,
|
||||||
handler: handlers.handlePush,
|
handler: handlers.handlePush,
|
||||||
},
|
},
|
||||||
|
"pull-and-push": {
|
||||||
|
disabled: disabled.pullAndPushDisabled,
|
||||||
|
status: statuses.pullAndPushStatus,
|
||||||
|
icon: <ArrowDownUp size={16} color={iconColor} />,
|
||||||
|
handler: handlers.handlePullAndPush,
|
||||||
|
},
|
||||||
pr: {
|
pr: {
|
||||||
disabled: disabled.prDisabled,
|
disabled: disabled.prDisabled,
|
||||||
status: policy.hasPullRequest ? "idle" : statuses.prCreateStatus,
|
status: policy.hasPullRequest ? "idle" : statuses.prCreateStatus,
|
||||||
@@ -1879,6 +1904,9 @@ export function GitDiffPane({
|
|||||||
const pushStatus = useCheckoutGitActionsStore((state) =>
|
const pushStatus = useCheckoutGitActionsStore((state) =>
|
||||||
state.getStatus({ serverId, cwd, actionId: "push" }),
|
state.getStatus({ serverId, cwd, actionId: "push" }),
|
||||||
);
|
);
|
||||||
|
const pullAndPushStatus = useCheckoutGitActionsStore((state) =>
|
||||||
|
state.getStatus({ serverId, cwd, actionId: "pull-and-push" }),
|
||||||
|
);
|
||||||
const prCreateStatus = useCheckoutGitActionsStore((state) =>
|
const prCreateStatus = useCheckoutGitActionsStore((state) =>
|
||||||
state.getStatus({ serverId, cwd, actionId: "create-pr" }),
|
state.getStatus({ serverId, cwd, actionId: "create-pr" }),
|
||||||
);
|
);
|
||||||
@@ -1895,6 +1923,7 @@ export function GitDiffPane({
|
|||||||
const runCommit = useCheckoutGitActionsStore((state) => state.commit);
|
const runCommit = useCheckoutGitActionsStore((state) => state.commit);
|
||||||
const runPull = useCheckoutGitActionsStore((state) => state.pull);
|
const runPull = useCheckoutGitActionsStore((state) => state.pull);
|
||||||
const runPush = useCheckoutGitActionsStore((state) => state.push);
|
const runPush = useCheckoutGitActionsStore((state) => state.push);
|
||||||
|
const runPullAndPush = useCheckoutGitActionsStore((state) => state.pullAndPush);
|
||||||
const runCreatePr = useCheckoutGitActionsStore((state) => state.createPr);
|
const runCreatePr = useCheckoutGitActionsStore((state) => state.createPr);
|
||||||
const runMergeBranch = useCheckoutGitActionsStore((state) => state.mergeBranch);
|
const runMergeBranch = useCheckoutGitActionsStore((state) => state.mergeBranch);
|
||||||
const runMergeFromBase = useCheckoutGitActionsStore((state) => state.mergeFromBase);
|
const runMergeFromBase = useCheckoutGitActionsStore((state) => state.mergeFromBase);
|
||||||
@@ -1938,6 +1967,7 @@ export function GitDiffPane({
|
|||||||
runCommit,
|
runCommit,
|
||||||
runPull,
|
runPull,
|
||||||
runPush,
|
runPush,
|
||||||
|
runPullAndPush,
|
||||||
runCreatePr,
|
runCreatePr,
|
||||||
runMergeBranch,
|
runMergeBranch,
|
||||||
runMergeFromBase,
|
runMergeFromBase,
|
||||||
@@ -1950,6 +1980,7 @@ export function GitDiffPane({
|
|||||||
runMergeBranch,
|
runMergeBranch,
|
||||||
runMergeFromBase,
|
runMergeFromBase,
|
||||||
runPull,
|
runPull,
|
||||||
|
runPullAndPush,
|
||||||
runPush,
|
runPush,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -1958,6 +1989,7 @@ export function GitDiffPane({
|
|||||||
handleCommit,
|
handleCommit,
|
||||||
handlePull,
|
handlePull,
|
||||||
handlePush,
|
handlePush,
|
||||||
|
handlePullAndPush,
|
||||||
handleCreatePr,
|
handleCreatePr,
|
||||||
handleMergeBranch,
|
handleMergeBranch,
|
||||||
handleMergeFromBase,
|
handleMergeFromBase,
|
||||||
@@ -2081,6 +2113,7 @@ export function GitDiffPane({
|
|||||||
commitStatus,
|
commitStatus,
|
||||||
pullStatus,
|
pullStatus,
|
||||||
pushStatus,
|
pushStatus,
|
||||||
|
pullAndPushStatus,
|
||||||
prCreateStatus,
|
prCreateStatus,
|
||||||
mergeStatus,
|
mergeStatus,
|
||||||
mergeFromBaseStatus,
|
mergeFromBaseStatus,
|
||||||
@@ -2092,6 +2125,7 @@ export function GitDiffPane({
|
|||||||
mergeFromBaseStatus,
|
mergeFromBaseStatus,
|
||||||
mergeStatus,
|
mergeStatus,
|
||||||
prCreateStatus,
|
prCreateStatus,
|
||||||
|
pullAndPushStatus,
|
||||||
pullStatus,
|
pullStatus,
|
||||||
pushStatus,
|
pushStatus,
|
||||||
],
|
],
|
||||||
@@ -2155,6 +2189,7 @@ export function GitDiffPane({
|
|||||||
handleCommit,
|
handleCommit,
|
||||||
handlePull,
|
handlePull,
|
||||||
handlePush,
|
handlePush,
|
||||||
|
handlePullAndPush,
|
||||||
handleCreatePr,
|
handleCreatePr,
|
||||||
handleMergeBranch,
|
handleMergeBranch,
|
||||||
handleMergeFromBase,
|
handleMergeFromBase,
|
||||||
@@ -2169,6 +2204,7 @@ export function GitDiffPane({
|
|||||||
handleMergeFromBase,
|
handleMergeFromBase,
|
||||||
handlePr,
|
handlePr,
|
||||||
handlePull,
|
handlePull,
|
||||||
|
handlePullAndPush,
|
||||||
handlePush,
|
handlePush,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ function useGitActionStatuses(
|
|||||||
commitStatus: CheckoutGitActionStatus;
|
commitStatus: CheckoutGitActionStatus;
|
||||||
pullStatus: CheckoutGitActionStatus;
|
pullStatus: CheckoutGitActionStatus;
|
||||||
pushStatus: CheckoutGitActionStatus;
|
pushStatus: CheckoutGitActionStatus;
|
||||||
|
pullAndPushStatus: CheckoutGitActionStatus;
|
||||||
prCreateStatus: CheckoutGitActionStatus;
|
prCreateStatus: CheckoutGitActionStatus;
|
||||||
mergeStatus: CheckoutGitActionStatus;
|
mergeStatus: CheckoutGitActionStatus;
|
||||||
mergeFromBaseStatus: CheckoutGitActionStatus;
|
mergeFromBaseStatus: CheckoutGitActionStatus;
|
||||||
@@ -68,6 +69,9 @@ function useGitActionStatuses(
|
|||||||
const pushStatus = useCheckoutGitActionsStore((state) =>
|
const pushStatus = useCheckoutGitActionsStore((state) =>
|
||||||
state.getStatus({ serverId, cwd, actionId: "push" }),
|
state.getStatus({ serverId, cwd, actionId: "push" }),
|
||||||
);
|
);
|
||||||
|
const pullAndPushStatus = useCheckoutGitActionsStore((state) =>
|
||||||
|
state.getStatus({ serverId, cwd, actionId: "pull-and-push" }),
|
||||||
|
);
|
||||||
const prCreateStatus = useCheckoutGitActionsStore((state) =>
|
const prCreateStatus = useCheckoutGitActionsStore((state) =>
|
||||||
state.getStatus({ serverId, cwd, actionId: "create-pr" }),
|
state.getStatus({ serverId, cwd, actionId: "create-pr" }),
|
||||||
);
|
);
|
||||||
@@ -84,6 +88,7 @@ function useGitActionStatuses(
|
|||||||
commitStatus,
|
commitStatus,
|
||||||
pullStatus,
|
pullStatus,
|
||||||
pushStatus,
|
pushStatus,
|
||||||
|
pullAndPushStatus,
|
||||||
prCreateStatus,
|
prCreateStatus,
|
||||||
mergeStatus,
|
mergeStatus,
|
||||||
mergeFromBaseStatus,
|
mergeFromBaseStatus,
|
||||||
@@ -180,6 +185,7 @@ function useGitActionRunners() {
|
|||||||
const runCommit = useCheckoutGitActionsStore((state) => state.commit);
|
const runCommit = useCheckoutGitActionsStore((state) => state.commit);
|
||||||
const runPull = useCheckoutGitActionsStore((state) => state.pull);
|
const runPull = useCheckoutGitActionsStore((state) => state.pull);
|
||||||
const runPush = useCheckoutGitActionsStore((state) => state.push);
|
const runPush = useCheckoutGitActionsStore((state) => state.push);
|
||||||
|
const runPullAndPush = useCheckoutGitActionsStore((state) => state.pullAndPush);
|
||||||
const runCreatePr = useCheckoutGitActionsStore((state) => state.createPr);
|
const runCreatePr = useCheckoutGitActionsStore((state) => state.createPr);
|
||||||
const runMergeBranch = useCheckoutGitActionsStore((state) => state.mergeBranch);
|
const runMergeBranch = useCheckoutGitActionsStore((state) => state.mergeBranch);
|
||||||
const runMergeFromBase = useCheckoutGitActionsStore((state) => state.mergeFromBase);
|
const runMergeFromBase = useCheckoutGitActionsStore((state) => state.mergeFromBase);
|
||||||
@@ -188,6 +194,7 @@ function useGitActionRunners() {
|
|||||||
runCommit,
|
runCommit,
|
||||||
runPull,
|
runPull,
|
||||||
runPush,
|
runPush,
|
||||||
|
runPullAndPush,
|
||||||
runCreatePr,
|
runCreatePr,
|
||||||
runMergeBranch,
|
runMergeBranch,
|
||||||
runMergeFromBase,
|
runMergeFromBase,
|
||||||
@@ -202,6 +209,7 @@ interface UseGitActionsInput {
|
|||||||
commit: ReactElement;
|
commit: ReactElement;
|
||||||
pull: ReactElement;
|
pull: ReactElement;
|
||||||
push: ReactElement;
|
push: ReactElement;
|
||||||
|
pullAndPush: ReactElement;
|
||||||
viewPr: ReactElement;
|
viewPr: ReactElement;
|
||||||
createPr: ReactElement;
|
createPr: ReactElement;
|
||||||
merge: ReactElement;
|
merge: ReactElement;
|
||||||
@@ -284,6 +292,7 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
|||||||
commitStatus,
|
commitStatus,
|
||||||
pullStatus,
|
pullStatus,
|
||||||
pushStatus,
|
pushStatus,
|
||||||
|
pullAndPushStatus,
|
||||||
prCreateStatus,
|
prCreateStatus,
|
||||||
mergeStatus,
|
mergeStatus,
|
||||||
mergeFromBaseStatus,
|
mergeFromBaseStatus,
|
||||||
@@ -294,6 +303,7 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
|||||||
runCommit,
|
runCommit,
|
||||||
runPull,
|
runPull,
|
||||||
runPush,
|
runPush,
|
||||||
|
runPullAndPush,
|
||||||
runCreatePr,
|
runCreatePr,
|
||||||
runMergeBranch,
|
runMergeBranch,
|
||||||
runMergeFromBase,
|
runMergeFromBase,
|
||||||
@@ -349,6 +359,17 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
|||||||
});
|
});
|
||||||
}, [cwd, runPush, serverId, toastActionError, toastActionSuccess]);
|
}, [cwd, runPush, serverId, toastActionError, toastActionSuccess]);
|
||||||
|
|
||||||
|
const handlePullAndPush = useCallback(() => {
|
||||||
|
void runPullAndPush({ serverId, cwd })
|
||||||
|
.then(() => {
|
||||||
|
toastActionSuccess("Pulled and pushed");
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toastActionError(err, "Failed to pull and push");
|
||||||
|
});
|
||||||
|
}, [cwd, runPullAndPush, serverId, toastActionError, toastActionSuccess]);
|
||||||
|
|
||||||
const handleCreatePr = useCallback(() => {
|
const handleCreatePr = useCallback(() => {
|
||||||
void persistShipDefault("pr");
|
void persistShipDefault("pr");
|
||||||
void runCreatePr({ serverId, cwd })
|
void runCreatePr({ serverId, cwd })
|
||||||
@@ -449,6 +470,7 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
|||||||
const mergeDisabled = isActionDisabled(actionsDisabled, mergeStatus);
|
const mergeDisabled = isActionDisabled(actionsDisabled, mergeStatus);
|
||||||
const mergeFromBaseDisabled = isActionDisabled(actionsDisabled, mergeFromBaseStatus);
|
const mergeFromBaseDisabled = isActionDisabled(actionsDisabled, mergeFromBaseStatus);
|
||||||
const pushDisabled = isActionDisabled(actionsDisabled, pushStatus);
|
const pushDisabled = isActionDisabled(actionsDisabled, pushStatus);
|
||||||
|
const pullAndPushDisabled = isActionDisabled(actionsDisabled, pullAndPushStatus);
|
||||||
const archiveDisabled = isActionDisabled(actionsDisabled, archiveStatus);
|
const archiveDisabled = isActionDisabled(actionsDisabled, archiveStatus);
|
||||||
|
|
||||||
const branchLabel = resolveBranchLabel({
|
const branchLabel = resolveBranchLabel({
|
||||||
@@ -502,6 +524,12 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
|||||||
icon: icons.push,
|
icon: icons.push,
|
||||||
handler: handlePush,
|
handler: handlePush,
|
||||||
},
|
},
|
||||||
|
"pull-and-push": {
|
||||||
|
disabled: pullAndPushDisabled,
|
||||||
|
status: pullAndPushStatus,
|
||||||
|
icon: icons.pullAndPush,
|
||||||
|
handler: handlePullAndPush,
|
||||||
|
},
|
||||||
pr: {
|
pr: {
|
||||||
disabled: prDisabled,
|
disabled: prDisabled,
|
||||||
status: hasPullRequest ? "idle" : prCreateStatus,
|
status: hasPullRequest ? "idle" : prCreateStatus,
|
||||||
@@ -547,6 +575,7 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
|||||||
commitDisabled,
|
commitDisabled,
|
||||||
pullDisabled,
|
pullDisabled,
|
||||||
pushDisabled,
|
pushDisabled,
|
||||||
|
pullAndPushDisabled,
|
||||||
prDisabled,
|
prDisabled,
|
||||||
mergeDisabled,
|
mergeDisabled,
|
||||||
mergeFromBaseDisabled,
|
mergeFromBaseDisabled,
|
||||||
@@ -554,6 +583,7 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
|||||||
commitStatus,
|
commitStatus,
|
||||||
pullStatus,
|
pullStatus,
|
||||||
pushStatus,
|
pushStatus,
|
||||||
|
pullAndPushStatus,
|
||||||
prCreateStatus,
|
prCreateStatus,
|
||||||
mergeStatus,
|
mergeStatus,
|
||||||
mergeFromBaseStatus,
|
mergeFromBaseStatus,
|
||||||
@@ -561,6 +591,7 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
|||||||
handleCommit,
|
handleCommit,
|
||||||
handlePull,
|
handlePull,
|
||||||
handlePush,
|
handlePush,
|
||||||
|
handlePullAndPush,
|
||||||
handlePrAction,
|
handlePrAction,
|
||||||
handleMergeBranch,
|
handleMergeBranch,
|
||||||
handleMergeFromBase,
|
handleMergeFromBase,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { withUnistyles } from "react-native-unistyles";
|
import { withUnistyles } from "react-native-unistyles";
|
||||||
import {
|
import {
|
||||||
Archive,
|
Archive,
|
||||||
|
ArrowDownUp,
|
||||||
Download,
|
Download,
|
||||||
GitCommitHorizontal,
|
GitCommitHorizontal,
|
||||||
GitMerge,
|
GitMerge,
|
||||||
@@ -21,6 +22,7 @@ interface WorkspaceGitActionsProps {
|
|||||||
const ThemedGitCommitHorizontal = withUnistyles(GitCommitHorizontal);
|
const ThemedGitCommitHorizontal = withUnistyles(GitCommitHorizontal);
|
||||||
const ThemedDownload = withUnistyles(Download);
|
const ThemedDownload = withUnistyles(Download);
|
||||||
const ThemedUpload = withUnistyles(Upload);
|
const ThemedUpload = withUnistyles(Upload);
|
||||||
|
const ThemedArrowDownUp = withUnistyles(ArrowDownUp);
|
||||||
const ThemedGitHubIcon = withUnistyles(GitHubIcon);
|
const ThemedGitHubIcon = withUnistyles(GitHubIcon);
|
||||||
const ThemedGitMerge = withUnistyles(GitMerge);
|
const ThemedGitMerge = withUnistyles(GitMerge);
|
||||||
const ThemedRefreshCcw = withUnistyles(RefreshCcw);
|
const ThemedRefreshCcw = withUnistyles(RefreshCcw);
|
||||||
@@ -34,6 +36,7 @@ const ICONS = {
|
|||||||
commit: <ThemedGitCommitHorizontal size={16} uniProps={mutedColorMapping} />,
|
commit: <ThemedGitCommitHorizontal size={16} uniProps={mutedColorMapping} />,
|
||||||
pull: <ThemedDownload size={16} uniProps={mutedColorMapping} />,
|
pull: <ThemedDownload size={16} uniProps={mutedColorMapping} />,
|
||||||
push: <ThemedUpload size={16} uniProps={mutedColorMapping} />,
|
push: <ThemedUpload size={16} uniProps={mutedColorMapping} />,
|
||||||
|
pullAndPush: <ThemedArrowDownUp size={16} uniProps={mutedColorMapping} />,
|
||||||
viewPr: <ThemedGitHubIcon size={16} uniProps={mutedColorMapping} />,
|
viewPr: <ThemedGitHubIcon size={16} uniProps={mutedColorMapping} />,
|
||||||
createPr: <ThemedGitHubIcon size={16} uniProps={mutedColorMapping} />,
|
createPr: <ThemedGitHubIcon size={16} uniProps={mutedColorMapping} />,
|
||||||
merge: <ThemedGitMerge size={16} uniProps={mutedColorMapping} />,
|
merge: <ThemedGitMerge size={16} uniProps={mutedColorMapping} />,
|
||||||
|
|||||||
@@ -93,6 +93,72 @@ describe("checkout-git-actions-store", () => {
|
|||||||
expect(store.getStatus({ serverId, cwd, actionId: "commit" })).toBe("idle");
|
expect(store.getStatus({ serverId, cwd, actionId: "commit" })).toBe("idle");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("runs pull then push sequentially for pull-and-push", async () => {
|
||||||
|
const order: string[] = [];
|
||||||
|
const client = {
|
||||||
|
checkoutPull: vi.fn(async () => {
|
||||||
|
order.push("pull");
|
||||||
|
return {};
|
||||||
|
}),
|
||||||
|
checkoutPush: vi.fn(async () => {
|
||||||
|
order.push("push");
|
||||||
|
return {};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
useSessionStore.setState((state) => ({
|
||||||
|
...state,
|
||||||
|
sessions: {
|
||||||
|
...state.sessions,
|
||||||
|
[serverId]: { client } as unknown as (typeof state.sessions)[string],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
await useCheckoutGitActionsStore.getState().pullAndPush({ serverId, cwd });
|
||||||
|
|
||||||
|
expect(order).toEqual(["pull", "push"]);
|
||||||
|
expect(client.checkoutPull).toHaveBeenCalledWith(cwd);
|
||||||
|
expect(client.checkoutPush).toHaveBeenCalledWith(cwd);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not push when pull fails for pull-and-push", async () => {
|
||||||
|
const client = {
|
||||||
|
checkoutPull: vi.fn(async () => ({ error: { message: "pull conflict" } })),
|
||||||
|
checkoutPush: vi.fn(async () => ({})),
|
||||||
|
};
|
||||||
|
useSessionStore.setState((state) => ({
|
||||||
|
...state,
|
||||||
|
sessions: {
|
||||||
|
...state.sessions,
|
||||||
|
[serverId]: { client } as unknown as (typeof state.sessions)[string],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
useCheckoutGitActionsStore.getState().pullAndPush({ serverId, cwd }),
|
||||||
|
).rejects.toThrow("pull conflict");
|
||||||
|
expect(client.checkoutPush).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("surfaces push errors from pull-and-push after a successful pull", async () => {
|
||||||
|
const client = {
|
||||||
|
checkoutPull: vi.fn(async () => ({})),
|
||||||
|
checkoutPush: vi.fn(async () => ({ error: { message: "push rejected" } })),
|
||||||
|
};
|
||||||
|
useSessionStore.setState((state) => ({
|
||||||
|
...state,
|
||||||
|
sessions: {
|
||||||
|
...state.sessions,
|
||||||
|
[serverId]: { client } as unknown as (typeof state.sessions)[string],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
useCheckoutGitActionsStore.getState().pullAndPush({ serverId, cwd }),
|
||||||
|
).rejects.toThrow("push rejected");
|
||||||
|
expect(client.checkoutPull).toHaveBeenCalledTimes(1);
|
||||||
|
expect(client.checkoutPush).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("invalidates checkout PR status and every PR pane timeline for a checkout", async () => {
|
it("invalidates checkout PR status and every PR pane timeline for a checkout", async () => {
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export type CheckoutGitAsyncActionId =
|
|||||||
| "commit"
|
| "commit"
|
||||||
| "pull"
|
| "pull"
|
||||||
| "push"
|
| "push"
|
||||||
|
| "pull-and-push"
|
||||||
| "create-pr"
|
| "create-pr"
|
||||||
| "merge-branch"
|
| "merge-branch"
|
||||||
| "merge-from-base"
|
| "merge-from-base"
|
||||||
@@ -234,6 +235,7 @@ interface CheckoutGitActionsStoreState {
|
|||||||
commit: (params: { serverId: string; cwd: string }) => Promise<void>;
|
commit: (params: { serverId: string; cwd: string }) => Promise<void>;
|
||||||
pull: (params: { serverId: string; cwd: string }) => Promise<void>;
|
pull: (params: { serverId: string; cwd: string }) => Promise<void>;
|
||||||
push: (params: { serverId: string; cwd: string }) => Promise<void>;
|
push: (params: { serverId: string; cwd: string }) => Promise<void>;
|
||||||
|
pullAndPush: (params: { serverId: string; cwd: string }) => Promise<void>;
|
||||||
createPr: (params: { serverId: string; cwd: string }) => Promise<void>;
|
createPr: (params: { serverId: string; cwd: string }) => Promise<void>;
|
||||||
mergeBranch: (params: { serverId: string; cwd: string; baseRef: string }) => Promise<void>;
|
mergeBranch: (params: { serverId: string; cwd: string; baseRef: string }) => Promise<void>;
|
||||||
mergeFromBase: (params: { serverId: string; cwd: string; baseRef: string }) => Promise<void>;
|
mergeFromBase: (params: { serverId: string; cwd: string; baseRef: string }) => Promise<void>;
|
||||||
@@ -347,6 +349,25 @@ export const useCheckoutGitActionsStore = create<CheckoutGitActionsStoreState>()
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
pullAndPush: async ({ serverId, cwd }) => {
|
||||||
|
await runCheckoutAction({
|
||||||
|
serverId,
|
||||||
|
cwd,
|
||||||
|
actionId: "pull-and-push",
|
||||||
|
run: async () => {
|
||||||
|
const client = resolveClient(serverId);
|
||||||
|
const pullPayload = await client.checkoutPull(cwd);
|
||||||
|
if (pullPayload.error) {
|
||||||
|
throw new Error(pullPayload.error.message);
|
||||||
|
}
|
||||||
|
const pushPayload = await client.checkoutPush(cwd);
|
||||||
|
if (pushPayload.error) {
|
||||||
|
throw new Error(pushPayload.error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
createPr: async ({ serverId, cwd }) => {
|
createPr: async ({ serverId, cwd }) => {
|
||||||
await runCheckoutAction({
|
await runCheckoutAction({
|
||||||
serverId,
|
serverId,
|
||||||
|
|||||||
Reference in New Issue
Block a user