git-p4son 0.3.1__tar.gz → 0.3.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.3.1 → git_p4son-0.3.2}/PKG-INFO +81 -3
- {git_p4son-0.3.1 → git_p4son-0.3.2}/README.md +80 -2
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/__init__.py +1 -1
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/cli.py +38 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/complete.py +4 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/perforce.py +27 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/sync.py +30 -9
- git_p4son-0.3.2/git_p4son/sync_split.py +148 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son.egg-info/PKG-INFO +81 -3
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son.egg-info/SOURCES.txt +3 -1
- {git_p4son-0.3.1 → git_p4son-0.3.2}/pyproject.toml +1 -1
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_complete.py +3 -2
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_perforce.py +48 -2
- git_p4son-0.3.2/tests/test_sync_split.py +260 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/LICENSE +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/__main__.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/alias.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/changelist_store.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/common.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/completions/_git-p4son +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/completions/git-p4son.bash +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/completions/git-p4son.ps1 +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/config.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/git.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/hooks.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/init.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/lib.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/list_changes.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/log.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/new.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/review.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/state.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son/update.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son.egg-info/dependency_links.txt +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son.egg-info/entry_points.txt +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/git_p4son.egg-info/top_level.txt +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/setup.cfg +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_alias.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_changelist_store.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_cli.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_common.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_config.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_git.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_hooks.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_init.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_lib_changelist.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_lib_edit.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_lib_review.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_list_changes.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_log.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_new.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_review.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_state.py +0 -0
- {git_p4son-0.3.1 → git_p4son-0.3.2}/tests/test_sync.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-p4son
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -233,8 +233,8 @@ commit any files that need attention.
|
|
|
233
233
|
|
|
234
234
|
## Usage
|
|
235
235
|
|
|
236
|
-
git-p4son provides
|
|
237
|
-
and `completion`.
|
|
236
|
+
git-p4son provides nine commands: `init`, `sync`, `sync-split`, `new`, `update`, `review`, `list-changes`,
|
|
237
|
+
`alias`, and `completion`.
|
|
238
238
|
|
|
239
239
|
To see help for any command, use `-h`:
|
|
240
240
|
|
|
@@ -315,6 +315,54 @@ changelists is given, the pre-sync hooks run once, before the first sync.
|
|
|
315
315
|
After a successful `git-p4son sync` that actually performs sync work, git-p4son runs [hooks](#hooks)
|
|
316
316
|
from `.git-p4son/hooks/post-sync/`.
|
|
317
317
|
|
|
318
|
+
### Sync-Split Command
|
|
319
|
+
|
|
320
|
+
Sync forward from the last synced changelist, splitting selected users' changelists into their own commits:
|
|
321
|
+
|
|
322
|
+
```sh
|
|
323
|
+
git p4son sync-split [changelist] [--user NAME ...] [--dry-run]
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Syncing straight to the latest changelist lumps your own submitted changelists together with everyone
|
|
327
|
+
else's in a single commit. `sync-split` resolves the sequence for you: it looks up the last synced
|
|
328
|
+
changelist in the git log, asks Perforce which changelists the selected users submitted since then, and
|
|
329
|
+
syncs the changelist submitted immediately before each of those first. The result is a git history where
|
|
330
|
+
every changelist those users submitted is a commit containing nothing but their own change.
|
|
331
|
+
|
|
332
|
+
By default the selected user is you (the current p4 user), which covers the common case of reviewing your
|
|
333
|
+
own submits one at a time. Repeat `--user` to split out several people's changelists in the same run.
|
|
334
|
+
|
|
335
|
+
It is equivalent to working out the numbers by hand and running
|
|
336
|
+
`git p4son sync <before-theirs> <theirs> ... head`, and it delegates to `sync` once the sequence is
|
|
337
|
+
resolved, so [pre-sync and post-sync hooks](#hooks), writable-file merging, and the clean-workspace checks
|
|
338
|
+
all behave exactly as they do for `sync`.
|
|
339
|
+
|
|
340
|
+
**Arguments:**
|
|
341
|
+
- `changelist` (optional): Changelist number to sync up to, or `head` for the latest. Omit to sync to the
|
|
342
|
+
latest changelist affecting the workspace.
|
|
343
|
+
|
|
344
|
+
**Options:**
|
|
345
|
+
- `-u, --user NAME`: Perforce user whose changelists to split into their own commits. Repeat the flag to
|
|
346
|
+
select several users. Defaults to the current p4 user (from `p4 info`).
|
|
347
|
+
- `-n, --dry-run`: Print the resolved sync sequence without syncing.
|
|
348
|
+
|
|
349
|
+
**Examples:**
|
|
350
|
+
```sh
|
|
351
|
+
git p4son sync-split # sync to latest, your own changelists split out
|
|
352
|
+
git p4son sync-split head # same, explicit
|
|
353
|
+
git p4son sync-split 12345 # stop at changelist 12345
|
|
354
|
+
git p4son sync-split --dry-run # show the sequence that would be synced
|
|
355
|
+
git p4son sync-split -u alice # split out alice's changelists instead of yours
|
|
356
|
+
git p4son sync-split -u alice -u bob # split out both alice's and bob's changelists
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
For example, with the last sync at CL 100 and your own changelists at 102 and 105, `sync-split` resolves
|
|
360
|
+
to `git p4son sync 101 102 104 105 106`, producing five commits where CL 102 and CL 105 each hold only
|
|
361
|
+
your change.
|
|
362
|
+
|
|
363
|
+
`sync-split` only moves forward: it requires a previous sync to start from, and refuses a target older
|
|
364
|
+
than the last synced changelist (use `git p4son sync --force` for that).
|
|
365
|
+
|
|
318
366
|
### New Command
|
|
319
367
|
|
|
320
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.
|
|
@@ -581,6 +629,36 @@ before the first sync.
|
|
|
581
629
|
After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
|
|
582
630
|
`.git-p4son/hooks/post-sync/`.
|
|
583
631
|
|
|
632
|
+
### Example hooks
|
|
633
|
+
|
|
634
|
+
The [`examples/hooks/`](examples/hooks/) directory holds ready-made hooks. Nothing installs them for you:
|
|
635
|
+
symlink or copy the one you want into the matching hook directory in your workspace. Symlinking keeps the hook
|
|
636
|
+
up to date with the clone it points at, and on macOS and Linux the executable bit comes from the file in the
|
|
637
|
+
clone, so no `chmod` is needed.
|
|
638
|
+
|
|
639
|
+
#### block-while-running (pre-sync)
|
|
640
|
+
|
|
641
|
+
Aborts the sync while a named process is running, so a sync cannot swap out assets that a running editor still
|
|
642
|
+
has loaded:
|
|
643
|
+
|
|
644
|
+
```sh
|
|
645
|
+
ln -s /path/to/git-p4son/examples/hooks/pre-sync/block-while-running.py .git-p4son/hooks/pre-sync/
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
Which processes block a sync is read from your workspace's own `.git-p4son/config.toml`, so a symlinked hook
|
|
649
|
+
stays per-project:
|
|
650
|
+
|
|
651
|
+
```toml
|
|
652
|
+
[hooks.block-while-running]
|
|
653
|
+
processes = ["UnrealEditor", "UnrealLightmass"]
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
Names are matched with any `.exe` suffix stripped and case ignored, so a single entry covers
|
|
657
|
+
`UnrealEditor.exe` on Windows and `UnrealEditor` on macOS. Without that table the hook checks nothing and the
|
|
658
|
+
sync proceeds; a table that is present but malformed aborts the sync rather than silently skipping the check.
|
|
659
|
+
|
|
660
|
+
Set `GIT_P4SON_SKIP_PROCESS_CHECK=1` to run one sync without the check.
|
|
661
|
+
|
|
584
662
|
## Shell Completions
|
|
585
663
|
|
|
586
664
|
Tab completion is available for bash, zsh, and PowerShell, including commands, flags, and dynamic alias names.
|
|
@@ -210,8 +210,8 @@ commit any files that need attention.
|
|
|
210
210
|
|
|
211
211
|
## Usage
|
|
212
212
|
|
|
213
|
-
git-p4son provides
|
|
214
|
-
and `completion`.
|
|
213
|
+
git-p4son provides nine commands: `init`, `sync`, `sync-split`, `new`, `update`, `review`, `list-changes`,
|
|
214
|
+
`alias`, and `completion`.
|
|
215
215
|
|
|
216
216
|
To see help for any command, use `-h`:
|
|
217
217
|
|
|
@@ -292,6 +292,54 @@ changelists is given, the pre-sync hooks run once, before the first sync.
|
|
|
292
292
|
After a successful `git-p4son sync` that actually performs sync work, git-p4son runs [hooks](#hooks)
|
|
293
293
|
from `.git-p4son/hooks/post-sync/`.
|
|
294
294
|
|
|
295
|
+
### Sync-Split Command
|
|
296
|
+
|
|
297
|
+
Sync forward from the last synced changelist, splitting selected users' changelists into their own commits:
|
|
298
|
+
|
|
299
|
+
```sh
|
|
300
|
+
git p4son sync-split [changelist] [--user NAME ...] [--dry-run]
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Syncing straight to the latest changelist lumps your own submitted changelists together with everyone
|
|
304
|
+
else's in a single commit. `sync-split` resolves the sequence for you: it looks up the last synced
|
|
305
|
+
changelist in the git log, asks Perforce which changelists the selected users submitted since then, and
|
|
306
|
+
syncs the changelist submitted immediately before each of those first. The result is a git history where
|
|
307
|
+
every changelist those users submitted is a commit containing nothing but their own change.
|
|
308
|
+
|
|
309
|
+
By default the selected user is you (the current p4 user), which covers the common case of reviewing your
|
|
310
|
+
own submits one at a time. Repeat `--user` to split out several people's changelists in the same run.
|
|
311
|
+
|
|
312
|
+
It is equivalent to working out the numbers by hand and running
|
|
313
|
+
`git p4son sync <before-theirs> <theirs> ... head`, and it delegates to `sync` once the sequence is
|
|
314
|
+
resolved, so [pre-sync and post-sync hooks](#hooks), writable-file merging, and the clean-workspace checks
|
|
315
|
+
all behave exactly as they do for `sync`.
|
|
316
|
+
|
|
317
|
+
**Arguments:**
|
|
318
|
+
- `changelist` (optional): Changelist number to sync up to, or `head` for the latest. Omit to sync to the
|
|
319
|
+
latest changelist affecting the workspace.
|
|
320
|
+
|
|
321
|
+
**Options:**
|
|
322
|
+
- `-u, --user NAME`: Perforce user whose changelists to split into their own commits. Repeat the flag to
|
|
323
|
+
select several users. Defaults to the current p4 user (from `p4 info`).
|
|
324
|
+
- `-n, --dry-run`: Print the resolved sync sequence without syncing.
|
|
325
|
+
|
|
326
|
+
**Examples:**
|
|
327
|
+
```sh
|
|
328
|
+
git p4son sync-split # sync to latest, your own changelists split out
|
|
329
|
+
git p4son sync-split head # same, explicit
|
|
330
|
+
git p4son sync-split 12345 # stop at changelist 12345
|
|
331
|
+
git p4son sync-split --dry-run # show the sequence that would be synced
|
|
332
|
+
git p4son sync-split -u alice # split out alice's changelists instead of yours
|
|
333
|
+
git p4son sync-split -u alice -u bob # split out both alice's and bob's changelists
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
For example, with the last sync at CL 100 and your own changelists at 102 and 105, `sync-split` resolves
|
|
337
|
+
to `git p4son sync 101 102 104 105 106`, producing five commits where CL 102 and CL 105 each hold only
|
|
338
|
+
your change.
|
|
339
|
+
|
|
340
|
+
`sync-split` only moves forward: it requires a previous sync to start from, and refuses a target older
|
|
341
|
+
than the last synced changelist (use `git p4son sync --force` for that).
|
|
342
|
+
|
|
295
343
|
### New Command
|
|
296
344
|
|
|
297
345
|
Create a new Perforce changelist and add changed files to it. Description will contain an enumerated list of git commits since the base branch.
|
|
@@ -558,6 +606,36 @@ before the first sync.
|
|
|
558
606
|
After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
|
|
559
607
|
`.git-p4son/hooks/post-sync/`.
|
|
560
608
|
|
|
609
|
+
### Example hooks
|
|
610
|
+
|
|
611
|
+
The [`examples/hooks/`](examples/hooks/) directory holds ready-made hooks. Nothing installs them for you:
|
|
612
|
+
symlink or copy the one you want into the matching hook directory in your workspace. Symlinking keeps the hook
|
|
613
|
+
up to date with the clone it points at, and on macOS and Linux the executable bit comes from the file in the
|
|
614
|
+
clone, so no `chmod` is needed.
|
|
615
|
+
|
|
616
|
+
#### block-while-running (pre-sync)
|
|
617
|
+
|
|
618
|
+
Aborts the sync while a named process is running, so a sync cannot swap out assets that a running editor still
|
|
619
|
+
has loaded:
|
|
620
|
+
|
|
621
|
+
```sh
|
|
622
|
+
ln -s /path/to/git-p4son/examples/hooks/pre-sync/block-while-running.py .git-p4son/hooks/pre-sync/
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
Which processes block a sync is read from your workspace's own `.git-p4son/config.toml`, so a symlinked hook
|
|
626
|
+
stays per-project:
|
|
627
|
+
|
|
628
|
+
```toml
|
|
629
|
+
[hooks.block-while-running]
|
|
630
|
+
processes = ["UnrealEditor", "UnrealLightmass"]
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
Names are matched with any `.exe` suffix stripped and case ignored, so a single entry covers
|
|
634
|
+
`UnrealEditor.exe` on Windows and `UnrealEditor` on macOS. Without that table the hook checks nothing and the
|
|
635
|
+
sync proceeds; a table that is present but malformed aborts the sync rather than silently skipping the check.
|
|
636
|
+
|
|
637
|
+
Set `GIT_P4SON_SKIP_PROCESS_CHECK=1` to run one sync without the check.
|
|
638
|
+
|
|
561
639
|
## Shell Completions
|
|
562
640
|
|
|
563
641
|
Tab completion is available for bash, zsh, and PowerShell, including commands, flags, and dynamic alias names.
|
|
@@ -9,6 +9,7 @@ import time
|
|
|
9
9
|
from importlib.resources import files
|
|
10
10
|
from . import __version__
|
|
11
11
|
from .sync import sync_command
|
|
12
|
+
from .sync_split import sync_split_command
|
|
12
13
|
from .new import new_command
|
|
13
14
|
from .update import update_command
|
|
14
15
|
from .list_changes import list_changes_command
|
|
@@ -36,6 +37,9 @@ Examples:
|
|
|
36
37
|
git-p4son sync 123 156 178 # Sync each changelist in sequence, one commit each
|
|
37
38
|
git-p4son sync 123 156 head # Sync 123, 156, then the latest changelist
|
|
38
39
|
git-p4son sync last-synced # Re-sync the last synced changelist
|
|
40
|
+
git-p4son sync-split # Sync to latest, your own changelists split out
|
|
41
|
+
git-p4son sync-split 12345 # Same, but stop at changelist 12345
|
|
42
|
+
git-p4son sync-split -u alice -u bob # Split out alice's and bob's changelists
|
|
39
43
|
git-p4son new -m "Fix bug" # Create changelist, alias defaults to branch name
|
|
40
44
|
git-p4son new -m "Fix bug" --review # Create changelist, create Swarm review
|
|
41
45
|
git-p4son new -m "Fix bug" --no-alias # Create changelist without saving an alias
|
|
@@ -96,6 +100,38 @@ Examples:
|
|
|
96
100
|
help='Allow syncing to changelists older than the current one.'
|
|
97
101
|
)
|
|
98
102
|
|
|
103
|
+
# Sync-split subcommand
|
|
104
|
+
sync_split_parser = subparsers.add_parser(
|
|
105
|
+
'sync-split',
|
|
106
|
+
help='Sync forward, splitting a user\'s changelists into own commits',
|
|
107
|
+
description='Sync from the last synced changelist up to a target '
|
|
108
|
+
'changelist (the latest by default), syncing the changelist submitted '
|
|
109
|
+
'just before each of the selected users\' submits first so that every '
|
|
110
|
+
'changelist they submitted lands in a git commit of its own.'
|
|
111
|
+
)
|
|
112
|
+
sync_split_parser.add_argument(
|
|
113
|
+
'changelist',
|
|
114
|
+
nargs='?',
|
|
115
|
+
default=None,
|
|
116
|
+
metavar='CHANGELIST',
|
|
117
|
+
help='Changelist number to sync up to, or "head" for the latest. '
|
|
118
|
+
'Omit to sync to the latest changelist affecting the workspace'
|
|
119
|
+
)
|
|
120
|
+
sync_split_parser.add_argument(
|
|
121
|
+
'-u', '--user',
|
|
122
|
+
action='append',
|
|
123
|
+
default=None,
|
|
124
|
+
metavar='NAME',
|
|
125
|
+
help='Perforce user whose changelists to split into their own '
|
|
126
|
+
'commits. Repeat to select several users. '
|
|
127
|
+
'Defaults to the current p4 user'
|
|
128
|
+
)
|
|
129
|
+
sync_split_parser.add_argument(
|
|
130
|
+
'-n', '--dry-run',
|
|
131
|
+
action='store_true',
|
|
132
|
+
help='Print the resolved sync sequence without syncing'
|
|
133
|
+
)
|
|
134
|
+
|
|
99
135
|
# New subcommand
|
|
100
136
|
new_parser = subparsers.add_parser(
|
|
101
137
|
'new',
|
|
@@ -429,6 +465,8 @@ def run_command(args: argparse.Namespace) -> int:
|
|
|
429
465
|
|
|
430
466
|
if args.command == 'sync':
|
|
431
467
|
return sync_command(args)
|
|
468
|
+
elif args.command == 'sync-split':
|
|
469
|
+
return sync_split_command(args)
|
|
432
470
|
elif args.command == 'new':
|
|
433
471
|
return new_command(args)
|
|
434
472
|
elif args.command == 'update':
|
|
@@ -117,6 +117,10 @@ def _complete_positional(command, subcommand, positional_count,
|
|
|
117
117
|
return _filter(
|
|
118
118
|
[('head', 'Sync to the latest changelist')], prefix)
|
|
119
119
|
|
|
120
|
+
if command == 'sync-split' and positional_count == 0:
|
|
121
|
+
return _filter(
|
|
122
|
+
[('head', 'Sync to the latest changelist')], prefix)
|
|
123
|
+
|
|
120
124
|
if command == 'update' and positional_count == 0:
|
|
121
125
|
branch_candidates = _get_branch_candidates(prefix, workspace_dir)
|
|
122
126
|
return branch_candidates + _filter(aliases, prefix)
|
|
@@ -204,6 +204,33 @@ def get_latest_changelist(depot_root: str, workspace_dir: str) -> int:
|
|
|
204
204
|
return int(fields['change'])
|
|
205
205
|
|
|
206
206
|
|
|
207
|
+
@dataclass
|
|
208
|
+
class P4Change:
|
|
209
|
+
"""A submitted changelist affecting the depot root."""
|
|
210
|
+
|
|
211
|
+
change: int
|
|
212
|
+
user: str
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def get_submitted_changes(depot_root: str, start: int, end: int,
|
|
216
|
+
workspace_dir: str) -> list[P4Change]:
|
|
217
|
+
"""List changelists submitted to the depot root in [start, end], oldest first."""
|
|
218
|
+
res = run(['p4', '-ztag', 'changes', '-s', 'submitted',
|
|
219
|
+
f'{depot_root}/...@{start},@{end}'], cwd=workspace_dir)
|
|
220
|
+
records = parse_ztag_multi_output(res.stdout)
|
|
221
|
+
changes = [P4Change(change=int(r['change']), user=r.get('user', ''))
|
|
222
|
+
for r in records if 'change' in r]
|
|
223
|
+
return sorted(changes, key=lambda c: c.change)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def get_p4_user(workspace_dir: str) -> str | None:
|
|
227
|
+
"""Get the Perforce user name of the current connection."""
|
|
228
|
+
res = run(['p4', '-ztag', 'info'], cwd=workspace_dir)
|
|
229
|
+
fields = parse_ztag_output(res.stdout)
|
|
230
|
+
user = fields.get('userName')
|
|
231
|
+
return user or None
|
|
232
|
+
|
|
233
|
+
|
|
207
234
|
# --- file operations ---
|
|
208
235
|
|
|
209
236
|
def get_changelist_for_file(filename: str, workspace_dir: str) -> tuple[str, str] | None:
|
|
@@ -30,6 +30,7 @@ from .perforce import (
|
|
|
30
30
|
p4_fstat_file_info,
|
|
31
31
|
p4_get_opened_files,
|
|
32
32
|
p4_sync_preview,
|
|
33
|
+
P4ClientSpec,
|
|
33
34
|
P4SyncOutputProcessor,
|
|
34
35
|
P4SyncPreviewFile,
|
|
35
36
|
)
|
|
@@ -554,28 +555,48 @@ def _sync_pass(changelist: int, label: str, depot_root: str,
|
|
|
554
555
|
return prep
|
|
555
556
|
|
|
556
557
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
558
|
+
@dataclass
|
|
559
|
+
class ResolvedDepot:
|
|
560
|
+
"""The configured depot root with any placeholder expanded, plus the
|
|
561
|
+
client spec it was resolved against."""
|
|
562
|
+
depot_root: str
|
|
563
|
+
client_spec: P4ClientSpec | None
|
|
561
564
|
|
|
565
|
+
|
|
566
|
+
def resolve_depot_root(workspace_dir: str) -> ResolvedDepot | None:
|
|
567
|
+
"""Resolve the configured depot root, or None (with an error logged).
|
|
568
|
+
|
|
569
|
+
The client spec is queried once here: its name resolves a $(workspace)
|
|
570
|
+
placeholder in the depot root, and its line-ending/clobber options feed
|
|
571
|
+
the writable-file handling in sync.
|
|
572
|
+
"""
|
|
562
573
|
log.heading('Finding depot root')
|
|
563
574
|
depot_root = get_depot_root(workspace_dir)
|
|
564
575
|
if not depot_root:
|
|
565
576
|
log.error('No depot root configured. Run "git p4son init" first.')
|
|
566
|
-
return
|
|
577
|
+
return None
|
|
567
578
|
|
|
568
|
-
# The client spec is queried once here: its name resolves a $(workspace)
|
|
569
|
-
# placeholder in the depot root, and its line-ending/clobber options feed
|
|
570
|
-
# the writable-file handling further down.
|
|
571
579
|
client_spec = get_client_spec(workspace_dir)
|
|
572
580
|
if WORKSPACE_PLACEHOLDER in depot_root and not client_spec:
|
|
573
581
|
log.error('Cannot resolve $(workspace) in depot root: not inside a '
|
|
574
582
|
'Perforce workspace')
|
|
575
|
-
return
|
|
583
|
+
return None
|
|
576
584
|
if client_spec:
|
|
577
585
|
depot_root = expand_depot_root(depot_root, client_spec.name)
|
|
578
586
|
log.success(depot_root)
|
|
587
|
+
return ResolvedDepot(depot_root=depot_root, client_spec=client_spec)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def sync_command(args: argparse.Namespace) -> int:
|
|
591
|
+
"""Execute the sync command."""
|
|
592
|
+
workspace_dir = args.workspace_dir
|
|
593
|
+
invocation_dir = vars(args).get('invocation_dir', workspace_dir)
|
|
594
|
+
|
|
595
|
+
resolved = resolve_depot_root(workspace_dir)
|
|
596
|
+
if resolved is None:
|
|
597
|
+
return 1
|
|
598
|
+
depot_root = resolved.depot_root
|
|
599
|
+
client_spec = resolved.client_spec
|
|
579
600
|
|
|
580
601
|
log.heading('Checking git workspace')
|
|
581
602
|
dirty_files = get_dirty_files(workspace_dir)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sync-split command implementation for git-p4son.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from .log import log
|
|
8
|
+
from .perforce import (
|
|
9
|
+
P4Change,
|
|
10
|
+
get_latest_changelist,
|
|
11
|
+
get_p4_user,
|
|
12
|
+
get_submitted_changes,
|
|
13
|
+
)
|
|
14
|
+
from .sync import git_last_sync, resolve_depot_root, sync_command
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_sync_targets(changes: list[P4Change], users: list[str],
|
|
18
|
+
last_synced: int, upper: int) -> list[int]:
|
|
19
|
+
"""Build the sync sequence that splits out the given users' changelists.
|
|
20
|
+
|
|
21
|
+
changes is every submitted changelist affecting the depot root in
|
|
22
|
+
[last_synced, upper], oldest first. Each changelist submitted by one of
|
|
23
|
+
users gets the changelist submitted just before it synced first, so that
|
|
24
|
+
submit lands in a commit containing nothing else. Changelists at or below
|
|
25
|
+
last_synced are already in git and dropped, and the sequence always ends
|
|
26
|
+
at upper.
|
|
27
|
+
"""
|
|
28
|
+
targets: list[int] = []
|
|
29
|
+
lowered = {u.lower() for u in users}
|
|
30
|
+
for i, change in enumerate(changes):
|
|
31
|
+
if change.change <= last_synced or change.user.lower() not in lowered:
|
|
32
|
+
continue
|
|
33
|
+
last = targets[-1] if targets else last_synced
|
|
34
|
+
if i > 0 and changes[i - 1].change > last:
|
|
35
|
+
targets.append(changes[i - 1].change)
|
|
36
|
+
last = targets[-1]
|
|
37
|
+
if change.change > last:
|
|
38
|
+
targets.append(change.change)
|
|
39
|
+
if not targets or upper > targets[-1]:
|
|
40
|
+
targets.append(upper)
|
|
41
|
+
return targets
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _resolve_users(args: argparse.Namespace, workspace_dir: str) -> list[str]:
|
|
45
|
+
"""Resolve the users whose changelists to split out, or [] on failure.
|
|
46
|
+
|
|
47
|
+
Defaults to the current Perforce user when no --user was given. Repeated
|
|
48
|
+
names are collapsed, keeping the order they were given in.
|
|
49
|
+
"""
|
|
50
|
+
log.heading('Finding Perforce users')
|
|
51
|
+
given = args.user or []
|
|
52
|
+
if not given:
|
|
53
|
+
current = get_p4_user(workspace_dir)
|
|
54
|
+
if not current:
|
|
55
|
+
log.error('Cannot determine the current Perforce user. '
|
|
56
|
+
'Pass --user NAME.')
|
|
57
|
+
return []
|
|
58
|
+
given = [current]
|
|
59
|
+
|
|
60
|
+
users: list[str] = []
|
|
61
|
+
seen: set[str] = set()
|
|
62
|
+
for user in given:
|
|
63
|
+
if user.lower() not in seen:
|
|
64
|
+
seen.add(user.lower())
|
|
65
|
+
users.append(user)
|
|
66
|
+
log.success(', '.join(users))
|
|
67
|
+
return users
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def sync_split_command(args: argparse.Namespace) -> int:
|
|
71
|
+
"""Execute the sync-split command."""
|
|
72
|
+
workspace_dir = args.workspace_dir
|
|
73
|
+
|
|
74
|
+
resolved = resolve_depot_root(workspace_dir)
|
|
75
|
+
if resolved is None:
|
|
76
|
+
return 1
|
|
77
|
+
depot_root = resolved.depot_root
|
|
78
|
+
|
|
79
|
+
log.heading('Finding last synced changelist')
|
|
80
|
+
last_sync = git_last_sync(workspace_dir)
|
|
81
|
+
if not last_sync:
|
|
82
|
+
log.error('No previous sync found. Run "git p4son sync" once to '
|
|
83
|
+
'establish a starting point.')
|
|
84
|
+
return 1
|
|
85
|
+
log.success(f'CL {last_sync.changelist}')
|
|
86
|
+
last_synced = last_sync.changelist
|
|
87
|
+
|
|
88
|
+
if args.changelist is None or args.changelist.lower() == 'head':
|
|
89
|
+
log.heading('Finding latest changelist')
|
|
90
|
+
upper = get_latest_changelist(depot_root, workspace_dir)
|
|
91
|
+
log.success(f'CL {upper}')
|
|
92
|
+
else:
|
|
93
|
+
try:
|
|
94
|
+
upper = int(args.changelist)
|
|
95
|
+
except ValueError:
|
|
96
|
+
log.error(f'Invalid changelist number: {args.changelist}')
|
|
97
|
+
return 1
|
|
98
|
+
|
|
99
|
+
# sync-split only ever moves forward from the last synced changelist: the
|
|
100
|
+
# range it scans for the users' submits starts there.
|
|
101
|
+
if upper == last_synced:
|
|
102
|
+
log.info('Already synced, nothing to do.')
|
|
103
|
+
return 0
|
|
104
|
+
if upper < last_synced:
|
|
105
|
+
log.error(f'Cannot sync back to CL {upper} '
|
|
106
|
+
f'(currently at CL {last_synced}). '
|
|
107
|
+
'Use "git p4son sync --force" to sync to an older '
|
|
108
|
+
'changelist.')
|
|
109
|
+
return 1
|
|
110
|
+
|
|
111
|
+
users = _resolve_users(args, workspace_dir)
|
|
112
|
+
if not users:
|
|
113
|
+
return 1
|
|
114
|
+
|
|
115
|
+
# One query covers both jobs: which changelists in the range belong to
|
|
116
|
+
# the given users, and which changelist was submitted immediately before
|
|
117
|
+
# each of them (its predecessor in this same list).
|
|
118
|
+
log.heading(f'Finding changelists submitted to {depot_root} '
|
|
119
|
+
f'in CL {last_synced}..{upper}')
|
|
120
|
+
changes = get_submitted_changes(depot_root, last_synced, upper,
|
|
121
|
+
workspace_dir)
|
|
122
|
+
log.success(f'{len(changes)} changelists')
|
|
123
|
+
|
|
124
|
+
lowered = {u.lower() for u in users}
|
|
125
|
+
matched = [c for c in changes
|
|
126
|
+
if c.change > last_synced and c.user.lower() in lowered]
|
|
127
|
+
log.heading('Finding changelists to split into their own commits')
|
|
128
|
+
if matched:
|
|
129
|
+
for change in matched:
|
|
130
|
+
log.info(f'CL {change.change} ({change.user})')
|
|
131
|
+
label = 'changelist' if len(matched) == 1 else 'changelists'
|
|
132
|
+
log.success(f'{len(matched)} {label} to split out')
|
|
133
|
+
else:
|
|
134
|
+
log.warning('None found, syncing to the target changelist only')
|
|
135
|
+
|
|
136
|
+
targets = build_sync_targets(changes, users, last_synced, upper)
|
|
137
|
+
|
|
138
|
+
log.heading('Sync sequence')
|
|
139
|
+
log.success(' '.join(str(cl) for cl in targets))
|
|
140
|
+
|
|
141
|
+
if args.dry_run:
|
|
142
|
+
log.info(f'Would run: git p4son sync '
|
|
143
|
+
f'{" ".join(str(cl) for cl in targets)}')
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
args.changelist = [str(cl) for cl in targets]
|
|
147
|
+
args.force = False
|
|
148
|
+
return sync_command(args)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-p4son
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -233,8 +233,8 @@ commit any files that need attention.
|
|
|
233
233
|
|
|
234
234
|
## Usage
|
|
235
235
|
|
|
236
|
-
git-p4son provides
|
|
237
|
-
and `completion`.
|
|
236
|
+
git-p4son provides nine commands: `init`, `sync`, `sync-split`, `new`, `update`, `review`, `list-changes`,
|
|
237
|
+
`alias`, and `completion`.
|
|
238
238
|
|
|
239
239
|
To see help for any command, use `-h`:
|
|
240
240
|
|
|
@@ -315,6 +315,54 @@ changelists is given, the pre-sync hooks run once, before the first sync.
|
|
|
315
315
|
After a successful `git-p4son sync` that actually performs sync work, git-p4son runs [hooks](#hooks)
|
|
316
316
|
from `.git-p4son/hooks/post-sync/`.
|
|
317
317
|
|
|
318
|
+
### Sync-Split Command
|
|
319
|
+
|
|
320
|
+
Sync forward from the last synced changelist, splitting selected users' changelists into their own commits:
|
|
321
|
+
|
|
322
|
+
```sh
|
|
323
|
+
git p4son sync-split [changelist] [--user NAME ...] [--dry-run]
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Syncing straight to the latest changelist lumps your own submitted changelists together with everyone
|
|
327
|
+
else's in a single commit. `sync-split` resolves the sequence for you: it looks up the last synced
|
|
328
|
+
changelist in the git log, asks Perforce which changelists the selected users submitted since then, and
|
|
329
|
+
syncs the changelist submitted immediately before each of those first. The result is a git history where
|
|
330
|
+
every changelist those users submitted is a commit containing nothing but their own change.
|
|
331
|
+
|
|
332
|
+
By default the selected user is you (the current p4 user), which covers the common case of reviewing your
|
|
333
|
+
own submits one at a time. Repeat `--user` to split out several people's changelists in the same run.
|
|
334
|
+
|
|
335
|
+
It is equivalent to working out the numbers by hand and running
|
|
336
|
+
`git p4son sync <before-theirs> <theirs> ... head`, and it delegates to `sync` once the sequence is
|
|
337
|
+
resolved, so [pre-sync and post-sync hooks](#hooks), writable-file merging, and the clean-workspace checks
|
|
338
|
+
all behave exactly as they do for `sync`.
|
|
339
|
+
|
|
340
|
+
**Arguments:**
|
|
341
|
+
- `changelist` (optional): Changelist number to sync up to, or `head` for the latest. Omit to sync to the
|
|
342
|
+
latest changelist affecting the workspace.
|
|
343
|
+
|
|
344
|
+
**Options:**
|
|
345
|
+
- `-u, --user NAME`: Perforce user whose changelists to split into their own commits. Repeat the flag to
|
|
346
|
+
select several users. Defaults to the current p4 user (from `p4 info`).
|
|
347
|
+
- `-n, --dry-run`: Print the resolved sync sequence without syncing.
|
|
348
|
+
|
|
349
|
+
**Examples:**
|
|
350
|
+
```sh
|
|
351
|
+
git p4son sync-split # sync to latest, your own changelists split out
|
|
352
|
+
git p4son sync-split head # same, explicit
|
|
353
|
+
git p4son sync-split 12345 # stop at changelist 12345
|
|
354
|
+
git p4son sync-split --dry-run # show the sequence that would be synced
|
|
355
|
+
git p4son sync-split -u alice # split out alice's changelists instead of yours
|
|
356
|
+
git p4son sync-split -u alice -u bob # split out both alice's and bob's changelists
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
For example, with the last sync at CL 100 and your own changelists at 102 and 105, `sync-split` resolves
|
|
360
|
+
to `git p4son sync 101 102 104 105 106`, producing five commits where CL 102 and CL 105 each hold only
|
|
361
|
+
your change.
|
|
362
|
+
|
|
363
|
+
`sync-split` only moves forward: it requires a previous sync to start from, and refuses a target older
|
|
364
|
+
than the last synced changelist (use `git p4son sync --force` for that).
|
|
365
|
+
|
|
318
366
|
### New Command
|
|
319
367
|
|
|
320
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.
|
|
@@ -581,6 +629,36 @@ before the first sync.
|
|
|
581
629
|
After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
|
|
582
630
|
`.git-p4son/hooks/post-sync/`.
|
|
583
631
|
|
|
632
|
+
### Example hooks
|
|
633
|
+
|
|
634
|
+
The [`examples/hooks/`](examples/hooks/) directory holds ready-made hooks. Nothing installs them for you:
|
|
635
|
+
symlink or copy the one you want into the matching hook directory in your workspace. Symlinking keeps the hook
|
|
636
|
+
up to date with the clone it points at, and on macOS and Linux the executable bit comes from the file in the
|
|
637
|
+
clone, so no `chmod` is needed.
|
|
638
|
+
|
|
639
|
+
#### block-while-running (pre-sync)
|
|
640
|
+
|
|
641
|
+
Aborts the sync while a named process is running, so a sync cannot swap out assets that a running editor still
|
|
642
|
+
has loaded:
|
|
643
|
+
|
|
644
|
+
```sh
|
|
645
|
+
ln -s /path/to/git-p4son/examples/hooks/pre-sync/block-while-running.py .git-p4son/hooks/pre-sync/
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
Which processes block a sync is read from your workspace's own `.git-p4son/config.toml`, so a symlinked hook
|
|
649
|
+
stays per-project:
|
|
650
|
+
|
|
651
|
+
```toml
|
|
652
|
+
[hooks.block-while-running]
|
|
653
|
+
processes = ["UnrealEditor", "UnrealLightmass"]
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
Names are matched with any `.exe` suffix stripped and case ignored, so a single entry covers
|
|
657
|
+
`UnrealEditor.exe` on Windows and `UnrealEditor` on macOS. Without that table the hook checks nothing and the
|
|
658
|
+
sync proceeds; a table that is present but malformed aborts the sync rather than silently skipping the check.
|
|
659
|
+
|
|
660
|
+
Set `GIT_P4SON_SKIP_PROCESS_CHECK=1` to run one sync without the check.
|
|
661
|
+
|
|
584
662
|
## Shell Completions
|
|
585
663
|
|
|
586
664
|
Tab completion is available for bash, zsh, and PowerShell, including commands, flags, and dynamic alias names.
|
|
@@ -20,6 +20,7 @@ git_p4son/perforce.py
|
|
|
20
20
|
git_p4son/review.py
|
|
21
21
|
git_p4son/state.py
|
|
22
22
|
git_p4son/sync.py
|
|
23
|
+
git_p4son/sync_split.py
|
|
23
24
|
git_p4son/update.py
|
|
24
25
|
git_p4son.egg-info/PKG-INFO
|
|
25
26
|
git_p4son.egg-info/SOURCES.txt
|
|
@@ -47,4 +48,5 @@ tests/test_new.py
|
|
|
47
48
|
tests/test_perforce.py
|
|
48
49
|
tests/test_review.py
|
|
49
50
|
tests/test_state.py
|
|
50
|
-
tests/test_sync.py
|
|
51
|
+
tests/test_sync.py
|
|
52
|
+
tests/test_sync_split.py
|
|
@@ -136,7 +136,8 @@ class TestComplete(unittest.TestCase):
|
|
|
136
136
|
self.assertIn('alias', names)
|
|
137
137
|
self.assertIn('review', names)
|
|
138
138
|
self.assertIn('init', names)
|
|
139
|
-
self.
|
|
139
|
+
self.assertIn('sync-split', names)
|
|
140
|
+
self.assertEqual(len(names), 8)
|
|
140
141
|
|
|
141
142
|
def test_empty_excludes_complete(self, _ws, _aliases):
|
|
142
143
|
result = _complete(self.parser, [''], workspace_dir='/ws')
|
|
@@ -151,7 +152,7 @@ class TestComplete(unittest.TestCase):
|
|
|
151
152
|
def test_prefix_filters_commands(self, _ws, _aliases):
|
|
152
153
|
result = _complete(self.parser, ['sy'], workspace_dir='/ws')
|
|
153
154
|
names = self._names(result)
|
|
154
|
-
self.assertEqual(names, ['sync'])
|
|
155
|
+
self.assertEqual(names, ['sync', 'sync-split'])
|
|
155
156
|
|
|
156
157
|
def test_hidden_command_not_completed(self, _ws, _aliases):
|
|
157
158
|
result = _complete(self.parser, ['c'], workspace_dir='/ws')
|
|
@@ -5,8 +5,9 @@ from unittest import mock
|
|
|
5
5
|
|
|
6
6
|
from git_p4son.perforce import (
|
|
7
7
|
P4ClientSpec, P4FileInfo, P4SyncPreviewFile, get_client_spec,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
get_p4_user, get_submitted_changes, is_binary_file_type,
|
|
9
|
+
p4_fstat_file_info, p4_sync_preview, parse_ztag_multi_output,
|
|
10
|
+
parse_ztag_output,
|
|
10
11
|
)
|
|
11
12
|
from tests.helpers import make_run_result
|
|
12
13
|
|
|
@@ -253,3 +254,48 @@ class TestGetClientSpec(unittest.TestCase):
|
|
|
253
254
|
])
|
|
254
255
|
spec = get_client_spec('/ws')
|
|
255
256
|
self.assertIsNone(spec)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class TestGetSubmittedChanges(unittest.TestCase):
|
|
260
|
+
@mock.patch('git_p4son.perforce.run')
|
|
261
|
+
def test_parses_and_sorts_changes(self, mock_run):
|
|
262
|
+
# p4 lists newest first; the sequence builder needs oldest first.
|
|
263
|
+
mock_run.return_value = make_run_result(stdout=[
|
|
264
|
+
'... change 105',
|
|
265
|
+
'... user andreas',
|
|
266
|
+
'... desc Fix the thing',
|
|
267
|
+
'',
|
|
268
|
+
'... change 102',
|
|
269
|
+
'... user colleague',
|
|
270
|
+
'... desc Some other work',
|
|
271
|
+
'',
|
|
272
|
+
])
|
|
273
|
+
changes = get_submitted_changes('//myclient', 100, 105, '/ws')
|
|
274
|
+
self.assertEqual([(c.change, c.user) for c in changes],
|
|
275
|
+
[(102, 'colleague'), (105, 'andreas')])
|
|
276
|
+
mock_run.assert_called_once_with(
|
|
277
|
+
['p4', '-ztag', 'changes', '-s', 'submitted',
|
|
278
|
+
'//myclient/...@100,@105'], cwd='/ws')
|
|
279
|
+
|
|
280
|
+
@mock.patch('git_p4son.perforce.run')
|
|
281
|
+
def test_no_changes_in_range(self, mock_run):
|
|
282
|
+
mock_run.return_value = make_run_result(stdout=[])
|
|
283
|
+
self.assertEqual(get_submitted_changes('//myclient', 100, 105, '/ws'),
|
|
284
|
+
[])
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class TestGetP4User(unittest.TestCase):
|
|
288
|
+
@mock.patch('git_p4son.perforce.run')
|
|
289
|
+
def test_reads_user_name(self, mock_run):
|
|
290
|
+
mock_run.return_value = make_run_result(stdout=[
|
|
291
|
+
'... userName andreas',
|
|
292
|
+
'... clientName my-ws',
|
|
293
|
+
])
|
|
294
|
+
self.assertEqual(get_p4_user('/ws'), 'andreas')
|
|
295
|
+
mock_run.assert_called_once_with(
|
|
296
|
+
['p4', '-ztag', 'info'], cwd='/ws')
|
|
297
|
+
|
|
298
|
+
@mock.patch('git_p4son.perforce.run')
|
|
299
|
+
def test_missing_user_name(self, mock_run):
|
|
300
|
+
mock_run.return_value = make_run_result(stdout=['... clientName x'])
|
|
301
|
+
self.assertIsNone(get_p4_user('/ws'))
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Tests for git_p4son.sync_split module."""
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
from unittest import mock
|
|
5
|
+
|
|
6
|
+
from git_p4son.perforce import P4Change
|
|
7
|
+
from git_p4son.sync import LastSync, ResolvedDepot
|
|
8
|
+
from git_p4son.sync_split import (
|
|
9
|
+
build_sync_targets, sync_split_command,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _changes(*pairs):
|
|
14
|
+
"""Build a change list from (changelist, user) pairs."""
|
|
15
|
+
return [P4Change(change=cl, user=user) for cl, user in pairs]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestBuildSyncTargets(unittest.TestCase):
|
|
19
|
+
"""The sequence must isolate each of the user's own changelists in a
|
|
20
|
+
commit of its own while staying strictly increasing."""
|
|
21
|
+
|
|
22
|
+
def test_single_own_changelist(self):
|
|
23
|
+
changes = _changes((100, 'other'), (101, 'other'),
|
|
24
|
+
(102, 'me'), (103, 'other'))
|
|
25
|
+
targets = build_sync_targets(changes, ['me'], 100, 103)
|
|
26
|
+
self.assertEqual(targets, [101, 102, 103])
|
|
27
|
+
|
|
28
|
+
def test_two_own_changelists(self):
|
|
29
|
+
changes = _changes((100, 'other'), (101, 'other'), (102, 'me'),
|
|
30
|
+
(103, 'other'), (104, 'other'), (105, 'me'),
|
|
31
|
+
(106, 'other'))
|
|
32
|
+
targets = build_sync_targets(changes, ['me'], 100, 106)
|
|
33
|
+
self.assertEqual(targets, [101, 102, 104, 105, 106])
|
|
34
|
+
|
|
35
|
+
def test_back_to_back_own_changelists(self):
|
|
36
|
+
"""A predecessor that is itself one of the user's changelists is
|
|
37
|
+
already a target, so it must not be repeated."""
|
|
38
|
+
changes = _changes((100, 'other'), (101, 'other'),
|
|
39
|
+
(102, 'me'), (103, 'me'))
|
|
40
|
+
targets = build_sync_targets(changes, ['me'], 100, 103)
|
|
41
|
+
self.assertEqual(targets, [101, 102, 103])
|
|
42
|
+
|
|
43
|
+
def test_predecessor_is_last_synced(self):
|
|
44
|
+
"""The changelist before the user's own submit is already in git,
|
|
45
|
+
so only the user's own submit is synced separately."""
|
|
46
|
+
changes = _changes((100, 'other'), (101, 'me'), (102, 'other'))
|
|
47
|
+
targets = build_sync_targets(changes, ['me'], 100, 102)
|
|
48
|
+
self.assertEqual(targets, [101, 102])
|
|
49
|
+
|
|
50
|
+
def test_own_changelist_is_the_upper_bound(self):
|
|
51
|
+
changes = _changes((100, 'other'), (101, 'other'), (102, 'me'))
|
|
52
|
+
targets = build_sync_targets(changes, ['me'], 100, 102)
|
|
53
|
+
self.assertEqual(targets, [101, 102])
|
|
54
|
+
|
|
55
|
+
def test_no_own_changelists(self):
|
|
56
|
+
changes = _changes((100, 'other'), (101, 'other'), (102, 'other'))
|
|
57
|
+
targets = build_sync_targets(changes, ['me'], 100, 102)
|
|
58
|
+
self.assertEqual(targets, [102])
|
|
59
|
+
|
|
60
|
+
def test_no_changes_at_all(self):
|
|
61
|
+
"""Nothing affected the depot root in the range, so the upper bound
|
|
62
|
+
is still synced: it may be a changelist elsewhere in the depot."""
|
|
63
|
+
targets = build_sync_targets([], ['me'], 100, 120)
|
|
64
|
+
self.assertEqual(targets, [120])
|
|
65
|
+
|
|
66
|
+
def test_user_match_is_case_insensitive(self):
|
|
67
|
+
changes = _changes((100, 'other'), (101, 'other'), (102, 'Me'))
|
|
68
|
+
targets = build_sync_targets(changes, ['me'], 100, 103)
|
|
69
|
+
self.assertEqual(targets, [101, 102, 103])
|
|
70
|
+
|
|
71
|
+
def test_two_users_both_split_out(self):
|
|
72
|
+
changes = _changes((100, 'other'), (101, 'other'), (102, 'alice'),
|
|
73
|
+
(103, 'other'), (104, 'other'), (105, 'bob'),
|
|
74
|
+
(106, 'other'))
|
|
75
|
+
targets = build_sync_targets(changes, ['alice', 'bob'], 100, 106)
|
|
76
|
+
self.assertEqual(targets, [101, 102, 104, 105, 106])
|
|
77
|
+
|
|
78
|
+
def test_adjacent_changelists_from_two_selected_users(self):
|
|
79
|
+
"""alice's changelist is the predecessor of bob's, so it is already
|
|
80
|
+
a target and must not be repeated."""
|
|
81
|
+
changes = _changes((100, 'other'), (101, 'other'),
|
|
82
|
+
(102, 'alice'), (103, 'bob'))
|
|
83
|
+
targets = build_sync_targets(changes, ['alice', 'bob'], 100, 103)
|
|
84
|
+
self.assertEqual(targets, [101, 102, 103])
|
|
85
|
+
|
|
86
|
+
def test_unselected_user_is_not_split_out(self):
|
|
87
|
+
changes = _changes((100, 'other'), (101, 'other'), (102, 'alice'),
|
|
88
|
+
(103, 'other'), (104, 'bob'))
|
|
89
|
+
targets = build_sync_targets(changes, ['alice'], 100, 104)
|
|
90
|
+
self.assertEqual(targets, [101, 102, 104])
|
|
91
|
+
|
|
92
|
+
def test_targets_are_strictly_increasing(self):
|
|
93
|
+
changes = _changes((100, 'me'), (101, 'me'), (102, 'other'),
|
|
94
|
+
(103, 'me'), (104, 'me'), (105, 'other'))
|
|
95
|
+
targets = build_sync_targets(changes, ['me'], 100, 105)
|
|
96
|
+
self.assertEqual(targets, sorted(set(targets)))
|
|
97
|
+
self.assertEqual(targets, [101, 102, 103, 104, 105])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestSyncMineCommand(unittest.TestCase):
|
|
101
|
+
|
|
102
|
+
def setUp(self):
|
|
103
|
+
patcher = mock.patch(
|
|
104
|
+
'git_p4son.sync_split.resolve_depot_root',
|
|
105
|
+
return_value=ResolvedDepot(depot_root='//myclient',
|
|
106
|
+
client_spec=None))
|
|
107
|
+
patcher.start()
|
|
108
|
+
self.addCleanup(patcher.stop)
|
|
109
|
+
|
|
110
|
+
def _args(self, **overrides):
|
|
111
|
+
values = dict(changelist=None, user=None, dry_run=False,
|
|
112
|
+
workspace_dir='/ws')
|
|
113
|
+
values.update(overrides)
|
|
114
|
+
return mock.Mock(**values)
|
|
115
|
+
|
|
116
|
+
@mock.patch('git_p4son.sync_split.sync_command', return_value=0)
|
|
117
|
+
@mock.patch('git_p4son.sync_split.get_submitted_changes')
|
|
118
|
+
@mock.patch('git_p4son.sync_split.get_p4_user', return_value='me')
|
|
119
|
+
@mock.patch('git_p4son.sync_split.get_latest_changelist', return_value=106)
|
|
120
|
+
@mock.patch('git_p4son.sync_split.git_last_sync',
|
|
121
|
+
return_value=LastSync(changelist=100, commit='abc123'))
|
|
122
|
+
def test_delegates_resolved_sequence_to_sync(
|
|
123
|
+
self, _last_sync, _latest, _user, mock_changes, mock_sync):
|
|
124
|
+
mock_changes.return_value = _changes(
|
|
125
|
+
(100, 'other'), (101, 'other'), (102, 'me'),
|
|
126
|
+
(103, 'other'), (104, 'other'), (105, 'me'), (106, 'other'))
|
|
127
|
+
args = self._args()
|
|
128
|
+
rc = sync_split_command(args)
|
|
129
|
+
self.assertEqual(rc, 0)
|
|
130
|
+
mock_sync.assert_called_once_with(args)
|
|
131
|
+
self.assertEqual(args.changelist, ['101', '102', '104', '105', '106'])
|
|
132
|
+
self.assertFalse(args.force)
|
|
133
|
+
|
|
134
|
+
@mock.patch('git_p4son.sync_split.sync_command', return_value=0)
|
|
135
|
+
@mock.patch('git_p4son.sync_split.get_submitted_changes')
|
|
136
|
+
@mock.patch('git_p4son.sync_split.get_p4_user', return_value='me')
|
|
137
|
+
@mock.patch('git_p4son.sync_split.get_latest_changelist')
|
|
138
|
+
@mock.patch('git_p4son.sync_split.git_last_sync',
|
|
139
|
+
return_value=LastSync(changelist=100, commit='abc123'))
|
|
140
|
+
def test_explicit_upper_bound_skips_head_lookup(
|
|
141
|
+
self, _last_sync, mock_latest, _user, mock_changes, mock_sync):
|
|
142
|
+
mock_changes.return_value = _changes(
|
|
143
|
+
(100, 'other'), (101, 'other'), (102, 'me'))
|
|
144
|
+
args = self._args(changelist='103')
|
|
145
|
+
rc = sync_split_command(args)
|
|
146
|
+
self.assertEqual(rc, 0)
|
|
147
|
+
mock_latest.assert_not_called()
|
|
148
|
+
mock_changes.assert_called_once_with('//myclient', 100, 103, '/ws')
|
|
149
|
+
self.assertEqual(args.changelist, ['101', '102', '103'])
|
|
150
|
+
|
|
151
|
+
@mock.patch('git_p4son.sync_split.sync_command', return_value=0)
|
|
152
|
+
@mock.patch('git_p4son.sync_split.get_submitted_changes')
|
|
153
|
+
@mock.patch('git_p4son.sync_split.get_p4_user')
|
|
154
|
+
@mock.patch('git_p4son.sync_split.get_latest_changelist', return_value=103)
|
|
155
|
+
@mock.patch('git_p4son.sync_split.git_last_sync',
|
|
156
|
+
return_value=LastSync(changelist=100, commit='abc123'))
|
|
157
|
+
def test_user_option_overrides_p4_user(
|
|
158
|
+
self, _last_sync, _latest, mock_p4_user, mock_changes, _sync):
|
|
159
|
+
mock_changes.return_value = _changes(
|
|
160
|
+
(100, 'other'), (101, 'other'), (102, 'colleague'))
|
|
161
|
+
args = self._args(user=['colleague'])
|
|
162
|
+
rc = sync_split_command(args)
|
|
163
|
+
self.assertEqual(rc, 0)
|
|
164
|
+
mock_p4_user.assert_not_called()
|
|
165
|
+
self.assertEqual(args.changelist, ['101', '102', '103'])
|
|
166
|
+
|
|
167
|
+
@mock.patch('git_p4son.sync_split.sync_command')
|
|
168
|
+
@mock.patch('git_p4son.sync_split.get_submitted_changes')
|
|
169
|
+
@mock.patch('git_p4son.sync_split.get_p4_user', return_value='me')
|
|
170
|
+
@mock.patch('git_p4son.sync_split.get_latest_changelist', return_value=103)
|
|
171
|
+
@mock.patch('git_p4son.sync_split.git_last_sync',
|
|
172
|
+
return_value=LastSync(changelist=100, commit='abc123'))
|
|
173
|
+
def test_dry_run_does_not_sync(
|
|
174
|
+
self, _last_sync, _latest, _user, mock_changes, mock_sync):
|
|
175
|
+
mock_changes.return_value = _changes(
|
|
176
|
+
(100, 'other'), (101, 'other'), (102, 'me'))
|
|
177
|
+
rc = sync_split_command(self._args(dry_run=True))
|
|
178
|
+
self.assertEqual(rc, 0)
|
|
179
|
+
mock_sync.assert_not_called()
|
|
180
|
+
|
|
181
|
+
@mock.patch('git_p4son.sync_split.git_last_sync', return_value=None)
|
|
182
|
+
def test_no_previous_sync_aborts(self, _last_sync):
|
|
183
|
+
"""Without a synced changelist there is no lower bound for the scan."""
|
|
184
|
+
rc = sync_split_command(self._args())
|
|
185
|
+
self.assertEqual(rc, 1)
|
|
186
|
+
|
|
187
|
+
@mock.patch('git_p4son.sync_split.sync_command')
|
|
188
|
+
@mock.patch('git_p4son.sync_split.get_latest_changelist', return_value=100)
|
|
189
|
+
@mock.patch('git_p4son.sync_split.git_last_sync',
|
|
190
|
+
return_value=LastSync(changelist=100, commit='abc123'))
|
|
191
|
+
def test_already_at_target_is_a_no_op(
|
|
192
|
+
self, _last_sync, _latest, mock_sync):
|
|
193
|
+
rc = sync_split_command(self._args())
|
|
194
|
+
self.assertEqual(rc, 0)
|
|
195
|
+
mock_sync.assert_not_called()
|
|
196
|
+
|
|
197
|
+
@mock.patch('git_p4son.sync_split.sync_command')
|
|
198
|
+
@mock.patch('git_p4son.sync_split.git_last_sync',
|
|
199
|
+
return_value=LastSync(changelist=100, commit='abc123'))
|
|
200
|
+
def test_older_target_aborts(self, _last_sync, mock_sync):
|
|
201
|
+
rc = sync_split_command(self._args(changelist='90'))
|
|
202
|
+
self.assertEqual(rc, 1)
|
|
203
|
+
mock_sync.assert_not_called()
|
|
204
|
+
|
|
205
|
+
@mock.patch('git_p4son.sync_split.git_last_sync',
|
|
206
|
+
return_value=LastSync(changelist=100, commit='abc123'))
|
|
207
|
+
def test_invalid_changelist_aborts(self, _last_sync):
|
|
208
|
+
rc = sync_split_command(self._args(changelist='not-a-number'))
|
|
209
|
+
self.assertEqual(rc, 1)
|
|
210
|
+
|
|
211
|
+
@mock.patch('git_p4son.sync_split.get_p4_user', return_value=None)
|
|
212
|
+
@mock.patch('git_p4son.sync_split.get_latest_changelist', return_value=103)
|
|
213
|
+
@mock.patch('git_p4son.sync_split.git_last_sync',
|
|
214
|
+
return_value=LastSync(changelist=100, commit='abc123'))
|
|
215
|
+
def test_unknown_p4_user_aborts(self, _last_sync, _latest, _user):
|
|
216
|
+
rc = sync_split_command(self._args())
|
|
217
|
+
self.assertEqual(rc, 1)
|
|
218
|
+
|
|
219
|
+
@mock.patch('git_p4son.sync_split.resolve_depot_root', return_value=None)
|
|
220
|
+
def test_unresolved_depot_root_aborts(self, _resolved):
|
|
221
|
+
rc = sync_split_command(self._args())
|
|
222
|
+
self.assertEqual(rc, 1)
|
|
223
|
+
|
|
224
|
+
@mock.patch('git_p4son.sync_split.sync_command', return_value=0)
|
|
225
|
+
@mock.patch('git_p4son.sync_split.get_submitted_changes')
|
|
226
|
+
@mock.patch('git_p4son.sync_split.get_p4_user')
|
|
227
|
+
@mock.patch('git_p4son.sync_split.get_latest_changelist', return_value=106)
|
|
228
|
+
@mock.patch('git_p4son.sync_split.git_last_sync',
|
|
229
|
+
return_value=LastSync(changelist=100, commit='abc123'))
|
|
230
|
+
def test_multiple_users_are_all_split_out(
|
|
231
|
+
self, _last_sync, _latest, mock_p4_user, mock_changes, _sync):
|
|
232
|
+
mock_changes.return_value = _changes(
|
|
233
|
+
(100, 'other'), (101, 'other'), (102, 'alice'),
|
|
234
|
+
(103, 'other'), (104, 'other'), (105, 'bob'), (106, 'other'))
|
|
235
|
+
args = self._args(user=['alice', 'bob'])
|
|
236
|
+
rc = sync_split_command(args)
|
|
237
|
+
self.assertEqual(rc, 0)
|
|
238
|
+
mock_p4_user.assert_not_called()
|
|
239
|
+
self.assertEqual(args.changelist, ['101', '102', '104', '105', '106'])
|
|
240
|
+
|
|
241
|
+
@mock.patch('git_p4son.sync_split.sync_command', return_value=0)
|
|
242
|
+
@mock.patch('git_p4son.sync_split.get_submitted_changes')
|
|
243
|
+
@mock.patch('git_p4son.sync_split.get_p4_user')
|
|
244
|
+
@mock.patch('git_p4son.sync_split.get_latest_changelist', return_value=103)
|
|
245
|
+
@mock.patch('git_p4son.sync_split.git_last_sync',
|
|
246
|
+
return_value=LastSync(changelist=100, commit='abc123'))
|
|
247
|
+
def test_repeated_user_names_are_collapsed(
|
|
248
|
+
self, _last_sync, _latest, _p4_user, mock_changes, _sync):
|
|
249
|
+
"""Naming the same user twice (in any casing) must not change the
|
|
250
|
+
resolved sequence."""
|
|
251
|
+
mock_changes.return_value = _changes(
|
|
252
|
+
(100, 'other'), (101, 'other'), (102, 'alice'))
|
|
253
|
+
args = self._args(user=['alice', 'Alice'])
|
|
254
|
+
rc = sync_split_command(args)
|
|
255
|
+
self.assertEqual(rc, 0)
|
|
256
|
+
self.assertEqual(args.changelist, ['101', '102', '103'])
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
if __name__ == '__main__':
|
|
260
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|