git-p4son 0.3.1__tar.gz → 0.3.3__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 (54) hide show
  1. {git_p4son-0.3.1 → git_p4son-0.3.3}/PKG-INFO +97 -3
  2. {git_p4son-0.3.1 → git_p4son-0.3.3}/README.md +96 -2
  3. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/__init__.py +1 -1
  4. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/cli.py +38 -0
  5. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/complete.py +4 -0
  6. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/perforce.py +27 -0
  7. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/sync.py +208 -127
  8. git_p4son-0.3.3/git_p4son/sync_split.py +162 -0
  9. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son.egg-info/PKG-INFO +97 -3
  10. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son.egg-info/SOURCES.txt +3 -1
  11. {git_p4son-0.3.1 → git_p4son-0.3.3}/pyproject.toml +1 -1
  12. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_complete.py +3 -2
  13. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_perforce.py +48 -2
  14. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_sync.py +11 -4
  15. git_p4son-0.3.3/tests/test_sync_split.py +439 -0
  16. {git_p4son-0.3.1 → git_p4son-0.3.3}/LICENSE +0 -0
  17. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/__main__.py +0 -0
  18. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/alias.py +0 -0
  19. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/changelist_store.py +0 -0
  20. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/common.py +0 -0
  21. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/completions/_git-p4son +0 -0
  22. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/completions/git-p4son.bash +0 -0
  23. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/completions/git-p4son.ps1 +0 -0
  24. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/config.py +0 -0
  25. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/git.py +0 -0
  26. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/hooks.py +0 -0
  27. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/init.py +0 -0
  28. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/lib.py +0 -0
  29. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/list_changes.py +0 -0
  30. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/log.py +0 -0
  31. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/new.py +0 -0
  32. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/review.py +0 -0
  33. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/state.py +0 -0
  34. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son/update.py +0 -0
  35. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son.egg-info/dependency_links.txt +0 -0
  36. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son.egg-info/entry_points.txt +0 -0
  37. {git_p4son-0.3.1 → git_p4son-0.3.3}/git_p4son.egg-info/top_level.txt +0 -0
  38. {git_p4son-0.3.1 → git_p4son-0.3.3}/setup.cfg +0 -0
  39. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_alias.py +0 -0
  40. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_changelist_store.py +0 -0
  41. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_cli.py +0 -0
  42. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_common.py +0 -0
  43. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_config.py +0 -0
  44. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_git.py +0 -0
  45. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_hooks.py +0 -0
  46. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_init.py +0 -0
  47. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_lib_changelist.py +0 -0
  48. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_lib_edit.py +0 -0
  49. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_lib_review.py +0 -0
  50. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_list_changes.py +0 -0
  51. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_log.py +0 -0
  52. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_new.py +0 -0
  53. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_review.py +0 -0
  54. {git_p4son-0.3.1 → git_p4son-0.3.3}/tests/test_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-p4son
3
- Version: 0.3.1
3
+ Version: 0.3.3
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
@@ -233,8 +233,8 @@ commit any files that need attention.
233
233
 
234
234
  ## Usage
235
235
 
236
- git-p4son provides eight commands: `init`, `sync`, `new`, `update`, `review`, `list-changes`, `alias`,
237
- and `completion`.
236
+ git-p4son provides nine commands: `init`, `sync`, `sync-split`, `new`, `update`, `review`, `list-changes`,
237
+ `alias`, and `completion`.
238
238
 
239
239
  To see help for any command, use `-h`:
240
240
 
@@ -315,6 +315,57 @@ changelists is given, the pre-sync hooks run once, before the first sync.
315
315
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs [hooks](#hooks)
316
316
  from `.git-p4son/hooks/post-sync/`.
317
317
 
318
+ ### Sync-Split Command
319
+
320
+ Sync forward from the last synced changelist, splitting selected users' changelists into their own commits:
321
+
322
+ ```sh
323
+ git p4son sync-split [changelist] [--user NAME ...] [--dry-run]
324
+ ```
325
+
326
+ Syncing straight to the latest changelist lumps your own submitted changelists together with everyone
327
+ else's in a single commit. `sync-split` resolves the sequence for you: it looks up the last synced
328
+ changelist in the git log, asks Perforce which changelists the selected users submitted since then, and
329
+ syncs the changelist submitted immediately before each of those first. The result is a git history where
330
+ every changelist those users submitted is a commit containing nothing but their own change.
331
+
332
+ By default the selected user is you (the current p4 user), which covers the common case of reviewing your
333
+ own submits one at a time. Repeat `--user` to split out several people's changelists in the same run.
334
+
335
+ It is equivalent to working out the numbers by hand and running
336
+ `git p4son sync <before-theirs> <theirs> ... head`, and it delegates to `sync` once the sequence is
337
+ resolved, so [pre-sync and post-sync hooks](#hooks), writable-file merging, and the clean-workspace checks
338
+ all behave exactly as they do for `sync`. The clean-workspace checks and the pre-sync hooks are the one
339
+ thing that runs earlier: resolving the sequence costs several Perforce queries, so they go first, and a
340
+ dirty workspace or a hook that aborts the sync says so before that work. They run once for the whole sync,
341
+ and not at all for a `--dry-run` or when there is nothing to sync.
342
+
343
+ **Arguments:**
344
+ - `changelist` (optional): Changelist number to sync up to, or `head` for the latest. Omit to sync to the
345
+ latest changelist affecting the workspace.
346
+
347
+ **Options:**
348
+ - `-u, --user NAME`: Perforce user whose changelists to split into their own commits. Repeat the flag to
349
+ select several users. Defaults to the current p4 user (from `p4 info`).
350
+ - `-n, --dry-run`: Print the resolved sync sequence without syncing.
351
+
352
+ **Examples:**
353
+ ```sh
354
+ git p4son sync-split # sync to latest, your own changelists split out
355
+ git p4son sync-split head # same, explicit
356
+ git p4son sync-split 12345 # stop at changelist 12345
357
+ git p4son sync-split --dry-run # show the sequence that would be synced
358
+ git p4son sync-split -u alice # split out alice's changelists instead of yours
359
+ git p4son sync-split -u alice -u bob # split out both alice's and bob's changelists
360
+ ```
361
+
362
+ For example, with the last sync at CL 100 and your own changelists at 102 and 105, `sync-split` resolves
363
+ to `git p4son sync 101 102 104 105 106`, producing five commits where CL 102 and CL 105 each hold only
364
+ your change.
365
+
366
+ `sync-split` only moves forward: it requires a previous sync to start from, and refuses a target older
367
+ than the last synced changelist (use `git p4son sync --force` for that).
368
+
318
369
  ### New Command
319
370
 
320
371
  Create a new Perforce changelist and add changed files to it. Description will contain an enumerated list of git commits since the base branch.
@@ -581,6 +632,49 @@ before the first sync.
581
632
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
582
633
  `.git-p4son/hooks/post-sync/`.
583
634
 
635
+ ### Example hooks
636
+
637
+ The [`examples/hooks/`](examples/hooks/) directory holds ready-made hooks. Nothing installs them for you:
638
+ symlink or copy the one you want into the matching hook directory in your workspace. Symlinking keeps the hook
639
+ up to date with the clone it points at, and on macOS and Linux the executable bit comes from the file in the
640
+ clone, so no `chmod` is needed.
641
+
642
+ #### block-while-running (pre-sync)
643
+
644
+ Aborts the sync while a named process is running, so a sync cannot swap out assets that a running editor still
645
+ has loaded. Link it into your workspace's `pre-sync` hook directory - on macOS and Linux:
646
+
647
+ ```sh
648
+ ln -s /path/to/git-p4son/examples/hooks/pre-sync/block-while-running.py .git-p4son/hooks/pre-sync/
649
+ ```
650
+
651
+ On Windows, from PowerShell. Note that `-Path` is the link to create and `-Target` the existing file, the
652
+ reverse of the `ln -s` argument order, and that `-Target` should be absolute: a relative path is resolved
653
+ against the current directory and quietly produces a broken link.
654
+
655
+ ```powershell
656
+ New-Item -ItemType SymbolicLink `
657
+ -Path "C:\p4\my-workspace\.git-p4son\hooks\pre-sync\block-while-running.py" `
658
+ -Target "C:\src\git-p4son\examples\hooks\pre-sync\block-while-running.py"
659
+ ```
660
+
661
+ Creating a symlink on Windows needs either an elevated shell or Developer Mode enabled (Settings > System >
662
+ For developers). Without one of those, copy the file instead and re-copy it whenever the clone updates.
663
+
664
+ Which processes block a sync is read from your workspace's own `.git-p4son/config.toml`, so a symlinked hook
665
+ stays per-project:
666
+
667
+ ```toml
668
+ [hooks.block-while-running]
669
+ processes = ["UnrealEditor", "UnrealLightmass"]
670
+ ```
671
+
672
+ Names are matched with any `.exe` suffix stripped and case ignored, so a single entry covers
673
+ `UnrealEditor.exe` on Windows and `UnrealEditor` on macOS. Without that table the hook checks nothing and the
674
+ sync proceeds; a table that is present but malformed aborts the sync rather than silently skipping the check.
675
+
676
+ Set `GIT_P4SON_SKIP_PROCESS_CHECK=1` to run one sync without the check.
677
+
584
678
  ## Shell Completions
585
679
 
586
680
  Tab completion is available for bash, zsh, and PowerShell, including commands, flags, and dynamic alias names.
@@ -210,8 +210,8 @@ commit any files that need attention.
210
210
 
211
211
  ## Usage
212
212
 
213
- git-p4son provides eight commands: `init`, `sync`, `new`, `update`, `review`, `list-changes`, `alias`,
214
- and `completion`.
213
+ git-p4son provides nine commands: `init`, `sync`, `sync-split`, `new`, `update`, `review`, `list-changes`,
214
+ `alias`, and `completion`.
215
215
 
216
216
  To see help for any command, use `-h`:
217
217
 
@@ -292,6 +292,57 @@ changelists is given, the pre-sync hooks run once, before the first sync.
292
292
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs [hooks](#hooks)
293
293
  from `.git-p4son/hooks/post-sync/`.
294
294
 
295
+ ### Sync-Split Command
296
+
297
+ Sync forward from the last synced changelist, splitting selected users' changelists into their own commits:
298
+
299
+ ```sh
300
+ git p4son sync-split [changelist] [--user NAME ...] [--dry-run]
301
+ ```
302
+
303
+ Syncing straight to the latest changelist lumps your own submitted changelists together with everyone
304
+ else's in a single commit. `sync-split` resolves the sequence for you: it looks up the last synced
305
+ changelist in the git log, asks Perforce which changelists the selected users submitted since then, and
306
+ syncs the changelist submitted immediately before each of those first. The result is a git history where
307
+ every changelist those users submitted is a commit containing nothing but their own change.
308
+
309
+ By default the selected user is you (the current p4 user), which covers the common case of reviewing your
310
+ own submits one at a time. Repeat `--user` to split out several people's changelists in the same run.
311
+
312
+ It is equivalent to working out the numbers by hand and running
313
+ `git p4son sync <before-theirs> <theirs> ... head`, and it delegates to `sync` once the sequence is
314
+ resolved, so [pre-sync and post-sync hooks](#hooks), writable-file merging, and the clean-workspace checks
315
+ all behave exactly as they do for `sync`. The clean-workspace checks and the pre-sync hooks are the one
316
+ thing that runs earlier: resolving the sequence costs several Perforce queries, so they go first, and a
317
+ dirty workspace or a hook that aborts the sync says so before that work. They run once for the whole sync,
318
+ and not at all for a `--dry-run` or when there is nothing to sync.
319
+
320
+ **Arguments:**
321
+ - `changelist` (optional): Changelist number to sync up to, or `head` for the latest. Omit to sync to the
322
+ latest changelist affecting the workspace.
323
+
324
+ **Options:**
325
+ - `-u, --user NAME`: Perforce user whose changelists to split into their own commits. Repeat the flag to
326
+ select several users. Defaults to the current p4 user (from `p4 info`).
327
+ - `-n, --dry-run`: Print the resolved sync sequence without syncing.
328
+
329
+ **Examples:**
330
+ ```sh
331
+ git p4son sync-split # sync to latest, your own changelists split out
332
+ git p4son sync-split head # same, explicit
333
+ git p4son sync-split 12345 # stop at changelist 12345
334
+ git p4son sync-split --dry-run # show the sequence that would be synced
335
+ git p4son sync-split -u alice # split out alice's changelists instead of yours
336
+ git p4son sync-split -u alice -u bob # split out both alice's and bob's changelists
337
+ ```
338
+
339
+ For example, with the last sync at CL 100 and your own changelists at 102 and 105, `sync-split` resolves
340
+ to `git p4son sync 101 102 104 105 106`, producing five commits where CL 102 and CL 105 each hold only
341
+ your change.
342
+
343
+ `sync-split` only moves forward: it requires a previous sync to start from, and refuses a target older
344
+ than the last synced changelist (use `git p4son sync --force` for that).
345
+
295
346
  ### New Command
296
347
 
297
348
  Create a new Perforce changelist and add changed files to it. Description will contain an enumerated list of git commits since the base branch.
@@ -558,6 +609,49 @@ before the first sync.
558
609
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
559
610
  `.git-p4son/hooks/post-sync/`.
560
611
 
612
+ ### Example hooks
613
+
614
+ The [`examples/hooks/`](examples/hooks/) directory holds ready-made hooks. Nothing installs them for you:
615
+ symlink or copy the one you want into the matching hook directory in your workspace. Symlinking keeps the hook
616
+ up to date with the clone it points at, and on macOS and Linux the executable bit comes from the file in the
617
+ clone, so no `chmod` is needed.
618
+
619
+ #### block-while-running (pre-sync)
620
+
621
+ Aborts the sync while a named process is running, so a sync cannot swap out assets that a running editor still
622
+ has loaded. Link it into your workspace's `pre-sync` hook directory - on macOS and Linux:
623
+
624
+ ```sh
625
+ ln -s /path/to/git-p4son/examples/hooks/pre-sync/block-while-running.py .git-p4son/hooks/pre-sync/
626
+ ```
627
+
628
+ On Windows, from PowerShell. Note that `-Path` is the link to create and `-Target` the existing file, the
629
+ reverse of the `ln -s` argument order, and that `-Target` should be absolute: a relative path is resolved
630
+ against the current directory and quietly produces a broken link.
631
+
632
+ ```powershell
633
+ New-Item -ItemType SymbolicLink `
634
+ -Path "C:\p4\my-workspace\.git-p4son\hooks\pre-sync\block-while-running.py" `
635
+ -Target "C:\src\git-p4son\examples\hooks\pre-sync\block-while-running.py"
636
+ ```
637
+
638
+ Creating a symlink on Windows needs either an elevated shell or Developer Mode enabled (Settings > System >
639
+ For developers). Without one of those, copy the file instead and re-copy it whenever the clone updates.
640
+
641
+ Which processes block a sync is read from your workspace's own `.git-p4son/config.toml`, so a symlinked hook
642
+ stays per-project:
643
+
644
+ ```toml
645
+ [hooks.block-while-running]
646
+ processes = ["UnrealEditor", "UnrealLightmass"]
647
+ ```
648
+
649
+ Names are matched with any `.exe` suffix stripped and case ignored, so a single entry covers
650
+ `UnrealEditor.exe` on Windows and `UnrealEditor` on macOS. Without that table the hook checks nothing and the
651
+ sync proceeds; a table that is present but malformed aborts the sync rather than silently skipping the check.
652
+
653
+ Set `GIT_P4SON_SKIP_PROCESS_CHECK=1` to run one sync without the check.
654
+
561
655
  ## Shell Completions
562
656
 
563
657
  Tab completion is available for bash, zsh, and PowerShell, including commands, flags, and dynamic alias names.
@@ -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.3.1"
10
+ __version__ = "0.3.3"
11
11
  __author__ = "Andreas Andersson"
12
12
  __email__ = "andreas@neoboid.com"
@@ -9,6 +9,7 @@ import time
9
9
  from importlib.resources import files
10
10
  from . import __version__
11
11
  from .sync import sync_command
12
+ from .sync_split import sync_split_command
12
13
  from .new import new_command
13
14
  from .update import update_command
14
15
  from .list_changes import list_changes_command
@@ -36,6 +37,9 @@ Examples:
36
37
  git-p4son sync 123 156 178 # Sync each changelist in sequence, one commit each
37
38
  git-p4son sync 123 156 head # Sync 123, 156, then the latest changelist
38
39
  git-p4son sync last-synced # Re-sync the last synced changelist
40
+ git-p4son sync-split # Sync to latest, your own changelists split out
41
+ git-p4son sync-split 12345 # Same, but stop at changelist 12345
42
+ git-p4son sync-split -u alice -u bob # Split out alice's and bob's changelists
39
43
  git-p4son new -m "Fix bug" # Create changelist, alias defaults to branch name
40
44
  git-p4son new -m "Fix bug" --review # Create changelist, create Swarm review
41
45
  git-p4son new -m "Fix bug" --no-alias # Create changelist without saving an alias
@@ -96,6 +100,38 @@ Examples:
96
100
  help='Allow syncing to changelists older than the current one.'
97
101
  )
98
102
 
103
+ # Sync-split subcommand
104
+ sync_split_parser = subparsers.add_parser(
105
+ 'sync-split',
106
+ help='Sync forward, splitting a user\'s changelists into own commits',
107
+ description='Sync from the last synced changelist up to a target '
108
+ 'changelist (the latest by default), syncing the changelist submitted '
109
+ 'just before each of the selected users\' submits first so that every '
110
+ 'changelist they submitted lands in a git commit of its own.'
111
+ )
112
+ sync_split_parser.add_argument(
113
+ 'changelist',
114
+ nargs='?',
115
+ default=None,
116
+ metavar='CHANGELIST',
117
+ help='Changelist number to sync up to, or "head" for the latest. '
118
+ 'Omit to sync to the latest changelist affecting the workspace'
119
+ )
120
+ sync_split_parser.add_argument(
121
+ '-u', '--user',
122
+ action='append',
123
+ default=None,
124
+ metavar='NAME',
125
+ help='Perforce user whose changelists to split into their own '
126
+ 'commits. Repeat to select several users. '
127
+ 'Defaults to the current p4 user'
128
+ )
129
+ sync_split_parser.add_argument(
130
+ '-n', '--dry-run',
131
+ action='store_true',
132
+ help='Print the resolved sync sequence without syncing'
133
+ )
134
+
99
135
  # New subcommand
100
136
  new_parser = subparsers.add_parser(
101
137
  'new',
@@ -429,6 +465,8 @@ def run_command(args: argparse.Namespace) -> int:
429
465
 
430
466
  if args.command == 'sync':
431
467
  return sync_command(args)
468
+ elif args.command == 'sync-split':
469
+ return sync_split_command(args)
432
470
  elif args.command == 'new':
433
471
  return new_command(args)
434
472
  elif args.command == 'update':
@@ -117,6 +117,10 @@ def _complete_positional(command, subcommand, positional_count,
117
117
  return _filter(
118
118
  [('head', 'Sync to the latest changelist')], prefix)
119
119
 
120
+ if command == 'sync-split' and positional_count == 0:
121
+ return _filter(
122
+ [('head', 'Sync to the latest changelist')], prefix)
123
+
120
124
  if command == 'update' and positional_count == 0:
121
125
  branch_candidates = _get_branch_candidates(prefix, workspace_dir)
122
126
  return branch_candidates + _filter(aliases, prefix)
@@ -204,6 +204,33 @@ def get_latest_changelist(depot_root: str, workspace_dir: str) -> int:
204
204
  return int(fields['change'])
205
205
 
206
206
 
207
+ @dataclass
208
+ class P4Change:
209
+ """A submitted changelist affecting the depot root."""
210
+
211
+ change: int
212
+ user: str
213
+
214
+
215
+ def get_submitted_changes(depot_root: str, start: int, end: int,
216
+ workspace_dir: str) -> list[P4Change]:
217
+ """List changelists submitted to the depot root in [start, end], oldest first."""
218
+ res = run(['p4', '-ztag', 'changes', '-s', 'submitted',
219
+ f'{depot_root}/...@{start},@{end}'], cwd=workspace_dir)
220
+ records = parse_ztag_multi_output(res.stdout)
221
+ changes = [P4Change(change=int(r['change']), user=r.get('user', ''))
222
+ for r in records if 'change' in r]
223
+ return sorted(changes, key=lambda c: c.change)
224
+
225
+
226
+ def get_p4_user(workspace_dir: str) -> str | None:
227
+ """Get the Perforce user name of the current connection."""
228
+ res = run(['p4', '-ztag', 'info'], cwd=workspace_dir)
229
+ fields = parse_ztag_output(res.stdout)
230
+ user = fields.get('userName')
231
+ return user or None
232
+
233
+
207
234
  # --- file operations ---
208
235
 
209
236
  def get_changelist_for_file(filename: str, workspace_dir: str) -> tuple[str, str] | None: