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