* feat(app): expose archive for every workspace Workspace actions now archive one workspace by id. The daemon remains responsible for backing-specific cleanup, including removing a managed worktree only after its final workspace is archived. * refactor(app): clarify archive descriptor name * fix(app): preserve workspace archive safety * fix(app): allow archive without upstream * refactor(app): remove stale archive callback * test(app): cover workspace archive ownership
133 lines
4.3 KiB
TypeScript
133 lines
4.3 KiB
TypeScript
import { confirmDialog } from "@/utils/confirm-dialog";
|
|
import { i18n } from "@/i18n/i18next";
|
|
|
|
export interface WorktreeArchiveRisk {
|
|
isDirty?: boolean | null;
|
|
aheadOfOrigin?: number | null;
|
|
diffStat?: { additions: number; deletions: number } | null;
|
|
}
|
|
|
|
export interface WorktreeArchiveRiskInput {
|
|
archiveHasUncommittedChanges?: boolean | null;
|
|
archiveUnpushedCommitCount?: number | null;
|
|
diffStat?: WorktreeArchiveRisk["diffStat"];
|
|
}
|
|
|
|
export interface WorktreeArchiveConfirmationInput extends WorktreeArchiveRisk {
|
|
workspaceName: string;
|
|
}
|
|
|
|
export interface WorktreeArchiveWarningLabels {
|
|
title: (workspaceName: string) => string;
|
|
confirm: string;
|
|
cancel: string;
|
|
uncommittedChanges: string;
|
|
uncommittedChangesWithDiff: (diffStat: string) => string;
|
|
addedLine: (count: number) => string;
|
|
deletedLine: (count: number) => string;
|
|
unpushedCommit: (count: number) => string;
|
|
}
|
|
|
|
export const DEFAULT_WORKTREE_ARCHIVE_WARNING_LABELS: WorktreeArchiveWarningLabels = {
|
|
title: (workspaceName) => i18n.t("workspace.git.actions.archiveWarning.title", { workspaceName }),
|
|
confirm: i18n.t("workspace.git.actions.archiveWarning.confirm"),
|
|
cancel: i18n.t("workspace.git.actions.archiveWarning.cancel"),
|
|
uncommittedChanges: i18n.t("workspace.git.actions.archiveWarning.uncommittedChanges"),
|
|
uncommittedChangesWithDiff: (diffStat) =>
|
|
i18n.t("workspace.git.actions.archiveWarning.uncommittedChangesWithDiff", { diffStat }),
|
|
addedLine: (count) =>
|
|
count === 1
|
|
? i18n.t("workspace.git.actions.archiveWarning.addedLine", { count })
|
|
: i18n.t("workspace.git.actions.archiveWarning.addedLines", { count }),
|
|
deletedLine: (count) =>
|
|
count === 1
|
|
? i18n.t("workspace.git.actions.archiveWarning.deletedLine", { count })
|
|
: i18n.t("workspace.git.actions.archiveWarning.deletedLines", { count }),
|
|
unpushedCommit: (count) =>
|
|
count === 1
|
|
? i18n.t("workspace.git.actions.archiveWarning.unpushedCommit", { count })
|
|
: i18n.t("workspace.git.actions.archiveWarning.unpushedCommits", { count }),
|
|
};
|
|
|
|
export function toWorktreeArchiveRisk(input: WorktreeArchiveRiskInput): WorktreeArchiveRisk {
|
|
return {
|
|
isDirty: input.archiveHasUncommittedChanges,
|
|
aheadOfOrigin: input.archiveUnpushedCommitCount,
|
|
diffStat: input.diffStat,
|
|
};
|
|
}
|
|
|
|
function formatDiffStat(
|
|
diffStat: WorktreeArchiveRisk["diffStat"],
|
|
labels: WorktreeArchiveWarningLabels,
|
|
): string | null {
|
|
if (!diffStat) {
|
|
return null;
|
|
}
|
|
|
|
const parts: string[] = [];
|
|
if (diffStat.additions > 0) {
|
|
parts.push(labels.addedLine(diffStat.additions));
|
|
}
|
|
if (diffStat.deletions > 0) {
|
|
parts.push(labels.deletedLine(diffStat.deletions));
|
|
}
|
|
|
|
return parts.length > 0 ? parts.join(", ") : null;
|
|
}
|
|
|
|
export function buildWorktreeArchiveRiskReasons(
|
|
input: WorktreeArchiveRisk,
|
|
labels: WorktreeArchiveWarningLabels = DEFAULT_WORKTREE_ARCHIVE_WARNING_LABELS,
|
|
): string[] {
|
|
const reasons: string[] = [];
|
|
const diffStat = input.diffStat;
|
|
const hasDiffStatChanges = diffStat ? diffStat.additions > 0 || diffStat.deletions > 0 : false;
|
|
const hasUncommittedChanges =
|
|
input.isDirty === true || (input.isDirty == null && hasDiffStatChanges);
|
|
|
|
if (hasUncommittedChanges) {
|
|
const diffStatLabel = formatDiffStat(diffStat, labels);
|
|
reasons.push(
|
|
diffStatLabel ? labels.uncommittedChangesWithDiff(diffStatLabel) : labels.uncommittedChanges,
|
|
);
|
|
}
|
|
|
|
if ((input.aheadOfOrigin ?? 0) > 0) {
|
|
const aheadOfOrigin = input.aheadOfOrigin ?? 0;
|
|
reasons.push(labels.unpushedCommit(aheadOfOrigin));
|
|
}
|
|
|
|
return reasons;
|
|
}
|
|
|
|
export function buildWorktreeArchiveConfirmationMessage(
|
|
input: WorktreeArchiveConfirmationInput,
|
|
labels: WorktreeArchiveWarningLabels = DEFAULT_WORKTREE_ARCHIVE_WARNING_LABELS,
|
|
): string | null {
|
|
const reasons = buildWorktreeArchiveRiskReasons(input, labels);
|
|
if (reasons.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return reasons.join("\n");
|
|
}
|
|
|
|
export async function confirmRiskyWorktreeArchive(
|
|
input: WorktreeArchiveConfirmationInput,
|
|
labels: WorktreeArchiveWarningLabels = DEFAULT_WORKTREE_ARCHIVE_WARNING_LABELS,
|
|
): Promise<boolean> {
|
|
const message = buildWorktreeArchiveConfirmationMessage(input, labels);
|
|
if (!message) {
|
|
return true;
|
|
}
|
|
|
|
return await confirmDialog({
|
|
title: labels.title(input.workspaceName),
|
|
message,
|
|
confirmLabel: labels.confirm,
|
|
cancelLabel: labels.cancel,
|
|
destructive: true,
|
|
});
|
|
}
|