git-p4son 0.3.0__tar.gz → 0.3.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.
Files changed (54) hide show
  1. {git_p4son-0.3.0 → git_p4son-0.3.2}/PKG-INFO +107 -6
  2. {git_p4son-0.3.0 → git_p4son-0.3.2}/README.md +106 -5
  3. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/__init__.py +1 -1
  4. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/changelist_store.py +1 -1
  5. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/cli.py +49 -4
  6. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/complete.py +14 -5
  7. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/config.py +11 -0
  8. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/init.py +31 -17
  9. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/perforce.py +27 -0
  10. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/sync.py +170 -70
  11. git_p4son-0.3.2/git_p4son/sync_split.py +148 -0
  12. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son.egg-info/PKG-INFO +107 -6
  13. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son.egg-info/SOURCES.txt +3 -1
  14. {git_p4son-0.3.0 → git_p4son-0.3.2}/pyproject.toml +1 -1
  15. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_changelist_store.py +5 -0
  16. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_cli.py +7 -3
  17. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_complete.py +18 -2
  18. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_config.py +22 -1
  19. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_init.py +37 -5
  20. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_perforce.py +48 -2
  21. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_sync.py +382 -14
  22. git_p4son-0.3.2/tests/test_sync_split.py +260 -0
  23. {git_p4son-0.3.0 → git_p4son-0.3.2}/LICENSE +0 -0
  24. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/__main__.py +0 -0
  25. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/alias.py +0 -0
  26. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/common.py +0 -0
  27. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/completions/_git-p4son +0 -0
  28. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/completions/git-p4son.bash +0 -0
  29. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/completions/git-p4son.ps1 +0 -0
  30. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/git.py +0 -0
  31. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/hooks.py +0 -0
  32. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/lib.py +0 -0
  33. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/list_changes.py +0 -0
  34. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/log.py +0 -0
  35. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/new.py +0 -0
  36. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/review.py +0 -0
  37. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/state.py +0 -0
  38. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son/update.py +0 -0
  39. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son.egg-info/dependency_links.txt +0 -0
  40. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son.egg-info/entry_points.txt +0 -0
  41. {git_p4son-0.3.0 → git_p4son-0.3.2}/git_p4son.egg-info/top_level.txt +0 -0
  42. {git_p4son-0.3.0 → git_p4son-0.3.2}/setup.cfg +0 -0
  43. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_alias.py +0 -0
  44. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_common.py +0 -0
  45. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_git.py +0 -0
  46. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_hooks.py +0 -0
  47. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_lib_changelist.py +0 -0
  48. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_lib_edit.py +0 -0
  49. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_lib_review.py +0 -0
  50. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_list_changes.py +0 -0
  51. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_log.py +0 -0
  52. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_new.py +0 -0
  53. {git_p4son-0.3.0 → git_p4son-0.3.2}/tests/test_review.py +0 -0
  54. {git_p4son-0.3.0 → git_p4son-0.3.2}/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.0
3
+ Version: 0.3.2
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
 
@@ -266,6 +266,11 @@ The depot root determines which part of the Perforce workspace git-p4son syncs.
266
266
  workspace or just the directory's subtree where the git root is placed. The selection is saved in
267
267
  `.git-p4son/config.toml` and used by all subsequent commands.
268
268
 
269
+ The saved root uses a `$(workspace)` placeholder for the workspace (client) name, e.g.
270
+ `root = "//$(workspace)/Engine"`. git-p4son substitutes the live workspace name each time it runs, so renaming
271
+ the Perforce workspace does not break the config. You can still store a concrete name (e.g. `//my-workspace/Engine`)
272
+ by hand-editing `config.toml`; both forms work.
273
+
269
274
  The `.gitignore` is set up using this priority:
270
275
  - If `.gitignore` already exists, it is left as is
271
276
  - If `.p4ignore` exists, it is copied to `.gitignore` as a starting point
@@ -276,12 +281,14 @@ The `.gitignore` is set up using this priority:
276
281
  Sync local git repository with a Perforce workspace:
277
282
 
278
283
  ```sh
279
- git p4son sync [changelist] [--force]
284
+ git p4son sync [changelist ...] [--force]
280
285
  ```
281
286
 
282
287
  **Arguments:**
283
- - `changelist` (optional): Changelist number, or `last-synced` to re-sync the last synced changelist. Omit to
284
- sync to the latest changelist affecting the workspace.
288
+ - `changelist` (optional): Changelist number, `last-synced` to re-sync the last synced changelist, or `head`
289
+ to sync to the latest changelist. Omit to sync to the latest changelist affecting the workspace. Several
290
+ strictly increasing changelist numbers may be given to sync each in sequence, producing one commit per
291
+ changelist. A trailing `head` may close out such a sequence to finish at the latest changelist.
285
292
 
286
293
  **Options:**
287
294
  - `-f, --force`: Allow syncing to changelists older than the current one.
@@ -289,16 +296,73 @@ git p4son sync [changelist] [--force]
289
296
  **Examples:**
290
297
  ```sh
291
298
  git p4son sync # sync to latest
299
+ git p4son sync head # sync to latest (explicit)
292
300
  git p4son sync 12345
301
+ git p4son sync 123 156 178 # sync each changelist in sequence, one commit each
302
+ git p4son sync 123 156 head # sync 123, 156, then the latest changelist
293
303
  git p4son sync last-synced
294
304
  git p4son sync 12345 --force
295
305
  ```
296
306
 
307
+ #### pre-sync hook
308
+
309
+ Before `git-p4son sync` syncs any changelist, git-p4son runs [hooks](#hooks) from
310
+ `.git-p4son/hooks/pre-sync/`. If any hook exits non-zero the sync is aborted. When a sequence of
311
+ changelists is given, the pre-sync hooks run once, before the first sync.
312
+
297
313
  #### post-sync hook
298
314
 
299
315
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs [hooks](#hooks)
300
316
  from `.git-p4son/hooks/post-sync/`.
301
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`.
339
+
340
+ **Arguments:**
341
+ - `changelist` (optional): Changelist number to sync up to, or `head` for the latest. Omit to sync to the
342
+ latest changelist affecting the workspace.
343
+
344
+ **Options:**
345
+ - `-u, --user NAME`: Perforce user whose changelists to split into their own commits. Repeat the flag to
346
+ select several users. Defaults to the current p4 user (from `p4 info`).
347
+ - `-n, --dry-run`: Print the resolved sync sequence without syncing.
348
+
349
+ **Examples:**
350
+ ```sh
351
+ git p4son sync-split # sync to latest, your own changelists split out
352
+ git p4son sync-split head # same, explicit
353
+ git p4son sync-split 12345 # stop at changelist 12345
354
+ git p4son sync-split --dry-run # show the sequence that would be synced
355
+ git p4son sync-split -u alice # split out alice's changelists instead of yours
356
+ git p4son sync-split -u alice -u bob # split out both alice's and bob's changelists
357
+ ```
358
+
359
+ For example, with the last sync at CL 100 and your own changelists at 102 and 105, `sync-split` resolves
360
+ to `git p4son sync 101 102 104 105 106`, producing five commits where CL 102 and CL 105 each hold only
361
+ your change.
362
+
363
+ `sync-split` only moves forward: it requires a previous sync to start from, and refuses a target older
364
+ than the last synced changelist (use `git p4son sync --force` for that).
365
+
302
366
  ### New Command
303
367
 
304
368
  Create a new Perforce changelist and add changed files to it. Description will contain an enumerated list of git commits since the base branch.
@@ -553,11 +617,48 @@ On Windows, `.ps1`, `.nu`, `.sh`, and `.py` files are run with `powershell.exe`,
553
617
 
554
618
  Non-executable or unrecognized hook files are skipped with a warning.
555
619
 
620
+ ### pre-sync hook
621
+
622
+ Before `git-p4son sync` syncs any changelist, git-p4son runs executable hooks from
623
+ `.git-p4son/hooks/pre-sync/`. If any hook exits with a non-zero code the sync is aborted before any
624
+ changelist is synced. When a sequence of changelists is synced, the pre-sync hooks run only once,
625
+ before the first sync.
626
+
556
627
  ### post-sync hook
557
628
 
558
629
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
559
630
  `.git-p4son/hooks/post-sync/`.
560
631
 
632
+ ### Example hooks
633
+
634
+ The [`examples/hooks/`](examples/hooks/) directory holds ready-made hooks. Nothing installs them for you:
635
+ symlink or copy the one you want into the matching hook directory in your workspace. Symlinking keeps the hook
636
+ up to date with the clone it points at, and on macOS and Linux the executable bit comes from the file in the
637
+ clone, so no `chmod` is needed.
638
+
639
+ #### block-while-running (pre-sync)
640
+
641
+ Aborts the sync while a named process is running, so a sync cannot swap out assets that a running editor still
642
+ has loaded:
643
+
644
+ ```sh
645
+ ln -s /path/to/git-p4son/examples/hooks/pre-sync/block-while-running.py .git-p4son/hooks/pre-sync/
646
+ ```
647
+
648
+ Which processes block a sync is read from your workspace's own `.git-p4son/config.toml`, so a symlinked hook
649
+ stays per-project:
650
+
651
+ ```toml
652
+ [hooks.block-while-running]
653
+ processes = ["UnrealEditor", "UnrealLightmass"]
654
+ ```
655
+
656
+ Names are matched with any `.exe` suffix stripped and case ignored, so a single entry covers
657
+ `UnrealEditor.exe` on Windows and `UnrealEditor` on macOS. Without that table the hook checks nothing and the
658
+ sync proceeds; a table that is present but malformed aborts the sync rather than silently skipping the check.
659
+
660
+ Set `GIT_P4SON_SKIP_PROCESS_CHECK=1` to run one sync without the check.
661
+
561
662
  ## Shell Completions
562
663
 
563
664
  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
 
@@ -243,6 +243,11 @@ The depot root determines which part of the Perforce workspace git-p4son syncs.
243
243
  workspace or just the directory's subtree where the git root is placed. The selection is saved in
244
244
  `.git-p4son/config.toml` and used by all subsequent commands.
245
245
 
246
+ The saved root uses a `$(workspace)` placeholder for the workspace (client) name, e.g.
247
+ `root = "//$(workspace)/Engine"`. git-p4son substitutes the live workspace name each time it runs, so renaming
248
+ the Perforce workspace does not break the config. You can still store a concrete name (e.g. `//my-workspace/Engine`)
249
+ by hand-editing `config.toml`; both forms work.
250
+
246
251
  The `.gitignore` is set up using this priority:
247
252
  - If `.gitignore` already exists, it is left as is
248
253
  - If `.p4ignore` exists, it is copied to `.gitignore` as a starting point
@@ -253,12 +258,14 @@ The `.gitignore` is set up using this priority:
253
258
  Sync local git repository with a Perforce workspace:
254
259
 
255
260
  ```sh
256
- git p4son sync [changelist] [--force]
261
+ git p4son sync [changelist ...] [--force]
257
262
  ```
258
263
 
259
264
  **Arguments:**
260
- - `changelist` (optional): Changelist number, or `last-synced` to re-sync the last synced changelist. Omit to
261
- sync to the latest changelist affecting the workspace.
265
+ - `changelist` (optional): Changelist number, `last-synced` to re-sync the last synced changelist, or `head`
266
+ to sync to the latest changelist. Omit to sync to the latest changelist affecting the workspace. Several
267
+ strictly increasing changelist numbers may be given to sync each in sequence, producing one commit per
268
+ changelist. A trailing `head` may close out such a sequence to finish at the latest changelist.
262
269
 
263
270
  **Options:**
264
271
  - `-f, --force`: Allow syncing to changelists older than the current one.
@@ -266,16 +273,73 @@ git p4son sync [changelist] [--force]
266
273
  **Examples:**
267
274
  ```sh
268
275
  git p4son sync # sync to latest
276
+ git p4son sync head # sync to latest (explicit)
269
277
  git p4son sync 12345
278
+ git p4son sync 123 156 178 # sync each changelist in sequence, one commit each
279
+ git p4son sync 123 156 head # sync 123, 156, then the latest changelist
270
280
  git p4son sync last-synced
271
281
  git p4son sync 12345 --force
272
282
  ```
273
283
 
284
+ #### pre-sync hook
285
+
286
+ Before `git-p4son sync` syncs any changelist, git-p4son runs [hooks](#hooks) from
287
+ `.git-p4son/hooks/pre-sync/`. If any hook exits non-zero the sync is aborted. When a sequence of
288
+ changelists is given, the pre-sync hooks run once, before the first sync.
289
+
274
290
  #### post-sync hook
275
291
 
276
292
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs [hooks](#hooks)
277
293
  from `.git-p4son/hooks/post-sync/`.
278
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`.
316
+
317
+ **Arguments:**
318
+ - `changelist` (optional): Changelist number to sync up to, or `head` for the latest. Omit to sync to the
319
+ latest changelist affecting the workspace.
320
+
321
+ **Options:**
322
+ - `-u, --user NAME`: Perforce user whose changelists to split into their own commits. Repeat the flag to
323
+ select several users. Defaults to the current p4 user (from `p4 info`).
324
+ - `-n, --dry-run`: Print the resolved sync sequence without syncing.
325
+
326
+ **Examples:**
327
+ ```sh
328
+ git p4son sync-split # sync to latest, your own changelists split out
329
+ git p4son sync-split head # same, explicit
330
+ git p4son sync-split 12345 # stop at changelist 12345
331
+ git p4son sync-split --dry-run # show the sequence that would be synced
332
+ git p4son sync-split -u alice # split out alice's changelists instead of yours
333
+ git p4son sync-split -u alice -u bob # split out both alice's and bob's changelists
334
+ ```
335
+
336
+ For example, with the last sync at CL 100 and your own changelists at 102 and 105, `sync-split` resolves
337
+ to `git p4son sync 101 102 104 105 106`, producing five commits where CL 102 and CL 105 each hold only
338
+ your change.
339
+
340
+ `sync-split` only moves forward: it requires a previous sync to start from, and refuses a target older
341
+ than the last synced changelist (use `git p4son sync --force` for that).
342
+
279
343
  ### New Command
280
344
 
281
345
  Create a new Perforce changelist and add changed files to it. Description will contain an enumerated list of git commits since the base branch.
@@ -530,11 +594,48 @@ On Windows, `.ps1`, `.nu`, `.sh`, and `.py` files are run with `powershell.exe`,
530
594
 
531
595
  Non-executable or unrecognized hook files are skipped with a warning.
532
596
 
597
+ ### pre-sync hook
598
+
599
+ Before `git-p4son sync` syncs any changelist, git-p4son runs executable hooks from
600
+ `.git-p4son/hooks/pre-sync/`. If any hook exits with a non-zero code the sync is aborted before any
601
+ changelist is synced. When a sequence of changelists is synced, the pre-sync hooks run only once,
602
+ before the first sync.
603
+
533
604
  ### post-sync hook
534
605
 
535
606
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
536
607
  `.git-p4son/hooks/post-sync/`.
537
608
 
609
+ ### Example hooks
610
+
611
+ The [`examples/hooks/`](examples/hooks/) directory holds ready-made hooks. Nothing installs them for you:
612
+ symlink or copy the one you want into the matching hook directory in your workspace. Symlinking keeps the hook
613
+ up to date with the clone it points at, and on macOS and Linux the executable bit comes from the file in the
614
+ clone, so no `chmod` is needed.
615
+
616
+ #### block-while-running (pre-sync)
617
+
618
+ Aborts the sync while a named process is running, so a sync cannot swap out assets that a running editor still
619
+ has loaded:
620
+
621
+ ```sh
622
+ ln -s /path/to/git-p4son/examples/hooks/pre-sync/block-while-running.py .git-p4son/hooks/pre-sync/
623
+ ```
624
+
625
+ Which processes block a sync is read from your workspace's own `.git-p4son/config.toml`, so a symlinked hook
626
+ stays per-project:
627
+
628
+ ```toml
629
+ [hooks.block-while-running]
630
+ processes = ["UnrealEditor", "UnrealLightmass"]
631
+ ```
632
+
633
+ Names are matched with any `.exe` suffix stripped and case ignored, so a single entry covers
634
+ `UnrealEditor.exe` on Windows and `UnrealEditor` on macOS. Without that table the hook checks nothing and the
635
+ sync proceeds; a table that is present but malformed aborts the sync rather than silently skipping the check.
636
+
637
+ Set `GIT_P4SON_SKIP_PROCESS_CHECK=1` to run one sync without the check.
638
+
538
639
  ## Shell Completions
539
640
 
540
641
  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.0"
10
+ __version__ = "0.3.2"
11
11
  __author__ = "Andreas Andersson"
12
12
  __email__ = "andreas@neoboid.com"
@@ -11,7 +11,7 @@ from . import CONFIG_DIR
11
11
  from .log import log
12
12
 
13
13
 
14
- RESERVED_KEYWORDS = frozenset({'last-synced', 'branch'})
14
+ RESERVED_KEYWORDS = frozenset({'last-synced', 'head', 'branch'})
15
15
 
16
16
  # Allowed characters: ASCII letters, digits, hyphen, underscore, dot.
17
17
  # Must not start or end with a dot, so "." and ".." are rejected and the
@@ -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
@@ -31,8 +32,14 @@ def create_parser() -> argparse.ArgumentParser:
31
32
  epilog="""
32
33
  Examples:
33
34
  git-p4son sync # Sync to the latest changelist
35
+ git-p4son sync head # Sync to the latest changelist (explicit)
34
36
  git-p4son sync 12345 # Sync to changelist 12345
37
+ git-p4son sync 123 156 178 # Sync each changelist in sequence, one commit each
38
+ git-p4son sync 123 156 head # Sync 123, 156, then the latest changelist
35
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
36
43
  git-p4son new -m "Fix bug" # Create changelist, alias defaults to branch name
37
44
  git-p4son new -m "Fix bug" --review # Create changelist, create Swarm review
38
45
  git-p4son new -m "Fix bug" --no-alias # Create changelist without saving an alias
@@ -78,10 +85,14 @@ Examples:
78
85
  )
79
86
  sync_parser.add_argument(
80
87
  'changelist',
81
- nargs='?',
82
- default=None,
83
- help='Changelist number to sync, or "last-synced" to re-sync the last synced changelist. '
84
- 'Omit to sync to the latest changelist affecting the workspace'
88
+ nargs='*',
89
+ default=[],
90
+ metavar='CHANGELIST',
91
+ help='Changelist number(s) to sync, "last-synced" to re-sync the last synced changelist, '
92
+ 'or "head" to sync to the latest changelist. Give several strictly increasing '
93
+ 'changelist numbers to sync each in sequence, one commit per changelist; "head" may '
94
+ 'close out such a sequence as a trailing target. Omit to sync to the latest '
95
+ 'changelist affecting the workspace'
85
96
  )
86
97
  sync_parser.add_argument(
87
98
  '-f', '--force',
@@ -89,6 +100,38 @@ Examples:
89
100
  help='Allow syncing to changelists older than the current one.'
90
101
  )
91
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
+
92
135
  # New subcommand
93
136
  new_parser = subparsers.add_parser(
94
137
  'new',
@@ -422,6 +465,8 @@ def run_command(args: argparse.Namespace) -> int:
422
465
 
423
466
  if args.command == 'sync':
424
467
  return sync_command(args)
468
+ elif args.command == 'sync-split':
469
+ return sync_split_command(args)
425
470
  elif args.command == 'new':
426
471
  return new_command(args)
427
472
  elif args.command == 'update':
@@ -106,11 +106,20 @@ def _complete_positional(command, subcommand, positional_count,
106
106
  """Complete a positional argument."""
107
107
  aliases = _get_alias_names(workspace_dir)
108
108
 
109
- if command == 'sync' and positional_count == 0:
110
- candidates = [
111
- ('last-synced', 'Re-sync last synced changelist'),
112
- ]
113
- return _filter(candidates, prefix)
109
+ if command == 'sync':
110
+ if positional_count == 0:
111
+ candidates = [
112
+ ('head', 'Sync to the latest changelist'),
113
+ ('last-synced', 'Re-sync last synced changelist'),
114
+ ]
115
+ return _filter(candidates, prefix)
116
+ # "head" may also close out a sequence of changelist numbers.
117
+ return _filter(
118
+ [('head', 'Sync to the latest changelist')], prefix)
119
+
120
+ if command == 'sync-split' and positional_count == 0:
121
+ return _filter(
122
+ [('head', 'Sync to the latest changelist')], prefix)
114
123
 
115
124
  if command == 'update' and positional_count == 0:
116
125
  branch_candidates = _get_branch_candidates(prefix, workspace_dir)
@@ -12,6 +12,12 @@ from . import CONFIG_DIR
12
12
 
13
13
  _BARE_KEY_RE = re.compile(r'^[A-Za-z0-9_-]+$')
14
14
 
15
+ # Placeholder allowed in a stored depot root, substituted with the live
16
+ # Perforce client (workspace) name each time the root is used. Storing e.g.
17
+ # root = "//$(workspace)/Engine" keeps the config working after the workspace
18
+ # is renamed, at the cost of one client-name lookup per command.
19
+ WORKSPACE_PLACEHOLDER = '$(workspace)'
20
+
15
21
 
16
22
  def config_path(workspace_dir: str) -> str:
17
23
  """Return the path to the config file."""
@@ -94,3 +100,8 @@ def get_depot_root(workspace_dir: str) -> str | None:
94
100
  """Get the depot root from config, or None if not configured."""
95
101
  config = load_config(workspace_dir)
96
102
  return config.get('depot', {}).get('root')
103
+
104
+
105
+ def expand_depot_root(depot_root: str, workspace_name: str) -> str:
106
+ """Substitute the live workspace name for the $(workspace) placeholder."""
107
+ return depot_root.replace(WORKSPACE_PLACEHOLDER, workspace_name)
@@ -10,7 +10,12 @@ import os
10
10
  import shutil
11
11
 
12
12
  from .common import CommandError, run, run_with_output
13
- from .config import get_depot_root, save_config
13
+ from .config import (
14
+ WORKSPACE_PLACEHOLDER,
15
+ expand_depot_root,
16
+ get_depot_root,
17
+ save_config,
18
+ )
14
19
  from .log import log
15
20
  from .perforce import get_client_spec
16
21
  from .git import resolve_editor
@@ -26,31 +31,38 @@ def _validate_depot_root(depot_root: str, cwd: str) -> bool:
26
31
  return False
27
32
 
28
33
 
29
- def _compute_cwd_depot_root(client_name: str, cwd: str,
30
- p4_workspace_root: str) -> str | None:
31
- """Compute depot root for cwd relative to workspace root.
34
+ def _compute_cwd_depot_root(cwd: str, p4_workspace_root: str) -> str | None:
35
+ """Compute the depot root template for cwd relative to workspace root.
32
36
 
33
- Returns None if cwd is the workspace root (identical to entire workspace).
37
+ Uses the $(workspace) placeholder in place of the client name so the stored
38
+ root survives a workspace rename. Returns None if cwd is the workspace root
39
+ (identical to the entire workspace).
34
40
  """
35
41
  rel = os.path.relpath(cwd, p4_workspace_root)
36
42
  if rel == '.':
37
43
  return None
38
44
  rel_posix = rel.replace(os.sep, '/')
39
- return f'//{client_name}/{rel_posix}'
45
+ return f'//{WORKSPACE_PLACEHOLDER}/{rel_posix}'
40
46
 
41
47
 
42
48
  def _select_depot_root(client_name: str, cwd: str,
43
49
  p4_workspace_root: str) -> str | None:
44
- """Interactive prompt for depot root selection. Returns root or None to abort."""
45
- entire_root = f'//{client_name}'
46
- cwd_root = _compute_cwd_depot_root(client_name, cwd, p4_workspace_root)
50
+ """Interactive prompt for depot root selection.
51
+
52
+ Returns a depot root template (with the $(workspace) placeholder) to store,
53
+ or None to abort. Menu entries and validation use the resolved path.
54
+ """
55
+ entire_root = f'//{WORKSPACE_PLACEHOLDER}'
56
+ cwd_root = _compute_cwd_depot_root(cwd, p4_workspace_root)
47
57
 
48
58
  while True:
49
59
  print()
50
60
  print('Select the depot path that git-p4son should sync:')
51
- print(f' 1. Entire workspace: {entire_root}/...')
61
+ print(f' 1. Entire workspace: '
62
+ f'{expand_depot_root(entire_root, client_name)}/...')
52
63
  if cwd_root:
53
- print(f' 2. Current directory: {cwd_root}/...')
64
+ print(f' 2. Current directory: '
65
+ f'{expand_depot_root(cwd_root, client_name)}/...')
54
66
  abort_num = '3'
55
67
  else:
56
68
  abort_num = '2'
@@ -72,10 +84,11 @@ def _select_depot_root(client_name: str, cwd: str,
72
84
  print(f'Invalid choice: {choice}')
73
85
  continue
74
86
 
75
- if _validate_depot_root(depot_root, cwd):
87
+ resolved = expand_depot_root(depot_root, client_name)
88
+ if _validate_depot_root(resolved, cwd):
76
89
  return depot_root
77
90
 
78
- log.error(f'Depot root {depot_root}/... is not valid')
91
+ log.error(f'Depot root {resolved}/... is not valid')
79
92
 
80
93
 
81
94
  def _configure_depot_root(client_name: str, cwd: str,
@@ -84,17 +97,18 @@ def _configure_depot_root(client_name: str, cwd: str,
84
97
  log.heading('Finding git-p4son depot root')
85
98
  depot_root = get_depot_root(cwd)
86
99
  if depot_root:
87
- log.success(f'{depot_root}/...')
100
+ log.success(f'{expand_depot_root(depot_root, client_name)}/...')
88
101
  else:
89
102
  log.warning('no root configured')
90
103
 
91
104
  if depot_root:
92
105
  log.heading('Validating depot root')
93
- if _validate_depot_root(depot_root, cwd):
106
+ resolved = expand_depot_root(depot_root, client_name)
107
+ if _validate_depot_root(resolved, cwd):
94
108
  log.success('all good')
95
109
  return True
96
110
  else:
97
- log.error(f'{depot_root}/... is not valid')
111
+ log.error(f'{resolved}/... is not valid')
98
112
  depot_root = None
99
113
 
100
114
  if not depot_root:
@@ -104,7 +118,7 @@ def _configure_depot_root(client_name: str, cwd: str,
104
118
  log.error('aborting')
105
119
  return False
106
120
 
107
- log.success(f'{depot_root}/...')
121
+ log.success(f'{expand_depot_root(depot_root, client_name)}/...')
108
122
  save_config(cwd, {'depot': {'root': depot_root}})
109
123
  return True
110
124
 
@@ -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: