easy-worktree 0.1.0__py3-none-any.whl → 0.1.1__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
@@ -2,6 +2,7 @@
2
2
  """
3
3
  Git worktree を簡単に管理するための CLI ツール
4
4
  """
5
+
5
6
  import os
6
7
  import subprocess
7
8
  import shutil
@@ -15,233 +16,180 @@ import toml
15
16
  # 言語判定
16
17
  def is_japanese() -> bool:
17
18
  """LANG環境変数から日本語かどうかを判定"""
18
- lang = os.environ.get('LANG', '')
19
- return 'ja' in lang.lower()
19
+ lang = os.environ.get("LANG", "")
20
+ return "ja" in lang.lower()
20
21
 
21
22
 
22
23
  # メッセージ辞書
23
24
  MESSAGES = {
24
- 'error': {
25
- 'en': 'Error: {}',
26
- 'ja': 'エラー: {}'
27
- },
28
- 'usage': {
29
- 'en': 'Usage: wt clone <repository_url>',
30
- 'ja': '使用方法: wt clone <repository_url>'
31
- },
32
- 'usage_add': {
33
- 'en': 'Usage: wt add (ad) <work_name> [<base_branch>]',
34
- 'ja': '使用方法: wt add (ad) <作業名> [<base_branch>]'
35
- },
36
- 'usage_rm': {
37
- 'en': 'Usage: wt rm <work_name>',
38
- 'ja': '使用方法: wt rm <作業名>'
39
- },
40
- 'base_not_found': {
41
- 'en': 'Main repository directory not found',
42
- 'ja': 'メインリポジトリのディレクトリが見つかりません'
43
- },
44
- 'run_in_wt_dir': {
45
- 'en': 'Please run inside WT_<repository_name>/ directory',
46
- 'ja': 'WT_<repository_name>/ ディレクトリ内で実行してください'
47
- },
48
- 'already_exists': {
49
- 'en': '{} already exists',
50
- 'ja': '{} はすでに存在します'
51
- },
52
- 'cloning': {
53
- 'en': 'Cloning: {} -> {}',
54
- 'ja': 'クローン中: {} -> {}'
55
- },
56
- 'completed_clone': {
57
- 'en': 'Completed: cloned to {}',
58
- 'ja': '完了: {} にクローンしました'
25
+ "error": {"en": "Error: {}", "ja": "エラー: {}"},
26
+ "usage": {
27
+ "en": "Usage: wt clone <repository_url>",
28
+ "ja": "使用方法: wt clone <repository_url>",
59
29
  },
60
- 'not_git_repo': {
61
- 'en': 'Current directory is not a git repository',
62
- 'ja': '現在のディレクトリは git リポジトリではありません'
30
+ "usage_add": {
31
+ "en": "Usage: wt add (ad) <work_name> [<base_branch>]",
32
+ "ja": "使用方法: wt add (ad) <作業名> [<base_branch>]",
63
33
  },
64
- 'run_at_root': {
65
- 'en': 'Please run at repository root directory {}',
66
- 'ja': 'リポジトリのルートディレクトリ {} で実行してください'
34
+ "usage_rm": {"en": "Usage: wt rm <work_name>", "ja": "使用方法: wt rm <作業名>"},
35
+ "base_not_found": {
36
+ "en": "Main repository directory not found",
37
+ "ja": "メインリポジトリのディレクトリが見つかりません",
67
38
  },
68
- 'creating_dir': {
69
- 'en': 'Creating {}...',
70
- 'ja': '{} を作成中...'
39
+ "run_in_wt_dir": {
40
+ "en": "Please run inside WT_<repository_name>/ directory",
41
+ "ja": "WT_<repository_name>/ ディレクトリ内で実行してください",
71
42
  },
72
- 'moving': {
73
- 'en': 'Moving {} -> {}...',
74
- 'ja': '{} -> {} に移動中...'
43
+ "already_exists": {"en": "{} already exists", "ja": "{} はすでに存在します"},
44
+ "cloning": {"en": "Cloning: {} -> {}", "ja": "クローン中: {} -> {}"},
45
+ "completed_clone": {
46
+ "en": "Completed: cloned to {}",
47
+ "ja": "完了: {} にクローンしました",
75
48
  },
76
- 'completed_move': {
77
- 'en': 'Completed: moved to {}',
78
- 'ja': '完了: {} に移動しました'
49
+ "not_git_repo": {
50
+ "en": "Current directory is not a git repository",
51
+ "ja": "現在のディレクトリは git リポジトリではありません",
79
52
  },
80
- 'use_wt_from': {
81
- 'en': 'Use wt command from {} from next time',
82
- 'ja': '次回から {} で wt コマンドを使用してください'
53
+ "run_at_root": {
54
+ "en": "Please run at repository root directory {}",
55
+ "ja": "リポジトリのルートディレクトリ {} で実行してください",
83
56
  },
84
- 'fetching': {
85
- 'en': 'Fetching latest information from remote...',
86
- 'ja': 'リモートから最新情報を取得中...'
57
+ "creating_dir": {"en": "Creating {}...", "ja": "{} を作成中..."},
58
+ "moving": {"en": "Moving {} -> {}...", "ja": "{} -> {} に移動中..."},
59
+ "completed_move": {"en": "Completed: moved to {}", "ja": "完了: {} に移動しました"},
60
+ "use_wt_from": {
61
+ "en": "Use wt command from {} from next time",
62
+ "ja": "次回から {} で wt コマンドを使用してください",
87
63
  },
88
- 'creating_worktree': {
89
- 'en': 'Creating worktree: {}',
90
- 'ja': 'worktree を作成中: {}'
64
+ "fetching": {
65
+ "en": "Fetching latest information from remote...",
66
+ "ja": "リモートから最新情報を取得中...",
91
67
  },
92
- 'completed_worktree': {
93
- 'en': 'Completed: created worktree at {}',
94
- 'ja': '完了: {} worktree を作成しました'
68
+ "creating_worktree": {"en": "Creating worktree: {}", "ja": "worktree を作成中: {}"},
69
+ "completed_worktree": {
70
+ "en": "Completed: created worktree at {}",
71
+ "ja": "完了: {} に worktree を作成しました",
95
72
  },
96
- 'removing_worktree': {
97
- 'en': 'Removing worktree: {}',
98
- 'ja': 'worktree を削除中: {}'
73
+ "removing_worktree": {"en": "Removing worktree: {}", "ja": "worktree を削除中: {}"},
74
+ "completed_remove": {
75
+ "en": "Completed: removed {}",
76
+ "ja": "完了: {} を削除しました",
99
77
  },
100
- 'completed_remove': {
101
- 'en': 'Completed: removed {}',
102
- 'ja': '完了: {} を削除しました'
78
+ "creating_branch": {
79
+ "en": "Creating new branch '{}' from '{}'",
80
+ "ja": "ブランチ '{}' を '{}' から作成しています",
103
81
  },
104
- 'creating_branch': {
105
- 'en': "Creating new branch '{}' from '{}'",
106
- 'ja': "ブランチ '{}' を '{}' から作成しています"
82
+ "default_branch_not_found": {
83
+ "en": "Could not find default branch (main/master)",
84
+ "ja": "デフォルトブランチ (main/master) が見つかりません",
107
85
  },
108
- 'default_branch_not_found': {
109
- 'en': 'Could not find default branch (main/master)',
110
- 'ja': 'デフォルトブランチ (main/master) が見つかりません'
86
+ "running_hook": {
87
+ "en": "Running post-add hook: {}",
88
+ "ja": "post-add hook を実行中: {}",
111
89
  },
112
- 'running_hook': {
113
- 'en': 'Running post-add hook: {}',
114
- 'ja': 'post-add hook を実行中: {}'
90
+ "hook_not_executable": {
91
+ "en": "Warning: hook is not executable: {}",
92
+ "ja": "警告: hook が実行可能ではありません: {}",
115
93
  },
116
- 'hook_not_executable': {
117
- 'en': 'Warning: hook is not executable: {}',
118
- 'ja': '警告: hook が実行可能ではありません: {}'
94
+ "hook_failed": {
95
+ "en": "Warning: hook exited with code {}",
96
+ "ja": "警告: hook が終了コード {} で終了しました",
119
97
  },
120
- 'hook_failed': {
121
- 'en': 'Warning: hook exited with code {}',
122
- 'ja': '警告: hook が終了コード {} で終了しました'
98
+ "usage_clean": {
99
+ "en": "Usage: wt clean (cl) [--days N] [--merged] [--closed] [--all]",
100
+ "ja": "使用方法: wt clean (cl) [--days N] [--merged] [--closed] [--all]",
123
101
  },
124
- 'usage_clean': {
125
- 'en': 'Usage: wt clean (cl) [--days N] [--merged] [--closed] [--all]',
126
- 'ja': '使用方法: wt clean (cl) [--days N] [--merged] [--closed] [--all]'
102
+ "alias_updated": {
103
+ "en": "Updated alias: {} -> {}",
104
+ "ja": "エイリアスを更新しました: {} -> {}",
127
105
  },
128
- 'alias_updated': {
129
- 'en': 'Updated alias: {} -> {}',
130
- 'ja': 'エイリアスを更新しました: {} -> {}'
106
+ "no_clean_targets": {
107
+ "en": "No worktrees to clean",
108
+ "ja": "クリーンアップ対象の worktree がありません",
131
109
  },
132
- 'no_clean_targets': {
133
- 'en': 'No worktrees to clean',
134
- 'ja': 'クリーンアップ対象の worktree がありません'
110
+ "clean_target": {
111
+ "en": "Will remove: {} (created: {}, clean)",
112
+ "ja": "削除対象: {} (作成日時: {}, 変更なし)",
135
113
  },
136
- 'clean_target': {
137
- 'en': 'Will remove: {} (created: {}, clean)',
138
- 'ja': '削除対象: {} (作成日時: {}, 変更なし)'
114
+ "clean_confirm": {
115
+ "en": "Remove {} worktree(s)? [y/N]: ",
116
+ "ja": "{} 個の worktree を削除しますか? [y/N]: ",
139
117
  },
140
- 'clean_confirm': {
141
- 'en': 'Remove {} worktree(s)? [y/N]: ',
142
- 'ja': '{} 個の worktree を削除しますか? [y/N]: '
118
+ "alias_removed": {"en": "Removed alias: {}", "ja": "エイリアスを削除しました: {}"},
119
+ "alias_not_found": {
120
+ "en": "Alias not found: {}",
121
+ "ja": "エイリアスが見つかりません: {}",
143
122
  },
144
- 'alias_removed': {
145
- 'en': 'Removed alias: {}',
146
- 'ja': 'エイリアスを削除しました: {}'
123
+ "worktree_name": {"en": "Worktree", "ja": "Worktree"},
124
+ "branch_name": {"en": "Branch", "ja": "ブランチ"},
125
+ "created_at": {"en": "Created", "ja": "作成日時"},
126
+ "last_commit": {"en": "Last Commit", "ja": "最終コミット"},
127
+ "status_label": {"en": "Status", "ja": "状態"},
128
+ "changes_label": {"en": "Changes", "ja": "変更"},
129
+ "syncing": {"en": "Syncing: {} -> {}", "ja": "同期中: {} -> {}"},
130
+ "completed_sync": {
131
+ "en": "Completed sync of {} files",
132
+ "ja": "{} 個のファイルを同期しました",
147
133
  },
148
- 'alias_not_found': {
149
- 'en': 'Alias not found: {}',
150
- 'ja': 'エイリアスが見つかりません: {}'
134
+ "usage_sync": {
135
+ "en": "Usage: wt sync (sy) [files...] [--from <name>] [--to <name>]",
136
+ "ja": "使用方法: wt sync (sy) [files...] [--from <name>] [--to <name>]",
151
137
  },
152
- 'worktree_name': {
153
- 'en': 'Worktree',
154
- 'ja': 'Worktree'
138
+ "usage_pr": {
139
+ "en": "Usage: wt pr add <number>",
140
+ "ja": "使用方法: wt pr add <number>",
155
141
  },
156
- 'branch_name': {
157
- 'en': 'Branch',
158
- 'ja': 'ブランチ'
142
+ "usage_stash": {
143
+ "en": "Usage: wt stash (st) <work_name> [<base_branch>]",
144
+ "ja": "使用方法: wt stash (st) <work_name> [<base_branch>]",
159
145
  },
160
- 'created_at': {
161
- 'en': 'Created',
162
- 'ja': '作成日時'
146
+ "stashing_changes": {
147
+ "en": "Stashing local changes...",
148
+ "ja": "ローカルの変更をスタッシュ中...",
163
149
  },
164
- 'last_commit': {
165
- 'en': 'Last Commit',
166
- 'ja': '最終コミット'
150
+ "popping_stash": {
151
+ "en": "Moving changes to new worktree...",
152
+ "ja": "変更を新しい worktree に移動中...",
167
153
  },
168
- 'status_label': {
169
- 'en': 'Status',
170
- 'ja': '状態'
154
+ "nothing_to_stash": {
155
+ "en": "No local changes to stash.",
156
+ "ja": "スタッシュする変更がありません",
171
157
  },
172
- 'changes_label': {
173
- 'en': 'Changes',
174
- 'ja': '変更'
175
- },
176
- 'syncing': {
177
- 'en': 'Syncing: {} -> {}',
178
- 'ja': '同期中: {} -> {}'
179
- },
180
- 'completed_sync': {
181
- 'en': 'Completed sync of {} files',
182
- 'ja': '{} 個のファイルを同期しました'
183
- },
184
- 'usage_sync': {
185
- 'en': 'Usage: wt sync (sy) [files...] [--from <name>] [--to <name>]',
186
- 'ja': '使用方法: wt sync (sy) [files...] [--from <name>] [--to <name>]'
187
- },
188
- 'usage_pr': {
189
- 'en': 'Usage: wt pr add <number>',
190
- 'ja': '使用方法: wt pr add <number>'
191
- },
192
- 'usage_stash': {
193
- 'en': 'Usage: wt stash (st) <work_name> [<base_branch>]',
194
- 'ja': '使用方法: wt stash (st) <work_name> [<base_branch>]'
195
- },
196
- 'stashing_changes': {
197
- 'en': 'Stashing local changes...',
198
- 'ja': 'ローカルの変更をスタッシュ中...'
199
- },
200
- 'popping_stash': {
201
- 'en': 'Moving changes to new worktree...',
202
- 'ja': '変更を新しい worktree に移動中...'
203
- },
204
- 'nothing_to_stash': {
205
- 'en': 'No local changes to stash.',
206
- 'ja': 'スタッシュする変更がありません'
207
- }
208
158
  }
209
159
 
210
160
 
211
161
  def msg(key: str, *args) -> str:
212
162
  """言語に応じたメッセージを取得"""
213
- lang = 'ja' if is_japanese() else 'en'
163
+ lang = "ja" if is_japanese() else "en"
214
164
  message = MESSAGES.get(key, {}).get(lang, key)
215
165
  if args:
216
166
  return message.format(*args)
217
167
  return message
218
168
 
219
169
 
220
- def run_command(cmd: list[str], cwd: Path = None, check: bool = True) -> subprocess.CompletedProcess:
170
+ def run_command(
171
+ cmd: list[str], cwd: Path = None, check: bool = True
172
+ ) -> subprocess.CompletedProcess:
221
173
  """コマンドを実行"""
222
174
  try:
223
175
  # print(f"DEBUG: Running command: {cmd} cwd={cwd}", file=sys.stderr)
224
176
  result = subprocess.run(
225
- cmd,
226
- cwd=cwd,
227
- capture_output=True,
228
- text=True,
229
- check=check
177
+ cmd, cwd=cwd, capture_output=True, text=True, check=check
230
178
  )
231
179
  return result
232
180
  except subprocess.CalledProcessError as e:
233
- print(msg('error', e.stderr), file=sys.stderr)
181
+ print(msg("error", e.stderr), file=sys.stderr)
234
182
  sys.exit(1)
235
183
 
236
184
 
237
185
  def get_repository_name(url: str) -> str:
238
186
  """リポジトリ URL から名前を抽出"""
239
187
  # URL から .git を削除して最後の部分を取得
240
- match = re.search(r'/([^/]+?)(?:\.git)?$', url)
188
+ match = re.search(r"/([^/]+?)(?:\.git)?$", url)
241
189
  if match:
242
190
  name = match.group(1)
243
191
  # サービス名などが含まれる場合のクリーンアップ
244
- return name.split(':')[-1]
192
+ return name.split(":")[-1]
245
193
  # ローカルパスの場合
246
194
  return Path(url).stem
247
195
 
@@ -252,17 +200,17 @@ def load_config(base_dir: Path) -> dict:
252
200
  default_config = {
253
201
  "worktrees_dir": ".worktrees",
254
202
  "sync_files": [".env"],
255
- "auto_copy_on_add": True
203
+ "auto_copy_on_add": True,
256
204
  }
257
-
205
+
258
206
  if config_file.exists():
259
207
  try:
260
- with open(config_file, 'r', encoding='utf-8') as f:
208
+ with open(config_file, "r", encoding="utf-8") as f:
261
209
  user_config = toml.load(f)
262
210
  default_config.update(user_config)
263
211
  except Exception as e:
264
- print(msg('error', f"Failed to load config: {e}"), file=sys.stderr)
265
-
212
+ print(msg("error", f"Failed to load config: {e}"), file=sys.stderr)
213
+
266
214
  return default_config
267
215
 
268
216
 
@@ -271,8 +219,8 @@ def save_config(base_dir: Path, config: dict):
271
219
  wt_dir = base_dir / ".wt"
272
220
  wt_dir.mkdir(exist_ok=True)
273
221
  config_file = wt_dir / "config.toml"
274
-
275
- with open(config_file, 'w', encoding='utf-8') as f:
222
+
223
+ with open(config_file, "w", encoding="utf-8") as f:
276
224
  toml.dump(config, f)
277
225
 
278
226
 
@@ -286,32 +234,35 @@ def create_hook_template(base_dir: Path):
286
234
  # config.toml
287
235
  config_file = wt_dir / "config.toml"
288
236
  if not config_file.exists():
289
- save_config(base_dir, {
290
- "worktrees_dir": ".worktrees",
291
- "sync_files": [".env"],
292
- "auto_copy_on_add": True
293
- })
237
+ save_config(
238
+ base_dir,
239
+ {
240
+ "worktrees_dir": ".worktrees",
241
+ "sync_files": [".env"],
242
+ "auto_copy_on_add": True,
243
+ },
244
+ )
294
245
 
295
246
  # .gitignore (repository root) に worktrees_dir を追加
296
247
  config = load_config(base_dir)
297
248
  worktrees_dir_name = config.get("worktrees_dir", ".worktrees")
298
249
  root_gitignore = base_dir / ".gitignore"
299
-
250
+
300
251
  entries = [f"{worktrees_dir_name}/"]
301
-
252
+
302
253
  if root_gitignore.exists():
303
- content = root_gitignore.read_text(encoding='utf-8')
254
+ content = root_gitignore.read_text(encoding="utf-8")
304
255
  updated = False
305
256
  for entry in entries:
306
257
  if entry not in content:
307
- if content and not content.endswith('\n'):
308
- content += '\n'
258
+ if content and not content.endswith("\n"):
259
+ content += "\n"
309
260
  content += f"{entry}\n"
310
261
  updated = True
311
262
  if updated:
312
- root_gitignore.write_text(content, encoding='utf-8')
263
+ root_gitignore.write_text(content, encoding="utf-8")
313
264
  else:
314
- root_gitignore.write_text("\n".join(entries) + "\n", encoding='utf-8')
265
+ root_gitignore.write_text("\n".join(entries) + "\n", encoding="utf-8")
315
266
 
316
267
  # post-add hook テンプレート
317
268
  hook_file = wt_dir / "post-add"
@@ -530,10 +481,7 @@ The `post-add` hook is a script that runs automatically after creating a worktre
530
481
  def find_base_dir() -> Path | None:
531
482
  """現在のディレクトリまたは親ディレクトリから git root を探す"""
532
483
  try:
533
- result = run_command(
534
- ["git", "rev-parse", "--show-toplevel"],
535
- check=False
536
- )
484
+ result = run_command(["git", "rev-parse", "--show-toplevel"], check=False)
537
485
  if result.returncode == 0:
538
486
  return Path(result.stdout.strip())
539
487
  except Exception:
@@ -551,21 +499,21 @@ def find_base_dir() -> Path | None:
551
499
  def cmd_clone(args: list[str]):
552
500
  """wt clone <repository_url> [dest_dir] - Clone a repository"""
553
501
  if len(args) < 1:
554
- print(msg('usage'), file=sys.stderr)
502
+ print(msg("usage"), file=sys.stderr)
555
503
  sys.exit(1)
556
504
 
557
505
  repo_url = args[0]
558
506
  repo_name = get_repository_name(repo_url)
559
-
507
+
560
508
  dest_dir = Path(args[1]) if len(args) > 1 else Path(repo_name)
561
509
 
562
510
  if dest_dir.exists():
563
- print(msg('error', msg('already_exists', dest_dir)), file=sys.stderr)
511
+ print(msg("error", msg("already_exists", dest_dir)), file=sys.stderr)
564
512
  sys.exit(1)
565
513
 
566
- print(msg('cloning', repo_url, dest_dir), file=sys.stderr)
514
+ print(msg("cloning", repo_url, dest_dir), file=sys.stderr)
567
515
  run_command(["git", "clone", repo_url, str(dest_dir)])
568
- print(msg('completed_clone', dest_dir), file=sys.stderr)
516
+ print(msg("completed_clone", dest_dir), file=sys.stderr)
569
517
 
570
518
  # post-add hook と設定ファイルを作成
571
519
  create_hook_template(dest_dir)
@@ -577,13 +525,11 @@ def cmd_init(args: list[str]):
577
525
 
578
526
  # 現在のディレクトリが git リポジトリか確認
579
527
  result = run_command(
580
- ["git", "rev-parse", "--show-toplevel"],
581
- cwd=current_dir,
582
- check=False
528
+ ["git", "rev-parse", "--show-toplevel"], cwd=current_dir, check=False
583
529
  )
584
530
 
585
531
  if result.returncode != 0:
586
- print(msg('error', msg('not_git_repo')), file=sys.stderr)
532
+ print(msg("error", msg("not_git_repo")), file=sys.stderr)
587
533
  sys.exit(1)
588
534
 
589
535
  git_root = Path(result.stdout.strip())
@@ -595,24 +541,35 @@ def cmd_init(args: list[str]):
595
541
  def get_default_branch(base_dir: Path) -> str:
596
542
  """Detect default branch (main/master)"""
597
543
  # 1. Try origin/HEAD
598
- result = run_command(["git", "rev-parse", "--abbrev-ref", "origin/HEAD"], cwd=base_dir, check=False)
544
+ result = run_command(
545
+ ["git", "rev-parse", "--abbrev-ref", "origin/HEAD"], cwd=base_dir, check=False
546
+ )
599
547
  if result.returncode == 0:
600
548
  return result.stdout.strip().replace("origin/", "")
601
-
549
+
602
550
  # 2. Try common names
603
551
  for b in ["main", "master"]:
604
- if run_command(["git", "rev-parse", "--verify", b], cwd=base_dir, check=False).returncode == 0:
552
+ if (
553
+ run_command(
554
+ ["git", "rev-parse", "--verify", b], cwd=base_dir, check=False
555
+ ).returncode
556
+ == 0
557
+ ):
605
558
  return b
606
559
 
607
560
  # 3. Fallback to current HEAD
608
- result = run_command(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=base_dir, check=False)
561
+ result = run_command(
562
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=base_dir, check=False
563
+ )
609
564
  if result.returncode == 0:
610
565
  return result.stdout.strip()
611
-
566
+
612
567
  return None
613
568
 
614
569
 
615
- def run_post_add_hook(worktree_path: Path, work_name: str, base_dir: Path, branch: str = None):
570
+ def run_post_add_hook(
571
+ worktree_path: Path, work_name: str, base_dir: Path, branch: str = None
572
+ ):
616
573
  """worktree 作成後の hook を実行"""
617
574
  # .wt/post-add を探す
618
575
  hook_path = base_dir / ".wt" / "post-add"
@@ -621,20 +578,22 @@ def run_post_add_hook(worktree_path: Path, work_name: str, base_dir: Path, branc
621
578
  return # hook がなければ何もしない
622
579
 
623
580
  if not os.access(hook_path, os.X_OK):
624
- print(msg('hook_not_executable', hook_path), file=sys.stderr)
581
+ print(msg("hook_not_executable", hook_path), file=sys.stderr)
625
582
  return
626
583
 
627
584
  # 環境変数を設定
628
585
  env = os.environ.copy()
629
- env.update({
630
- 'WT_WORKTREE_PATH': str(worktree_path),
631
- 'WT_WORKTREE_NAME': work_name,
632
- 'WT_BASE_DIR': str(base_dir),
633
- 'WT_BRANCH': branch or work_name,
634
- 'WT_ACTION': 'add'
635
- })
636
-
637
- print(msg('running_hook', hook_path), file=sys.stderr)
586
+ env.update(
587
+ {
588
+ "WT_WORKTREE_PATH": str(worktree_path),
589
+ "WT_WORKTREE_NAME": work_name,
590
+ "WT_BASE_DIR": str(base_dir),
591
+ "WT_BRANCH": branch or work_name,
592
+ "WT_ACTION": "add",
593
+ }
594
+ )
595
+
596
+ print(msg("running_hook", hook_path), file=sys.stderr)
638
597
  try:
639
598
  result = subprocess.run(
640
599
  [str(hook_path)],
@@ -642,22 +601,27 @@ def run_post_add_hook(worktree_path: Path, work_name: str, base_dir: Path, branc
642
601
  env=env,
643
602
  stdout=sys.stderr, # stdout を stderr にリダイレクト (cd 連携のため)
644
603
  stderr=sys.stderr,
645
- check=False
604
+ check=False,
646
605
  )
647
606
 
648
607
  if result.returncode != 0:
649
- print(msg('hook_failed', result.returncode), file=sys.stderr)
608
+ print(msg("hook_failed", result.returncode), file=sys.stderr)
650
609
  except Exception as e:
651
- print(msg('error', str(e)), file=sys.stderr)
610
+ print(msg("error", str(e)), file=sys.stderr)
652
611
 
653
612
 
654
- def add_worktree(work_name: str, branch_to_use: str = None, new_branch_base: str = None, base_dir: Path = None) -> Path:
613
+ def add_worktree(
614
+ work_name: str,
615
+ branch_to_use: str = None,
616
+ new_branch_base: str = None,
617
+ base_dir: Path = None,
618
+ ) -> Path:
655
619
  """Core logic to add a worktree, reused by cmd_add and cmd_stash"""
656
620
  if not base_dir:
657
621
  base_dir = find_base_dir()
658
622
  if not base_dir:
659
- print(msg('error', msg('base_not_found')), file=sys.stderr)
660
- print(msg('run_in_wt_dir'), file=sys.stderr)
623
+ print(msg("error", msg("base_not_found")), file=sys.stderr)
624
+ print(msg("run_in_wt_dir"), file=sys.stderr)
661
625
  sys.exit(1)
662
626
 
663
627
  # 設定を読み込む
@@ -670,48 +634,58 @@ def add_worktree(work_name: str, branch_to_use: str = None, new_branch_base: str
670
634
  worktree_path = worktrees_dir / work_name
671
635
 
672
636
  if worktree_path.exists():
673
- print(msg('error', msg('already_exists', worktree_path)), file=sys.stderr)
637
+ print(msg("error", msg("already_exists", worktree_path)), file=sys.stderr)
674
638
  sys.exit(1)
675
639
 
676
640
  # ブランチを最新に更新
677
- print(msg('fetching'), file=sys.stderr)
641
+ print(msg("fetching"), file=sys.stderr)
678
642
  run_command(["git", "fetch", "--all"], cwd=base_dir)
679
643
 
680
644
  # 本体 (main) を base branch の最新に更新
681
645
  result = run_command(
682
- ["git", "rev-parse", "--abbrev-ref", "HEAD"],
683
- cwd=base_dir,
684
- check=False
646
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=base_dir, check=False
685
647
  )
686
648
  if result.returncode == 0:
687
649
  current_branch = result.stdout.strip()
688
650
  result = run_command(
689
651
  ["git", "rev-parse", "--verify", f"origin/{current_branch}"],
690
652
  cwd=base_dir,
691
- check=False
653
+ check=False,
692
654
  )
693
655
  if result.returncode == 0:
694
- run_command(["git", "pull", "origin", current_branch], cwd=base_dir, check=False)
656
+ run_command(
657
+ ["git", "pull", "origin", current_branch], cwd=base_dir, check=False
658
+ )
695
659
 
696
660
  # ブランチ作成/チェックアウト
697
661
  final_branch_name = None
698
662
  if new_branch_base:
699
663
  # 新しいブランチをベースから作成
700
664
  final_branch_name = work_name
701
- print(msg('creating_branch', final_branch_name, new_branch_base), file=sys.stderr)
665
+ print(
666
+ msg("creating_branch", final_branch_name, new_branch_base), file=sys.stderr
667
+ )
702
668
  result = run_command(
703
- ["git", "worktree", "add", "-b", final_branch_name, str(worktree_path), new_branch_base],
669
+ [
670
+ "git",
671
+ "worktree",
672
+ "add",
673
+ "-b",
674
+ final_branch_name,
675
+ str(worktree_path),
676
+ new_branch_base,
677
+ ],
704
678
  cwd=base_dir,
705
- check=False
679
+ check=False,
706
680
  )
707
681
  elif branch_to_use:
708
682
  # 指定されたブランチをチェックアウト
709
683
  final_branch_name = branch_to_use
710
- print(msg('creating_worktree', worktree_path), file=sys.stderr)
684
+ print(msg("creating_worktree", worktree_path), file=sys.stderr)
711
685
  result = run_command(
712
686
  ["git", "worktree", "add", str(worktree_path), final_branch_name],
713
687
  cwd=base_dir,
714
- check=False
688
+ check=False,
715
689
  )
716
690
  else:
717
691
  # 自動判定
@@ -721,35 +695,41 @@ def add_worktree(work_name: str, branch_to_use: str = None, new_branch_base: str
721
695
  check_local = run_command(
722
696
  ["git", "rev-parse", "--verify", final_branch_name],
723
697
  cwd=base_dir,
724
- check=False
698
+ check=False,
725
699
  )
726
700
  check_remote = run_command(
727
701
  ["git", "rev-parse", "--verify", f"origin/{final_branch_name}"],
728
702
  cwd=base_dir,
729
- check=False
703
+ check=False,
730
704
  )
731
705
 
732
706
  if check_local.returncode == 0 or check_remote.returncode == 0:
733
707
  if check_remote.returncode == 0:
734
- print(msg('creating_worktree', worktree_path), file=sys.stderr)
708
+ print(msg("creating_worktree", worktree_path), file=sys.stderr)
735
709
  result = run_command(
736
- ["git", "worktree", "add", str(worktree_path), f"origin/{final_branch_name}"],
710
+ [
711
+ "git",
712
+ "worktree",
713
+ "add",
714
+ str(worktree_path),
715
+ f"origin/{final_branch_name}",
716
+ ],
737
717
  cwd=base_dir,
738
- check=False
718
+ check=False,
739
719
  )
740
720
  else:
741
- print(msg('creating_worktree', worktree_path), file=sys.stderr)
721
+ print(msg("creating_worktree", worktree_path), file=sys.stderr)
742
722
  result = run_command(
743
723
  ["git", "worktree", "add", str(worktree_path), final_branch_name],
744
724
  cwd=base_dir,
745
- check=False
725
+ check=False,
746
726
  )
747
727
  else:
748
728
  # デフォルトブランチを探す
749
729
  result_sym = run_command(
750
730
  ["git", "symbolic-ref", "refs/remotes/origin/HEAD", "--short"],
751
731
  cwd=base_dir,
752
- check=False
732
+ check=False,
753
733
  )
754
734
 
755
735
  detected_base = None
@@ -758,25 +738,49 @@ def add_worktree(work_name: str, branch_to_use: str = None, new_branch_base: str
758
738
  else:
759
739
  # remote/local main/master の順に探す
760
740
  for b in ["origin/main", "origin/master", "main", "master"]:
761
- if run_command(["git", "rev-parse", "--verify", b], cwd=base_dir, check=False).returncode == 0:
741
+ if (
742
+ run_command(
743
+ ["git", "rev-parse", "--verify", b],
744
+ cwd=base_dir,
745
+ check=False,
746
+ ).returncode
747
+ == 0
748
+ ):
762
749
  detected_base = b
763
750
  break
764
-
751
+
765
752
  if not detected_base:
766
753
  # 最終手段として現在のブランチを使用
767
- res_curr = run_command(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=base_dir, check=False)
754
+ res_curr = run_command(
755
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
756
+ cwd=base_dir,
757
+ check=False,
758
+ )
768
759
  if res_curr.returncode == 0:
769
760
  detected_base = res_curr.stdout.strip()
770
761
 
771
762
  if not detected_base:
772
- print(msg('error', msg('default_branch_not_found')), file=sys.stderr)
763
+ print(
764
+ msg("error", msg("default_branch_not_found")), file=sys.stderr
765
+ )
773
766
  sys.exit(1)
774
767
 
775
- print(msg('creating_branch', final_branch_name, detected_base), file=sys.stderr)
768
+ print(
769
+ msg("creating_branch", final_branch_name, detected_base),
770
+ file=sys.stderr,
771
+ )
776
772
  result = run_command(
777
- ["git", "worktree", "add", "-b", final_branch_name, str(worktree_path), detected_base],
773
+ [
774
+ "git",
775
+ "worktree",
776
+ "add",
777
+ "-b",
778
+ final_branch_name,
779
+ str(worktree_path),
780
+ detected_base,
781
+ ],
778
782
  cwd=base_dir,
779
- check=False
783
+ check=False,
780
784
  )
781
785
 
782
786
  if result.returncode == 0:
@@ -787,8 +791,9 @@ def add_worktree(work_name: str, branch_to_use: str = None, new_branch_base: str
787
791
  src = base_dir / file_name
788
792
  dst = worktree_path / file_name
789
793
  if src.exists():
790
- print(msg('syncing', src, dst), file=sys.stderr)
794
+ print(msg("syncing", src, dst), file=sys.stderr)
791
795
  import shutil
796
+
792
797
  shutil.copy2(src, dst)
793
798
 
794
799
  # post-add hook
@@ -803,24 +808,24 @@ def add_worktree(work_name: str, branch_to_use: str = None, new_branch_base: str
803
808
  def cmd_add(args: list[str]):
804
809
  """wt add <work_name> [<base_branch>] - Add a worktree"""
805
810
  if len(args) < 1:
806
- print(msg('usage_add'), file=sys.stderr)
811
+ print(msg("usage_add"), file=sys.stderr)
807
812
  sys.exit(1)
808
813
 
809
814
  work_name = args[0]
810
815
  branch_to_use = args[1] if len(args) >= 2 else None
811
-
816
+
812
817
  add_worktree(work_name, branch_to_use=branch_to_use)
813
818
 
814
819
 
815
820
  def cmd_stash(args: list[str]):
816
821
  """wt stash <work_name> [<base_branch>] - Stash changes and move to new worktree"""
817
822
  if len(args) < 1:
818
- print(msg('usage_stash'), file=sys.stderr)
823
+ print(msg("usage_stash"), file=sys.stderr)
819
824
  sys.exit(1)
820
825
 
821
826
  base_dir = find_base_dir()
822
827
  if not base_dir:
823
- print(msg('error', msg('base_not_found')), file=sys.stderr)
828
+ print(msg("error", msg("base_not_found")), file=sys.stderr)
824
829
  sys.exit(1)
825
830
 
826
831
  # 変更があるかチェック
@@ -828,59 +833,67 @@ def cmd_stash(args: list[str]):
828
833
  has_changes = bool(result.stdout.strip())
829
834
 
830
835
  if has_changes:
831
- print(msg('stashing_changes'), file=sys.stderr)
836
+ print(msg("stashing_changes"), file=sys.stderr)
832
837
  # stash する
833
838
  # -u (include untracked)
834
- run_command(["git", "stash", "push", "-u", "-m", f"easy-worktree stash for {args[0]}"], cwd=base_dir)
839
+ run_command(
840
+ ["git", "stash", "push", "-u", "-m", f"easy-worktree stash for {args[0]}"],
841
+ cwd=base_dir,
842
+ )
835
843
  else:
836
- print(msg('nothing_to_stash'), file=sys.stderr)
844
+ print(msg("nothing_to_stash"), file=sys.stderr)
837
845
 
838
846
  # 新しい worktree を作成
839
847
  work_name = args[0]
840
-
848
+
841
849
  # base_branch が指定されていない場合は現在のブランチをベースにする
842
850
  # 指定されている場合はそれをベースにする
843
851
  new_branch_base = args[1] if len(args) >= 2 else None
844
852
  if not new_branch_base:
845
- res = run_command(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=base_dir, check=False)
853
+ res = run_command(
854
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=base_dir, check=False
855
+ )
846
856
  if res.returncode == 0:
847
857
  new_branch_base = res.stdout.strip()
848
858
 
849
859
  # aliasはサポートしないでおく(とりあえずシンプルに)
850
860
  # wt stash は常に新しいブランチを作成する振る舞いにする
851
- wt_path = add_worktree(work_name, new_branch_base=new_branch_base, base_dir=base_dir)
861
+ wt_path = add_worktree(
862
+ work_name, new_branch_base=new_branch_base, base_dir=base_dir
863
+ )
852
864
 
853
865
  if has_changes and wt_path:
854
- print(msg('popping_stash'), file=sys.stderr)
866
+ print(msg("popping_stash"), file=sys.stderr)
855
867
  # 新しい worktree で stash pop
856
868
  run_command(["git", "stash", "pop"], cwd=wt_path)
857
869
 
858
870
 
859
-
860
-
861
871
  def cmd_pr(args: list[str]):
862
872
  """wt pr <add|co> <number> - Pull Request management"""
863
873
  if len(args) < 2:
864
- print(msg('usage_pr'), file=sys.stderr)
874
+ print(msg("usage_pr"), file=sys.stderr)
865
875
  sys.exit(1)
866
876
 
867
877
  subcommand = args[0]
868
878
  pr_number = args[1]
869
-
879
+
870
880
  # Ensure pr_number is a digit
871
881
  if not pr_number.isdigit():
872
- print(msg('error', f"PR number must be a digit: {pr_number}"), file=sys.stderr)
882
+ print(msg("error", f"PR number must be a digit: {pr_number}"), file=sys.stderr)
873
883
  sys.exit(1)
874
884
 
875
885
  base_dir = find_base_dir()
876
886
  if not base_dir:
877
- print(msg('error', msg('base_not_found')), file=sys.stderr)
887
+ print(msg("error", msg("base_not_found")), file=sys.stderr)
878
888
  sys.exit(1)
879
889
 
880
890
  if subcommand == "add":
881
891
  # Check if gh command exists
882
892
  if shutil.which("gh") is None:
883
- print(msg('error', "GitHub CLI (gh) is required for this command"), file=sys.stderr)
893
+ print(
894
+ msg("error", "GitHub CLI (gh) is required for this command"),
895
+ file=sys.stderr,
896
+ )
884
897
  sys.exit(1)
885
898
 
886
899
  print(f"Verifying PR #{pr_number}...", file=sys.stderr)
@@ -888,108 +901,103 @@ def cmd_pr(args: list[str]):
888
901
  verify_cmd = ["gh", "pr", "view", pr_number, "--json", "number"]
889
902
  result = run_command(verify_cmd, cwd=base_dir, check=False)
890
903
  if result.returncode != 0:
891
- print(msg('error', f"PR #{pr_number} not found (or access denied)"), file=sys.stderr)
904
+ print(
905
+ msg("error", f"PR #{pr_number} not found (or access denied)"),
906
+ file=sys.stderr,
907
+ )
892
908
  sys.exit(1)
893
909
 
894
910
  branch_name = f"pr-{pr_number}"
895
911
  worktree_name = f"pr@{pr_number}"
896
-
912
+
897
913
  print(f"Fetching PR #{pr_number} contents...", file=sys.stderr)
898
914
  # Fetch PR head to a local branch
899
915
  # git fetch origin pull/ID/head:local-branch
900
916
  fetch_cmd = ["git", "fetch", "origin", f"pull/{pr_number}/head:{branch_name}"]
901
- # We might want to handle case where origin doesn't exist or pull ref is different,
917
+ # We might want to handle case where origin doesn't exist or pull ref is different,
902
918
  # but origin pull/ID/head is standard for GitHub.
903
919
  run_command(fetch_cmd, cwd=base_dir)
904
-
920
+
905
921
  print(f"Creating worktree {worktree_name}...", file=sys.stderr)
906
922
  add_worktree(worktree_name, branch_to_use=branch_name, base_dir=base_dir)
907
-
923
+
908
924
  elif subcommand == "co":
909
925
  # Just a shortcut for checkout pr@<number>
910
926
  cmd_checkout([f"pr@{pr_number}"])
911
927
  else:
912
- print(msg('usage_pr'), file=sys.stderr)
928
+ print(msg("usage_pr"), file=sys.stderr)
913
929
  sys.exit(1)
914
930
 
915
931
 
916
932
  def get_worktree_info(base_dir: Path) -> list[dict]:
917
933
  """worktree の詳細情報を取得"""
918
- result = run_command(
919
- ["git", "worktree", "list", "--porcelain"],
920
- cwd=base_dir
921
- )
934
+ result = run_command(["git", "worktree", "list", "--porcelain"], cwd=base_dir)
922
935
 
923
936
  worktrees = []
924
937
  current = {}
925
938
 
926
- for line in result.stdout.strip().split('\n'):
939
+ for line in result.stdout.strip().split("\n"):
927
940
  if not line:
928
941
  if current:
929
942
  worktrees.append(current)
930
943
  current = {}
931
944
  continue
932
945
 
933
- if line.startswith('worktree '):
934
- current['path'] = line.split(' ', 1)[1]
935
- elif line.startswith('HEAD '):
936
- current['head'] = line.split(' ', 1)[1]
937
- elif line.startswith('branch '):
938
- current['branch'] = line.split(' ', 1)[1].replace('refs/heads/', '')
939
- elif line.startswith('detached'):
940
- current['branch'] = 'DETACHED'
946
+ if line.startswith("worktree "):
947
+ current["path"] = line.split(" ", 1)[1]
948
+ elif line.startswith("HEAD "):
949
+ current["head"] = line.split(" ", 1)[1]
950
+ elif line.startswith("branch "):
951
+ current["branch"] = line.split(" ", 1)[1].replace("refs/heads/", "")
952
+ elif line.startswith("detached"):
953
+ current["branch"] = "DETACHED"
941
954
 
942
955
  if current:
943
956
  worktrees.append(current)
944
957
 
945
958
  # 各 worktree の詳細情報を取得
946
959
  for wt in worktrees:
947
- path = Path(wt['path'])
960
+ path = Path(wt["path"])
948
961
 
949
962
  # 作成日時(ディレクトリの作成時刻)
950
963
  if path.exists():
951
964
  stat_info = path.stat()
952
- wt['created'] = datetime.fromtimestamp(stat_info.st_ctime)
965
+ wt["created"] = datetime.fromtimestamp(stat_info.st_ctime)
953
966
 
954
967
  # 最終コミット日時
955
968
  result = run_command(
956
- ["git", "log", "-1", "--format=%ct", wt.get('head', 'HEAD')],
969
+ ["git", "log", "-1", "--format=%ct", wt.get("head", "HEAD")],
957
970
  cwd=base_dir,
958
- check=False
971
+ check=False,
959
972
  )
960
973
  if result.returncode == 0 and result.stdout.strip():
961
- wt['last_commit'] = datetime.fromtimestamp(int(result.stdout.strip()))
974
+ wt["last_commit"] = datetime.fromtimestamp(int(result.stdout.strip()))
962
975
 
963
976
  # git status(変更があるか)
964
- result = run_command(
965
- ["git", "status", "--porcelain"],
966
- cwd=path,
967
- check=False
968
- )
969
- wt['is_clean'] = result.returncode == 0 and not result.stdout.strip()
970
- wt['has_untracked'] = '??' in result.stdout
977
+ result = run_command(["git", "status", "--porcelain"], cwd=path, check=False)
978
+ wt["is_clean"] = result.returncode == 0 and not result.stdout.strip()
979
+ wt["has_untracked"] = "??" in result.stdout
971
980
 
972
981
  # Diff stats取得
973
982
  result_diff = run_command(
974
- ["git", "diff", "HEAD", "--shortstat"],
975
- cwd=path,
976
- check=False
983
+ ["git", "diff", "HEAD", "--shortstat"], cwd=path, check=False
977
984
  )
978
-
985
+
979
986
  insertions = 0
980
987
  deletions = 0
981
988
  if result_diff.returncode == 0 and result_diff.stdout.strip():
982
989
  out = result_diff.stdout.strip()
983
- m_plus = re.search(r'(\d+) insertion', out)
984
- m_minus = re.search(r'(\d+) deletion', out)
985
- if m_plus: insertions = int(m_plus.group(1))
986
- if m_minus: deletions = int(m_minus.group(1))
987
-
988
- wt['insertions'] = insertions
989
- wt['deletions'] = deletions
990
+ m_plus = re.search(r"(\d+) insertion", out)
991
+ m_minus = re.search(r"(\d+) deletion", out)
992
+ if m_plus:
993
+ insertions = int(m_plus.group(1))
994
+ if m_minus:
995
+ deletions = int(m_minus.group(1))
990
996
 
991
- return worktrees
997
+ wt["insertions"] = insertions
998
+ wt["deletions"] = deletions
992
999
 
1000
+ return worktrees
993
1001
 
994
1002
  if result.returncode == 0:
995
1003
  return result.stdout.strip()
@@ -998,65 +1006,76 @@ def get_worktree_info(base_dir: Path) -> list[dict]:
998
1006
 
999
1007
  def get_pr_info(branch: str, cwd: Path = None) -> str:
1000
1008
  """Get rich GitHub PR information for the branch"""
1001
- if not branch or branch == 'HEAD' or branch == 'DETACHED':
1009
+ if not branch or branch == "HEAD" or branch == "DETACHED":
1002
1010
  return ""
1003
-
1011
+
1004
1012
  # Check if gh command exists
1005
1013
  if shutil.which("gh") is None:
1006
1014
  return ""
1007
-
1015
+
1008
1016
  import json
1009
- cmd = ["gh", "pr", "list", "--head", branch, "--state", "all", "--json", "state,isDraft,url,createdAt,number"]
1017
+
1018
+ cmd = [
1019
+ "gh",
1020
+ "pr",
1021
+ "list",
1022
+ "--head",
1023
+ branch,
1024
+ "--state",
1025
+ "all",
1026
+ "--json",
1027
+ "state,isDraft,url,createdAt,number",
1028
+ ]
1010
1029
  result = run_command(cmd, cwd=cwd, check=False)
1011
-
1030
+
1012
1031
  if result.returncode != 0 or not result.stdout.strip():
1013
1032
  return ""
1014
-
1033
+
1015
1034
  try:
1016
1035
  prs = json.loads(result.stdout)
1017
1036
  if not prs:
1018
1037
  return ""
1019
-
1038
+
1020
1039
  pr = prs[0]
1021
- state = pr['state']
1022
- is_draft = pr['isDraft']
1023
- url = pr['url']
1024
- created_at_str = pr['createdAt']
1025
- number = pr['number']
1026
-
1040
+ state = pr["state"]
1041
+ is_draft = pr["isDraft"]
1042
+ url = pr["url"]
1043
+ created_at_str = pr["createdAt"]
1044
+ number = pr["number"]
1045
+
1027
1046
  # Parse created_at
1028
1047
  # ISO format: 2024-03-20T12:00:00Z
1029
1048
  try:
1030
1049
  # Localize to local timezone
1031
- dt_aware = datetime.fromisoformat(created_at_str.replace('Z', '+00:00'))
1050
+ dt_aware = datetime.fromisoformat(created_at_str.replace("Z", "+00:00"))
1032
1051
  dt_local = dt_aware.astimezone().replace(tzinfo=None)
1033
1052
  rel_time = get_relative_time(dt_local)
1034
1053
  except Exception:
1035
1054
  rel_time = "N/A"
1036
-
1055
+
1037
1056
  # Symbols and Colors
1038
1057
  GREEN = "\033[32m"
1039
1058
  GRAY = "\033[90m"
1040
1059
  MAGENTA = "\033[35m"
1041
1060
  RED = "\033[31m"
1042
1061
  RESET = "\033[0m"
1043
-
1062
+
1044
1063
  if is_draft:
1045
1064
  symbol = f"{GRAY}◌{RESET}"
1046
1065
  elif state == "OPEN":
1047
1066
  symbol = f"{GREEN}●{RESET}"
1048
1067
  elif state == "MERGED":
1049
1068
  symbol = f"{MAGENTA}✔{RESET}"
1050
- else: # CLOSED
1069
+ else: # CLOSED
1051
1070
  symbol = f"{RED}✘{RESET}"
1052
-
1071
+
1053
1072
  # Hyperlink for #NUMBER
1054
1073
  # ANSI sequence for hyperlink: ESC ] 8 ; ; URL ESC \ TEXT ESC ] 8 ; ; ESC \
1055
1074
  link_start = f"\x1b]8;;{url}\x1b\\"
1056
1075
  link_end = "\x1b]8;;\x1b\\"
1057
-
1076
+
1058
1077
  return f"{symbol} {link_start}#{number}{link_end} ({rel_time})"
1059
-
1078
+
1060
1079
  except Exception:
1061
1080
  return ""
1062
1081
 
@@ -1065,16 +1084,16 @@ def get_relative_time(dt: datetime) -> str:
1065
1084
  """Get relative time string"""
1066
1085
  if not dt:
1067
1086
  return "N/A"
1068
-
1087
+
1069
1088
  now = datetime.now()
1070
1089
  diff = now - dt
1071
-
1090
+
1072
1091
  seconds = diff.total_seconds()
1073
1092
  days = diff.days
1074
-
1093
+
1075
1094
  if days < 0:
1076
1095
  return "just now"
1077
-
1096
+
1078
1097
  if days == 0:
1079
1098
  if seconds < 60:
1080
1099
  return "just now"
@@ -1083,17 +1102,17 @@ def get_relative_time(dt: datetime) -> str:
1083
1102
  return f"{minutes}m ago"
1084
1103
  hours = int(seconds / 3600)
1085
1104
  return f"{hours}h ago"
1086
-
1105
+
1087
1106
  if days == 1:
1088
1107
  return "yesterday"
1089
-
1108
+
1090
1109
  if days < 30:
1091
1110
  return f"{days}d ago"
1092
-
1111
+
1093
1112
  if days < 365:
1094
1113
  months = int(days / 30)
1095
1114
  return f"{months}mo ago"
1096
-
1115
+
1097
1116
  years = int(days / 365)
1098
1117
  return f"{years}y ago"
1099
1118
 
@@ -1102,32 +1121,36 @@ def cmd_list(args: list[str]):
1102
1121
  """wt list - List worktrees"""
1103
1122
  base_dir = find_base_dir()
1104
1123
  if not base_dir:
1105
- print(msg('error', msg('base_not_found')), file=sys.stderr)
1124
+ print(msg("error", msg("base_not_found")), file=sys.stderr)
1106
1125
  sys.exit(1)
1107
1126
 
1108
1127
  # --quiet / -q オプション(xargs 用)
1109
- quiet = '--quiet' in args or '-q' in args
1110
- show_pr = '--pr' in args
1128
+ quiet = "--quiet" in args or "-q" in args
1129
+ show_pr = "--pr" in args
1111
1130
 
1112
1131
  worktrees = get_worktree_info(base_dir)
1113
1132
 
1114
1133
  # ソート: 作成日時の降順(最新が上)
1115
- worktrees.sort(key=lambda x: x.get('created', datetime.min), reverse=True)
1134
+ worktrees.sort(key=lambda x: x.get("created", datetime.min), reverse=True)
1116
1135
 
1117
1136
  # PR infoの取得
1118
1137
  if show_pr:
1119
1138
  for wt in worktrees:
1120
- branch = wt.get('branch', '')
1139
+ branch = wt.get("branch", "")
1121
1140
  if branch:
1122
- wt['pr_info'] = get_pr_info(branch, cwd=base_dir)
1141
+ wt["pr_info"] = get_pr_info(branch, cwd=base_dir)
1123
1142
 
1124
1143
  # 相対時間の計算
1125
1144
  for wt in worktrees:
1126
- wt['relative_time'] = get_relative_time(wt.get('created'))
1145
+ wt["relative_time"] = get_relative_time(wt.get("created"))
1127
1146
 
1128
1147
  if quiet:
1129
1148
  for wt in worktrees:
1130
- print(Path(wt['path']).name if Path(wt['path']).name != base_dir.name else "main")
1149
+ print(
1150
+ Path(wt["path"]).name
1151
+ if Path(wt["path"]).name != base_dir.name
1152
+ else "main"
1153
+ )
1131
1154
  return
1132
1155
 
1133
1156
  # "Changes" カラムの表示文字列作成
@@ -1135,12 +1158,12 @@ def cmd_list(args: list[str]):
1135
1158
  RED = "\033[31m"
1136
1159
  GRAY = "\033[90m"
1137
1160
  RESET = "\033[0m"
1138
-
1161
+
1139
1162
  for wt in worktrees:
1140
- plus = wt.get('insertions', 0)
1141
- minus = wt.get('deletions', 0)
1142
- untracked = wt.get('has_untracked', False)
1143
-
1163
+ plus = wt.get("insertions", 0)
1164
+ minus = wt.get("deletions", 0)
1165
+ untracked = wt.get("has_untracked", False)
1166
+
1144
1167
  parts = []
1145
1168
  clean_parts = []
1146
1169
  if plus > 0:
@@ -1152,25 +1175,51 @@ def cmd_list(args: list[str]):
1152
1175
  if untracked:
1153
1176
  parts.append(f"{GRAY}??{RESET}")
1154
1177
  clean_parts.append("??")
1155
-
1178
+
1156
1179
  if not parts:
1157
- wt['changes_display'] = "-"
1158
- wt['changes_clean_len'] = 1
1180
+ wt["changes_display"] = "-"
1181
+ wt["changes_clean_len"] = 1
1159
1182
  else:
1160
- wt['changes_display'] = " ".join(parts)
1161
- wt['changes_clean_len'] = len(" ".join(clean_parts))
1183
+ wt["changes_display"] = " ".join(parts)
1184
+ wt["changes_clean_len"] = len(" ".join(clean_parts))
1162
1185
 
1163
1186
  # カラム幅の計算
1164
- name_w = max(len(msg('worktree_name')), max((len(Path(wt['path']).name) for wt in worktrees), default=0)) + 2
1165
- branch_w = max(len(msg('branch_name')), max((len(wt.get('branch', 'N/A')) for wt in worktrees), default=0)) + 2
1166
- time_w = max(len("Created"), max((len(wt.get('relative_time', '')) for wt in worktrees), default=0)) + 2
1167
- status_w = max(len(msg('changes_label')), max((wt['changes_clean_len'] for wt in worktrees), default=0)) + 2
1187
+ name_w = (
1188
+ max(
1189
+ len(msg("worktree_name")),
1190
+ max((len(Path(wt["path"]).name) for wt in worktrees), default=0),
1191
+ )
1192
+ + 2
1193
+ )
1194
+ branch_w = (
1195
+ max(
1196
+ len(msg("branch_name")),
1197
+ max((len(wt.get("branch", "N/A")) for wt in worktrees), default=0),
1198
+ )
1199
+ + 2
1200
+ )
1201
+ time_w = (
1202
+ max(
1203
+ len("Created"),
1204
+ max((len(wt.get("relative_time", "")) for wt in worktrees), default=0),
1205
+ )
1206
+ + 2
1207
+ )
1208
+ status_w = (
1209
+ max(
1210
+ len(msg("changes_label")),
1211
+ max((wt["changes_clean_len"] for wt in worktrees), default=0),
1212
+ )
1213
+ + 2
1214
+ )
1168
1215
  pr_w = 0
1169
1216
  if show_pr:
1170
1217
  # PR info contains ANSI codes, so calculate real length
1171
1218
  import re
1219
+
1172
1220
  # More robust ANSI escape regex including hyperlinks
1173
- ansi_escape = re.compile(r'''
1221
+ ansi_escape = re.compile(
1222
+ r"""
1174
1223
  \x1B(?:
1175
1224
  [@-Z\\-_]
1176
1225
  |
@@ -1178,17 +1227,26 @@ def cmd_list(args: list[str]):
1178
1227
  |
1179
1228
  \][0-9]*;.*?(\x1B\\|\x07)
1180
1229
  )
1181
- ''', re.VERBOSE)
1230
+ """,
1231
+ re.VERBOSE,
1232
+ )
1233
+
1182
1234
  def clean_len(s):
1183
- return len(ansi_escape.sub('', s))
1184
-
1185
- pr_w = max(3, max((clean_len(wt.get('pr_info', '')) for wt in worktrees), default=0)) + 2
1235
+ return len(ansi_escape.sub("", s))
1236
+
1237
+ pr_w = (
1238
+ max(
1239
+ 3,
1240
+ max((clean_len(wt.get("pr_info", "")) for wt in worktrees), default=0),
1241
+ )
1242
+ + 2
1243
+ )
1186
1244
 
1187
1245
  # ヘッダー (色付き)
1188
1246
  CYAN = "\033[36m"
1189
1247
  RESET = "\033[0m"
1190
1248
  BOLD = "\033[1m"
1191
-
1249
+
1192
1250
  base_header = f"{msg('worktree_name'):<{name_w}} {msg('branch_name'):<{branch_w}} {'Created':<{time_w}} {msg('changes_label'):<{status_w}}"
1193
1251
  if show_pr:
1194
1252
  header = f"{BOLD}{base_header} PR{RESET}"
@@ -1196,49 +1254,64 @@ def cmd_list(args: list[str]):
1196
1254
  else:
1197
1255
  header = f"{BOLD}{base_header.rstrip()}{RESET}"
1198
1256
  separator_len = len(base_header.rstrip())
1199
-
1257
+
1200
1258
  print(header)
1201
1259
  print("-" * separator_len)
1202
1260
 
1203
1261
  for wt in worktrees:
1204
- path = Path(wt['path'])
1262
+ path = Path(wt["path"])
1205
1263
  name_display = path.name if path != base_dir else f"{CYAN}(main){RESET}"
1206
1264
  name_clean_len = len(path.name) if path != base_dir else 6
1207
1265
  name_padding = " " * (name_w - name_clean_len)
1208
-
1209
- branch = wt.get('branch', 'N/A')
1210
- rel_time = wt.get('relative_time', 'N/A')
1211
- changes_display = wt.get('changes_display', 'no changes')
1212
- changes_clean_len = wt.get('changes_clean_len', 1)
1213
-
1266
+
1267
+ branch = wt.get("branch", "N/A")
1268
+ rel_time = wt.get("relative_time", "N/A")
1269
+ changes_display = wt.get("changes_display", "no changes")
1270
+ changes_clean_len = wt.get("changes_clean_len", 1)
1271
+
1214
1272
  # ANSI コード分を補正して表示
1215
1273
  changes_padding = " " * (status_w - changes_clean_len)
1216
-
1217
- print(f"{name_display}{name_padding} {branch:<{branch_w}} {rel_time:<{time_w}} {changes_display}{changes_padding}", end='')
1274
+
1275
+ print(
1276
+ f"{name_display}{name_padding} {branch:<{branch_w}} {rel_time:<{time_w}} {changes_display}{changes_padding}",
1277
+ end="",
1278
+ )
1218
1279
  if show_pr:
1219
- print(f" {wt.get('pr_info', '')}", end='')
1280
+ print(f" {wt.get('pr_info', '')}", end="")
1220
1281
  print()
1221
1282
 
1222
1283
 
1223
1284
  def cmd_remove(args: list[str]):
1224
1285
  """wt rm/remove <work_name> - Remove a worktree"""
1225
1286
  if len(args) < 1:
1226
- print(msg('usage_rm'), file=sys.stderr)
1287
+ print(msg("usage_rm"), file=sys.stderr)
1227
1288
  sys.exit(1)
1228
1289
 
1229
1290
  base_dir = find_base_dir()
1230
1291
  if not base_dir:
1231
- print(msg('error', msg('base_not_found')), file=sys.stderr)
1292
+ print(msg("error", msg("base_not_found")), file=sys.stderr)
1232
1293
  sys.exit(1)
1233
1294
 
1234
- work_name = args[0]
1295
+ # Parse flags and find worktree name
1296
+ flags = []
1297
+ work_name = None
1298
+ for arg in args:
1299
+ if arg in ["-f", "--force"]:
1300
+ flags.append(arg)
1301
+ elif not work_name:
1302
+ work_name = arg
1303
+ else:
1304
+ # Additional non-flag arguments are currently ignored or could be treated as error
1305
+ pass
1306
+
1307
+ if not work_name:
1308
+ print(msg("usage_rm"), file=sys.stderr)
1309
+ sys.exit(1)
1235
1310
 
1236
1311
  # worktree を削除
1237
- print(msg('removing_worktree', work_name), file=sys.stderr)
1312
+ print(msg("removing_worktree", work_name), file=sys.stderr)
1238
1313
  result = run_command(
1239
- ["git", "worktree", "remove", work_name],
1240
- cwd=base_dir,
1241
- check=False
1314
+ ["git", "worktree", "remove"] + flags + [work_name], cwd=base_dir, check=False
1242
1315
  )
1243
1316
 
1244
1317
  if result.returncode == 0:
@@ -1253,22 +1326,22 @@ def cmd_clean(args: list[str]):
1253
1326
  """wt clean - Remove old/unused/merged worktrees"""
1254
1327
  base_dir = find_base_dir()
1255
1328
  if not base_dir:
1256
- print(msg('error', msg('base_not_found')), file=sys.stderr)
1329
+ print(msg("error", msg("base_not_found")), file=sys.stderr)
1257
1330
  sys.exit(1)
1258
1331
 
1259
1332
  # オプションを解析
1260
1333
  # オプションを解析
1261
- clean_all = '--all' in args
1262
- clean_merged = '--merged' in args
1263
- clean_closed = '--closed' in args
1334
+ clean_all = "--all" in args
1335
+ clean_merged = "--merged" in args
1336
+ clean_closed = "--closed" in args
1264
1337
  days = None
1265
1338
 
1266
1339
  for i, arg in enumerate(args):
1267
- if arg == '--days' and i + 1 < len(args):
1340
+ if arg == "--days" and i + 1 < len(args):
1268
1341
  try:
1269
1342
  days = int(args[i + 1])
1270
1343
  except ValueError:
1271
- print(msg('error', 'Invalid days value'), file=sys.stderr)
1344
+ print(msg("error", "Invalid days value"), file=sys.stderr)
1272
1345
  sys.exit(1)
1273
1346
 
1274
1347
  # worktree 情報を取得
@@ -1288,38 +1361,53 @@ def cmd_clean(args: list[str]):
1288
1361
  # マージ済みブランチを取得
1289
1362
  merged_branches = set()
1290
1363
  merged_pr_branches = set()
1291
-
1364
+
1292
1365
  # デフォルトブランチを取得して、それに対してマージされているかを確認
1293
1366
  default_branch = get_default_branch(base_dir)
1294
1367
  default_branch_sha = None
1295
1368
  if default_branch:
1296
- res_sha = run_command(["git", "rev-parse", default_branch], cwd=base_dir, check=False)
1369
+ res_sha = run_command(
1370
+ ["git", "rev-parse", default_branch], cwd=base_dir, check=False
1371
+ )
1297
1372
  if res_sha.returncode == 0:
1298
1373
  default_branch_sha = res_sha.stdout.strip()
1299
-
1374
+
1300
1375
  if clean_merged and default_branch:
1301
1376
  # Local merged branches (merged into default_branch)
1302
- result = run_command(["git", "branch", "--merged", default_branch], cwd=base_dir, check=False)
1377
+ result = run_command(
1378
+ ["git", "branch", "--merged", default_branch], cwd=base_dir, check=False
1379
+ )
1303
1380
  if result.returncode == 0:
1304
- for line in result.stdout.split('\n'):
1381
+ for line in result.stdout.split("\n"):
1305
1382
  # Extract branch name, removing '*', '+', and whitespace
1306
1383
  line = line.strip()
1307
- if line.startswith('* ') or line.startswith('+ '):
1384
+ if line.startswith("* ") or line.startswith("+ "):
1308
1385
  line = line[2:].strip()
1309
1386
  if line:
1310
1387
  merged_branches.add(line)
1311
-
1388
+
1312
1389
  # GitHub merged PRs
1313
1390
  if shutil.which("gh"):
1314
1391
  import json
1392
+
1315
1393
  # Get last 100 merged PRs
1316
- pr_cmd = ["gh", "pr", "list", "--state", "merged", "--limit", "100", "--json", "headRefName"]
1394
+ pr_cmd = [
1395
+ "gh",
1396
+ "pr",
1397
+ "list",
1398
+ "--state",
1399
+ "merged",
1400
+ "--limit",
1401
+ "100",
1402
+ "--json",
1403
+ "headRefName",
1404
+ ]
1317
1405
  pr_res = run_command(pr_cmd, cwd=base_dir, check=False)
1318
1406
  if pr_res.returncode == 0:
1319
1407
  try:
1320
1408
  pr_data = json.loads(pr_res.stdout)
1321
1409
  for pr in pr_data:
1322
- merged_pr_branches.add(pr['headRefName'])
1410
+ merged_pr_branches.add(pr["headRefName"])
1323
1411
  except:
1324
1412
  pass
1325
1413
 
@@ -1328,14 +1416,25 @@ def cmd_clean(args: list[str]):
1328
1416
  if clean_closed:
1329
1417
  if shutil.which("gh"):
1330
1418
  import json
1419
+
1331
1420
  # Get last 100 closed PRs
1332
- pr_cmd = ["gh", "pr", "list", "--state", "closed", "--limit", "100", "--json", "headRefName"]
1421
+ pr_cmd = [
1422
+ "gh",
1423
+ "pr",
1424
+ "list",
1425
+ "--state",
1426
+ "closed",
1427
+ "--limit",
1428
+ "100",
1429
+ "--json",
1430
+ "headRefName",
1431
+ ]
1333
1432
  pr_res = run_command(pr_cmd, cwd=base_dir, check=False)
1334
1433
  if pr_res.returncode == 0:
1335
1434
  try:
1336
1435
  pr_data = json.loads(pr_res.stdout)
1337
1436
  for pr in pr_data:
1338
- closed_pr_branches.add(pr['headRefName'])
1437
+ closed_pr_branches.add(pr["headRefName"])
1339
1438
  except:
1340
1439
  pass
1341
1440
 
@@ -1344,7 +1443,7 @@ def cmd_clean(args: list[str]):
1344
1443
  now = datetime.now()
1345
1444
 
1346
1445
  for wt in worktrees:
1347
- path = Path(wt['path'])
1446
+ path = Path(wt["path"])
1348
1447
 
1349
1448
  # base (git root) は除外
1350
1449
  if path == base_dir:
@@ -1356,28 +1455,33 @@ def cmd_clean(args: list[str]):
1356
1455
 
1357
1456
  reason = None
1358
1457
  # マージ済みの場合は無条件で対象(ただし dirty でないこと)
1359
- is_merged = (wt.get('branch') in merged_branches or wt.get('branch') in merged_pr_branches)
1458
+ is_merged = (
1459
+ wt.get("branch") in merged_branches
1460
+ or wt.get("branch") in merged_pr_branches
1461
+ )
1360
1462
  if clean_merged and is_merged:
1361
1463
  # Check safeguard: if branch points to same SHA as default branch and NOT in merged_pr_branches
1362
1464
  # it might be a new branch that hasn't diverged yet.
1363
- if default_branch_sha and wt.get('branch') not in merged_pr_branches:
1364
- wt_sha = run_command(["git", "rev-parse", wt.get('branch')], cwd=base_dir, check=False).stdout.strip()
1465
+ if default_branch_sha and wt.get("branch") not in merged_pr_branches:
1466
+ wt_sha = run_command(
1467
+ ["git", "rev-parse", wt.get("branch")], cwd=base_dir, check=False
1468
+ ).stdout.strip()
1365
1469
  if wt_sha == default_branch_sha:
1366
1470
  # Skip deletion for fresh branches
1367
1471
  continue
1368
1472
 
1369
- if wt.get('is_clean'):
1473
+ if wt.get("is_clean"):
1370
1474
  reason = "merged"
1371
1475
 
1372
- is_closed = wt.get('branch') in closed_pr_branches
1476
+ is_closed = wt.get("branch") in closed_pr_branches
1373
1477
  if not reason and clean_closed and is_closed:
1374
- if wt.get('is_clean'):
1478
+ if wt.get("is_clean"):
1375
1479
  reason = "closed"
1376
-
1480
+
1377
1481
  # 通常のクリーンアップ対象
1378
- if not reason and wt.get('is_clean'):
1482
+ if not reason and wt.get("is_clean"):
1379
1483
  if days is not None:
1380
- created = wt.get('created')
1484
+ created = wt.get("created")
1381
1485
  if created:
1382
1486
  age_days = (now - created).days
1383
1487
  if age_days >= days:
@@ -1386,24 +1490,28 @@ def cmd_clean(args: list[str]):
1386
1490
  reason = "clean"
1387
1491
 
1388
1492
  if reason:
1389
- wt['reason'] = reason
1493
+ wt["reason"] = reason
1390
1494
  targets.append(wt)
1391
1495
 
1392
1496
  if not targets:
1393
- print(msg('no_clean_targets'), file=sys.stderr)
1497
+ print(msg("no_clean_targets"), file=sys.stderr)
1394
1498
  return
1395
1499
 
1396
1500
  # 削除対象を表示
1397
1501
  for wt in targets:
1398
- path = Path(wt['path'])
1399
- created = wt.get('created').strftime('%Y-%m-%d %H:%M') if wt.get('created') else 'N/A'
1400
- print(f"{path.name} (reason: {wt['reason']}, created: {created})", file=sys.stderr)
1502
+ path = Path(wt["path"])
1503
+ created = (
1504
+ wt.get("created").strftime("%Y-%m-%d %H:%M") if wt.get("created") else "N/A"
1505
+ )
1506
+ print(
1507
+ f"{path.name} (reason: {wt['reason']}, created: {created})", file=sys.stderr
1508
+ )
1401
1509
 
1402
1510
  # 確認
1403
1511
  if not clean_all:
1404
1512
  try:
1405
- response = input(msg('clean_confirm', len(targets)))
1406
- if response.lower() not in ['y', 'yes']:
1513
+ response = input(msg("clean_confirm", len(targets)))
1514
+ if response.lower() not in ["y", "yes"]:
1407
1515
  print("Cancelled.")
1408
1516
  return
1409
1517
  except (EOFError, KeyboardInterrupt):
@@ -1412,12 +1520,10 @@ def cmd_clean(args: list[str]):
1412
1520
 
1413
1521
  # 削除実行
1414
1522
  for wt in targets:
1415
- path = Path(wt['path'])
1416
- print(msg('removing_worktree', path.name), file=sys.stderr)
1523
+ path = Path(wt["path"])
1524
+ print(msg("removing_worktree", path.name), file=sys.stderr)
1417
1525
  result = run_command(
1418
- ["git", "worktree", "remove", str(path)],
1419
- cwd=base_dir,
1420
- check=False
1526
+ ["git", "worktree", "remove", str(path)], cwd=base_dir, check=False
1421
1527
  )
1422
1528
 
1423
1529
  if result.returncode == 0:
@@ -1427,15 +1533,11 @@ def cmd_clean(args: list[str]):
1427
1533
  print(result.stderr, file=sys.stderr)
1428
1534
 
1429
1535
 
1430
-
1431
-
1432
-
1433
-
1434
1536
  def cmd_sync(args: list[str]):
1435
1537
  """wt sync [files...] [--from <name>] [--to <name>] - Sync files between worktrees"""
1436
1538
  base_dir = find_base_dir()
1437
1539
  if not base_dir:
1438
- print(msg('error', msg('base_not_found')), file=sys.stderr)
1540
+ print(msg("error", msg("base_not_found")), file=sys.stderr)
1439
1541
  sys.exit(1)
1440
1542
 
1441
1543
  config = load_config(base_dir)
@@ -1446,11 +1548,11 @@ def cmd_sync(args: list[str]):
1446
1548
  # 引数解析
1447
1549
  i = 0
1448
1550
  while i < len(args):
1449
- if args[i] == '--from' and i + 1 < len(args):
1450
- from_name = args[i+1]
1551
+ if args[i] == "--from" and i + 1 < len(args):
1552
+ from_name = args[i + 1]
1451
1553
  i += 2
1452
- elif args[i] == '--to' and i + 1 < len(args):
1453
- to_name = args[i+1]
1554
+ elif args[i] == "--to" and i + 1 < len(args):
1555
+ to_name = args[i + 1]
1454
1556
  i += 2
1455
1557
  else:
1456
1558
  files_to_sync.append(args[i])
@@ -1463,18 +1565,18 @@ def cmd_sync(args: list[str]):
1463
1565
  return
1464
1566
 
1465
1567
  worktrees = get_worktree_info(base_dir)
1466
-
1568
+
1467
1569
  # 送信元と送信先のパスを決定
1468
1570
  from_path = base_dir
1469
1571
  if from_name:
1470
1572
  found = False
1471
1573
  for wt in worktrees:
1472
- if Path(wt['path']).name == from_name:
1473
- from_path = Path(wt['path'])
1574
+ if Path(wt["path"]).name == from_name:
1575
+ from_path = Path(wt["path"])
1474
1576
  found = True
1475
1577
  break
1476
1578
  if not found:
1477
- print(msg('error', f"Worktree not found: {from_name}"), file=sys.stderr)
1579
+ print(msg("error", f"Worktree not found: {from_name}"), file=sys.stderr)
1478
1580
  sys.exit(1)
1479
1581
 
1480
1582
  dest_paths = []
@@ -1484,23 +1586,28 @@ def cmd_sync(args: list[str]):
1484
1586
  else:
1485
1587
  found = False
1486
1588
  for wt in worktrees:
1487
- if Path(wt['path']).name == to_name:
1488
- dest_paths = [Path(wt['path'])]
1589
+ if Path(wt["path"]).name == to_name:
1590
+ dest_paths = [Path(wt["path"])]
1489
1591
  found = True
1490
1592
  break
1491
1593
  if not found:
1492
- print(msg('error', f"Worktree not found: {to_name}"), file=sys.stderr)
1594
+ print(msg("error", f"Worktree not found: {to_name}"), file=sys.stderr)
1493
1595
  sys.exit(1)
1494
1596
  else:
1495
1597
  # 指定がない場合は現在のディレクトリが worktree ならそこへ、そうでなければ全自動(通常は base -> current)
1496
1598
  current_dir = Path.cwd()
1497
- if current_dir != base_dir and any(current_dir.is_relative_to(Path(wt['path'])) for wt in worktrees):
1599
+ if current_dir != base_dir and any(
1600
+ current_dir.is_relative_to(Path(wt["path"])) for wt in worktrees
1601
+ ):
1498
1602
  dest_paths = [current_dir]
1499
1603
  else:
1500
1604
  # base から全 worktree へ(安全のため、ユーザーが現在の worktree にいる場合はそこだけにするのが一般的だが、ここでは全 worktree とした)
1501
- dest_paths = [Path(wt['path']) for wt in worktrees if Path(wt['path']) != base_dir]
1605
+ dest_paths = [
1606
+ Path(wt["path"]) for wt in worktrees if Path(wt["path"]) != base_dir
1607
+ ]
1502
1608
 
1503
1609
  import shutil
1610
+
1504
1611
  count = 0
1505
1612
  for dst_root in dest_paths:
1506
1613
  if dst_root == from_path:
@@ -1509,7 +1616,7 @@ def cmd_sync(args: list[str]):
1509
1616
  src = from_path / f
1510
1617
  dst = dst_root / f
1511
1618
  if src.exists():
1512
- print(msg('syncing', src, dst), file=sys.stderr)
1619
+ print(msg("syncing", src, dst), file=sys.stderr)
1513
1620
  dst.parent.mkdir(parents=True, exist_ok=True)
1514
1621
  shutil.copy2(src, dst)
1515
1622
  count += 1
@@ -1519,13 +1626,13 @@ def cmd_passthrough(args: list[str]):
1519
1626
  """Passthrough other git worktree commands"""
1520
1627
  base_dir = find_base_dir()
1521
1628
  if not base_dir:
1522
- print(msg('error', msg('base_not_found')), file=sys.stderr)
1629
+ print(msg("error", msg("base_not_found")), file=sys.stderr)
1523
1630
  sys.exit(1)
1524
1631
 
1525
1632
  result = run_command(["git", "worktree"] + args, cwd=base_dir, check=False)
1526
- print(result.stdout, end='')
1633
+ print(result.stdout, end="")
1527
1634
  if result.stderr:
1528
- print(result.stderr, end='', file=sys.stderr)
1635
+ print(result.stderr, end="", file=sys.stderr)
1529
1636
  sys.exit(result.returncode)
1530
1637
 
1531
1638
 
@@ -1540,13 +1647,23 @@ def show_help():
1540
1647
  print("コマンド:")
1541
1648
  print(f" {'clone <repository_url>':<55} - リポジトリをクローン")
1542
1649
  print(f" {'init':<55} - 既存リポジトリをメインリポジトリとして構成")
1543
- print(f" {'add (ad) <作業名> [<base_branch>]':<55} - worktree を追加(デフォルト: 新規ブランチ作成)")
1650
+ print(
1651
+ f" {'add (ad) <作業名> [<base_branch>]':<55} - worktree を追加(デフォルト: 新規ブランチ作成)"
1652
+ )
1544
1653
  print(f" {'list (ls) [--pr]':<55} - worktree 一覧を表示")
1545
- print(f" {'stash (st) <作業名> [<base_branch>]':<55} - 現在の変更をスタッシュして新規 worktree に移動")
1546
- print(f" {'pr add <番号>':<55} - GitHub PR を取得して worktree を作成/パス表示")
1547
- print(f" {'rm/remove <作業名>':<55} - worktree を削除")
1548
- print(f" {'clean (cl) [--days N] [--merged] [--closed]':<55} - 不要な worktree を削除")
1549
- print(f" {'sync (sy) [files...] [--from <名>] [--to <名>]':<55} - ファイル(.env等)を同期")
1654
+ print(
1655
+ f" {'stash (st) <作業名> [<base_branch>]':<55} - 現在の変更をスタッシュして新規 worktree に移動"
1656
+ )
1657
+ print(
1658
+ f" {'pr add <番号>':<55} - GitHub PR を取得して worktree を作成/パス表示"
1659
+ )
1660
+ print(f" {'rm/remove <作業名> [-f|--force]':<55} - worktree を削除")
1661
+ print(
1662
+ f" {'clean (cl) [--days N] [--merged] [--closed]':<55} - 不要な worktree を削除"
1663
+ )
1664
+ print(
1665
+ f" {'sync (sy) [files...] [--from <名>] [--to <名>]':<55} - ファイル(.env等)を同期"
1666
+ )
1550
1667
  print()
1551
1668
  print("オプション:")
1552
1669
  print(f" {'-h, --help':<55} - このヘルプメッセージを表示")
@@ -1560,13 +1677,21 @@ def show_help():
1560
1677
  print("Commands:")
1561
1678
  print(f" {'clone <repository_url>':<55} - Clone a repository")
1562
1679
  print(f" {'init':<55} - Configure existing repository as main")
1563
- print(f" {'add (ad) <work_name> [<base_branch>]':<55} - Add a worktree (default: create new branch)")
1680
+ print(
1681
+ f" {'add (ad) <work_name> [<base_branch>]':<55} - Add a worktree (default: create new branch)"
1682
+ )
1564
1683
  print(f" {'list (ls) [--pr]':<55} - List worktrees")
1565
- print(f" {'stash (st) <work_name> [<base_branch>]':<55} - Stash current changes and move to new worktree")
1684
+ print(
1685
+ f" {'stash (st) <work_name> [<base_branch>]':<55} - Stash current changes and move to new worktree"
1686
+ )
1566
1687
  print(f" {'pr add <number>':<55} - Manage GitHub PRs as worktrees")
1567
- print(f" {'rm/remove <work_name>':<55} - Remove a worktree")
1568
- print(f" {'clean (cl) [--days N] [--merged] [--closed]':<55} - Remove unused/merged worktrees")
1569
- print(f" {'sync (sy) [files...] [--from <name>] [--to <name>]':<55} - Sync files (.env, etc.)")
1688
+ print(f" {'rm/remove <work_name> [-f|--force]':<55} - Remove a worktree")
1689
+ print(
1690
+ f" {'clean (cl) [--days N] [--merged] [--closed]':<55} - Remove unused/merged worktrees"
1691
+ )
1692
+ print(
1693
+ f" {'sync (sy) [files...] [--from <name>] [--to <name>]':<55} - Sync files (.env, etc.)"
1694
+ )
1570
1695
  print()
1571
1696
  print("Options:")
1572
1697
  print(f" {'-h, --help':<55} - Show this help message")
@@ -1575,7 +1700,7 @@ def show_help():
1575
1700
 
1576
1701
  def show_version():
1577
1702
  """Show version information"""
1578
- print("easy-worktree version 0.1.0")
1703
+ print("easy-worktree version 0.1.1")
1579
1704
 
1580
1705
 
1581
1706
  def main():