git-p4son 0.2.9__tar.gz → 0.2.11__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.
Files changed (52) hide show
  1. {git_p4son-0.2.9 → git_p4son-0.2.11}/PKG-INFO +46 -5
  2. {git_p4son-0.2.9 → git_p4son-0.2.11}/README.md +45 -4
  3. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/__init__.py +1 -1
  4. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/alias.py +54 -21
  5. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/changelist_store.py +36 -6
  6. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/cli.py +14 -7
  7. git_p4son-0.2.11/git_p4son/common.py +319 -0
  8. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/complete.py +5 -0
  9. git_p4son-0.2.11/git_p4son/config.py +89 -0
  10. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/git.py +53 -7
  11. git_p4son-0.2.11/git_p4son/hooks.py +121 -0
  12. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/init.py +16 -2
  13. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/lib.py +67 -17
  14. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/log.py +84 -13
  15. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/new.py +4 -2
  16. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/perforce.py +83 -26
  17. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/review.py +6 -0
  18. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/sync.py +54 -86
  19. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son.egg-info/PKG-INFO +46 -5
  20. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son.egg-info/SOURCES.txt +4 -0
  21. {git_p4son-0.2.9 → git_p4son-0.2.11}/pyproject.toml +1 -1
  22. git_p4son-0.2.11/tests/test_alias.py +106 -0
  23. {git_p4son-0.2.9 → git_p4son-0.2.11}/tests/test_changelist_store.py +59 -1
  24. {git_p4son-0.2.9 → git_p4son-0.2.11}/tests/test_cli.py +8 -1
  25. {git_p4son-0.2.9 → git_p4son-0.2.11}/tests/test_common.py +101 -6
  26. {git_p4son-0.2.9 → git_p4son-0.2.11}/tests/test_complete.py +40 -0
  27. {git_p4son-0.2.9 → git_p4son-0.2.11}/tests/test_config.py +47 -0
  28. git_p4son-0.2.11/tests/test_hooks.py +197 -0
  29. {git_p4son-0.2.9 → git_p4son-0.2.11}/tests/test_init.py +42 -5
  30. {git_p4son-0.2.9 → git_p4son-0.2.11}/tests/test_lib_changelist.py +82 -21
  31. {git_p4son-0.2.9 → git_p4son-0.2.11}/tests/test_lib_edit.py +81 -0
  32. {git_p4son-0.2.9 → git_p4son-0.2.11}/tests/test_lib_review.py +27 -11
  33. {git_p4son-0.2.9 → git_p4son-0.2.11}/tests/test_list_changes.py +2 -1
  34. git_p4son-0.2.11/tests/test_log.py +69 -0
  35. git_p4son-0.2.11/tests/test_new.py +56 -0
  36. {git_p4son-0.2.9 → git_p4son-0.2.11}/tests/test_perforce.py +21 -2
  37. {git_p4son-0.2.9 → git_p4son-0.2.11}/tests/test_review.py +16 -1
  38. {git_p4son-0.2.9 → git_p4son-0.2.11}/tests/test_sync.py +96 -50
  39. git_p4son-0.2.9/git_p4son/common.py +0 -235
  40. git_p4son-0.2.9/git_p4son/config.py +0 -42
  41. git_p4son-0.2.9/tests/test_log.py +0 -257
  42. {git_p4son-0.2.9 → git_p4son-0.2.11}/LICENSE +0 -0
  43. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/__main__.py +0 -0
  44. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/completions/_git-p4son +0 -0
  45. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/completions/git-p4son.bash +0 -0
  46. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/completions/git-p4son.ps1 +0 -0
  47. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/list_changes.py +0 -0
  48. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son/update.py +0 -0
  49. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son.egg-info/dependency_links.txt +0 -0
  50. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son.egg-info/entry_points.txt +0 -0
  51. {git_p4son-0.2.9 → git_p4son-0.2.11}/git_p4son.egg-info/top_level.txt +0 -0
  52. {git_p4son-0.2.9 → git_p4son-0.2.11}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-p4son
3
- Version: 0.2.9
3
+ Version: 0.2.11
4
4
  Summary: Utility for keeping a Perforce workspace and local git repo in sync
5
5
  Author-email: Andreas Andersson <andreas@neoboid.com>
6
6
  License-Expression: MIT
@@ -276,6 +276,11 @@ git p4son sync last-synced
276
276
  git p4son sync 12345 --force
277
277
  ```
278
278
 
279
+ #### post-sync hook
280
+
281
+ After a successful `git-p4son sync` that actually performs sync work, git-p4son runs [hooks](#hooks)
282
+ from `.git-p4son/hooks/post-sync/`.
283
+
279
284
  ### New Command
280
285
 
281
286
  Create a new Perforce changelist and add changed files to it. Description will contain an enumerated list of git commits since the base branch.
@@ -312,7 +317,11 @@ git p4son new -m "New feature" --review -b main
312
317
 
313
318
  ### Update Command
314
319
 
315
- Update an existing Perforce changelist description by replacing the enumerated commit list with the current commits since the base branch. By default also opens changed files for edit.
320
+ Update an existing Perforce changelist description. Commits since the base branch replace their existing
321
+ entries in the enumerated commit list (matched by subject) and new ones are appended; entries outside the
322
+ range are kept and the list is renumbered. So `update -b main` rebuilds the whole list without duplicating
323
+ it, while `update -b HEAD~3` only refreshes the last three entries. By default also opens changed files for
324
+ edit.
316
325
 
317
326
  ```sh
318
327
  git p4son update [changelist] [--base-branch BASE_BRANCH] [--dry-run] [--no-desc] [--no-edit] [--shelve]
@@ -459,16 +468,21 @@ git p4son alias delete myfeature
459
468
 
460
469
  #### alias clean
461
470
 
462
- Interactively review and delete changelist aliases:
471
+ Clean up changelist aliases:
463
472
 
464
473
  ```sh
465
474
  git p4son alias clean
466
475
  ```
467
476
 
468
- This command iterates through each alias, displays it, and prompts for action:
477
+ The command lists all aliases and asks how to proceed:
478
+ - `a` (all): Delete all aliases
479
+ - `i` (interactive): Review each alias one by one
480
+ - `q` (quit): Exit without deleting anything
481
+
482
+ In interactive mode, each alias is displayed in turn with a prompt:
469
483
  - `y` (yes): Delete this alias
470
484
  - `n` (no): Keep this alias
471
- - `a` (all): Delete this and all remaining aliases
485
+ - `a` (all): Delete this and all remaining aliases (handy for keeping the first few and sweeping the rest)
472
486
  - `q` (quit): Stop and keep remaining aliases
473
487
 
474
488
  **Examples:**
@@ -500,6 +514,32 @@ Most commands that accept an alias or changelist argument default to the current
500
514
  The keyword cannot be used in a detached HEAD state. Use `--no-alias` (on `new` and `review`) or supply an
501
515
  explicit alias name instead.
502
516
 
517
+ ## Hooks
518
+
519
+ Hooks work similarly to git hooks. They are executables (unix)/scripts (windows) that you place in
520
+ the `.git-p4son/hooks/[hook-name]/` directory. These scripts will be called to add or modify behavior
521
+ at defined times during git-p4son operations. Scripts get run in filename (lexicographically) sorted
522
+ order.
523
+
524
+ The hook process CWD is the directory where git-p4son was invoked, and `GIT_P4SON_REPO_ROOT_DIR` is
525
+ set to the git-p4son repository root.
526
+
527
+ On Windows, `.ps1`, `.nu`, `.sh`, and `.py` files are run with `powershell.exe`, `nushell.exe`, `bash.exe`, and
528
+ `python.exe` respectively. Customize or add Windows script associations in `.git-p4son/config.toml`:
529
+
530
+ ```toml
531
+ [hooks.extension-associations]
532
+ ".ps1" = ["pwsh.exe", "-NoProfile", "-File"]
533
+ ".py" = "python.exe"
534
+ ```
535
+
536
+ Non-executable or unrecognized hook files are skipped with a warning.
537
+
538
+ ### post-sync hook
539
+
540
+ After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
541
+ `.git-p4son/hooks/post-sync/`.
542
+
503
543
  ## Shell Completions
504
544
 
505
545
  Tab completion is available for bash, zsh, and PowerShell, including commands, flags, and dynamic alias names.
@@ -530,3 +570,4 @@ Add the following to your PowerShell profile (`$PROFILE`):
530
570
  ```
531
571
 
532
572
  All three enable completion for `git p4son <TAB>` and `git-p4son <TAB>`.
573
+
@@ -253,6 +253,11 @@ git p4son sync last-synced
253
253
  git p4son sync 12345 --force
254
254
  ```
255
255
 
256
+ #### post-sync hook
257
+
258
+ After a successful `git-p4son sync` that actually performs sync work, git-p4son runs [hooks](#hooks)
259
+ from `.git-p4son/hooks/post-sync/`.
260
+
256
261
  ### New Command
257
262
 
258
263
  Create a new Perforce changelist and add changed files to it. Description will contain an enumerated list of git commits since the base branch.
@@ -289,7 +294,11 @@ git p4son new -m "New feature" --review -b main
289
294
 
290
295
  ### Update Command
291
296
 
292
- Update an existing Perforce changelist description by replacing the enumerated commit list with the current commits since the base branch. By default also opens changed files for edit.
297
+ Update an existing Perforce changelist description. Commits since the base branch replace their existing
298
+ entries in the enumerated commit list (matched by subject) and new ones are appended; entries outside the
299
+ range are kept and the list is renumbered. So `update -b main` rebuilds the whole list without duplicating
300
+ it, while `update -b HEAD~3` only refreshes the last three entries. By default also opens changed files for
301
+ edit.
293
302
 
294
303
  ```sh
295
304
  git p4son update [changelist] [--base-branch BASE_BRANCH] [--dry-run] [--no-desc] [--no-edit] [--shelve]
@@ -436,16 +445,21 @@ git p4son alias delete myfeature
436
445
 
437
446
  #### alias clean
438
447
 
439
- Interactively review and delete changelist aliases:
448
+ Clean up changelist aliases:
440
449
 
441
450
  ```sh
442
451
  git p4son alias clean
443
452
  ```
444
453
 
445
- This command iterates through each alias, displays it, and prompts for action:
454
+ The command lists all aliases and asks how to proceed:
455
+ - `a` (all): Delete all aliases
456
+ - `i` (interactive): Review each alias one by one
457
+ - `q` (quit): Exit without deleting anything
458
+
459
+ In interactive mode, each alias is displayed in turn with a prompt:
446
460
  - `y` (yes): Delete this alias
447
461
  - `n` (no): Keep this alias
448
- - `a` (all): Delete this and all remaining aliases
462
+ - `a` (all): Delete this and all remaining aliases (handy for keeping the first few and sweeping the rest)
449
463
  - `q` (quit): Stop and keep remaining aliases
450
464
 
451
465
  **Examples:**
@@ -477,6 +491,32 @@ Most commands that accept an alias or changelist argument default to the current
477
491
  The keyword cannot be used in a detached HEAD state. Use `--no-alias` (on `new` and `review`) or supply an
478
492
  explicit alias name instead.
479
493
 
494
+ ## Hooks
495
+
496
+ Hooks work similarly to git hooks. They are executables (unix)/scripts (windows) that you place in
497
+ the `.git-p4son/hooks/[hook-name]/` directory. These scripts will be called to add or modify behavior
498
+ at defined times during git-p4son operations. Scripts get run in filename (lexicographically) sorted
499
+ order.
500
+
501
+ The hook process CWD is the directory where git-p4son was invoked, and `GIT_P4SON_REPO_ROOT_DIR` is
502
+ set to the git-p4son repository root.
503
+
504
+ On Windows, `.ps1`, `.nu`, `.sh`, and `.py` files are run with `powershell.exe`, `nushell.exe`, `bash.exe`, and
505
+ `python.exe` respectively. Customize or add Windows script associations in `.git-p4son/config.toml`:
506
+
507
+ ```toml
508
+ [hooks.extension-associations]
509
+ ".ps1" = ["pwsh.exe", "-NoProfile", "-File"]
510
+ ".py" = "python.exe"
511
+ ```
512
+
513
+ Non-executable or unrecognized hook files are skipped with a warning.
514
+
515
+ ### post-sync hook
516
+
517
+ After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
518
+ `.git-p4son/hooks/post-sync/`.
519
+
480
520
  ## Shell Completions
481
521
 
482
522
  Tab completion is available for bash, zsh, and PowerShell, including commands, flags, and dynamic alias names.
@@ -507,3 +547,4 @@ Add the following to your PowerShell profile (`$PROFILE`):
507
547
  ```
508
548
 
509
549
  All three enable completion for `git p4son <TAB>` and `git-p4son <TAB>`.
550
+
@@ -7,6 +7,6 @@ creating and updating changelists, and managing Swarm reviews.
7
7
 
8
8
  CONFIG_DIR = '.git-p4son'
9
9
 
10
- __version__ = "0.2.9"
10
+ __version__ = "0.2.11"
11
11
  __author__ = "Andreas Andersson"
12
12
  __email__ = "andreas@neoboid.com"
@@ -54,43 +54,52 @@ def alias_delete_command(args: argparse.Namespace) -> int:
54
54
  return 0
55
55
 
56
56
 
57
- def _prompt_delete(prompt: str) -> str | None:
58
- """Prompt until a valid y/n/a/q response is given. Returns None on EOF."""
57
+ def _prompt_choice(prefix: str, options: list[str]) -> str | None:
58
+ """Prompt until the response matches an option or its first letter.
59
+
60
+ The choices are rendered from the options, e.g. "[y]es / [n]o", so the
61
+ prompt and the accepted keys cannot drift apart. Options must have
62
+ unique first letters. Returns the chosen option, or None on EOF.
63
+ """
64
+ shorthands = {option[0]: option for option in options}
65
+ rendered = ' / '.join(f'[{option[0]}]{option[1:]}' for option in options)
66
+ keys = list(shorthands)
59
67
  while True:
60
68
  try:
61
- response = input(prompt).strip().lower()
69
+ response = input(f'{prefix} {rendered}: ').strip().lower()
62
70
  except EOFError:
63
71
  print()
64
72
  return None
65
73
 
66
- if response in ('y', 'yes'):
67
- return 'yes'
68
- elif response in ('n', 'no'):
69
- return 'no'
70
- elif response in ('a', 'all'):
71
- return 'all'
72
- elif response in ('q', 'quit'):
73
- return 'quit'
74
+ if response in shorthands:
75
+ return shorthands[response]
76
+ elif response in options:
77
+ return response
74
78
  else:
75
- print('Please enter y, n, a, or q')
79
+ print('Please enter ' + ', '.join(keys[:-1]) + f' or {keys[-1]}')
76
80
 
77
81
 
78
- def alias_clean_command(args: argparse.Namespace) -> int:
79
- """Execute the 'alias clean' command with interactive prompts."""
80
- workspace_dir = args.workspace_dir
82
+ def _clean_all(aliases: list[tuple[str, str]], workspace_dir: str) -> None:
83
+ """Delete every alias without further prompting."""
84
+ for name, _changelist in aliases:
85
+ if delete_changelist_alias(name, workspace_dir):
86
+ log.success(f'Deleted "{name}"')
81
87
 
82
- aliases = list_changelist_aliases(workspace_dir)
83
- if not aliases:
84
- log.success('No changelist aliases to clean')
85
- return 0
86
88
 
89
+ def _clean_interactive(aliases: list[tuple[str, str]],
90
+ workspace_dir: str) -> None:
91
+ """Review each alias in turn with yes/no/all/quit prompts.
92
+
93
+ The all option lets the user keep the first aliases and sweep the rest:
94
+ answer no until the last one worth keeping, then a deletes the
95
+ remainder without further prompts."""
87
96
  response = None
88
97
  for name, changelist in aliases:
89
98
  log.heading(f'{name} -> CL {changelist}')
90
99
 
91
100
  if response != 'all':
92
- response = _prompt_delete(
93
- 'Delete? [y]es / [n]o / [a]ll / [q]uit: ')
101
+ response = _prompt_choice(
102
+ 'Delete?', ['yes', 'no', 'all', 'quit'])
94
103
 
95
104
  if response is None or response == 'quit':
96
105
  log.info('Aborting')
@@ -102,6 +111,30 @@ def alias_clean_command(args: argparse.Namespace) -> int:
102
111
  if delete_changelist_alias(name, workspace_dir):
103
112
  log.success('Deleted')
104
113
 
114
+
115
+ def alias_clean_command(args: argparse.Namespace) -> int:
116
+ """Execute the 'alias clean' command."""
117
+ workspace_dir = args.workspace_dir
118
+
119
+ aliases = list_changelist_aliases(workspace_dir)
120
+ if not aliases:
121
+ log.success('No changelist aliases to clean')
122
+ return 0
123
+
124
+ log.info(f'Found {len(aliases)}')
125
+ for name, changelist in aliases:
126
+ log.info(f'{name} -> {changelist}')
127
+
128
+ mode = _prompt_choice('Delete?', ['all', 'interactive', 'quit'])
129
+ if mode is None or mode == 'quit':
130
+ log.info('Aborting')
131
+ return 0
132
+
133
+ if mode == 'all':
134
+ _clean_all(aliases, workspace_dir)
135
+ else:
136
+ _clean_interactive(aliases, workspace_dir)
137
+
105
138
  return 0
106
139
 
107
140
 
@@ -19,6 +19,13 @@ RESERVED_KEYWORDS = frozenset({'last-synced', 'branch'})
19
19
  # (Windows disallows trailing dots).
20
20
  _ALIAS_NAME_RE = re.compile(r'^[A-Za-z0-9_-]([A-Za-z0-9._-]*[A-Za-z0-9_-])?$')
21
21
 
22
+ # Windows device names; opening such a path hits the device namespace
23
+ # instead of creating a file, on any drive and regardless of extension.
24
+ _WINDOWS_RESERVED_NAMES = frozenset(
25
+ {'con', 'prn', 'aux', 'nul'}
26
+ | {f'com{i}' for i in range(1, 10)}
27
+ | {f'lpt{i}' for i in range(1, 10)})
28
+
22
29
 
23
30
  def validate_alias_name(name: str) -> str | None:
24
31
  """Return an error message if alias name is invalid, else None."""
@@ -30,6 +37,15 @@ def validate_alias_name(name: str) -> str | None:
30
37
  return (
31
38
  f'Invalid alias name "{name}": must contain only letters, digits, '
32
39
  'hyphens, underscores, and dots, and must not start or end with a dot')
40
+ if name.isdigit():
41
+ # Digit strings are always interpreted as changelist numbers, so
42
+ # such an alias could be created but never referenced.
43
+ return (
44
+ f'Invalid alias name "{name}": an all-digit name would be '
45
+ 'indistinguishable from a changelist number')
46
+ if name.split('.', 1)[0].lower() in _WINDOWS_RESERVED_NAMES:
47
+ return (
48
+ f'Invalid alias name "{name}": reserved device name on Windows')
33
49
  return None
34
50
 
35
51
 
@@ -38,6 +54,18 @@ def _changelists_dir(workspace_dir: str) -> str:
38
54
  return os.path.join(workspace_dir, CONFIG_DIR, 'changelists')
39
55
 
40
56
 
57
+ def _alias_path(name: str, workspace_dir: str) -> str | None:
58
+ """Validate name and return its store path, or None if invalid.
59
+
60
+ Validating on every lookup, not just on save, keeps raw user input
61
+ (e.g. "../../somefile") from escaping the store directory."""
62
+ error = validate_alias_name(name)
63
+ if error:
64
+ log.error(error)
65
+ return None
66
+ return os.path.join(_changelists_dir(workspace_dir), name)
67
+
68
+
41
69
  def alias_exists(name: str, workspace_dir: str) -> bool:
42
70
  """Check whether a changelist alias exists."""
43
71
  alias_path = os.path.join(_changelists_dir(workspace_dir), name)
@@ -46,13 +74,11 @@ def alias_exists(name: str, workspace_dir: str) -> bool:
46
74
 
47
75
  def save_changelist_alias(name: str, changelist: str, workspace_dir: str, force: bool = False) -> bool:
48
76
  """Save a changelist number under a named alias."""
49
- error = validate_alias_name(name)
50
- if error:
51
- log.error(error)
77
+ alias_path = _alias_path(name, workspace_dir)
78
+ if alias_path is None:
52
79
  return False
53
80
 
54
81
  changelists_dir = _changelists_dir(workspace_dir)
55
- alias_path = os.path.join(changelists_dir, name)
56
82
 
57
83
  if os.path.exists(alias_path) and not force:
58
84
  log.error(
@@ -71,7 +97,9 @@ def save_changelist_alias(name: str, changelist: str, workspace_dir: str, force:
71
97
 
72
98
  def load_changelist_alias(name: str, workspace_dir: str) -> str | None:
73
99
  """Load a changelist number from a named alias, or None if not found."""
74
- alias_path = os.path.join(_changelists_dir(workspace_dir), name)
100
+ alias_path = _alias_path(name, workspace_dir)
101
+ if alias_path is None:
102
+ return None
75
103
 
76
104
  if not os.path.exists(alias_path):
77
105
  log.error(f'No changelist alias found: {name}')
@@ -108,7 +136,9 @@ def list_changelist_aliases(workspace_dir: str) -> list[tuple[str, str]]:
108
136
 
109
137
  def delete_changelist_alias(name: str, workspace_dir: str) -> bool:
110
138
  """Delete a changelist alias file."""
111
- alias_path = os.path.join(_changelists_dir(workspace_dir), name)
139
+ alias_path = _alias_path(name, workspace_dir)
140
+ if alias_path is None:
141
+ return False
112
142
 
113
143
  if not os.path.exists(alias_path):
114
144
  log.error(f'No changelist alias found: {name}')
@@ -3,6 +3,7 @@ Main CLI entry point for git-p4son.
3
3
  """
4
4
 
5
5
  import argparse
6
+ import os
6
7
  import sys
7
8
  import time
8
9
  from importlib.resources import files
@@ -148,6 +149,7 @@ Examples:
148
149
  )
149
150
  new_parser.add_argument(
150
151
  '-s', '--sleep',
152
+ type=int,
151
153
  help='Sleep for the specified number of seconds after the command is done'
152
154
  )
153
155
 
@@ -155,9 +157,11 @@ Examples:
155
157
  update_parser = subparsers.add_parser(
156
158
  'update',
157
159
  help='Update an existing changelist description and open files for edit',
158
- description='Update an existing Perforce changelist description by '
159
- 'replacing the enumerated commit list with the current commits '
160
- 'since the base branch. By default also opens changed files for edit.'
160
+ description='Update an existing Perforce changelist description: '
161
+ 'commits since the base branch replace their existing entries in '
162
+ 'the enumerated commit list and new ones are appended; entries '
163
+ 'outside the range are kept. By default also opens changed files '
164
+ 'for edit.'
161
165
  )
162
166
  update_parser.add_argument(
163
167
  'changelist',
@@ -193,6 +197,7 @@ Examples:
193
197
  )
194
198
  update_parser.add_argument(
195
199
  '-s', '--sleep',
200
+ type=int,
196
201
  help='Sleep for the specified number of seconds after the command is done'
197
202
  )
198
203
 
@@ -267,9 +272,9 @@ Examples:
267
272
  # alias clean
268
273
  alias_subparsers.add_parser(
269
274
  'clean',
270
- help='Interactive cleanup of changelist aliases',
271
- description='Interactively review and delete changelist aliases with '
272
- 'yes/no/all/quit prompts'
275
+ help='Clean up changelist aliases',
276
+ description='List all changelist aliases, then delete all of them '
277
+ 'or review each one interactively with yes/no/all/quit prompts'
273
278
  )
274
279
 
275
280
  # Review subcommand
@@ -378,6 +383,8 @@ def completion_command(args: argparse.Namespace) -> int:
378
383
 
379
384
 
380
385
  def run_command(args: argparse.Namespace) -> int:
386
+ args.invocation_dir = os.getcwd()
387
+
381
388
  log.heading('Finding workspace directory')
382
389
  args.workspace_dir = get_workspace_dir()
383
390
  if not args.workspace_dir:
@@ -465,7 +472,7 @@ def main() -> int:
465
472
  exit_code = run_command(args)
466
473
 
467
474
  if exit_code == 0 and getattr(args, 'sleep', None) is not None:
468
- seconds = int(args.sleep)
475
+ seconds = args.sleep
469
476
  log.heading(f'Sleeping for {seconds} seconds')
470
477
  time.sleep(seconds)
471
478
  log.success('awake again')