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.
- {git_p4son-0.4.0 → git_p4son-0.4.2}/PKG-INFO +35 -51
- {git_p4son-0.4.0 → git_p4son-0.4.2}/README.md +34 -50
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/__init__.py +1 -1
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/changelist_store.py +2 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/cli.py +19 -2
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/common.py +19 -7
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/config.py +22 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/perforce.py +18 -0
- git_p4son-0.4.2/git_p4son/processes.py +98 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/review.py +2 -0
- git_p4son-0.4.2/git_p4son/state.py +38 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/sync.py +28 -6
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/sync_split.py +3 -1
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son.egg-info/PKG-INFO +35 -51
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son.egg-info/SOURCES.txt +2 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/pyproject.toml +1 -1
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_changelist_store.py +7 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_cli.py +19 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_common.py +19 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_complete.py +2 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_config.py +42 -0
- git_p4son-0.4.2/tests/test_processes.py +153 -0
- git_p4son-0.4.2/tests/test_state.py +39 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_sync.py +84 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_sync_split.py +46 -6
- git_p4son-0.4.0/git_p4son/state.py +0 -56
- git_p4son-0.4.0/tests/test_state.py +0 -64
- {git_p4son-0.4.0 → git_p4son-0.4.2}/LICENSE +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/__main__.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/alias.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/complete.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/completions/_git-p4son +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/completions/git-p4son.bash +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/completions/git-p4son.ps1 +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/depot.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/git.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/hooks.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/init.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/lib.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/list_changes.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/log.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/new.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/update.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son/writable.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son.egg-info/dependency_links.txt +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son.egg-info/entry_points.txt +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/git_p4son.egg-info/top_level.txt +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/setup.cfg +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_alias.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_depot.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_git.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_hooks.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_init.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_lib_changelist.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_lib_edit.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_lib_revert.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_lib_review.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_list_changes.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_log.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_new.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_perforce.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_review.py +0 -0
- {git_p4son-0.4.0 → git_p4son-0.4.2}/tests/test_update.py +0 -0
- {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.
|
|
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),
|
|
352
|
-
all behave exactly as they do for `sync`. The
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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),
|
|
329
|
-
all behave exactly as they do for `sync`. The
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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.
|
|
@@ -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
|
|
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)
|