easy-worktree 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
easy_worktree/__init__.py CHANGED
@@ -131,14 +131,14 @@ MESSAGES = {
131
131
  "en": "Completed sync of {} files",
132
132
  "ja": "{} 個のファイルを同期しました",
133
133
  },
134
- "usage_sync": {
135
- "en": "Usage: wt sync (sy) [files...] [--from <name>] [--to <name>]",
136
- "ja": "使用方法: wt sync (sy) [files...] [--from <name>] [--to <name>]",
137
- },
138
134
  "usage_pr": {
139
135
  "en": "Usage: wt pr add <number>",
140
136
  "ja": "使用方法: wt pr add <number>",
141
137
  },
138
+ "usage_setup": {
139
+ "en": "Usage: wt setup (su)",
140
+ "ja": "使用方法: wt setup (su)",
141
+ },
142
142
  "usage_stash": {
143
143
  "en": "Usage: wt stash (st) <work_name> [<base_branch>]",
144
144
  "ja": "使用方法: wt stash (st) <work_name> [<base_branch>]",
@@ -155,6 +155,35 @@ MESSAGES = {
155
155
  "en": "No local changes to stash.",
156
156
  "ja": "スタッシュする変更がありません",
157
157
  },
158
+ "select_switched": {
159
+ "en": "Switched worktree to: {}",
160
+ "ja": "作業ディレクトリを切り替えました: {}",
161
+ },
162
+ "select_not_found": {
163
+ "en": "Worktree not found: {}",
164
+ "ja": "worktree が見つかりません: {}",
165
+ },
166
+ "select_no_last": {
167
+ "en": "No previous selection found",
168
+ "ja": "以前の選択が見つかりません",
169
+ },
170
+ "setting_up": {"en": "Setting up: {} -> {}", "ja": "セットアップ中: {} -> {}"},
171
+ "completed_setup": {
172
+ "en": "Completed setup of {} files",
173
+ "ja": "{} 個のファイルをセットアップしました",
174
+ },
175
+ "suggest_setup": {
176
+ "en": "Some setup files are missing. Run 'wt setup' to initialize this worktree.",
177
+ "ja": "一部のセットアップファイルが不足しています。'wt setup' を実行して初期化してください。",
178
+ },
179
+ "nesting_error": {
180
+ "en": "Error: Already in a wt subshell ({}). Please 'exit' before switching.",
181
+ "ja": "エラー: すでに wt のサブシェル ({}) 内にいます。切り替える前に 'exit' してください。",
182
+ },
183
+ "jump_instruction": {
184
+ "en": "Jumping to '{}' ({}). Type 'exit' or Ctrl-D to return.",
185
+ "ja": "'{}' ({}) にジャンプします。戻るには 'exit' または Ctrl-D を入力してください。",
186
+ },
158
187
  }
159
188
 
160
189
 
@@ -197,10 +226,11 @@ def get_repository_name(url: str) -> str:
197
226
  def load_config(base_dir: Path) -> dict:
198
227
  """設定ファイルを読み込む"""
199
228
  config_file = base_dir / ".wt" / "config.toml"
229
+ local_config_file = base_dir / ".wt" / "config.local.toml"
230
+
200
231
  default_config = {
201
232
  "worktrees_dir": ".worktrees",
202
- "sync_files": [".env"],
203
- "auto_copy_on_add": True,
233
+ "setup_files": [".env"],
204
234
  }
205
235
 
206
236
  if config_file.exists():
@@ -211,6 +241,14 @@ def load_config(base_dir: Path) -> dict:
211
241
  except Exception as e:
212
242
  print(msg("error", f"Failed to load config: {e}"), file=sys.stderr)
213
243
 
244
+ if local_config_file.exists():
245
+ try:
246
+ with open(local_config_file, "r", encoding="utf-8") as f:
247
+ local_config = toml.load(f)
248
+ default_config.update(local_config)
249
+ except Exception as e:
250
+ print(msg("error", f"Failed to load local config: {e}"), file=sys.stderr)
251
+
214
252
  return default_config
215
253
 
216
254
 
@@ -238,8 +276,7 @@ def create_hook_template(base_dir: Path):
238
276
  base_dir,
239
277
  {
240
278
  "worktrees_dir": ".worktrees",
241
- "sync_files": [".env"],
242
- "auto_copy_on_add": True,
279
+ "setup_files": [".env"],
243
280
  },
244
281
  )
245
282
 
@@ -302,9 +339,24 @@ def create_hook_template(base_dir: Path):
302
339
 
303
340
  # .gitignore
304
341
  gitignore_file = wt_dir / ".gitignore"
342
+
343
+ ignores = ["post-add.local", "config.local.toml"]
344
+
305
345
  if not gitignore_file.exists():
306
- gitignore_content = "post-add.local\n"
346
+ gitignore_content = "\n".join(ignores) + "\n"
307
347
  gitignore_file.write_text(gitignore_content)
348
+ else:
349
+ content = gitignore_file.read_text()
350
+ updated = False
351
+ for ignore in ignores:
352
+ if ignore not in content:
353
+ if content and not content.endswith("\n"):
354
+ content += "\n"
355
+ content += f"{ignore}\n"
356
+ updated = True
357
+ if updated:
358
+ gitignore_file.write_text(content)
359
+
308
360
 
309
361
  # README.md (言語に応じて)
310
362
  readme_file = wt_dir / "README.md"
@@ -327,12 +379,12 @@ wt clone <repository_url>
327
379
  # 新しい worktree を作成(新規ブランチ)
328
380
  wt add <作業名>
329
381
 
382
+ # セットアップ(フック実行など)をスキップして作成
383
+ wt add <作業名> --skip-setup
384
+
330
385
  # 既存ブランチから worktree を作成
331
386
  wt add <作業名> <既存ブランチ名>
332
387
 
333
- # エイリアスを作成(current エイリアスで現在の作業を切り替え)
334
- wt add <作業名> --alias current
335
-
336
388
  # worktree 一覧を表示
337
389
  wt list
338
390
 
@@ -342,30 +394,18 @@ wt rm <作業名>
342
394
 
343
395
  詳細は https://github.com/igtm/easy-worktree を参照してください。
344
396
 
345
- ## エイリアスとは
397
+ ## 設定 (config.toml)
346
398
 
347
- エイリアスは、worktree へのシンボリックリンク(symbolic link)です。同じエイリアス名で異なる worktree を指すことで、固定されたパスで複数のブランチを切り替えられます。
399
+ `.wt/config.toml` で以下の設定が可能です。
348
400
 
349
- ### エイリアスの便利な使い方
350
-
351
- **VSCode ワークスペースでの活用**
352
-
353
- `current` などの固定エイリアスを VSCode のワークスペースとして開くことで、worktree を切り替えても VSCode を開き直す必要がなくなります。
354
-
355
- ```bash
356
- # 最初の作業
357
- wt add feature-a --alias current
358
- code current # VSCode で current を開く
359
-
360
- # 別の作業に切り替え(VSCode は開いたまま)
361
- wt add feature-b --alias current
362
- # current エイリアスが feature-b を指すようになる
401
+ ```toml
402
+ worktrees_dir = ".worktrees" # worktree を作成するディレクトリ名
403
+ setup_files = [".env"] # 自動セットアップでコピーするファイル一覧
363
404
  ```
364
405
 
365
- このように、エイリアスを使うことで:
366
- - VSCode のワークスペース設定が維持される
367
- - 拡張機能の設定やウィンドウレイアウトが保持される
368
- - ブランチ切り替えのたびにエディタを開き直す手間が不要
406
+ ### ローカル設定 (config.local.toml)
407
+
408
+ `config.local.toml` を作成すると、設定をローカルでのみ上書きできます。このファイルは自動的に `.gitignore` に追加され、リポジトリにはコミットされません。
369
409
 
370
410
  ## post-add フック
371
411
 
@@ -410,45 +450,33 @@ wt clone <repository_url>
410
450
  # Create a new worktree (new branch)
411
451
  wt add <work_name>
412
452
 
453
+ # Skip setup (hook execution etc)
454
+ wt add <work_name> --skip-setup
455
+
413
456
  # Create a worktree from an existing branch
414
457
  wt add <work_name> <existing_branch_name>
415
458
 
416
- # Create an alias (use "current" alias to switch between tasks)
417
- wt add <work_name> --alias current
418
-
419
459
  # List worktrees
420
460
  wt list
421
461
 
422
462
  # Remove a worktree
423
- wt rm <work_name>
463
+ wt remove <work_name>
424
464
  ```
425
465
 
426
466
  For more details, see https://github.com/igtm/easy-worktree
427
467
 
428
- ## What are Aliases?
429
-
430
- Aliases are symbolic links to worktrees. By pointing the same alias name to different worktrees, you can switch between multiple branches using a fixed path.
431
-
432
- ### Smart Use of Aliases
433
-
434
- **Using with VSCode Workspace**
468
+ ## Configuration (config.toml)
435
469
 
436
- By opening a fixed alias like `current` as a VSCode workspace, you can switch worktrees without needing to reopen VSCode.
470
+ You can customize behavior in `.wt/config.toml`:
437
471
 
438
- ```bash
439
- # First task
440
- wt add feature-a --alias current
441
- code current # Open current in VSCode
442
-
443
- # Switch to another task (VSCode stays open)
444
- wt add feature-b --alias current
445
- # The current alias now points to feature-b
472
+ ```toml
473
+ worktrees_dir = ".worktrees" # Directory where worktrees are created
474
+ setup_files = [".env"] # Files to auto-copy during setup
446
475
  ```
447
476
 
448
- Benefits of using aliases:
449
- - VSCode workspace settings are preserved
450
- - Extension settings and window layouts are maintained
451
- - No need to reopen the editor when switching branches
477
+ ### Local Configuration (config.local.toml)
478
+
479
+ You can create `config.local.toml` to override settings locally. This file is automatically added to `.gitignore` and serves as a local override that won't be committed.
452
480
 
453
481
  ## post-add Hook
454
482
 
@@ -480,6 +508,25 @@ The `post-add` hook is a script that runs automatically after creating a worktre
480
508
 
481
509
  def find_base_dir() -> Path | None:
482
510
  """現在のディレクトリまたは親ディレクトリから git root を探す"""
511
+ # ワークツリーでもメインリポジトリのルートを見つけられるように
512
+ try:
513
+ # --git-common-dir はメインリポジトリの .git ディレクトリを返す
514
+ result = run_command(["git", "rev-parse", "--git-common-dir"], check=False)
515
+ if result.returncode == 0:
516
+ git_common_dir = Path(result.stdout.strip())
517
+ if not git_common_dir.is_absolute():
518
+ # 相対パスの場合は CWD からのパス
519
+ git_common_dir = (Path.cwd() / git_common_dir).resolve()
520
+
521
+ # .git ディレクトリの親がベースディレクトリ
522
+ if git_common_dir.name == ".git":
523
+ return git_common_dir.parent
524
+ else:
525
+ # ベアリポジトリなどの場合はそのディレクトリ自体
526
+ return git_common_dir
527
+ except Exception:
528
+ pass
529
+
483
530
  try:
484
531
  result = run_command(["git", "rev-parse", "--show-toplevel"], check=False)
485
532
  if result.returncode == 0:
@@ -487,7 +534,7 @@ def find_base_dir() -> Path | None:
487
534
  except Exception:
488
535
  pass
489
536
 
490
- # git コマンドが失敗した場合、.git ディレクトリを探す
537
+ # fallback
491
538
  current = Path.cwd()
492
539
  for parent in [current] + list(current.parents):
493
540
  if (parent / ".git").exists():
@@ -615,6 +662,7 @@ def add_worktree(
615
662
  branch_to_use: str = None,
616
663
  new_branch_base: str = None,
617
664
  base_dir: Path = None,
665
+ skip_setup: bool = False,
618
666
  ) -> Path:
619
667
  """Core logic to add a worktree, reused by cmd_add and cmd_stash"""
620
668
  if not base_dir:
@@ -624,24 +672,24 @@ def add_worktree(
624
672
  print(msg("run_in_wt_dir"), file=sys.stderr)
625
673
  sys.exit(1)
626
674
 
627
- # 設定を読み込む
675
+ # settings loading
628
676
  config = load_config(base_dir)
629
677
  worktrees_dir_name = config.get("worktrees_dir", ".worktrees")
630
678
  worktrees_dir = base_dir / worktrees_dir_name
631
679
  worktrees_dir.mkdir(exist_ok=True)
632
680
 
633
- # worktree のパスを決定
681
+ # worktree path decision
634
682
  worktree_path = worktrees_dir / work_name
635
683
 
636
684
  if worktree_path.exists():
637
685
  print(msg("error", msg("already_exists", worktree_path)), file=sys.stderr)
638
686
  sys.exit(1)
639
687
 
640
- # ブランチを最新に更新
688
+ # update branch
641
689
  print(msg("fetching"), file=sys.stderr)
642
690
  run_command(["git", "fetch", "--all"], cwd=base_dir)
643
691
 
644
- # 本体 (main) base branch の最新に更新
692
+ # main update to base branch latest
645
693
  result = run_command(
646
694
  ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=base_dir, check=False
647
695
  )
@@ -657,10 +705,10 @@ def add_worktree(
657
705
  ["git", "pull", "origin", current_branch], cwd=base_dir, check=False
658
706
  )
659
707
 
660
- # ブランチ作成/チェックアウト
708
+ # create branch / checkout
661
709
  final_branch_name = None
662
710
  if new_branch_base:
663
- # 新しいブランチをベースから作成
711
+ # create new branch from base
664
712
  final_branch_name = work_name
665
713
  print(
666
714
  msg("creating_branch", final_branch_name, new_branch_base), file=sys.stderr
@@ -679,7 +727,7 @@ def add_worktree(
679
727
  check=False,
680
728
  )
681
729
  elif branch_to_use:
682
- # 指定されたブランチをチェックアウト
730
+ # checkout specified branch
683
731
  final_branch_name = branch_to_use
684
732
  print(msg("creating_worktree", worktree_path), file=sys.stderr)
685
733
  result = run_command(
@@ -688,8 +736,8 @@ def add_worktree(
688
736
  check=False,
689
737
  )
690
738
  else:
691
- # 自動判定
692
- # ブランチ名として work_name を使用
739
+ # auto detect
740
+ # use work_name as branch name
693
741
  final_branch_name = work_name
694
742
 
695
743
  check_local = run_command(
@@ -725,7 +773,7 @@ def add_worktree(
725
773
  check=False,
726
774
  )
727
775
  else:
728
- # デフォルトブランチを探す
776
+ # find default branch
729
777
  result_sym = run_command(
730
778
  ["git", "symbolic-ref", "refs/remotes/origin/HEAD", "--short"],
731
779
  cwd=base_dir,
@@ -736,7 +784,7 @@ def add_worktree(
736
784
  if result_sym.returncode == 0 and result_sym.stdout.strip():
737
785
  detected_base = result_sym.stdout.strip()
738
786
  else:
739
- # remote/local main/master の順に探す
787
+ # search in order: remote/local main/master
740
788
  for b in ["origin/main", "origin/master", "main", "master"]:
741
789
  if (
742
790
  run_command(
@@ -750,7 +798,7 @@ def add_worktree(
750
798
  break
751
799
 
752
800
  if not detected_base:
753
- # 最終手段として現在のブランチを使用
801
+ # fallback to current branch
754
802
  res_curr = run_command(
755
803
  ["git", "rev-parse", "--abbrev-ref", "HEAD"],
756
804
  cwd=base_dir,
@@ -784,20 +832,21 @@ def add_worktree(
784
832
  )
785
833
 
786
834
  if result.returncode == 0:
787
- # 自動同期
788
- if config.get("auto_copy_on_add"):
789
- sync_files = config.get("sync_files", [])
790
- for file_name in sync_files:
835
+ if not skip_setup:
836
+ # automatic sync
837
+ setup_files = config.get("setup_files", [])
838
+ for file_name in setup_files:
791
839
  src = base_dir / file_name
792
840
  dst = worktree_path / file_name
793
841
  if src.exists():
794
- print(msg("syncing", src, dst), file=sys.stderr)
842
+ print(msg("setting_up", src, dst), file=sys.stderr)
795
843
  import shutil
796
844
 
797
845
  shutil.copy2(src, dst)
798
846
 
799
- # post-add hook
800
- run_post_add_hook(worktree_path, work_name, base_dir, final_branch_name)
847
+ # post-add hook
848
+ run_post_add_hook(worktree_path, work_name, base_dir, final_branch_name)
849
+
801
850
  return worktree_path
802
851
  else:
803
852
  if result.stderr:
@@ -806,15 +855,29 @@ def add_worktree(
806
855
 
807
856
 
808
857
  def cmd_add(args: list[str]):
809
- """wt add <work_name> [<base_branch>] - Add a worktree"""
858
+ """wt add <work_name> [<base_branch>] [--skip-setup] - Add a worktree"""
810
859
  if len(args) < 1:
811
860
  print(msg("usage_add"), file=sys.stderr)
812
861
  sys.exit(1)
813
862
 
814
- work_name = args[0]
815
- branch_to_use = args[1] if len(args) >= 2 else None
863
+ # parse options
864
+ clean_args = []
865
+ skip_setup = False
866
+
867
+ for arg in args:
868
+ if arg == "--skip-setup":
869
+ skip_setup = True
870
+ else:
871
+ clean_args.append(arg)
872
+
873
+ if not clean_args:
874
+ print(msg("usage_add"), file=sys.stderr)
875
+ sys.exit(1)
876
+
877
+ work_name = clean_args[0]
878
+ branch_to_use = clean_args[1] if len(clean_args) >= 2 else None
816
879
 
817
- add_worktree(work_name, branch_to_use=branch_to_use)
880
+ add_worktree(work_name, branch_to_use=branch_to_use, skip_setup=skip_setup)
818
881
 
819
882
 
820
883
  def cmd_stash(args: list[str]):
@@ -1322,6 +1385,242 @@ def cmd_remove(args: list[str]):
1322
1385
  sys.exit(1)
1323
1386
 
1324
1387
 
1388
+ def cmd_checkout(args: list[str]):
1389
+ """wt co/checkout <work_name> - Get path to a worktree (for cd)"""
1390
+ if len(args) < 1:
1391
+ return
1392
+
1393
+ work_name = args[0]
1394
+ base_dir = find_base_dir()
1395
+ if not base_dir:
1396
+ print(msg("error", msg("base_not_found")), file=sys.stderr)
1397
+ sys.exit(1)
1398
+
1399
+ worktrees = get_worktree_info(base_dir)
1400
+ for wt in worktrees:
1401
+ p = Path(wt["path"])
1402
+ if p.name == work_name or (p == base_dir and work_name == "main"):
1403
+ print(str(p))
1404
+ return
1405
+
1406
+ print(msg("error", msg("select_not_found", work_name)), file=sys.stderr)
1407
+ sys.exit(1)
1408
+
1409
+
1410
+ def cmd_select(args: list[str]):
1411
+ """wt sl/select [<name>|-] - Manage/Switch worktree selection"""
1412
+ base_dir = find_base_dir()
1413
+ if not base_dir:
1414
+ print(msg("error", msg("base_not_found")), file=sys.stderr)
1415
+ sys.exit(1)
1416
+
1417
+ wt_dir = base_dir / ".wt"
1418
+ wt_dir.mkdir(exist_ok=True)
1419
+ last_sel_file = wt_dir / "last_selection"
1420
+
1421
+ # Get current selection name based on CWD or environment
1422
+ current_sel = os.environ.get("WT_SESSION_NAME")
1423
+ if not current_sel:
1424
+ cwd = Path.cwd().resolve()
1425
+ worktrees = get_worktree_info(base_dir)
1426
+ resolved_base = base_dir.resolve()
1427
+ for wt in worktrees:
1428
+ wt_path = Path(wt["path"]).resolve()
1429
+ if cwd == wt_path or cwd.is_relative_to(wt_path):
1430
+ current_sel = "main" if wt_path == resolved_base else wt_path.name
1431
+ break
1432
+
1433
+ worktrees = get_worktree_info(base_dir)
1434
+ names = []
1435
+ for wt in worktrees:
1436
+ p = Path(wt["path"])
1437
+ name = "main" if p == base_dir else p.name
1438
+ names.append(name)
1439
+
1440
+ if not args:
1441
+ # Interactive mode or list with highlight
1442
+ if shutil.which("fzf") and sys.stdin.isatty():
1443
+ # Run fzf
1444
+ try:
1445
+ # Prepare input for fzf with current highlighted
1446
+ fzf_input = ""
1447
+ for name in names:
1448
+ if name == current_sel:
1449
+ fzf_input += f"{name} (*)\n"
1450
+ else:
1451
+ fzf_input += f"{name}\n"
1452
+
1453
+ process = subprocess.Popen(
1454
+ ["fzf", "--height", "40%", "--reverse", "--header", "Select Worktree"],
1455
+ stdin=subprocess.PIPE,
1456
+ stdout=subprocess.PIPE,
1457
+ text=True,
1458
+ )
1459
+ stdout, _ = process.communicate(input=fzf_input)
1460
+
1461
+ if process.returncode == 0 and stdout.strip():
1462
+ selected = stdout.strip().split(" ")[0]
1463
+ switch_selection(selected, base_dir, current_sel, last_sel_file)
1464
+ return
1465
+ except Exception as e:
1466
+ print(f"fzf error: {e}", file=sys.stderr)
1467
+ # Fallback to listing
1468
+
1469
+ # List with highlight
1470
+ YELLOW = "\033[33m"
1471
+ RESET = "\033[0m"
1472
+ BOLD = "\033[1m"
1473
+
1474
+ for name in names:
1475
+ if name == current_sel:
1476
+ print(f"{YELLOW}{BOLD}{name}{RESET}")
1477
+ else:
1478
+ print(name)
1479
+ return
1480
+
1481
+ target = args[0]
1482
+
1483
+ if target == "-":
1484
+ if not last_sel_file.exists():
1485
+ print(msg("error", msg("select_no_last")), file=sys.stderr)
1486
+ sys.exit(1)
1487
+ target = last_sel_file.read_text().strip()
1488
+ if not target:
1489
+ print(msg("error", msg("select_no_last")), file=sys.stderr)
1490
+ sys.exit(1)
1491
+
1492
+ if target not in names:
1493
+ print(msg("error", msg("select_not_found", target)), file=sys.stderr)
1494
+ sys.exit(1)
1495
+
1496
+ switch_selection(target, base_dir, current_sel, last_sel_file)
1497
+
1498
+
1499
+ def cmd_current(args: list[str]):
1500
+ """wt current (cur) - Show name of the current worktree"""
1501
+ name = os.environ.get("WT_SESSION_NAME")
1502
+ if not name:
1503
+ base_dir = find_base_dir()
1504
+ if not base_dir:
1505
+ return
1506
+ cwd = Path.cwd().resolve()
1507
+ worktrees = get_worktree_info(base_dir)
1508
+ resolved_base = base_dir.resolve()
1509
+ for wt in worktrees:
1510
+ wt_path = Path(wt["path"]).resolve()
1511
+ if cwd == wt_path:
1512
+ name = "main" if wt_path == resolved_base else wt_path.name
1513
+ break
1514
+ if name:
1515
+ print(name)
1516
+
1517
+
1518
+ def switch_selection(target, base_dir, current_sel, last_sel_file):
1519
+ """Switch selection and update last_selection"""
1520
+ # Calculate target path
1521
+ target_path = base_dir
1522
+ if target != "main":
1523
+ config = load_config(base_dir)
1524
+ worktrees_dir_name = config.get("worktrees_dir", ".worktrees")
1525
+ target_path = base_dir / worktrees_dir_name / target
1526
+
1527
+ if not target_path.exists():
1528
+ print(msg("error", msg("select_not_found", target)), file=sys.stderr)
1529
+ sys.exit(1)
1530
+
1531
+ if target != current_sel:
1532
+ # Save last selection
1533
+ if current_sel:
1534
+ last_sel_file.write_text(current_sel)
1535
+
1536
+ print(msg("select_switched", target), file=sys.stderr)
1537
+
1538
+ # Check for setup files
1539
+ config = load_config(base_dir)
1540
+ setup_files = config.get("setup_files", [])
1541
+ missing = False
1542
+ for f in setup_files:
1543
+ if not (target_path / f).exists():
1544
+ missing = True
1545
+ break
1546
+ if missing:
1547
+ print(f"\033[33m{msg('suggest_setup')}\033[0m", file=sys.stderr)
1548
+
1549
+ if sys.stdout.isatty():
1550
+ # Check for nesting
1551
+ current_session = os.environ.get("WT_SESSION_NAME")
1552
+ if current_session:
1553
+ print(
1554
+ f"\033[31m{msg('nesting_error', current_session)}\033[0m", file=sys.stderr
1555
+ )
1556
+ sys.exit(1)
1557
+
1558
+ # Subshell jump
1559
+ shell = os.environ.get("SHELL", "/bin/sh")
1560
+ print(msg("jump_instruction", target, target_path), file=sys.stderr)
1561
+
1562
+ os.chdir(target_path)
1563
+ os.environ["WT_SESSION_NAME"] = target
1564
+ # Prepend to PS1 for visibility (if supported by shell)
1565
+ ps1 = os.environ.get("PS1", "$ ")
1566
+ if not ps1.startswith("(wt:"):
1567
+ os.environ["PS1"] = f"(wt:{target}) {ps1}"
1568
+
1569
+ # Set terminal title
1570
+ sys.stderr.write(f"\033]0;wt:{target}\007")
1571
+ sys.stderr.flush()
1572
+
1573
+ # Update tmux window name if inside tmux
1574
+ if os.environ.get("TMUX"):
1575
+ subprocess.run(["tmux", "rename-window", f"wt:{target}"], check=False)
1576
+
1577
+ os.execl(shell, shell)
1578
+ else:
1579
+ # Output path for script/backtick use
1580
+ print(str(target_path.absolute()))
1581
+
1582
+
1583
+ def cmd_setup(args: list[str]):
1584
+ """wt setup - Initialize current worktree (copy setup_files and run hooks)"""
1585
+ base_dir = find_base_dir()
1586
+ if not base_dir:
1587
+ print(msg("error", msg("base_not_found")), file=sys.stderr)
1588
+ sys.exit(1)
1589
+
1590
+ current_dir = Path.cwd()
1591
+ target_path = current_dir
1592
+
1593
+ config = load_config(base_dir)
1594
+ setup_files = config.get("setup_files", [])
1595
+
1596
+ import shutil
1597
+ count = 0
1598
+ for f in setup_files:
1599
+ src = base_dir / f
1600
+ dst = target_path / f
1601
+ if src.exists() and src != dst:
1602
+ print(msg("setting_up", src, dst), file=sys.stderr)
1603
+ dst.parent.mkdir(parents=True, exist_ok=True)
1604
+ shutil.copy2(src, dst)
1605
+ count += 1
1606
+
1607
+ if count > 0:
1608
+ print(msg("completed_setup", count), file=sys.stderr)
1609
+
1610
+ # Run post-add hook
1611
+ work_name = target_path.name
1612
+ if target_path == base_dir:
1613
+ work_name = "main"
1614
+
1615
+ # Get branch name for the current worktree
1616
+ branch = None
1617
+ result = run_command(["git", "branch", "--show-current"], cwd=target_path, check=False)
1618
+ if result.returncode == 0:
1619
+ branch = result.stdout.strip()
1620
+
1621
+ run_post_add_hook(target_path, work_name, base_dir, branch)
1622
+
1623
+
1325
1624
  def cmd_clean(args: list[str]):
1326
1625
  """wt clean - Remove old/unused/merged worktrees"""
1327
1626
  base_dir = find_base_dir()
@@ -1532,96 +1831,6 @@ def cmd_clean(args: list[str]):
1532
1831
  if result.stderr:
1533
1832
  print(result.stderr, file=sys.stderr)
1534
1833
 
1535
-
1536
- def cmd_sync(args: list[str]):
1537
- """wt sync [files...] [--from <name>] [--to <name>] - Sync files between worktrees"""
1538
- base_dir = find_base_dir()
1539
- if not base_dir:
1540
- print(msg("error", msg("base_not_found")), file=sys.stderr)
1541
- sys.exit(1)
1542
-
1543
- config = load_config(base_dir)
1544
- files_to_sync = []
1545
- from_name = None
1546
- to_name = None
1547
-
1548
- # 引数解析
1549
- i = 0
1550
- while i < len(args):
1551
- if args[i] == "--from" and i + 1 < len(args):
1552
- from_name = args[i + 1]
1553
- i += 2
1554
- elif args[i] == "--to" and i + 1 < len(args):
1555
- to_name = args[i + 1]
1556
- i += 2
1557
- else:
1558
- files_to_sync.append(args[i])
1559
- i += 1
1560
-
1561
- if not files_to_sync:
1562
- files_to_sync = config.get("sync_files", [])
1563
-
1564
- if not files_to_sync:
1565
- return
1566
-
1567
- worktrees = get_worktree_info(base_dir)
1568
-
1569
- # 送信元と送信先のパスを決定
1570
- from_path = base_dir
1571
- if from_name:
1572
- found = False
1573
- for wt in worktrees:
1574
- if Path(wt["path"]).name == from_name:
1575
- from_path = Path(wt["path"])
1576
- found = True
1577
- break
1578
- if not found:
1579
- print(msg("error", f"Worktree not found: {from_name}"), file=sys.stderr)
1580
- sys.exit(1)
1581
-
1582
- dest_paths = []
1583
- if to_name:
1584
- if to_name == "main":
1585
- dest_paths = [base_dir]
1586
- else:
1587
- found = False
1588
- for wt in worktrees:
1589
- if Path(wt["path"]).name == to_name:
1590
- dest_paths = [Path(wt["path"])]
1591
- found = True
1592
- break
1593
- if not found:
1594
- print(msg("error", f"Worktree not found: {to_name}"), file=sys.stderr)
1595
- sys.exit(1)
1596
- else:
1597
- # 指定がない場合は現在のディレクトリが worktree ならそこへ、そうでなければ全自動(通常は base -> current)
1598
- current_dir = Path.cwd()
1599
- if current_dir != base_dir and any(
1600
- current_dir.is_relative_to(Path(wt["path"])) for wt in worktrees
1601
- ):
1602
- dest_paths = [current_dir]
1603
- else:
1604
- # base から全 worktree へ(安全のため、ユーザーが現在の worktree にいる場合はそこだけにするのが一般的だが、ここでは全 worktree とした)
1605
- dest_paths = [
1606
- Path(wt["path"]) for wt in worktrees if Path(wt["path"]) != base_dir
1607
- ]
1608
-
1609
- import shutil
1610
-
1611
- count = 0
1612
- for dst_root in dest_paths:
1613
- if dst_root == from_path:
1614
- continue
1615
- for f in files_to_sync:
1616
- src = from_path / f
1617
- dst = dst_root / f
1618
- if src.exists():
1619
- print(msg("syncing", src, dst), file=sys.stderr)
1620
- dst.parent.mkdir(parents=True, exist_ok=True)
1621
- shutil.copy2(src, dst)
1622
- count += 1
1623
-
1624
-
1625
1834
  def cmd_passthrough(args: list[str]):
1626
1835
  """Passthrough other git worktree commands"""
1627
1836
  base_dir = find_base_dir()
@@ -1650,7 +1859,12 @@ def show_help():
1650
1859
  print(
1651
1860
  f" {'add (ad) <作業名> [<base_branch>]':<55} - worktree を追加(デフォルト: 新規ブランチ作成)"
1652
1861
  )
1862
+ print(
1863
+ f" {'select (sl) [<作業名>|-]':<55} - 作業ディレクトリを切り替え(fzf対応)"
1864
+ )
1653
1865
  print(f" {'list (ls) [--pr]':<55} - worktree 一覧を表示")
1866
+ print(f" {'co/checkout <作業名>':<55} - worktree のパスを表示")
1867
+ print(f" {'current (cur)':<55} - 現在の worktree 名を表示")
1654
1868
  print(
1655
1869
  f" {'stash (st) <作業名> [<base_branch>]':<55} - 現在の変更をスタッシュして新規 worktree に移動"
1656
1870
  )
@@ -1662,7 +1876,7 @@ def show_help():
1662
1876
  f" {'clean (cl) [--days N] [--merged] [--closed]':<55} - 不要な worktree を削除"
1663
1877
  )
1664
1878
  print(
1665
- f" {'sync (sy) [files...] [--from <名>] [--to <名>]':<55} - ファイル(.env等)を同期"
1879
+ f" {'setup (su)':<55} - 作業ディレクトリを初期化(ファイルコピー・フック実行)"
1666
1880
  )
1667
1881
  print()
1668
1882
  print("オプション:")
@@ -1680,7 +1894,12 @@ def show_help():
1680
1894
  print(
1681
1895
  f" {'add (ad) <work_name> [<base_branch>]':<55} - Add a worktree (default: create new branch)"
1682
1896
  )
1897
+ print(
1898
+ f" {'select (sl) [<name>|-]':<55} - Switch worktree selection (fzf support)"
1899
+ )
1683
1900
  print(f" {'list (ls) [--pr]':<55} - List worktrees")
1901
+ print(f" {'co/checkout <work_name>':<55} - Show path to a worktree")
1902
+ print(f" {'current (cur)':<55} - Show current worktree name")
1684
1903
  print(
1685
1904
  f" {'stash (st) <work_name> [<base_branch>]':<55} - Stash current changes and move to new worktree"
1686
1905
  )
@@ -1690,7 +1909,7 @@ def show_help():
1690
1909
  f" {'clean (cl) [--days N] [--merged] [--closed]':<55} - Remove unused/merged worktrees"
1691
1910
  )
1692
1911
  print(
1693
- f" {'sync (sy) [files...] [--from <name>] [--to <name>]':<55} - Sync files (.env, etc.)"
1912
+ f" {'setup (su)':<55} - Setup worktree (copy files and run hooks)"
1694
1913
  )
1695
1914
  print()
1696
1915
  print("Options:")
@@ -1735,12 +1954,18 @@ def main():
1735
1954
  cmd_remove(args)
1736
1955
  elif command in ["clean", "cl"]:
1737
1956
  cmd_clean(args)
1738
- elif command in ["sync", "sy"]:
1739
- cmd_sync(args)
1957
+ elif command in ["setup", "su"]:
1958
+ cmd_setup(args)
1740
1959
  elif command in ["stash", "st"]:
1741
1960
  cmd_stash(args)
1742
1961
  elif command == "pr":
1743
1962
  cmd_pr(args)
1963
+ elif command == "select" or command == "sl":
1964
+ cmd_select(args)
1965
+ elif command in ["current", "cur"]:
1966
+ cmd_current(args)
1967
+ elif command in ["co", "checkout"]:
1968
+ cmd_checkout(args)
1744
1969
  else:
1745
1970
  # その他のコマンドは git worktree にパススルー
1746
1971
  cmd_passthrough([command] + args)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easy-worktree
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Git worktree を簡単に管理するための CLI ツール
5
5
  Project-URL: Homepage, https://github.com/igtm/easy-worktree
6
6
  Project-URL: Repository, https://github.com/igtm/easy-worktree
@@ -35,11 +35,11 @@ It keeps the root of your git repository as your primary working area (main), wh
35
35
 
36
36
  ### Key Features
37
37
 
38
- - **Standardized directory structure**: Worktrees are created in a `.worktrees/` subdirectory (configurable). Keeps your root directory clean.
39
- - **Auto Sync**: Automatically sync files ignored by git (like `.env`) from the root to each worktree.
38
+ - **Smart Selection**: Quickly switch between worktrees with `wt select`. "Jump" into a new shell instantly without any special setup.
39
+ - **Auto Setup**: Automatically copy files (like `.env`) and run hooks to prepare each worktree.
40
40
  - **Clear Status**: `wt list` shows worktree branches, their status (clean/dirty), and associated GitHub PRs in a beautiful table.
41
41
  - **Smart Cleanup**: Easily batch remove merged branches or old unused worktrees.
42
- - **Two-letter shortcuts**: Fast execution with shortcuts like `ad`, `ls`, `st`, `sy`, `cl`.
42
+ - **Two-letter shortcuts**: Fast execution with shortcuts like `ad`, `ls`, `sl`, `su`, `st`, `cl`.
43
43
 
44
44
  ## Prerequisites
45
45
 
@@ -107,6 +107,14 @@ You can also specify a base branch:
107
107
  wt add feature-1 main
108
108
  ```
109
109
 
110
+ #### Skip Setup
111
+
112
+ If you want to create a worktree without running the automatic setup (file copy and hooks):
113
+
114
+ ```bash
115
+ wt add feature-1 --skip-setup
116
+ ```
117
+
110
118
  #### List worktrees
111
119
 
112
120
  ```bash
@@ -123,6 +131,23 @@ Quickly stash your current changes and move them to a new worktree.
123
131
  wt stash feature-2
124
132
  ```
125
133
 
134
+ #### Switch Worktree (shortcut: `sl`)
135
+
136
+ ```bash
137
+ wt select feature-1
138
+ ```
139
+
140
+ Running `wt select` will **automatically "jump"** you into the worktree directory by starting a new subshell.
141
+
142
+ - **Prompt**: Shows a `(wt:feature-1)` indicator.
143
+ - **Terminal Title**: Updates the window title to `wt:feature-1`.
144
+ - **Tmux**: Updates the tmux window name to `wt:feature-1` if running inside tmux.
145
+
146
+ To return to your original directory, simply type `exit` or press `Ctrl-D`.
147
+
148
+ * **Interactive Mode**: Running `wt select` without arguments opens an interactive picker using `fzf`.
149
+ * **Nesting Control**: If you are already in a `wt` subshell, it will warn you to avoid confusing nesting.
150
+
126
151
  #### PR Management
127
152
 
128
153
  Fetch a PR and create a worktree for it. (Requires `gh` CLI)
@@ -141,12 +166,63 @@ Removes the worktree and its directory.
141
166
 
142
167
  ### Useful Features
143
168
 
144
- #### Sync configuration files (shortcut: `sy`)
169
+ #### Setup Worktree (shortcut: `su`)
145
170
 
146
- Sync files like `.env` that are not in git from the root to your worktrees.
171
+ Initialize the current worktree by copying required files and running the `post-add` hook.
147
172
 
148
173
  ```bash
149
- wt sync .env
174
+ wt setup
175
+ ```
176
+
177
+ #### Visualization and External Tools
178
+
179
+ When you switch to a worktree using `wt select`, the following features are automatically enabled:
180
+ - **Terminal Title**: The window or tab title is updated to `wt:worktree-name`.
181
+ - **Tmux**: If you are inside tmux, the window name is automatically renamed to `wt:worktree-name`.
182
+
183
+ You can also use the `wt current` (or `cur`) command to display the current worktree name in external tools.
184
+
185
+ ##### Tmux Status Bar
186
+ Add the following to your `.tmux.conf` to show the worktree name in your status line:
187
+ ```tmux
188
+ set -g status-right "#(wt current) | %Y-%m-%d %H:%M"
189
+ ```
190
+
191
+ ##### Zsh / Bash Prompt
192
+ You can customize your prompt using the `$WT_SESSION_NAME` environment variable.
193
+
194
+ **Zsh (.zshrc)**:
195
+ ```zsh
196
+ RPROMPT='${WT_SESSION_NAME:+"(wt:$WT_SESSION_NAME)"} '"$RPROMPT"
197
+ ```
198
+
199
+ **Bash (.bashrc)**:
200
+ ```bash
201
+ PS1='$(if [ -n "$WT_SESSION_NAME" ]; then echo "($WT_SESSION_NAME) "; fi)'$PS1
202
+ ```
203
+
204
+ ##### Starship
205
+ Add a custom module to your `starship.toml`:
206
+ ```toml
207
+ [custom.easy_worktree]
208
+ command = "wt current"
209
+ when = 'test -n "$WT_SESSION_NAME"'
210
+ format = "via [$symbol$output]($style) "
211
+ symbol = "🌳 "
212
+ style = "bold green"
213
+ ```
214
+
215
+ ##### Powerlevel10k
216
+ Integrate beautiful worktree indicators by adding a custom segment to `.p10k.zsh`:
217
+
218
+ 1. Add `easy_worktree` to `POWERLEVEL9K_LEFT_PROMPT_ELEMENTS`.
219
+ 2. Define the following function:
220
+ ```zsh
221
+ function prompt_easy_worktree() {
222
+ if [[ -n $WT_SESSION_NAME ]]; then
223
+ p10k segment -f 255 -b 28 -i '🌳' -t "wt:$WT_SESSION_NAME"
224
+ fi
225
+ }
150
226
  ```
151
227
 
152
228
 
@@ -166,10 +242,13 @@ Customize behavior in `.wt/config.toml`:
166
242
 
167
243
  ```toml
168
244
  worktrees_dir = ".worktrees" # Directory where worktrees are created
169
- sync_files = [".env"] # Files to auto-sync
170
- auto_copy_on_add = true # Enable auto-sync on add
245
+ setup_files = [".env"] # Files to auto-copy during setup
171
246
  ```
172
247
 
248
+ #### Local Configuration Override
249
+
250
+ You can create `.wt/config.local.toml` to override settings locally. This file is automatically added to `.gitignore` and ignores `config.toml` settings.
251
+
173
252
  ## Hooks
174
253
 
175
254
  You can define scripts to run automatically after `wt add`.
@@ -0,0 +1,6 @@
1
+ easy_worktree/__init__.py,sha256=xVb2Da8saCuveeBOGt-mtycpxxdi-C_XoA8-D6nI7rU,64706
2
+ easy_worktree-0.1.3.dist-info/METADATA,sha256=kiKuXfa6l30h-ssl1pribwvZ89xOTPBYvAzLaBur8G4,6520
3
+ easy_worktree-0.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
4
+ easy_worktree-0.1.3.dist-info/entry_points.txt,sha256=Mf6MYDS2obZLvIJJFl-BbU8-SL0QGu5UWcC0FWnqtbg,42
5
+ easy_worktree-0.1.3.dist-info/licenses/LICENSE,sha256=7MGvWFDxXPqW2nrr9D7KHT0vWFiGwIUL5SQCj0IiAPc,1061
6
+ easy_worktree-0.1.3.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- easy_worktree/__init__.py,sha256=PxMDS1lRU_47fwM5dGMiMg-DPjweHxlDlRsuecO6_cs,56917
2
- easy_worktree-0.1.1.dist-info/METADATA,sha256=6Y_woHPXhCKTC8DjHKGIem2cwX83enGD6jRhZLZWAMA,4047
3
- easy_worktree-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
4
- easy_worktree-0.1.1.dist-info/entry_points.txt,sha256=Mf6MYDS2obZLvIJJFl-BbU8-SL0QGu5UWcC0FWnqtbg,42
5
- easy_worktree-0.1.1.dist-info/licenses/LICENSE,sha256=7MGvWFDxXPqW2nrr9D7KHT0vWFiGwIUL5SQCj0IiAPc,1061
6
- easy_worktree-0.1.1.dist-info/RECORD,,