@shutootaki/gwm
Version:
git worktree manager CLI
156 lines • 7.19 kB
JavaScript
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