feat: add git stash management
Add a Stashes dialog with create, apply, pop, and drop actions Include untracked files automatically when stashing Show file counts for current changes and stash entries
This commit is contained in:
@@ -54,6 +54,7 @@ import { getSessionWorktreeRepairActions, getMutationBlockingReasons } from '@/s
|
|||||||
import { IntegrateCommitsSection } from './git/IntegrateCommitsSection';
|
import { IntegrateCommitsSection } from './git/IntegrateCommitsSection';
|
||||||
|
|
||||||
import { GitHeader } from './git/GitHeader';
|
import { GitHeader } from './git/GitHeader';
|
||||||
|
import { StashesDialog } from './git/StashesDialog';
|
||||||
import { ChangesSection } from './git/ChangesSection';
|
import { ChangesSection } from './git/ChangesSection';
|
||||||
import { CommitSection } from './git/CommitSection';
|
import { CommitSection } from './git/CommitSection';
|
||||||
import { GitEmptyState } from './git/GitEmptyState';
|
import { GitEmptyState } from './git/GitEmptyState';
|
||||||
@@ -429,6 +430,7 @@ export const GitView: React.FC = () => {
|
|||||||
const [isGitmojiPickerOpen, setIsGitmojiPickerOpen] = React.useState(false);
|
const [isGitmojiPickerOpen, setIsGitmojiPickerOpen] = React.useState(false);
|
||||||
const actionPanelScrollRef = React.useRef<HTMLElement | null>(null);
|
const actionPanelScrollRef = React.useRef<HTMLElement | null>(null);
|
||||||
const [syncAction, setSyncAction] = React.useState<SyncAction>(null);
|
const [syncAction, setSyncAction] = React.useState<SyncAction>(null);
|
||||||
|
const [isStashesDialogOpen, setIsStashesDialogOpen] = React.useState(false);
|
||||||
const [commitAction, setCommitAction] = React.useState<CommitAction>(null);
|
const [commitAction, setCommitAction] = React.useState<CommitAction>(null);
|
||||||
const [logMaxCountLocal, setLogMaxCountLocal] = React.useState<number>(25);
|
const [logMaxCountLocal, setLogMaxCountLocal] = React.useState<number>(25);
|
||||||
const [isSettingIdentity, setIsSettingIdentity] = React.useState(false);
|
const [isSettingIdentity, setIsSettingIdentity] = React.useState(false);
|
||||||
@@ -2069,6 +2071,7 @@ export const GitView: React.FC = () => {
|
|||||||
isApplyingIdentity={isSettingIdentity}
|
isApplyingIdentity={isSettingIdentity}
|
||||||
isWorktreeMode={!!worktreeMetadata}
|
isWorktreeMode={!!worktreeMetadata}
|
||||||
onOpenHistory={() => setIsHistoryDialogOpen(true)}
|
onOpenHistory={() => setIsHistoryDialogOpen(true)}
|
||||||
|
onOpenStashes={() => setIsStashesDialogOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* In-progress operation banner */}
|
{/* In-progress operation banner */}
|
||||||
@@ -2274,6 +2277,18 @@ export const GitView: React.FC = () => {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<StashesDialog
|
||||||
|
open={isStashesDialogOpen}
|
||||||
|
onOpenChange={setIsStashesDialogOpen}
|
||||||
|
directory={currentDirectory}
|
||||||
|
hasUncommittedChanges={(status?.files?.length ?? 0) > 0}
|
||||||
|
uncommittedFileCount={status?.files?.length ?? 0}
|
||||||
|
onChanged={async () => {
|
||||||
|
await refreshStatusAndBranches(false);
|
||||||
|
await refreshLog();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<Dialog open={isGitmojiPickerOpen} onOpenChange={setIsGitmojiPickerOpen}>
|
<Dialog open={isGitmojiPickerOpen} onOpenChange={setIsGitmojiPickerOpen}>
|
||||||
<DialogContent className="max-w-md p-0 overflow-hidden">
|
<DialogContent className="max-w-md p-0 overflow-hidden">
|
||||||
<DialogHeader className="px-4 pt-4">
|
<DialogHeader className="px-4 pt-4">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
RiCodeLine,
|
RiCodeLine,
|
||||||
RiHeartLine,
|
RiHeartLine,
|
||||||
RiHistoryLine,
|
RiHistoryLine,
|
||||||
|
RiArchiveStackLine,
|
||||||
RiUser3Line,
|
RiUser3Line,
|
||||||
} from '@remixicon/react';
|
} from '@remixicon/react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -48,6 +49,7 @@ interface GitHeaderProps {
|
|||||||
isApplyingIdentity: boolean;
|
isApplyingIdentity: boolean;
|
||||||
isWorktreeMode: boolean;
|
isWorktreeMode: boolean;
|
||||||
onOpenHistory?: () => void;
|
onOpenHistory?: () => void;
|
||||||
|
onOpenStashes?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IDENTITY_ICON_MAP: Record<
|
const IDENTITY_ICON_MAP: Record<
|
||||||
@@ -206,6 +208,7 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
|
|||||||
isApplyingIdentity,
|
isApplyingIdentity,
|
||||||
isWorktreeMode,
|
isWorktreeMode,
|
||||||
onOpenHistory,
|
onOpenHistory,
|
||||||
|
onOpenStashes,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
if (!status) {
|
if (!status) {
|
||||||
@@ -229,6 +232,16 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
|
|||||||
<TooltipContent sideOffset={8}>{t('gitView.history.title')}</TooltipContent>
|
<TooltipContent sideOffset={8}>{t('gitView.history.title')}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
|
{onOpenStashes ? (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm" className="h-8 w-8 px-0" onClick={onOpenStashes}>
|
||||||
|
<RiArchiveStackLine className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent sideOffset={8}>{t('gitView.stashes.title')}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { RiArchiveStackLine, RiDeleteBinLine, RiInboxArchiveLine, RiInboxUnarchiveFill, RiInboxUnarchiveLine, RiLoader4Line, RiSearchLine } from '@remixicon/react';
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { toast } from '@/components/ui';
|
||||||
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import type { GitStashEntry } from '@/lib/api/types';
|
||||||
|
import { applyGitStash, countGitStashFiles, dropGitStash, listGitStashes, popGitStash, stashGitChanges } from '@/lib/gitApi';
|
||||||
|
|
||||||
|
interface StashesDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
directory: string | null;
|
||||||
|
hasUncommittedChanges: boolean;
|
||||||
|
uncommittedFileCount: number;
|
||||||
|
onChanged?: () => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type StashOperation = 'create' | `apply:${string}` | `pop:${string}` | `drop:${string}` | null;
|
||||||
|
|
||||||
|
export const StashesDialog: React.FC<StashesDialogProps> = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
directory,
|
||||||
|
hasUncommittedChanges,
|
||||||
|
uncommittedFileCount,
|
||||||
|
onChanged,
|
||||||
|
}) => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const [stashes, setStashes] = React.useState<GitStashEntry[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
const [query, setQuery] = React.useState('');
|
||||||
|
const [message, setMessage] = React.useState('');
|
||||||
|
const [operation, setOperation] = React.useState<StashOperation>(null);
|
||||||
|
const [fileCounts, setFileCounts] = React.useState<Record<string, number>>({});
|
||||||
|
|
||||||
|
const load = React.useCallback(async () => {
|
||||||
|
if (!directory) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await listGitStashes(directory);
|
||||||
|
setStashes(result.stashes);
|
||||||
|
setFileCounts({});
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : t('gitView.stashes.toast.loadFailed'));
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [directory, t]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (open) void load();
|
||||||
|
}, [load, open]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!open || !directory || stashes.length === 0) return;
|
||||||
|
let cancelled = false;
|
||||||
|
const refs = stashes.map((stash) => stash.ref);
|
||||||
|
void countGitStashFiles(directory, refs).then((result) => {
|
||||||
|
if (!cancelled) setFileCounts(result.counts);
|
||||||
|
}).catch(() => undefined);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [directory, open, stashes]);
|
||||||
|
|
||||||
|
const filtered = React.useMemo(() => {
|
||||||
|
const normalized = query.trim().toLowerCase();
|
||||||
|
if (!normalized) return stashes;
|
||||||
|
return stashes.filter((stash) => `${stash.ref} ${stash.message} ${stash.relativeTime}`.toLowerCase().includes(normalized));
|
||||||
|
}, [query, stashes]);
|
||||||
|
|
||||||
|
const refreshAfterChange = React.useCallback(async () => {
|
||||||
|
await load();
|
||||||
|
await onChanged?.();
|
||||||
|
}, [load, onChanged]);
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
if (!directory || operation) return;
|
||||||
|
setOperation('create');
|
||||||
|
try {
|
||||||
|
const result = await stashGitChanges(directory, { message: message.trim() || undefined });
|
||||||
|
if (result.created) {
|
||||||
|
toast.success(t('gitView.stashes.toast.created'));
|
||||||
|
setMessage('');
|
||||||
|
} else {
|
||||||
|
toast.info(t('gitView.stashes.toast.noChanges'));
|
||||||
|
}
|
||||||
|
await refreshAfterChange();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : t('gitView.stashes.toast.createFailed'));
|
||||||
|
} finally {
|
||||||
|
setOperation(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const runStashAction = async (stash: GitStashEntry, kind: 'apply' | 'pop' | 'drop') => {
|
||||||
|
if (!directory || operation) return;
|
||||||
|
if (kind === 'drop' && !window.confirm(t('gitView.stashes.confirm.drop', { ref: stash.ref }))) return;
|
||||||
|
setOperation(`${kind}:${stash.ref}`);
|
||||||
|
try {
|
||||||
|
if (kind === 'apply') await applyGitStash(directory, { ref: stash.ref });
|
||||||
|
if (kind === 'pop') await popGitStash(directory, { ref: stash.ref });
|
||||||
|
if (kind === 'drop') await dropGitStash(directory, { ref: stash.ref });
|
||||||
|
const successKey = kind === 'apply' ? 'gitView.stashes.toast.applySuccess' : kind === 'pop' ? 'gitView.stashes.toast.popSuccess' : 'gitView.stashes.toast.dropSuccess';
|
||||||
|
toast.success(t(successKey));
|
||||||
|
await refreshAfterChange();
|
||||||
|
} catch (error) {
|
||||||
|
const failedKey = kind === 'apply' ? 'gitView.stashes.toast.applyFailed' : kind === 'pop' ? 'gitView.stashes.toast.popFailed' : 'gitView.stashes.toast.dropFailed';
|
||||||
|
toast.error(error instanceof Error ? error.message : t(failedKey));
|
||||||
|
await refreshAfterChange();
|
||||||
|
} finally {
|
||||||
|
setOperation(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOperating = operation !== null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[70vh] flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<RiArchiveStackLine className="h-5 w-5" />
|
||||||
|
{t('gitView.stashes.title')}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>{t('gitView.stashes.description')}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="mt-2 rounded-lg border border-border/60 bg-[var(--surface-elevated)] p-3">
|
||||||
|
<div className="flex flex-col gap-2 sm:flex-row">
|
||||||
|
<Input
|
||||||
|
value={message}
|
||||||
|
onChange={(event) => setMessage(event.target.value)}
|
||||||
|
placeholder={t('gitView.stashes.messagePlaceholder')}
|
||||||
|
disabled={isOperating || !hasUncommittedChanges}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button onClick={handleCreate} disabled={!hasUncommittedChanges || isOperating || !directory}>
|
||||||
|
{operation === 'create' ? <RiLoader4Line className="size-4 animate-spin" /> : <RiInboxArchiveLine className="size-4" />}
|
||||||
|
{t('gitView.stashes.actions.stashCurrentWithCount', { count: uncommittedFileCount })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="typography-meta mt-2 text-muted-foreground">{t('gitView.stashes.includeUntrackedHint')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative mt-2">
|
||||||
|
<RiSearchLine className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input value={query} onChange={(event) => setQuery(event.target.value)} placeholder={t('gitView.stashes.searchPlaceholder')} className="pl-9" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-10 text-muted-foreground"><RiLoader4Line className="size-5 animate-spin" /></div>
|
||||||
|
) : filtered.length === 0 ? (
|
||||||
|
<div className="py-8 text-center text-muted-foreground">{query ? t('gitView.stashes.empty.search') : t('gitView.stashes.empty.list')}</div>
|
||||||
|
) : filtered.map((stash, index) => (
|
||||||
|
<div key={stash.ref} className="group flex items-center gap-2 rounded py-1.5 transition-colors hover:bg-interactive-hover/30">
|
||||||
|
<div className="min-w-0 flex-1 pl-2">
|
||||||
|
<p className="typography-small truncate text-foreground">{stash.message || t('gitView.stashes.untitled')}</p>
|
||||||
|
<p className="typography-meta truncate text-muted-foreground">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span>{index === 0 && !query.trim() ? t('gitView.stashes.latestLabel') : t('gitView.stashes.itemNumber', { number: index + 1 })}</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent sideOffset={6}>{stash.ref}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
{' · '}{stash.relativeTime} · {typeof fileCounts[stash.ref] === 'number' ? t('gitView.stashes.fileCount', { count: fileCounts[stash.ref] }) : t('gitView.stashes.fileCountLoading')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mr-2 flex shrink-0 items-center gap-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100">
|
||||||
|
<StashIconButton label={t('gitView.stashes.actions.apply')} loading={operation === `apply:${stash.ref}`} onClick={() => runStashAction(stash, 'apply')} disabled={isOperating}><RiInboxUnarchiveLine className="size-4" /></StashIconButton>
|
||||||
|
<StashIconButton label={t('gitView.stashes.actions.pop')} loading={operation === `pop:${stash.ref}`} onClick={() => runStashAction(stash, 'pop')} disabled={isOperating}><RiInboxUnarchiveFill className="size-4" /></StashIconButton>
|
||||||
|
<StashIconButton label={t('gitView.stashes.actions.drop')} loading={operation === `drop:${stash.ref}`} onClick={() => runStashAction(stash, 'drop')} disabled={isOperating} destructive><RiDeleteBinLine className="size-4" /></StashIconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StashIconButton: React.FC<{ label: string; loading: boolean; disabled: boolean; destructive?: boolean; onClick: () => void; children: React.ReactNode }> = ({ label, loading, disabled, destructive, onClick, children }) => (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button type="button" className={cn('flex h-6 w-6 items-center justify-center transition-colors disabled:opacity-50', destructive ? 'text-[var(--status-error)] hover:text-[var(--status-error)]' : 'text-muted-foreground hover:text-foreground')} onClick={onClick} disabled={disabled}>
|
||||||
|
{loading ? <RiLoader4Line className="size-4 animate-spin" /> : children}
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent sideOffset={6}>{label}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
@@ -209,6 +209,13 @@ export interface GitPullOptions {
|
|||||||
rebase?: boolean;
|
rebase?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GitStashEntry {
|
||||||
|
ref: string;
|
||||||
|
message: string;
|
||||||
|
relativeTime: string;
|
||||||
|
hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GitRemote {
|
export interface GitRemote {
|
||||||
name: string;
|
name: string;
|
||||||
fetchUrl: string;
|
fetchUrl: string;
|
||||||
@@ -429,6 +436,12 @@ export interface GitAPI {
|
|||||||
gitPush(directory: string, options?: { remote?: string; branch?: string; options?: string[] | Record<string, unknown> }): Promise<GitPushResult>;
|
gitPush(directory: string, options?: { remote?: string; branch?: string; options?: string[] | Record<string, unknown> }): Promise<GitPushResult>;
|
||||||
gitPull(directory: string, options?: GitPullOptions): Promise<GitPullResult>;
|
gitPull(directory: string, options?: GitPullOptions): Promise<GitPullResult>;
|
||||||
gitFetch(directory: string, options?: { remote?: string; branch?: string }): Promise<{ success: boolean }>;
|
gitFetch(directory: string, options?: { remote?: string; branch?: string }): Promise<{ success: boolean }>;
|
||||||
|
listGitStashes(directory: string): Promise<{ stashes: GitStashEntry[] }>;
|
||||||
|
countGitStashFiles(directory: string, refs: string[]): Promise<{ counts: Record<string, number> }>;
|
||||||
|
stashGitChanges(directory: string, options?: { message?: string }): Promise<{ success: boolean; created: boolean; message: string; output: string }>;
|
||||||
|
applyGitStash(directory: string, options: { ref: string }): Promise<{ success: boolean; ref: string }>;
|
||||||
|
popGitStash(directory: string, options: { ref: string }): Promise<{ success: boolean; ref: string }>;
|
||||||
|
dropGitStash(directory: string, options: { ref: string }): Promise<{ success: boolean; ref: string }>;
|
||||||
checkoutBranch(directory: string, branch: string): Promise<{ success: boolean; branch: string }>;
|
checkoutBranch(directory: string, branch: string): Promise<{ success: boolean; branch: string }>;
|
||||||
createBranch(directory: string, name: string, startPoint?: string): Promise<{ success: boolean; branch: string }>;
|
createBranch(directory: string, name: string, startPoint?: string): Promise<{ success: boolean; branch: string }>;
|
||||||
renameBranch(directory: string, oldName: string, newName: string): Promise<{ success: boolean; branch: string }>;
|
renameBranch(directory: string, oldName: string, newName: string): Promise<{ success: boolean; branch: string }>;
|
||||||
|
|||||||
@@ -546,6 +546,42 @@ export async function gitFetch(
|
|||||||
return gitHttp.gitFetch(directory, options);
|
return gitHttp.gitFetch(directory, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listGitStashes(directory: string): Promise<{ stashes: import('./api/types').GitStashEntry[] }> {
|
||||||
|
const runtime = getRuntimeGit();
|
||||||
|
if (runtime) return runtime.listGitStashes(directory);
|
||||||
|
return gitHttp.listGitStashes(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function countGitStashFiles(directory: string, refs: string[]): Promise<{ counts: Record<string, number> }> {
|
||||||
|
const runtime = getRuntimeGit();
|
||||||
|
if (runtime) return runtime.countGitStashFiles(directory, refs);
|
||||||
|
return gitHttp.countGitStashFiles(directory, refs);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stashGitChanges(directory: string, options: { message?: string } = {}): Promise<{ success: boolean; created: boolean; message: string; output: string }> {
|
||||||
|
const runtime = getRuntimeGit();
|
||||||
|
if (runtime) return runtime.stashGitChanges(directory, options);
|
||||||
|
return gitHttp.stashGitChanges(directory, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyGitStash(directory: string, options: { ref: string }): Promise<{ success: boolean; ref: string }> {
|
||||||
|
const runtime = getRuntimeGit();
|
||||||
|
if (runtime) return runtime.applyGitStash(directory, options);
|
||||||
|
return gitHttp.applyGitStash(directory, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function popGitStash(directory: string, options: { ref: string }): Promise<{ success: boolean; ref: string }> {
|
||||||
|
const runtime = getRuntimeGit();
|
||||||
|
if (runtime) return runtime.popGitStash(directory, options);
|
||||||
|
return gitHttp.popGitStash(directory, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dropGitStash(directory: string, options: { ref: string }): Promise<{ success: boolean; ref: string }> {
|
||||||
|
const runtime = getRuntimeGit();
|
||||||
|
if (runtime) return runtime.dropGitStash(directory, options);
|
||||||
|
return gitHttp.dropGitStash(directory, options);
|
||||||
|
}
|
||||||
|
|
||||||
export async function checkoutBranch(directory: string, branch: string): Promise<{ success: boolean; branch: string }> {
|
export async function checkoutBranch(directory: string, branch: string): Promise<{ success: boolean; branch: string }> {
|
||||||
const runtime = getRuntimeGit();
|
const runtime = getRuntimeGit();
|
||||||
if (runtime) return runtime.checkoutBranch(directory, branch);
|
if (runtime) return runtime.checkoutBranch(directory, branch);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type {
|
|||||||
GitPushResult,
|
GitPushResult,
|
||||||
GitPullResult,
|
GitPullResult,
|
||||||
GitPullOptions,
|
GitPullOptions,
|
||||||
|
GitStashEntry,
|
||||||
GitLogOptions,
|
GitLogOptions,
|
||||||
GitLogResponse,
|
GitLogResponse,
|
||||||
GitCommitFilesResponse,
|
GitCommitFilesResponse,
|
||||||
@@ -549,6 +550,58 @@ export async function gitFetch(
|
|||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listGitStashes(directory: string): Promise<{ stashes: GitStashEntry[] }> {
|
||||||
|
const response = await fetch(buildUrl(`${API_BASE}/stashes`, directory));
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ error: response.statusText }));
|
||||||
|
throw new Error(error.error || 'Failed to list stashes');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function countGitStashFiles(directory: string, refs: string[]): Promise<{ counts: Record<string, number> }> {
|
||||||
|
const response = await fetch(buildUrl(`${API_BASE}/stashes/file-counts`, directory), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ refs }),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ error: response.statusText }));
|
||||||
|
throw new Error(error.error || 'Failed to count stash files');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stashGitChanges(directory: string, options: { message?: string } = {}): Promise<{ success: boolean; created: boolean; message: string; output: string }> {
|
||||||
|
const response = await fetch(buildUrl(`${API_BASE}/stash`, directory), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(options),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ error: response.statusText }));
|
||||||
|
throw new Error(error.error || 'Failed to stash changes');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
const postStashRef = async (directory: string, path: string, options: { ref: string }): Promise<{ success: boolean; ref: string }> => {
|
||||||
|
const response = await fetch(buildUrl(`${API_BASE}/${path}`, directory), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(options),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ error: response.statusText }));
|
||||||
|
throw new Error(error.error || `Failed to ${path}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyGitStash = (directory: string, options: { ref: string }) => postStashRef(directory, 'stash/apply', options);
|
||||||
|
export const popGitStash = (directory: string, options: { ref: string }) => postStashRef(directory, 'stash/pop', options);
|
||||||
|
export const dropGitStash = (directory: string, options: { ref: string }) => postStashRef(directory, 'stash/drop', options);
|
||||||
|
|
||||||
export async function checkoutBranch(directory: string, branch: string): Promise<{ success: boolean; branch: string }> {
|
export async function checkoutBranch(directory: string, branch: string): Promise<{ success: boolean; branch: string }> {
|
||||||
const response = await fetch(buildUrl(`${API_BASE}/checkout`, directory), {
|
const response = await fetch(buildUrl(`${API_BASE}/checkout`, directory), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -842,27 +895,13 @@ export async function stash(
|
|||||||
directory: string,
|
directory: string,
|
||||||
options?: { message?: string; includeUntracked?: boolean }
|
options?: { message?: string; includeUntracked?: boolean }
|
||||||
): Promise<{ success: boolean }> {
|
): Promise<{ success: boolean }> {
|
||||||
const response = await fetch(buildUrl(`${API_BASE}/stash`, directory), {
|
await stashGitChanges(directory, { message: options?.message });
|
||||||
method: 'POST',
|
return { success: true };
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(options || {}),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json().catch(() => ({ error: response.statusText }));
|
|
||||||
throw new Error(error.error || 'Failed to stash');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stashPop(directory: string): Promise<{ success: boolean }> {
|
export async function stashPop(directory: string): Promise<{ success: boolean }> {
|
||||||
const response = await fetch(buildUrl(`${API_BASE}/stash/pop`, directory), {
|
await popGitStash(directory, { ref: 'stash@{0}' });
|
||||||
method: 'POST',
|
return { success: true };
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json().catch(() => ({ error: response.statusText }));
|
|
||||||
throw new Error(error.error || 'Failed to pop stash');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getConflictDetails(directory: string): Promise<MergeConflictDetails> {
|
export async function getConflictDetails(directory: string): Promise<MergeConflictDetails> {
|
||||||
|
|||||||
@@ -517,6 +517,34 @@ export const dict = {
|
|||||||
'gitView.stash.stepStash': 'Stash your uncommitted changes',
|
'gitView.stash.stepStash': 'Stash your uncommitted changes',
|
||||||
'gitView.stash.thisWill': 'This will:',
|
'gitView.stash.thisWill': 'This will:',
|
||||||
'gitView.stash.title': 'Uncommitted Changes',
|
'gitView.stash.title': 'Uncommitted Changes',
|
||||||
|
'gitView.stashes.actions.apply': 'Apply',
|
||||||
|
'gitView.stashes.actions.drop': 'Drop',
|
||||||
|
'gitView.stashes.actions.pop': 'Pop',
|
||||||
|
'gitView.stashes.actions.stashCurrent': 'Stash current changes',
|
||||||
|
'gitView.stashes.actions.stashCurrentWithCount': 'Stash {count} files',
|
||||||
|
'gitView.stashes.confirm.drop': 'Drop {ref}?',
|
||||||
|
'gitView.stashes.description': 'Save, restore, and clean up Git stashes for this repository.',
|
||||||
|
'gitView.stashes.empty.list': 'No stashes yet.',
|
||||||
|
'gitView.stashes.empty.search': 'No matching stashes.',
|
||||||
|
'gitView.stashes.includeUntrackedHint': 'Untracked files are included automatically.',
|
||||||
|
'gitView.stashes.fileCount': '{count} files',
|
||||||
|
'gitView.stashes.fileCountLoading': 'counting files...',
|
||||||
|
'gitView.stashes.itemNumber': '#{number}',
|
||||||
|
'gitView.stashes.latestLabel': 'Latest',
|
||||||
|
'gitView.stashes.messagePlaceholder': 'Stash name',
|
||||||
|
'gitView.stashes.searchPlaceholder': 'Search stashes',
|
||||||
|
'gitView.stashes.title': 'Stashes',
|
||||||
|
'gitView.stashes.toast.applyFailed': 'Failed to apply stash',
|
||||||
|
'gitView.stashes.toast.applySuccess': 'Stash applied',
|
||||||
|
'gitView.stashes.toast.createFailed': 'Failed to stash changes',
|
||||||
|
'gitView.stashes.toast.created': 'Changes stashed',
|
||||||
|
'gitView.stashes.toast.dropFailed': 'Failed to drop stash',
|
||||||
|
'gitView.stashes.toast.dropSuccess': 'Stash dropped',
|
||||||
|
'gitView.stashes.toast.loadFailed': 'Failed to load stashes',
|
||||||
|
'gitView.stashes.toast.noChanges': 'No local changes to stash',
|
||||||
|
'gitView.stashes.toast.popFailed': 'Failed to pop stash',
|
||||||
|
'gitView.stashes.toast.popSuccess': 'Stash popped',
|
||||||
|
'gitView.stashes.untitled': 'Untitled stash',
|
||||||
'gitView.sync.fetch': 'Fetch',
|
'gitView.sync.fetch': 'Fetch',
|
||||||
'gitView.sync.fetchFromRemote': 'Fetch from {name}',
|
'gitView.sync.fetchFromRemote': 'Fetch from {name}',
|
||||||
'gitView.sync.fetchTooltip': 'Fetch from remote',
|
'gitView.sync.fetchTooltip': 'Fetch from remote',
|
||||||
|
|||||||
@@ -518,6 +518,34 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"gitView.stash.stepStash": "Guarda tus cambios sin commit",
|
"gitView.stash.stepStash": "Guarda tus cambios sin commit",
|
||||||
"gitView.stash.thisWill": "Esto hará:",
|
"gitView.stash.thisWill": "Esto hará:",
|
||||||
"gitView.stash.title": "Cambios sin commit",
|
"gitView.stash.title": "Cambios sin commit",
|
||||||
|
"gitView.stashes.actions.apply": "Apply",
|
||||||
|
"gitView.stashes.actions.drop": "Drop",
|
||||||
|
"gitView.stashes.actions.pop": "Pop",
|
||||||
|
"gitView.stashes.actions.stashCurrent": "Stash current changes",
|
||||||
|
"gitView.stashes.actions.stashCurrentWithCount": "Stash {count} files",
|
||||||
|
"gitView.stashes.confirm.drop": "Drop {ref}?",
|
||||||
|
"gitView.stashes.description": "Save, restore, and clean up Git stashes for this repository.",
|
||||||
|
"gitView.stashes.empty.list": "No stashes yet.",
|
||||||
|
"gitView.stashes.empty.search": "No matching stashes.",
|
||||||
|
"gitView.stashes.includeUntrackedHint": "Untracked files are included automatically.",
|
||||||
|
"gitView.stashes.fileCount": "{count} files",
|
||||||
|
"gitView.stashes.fileCountLoading": "counting files...",
|
||||||
|
"gitView.stashes.itemNumber": "#{number}",
|
||||||
|
"gitView.stashes.latestLabel": "Latest",
|
||||||
|
"gitView.stashes.messagePlaceholder": "Stash name",
|
||||||
|
"gitView.stashes.searchPlaceholder": "Search stashes",
|
||||||
|
"gitView.stashes.title": "Stashes",
|
||||||
|
"gitView.stashes.toast.applyFailed": "Failed to apply stash",
|
||||||
|
"gitView.stashes.toast.applySuccess": "Stash applied",
|
||||||
|
"gitView.stashes.toast.createFailed": "Failed to stash changes",
|
||||||
|
"gitView.stashes.toast.created": "Changes stashed",
|
||||||
|
"gitView.stashes.toast.dropFailed": "Failed to drop stash",
|
||||||
|
"gitView.stashes.toast.dropSuccess": "Stash dropped",
|
||||||
|
"gitView.stashes.toast.loadFailed": "Failed to load stashes",
|
||||||
|
"gitView.stashes.toast.noChanges": "No local changes to stash",
|
||||||
|
"gitView.stashes.toast.popFailed": "Failed to pop stash",
|
||||||
|
"gitView.stashes.toast.popSuccess": "Stash popped",
|
||||||
|
"gitView.stashes.untitled": "Untitled stash",
|
||||||
"gitView.sync.fetch": "Fetch",
|
"gitView.sync.fetch": "Fetch",
|
||||||
"gitView.sync.fetchFromRemote": "Fetch de {name}",
|
"gitView.sync.fetchFromRemote": "Fetch de {name}",
|
||||||
"gitView.sync.fetchTooltip": "Fetch del remoto",
|
"gitView.sync.fetchTooltip": "Fetch del remoto",
|
||||||
|
|||||||
@@ -518,6 +518,34 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'gitView.stash.stepStash': '커밋하지 않은 변경 사항 stash',
|
'gitView.stash.stepStash': '커밋하지 않은 변경 사항 stash',
|
||||||
'gitView.stash.thisWill': '다음 작업을 수행합니다:',
|
'gitView.stash.thisWill': '다음 작업을 수행합니다:',
|
||||||
'gitView.stash.title': '커밋하지 않은 변경 사항',
|
'gitView.stash.title': '커밋하지 않은 변경 사항',
|
||||||
|
'gitView.stashes.actions.apply': 'Apply',
|
||||||
|
'gitView.stashes.actions.drop': 'Drop',
|
||||||
|
'gitView.stashes.actions.pop': 'Pop',
|
||||||
|
'gitView.stashes.actions.stashCurrent': 'Stash current changes',
|
||||||
|
'gitView.stashes.actions.stashCurrentWithCount': 'Stash {count} files',
|
||||||
|
'gitView.stashes.confirm.drop': 'Drop {ref}?',
|
||||||
|
'gitView.stashes.description': 'Save, restore, and clean up Git stashes for this repository.',
|
||||||
|
'gitView.stashes.empty.list': 'No stashes yet.',
|
||||||
|
'gitView.stashes.empty.search': 'No matching stashes.',
|
||||||
|
'gitView.stashes.includeUntrackedHint': 'Untracked files are included automatically.',
|
||||||
|
'gitView.stashes.fileCount': '{count} files',
|
||||||
|
'gitView.stashes.fileCountLoading': 'counting files...',
|
||||||
|
'gitView.stashes.itemNumber': '#{number}',
|
||||||
|
'gitView.stashes.latestLabel': 'Latest',
|
||||||
|
'gitView.stashes.messagePlaceholder': 'Stash name',
|
||||||
|
'gitView.stashes.searchPlaceholder': 'Search stashes',
|
||||||
|
'gitView.stashes.title': 'Stashes',
|
||||||
|
'gitView.stashes.toast.applyFailed': 'Failed to apply stash',
|
||||||
|
'gitView.stashes.toast.applySuccess': 'Stash applied',
|
||||||
|
'gitView.stashes.toast.createFailed': 'Failed to stash changes',
|
||||||
|
'gitView.stashes.toast.created': 'Changes stashed',
|
||||||
|
'gitView.stashes.toast.dropFailed': 'Failed to drop stash',
|
||||||
|
'gitView.stashes.toast.dropSuccess': 'Stash dropped',
|
||||||
|
'gitView.stashes.toast.loadFailed': 'Failed to load stashes',
|
||||||
|
'gitView.stashes.toast.noChanges': 'No local changes to stash',
|
||||||
|
'gitView.stashes.toast.popFailed': 'Failed to pop stash',
|
||||||
|
'gitView.stashes.toast.popSuccess': 'Stash popped',
|
||||||
|
'gitView.stashes.untitled': 'Untitled stash',
|
||||||
'gitView.sync.fetch': '가져오기',
|
'gitView.sync.fetch': '가져오기',
|
||||||
'gitView.sync.fetchFromRemote': '{name}에서 가져오기',
|
'gitView.sync.fetchFromRemote': '{name}에서 가져오기',
|
||||||
'gitView.sync.fetchTooltip': '리모트에서 가져오기',
|
'gitView.sync.fetchTooltip': '리모트에서 가져오기',
|
||||||
|
|||||||
@@ -518,6 +518,34 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"gitView.stash.stepStash": "Salva suas alterações sem commit",
|
"gitView.stash.stepStash": "Salva suas alterações sem commit",
|
||||||
"gitView.stash.thisWill": "Isso fará:",
|
"gitView.stash.thisWill": "Isso fará:",
|
||||||
"gitView.stash.title": "Alterações sem commit",
|
"gitView.stash.title": "Alterações sem commit",
|
||||||
|
"gitView.stashes.actions.apply": "Apply",
|
||||||
|
"gitView.stashes.actions.drop": "Drop",
|
||||||
|
"gitView.stashes.actions.pop": "Pop",
|
||||||
|
"gitView.stashes.actions.stashCurrent": "Stash current changes",
|
||||||
|
"gitView.stashes.actions.stashCurrentWithCount": "Stash {count} files",
|
||||||
|
"gitView.stashes.confirm.drop": "Drop {ref}?",
|
||||||
|
"gitView.stashes.description": "Save, restore, and clean up Git stashes for this repository.",
|
||||||
|
"gitView.stashes.empty.list": "No stashes yet.",
|
||||||
|
"gitView.stashes.empty.search": "No matching stashes.",
|
||||||
|
"gitView.stashes.includeUntrackedHint": "Untracked files are included automatically.",
|
||||||
|
"gitView.stashes.fileCount": "{count} files",
|
||||||
|
"gitView.stashes.fileCountLoading": "counting files...",
|
||||||
|
"gitView.stashes.itemNumber": "#{number}",
|
||||||
|
"gitView.stashes.latestLabel": "Latest",
|
||||||
|
"gitView.stashes.messagePlaceholder": "Stash name",
|
||||||
|
"gitView.stashes.searchPlaceholder": "Search stashes",
|
||||||
|
"gitView.stashes.title": "Stashes",
|
||||||
|
"gitView.stashes.toast.applyFailed": "Failed to apply stash",
|
||||||
|
"gitView.stashes.toast.applySuccess": "Stash applied",
|
||||||
|
"gitView.stashes.toast.createFailed": "Failed to stash changes",
|
||||||
|
"gitView.stashes.toast.created": "Changes stashed",
|
||||||
|
"gitView.stashes.toast.dropFailed": "Failed to drop stash",
|
||||||
|
"gitView.stashes.toast.dropSuccess": "Stash dropped",
|
||||||
|
"gitView.stashes.toast.loadFailed": "Failed to load stashes",
|
||||||
|
"gitView.stashes.toast.noChanges": "No local changes to stash",
|
||||||
|
"gitView.stashes.toast.popFailed": "Failed to pop stash",
|
||||||
|
"gitView.stashes.toast.popSuccess": "Stash popped",
|
||||||
|
"gitView.stashes.untitled": "Untitled stash",
|
||||||
"gitView.sync.fetch": "Fetch",
|
"gitView.sync.fetch": "Fetch",
|
||||||
"gitView.sync.fetchFromRemote": "Fetch de {name}",
|
"gitView.sync.fetchFromRemote": "Fetch de {name}",
|
||||||
"gitView.sync.fetchTooltip": "Fetch do remoto",
|
"gitView.sync.fetchTooltip": "Fetch do remoto",
|
||||||
|
|||||||
@@ -518,6 +518,34 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"gitView.stash.stepStash": "Сховати незакомічені зміни",
|
"gitView.stash.stepStash": "Сховати незакомічені зміни",
|
||||||
"gitView.stash.thisWill": "Це:",
|
"gitView.stash.thisWill": "Це:",
|
||||||
"gitView.stash.title": "Незакомічені зміни",
|
"gitView.stash.title": "Незакомічені зміни",
|
||||||
|
"gitView.stashes.actions.apply": "Apply",
|
||||||
|
"gitView.stashes.actions.drop": "Drop",
|
||||||
|
"gitView.stashes.actions.pop": "Pop",
|
||||||
|
"gitView.stashes.actions.stashCurrent": "Сховати поточні зміни",
|
||||||
|
"gitView.stashes.actions.stashCurrentWithCount": "Сховати файлів: {count}",
|
||||||
|
"gitView.stashes.confirm.drop": "Видалити {ref}?",
|
||||||
|
"gitView.stashes.description": "Зберігайте, відновлюйте й очищайте Git stashes для цього репозиторію.",
|
||||||
|
"gitView.stashes.empty.list": "Stashes ще немає.",
|
||||||
|
"gitView.stashes.empty.search": "Збігів не знайдено.",
|
||||||
|
"gitView.stashes.includeUntrackedHint": "Untracked файли додаються автоматично.",
|
||||||
|
"gitView.stashes.fileCount": "Файлів: {count}",
|
||||||
|
"gitView.stashes.fileCountLoading": "рахуємо файли...",
|
||||||
|
"gitView.stashes.itemNumber": "#{number}",
|
||||||
|
"gitView.stashes.latestLabel": "Останній",
|
||||||
|
"gitView.stashes.messagePlaceholder": "Назва stash",
|
||||||
|
"gitView.stashes.searchPlaceholder": "Пошук stashes",
|
||||||
|
"gitView.stashes.title": "Stashes",
|
||||||
|
"gitView.stashes.toast.applyFailed": "Не вдалося застосувати stash",
|
||||||
|
"gitView.stashes.toast.applySuccess": "Stash застосовано",
|
||||||
|
"gitView.stashes.toast.createFailed": "Не вдалося сховати зміни",
|
||||||
|
"gitView.stashes.toast.created": "Зміни сховано",
|
||||||
|
"gitView.stashes.toast.dropFailed": "Не вдалося видалити stash",
|
||||||
|
"gitView.stashes.toast.dropSuccess": "Stash видалено",
|
||||||
|
"gitView.stashes.toast.loadFailed": "Не вдалося завантажити stashes",
|
||||||
|
"gitView.stashes.toast.noChanges": "Немає локальних змін для stash",
|
||||||
|
"gitView.stashes.toast.popFailed": "Не вдалося pop stash",
|
||||||
|
"gitView.stashes.toast.popSuccess": "Stash popped",
|
||||||
|
"gitView.stashes.untitled": "Stash без назви",
|
||||||
"gitView.sync.fetch": "Fetch",
|
"gitView.sync.fetch": "Fetch",
|
||||||
"gitView.sync.fetchFromRemote": "Fetch з {name}",
|
"gitView.sync.fetchFromRemote": "Fetch з {name}",
|
||||||
"gitView.sync.fetchTooltip": "Отримати з віддаленого",
|
"gitView.sync.fetchTooltip": "Отримати з віддаленого",
|
||||||
|
|||||||
@@ -518,6 +518,34 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'gitView.stash.stepStash': '储藏未提交的更改',
|
'gitView.stash.stepStash': '储藏未提交的更改',
|
||||||
'gitView.stash.thisWill': '这将会:',
|
'gitView.stash.thisWill': '这将会:',
|
||||||
'gitView.stash.title': '未提交的更改',
|
'gitView.stash.title': '未提交的更改',
|
||||||
|
'gitView.stashes.actions.apply': 'Apply',
|
||||||
|
'gitView.stashes.actions.drop': 'Drop',
|
||||||
|
'gitView.stashes.actions.pop': 'Pop',
|
||||||
|
'gitView.stashes.actions.stashCurrent': 'Stash current changes',
|
||||||
|
'gitView.stashes.actions.stashCurrentWithCount': 'Stash {count} files',
|
||||||
|
'gitView.stashes.confirm.drop': 'Drop {ref}?',
|
||||||
|
'gitView.stashes.description': 'Save, restore, and clean up Git stashes for this repository.',
|
||||||
|
'gitView.stashes.empty.list': 'No stashes yet.',
|
||||||
|
'gitView.stashes.empty.search': 'No matching stashes.',
|
||||||
|
'gitView.stashes.includeUntrackedHint': 'Untracked files are included automatically.',
|
||||||
|
'gitView.stashes.fileCount': '{count} files',
|
||||||
|
'gitView.stashes.fileCountLoading': 'counting files...',
|
||||||
|
'gitView.stashes.itemNumber': '#{number}',
|
||||||
|
'gitView.stashes.latestLabel': 'Latest',
|
||||||
|
'gitView.stashes.messagePlaceholder': 'Stash name',
|
||||||
|
'gitView.stashes.searchPlaceholder': 'Search stashes',
|
||||||
|
'gitView.stashes.title': 'Stashes',
|
||||||
|
'gitView.stashes.toast.applyFailed': 'Failed to apply stash',
|
||||||
|
'gitView.stashes.toast.applySuccess': 'Stash applied',
|
||||||
|
'gitView.stashes.toast.createFailed': 'Failed to stash changes',
|
||||||
|
'gitView.stashes.toast.created': 'Changes stashed',
|
||||||
|
'gitView.stashes.toast.dropFailed': 'Failed to drop stash',
|
||||||
|
'gitView.stashes.toast.dropSuccess': 'Stash dropped',
|
||||||
|
'gitView.stashes.toast.loadFailed': 'Failed to load stashes',
|
||||||
|
'gitView.stashes.toast.noChanges': 'No local changes to stash',
|
||||||
|
'gitView.stashes.toast.popFailed': 'Failed to pop stash',
|
||||||
|
'gitView.stashes.toast.popSuccess': 'Stash popped',
|
||||||
|
'gitView.stashes.untitled': 'Untitled stash',
|
||||||
'gitView.sync.fetch': '获取',
|
'gitView.sync.fetch': '获取',
|
||||||
'gitView.sync.fetchFromRemote': '从 {name} 获取',
|
'gitView.sync.fetchFromRemote': '从 {name} 获取',
|
||||||
'gitView.sync.fetchTooltip': '从远程获取',
|
'gitView.sync.fetchTooltip': '从远程获取',
|
||||||
|
|||||||
@@ -277,6 +277,42 @@ export async function handleStandardGitBridgeMessage(message: BridgeMessageInput
|
|||||||
return { id, type, success: true, data: result };
|
return { id, type, success: true, data: result };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'api:git/stashes': {
|
||||||
|
const { directory } = (payload || {}) as { directory?: string };
|
||||||
|
const dirError = requireDirectory(id, type, directory);
|
||||||
|
if (dirError) return dirError;
|
||||||
|
return { id, type, success: true, data: { stashes: await gitService.listGitStashes(directory!) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'api:git/stashes/file-counts': {
|
||||||
|
const { directory, refs } = (payload || {}) as { directory?: string; refs?: string[] };
|
||||||
|
const dirError = requireDirectory(id, type, directory);
|
||||||
|
if (dirError) return dirError;
|
||||||
|
return { id, type, success: true, data: { counts: await gitService.countGitStashFiles(directory!, refs ?? []) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'api:git/stash': {
|
||||||
|
const { directory, message } = (payload || {}) as { directory?: string; message?: string };
|
||||||
|
const dirError = requireDirectory(id, type, directory);
|
||||||
|
if (dirError) return dirError;
|
||||||
|
return { id, type, success: true, data: await gitService.stashGitChanges(directory!, { message }) };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'api:git/stash/apply':
|
||||||
|
case 'api:git/stash/pop':
|
||||||
|
case 'api:git/stash/drop': {
|
||||||
|
const { directory, ref } = (payload || {}) as { directory?: string; ref?: string };
|
||||||
|
const dirError = requireDirectory(id, type, directory);
|
||||||
|
if (dirError) return dirError;
|
||||||
|
const stashRef = ref || 'stash@{0}';
|
||||||
|
const data = type === 'api:git/stash/apply'
|
||||||
|
? await gitService.applyGitStash(directory!, { ref: stashRef })
|
||||||
|
: type === 'api:git/stash/pop'
|
||||||
|
? await gitService.popGitStash(directory!, { ref: stashRef })
|
||||||
|
: await gitService.dropGitStash(directory!, { ref: stashRef });
|
||||||
|
return { id, type, success: true, data };
|
||||||
|
}
|
||||||
|
|
||||||
case 'api:git/remotes': {
|
case 'api:git/remotes': {
|
||||||
const { directory, method, remote } = (payload || {}) as {
|
const { directory, method, remote } = (payload || {}) as {
|
||||||
directory?: string;
|
directory?: string;
|
||||||
@@ -357,26 +393,6 @@ export async function handleStandardGitBridgeMessage(message: BridgeMessageInput
|
|||||||
return { id, type, success: true, data: result };
|
return { id, type, success: true, data: result };
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'api:git/stash': {
|
|
||||||
const { directory, message, includeUntracked } = (payload || {}) as {
|
|
||||||
directory?: string;
|
|
||||||
message?: string;
|
|
||||||
includeUntracked?: boolean;
|
|
||||||
};
|
|
||||||
const dirError = requireDirectory(id, type, directory);
|
|
||||||
if (dirError) return dirError;
|
|
||||||
const result = await gitService.stash(directory!, { message, includeUntracked });
|
|
||||||
return { id, type, success: true, data: result };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'api:git/stash/pop': {
|
|
||||||
const { directory } = (payload || {}) as { directory?: string };
|
|
||||||
const dirError = requireDirectory(id, type, directory);
|
|
||||||
if (dirError) return dirError;
|
|
||||||
const result = await gitService.stashPop(directory!);
|
|
||||||
return { id, type, success: true, data: result };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'api:git/log': {
|
case 'api:git/log': {
|
||||||
const { directory, maxCount, from, to, file } = (payload || {}) as {
|
const { directory, maxCount, from, to, file } = (payload || {}) as {
|
||||||
directory?: string;
|
directory?: string;
|
||||||
|
|||||||
@@ -2399,6 +2399,63 @@ export async function gitPull(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listGitStashes(directory: string): Promise<Array<{ ref: string; message: string; relativeTime: string; hash: string }>> {
|
||||||
|
const result = await execGit(['stash', 'list', '--format=%gd%x1f%gs%x1f%cr%x1f%H'], directory);
|
||||||
|
if (result.exitCode !== 0) throw new Error(result.stderr.trim() || 'Failed to list stashes');
|
||||||
|
return result.stdout.split('\n').map((line) => line.trim()).filter(Boolean).map((line) => {
|
||||||
|
const [ref = '', message = '', relativeTime = '', hash = ''] = line.split('\x1f');
|
||||||
|
return { ref, message, relativeTime, hash };
|
||||||
|
}).filter((entry) => entry.ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function countGitStashFiles(directory: string, refs: string[]): Promise<Record<string, number>> {
|
||||||
|
const uniqueRefs = Array.from(new Set(refs.map((ref) => String(ref || '').trim()).filter(Boolean)));
|
||||||
|
const counts: Record<string, number> = {};
|
||||||
|
const concurrency = 4;
|
||||||
|
let cursor = 0;
|
||||||
|
|
||||||
|
const worker = async () => {
|
||||||
|
while (cursor < uniqueRefs.length) {
|
||||||
|
const ref = uniqueRefs[cursor++];
|
||||||
|
if (!ref) continue;
|
||||||
|
const names = await execGit(['stash', 'show', '--name-only', ref], directory);
|
||||||
|
counts[ref] = names.exitCode === 0 ? names.stdout.split('\n').map((line) => line.trim()).filter(Boolean).length : 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(Array.from({ length: Math.min(concurrency, uniqueRefs.length) }, () => worker()));
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stashGitChanges(directory: string, options: { message?: string } = {}): Promise<{ success: boolean; created: boolean; message: string; output: string }> {
|
||||||
|
const message = options.message?.trim() || `OpenChamber stash ${new Date().toISOString()}`;
|
||||||
|
const result = await execGit(['stash', 'push', '--include-untracked', '-m', message], directory);
|
||||||
|
if (result.exitCode !== 0) throw new Error(result.stderr.trim() || 'Failed to stash changes');
|
||||||
|
const output = result.stdout.trim() || result.stderr.trim();
|
||||||
|
return { success: true, created: !/no local changes/i.test(output), message, output };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyGitStash(directory: string, options: { ref: string }): Promise<{ success: boolean; ref: string }> {
|
||||||
|
const ref = options.ref || 'stash@{0}';
|
||||||
|
const result = await execGit(['stash', 'apply', ref], directory);
|
||||||
|
if (result.exitCode !== 0) throw new Error(result.stderr.trim() || result.stdout.trim() || 'Failed to apply stash');
|
||||||
|
return { success: true, ref };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dropGitStash(directory: string, options: { ref: string }): Promise<{ success: boolean; ref: string }> {
|
||||||
|
const ref = options.ref || 'stash@{0}';
|
||||||
|
const result = await execGit(['stash', 'drop', ref], directory);
|
||||||
|
if (result.exitCode !== 0) throw new Error(result.stderr.trim() || result.stdout.trim() || 'Failed to drop stash');
|
||||||
|
return { success: true, ref };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function popGitStash(directory: string, options: { ref: string }): Promise<{ success: boolean; ref: string }> {
|
||||||
|
const ref = options.ref || 'stash@{0}';
|
||||||
|
await applyGitStash(directory, { ref });
|
||||||
|
await dropGitStash(directory, { ref });
|
||||||
|
return { success: true, ref };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch from remote
|
* Fetch from remote
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -210,6 +210,13 @@ export const createVSCodeGitAPI = (): GitAPI => ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
listGitStashes: async (directory: string) => sendBridgeMessage('api:git/stashes', { directory }),
|
||||||
|
countGitStashFiles: async (directory: string, refs: string[]) => sendBridgeMessage('api:git/stashes/file-counts', { directory, refs }),
|
||||||
|
stashGitChanges: async (directory: string, options?: { message?: string }) => sendBridgeMessage('api:git/stash', { directory, message: options?.message }),
|
||||||
|
applyGitStash: async (directory: string, options: { ref: string }) => sendBridgeMessage('api:git/stash/apply', { directory, ref: options.ref }),
|
||||||
|
popGitStash: async (directory: string, options: { ref: string }) => sendBridgeMessage('api:git/stash/pop', { directory, ref: options.ref }),
|
||||||
|
dropGitStash: async (directory: string, options: { ref: string }) => sendBridgeMessage('api:git/stash/drop', { directory, ref: options.ref }),
|
||||||
|
|
||||||
checkoutBranch: async (directory: string, branch: string): Promise<{ success: boolean; branch: string }> => {
|
checkoutBranch: async (directory: string, branch: string): Promise<{ success: boolean; branch: string }> => {
|
||||||
return sendBridgeMessage<{ success: boolean; branch: string }>('api:git/checkout', {
|
return sendBridgeMessage<{ success: boolean; branch: string }>('api:git/checkout', {
|
||||||
directory,
|
directory,
|
||||||
|
|||||||
@@ -69,8 +69,12 @@ The following functions are exported and used by the web server:
|
|||||||
- `getConflictDetails(directory)`: Get detailed conflict information including operation type, unmerged files, and diff.
|
- `getConflictDetails(directory)`: Get detailed conflict information including operation type, unmerged files, and diff.
|
||||||
|
|
||||||
### Stash Operations
|
### Stash Operations
|
||||||
- `stash(directory, options)`: Stash changes (supports message and includeUntracked options).
|
- `listStashes(directory)`: List stash entries with ref, message, relative time, and hash.
|
||||||
- `stashPop(directory)`: Pop and apply the most recent stash.
|
- `countStashFiles(directory, refs)`: Batch-count changed files for stash refs with bounded concurrency.
|
||||||
|
- `stashPush(directory, options)`: Stash changes, always including untracked files, with optional message.
|
||||||
|
- `stashApply(directory, options)`: Apply a stash by ref without removing it.
|
||||||
|
- `stashPop(directory, options)`: Apply a stash by ref and drop it only after a successful apply.
|
||||||
|
- `stashDrop(directory, options)`: Drop a stash by ref.
|
||||||
|
|
||||||
## Internal Helpers
|
## Internal Helpers
|
||||||
|
|
||||||
|
|||||||
@@ -336,6 +336,78 @@ export function registerGitRoutes(app) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/git/stashes', async (req, res) => {
|
||||||
|
const { listStashes } = await getGitLibraries();
|
||||||
|
try {
|
||||||
|
const directory = req.query.directory;
|
||||||
|
if (!directory) return res.status(400).json({ error: 'directory parameter is required' });
|
||||||
|
res.json({ stashes: await listStashes(directory) });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to list stashes:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Failed to list stashes' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/git/stashes/file-counts', async (req, res) => {
|
||||||
|
const { countStashFiles } = await getGitLibraries();
|
||||||
|
try {
|
||||||
|
const directory = req.query.directory;
|
||||||
|
if (!directory) return res.status(400).json({ error: 'directory parameter is required' });
|
||||||
|
res.json({ counts: await countStashFiles(directory, req.body?.refs) });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to count stash files:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Failed to count stash files' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/git/stash', async (req, res) => {
|
||||||
|
const { stashPush } = await getGitLibraries();
|
||||||
|
try {
|
||||||
|
const directory = req.query.directory;
|
||||||
|
if (!directory) return res.status(400).json({ error: 'directory parameter is required' });
|
||||||
|
res.json(await stashPush(directory, req.body));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to stash changes:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Failed to stash changes' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/git/stash/apply', async (req, res) => {
|
||||||
|
const { stashApply } = await getGitLibraries();
|
||||||
|
try {
|
||||||
|
const directory = req.query.directory;
|
||||||
|
if (!directory) return res.status(400).json({ error: 'directory parameter is required' });
|
||||||
|
res.json(await stashApply(directory, req.body));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to apply stash:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Failed to apply stash' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/git/stash/pop', async (req, res) => {
|
||||||
|
const { stashPop } = await getGitLibraries();
|
||||||
|
try {
|
||||||
|
const directory = req.query.directory;
|
||||||
|
if (!directory) return res.status(400).json({ error: 'directory parameter is required' });
|
||||||
|
res.json(await stashPop(directory, req.body));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to pop stash:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Failed to pop stash' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/git/stash/drop', async (req, res) => {
|
||||||
|
const { stashDrop } = await getGitLibraries();
|
||||||
|
try {
|
||||||
|
const directory = req.query.directory;
|
||||||
|
if (!directory) return res.status(400).json({ error: 'directory parameter is required' });
|
||||||
|
res.json(await stashDrop(directory, req.body));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to drop stash:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Failed to drop stash' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/git/fetch', async (req, res) => {
|
app.post('/api/git/fetch', async (req, res) => {
|
||||||
const { fetch: gitFetch } = await getGitLibraries();
|
const { fetch: gitFetch } = await getGitLibraries();
|
||||||
try {
|
try {
|
||||||
@@ -501,38 +573,6 @@ export function registerGitRoutes(app) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/git/stash', async (req, res) => {
|
|
||||||
const { stash } = await getGitLibraries();
|
|
||||||
try {
|
|
||||||
const directory = req.query.directory;
|
|
||||||
if (!directory) {
|
|
||||||
return res.status(400).json({ error: 'directory parameter is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await stash(directory, req.body);
|
|
||||||
res.json(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to stash:', error);
|
|
||||||
res.status(500).json({ error: error.message || 'Failed to stash' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/git/stash/pop', async (req, res) => {
|
|
||||||
const { stashPop } = await getGitLibraries();
|
|
||||||
try {
|
|
||||||
const directory = req.query.directory;
|
|
||||||
if (!directory) {
|
|
||||||
return res.status(400).json({ error: 'directory parameter is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await stashPop(directory);
|
|
||||||
res.json(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to pop stash:', error);
|
|
||||||
res.status(500).json({ error: error.message || 'Failed to pop stash' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/git/commit', async (req, res) => {
|
app.post('/api/git/commit', async (req, res) => {
|
||||||
const { commit } = await getGitLibraries();
|
const { commit } = await getGitLibraries();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1828,6 +1828,78 @@ export async function pull(directory, options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listStashes(directory) {
|
||||||
|
const git = await createGit(directory);
|
||||||
|
const output = await git.raw(['stash', 'list', '--format=%gd%x1f%gs%x1f%cr%x1f%H']);
|
||||||
|
return String(output || '')
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((line) => {
|
||||||
|
const [ref = '', message = '', relativeTime = '', hash = ''] = line.split('\x1f');
|
||||||
|
return { ref, message, relativeTime, hash };
|
||||||
|
})
|
||||||
|
.filter((entry) => entry.ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function countStashFiles(directory, refs = []) {
|
||||||
|
const git = await createGit(directory);
|
||||||
|
const uniqueRefs = Array.from(new Set((Array.isArray(refs) ? refs : []).map((ref) => String(ref || '').trim()).filter(Boolean)));
|
||||||
|
const counts = {};
|
||||||
|
const concurrency = 4;
|
||||||
|
let cursor = 0;
|
||||||
|
|
||||||
|
const worker = async () => {
|
||||||
|
while (cursor < uniqueRefs.length) {
|
||||||
|
const ref = uniqueRefs[cursor++];
|
||||||
|
if (!ref) continue;
|
||||||
|
try {
|
||||||
|
const names = await git.raw(['stash', 'show', '--name-only', ref]);
|
||||||
|
counts[ref] = String(names || '').split('\n').map((line) => line.trim()).filter(Boolean).length;
|
||||||
|
} catch {
|
||||||
|
counts[ref] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(Array.from({ length: Math.min(concurrency, uniqueRefs.length) }, () => worker()));
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
export async function stashPush(directory, options = {}) {
|
||||||
|
const git = await createGit(directory);
|
||||||
|
const message = typeof options.message === 'string' && options.message.trim()
|
||||||
|
? options.message.trim()
|
||||||
|
: `OpenChamber stash ${new Date().toISOString()}`;
|
||||||
|
const output = await git.raw(['stash', 'push', '--include-untracked', '-m', message]);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
created: !/no local changes/i.test(String(output || '')),
|
||||||
|
message,
|
||||||
|
output: String(output || '').trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stashApply(directory, options = {}) {
|
||||||
|
const git = await createGit(directory);
|
||||||
|
const ref = typeof options.ref === 'string' && options.ref.trim() ? options.ref.trim() : 'stash@{0}';
|
||||||
|
await git.raw(['stash', 'apply', ref]);
|
||||||
|
return { success: true, ref };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stashDrop(directory, options = {}) {
|
||||||
|
const git = await createGit(directory);
|
||||||
|
const ref = typeof options.ref === 'string' && options.ref.trim() ? options.ref.trim() : 'stash@{0}';
|
||||||
|
await git.raw(['stash', 'drop', ref]);
|
||||||
|
return { success: true, ref };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stashPop(directory, options = {}) {
|
||||||
|
const ref = typeof options.ref === 'string' && options.ref.trim() ? options.ref.trim() : 'stash@{0}';
|
||||||
|
await stashApply(directory, { ref });
|
||||||
|
await stashDrop(directory, { ref });
|
||||||
|
return { success: true, ref };
|
||||||
|
}
|
||||||
|
|
||||||
export async function push(directory, options = {}) {
|
export async function push(directory, options = {}) {
|
||||||
const git = await createGit(directory);
|
const git = await createGit(directory);
|
||||||
|
|
||||||
@@ -3267,40 +3339,3 @@ export async function getConflictDetails(directory) {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== Stash Operations ==============
|
|
||||||
|
|
||||||
export async function stash(directory, options = {}) {
|
|
||||||
const git = await createGit(directory);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const args = ['stash', 'push'];
|
|
||||||
|
|
||||||
// Include untracked files by default
|
|
||||||
if (options.includeUntracked !== false) {
|
|
||||||
args.push('--include-untracked');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.message) {
|
|
||||||
args.push('-m', options.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
await git.raw(args);
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to stash:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function stashPop(directory) {
|
|
||||||
const git = await createGit(directory);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await git.raw(['stash', 'pop']);
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to pop stash:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ export const createWebGitAPI = (): GitAPI => ({
|
|||||||
gitPush: gitApiHttp.gitPush,
|
gitPush: gitApiHttp.gitPush,
|
||||||
gitPull: gitApiHttp.gitPull,
|
gitPull: gitApiHttp.gitPull,
|
||||||
gitFetch: gitApiHttp.gitFetch,
|
gitFetch: gitApiHttp.gitFetch,
|
||||||
|
listGitStashes: gitApiHttp.listGitStashes,
|
||||||
|
countGitStashFiles: gitApiHttp.countGitStashFiles,
|
||||||
|
stashGitChanges: gitApiHttp.stashGitChanges,
|
||||||
|
applyGitStash: gitApiHttp.applyGitStash,
|
||||||
|
popGitStash: gitApiHttp.popGitStash,
|
||||||
|
dropGitStash: gitApiHttp.dropGitStash,
|
||||||
checkoutBranch: gitApiHttp.checkoutBranch,
|
checkoutBranch: gitApiHttp.checkoutBranch,
|
||||||
createBranch: gitApiHttp.createBranch,
|
createBranch: gitApiHttp.createBranch,
|
||||||
renameBranch: gitApiHttp.renameBranch,
|
renameBranch: gitApiHttp.renameBranch,
|
||||||
|
|||||||
Reference in New Issue
Block a user