git-p4son 0.3.4__tar.gz → 0.4.0__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.0}/PKG-INFO +72 -4
- {git_p4son-0.3.4 → git_p4son-0.4.0}/README.md +71 -3
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/__init__.py +1 -1
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/cli.py +40 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/common.py +34 -2
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/complete.py +12 -9
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/config.py +0 -17
- git_p4son-0.4.0/git_p4son/depot.py +62 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/git.py +36 -19
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/init.py +50 -6
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/lib.py +77 -1
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/log.py +32 -4
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/new.py +20 -1
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/perforce.py +57 -8
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/sync.py +33 -50
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/sync_split.py +5 -3
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/update.py +20 -1
- git_p4son-0.4.0/git_p4son/writable.py +155 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son.egg-info/PKG-INFO +72 -4
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son.egg-info/SOURCES.txt +7 -1
- {git_p4son-0.3.4 → git_p4son-0.4.0}/pyproject.toml +1 -1
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_cli.py +16 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_common.py +48 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_complete.py +7 -1
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_config.py +0 -33
- git_p4son-0.4.0/tests/test_depot.py +42 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_git.py +54 -1
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_init.py +94 -0
- git_p4son-0.4.0/tests/test_lib_revert.py +251 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_lib_review.py +10 -20
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_log.py +60 -0
- git_p4son-0.4.0/tests/test_new.py +121 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_sync.py +163 -43
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_sync_split.py +24 -6
- git_p4son-0.4.0/tests/test_update.py +85 -0
- git_p4son-0.4.0/tests/test_writable.py +239 -0
- git_p4son-0.3.4/tests/test_new.py +0 -56
- {git_p4son-0.3.4 → git_p4son-0.4.0}/LICENSE +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/__main__.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/alias.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/changelist_store.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/completions/_git-p4son +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/completions/git-p4son.bash +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/completions/git-p4son.ps1 +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/hooks.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/list_changes.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/review.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son/state.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son.egg-info/dependency_links.txt +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son.egg-info/entry_points.txt +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/git_p4son.egg-info/top_level.txt +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/setup.cfg +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_alias.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_changelist_store.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_hooks.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_lib_changelist.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_lib_edit.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_list_changes.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_perforce.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_review.py +0 -0
- {git_p4son-0.3.4 → git_p4son-0.4.0}/tests/test_state.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-p4son
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
|
@@ -284,6 +287,9 @@ The `.gitignore` is set up using this priority:
|
|
|
284
287
|
- If `.p4ignore` exists, it is copied to `.gitignore` as a starting point
|
|
285
288
|
- Otherwise, an empty `.gitignore` is created
|
|
286
289
|
|
|
290
|
+
`init` also asks whether to turn on [writable mode](#writable-command), defaulting to the current setting. It only
|
|
291
|
+
saves the answer; run `git p4son writable apply` once your files are committed to update their permissions.
|
|
292
|
+
|
|
287
293
|
### Sync Command
|
|
288
294
|
|
|
289
295
|
Sync local git repository with a Perforce workspace:
|
|
@@ -379,6 +385,10 @@ than the last synced changelist (use `git p4son sync --force` for that).
|
|
|
379
385
|
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
386
|
Optionally creates a Swarm review.
|
|
381
387
|
|
|
388
|
+
Opening files requires a clean git workspace, untracked files included; `new` refuses to run otherwise. The check
|
|
389
|
+
is skipped with `--no-edit`. After opening files, unchanged ones are reverted (see
|
|
390
|
+
[Keeping the changelist in step with git](#keeping-the-changelist-in-step-with-git)).
|
|
391
|
+
|
|
382
392
|
```sh
|
|
383
393
|
git p4son new -m <message> [alias] [--base-branch BASE_BRANCH] [--force] [--dry-run] [--no-edit] [--no-alias]
|
|
384
394
|
[--shelve] [--review]
|
|
@@ -414,7 +424,10 @@ Update an existing Perforce changelist description. Commits since the base branc
|
|
|
414
424
|
entries in the enumerated commit list (matched by subject) and new ones are appended; entries outside the
|
|
415
425
|
range are kept and the list is renumbered. So `update -b main` rebuilds the whole list without duplicating
|
|
416
426
|
it, while `update -b HEAD~3` only refreshes the last three entries. By default also opens changed files for
|
|
417
|
-
edit
|
|
427
|
+
edit, and reverts files that are no longer part of the git change (see
|
|
428
|
+
[Keeping the changelist in step with git](#keeping-the-changelist-in-step-with-git)). Opening files requires a
|
|
429
|
+
clean git workspace, untracked files included; `update` refuses to run otherwise. The check is skipped with
|
|
430
|
+
`--no-edit`.
|
|
418
431
|
|
|
419
432
|
```sh
|
|
420
433
|
git p4son update [changelist] [--base-branch BASE_BRANCH] [--dry-run] [--no-desc] [--no-edit] [--shelve]
|
|
@@ -440,6 +453,27 @@ git p4son update 12345
|
|
|
440
453
|
git p4son update myalias -b main
|
|
441
454
|
```
|
|
442
455
|
|
|
456
|
+
### Keeping the changelist in step with git
|
|
457
|
+
|
|
458
|
+
`new` and `update` open every file changed since the base branch in the changelist. A file can later stop being
|
|
459
|
+
part of the change, for example when a later commit undoes an edit, or a file is added in one commit and deleted
|
|
460
|
+
in the next. Such files would otherwise stay opened as no-ops, so after opening files both commands revert them:
|
|
461
|
+
|
|
462
|
+
- Files opened for edit that are unchanged compared to the depot are reverted with `p4 revert -a`. Changed files
|
|
463
|
+
stay opened.
|
|
464
|
+
- Files opened for add that are missing from disk are reverted.
|
|
465
|
+
- Files opened for delete that git still has (deleted in one commit, restored in a later one) are reverted and
|
|
466
|
+
restored from git.
|
|
467
|
+
|
|
468
|
+
Only files git-p4son has authority over are touched: files tracked by git, and files opened for add that are
|
|
469
|
+
missing from disk. Anything else in the changelist, such as binaries that only exist in Perforce and were opened
|
|
470
|
+
by hand, is left alone even when unchanged. Files opened with other actions (`move/add`, `integrate`, ...) are
|
|
471
|
+
also left alone. This step is skipped with `--no-edit`.
|
|
472
|
+
|
|
473
|
+
Shelving (`--shelve`, `--review`) replaces the whole shelf with the changelist's opened files using
|
|
474
|
+
`p4 shelve -r -a leaveunchanged`, so the shelf never holds stale files, and unchanged files that are still opened
|
|
475
|
+
are left out of it.
|
|
476
|
+
|
|
443
477
|
### Review Command
|
|
444
478
|
|
|
445
479
|
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 +617,40 @@ In interactive mode, each alias is displayed in turn with a prompt:
|
|
|
583
617
|
git p4son alias clean
|
|
584
618
|
```
|
|
585
619
|
|
|
620
|
+
### Writable Command
|
|
621
|
+
|
|
622
|
+
Writable mode keeps git-tracked files writable, so you can edit them without running `p4 edit` first. git-p4son
|
|
623
|
+
opens changed files in Perforce itself when you run `new`, `update` or `review`. Git-ignored files, such as content
|
|
624
|
+
and other Perforce-only files, stay read-only and are checked out through Perforce as usual.
|
|
625
|
+
|
|
626
|
+
```sh
|
|
627
|
+
git p4son writable # Show whether writable mode is on
|
|
628
|
+
git p4son writable enable # Turn writable mode on, then apply it
|
|
629
|
+
git p4son writable disable # Turn writable mode off, then apply it
|
|
630
|
+
git p4son writable apply # Make git-tracked files match the current mode
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
`apply` makes every git-tracked file writable when the mode is on. When it's off, it makes them read-only, except
|
|
634
|
+
files opened in Perforce in any changelist. On a workspace with the `allwrite` option, making files read-only is
|
|
635
|
+
skipped, since the client spec wants every file writable. Git-ignored files are never changed.
|
|
636
|
+
|
|
637
|
+
The setting is stored in `.git-p4son/config.toml`:
|
|
638
|
+
|
|
639
|
+
```toml
|
|
640
|
+
[core]
|
|
641
|
+
writable = true
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
With the mode on, `sync` makes the files it synced writable again if git tracks them, since Perforce writes synced
|
|
645
|
+
files read-only. Files made read-only outside git-p4son, for example by submitting from P4V, `p4 revert`, or "Get
|
|
646
|
+
Latest" in P4V, stay read-only until a later sync touches them. Run `git p4son writable apply` to fix them right
|
|
647
|
+
away.
|
|
648
|
+
|
|
649
|
+
This replaces the `post-sync` hook some users run to make every tracked file writable after a sync.
|
|
650
|
+
|
|
651
|
+
Turning the mode off also makes tracked files whose Perforce type carries the `+w` (always writable) modifier
|
|
652
|
+
read-only. The next sync of such a file makes it writable again.
|
|
653
|
+
|
|
586
654
|
### Completion Command
|
|
587
655
|
|
|
588
656
|
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
|
|
|
@@ -261,6 +264,9 @@ The `.gitignore` is set up using this priority:
|
|
|
261
264
|
- If `.p4ignore` exists, it is copied to `.gitignore` as a starting point
|
|
262
265
|
- Otherwise, an empty `.gitignore` is created
|
|
263
266
|
|
|
267
|
+
`init` also asks whether to turn on [writable mode](#writable-command), defaulting to the current setting. It only
|
|
268
|
+
saves the answer; run `git p4son writable apply` once your files are committed to update their permissions.
|
|
269
|
+
|
|
264
270
|
### Sync Command
|
|
265
271
|
|
|
266
272
|
Sync local git repository with a Perforce workspace:
|
|
@@ -356,6 +362,10 @@ than the last synced changelist (use `git p4son sync --force` for that).
|
|
|
356
362
|
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
363
|
Optionally creates a Swarm review.
|
|
358
364
|
|
|
365
|
+
Opening files requires a clean git workspace, untracked files included; `new` refuses to run otherwise. The check
|
|
366
|
+
is skipped with `--no-edit`. After opening files, unchanged ones are reverted (see
|
|
367
|
+
[Keeping the changelist in step with git](#keeping-the-changelist-in-step-with-git)).
|
|
368
|
+
|
|
359
369
|
```sh
|
|
360
370
|
git p4son new -m <message> [alias] [--base-branch BASE_BRANCH] [--force] [--dry-run] [--no-edit] [--no-alias]
|
|
361
371
|
[--shelve] [--review]
|
|
@@ -391,7 +401,10 @@ Update an existing Perforce changelist description. Commits since the base branc
|
|
|
391
401
|
entries in the enumerated commit list (matched by subject) and new ones are appended; entries outside the
|
|
392
402
|
range are kept and the list is renumbered. So `update -b main` rebuilds the whole list without duplicating
|
|
393
403
|
it, while `update -b HEAD~3` only refreshes the last three entries. By default also opens changed files for
|
|
394
|
-
edit
|
|
404
|
+
edit, and reverts files that are no longer part of the git change (see
|
|
405
|
+
[Keeping the changelist in step with git](#keeping-the-changelist-in-step-with-git)). Opening files requires a
|
|
406
|
+
clean git workspace, untracked files included; `update` refuses to run otherwise. The check is skipped with
|
|
407
|
+
`--no-edit`.
|
|
395
408
|
|
|
396
409
|
```sh
|
|
397
410
|
git p4son update [changelist] [--base-branch BASE_BRANCH] [--dry-run] [--no-desc] [--no-edit] [--shelve]
|
|
@@ -417,6 +430,27 @@ git p4son update 12345
|
|
|
417
430
|
git p4son update myalias -b main
|
|
418
431
|
```
|
|
419
432
|
|
|
433
|
+
### Keeping the changelist in step with git
|
|
434
|
+
|
|
435
|
+
`new` and `update` open every file changed since the base branch in the changelist. A file can later stop being
|
|
436
|
+
part of the change, for example when a later commit undoes an edit, or a file is added in one commit and deleted
|
|
437
|
+
in the next. Such files would otherwise stay opened as no-ops, so after opening files both commands revert them:
|
|
438
|
+
|
|
439
|
+
- Files opened for edit that are unchanged compared to the depot are reverted with `p4 revert -a`. Changed files
|
|
440
|
+
stay opened.
|
|
441
|
+
- Files opened for add that are missing from disk are reverted.
|
|
442
|
+
- Files opened for delete that git still has (deleted in one commit, restored in a later one) are reverted and
|
|
443
|
+
restored from git.
|
|
444
|
+
|
|
445
|
+
Only files git-p4son has authority over are touched: files tracked by git, and files opened for add that are
|
|
446
|
+
missing from disk. Anything else in the changelist, such as binaries that only exist in Perforce and were opened
|
|
447
|
+
by hand, is left alone even when unchanged. Files opened with other actions (`move/add`, `integrate`, ...) are
|
|
448
|
+
also left alone. This step is skipped with `--no-edit`.
|
|
449
|
+
|
|
450
|
+
Shelving (`--shelve`, `--review`) replaces the whole shelf with the changelist's opened files using
|
|
451
|
+
`p4 shelve -r -a leaveunchanged`, so the shelf never holds stale files, and unchanged files that are still opened
|
|
452
|
+
are left out of it.
|
|
453
|
+
|
|
420
454
|
### Review Command
|
|
421
455
|
|
|
422
456
|
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 +594,40 @@ In interactive mode, each alias is displayed in turn with a prompt:
|
|
|
560
594
|
git p4son alias clean
|
|
561
595
|
```
|
|
562
596
|
|
|
597
|
+
### Writable Command
|
|
598
|
+
|
|
599
|
+
Writable mode keeps git-tracked files writable, so you can edit them without running `p4 edit` first. git-p4son
|
|
600
|
+
opens changed files in Perforce itself when you run `new`, `update` or `review`. Git-ignored files, such as content
|
|
601
|
+
and other Perforce-only files, stay read-only and are checked out through Perforce as usual.
|
|
602
|
+
|
|
603
|
+
```sh
|
|
604
|
+
git p4son writable # Show whether writable mode is on
|
|
605
|
+
git p4son writable enable # Turn writable mode on, then apply it
|
|
606
|
+
git p4son writable disable # Turn writable mode off, then apply it
|
|
607
|
+
git p4son writable apply # Make git-tracked files match the current mode
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
`apply` makes every git-tracked file writable when the mode is on. When it's off, it makes them read-only, except
|
|
611
|
+
files opened in Perforce in any changelist. On a workspace with the `allwrite` option, making files read-only is
|
|
612
|
+
skipped, since the client spec wants every file writable. Git-ignored files are never changed.
|
|
613
|
+
|
|
614
|
+
The setting is stored in `.git-p4son/config.toml`:
|
|
615
|
+
|
|
616
|
+
```toml
|
|
617
|
+
[core]
|
|
618
|
+
writable = true
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
With the mode on, `sync` makes the files it synced writable again if git tracks them, since Perforce writes synced
|
|
622
|
+
files read-only. Files made read-only outside git-p4son, for example by submitting from P4V, `p4 revert`, or "Get
|
|
623
|
+
Latest" in P4V, stay read-only until a later sync touches them. Run `git p4son writable apply` to fix them right
|
|
624
|
+
away.
|
|
625
|
+
|
|
626
|
+
This replaces the `post-sync` hook some users run to make every tracked file writable after a sync.
|
|
627
|
+
|
|
628
|
+
Turning the mode off also makes tracked files whose Perforce type carries the `+w` (always writable) modifier
|
|
629
|
+
read-only. The next sync of such a file makes it writable again.
|
|
630
|
+
|
|
563
631
|
### Completion Command
|
|
564
632
|
|
|
565
633
|
Print the path to a shell completion script:
|
|
@@ -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:
|
|
@@ -46,6 +47,9 @@ Examples:
|
|
|
46
47
|
git-p4son update --shelve # Update changelist for current branch and re-shelve
|
|
47
48
|
git-p4son update 12345 # Update changelist 12345
|
|
48
49
|
git-p4son list-changes --base-branch main # List commit subjects since main branch
|
|
50
|
+
git-p4son writable # Show whether writable mode is on
|
|
51
|
+
git-p4son writable enable # Keep git-tracked files writable
|
|
52
|
+
git-p4son writable apply # Make tracked files match the writable mode
|
|
49
53
|
"""
|
|
50
54
|
)
|
|
51
55
|
|
|
@@ -352,6 +356,40 @@ Examples:
|
|
|
352
356
|
help='Print the generated rebase todo without executing'
|
|
353
357
|
)
|
|
354
358
|
|
|
359
|
+
# Writable subcommand
|
|
360
|
+
writable_parser = subparsers.add_parser(
|
|
361
|
+
'writable',
|
|
362
|
+
help='Show or apply writable mode for git-tracked files',
|
|
363
|
+
description='Writable mode keeps git-tracked files writable so they '
|
|
364
|
+
'can be edited without a manual p4 edit. Git-ignored files are left '
|
|
365
|
+
'to Perforce. Without an action, shows whether the mode is on.'
|
|
366
|
+
)
|
|
367
|
+
writable_subparsers = writable_parser.add_subparsers(
|
|
368
|
+
dest='writable_action',
|
|
369
|
+
help='Available writable actions',
|
|
370
|
+
metavar='ACTION'
|
|
371
|
+
)
|
|
372
|
+
writable_subparsers.add_parser(
|
|
373
|
+
'enable',
|
|
374
|
+
help='Turn writable mode on and make git-tracked files writable',
|
|
375
|
+
description='Turn writable mode on, then make every git-tracked file '
|
|
376
|
+
'writable. Sync keeps the files it syncs writable from then on.'
|
|
377
|
+
)
|
|
378
|
+
writable_subparsers.add_parser(
|
|
379
|
+
'disable',
|
|
380
|
+
help='Turn writable mode off and make git-tracked files read-only',
|
|
381
|
+
description='Turn writable mode off, then make git-tracked files '
|
|
382
|
+
'read-only, except files opened in Perforce.'
|
|
383
|
+
)
|
|
384
|
+
writable_subparsers.add_parser(
|
|
385
|
+
'apply',
|
|
386
|
+
help='Make git-tracked files match the writable mode',
|
|
387
|
+
description='With writable mode on, make every git-tracked file '
|
|
388
|
+
'writable. With it off, make them read-only, except files opened in '
|
|
389
|
+
'Perforce. Use it to fix files made read-only outside git-p4son, '
|
|
390
|
+
'for example by a submit.'
|
|
391
|
+
)
|
|
392
|
+
|
|
355
393
|
# Completion subcommand (prints shell completion script path)
|
|
356
394
|
completion_parser = subparsers.add_parser(
|
|
357
395
|
'completion',
|
|
@@ -477,6 +515,8 @@ def run_command(args: argparse.Namespace) -> int:
|
|
|
477
515
|
return alias_command(args)
|
|
478
516
|
elif args.command == 'review':
|
|
479
517
|
return review_command(args)
|
|
518
|
+
elif args.command == 'writable':
|
|
519
|
+
return writable_command(args)
|
|
480
520
|
elif args.command == '_sequence-editor':
|
|
481
521
|
return sequence_editor_command(args)
|
|
482
522
|
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:
|
|
@@ -242,7 +273,8 @@ def run_with_output(command: list[str], cwd: str = '.',
|
|
|
242
273
|
Returns:
|
|
243
274
|
RunResult object with returncode, stdout, and stderr
|
|
244
275
|
"""
|
|
245
|
-
log.command(
|
|
276
|
+
log.command(_command_line_for_log(command),
|
|
277
|
+
truncate_for_spinner=True)
|
|
246
278
|
log.start_spinner()
|
|
247
279
|
|
|
248
280
|
start_timestamp = timer()
|
|
@@ -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,12 +12,6 @@ from . import CONFIG_DIR
|
|
|
12
12
|
|
|
13
13
|
_BARE_KEY_RE = re.compile(r'^[A-Za-z0-9_-]+$')
|
|
14
14
|
|
|
15
|
-
# Placeholder allowed in a stored depot root, substituted with the live
|
|
16
|
-
# Perforce client (workspace) name each time the root is used. Storing e.g.
|
|
17
|
-
# root = "//$(workspace)/Engine" keeps the config working after the workspace
|
|
18
|
-
# is renamed, at the cost of one client-name lookup per command.
|
|
19
|
-
WORKSPACE_PLACEHOLDER = '$(workspace)'
|
|
20
|
-
|
|
21
15
|
|
|
22
16
|
def config_path(workspace_dir: str) -> str:
|
|
23
17
|
"""Return the path to the config file."""
|
|
@@ -94,14 +88,3 @@ def _write_table(f, name: str, table: dict) -> None:
|
|
|
94
88
|
f.write('\n')
|
|
95
89
|
for key, subtable in subtables.items():
|
|
96
90
|
_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)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Depot root resolution for git-p4son.
|
|
3
|
+
|
|
4
|
+
The depot root is the Perforce path git-p4son syncs, stored in
|
|
5
|
+
.git-p4son/config.toml. This module reads it, expands the $(workspace)
|
|
6
|
+
placeholder, and resolves it into the path Perforce commands run against.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
from .config import load_config
|
|
12
|
+
from .log import log
|
|
13
|
+
from .perforce import P4ClientSpec, get_client_spec
|
|
14
|
+
|
|
15
|
+
# Placeholder allowed in a stored depot root, substituted with the live
|
|
16
|
+
# Perforce client (workspace) name each time the root is used. Storing e.g.
|
|
17
|
+
# root = "//$(workspace)/Engine" keeps the config working after the workspace
|
|
18
|
+
# is renamed, at the cost of one client-name lookup per command.
|
|
19
|
+
WORKSPACE_PLACEHOLDER = '$(workspace)'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_depot_root(workspace_dir: str) -> str | None:
|
|
23
|
+
"""Get the depot root from config, or None if not configured."""
|
|
24
|
+
config = load_config(workspace_dir)
|
|
25
|
+
return config.get('depot', {}).get('root')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def expand_depot_root(depot_root: str, workspace_name: str) -> str:
|
|
29
|
+
"""Substitute the live workspace name for the $(workspace) placeholder."""
|
|
30
|
+
return depot_root.replace(WORKSPACE_PLACEHOLDER, workspace_name)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ResolvedDepot:
|
|
35
|
+
"""The configured depot root with any placeholder expanded, plus the
|
|
36
|
+
client spec it was resolved against."""
|
|
37
|
+
depot_root: str
|
|
38
|
+
client_spec: P4ClientSpec | None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def resolve_depot_root(workspace_dir: str) -> ResolvedDepot | None:
|
|
42
|
+
"""Resolve the configured depot root, or None (with an error logged).
|
|
43
|
+
|
|
44
|
+
The client spec is queried once here: its name resolves a $(workspace)
|
|
45
|
+
placeholder in the depot root, and its line-ending/clobber options feed
|
|
46
|
+
the writable-file handling in sync.
|
|
47
|
+
"""
|
|
48
|
+
log.heading('Finding depot root')
|
|
49
|
+
depot_root = get_depot_root(workspace_dir)
|
|
50
|
+
if not depot_root:
|
|
51
|
+
log.error('No depot root configured. Run "git p4son init" first.')
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
client_spec = get_client_spec(workspace_dir)
|
|
55
|
+
if WORKSPACE_PLACEHOLDER in depot_root and not client_spec:
|
|
56
|
+
log.error('Cannot resolve $(workspace) in depot root: not inside a '
|
|
57
|
+
'Perforce workspace')
|
|
58
|
+
return None
|
|
59
|
+
if client_spec:
|
|
60
|
+
depot_root = expand_depot_root(depot_root, client_spec.name)
|
|
61
|
+
log.success(depot_root)
|
|
62
|
+
return ResolvedDepot(depot_root=depot_root, client_spec=client_spec)
|
|
@@ -10,6 +10,7 @@ import re
|
|
|
10
10
|
from .common import (
|
|
11
11
|
CommandError,
|
|
12
12
|
RunError,
|
|
13
|
+
batched_command_log,
|
|
13
14
|
normalize_workspace_path,
|
|
14
15
|
run,
|
|
15
16
|
run_with_output,
|
|
@@ -252,18 +253,28 @@ def get_tracked_files(filepaths: list[str], workspace_dir: str) -> set[str]:
|
|
|
252
253
|
tracked: set[str] = set()
|
|
253
254
|
chunks = _chunk_paths_by_length(
|
|
254
255
|
list(by_git_path), _PATHSPEC_LENGTH_BUDGET)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
for
|
|
262
|
-
|
|
263
|
-
|
|
256
|
+
# -z output is NUL-separated and verbatim; without it paths with
|
|
257
|
+
# non-ASCII characters are C-quoted and would never match.
|
|
258
|
+
command = ['git', 'ls-files', '-z', '--']
|
|
259
|
+
with batched_command_log(command, len(by_git_path), len(chunks)):
|
|
260
|
+
for chunk in chunks:
|
|
261
|
+
result = run(command + chunk, cwd=workspace_dir)
|
|
262
|
+
for line in result.stdout:
|
|
263
|
+
for git_path in line.split('\0'):
|
|
264
|
+
if git_path in by_git_path:
|
|
265
|
+
tracked.add(by_git_path[git_path])
|
|
264
266
|
return tracked
|
|
265
267
|
|
|
266
268
|
|
|
269
|
+
def list_tracked_files(workspace_dir: str) -> list[str]:
|
|
270
|
+
"""Return every file tracked by git, as repo-relative slash paths."""
|
|
271
|
+
# -z output is NUL-separated and verbatim, so non-ASCII paths come back
|
|
272
|
+
# as-is rather than C-quoted. run() splits its output on newlines, so
|
|
273
|
+
# they are rejoined first in case a path contains one.
|
|
274
|
+
result = run(['git', 'ls-files', '-z'], cwd=workspace_dir)
|
|
275
|
+
return [path for path in '\n'.join(result.stdout).split('\0') if path]
|
|
276
|
+
|
|
277
|
+
|
|
267
278
|
# --- file retrieval ---
|
|
268
279
|
|
|
269
280
|
def get_file_at_commit(filepath: str, commit: str,
|
|
@@ -361,14 +372,23 @@ def find_base_commits(filepaths: list[str], before_commit: str,
|
|
|
361
372
|
by_git_path = {fp.replace('\\', '/'): fp for fp in filepaths}
|
|
362
373
|
chunks = _chunk_paths_by_length(
|
|
363
374
|
list(by_git_path), _PATHSPEC_LENGTH_BUDGET)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
375
|
+
with batched_command_log(_find_base_commits_command(before_commit),
|
|
376
|
+
len(by_git_path), len(chunks)):
|
|
377
|
+
for chunk in chunks:
|
|
378
|
+
chunk_result = _find_base_commits_chunk(
|
|
379
|
+
chunk, before_commit, workspace_dir)
|
|
380
|
+
for git_path, sha in chunk_result.items():
|
|
381
|
+
result[by_git_path[git_path]] = sha
|
|
369
382
|
return result
|
|
370
383
|
|
|
371
384
|
|
|
385
|
+
def _find_base_commits_command(before_commit: str) -> list[str]:
|
|
386
|
+
"""The history walk behind find_base_commits, up to its `--`."""
|
|
387
|
+
return ['git', '-c', 'core.quotePath=false', 'log', '--no-renames',
|
|
388
|
+
'--name-status', '--pretty=format:%x01%H%x01%s',
|
|
389
|
+
before_commit, '--']
|
|
390
|
+
|
|
391
|
+
|
|
372
392
|
def _find_base_commits_chunk(git_paths: list[str], before_commit: str,
|
|
373
393
|
workspace_dir: str) -> dict[str, str | None]:
|
|
374
394
|
"""One newest-first history walk resolving baselines for git_paths.
|
|
@@ -378,11 +398,8 @@ def _find_base_commits_chunk(git_paths: list[str], before_commit: str,
|
|
|
378
398
|
no sync commit ever touched. core.quotePath is disabled so non-ASCII
|
|
379
399
|
paths in --name-status output match the input verbatim."""
|
|
380
400
|
result: dict[str, str | None] = dict.fromkeys(git_paths)
|
|
381
|
-
res = run(
|
|
382
|
-
|
|
383
|
-
'--name-status', '--pretty=format:%x01%H%x01%s',
|
|
384
|
-
before_commit, '--'] + git_paths,
|
|
385
|
-
cwd=workspace_dir, fail_on_returncode=False)
|
|
401
|
+
res = run(_find_base_commits_command(before_commit) + git_paths,
|
|
402
|
+
cwd=workspace_dir, fail_on_returncode=False)
|
|
386
403
|
if res.returncode != 0:
|
|
387
404
|
return result
|
|
388
405
|
|