git-p4son 0.3.2__tar.gz → 0.3.3__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.2 → git_p4son-0.3.3}/PKG-INFO +19 -3
- {git_p4son-0.3.2 → git_p4son-0.3.3}/README.md +18 -2
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/__init__.py +1 -1
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/sync.py +181 -121
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/sync_split.py +15 -1
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son.egg-info/PKG-INFO +19 -3
- {git_p4son-0.3.2 → git_p4son-0.3.3}/pyproject.toml +1 -1
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_sync.py +11 -4
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_sync_split.py +181 -2
- {git_p4son-0.3.2 → git_p4son-0.3.3}/LICENSE +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/__main__.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/alias.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/changelist_store.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/cli.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/common.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/complete.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/completions/_git-p4son +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/completions/git-p4son.bash +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/completions/git-p4son.ps1 +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/config.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/git.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/hooks.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/init.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/lib.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/list_changes.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/log.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/new.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/perforce.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/review.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/state.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son/update.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son.egg-info/SOURCES.txt +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son.egg-info/dependency_links.txt +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son.egg-info/entry_points.txt +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/git_p4son.egg-info/top_level.txt +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/setup.cfg +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_alias.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_changelist_store.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_cli.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_common.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_complete.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_config.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_git.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_hooks.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_init.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_lib_changelist.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_lib_edit.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_lib_review.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_list_changes.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_log.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_new.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_perforce.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/tests/test_review.py +0 -0
- {git_p4son-0.3.2 → git_p4son-0.3.3}/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.
|
|
3
|
+
Version: 0.3.3
|
|
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
|
|
@@ -335,7 +335,10 @@ own submits one at a time. Repeat `--user` to split out several people's changel
|
|
|
335
335
|
It is equivalent to working out the numbers by hand and running
|
|
336
336
|
`git p4son sync <before-theirs> <theirs> ... head`, and it delegates to `sync` once the sequence is
|
|
337
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`.
|
|
338
|
+
all behave exactly as they do for `sync`. The clean-workspace checks and the pre-sync hooks are the one
|
|
339
|
+
thing that runs earlier: resolving the sequence costs several Perforce queries, so they go first, and a
|
|
340
|
+
dirty workspace or a hook that aborts the sync says so before that work. They run once for the whole sync,
|
|
341
|
+
and not at all for a `--dry-run` or when there is nothing to sync.
|
|
339
342
|
|
|
340
343
|
**Arguments:**
|
|
341
344
|
- `changelist` (optional): Changelist number to sync up to, or `head` for the latest. Omit to sync to the
|
|
@@ -639,12 +642,25 @@ clone, so no `chmod` is needed.
|
|
|
639
642
|
#### block-while-running (pre-sync)
|
|
640
643
|
|
|
641
644
|
Aborts the sync while a named process is running, so a sync cannot swap out assets that a running editor still
|
|
642
|
-
has loaded:
|
|
645
|
+
has loaded. Link it into your workspace's `pre-sync` hook directory - on macOS and Linux:
|
|
643
646
|
|
|
644
647
|
```sh
|
|
645
648
|
ln -s /path/to/git-p4son/examples/hooks/pre-sync/block-while-running.py .git-p4son/hooks/pre-sync/
|
|
646
649
|
```
|
|
647
650
|
|
|
651
|
+
On Windows, from PowerShell. Note that `-Path` is the link to create and `-Target` the existing file, the
|
|
652
|
+
reverse of the `ln -s` argument order, and that `-Target` should be absolute: a relative path is resolved
|
|
653
|
+
against the current directory and quietly produces a broken link.
|
|
654
|
+
|
|
655
|
+
```powershell
|
|
656
|
+
New-Item -ItemType SymbolicLink `
|
|
657
|
+
-Path "C:\p4\my-workspace\.git-p4son\hooks\pre-sync\block-while-running.py" `
|
|
658
|
+
-Target "C:\src\git-p4son\examples\hooks\pre-sync\block-while-running.py"
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
Creating a symlink on Windows needs either an elevated shell or Developer Mode enabled (Settings > System >
|
|
662
|
+
For developers). Without one of those, copy the file instead and re-copy it whenever the clone updates.
|
|
663
|
+
|
|
648
664
|
Which processes block a sync is read from your workspace's own `.git-p4son/config.toml`, so a symlinked hook
|
|
649
665
|
stays per-project:
|
|
650
666
|
|
|
@@ -312,7 +312,10 @@ own submits one at a time. Repeat `--user` to split out several people's changel
|
|
|
312
312
|
It is equivalent to working out the numbers by hand and running
|
|
313
313
|
`git p4son sync <before-theirs> <theirs> ... head`, and it delegates to `sync` once the sequence is
|
|
314
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`.
|
|
315
|
+
all behave exactly as they do for `sync`. The clean-workspace checks and the pre-sync hooks are the one
|
|
316
|
+
thing that runs earlier: resolving the sequence costs several Perforce queries, so they go first, and a
|
|
317
|
+
dirty workspace or a hook that aborts the sync says so before that work. They run once for the whole sync,
|
|
318
|
+
and not at all for a `--dry-run` or when there is nothing to sync.
|
|
316
319
|
|
|
317
320
|
**Arguments:**
|
|
318
321
|
- `changelist` (optional): Changelist number to sync up to, or `head` for the latest. Omit to sync to the
|
|
@@ -616,12 +619,25 @@ clone, so no `chmod` is needed.
|
|
|
616
619
|
#### block-while-running (pre-sync)
|
|
617
620
|
|
|
618
621
|
Aborts the sync while a named process is running, so a sync cannot swap out assets that a running editor still
|
|
619
|
-
has loaded:
|
|
622
|
+
has loaded. Link it into your workspace's `pre-sync` hook directory - on macOS and Linux:
|
|
620
623
|
|
|
621
624
|
```sh
|
|
622
625
|
ln -s /path/to/git-p4son/examples/hooks/pre-sync/block-while-running.py .git-p4son/hooks/pre-sync/
|
|
623
626
|
```
|
|
624
627
|
|
|
628
|
+
On Windows, from PowerShell. Note that `-Path` is the link to create and `-Target` the existing file, the
|
|
629
|
+
reverse of the `ln -s` argument order, and that `-Target` should be absolute: a relative path is resolved
|
|
630
|
+
against the current directory and quietly produces a broken link.
|
|
631
|
+
|
|
632
|
+
```powershell
|
|
633
|
+
New-Item -ItemType SymbolicLink `
|
|
634
|
+
-Path "C:\p4\my-workspace\.git-p4son\hooks\pre-sync\block-while-running.py" `
|
|
635
|
+
-Target "C:\src\git-p4son\examples\hooks\pre-sync\block-while-running.py"
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
Creating a symlink on Windows needs either an elevated shell or Developer Mode enabled (Settings > System >
|
|
639
|
+
For developers). Without one of those, copy the file instead and re-copy it whenever the clone updates.
|
|
640
|
+
|
|
625
641
|
Which processes block a sync is read from your workspace's own `.git-p4son/config.toml`, so a symlinked hook
|
|
626
642
|
stays per-project:
|
|
627
643
|
|
|
@@ -36,6 +36,9 @@ from .perforce import (
|
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
LAST_SYNCED_LABEL = 'last synced'
|
|
40
|
+
|
|
41
|
+
|
|
39
42
|
@dataclass
|
|
40
43
|
class LastSync:
|
|
41
44
|
"""Info about the most recent p4son sync commit."""
|
|
@@ -524,11 +527,50 @@ def _handle_clobber_warning(clobber: bool, workspace_dir: str) -> bool:
|
|
|
524
527
|
return True
|
|
525
528
|
|
|
526
529
|
|
|
530
|
+
def _check_git_workspace_clean(workspace_dir: str) -> bool:
|
|
531
|
+
"""Report whether the git workspace has no uncommitted changes."""
|
|
532
|
+
log.heading('Checking git workspace')
|
|
533
|
+
dirty_files = get_dirty_files(workspace_dir)
|
|
534
|
+
if dirty_files:
|
|
535
|
+
for filename, change in dirty_files:
|
|
536
|
+
log.file_change(filename, change)
|
|
537
|
+
log.error('Workspace is not clean')
|
|
538
|
+
return False
|
|
539
|
+
log.success('clean')
|
|
540
|
+
return True
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _check_p4_workspace_clean(depot_root: str, workspace_dir: str) -> bool:
|
|
544
|
+
"""Report whether the p4 workspace has no git-tracked files opened.
|
|
545
|
+
|
|
546
|
+
Files opened in Perforce that git does not track cannot collide with the
|
|
547
|
+
commit a sync makes, so they only warrant a warning.
|
|
548
|
+
"""
|
|
549
|
+
log.heading('Checking p4 workspace')
|
|
550
|
+
opened_files = p4_get_opened_files(depot_root, workspace_dir)
|
|
551
|
+
tracked_opened_files = [
|
|
552
|
+
(filename, change)
|
|
553
|
+
for filename, change in opened_files
|
|
554
|
+
if is_file_tracked(filename, workspace_dir)
|
|
555
|
+
]
|
|
556
|
+
if tracked_opened_files:
|
|
557
|
+
for filename, change in tracked_opened_files:
|
|
558
|
+
log.file_change(filename, change)
|
|
559
|
+
log.error('Workspace has p4-opened files tracked by git')
|
|
560
|
+
return False
|
|
561
|
+
if opened_files:
|
|
562
|
+
log.warning(
|
|
563
|
+
f'Ignoring {len(opened_files)} p4-opened files not tracked by git')
|
|
564
|
+
else:
|
|
565
|
+
log.success('Clean')
|
|
566
|
+
return True
|
|
567
|
+
|
|
568
|
+
|
|
527
569
|
def _run_pre_sync_hooks(workspace_dir: str, invocation_dir: str) -> bool:
|
|
528
570
|
"""Run pre-sync hooks; return False when any hook fails, aborting the sync.
|
|
529
571
|
|
|
530
|
-
|
|
531
|
-
|
|
572
|
+
All hooks run (they are independent), then the sync is aborted if any
|
|
573
|
+
returned a non-zero code.
|
|
532
574
|
"""
|
|
533
575
|
results = run_hooks('pre-sync', workspace_dir, invocation_dir)
|
|
534
576
|
if any(result.returncode != 0 for result in results):
|
|
@@ -537,6 +579,26 @@ def _run_pre_sync_hooks(workspace_dir: str, invocation_dir: str) -> bool:
|
|
|
537
579
|
return True
|
|
538
580
|
|
|
539
581
|
|
|
582
|
+
def sync_preflight(depot_root: str, workspace_dir: str, invocation_dir: str,
|
|
583
|
+
already_done: bool = False) -> bool:
|
|
584
|
+
"""Gate a sync: both workspaces clean, then the pre-sync hooks.
|
|
585
|
+
|
|
586
|
+
Returns False when the sync must not go ahead. Callers run this once they
|
|
587
|
+
know a sync will actually be attempted, so that a run with nothing to sync
|
|
588
|
+
neither checks the workspaces nor fires the hooks. already_done covers a
|
|
589
|
+
caller that ran it before delegating here: sync-split runs it up front,
|
|
590
|
+
ahead of the p4 queries that resolve its changelist sequence, so neither
|
|
591
|
+
the checks nor the hooks happen twice.
|
|
592
|
+
"""
|
|
593
|
+
if already_done:
|
|
594
|
+
return True
|
|
595
|
+
if not _check_git_workspace_clean(workspace_dir):
|
|
596
|
+
return False
|
|
597
|
+
if not _check_p4_workspace_clean(depot_root, workspace_dir):
|
|
598
|
+
return False
|
|
599
|
+
return _run_pre_sync_hooks(workspace_dir, invocation_dir)
|
|
600
|
+
|
|
601
|
+
|
|
540
602
|
def _sync_pass(changelist: int, label: str, depot_root: str,
|
|
541
603
|
workspace_dir: str, pre_sync_head_commit: str, temp_root: str,
|
|
542
604
|
uses_crlf: bool, clobber: bool) -> WritableSyncFileSet:
|
|
@@ -587,10 +649,82 @@ def resolve_depot_root(workspace_dir: str) -> ResolvedDepot | None:
|
|
|
587
649
|
return ResolvedDepot(depot_root=depot_root, client_spec=client_spec)
|
|
588
650
|
|
|
589
651
|
|
|
652
|
+
def _latest_target(depot_root: str, workspace_dir: str) -> tuple[int, str]:
|
|
653
|
+
"""Look up the latest submitted changelist as a sync target."""
|
|
654
|
+
log.heading('Finding latest changelist')
|
|
655
|
+
latest = get_latest_changelist(depot_root, workspace_dir)
|
|
656
|
+
log.success(f'CL {latest}')
|
|
657
|
+
return (latest, 'latest')
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _resolve_sync_targets(
|
|
661
|
+
raw: list[str], last_sync: LastSync | None, depot_root: str,
|
|
662
|
+
workspace_dir: str, force: bool) -> list[tuple[int, str]] | None:
|
|
663
|
+
"""Resolve the ordered (changelist, label) targets a sync will visit.
|
|
664
|
+
|
|
665
|
+
Returns None when the arguments are invalid, and an empty list when the
|
|
666
|
+
workspace already sits at the only changelist asked for.
|
|
667
|
+
"""
|
|
668
|
+
lowered = [c.lower() for c in raw]
|
|
669
|
+
|
|
670
|
+
# "head" resolves to the latest changelist, the largest in an increasing
|
|
671
|
+
# sequence, so it may only appear as the final target.
|
|
672
|
+
if 'head' in lowered and lowered.index('head') != len(lowered) - 1:
|
|
673
|
+
log.error('The "head" keyword must come last')
|
|
674
|
+
return None
|
|
675
|
+
|
|
676
|
+
if not raw:
|
|
677
|
+
targets = [_latest_target(depot_root, workspace_dir)]
|
|
678
|
+
else:
|
|
679
|
+
targets = []
|
|
680
|
+
for c in raw:
|
|
681
|
+
if c.lower() == 'head':
|
|
682
|
+
targets.append(_latest_target(depot_root, workspace_dir))
|
|
683
|
+
else:
|
|
684
|
+
try:
|
|
685
|
+
targets.append((int(c), 'specified'))
|
|
686
|
+
except ValueError:
|
|
687
|
+
log.error(f'Invalid changelist number: {c}')
|
|
688
|
+
return None
|
|
689
|
+
|
|
690
|
+
# Targets are synced in the given order, so they must be strictly
|
|
691
|
+
# increasing: no syncing back and forth.
|
|
692
|
+
numbers = [cl for cl, _ in targets]
|
|
693
|
+
for prev, curr in zip(numbers, numbers[1:]):
|
|
694
|
+
if curr <= prev:
|
|
695
|
+
log.error('Changelists must be strictly increasing, '
|
|
696
|
+
f'got {prev} then {curr}')
|
|
697
|
+
return None
|
|
698
|
+
|
|
699
|
+
# Syncing to a changelist older than the current one needs --force.
|
|
700
|
+
# Targets are strictly increasing, so the smallest is numbers[0]; an
|
|
701
|
+
# equal-to-current changelist (dropped just below) is not "older".
|
|
702
|
+
if last_sync and numbers[0] < last_sync.changelist:
|
|
703
|
+
if not force:
|
|
704
|
+
log.error(
|
|
705
|
+
f'Cannot sync to CL {numbers[0]} '
|
|
706
|
+
f'(currently at CL {last_sync.changelist}) without --force.')
|
|
707
|
+
return None
|
|
708
|
+
log.warning(
|
|
709
|
+
f'Syncing to older CL {numbers[0]} '
|
|
710
|
+
f'(currently at CL {last_sync.changelist}) with --force')
|
|
711
|
+
|
|
712
|
+
# Drop a target equal to the last synced changelist: we are already
|
|
713
|
+
# there, so there is nothing to sync or commit for it. An explicitly
|
|
714
|
+
# requested *older* changelist is kept and synced as the user asked.
|
|
715
|
+
if last_sync and last_sync.changelist in numbers:
|
|
716
|
+
log.info(f'Skipping CL {last_sync.changelist} (already synced)')
|
|
717
|
+
targets = [(cl, lbl) for cl, lbl in targets
|
|
718
|
+
if cl != last_sync.changelist]
|
|
719
|
+
|
|
720
|
+
return targets
|
|
721
|
+
|
|
722
|
+
|
|
590
723
|
def sync_command(args: argparse.Namespace) -> int:
|
|
591
724
|
"""Execute the sync command."""
|
|
592
725
|
workspace_dir = args.workspace_dir
|
|
593
726
|
invocation_dir = vars(args).get('invocation_dir', workspace_dir)
|
|
727
|
+
preflight_done = vars(args).get('preflight_done', False)
|
|
594
728
|
|
|
595
729
|
resolved = resolve_depot_root(workspace_dir)
|
|
596
730
|
if resolved is None:
|
|
@@ -598,44 +732,40 @@ def sync_command(args: argparse.Namespace) -> int:
|
|
|
598
732
|
depot_root = resolved.depot_root
|
|
599
733
|
client_spec = resolved.client_spec
|
|
600
734
|
|
|
601
|
-
log.heading('
|
|
602
|
-
dirty_files = get_dirty_files(workspace_dir)
|
|
603
|
-
if dirty_files:
|
|
604
|
-
for filename, change in dirty_files:
|
|
605
|
-
log.file_change(filename, change)
|
|
606
|
-
log.error('Workspace is not clean')
|
|
607
|
-
return 1
|
|
608
|
-
log.success('clean')
|
|
609
|
-
|
|
610
|
-
log.heading('Checking p4 workspace')
|
|
611
|
-
opened_files = p4_get_opened_files(depot_root, workspace_dir)
|
|
612
|
-
tracked_opened_files = [
|
|
613
|
-
(filename, change)
|
|
614
|
-
for filename, change in opened_files
|
|
615
|
-
if is_file_tracked(filename, workspace_dir)
|
|
616
|
-
]
|
|
617
|
-
if tracked_opened_files:
|
|
618
|
-
for filename, change in tracked_opened_files:
|
|
619
|
-
log.file_change(filename, change)
|
|
620
|
-
log.error('Workspace has p4-opened files tracked by git')
|
|
621
|
-
return 1
|
|
622
|
-
if opened_files:
|
|
623
|
-
log.warning(
|
|
624
|
-
f'Ignoring {len(opened_files)} p4-opened files not tracked by git')
|
|
625
|
-
else:
|
|
626
|
-
log.success('Clean')
|
|
627
|
-
|
|
628
|
-
last_changelist_label = 'last synced'
|
|
629
|
-
log.heading(f'Finding {last_changelist_label} changelist')
|
|
735
|
+
log.heading(f'Finding {LAST_SYNCED_LABEL} changelist')
|
|
630
736
|
last_sync = git_last_sync(workspace_dir)
|
|
631
737
|
if last_sync:
|
|
632
738
|
log.success(f'CL {last_sync.changelist}')
|
|
633
739
|
else:
|
|
634
740
|
log.warning('No previous sync found')
|
|
635
741
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
742
|
+
# Work out what the sync will do before touching either workspace, so a
|
|
743
|
+
# run with nothing to sync gets through without checks or hooks.
|
|
744
|
+
lowered = [c.lower() for c in args.changelist]
|
|
745
|
+
|
|
746
|
+
# "last-synced" is a keyword that only makes sense on its own. It re-syncs
|
|
747
|
+
# the changelist git is already at rather than advancing, so it resolves
|
|
748
|
+
# to no targets and takes its own path further down.
|
|
749
|
+
resync_last_synced = 'last-synced' in lowered
|
|
750
|
+
targets: list[tuple[int, str]] = []
|
|
751
|
+
if resync_last_synced:
|
|
752
|
+
if lowered != ['last-synced']:
|
|
753
|
+
log.error('The "last-synced" keyword cannot be combined with '
|
|
754
|
+
'other changelists')
|
|
755
|
+
return 1
|
|
756
|
+
if not last_sync:
|
|
757
|
+
log.error('No previous sync found, cannot use "last-synced"')
|
|
758
|
+
return 1
|
|
759
|
+
else:
|
|
760
|
+
resolved_targets = _resolve_sync_targets(
|
|
761
|
+
args.changelist, last_sync, depot_root, workspace_dir, args.force)
|
|
762
|
+
if resolved_targets is None:
|
|
763
|
+
return 1
|
|
764
|
+
if not resolved_targets:
|
|
765
|
+
log.info('Already synced, nothing to do.')
|
|
766
|
+
log.heading('Skipping post-sync hooks')
|
|
767
|
+
return 0
|
|
768
|
+
targets = resolved_targets
|
|
639
769
|
|
|
640
770
|
# Workspace line ending governs how staged git content is normalized so
|
|
641
771
|
# the post-sync merge doesn't conflict on LF-vs-CRLF differences alone.
|
|
@@ -645,112 +775,42 @@ def sync_command(args: argparse.Namespace) -> int:
|
|
|
645
775
|
uses_crlf = bool(client_spec and client_spec.uses_crlf)
|
|
646
776
|
clobber = bool(client_spec and client_spec.clobber)
|
|
647
777
|
|
|
778
|
+
# Prompted before the preflight so declining here costs nothing: no
|
|
779
|
+
# workspace queries, and no hooks fired for a sync that is abandoned.
|
|
648
780
|
if not _handle_clobber_warning(clobber, workspace_dir):
|
|
649
781
|
return 1
|
|
650
782
|
|
|
783
|
+
# The single gate: both workspaces clean, then the pre-sync hooks. Runs
|
|
784
|
+
# once for the whole sync, covering the catch-up pass as well, and is
|
|
785
|
+
# skipped outright when the caller (sync-split) already ran it.
|
|
786
|
+
if not sync_preflight(depot_root, workspace_dir, invocation_dir,
|
|
787
|
+
preflight_done):
|
|
788
|
+
return 1
|
|
789
|
+
|
|
790
|
+
log.heading('Finding HEAD commit')
|
|
791
|
+
pre_sync_head_commit = get_head_commit(workspace_dir)
|
|
792
|
+
log.success(f'{pre_sync_head_commit}')
|
|
793
|
+
|
|
651
794
|
# Temp root for staging HEAD/baseline file content between classification
|
|
652
795
|
# and the post-sync merge. Cleaned up automatically on exit.
|
|
653
796
|
with tempfile.TemporaryDirectory(prefix='git-p4son-sync-') as temp_root:
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
# "last-synced" is a keyword that only makes sense on its own.
|
|
658
|
-
if 'last-synced' in lowered:
|
|
659
|
-
if lowered != ['last-synced']:
|
|
660
|
-
log.error('The "last-synced" keyword cannot be combined with '
|
|
661
|
-
'other changelists')
|
|
662
|
-
return 1
|
|
663
|
-
if not last_sync:
|
|
664
|
-
log.error('No previous sync found, cannot use "last-synced"')
|
|
665
|
-
return 1
|
|
666
|
-
if not _run_pre_sync_hooks(workspace_dir, invocation_dir):
|
|
667
|
-
return 1
|
|
668
|
-
_sync_pass(last_sync.changelist, last_changelist_label, depot_root,
|
|
797
|
+
if resync_last_synced:
|
|
798
|
+
_sync_pass(last_sync.changelist, LAST_SYNCED_LABEL, depot_root,
|
|
669
799
|
workspace_dir, pre_sync_head_commit, temp_root,
|
|
670
800
|
uses_crlf, clobber)
|
|
671
801
|
run_hooks('post-sync', workspace_dir, invocation_dir)
|
|
672
802
|
return 0
|
|
673
803
|
|
|
674
|
-
# "head" resolves to the latest changelist, the largest in an
|
|
675
|
-
# increasing sequence, so it may only appear as the final target.
|
|
676
|
-
if 'head' in lowered and lowered.index('head') != len(lowered) - 1:
|
|
677
|
-
log.error('The "head" keyword must come last')
|
|
678
|
-
return 1
|
|
679
|
-
|
|
680
|
-
# Resolve the ordered list of (changelist, label) targets to sync,
|
|
681
|
-
# replacing a trailing "head" with the concrete latest changelist so
|
|
682
|
-
# the increasing-order check below compares real numbers.
|
|
683
|
-
if not raw:
|
|
684
|
-
log.heading('Finding latest changelist')
|
|
685
|
-
latest = get_latest_changelist(depot_root, workspace_dir)
|
|
686
|
-
log.success(f'CL {latest}')
|
|
687
|
-
targets = [(latest, 'latest')]
|
|
688
|
-
else:
|
|
689
|
-
targets = []
|
|
690
|
-
for c in raw:
|
|
691
|
-
if c.lower() == 'head':
|
|
692
|
-
log.heading('Finding latest changelist')
|
|
693
|
-
latest = get_latest_changelist(depot_root, workspace_dir)
|
|
694
|
-
log.success(f'CL {latest}')
|
|
695
|
-
targets.append((latest, 'latest'))
|
|
696
|
-
else:
|
|
697
|
-
try:
|
|
698
|
-
targets.append((int(c), 'specified'))
|
|
699
|
-
except ValueError:
|
|
700
|
-
log.error(f'Invalid changelist number: {c}')
|
|
701
|
-
return 1
|
|
702
|
-
|
|
703
|
-
# Targets are synced in the given order, so they must be strictly
|
|
704
|
-
# increasing: no syncing back and forth.
|
|
705
|
-
numbers = [cl for cl, _ in targets]
|
|
706
|
-
for prev, curr in zip(numbers, numbers[1:]):
|
|
707
|
-
if curr <= prev:
|
|
708
|
-
log.error('Changelists must be strictly increasing, '
|
|
709
|
-
f'got {prev} then {curr}')
|
|
710
|
-
return 1
|
|
711
|
-
|
|
712
|
-
last_changelist = last_sync.changelist if last_sync else None
|
|
713
|
-
|
|
714
|
-
# Syncing to a changelist older than the current one needs --force.
|
|
715
|
-
# Targets are strictly increasing, so the smallest is numbers[0]; an
|
|
716
|
-
# equal-to-current changelist (dropped just below) is not "older".
|
|
717
|
-
if last_sync and numbers[0] < last_sync.changelist:
|
|
718
|
-
if not args.force:
|
|
719
|
-
log.error(
|
|
720
|
-
f'Cannot sync to CL {numbers[0]} '
|
|
721
|
-
f'(currently at CL {last_sync.changelist}) without --force.')
|
|
722
|
-
return 1
|
|
723
|
-
log.warning(
|
|
724
|
-
f'Syncing to older CL {numbers[0]} '
|
|
725
|
-
f'(currently at CL {last_sync.changelist}) with --force')
|
|
726
|
-
|
|
727
|
-
# Drop a target equal to the last synced changelist: we are already
|
|
728
|
-
# there, so there is nothing to sync or commit for it. An explicitly
|
|
729
|
-
# requested *older* changelist is kept and synced as the user asked.
|
|
730
|
-
if last_changelist is not None and last_changelist in numbers:
|
|
731
|
-
log.info(f'Skipping CL {last_changelist} (already synced)')
|
|
732
|
-
targets = [(cl, lbl) for cl, lbl in targets
|
|
733
|
-
if cl != last_changelist]
|
|
734
|
-
|
|
735
|
-
if not targets:
|
|
736
|
-
log.info('Already synced, nothing to do.')
|
|
737
|
-
log.heading('Skipping post-sync hooks')
|
|
738
|
-
return 0
|
|
739
|
-
|
|
740
|
-
# Pre-sync hooks run once, before syncing any changelist (including the
|
|
741
|
-
# catch-up pass), and abort the whole sync if any of them fail.
|
|
742
|
-
if not _run_pre_sync_hooks(workspace_dir, invocation_dir):
|
|
743
|
-
return 1
|
|
744
|
-
|
|
745
804
|
all_changed: list[ChangedFile] = []
|
|
746
805
|
all_ignored: list[str] = []
|
|
806
|
+
last_changelist = last_sync.changelist if last_sync else None
|
|
747
807
|
|
|
748
808
|
# Catch-up pass to the last synced changelist. This makes locally
|
|
749
809
|
# modified (writable) files read-only and stages their content so the
|
|
750
810
|
# post-sync merge can restore local changes; its result is folded into
|
|
751
811
|
# the first target commit.
|
|
752
812
|
if last_changelist is not None:
|
|
753
|
-
prep = _sync_pass(last_changelist,
|
|
813
|
+
prep = _sync_pass(last_changelist, LAST_SYNCED_LABEL,
|
|
754
814
|
depot_root, workspace_dir, pre_sync_head_commit,
|
|
755
815
|
temp_root, uses_crlf, clobber)
|
|
756
816
|
all_changed.extend(prep.changed)
|
|
@@ -11,7 +11,9 @@ from .perforce import (
|
|
|
11
11
|
get_p4_user,
|
|
12
12
|
get_submitted_changes,
|
|
13
13
|
)
|
|
14
|
-
from .sync import
|
|
14
|
+
from .sync import (
|
|
15
|
+
git_last_sync, resolve_depot_root, sync_command, sync_preflight,
|
|
16
|
+
)
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
def build_sync_targets(changes: list[P4Change], users: list[str],
|
|
@@ -70,6 +72,7 @@ def _resolve_users(args: argparse.Namespace, workspace_dir: str) -> list[str]:
|
|
|
70
72
|
def sync_split_command(args: argparse.Namespace) -> int:
|
|
71
73
|
"""Execute the sync-split command."""
|
|
72
74
|
workspace_dir = args.workspace_dir
|
|
75
|
+
invocation_dir = vars(args).get('invocation_dir', workspace_dir)
|
|
73
76
|
|
|
74
77
|
resolved = resolve_depot_root(workspace_dir)
|
|
75
78
|
if resolved is None:
|
|
@@ -108,6 +111,17 @@ def sync_split_command(args: argparse.Namespace) -> int:
|
|
|
108
111
|
'changelist.')
|
|
109
112
|
return 1
|
|
110
113
|
|
|
114
|
+
# The workspace checks and pre-sync hooks run here rather than being left
|
|
115
|
+
# to sync_command, which only reaches them after all of the resolution
|
|
116
|
+
# below. That resolution costs several p4 round trips, so a dirty
|
|
117
|
+
# workspace or a hook vetoing the sync should get to say so first.
|
|
118
|
+
# sync_command is told it is done so none of it happens twice, and a dry
|
|
119
|
+
# run syncs nothing, so it skips the gate entirely.
|
|
120
|
+
if not args.dry_run:
|
|
121
|
+
if not sync_preflight(depot_root, workspace_dir, invocation_dir):
|
|
122
|
+
return 1
|
|
123
|
+
args.preflight_done = True
|
|
124
|
+
|
|
111
125
|
users = _resolve_users(args, workspace_dir)
|
|
112
126
|
if not users:
|
|
113
127
|
return 1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-p4son
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
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
|
|
@@ -335,7 +335,10 @@ own submits one at a time. Repeat `--user` to split out several people's changel
|
|
|
335
335
|
It is equivalent to working out the numbers by hand and running
|
|
336
336
|
`git p4son sync <before-theirs> <theirs> ... head`, and it delegates to `sync` once the sequence is
|
|
337
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`.
|
|
338
|
+
all behave exactly as they do for `sync`. The clean-workspace checks and the pre-sync hooks are the one
|
|
339
|
+
thing that runs earlier: resolving the sequence costs several Perforce queries, so they go first, and a
|
|
340
|
+
dirty workspace or a hook that aborts the sync says so before that work. They run once for the whole sync,
|
|
341
|
+
and not at all for a `--dry-run` or when there is nothing to sync.
|
|
339
342
|
|
|
340
343
|
**Arguments:**
|
|
341
344
|
- `changelist` (optional): Changelist number to sync up to, or `head` for the latest. Omit to sync to the
|
|
@@ -639,12 +642,25 @@ clone, so no `chmod` is needed.
|
|
|
639
642
|
#### block-while-running (pre-sync)
|
|
640
643
|
|
|
641
644
|
Aborts the sync while a named process is running, so a sync cannot swap out assets that a running editor still
|
|
642
|
-
has loaded:
|
|
645
|
+
has loaded. Link it into your workspace's `pre-sync` hook directory - on macOS and Linux:
|
|
643
646
|
|
|
644
647
|
```sh
|
|
645
648
|
ln -s /path/to/git-p4son/examples/hooks/pre-sync/block-while-running.py .git-p4son/hooks/pre-sync/
|
|
646
649
|
```
|
|
647
650
|
|
|
651
|
+
On Windows, from PowerShell. Note that `-Path` is the link to create and `-Target` the existing file, the
|
|
652
|
+
reverse of the `ln -s` argument order, and that `-Target` should be absolute: a relative path is resolved
|
|
653
|
+
against the current directory and quietly produces a broken link.
|
|
654
|
+
|
|
655
|
+
```powershell
|
|
656
|
+
New-Item -ItemType SymbolicLink `
|
|
657
|
+
-Path "C:\p4\my-workspace\.git-p4son\hooks\pre-sync\block-while-running.py" `
|
|
658
|
+
-Target "C:\src\git-p4son\examples\hooks\pre-sync\block-while-running.py"
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
Creating a symlink on Windows needs either an elevated shell or Developer Mode enabled (Settings > System >
|
|
662
|
+
For developers). Without one of those, copy the file instead and re-copy it whenever the clone updates.
|
|
663
|
+
|
|
648
664
|
Which processes block a sync is read from your workspace's own `.git-p4son/config.toml`, so a symlinked hook
|
|
649
665
|
stays per-project:
|
|
650
666
|
|
|
@@ -866,21 +866,23 @@ class TestSyncCommand(unittest.TestCase):
|
|
|
866
866
|
rc = sync_command(args)
|
|
867
867
|
self.assertEqual(rc, 1)
|
|
868
868
|
|
|
869
|
+
@mock.patch('git_p4son.sync.git_last_sync', return_value=None)
|
|
869
870
|
@mock.patch('git_p4son.sync.get_dirty_files',
|
|
870
871
|
return_value=[('file.txt', 'modify')])
|
|
871
872
|
@mock.patch('git_p4son.sync.get_depot_root', return_value='//myclient')
|
|
872
|
-
def test_dirty_git_workspace_aborts(self, _depot, _git_clean):
|
|
873
|
+
def test_dirty_git_workspace_aborts(self, _depot, _git_clean, _last_sync):
|
|
873
874
|
args = mock.Mock(changelist=['100'], force=False, workspace_dir='/ws')
|
|
874
875
|
rc = sync_command(args)
|
|
875
876
|
self.assertEqual(rc, 1)
|
|
876
877
|
|
|
878
|
+
@mock.patch('git_p4son.sync.git_last_sync', return_value=None)
|
|
877
879
|
@mock.patch('git_p4son.sync.is_file_tracked', return_value=True)
|
|
878
880
|
@mock.patch('git_p4son.sync.get_dirty_files', return_value=[])
|
|
879
881
|
@mock.patch('git_p4son.sync.p4_get_opened_files',
|
|
880
882
|
return_value=[('foo.txt', 'modify')])
|
|
881
883
|
@mock.patch('git_p4son.sync.get_depot_root', return_value='//myclient')
|
|
882
884
|
def test_dirty_p4_workspace_aborts(self, _depot, _p4clean, _git_clean,
|
|
883
|
-
_tracked):
|
|
885
|
+
_tracked, _last_sync):
|
|
884
886
|
args = mock.Mock(changelist=['100'], force=False, workspace_dir='/ws')
|
|
885
887
|
rc = sync_command(args)
|
|
886
888
|
self.assertEqual(rc, 1)
|
|
@@ -943,12 +945,13 @@ class TestSyncCommand(unittest.TestCase):
|
|
|
943
945
|
|
|
944
946
|
@mock.patch('git_p4son.sync._handle_clobber_warning', return_value=False)
|
|
945
947
|
@mock.patch('git_p4son.sync.get_head_commit', return_value='def456')
|
|
948
|
+
@mock.patch('git_p4son.sync.get_latest_changelist', return_value=200)
|
|
946
949
|
@mock.patch('git_p4son.sync.git_last_sync', return_value=None)
|
|
947
950
|
@mock.patch('git_p4son.sync.p4_get_opened_files', return_value=[])
|
|
948
951
|
@mock.patch('git_p4son.sync.get_dirty_files', return_value=[])
|
|
949
952
|
@mock.patch('git_p4son.sync.get_depot_root', return_value='//myclient')
|
|
950
953
|
def test_aborts_when_clobber_warning_declined(
|
|
951
|
-
self, _depot, _git_clean, _p4clean, _last_sync, _head,
|
|
954
|
+
self, _depot, _git_clean, _p4clean, _last_sync, _latest, _head,
|
|
952
955
|
_warn):
|
|
953
956
|
args = mock.Mock(changelist=[], force=False, workspace_dir='/ws')
|
|
954
957
|
rc = sync_command(args)
|
|
@@ -961,12 +964,16 @@ class TestSyncCommand(unittest.TestCase):
|
|
|
961
964
|
@mock.patch('git_p4son.sync.p4_get_opened_files', return_value=[])
|
|
962
965
|
@mock.patch('git_p4son.sync.get_dirty_files', return_value=[])
|
|
963
966
|
@mock.patch('git_p4son.sync.get_depot_root', return_value='//myclient')
|
|
964
|
-
def test_same_cl_is_noop(self, _depot,
|
|
967
|
+
def test_same_cl_is_noop(self, _depot, mock_git_clean, mock_p4clean,
|
|
965
968
|
mock_last_sync, _head, mock_run_hooks, mock_log):
|
|
966
969
|
mock_last_sync.return_value = LastSync(changelist=100, commit='abc')
|
|
967
970
|
args = mock.Mock(changelist=['100'], force=False, workspace_dir='/ws')
|
|
968
971
|
rc = sync_command(args)
|
|
969
972
|
self.assertEqual(rc, 0)
|
|
973
|
+
# Nothing to sync, so the whole preflight is skipped: no workspace
|
|
974
|
+
# queries and no hooks, not merely no hooks.
|
|
975
|
+
mock_git_clean.assert_not_called()
|
|
976
|
+
mock_p4clean.assert_not_called()
|
|
970
977
|
mock_run_hooks.assert_not_called()
|
|
971
978
|
mock_log.heading.assert_any_call('Skipping post-sync hooks')
|
|
972
979
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"""Tests for git_p4son.sync_split module."""
|
|
2
2
|
|
|
3
|
+
import argparse
|
|
3
4
|
import unittest
|
|
4
5
|
from unittest import mock
|
|
5
6
|
|
|
6
7
|
from git_p4son.perforce import P4Change
|
|
7
|
-
from git_p4son.sync import LastSync, ResolvedDepot
|
|
8
|
+
from git_p4son.sync import LastSync, ResolvedDepot, sync_preflight
|
|
8
9
|
from git_p4son.sync_split import (
|
|
9
10
|
build_sync_targets, sync_split_command,
|
|
10
11
|
)
|
|
12
|
+
from tests.helpers import make_run_result
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
def _changes(*pairs):
|
|
@@ -97,7 +99,7 @@ class TestBuildSyncTargets(unittest.TestCase):
|
|
|
97
99
|
self.assertEqual(targets, [101, 102, 103, 104, 105])
|
|
98
100
|
|
|
99
101
|
|
|
100
|
-
class
|
|
102
|
+
class TestSyncSplitCommand(unittest.TestCase):
|
|
101
103
|
|
|
102
104
|
def setUp(self):
|
|
103
105
|
patcher = mock.patch(
|
|
@@ -106,6 +108,11 @@ class TestSyncMineCommand(unittest.TestCase):
|
|
|
106
108
|
client_spec=None))
|
|
107
109
|
patcher.start()
|
|
108
110
|
self.addCleanup(patcher.stop)
|
|
111
|
+
# The gate has its own tests; here it must simply not shell out.
|
|
112
|
+
gate = mock.patch('git_p4son.sync_split.sync_preflight',
|
|
113
|
+
return_value=True)
|
|
114
|
+
gate.start()
|
|
115
|
+
self.addCleanup(gate.stop)
|
|
109
116
|
|
|
110
117
|
def _args(self, **overrides):
|
|
111
118
|
values = dict(changelist=None, user=None, dry_run=False,
|
|
@@ -221,6 +228,178 @@ class TestSyncMineCommand(unittest.TestCase):
|
|
|
221
228
|
rc = sync_split_command(self._args())
|
|
222
229
|
self.assertEqual(rc, 1)
|
|
223
230
|
|
|
231
|
+
|
|
232
|
+
class TestSyncSplitPreSyncHooks(unittest.TestCase):
|
|
233
|
+
"""The hooks must fire before the p4 round trips they exist to save,
|
|
234
|
+
exactly once, and never for a run that syncs nothing."""
|
|
235
|
+
|
|
236
|
+
def setUp(self):
|
|
237
|
+
patcher = mock.patch(
|
|
238
|
+
'git_p4son.sync_split.resolve_depot_root',
|
|
239
|
+
return_value=ResolvedDepot(depot_root='//myclient',
|
|
240
|
+
client_spec=None))
|
|
241
|
+
patcher.start()
|
|
242
|
+
self.addCleanup(patcher.stop)
|
|
243
|
+
|
|
244
|
+
def _args(self, **overrides):
|
|
245
|
+
values = dict(changelist=None, user=None, dry_run=False,
|
|
246
|
+
workspace_dir='/ws', invocation_dir='/invoked')
|
|
247
|
+
values.update(overrides)
|
|
248
|
+
return argparse.Namespace(**values)
|
|
249
|
+
|
|
250
|
+
@mock.patch('git_p4son.sync_split.sync_command', return_value=0)
|
|
251
|
+
@mock.patch('git_p4son.sync_split.get_submitted_changes')
|
|
252
|
+
@mock.patch('git_p4son.sync_split.get_p4_user', return_value='me')
|
|
253
|
+
@mock.patch('git_p4son.sync_split.get_latest_changelist', return_value=103)
|
|
254
|
+
@mock.patch('git_p4son.sync_split.git_last_sync',
|
|
255
|
+
return_value=LastSync(changelist=100, commit='abc123'))
|
|
256
|
+
@mock.patch('git_p4son.sync_split.sync_preflight', return_value=True)
|
|
257
|
+
def test_hooks_run_before_the_expensive_queries(
|
|
258
|
+
self, mock_hooks, _last_sync, _latest, mock_user, mock_changes,
|
|
259
|
+
_sync):
|
|
260
|
+
"""The p4 user and changes lookups must not happen until the hooks
|
|
261
|
+
have had their say."""
|
|
262
|
+
order = []
|
|
263
|
+
mock_hooks.side_effect = lambda *a, **k: order.append('hooks') or True
|
|
264
|
+
mock_user.side_effect = lambda *a: order.append('p4 info') or 'me'
|
|
265
|
+
mock_changes.side_effect = lambda *a: order.append('p4 changes') or []
|
|
266
|
+
rc = sync_split_command(self._args())
|
|
267
|
+
self.assertEqual(rc, 0)
|
|
268
|
+
self.assertEqual(order, ['hooks', 'p4 info', 'p4 changes'])
|
|
269
|
+
|
|
270
|
+
@mock.patch('git_p4son.sync_split.sync_command', return_value=0)
|
|
271
|
+
@mock.patch('git_p4son.sync_split.get_submitted_changes', return_value=[])
|
|
272
|
+
@mock.patch('git_p4son.sync_split.get_p4_user', return_value='me')
|
|
273
|
+
@mock.patch('git_p4son.sync_split.get_latest_changelist', return_value=103)
|
|
274
|
+
@mock.patch('git_p4son.sync_split.git_last_sync',
|
|
275
|
+
return_value=LastSync(changelist=100, commit='abc123'))
|
|
276
|
+
@mock.patch('git_p4son.sync_split.sync_preflight', return_value=True)
|
|
277
|
+
def test_sync_command_is_told_not_to_rerun_them(
|
|
278
|
+
self, _hooks, _last_sync, _latest, _user, _changes, _sync):
|
|
279
|
+
args = self._args()
|
|
280
|
+
sync_split_command(args)
|
|
281
|
+
self.assertTrue(args.preflight_done)
|
|
282
|
+
|
|
283
|
+
@mock.patch('git_p4son.sync_split.get_submitted_changes')
|
|
284
|
+
@mock.patch('git_p4son.sync_split.get_p4_user')
|
|
285
|
+
@mock.patch('git_p4son.sync_split.sync_command')
|
|
286
|
+
@mock.patch('git_p4son.sync_split.get_latest_changelist', return_value=103)
|
|
287
|
+
@mock.patch('git_p4son.sync_split.git_last_sync',
|
|
288
|
+
return_value=LastSync(changelist=100, commit='abc123'))
|
|
289
|
+
@mock.patch('git_p4son.sync_split.sync_preflight', return_value=False)
|
|
290
|
+
def test_failing_hook_aborts_before_any_query(
|
|
291
|
+
self, _hooks, _last_sync, _latest, mock_sync, mock_user,
|
|
292
|
+
mock_changes):
|
|
293
|
+
rc = sync_split_command(self._args())
|
|
294
|
+
self.assertEqual(rc, 1)
|
|
295
|
+
mock_user.assert_not_called()
|
|
296
|
+
mock_changes.assert_not_called()
|
|
297
|
+
mock_sync.assert_not_called()
|
|
298
|
+
|
|
299
|
+
@mock.patch('git_p4son.sync_split.get_submitted_changes', return_value=[])
|
|
300
|
+
@mock.patch('git_p4son.sync_split.get_p4_user', return_value='me')
|
|
301
|
+
@mock.patch('git_p4son.sync_split.get_latest_changelist', return_value=103)
|
|
302
|
+
@mock.patch('git_p4son.sync_split.git_last_sync',
|
|
303
|
+
return_value=LastSync(changelist=100, commit='abc123'))
|
|
304
|
+
@mock.patch('git_p4son.sync_split.sync_preflight')
|
|
305
|
+
def test_dry_run_does_not_fire_hooks(
|
|
306
|
+
self, mock_hooks, _last_sync, _latest, _user, _changes):
|
|
307
|
+
rc = sync_split_command(self._args(dry_run=True))
|
|
308
|
+
self.assertEqual(rc, 0)
|
|
309
|
+
mock_hooks.assert_not_called()
|
|
310
|
+
|
|
311
|
+
@mock.patch('git_p4son.sync_split.get_latest_changelist', return_value=100)
|
|
312
|
+
@mock.patch('git_p4son.sync_split.git_last_sync',
|
|
313
|
+
return_value=LastSync(changelist=100, commit='abc123'))
|
|
314
|
+
@mock.patch('git_p4son.sync_split.sync_preflight')
|
|
315
|
+
def test_nothing_to_sync_does_not_fire_hooks(
|
|
316
|
+
self, mock_hooks, _last_sync, _latest):
|
|
317
|
+
"""Already at the target: sync skips its hooks in that case, and
|
|
318
|
+
so must sync-split."""
|
|
319
|
+
rc = sync_split_command(self._args())
|
|
320
|
+
self.assertEqual(rc, 0)
|
|
321
|
+
mock_hooks.assert_not_called()
|
|
322
|
+
|
|
323
|
+
@mock.patch('git_p4son.sync_split.sync_preflight')
|
|
324
|
+
@mock.patch('git_p4son.sync_split.git_last_sync', return_value=None)
|
|
325
|
+
def test_unsynced_workspace_does_not_fire_hooks(
|
|
326
|
+
self, _last_sync, mock_hooks):
|
|
327
|
+
rc = sync_split_command(self._args())
|
|
328
|
+
self.assertEqual(rc, 1)
|
|
329
|
+
mock_hooks.assert_not_called()
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class TestSyncPreflight(unittest.TestCase):
|
|
333
|
+
"""The one gate both commands share: workspace checks then hooks, in
|
|
334
|
+
that order, and skipped wholesale when a caller already ran it."""
|
|
335
|
+
|
|
336
|
+
@mock.patch('git_p4son.sync.run_hooks')
|
|
337
|
+
@mock.patch('git_p4son.sync.p4_get_opened_files')
|
|
338
|
+
@mock.patch('git_p4son.sync.get_dirty_files')
|
|
339
|
+
def test_already_done_skips_everything(
|
|
340
|
+
self, mock_dirty, mock_opened, mock_run_hooks):
|
|
341
|
+
self.assertTrue(sync_preflight('//ws', '/ws', '/invoked', True))
|
|
342
|
+
mock_dirty.assert_not_called()
|
|
343
|
+
mock_opened.assert_not_called()
|
|
344
|
+
mock_run_hooks.assert_not_called()
|
|
345
|
+
|
|
346
|
+
@mock.patch('git_p4son.sync.run_hooks', return_value=[])
|
|
347
|
+
@mock.patch('git_p4son.sync.p4_get_opened_files', return_value=[])
|
|
348
|
+
@mock.patch('git_p4son.sync.get_dirty_files', return_value=[])
|
|
349
|
+
def test_clean_workspaces_run_the_hooks(
|
|
350
|
+
self, _dirty, _opened, mock_run_hooks):
|
|
351
|
+
self.assertTrue(sync_preflight('//ws', '/ws', '/invoked'))
|
|
352
|
+
mock_run_hooks.assert_called_once_with('pre-sync', '/ws', '/invoked')
|
|
353
|
+
|
|
354
|
+
@mock.patch('git_p4son.sync.run_hooks')
|
|
355
|
+
@mock.patch('git_p4son.sync.p4_get_opened_files')
|
|
356
|
+
@mock.patch('git_p4son.sync.get_dirty_files',
|
|
357
|
+
return_value=[('a.txt', 'modify')])
|
|
358
|
+
def test_dirty_git_stops_before_p4_and_hooks(
|
|
359
|
+
self, _dirty, mock_opened, mock_run_hooks):
|
|
360
|
+
self.assertFalse(sync_preflight('//ws', '/ws', '/invoked'))
|
|
361
|
+
mock_opened.assert_not_called()
|
|
362
|
+
mock_run_hooks.assert_not_called()
|
|
363
|
+
|
|
364
|
+
@mock.patch('git_p4son.sync.run_hooks')
|
|
365
|
+
@mock.patch('git_p4son.sync.is_file_tracked', return_value=True)
|
|
366
|
+
@mock.patch('git_p4son.sync.p4_get_opened_files',
|
|
367
|
+
return_value=[('a.txt', 'modify')])
|
|
368
|
+
@mock.patch('git_p4son.sync.get_dirty_files', return_value=[])
|
|
369
|
+
def test_dirty_p4_stops_before_hooks(
|
|
370
|
+
self, _dirty, _opened, _tracked, mock_run_hooks):
|
|
371
|
+
self.assertFalse(sync_preflight('//ws', '/ws', '/invoked'))
|
|
372
|
+
mock_run_hooks.assert_not_called()
|
|
373
|
+
|
|
374
|
+
@mock.patch('git_p4son.sync.run_hooks',
|
|
375
|
+
return_value=[make_run_result(returncode=1)])
|
|
376
|
+
@mock.patch('git_p4son.sync.p4_get_opened_files', return_value=[])
|
|
377
|
+
@mock.patch('git_p4son.sync.get_dirty_files', return_value=[])
|
|
378
|
+
def test_failing_hook_fails_the_gate(self, _dirty, _opened, _hooks):
|
|
379
|
+
self.assertFalse(sync_preflight('//ws', '/ws', '/invoked'))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class TestSyncSplitCommandMultiUser(unittest.TestCase):
|
|
383
|
+
|
|
384
|
+
def setUp(self):
|
|
385
|
+
patcher = mock.patch(
|
|
386
|
+
'git_p4son.sync_split.resolve_depot_root',
|
|
387
|
+
return_value=ResolvedDepot(depot_root='//myclient',
|
|
388
|
+
client_spec=None))
|
|
389
|
+
patcher.start()
|
|
390
|
+
self.addCleanup(patcher.stop)
|
|
391
|
+
# The gate has its own tests; here it must simply not shell out.
|
|
392
|
+
gate = mock.patch('git_p4son.sync_split.sync_preflight',
|
|
393
|
+
return_value=True)
|
|
394
|
+
gate.start()
|
|
395
|
+
self.addCleanup(gate.stop)
|
|
396
|
+
|
|
397
|
+
def _args(self, **overrides):
|
|
398
|
+
values = dict(changelist=None, user=None, dry_run=False,
|
|
399
|
+
workspace_dir='/ws')
|
|
400
|
+
values.update(overrides)
|
|
401
|
+
return mock.Mock(**values)
|
|
402
|
+
|
|
224
403
|
@mock.patch('git_p4son.sync_split.sync_command', return_value=0)
|
|
225
404
|
@mock.patch('git_p4son.sync_split.get_submitted_changes')
|
|
226
405
|
@mock.patch('git_p4son.sync_split.get_p4_user')
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|