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 use std::path::PathBuf;
15
16 let config = if let Some(config_path) = &args.config {
18 Config::from_path(config_path)?
19 } else {
20 Config::new()
21 };
22
23 let worktree_path = if let Some(path) = &args.path {
26 path.clone()
27 } else {
28 let dir_name = args.branch.replace('/', "-");
30
31 if let Some(base) = &config.settings.worktree_base {
33 base.join(&dir_name)
34 } else {
35 PathBuf::from("worktrees").join(&dir_name)
37 }
38 };
39
40 let mut git = GitManager::new(std::path::Path::new("."))?;
42
43 let mut worktree_args = Vec::new();
45
46 let branch_exists = git.branch_exists(&args.branch)?;
48
49 if let Some(branch) = &args.new_branch {
51 worktree_args.push("-b");
52 worktree_args.push(branch.as_str());
53 } else if let Some(branch) = &args.force_branch {
54 worktree_args.push("-B");
55 worktree_args.push(branch.as_str());
56 } else if !args.no_create && !args.detach {
57 if !branch_exists {
60 worktree_args.push("-b");
61 } else {
62 worktree_args.push("-B");
63 }
64 worktree_args.push(args.branch.as_str());
65 } else if !branch_exists && !args.detach && args.no_create {
66 return Err(TwinError::git(format!(
68 "Branch '{}' does not exist. Use without --no-create to create it automatically.",
69 args.branch
70 )));
71 }
72 if args.detach {
73 worktree_args.push("--detach");
74 }
75 if args.lock {
76 worktree_args.push("--lock");
77 }
78 if args.track {
79 worktree_args.push("--track");
80 }
81 if args.no_track {
82 worktree_args.push("--no-track");
83 }
84 if args.guess_remote {
85 worktree_args.push("--guess-remote");
86 }
87 if args.no_guess_remote {
88 worktree_args.push("--no-guess-remote");
89 }
90 if args.no_checkout {
91 worktree_args.push("--no-checkout");
92 }
93 if args.quiet {
94 worktree_args.push("--quiet");
95 }
96
97 let path_str = worktree_path.to_string_lossy();
99 worktree_args.push(&path_str);
100
101 let branch_str = args.branch.clone();
103
104 if args.new_branch.is_none() && args.force_branch.is_none() {
107 if !branch_exists && !args.detach {
108 } else if args.detach {
111 worktree_args.push("HEAD");
113 } else {
114 worktree_args.push(&branch_str);
116 }
117 }
118
119 let worktree_path_absolute = if worktree_path.is_relative() {
121 std::env::current_dir()?
122 .join(&worktree_path)
123 .canonicalize()
124 .unwrap_or_else(|_| {
125 let cwd = std::env::current_dir().unwrap();
127 let mut result = cwd.clone();
128 for component in worktree_path.components() {
129 match component {
130 std::path::Component::ParentDir => {
131 result.pop();
132 }
133 std::path::Component::Normal(name) => {
134 result.push(name);
135 }
136 _ => {}
137 }
138 }
139 result
140 })
141 } else {
142 worktree_path.clone()
143 };
144
145 if args.git_only {
147 let output = git.add_worktree_with_options(&worktree_args)?;
148 if !args.quiet {
149 print!("{}", String::from_utf8_lossy(&output.stdout));
150 }
151 return Ok(());
152 }
153
154 let branch_name = args
156 .new_branch
157 .as_ref()
158 .or(args.force_branch.as_ref())
159 .cloned()
160 .unwrap_or_else(|| args.branch.clone());
161
162 let hook_executor = HookExecutor::new();
164 let hook_context = HookContext::new(
165 branch_name.clone(), worktree_path_absolute.clone(),
167 branch_name.clone(),
168 git.get_repo_path().to_path_buf(),
169 );
170
171 if !config.settings.hooks.pre_create.is_empty() {
173 for hook in &config.settings.hooks.pre_create {
174 match hook_executor.execute(HookType::PreCreate, hook, &hook_context) {
175 Ok(result) => {
176 if !result.success && !hook.continue_on_error {
177 return Err(TwinError::hook(
178 format!("Pre-create hook failed: {}", hook.command),
179 "pre_create",
180 result.exit_code,
181 ));
182 }
183 }
184 Err(e) if !hook.continue_on_error => return Err(e),
185 Err(e) => eprintln!("Warning: Pre-create hook failed: {e}"),
186 }
187 }
188 }
189
190 let output = git.add_worktree_with_options(&worktree_args)?;
192 let _worktree_info = git.get_worktree_info(&worktree_path)?;
193
194 if !config.settings.files.is_empty() && !args.git_only {
196 let symlink_manager = create_symlink_manager();
197 let repo_root = git.get_repo_path();
198 let mut failed_links = Vec::new();
199
200 for mapping in &config.settings.files {
201 let source = if repo_root == std::path::Path::new(".") {
203 std::env::current_dir()?.join(&mapping.path)
204 } else if repo_root.is_absolute() {
205 repo_root.join(&mapping.path)
206 } else {
207 std::env::current_dir()?.join(repo_root).join(&mapping.path)
208 };
209 let target = worktree_path_absolute.join(&mapping.path);
210
211 if !source.exists() {
213 eprintln!(
214 "⚠️ Warning: Source file not found, skipping: {}",
215 source.display()
216 );
217 failed_links.push(mapping.path.clone());
218 continue;
219 }
220
221 if let Some(parent) = target.parent() {
223 if let Err(e) = std::fs::create_dir_all(parent) {
224 eprintln!(
225 "⚠️ Warning: Failed to create directory {}: {}",
226 parent.display(),
227 e
228 );
229 failed_links.push(mapping.path.clone());
230 continue;
231 }
232 }
233
234 match symlink_manager.create_symlink(&source, &target) {
236 Ok(_) => {
237 if !args.quiet {
238 eprintln!(
239 "✓ Created symlink: {} -> {}",
240 target.display(),
241 source.display()
242 );
243 }
244 }
245 Err(e) => {
246 eprintln!(
247 "⚠️ Warning: Failed to create symlink for {}: {}",
248 mapping.path.display(),
249 e
250 );
251 failed_links.push(mapping.path.clone());
252 }
253 }
254 }
255
256 if !failed_links.is_empty() && !args.quiet {
258 eprintln!("⚠️ {} symlink(s) could not be created", failed_links.len());
259 eprintln!(" The worktree was created successfully, but some symlinks failed.");
260 }
261 }
262
263 if !config.settings.hooks.post_create.is_empty() {
265 for hook in &config.settings.hooks.post_create {
266 match hook_executor.execute(HookType::PostCreate, hook, &hook_context) {
267 Ok(result) => {
268 if !result.success && !hook.continue_on_error {
269 eprintln!("Error: Post-create hook failed: {}", hook.command);
270 }
272 }
273 Err(e) => eprintln!("Warning: Post-create hook failed: {e}"),
274 }
275 }
276 }
277
278 if args.print_path {
280 println!("{}", worktree_path_absolute.display());
281 } else if args.cd_command {
282 println!("cd \"{}\"", worktree_path_absolute.display());
283 } else if !args.quiet {
284 print!("{}", String::from_utf8_lossy(&output.stdout));
286 if !config.settings.files.is_empty() {
287 println!("✓ シンボリックリンクを作成しました");
288 }
289 }
290
291 Ok(())
292}
293
294pub async fn handle_list(args: ListArgs) -> TwinResult<()> {
295 use crate::git::GitManager;
296
297 let mut git = GitManager::new(std::path::Path::new("."))?;
299 let worktrees = git.list_worktrees()?;
300
301 let formatter = OutputFormatter::new(&args.format);
302 formatter.format_worktrees(&worktrees)?;
303
304 Ok(())
305}
306
307pub async fn handle_remove(args: RemoveArgs) -> TwinResult<()> {
308 use crate::git::GitManager;
309 use crate::hooks::{HookContext, HookExecutor, HookType};
310 use crate::symlink::create_symlink_manager;
311 use std::path::PathBuf;
312
313 let mut git = GitManager::new(std::path::Path::new("."))?;
315
316 let worktrees = git.list_worktrees()?;
318 let worktree = worktrees.iter().find(|w| {
319 w.branch == args.worktree
320 || w.path.file_name().map(|n| n.to_string_lossy()) == Some(args.worktree.clone().into())
321 || w.path.to_string_lossy() == args.worktree
322 });
323
324 let path = if let Some(wt) = worktree {
325 wt.path.clone()
326 } else {
327 PathBuf::from(&args.worktree)
329 };
330
331 if !args.force {
333 use std::io::{self, Write};
334 print!("Worktree '{}' を削除しますか? [y/N]: ", path.display());
335 io::stdout().flush()?;
336
337 let mut input = String::new();
338 io::stdin().read_line(&mut input)?;
339
340 if !input.trim().eq_ignore_ascii_case("y") {
341 println!("削除をキャンセルしました");
342 return Ok(());
343 }
344 }
345
346 let config = if let Some(config_path) = &args.config {
348 Config::from_path(config_path)?
349 } else {
350 Config::new()
351 };
352
353 let branch_name = worktree.map(|w| w.branch.clone()).unwrap_or_else(|| {
355 path.file_name()
356 .and_then(|n| n.to_str())
357 .unwrap_or("worktree")
358 .to_string()
359 });
360
361 let hook_executor = HookExecutor::new();
362 let hook_context = HookContext::new(
363 branch_name.clone(),
364 path.clone(),
365 branch_name.clone(),
366 git.get_repo_path().to_path_buf(),
367 );
368
369 if !config.settings.hooks.pre_remove.is_empty() && !args.git_only {
371 for hook in &config.settings.hooks.pre_remove {
372 match hook_executor.execute(HookType::PreRemove, hook, &hook_context) {
373 Ok(result) => {
374 if !result.success && !hook.continue_on_error {
375 return Err(TwinError::hook(
376 format!("Pre-remove hook failed: {}", hook.command),
377 "pre_remove",
378 result.exit_code,
379 ));
380 }
381 }
382 Err(e) if !hook.continue_on_error => return Err(e),
383 Err(e) => eprintln!("Warning: Pre-remove hook failed: {e}"),
384 }
385 }
386 }
387
388 if !config.settings.files.is_empty() && !args.git_only {
391 let symlink_manager = create_symlink_manager();
392 let mut failed_cleanups = Vec::new();
393
394 for mapping in &config.settings.files {
395 let target = path.join(&mapping.path);
396
397 if target.exists() || target.is_symlink() {
399 match symlink_manager.remove_symlink(&target) {
400 Ok(_) => {
401 if !args.quiet {
402 eprintln!("✓ Removed symlink: {}", target.display());
403 }
404 }
405 Err(e) => {
406 eprintln!(
407 "⚠️ Warning: Failed to remove symlink {}: {}",
408 target.display(),
409 e
410 );
411 failed_cleanups.push(mapping.path.clone());
412 }
413 }
414 }
415 }
416
417 if !failed_cleanups.is_empty() && !args.quiet {
418 eprintln!(
419 "⚠️ {} symlink(s) could not be removed",
420 failed_cleanups.len()
421 );
422 eprintln!(" Proceeding with worktree removal anyway.");
423 }
424 }
425
426 git.remove_worktree(&path, args.force)?;
428
429 if !config.settings.hooks.post_remove.is_empty() && !args.git_only {
431 for hook in &config.settings.hooks.post_remove {
432 match hook_executor.execute(HookType::PostRemove, hook, &hook_context) {
433 Ok(result) => {
434 if !result.success && !hook.continue_on_error {
435 eprintln!("Error: Post-remove hook failed: {}", hook.command);
436 }
438 }
439 Err(e) => eprintln!("Warning: Post-remove hook failed: {e}"),
440 }
441 }
442 }
443
444 println!("✓ Worktree '{}' を削除しました", path.display());
445
446 Ok(())
447}
448
449pub async fn handle_config(args: ConfigArgs) -> TwinResult<()> {
450 use std::path::PathBuf;
451
452 let config_path = PathBuf::from(".twin.toml");
454
455 if let Some(subcommand) = &args.subcommand {
457 match subcommand.as_str() {
458 "default" => {
459 println!("# Twin設定ファイル (.twin.toml)");
461 println!("# このファイルをプロジェクトルートに配置してください");
462 println!();
463 println!("# Worktreeのベースディレクトリ(省略時: ../ブランチ名)");
464 println!("# worktree_base = \"../workspaces\"");
465 println!();
466 println!("# ファイルマッピング設定");
467 println!("# Worktree作成時に自動的にシンボリックリンクやコピーを作成します");
468 println!("# [[files]]");
469 println!("# path = \".env.template\" # ソースファイルのパス");
470 println!("# mapping_type = \"copy\" # \"symlink\" または \"copy\"");
471 println!("# description = \"環境変数設定\" # 説明(省略可)");
472 println!("# skip_if_exists = true # 既存ファイルをスキップ(省略可)");
473 println!();
474 println!("# [[files]]");
475 println!("# path = \".claude/config.json\"");
476 println!("# mapping_type = \"symlink\"");
477 println!();
478 println!("# フック設定(環境作成・削除時に実行するコマンド)");
479 println!("[hooks]");
480 println!("# pre_create = [");
481 println!("# {{ command = \"echo\", args = [\"Creating: {{branch}}\"] }}");
482 println!("# ]");
483 println!("# post_create = [");
484 println!(
485 "# {{ command = \"npm\", args = [\"install\"], continue_on_error = true }}"
486 );
487 println!("# ]");
488 println!("# pre_remove = []");
489 println!("# post_remove = []");
490
491 return Ok(());
492 }
493 _ => {
494 println!("不明なサブコマンド: {subcommand}");
495 return Ok(());
496 }
497 }
498 }
499
500 if args.show {
501 if config_path.exists() {
503 let config = Config::from_path(&config_path)?;
504 println!("{config:#?}");
505 } else {
506 println!("設定ファイルが見つかりません: {}", config_path.display());
507 }
508 } else if let Some(set_value) = args.set {
509 let parts: Vec<&str> = set_value.splitn(2, '=').collect();
511 if parts.len() != 2 {
512 return Err(crate::core::error::TwinError::Config {
513 message: "設定値は 'key=value' 形式で指定してください".to_string(),
514 path: None,
515 source: None,
516 });
517 }
518
519 println!("設定 '{}' を '{}' に設定しました", parts[0], parts[1]);
520 println!("注: この機能は現在実装中です");
521 } else if let Some(key) = args.get {
522 if config_path.exists() {
524 let _config = Config::from_path(&config_path)?;
525 println!("キー '{key}' の値を取得します");
526 println!("注: この機能は現在実装中です");
527 } else {
528 println!("設定ファイルが見つかりません: {}", config_path.display());
529 }
530 } else {
531 println!("使用方法:");
532 println!(" twin config default : デフォルト設定をTOML形式で出力");
533 println!(" twin config --show : 現在の設定を表示");
534 println!(" twin config --set key=value : 設定値をセット");
535 println!(" twin config --get key : 設定値を取得");
536 }
537
538 Ok(())
539}
540
541pub async fn handle_init(args: InitArgs) -> TwinResult<()> {
543 let config_path = crate::config::Config::init(args.path, args.force).await?;
551
552 println!("✅ 設定ファイルを作成しました: {}", config_path.display());
553 println!();
554 println!("設定ファイルを編集して、プロジェクトに合わせてカスタマイズできます。");
555 println!("主な設定項目:");
556 println!(" - worktree_base: ワークツリーのベースディレクトリ");
557 println!(" - branch_prefix: ブランチ名のプレフィックス");
558 println!(" - files: シンボリックリンク/コピーするファイルマッピング");
559 println!(" - hooks: 各種フック(add, remove時の処理)");
560
561 Ok(())
562}