1use crate::cli::output::OutputFormatter;
2use crate::cli::*;
3use crate::core::{Config, TwinError, TwinResult};
4
5pub async fn handle_create(args: AddArgs) -> TwinResult<()> {
7 handle_add(args).await
8}
9
10pub async fn handle_add(args: AddArgs) -> TwinResult<()> {
11 use crate::git::GitManager;
12 use crate::hooks::{HookContext, HookExecutor, HookType};
13 use crate::symlink::create_symlink_manager;
14
15 let config = if let Some(config_path) = &args.config {
17 Config::from_path(config_path)?
18 } else {
19 Config::new()
20 };
21
22 let mut worktree_args = Vec::new();
24
25 if let Some(branch) = &args.new_branch {
27 worktree_args.push("-b");
28 worktree_args.push(branch.as_str());
29 }
30 if let Some(branch) = &args.force_branch {
31 worktree_args.push("-B");
32 worktree_args.push(branch.as_str());
33 }
34 if args.detach {
35 worktree_args.push("--detach");
36 }
37 if args.lock {
38 worktree_args.push("--lock");
39 }
40 if args.track {
41 worktree_args.push("--track");
42 }
43 if args.no_track {
44 worktree_args.push("--no-track");
45 }
46 if args.guess_remote {
47 worktree_args.push("--guess-remote");
48 }
49 if args.no_guess_remote {
50 worktree_args.push("--no-guess-remote");
51 }
52 if args.no_checkout {
53 worktree_args.push("--no-checkout");
54 }
55 if args.quiet {
56 worktree_args.push("--quiet");
57 }
58
59 let path_str = args.path.to_string_lossy();
61 worktree_args.push(&path_str);
62
63 let branch_str;
65 if let Some(branch) = &args.branch {
66 branch_str = branch.clone();
67 worktree_args.push(&branch_str);
68 }
69
70 let worktree_path = if args.path.is_relative() {
72 std::env::current_dir()?
73 .join(&args.path)
74 .canonicalize()
75 .unwrap_or_else(|_| {
76 let cwd = std::env::current_dir().unwrap();
78 let mut result = cwd.clone();
79 for component in args.path.components() {
80 match component {
81 std::path::Component::ParentDir => {
82 result.pop();
83 }
84 std::path::Component::Normal(name) => {
85 result.push(name);
86 }
87 _ => {}
88 }
89 }
90 result
91 })
92 } else {
93 args.path.clone()
94 };
95
96 let mut git = GitManager::new(std::path::Path::new("."))?;
98
99 if args.git_only {
101 let output = git.add_worktree_with_options(&worktree_args)?;
102 if !args.quiet {
103 print!("{}", String::from_utf8_lossy(&output.stdout));
104 }
105 return Ok(());
106 }
107
108 let branch_name = args
110 .new_branch
111 .as_ref()
112 .or(args.force_branch.as_ref())
113 .or(args.branch.as_ref())
114 .cloned()
115 .unwrap_or_else(|| {
116 args.path
118 .file_name()
119 .and_then(|n| n.to_str())
120 .unwrap_or("worktree")
121 .to_string()
122 });
123
124 let hook_executor = HookExecutor::new();
126 let hook_context = HookContext::new(
127 branch_name.clone(), worktree_path.clone(),
129 branch_name.clone(),
130 git.get_repo_path().to_path_buf(),
131 );
132
133 if !config.settings.hooks.pre_create.is_empty() {
135 for hook in &config.settings.hooks.pre_create {
136 match hook_executor.execute(HookType::PreCreate, hook, &hook_context) {
137 Ok(result) => {
138 if !result.success && !hook.continue_on_error {
139 return Err(TwinError::hook(
140 format!("Pre-create hook failed: {}", hook.command),
141 "pre_create",
142 result.exit_code,
143 ));
144 }
145 }
146 Err(e) if !hook.continue_on_error => return Err(e),
147 Err(e) => eprintln!("Warning: Pre-create hook failed: {e}"),
148 }
149 }
150 }
151
152 let output = git.add_worktree_with_options(&worktree_args)?;
154 let _worktree_info = git.get_worktree_info(&worktree_path)?;
155
156 if !config.settings.files.is_empty() && !args.git_only {
158 let symlink_manager = create_symlink_manager();
159 let repo_root = git.get_repo_path();
160 let mut failed_links = Vec::new();
161
162 for mapping in &config.settings.files {
163 let source = if repo_root == std::path::Path::new(".") {
165 std::env::current_dir()?.join(&mapping.path)
166 } else if repo_root.is_absolute() {
167 repo_root.join(&mapping.path)
168 } else {
169 std::env::current_dir()?.join(repo_root).join(&mapping.path)
170 };
171 let target = worktree_path.join(&mapping.path);
172
173 if !source.exists() {
175 eprintln!(
176 "⚠️ Warning: Source file not found, skipping: {}",
177 source.display()
178 );
179 failed_links.push(mapping.path.clone());
180 continue;
181 }
182
183 if let Some(parent) = target.parent() {
185 if let Err(e) = std::fs::create_dir_all(parent) {
186 eprintln!(
187 "⚠️ Warning: Failed to create directory {}: {}",
188 parent.display(),
189 e
190 );
191 failed_links.push(mapping.path.clone());
192 continue;
193 }
194 }
195
196 match symlink_manager.create_symlink(&source, &target) {
198 Ok(_) => {
199 if !args.quiet {
200 eprintln!(
201 "✓ Created symlink: {} -> {}",
202 target.display(),
203 source.display()
204 );
205 }
206 }
207 Err(e) => {
208 eprintln!(
209 "⚠️ Warning: Failed to create symlink for {}: {}",
210 mapping.path.display(),
211 e
212 );
213 failed_links.push(mapping.path.clone());
214 }
215 }
216 }
217
218 if !failed_links.is_empty() && !args.quiet {
220 eprintln!("⚠️ {} symlink(s) could not be created", failed_links.len());
221 eprintln!(" The worktree was created successfully, but some symlinks failed.");
222 }
223 }
224
225 if !config.settings.hooks.post_create.is_empty() {
227 for hook in &config.settings.hooks.post_create {
228 match hook_executor.execute(HookType::PostCreate, hook, &hook_context) {
229 Ok(result) => {
230 if !result.success && !hook.continue_on_error {
231 eprintln!("Error: Post-create hook failed: {}", hook.command);
232 }
234 }
235 Err(e) => eprintln!("Warning: Post-create hook failed: {e}"),
236 }
237 }
238 }
239
240 if args.print_path {
242 println!("{}", worktree_path.display());
243 } else if args.cd_command {
244 println!("cd \"{}\"", worktree_path.display());
245 } else if !args.quiet {
246 print!("{}", String::from_utf8_lossy(&output.stdout));
248 if !config.settings.files.is_empty() {
249 println!("✓ シンボリックリンクを作成しました");
250 }
251 }
252
253 Ok(())
254}
255
256pub async fn handle_list(args: ListArgs) -> TwinResult<()> {
257 use crate::git::GitManager;
258
259 let mut git = GitManager::new(std::path::Path::new("."))?;
261 let worktrees = git.list_worktrees()?;
262
263 let formatter = OutputFormatter::new(&args.format);
264 formatter.format_worktrees(&worktrees)?;
265
266 Ok(())
267}
268
269pub async fn handle_remove(args: RemoveArgs) -> TwinResult<()> {
270 use crate::git::GitManager;
271 use crate::hooks::{HookContext, HookExecutor, HookType};
272 use crate::symlink::create_symlink_manager;
273 use std::path::PathBuf;
274
275 let mut git = GitManager::new(std::path::Path::new("."))?;
277
278 let worktrees = git.list_worktrees()?;
280 let worktree = worktrees.iter().find(|w| {
281 w.branch == args.worktree
282 || w.path.file_name().map(|n| n.to_string_lossy()) == Some(args.worktree.clone().into())
283 || w.path.to_string_lossy() == args.worktree
284 });
285
286 let path = if let Some(wt) = worktree {
287 wt.path.clone()
288 } else {
289 PathBuf::from(&args.worktree)
291 };
292
293 if !args.force {
295 use std::io::{self, Write};
296 print!("Worktree '{}' を削除しますか? [y/N]: ", path.display());
297 io::stdout().flush()?;
298
299 let mut input = String::new();
300 io::stdin().read_line(&mut input)?;
301
302 if !input.trim().eq_ignore_ascii_case("y") {
303 println!("削除をキャンセルしました");
304 return Ok(());
305 }
306 }
307
308 let config = if let Some(config_path) = &args.config {
310 Config::from_path(config_path)?
311 } else {
312 Config::new()
313 };
314
315 let branch_name = worktree.map(|w| w.branch.clone()).unwrap_or_else(|| {
317 path.file_name()
318 .and_then(|n| n.to_str())
319 .unwrap_or("worktree")
320 .to_string()
321 });
322
323 let hook_executor = HookExecutor::new();
324 let hook_context = HookContext::new(
325 branch_name.clone(),
326 path.clone(),
327 branch_name.clone(),
328 git.get_repo_path().to_path_buf(),
329 );
330
331 if !config.settings.hooks.pre_remove.is_empty() && !args.git_only {
333 for hook in &config.settings.hooks.pre_remove {
334 match hook_executor.execute(HookType::PreRemove, hook, &hook_context) {
335 Ok(result) => {
336 if !result.success && !hook.continue_on_error {
337 return Err(TwinError::hook(
338 format!("Pre-remove hook failed: {}", hook.command),
339 "pre_remove",
340 result.exit_code,
341 ));
342 }
343 }
344 Err(e) if !hook.continue_on_error => return Err(e),
345 Err(e) => eprintln!("Warning: Pre-remove hook failed: {e}"),
346 }
347 }
348 }
349
350 if !config.settings.files.is_empty() && !args.git_only {
353 let symlink_manager = create_symlink_manager();
354 let mut failed_cleanups = Vec::new();
355
356 for mapping in &config.settings.files {
357 let target = path.join(&mapping.path);
358
359 if target.exists() || target.is_symlink() {
361 match symlink_manager.remove_symlink(&target) {
362 Ok(_) => {
363 if !args.quiet {
364 eprintln!("✓ Removed symlink: {}", target.display());
365 }
366 }
367 Err(e) => {
368 eprintln!(
369 "⚠️ Warning: Failed to remove symlink {}: {}",
370 target.display(),
371 e
372 );
373 failed_cleanups.push(mapping.path.clone());
374 }
375 }
376 }
377 }
378
379 if !failed_cleanups.is_empty() && !args.quiet {
380 eprintln!(
381 "⚠️ {} symlink(s) could not be removed",
382 failed_cleanups.len()
383 );
384 eprintln!(" Proceeding with worktree removal anyway.");
385 }
386 }
387
388 git.remove_worktree(&path, args.force)?;
390
391 if !config.settings.hooks.post_remove.is_empty() && !args.git_only {
393 for hook in &config.settings.hooks.post_remove {
394 match hook_executor.execute(HookType::PostRemove, hook, &hook_context) {
395 Ok(result) => {
396 if !result.success && !hook.continue_on_error {
397 eprintln!("Error: Post-remove hook failed: {}", hook.command);
398 }
400 }
401 Err(e) => eprintln!("Warning: Post-remove hook failed: {e}"),
402 }
403 }
404 }
405
406 println!("✓ Worktree '{}' を削除しました", path.display());
407
408 Ok(())
409}
410
411pub async fn handle_config(args: ConfigArgs) -> TwinResult<()> {
412 use std::path::PathBuf;
413
414 let config_path = PathBuf::from(".twin.toml");
416
417 if let Some(subcommand) = &args.subcommand {
419 match subcommand.as_str() {
420 "default" => {
421 println!("# Twin設定ファイル (.twin.toml)");
423 println!("# このファイルをプロジェクトルートに配置してください");
424 println!();
425 println!("# Worktreeのベースディレクトリ(省略時: ../ブランチ名)");
426 println!("# worktree_base = \"../workspaces\"");
427 println!();
428 println!("# ファイルマッピング設定");
429 println!("# Worktree作成時に自動的にシンボリックリンクやコピーを作成します");
430 println!("# [[files]]");
431 println!("# path = \".env.template\" # ソースファイルのパス");
432 println!("# mapping_type = \"copy\" # \"symlink\" または \"copy\"");
433 println!("# description = \"環境変数設定\" # 説明(省略可)");
434 println!("# skip_if_exists = true # 既存ファイルをスキップ(省略可)");
435 println!();
436 println!("# [[files]]");
437 println!("# path = \".claude/config.json\"");
438 println!("# mapping_type = \"symlink\"");
439 println!();
440 println!("# フック設定(環境作成・削除時に実行するコマンド)");
441 println!("[hooks]");
442 println!("# pre_create = [");
443 println!("# {{ command = \"echo\", args = [\"Creating: {{branch}}\"] }}");
444 println!("# ]");
445 println!("# post_create = [");
446 println!(
447 "# {{ command = \"npm\", args = [\"install\"], continue_on_error = true }}"
448 );
449 println!("# ]");
450 println!("# pre_remove = []");
451 println!("# post_remove = []");
452
453 return Ok(());
454 }
455 _ => {
456 println!("不明なサブコマンド: {subcommand}");
457 return Ok(());
458 }
459 }
460 }
461
462 if args.show {
463 if config_path.exists() {
465 let config = Config::from_path(&config_path)?;
466 println!("{config:#?}");
467 } else {
468 println!("設定ファイルが見つかりません: {}", config_path.display());
469 }
470 } else if let Some(set_value) = args.set {
471 let parts: Vec<&str> = set_value.splitn(2, '=').collect();
473 if parts.len() != 2 {
474 return Err(crate::core::error::TwinError::Config {
475 message: "設定値は 'key=value' 形式で指定してください".to_string(),
476 path: None,
477 source: None,
478 });
479 }
480
481 println!("設定 '{}' を '{}' に設定しました", parts[0], parts[1]);
482 println!("注: この機能は現在実装中です");
483 } else if let Some(key) = args.get {
484 if config_path.exists() {
486 let _config = Config::from_path(&config_path)?;
487 println!("キー '{key}' の値を取得します");
488 println!("注: この機能は現在実装中です");
489 } else {
490 println!("設定ファイルが見つかりません: {}", config_path.display());
491 }
492 } else {
493 println!("使用方法:");
494 println!(" twin config default : デフォルト設定をTOML形式で出力");
495 println!(" twin config --show : 現在の設定を表示");
496 println!(" twin config --set key=value : 設定値をセット");
497 println!(" twin config --get key : 設定値を取得");
498 }
499
500 Ok(())
501}