UNPKG

@shutootaki/gwm

Version:
156 lines 7.19 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useEffect, useState, useRef } from 'react'; import { Box, Text, useInput } from 'ink'; import { fetchAndPrune, getCleanableWorktrees, removeWorktree, } from '../utils/index.js'; import Spinner from 'ink-spinner'; import { WorktreeTable } from './WorktreeTable.js'; export const WorktreeClean = ({ dryRun = false, force = false, }) => { const [loading, setLoading] = useState(true); const [cleanables, setCleanables] = useState([]); const [error, setError] = useState(null); const [removing, setRemoving] = useState(false); const [success, setSuccess] = useState([]); const [stage, setStage] = useState('done'); // すでに削除処理が走っているかを保持するフラグ const isProcessing = useRef(false); // アンマウント時に処理中フラグをリセット useEffect(() => { return () => { isProcessing.current = false; }; }, []); // ローディングフェーズを段階的に追跡 const [loadingStage, setLoadingStage] = useState('fetch'); // 削除進捗 const [progress, setProgress] = useState(null); // レンダリング頻度を抑えるための待機時間(ms) const YIELD_INTERVAL = 16; // 約60fps 相当 useEffect(() => { const init = async () => { try { setLoading(true); setLoadingStage('fetch'); fetchAndPrune(); // fetch 完了後にスキャンフェーズへ setLoadingStage('scan'); const list = await getCleanableWorktrees(); setCleanables(list); setLoading(false); // --dry-run: 一覧表示のみ if (dryRun) { setStage('done'); return; } // --force: 即削除 if (force && list.length > 0) { await handleRemoveAll(list); return; } // 通常モード: 確認ステージ if (list.length === 0) { setStage('done'); } else { setStage('confirm'); } } catch (e) { setError(e instanceof Error ? e.message : String(e)); setLoading(false); } }; init(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [dryRun, force]); // 確認プロンプト用 useInput((input, key) => { if (key.escape) { setError('Cancelled'); setStage('done'); return; } if (key.return) { handleRemoveAll(); } }, { isActive: stage === 'confirm' }); const handleRemoveAll = async (targets) => { // 多重実行を防止 if (isProcessing.current) return; isProcessing.current = true; const list = targets ?? cleanables; if (list.length === 0) { setError('No worktrees selected'); isProcessing.current = false; return; } setRemoving(true); setProgress({ current: 0, total: list.length }); const removed = []; const errs = []; try { for (let i = 0; i < list.length; i++) { const item = list[i]; setProgress({ current: i + 1, total: list.length, path: item.worktree.path, }); try { removeWorktree(item.worktree.path, true); removed.push(item.worktree.path); } catch (e) { errs.push(`${item.worktree.path}: ${e instanceof Error ? e.message : String(e)}`); } // レンダリングのためにイベントループを解放 await new Promise((r) => setTimeout(r, YIELD_INTERVAL)); } // 進捗リセット setProgress(null); if (errs.length > 0) setError(errs.join('\n')); if (removed.length > 0) setSuccess(removed); } finally { // 後片付けを確実に実行 setRemoving(false); setStage('done'); isProcessing.current = false; } }; // --------------- RENDERING ----------------- if (loading) { const msg = loadingStage === 'fetch' ? 'Fetching remote status (git fetch --prune)…' : 'Analyzing worktrees…'; return (_jsx(Box, { children: _jsxs(Text, { children: [_jsxs(Text, { color: "cyan", children: [_jsx(Spinner, { type: "dots" }), ' '] }), msg] }) })); } if (removing) { if (progress) { return (_jsx(Box, { children: _jsxs(Text, { children: [_jsxs(Text, { color: "cyan", children: [_jsx(Spinner, { type: "dots" }), ' '] }), `Removing (${progress.current}/${progress.total}) ${progress.path}`] }) })); } return (_jsx(Box, { children: _jsxs(Text, { children: [_jsxs(Text, { color: "cyan", children: [_jsx(Spinner, { type: "dots" }), ' '] }), "Removing worktrees..."] }) })); } if (success.length > 0) { return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 Successfully cleaned ", success.length, " worktree(s):"] }), success.map((p) => (_jsxs(Text, { children: [" ", p] }, p))), error && _jsxs(Text, { color: "red", children: ["Some errors occurred:\\n", error] })] })); } if (error && stage === 'done') { return (_jsx(Box, { children: _jsxs(Text, { color: "red", children: ["\u2717 Error: ", error] }) })); } if (cleanables.length === 0) { return (_jsx(Box, { children: _jsx(Text, { children: "No cleanable worktrees found." }) })); } // 確認ステージ if (stage === 'confirm') { return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Found ", cleanables.length, " cleanable worktree(s):"] }), cleanables.map((c) => (_jsxs(Text, { children: ["\u2022 ", c.worktree.branch.padEnd(30), " ", c.worktree.path] }, c.worktree.path))), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { color: "cyan", children: "Press Enter to delete all listed worktrees, or Esc to cancel." }) })] })); } // --dry-run モード: 候補一覧のみ表示して終了 if (dryRun && stage === 'done') { return (_jsx(WorktreeTable, { worktrees: cleanables.map((c) => c.worktree), title: `Found ${cleanables.length} cleanable worktree(s) (dry-run)`, footer: _jsxs(Text, { color: "gray", children: ["No changes will be made. Remove ", _jsx(Text, { color: "cyan", children: "--dry-run" }), ' ', "to actually clean."] }) })); } // interactive MultiSelectList は削除済みのため、ここには到達しない return null; }; //# sourceMappingURL=WorktreeClean.js.map