git-p4son 0.3.4__tar.gz → 0.4.1__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.3.4 → git_p4son-0.4.1}/PKG-INFO +79 -5
- {git_p4son-0.3.4 → git_p4son-0.4.1}/README.md +78 -4
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/__init__.py +1 -1
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/changelist_store.py +2 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/cli.py +49 -2
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/common.py +53 -9
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/complete.py +12 -9
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/config.py +21 -16
- git_p4son-0.4.1/git_p4son/depot.py +62 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/git.py +36 -19
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/init.py +50 -6
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/lib.py +77 -1
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/log.py +32 -4
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/new.py +20 -1
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/perforce.py +75 -8
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/review.py +2 -0
- git_p4son-0.4.1/git_p4son/state.py +38 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/sync.py +44 -50
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/sync_split.py +5 -3
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/update.py +20 -1
- git_p4son-0.4.1/git_p4son/writable.py +155 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son.egg-info/PKG-INFO +79 -5
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son.egg-info/SOURCES.txt +7 -1
- {git_p4son-0.3.4 → git_p4son-0.4.1}/pyproject.toml +1 -1
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_changelist_store.py +7 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_cli.py +27 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_common.py +67 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_complete.py +9 -1
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_config.py +42 -33
- git_p4son-0.4.1/tests/test_depot.py +42 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_git.py +54 -1
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_init.py +94 -0
- git_p4son-0.4.1/tests/test_lib_revert.py +251 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_lib_review.py +10 -20
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_log.py +60 -0
- git_p4son-0.4.1/tests/test_new.py +121 -0
- git_p4son-0.4.1/tests/test_state.py +39 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_sync.py +247 -43
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_sync_split.py +24 -6
- git_p4son-0.4.1/tests/test_update.py +85 -0
- git_p4son-0.4.1/tests/test_writable.py +239 -0
- git_p4son-0.3.4/git_p4son/state.py +0 -56
- git_p4son-0.3.4/tests/test_new.py +0 -56
- git_p4son-0.3.4/tests/test_state.py +0 -64
- {git_p4son-0.3.4 → git_p4son-0.4.1}/LICENSE +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/__main__.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/alias.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/completions/_git-p4son +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/completions/git-p4son.bash +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/completions/git-p4son.ps1 +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/hooks.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son/list_changes.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son.egg-info/dependency_links.txt +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son.egg-info/entry_points.txt +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/git_p4son.egg-info/top_level.txt +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/setup.cfg +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_alias.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_hooks.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_lib_changelist.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_lib_edit.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_list_changes.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_perforce.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.1}/tests/test_review.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-p4son
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
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
|
|
@@ -239,10 +239,13 @@ check. Only files with local changes are made read-only for the duration of the
|
|
|
239
239
|
Perforce would otherwise refuse to overwrite. The same digest check applies to git-ignored files: unchanged ones sync
|
|
240
240
|
normally, and after the sync git-p4son lists the ones Perforce skipped because they were modified locally.
|
|
241
241
|
|
|
242
|
+
**Writable mode** - With [writable mode](#writable-command) on, the files a sync touches are made writable again after
|
|
243
|
+
the sync if git tracks them. The sync itself works as described above, since the workspace is not `allwrite`.
|
|
244
|
+
|
|
242
245
|
## Usage
|
|
243
246
|
|
|
244
|
-
git-p4son provides
|
|
245
|
-
`alias`, and `completion`.
|
|
247
|
+
git-p4son provides ten commands: `init`, `sync`, `sync-split`, `new`, `update`, `review`, `list-changes`,
|
|
248
|
+
`alias`, `writable`, and `completion`.
|
|
246
249
|
|
|
247
250
|
To see help for any command, use `-h`:
|
|
248
251
|
|
|
@@ -274,6 +277,9 @@ The depot root determines which part of the Perforce workspace git-p4son syncs.
|
|
|
274
277
|
workspace or just the directory's subtree where the git root is placed. The selection is saved in
|
|
275
278
|
`.git-p4son/config.toml` and used by all subsequent commands.
|
|
276
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
|
+
|
|
277
283
|
The saved root uses a `$(workspace)` placeholder for the workspace (client) name, e.g.
|
|
278
284
|
`root = "//$(workspace)/Engine"`. git-p4son substitutes the live workspace name each time it runs, so renaming
|
|
279
285
|
the Perforce workspace does not break the config. You can still store a concrete name (e.g. `//my-workspace/Engine`)
|
|
@@ -284,12 +290,15 @@ The `.gitignore` is set up using this priority:
|
|
|
284
290
|
- If `.p4ignore` exists, it is copied to `.gitignore` as a starting point
|
|
285
291
|
- Otherwise, an empty `.gitignore` is created
|
|
286
292
|
|
|
293
|
+
`init` also asks whether to turn on [writable mode](#writable-command), defaulting to the current setting. It only
|
|
294
|
+
saves the answer; run `git p4son writable apply` once your files are committed to update their permissions.
|
|
295
|
+
|
|
287
296
|
### Sync Command
|
|
288
297
|
|
|
289
298
|
Sync local git repository with a Perforce workspace:
|
|
290
299
|
|
|
291
300
|
```sh
|
|
292
|
-
git p4son sync [changelist ...] [--force]
|
|
301
|
+
git p4son sync [changelist ...] [--force] [--dry-run]
|
|
293
302
|
```
|
|
294
303
|
|
|
295
304
|
**Arguments:**
|
|
@@ -300,6 +309,8 @@ git p4son sync [changelist ...] [--force]
|
|
|
300
309
|
|
|
301
310
|
**Options:**
|
|
302
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.
|
|
303
314
|
|
|
304
315
|
**Examples:**
|
|
305
316
|
```sh
|
|
@@ -310,6 +321,7 @@ git p4son sync 123 156 178 # sync each changelist in sequence, one commit each
|
|
|
310
321
|
git p4son sync 123 156 head # sync 123, 156, then the latest changelist
|
|
311
322
|
git p4son sync last-synced
|
|
312
323
|
git p4son sync 12345 --force
|
|
324
|
+
git p4son sync --dry-run # show the changelists a sync to latest would visit
|
|
313
325
|
```
|
|
314
326
|
|
|
315
327
|
#### pre-sync hook
|
|
@@ -379,6 +391,10 @@ than the last synced changelist (use `git p4son sync --force` for that).
|
|
|
379
391
|
Create a new Perforce changelist and add changed files to it. Description will contain an enumerated list of git commits since the base branch.
|
|
380
392
|
Optionally creates a Swarm review.
|
|
381
393
|
|
|
394
|
+
Opening files requires a clean git workspace, untracked files included; `new` refuses to run otherwise. The check
|
|
395
|
+
is skipped with `--no-edit`. After opening files, unchanged ones are reverted (see
|
|
396
|
+
[Keeping the changelist in step with git](#keeping-the-changelist-in-step-with-git)).
|
|
397
|
+
|
|
382
398
|
```sh
|
|
383
399
|
git p4son new -m <message> [alias] [--base-branch BASE_BRANCH] [--force] [--dry-run] [--no-edit] [--no-alias]
|
|
384
400
|
[--shelve] [--review]
|
|
@@ -414,7 +430,10 @@ Update an existing Perforce changelist description. Commits since the base branc
|
|
|
414
430
|
entries in the enumerated commit list (matched by subject) and new ones are appended; entries outside the
|
|
415
431
|
range are kept and the list is renumbered. So `update -b main` rebuilds the whole list without duplicating
|
|
416
432
|
it, while `update -b HEAD~3` only refreshes the last three entries. By default also opens changed files for
|
|
417
|
-
edit
|
|
433
|
+
edit, and reverts files that are no longer part of the git change (see
|
|
434
|
+
[Keeping the changelist in step with git](#keeping-the-changelist-in-step-with-git)). Opening files requires a
|
|
435
|
+
clean git workspace, untracked files included; `update` refuses to run otherwise. The check is skipped with
|
|
436
|
+
`--no-edit`.
|
|
418
437
|
|
|
419
438
|
```sh
|
|
420
439
|
git p4son update [changelist] [--base-branch BASE_BRANCH] [--dry-run] [--no-desc] [--no-edit] [--shelve]
|
|
@@ -440,6 +459,27 @@ git p4son update 12345
|
|
|
440
459
|
git p4son update myalias -b main
|
|
441
460
|
```
|
|
442
461
|
|
|
462
|
+
### Keeping the changelist in step with git
|
|
463
|
+
|
|
464
|
+
`new` and `update` open every file changed since the base branch in the changelist. A file can later stop being
|
|
465
|
+
part of the change, for example when a later commit undoes an edit, or a file is added in one commit and deleted
|
|
466
|
+
in the next. Such files would otherwise stay opened as no-ops, so after opening files both commands revert them:
|
|
467
|
+
|
|
468
|
+
- Files opened for edit that are unchanged compared to the depot are reverted with `p4 revert -a`. Changed files
|
|
469
|
+
stay opened.
|
|
470
|
+
- Files opened for add that are missing from disk are reverted.
|
|
471
|
+
- Files opened for delete that git still has (deleted in one commit, restored in a later one) are reverted and
|
|
472
|
+
restored from git.
|
|
473
|
+
|
|
474
|
+
Only files git-p4son has authority over are touched: files tracked by git, and files opened for add that are
|
|
475
|
+
missing from disk. Anything else in the changelist, such as binaries that only exist in Perforce and were opened
|
|
476
|
+
by hand, is left alone even when unchanged. Files opened with other actions (`move/add`, `integrate`, ...) are
|
|
477
|
+
also left alone. This step is skipped with `--no-edit`.
|
|
478
|
+
|
|
479
|
+
Shelving (`--shelve`, `--review`) replaces the whole shelf with the changelist's opened files using
|
|
480
|
+
`p4 shelve -r -a leaveunchanged`, so the shelf never holds stale files, and unchanged files that are still opened
|
|
481
|
+
are left out of it.
|
|
482
|
+
|
|
443
483
|
### Review Command
|
|
444
484
|
|
|
445
485
|
Automate the interactive rebase workflow for creating Swarm reviews. This command generates a rebase todo with `exec` lines that run `git p4son new --review` on the first commit and `git p4son update --shelve` on each subsequent commit, then opens it in your editor for review before executing.
|
|
@@ -583,6 +623,40 @@ In interactive mode, each alias is displayed in turn with a prompt:
|
|
|
583
623
|
git p4son alias clean
|
|
584
624
|
```
|
|
585
625
|
|
|
626
|
+
### Writable Command
|
|
627
|
+
|
|
628
|
+
Writable mode keeps git-tracked files writable, so you can edit them without running `p4 edit` first. git-p4son
|
|
629
|
+
opens changed files in Perforce itself when you run `new`, `update` or `review`. Git-ignored files, such as content
|
|
630
|
+
and other Perforce-only files, stay read-only and are checked out through Perforce as usual.
|
|
631
|
+
|
|
632
|
+
```sh
|
|
633
|
+
git p4son writable # Show whether writable mode is on
|
|
634
|
+
git p4son writable enable # Turn writable mode on, then apply it
|
|
635
|
+
git p4son writable disable # Turn writable mode off, then apply it
|
|
636
|
+
git p4son writable apply # Make git-tracked files match the current mode
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
`apply` makes every git-tracked file writable when the mode is on. When it's off, it makes them read-only, except
|
|
640
|
+
files opened in Perforce in any changelist. On a workspace with the `allwrite` option, making files read-only is
|
|
641
|
+
skipped, since the client spec wants every file writable. Git-ignored files are never changed.
|
|
642
|
+
|
|
643
|
+
The setting is stored in `.git-p4son/config.toml`:
|
|
644
|
+
|
|
645
|
+
```toml
|
|
646
|
+
[core]
|
|
647
|
+
writable = true
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
With the mode on, `sync` makes the files it synced writable again if git tracks them, since Perforce writes synced
|
|
651
|
+
files read-only. Files made read-only outside git-p4son, for example by submitting from P4V, `p4 revert`, or "Get
|
|
652
|
+
Latest" in P4V, stay read-only until a later sync touches them. Run `git p4son writable apply` to fix them right
|
|
653
|
+
away.
|
|
654
|
+
|
|
655
|
+
This replaces the `post-sync` hook some users run to make every tracked file writable after a sync.
|
|
656
|
+
|
|
657
|
+
Turning the mode off also makes tracked files whose Perforce type carries the `+w` (always writable) modifier
|
|
658
|
+
read-only. The next sync of such a file makes it writable again.
|
|
659
|
+
|
|
586
660
|
### Completion Command
|
|
587
661
|
|
|
588
662
|
Print the path to a shell completion script:
|
|
@@ -216,10 +216,13 @@ check. Only files with local changes are made read-only for the duration of the
|
|
|
216
216
|
Perforce would otherwise refuse to overwrite. The same digest check applies to git-ignored files: unchanged ones sync
|
|
217
217
|
normally, and after the sync git-p4son lists the ones Perforce skipped because they were modified locally.
|
|
218
218
|
|
|
219
|
+
**Writable mode** - With [writable mode](#writable-command) on, the files a sync touches are made writable again after
|
|
220
|
+
the sync if git tracks them. The sync itself works as described above, since the workspace is not `allwrite`.
|
|
221
|
+
|
|
219
222
|
## Usage
|
|
220
223
|
|
|
221
|
-
git-p4son provides
|
|
222
|
-
`alias`, and `completion`.
|
|
224
|
+
git-p4son provides ten commands: `init`, `sync`, `sync-split`, `new`, `update`, `review`, `list-changes`,
|
|
225
|
+
`alias`, `writable`, and `completion`.
|
|
223
226
|
|
|
224
227
|
To see help for any command, use `-h`:
|
|
225
228
|
|
|
@@ -251,6 +254,9 @@ The depot root determines which part of the Perforce workspace git-p4son syncs.
|
|
|
251
254
|
workspace or just the directory's subtree where the git root is placed. The selection is saved in
|
|
252
255
|
`.git-p4son/config.toml` and used by all subsequent commands.
|
|
253
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
|
+
|
|
254
260
|
The saved root uses a `$(workspace)` placeholder for the workspace (client) name, e.g.
|
|
255
261
|
`root = "//$(workspace)/Engine"`. git-p4son substitutes the live workspace name each time it runs, so renaming
|
|
256
262
|
the Perforce workspace does not break the config. You can still store a concrete name (e.g. `//my-workspace/Engine`)
|
|
@@ -261,12 +267,15 @@ The `.gitignore` is set up using this priority:
|
|
|
261
267
|
- If `.p4ignore` exists, it is copied to `.gitignore` as a starting point
|
|
262
268
|
- Otherwise, an empty `.gitignore` is created
|
|
263
269
|
|
|
270
|
+
`init` also asks whether to turn on [writable mode](#writable-command), defaulting to the current setting. It only
|
|
271
|
+
saves the answer; run `git p4son writable apply` once your files are committed to update their permissions.
|
|
272
|
+
|
|
264
273
|
### Sync Command
|
|
265
274
|
|
|
266
275
|
Sync local git repository with a Perforce workspace:
|
|
267
276
|
|
|
268
277
|
```sh
|
|
269
|
-
git p4son sync [changelist ...] [--force]
|
|
278
|
+
git p4son sync [changelist ...] [--force] [--dry-run]
|
|
270
279
|
```
|
|
271
280
|
|
|
272
281
|
**Arguments:**
|
|
@@ -277,6 +286,8 @@ git p4son sync [changelist ...] [--force]
|
|
|
277
286
|
|
|
278
287
|
**Options:**
|
|
279
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.
|
|
280
291
|
|
|
281
292
|
**Examples:**
|
|
282
293
|
```sh
|
|
@@ -287,6 +298,7 @@ git p4son sync 123 156 178 # sync each changelist in sequence, one commit each
|
|
|
287
298
|
git p4son sync 123 156 head # sync 123, 156, then the latest changelist
|
|
288
299
|
git p4son sync last-synced
|
|
289
300
|
git p4son sync 12345 --force
|
|
301
|
+
git p4son sync --dry-run # show the changelists a sync to latest would visit
|
|
290
302
|
```
|
|
291
303
|
|
|
292
304
|
#### pre-sync hook
|
|
@@ -356,6 +368,10 @@ than the last synced changelist (use `git p4son sync --force` for that).
|
|
|
356
368
|
Create a new Perforce changelist and add changed files to it. Description will contain an enumerated list of git commits since the base branch.
|
|
357
369
|
Optionally creates a Swarm review.
|
|
358
370
|
|
|
371
|
+
Opening files requires a clean git workspace, untracked files included; `new` refuses to run otherwise. The check
|
|
372
|
+
is skipped with `--no-edit`. After opening files, unchanged ones are reverted (see
|
|
373
|
+
[Keeping the changelist in step with git](#keeping-the-changelist-in-step-with-git)).
|
|
374
|
+
|
|
359
375
|
```sh
|
|
360
376
|
git p4son new -m <message> [alias] [--base-branch BASE_BRANCH] [--force] [--dry-run] [--no-edit] [--no-alias]
|
|
361
377
|
[--shelve] [--review]
|
|
@@ -391,7 +407,10 @@ Update an existing Perforce changelist description. Commits since the base branc
|
|
|
391
407
|
entries in the enumerated commit list (matched by subject) and new ones are appended; entries outside the
|
|
392
408
|
range are kept and the list is renumbered. So `update -b main` rebuilds the whole list without duplicating
|
|
393
409
|
it, while `update -b HEAD~3` only refreshes the last three entries. By default also opens changed files for
|
|
394
|
-
edit
|
|
410
|
+
edit, and reverts files that are no longer part of the git change (see
|
|
411
|
+
[Keeping the changelist in step with git](#keeping-the-changelist-in-step-with-git)). Opening files requires a
|
|
412
|
+
clean git workspace, untracked files included; `update` refuses to run otherwise. The check is skipped with
|
|
413
|
+
`--no-edit`.
|
|
395
414
|
|
|
396
415
|
```sh
|
|
397
416
|
git p4son update [changelist] [--base-branch BASE_BRANCH] [--dry-run] [--no-desc] [--no-edit] [--shelve]
|
|
@@ -417,6 +436,27 @@ git p4son update 12345
|
|
|
417
436
|
git p4son update myalias -b main
|
|
418
437
|
```
|
|
419
438
|
|
|
439
|
+
### Keeping the changelist in step with git
|
|
440
|
+
|
|
441
|
+
`new` and `update` open every file changed since the base branch in the changelist. A file can later stop being
|
|
442
|
+
part of the change, for example when a later commit undoes an edit, or a file is added in one commit and deleted
|
|
443
|
+
in the next. Such files would otherwise stay opened as no-ops, so after opening files both commands revert them:
|
|
444
|
+
|
|
445
|
+
- Files opened for edit that are unchanged compared to the depot are reverted with `p4 revert -a`. Changed files
|
|
446
|
+
stay opened.
|
|
447
|
+
- Files opened for add that are missing from disk are reverted.
|
|
448
|
+
- Files opened for delete that git still has (deleted in one commit, restored in a later one) are reverted and
|
|
449
|
+
restored from git.
|
|
450
|
+
|
|
451
|
+
Only files git-p4son has authority over are touched: files tracked by git, and files opened for add that are
|
|
452
|
+
missing from disk. Anything else in the changelist, such as binaries that only exist in Perforce and were opened
|
|
453
|
+
by hand, is left alone even when unchanged. Files opened with other actions (`move/add`, `integrate`, ...) are
|
|
454
|
+
also left alone. This step is skipped with `--no-edit`.
|
|
455
|
+
|
|
456
|
+
Shelving (`--shelve`, `--review`) replaces the whole shelf with the changelist's opened files using
|
|
457
|
+
`p4 shelve -r -a leaveunchanged`, so the shelf never holds stale files, and unchanged files that are still opened
|
|
458
|
+
are left out of it.
|
|
459
|
+
|
|
420
460
|
### Review Command
|
|
421
461
|
|
|
422
462
|
Automate the interactive rebase workflow for creating Swarm reviews. This command generates a rebase todo with `exec` lines that run `git p4son new --review` on the first commit and `git p4son update --shelve` on each subsequent commit, then opens it in your editor for review before executing.
|
|
@@ -560,6 +600,40 @@ In interactive mode, each alias is displayed in turn with a prompt:
|
|
|
560
600
|
git p4son alias clean
|
|
561
601
|
```
|
|
562
602
|
|
|
603
|
+
### Writable Command
|
|
604
|
+
|
|
605
|
+
Writable mode keeps git-tracked files writable, so you can edit them without running `p4 edit` first. git-p4son
|
|
606
|
+
opens changed files in Perforce itself when you run `new`, `update` or `review`. Git-ignored files, such as content
|
|
607
|
+
and other Perforce-only files, stay read-only and are checked out through Perforce as usual.
|
|
608
|
+
|
|
609
|
+
```sh
|
|
610
|
+
git p4son writable # Show whether writable mode is on
|
|
611
|
+
git p4son writable enable # Turn writable mode on, then apply it
|
|
612
|
+
git p4son writable disable # Turn writable mode off, then apply it
|
|
613
|
+
git p4son writable apply # Make git-tracked files match the current mode
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
`apply` makes every git-tracked file writable when the mode is on. When it's off, it makes them read-only, except
|
|
617
|
+
files opened in Perforce in any changelist. On a workspace with the `allwrite` option, making files read-only is
|
|
618
|
+
skipped, since the client spec wants every file writable. Git-ignored files are never changed.
|
|
619
|
+
|
|
620
|
+
The setting is stored in `.git-p4son/config.toml`:
|
|
621
|
+
|
|
622
|
+
```toml
|
|
623
|
+
[core]
|
|
624
|
+
writable = true
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
With the mode on, `sync` makes the files it synced writable again if git tracks them, since Perforce writes synced
|
|
628
|
+
files read-only. Files made read-only outside git-p4son, for example by submitting from P4V, `p4 revert`, or "Get
|
|
629
|
+
Latest" in P4V, stay read-only until a later sync touches them. Run `git p4son writable apply` to fix them right
|
|
630
|
+
away.
|
|
631
|
+
|
|
632
|
+
This replaces the `post-sync` hook some users run to make every tracked file writable after a sync.
|
|
633
|
+
|
|
634
|
+
Turning the mode off also makes tracked files whose Perforce type carries the `+w` (always writable) modifier
|
|
635
|
+
read-only. The next sync of such a file makes it writable again.
|
|
636
|
+
|
|
563
637
|
### Completion Command
|
|
564
638
|
|
|
565
639
|
Print the path to a shell completion script:
|
|
@@ -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)
|
|
@@ -21,6 +21,7 @@ from .common import CommandError, RunError, branch_to_alias
|
|
|
21
21
|
from .git import get_current_branch, get_head_subject, get_workspace_dir
|
|
22
22
|
from .log import log
|
|
23
23
|
from .complete import run_complete
|
|
24
|
+
from .writable import writable_command
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
def create_parser() -> argparse.ArgumentParser:
|
|
@@ -37,6 +38,7 @@ Examples:
|
|
|
37
38
|
git-p4son sync 123 156 178 # Sync each changelist in sequence, one commit each
|
|
38
39
|
git-p4son sync 123 156 head # Sync 123, 156, then the latest changelist
|
|
39
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
|
|
40
42
|
git-p4son sync-split # Sync to latest, your own changelists split out
|
|
41
43
|
git-p4son sync-split 12345 # Same, but stop at changelist 12345
|
|
42
44
|
git-p4son sync-split -u alice -u bob # Split out alice's and bob's changelists
|
|
@@ -46,6 +48,9 @@ Examples:
|
|
|
46
48
|
git-p4son update --shelve # Update changelist for current branch and re-shelve
|
|
47
49
|
git-p4son update 12345 # Update changelist 12345
|
|
48
50
|
git-p4son list-changes --base-branch main # List commit subjects since main branch
|
|
51
|
+
git-p4son writable # Show whether writable mode is on
|
|
52
|
+
git-p4son writable enable # Keep git-tracked files writable
|
|
53
|
+
git-p4son writable apply # Make tracked files match the writable mode
|
|
49
54
|
"""
|
|
50
55
|
)
|
|
51
56
|
|
|
@@ -99,6 +104,11 @@ Examples:
|
|
|
99
104
|
action='store_true',
|
|
100
105
|
help='Allow syncing to changelists older than the current one.'
|
|
101
106
|
)
|
|
107
|
+
sync_parser.add_argument(
|
|
108
|
+
'-n', '--dry-run',
|
|
109
|
+
action='store_true',
|
|
110
|
+
help='Print the resolved sync sequence without syncing'
|
|
111
|
+
)
|
|
102
112
|
|
|
103
113
|
# Sync-split subcommand
|
|
104
114
|
sync_split_parser = subparsers.add_parser(
|
|
@@ -352,6 +362,40 @@ Examples:
|
|
|
352
362
|
help='Print the generated rebase todo without executing'
|
|
353
363
|
)
|
|
354
364
|
|
|
365
|
+
# Writable subcommand
|
|
366
|
+
writable_parser = subparsers.add_parser(
|
|
367
|
+
'writable',
|
|
368
|
+
help='Show or apply writable mode for git-tracked files',
|
|
369
|
+
description='Writable mode keeps git-tracked files writable so they '
|
|
370
|
+
'can be edited without a manual p4 edit. Git-ignored files are left '
|
|
371
|
+
'to Perforce. Without an action, shows whether the mode is on.'
|
|
372
|
+
)
|
|
373
|
+
writable_subparsers = writable_parser.add_subparsers(
|
|
374
|
+
dest='writable_action',
|
|
375
|
+
help='Available writable actions',
|
|
376
|
+
metavar='ACTION'
|
|
377
|
+
)
|
|
378
|
+
writable_subparsers.add_parser(
|
|
379
|
+
'enable',
|
|
380
|
+
help='Turn writable mode on and make git-tracked files writable',
|
|
381
|
+
description='Turn writable mode on, then make every git-tracked file '
|
|
382
|
+
'writable. Sync keeps the files it syncs writable from then on.'
|
|
383
|
+
)
|
|
384
|
+
writable_subparsers.add_parser(
|
|
385
|
+
'disable',
|
|
386
|
+
help='Turn writable mode off and make git-tracked files read-only',
|
|
387
|
+
description='Turn writable mode off, then make git-tracked files '
|
|
388
|
+
'read-only, except files opened in Perforce.'
|
|
389
|
+
)
|
|
390
|
+
writable_subparsers.add_parser(
|
|
391
|
+
'apply',
|
|
392
|
+
help='Make git-tracked files match the writable mode',
|
|
393
|
+
description='With writable mode on, make every git-tracked file '
|
|
394
|
+
'writable. With it off, make them read-only, except files opened in '
|
|
395
|
+
'Perforce. Use it to fix files made read-only outside git-p4son, '
|
|
396
|
+
'for example by a submit.'
|
|
397
|
+
)
|
|
398
|
+
|
|
355
399
|
# Completion subcommand (prints shell completion script path)
|
|
356
400
|
completion_parser = subparsers.add_parser(
|
|
357
401
|
'completion',
|
|
@@ -370,10 +414,11 @@ Examples:
|
|
|
370
414
|
help='Print the directory instead of the full file path'
|
|
371
415
|
)
|
|
372
416
|
|
|
373
|
-
# Hidden _sequence-editor subcommand (used internally by review)
|
|
417
|
+
# Hidden _sequence-editor subcommand (used internally by review). It is
|
|
418
|
+
# given no help: argparse lists a subcommand with help=SUPPRESS anyway,
|
|
419
|
+
# as "==SUPPRESS==", but leaves one without help out of the list.
|
|
374
420
|
seq_editor_parser = subparsers.add_parser(
|
|
375
421
|
'_sequence-editor',
|
|
376
|
-
help=argparse.SUPPRESS,
|
|
377
422
|
)
|
|
378
423
|
seq_editor_parser.add_argument(
|
|
379
424
|
'filename',
|
|
@@ -477,6 +522,8 @@ def run_command(args: argparse.Namespace) -> int:
|
|
|
477
522
|
return alias_command(args)
|
|
478
523
|
elif args.command == 'review':
|
|
479
524
|
return review_command(args)
|
|
525
|
+
elif args.command == 'writable':
|
|
526
|
+
return writable_command(args)
|
|
480
527
|
elif args.command == '_sequence-editor':
|
|
481
528
|
return sequence_editor_command(args)
|
|
482
529
|
else:
|
|
@@ -11,6 +11,7 @@ import subprocess
|
|
|
11
11
|
import sys
|
|
12
12
|
import threading
|
|
13
13
|
import time
|
|
14
|
+
from contextlib import AbstractContextManager, nullcontext
|
|
14
15
|
from timeit import default_timer as timer
|
|
15
16
|
from datetime import timedelta
|
|
16
17
|
from typing import IO, Callable
|
|
@@ -127,6 +128,12 @@ class RunResult:
|
|
|
127
128
|
self.elapsed: timedelta | None = elapsed
|
|
128
129
|
|
|
129
130
|
|
|
131
|
+
# Commands that take a list of files pass it after a `--` separator. With
|
|
132
|
+
# thousands of files synced, printing every one buries the output, so a
|
|
133
|
+
# longer list is logged as a count. Verbose mode still prints it in full.
|
|
134
|
+
_MAX_LOGGED_PATHS = 3
|
|
135
|
+
|
|
136
|
+
|
|
130
137
|
def join_command_line(command: list[str]) -> str:
|
|
131
138
|
command_line = ''
|
|
132
139
|
for c in command:
|
|
@@ -137,6 +144,30 @@ def join_command_line(command: list[str]) -> str:
|
|
|
137
144
|
return command_line
|
|
138
145
|
|
|
139
146
|
|
|
147
|
+
def _command_line_for_log(command: list[str]) -> str:
|
|
148
|
+
"""The command line as printed, with a long path list summarized."""
|
|
149
|
+
if log.verbose_mode or '--' not in command:
|
|
150
|
+
return join_command_line(command)
|
|
151
|
+
separator = command.index('--')
|
|
152
|
+
paths = command[separator + 1:]
|
|
153
|
+
if len(paths) <= _MAX_LOGGED_PATHS:
|
|
154
|
+
return join_command_line(command)
|
|
155
|
+
return join_command_line(command[:separator + 1]) + f' <{len(paths)} paths>'
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def batched_command_log(prefix: list[str], path_count: int,
|
|
159
|
+
batch_count: int) -> AbstractContextManager[None]:
|
|
160
|
+
"""Log a command run over a path list in several batches as one line.
|
|
161
|
+
|
|
162
|
+
prefix is the command up to and including its `--` separator. A single
|
|
163
|
+
batch is logged as the command itself."""
|
|
164
|
+
if batch_count <= 1:
|
|
165
|
+
return nullcontext()
|
|
166
|
+
return log.command_batch(
|
|
167
|
+
join_command_line(prefix)
|
|
168
|
+
+ f' <{path_count} paths in {batch_count} batches>')
|
|
169
|
+
|
|
170
|
+
|
|
140
171
|
def run(command: list[str], cwd: str = '.', dry_run: bool = False,
|
|
141
172
|
input: str | None = None,
|
|
142
173
|
env: dict[str, str] | None = None,
|
|
@@ -159,7 +190,7 @@ def run(command: list[str], cwd: str = '.', dry_run: bool = False,
|
|
|
159
190
|
RunResult object with returncode, stdout, and stderr
|
|
160
191
|
"""
|
|
161
192
|
use_spinner = input is None and not dry_run
|
|
162
|
-
log.command(
|
|
193
|
+
log.command(_command_line_for_log(command),
|
|
163
194
|
truncate_for_spinner=use_spinner)
|
|
164
195
|
|
|
165
196
|
if dry_run:
|
|
@@ -225,6 +256,16 @@ def enqueue_lines(stream: IO[str], output_queue: queue.Queue[str]) -> None:
|
|
|
225
256
|
output_queue.put(line.rstrip())
|
|
226
257
|
|
|
227
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
|
+
|
|
228
269
|
def run_with_output(command: list[str], cwd: str = '.',
|
|
229
270
|
on_output: Callable[..., None] | None = None,
|
|
230
271
|
env: dict[str, str] | None = None) -> RunResult:
|
|
@@ -237,12 +278,15 @@ def run_with_output(command: list[str], cwd: str = '.',
|
|
|
237
278
|
on_output: Callback function for processing output lines
|
|
238
279
|
If set the funciton will be called with each
|
|
239
280
|
line and stream (stdout/stderr) as they are written.
|
|
281
|
+
An exception raised from it terminates the command
|
|
282
|
+
and propagates.
|
|
240
283
|
env: Optional environment variables to add or override
|
|
241
284
|
|
|
242
285
|
Returns:
|
|
243
286
|
RunResult object with returncode, stdout, and stderr
|
|
244
287
|
"""
|
|
245
|
-
log.command(
|
|
288
|
+
log.command(_command_line_for_log(command),
|
|
289
|
+
truncate_for_spinner=True)
|
|
246
290
|
log.start_spinner()
|
|
247
291
|
|
|
248
292
|
start_timestamp = timer()
|
|
@@ -320,14 +364,14 @@ def run_with_output(command: list[str], cwd: str = '.',
|
|
|
320
364
|
except KeyboardInterrupt:
|
|
321
365
|
log.stop_spinner()
|
|
322
366
|
log.error("CTRL-C pressed, terminate subprocess")
|
|
323
|
-
process
|
|
324
|
-
try:
|
|
325
|
-
process.wait(timeout=5)
|
|
326
|
-
except subprocess.TimeoutExpired:
|
|
327
|
-
log.error(
|
|
328
|
-
"Subprocess did not terminate in time. Forcing kill...")
|
|
329
|
-
process.kill()
|
|
367
|
+
_terminate(process)
|
|
330
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
|
|
331
375
|
|
|
332
376
|
log.stop_spinner()
|
|
333
377
|
|
|
@@ -14,6 +14,8 @@ from .git import get_current_branch, get_workspace_dir
|
|
|
14
14
|
from .log import log
|
|
15
15
|
|
|
16
16
|
_HIDDEN_COMMANDS = frozenset({'complete', 'completion', '_sequence-editor'})
|
|
17
|
+
# Commands whose first positional is a nested action (alias list, ...).
|
|
18
|
+
_COMMANDS_WITH_ACTIONS = frozenset({'alias', 'writable'})
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
def _get_subparsers_action(parser):
|
|
@@ -125,15 +127,16 @@ def _complete_positional(command, subcommand, positional_count,
|
|
|
125
127
|
branch_candidates = _get_branch_candidates(prefix, workspace_dir)
|
|
126
128
|
return branch_candidates + _filter(aliases, prefix)
|
|
127
129
|
|
|
128
|
-
if command
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
130
|
+
if (command in _COMMANDS_WITH_ACTIONS and subcommand is None
|
|
131
|
+
and positional_count == 0):
|
|
132
|
+
nested = _get_subparsers_action(command_parser)
|
|
133
|
+
if nested and hasattr(nested, '_choices_actions'):
|
|
134
|
+
candidates = [(ca.dest, ca.help or '')
|
|
135
|
+
for ca in nested._choices_actions]
|
|
136
|
+
return _filter(candidates, prefix)
|
|
137
|
+
return []
|
|
136
138
|
|
|
139
|
+
if command == 'alias':
|
|
137
140
|
if subcommand == 'delete' and positional_count == 0:
|
|
138
141
|
branch_candidates = _get_branch_candidates(prefix, workspace_dir)
|
|
139
142
|
return branch_candidates + _filter(aliases, prefix)
|
|
@@ -193,7 +196,7 @@ def _complete(parser, words, workspace_dir=None):
|
|
|
193
196
|
current_parser = command_parser
|
|
194
197
|
continue
|
|
195
198
|
|
|
196
|
-
if command
|
|
199
|
+
if command in _COMMANDS_WITH_ACTIONS and subcommand is None:
|
|
197
200
|
nested = _get_subparsers_action(command_parser)
|
|
198
201
|
if nested and word in nested.choices:
|
|
199
202
|
subcommand = word
|
|
@@ -12,11 +12,26 @@ from . import CONFIG_DIR
|
|
|
12
12
|
|
|
13
13
|
_BARE_KEY_RE = re.compile(r'^[A-Za-z0-9_-]+$')
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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)
|
|
20
35
|
|
|
21
36
|
|
|
22
37
|
def config_path(workspace_dir: str) -> str:
|
|
@@ -55,6 +70,7 @@ def save_config(workspace_dir: str, config: dict) -> None:
|
|
|
55
70
|
merged = load_config(workspace_dir)
|
|
56
71
|
for section, values in config.items():
|
|
57
72
|
merged.setdefault(section, {}).update(values)
|
|
73
|
+
ensure_config_dir(workspace_dir)
|
|
58
74
|
write_toml(config_path(workspace_dir), merged)
|
|
59
75
|
|
|
60
76
|
|
|
@@ -94,14 +110,3 @@ def _write_table(f, name: str, table: dict) -> None:
|
|
|
94
110
|
f.write('\n')
|
|
95
111
|
for key, subtable in subtables.items():
|
|
96
112
|
_write_table(f, f'{name}.{_format_key(key)}', subtable)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def get_depot_root(workspace_dir: str) -> str | None:
|
|
100
|
-
"""Get the depot root from config, or None if not configured."""
|
|
101
|
-
config = load_config(workspace_dir)
|
|
102
|
-
return config.get('depot', {}).get('root')
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def expand_depot_root(depot_root: str, workspace_name: str) -> str:
|
|
106
|
-
"""Substitute the live workspace name for the $(workspace) placeholder."""
|
|
107
|
-
return depot_root.replace(WORKSPACE_PLACEHOLDER, workspace_name)
|