easy-worktree 0.1.1__py3-none-any.whl → 0.1.2__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 +315 -161
- {easy_worktree-0.1.1.dist-info → easy_worktree-0.1.2.dist-info}/METADATA +77 -9
- easy_worktree-0.1.2.dist-info/RECORD +6 -0
- easy_worktree-0.1.1.dist-info/RECORD +0 -6
- {easy_worktree-0.1.1.dist-info → easy_worktree-0.1.2.dist-info}/WHEEL +0 -0
- {easy_worktree-0.1.1.dist-info → easy_worktree-0.1.2.dist-info}/entry_points.txt +0 -0
- {easy_worktree-0.1.1.dist-info → easy_worktree-0.1.2.dist-info}/licenses/LICENSE +0 -0
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
|
|
|
@@ -199,7 +228,7 @@ def load_config(base_dir: Path) -> dict:
|
|
|
199
228
|
config_file = base_dir / ".wt" / "config.toml"
|
|
200
229
|
default_config = {
|
|
201
230
|
"worktrees_dir": ".worktrees",
|
|
202
|
-
"
|
|
231
|
+
"setup_files": [".env"],
|
|
203
232
|
"auto_copy_on_add": True,
|
|
204
233
|
}
|
|
205
234
|
|
|
@@ -238,7 +267,7 @@ def create_hook_template(base_dir: Path):
|
|
|
238
267
|
base_dir,
|
|
239
268
|
{
|
|
240
269
|
"worktrees_dir": ".worktrees",
|
|
241
|
-
"
|
|
270
|
+
"setup_files": [".env"],
|
|
242
271
|
"auto_copy_on_add": True,
|
|
243
272
|
},
|
|
244
273
|
)
|
|
@@ -330,9 +359,6 @@ wt add <作業名>
|
|
|
330
359
|
# 既存ブランチから worktree を作成
|
|
331
360
|
wt add <作業名> <既存ブランチ名>
|
|
332
361
|
|
|
333
|
-
# エイリアスを作成(current エイリアスで現在の作業を切り替え)
|
|
334
|
-
wt add <作業名> --alias current
|
|
335
|
-
|
|
336
362
|
# worktree 一覧を表示
|
|
337
363
|
wt list
|
|
338
364
|
|
|
@@ -342,31 +368,6 @@ wt rm <作業名>
|
|
|
342
368
|
|
|
343
369
|
詳細は https://github.com/igtm/easy-worktree を参照してください。
|
|
344
370
|
|
|
345
|
-
## エイリアスとは
|
|
346
|
-
|
|
347
|
-
エイリアスは、worktree へのシンボリックリンク(symbolic link)です。同じエイリアス名で異なる worktree を指すことで、固定されたパスで複数のブランチを切り替えられます。
|
|
348
|
-
|
|
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 を指すようになる
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
このように、エイリアスを使うことで:
|
|
366
|
-
- VSCode のワークスペース設定が維持される
|
|
367
|
-
- 拡張機能の設定やウィンドウレイアウトが保持される
|
|
368
|
-
- ブランチ切り替えのたびにエディタを開き直す手間が不要
|
|
369
|
-
|
|
370
371
|
## post-add フック
|
|
371
372
|
|
|
372
373
|
`post-add` フックは、worktree 作成後に自動実行されるスクリプトです。
|
|
@@ -413,43 +414,15 @@ wt add <work_name>
|
|
|
413
414
|
# Create a worktree from an existing branch
|
|
414
415
|
wt add <work_name> <existing_branch_name>
|
|
415
416
|
|
|
416
|
-
# Create an alias (use "current" alias to switch between tasks)
|
|
417
|
-
wt add <work_name> --alias current
|
|
418
|
-
|
|
419
417
|
# List worktrees
|
|
420
418
|
wt list
|
|
421
419
|
|
|
422
420
|
# Remove a worktree
|
|
423
|
-
wt
|
|
421
|
+
wt remove <work_name>
|
|
424
422
|
```
|
|
425
423
|
|
|
426
424
|
For more details, see https://github.com/igtm/easy-worktree
|
|
427
425
|
|
|
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**
|
|
435
|
-
|
|
436
|
-
By opening a fixed alias like `current` as a VSCode workspace, you can switch worktrees without needing to reopen VSCode.
|
|
437
|
-
|
|
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
|
|
446
|
-
```
|
|
447
|
-
|
|
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
|
|
452
|
-
|
|
453
426
|
## post-add Hook
|
|
454
427
|
|
|
455
428
|
The `post-add` hook is a script that runs automatically after creating a worktree.
|
|
@@ -480,6 +453,25 @@ The `post-add` hook is a script that runs automatically after creating a worktre
|
|
|
480
453
|
|
|
481
454
|
def find_base_dir() -> Path | None:
|
|
482
455
|
"""現在のディレクトリまたは親ディレクトリから git root を探す"""
|
|
456
|
+
# ワークツリーでもメインリポジトリのルートを見つけられるように
|
|
457
|
+
try:
|
|
458
|
+
# --git-common-dir はメインリポジトリの .git ディレクトリを返す
|
|
459
|
+
result = run_command(["git", "rev-parse", "--git-common-dir"], check=False)
|
|
460
|
+
if result.returncode == 0:
|
|
461
|
+
git_common_dir = Path(result.stdout.strip())
|
|
462
|
+
if not git_common_dir.is_absolute():
|
|
463
|
+
# 相対パスの場合は CWD からのパス
|
|
464
|
+
git_common_dir = (Path.cwd() / git_common_dir).resolve()
|
|
465
|
+
|
|
466
|
+
# .git ディレクトリの親がベースディレクトリ
|
|
467
|
+
if git_common_dir.name == ".git":
|
|
468
|
+
return git_common_dir.parent
|
|
469
|
+
else:
|
|
470
|
+
# ベアリポジトリなどの場合はそのディレクトリ自体
|
|
471
|
+
return git_common_dir
|
|
472
|
+
except Exception:
|
|
473
|
+
pass
|
|
474
|
+
|
|
483
475
|
try:
|
|
484
476
|
result = run_command(["git", "rev-parse", "--show-toplevel"], check=False)
|
|
485
477
|
if result.returncode == 0:
|
|
@@ -487,7 +479,7 @@ def find_base_dir() -> Path | None:
|
|
|
487
479
|
except Exception:
|
|
488
480
|
pass
|
|
489
481
|
|
|
490
|
-
#
|
|
482
|
+
# fallback
|
|
491
483
|
current = Path.cwd()
|
|
492
484
|
for parent in [current] + list(current.parents):
|
|
493
485
|
if (parent / ".git").exists():
|
|
@@ -786,12 +778,12 @@ def add_worktree(
|
|
|
786
778
|
if result.returncode == 0:
|
|
787
779
|
# 自動同期
|
|
788
780
|
if config.get("auto_copy_on_add"):
|
|
789
|
-
|
|
790
|
-
for file_name in
|
|
781
|
+
setup_files = config.get("setup_files", [])
|
|
782
|
+
for file_name in setup_files:
|
|
791
783
|
src = base_dir / file_name
|
|
792
784
|
dst = worktree_path / file_name
|
|
793
785
|
if src.exists():
|
|
794
|
-
print(msg("
|
|
786
|
+
print(msg("setting_up", src, dst), file=sys.stderr)
|
|
795
787
|
import shutil
|
|
796
788
|
|
|
797
789
|
shutil.copy2(src, dst)
|
|
@@ -1322,6 +1314,242 @@ def cmd_remove(args: list[str]):
|
|
|
1322
1314
|
sys.exit(1)
|
|
1323
1315
|
|
|
1324
1316
|
|
|
1317
|
+
def cmd_checkout(args: list[str]):
|
|
1318
|
+
"""wt co/checkout <work_name> - Get path to a worktree (for cd)"""
|
|
1319
|
+
if len(args) < 1:
|
|
1320
|
+
return
|
|
1321
|
+
|
|
1322
|
+
work_name = args[0]
|
|
1323
|
+
base_dir = find_base_dir()
|
|
1324
|
+
if not base_dir:
|
|
1325
|
+
print(msg("error", msg("base_not_found")), file=sys.stderr)
|
|
1326
|
+
sys.exit(1)
|
|
1327
|
+
|
|
1328
|
+
worktrees = get_worktree_info(base_dir)
|
|
1329
|
+
for wt in worktrees:
|
|
1330
|
+
p = Path(wt["path"])
|
|
1331
|
+
if p.name == work_name or (p == base_dir and work_name == "main"):
|
|
1332
|
+
print(str(p))
|
|
1333
|
+
return
|
|
1334
|
+
|
|
1335
|
+
print(msg("error", msg("select_not_found", work_name)), file=sys.stderr)
|
|
1336
|
+
sys.exit(1)
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
def cmd_select(args: list[str]):
|
|
1340
|
+
"""wt sl/select [<name>|-] - Manage/Switch worktree selection"""
|
|
1341
|
+
base_dir = find_base_dir()
|
|
1342
|
+
if not base_dir:
|
|
1343
|
+
print(msg("error", msg("base_not_found")), file=sys.stderr)
|
|
1344
|
+
sys.exit(1)
|
|
1345
|
+
|
|
1346
|
+
wt_dir = base_dir / ".wt"
|
|
1347
|
+
wt_dir.mkdir(exist_ok=True)
|
|
1348
|
+
last_sel_file = wt_dir / "last_selection"
|
|
1349
|
+
|
|
1350
|
+
# Get current selection name based on CWD or environment
|
|
1351
|
+
current_sel = os.environ.get("WT_SESSION_NAME")
|
|
1352
|
+
if not current_sel:
|
|
1353
|
+
cwd = Path.cwd().resolve()
|
|
1354
|
+
worktrees = get_worktree_info(base_dir)
|
|
1355
|
+
resolved_base = base_dir.resolve()
|
|
1356
|
+
for wt in worktrees:
|
|
1357
|
+
wt_path = Path(wt["path"]).resolve()
|
|
1358
|
+
if cwd == wt_path or cwd.is_relative_to(wt_path):
|
|
1359
|
+
current_sel = "main" if wt_path == resolved_base else wt_path.name
|
|
1360
|
+
break
|
|
1361
|
+
|
|
1362
|
+
worktrees = get_worktree_info(base_dir)
|
|
1363
|
+
names = []
|
|
1364
|
+
for wt in worktrees:
|
|
1365
|
+
p = Path(wt["path"])
|
|
1366
|
+
name = "main" if p == base_dir else p.name
|
|
1367
|
+
names.append(name)
|
|
1368
|
+
|
|
1369
|
+
if not args:
|
|
1370
|
+
# Interactive mode or list with highlight
|
|
1371
|
+
if shutil.which("fzf") and sys.stdin.isatty():
|
|
1372
|
+
# Run fzf
|
|
1373
|
+
try:
|
|
1374
|
+
# Prepare input for fzf with current highlighted
|
|
1375
|
+
fzf_input = ""
|
|
1376
|
+
for name in names:
|
|
1377
|
+
if name == current_sel:
|
|
1378
|
+
fzf_input += f"{name} (*)\n"
|
|
1379
|
+
else:
|
|
1380
|
+
fzf_input += f"{name}\n"
|
|
1381
|
+
|
|
1382
|
+
process = subprocess.Popen(
|
|
1383
|
+
["fzf", "--height", "40%", "--reverse", "--header", "Select Worktree"],
|
|
1384
|
+
stdin=subprocess.PIPE,
|
|
1385
|
+
stdout=subprocess.PIPE,
|
|
1386
|
+
text=True,
|
|
1387
|
+
)
|
|
1388
|
+
stdout, _ = process.communicate(input=fzf_input)
|
|
1389
|
+
|
|
1390
|
+
if process.returncode == 0 and stdout.strip():
|
|
1391
|
+
selected = stdout.strip().split(" ")[0]
|
|
1392
|
+
switch_selection(selected, base_dir, current_sel, last_sel_file)
|
|
1393
|
+
return
|
|
1394
|
+
except Exception as e:
|
|
1395
|
+
print(f"fzf error: {e}", file=sys.stderr)
|
|
1396
|
+
# Fallback to listing
|
|
1397
|
+
|
|
1398
|
+
# List with highlight
|
|
1399
|
+
YELLOW = "\033[33m"
|
|
1400
|
+
RESET = "\033[0m"
|
|
1401
|
+
BOLD = "\033[1m"
|
|
1402
|
+
|
|
1403
|
+
for name in names:
|
|
1404
|
+
if name == current_sel:
|
|
1405
|
+
print(f"{YELLOW}{BOLD}{name}{RESET}")
|
|
1406
|
+
else:
|
|
1407
|
+
print(name)
|
|
1408
|
+
return
|
|
1409
|
+
|
|
1410
|
+
target = args[0]
|
|
1411
|
+
|
|
1412
|
+
if target == "-":
|
|
1413
|
+
if not last_sel_file.exists():
|
|
1414
|
+
print(msg("error", msg("select_no_last")), file=sys.stderr)
|
|
1415
|
+
sys.exit(1)
|
|
1416
|
+
target = last_sel_file.read_text().strip()
|
|
1417
|
+
if not target:
|
|
1418
|
+
print(msg("error", msg("select_no_last")), file=sys.stderr)
|
|
1419
|
+
sys.exit(1)
|
|
1420
|
+
|
|
1421
|
+
if target not in names:
|
|
1422
|
+
print(msg("error", msg("select_not_found", target)), file=sys.stderr)
|
|
1423
|
+
sys.exit(1)
|
|
1424
|
+
|
|
1425
|
+
switch_selection(target, base_dir, current_sel, last_sel_file)
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
def cmd_current(args: list[str]):
|
|
1429
|
+
"""wt current (cur) - Show name of the current worktree"""
|
|
1430
|
+
name = os.environ.get("WT_SESSION_NAME")
|
|
1431
|
+
if not name:
|
|
1432
|
+
base_dir = find_base_dir()
|
|
1433
|
+
if not base_dir:
|
|
1434
|
+
return
|
|
1435
|
+
cwd = Path.cwd().resolve()
|
|
1436
|
+
worktrees = get_worktree_info(base_dir)
|
|
1437
|
+
resolved_base = base_dir.resolve()
|
|
1438
|
+
for wt in worktrees:
|
|
1439
|
+
wt_path = Path(wt["path"]).resolve()
|
|
1440
|
+
if cwd == wt_path:
|
|
1441
|
+
name = "main" if wt_path == resolved_base else wt_path.name
|
|
1442
|
+
break
|
|
1443
|
+
if name:
|
|
1444
|
+
print(name)
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
def switch_selection(target, base_dir, current_sel, last_sel_file):
|
|
1448
|
+
"""Switch selection and update last_selection"""
|
|
1449
|
+
# Calculate target path
|
|
1450
|
+
target_path = base_dir
|
|
1451
|
+
if target != "main":
|
|
1452
|
+
config = load_config(base_dir)
|
|
1453
|
+
worktrees_dir_name = config.get("worktrees_dir", ".worktrees")
|
|
1454
|
+
target_path = base_dir / worktrees_dir_name / target
|
|
1455
|
+
|
|
1456
|
+
if not target_path.exists():
|
|
1457
|
+
print(msg("error", msg("select_not_found", target)), file=sys.stderr)
|
|
1458
|
+
sys.exit(1)
|
|
1459
|
+
|
|
1460
|
+
if target != current_sel:
|
|
1461
|
+
# Save last selection
|
|
1462
|
+
if current_sel:
|
|
1463
|
+
last_sel_file.write_text(current_sel)
|
|
1464
|
+
|
|
1465
|
+
print(msg("select_switched", target), file=sys.stderr)
|
|
1466
|
+
|
|
1467
|
+
# Check for setup files
|
|
1468
|
+
config = load_config(base_dir)
|
|
1469
|
+
setup_files = config.get("setup_files", [])
|
|
1470
|
+
missing = False
|
|
1471
|
+
for f in setup_files:
|
|
1472
|
+
if not (target_path / f).exists():
|
|
1473
|
+
missing = True
|
|
1474
|
+
break
|
|
1475
|
+
if missing:
|
|
1476
|
+
print(f"\033[33m{msg('suggest_setup')}\033[0m", file=sys.stderr)
|
|
1477
|
+
|
|
1478
|
+
if sys.stdout.isatty():
|
|
1479
|
+
# Check for nesting
|
|
1480
|
+
current_session = os.environ.get("WT_SESSION_NAME")
|
|
1481
|
+
if current_session:
|
|
1482
|
+
print(
|
|
1483
|
+
f"\033[31m{msg('nesting_error', current_session)}\033[0m", file=sys.stderr
|
|
1484
|
+
)
|
|
1485
|
+
sys.exit(1)
|
|
1486
|
+
|
|
1487
|
+
# Subshell jump
|
|
1488
|
+
shell = os.environ.get("SHELL", "/bin/sh")
|
|
1489
|
+
print(msg("jump_instruction", target, target_path), file=sys.stderr)
|
|
1490
|
+
|
|
1491
|
+
os.chdir(target_path)
|
|
1492
|
+
os.environ["WT_SESSION_NAME"] = target
|
|
1493
|
+
# Prepend to PS1 for visibility (if supported by shell)
|
|
1494
|
+
ps1 = os.environ.get("PS1", "$ ")
|
|
1495
|
+
if not ps1.startswith("(wt:"):
|
|
1496
|
+
os.environ["PS1"] = f"(wt:{target}) {ps1}"
|
|
1497
|
+
|
|
1498
|
+
# Set terminal title
|
|
1499
|
+
sys.stderr.write(f"\033]0;wt:{target}\007")
|
|
1500
|
+
sys.stderr.flush()
|
|
1501
|
+
|
|
1502
|
+
# Update tmux window name if inside tmux
|
|
1503
|
+
if os.environ.get("TMUX"):
|
|
1504
|
+
subprocess.run(["tmux", "rename-window", f"wt:{target}"], check=False)
|
|
1505
|
+
|
|
1506
|
+
os.execl(shell, shell)
|
|
1507
|
+
else:
|
|
1508
|
+
# Output path for script/backtick use
|
|
1509
|
+
print(str(target_path.absolute()))
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
def cmd_setup(args: list[str]):
|
|
1513
|
+
"""wt setup - Initialize current worktree (copy setup_files and run hooks)"""
|
|
1514
|
+
base_dir = find_base_dir()
|
|
1515
|
+
if not base_dir:
|
|
1516
|
+
print(msg("error", msg("base_not_found")), file=sys.stderr)
|
|
1517
|
+
sys.exit(1)
|
|
1518
|
+
|
|
1519
|
+
current_dir = Path.cwd()
|
|
1520
|
+
target_path = current_dir
|
|
1521
|
+
|
|
1522
|
+
config = load_config(base_dir)
|
|
1523
|
+
setup_files = config.get("setup_files", [])
|
|
1524
|
+
|
|
1525
|
+
import shutil
|
|
1526
|
+
count = 0
|
|
1527
|
+
for f in setup_files:
|
|
1528
|
+
src = base_dir / f
|
|
1529
|
+
dst = target_path / f
|
|
1530
|
+
if src.exists() and src != dst:
|
|
1531
|
+
print(msg("setting_up", src, dst), file=sys.stderr)
|
|
1532
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
1533
|
+
shutil.copy2(src, dst)
|
|
1534
|
+
count += 1
|
|
1535
|
+
|
|
1536
|
+
if count > 0:
|
|
1537
|
+
print(msg("completed_setup", count), file=sys.stderr)
|
|
1538
|
+
|
|
1539
|
+
# Run post-add hook
|
|
1540
|
+
work_name = target_path.name
|
|
1541
|
+
if target_path == base_dir:
|
|
1542
|
+
work_name = "main"
|
|
1543
|
+
|
|
1544
|
+
# Get branch name for the current worktree
|
|
1545
|
+
branch = None
|
|
1546
|
+
result = run_command(["git", "branch", "--show-current"], cwd=target_path, check=False)
|
|
1547
|
+
if result.returncode == 0:
|
|
1548
|
+
branch = result.stdout.strip()
|
|
1549
|
+
|
|
1550
|
+
run_post_add_hook(target_path, work_name, base_dir, branch)
|
|
1551
|
+
|
|
1552
|
+
|
|
1325
1553
|
def cmd_clean(args: list[str]):
|
|
1326
1554
|
"""wt clean - Remove old/unused/merged worktrees"""
|
|
1327
1555
|
base_dir = find_base_dir()
|
|
@@ -1532,96 +1760,6 @@ def cmd_clean(args: list[str]):
|
|
|
1532
1760
|
if result.stderr:
|
|
1533
1761
|
print(result.stderr, file=sys.stderr)
|
|
1534
1762
|
|
|
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
1763
|
def cmd_passthrough(args: list[str]):
|
|
1626
1764
|
"""Passthrough other git worktree commands"""
|
|
1627
1765
|
base_dir = find_base_dir()
|
|
@@ -1650,7 +1788,12 @@ def show_help():
|
|
|
1650
1788
|
print(
|
|
1651
1789
|
f" {'add (ad) <作業名> [<base_branch>]':<55} - worktree を追加(デフォルト: 新規ブランチ作成)"
|
|
1652
1790
|
)
|
|
1791
|
+
print(
|
|
1792
|
+
f" {'select (sl) [<作業名>|-]':<55} - 作業ディレクトリを切り替え(fzf対応)"
|
|
1793
|
+
)
|
|
1653
1794
|
print(f" {'list (ls) [--pr]':<55} - worktree 一覧を表示")
|
|
1795
|
+
print(f" {'co/checkout <作業名>':<55} - worktree のパスを表示")
|
|
1796
|
+
print(f" {'current (cur)':<55} - 現在の worktree 名を表示")
|
|
1654
1797
|
print(
|
|
1655
1798
|
f" {'stash (st) <作業名> [<base_branch>]':<55} - 現在の変更をスタッシュして新規 worktree に移動"
|
|
1656
1799
|
)
|
|
@@ -1662,7 +1805,7 @@ def show_help():
|
|
|
1662
1805
|
f" {'clean (cl) [--days N] [--merged] [--closed]':<55} - 不要な worktree を削除"
|
|
1663
1806
|
)
|
|
1664
1807
|
print(
|
|
1665
|
-
f" {'
|
|
1808
|
+
f" {'setup (su)':<55} - 作業ディレクトリを初期化(ファイルコピー・フック実行)"
|
|
1666
1809
|
)
|
|
1667
1810
|
print()
|
|
1668
1811
|
print("オプション:")
|
|
@@ -1680,7 +1823,12 @@ def show_help():
|
|
|
1680
1823
|
print(
|
|
1681
1824
|
f" {'add (ad) <work_name> [<base_branch>]':<55} - Add a worktree (default: create new branch)"
|
|
1682
1825
|
)
|
|
1826
|
+
print(
|
|
1827
|
+
f" {'select (sl) [<name>|-]':<55} - Switch worktree selection (fzf support)"
|
|
1828
|
+
)
|
|
1683
1829
|
print(f" {'list (ls) [--pr]':<55} - List worktrees")
|
|
1830
|
+
print(f" {'co/checkout <work_name>':<55} - Show path to a worktree")
|
|
1831
|
+
print(f" {'current (cur)':<55} - Show current worktree name")
|
|
1684
1832
|
print(
|
|
1685
1833
|
f" {'stash (st) <work_name> [<base_branch>]':<55} - Stash current changes and move to new worktree"
|
|
1686
1834
|
)
|
|
@@ -1690,7 +1838,7 @@ def show_help():
|
|
|
1690
1838
|
f" {'clean (cl) [--days N] [--merged] [--closed]':<55} - Remove unused/merged worktrees"
|
|
1691
1839
|
)
|
|
1692
1840
|
print(
|
|
1693
|
-
f" {'
|
|
1841
|
+
f" {'setup (su)':<55} - Setup worktree (copy files and run hooks)"
|
|
1694
1842
|
)
|
|
1695
1843
|
print()
|
|
1696
1844
|
print("Options:")
|
|
@@ -1735,12 +1883,18 @@ def main():
|
|
|
1735
1883
|
cmd_remove(args)
|
|
1736
1884
|
elif command in ["clean", "cl"]:
|
|
1737
1885
|
cmd_clean(args)
|
|
1738
|
-
elif command in ["
|
|
1739
|
-
|
|
1886
|
+
elif command in ["setup", "su"]:
|
|
1887
|
+
cmd_setup(args)
|
|
1740
1888
|
elif command in ["stash", "st"]:
|
|
1741
1889
|
cmd_stash(args)
|
|
1742
1890
|
elif command == "pr":
|
|
1743
1891
|
cmd_pr(args)
|
|
1892
|
+
elif command == "select" or command == "sl":
|
|
1893
|
+
cmd_select(args)
|
|
1894
|
+
elif command in ["current", "cur"]:
|
|
1895
|
+
cmd_current(args)
|
|
1896
|
+
elif command in ["co", "checkout"]:
|
|
1897
|
+
cmd_checkout(args)
|
|
1744
1898
|
else:
|
|
1745
1899
|
# その他のコマンドは git worktree にパススルー
|
|
1746
1900
|
cmd_passthrough([command] + args)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: easy-worktree
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
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
|
-
- **
|
|
39
|
-
- **Auto
|
|
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`, `
|
|
42
|
+
- **Two-letter shortcuts**: Fast execution with shortcuts like `ad`, `ls`, `sl`, `su`, `st`, `cl`.
|
|
43
43
|
|
|
44
44
|
## Prerequisites
|
|
45
45
|
|
|
@@ -123,6 +123,23 @@ Quickly stash your current changes and move them to a new worktree.
|
|
|
123
123
|
wt stash feature-2
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
+
#### Switch Worktree (shortcut: `sl`)
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
wt select feature-1
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Running `wt select` will **automatically "jump"** you into the worktree directory by starting a new subshell.
|
|
133
|
+
|
|
134
|
+
- **Prompt**: Shows a `(wt:feature-1)` indicator.
|
|
135
|
+
- **Terminal Title**: Updates the window title to `wt:feature-1`.
|
|
136
|
+
- **Tmux**: Updates the tmux window name to `wt:feature-1` if running inside tmux.
|
|
137
|
+
|
|
138
|
+
To return to your original directory, simply type `exit` or press `Ctrl-D`.
|
|
139
|
+
|
|
140
|
+
* **Interactive Mode**: Running `wt select` without arguments opens an interactive picker using `fzf`.
|
|
141
|
+
* **Nesting Control**: If you are already in a `wt` subshell, it will warn you to avoid confusing nesting.
|
|
142
|
+
|
|
126
143
|
#### PR Management
|
|
127
144
|
|
|
128
145
|
Fetch a PR and create a worktree for it. (Requires `gh` CLI)
|
|
@@ -141,12 +158,63 @@ Removes the worktree and its directory.
|
|
|
141
158
|
|
|
142
159
|
### Useful Features
|
|
143
160
|
|
|
144
|
-
####
|
|
161
|
+
#### Setup Worktree (shortcut: `su`)
|
|
162
|
+
|
|
163
|
+
Initialize the current worktree by copying required files and running the `post-add` hook.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
wt setup
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### Visualization and External Tools
|
|
170
|
+
|
|
171
|
+
When you switch to a worktree using `wt select`, the following features are automatically enabled:
|
|
172
|
+
- **Terminal Title**: The window or tab title is updated to `wt:worktree-name`.
|
|
173
|
+
- **Tmux**: If you are inside tmux, the window name is automatically renamed to `wt:worktree-name`.
|
|
174
|
+
|
|
175
|
+
You can also use the `wt current` (or `cur`) command to display the current worktree name in external tools.
|
|
176
|
+
|
|
177
|
+
##### Tmux Status Bar
|
|
178
|
+
Add the following to your `.tmux.conf` to show the worktree name in your status line:
|
|
179
|
+
```tmux
|
|
180
|
+
set -g status-right "#(wt current) | %Y-%m-%d %H:%M"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
##### Zsh / Bash Prompt
|
|
184
|
+
You can customize your prompt using the `$WT_SESSION_NAME` environment variable.
|
|
145
185
|
|
|
146
|
-
|
|
186
|
+
**Zsh (.zshrc)**:
|
|
187
|
+
```zsh
|
|
188
|
+
RPROMPT='${WT_SESSION_NAME:+"(wt:$WT_SESSION_NAME)"} '"$RPROMPT"
|
|
189
|
+
```
|
|
147
190
|
|
|
191
|
+
**Bash (.bashrc)**:
|
|
148
192
|
```bash
|
|
149
|
-
|
|
193
|
+
PS1='$(if [ -n "$WT_SESSION_NAME" ]; then echo "($WT_SESSION_NAME) "; fi)'$PS1
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
##### Starship
|
|
197
|
+
Add a custom module to your `starship.toml`:
|
|
198
|
+
```toml
|
|
199
|
+
[custom.easy_worktree]
|
|
200
|
+
command = "wt current"
|
|
201
|
+
when = 'test -n "$WT_SESSION_NAME"'
|
|
202
|
+
format = "via [$symbol$output]($style) "
|
|
203
|
+
symbol = "🌳 "
|
|
204
|
+
style = "bold green"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
##### Powerlevel10k
|
|
208
|
+
Integrate beautiful worktree indicators by adding a custom segment to `.p10k.zsh`:
|
|
209
|
+
|
|
210
|
+
1. Add `easy_worktree` to `POWERLEVEL9K_LEFT_PROMPT_ELEMENTS`.
|
|
211
|
+
2. Define the following function:
|
|
212
|
+
```zsh
|
|
213
|
+
function prompt_easy_worktree() {
|
|
214
|
+
if [[ -n $WT_SESSION_NAME ]]; then
|
|
215
|
+
p10k segment -f 255 -b 28 -i '🌳' -t "wt:$WT_SESSION_NAME"
|
|
216
|
+
fi
|
|
217
|
+
}
|
|
150
218
|
```
|
|
151
219
|
|
|
152
220
|
|
|
@@ -166,8 +234,8 @@ Customize behavior in `.wt/config.toml`:
|
|
|
166
234
|
|
|
167
235
|
```toml
|
|
168
236
|
worktrees_dir = ".worktrees" # Directory where worktrees are created
|
|
169
|
-
|
|
170
|
-
auto_copy_on_add = true # Enable auto-
|
|
237
|
+
setup_files = [".env"] # Files to auto-copy during setup
|
|
238
|
+
auto_copy_on_add = true # Enable auto-copy on worktree creation
|
|
171
239
|
```
|
|
172
240
|
|
|
173
241
|
## Hooks
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
easy_worktree/__init__.py,sha256=yHHZKyeljLEChIumw8ybGUmnvoCpoCJRBO_JE-l1Vmc,62539
|
|
2
|
+
easy_worktree-0.1.2.dist-info/METADATA,sha256=v4sCzlh43PBw0YPoNepc-orui3iobogZ_hKwyKep0Ug,6249
|
|
3
|
+
easy_worktree-0.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
4
|
+
easy_worktree-0.1.2.dist-info/entry_points.txt,sha256=Mf6MYDS2obZLvIJJFl-BbU8-SL0QGu5UWcC0FWnqtbg,42
|
|
5
|
+
easy_worktree-0.1.2.dist-info/licenses/LICENSE,sha256=7MGvWFDxXPqW2nrr9D7KHT0vWFiGwIUL5SQCj0IiAPc,1061
|
|
6
|
+
easy_worktree-0.1.2.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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|