git-p4son 0.4.0__tar.gz → 0.4.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 (64) hide show
  1. {git_p4son-0.4.0 → git_p4son-0.4.2}/PKG-INFO +35 -51
  2. {git_p4son-0.4.0 → git_p4son-0.4.2}/README.md +34 -50
  3. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/__init__.py +1 -1
  4. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/changelist_store.py +2 -0
  5. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/cli.py +19 -2
  6. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/common.py +19 -7
  7. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/config.py +22 -0
  8. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/perforce.py +18 -0
  9. git_p4son-0.4.2/git_p4son/processes.py +98 -0
  10. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/review.py +2 -0
  11. git_p4son-0.4.2/git_p4son/state.py +38 -0
  12. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/sync.py +28 -6
  13. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/sync_split.py +3 -1
  14. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son.egg-info/PKG-INFO +35 -51
  15. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son.egg-info/SOURCES.txt +2 -0
  16. {git_p4son-0.4.0 → git_p4son-0.4.2}/pyproject.toml +1 -1
  17. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_changelist_store.py +7 -0
  18. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_cli.py +19 -0
  19. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_common.py +19 -0
  20. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_complete.py +2 -0
  21. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_config.py +42 -0
  22. git_p4son-0.4.2/tests/test_processes.py +153 -0
  23. git_p4son-0.4.2/tests/test_state.py +39 -0
  24. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_sync.py +84 -0
  25. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_sync_split.py +46 -6
  26. git_p4son-0.4.0/git_p4son/state.py +0 -56
  27. git_p4son-0.4.0/tests/test_state.py +0 -64
  28. {git_p4son-0.4.0 → git_p4son-0.4.2}/LICENSE +0 -0
  29. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/__main__.py +0 -0
  30. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/alias.py +0 -0
  31. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/complete.py +0 -0
  32. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/completions/_git-p4son +0 -0
  33. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/completions/git-p4son.bash +0 -0
  34. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/completions/git-p4son.ps1 +0 -0
  35. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/depot.py +0 -0
  36. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/git.py +0 -0
  37. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/hooks.py +0 -0
  38. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/init.py +0 -0
  39. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/lib.py +0 -0
  40. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/list_changes.py +0 -0
  41. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/log.py +0 -0
  42. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/new.py +0 -0
  43. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/update.py +0 -0
  44. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/writable.py +0 -0
  45. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son.egg-info/dependency_links.txt +0 -0
  46. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son.egg-info/entry_points.txt +0 -0
  47. {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son.egg-info/top_level.txt +0 -0
  48. {git_p4son-0.4.0 → git_p4son-0.4.2}/setup.cfg +0 -0
  49. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_alias.py +0 -0
  50. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_depot.py +0 -0
  51. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_git.py +0 -0
  52. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_hooks.py +0 -0
  53. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_init.py +0 -0
  54. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_lib_changelist.py +0 -0
  55. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_lib_edit.py +0 -0
  56. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_lib_revert.py +0 -0
  57. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_lib_review.py +0 -0
  58. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_list_changes.py +0 -0
  59. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_log.py +0 -0
  60. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_new.py +0 -0
  61. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_perforce.py +0 -0
  62. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_review.py +0 -0
  63. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_update.py +0 -0
  64. {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_writable.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-p4son
3
- Version: 0.4.0
3
+ Version: 0.4.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
@@ -277,6 +277,9 @@ The depot root determines which part of the Perforce workspace git-p4son syncs.
277
277
  workspace or just the directory's subtree where the git root is placed. The selection is saved in
278
278
  `.git-p4son/config.toml` and used by all subsequent commands.
279
279
 
280
+ Nothing in `.git-p4son/` belongs in git. The folder has its own `.gitignore` that ignores everything in it, so there
281
+ is no need to list it in the workspace `.gitignore`.
282
+
280
283
  The saved root uses a `$(workspace)` placeholder for the workspace (client) name, e.g.
281
284
  `root = "//$(workspace)/Engine"`. git-p4son substitutes the live workspace name each time it runs, so renaming
282
285
  the Perforce workspace does not break the config. You can still store a concrete name (e.g. `//my-workspace/Engine`)
@@ -295,7 +298,7 @@ saves the answer; run `git p4son writable apply` once your files are committed t
295
298
  Sync local git repository with a Perforce workspace:
296
299
 
297
300
  ```sh
298
- git p4son sync [changelist ...] [--force]
301
+ git p4son sync [changelist ...] [--force] [--dry-run] [--ignore-blocking-processes]
299
302
  ```
300
303
 
301
304
  **Arguments:**
@@ -306,6 +309,9 @@ git p4son sync [changelist ...] [--force]
306
309
 
307
310
  **Options:**
308
311
  - `-f, --force`: Allow syncing to changelists older than the current one.
312
+ - `-n, --dry-run`: Print the resolved sync sequence without syncing. The arguments are validated as for a
313
+ real sync, but nothing else runs: no clean-workspace checks, no hooks, and no prompts.
314
+ - `--ignore-blocking-processes`: Sync even while a [blocking process](#blocking-processes) is running.
309
315
 
310
316
  **Examples:**
311
317
  ```sh
@@ -316,8 +322,27 @@ git p4son sync 123 156 178 # sync each changelist in sequence, one commit each
316
322
  git p4son sync 123 156 head # sync 123, 156, then the latest changelist
317
323
  git p4son sync last-synced
318
324
  git p4son sync 12345 --force
325
+ git p4son sync --dry-run # show the changelists a sync to latest would visit
319
326
  ```
320
327
 
328
+ #### Blocking processes
329
+
330
+ Syncing while e.g. the Unreal editor is open lets p4 replace assets the editor still has loaded, so the editor
331
+ keeps working against files that no longer match what is on disk. List such processes in `.git-p4son/config.toml`
332
+ and `sync` refuses to start while any of them is running:
333
+
334
+ ```toml
335
+ [sync]
336
+ blocking-processes = ["UnrealEditor", "UnrealLightmass"]
337
+ ```
338
+
339
+ Names are matched with any `.exe` suffix stripped and case ignored, so a single entry covers `UnrealEditor.exe` on
340
+ Windows and `UnrealEditor` on macOS. The check runs before the clean-workspace checks and the pre-sync hooks. A
341
+ setting that is present but malformed, or a process list that cannot be read, aborts the sync rather than silently
342
+ skipping the check.
343
+
344
+ Pass `--ignore-blocking-processes` to sync once without the check, or remove the setting to turn it off for good.
345
+
321
346
  #### pre-sync hook
322
347
 
323
348
  Before `git-p4son sync` syncs any changelist, git-p4son runs [hooks](#hooks) from
@@ -334,7 +359,7 @@ from `.git-p4son/hooks/post-sync/`.
334
359
  Sync forward from the last synced changelist, splitting selected users' changelists into their own commits:
335
360
 
336
361
  ```sh
337
- git p4son sync-split [changelist] [--user NAME ...] [--dry-run]
362
+ git p4son sync-split [changelist] [--user NAME ...] [--dry-run] [--ignore-blocking-processes]
338
363
  ```
339
364
 
340
365
  Syncing straight to the latest changelist lumps your own submitted changelists together with everyone
@@ -348,11 +373,12 @@ own submits one at a time. Repeat `--user` to split out several people's changel
348
373
 
349
374
  It is equivalent to working out the numbers by hand and running
350
375
  `git p4son sync <before-theirs> <theirs> ... head`, and it delegates to `sync` once the sequence is
351
- resolved, so [pre-sync and post-sync hooks](#hooks), writable-file merging, and the clean-workspace checks
352
- all behave exactly as they do for `sync`. The clean-workspace checks and the pre-sync hooks are the one
353
- thing that runs earlier: resolving the sequence costs several Perforce queries, so they go first, and a
354
- dirty workspace or a hook that aborts the sync says so before that work. They run once for the whole sync,
355
- and not at all for a `--dry-run` or when there is nothing to sync.
376
+ resolved, so [pre-sync and post-sync hooks](#hooks), [blocking processes](#blocking-processes), writable-file
377
+ merging, and the clean-workspace checks all behave exactly as they do for `sync`. The blocking processes check,
378
+ the clean-workspace checks and the pre-sync hooks are the one thing that runs earlier: resolving the sequence
379
+ costs several Perforce queries, so they go first, and a running blocking process, a dirty workspace or a hook
380
+ that aborts the sync says so before that work. They run once for the whole sync, and not at all for a
381
+ `--dry-run` or when there is nothing to sync.
356
382
 
357
383
  **Arguments:**
358
384
  - `changelist` (optional): Changelist number to sync up to, or `head` for the latest. Omit to sync to the
@@ -362,6 +388,7 @@ and not at all for a `--dry-run` or when there is nothing to sync.
362
388
  - `-u, --user NAME`: Perforce user whose changelists to split into their own commits. Repeat the flag to
363
389
  select several users. Defaults to the current p4 user (from `p4 info`).
364
390
  - `-n, --dry-run`: Print the resolved sync sequence without syncing.
391
+ - `--ignore-blocking-processes`: Sync even while a [blocking process](#blocking-processes) is running.
365
392
 
366
393
  **Examples:**
367
394
  ```sh
@@ -708,49 +735,6 @@ before the first sync.
708
735
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
709
736
  `.git-p4son/hooks/post-sync/`.
710
737
 
711
- ### Example hooks
712
-
713
- The [`examples/hooks/`](examples/hooks/) directory holds ready-made hooks. Nothing installs them for you:
714
- symlink or copy the one you want into the matching hook directory in your workspace. Symlinking keeps the hook
715
- up to date with the clone it points at, and on macOS and Linux the executable bit comes from the file in the
716
- clone, so no `chmod` is needed.
717
-
718
- #### block-while-running (pre-sync)
719
-
720
- Aborts the sync while a named process is running, so a sync cannot swap out assets that a running editor still
721
- has loaded. Link it into your workspace's `pre-sync` hook directory - on macOS and Linux:
722
-
723
- ```sh
724
- ln -s /path/to/git-p4son/examples/hooks/pre-sync/block-while-running.py .git-p4son/hooks/pre-sync/
725
- ```
726
-
727
- On Windows, from PowerShell. Note that `-Path` is the link to create and `-Target` the existing file, the
728
- reverse of the `ln -s` argument order, and that `-Target` should be absolute: a relative path is resolved
729
- against the current directory and quietly produces a broken link.
730
-
731
- ```powershell
732
- New-Item -ItemType SymbolicLink `
733
- -Path "C:\p4\my-workspace\.git-p4son\hooks\pre-sync\block-while-running.py" `
734
- -Target "C:\src\git-p4son\examples\hooks\pre-sync\block-while-running.py"
735
- ```
736
-
737
- Creating a symlink on Windows needs either an elevated shell or Developer Mode enabled (Settings > System >
738
- For developers). Without one of those, copy the file instead and re-copy it whenever the clone updates.
739
-
740
- Which processes block a sync is read from your workspace's own `.git-p4son/config.toml`, so a symlinked hook
741
- stays per-project:
742
-
743
- ```toml
744
- [hooks.block-while-running]
745
- processes = ["UnrealEditor", "UnrealLightmass"]
746
- ```
747
-
748
- Names are matched with any `.exe` suffix stripped and case ignored, so a single entry covers
749
- `UnrealEditor.exe` on Windows and `UnrealEditor` on macOS. Without that table the hook checks nothing and the
750
- sync proceeds; a table that is present but malformed aborts the sync rather than silently skipping the check.
751
-
752
- Set `GIT_P4SON_SKIP_PROCESS_CHECK=1` to run one sync without the check.
753
-
754
738
  ## Shell Completions
755
739
 
756
740
  Tab completion is available for bash, zsh, and PowerShell, including commands, flags, and dynamic alias names.
@@ -254,6 +254,9 @@ The depot root determines which part of the Perforce workspace git-p4son syncs.
254
254
  workspace or just the directory's subtree where the git root is placed. The selection is saved in
255
255
  `.git-p4son/config.toml` and used by all subsequent commands.
256
256
 
257
+ Nothing in `.git-p4son/` belongs in git. The folder has its own `.gitignore` that ignores everything in it, so there
258
+ is no need to list it in the workspace `.gitignore`.
259
+
257
260
  The saved root uses a `$(workspace)` placeholder for the workspace (client) name, e.g.
258
261
  `root = "//$(workspace)/Engine"`. git-p4son substitutes the live workspace name each time it runs, so renaming
259
262
  the Perforce workspace does not break the config. You can still store a concrete name (e.g. `//my-workspace/Engine`)
@@ -272,7 +275,7 @@ saves the answer; run `git p4son writable apply` once your files are committed t
272
275
  Sync local git repository with a Perforce workspace:
273
276
 
274
277
  ```sh
275
- git p4son sync [changelist ...] [--force]
278
+ git p4son sync [changelist ...] [--force] [--dry-run] [--ignore-blocking-processes]
276
279
  ```
277
280
 
278
281
  **Arguments:**
@@ -283,6 +286,9 @@ git p4son sync [changelist ...] [--force]
283
286
 
284
287
  **Options:**
285
288
  - `-f, --force`: Allow syncing to changelists older than the current one.
289
+ - `-n, --dry-run`: Print the resolved sync sequence without syncing. The arguments are validated as for a
290
+ real sync, but nothing else runs: no clean-workspace checks, no hooks, and no prompts.
291
+ - `--ignore-blocking-processes`: Sync even while a [blocking process](#blocking-processes) is running.
286
292
 
287
293
  **Examples:**
288
294
  ```sh
@@ -293,8 +299,27 @@ git p4son sync 123 156 178 # sync each changelist in sequence, one commit each
293
299
  git p4son sync 123 156 head # sync 123, 156, then the latest changelist
294
300
  git p4son sync last-synced
295
301
  git p4son sync 12345 --force
302
+ git p4son sync --dry-run # show the changelists a sync to latest would visit
296
303
  ```
297
304
 
305
+ #### Blocking processes
306
+
307
+ Syncing while e.g. the Unreal editor is open lets p4 replace assets the editor still has loaded, so the editor
308
+ keeps working against files that no longer match what is on disk. List such processes in `.git-p4son/config.toml`
309
+ and `sync` refuses to start while any of them is running:
310
+
311
+ ```toml
312
+ [sync]
313
+ blocking-processes = ["UnrealEditor", "UnrealLightmass"]
314
+ ```
315
+
316
+ Names are matched with any `.exe` suffix stripped and case ignored, so a single entry covers `UnrealEditor.exe` on
317
+ Windows and `UnrealEditor` on macOS. The check runs before the clean-workspace checks and the pre-sync hooks. A
318
+ setting that is present but malformed, or a process list that cannot be read, aborts the sync rather than silently
319
+ skipping the check.
320
+
321
+ Pass `--ignore-blocking-processes` to sync once without the check, or remove the setting to turn it off for good.
322
+
298
323
  #### pre-sync hook
299
324
 
300
325
  Before `git-p4son sync` syncs any changelist, git-p4son runs [hooks](#hooks) from
@@ -311,7 +336,7 @@ from `.git-p4son/hooks/post-sync/`.
311
336
  Sync forward from the last synced changelist, splitting selected users' changelists into their own commits:
312
337
 
313
338
  ```sh
314
- git p4son sync-split [changelist] [--user NAME ...] [--dry-run]
339
+ git p4son sync-split [changelist] [--user NAME ...] [--dry-run] [--ignore-blocking-processes]
315
340
  ```
316
341
 
317
342
  Syncing straight to the latest changelist lumps your own submitted changelists together with everyone
@@ -325,11 +350,12 @@ own submits one at a time. Repeat `--user` to split out several people's changel
325
350
 
326
351
  It is equivalent to working out the numbers by hand and running
327
352
  `git p4son sync <before-theirs> <theirs> ... head`, and it delegates to `sync` once the sequence is
328
- resolved, so [pre-sync and post-sync hooks](#hooks), writable-file merging, and the clean-workspace checks
329
- all behave exactly as they do for `sync`. The clean-workspace checks and the pre-sync hooks are the one
330
- thing that runs earlier: resolving the sequence costs several Perforce queries, so they go first, and a
331
- dirty workspace or a hook that aborts the sync says so before that work. They run once for the whole sync,
332
- and not at all for a `--dry-run` or when there is nothing to sync.
353
+ resolved, so [pre-sync and post-sync hooks](#hooks), [blocking processes](#blocking-processes), writable-file
354
+ merging, and the clean-workspace checks all behave exactly as they do for `sync`. The blocking processes check,
355
+ the clean-workspace checks and the pre-sync hooks are the one thing that runs earlier: resolving the sequence
356
+ costs several Perforce queries, so they go first, and a running blocking process, a dirty workspace or a hook
357
+ that aborts the sync says so before that work. They run once for the whole sync, and not at all for a
358
+ `--dry-run` or when there is nothing to sync.
333
359
 
334
360
  **Arguments:**
335
361
  - `changelist` (optional): Changelist number to sync up to, or `head` for the latest. Omit to sync to the
@@ -339,6 +365,7 @@ and not at all for a `--dry-run` or when there is nothing to sync.
339
365
  - `-u, --user NAME`: Perforce user whose changelists to split into their own commits. Repeat the flag to
340
366
  select several users. Defaults to the current p4 user (from `p4 info`).
341
367
  - `-n, --dry-run`: Print the resolved sync sequence without syncing.
368
+ - `--ignore-blocking-processes`: Sync even while a [blocking process](#blocking-processes) is running.
342
369
 
343
370
  **Examples:**
344
371
  ```sh
@@ -685,49 +712,6 @@ before the first sync.
685
712
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
686
713
  `.git-p4son/hooks/post-sync/`.
687
714
 
688
- ### Example hooks
689
-
690
- The [`examples/hooks/`](examples/hooks/) directory holds ready-made hooks. Nothing installs them for you:
691
- symlink or copy the one you want into the matching hook directory in your workspace. Symlinking keeps the hook
692
- up to date with the clone it points at, and on macOS and Linux the executable bit comes from the file in the
693
- clone, so no `chmod` is needed.
694
-
695
- #### block-while-running (pre-sync)
696
-
697
- Aborts the sync while a named process is running, so a sync cannot swap out assets that a running editor still
698
- has loaded. Link it into your workspace's `pre-sync` hook directory - on macOS and Linux:
699
-
700
- ```sh
701
- ln -s /path/to/git-p4son/examples/hooks/pre-sync/block-while-running.py .git-p4son/hooks/pre-sync/
702
- ```
703
-
704
- On Windows, from PowerShell. Note that `-Path` is the link to create and `-Target` the existing file, the
705
- reverse of the `ln -s` argument order, and that `-Target` should be absolute: a relative path is resolved
706
- against the current directory and quietly produces a broken link.
707
-
708
- ```powershell
709
- New-Item -ItemType SymbolicLink `
710
- -Path "C:\p4\my-workspace\.git-p4son\hooks\pre-sync\block-while-running.py" `
711
- -Target "C:\src\git-p4son\examples\hooks\pre-sync\block-while-running.py"
712
- ```
713
-
714
- Creating a symlink on Windows needs either an elevated shell or Developer Mode enabled (Settings > System >
715
- For developers). Without one of those, copy the file instead and re-copy it whenever the clone updates.
716
-
717
- Which processes block a sync is read from your workspace's own `.git-p4son/config.toml`, so a symlinked hook
718
- stays per-project:
719
-
720
- ```toml
721
- [hooks.block-while-running]
722
- processes = ["UnrealEditor", "UnrealLightmass"]
723
- ```
724
-
725
- Names are matched with any `.exe` suffix stripped and case ignored, so a single entry covers
726
- `UnrealEditor.exe` on Windows and `UnrealEditor` on macOS. Without that table the hook checks nothing and the
727
- sync proceeds; a table that is present but malformed aborts the sync rather than silently skipping the check.
728
-
729
- Set `GIT_P4SON_SKIP_PROCESS_CHECK=1` to run one sync without the check.
730
-
731
715
  ## Shell Completions
732
716
 
733
717
  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.4.0"
10
+ __version__ = "0.4.2"
11
11
  __author__ = "Andreas Andersson"
12
12
  __email__ = "andreas@neoboid.com"
@@ -8,6 +8,7 @@ import os
8
8
  import re
9
9
 
10
10
  from . import CONFIG_DIR
11
+ from .config import ensure_config_dir
11
12
  from .log import log
12
13
 
13
14
 
@@ -85,6 +86,7 @@ def save_changelist_alias(name: str, changelist: str, workspace_dir: str, force:
85
86
  f'Alias "{name}" already exists (use -f/--force to overwrite)')
86
87
  return False
87
88
 
89
+ ensure_config_dir(workspace_dir)
88
90
  if not os.path.isdir(changelists_dir):
89
91
  log.info(f'Creating {changelists_dir}')
90
92
  os.makedirs(changelists_dir, exist_ok=True)
@@ -38,6 +38,7 @@ Examples:
38
38
  git-p4son sync 123 156 178 # Sync each changelist in sequence, one commit each
39
39
  git-p4son sync 123 156 head # Sync 123, 156, then the latest changelist
40
40
  git-p4son sync last-synced # Re-sync the last synced changelist
41
+ git-p4son sync --dry-run # Show the changelists a sync would visit
41
42
  git-p4son sync-split # Sync to latest, your own changelists split out
42
43
  git-p4son sync-split 12345 # Same, but stop at changelist 12345
43
44
  git-p4son sync-split -u alice -u bob # Split out alice's and bob's changelists
@@ -103,6 +104,16 @@ Examples:
103
104
  action='store_true',
104
105
  help='Allow syncing to changelists older than the current one.'
105
106
  )
107
+ sync_parser.add_argument(
108
+ '-n', '--dry-run',
109
+ action='store_true',
110
+ help='Print the resolved sync sequence without syncing'
111
+ )
112
+ sync_parser.add_argument(
113
+ '--ignore-blocking-processes',
114
+ action='store_true',
115
+ help='Sync even while a configured blocking process is running'
116
+ )
106
117
 
107
118
  # Sync-split subcommand
108
119
  sync_split_parser = subparsers.add_parser(
@@ -135,6 +146,11 @@ Examples:
135
146
  action='store_true',
136
147
  help='Print the resolved sync sequence without syncing'
137
148
  )
149
+ sync_split_parser.add_argument(
150
+ '--ignore-blocking-processes',
151
+ action='store_true',
152
+ help='Sync even while a configured blocking process is running'
153
+ )
138
154
 
139
155
  # New subcommand
140
156
  new_parser = subparsers.add_parser(
@@ -408,10 +424,11 @@ Examples:
408
424
  help='Print the directory instead of the full file path'
409
425
  )
410
426
 
411
- # Hidden _sequence-editor subcommand (used internally by review)
427
+ # Hidden _sequence-editor subcommand (used internally by review). It is
428
+ # given no help: argparse lists a subcommand with help=SUPPRESS anyway,
429
+ # as "==SUPPRESS==", but leaves one without help out of the list.
412
430
  seq_editor_parser = subparsers.add_parser(
413
431
  '_sequence-editor',
414
- help=argparse.SUPPRESS,
415
432
  )
416
433
  seq_editor_parser.add_argument(
417
434
  'filename',
@@ -256,6 +256,16 @@ def enqueue_lines(stream: IO[str], output_queue: queue.Queue[str]) -> None:
256
256
  output_queue.put(line.rstrip())
257
257
 
258
258
 
259
+ def _terminate(process: subprocess.Popen) -> None:
260
+ """Terminate a subprocess, killing it if it does not exit in time."""
261
+ process.terminate()
262
+ try:
263
+ process.wait(timeout=5)
264
+ except subprocess.TimeoutExpired:
265
+ log.error("Subprocess did not terminate in time. Forcing kill...")
266
+ process.kill()
267
+
268
+
259
269
  def run_with_output(command: list[str], cwd: str = '.',
260
270
  on_output: Callable[..., None] | None = None,
261
271
  env: dict[str, str] | None = None) -> RunResult:
@@ -268,6 +278,8 @@ def run_with_output(command: list[str], cwd: str = '.',
268
278
  on_output: Callback function for processing output lines
269
279
  If set the funciton will be called with each
270
280
  line and stream (stdout/stderr) as they are written.
281
+ An exception raised from it terminates the command
282
+ and propagates.
271
283
  env: Optional environment variables to add or override
272
284
 
273
285
  Returns:
@@ -352,14 +364,14 @@ def run_with_output(command: list[str], cwd: str = '.',
352
364
  except KeyboardInterrupt:
353
365
  log.stop_spinner()
354
366
  log.error("CTRL-C pressed, terminate subprocess")
355
- process.terminate()
356
- try:
357
- process.wait(timeout=5)
358
- except subprocess.TimeoutExpired:
359
- log.error(
360
- "Subprocess did not terminate in time. Forcing kill...")
361
- process.kill()
367
+ _terminate(process)
362
368
  sys.exit(1)
369
+ except Exception:
370
+ # on_output raising aborts the command: stop the subprocess
371
+ # rather than letting it run on unobserved.
372
+ log.stop_spinner()
373
+ _terminate(process)
374
+ raise
363
375
 
364
376
  log.stop_spinner()
365
377
 
@@ -13,6 +13,27 @@ from . import CONFIG_DIR
13
13
  _BARE_KEY_RE = re.compile(r'^[A-Za-z0-9_-]+$')
14
14
 
15
15
 
16
+ def ensure_config_dir(workspace_dir: str) -> None:
17
+ """Create the config dir with a .gitignore that ignores everything in it.
18
+
19
+ Nothing in the config dir belongs in git. A .gitignore of its own keeps
20
+ it out without touching the workspace .gitignore, which may be checked
21
+ into Perforce. The file is rewritten when its content differs, which
22
+ also replaces the state.toml-only ignore file of earlier versions."""
23
+ config_dir = os.path.join(workspace_dir, CONFIG_DIR)
24
+ os.makedirs(config_dir, exist_ok=True)
25
+ gitignore = os.path.join(config_dir, '.gitignore')
26
+ content = '*\n'
27
+ try:
28
+ with open(gitignore, encoding='utf-8') as f:
29
+ if f.read() == content:
30
+ return
31
+ except FileNotFoundError:
32
+ pass
33
+ with open(gitignore, 'w', encoding='utf-8') as f:
34
+ f.write(content)
35
+
36
+
16
37
  def config_path(workspace_dir: str) -> str:
17
38
  """Return the path to the config file."""
18
39
  return os.path.join(workspace_dir, CONFIG_DIR, 'config.toml')
@@ -49,6 +70,7 @@ def save_config(workspace_dir: str, config: dict) -> None:
49
70
  merged = load_config(workspace_dir)
50
71
  for section, values in config.items():
51
72
  merged.setdefault(section, {}).update(values)
73
+ ensure_config_dir(workspace_dir)
52
74
  write_toml(config_path(workspace_dir), merged)
53
75
 
54
76
 
@@ -452,6 +452,16 @@ def parse_p4_sync_line(line: str) -> tuple[str | None, str | None]:
452
452
  return (None, None)
453
453
 
454
454
 
455
+ # p4 writes a synced file to a temp file and renames it into place. On
456
+ # Windows the rename fails when another process holds the target open, e.g.
457
+ # an editor with the asset loaded, leaving the file unsynced.
458
+ _RENAME_FAILED_PREFIX = 'rename: failed to rename '
459
+
460
+
461
+ class P4SyncAbortError(CommandError):
462
+ """Raised to stop a p4 sync that can no longer succeed."""
463
+
464
+
455
465
  class P4SyncOutputProcessor:
456
466
  """Process p4 sync output in real-time."""
457
467
 
@@ -465,6 +475,14 @@ class P4SyncOutputProcessor:
465
475
  log.info('all files up to date')
466
476
  return
467
477
 
478
+ if line.startswith(_RENAME_FAILED_PREFIX):
479
+ log.error(line)
480
+ raise P4SyncAbortError(
481
+ 'Aborting sync: p4 could not replace a file, most likely '
482
+ 'because another process has it open. The workspace is left '
483
+ 'partially synced: close that process, then commit or '
484
+ 'restore the synced files before syncing again.')
485
+
468
486
  mode, filename = parse_p4_sync_line(line)
469
487
  if not mode or not filename:
470
488
  log.warning(f'Unparsable line: {line}')
@@ -0,0 +1,98 @@
1
+ """
2
+ Blocking processes for git-p4son.
3
+
4
+ A sync is refused while any of the configured blocking processes is running.
5
+ Syncing while e.g. the Unreal editor is open lets p4 replace assets the editor
6
+ still has loaded, so the editor keeps working against files that no longer
7
+ match what is on disk.
8
+ """
9
+
10
+ import csv
11
+ import os
12
+
13
+ from .common import RunError, run
14
+ from .config import load_config
15
+ from .log import log
16
+
17
+ IGNORE_FLAG = '--ignore-blocking-processes'
18
+
19
+
20
+ def get_blocking_processes(workspace_dir: str) -> list[str]:
21
+ """Return the configured blocking process names; raise ValueError if malformed."""
22
+ sync = load_config(workspace_dir).get('sync', {})
23
+ if not isinstance(sync, dict):
24
+ raise ValueError('sync in config.toml is not a table')
25
+ names = sync.get('blocking-processes', [])
26
+ if (not isinstance(names, list)
27
+ or not all(isinstance(name, str) for name in names)):
28
+ raise ValueError(
29
+ 'sync.blocking-processes in config.toml must be a list of strings')
30
+ return names
31
+
32
+
33
+ def normalize_process_name(name: str) -> str:
34
+ """Reduce a process name to a form comparable across platforms.
35
+
36
+ Any directory and .exe suffix are dropped and case is ignored, so
37
+ "UnrealEditor" matches "UnrealEditor.exe" on Windows and a full path in
38
+ ps output."""
39
+ name = os.path.basename(name.strip())
40
+ if name.lower().endswith('.exe'):
41
+ name = name[:-len('.exe')]
42
+ return name.lower()
43
+
44
+
45
+ def get_running_processes() -> set[str]:
46
+ """Return the normalized names of all currently running processes."""
47
+ if os.name == 'nt':
48
+ # /NH drops the header row. CSV output keeps an image name that
49
+ # contains spaces or commas inside one quoted field.
50
+ result = run(['tasklist', '/NH', '/FO', 'CSV'])
51
+ lines = [line for line in result.stdout if line.strip()]
52
+ return {normalize_process_name(row[0])
53
+ for row in csv.reader(lines) if row}
54
+ # -ww stops ps truncating output to the terminal width, which would
55
+ # otherwise cut long process names short.
56
+ result = run(['ps', '-A', '-ww', '-o', 'comm='])
57
+ return {normalize_process_name(line)
58
+ for line in result.stdout if line.strip()}
59
+
60
+
61
+ def check_no_blocking_processes(workspace_dir: str, ignore: bool) -> bool:
62
+ """Report whether the sync may go ahead as far as running processes go.
63
+
64
+ Nothing configured means nothing to check, so no process listing is
65
+ made. A malformed config or a process list that cannot be read fails
66
+ the check: going ahead would silently drop the protection. ignore skips
67
+ all of it, config included."""
68
+ if ignore:
69
+ log.heading('Checking for blocking processes')
70
+ log.warning(f'Skipped ({IGNORE_FLAG})')
71
+ return True
72
+
73
+ try:
74
+ names = get_blocking_processes(workspace_dir)
75
+ except ValueError as error:
76
+ log.error(f'Could not read the blocking processes: {error}')
77
+ return False
78
+ if not names:
79
+ return True
80
+
81
+ log.heading('Checking for blocking processes')
82
+ try:
83
+ running = get_running_processes()
84
+ except (OSError, RunError) as error:
85
+ log.error(f'Could not list running processes: {error}')
86
+ return False
87
+
88
+ blocking = sorted(name for name in names
89
+ if normalize_process_name(name) in running)
90
+ if not blocking:
91
+ log.success('None running')
92
+ return True
93
+
94
+ for name in blocking:
95
+ log.info(name)
96
+ log.error('Refusing to sync while these blocking processes are running. '
97
+ f'Close them and sync again, or pass {IGNORE_FLAG}')
98
+ return False
@@ -10,6 +10,7 @@ import os
10
10
  import shlex
11
11
  import subprocess
12
12
  from . import CONFIG_DIR
13
+ from .config import ensure_config_dir
13
14
  from .changelist_store import alias_exists, validate_alias_name
14
15
  from .git import get_commit_lines_since, resolve_editor
15
16
  from .log import log
@@ -112,6 +113,7 @@ def review_command(args: argparse.Namespace) -> int:
112
113
 
113
114
  # Write todo to .git-p4son/reviews/todo
114
115
  reviews_dir = _reviews_dir(workspace_dir)
116
+ ensure_config_dir(workspace_dir)
115
117
  os.makedirs(reviews_dir, exist_ok=True)
116
118
  todo_file = _todo_path(workspace_dir)
117
119
  with open(todo_file, 'w') as f:
@@ -0,0 +1,38 @@
1
+ """
2
+ Local, per-user state for git-p4son.
3
+
4
+ Stored in .git-p4son/state.toml. State here reflects a single user's workspace
5
+ and preferences, e.g. dismissed warnings.
6
+ """
7
+
8
+ import os
9
+
10
+ from . import CONFIG_DIR
11
+ from .config import ensure_config_dir, load_toml, write_toml
12
+
13
+
14
+ def state_path(workspace_dir: str) -> str:
15
+ """Return the path to the local state file."""
16
+ return os.path.join(workspace_dir, CONFIG_DIR, 'state.toml')
17
+
18
+
19
+ def _load(workspace_dir: str) -> dict:
20
+ return load_toml(state_path(workspace_dir))
21
+
22
+
23
+ def _save(workspace_dir: str, state: dict) -> None:
24
+ ensure_config_dir(workspace_dir)
25
+ write_toml(state_path(workspace_dir), state)
26
+
27
+
28
+ def is_clobber_warning_dismissed(workspace_dir: str) -> bool:
29
+ """Return whether the user permanently dismissed the clobber warning."""
30
+ return bool(_load(workspace_dir).get('clobber', {})
31
+ .get('dismiss_warning', False))
32
+
33
+
34
+ def dismiss_clobber_warning(workspace_dir: str) -> None:
35
+ """Persist that the clobber warning should never be shown again."""
36
+ state = _load(workspace_dir)
37
+ state.setdefault('clobber', {})['dismiss_warning'] = True
38
+ _save(workspace_dir, state)