* fix(app): validate persisted client state Persisted replica data could satisfy TypeScript through an assertion while missing fields required by renderers. Treat every AsyncStorage value as untrusted input, clear values that fail strict schemas, and version the replica DTO so incompatible caches cannot hydrate. * fix(app): preserve validated legacy state * fix(app): preserve validated preferences Preserve supported legacy location fields and every shipped theme while keeping unknown persisted fields rejected. Align browser test seeds with the current preference shape so strict validation exercises the intended provider. * fix(app): migrate legacy agent preferences Released clients stored host/location fields and per-provider thinkingOptionId together. Migrate those exact strict bytes into current preferences without admitting unknown fields. * fix(app): preserve drafts with legacy reviews Accept the exact strict shape released for workspace review attachments so migration can discard that obsolete attachment without clearing every persisted draft.
1129 lines
38 KiB
TypeScript
1129 lines
38 KiB
TypeScript
import { useState, useCallback, useEffect, useMemo, type ReactElement } from "react";
|
|
import { Info } from "lucide-react-native";
|
|
import { withUnistyles } from "react-native-unistyles";
|
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
import { z } from "zod";
|
|
import { useTranslation } from "react-i18next";
|
|
import type { Theme } from "@/styles/theme";
|
|
import { getForgePresentation, type Forge } from "@/git/forge";
|
|
import { ForgeBrandIcon, getForgeBrandColorMapping } from "@/git/forge-icon";
|
|
import { type CheckoutGitActionStatus, useCheckoutGitActionsStore } from "@/git/actions-store";
|
|
import { type CheckoutStatusPayload, useCheckoutStatusQuery } from "@/git/use-status-query";
|
|
import { type CheckoutPrStatusPayload, useCheckoutPrStatusQuery } from "@/git/use-pr-status-query";
|
|
import {
|
|
buildGitActions,
|
|
narrowPullRequestState,
|
|
type BuildGitActionsInput,
|
|
type GitAction,
|
|
type GitActions,
|
|
} from "@/git/policy";
|
|
import { deriveMergeCapability } from "@/git/merge-capability";
|
|
import type { CheckoutPrMergeMethod } from "@getpaseo/protocol/messages";
|
|
import { openExternalUrl } from "@/utils/open-external-url";
|
|
import { useToast } from "@/contexts/toast-context";
|
|
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
|
|
import {
|
|
useActiveWorkspaceSelection,
|
|
type ActiveWorkspaceSelection,
|
|
} from "@/stores/navigation-active-workspace-store";
|
|
import { redirectIfArchivingActiveWorkspace } from "@/utils/sidebar-workspace-archive-redirect";
|
|
import { type WorktreeArchiveWarningLabels } from "@/git/worktree-archive-warning";
|
|
import { useWorkspaceArchive } from "@/workspace/use-workspace-archive";
|
|
import { resolveWorkspaceMapKeyByIdentity } from "@/utils/workspace-identity";
|
|
import { readValidatedString } from "@/storage/validated-storage";
|
|
|
|
export type { GitActionId, GitAction, GitActions } from "@/git/policy";
|
|
|
|
const forgeMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
|
const ThemedInfo = withUnistyles(Info, (theme) => ({ color: theme.colors.foreground }));
|
|
|
|
export function useGitActionRunner(): (action: GitAction) => void {
|
|
const toast = useToast();
|
|
|
|
return useCallback(
|
|
(action: GitAction) => {
|
|
if (action.disabled) return;
|
|
if (action.unavailableMessage) {
|
|
toast.show(action.unavailableMessage, {
|
|
durationMs: 3200,
|
|
icon: <ThemedInfo size={16} />,
|
|
});
|
|
return;
|
|
}
|
|
action.handler();
|
|
},
|
|
[toast],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The leading icon for every change-request action (create/view/merge) is the
|
|
* forge's brand mark, tinted in its brand color (the GitLab tanuki orange, etc).
|
|
* GitHub and unknown forges have no brand color and render in a neutral tone.
|
|
* The merge variants all share this one icon, so we build it once.
|
|
*/
|
|
function renderForgePrIcon(forge: Forge): ReactElement {
|
|
const icon = getForgePresentation(forge).icon;
|
|
return (
|
|
<ForgeBrandIcon
|
|
iconKind={icon}
|
|
size={16}
|
|
uniProps={getForgeBrandColorMapping(icon) ?? forgeMutedColorMapping}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function forgeVocabulary(forge: Forge): { context: "mr" | undefined } {
|
|
return { context: getForgePresentation(forge).changeRequestContext };
|
|
}
|
|
|
|
function openURLInNewTab(url: string): void {
|
|
void openExternalUrl(url);
|
|
}
|
|
|
|
function isActionDisabled(actionsDisabled: boolean, status: CheckoutGitActionStatus): boolean {
|
|
return actionsDisabled || status === "pending";
|
|
}
|
|
|
|
function resolveBranchLabel(input: {
|
|
currentBranch: string | null | undefined;
|
|
notGit: boolean;
|
|
notRepositoryLabel: string;
|
|
unknownLabel: string;
|
|
}): string {
|
|
if (input.currentBranch && input.currentBranch !== "HEAD") {
|
|
return input.currentBranch;
|
|
}
|
|
if (input.notGit) {
|
|
return input.notRepositoryLabel;
|
|
}
|
|
return input.unknownLabel;
|
|
}
|
|
|
|
function formatBaseRefLabel(baseRef: string | undefined, fallbackLabel: string): string {
|
|
if (!baseRef) return fallbackLabel;
|
|
const trimmed = baseRef.replace(/^refs\/(heads|remotes)\//, "").trim();
|
|
return trimmed.startsWith("origin/") ? trimmed.slice("origin/".length) : trimmed;
|
|
}
|
|
|
|
type PrStatusValue = NonNullable<CheckoutPrStatusPayload["status"]> | null;
|
|
|
|
interface DeriveGitActionsStateArgs {
|
|
isGit: boolean;
|
|
status: CheckoutStatusPayload | null;
|
|
gitStatus: CheckoutStatusPayload | null;
|
|
prStatus: PrStatusValue;
|
|
hasUncommittedChanges: boolean;
|
|
postShipArchiveSuggested: boolean;
|
|
isStatusLoading: boolean;
|
|
baseRefLabel: string;
|
|
}
|
|
|
|
interface DerivedGitActionsState {
|
|
actionsDisabled: boolean;
|
|
aheadCount: number;
|
|
behindBaseCount: number;
|
|
aheadOfOrigin: number | null;
|
|
behindOfOrigin: number | null;
|
|
hasPullRequest: boolean;
|
|
hasRemote: boolean;
|
|
isPaseoOwnedWorktree: boolean;
|
|
isOnBaseBranch: boolean;
|
|
shouldPromoteArchive: boolean;
|
|
}
|
|
|
|
interface GitCommitCounts {
|
|
aheadCount: number;
|
|
behindBaseCount: 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 ?? null,
|
|
behindOfOrigin: gitStatus?.behindOfOrigin ?? null,
|
|
};
|
|
}
|
|
|
|
function computeShouldPromoteArchive(input: {
|
|
hasUncommittedChanges: boolean;
|
|
postShipArchiveSuggested: boolean;
|
|
isMergedPullRequest: boolean;
|
|
}): boolean {
|
|
return (
|
|
!input.hasUncommittedChanges && (input.postShipArchiveSuggested || input.isMergedPullRequest)
|
|
);
|
|
}
|
|
|
|
function deriveGitActionsState(args: DeriveGitActionsStateArgs): DerivedGitActionsState {
|
|
const {
|
|
isGit,
|
|
status,
|
|
gitStatus,
|
|
prStatus,
|
|
hasUncommittedChanges,
|
|
postShipArchiveSuggested,
|
|
isStatusLoading,
|
|
baseRefLabel,
|
|
} = args;
|
|
const actionsDisabled = !isGit || Boolean(status?.error) || isStatusLoading;
|
|
const isPaseoOwnedWorktree = gitStatus?.isPaseoOwnedWorktree ?? false;
|
|
const isMergedPullRequest = Boolean(prStatus?.isMerged);
|
|
return {
|
|
actionsDisabled,
|
|
...extractGitCommitCounts(gitStatus),
|
|
hasPullRequest: Boolean(prStatus?.url),
|
|
hasRemote: gitStatus?.hasRemote ?? false,
|
|
isPaseoOwnedWorktree,
|
|
isOnBaseBranch: gitStatus?.currentBranch === baseRefLabel,
|
|
shouldPromoteArchive: computeShouldPromoteArchive({
|
|
hasUncommittedChanges,
|
|
postShipArchiveSuggested,
|
|
isMergedPullRequest,
|
|
}),
|
|
};
|
|
}
|
|
|
|
interface UseGitActionsInput {
|
|
serverId: string;
|
|
cwd: string;
|
|
icons: {
|
|
commit: ReactElement;
|
|
pull: ReactElement;
|
|
push: ReactElement;
|
|
pullAndPush: ReactElement;
|
|
merge: ReactElement;
|
|
mergeFromBase: ReactElement;
|
|
archive: ReactElement;
|
|
};
|
|
}
|
|
|
|
interface UseGitActionsResult {
|
|
gitActions: GitActions;
|
|
branchLabel: string;
|
|
isGit: boolean;
|
|
}
|
|
|
|
interface UseWorkspaceScreenArchiveControllerInput {
|
|
serverId: string;
|
|
activeWorkspaceSelection: ActiveWorkspaceSelection | null;
|
|
workspaceDirectory: string | null | undefined;
|
|
branchLabel: string;
|
|
gitStatus: CheckoutStatusPayload | null;
|
|
t: (key: string, options?: Record<string, unknown>) => string;
|
|
}
|
|
|
|
function resolveArchiveWorkspaceDescriptor(input: {
|
|
workspaces: Map<string, WorkspaceDescriptor> | undefined;
|
|
activeWorkspaceSelection: ActiveWorkspaceSelection | null;
|
|
workspaceDirectory: string | null | undefined;
|
|
}): WorkspaceDescriptor | null {
|
|
const activeWorkspaceKey = input.activeWorkspaceSelection
|
|
? resolveWorkspaceMapKeyByIdentity({
|
|
workspaces: input.workspaces,
|
|
workspaceId: input.activeWorkspaceSelection.workspaceId,
|
|
})
|
|
: null;
|
|
if (activeWorkspaceKey) {
|
|
return input.workspaces?.get(activeWorkspaceKey) ?? null;
|
|
}
|
|
if (!input.workspaceDirectory) {
|
|
return null;
|
|
}
|
|
for (const candidate of input.workspaces?.values() ?? []) {
|
|
if (candidate.workspaceDirectory === input.workspaceDirectory) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function resolveWorkspaceArchiveRisk(
|
|
workspace: WorkspaceDescriptor | null,
|
|
gitStatus: CheckoutStatusPayload | null,
|
|
): { isDirty: boolean | null | undefined; aheadOfOrigin: number | null | undefined } {
|
|
return {
|
|
isDirty: gitStatus?.isDirty ?? workspace?.gitRuntime?.isDirty,
|
|
aheadOfOrigin: gitStatus?.aheadOfOrigin ?? workspace?.gitRuntime?.aheadOfOrigin,
|
|
};
|
|
}
|
|
|
|
function canArchiveWorkspace(
|
|
workspace: WorkspaceDescriptor | null,
|
|
risk: ReturnType<typeof resolveWorkspaceArchiveRisk>,
|
|
): boolean {
|
|
return (
|
|
workspace !== null &&
|
|
(workspace.workspaceKind !== "worktree" ||
|
|
(risk.isDirty !== undefined && risk.aheadOfOrigin !== undefined))
|
|
);
|
|
}
|
|
|
|
function useWorkspaceScreenArchiveController({
|
|
serverId,
|
|
activeWorkspaceSelection,
|
|
workspaceDirectory,
|
|
branchLabel,
|
|
gitStatus,
|
|
t,
|
|
}: UseWorkspaceScreenArchiveControllerInput) {
|
|
const sessionWorkspaces = useSessionStore((state) => state.sessions[serverId]?.workspaces);
|
|
const [isHidingWorkspace, setIsHidingWorkspace] = useState(false);
|
|
const workspaceDescriptor = useMemo(
|
|
() =>
|
|
resolveArchiveWorkspaceDescriptor({
|
|
workspaces: sessionWorkspaces,
|
|
activeWorkspaceSelection,
|
|
workspaceDirectory,
|
|
}),
|
|
[activeWorkspaceSelection, sessionWorkspaces, workspaceDirectory],
|
|
);
|
|
const archiveRisk = resolveWorkspaceArchiveRisk(workspaceDescriptor, gitStatus);
|
|
|
|
const controller = useWorkspaceArchive({
|
|
serverId,
|
|
workspaceId: workspaceDescriptor?.id ?? "",
|
|
workspaceKind: workspaceDescriptor?.workspaceKind ?? "directory",
|
|
name: workspaceDescriptor?.name ?? branchLabel,
|
|
isDirty: archiveRisk.isDirty,
|
|
aheadOfOrigin: archiveRisk.aheadOfOrigin,
|
|
diffStat: workspaceDescriptor?.diffStat ?? null,
|
|
warningLabels: getWorktreeArchiveWarningLabels(t),
|
|
onSetHiding: setIsHidingWorkspace,
|
|
onArchiveStarted: () => {
|
|
if (!activeWorkspaceSelection) {
|
|
return;
|
|
}
|
|
redirectIfArchivingActiveWorkspace({
|
|
serverId,
|
|
workspaceId: activeWorkspaceSelection.workspaceId,
|
|
activeWorkspaceSelection,
|
|
});
|
|
},
|
|
});
|
|
|
|
return {
|
|
...controller,
|
|
isArchiving: workspaceDescriptor?.archivingAt != null || isHidingWorkspace,
|
|
canArchive: canArchiveWorkspace(workspaceDescriptor, archiveRisk),
|
|
};
|
|
}
|
|
|
|
export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): UseGitActionsResult {
|
|
const { t } = useTranslation();
|
|
const toast = useToast();
|
|
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
|
const [postShipArchiveSuggested, setPostShipArchiveSuggested] = useState(false);
|
|
const [shipDefault, setShipDefault] = useState<"merge" | "pr">("pr");
|
|
|
|
const { status, isLoading: isStatusLoading } = useCheckoutStatusQuery({ serverId, cwd });
|
|
const gitStatus = status && status.isGit ? status : null;
|
|
const isGit = Boolean(gitStatus);
|
|
const notGit = status !== null && !status.isGit && !status.error;
|
|
const baseRef = gitStatus?.baseRef ?? undefined;
|
|
|
|
const hasUncommittedChanges = Boolean(gitStatus?.isDirty);
|
|
|
|
const {
|
|
status: prStatus,
|
|
githubFeaturesEnabled,
|
|
forge,
|
|
} = useCheckoutPrStatusQuery({
|
|
serverId,
|
|
cwd,
|
|
enabled: isGit,
|
|
});
|
|
const prIcon = useMemo(() => renderForgePrIcon(forge), [forge]);
|
|
const baseRefLabel = useMemo(
|
|
() => formatBaseRefLabel(baseRef, t("workspace.git.diff.base")),
|
|
[baseRef, t],
|
|
);
|
|
const branchLabel = resolveBranchLabel({
|
|
currentBranch: gitStatus?.currentBranch,
|
|
notGit,
|
|
notRepositoryLabel: t("workspace.git.diff.notRepository"),
|
|
unknownLabel: t("workspace.git.diff.branchUnknown"),
|
|
});
|
|
|
|
// Ship default persistence
|
|
const shipDefaultStorageKey = useMemo(() => {
|
|
if (!gitStatus?.repoRoot) {
|
|
return null;
|
|
}
|
|
return `@paseo:changes-ship-default:${gitStatus.repoRoot}`;
|
|
}, [gitStatus?.repoRoot]);
|
|
|
|
useEffect(() => {
|
|
if (!shipDefaultStorageKey) {
|
|
setShipDefault("pr");
|
|
return;
|
|
}
|
|
let isActive = true;
|
|
setShipDefault("pr");
|
|
readValidatedString(AsyncStorage, shipDefaultStorageKey, z.enum(["pr", "merge"]))
|
|
.then((value) => {
|
|
if (!isActive) return;
|
|
if (value) {
|
|
setShipDefault(value);
|
|
return;
|
|
}
|
|
setShipDefault("pr");
|
|
return;
|
|
})
|
|
.catch(() => undefined);
|
|
return () => {
|
|
isActive = false;
|
|
};
|
|
}, [shipDefaultStorageKey]);
|
|
|
|
const persistShipDefault = useCallback(
|
|
async (next: "merge" | "pr") => {
|
|
setShipDefault(next);
|
|
if (!shipDefaultStorageKey) return;
|
|
try {
|
|
await AsyncStorage.setItem(shipDefaultStorageKey, next);
|
|
} catch {
|
|
// Ignore persistence failures; default will reset to "pr".
|
|
}
|
|
},
|
|
[shipDefaultStorageKey],
|
|
);
|
|
|
|
useEffect(() => {
|
|
setPostShipArchiveSuggested(false);
|
|
}, [cwd]);
|
|
|
|
const commitStatus = useCheckoutGitActionsStore((s) =>
|
|
s.getStatus({ serverId, cwd, actionId: "commit" }),
|
|
);
|
|
const pullStatus = useCheckoutGitActionsStore((s) =>
|
|
s.getStatus({ serverId, cwd, actionId: "pull" }),
|
|
);
|
|
const pushStatus = useCheckoutGitActionsStore((s) =>
|
|
s.getStatus({ serverId, cwd, actionId: "push" }),
|
|
);
|
|
const pullAndPushStatus = useCheckoutGitActionsStore((s) =>
|
|
s.getStatus({ serverId, cwd, actionId: "pull-and-push" }),
|
|
);
|
|
const prCreateStatus = useCheckoutGitActionsStore((s) =>
|
|
s.getStatus({ serverId, cwd, actionId: "create-pr" }),
|
|
);
|
|
const mergePrStatuses: Record<CheckoutPrMergeMethod, CheckoutGitActionStatus> = {
|
|
squash: useCheckoutGitActionsStore((s) =>
|
|
s.getStatus({ serverId, cwd, actionId: "merge-pr-squash" }),
|
|
),
|
|
merge: useCheckoutGitActionsStore((s) =>
|
|
s.getStatus({ serverId, cwd, actionId: "merge-pr-merge" }),
|
|
),
|
|
rebase: useCheckoutGitActionsStore((s) =>
|
|
s.getStatus({ serverId, cwd, actionId: "merge-pr-rebase" }),
|
|
),
|
|
};
|
|
const enablePrAutoMergeStatuses: Record<CheckoutPrMergeMethod, CheckoutGitActionStatus> = {
|
|
squash: useCheckoutGitActionsStore((s) =>
|
|
s.getStatus({ serverId, cwd, actionId: "enable-pr-auto-merge-squash" }),
|
|
),
|
|
merge: useCheckoutGitActionsStore((s) =>
|
|
s.getStatus({ serverId, cwd, actionId: "enable-pr-auto-merge-merge" }),
|
|
),
|
|
rebase: useCheckoutGitActionsStore((s) =>
|
|
s.getStatus({ serverId, cwd, actionId: "enable-pr-auto-merge-rebase" }),
|
|
),
|
|
};
|
|
const disablePrAutoMergeStatus = useCheckoutGitActionsStore((s) =>
|
|
s.getStatus({ serverId, cwd, actionId: "disable-pr-auto-merge" }),
|
|
);
|
|
const mergeStatus = useCheckoutGitActionsStore((s) =>
|
|
s.getStatus({ serverId, cwd, actionId: "merge-branch" }),
|
|
);
|
|
const mergeFromBaseStatus = useCheckoutGitActionsStore((s) =>
|
|
s.getStatus({ serverId, cwd, actionId: "merge-from-base" }),
|
|
);
|
|
|
|
const runCommit = useCheckoutGitActionsStore((s) => s.commit);
|
|
const runPull = useCheckoutGitActionsStore((s) => s.pull);
|
|
const runPush = useCheckoutGitActionsStore((s) => s.push);
|
|
const runPullAndPush = useCheckoutGitActionsStore((s) => s.pullAndPush);
|
|
const runCreatePr = useCheckoutGitActionsStore((s) => s.createPr);
|
|
const runMergePr = useCheckoutGitActionsStore((s) => s.mergePr);
|
|
const runEnablePrAutoMerge = useCheckoutGitActionsStore((s) => s.enablePrAutoMerge);
|
|
const runDisablePrAutoMerge = useCheckoutGitActionsStore((s) => s.disablePrAutoMerge);
|
|
const runMergeBranch = useCheckoutGitActionsStore((s) => s.mergeBranch);
|
|
const runMergeFromBase = useCheckoutGitActionsStore((s) => s.mergeFromBase);
|
|
const githubAutoMergeActionsEnabled = useSessionStore(
|
|
(s) =>
|
|
s.sessions[serverId]?.serverInfo?.features?.checkoutForgeSetAutoMerge === true ||
|
|
s.sessions[serverId]?.serverInfo?.features?.checkoutGithubSetAutoMerge === true,
|
|
);
|
|
|
|
const toastActionError = useCallback(
|
|
(error: unknown, fallback: string) => {
|
|
const message = error instanceof Error ? error.message : fallback;
|
|
toast.error(message);
|
|
},
|
|
[toast],
|
|
);
|
|
|
|
const toastActionSuccess = useCallback(
|
|
(message: string) => {
|
|
toast.show(message, { variant: "success" });
|
|
},
|
|
[toast],
|
|
);
|
|
|
|
// Handlers
|
|
const handleCommit = useCallback(() => {
|
|
void runCommit({ serverId, cwd })
|
|
.then(() => {
|
|
toastActionSuccess(t("workspace.git.actions.commit.success"));
|
|
return;
|
|
})
|
|
.catch((err) => {
|
|
toastActionError(err, t("workspace.git.actions.toasts.failedCommit"));
|
|
});
|
|
}, [cwd, runCommit, serverId, t, toastActionError, toastActionSuccess]);
|
|
|
|
const handlePull = useCallback(() => {
|
|
void runPull({ serverId, cwd })
|
|
.then(() => {
|
|
toastActionSuccess(t("workspace.git.actions.pull.success"));
|
|
return;
|
|
})
|
|
.catch((err) => {
|
|
toastActionError(err, t("workspace.git.actions.toasts.failedPull"));
|
|
});
|
|
}, [cwd, runPull, serverId, t, toastActionError, toastActionSuccess]);
|
|
|
|
const handlePush = useCallback(() => {
|
|
void runPush({ serverId, cwd })
|
|
.then(() => {
|
|
toastActionSuccess(t("workspace.git.actions.push.success"));
|
|
return;
|
|
})
|
|
.catch((err) => {
|
|
toastActionError(err, t("workspace.git.actions.toasts.failedPush"));
|
|
});
|
|
}, [cwd, runPush, serverId, t, toastActionError, toastActionSuccess]);
|
|
|
|
const handlePullAndPush = useCallback(() => {
|
|
void runPullAndPush({ serverId, cwd })
|
|
.then(() => {
|
|
toastActionSuccess(t("workspace.git.actions.pullAndPush.success"));
|
|
return;
|
|
})
|
|
.catch((err) => {
|
|
toastActionError(err, t("workspace.git.actions.toasts.failedPullAndPush"));
|
|
});
|
|
}, [cwd, runPullAndPush, serverId, t, toastActionError, toastActionSuccess]);
|
|
|
|
const handleCreatePr = useCallback(() => {
|
|
void persistShipDefault("pr");
|
|
void runCreatePr({ serverId, cwd })
|
|
.then(() => {
|
|
toastActionSuccess(t("workspace.git.actions.createPr.success", forgeVocabulary(forge)));
|
|
return;
|
|
})
|
|
.catch((err) => {
|
|
toastActionError(err, t("workspace.git.actions.toasts.failedCreatePr"));
|
|
});
|
|
}, [
|
|
cwd,
|
|
forge,
|
|
persistShipDefault,
|
|
runCreatePr,
|
|
serverId,
|
|
t,
|
|
toastActionError,
|
|
toastActionSuccess,
|
|
]);
|
|
|
|
const handleMergePr = useCallback(
|
|
(method: CheckoutPrMergeMethod) => {
|
|
void persistShipDefault("pr");
|
|
void runMergePr({ serverId, cwd, method })
|
|
.then(() => {
|
|
setPostShipArchiveSuggested(true);
|
|
toastActionSuccess(t("workspace.git.actions.mergePr.success", forgeVocabulary(forge)));
|
|
return;
|
|
})
|
|
.catch((err) => {
|
|
toastActionError(err, t("workspace.git.actions.toasts.failedMergePr"));
|
|
});
|
|
},
|
|
[cwd, forge, persistShipDefault, runMergePr, serverId, t, toastActionError, toastActionSuccess],
|
|
);
|
|
|
|
const handleEnablePrAutoMerge = useCallback(
|
|
(method: CheckoutPrMergeMethod) => {
|
|
void persistShipDefault("pr");
|
|
void runEnablePrAutoMerge({ serverId, cwd, method })
|
|
.then(() => {
|
|
toastActionSuccess(t("workspace.git.actions.autoMerge.enabled"));
|
|
return;
|
|
})
|
|
.catch((err) => {
|
|
toastActionError(err, t("workspace.git.actions.toasts.failedEnableAutoMerge"));
|
|
});
|
|
},
|
|
[
|
|
cwd,
|
|
persistShipDefault,
|
|
runEnablePrAutoMerge,
|
|
serverId,
|
|
t,
|
|
toastActionError,
|
|
toastActionSuccess,
|
|
],
|
|
);
|
|
|
|
const handleDisablePrAutoMerge = useCallback(() => {
|
|
void runDisablePrAutoMerge({ serverId, cwd })
|
|
.then(() => {
|
|
toastActionSuccess(t("workspace.git.actions.autoMerge.disabled"));
|
|
return;
|
|
})
|
|
.catch((err) => {
|
|
toastActionError(err, t("workspace.git.actions.toasts.failedDisableAutoMerge"));
|
|
});
|
|
}, [cwd, runDisablePrAutoMerge, serverId, t, toastActionError, toastActionSuccess]);
|
|
|
|
const handleMergeBranch = useCallback(() => {
|
|
if (!baseRef) {
|
|
toast.error(t("workspace.git.actions.toasts.baseRefUnavailable"));
|
|
return;
|
|
}
|
|
void persistShipDefault("merge");
|
|
void runMergeBranch({ serverId, cwd, baseRef })
|
|
.then(() => {
|
|
setPostShipArchiveSuggested(true);
|
|
toastActionSuccess(t("workspace.git.actions.mergeBranch.success"));
|
|
return;
|
|
})
|
|
.catch((err) => {
|
|
toastActionError(err, t("workspace.git.actions.toasts.failedMerge"));
|
|
});
|
|
}, [
|
|
baseRef,
|
|
cwd,
|
|
persistShipDefault,
|
|
runMergeBranch,
|
|
serverId,
|
|
t,
|
|
toast,
|
|
toastActionError,
|
|
toastActionSuccess,
|
|
]);
|
|
|
|
const handleMergeFromBase = useCallback(() => {
|
|
if (!baseRef) {
|
|
toast.error(t("workspace.git.actions.toasts.baseRefUnavailable"));
|
|
return;
|
|
}
|
|
void runMergeFromBase({ serverId, cwd, baseRef })
|
|
.then(() => {
|
|
toastActionSuccess(t("workspace.git.actions.mergeFromBase.success"));
|
|
return;
|
|
})
|
|
.catch((err) => {
|
|
toastActionError(err, t("workspace.git.actions.toasts.failedMergeFromBase"));
|
|
});
|
|
}, [baseRef, cwd, runMergeFromBase, serverId, t, toast, toastActionError, toastActionSuccess]);
|
|
|
|
const archiveController = useWorkspaceScreenArchiveController({
|
|
serverId,
|
|
activeWorkspaceSelection,
|
|
workspaceDirectory: status?.cwd,
|
|
branchLabel,
|
|
gitStatus,
|
|
t,
|
|
});
|
|
|
|
const handleArchiveWorkspace = useCallback(() => {
|
|
archiveController.archive();
|
|
}, [archiveController]);
|
|
|
|
const derived = deriveGitActionsState({
|
|
isGit,
|
|
status,
|
|
gitStatus,
|
|
prStatus,
|
|
hasUncommittedChanges,
|
|
postShipArchiveSuggested,
|
|
isStatusLoading,
|
|
baseRefLabel,
|
|
});
|
|
const {
|
|
actionsDisabled,
|
|
aheadCount,
|
|
behindBaseCount,
|
|
aheadOfOrigin,
|
|
behindOfOrigin,
|
|
hasPullRequest,
|
|
hasRemote,
|
|
isPaseoOwnedWorktree,
|
|
isOnBaseBranch,
|
|
shouldPromoteArchive,
|
|
} = derived;
|
|
|
|
const handlePrAction = useCallback(() => {
|
|
if (prStatus?.url) {
|
|
openURLInNewTab(prStatus.url);
|
|
return;
|
|
}
|
|
handleCreatePr();
|
|
}, [prStatus?.url, handleCreatePr]);
|
|
|
|
// Build actions
|
|
const gitActionsInput = useMemo<BuildGitActionsInput>(() => {
|
|
const presentation = getForgePresentation(forge);
|
|
return {
|
|
isGit,
|
|
githubFeaturesEnabled,
|
|
forgeBrandLabel: presentation.brandLabel,
|
|
forgeChangeRequestNoun: presentation.changeRequestAbbrev,
|
|
githubAutoMergeActionsEnabled,
|
|
hasPullRequest,
|
|
pullRequestUrl: prStatus?.url ?? null,
|
|
pullRequestState: narrowPullRequestState(prStatus?.state),
|
|
pullRequestIsDraft: prStatus?.isDraft ?? false,
|
|
pullRequestIsMerged: prStatus?.isMerged ?? false,
|
|
pullRequestMergeable: prStatus?.mergeable ?? "UNKNOWN",
|
|
mergeCapability: deriveMergeCapability(prStatus?.forgeSpecific, prStatus?.github),
|
|
hasRemote,
|
|
isPaseoOwnedWorktree,
|
|
isOnBaseBranch,
|
|
hasUncommittedChanges,
|
|
baseRefAvailable: Boolean(baseRef),
|
|
baseRefLabel,
|
|
aheadCount,
|
|
behindBaseCount,
|
|
aheadOfOrigin,
|
|
behindOfOrigin,
|
|
shouldPromoteArchive,
|
|
shipDefault,
|
|
runtime: {
|
|
commit: {
|
|
disabled: isActionDisabled(actionsDisabled, commitStatus),
|
|
status: commitStatus,
|
|
icon: icons.commit,
|
|
handler: handleCommit,
|
|
},
|
|
pull: {
|
|
disabled: isActionDisabled(actionsDisabled, pullStatus),
|
|
status: pullStatus,
|
|
icon: icons.pull,
|
|
handler: handlePull,
|
|
},
|
|
push: {
|
|
disabled: isActionDisabled(actionsDisabled, pushStatus),
|
|
status: pushStatus,
|
|
icon: icons.push,
|
|
handler: handlePush,
|
|
},
|
|
"pull-and-push": {
|
|
disabled: isActionDisabled(actionsDisabled, pullAndPushStatus),
|
|
status: pullAndPushStatus,
|
|
icon: icons.pullAndPush,
|
|
handler: handlePullAndPush,
|
|
},
|
|
pr: {
|
|
disabled: isActionDisabled(actionsDisabled, prCreateStatus),
|
|
status: hasPullRequest ? "idle" : prCreateStatus,
|
|
icon: prIcon,
|
|
handler: handlePrAction,
|
|
},
|
|
"merge-pr-squash": {
|
|
disabled: isActionDisabled(actionsDisabled, mergePrStatuses.squash),
|
|
status: mergePrStatuses.squash,
|
|
icon: prIcon,
|
|
handler: () => handleMergePr("squash"),
|
|
},
|
|
"merge-pr-merge": {
|
|
disabled: isActionDisabled(actionsDisabled, mergePrStatuses.merge),
|
|
status: mergePrStatuses.merge,
|
|
icon: prIcon,
|
|
handler: () => handleMergePr("merge"),
|
|
},
|
|
"merge-pr-rebase": {
|
|
disabled: isActionDisabled(actionsDisabled, mergePrStatuses.rebase),
|
|
status: mergePrStatuses.rebase,
|
|
icon: prIcon,
|
|
handler: () => handleMergePr("rebase"),
|
|
},
|
|
"enable-pr-auto-merge-squash": {
|
|
disabled: isActionDisabled(actionsDisabled, enablePrAutoMergeStatuses.squash),
|
|
status: enablePrAutoMergeStatuses.squash,
|
|
icon: prIcon,
|
|
handler: () => handleEnablePrAutoMerge("squash"),
|
|
},
|
|
"enable-pr-auto-merge-merge": {
|
|
disabled: isActionDisabled(actionsDisabled, enablePrAutoMergeStatuses.merge),
|
|
status: enablePrAutoMergeStatuses.merge,
|
|
icon: prIcon,
|
|
handler: () => handleEnablePrAutoMerge("merge"),
|
|
},
|
|
"enable-pr-auto-merge-rebase": {
|
|
disabled: isActionDisabled(actionsDisabled, enablePrAutoMergeStatuses.rebase),
|
|
status: enablePrAutoMergeStatuses.rebase,
|
|
icon: prIcon,
|
|
handler: () => handleEnablePrAutoMerge("rebase"),
|
|
},
|
|
"disable-pr-auto-merge": {
|
|
disabled: isActionDisabled(actionsDisabled, disablePrAutoMergeStatus),
|
|
status: disablePrAutoMergeStatus,
|
|
icon: prIcon,
|
|
handler: handleDisablePrAutoMerge,
|
|
},
|
|
"merge-branch": {
|
|
disabled: isActionDisabled(actionsDisabled, mergeStatus),
|
|
status: mergeStatus,
|
|
icon: icons.merge,
|
|
handler: handleMergeBranch,
|
|
},
|
|
"merge-from-base": {
|
|
disabled: isActionDisabled(actionsDisabled, mergeFromBaseStatus),
|
|
status: mergeFromBaseStatus,
|
|
icon: icons.mergeFromBase,
|
|
handler: handleMergeFromBase,
|
|
},
|
|
"archive-workspace": {
|
|
disabled: !archiveController.canArchive || archiveController.isArchiving,
|
|
status: archiveController.isArchiving ? "pending" : "idle",
|
|
icon: icons.archive,
|
|
handler: handleArchiveWorkspace,
|
|
},
|
|
},
|
|
};
|
|
}, [
|
|
isGit,
|
|
hasRemote,
|
|
hasPullRequest,
|
|
prStatus?.url,
|
|
prStatus?.state,
|
|
prStatus?.isDraft,
|
|
prStatus?.isMerged,
|
|
prStatus?.mergeable,
|
|
prStatus?.forgeSpecific,
|
|
prStatus?.github,
|
|
aheadCount,
|
|
behindBaseCount,
|
|
isPaseoOwnedWorktree,
|
|
isOnBaseBranch,
|
|
githubFeaturesEnabled,
|
|
forge,
|
|
githubAutoMergeActionsEnabled,
|
|
hasUncommittedChanges,
|
|
aheadOfOrigin,
|
|
behindOfOrigin,
|
|
shipDefault,
|
|
baseRefLabel,
|
|
shouldPromoteArchive,
|
|
actionsDisabled,
|
|
commitStatus,
|
|
pullStatus,
|
|
pushStatus,
|
|
pullAndPushStatus,
|
|
prCreateStatus,
|
|
mergePrStatuses.squash,
|
|
mergePrStatuses.merge,
|
|
mergePrStatuses.rebase,
|
|
enablePrAutoMergeStatuses.squash,
|
|
enablePrAutoMergeStatuses.merge,
|
|
enablePrAutoMergeStatuses.rebase,
|
|
disablePrAutoMergeStatus,
|
|
mergeStatus,
|
|
mergeFromBaseStatus,
|
|
archiveController.canArchive,
|
|
archiveController.isArchiving,
|
|
handleCommit,
|
|
handlePull,
|
|
handlePush,
|
|
handlePullAndPush,
|
|
handlePrAction,
|
|
handleMergePr,
|
|
handleEnablePrAutoMerge,
|
|
handleDisablePrAutoMerge,
|
|
handleMergeBranch,
|
|
handleMergeFromBase,
|
|
handleArchiveWorkspace,
|
|
icons,
|
|
prIcon,
|
|
baseRef,
|
|
]);
|
|
|
|
const gitActions: GitActions = useMemo(
|
|
() =>
|
|
translateGitActions(buildGitActions(gitActionsInput), {
|
|
baseRefLabel,
|
|
hasPullRequest,
|
|
forge,
|
|
t,
|
|
}),
|
|
[gitActionsInput, baseRefLabel, hasPullRequest, forge, t],
|
|
);
|
|
|
|
return { gitActions, branchLabel, isGit };
|
|
}
|
|
|
|
function translateGitActions(
|
|
actions: GitActions,
|
|
input: {
|
|
baseRefLabel: string;
|
|
hasPullRequest: boolean;
|
|
forge: Forge;
|
|
t: (key: string, options?: Record<string, unknown>) => string;
|
|
},
|
|
): GitActions {
|
|
return {
|
|
primary: actions.primary ? translateGitAction(actions.primary, input) : null,
|
|
secondary: actions.secondary.map((action) => translateGitAction(action, input)),
|
|
menu: actions.menu.map((action) => translateGitAction(action, input)),
|
|
};
|
|
}
|
|
|
|
function translateGitAction(
|
|
action: GitAction,
|
|
{
|
|
baseRefLabel,
|
|
hasPullRequest,
|
|
forge,
|
|
t,
|
|
}: {
|
|
baseRefLabel: string;
|
|
hasPullRequest: boolean;
|
|
forge: Forge;
|
|
t: (key: string, options?: Record<string, unknown>) => string;
|
|
},
|
|
): GitAction {
|
|
const labels = getTranslatedGitActionLabels(action, { baseRefLabel, hasPullRequest, forge, t });
|
|
return {
|
|
...action,
|
|
...labels,
|
|
unavailableMessage: translateGitActionUnavailableMessage(action.unavailableMessage, {
|
|
baseRefLabel,
|
|
t,
|
|
}),
|
|
};
|
|
}
|
|
|
|
function getTranslatedGitActionLabels(
|
|
action: GitAction,
|
|
{
|
|
baseRefLabel,
|
|
hasPullRequest,
|
|
forge,
|
|
t,
|
|
}: {
|
|
baseRefLabel: string;
|
|
hasPullRequest: boolean;
|
|
forge: Forge;
|
|
t: (key: string, options?: Record<string, unknown>) => string;
|
|
},
|
|
): Pick<GitAction, "label" | "pendingLabel" | "successLabel"> {
|
|
switch (action.id) {
|
|
case "commit":
|
|
return {
|
|
label: t("workspace.git.actions.commit.label"),
|
|
pendingLabel: t("workspace.git.actions.commit.pending"),
|
|
successLabel: t("workspace.git.actions.commit.success"),
|
|
};
|
|
case "pull":
|
|
return {
|
|
label: t("workspace.git.actions.pull.label"),
|
|
pendingLabel: t("workspace.git.actions.pull.pending"),
|
|
successLabel: t("workspace.git.actions.pull.success"),
|
|
};
|
|
case "push":
|
|
return {
|
|
label: t("workspace.git.actions.push.label"),
|
|
pendingLabel: t("workspace.git.actions.push.pending"),
|
|
successLabel: t("workspace.git.actions.push.success"),
|
|
};
|
|
case "pull-and-push":
|
|
return {
|
|
label: t("workspace.git.actions.pullAndPush.label"),
|
|
pendingLabel: t("workspace.git.actions.pullAndPush.pending"),
|
|
successLabel: t("workspace.git.actions.pullAndPush.success"),
|
|
};
|
|
case "pr":
|
|
return hasPullRequest
|
|
? {
|
|
label: t("workspace.git.actions.viewPr", forgeVocabulary(forge)),
|
|
pendingLabel: t("workspace.git.actions.viewPr", forgeVocabulary(forge)),
|
|
successLabel: t("workspace.git.actions.viewPr", forgeVocabulary(forge)),
|
|
}
|
|
: {
|
|
label: t("workspace.git.actions.createPr.label", forgeVocabulary(forge)),
|
|
pendingLabel: t("workspace.git.actions.createPr.pending", forgeVocabulary(forge)),
|
|
successLabel: t("workspace.git.actions.createPr.success", forgeVocabulary(forge)),
|
|
};
|
|
case "merge-pr-squash":
|
|
return {
|
|
label: t("workspace.git.actions.mergePr.squash", forgeVocabulary(forge)),
|
|
pendingLabel: t("workspace.git.actions.mergePr.pending", forgeVocabulary(forge)),
|
|
successLabel: t("workspace.git.actions.mergePr.success", forgeVocabulary(forge)),
|
|
};
|
|
case "merge-pr-merge":
|
|
return {
|
|
label: t("workspace.git.actions.mergePr.merge", forgeVocabulary(forge)),
|
|
pendingLabel: t("workspace.git.actions.mergePr.pending", forgeVocabulary(forge)),
|
|
successLabel: t("workspace.git.actions.mergePr.success", forgeVocabulary(forge)),
|
|
};
|
|
case "merge-pr-rebase":
|
|
return {
|
|
label: t("workspace.git.actions.mergePr.rebase", forgeVocabulary(forge)),
|
|
pendingLabel: t("workspace.git.actions.mergePr.pending", forgeVocabulary(forge)),
|
|
successLabel: t("workspace.git.actions.mergePr.success", forgeVocabulary(forge)),
|
|
};
|
|
case "enable-pr-auto-merge-squash":
|
|
return {
|
|
label: t("workspace.git.actions.autoMerge.enableSquash"),
|
|
pendingLabel: t("workspace.git.actions.autoMerge.enabling"),
|
|
successLabel: t("workspace.git.actions.autoMerge.enabled"),
|
|
};
|
|
case "enable-pr-auto-merge-merge":
|
|
return {
|
|
label: t("workspace.git.actions.autoMerge.enableMerge"),
|
|
pendingLabel: t("workspace.git.actions.autoMerge.enabling"),
|
|
successLabel: t("workspace.git.actions.autoMerge.enabled"),
|
|
};
|
|
case "enable-pr-auto-merge-rebase":
|
|
return {
|
|
label: t("workspace.git.actions.autoMerge.enableRebase"),
|
|
pendingLabel: t("workspace.git.actions.autoMerge.enabling"),
|
|
successLabel: t("workspace.git.actions.autoMerge.enabled"),
|
|
};
|
|
case "disable-pr-auto-merge":
|
|
return {
|
|
label: t("workspace.git.actions.autoMerge.enabled"),
|
|
pendingLabel: t("workspace.git.actions.autoMerge.disabling"),
|
|
successLabel: t("workspace.git.actions.autoMerge.disabled"),
|
|
};
|
|
case "merge-branch":
|
|
return {
|
|
label: t("workspace.git.actions.mergeBranch.label"),
|
|
pendingLabel: t("workspace.git.actions.mergeBranch.pending"),
|
|
successLabel: t("workspace.git.actions.mergeBranch.success"),
|
|
};
|
|
case "merge-from-base":
|
|
return {
|
|
label: t("workspace.git.actions.mergeFromBase.label", { baseRef: baseRefLabel }),
|
|
pendingLabel: t("workspace.git.actions.mergeFromBase.pending"),
|
|
successLabel: t("workspace.git.actions.mergeFromBase.success"),
|
|
};
|
|
case "archive-workspace":
|
|
return {
|
|
label: t("workspace.git.actions.archive.label"),
|
|
pendingLabel: t("workspace.git.actions.archive.pending"),
|
|
successLabel: t("workspace.git.actions.archive.success"),
|
|
};
|
|
}
|
|
}
|
|
|
|
function translateGitActionUnavailableMessage(
|
|
message: string | undefined,
|
|
{
|
|
baseRefLabel,
|
|
t,
|
|
}: {
|
|
baseRefLabel: string;
|
|
t: (key: string, options?: Record<string, unknown>) => string;
|
|
},
|
|
): string | undefined {
|
|
if (!message) return undefined;
|
|
const keyByMessage: Record<string, string> = {
|
|
"Pull isn't available here because this branch is not connected to a remote yet":
|
|
"workspace.git.actions.unavailable.pullNoRemote",
|
|
"Pull isn't available while you have local changes so commit or stash them first":
|
|
"workspace.git.actions.unavailable.pullDirty",
|
|
"Pull isn't available because this branch is already up to date":
|
|
"workspace.git.actions.unavailable.pullUpToDate",
|
|
"Push isn't available here because this branch is not connected to a remote yet":
|
|
"workspace.git.actions.unavailable.pushNoRemote",
|
|
"Push isn't available yet because there are newer changes to bring in first":
|
|
"workspace.git.actions.unavailable.pushBehind",
|
|
"Push isn't available because there is nothing new to send":
|
|
"workspace.git.actions.unavailable.pushNothing",
|
|
"Pull and push isn't available here because this branch is not connected to a remote yet":
|
|
"workspace.git.actions.unavailable.pullAndPushNoRemote",
|
|
"Pull and push isn't available while you have local changes so commit or stash them first":
|
|
"workspace.git.actions.unavailable.pullAndPushDirty",
|
|
"Pull and push isn't available because this branch is already in sync":
|
|
"workspace.git.actions.unavailable.pullAndPushInSync",
|
|
"Create PR isn't available because this branch doesn't have any new commits yet":
|
|
"workspace.git.actions.unavailable.createPrNoCommits",
|
|
"Merge isn't available because we couldn't determine the base branch":
|
|
"workspace.git.actions.unavailable.mergeNoBase",
|
|
"Merge isn't available while you have local changes so commit or stash them first":
|
|
"workspace.git.actions.unavailable.mergeDirty",
|
|
"Merge isn't available because this branch doesn't have anything new to merge yet":
|
|
"workspace.git.actions.unavailable.mergeNothing",
|
|
"Update isn't available because we couldn't determine the base branch":
|
|
"workspace.git.actions.unavailable.updateNoBase",
|
|
"Update isn't available while you have local changes so commit or stash them first":
|
|
"workspace.git.actions.unavailable.updateDirty",
|
|
"Merge PR isn't available right now because GitHub isn't connected":
|
|
"workspace.git.actions.unavailable.mergePrNoGithub",
|
|
"Archive isn't available here because this workspace was not created as a Paseo worktree":
|
|
"workspace.git.actions.unavailable.archiveNotWorktree",
|
|
"Merge PR isn't available because there isn't a pull request yet":
|
|
"workspace.git.actions.unavailable.mergePrMissing",
|
|
"Merge PR isn't available because the pull request is still a draft":
|
|
"workspace.git.actions.unavailable.mergePrDraft",
|
|
"Merge PR isn't available because the pull request is already merged":
|
|
"workspace.git.actions.unavailable.mergePrMerged",
|
|
"Merge PR isn't available because the pull request is closed":
|
|
"workspace.git.actions.unavailable.mergePrClosed",
|
|
"Merge PR isn't available because the pull request has conflicts":
|
|
"workspace.git.actions.unavailable.mergePrConflicts",
|
|
"Merge PR isn't available here because this repository uses a merge queue":
|
|
"workspace.git.actions.unavailable.mergePrQueue",
|
|
"Auto-merge is enabled, but this account can't disable it":
|
|
"workspace.git.actions.unavailable.autoMergeCannotDisable",
|
|
};
|
|
if (
|
|
message.startsWith("Update isn't available because this branch is already up to date with ")
|
|
) {
|
|
return t("workspace.git.actions.unavailable.updateCurrent", { baseRef: baseRefLabel });
|
|
}
|
|
const key = keyByMessage[message];
|
|
return key ? t(key) : message;
|
|
}
|
|
|
|
function getWorktreeArchiveWarningLabels(
|
|
t: (key: string, options?: Record<string, unknown>) => string,
|
|
): WorktreeArchiveWarningLabels {
|
|
return {
|
|
title: (workspaceName) => t("workspace.git.actions.archiveWarning.title", { workspaceName }),
|
|
confirm: t("workspace.git.actions.archiveWarning.confirm"),
|
|
cancel: t("workspace.git.actions.archiveWarning.cancel"),
|
|
uncommittedChanges: t("workspace.git.actions.archiveWarning.uncommittedChanges"),
|
|
uncommittedChangesWithDiff: (diffStat) =>
|
|
t("workspace.git.actions.archiveWarning.uncommittedChangesWithDiff", { diffStat }),
|
|
addedLine: (count) =>
|
|
t(
|
|
count === 1
|
|
? "workspace.git.actions.archiveWarning.addedLine"
|
|
: "workspace.git.actions.archiveWarning.addedLines",
|
|
{ count },
|
|
),
|
|
deletedLine: (count) =>
|
|
t(
|
|
count === 1
|
|
? "workspace.git.actions.archiveWarning.deletedLine"
|
|
: "workspace.git.actions.archiveWarning.deletedLines",
|
|
{ count },
|
|
),
|
|
unpushedCommit: (count) =>
|
|
t(
|
|
count === 1
|
|
? "workspace.git.actions.archiveWarning.unpushedCommit"
|
|
: "workspace.git.actions.archiveWarning.unpushedCommits",
|
|
{ count },
|
|
),
|
|
};
|
|
}
|