worktree-aid 1.0__tar.gz → 1.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: worktree-aid
3
- Version: 1.0
3
+ Version: 1.2
4
4
  Summary: Linux command line tool to aid creating, switching, and removing git worktrees
5
5
  Author-email: Mark Blakeney <mark.blakeney@bullet-systems.net>
6
6
  License-Expression: GPL-3.0-or-later
@@ -70,8 +70,8 @@ Commands:
70
70
  {add,a,rm,r,cd,c,ls,l,init}
71
71
  add (a) Add new worktree + branch.
72
72
  rm (r) Remove worktree + branch.
73
- cd (c) Change directory to specified worktree.
74
- ls (l) List current worktrees.
73
+ cd (c) Change worktree directory.
74
+ ls (l) List worktrees.
75
75
  init Output shell initialization code.
76
76
  ```
77
77
 
@@ -81,7 +81,7 @@ individual command:
81
81
  ### Command `add`
82
82
 
83
83
  ```
84
- usage: wt add [-h] [-d] [worktree]
84
+ usage: wt add [-h] [-d] [-c] [worktree]
85
85
 
86
86
  Add new worktree + branch.
87
87
 
@@ -92,6 +92,7 @@ positional arguments:
92
92
  options:
93
93
  -h, --help show help message and exit
94
94
  -d, --detach add detached worktree only, i.e. without adding a new branch
95
+ -c, --no-cd do not change directory to new worktree after adding it
95
96
 
96
97
  aliases: a
97
98
  ```
@@ -105,7 +106,8 @@ Remove worktree + branch.
105
106
 
106
107
  positional arguments:
107
108
  worktree worktree + branch name to remove. If not specified then
108
- fuzzy finder will prompt with a list of worktrees.
109
+ fuzzy finder will prompt with a list of worktrees, with
110
+ the current worktree as the default selection.
109
111
 
110
112
  options:
111
113
  -h, --help show help message and exit
@@ -121,11 +123,12 @@ aliases: r
121
123
  ```
122
124
  usage: wt cd [-h] [worktree]
123
125
 
124
- Change directory to specified worktree.
126
+ Change worktree directory.
125
127
 
126
128
  positional arguments:
127
- worktree Worktree name to change directory to. If not specified then
128
- fuzzy finder will prompt with a list of worktrees.
129
+ worktree Worktree name to change directory to. "/" is a shortcut to the
130
+ toplevel repository. If not specified then fuzzy finder will
131
+ prompt with a list of worktrees.
129
132
 
130
133
  options:
131
134
  -h, --help show help message and exit
@@ -138,7 +141,7 @@ aliases: c
138
141
  ```
139
142
  usage: wt ls [-h]
140
143
 
141
- List current worktrees.
144
+ List worktrees.
142
145
 
143
146
  options:
144
147
  -h, --help show help message and exit
@@ -185,7 +188,7 @@ $ yay -S worktree-aid # or your preferred AUR helper
185
188
 
186
189
  You also need to install a fuzzy finder program such as [`fzf`][fzf] which is
187
190
  the default used by `worktree-aid`. See [fuzzy finder installation
188
- instructions](fuzzy-finder-installation) for possible alternatives.
191
+ instructions](#fuzzy-finder-integration) for possible alternatives.
189
192
 
190
193
  ## Setup
191
194
 
@@ -253,7 +256,7 @@ The following place-markers can be used in the definition of the base directory:
253
256
  - `{user}`: Substituted with the name of the user.
254
257
  - `{home}`: Substituted with the home directory of the user (also can use `~` at start of a path).
255
258
 
256
- Most likely if you will want to set a custom basedir then you will set `-B` as a
259
+ Most likely if you want to set a custom basedir then you will set `-B` as a
257
260
  [default option](#default-options).
258
261
 
259
262
  ## Display as Relative Worktree Directories
@@ -58,8 +58,8 @@ Commands:
58
58
  {add,a,rm,r,cd,c,ls,l,init}
59
59
  add (a) Add new worktree + branch.
60
60
  rm (r) Remove worktree + branch.
61
- cd (c) Change directory to specified worktree.
62
- ls (l) List current worktrees.
61
+ cd (c) Change worktree directory.
62
+ ls (l) List worktrees.
63
63
  init Output shell initialization code.
64
64
  ```
65
65
 
@@ -69,7 +69,7 @@ individual command:
69
69
  ### Command `add`
70
70
 
71
71
  ```
72
- usage: wt add [-h] [-d] [worktree]
72
+ usage: wt add [-h] [-d] [-c] [worktree]
73
73
 
74
74
  Add new worktree + branch.
75
75
 
@@ -80,6 +80,7 @@ positional arguments:
80
80
  options:
81
81
  -h, --help show help message and exit
82
82
  -d, --detach add detached worktree only, i.e. without adding a new branch
83
+ -c, --no-cd do not change directory to new worktree after adding it
83
84
 
84
85
  aliases: a
85
86
  ```
@@ -93,7 +94,8 @@ Remove worktree + branch.
93
94
 
94
95
  positional arguments:
95
96
  worktree worktree + branch name to remove. If not specified then
96
- fuzzy finder will prompt with a list of worktrees.
97
+ fuzzy finder will prompt with a list of worktrees, with
98
+ the current worktree as the default selection.
97
99
 
98
100
  options:
99
101
  -h, --help show help message and exit
@@ -109,11 +111,12 @@ aliases: r
109
111
  ```
110
112
  usage: wt cd [-h] [worktree]
111
113
 
112
- Change directory to specified worktree.
114
+ Change worktree directory.
113
115
 
114
116
  positional arguments:
115
- worktree Worktree name to change directory to. If not specified then
116
- fuzzy finder will prompt with a list of worktrees.
117
+ worktree Worktree name to change directory to. "/" is a shortcut to the
118
+ toplevel repository. If not specified then fuzzy finder will
119
+ prompt with a list of worktrees.
117
120
 
118
121
  options:
119
122
  -h, --help show help message and exit
@@ -126,7 +129,7 @@ aliases: c
126
129
  ```
127
130
  usage: wt ls [-h]
128
131
 
129
- List current worktrees.
132
+ List worktrees.
130
133
 
131
134
  options:
132
135
  -h, --help show help message and exit
@@ -173,7 +176,7 @@ $ yay -S worktree-aid # or your preferred AUR helper
173
176
 
174
177
  You also need to install a fuzzy finder program such as [`fzf`][fzf] which is
175
178
  the default used by `worktree-aid`. See [fuzzy finder installation
176
- instructions](fuzzy-finder-installation) for possible alternatives.
179
+ instructions](#fuzzy-finder-integration) for possible alternatives.
177
180
 
178
181
  ## Setup
179
182
 
@@ -241,7 +244,7 @@ The following place-markers can be used in the definition of the base directory:
241
244
  - `{user}`: Substituted with the name of the user.
242
245
  - `{home}`: Substituted with the home directory of the user (also can use `~` at start of a path).
243
246
 
244
- Most likely if you will want to set a custom basedir then you will set `-B` as a
247
+ Most likely if you want to set a custom basedir then you will set `-B` as a
245
248
  [default option](#default-options).
246
249
 
247
250
  ## Display as Relative Worktree Directories
@@ -4,7 +4,7 @@ check:
4
4
  ruff check {{PYFILES}}
5
5
  ty check {{PYFILES}}
6
6
  vermin -vv --no-tips -i {{PYFILES}}
7
- #md-link-checker
7
+ md-link-checker
8
8
 
9
9
  build:
10
10
  rm -rf dist
@@ -14,7 +14,7 @@ upload: build
14
14
  uv-publish
15
15
 
16
16
  doc:
17
- update-readme-usage -A -S "s/worktree-aid/wt/g"
17
+ update-readme-usage -A -S "s/worktree[-_]aid/wt/g"
18
18
 
19
19
  format:
20
20
  ruff check --select I --fix {{PYFILES}} && ruff format {{PYFILES}}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: worktree-aid
3
- Version: 1.0
3
+ Version: 1.2
4
4
  Summary: Linux command line tool to aid creating, switching, and removing git worktrees
5
5
  Author-email: Mark Blakeney <mark.blakeney@bullet-systems.net>
6
6
  License-Expression: GPL-3.0-or-later
@@ -70,8 +70,8 @@ Commands:
70
70
  {add,a,rm,r,cd,c,ls,l,init}
71
71
  add (a) Add new worktree + branch.
72
72
  rm (r) Remove worktree + branch.
73
- cd (c) Change directory to specified worktree.
74
- ls (l) List current worktrees.
73
+ cd (c) Change worktree directory.
74
+ ls (l) List worktrees.
75
75
  init Output shell initialization code.
76
76
  ```
77
77
 
@@ -81,7 +81,7 @@ individual command:
81
81
  ### Command `add`
82
82
 
83
83
  ```
84
- usage: wt add [-h] [-d] [worktree]
84
+ usage: wt add [-h] [-d] [-c] [worktree]
85
85
 
86
86
  Add new worktree + branch.
87
87
 
@@ -92,6 +92,7 @@ positional arguments:
92
92
  options:
93
93
  -h, --help show help message and exit
94
94
  -d, --detach add detached worktree only, i.e. without adding a new branch
95
+ -c, --no-cd do not change directory to new worktree after adding it
95
96
 
96
97
  aliases: a
97
98
  ```
@@ -105,7 +106,8 @@ Remove worktree + branch.
105
106
 
106
107
  positional arguments:
107
108
  worktree worktree + branch name to remove. If not specified then
108
- fuzzy finder will prompt with a list of worktrees.
109
+ fuzzy finder will prompt with a list of worktrees, with
110
+ the current worktree as the default selection.
109
111
 
110
112
  options:
111
113
  -h, --help show help message and exit
@@ -121,11 +123,12 @@ aliases: r
121
123
  ```
122
124
  usage: wt cd [-h] [worktree]
123
125
 
124
- Change directory to specified worktree.
126
+ Change worktree directory.
125
127
 
126
128
  positional arguments:
127
- worktree Worktree name to change directory to. If not specified then
128
- fuzzy finder will prompt with a list of worktrees.
129
+ worktree Worktree name to change directory to. "/" is a shortcut to the
130
+ toplevel repository. If not specified then fuzzy finder will
131
+ prompt with a list of worktrees.
129
132
 
130
133
  options:
131
134
  -h, --help show help message and exit
@@ -138,7 +141,7 @@ aliases: c
138
141
  ```
139
142
  usage: wt ls [-h]
140
143
 
141
- List current worktrees.
144
+ List worktrees.
142
145
 
143
146
  options:
144
147
  -h, --help show help message and exit
@@ -185,7 +188,7 @@ $ yay -S worktree-aid # or your preferred AUR helper
185
188
 
186
189
  You also need to install a fuzzy finder program such as [`fzf`][fzf] which is
187
190
  the default used by `worktree-aid`. See [fuzzy finder installation
188
- instructions](fuzzy-finder-installation) for possible alternatives.
191
+ instructions](#fuzzy-finder-integration) for possible alternatives.
189
192
 
190
193
  ## Setup
191
194
 
@@ -253,7 +256,7 @@ The following place-markers can be used in the definition of the base directory:
253
256
  - `{user}`: Substituted with the name of the user.
254
257
  - `{home}`: Substituted with the home directory of the user (also can use `~` at start of a path).
255
258
 
256
- Most likely if you will want to set a custom basedir then you will set `-B` as a
259
+ Most likely if you want to set a custom basedir then you will set `-B` as a
257
260
  [default option](#default-options).
258
261
 
259
262
  ## Display as Relative Worktree Directories
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.2",
3
+ "distance": 0,
4
+ "node": "gcb78b4dc270eb62209b736dedbcf8e12ad2591ec",
5
+ "dirty": false,
6
+ "branch": "main",
7
+ "node_date": "2026-08-02"
8
+ }
@@ -12,7 +12,7 @@ import shlex
12
12
  import subprocess
13
13
  import sys
14
14
  from argparse import SUPPRESS, ArgumentParser, Namespace
15
- from collections.abc import Callable, Sequence
15
+ from collections.abc import Sequence
16
16
  from dataclasses import dataclass
17
17
  from pathlib import Path
18
18
  from typing import Any
@@ -34,9 +34,6 @@ DEFAULT_FUZZY = 'fzf'
34
34
  # directory.
35
35
  BASEDIR = '../worktrees/{repo}'
36
36
 
37
- # Set of worktree names that are shortcuts to the top-level worktree.
38
- TOPLEVEL_SHORTCUTS = {'/'}
39
-
40
37
  # Template for the shell code injected into user's shell session
41
38
  SHELLCODE = """
42
39
  !cmd() {
@@ -78,11 +75,11 @@ def run(
78
75
  cmd: Sequence[str],
79
76
  *,
80
77
  stdin: str | None = None,
81
- capture: bool = True,
78
+ stdout: Any = subprocess.PIPE,
82
79
  ignore_error: bool = False,
83
80
  ) -> str:
84
81
  "Run command and return stdout"
85
- stdout = subprocess.PIPE if capture else open('/dev/tty', 'w')
82
+ capture = stdout == subprocess.PIPE
86
83
  try:
87
84
  res = subprocess.run(cmd, stdout=stdout, text=capture, input=stdin)
88
85
  except Exception as e:
@@ -94,7 +91,6 @@ def run(
94
91
  if capture:
95
92
  return res.stdout.strip()
96
93
 
97
- stdout.close() # type: ignore
98
94
  return ''
99
95
 
100
96
 
@@ -118,18 +114,18 @@ def unexpanduser(path: Path) -> Path:
118
114
  return Path('~', *path.parts[len(HOME.parts) :])
119
115
 
120
116
 
121
- def generate_new_name(exists: Callable[[str], Any]) -> str:
117
+ def generate_new_name(exists: set[str]) -> str:
122
118
  "Generate a new worktree name"
123
119
  from coolname import generate_slug
124
120
 
125
121
  for _ in range(100):
126
- if not exists(name := generate_slug(2)):
122
+ if (name := generate_slug(2)) not in exists:
127
123
  return name
128
124
 
129
125
  sys.exit('error: failed to generate a new worktree name.')
130
126
 
131
127
 
132
- def print_version(args: Namespace) -> int:
128
+ def print_version(args: Namespace) -> None:
133
129
  "Print program version"
134
130
  from importlib import metadata
135
131
 
@@ -138,25 +134,16 @@ def print_version(args: Namespace) -> int:
138
134
  except Exception:
139
135
  version = '?'
140
136
 
141
- if args._:
142
- with open('/dev/tty', 'w') as tty:
143
- print(version, file=tty)
144
-
145
- return 2
137
+ print(version, file=args._stdout)
146
138
 
147
- print(version)
148
- return 0
149
139
 
150
-
151
- def print_help(args: Namespace) -> int:
140
+ def print_help(args: Namespace) -> None:
152
141
  "Print program help message"
153
142
  if 'parser' in args:
154
143
  args.parser.print_help(sys.stderr)
155
144
  else:
156
145
  args._opt.print_help(sys.stderr)
157
146
 
158
- return 2 if args._ else 0
159
-
160
147
 
161
148
  @dataclass
162
149
  class Tree:
@@ -172,6 +159,7 @@ class Trees:
172
159
  "Class to manage the collection of worktrees"
173
160
 
174
161
  trees: list[Tree]
162
+ names: dict[str, Tree]
175
163
  toplevel: Tree
176
164
  current: Tree
177
165
 
@@ -179,6 +167,7 @@ class Trees:
179
167
  def fetch(cls, args: Namespace) -> None:
180
168
  "Get worktrees"
181
169
  trees = []
170
+ names = {}
182
171
  tree = None
183
172
  cwdparts = Path.cwd().parts
184
173
  phere = pindex = -1
@@ -207,6 +196,9 @@ class Trees:
207
196
  pindex = len(trees)
208
197
 
209
198
  trees.append(tree := Tree(path, path_display))
199
+
200
+ if (name := path.name) not in names or name == tree.branch:
201
+ names[name] = tree
210
202
  elif tree:
211
203
  if field == 'HEAD':
212
204
  tree.head = value[:HASH_LEN]
@@ -224,6 +216,7 @@ class Trees:
224
216
 
225
217
  cls.current = trees[0]
226
218
  cls.trees = trees
219
+ cls.names = names
227
220
 
228
221
  @classmethod
229
222
  def get_trees(cls) -> list[str]:
@@ -244,25 +237,20 @@ class Trees:
244
237
  return lines
245
238
 
246
239
  @classmethod
247
- def get_tree(cls, text: str) -> Tree | None:
248
- "Return first tree where branch, then path name, then head matches given text"
249
- for tree in cls.trees:
250
- if tree.branch == text:
251
- return tree
252
-
253
- for tree in cls.trees:
254
- if tree.path.name == text:
255
- return tree
256
-
257
- for tree in cls.trees:
258
- if tree.head == text:
259
- return tree
240
+ def get_tree(cls, name: str) -> Tree | None:
241
+ "Return worktree with given name, or None if not found"
242
+ return cls.current if cls.current.path.name == name else cls.names.get(name)
260
243
 
261
244
  @classmethod
262
245
  def create_worktree(cls, name: str, args: Namespace) -> Path:
263
246
  "Create a new worktree and branch with the given name"
264
247
  if not name:
265
- name = generate_new_name(cls.get_tree)
248
+ # If no name is given, generate a new name that does not conflict
249
+ # with existing worktrees or branches
250
+ existing = {t.path.name for t in cls.trees}
251
+ branches = run('git --no-pager branch --list'.split()).splitlines()
252
+ existing.update(b.split(maxsplit=1)[1] for b in branches)
253
+ name = generate_new_name(existing)
266
254
 
267
255
  try:
268
256
  basedir = args.basedir.format(
@@ -272,14 +260,14 @@ class Trees:
272
260
  sys.exit(f'error: failed to format basedir: {e}')
273
261
 
274
262
  basepath = Path(basedir).expanduser()
275
- basepath = cls.toplevel.path / basepath
263
+ basepath = (cls.toplevel.path / basepath).resolve()
276
264
  basepath.mkdir(parents=True, exist_ok=True)
277
265
  path = basepath / name
278
266
 
279
267
  cmd = 'git worktree add'.split()
280
268
  if args.detach:
281
269
  cmd.append('--detach')
282
- run(cmd + [str(path)], capture=False)
270
+ run(cmd + [str(path)], stdout=args._stdout)
283
271
  return path
284
272
 
285
273
  @classmethod
@@ -300,15 +288,14 @@ class Trees:
300
288
  if args.force:
301
289
  cmd.append('--force')
302
290
 
303
- run(cmd + [str(tree.path)], capture=False)
291
+ run(cmd + [str(tree.path)], stdout=args._stdout)
304
292
 
305
293
  path_display = os.path.relpath(tree.path) if args.relative else str(tree.path)
306
- with open('/dev/tty', 'w') as tty:
307
- print(f'Removed worktree "{path_display}"', file=tty)
294
+ print(f'Removed worktree "{path_display}"', file=args._stdout)
308
295
 
309
296
  if tree.branch and not args.keep_branch:
310
297
  cmd = 'git branch'.split() + ['-D' if args.force else '-d']
311
- run(cmd + [tree.branch], capture=False, ignore_error=True)
298
+ run(cmd + [tree.branch], stdout=args._stdout, ignore_error=True)
312
299
 
313
300
  return cls.toplevel.path if tree == cls.current else None
314
301
 
@@ -402,18 +389,30 @@ def main() -> int:
402
389
 
403
390
  args = opt.parse_args()
404
391
  args._opt = opt
392
+ if args._:
393
+ try:
394
+ args._stdout = open('/dev/tty', 'w')
395
+ except Exception as e:
396
+ sys.exit(f'error: can not write to terminal in shell function mode: {e}')
405
397
 
406
- if args.version:
407
- return print_version(args)
408
-
409
- if args.help or 'func' not in args:
410
- return print_help(args)
398
+ shell_return = 2
399
+ else:
400
+ args._stdout = sys.stdout
401
+ shell_return = 0
411
402
 
412
- if out := args.func(args):
403
+ if args.version:
404
+ print_version(args)
405
+ elif args.help or 'func' not in args:
406
+ print_help(args)
407
+ elif out := args.func(args):
413
408
  print(out)
414
- return 0
409
+ shell_return = 0
415
410
 
416
- return 2 if args._ else 0
411
+ # Code checkers like us to explicitly close files we open
412
+ if args._:
413
+ args._stdout.close()
414
+
415
+ return shell_return
417
416
 
418
417
 
419
418
  # COMMAND
@@ -430,6 +429,12 @@ class add_:
430
429
  action='store_true',
431
430
  help='add detached worktree only, i.e. without adding a new branch',
432
431
  )
432
+ parser.add_argument(
433
+ '-c',
434
+ '--no-cd',
435
+ action='store_true',
436
+ help='do not change directory to new worktree after adding it',
437
+ )
433
438
  parser.add_argument(
434
439
  'worktree',
435
440
  default='',
@@ -440,7 +445,8 @@ class add_:
440
445
  @staticmethod
441
446
  def run(args: Namespace) -> str | None:
442
447
  Trees.fetch(args)
443
- return str(Trees.create_worktree(args.worktree, args))
448
+ path = Trees.create_worktree(args.worktree, args)
449
+ return str(path) if path and not args.no_cd else None
444
450
 
445
451
 
446
452
  # COMMAND
@@ -469,7 +475,8 @@ class rm_:
469
475
  default='',
470
476
  nargs='?',
471
477
  help='worktree + branch name to remove. If not specified then '
472
- 'fuzzy finder will prompt with a list of worktrees.',
478
+ 'fuzzy finder will prompt with a list of worktrees, with the '
479
+ 'current worktree as the default selection.',
473
480
  )
474
481
 
475
482
  @staticmethod
@@ -480,15 +487,13 @@ class rm_:
480
487
  return None
481
488
  name = path.name
482
489
 
483
- if not (path := Trees.remove_worktree(name, args)):
484
- return None
485
-
486
- return str(path)
490
+ path = Trees.remove_worktree(name, args)
491
+ return str(path) if path else None
487
492
 
488
493
 
489
494
  # COMMAND
490
495
  class cd_:
491
- "Change directory to specified worktree."
496
+ "Change worktree directory."
492
497
 
493
498
  aliases = ('c',)
494
499
 
@@ -498,15 +503,15 @@ class cd_:
498
503
  'worktree',
499
504
  default='',
500
505
  nargs='?',
501
- help='Worktree name to change directory to. If not specified then '
502
- 'fuzzy finder will prompt with a list of worktrees.',
506
+ help='Worktree name to change directory to. "/" is a shortcut to the toplevel repository. '
507
+ 'If not specified then fuzzy finder will prompt with a list of worktrees.',
503
508
  )
504
509
 
505
510
  @staticmethod
506
511
  def run(args: Namespace) -> str | None:
507
512
  Trees.fetch(args)
508
513
  if name := args.worktree:
509
- if name in TOPLEVEL_SHORTCUTS:
514
+ if name == '/':
510
515
  path = Trees.toplevel.path
511
516
  elif tree := Trees.get_tree(name):
512
517
  path = tree.path
@@ -520,7 +525,7 @@ class cd_:
520
525
 
521
526
  # COMMAND
522
527
  class ls_:
523
- "List current worktrees."
528
+ "List worktrees."
524
529
 
525
530
  aliases = ('l',)
526
531
 
@@ -528,9 +533,8 @@ class ls_:
528
533
  def run(args: Namespace) -> str | None:
529
534
  Trees.fetch(args)
530
535
  trees = Trees.get_trees()
531
- with open('/dev/tty', 'w') as tty:
532
- for line in reversed(trees):
533
- print(line, file=tty)
536
+ for line in reversed(trees):
537
+ print(line, file=args._stdout)
534
538
 
535
539
  return None
536
540
 
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "1.0",
3
- "distance": 0,
4
- "node": "g5fd802c5aef974791beafe1a571d4a74c6773fd1",
5
- "dirty": false,
6
- "branch": "main",
7
- "node_date": "2026-08-01"
8
- }
File without changes
File without changes
File without changes