UNPKG

@shutootaki/gwm

Version:
271 lines 12 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { useState, useEffect, useMemo, useCallback, useRef, } from 'react'; import { SelectList } from './SelectList.js'; import { TextInput, validateBranchName, generateWorktreePreview, } from './TextInput.js'; import { formatErrorForDisplay } from '../utils/index.js'; import { useWorktree } from '../hooks/useWorktree.js'; import { LoadingSpinner } from './ui/LoadingSpinner.js'; import { Notice } from './ui/Notice.js'; import { HookConfirmation } from './ui/HookConfirmation.js'; import { getRemoteBranchesWithInfo } from '../utils/git.js'; import { escapeShellArg, execAsync } from '../utils/shell.js'; import { loadConfigWithSource } from '../config/loader.js'; import { verifyTrust, trustRepository, } from '../trust/index.js'; export const WorktreeAdd = ({ branchName, isRemote = false, fromBranch, openCode = false, openCursor = false, outputPath = false, skipHooks = false, }) => { const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [remoteBranches, setRemoteBranches] = useState([]); const [isRemoteBranchesLoaded, setIsRemoteBranchesLoaded] = useState(false); const [pendingConfirmation, setPendingConfirmation] = useState(null); // 初期表示モードは引数有無に応じて決定する // * branchName 指定あり → すぐに worktree 作成するので "loading" 表示のみ // * -r 指定でリモート一覧を取りに行く場合も "loading" // * それ以外は新規ブランチ入力モード const initialViewMode = branchName ? 'loading' : isRemote ? 'loading' : 'input'; const [viewMode, setViewMode] = useState(initialViewMode); // ローディング時のラベル(状況に応じて変更) const [loadingLabel, setLoadingLabel] = useState(branchName ? 'Creating worktree...' : 'Fetching remote branches...'); // onSuccess / onError の参照が毎レンダー変わらないように useCallback でメモ化 const handleSuccess = useCallback((data) => { setSuccess(JSON.stringify(data)); }, []); const handleError = useCallback((message) => { setError(message); }, []); const handleHooksStart = useCallback(() => { setViewMode('hooks'); }, []); const { createWorktree } = useWorktree({ fromBranch, openCode, openCursor, outputPath, skipHooks, onSuccess: handleSuccess, onError: handleError, onHooksStart: handleHooksStart, }); // createWorktree の最新参照を保持(useEffect の依存配列から除外するため) const createWorktreeRef = useRef(createWorktree); useEffect(() => { createWorktreeRef.current = createWorktree; }, [createWorktree]); const fetchRemoteBranches = async () => { try { // リモート一覧を取得して fetch const { stdout } = await execAsync('git remote', { cwd: process.cwd(), encoding: 'utf8', }); const remotes = stdout .split('\n') .map((r) => r.trim()) .filter(Boolean); const targetRemotes = remotes.includes('origin') ? ['origin'] : remotes; for (const remote of targetRemotes) { await execAsync(`git fetch --prune ${escapeShellArg(remote)}`, { cwd: process.cwd(), }); } // リモートブランチの詳細情報を取得 const branches = await getRemoteBranchesWithInfo(); setRemoteBranches(branches); setIsRemoteBranchesLoaded(true); setViewMode('select'); } catch (err) { setError(formatErrorForDisplay(err)); } }; // remoteBranches の SelectList 用変換をメモ化 const selectItems = useMemo(() => remoteBranches.map((branch) => ({ label: branch.name, value: branch.name, description: branch.lastCommitMessage, metadata: { lastCommitDate: branch.lastCommitDate, lastCommitterName: branch.lastCommitterName, lastCommitMessage: branch.lastCommitMessage, }, })), [remoteBranches]); /** * worktree作成前に信頼確認を行い、必要に応じて確認UIを表示 */ const checkTrustAndCreate = useCallback((branch, isRemoteBranch) => { // --skip-hooks が指定されている場合は確認不要 if (skipHooks) { createWorktree(branch, isRemoteBranch); return; } // 設定をソース情報付きで読み込む const { config, hasProjectHooks, repoRoot } = loadConfigWithSource(); // リポジトリ外の場合は確認不要 if (!repoRoot) { createWorktree(branch, isRemoteBranch); return; } // 信頼状態を確認 const trustStatus = verifyTrust(repoRoot, config, hasProjectHooks); if (trustStatus.status === 'needs-confirmation') { // 確認が必要 → 確認UIを表示 setPendingConfirmation({ trustStatus, branch, isRemote: isRemoteBranch, }); setViewMode('confirm'); } else { // 信頼済み or hooks なし → そのまま作成 createWorktree(branch, isRemoteBranch); } }, [skipHooks, createWorktree]); /** * 確認UIでの選択を処理 */ const handleConfirmChoice = useCallback((choice) => { if (!pendingConfirmation) return; const { trustStatus, branch, isRemote: isRemoteBranch, } = pendingConfirmation; if (choice === 'cancel') { // キャンセル → 中止 setError('Operation cancelled.'); setPendingConfirmation(null); return; } if (choice === 'trust') { // 信頼してキャッシュに保存 const { repoRoot } = loadConfigWithSource(); if (repoRoot) { trustRepository(repoRoot, trustStatus.configPath, trustStatus.configHash, trustStatus.commands); } } // trust または once → worktree作成を続行 setPendingConfirmation(null); setViewMode('loading'); setLoadingLabel('Creating worktree...'); createWorktree(branch, isRemoteBranch); }, [pendingConfirmation, createWorktree]); const handleBranchSelect = (item) => { checkTrustAndCreate(item.value, true); }; const handleBranchInput = (branchName) => { checkTrustAndCreate(branchName, false); }; const handleCancel = () => { setError('Operation cancelled'); }; const handleModeSwitch = () => { if (viewMode === 'input') { // 新規ブランチ入力からリモートブランチ選択に切り替え if (!isRemoteBranchesLoaded) { setLoadingLabel('Fetching remote branches...'); setViewMode('loading'); fetchRemoteBranches(); } else { setViewMode('select'); } } else if (viewMode === 'select') { // リモートブランチ選択から新規ブランチ入力に切り替え setViewMode('input'); } }; // checkTrustAndCreate の最新参照を保持(useEffect から参照するため) const checkTrustAndCreateRef = useRef(checkTrustAndCreate); useEffect(() => { checkTrustAndCreateRef.current = checkTrustAndCreate; }, [checkTrustAndCreate]); // 初回マウント時の処理 useEffect(() => { if (branchName) { // 引数でブランチ名が指定された場合、信頼確認してから作成 checkTrustAndCreateRef.current(branchName, isRemote); } else if (isRemote) { // -r フラグが指定された場合、リモートブランチ選択モードに setLoadingLabel('Fetching remote branches...'); setViewMode('loading'); fetchRemoteBranches(); } else { // 引数なしの場合、デフォルトで新規ブランチ入力モード setViewMode('input'); } }, [branchName, isRemote]); // 成功してエディタを開いた場合は 1 秒後に自動で CLI を終了する useEffect(() => { if (success && (openCode || openCursor)) { const timer = setTimeout(() => process.exit(0), 1000); return () => clearTimeout(timer); } }, [success, openCode, openCursor]); if (success) { // エディタを開くオプションが指定されている場合は簡易表示 if (openCode || openCursor) { let worktreePath; let actions = []; try { const parsed = JSON.parse(success); worktreePath = parsed.path; actions = parsed.actions || []; } catch { worktreePath = success; } const editorName = openCode ? 'VS Code' : 'Cursor'; // hooks実行結果をフィルタリング(Executedまたはfailedを含むもの) const hookMessages = actions.filter((a) => a.includes('hook') || a.includes('Hook')); const messages = [ `✓ ${worktreePath}`, ...hookMessages.map((h) => `• ${h}`), ]; return (_jsx(Notice, { variant: "success", title: `Worktree created and opened in ${editorName}`, messages: messages })); } let worktreePath; let actions = []; try { const parsed = JSON.parse(success); worktreePath = parsed.path; actions = parsed.actions || []; } catch { // 後方互換性のため、文字列の場合はそのままパスとして扱う worktreePath = success; } // worktreePathからブランチ名を抽出(最後のディレクトリ名) const branchName = worktreePath.split('/').pop() || ''; return (_jsx(Notice, { variant: "success", title: "Worktree created successfully!", messages: [ `Location: ${worktreePath}`, ...actions.map((a) => `• ${a}`), 'To navigate to the created worktree, run:', ` $ gwm go ${branchName}`, ` $ cd "${worktreePath}"`, ] })); } if (error) { return (_jsx(Notice, { variant: "error", title: "Failed to create worktree", messages: [error, 'Branch may already exist or permission issue'] })); } if (viewMode === 'input') { return (_jsx(TextInput, { title: "Create new worktree", placeholder: "Enter new branch name...", onSubmit: handleBranchInput, onCancel: handleCancel, onModeSwitch: handleModeSwitch, validate: validateBranchName, preview: generateWorktreePreview })); } if (viewMode === 'select') { return (_jsx(SelectList, { items: selectItems, onSelect: handleBranchSelect, onCancel: handleCancel, title: "Create worktree from remote branch", placeholder: "Search remote branches..." })); } // viewMode === 'confirm'(確認UI表示) if (viewMode === 'confirm' && pendingConfirmation) { return (_jsx(HookConfirmation, { reason: pendingConfirmation.trustStatus.reason, commands: pendingConfirmation.trustStatus.commands, onChoice: handleConfirmChoice })); } // viewMode === 'hooks'(フック実行中はスピナーを非表示) if (viewMode === 'hooks') { return null; } // viewMode === 'loading' return _jsx(LoadingSpinner, { label: loadingLabel }); }; //# sourceMappingURL=WorktreeAdd.js.map