@shutootaki/gwm
Version:
git worktree manager CLI
144 lines • 7.22 kB
JavaScript
import { useCallback, useMemo } from 'react';
import { execSync, spawnSync } from 'child_process';
import { join } from 'path';
import { loadConfig } from '../config.js';
import { getRepositoryName, getMainWorktreePath, getIgnoredFiles, copyFiles, getRepoRoot, } from '../utils/git.js';
import { detectVirtualEnvs, suggestSetupCommands, getVirtualEnvExcludePatterns, } from '../utils/virtualenv.js';
import { escapeShellArg } from '../utils/shell.js';
import { openWithEditor } from '../utils/editor.js';
import { formatErrorForDisplay } from '../utils/index.js';
import { runPostCreateHooks } from './runner/index.js';
export function useWorktree({ fromBranch, openCode = false, openCursor = false, outputPath = false, skipHooks = false, onSuccess, onError, onHooksStart, }) {
const config = useMemo(() => loadConfig(), []);
const createWorktree = useCallback(async (branch, isRemote) => {
try {
const repoName = getRepositoryName();
const sanitizedBranch = branch.replace(/\//g, '-');
const worktreePath = join(config.worktree_base_path, repoName, sanitizedBranch);
let command;
// 仮想環境隔離機能が有効かどうか判定(後方互換)
const isIsolationEnabled = (() => {
const veh = config.virtual_env_handling;
if (!veh)
return false;
if (typeof veh.isolate_virtual_envs === 'boolean')
return veh.isolate_virtual_envs;
return veh.mode === 'skip';
})();
// まずローカルブランチの存在を確認(isRemote= true の場合も含む)
const localExists = (() => {
try {
execSync(`git show-ref --verify --quiet ${escapeShellArg(`refs/heads/${branch}`)}`);
return true;
}
catch {
return false;
}
})();
if (localExists) {
// 既存ブランチをそのまま利用
command = `git worktree add ${escapeShellArg(worktreePath)} ${escapeShellArg(branch)}`;
}
else if (isRemote) {
// リモートブランチからチェックアウト
command = `git worktree add ${escapeShellArg(worktreePath)} -b ${escapeShellArg(branch)} ${escapeShellArg(`origin/${branch}`)}`;
}
else {
// 新規ローカルブランチとして baseBranch から作成
const baseBranch = fromBranch || config.main_branches[0];
command = `git worktree add ${escapeShellArg(worktreePath)} -b ${escapeShellArg(branch)} ${escapeShellArg(baseBranch)}`;
}
execSync(command);
const actions = [];
// gitignoreされたファイルのコピー処理
if (config.copy_ignored_files?.enabled) {
const mainWorktreePath = getMainWorktreePath();
if (mainWorktreePath && mainWorktreePath !== worktreePath) {
const ignoredFiles = getIgnoredFiles(mainWorktreePath, config.copy_ignored_files.patterns ?? [], [
...(config.copy_ignored_files.exclude_patterns ?? []),
...(isIsolationEnabled ? getVirtualEnvExcludePatterns() : []),
], isIsolationEnabled);
if (ignoredFiles.length > 0) {
const { copied, skippedVirtualEnvs, skippedOversize } = await copyFiles(mainWorktreePath, worktreePath, ignoredFiles);
if (copied.length > 0) {
actions.push(`Copied ${copied.length} ignored file(s): ${copied.join(', ')}`);
}
if (skippedVirtualEnvs.length > 0) {
actions.push(`Skipped virtual environment(s): ${skippedVirtualEnvs.join(', ')}`);
}
if (skippedOversize.length > 0) {
actions.push(`Skipped oversize file(s): ${skippedOversize.join(', ')}`);
}
}
}
}
// 仮想環境の検出とセットアップ提案(隔離有効時のみ)
if (isIsolationEnabled) {
const mainPath = getMainWorktreePath();
if (mainPath) {
const detectedEnvs = detectVirtualEnvs(mainPath);
if (detectedEnvs.length > 0) {
actions.push('', '📦 Virtual environments detected in the source worktree:', ...detectedEnvs.map((env) => ` - ${env.language}: ${env.path}`), '', '💡 To set up your development environment, run:', ...suggestSetupCommands(detectedEnvs));
}
}
}
// エディタを先に開く(hooks 実行中にコードを確認できるように)
if (openCode) {
const ok = openWithEditor(worktreePath, 'code');
actions.push(ok ? 'VS Code opened' : 'VS Code failed to open (not installed?)');
}
if (openCursor) {
const ok = openWithEditor(worktreePath, 'cursor');
actions.push(ok ? 'Cursor opened' : 'Cursor failed to open (not installed?)');
}
// post_create hook の実行
if (!skipHooks) {
onHooksStart?.();
const hookContext = {
worktreePath,
branchName: branch,
repoRoot: getRepoRoot(),
repoName,
};
const hookResult = await runPostCreateHooks(config, hookContext);
if (!hookResult.success) {
// hook 失敗時は actions に情報を追加するが、worktree は残す
actions.push(`Hook failed: ${hookResult.failedCommand}`);
}
else if (hookResult.executedCount > 0) {
actions.push(`Executed ${hookResult.executedCount} post_create hook(s)`);
}
}
// すべての処理完了後に通知
onSuccess?.({ path: worktreePath, actions });
if (outputPath) {
const userShell = process.env.SHELL ||
(process.platform === 'win32'
? process.env.COMSPEC || 'cmd.exe'
: '/bin/bash');
spawnSync(userShell, {
cwd: worktreePath,
stdio: 'inherit',
env: process.env,
});
// サブシェル終了後に CLI も終了
process.exit(0);
}
}
catch (err) {
onError?.(formatErrorForDisplay(err));
}
}, [
config,
fromBranch,
openCode,
openCursor,
outputPath,
skipHooks,
onSuccess,
onError,
onHooksStart,
]);
return { createWorktree };
}
//# sourceMappingURL=useWorktree.js.map