easy-worktree 0.0.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.
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Git worktree を簡単に管理するための CLI ツール
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# 言語判定
|
|
13
|
+
def is_japanese() -> bool:
|
|
14
|
+
"""LANG環境変数から日本語かどうかを判定"""
|
|
15
|
+
lang = os.environ.get('LANG', '')
|
|
16
|
+
return 'ja' in lang.lower()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# メッセージ辞書
|
|
20
|
+
MESSAGES = {
|
|
21
|
+
'error': {
|
|
22
|
+
'en': 'Error: {}',
|
|
23
|
+
'ja': 'エラー: {}'
|
|
24
|
+
},
|
|
25
|
+
'usage': {
|
|
26
|
+
'en': 'Usage: wt clone <repository_url>',
|
|
27
|
+
'ja': '使用方法: wt clone <repository_url>'
|
|
28
|
+
},
|
|
29
|
+
'usage_add': {
|
|
30
|
+
'en': 'Usage: wt add <work_name> [<base_branch>]',
|
|
31
|
+
'ja': '使用方法: wt add <作業名> [<base_branch>]'
|
|
32
|
+
},
|
|
33
|
+
'usage_rm': {
|
|
34
|
+
'en': 'Usage: wt rm <work_name>',
|
|
35
|
+
'ja': '使用方法: wt rm <作業名>'
|
|
36
|
+
},
|
|
37
|
+
'base_not_found': {
|
|
38
|
+
'en': '_base/ directory not found',
|
|
39
|
+
'ja': '_base/ ディレクトリが見つかりません'
|
|
40
|
+
},
|
|
41
|
+
'run_in_wt_dir': {
|
|
42
|
+
'en': 'Please run inside WT_<repository_name>/ directory',
|
|
43
|
+
'ja': 'WT_<repository_name>/ ディレクトリ内で実行してください'
|
|
44
|
+
},
|
|
45
|
+
'already_exists': {
|
|
46
|
+
'en': '{} already exists',
|
|
47
|
+
'ja': '{} はすでに存在します'
|
|
48
|
+
},
|
|
49
|
+
'cloning': {
|
|
50
|
+
'en': 'Cloning: {} -> {}',
|
|
51
|
+
'ja': 'クローン中: {} -> {}'
|
|
52
|
+
},
|
|
53
|
+
'completed_clone': {
|
|
54
|
+
'en': 'Completed: cloned to {}',
|
|
55
|
+
'ja': '完了: {} にクローンしました'
|
|
56
|
+
},
|
|
57
|
+
'not_git_repo': {
|
|
58
|
+
'en': 'Current directory is not a git repository',
|
|
59
|
+
'ja': '現在のディレクトリは git リポジトリではありません'
|
|
60
|
+
},
|
|
61
|
+
'run_at_root': {
|
|
62
|
+
'en': 'Please run at repository root directory {}',
|
|
63
|
+
'ja': 'リポジトリのルートディレクトリ {} で実行してください'
|
|
64
|
+
},
|
|
65
|
+
'creating_dir': {
|
|
66
|
+
'en': 'Creating {}...',
|
|
67
|
+
'ja': '{} を作成中...'
|
|
68
|
+
},
|
|
69
|
+
'moving': {
|
|
70
|
+
'en': 'Moving {} -> {}...',
|
|
71
|
+
'ja': '{} -> {} に移動中...'
|
|
72
|
+
},
|
|
73
|
+
'completed_move': {
|
|
74
|
+
'en': 'Completed: moved to {}',
|
|
75
|
+
'ja': '完了: {} に移動しました'
|
|
76
|
+
},
|
|
77
|
+
'use_wt_from': {
|
|
78
|
+
'en': 'Use wt command from {} from next time',
|
|
79
|
+
'ja': '次回から {} で wt コマンドを使用してください'
|
|
80
|
+
},
|
|
81
|
+
'fetching': {
|
|
82
|
+
'en': 'Fetching latest information from remote...',
|
|
83
|
+
'ja': 'リモートから最新情報を取得中...'
|
|
84
|
+
},
|
|
85
|
+
'creating_worktree': {
|
|
86
|
+
'en': 'Creating worktree: {}',
|
|
87
|
+
'ja': 'worktree を作成中: {}'
|
|
88
|
+
},
|
|
89
|
+
'completed_worktree': {
|
|
90
|
+
'en': 'Completed: created worktree at {}',
|
|
91
|
+
'ja': '完了: {} に worktree を作成しました'
|
|
92
|
+
},
|
|
93
|
+
'removing_worktree': {
|
|
94
|
+
'en': 'Removing worktree: {}',
|
|
95
|
+
'ja': 'worktree を削除中: {}'
|
|
96
|
+
},
|
|
97
|
+
'completed_remove': {
|
|
98
|
+
'en': 'Completed: removed {}',
|
|
99
|
+
'ja': '完了: {} を削除しました'
|
|
100
|
+
},
|
|
101
|
+
'creating_branch': {
|
|
102
|
+
'en': "Creating new branch '{}' from '{}'",
|
|
103
|
+
'ja': "'{}' から新しいブランチ '{}' を作成"
|
|
104
|
+
},
|
|
105
|
+
'default_branch_not_found': {
|
|
106
|
+
'en': 'Could not find default branch (main/master)',
|
|
107
|
+
'ja': 'デフォルトブランチ (main/master) が見つかりません'
|
|
108
|
+
},
|
|
109
|
+
'running_hook': {
|
|
110
|
+
'en': 'Running post-add hook: {}',
|
|
111
|
+
'ja': 'post-add hook を実行中: {}'
|
|
112
|
+
},
|
|
113
|
+
'hook_not_executable': {
|
|
114
|
+
'en': 'Warning: hook is not executable: {}',
|
|
115
|
+
'ja': '警告: hook が実行可能ではありません: {}'
|
|
116
|
+
},
|
|
117
|
+
'hook_failed': {
|
|
118
|
+
'en': 'Warning: hook exited with code {}',
|
|
119
|
+
'ja': '警告: hook が終了コード {} で終了しました'
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def msg(key: str, *args) -> str:
|
|
125
|
+
"""言語に応じたメッセージを取得"""
|
|
126
|
+
lang = 'ja' if is_japanese() else 'en'
|
|
127
|
+
message = MESSAGES.get(key, {}).get(lang, key)
|
|
128
|
+
if args:
|
|
129
|
+
return message.format(*args)
|
|
130
|
+
return message
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def run_command(cmd: list[str], cwd: Path = None, check: bool = True) -> subprocess.CompletedProcess:
|
|
134
|
+
"""コマンドを実行"""
|
|
135
|
+
try:
|
|
136
|
+
result = subprocess.run(
|
|
137
|
+
cmd,
|
|
138
|
+
cwd=cwd,
|
|
139
|
+
capture_output=True,
|
|
140
|
+
text=True,
|
|
141
|
+
check=check
|
|
142
|
+
)
|
|
143
|
+
return result
|
|
144
|
+
except subprocess.CalledProcessError as e:
|
|
145
|
+
print(msg('error', e.stderr), file=sys.stderr)
|
|
146
|
+
sys.exit(1)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_repository_name(url: str) -> str:
|
|
150
|
+
"""リポジトリ URL から名前を抽出"""
|
|
151
|
+
# URL から .git を削除して最後の部分を取得
|
|
152
|
+
match = re.search(r'/([^/]+?)(?:\.git)?$', url)
|
|
153
|
+
if match:
|
|
154
|
+
return match.group(1)
|
|
155
|
+
# ローカルパスの場合
|
|
156
|
+
return Path(url).name
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def create_hook_template(base_dir: Path):
|
|
160
|
+
"""post-add hook のテンプレートを作成"""
|
|
161
|
+
wt_dir = base_dir / ".wt"
|
|
162
|
+
hook_file = wt_dir / "post-add"
|
|
163
|
+
|
|
164
|
+
# 既に存在する場合は何もしない
|
|
165
|
+
if hook_file.exists():
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
# .wt ディレクトリを作成
|
|
169
|
+
wt_dir.mkdir(exist_ok=True)
|
|
170
|
+
|
|
171
|
+
# テンプレートを作成
|
|
172
|
+
template = """#!/bin/bash
|
|
173
|
+
# Post-add hook for easy-worktree
|
|
174
|
+
# This script is automatically executed after creating a new worktree
|
|
175
|
+
#
|
|
176
|
+
# Available environment variables:
|
|
177
|
+
# WT_WORKTREE_PATH - Path to the created worktree
|
|
178
|
+
# WT_WORKTREE_NAME - Name of the worktree
|
|
179
|
+
# WT_BASE_DIR - Path to the _base/ directory
|
|
180
|
+
# WT_BRANCH - Branch name
|
|
181
|
+
# WT_ACTION - Action name (add)
|
|
182
|
+
#
|
|
183
|
+
# Example: Install dependencies and copy configuration files
|
|
184
|
+
#
|
|
185
|
+
# set -e
|
|
186
|
+
#
|
|
187
|
+
# echo "Initializing worktree: $WT_WORKTREE_NAME"
|
|
188
|
+
#
|
|
189
|
+
# # Install npm packages
|
|
190
|
+
# if [ -f package.json ]; then
|
|
191
|
+
# npm install
|
|
192
|
+
# fi
|
|
193
|
+
#
|
|
194
|
+
# # Copy .env file
|
|
195
|
+
# if [ -f "$WT_BASE_DIR/.env.example" ]; then
|
|
196
|
+
# cp "$WT_BASE_DIR/.env.example" .env
|
|
197
|
+
# fi
|
|
198
|
+
#
|
|
199
|
+
# echo "Setup completed!"
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
hook_file.write_text(template)
|
|
203
|
+
# 実行権限を付与
|
|
204
|
+
hook_file.chmod(0o755)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def find_base_dir() -> Path | None:
|
|
208
|
+
"""現在のディレクトリまたは親ディレクトリから _base/ を探す"""
|
|
209
|
+
current = Path.cwd()
|
|
210
|
+
|
|
211
|
+
# 現在のディレクトリに _base/ がある場合
|
|
212
|
+
base_dir = current / "_base"
|
|
213
|
+
if base_dir.exists() and base_dir.is_dir():
|
|
214
|
+
return base_dir
|
|
215
|
+
|
|
216
|
+
# 親ディレクトリに _base/ がある場合(worktree の中にいる場合)
|
|
217
|
+
base_dir = current.parent / "_base"
|
|
218
|
+
if base_dir.exists() and base_dir.is_dir():
|
|
219
|
+
return base_dir
|
|
220
|
+
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def cmd_clone(args: list[str]):
|
|
225
|
+
"""wt clone <repository_url> - Clone a repository"""
|
|
226
|
+
if len(args) < 1:
|
|
227
|
+
print(msg('usage'), file=sys.stderr)
|
|
228
|
+
sys.exit(1)
|
|
229
|
+
|
|
230
|
+
repo_url = args[0]
|
|
231
|
+
repo_name = get_repository_name(repo_url)
|
|
232
|
+
|
|
233
|
+
# WT_<repository_name>/_base にクローン
|
|
234
|
+
parent_dir = Path(f"WT_{repo_name}")
|
|
235
|
+
base_dir = parent_dir / "_base"
|
|
236
|
+
|
|
237
|
+
if base_dir.exists():
|
|
238
|
+
print(msg('error', msg('already_exists', base_dir)), file=sys.stderr)
|
|
239
|
+
sys.exit(1)
|
|
240
|
+
|
|
241
|
+
parent_dir.mkdir(exist_ok=True)
|
|
242
|
+
|
|
243
|
+
print(msg('cloning', repo_url, base_dir))
|
|
244
|
+
run_command(["git", "clone", repo_url, str(base_dir)])
|
|
245
|
+
print(msg('completed_clone', base_dir))
|
|
246
|
+
|
|
247
|
+
# post-add hook テンプレートを作成
|
|
248
|
+
create_hook_template(base_dir)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def cmd_init(args: list[str]):
|
|
252
|
+
"""wt init - Move existing git repository to WT_<repo>/_base/"""
|
|
253
|
+
current_dir = Path.cwd()
|
|
254
|
+
|
|
255
|
+
# 現在のディレクトリが git リポジトリか確認
|
|
256
|
+
result = run_command(
|
|
257
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
258
|
+
cwd=current_dir,
|
|
259
|
+
check=False
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if result.returncode != 0:
|
|
263
|
+
print(msg('error', msg('not_git_repo')), file=sys.stderr)
|
|
264
|
+
sys.exit(1)
|
|
265
|
+
|
|
266
|
+
git_root = Path(result.stdout.strip())
|
|
267
|
+
|
|
268
|
+
# カレントディレクトリがリポジトリのルートでない場合はエラー
|
|
269
|
+
if git_root != current_dir:
|
|
270
|
+
print(msg('error', msg('run_at_root', git_root)), file=sys.stderr)
|
|
271
|
+
sys.exit(1)
|
|
272
|
+
|
|
273
|
+
# リポジトリ名を取得(remote origin から、なければディレクトリ名)
|
|
274
|
+
result = run_command(
|
|
275
|
+
["git", "remote", "get-url", "origin"],
|
|
276
|
+
cwd=current_dir,
|
|
277
|
+
check=False
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
281
|
+
repo_name = get_repository_name(result.stdout.strip())
|
|
282
|
+
else:
|
|
283
|
+
# リモートがない場合は現在のディレクトリ名を使用
|
|
284
|
+
repo_name = current_dir.name
|
|
285
|
+
|
|
286
|
+
# 親ディレクトリと新しいパスを決定
|
|
287
|
+
parent_of_current = current_dir.parent
|
|
288
|
+
wt_parent_dir = parent_of_current / f"WT_{repo_name}"
|
|
289
|
+
new_base_dir = wt_parent_dir / "_base"
|
|
290
|
+
|
|
291
|
+
# すでに WT_<repo> が存在するかチェック
|
|
292
|
+
if wt_parent_dir.exists():
|
|
293
|
+
print(msg('error', msg('already_exists', wt_parent_dir)), file=sys.stderr)
|
|
294
|
+
sys.exit(1)
|
|
295
|
+
|
|
296
|
+
# WT_<repo>/ ディレクトリを作成
|
|
297
|
+
print(msg('creating_dir', wt_parent_dir))
|
|
298
|
+
wt_parent_dir.mkdir(exist_ok=True)
|
|
299
|
+
|
|
300
|
+
# 現在のディレクトリを WT_<repo>/_base/ に移動
|
|
301
|
+
print(msg('moving', current_dir, new_base_dir))
|
|
302
|
+
current_dir.rename(new_base_dir)
|
|
303
|
+
|
|
304
|
+
print(msg('completed_move', new_base_dir))
|
|
305
|
+
print(msg('use_wt_from', wt_parent_dir))
|
|
306
|
+
|
|
307
|
+
# post-add hook テンプレートを作成
|
|
308
|
+
create_hook_template(new_base_dir)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def run_post_add_hook(worktree_path: Path, work_name: str, base_dir: Path, branch: str = None):
|
|
312
|
+
"""worktree 作成後の hook を実行"""
|
|
313
|
+
# .wt/post-add を探す
|
|
314
|
+
hook_path = base_dir / ".wt" / "post-add"
|
|
315
|
+
|
|
316
|
+
if not hook_path.exists() or not hook_path.is_file():
|
|
317
|
+
return # hook がなければ何もしない
|
|
318
|
+
|
|
319
|
+
if not os.access(hook_path, os.X_OK):
|
|
320
|
+
print(msg('hook_not_executable', hook_path), file=sys.stderr)
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
# 環境変数を設定
|
|
324
|
+
env = os.environ.copy()
|
|
325
|
+
env.update({
|
|
326
|
+
'WT_WORKTREE_PATH': str(worktree_path),
|
|
327
|
+
'WT_WORKTREE_NAME': work_name,
|
|
328
|
+
'WT_BASE_DIR': str(base_dir),
|
|
329
|
+
'WT_BRANCH': branch or work_name,
|
|
330
|
+
'WT_ACTION': 'add'
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
print(msg('running_hook', hook_path))
|
|
334
|
+
try:
|
|
335
|
+
result = subprocess.run(
|
|
336
|
+
[str(hook_path)],
|
|
337
|
+
cwd=worktree_path, # worktree 内で実行
|
|
338
|
+
env=env,
|
|
339
|
+
check=False
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if result.returncode != 0:
|
|
343
|
+
print(msg('hook_failed', result.returncode), file=sys.stderr)
|
|
344
|
+
except Exception as e:
|
|
345
|
+
print(msg('error', str(e)), file=sys.stderr)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def cmd_add(args: list[str]):
|
|
349
|
+
"""wt add <work_name> [<base_branch>] - Add a worktree"""
|
|
350
|
+
if len(args) < 1:
|
|
351
|
+
print(msg('usage_add'), file=sys.stderr)
|
|
352
|
+
sys.exit(1)
|
|
353
|
+
|
|
354
|
+
base_dir = find_base_dir()
|
|
355
|
+
if not base_dir:
|
|
356
|
+
print(msg('error', msg('base_not_found')), file=sys.stderr)
|
|
357
|
+
print(msg('run_in_wt_dir'), file=sys.stderr)
|
|
358
|
+
sys.exit(1)
|
|
359
|
+
|
|
360
|
+
work_name = args[0]
|
|
361
|
+
|
|
362
|
+
# worktree のパスを決定(_base の親ディレクトリに作成)
|
|
363
|
+
worktree_path = base_dir.parent / work_name
|
|
364
|
+
|
|
365
|
+
if worktree_path.exists():
|
|
366
|
+
print(msg('error', msg('already_exists', worktree_path)), file=sys.stderr)
|
|
367
|
+
sys.exit(1)
|
|
368
|
+
|
|
369
|
+
# ブランチを最新に更新
|
|
370
|
+
print(msg('fetching'))
|
|
371
|
+
run_command(["git", "fetch", "--all"], cwd=base_dir)
|
|
372
|
+
|
|
373
|
+
# ブランチ名が指定されている場合は既存ブランチをチェックアウト
|
|
374
|
+
# 指定されていない場合は新しいブランチを作成
|
|
375
|
+
branch_name = None
|
|
376
|
+
if len(args) >= 2:
|
|
377
|
+
# 既存ブランチをチェックアウト
|
|
378
|
+
branch_name = args[1]
|
|
379
|
+
print(msg('creating_worktree', worktree_path))
|
|
380
|
+
result = run_command(
|
|
381
|
+
["git", "worktree", "add", str(worktree_path), branch_name],
|
|
382
|
+
cwd=base_dir,
|
|
383
|
+
check=False
|
|
384
|
+
)
|
|
385
|
+
else:
|
|
386
|
+
# 新しいブランチを作成
|
|
387
|
+
branch_name = work_name
|
|
388
|
+
# デフォルトブランチを探す(origin/main または origin/master)
|
|
389
|
+
result = run_command(
|
|
390
|
+
["git", "symbolic-ref", "refs/remotes/origin/HEAD", "--short"],
|
|
391
|
+
cwd=base_dir,
|
|
392
|
+
check=False
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
396
|
+
base_branch = result.stdout.strip()
|
|
397
|
+
else:
|
|
398
|
+
# symbolic-ref が失敗した場合は手動でチェック
|
|
399
|
+
result_main = run_command(
|
|
400
|
+
["git", "rev-parse", "--verify", "origin/main"],
|
|
401
|
+
cwd=base_dir,
|
|
402
|
+
check=False
|
|
403
|
+
)
|
|
404
|
+
result_master = run_command(
|
|
405
|
+
["git", "rev-parse", "--verify", "origin/master"],
|
|
406
|
+
cwd=base_dir,
|
|
407
|
+
check=False
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
if result_main.returncode == 0:
|
|
411
|
+
base_branch = "origin/main"
|
|
412
|
+
elif result_master.returncode == 0:
|
|
413
|
+
base_branch = "origin/master"
|
|
414
|
+
else:
|
|
415
|
+
print(msg('error', msg('default_branch_not_found')), file=sys.stderr)
|
|
416
|
+
sys.exit(1)
|
|
417
|
+
|
|
418
|
+
print(msg('creating_branch', base_branch, work_name))
|
|
419
|
+
result = run_command(
|
|
420
|
+
["git", "worktree", "add", "-b", work_name, str(worktree_path), base_branch],
|
|
421
|
+
cwd=base_dir,
|
|
422
|
+
check=False
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
if result.returncode == 0:
|
|
426
|
+
print(msg('completed_worktree', worktree_path))
|
|
427
|
+
# post-add hook を実行
|
|
428
|
+
run_post_add_hook(worktree_path, work_name, base_dir, branch_name)
|
|
429
|
+
else:
|
|
430
|
+
# エラーメッセージを表示
|
|
431
|
+
if result.stderr:
|
|
432
|
+
print(result.stderr, file=sys.stderr)
|
|
433
|
+
sys.exit(1)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def cmd_list(args: list[str]):
|
|
437
|
+
"""wt list - List worktrees"""
|
|
438
|
+
base_dir = find_base_dir()
|
|
439
|
+
if not base_dir:
|
|
440
|
+
print(msg('error', msg('base_not_found')), file=sys.stderr)
|
|
441
|
+
sys.exit(1)
|
|
442
|
+
|
|
443
|
+
result = run_command(["git", "worktree", "list"] + args, cwd=base_dir)
|
|
444
|
+
print(result.stdout, end='')
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def cmd_remove(args: list[str]):
|
|
448
|
+
"""wt rm/remove <work_name> - Remove a worktree"""
|
|
449
|
+
if len(args) < 1:
|
|
450
|
+
print(msg('usage_rm'), file=sys.stderr)
|
|
451
|
+
sys.exit(1)
|
|
452
|
+
|
|
453
|
+
base_dir = find_base_dir()
|
|
454
|
+
if not base_dir:
|
|
455
|
+
print(msg('error', msg('base_not_found')), file=sys.stderr)
|
|
456
|
+
sys.exit(1)
|
|
457
|
+
|
|
458
|
+
work_name = args[0]
|
|
459
|
+
|
|
460
|
+
# worktree を削除
|
|
461
|
+
print(msg('removing_worktree', work_name))
|
|
462
|
+
result = run_command(
|
|
463
|
+
["git", "worktree", "remove", work_name],
|
|
464
|
+
cwd=base_dir,
|
|
465
|
+
check=False
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
if result.returncode == 0:
|
|
469
|
+
print(msg('completed_remove', work_name))
|
|
470
|
+
else:
|
|
471
|
+
if result.stderr:
|
|
472
|
+
print(result.stderr, file=sys.stderr)
|
|
473
|
+
sys.exit(1)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def cmd_passthrough(args: list[str]):
|
|
477
|
+
"""Passthrough other git worktree commands"""
|
|
478
|
+
base_dir = find_base_dir()
|
|
479
|
+
if not base_dir:
|
|
480
|
+
print(msg('error', msg('base_not_found')), file=sys.stderr)
|
|
481
|
+
sys.exit(1)
|
|
482
|
+
|
|
483
|
+
result = run_command(["git", "worktree"] + args, cwd=base_dir, check=False)
|
|
484
|
+
print(result.stdout, end='')
|
|
485
|
+
if result.stderr:
|
|
486
|
+
print(result.stderr, end='', file=sys.stderr)
|
|
487
|
+
sys.exit(result.returncode)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def show_help():
|
|
491
|
+
"""Show help message"""
|
|
492
|
+
if is_japanese():
|
|
493
|
+
print("easy-worktree - Git worktree を簡単に管理するための CLI ツール")
|
|
494
|
+
print()
|
|
495
|
+
print("使用方法:")
|
|
496
|
+
print(" wt <command> [options]")
|
|
497
|
+
print()
|
|
498
|
+
print("コマンド:")
|
|
499
|
+
print(" clone <repository_url> - リポジトリをクローン")
|
|
500
|
+
print(" init - 既存リポジトリを WT_<repo>/_base/ に移動")
|
|
501
|
+
print(" add <作業名> [<base_branch>] - worktree を追加(デフォルト: 新規ブランチ作成)")
|
|
502
|
+
print(" list - worktree 一覧を表示")
|
|
503
|
+
print(" rm <作業名> - worktree を削除")
|
|
504
|
+
print(" remove <作業名> - worktree を削除")
|
|
505
|
+
print(" <git-worktree-command> - その他の git worktree コマンド")
|
|
506
|
+
print()
|
|
507
|
+
print("オプション:")
|
|
508
|
+
print(" -h, --help - このヘルプメッセージを表示")
|
|
509
|
+
print(" -v, --version - バージョン情報を表示")
|
|
510
|
+
else:
|
|
511
|
+
print("easy-worktree - Simple CLI tool for managing Git worktrees")
|
|
512
|
+
print()
|
|
513
|
+
print("Usage:")
|
|
514
|
+
print(" wt <command> [options]")
|
|
515
|
+
print()
|
|
516
|
+
print("Commands:")
|
|
517
|
+
print(" clone <repository_url> - Clone a repository")
|
|
518
|
+
print(" init - Move existing repo to WT_<repo>/_base/")
|
|
519
|
+
print(" add <work_name> [<base_branch>] - Add a worktree (default: create new branch)")
|
|
520
|
+
print(" list - List worktrees")
|
|
521
|
+
print(" rm <work_name> - Remove a worktree")
|
|
522
|
+
print(" remove <work_name> - Remove a worktree")
|
|
523
|
+
print(" <git-worktree-command> - Other git worktree commands")
|
|
524
|
+
print()
|
|
525
|
+
print("Options:")
|
|
526
|
+
print(" -h, --help - Show this help message")
|
|
527
|
+
print(" -v, --version - Show version information")
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def show_version():
|
|
531
|
+
"""Show version information"""
|
|
532
|
+
print("easy-worktree version 0.0.1")
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def main():
|
|
536
|
+
"""メインエントリポイント"""
|
|
537
|
+
# ヘルプとバージョンのオプションは _base/ なしでも動作する
|
|
538
|
+
if len(sys.argv) < 2:
|
|
539
|
+
show_help()
|
|
540
|
+
sys.exit(1)
|
|
541
|
+
|
|
542
|
+
command = sys.argv[1]
|
|
543
|
+
args = sys.argv[2:]
|
|
544
|
+
|
|
545
|
+
# -h, --help オプション
|
|
546
|
+
if command in ["-h", "--help"]:
|
|
547
|
+
show_help()
|
|
548
|
+
sys.exit(0)
|
|
549
|
+
|
|
550
|
+
# -v, --version オプション
|
|
551
|
+
if command in ["-v", "--version"]:
|
|
552
|
+
show_version()
|
|
553
|
+
sys.exit(0)
|
|
554
|
+
|
|
555
|
+
if command == "clone":
|
|
556
|
+
cmd_clone(args)
|
|
557
|
+
elif command == "init":
|
|
558
|
+
cmd_init(args)
|
|
559
|
+
elif command == "add":
|
|
560
|
+
cmd_add(args)
|
|
561
|
+
elif command == "list":
|
|
562
|
+
cmd_list(args)
|
|
563
|
+
elif command in ["rm", "remove"]:
|
|
564
|
+
cmd_remove(args)
|
|
565
|
+
else:
|
|
566
|
+
# その他のコマンドは git worktree にパススルー
|
|
567
|
+
cmd_passthrough([command] + args)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
if __name__ == "__main__":
|
|
571
|
+
main()
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: easy-worktree
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Git worktree を簡単に管理するための CLI ツール
|
|
5
|
+
Project-URL: Homepage, https://github.com/igtm/easy-worktree
|
|
6
|
+
Project-URL: Repository, https://github.com/igtm/easy-worktree
|
|
7
|
+
Project-URL: Issues, https://github.com/igtm/easy-worktree/issues
|
|
8
|
+
Author: igtm
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cli,git,tool,worktree
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# easy-worktree
|
|
23
|
+
|
|
24
|
+
A CLI tool for easy Git worktree management
|
|
25
|
+
|
|
26
|
+
[日本語版 README](README_ja.md)
|
|
27
|
+
|
|
28
|
+
## Overview
|
|
29
|
+
|
|
30
|
+
`easy-worktree` simplifies git worktree management by establishing conventions, reducing the cognitive load of managing multiple working trees.
|
|
31
|
+
|
|
32
|
+
### Key Features
|
|
33
|
+
|
|
34
|
+
- **Standardized directory structure**: Creates a `_base/` directory within `WT_<repository_name>/` as the main repository
|
|
35
|
+
- **Easy worktree management**: Create and remove worktrees from `_base/`
|
|
36
|
+
- **Automatic branch updates**: Runs `git fetch --all` automatically when creating worktrees
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install easy-worktree
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or install the development version:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git clone https://github.com/igtm/easy-worktree.git
|
|
48
|
+
cd easy-worktree
|
|
49
|
+
pip install -e .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### Clone a new repository
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
wt clone https://github.com/user/repo.git
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This creates the following structure:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
WT_repo/
|
|
64
|
+
_base/ # Main repository (typically don't modify directly)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Convert an existing repository to easy-worktree structure
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
cd my-repo/
|
|
71
|
+
wt init
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The current directory will be moved to `../WT_my-repo/_base/`.
|
|
75
|
+
|
|
76
|
+
### Add a worktree
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
cd WT_repo/
|
|
80
|
+
wt add feature-1
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This creates the following structure:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
WT_repo/
|
|
87
|
+
_base/
|
|
88
|
+
feature-1/ # Working worktree
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
You can also specify a branch name:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
wt add feature-1 main
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### List worktrees
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
wt list
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Remove a worktree
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
wt rm feature-1
|
|
107
|
+
# or
|
|
108
|
+
wt remove feature-1
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Initialization hook (post-add)
|
|
112
|
+
|
|
113
|
+
You can set up a script to run automatically after creating a worktree.
|
|
114
|
+
|
|
115
|
+
**Hook location**: `_base/.wt/post-add`
|
|
116
|
+
|
|
117
|
+
**Automatic creation**: When you run `wt clone` or `wt init`, a template file is automatically created at `_base/.wt/post-add` (won't overwrite if it already exists). Edit this file to describe your project-specific initialization process.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Example: editing the hook script
|
|
121
|
+
vim WT_repo/_base/.wt/post-add
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
#!/bin/bash
|
|
126
|
+
set -e
|
|
127
|
+
|
|
128
|
+
echo "Initializing worktree: $WT_WORKTREE_NAME"
|
|
129
|
+
|
|
130
|
+
# Install npm packages
|
|
131
|
+
if [ -f package.json ]; then
|
|
132
|
+
npm install
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# Copy .env file
|
|
136
|
+
if [ -f "$WT_BASE_DIR/.env.example" ]; then
|
|
137
|
+
cp "$WT_BASE_DIR/.env.example" .env
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
echo "Setup completed!"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Don't forget to make it executable:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
chmod +x WT_repo/_base/.wt/post-add
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Available environment variables**:
|
|
150
|
+
- `WT_WORKTREE_PATH`: Path to the created worktree
|
|
151
|
+
- `WT_WORKTREE_NAME`: Name of the worktree
|
|
152
|
+
- `WT_BASE_DIR`: Path to the `_base/` directory
|
|
153
|
+
- `WT_BRANCH`: Branch name
|
|
154
|
+
- `WT_ACTION`: Action name (`add`)
|
|
155
|
+
|
|
156
|
+
The hook runs within the newly created worktree directory.
|
|
157
|
+
|
|
158
|
+
### Other git worktree commands
|
|
159
|
+
|
|
160
|
+
`wt` also supports other git worktree commands:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
wt prune
|
|
164
|
+
wt lock <worktree>
|
|
165
|
+
wt unlock <worktree>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Directory Structure
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
WT_<repository_name>/ # Project root directory
|
|
172
|
+
_base/ # Main git repository
|
|
173
|
+
feature-1/ # Worktree 1
|
|
174
|
+
bugfix-123/ # Worktree 2
|
|
175
|
+
...
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
You can run `wt` commands from `WT_<repository_name>/` or from within any worktree directory.
|
|
179
|
+
|
|
180
|
+
## Requirements
|
|
181
|
+
|
|
182
|
+
- Python >= 3.11
|
|
183
|
+
- Git
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT License
|
|
188
|
+
|
|
189
|
+
## Contributing
|
|
190
|
+
|
|
191
|
+
Issues and Pull Requests are welcome!
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
easy_worktree/__init__.py,sha256=TcW_V6eZesS-brvH3G7oxaGYrKkSKWwnk0NK0zux5rg,18175
|
|
2
|
+
easy_worktree-0.0.1.dist-info/METADATA,sha256=uBUl869lSTaY6hHoTibVf10UZNupqXogLGAEH11CGLM,4020
|
|
3
|
+
easy_worktree-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
4
|
+
easy_worktree-0.0.1.dist-info/entry_points.txt,sha256=Mf6MYDS2obZLvIJJFl-BbU8-SL0QGu5UWcC0FWnqtbg,42
|
|
5
|
+
easy_worktree-0.0.1.dist-info/licenses/LICENSE,sha256=7MGvWFDxXPqW2nrr9D7KHT0vWFiGwIUL5SQCj0IiAPc,1061
|
|
6
|
+
easy_worktree-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 igtm
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|