git-p4son 0.4.1__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 (62) hide show
  1. {git_p4son-0.4.1 → git_p4son-0.4.2}/PKG-INFO +29 -51
  2. {git_p4son-0.4.1 → git_p4son-0.4.2}/README.md +28 -50
  3. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/__init__.py +1 -1
  4. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/cli.py +10 -0
  5. git_p4son-0.4.2/git_p4son/processes.py +98 -0
  6. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/sync.py +17 -6
  7. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/sync_split.py +3 -1
  8. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son.egg-info/PKG-INFO +29 -51
  9. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son.egg-info/SOURCES.txt +2 -0
  10. {git_p4son-0.4.1 → git_p4son-0.4.2}/pyproject.toml +1 -1
  11. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_cli.py +8 -0
  12. git_p4son-0.4.2/tests/test_processes.py +153 -0
  13. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_sync_split.py +46 -6
  14. {git_p4son-0.4.1 → git_p4son-0.4.2}/LICENSE +0 -0
  15. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/__main__.py +0 -0
  16. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/alias.py +0 -0
  17. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/changelist_store.py +0 -0
  18. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/common.py +0 -0
  19. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/complete.py +0 -0
  20. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/completions/_git-p4son +0 -0
  21. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/completions/git-p4son.bash +0 -0
  22. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/completions/git-p4son.ps1 +0 -0
  23. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/config.py +0 -0
  24. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/depot.py +0 -0
  25. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/git.py +0 -0
  26. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/hooks.py +0 -0
  27. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/init.py +0 -0
  28. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/lib.py +0 -0
  29. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/list_changes.py +0 -0
  30. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/log.py +0 -0
  31. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/new.py +0 -0
  32. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/perforce.py +0 -0
  33. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/review.py +0 -0
  34. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/state.py +0 -0
  35. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/update.py +0 -0
  36. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son/writable.py +0 -0
  37. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son.egg-info/dependency_links.txt +0 -0
  38. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son.egg-info/entry_points.txt +0 -0
  39. {git_p4son-0.4.1 → git_p4son-0.4.2}/git_p4son.egg-info/top_level.txt +0 -0
  40. {git_p4son-0.4.1 → git_p4son-0.4.2}/setup.cfg +0 -0
  41. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_alias.py +0 -0
  42. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_changelist_store.py +0 -0
  43. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_common.py +0 -0
  44. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_complete.py +0 -0
  45. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_config.py +0 -0
  46. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_depot.py +0 -0
  47. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_git.py +0 -0
  48. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_hooks.py +0 -0
  49. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_init.py +0 -0
  50. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_lib_changelist.py +0 -0
  51. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_lib_edit.py +0 -0
  52. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_lib_revert.py +0 -0
  53. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_lib_review.py +0 -0
  54. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_list_changes.py +0 -0
  55. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_log.py +0 -0
  56. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_new.py +0 -0
  57. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_perforce.py +0 -0
  58. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_review.py +0 -0
  59. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_state.py +0 -0
  60. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_sync.py +0 -0
  61. {git_p4son-0.4.1 → git_p4son-0.4.2}/tests/test_update.py +0 -0
  62. {git_p4son-0.4.1 → 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.1
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
@@ -298,7 +298,7 @@ saves the answer; run `git p4son writable apply` once your files are committed t
298
298
  Sync local git repository with a Perforce workspace:
299
299
 
300
300
  ```sh
301
- git p4son sync [changelist ...] [--force] [--dry-run]
301
+ git p4son sync [changelist ...] [--force] [--dry-run] [--ignore-blocking-processes]
302
302
  ```
303
303
 
304
304
  **Arguments:**
@@ -311,6 +311,7 @@ git p4son sync [changelist ...] [--force] [--dry-run]
311
311
  - `-f, --force`: Allow syncing to changelists older than the current one.
312
312
  - `-n, --dry-run`: Print the resolved sync sequence without syncing. The arguments are validated as for a
313
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.
314
315
 
315
316
  **Examples:**
316
317
  ```sh
@@ -324,6 +325,24 @@ git p4son sync 12345 --force
324
325
  git p4son sync --dry-run # show the changelists a sync to latest would visit
325
326
  ```
326
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
+
327
346
  #### pre-sync hook
328
347
 
329
348
  Before `git-p4son sync` syncs any changelist, git-p4son runs [hooks](#hooks) from
@@ -340,7 +359,7 @@ from `.git-p4son/hooks/post-sync/`.
340
359
  Sync forward from the last synced changelist, splitting selected users' changelists into their own commits:
341
360
 
342
361
  ```sh
343
- git p4son sync-split [changelist] [--user NAME ...] [--dry-run]
362
+ git p4son sync-split [changelist] [--user NAME ...] [--dry-run] [--ignore-blocking-processes]
344
363
  ```
345
364
 
346
365
  Syncing straight to the latest changelist lumps your own submitted changelists together with everyone
@@ -354,11 +373,12 @@ own submits one at a time. Repeat `--user` to split out several people's changel
354
373
 
355
374
  It is equivalent to working out the numbers by hand and running
356
375
  `git p4son sync <before-theirs> <theirs> ... head`, and it delegates to `sync` once the sequence is
357
- resolved, so [pre-sync and post-sync hooks](#hooks), writable-file merging, and the clean-workspace checks
358
- all behave exactly as they do for `sync`. The clean-workspace checks and the pre-sync hooks are the one
359
- thing that runs earlier: resolving the sequence costs several Perforce queries, so they go first, and a
360
- dirty workspace or a hook that aborts the sync says so before that work. They run once for the whole sync,
361
- 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.
362
382
 
363
383
  **Arguments:**
364
384
  - `changelist` (optional): Changelist number to sync up to, or `head` for the latest. Omit to sync to the
@@ -368,6 +388,7 @@ and not at all for a `--dry-run` or when there is nothing to sync.
368
388
  - `-u, --user NAME`: Perforce user whose changelists to split into their own commits. Repeat the flag to
369
389
  select several users. Defaults to the current p4 user (from `p4 info`).
370
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.
371
392
 
372
393
  **Examples:**
373
394
  ```sh
@@ -714,49 +735,6 @@ before the first sync.
714
735
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
715
736
  `.git-p4son/hooks/post-sync/`.
716
737
 
717
- ### Example hooks
718
-
719
- The [`examples/hooks/`](examples/hooks/) directory holds ready-made hooks. Nothing installs them for you:
720
- symlink or copy the one you want into the matching hook directory in your workspace. Symlinking keeps the hook
721
- up to date with the clone it points at, and on macOS and Linux the executable bit comes from the file in the
722
- clone, so no `chmod` is needed.
723
-
724
- #### block-while-running (pre-sync)
725
-
726
- Aborts the sync while a named process is running, so a sync cannot swap out assets that a running editor still
727
- has loaded. Link it into your workspace's `pre-sync` hook directory - on macOS and Linux:
728
-
729
- ```sh
730
- ln -s /path/to/git-p4son/examples/hooks/pre-sync/block-while-running.py .git-p4son/hooks/pre-sync/
731
- ```
732
-
733
- On Windows, from PowerShell. Note that `-Path` is the link to create and `-Target` the existing file, the
734
- reverse of the `ln -s` argument order, and that `-Target` should be absolute: a relative path is resolved
735
- against the current directory and quietly produces a broken link.
736
-
737
- ```powershell
738
- New-Item -ItemType SymbolicLink `
739
- -Path "C:\p4\my-workspace\.git-p4son\hooks\pre-sync\block-while-running.py" `
740
- -Target "C:\src\git-p4son\examples\hooks\pre-sync\block-while-running.py"
741
- ```
742
-
743
- Creating a symlink on Windows needs either an elevated shell or Developer Mode enabled (Settings > System >
744
- For developers). Without one of those, copy the file instead and re-copy it whenever the clone updates.
745
-
746
- Which processes block a sync is read from your workspace's own `.git-p4son/config.toml`, so a symlinked hook
747
- stays per-project:
748
-
749
- ```toml
750
- [hooks.block-while-running]
751
- processes = ["UnrealEditor", "UnrealLightmass"]
752
- ```
753
-
754
- Names are matched with any `.exe` suffix stripped and case ignored, so a single entry covers
755
- `UnrealEditor.exe` on Windows and `UnrealEditor` on macOS. Without that table the hook checks nothing and the
756
- sync proceeds; a table that is present but malformed aborts the sync rather than silently skipping the check.
757
-
758
- Set `GIT_P4SON_SKIP_PROCESS_CHECK=1` to run one sync without the check.
759
-
760
738
  ## Shell Completions
761
739
 
762
740
  Tab completion is available for bash, zsh, and PowerShell, including commands, flags, and dynamic alias names.
@@ -275,7 +275,7 @@ saves the answer; run `git p4son writable apply` once your files are committed t
275
275
  Sync local git repository with a Perforce workspace:
276
276
 
277
277
  ```sh
278
- git p4son sync [changelist ...] [--force] [--dry-run]
278
+ git p4son sync [changelist ...] [--force] [--dry-run] [--ignore-blocking-processes]
279
279
  ```
280
280
 
281
281
  **Arguments:**
@@ -288,6 +288,7 @@ git p4son sync [changelist ...] [--force] [--dry-run]
288
288
  - `-f, --force`: Allow syncing to changelists older than the current one.
289
289
  - `-n, --dry-run`: Print the resolved sync sequence without syncing. The arguments are validated as for a
290
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.
291
292
 
292
293
  **Examples:**
293
294
  ```sh
@@ -301,6 +302,24 @@ git p4son sync 12345 --force
301
302
  git p4son sync --dry-run # show the changelists a sync to latest would visit
302
303
  ```
303
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
+
304
323
  #### pre-sync hook
305
324
 
306
325
  Before `git-p4son sync` syncs any changelist, git-p4son runs [hooks](#hooks) from
@@ -317,7 +336,7 @@ from `.git-p4son/hooks/post-sync/`.
317
336
  Sync forward from the last synced changelist, splitting selected users' changelists into their own commits:
318
337
 
319
338
  ```sh
320
- git p4son sync-split [changelist] [--user NAME ...] [--dry-run]
339
+ git p4son sync-split [changelist] [--user NAME ...] [--dry-run] [--ignore-blocking-processes]
321
340
  ```
322
341
 
323
342
  Syncing straight to the latest changelist lumps your own submitted changelists together with everyone
@@ -331,11 +350,12 @@ own submits one at a time. Repeat `--user` to split out several people's changel
331
350
 
332
351
  It is equivalent to working out the numbers by hand and running
333
352
  `git p4son sync <before-theirs> <theirs> ... head`, and it delegates to `sync` once the sequence is
334
- resolved, so [pre-sync and post-sync hooks](#hooks), writable-file merging, and the clean-workspace checks
335
- all behave exactly as they do for `sync`. The clean-workspace checks and the pre-sync hooks are the one
336
- thing that runs earlier: resolving the sequence costs several Perforce queries, so they go first, and a
337
- dirty workspace or a hook that aborts the sync says so before that work. They run once for the whole sync,
338
- 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.
339
359
 
340
360
  **Arguments:**
341
361
  - `changelist` (optional): Changelist number to sync up to, or `head` for the latest. Omit to sync to the
@@ -345,6 +365,7 @@ and not at all for a `--dry-run` or when there is nothing to sync.
345
365
  - `-u, --user NAME`: Perforce user whose changelists to split into their own commits. Repeat the flag to
346
366
  select several users. Defaults to the current p4 user (from `p4 info`).
347
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.
348
369
 
349
370
  **Examples:**
350
371
  ```sh
@@ -691,49 +712,6 @@ before the first sync.
691
712
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
692
713
  `.git-p4son/hooks/post-sync/`.
693
714
 
694
- ### Example hooks
695
-
696
- The [`examples/hooks/`](examples/hooks/) directory holds ready-made hooks. Nothing installs them for you:
697
- symlink or copy the one you want into the matching hook directory in your workspace. Symlinking keeps the hook
698
- up to date with the clone it points at, and on macOS and Linux the executable bit comes from the file in the
699
- clone, so no `chmod` is needed.
700
-
701
- #### block-while-running (pre-sync)
702
-
703
- Aborts the sync while a named process is running, so a sync cannot swap out assets that a running editor still
704
- has loaded. Link it into your workspace's `pre-sync` hook directory - on macOS and Linux:
705
-
706
- ```sh
707
- ln -s /path/to/git-p4son/examples/hooks/pre-sync/block-while-running.py .git-p4son/hooks/pre-sync/
708
- ```
709
-
710
- On Windows, from PowerShell. Note that `-Path` is the link to create and `-Target` the existing file, the
711
- reverse of the `ln -s` argument order, and that `-Target` should be absolute: a relative path is resolved
712
- against the current directory and quietly produces a broken link.
713
-
714
- ```powershell
715
- New-Item -ItemType SymbolicLink `
716
- -Path "C:\p4\my-workspace\.git-p4son\hooks\pre-sync\block-while-running.py" `
717
- -Target "C:\src\git-p4son\examples\hooks\pre-sync\block-while-running.py"
718
- ```
719
-
720
- Creating a symlink on Windows needs either an elevated shell or Developer Mode enabled (Settings > System >
721
- For developers). Without one of those, copy the file instead and re-copy it whenever the clone updates.
722
-
723
- Which processes block a sync is read from your workspace's own `.git-p4son/config.toml`, so a symlinked hook
724
- stays per-project:
725
-
726
- ```toml
727
- [hooks.block-while-running]
728
- processes = ["UnrealEditor", "UnrealLightmass"]
729
- ```
730
-
731
- Names are matched with any `.exe` suffix stripped and case ignored, so a single entry covers
732
- `UnrealEditor.exe` on Windows and `UnrealEditor` on macOS. Without that table the hook checks nothing and the
733
- sync proceeds; a table that is present but malformed aborts the sync rather than silently skipping the check.
734
-
735
- Set `GIT_P4SON_SKIP_PROCESS_CHECK=1` to run one sync without the check.
736
-
737
715
  ## Shell Completions
738
716
 
739
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.1"
10
+ __version__ = "0.4.2"
11
11
  __author__ = "Andreas Andersson"
12
12
  __email__ = "andreas@neoboid.com"
@@ -109,6 +109,11 @@ Examples:
109
109
  action='store_true',
110
110
  help='Print the resolved sync sequence without syncing'
111
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
+ )
112
117
 
113
118
  # Sync-split subcommand
114
119
  sync_split_parser = subparsers.add_parser(
@@ -141,6 +146,11 @@ Examples:
141
146
  action='store_true',
142
147
  help='Print the resolved sync sequence without syncing'
143
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
+ )
144
154
 
145
155
  # New subcommand
146
156
  new_parser = subparsers.add_parser(
@@ -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
@@ -23,6 +23,7 @@ from .hooks import run_hooks
23
23
  from .lib import check_git_workspace_clean
24
24
  from .depot import resolve_depot_root
25
25
  from .log import log
26
+ from .processes import check_no_blocking_processes
26
27
  from .writable import is_writable_mode, make_writable
27
28
  from .perforce import (
28
29
  get_latest_changelist,
@@ -645,8 +646,10 @@ def _run_pre_sync_hooks(workspace_dir: str, invocation_dir: str) -> bool:
645
646
 
646
647
 
647
648
  def sync_preflight(depot_root: str, workspace_dir: str, invocation_dir: str,
648
- already_done: bool = False) -> bool:
649
- """Gate a sync: both workspaces clean, then the pre-sync hooks.
649
+ already_done: bool = False,
650
+ ignore_blocking_processes: bool = False) -> bool:
651
+ """Gate a sync: no blocking processes, both workspaces clean, then the
652
+ pre-sync hooks.
650
653
 
651
654
  Returns False when the sync must not go ahead. Callers run this once they
652
655
  know a sync will actually be attempted, so that a run with nothing to sync
@@ -657,6 +660,11 @@ def sync_preflight(depot_root: str, workspace_dir: str, invocation_dir: str,
657
660
  """
658
661
  if already_done:
659
662
  return True
663
+ # First: it is the cheapest check, and an editor left open is the most
664
+ # common reason not to sync.
665
+ if not check_no_blocking_processes(workspace_dir,
666
+ ignore_blocking_processes):
667
+ return False
660
668
  if not check_git_workspace_clean(workspace_dir):
661
669
  return False
662
670
  if not _check_p4_workspace_clean(depot_root, workspace_dir):
@@ -776,6 +784,8 @@ def sync_command(args: argparse.Namespace) -> int:
776
784
  workspace_dir = args.workspace_dir
777
785
  invocation_dir = vars(args).get('invocation_dir', workspace_dir)
778
786
  preflight_done = vars(args).get('preflight_done', False)
787
+ ignore_blocking_processes = vars(args).get('ignore_blocking_processes',
788
+ False)
779
789
 
780
790
  # sync-split resolves the depot root before handing over, so it passes
781
791
  # the result on rather than having the client spec queried twice.
@@ -846,11 +856,12 @@ def sync_command(args: argparse.Namespace) -> int:
846
856
  if not _handle_clobber_warning(clobber, workspace_dir):
847
857
  return 1
848
858
 
849
- # The single gate: both workspaces clean, then the pre-sync hooks. Runs
850
- # once for the whole sync, covering the catch-up pass as well, and is
851
- # skipped outright when the caller (sync-split) already ran it.
859
+ # The single gate: no blocking processes, both workspaces clean, then
860
+ # the pre-sync hooks. Runs once for the whole sync, covering the
861
+ # catch-up pass as well, and is skipped outright when the caller
862
+ # (sync-split) already ran it.
852
863
  if not sync_preflight(depot_root, workspace_dir, invocation_dir,
853
- preflight_done):
864
+ preflight_done, ignore_blocking_processes):
854
865
  return 1
855
866
 
856
867
  log.heading('Finding HEAD commit')
@@ -117,7 +117,9 @@ def sync_split_command(args: argparse.Namespace) -> int:
117
117
  # sync_command is told it is done so none of it happens twice, and a dry
118
118
  # run syncs nothing, so it skips the gate entirely.
119
119
  if not args.dry_run:
120
- if not sync_preflight(depot_root, workspace_dir, invocation_dir):
120
+ if not sync_preflight(
121
+ depot_root, workspace_dir, invocation_dir,
122
+ ignore_blocking_processes=args.ignore_blocking_processes):
121
123
  return 1
122
124
  args.preflight_done = True
123
125
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-p4son
3
- Version: 0.4.1
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
@@ -298,7 +298,7 @@ saves the answer; run `git p4son writable apply` once your files are committed t
298
298
  Sync local git repository with a Perforce workspace:
299
299
 
300
300
  ```sh
301
- git p4son sync [changelist ...] [--force] [--dry-run]
301
+ git p4son sync [changelist ...] [--force] [--dry-run] [--ignore-blocking-processes]
302
302
  ```
303
303
 
304
304
  **Arguments:**
@@ -311,6 +311,7 @@ git p4son sync [changelist ...] [--force] [--dry-run]
311
311
  - `-f, --force`: Allow syncing to changelists older than the current one.
312
312
  - `-n, --dry-run`: Print the resolved sync sequence without syncing. The arguments are validated as for a
313
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.
314
315
 
315
316
  **Examples:**
316
317
  ```sh
@@ -324,6 +325,24 @@ git p4son sync 12345 --force
324
325
  git p4son sync --dry-run # show the changelists a sync to latest would visit
325
326
  ```
326
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
+
327
346
  #### pre-sync hook
328
347
 
329
348
  Before `git-p4son sync` syncs any changelist, git-p4son runs [hooks](#hooks) from
@@ -340,7 +359,7 @@ from `.git-p4son/hooks/post-sync/`.
340
359
  Sync forward from the last synced changelist, splitting selected users' changelists into their own commits:
341
360
 
342
361
  ```sh
343
- git p4son sync-split [changelist] [--user NAME ...] [--dry-run]
362
+ git p4son sync-split [changelist] [--user NAME ...] [--dry-run] [--ignore-blocking-processes]
344
363
  ```
345
364
 
346
365
  Syncing straight to the latest changelist lumps your own submitted changelists together with everyone
@@ -354,11 +373,12 @@ own submits one at a time. Repeat `--user` to split out several people's changel
354
373
 
355
374
  It is equivalent to working out the numbers by hand and running
356
375
  `git p4son sync <before-theirs> <theirs> ... head`, and it delegates to `sync` once the sequence is
357
- resolved, so [pre-sync and post-sync hooks](#hooks), writable-file merging, and the clean-workspace checks
358
- all behave exactly as they do for `sync`. The clean-workspace checks and the pre-sync hooks are the one
359
- thing that runs earlier: resolving the sequence costs several Perforce queries, so they go first, and a
360
- dirty workspace or a hook that aborts the sync says so before that work. They run once for the whole sync,
361
- 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.
362
382
 
363
383
  **Arguments:**
364
384
  - `changelist` (optional): Changelist number to sync up to, or `head` for the latest. Omit to sync to the
@@ -368,6 +388,7 @@ and not at all for a `--dry-run` or when there is nothing to sync.
368
388
  - `-u, --user NAME`: Perforce user whose changelists to split into their own commits. Repeat the flag to
369
389
  select several users. Defaults to the current p4 user (from `p4 info`).
370
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.
371
392
 
372
393
  **Examples:**
373
394
  ```sh
@@ -714,49 +735,6 @@ before the first sync.
714
735
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
715
736
  `.git-p4son/hooks/post-sync/`.
716
737
 
717
- ### Example hooks
718
-
719
- The [`examples/hooks/`](examples/hooks/) directory holds ready-made hooks. Nothing installs them for you:
720
- symlink or copy the one you want into the matching hook directory in your workspace. Symlinking keeps the hook
721
- up to date with the clone it points at, and on macOS and Linux the executable bit comes from the file in the
722
- clone, so no `chmod` is needed.
723
-
724
- #### block-while-running (pre-sync)
725
-
726
- Aborts the sync while a named process is running, so a sync cannot swap out assets that a running editor still
727
- has loaded. Link it into your workspace's `pre-sync` hook directory - on macOS and Linux:
728
-
729
- ```sh
730
- ln -s /path/to/git-p4son/examples/hooks/pre-sync/block-while-running.py .git-p4son/hooks/pre-sync/
731
- ```
732
-
733
- On Windows, from PowerShell. Note that `-Path` is the link to create and `-Target` the existing file, the
734
- reverse of the `ln -s` argument order, and that `-Target` should be absolute: a relative path is resolved
735
- against the current directory and quietly produces a broken link.
736
-
737
- ```powershell
738
- New-Item -ItemType SymbolicLink `
739
- -Path "C:\p4\my-workspace\.git-p4son\hooks\pre-sync\block-while-running.py" `
740
- -Target "C:\src\git-p4son\examples\hooks\pre-sync\block-while-running.py"
741
- ```
742
-
743
- Creating a symlink on Windows needs either an elevated shell or Developer Mode enabled (Settings > System >
744
- For developers). Without one of those, copy the file instead and re-copy it whenever the clone updates.
745
-
746
- Which processes block a sync is read from your workspace's own `.git-p4son/config.toml`, so a symlinked hook
747
- stays per-project:
748
-
749
- ```toml
750
- [hooks.block-while-running]
751
- processes = ["UnrealEditor", "UnrealLightmass"]
752
- ```
753
-
754
- Names are matched with any `.exe` suffix stripped and case ignored, so a single entry covers
755
- `UnrealEditor.exe` on Windows and `UnrealEditor` on macOS. Without that table the hook checks nothing and the
756
- sync proceeds; a table that is present but malformed aborts the sync rather than silently skipping the check.
757
-
758
- Set `GIT_P4SON_SKIP_PROCESS_CHECK=1` to run one sync without the check.
759
-
760
738
  ## Shell Completions
761
739
 
762
740
  Tab completion is available for bash, zsh, and PowerShell, including commands, flags, and dynamic alias names.
@@ -18,6 +18,7 @@ git_p4son/list_changes.py
18
18
  git_p4son/log.py
19
19
  git_p4son/new.py
20
20
  git_p4son/perforce.py
21
+ git_p4son/processes.py
21
22
  git_p4son/review.py
22
23
  git_p4son/state.py
23
24
  git_p4son/sync.py
@@ -50,6 +51,7 @@ tests/test_list_changes.py
50
51
  tests/test_log.py
51
52
  tests/test_new.py
52
53
  tests/test_perforce.py
54
+ tests/test_processes.py
53
55
  tests/test_review.py
54
56
  tests/test_state.py
55
57
  tests/test_sync.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "git-p4son"
7
- version = "0.4.1"
7
+ version = "0.4.2"
8
8
  description = "Utility for keeping a Perforce workspace and local git repo in sync"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -53,6 +53,14 @@ class TestCreateParser(unittest.TestCase):
53
53
  args = self.parser.parse_args(['sync', flag])
54
54
  self.assertTrue(args.dry_run)
55
55
 
56
+ def test_ignore_blocking_processes(self):
57
+ for command in ('sync', 'sync-split'):
58
+ self.assertFalse(self.parser.parse_args(
59
+ [command]).ignore_blocking_processes)
60
+ self.assertTrue(self.parser.parse_args(
61
+ [command, '--ignore-blocking-processes'])
62
+ .ignore_blocking_processes)
63
+
56
64
  def test_new_command_no_message(self):
57
65
  args = self.parser.parse_args(['new'])
58
66
  self.assertEqual(args.command, 'new')
@@ -0,0 +1,153 @@
1
+ """Tests for git_p4son.processes module."""
2
+
3
+ import os
4
+ import tempfile
5
+ import unittest
6
+ from unittest import mock
7
+
8
+ from git_p4son.common import RunError
9
+ from git_p4son.processes import (
10
+ check_no_blocking_processes, get_blocking_processes,
11
+ get_running_processes, normalize_process_name,
12
+ )
13
+ from tests.helpers import make_run_result
14
+
15
+
16
+ class _WorkspaceTestCase(unittest.TestCase):
17
+
18
+ def setUp(self):
19
+ self._tmp = tempfile.TemporaryDirectory()
20
+ self.addCleanup(self._tmp.cleanup)
21
+ self.workspace = self._tmp.name
22
+
23
+ def write_config(self, text):
24
+ config_dir = os.path.join(self.workspace, '.git-p4son')
25
+ os.makedirs(config_dir, exist_ok=True)
26
+ with open(os.path.join(config_dir, 'config.toml'), 'w',
27
+ encoding='utf-8') as f:
28
+ f.write(text)
29
+
30
+
31
+ class TestGetBlockingProcesses(_WorkspaceTestCase):
32
+
33
+ def test_no_config_file(self):
34
+ self.assertEqual(get_blocking_processes(self.workspace), [])
35
+
36
+ def test_no_sync_table(self):
37
+ self.write_config('[depot]\nroot = "//ws"\n')
38
+ self.assertEqual(get_blocking_processes(self.workspace), [])
39
+
40
+ def test_no_key(self):
41
+ self.write_config('[sync]\nother = 1\n')
42
+ self.assertEqual(get_blocking_processes(self.workspace), [])
43
+
44
+ def test_names(self):
45
+ self.write_config(
46
+ '[sync]\nblocking-processes = ["UnrealEditor", "Lightmass"]\n')
47
+ self.assertEqual(get_blocking_processes(self.workspace),
48
+ ['UnrealEditor', 'Lightmass'])
49
+
50
+ def test_not_a_list(self):
51
+ self.write_config('[sync]\nblocking-processes = "UnrealEditor"\n')
52
+ with self.assertRaises(ValueError):
53
+ get_blocking_processes(self.workspace)
54
+
55
+ def test_non_string_entry(self):
56
+ self.write_config('[sync]\nblocking-processes = ["a", 1]\n')
57
+ with self.assertRaises(ValueError):
58
+ get_blocking_processes(self.workspace)
59
+
60
+ def test_sync_not_a_table(self):
61
+ self.write_config('sync = 1\n')
62
+ with self.assertRaises(ValueError):
63
+ get_blocking_processes(self.workspace)
64
+
65
+
66
+ class TestNormalizeProcessName(unittest.TestCase):
67
+
68
+ def test_strips_exe_and_case(self):
69
+ self.assertEqual(normalize_process_name('UnrealEditor.EXE'),
70
+ 'unrealeditor')
71
+
72
+ def test_strips_directory_and_whitespace(self):
73
+ self.assertEqual(
74
+ normalize_process_name(
75
+ ' /Applications/Foo.app/Contents/MacOS/Foo \n'),
76
+ 'foo')
77
+
78
+ def test_exe_only_stripped_as_suffix(self):
79
+ self.assertEqual(normalize_process_name('exeditor'), 'exeditor')
80
+
81
+
82
+ class TestGetRunningProcesses(unittest.TestCase):
83
+
84
+ @mock.patch('git_p4son.processes.os.name', 'posix')
85
+ @mock.patch('git_p4son.processes.run')
86
+ def test_ps(self, mock_run):
87
+ mock_run.return_value = make_run_result(
88
+ stdout=['/usr/bin/zsh', '', 'UnrealEditor'])
89
+ self.assertEqual(get_running_processes(), {'zsh', 'unrealeditor'})
90
+ self.assertEqual(mock_run.call_args.args[0][0], 'ps')
91
+
92
+ @mock.patch('git_p4son.processes.os.name', 'nt')
93
+ @mock.patch('git_p4son.processes.run')
94
+ def test_tasklist(self, mock_run):
95
+ mock_run.return_value = make_run_result(stdout=[
96
+ '',
97
+ '"UnrealEditor.exe","1234","Console","1","2,000 K"',
98
+ '"My, App.exe","42","Console","1","100 K"',
99
+ ])
100
+ self.assertEqual(get_running_processes(),
101
+ {'unrealeditor', 'my, app'})
102
+ self.assertEqual(mock_run.call_args.args[0][0], 'tasklist')
103
+
104
+
105
+ class TestCheckNoBlockingProcesses(_WorkspaceTestCase):
106
+
107
+ def setUp(self):
108
+ super().setUp()
109
+ patcher = mock.patch('git_p4son.processes.log')
110
+ self.mock_log = patcher.start()
111
+ self.addCleanup(patcher.stop)
112
+ patcher = mock.patch('git_p4son.processes.get_running_processes',
113
+ return_value={'zsh', 'unrealeditor'})
114
+ self.mock_running = patcher.start()
115
+ self.addCleanup(patcher.stop)
116
+
117
+ def test_nothing_configured_lists_nothing(self):
118
+ self.assertTrue(check_no_blocking_processes(self.workspace, False))
119
+ self.mock_running.assert_not_called()
120
+
121
+ def test_none_running(self):
122
+ self.write_config('[sync]\nblocking-processes = ["Lightmass"]\n')
123
+ self.assertTrue(check_no_blocking_processes(self.workspace, False))
124
+
125
+ def test_running_blocks(self):
126
+ self.write_config(
127
+ '[sync]\nblocking-processes = ["UnrealEditor.exe", "Lightmass"]\n')
128
+ self.assertFalse(check_no_blocking_processes(self.workspace, False))
129
+ self.mock_log.info.assert_called_once_with('UnrealEditor.exe')
130
+
131
+ def test_ignore_skips_listing(self):
132
+ self.write_config('[sync]\nblocking-processes = ["UnrealEditor"]\n')
133
+ self.assertTrue(check_no_blocking_processes(self.workspace, True))
134
+ self.mock_running.assert_not_called()
135
+
136
+ def test_ignore_skips_malformed_config(self):
137
+ self.write_config('[sync]\nblocking-processes = "UnrealEditor"\n')
138
+ self.assertTrue(check_no_blocking_processes(self.workspace, True))
139
+
140
+ def test_malformed_config_fails(self):
141
+ self.write_config('[sync]\nblocking-processes = "UnrealEditor"\n')
142
+ self.assertFalse(check_no_blocking_processes(self.workspace, False))
143
+
144
+ def test_invalid_toml_fails(self):
145
+ self.write_config('[sync\n')
146
+ self.assertFalse(check_no_blocking_processes(self.workspace, False))
147
+
148
+ def test_listing_failure_fails(self):
149
+ self.write_config('[sync]\nblocking-processes = ["UnrealEditor"]\n')
150
+ for error in (OSError('no ps'), RunError('ps failed')):
151
+ self.mock_running.side_effect = error
152
+ self.assertFalse(
153
+ check_no_blocking_processes(self.workspace, False))
@@ -117,7 +117,7 @@ class TestSyncSplitCommand(unittest.TestCase):
117
117
 
118
118
  def _args(self, **overrides):
119
119
  values = dict(changelist=None, user=None, dry_run=False,
120
- workspace_dir='/ws')
120
+ ignore_blocking_processes=False, workspace_dir='/ws')
121
121
  values.update(overrides)
122
122
  return mock.Mock(**values)
123
123
 
@@ -244,7 +244,7 @@ class TestSyncSplitPreSyncHooks(unittest.TestCase):
244
244
 
245
245
  def _args(self, **overrides):
246
246
  values = dict(changelist=None, user=None, dry_run=False,
247
- workspace_dir='/ws', invocation_dir='/invoked')
247
+ ignore_blocking_processes=False, workspace_dir='/ws', invocation_dir='/invoked')
248
248
  values.update(overrides)
249
249
  return argparse.Namespace(**values)
250
250
 
@@ -281,6 +281,19 @@ class TestSyncSplitPreSyncHooks(unittest.TestCase):
281
281
  sync_split_command(args)
282
282
  self.assertTrue(args.preflight_done)
283
283
 
284
+ @mock.patch('git_p4son.sync_split.sync_command', return_value=0)
285
+ @mock.patch('git_p4son.sync_split.get_submitted_changes', return_value=[])
286
+ @mock.patch('git_p4son.sync_split.get_p4_user', return_value='me')
287
+ @mock.patch('git_p4son.sync_split.get_latest_changelist', return_value=103)
288
+ @mock.patch('git_p4son.sync_split.git_last_sync',
289
+ return_value=LastSync(changelist=100, commit='abc123'))
290
+ @mock.patch('git_p4son.sync_split.sync_preflight', return_value=True)
291
+ def test_ignore_blocking_processes_reaches_the_gate(
292
+ self, mock_gate, _last_sync, _latest, _user, _changes, _sync):
293
+ sync_split_command(self._args(ignore_blocking_processes=True))
294
+ self.assertTrue(
295
+ mock_gate.call_args.kwargs['ignore_blocking_processes'])
296
+
284
297
  @mock.patch('git_p4son.sync_split.sync_command', return_value=0)
285
298
  @mock.patch('git_p4son.sync_split.get_submitted_changes', return_value=[])
286
299
  @mock.patch('git_p4son.sync_split.get_p4_user', return_value='me')
@@ -348,15 +361,42 @@ class TestSyncSplitPreSyncHooks(unittest.TestCase):
348
361
 
349
362
 
350
363
  class TestSyncPreflight(unittest.TestCase):
351
- """The one gate both commands share: workspace checks then hooks, in
352
- that order, and skipped wholesale when a caller already ran it."""
364
+ """The one gate both commands share: blocking processes, workspace
365
+ checks then hooks, in that order, and skipped wholesale when a caller
366
+ already ran it."""
367
+
368
+ @mock.patch('git_p4son.sync.run_hooks')
369
+ @mock.patch('git_p4son.sync.p4_get_opened_files')
370
+ @mock.patch('git_p4son.lib.get_dirty_files')
371
+ @mock.patch('git_p4son.sync.check_no_blocking_processes',
372
+ return_value=False)
373
+ def test_blocking_process_stops_before_everything_else(
374
+ self, mock_blocking, mock_dirty, mock_opened, mock_run_hooks):
375
+ self.assertFalse(sync_preflight('//ws', '/ws', '/invoked'))
376
+ mock_blocking.assert_called_once_with('/ws', False)
377
+ mock_dirty.assert_not_called()
378
+ mock_opened.assert_not_called()
379
+ mock_run_hooks.assert_not_called()
380
+
381
+ @mock.patch('git_p4son.sync.run_hooks', return_value=[])
382
+ @mock.patch('git_p4son.sync.p4_get_opened_files', return_value=[])
383
+ @mock.patch('git_p4son.lib.get_dirty_files', return_value=[])
384
+ @mock.patch('git_p4son.sync.check_no_blocking_processes',
385
+ return_value=True)
386
+ def test_ignore_blocking_processes_is_passed_on(
387
+ self, mock_blocking, _dirty, _opened, _hooks):
388
+ self.assertTrue(sync_preflight('//ws', '/ws', '/invoked',
389
+ ignore_blocking_processes=True))
390
+ mock_blocking.assert_called_once_with('/ws', True)
353
391
 
354
392
  @mock.patch('git_p4son.sync.run_hooks')
355
393
  @mock.patch('git_p4son.sync.p4_get_opened_files')
356
394
  @mock.patch('git_p4son.lib.get_dirty_files')
395
+ @mock.patch('git_p4son.sync.check_no_blocking_processes')
357
396
  def test_already_done_skips_everything(
358
- self, mock_dirty, mock_opened, mock_run_hooks):
397
+ self, mock_blocking, mock_dirty, mock_opened, mock_run_hooks):
359
398
  self.assertTrue(sync_preflight('//ws', '/ws', '/invoked', True))
399
+ mock_blocking.assert_not_called()
360
400
  mock_dirty.assert_not_called()
361
401
  mock_opened.assert_not_called()
362
402
  mock_run_hooks.assert_not_called()
@@ -414,7 +454,7 @@ class TestSyncSplitCommandMultiUser(unittest.TestCase):
414
454
 
415
455
  def _args(self, **overrides):
416
456
  values = dict(changelist=None, user=None, dry_run=False,
417
- workspace_dir='/ws')
457
+ ignore_blocking_processes=False, workspace_dir='/ws')
418
458
  values.update(overrides)
419
459
  return mock.Mock(**values)
420
460
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes