git-p4son 0.3.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {git_p4son-0.3.0 → git_p4son-0.3.1}/PKG-INFO +27 -4
- {git_p4son-0.3.0 → git_p4son-0.3.1}/README.md +26 -3
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/__init__.py +1 -1
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/changelist_store.py +1 -1
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/cli.py +11 -4
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/complete.py +10 -5
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/config.py +11 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/init.py +31 -17
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/sync.py +144 -65
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son.egg-info/PKG-INFO +27 -4
- {git_p4son-0.3.0 → git_p4son-0.3.1}/pyproject.toml +1 -1
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_changelist_store.py +5 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_cli.py +7 -3
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_complete.py +15 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_config.py +22 -1
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_init.py +37 -5
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_sync.py +382 -14
- {git_p4son-0.3.0 → git_p4son-0.3.1}/LICENSE +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/__main__.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/alias.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/common.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/completions/_git-p4son +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/completions/git-p4son.bash +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/completions/git-p4son.ps1 +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/git.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/hooks.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/lib.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/list_changes.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/log.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/new.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/perforce.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/review.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/state.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son/update.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son.egg-info/SOURCES.txt +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son.egg-info/dependency_links.txt +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son.egg-info/entry_points.txt +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/git_p4son.egg-info/top_level.txt +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/setup.cfg +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_alias.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_common.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_git.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_hooks.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_lib_changelist.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_lib_edit.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_lib_review.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_list_changes.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_log.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_new.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_perforce.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/tests/test_review.py +0 -0
- {git_p4son-0.3.0 → git_p4son-0.3.1}/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.1
|
|
4
4
|
Summary: Utility for keeping a Perforce workspace and local git repo in sync
|
|
5
5
|
Author-email: Andreas Andersson <andreas@neoboid.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -266,6 +266,11 @@ The depot root determines which part of the Perforce workspace git-p4son syncs.
|
|
|
266
266
|
workspace or just the directory's subtree where the git root is placed. The selection is saved in
|
|
267
267
|
`.git-p4son/config.toml` and used by all subsequent commands.
|
|
268
268
|
|
|
269
|
+
The saved root uses a `$(workspace)` placeholder for the workspace (client) name, e.g.
|
|
270
|
+
`root = "//$(workspace)/Engine"`. git-p4son substitutes the live workspace name each time it runs, so renaming
|
|
271
|
+
the Perforce workspace does not break the config. You can still store a concrete name (e.g. `//my-workspace/Engine`)
|
|
272
|
+
by hand-editing `config.toml`; both forms work.
|
|
273
|
+
|
|
269
274
|
The `.gitignore` is set up using this priority:
|
|
270
275
|
- If `.gitignore` already exists, it is left as is
|
|
271
276
|
- If `.p4ignore` exists, it is copied to `.gitignore` as a starting point
|
|
@@ -276,12 +281,14 @@ The `.gitignore` is set up using this priority:
|
|
|
276
281
|
Sync local git repository with a Perforce workspace:
|
|
277
282
|
|
|
278
283
|
```sh
|
|
279
|
-
git p4son sync [changelist] [--force]
|
|
284
|
+
git p4son sync [changelist ...] [--force]
|
|
280
285
|
```
|
|
281
286
|
|
|
282
287
|
**Arguments:**
|
|
283
|
-
- `changelist` (optional): Changelist number,
|
|
284
|
-
sync to the latest changelist affecting the workspace.
|
|
288
|
+
- `changelist` (optional): Changelist number, `last-synced` to re-sync the last synced changelist, or `head`
|
|
289
|
+
to sync to the latest changelist. Omit to sync to the latest changelist affecting the workspace. Several
|
|
290
|
+
strictly increasing changelist numbers may be given to sync each in sequence, producing one commit per
|
|
291
|
+
changelist. A trailing `head` may close out such a sequence to finish at the latest changelist.
|
|
285
292
|
|
|
286
293
|
**Options:**
|
|
287
294
|
- `-f, --force`: Allow syncing to changelists older than the current one.
|
|
@@ -289,11 +296,20 @@ git p4son sync [changelist] [--force]
|
|
|
289
296
|
**Examples:**
|
|
290
297
|
```sh
|
|
291
298
|
git p4son sync # sync to latest
|
|
299
|
+
git p4son sync head # sync to latest (explicit)
|
|
292
300
|
git p4son sync 12345
|
|
301
|
+
git p4son sync 123 156 178 # sync each changelist in sequence, one commit each
|
|
302
|
+
git p4son sync 123 156 head # sync 123, 156, then the latest changelist
|
|
293
303
|
git p4son sync last-synced
|
|
294
304
|
git p4son sync 12345 --force
|
|
295
305
|
```
|
|
296
306
|
|
|
307
|
+
#### pre-sync hook
|
|
308
|
+
|
|
309
|
+
Before `git-p4son sync` syncs any changelist, git-p4son runs [hooks](#hooks) from
|
|
310
|
+
`.git-p4son/hooks/pre-sync/`. If any hook exits non-zero the sync is aborted. When a sequence of
|
|
311
|
+
changelists is given, the pre-sync hooks run once, before the first sync.
|
|
312
|
+
|
|
297
313
|
#### post-sync hook
|
|
298
314
|
|
|
299
315
|
After a successful `git-p4son sync` that actually performs sync work, git-p4son runs [hooks](#hooks)
|
|
@@ -553,6 +569,13 @@ On Windows, `.ps1`, `.nu`, `.sh`, and `.py` files are run with `powershell.exe`,
|
|
|
553
569
|
|
|
554
570
|
Non-executable or unrecognized hook files are skipped with a warning.
|
|
555
571
|
|
|
572
|
+
### pre-sync hook
|
|
573
|
+
|
|
574
|
+
Before `git-p4son sync` syncs any changelist, git-p4son runs executable hooks from
|
|
575
|
+
`.git-p4son/hooks/pre-sync/`. If any hook exits with a non-zero code the sync is aborted before any
|
|
576
|
+
changelist is synced. When a sequence of changelists is synced, the pre-sync hooks run only once,
|
|
577
|
+
before the first sync.
|
|
578
|
+
|
|
556
579
|
### post-sync hook
|
|
557
580
|
|
|
558
581
|
After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
|
|
@@ -243,6 +243,11 @@ The depot root determines which part of the Perforce workspace git-p4son syncs.
|
|
|
243
243
|
workspace or just the directory's subtree where the git root is placed. The selection is saved in
|
|
244
244
|
`.git-p4son/config.toml` and used by all subsequent commands.
|
|
245
245
|
|
|
246
|
+
The saved root uses a `$(workspace)` placeholder for the workspace (client) name, e.g.
|
|
247
|
+
`root = "//$(workspace)/Engine"`. git-p4son substitutes the live workspace name each time it runs, so renaming
|
|
248
|
+
the Perforce workspace does not break the config. You can still store a concrete name (e.g. `//my-workspace/Engine`)
|
|
249
|
+
by hand-editing `config.toml`; both forms work.
|
|
250
|
+
|
|
246
251
|
The `.gitignore` is set up using this priority:
|
|
247
252
|
- If `.gitignore` already exists, it is left as is
|
|
248
253
|
- If `.p4ignore` exists, it is copied to `.gitignore` as a starting point
|
|
@@ -253,12 +258,14 @@ The `.gitignore` is set up using this priority:
|
|
|
253
258
|
Sync local git repository with a Perforce workspace:
|
|
254
259
|
|
|
255
260
|
```sh
|
|
256
|
-
git p4son sync [changelist] [--force]
|
|
261
|
+
git p4son sync [changelist ...] [--force]
|
|
257
262
|
```
|
|
258
263
|
|
|
259
264
|
**Arguments:**
|
|
260
|
-
- `changelist` (optional): Changelist number,
|
|
261
|
-
sync to the latest changelist affecting the workspace.
|
|
265
|
+
- `changelist` (optional): Changelist number, `last-synced` to re-sync the last synced changelist, or `head`
|
|
266
|
+
to sync to the latest changelist. Omit to sync to the latest changelist affecting the workspace. Several
|
|
267
|
+
strictly increasing changelist numbers may be given to sync each in sequence, producing one commit per
|
|
268
|
+
changelist. A trailing `head` may close out such a sequence to finish at the latest changelist.
|
|
262
269
|
|
|
263
270
|
**Options:**
|
|
264
271
|
- `-f, --force`: Allow syncing to changelists older than the current one.
|
|
@@ -266,11 +273,20 @@ git p4son sync [changelist] [--force]
|
|
|
266
273
|
**Examples:**
|
|
267
274
|
```sh
|
|
268
275
|
git p4son sync # sync to latest
|
|
276
|
+
git p4son sync head # sync to latest (explicit)
|
|
269
277
|
git p4son sync 12345
|
|
278
|
+
git p4son sync 123 156 178 # sync each changelist in sequence, one commit each
|
|
279
|
+
git p4son sync 123 156 head # sync 123, 156, then the latest changelist
|
|
270
280
|
git p4son sync last-synced
|
|
271
281
|
git p4son sync 12345 --force
|
|
272
282
|
```
|
|
273
283
|
|
|
284
|
+
#### pre-sync hook
|
|
285
|
+
|
|
286
|
+
Before `git-p4son sync` syncs any changelist, git-p4son runs [hooks](#hooks) from
|
|
287
|
+
`.git-p4son/hooks/pre-sync/`. If any hook exits non-zero the sync is aborted. When a sequence of
|
|
288
|
+
changelists is given, the pre-sync hooks run once, before the first sync.
|
|
289
|
+
|
|
274
290
|
#### post-sync hook
|
|
275
291
|
|
|
276
292
|
After a successful `git-p4son sync` that actually performs sync work, git-p4son runs [hooks](#hooks)
|
|
@@ -530,6 +546,13 @@ On Windows, `.ps1`, `.nu`, `.sh`, and `.py` files are run with `powershell.exe`,
|
|
|
530
546
|
|
|
531
547
|
Non-executable or unrecognized hook files are skipped with a warning.
|
|
532
548
|
|
|
549
|
+
### pre-sync hook
|
|
550
|
+
|
|
551
|
+
Before `git-p4son sync` syncs any changelist, git-p4son runs executable hooks from
|
|
552
|
+
`.git-p4son/hooks/pre-sync/`. If any hook exits with a non-zero code the sync is aborted before any
|
|
553
|
+
changelist is synced. When a sequence of changelists is synced, the pre-sync hooks run only once,
|
|
554
|
+
before the first sync.
|
|
555
|
+
|
|
533
556
|
### post-sync hook
|
|
534
557
|
|
|
535
558
|
After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
|
|
@@ -11,7 +11,7 @@ from . import CONFIG_DIR
|
|
|
11
11
|
from .log import log
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
RESERVED_KEYWORDS = frozenset({'last-synced', 'branch'})
|
|
14
|
+
RESERVED_KEYWORDS = frozenset({'last-synced', 'head', 'branch'})
|
|
15
15
|
|
|
16
16
|
# Allowed characters: ASCII letters, digits, hyphen, underscore, dot.
|
|
17
17
|
# Must not start or end with a dot, so "." and ".." are rejected and the
|
|
@@ -31,7 +31,10 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
31
31
|
epilog="""
|
|
32
32
|
Examples:
|
|
33
33
|
git-p4son sync # Sync to the latest changelist
|
|
34
|
+
git-p4son sync head # Sync to the latest changelist (explicit)
|
|
34
35
|
git-p4son sync 12345 # Sync to changelist 12345
|
|
36
|
+
git-p4son sync 123 156 178 # Sync each changelist in sequence, one commit each
|
|
37
|
+
git-p4son sync 123 156 head # Sync 123, 156, then the latest changelist
|
|
35
38
|
git-p4son sync last-synced # Re-sync the last synced changelist
|
|
36
39
|
git-p4son new -m "Fix bug" # Create changelist, alias defaults to branch name
|
|
37
40
|
git-p4son new -m "Fix bug" --review # Create changelist, create Swarm review
|
|
@@ -78,10 +81,14 @@ Examples:
|
|
|
78
81
|
)
|
|
79
82
|
sync_parser.add_argument(
|
|
80
83
|
'changelist',
|
|
81
|
-
nargs='
|
|
82
|
-
default=
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
nargs='*',
|
|
85
|
+
default=[],
|
|
86
|
+
metavar='CHANGELIST',
|
|
87
|
+
help='Changelist number(s) to sync, "last-synced" to re-sync the last synced changelist, '
|
|
88
|
+
'or "head" to sync to the latest changelist. Give several strictly increasing '
|
|
89
|
+
'changelist numbers to sync each in sequence, one commit per changelist; "head" may '
|
|
90
|
+
'close out such a sequence as a trailing target. Omit to sync to the latest '
|
|
91
|
+
'changelist affecting the workspace'
|
|
85
92
|
)
|
|
86
93
|
sync_parser.add_argument(
|
|
87
94
|
'-f', '--force',
|
|
@@ -106,11 +106,16 @@ def _complete_positional(command, subcommand, positional_count,
|
|
|
106
106
|
"""Complete a positional argument."""
|
|
107
107
|
aliases = _get_alias_names(workspace_dir)
|
|
108
108
|
|
|
109
|
-
if command == 'sync'
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
if command == 'sync':
|
|
110
|
+
if positional_count == 0:
|
|
111
|
+
candidates = [
|
|
112
|
+
('head', 'Sync to the latest changelist'),
|
|
113
|
+
('last-synced', 'Re-sync last synced changelist'),
|
|
114
|
+
]
|
|
115
|
+
return _filter(candidates, prefix)
|
|
116
|
+
# "head" may also close out a sequence of changelist numbers.
|
|
117
|
+
return _filter(
|
|
118
|
+
[('head', 'Sync to the latest changelist')], prefix)
|
|
114
119
|
|
|
115
120
|
if command == 'update' and positional_count == 0:
|
|
116
121
|
branch_candidates = _get_branch_candidates(prefix, workspace_dir)
|
|
@@ -12,6 +12,12 @@ from . import CONFIG_DIR
|
|
|
12
12
|
|
|
13
13
|
_BARE_KEY_RE = re.compile(r'^[A-Za-z0-9_-]+$')
|
|
14
14
|
|
|
15
|
+
# Placeholder allowed in a stored depot root, substituted with the live
|
|
16
|
+
# Perforce client (workspace) name each time the root is used. Storing e.g.
|
|
17
|
+
# root = "//$(workspace)/Engine" keeps the config working after the workspace
|
|
18
|
+
# is renamed, at the cost of one client-name lookup per command.
|
|
19
|
+
WORKSPACE_PLACEHOLDER = '$(workspace)'
|
|
20
|
+
|
|
15
21
|
|
|
16
22
|
def config_path(workspace_dir: str) -> str:
|
|
17
23
|
"""Return the path to the config file."""
|
|
@@ -94,3 +100,8 @@ def get_depot_root(workspace_dir: str) -> str | None:
|
|
|
94
100
|
"""Get the depot root from config, or None if not configured."""
|
|
95
101
|
config = load_config(workspace_dir)
|
|
96
102
|
return config.get('depot', {}).get('root')
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def expand_depot_root(depot_root: str, workspace_name: str) -> str:
|
|
106
|
+
"""Substitute the live workspace name for the $(workspace) placeholder."""
|
|
107
|
+
return depot_root.replace(WORKSPACE_PLACEHOLDER, workspace_name)
|
|
@@ -10,7 +10,12 @@ import os
|
|
|
10
10
|
import shutil
|
|
11
11
|
|
|
12
12
|
from .common import CommandError, run, run_with_output
|
|
13
|
-
from .config import
|
|
13
|
+
from .config import (
|
|
14
|
+
WORKSPACE_PLACEHOLDER,
|
|
15
|
+
expand_depot_root,
|
|
16
|
+
get_depot_root,
|
|
17
|
+
save_config,
|
|
18
|
+
)
|
|
14
19
|
from .log import log
|
|
15
20
|
from .perforce import get_client_spec
|
|
16
21
|
from .git import resolve_editor
|
|
@@ -26,31 +31,38 @@ def _validate_depot_root(depot_root: str, cwd: str) -> bool:
|
|
|
26
31
|
return False
|
|
27
32
|
|
|
28
33
|
|
|
29
|
-
def _compute_cwd_depot_root(
|
|
30
|
-
|
|
31
|
-
"""Compute depot root for cwd relative to workspace root.
|
|
34
|
+
def _compute_cwd_depot_root(cwd: str, p4_workspace_root: str) -> str | None:
|
|
35
|
+
"""Compute the depot root template for cwd relative to workspace root.
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
Uses the $(workspace) placeholder in place of the client name so the stored
|
|
38
|
+
root survives a workspace rename. Returns None if cwd is the workspace root
|
|
39
|
+
(identical to the entire workspace).
|
|
34
40
|
"""
|
|
35
41
|
rel = os.path.relpath(cwd, p4_workspace_root)
|
|
36
42
|
if rel == '.':
|
|
37
43
|
return None
|
|
38
44
|
rel_posix = rel.replace(os.sep, '/')
|
|
39
|
-
return f'//{
|
|
45
|
+
return f'//{WORKSPACE_PLACEHOLDER}/{rel_posix}'
|
|
40
46
|
|
|
41
47
|
|
|
42
48
|
def _select_depot_root(client_name: str, cwd: str,
|
|
43
49
|
p4_workspace_root: str) -> str | None:
|
|
44
|
-
"""Interactive prompt for depot root selection.
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
"""Interactive prompt for depot root selection.
|
|
51
|
+
|
|
52
|
+
Returns a depot root template (with the $(workspace) placeholder) to store,
|
|
53
|
+
or None to abort. Menu entries and validation use the resolved path.
|
|
54
|
+
"""
|
|
55
|
+
entire_root = f'//{WORKSPACE_PLACEHOLDER}'
|
|
56
|
+
cwd_root = _compute_cwd_depot_root(cwd, p4_workspace_root)
|
|
47
57
|
|
|
48
58
|
while True:
|
|
49
59
|
print()
|
|
50
60
|
print('Select the depot path that git-p4son should sync:')
|
|
51
|
-
print(f' 1. Entire workspace:
|
|
61
|
+
print(f' 1. Entire workspace: '
|
|
62
|
+
f'{expand_depot_root(entire_root, client_name)}/...')
|
|
52
63
|
if cwd_root:
|
|
53
|
-
print(f' 2. Current directory:
|
|
64
|
+
print(f' 2. Current directory: '
|
|
65
|
+
f'{expand_depot_root(cwd_root, client_name)}/...')
|
|
54
66
|
abort_num = '3'
|
|
55
67
|
else:
|
|
56
68
|
abort_num = '2'
|
|
@@ -72,10 +84,11 @@ def _select_depot_root(client_name: str, cwd: str,
|
|
|
72
84
|
print(f'Invalid choice: {choice}')
|
|
73
85
|
continue
|
|
74
86
|
|
|
75
|
-
|
|
87
|
+
resolved = expand_depot_root(depot_root, client_name)
|
|
88
|
+
if _validate_depot_root(resolved, cwd):
|
|
76
89
|
return depot_root
|
|
77
90
|
|
|
78
|
-
log.error(f'Depot root {
|
|
91
|
+
log.error(f'Depot root {resolved}/... is not valid')
|
|
79
92
|
|
|
80
93
|
|
|
81
94
|
def _configure_depot_root(client_name: str, cwd: str,
|
|
@@ -84,17 +97,18 @@ def _configure_depot_root(client_name: str, cwd: str,
|
|
|
84
97
|
log.heading('Finding git-p4son depot root')
|
|
85
98
|
depot_root = get_depot_root(cwd)
|
|
86
99
|
if depot_root:
|
|
87
|
-
log.success(f'{depot_root}/...')
|
|
100
|
+
log.success(f'{expand_depot_root(depot_root, client_name)}/...')
|
|
88
101
|
else:
|
|
89
102
|
log.warning('no root configured')
|
|
90
103
|
|
|
91
104
|
if depot_root:
|
|
92
105
|
log.heading('Validating depot root')
|
|
93
|
-
|
|
106
|
+
resolved = expand_depot_root(depot_root, client_name)
|
|
107
|
+
if _validate_depot_root(resolved, cwd):
|
|
94
108
|
log.success('all good')
|
|
95
109
|
return True
|
|
96
110
|
else:
|
|
97
|
-
log.error(f'{
|
|
111
|
+
log.error(f'{resolved}/... is not valid')
|
|
98
112
|
depot_root = None
|
|
99
113
|
|
|
100
114
|
if not depot_root:
|
|
@@ -104,7 +118,7 @@ def _configure_depot_root(client_name: str, cwd: str,
|
|
|
104
118
|
log.error('aborting')
|
|
105
119
|
return False
|
|
106
120
|
|
|
107
|
-
log.success(f'{depot_root}/...')
|
|
121
|
+
log.success(f'{expand_depot_root(depot_root, client_name)}/...')
|
|
108
122
|
save_config(cwd, {'depot': {'root': depot_root}})
|
|
109
123
|
return True
|
|
110
124
|
|
|
@@ -13,7 +13,7 @@ from dataclasses import dataclass, field
|
|
|
13
13
|
from typing import IO
|
|
14
14
|
|
|
15
15
|
from .common import RunError, prompt_choice, run_with_output
|
|
16
|
-
from .config import get_depot_root
|
|
16
|
+
from .config import WORKSPACE_PLACEHOLDER, expand_depot_root, get_depot_root
|
|
17
17
|
from .state import dismiss_clobber_warning, is_clobber_warning_dismissed
|
|
18
18
|
from .git import (
|
|
19
19
|
add_all_files, commit, find_base_commits, get_blob_oids,
|
|
@@ -523,6 +523,37 @@ def _handle_clobber_warning(clobber: bool, workspace_dir: str) -> bool:
|
|
|
523
523
|
return True
|
|
524
524
|
|
|
525
525
|
|
|
526
|
+
def _run_pre_sync_hooks(workspace_dir: str, invocation_dir: str) -> bool:
|
|
527
|
+
"""Run pre-sync hooks; return False when any hook fails, aborting the sync.
|
|
528
|
+
|
|
529
|
+
Runs once before any changelist is synced. All hooks run (they are
|
|
530
|
+
independent), then the sync is aborted if any returned a non-zero code.
|
|
531
|
+
"""
|
|
532
|
+
results = run_hooks('pre-sync', workspace_dir, invocation_dir)
|
|
533
|
+
if any(result.returncode != 0 for result in results):
|
|
534
|
+
log.error('Aborting sync because a pre-sync hook failed')
|
|
535
|
+
return False
|
|
536
|
+
return True
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _sync_pass(changelist: int, label: str, depot_root: str,
|
|
540
|
+
workspace_dir: str, pre_sync_head_commit: str, temp_root: str,
|
|
541
|
+
uses_crlf: bool, clobber: bool) -> WritableSyncFileSet:
|
|
542
|
+
"""Run one sync pass: preview, prepare writable files, and p4 sync.
|
|
543
|
+
|
|
544
|
+
Returns the classified writable file set. p4 sync is skipped when the
|
|
545
|
+
preview is empty (nothing to sync).
|
|
546
|
+
"""
|
|
547
|
+
preview = p4_sync_preview(changelist, depot_root, workspace_dir)
|
|
548
|
+
prep = prepare_writable_files(preview, workspace_dir, pre_sync_head_commit,
|
|
549
|
+
temp_root, uses_crlf=uses_crlf,
|
|
550
|
+
clobber=clobber)
|
|
551
|
+
if preview:
|
|
552
|
+
p4_sync(changelist, label, depot_root, workspace_dir,
|
|
553
|
+
expected_clobber=set(prep.ignored))
|
|
554
|
+
return prep
|
|
555
|
+
|
|
556
|
+
|
|
526
557
|
def sync_command(args: argparse.Namespace) -> int:
|
|
527
558
|
"""Execute the sync command."""
|
|
528
559
|
workspace_dir = args.workspace_dir
|
|
@@ -533,6 +564,17 @@ def sync_command(args: argparse.Namespace) -> int:
|
|
|
533
564
|
if not depot_root:
|
|
534
565
|
log.error('No depot root configured. Run "git p4son init" first.')
|
|
535
566
|
return 1
|
|
567
|
+
|
|
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
|
+
client_spec = get_client_spec(workspace_dir)
|
|
572
|
+
if WORKSPACE_PLACEHOLDER in depot_root and not client_spec:
|
|
573
|
+
log.error('Cannot resolve $(workspace) in depot root: not inside a '
|
|
574
|
+
'Perforce workspace')
|
|
575
|
+
return 1
|
|
576
|
+
if client_spec:
|
|
577
|
+
depot_root = expand_depot_root(depot_root, client_spec.name)
|
|
536
578
|
log.success(depot_root)
|
|
537
579
|
|
|
538
580
|
log.heading('Checking git workspace')
|
|
@@ -578,7 +620,7 @@ def sync_command(args: argparse.Namespace) -> int:
|
|
|
578
620
|
# the post-sync merge doesn't conflict on LF-vs-CRLF differences alone.
|
|
579
621
|
# The clobber option changes whether git-ignored writable files survive
|
|
580
622
|
# the sync, which the prepare summary needs to report accurately.
|
|
581
|
-
client_spec
|
|
623
|
+
# (client_spec was fetched above to resolve the depot root.)
|
|
582
624
|
uses_crlf = bool(client_spec and client_spec.uses_crlf)
|
|
583
625
|
clobber = bool(client_spec and client_spec.clobber)
|
|
584
626
|
|
|
@@ -588,94 +630,131 @@ def sync_command(args: argparse.Namespace) -> int:
|
|
|
588
630
|
# Temp root for staging HEAD/baseline file content between classification
|
|
589
631
|
# and the post-sync merge. Cleaned up automatically on exit.
|
|
590
632
|
with tempfile.TemporaryDirectory(prefix='git-p4son-sync-') as temp_root:
|
|
591
|
-
|
|
592
|
-
|
|
633
|
+
raw = args.changelist
|
|
634
|
+
lowered = [c.lower() for c in raw]
|
|
635
|
+
|
|
636
|
+
# "last-synced" is a keyword that only makes sense on its own.
|
|
637
|
+
if 'last-synced' in lowered:
|
|
638
|
+
if lowered != ['last-synced']:
|
|
639
|
+
log.error('The "last-synced" keyword cannot be combined with '
|
|
640
|
+
'other changelists')
|
|
641
|
+
return 1
|
|
593
642
|
if not last_sync:
|
|
594
643
|
log.error('No previous sync found, cannot use "last-synced"')
|
|
595
644
|
return 1
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
uses_crlf=uses_crlf, clobber=clobber)
|
|
602
|
-
p4_sync(last_sync.changelist, last_changelist_label,
|
|
603
|
-
depot_root, workspace_dir,
|
|
604
|
-
expected_clobber=set(prep.ignored))
|
|
645
|
+
if not _run_pre_sync_hooks(workspace_dir, invocation_dir):
|
|
646
|
+
return 1
|
|
647
|
+
_sync_pass(last_sync.changelist, last_changelist_label, depot_root,
|
|
648
|
+
workspace_dir, pre_sync_head_commit, temp_root,
|
|
649
|
+
uses_crlf, clobber)
|
|
605
650
|
run_hooks('post-sync', workspace_dir, invocation_dir)
|
|
606
651
|
return 0
|
|
607
652
|
|
|
608
|
-
#
|
|
609
|
-
|
|
653
|
+
# "head" resolves to the latest changelist, the largest in an
|
|
654
|
+
# increasing sequence, so it may only appear as the final target.
|
|
655
|
+
if 'head' in lowered and lowered.index('head') != len(lowered) - 1:
|
|
656
|
+
log.error('The "head" keyword must come last')
|
|
657
|
+
return 1
|
|
658
|
+
|
|
659
|
+
# Resolve the ordered list of (changelist, label) targets to sync,
|
|
660
|
+
# replacing a trailing "head" with the concrete latest changelist so
|
|
661
|
+
# the increasing-order check below compares real numbers.
|
|
662
|
+
if not raw:
|
|
610
663
|
log.heading('Finding latest changelist')
|
|
611
|
-
|
|
612
|
-
log.success(f'CL {
|
|
613
|
-
|
|
664
|
+
latest = get_latest_changelist(depot_root, workspace_dir)
|
|
665
|
+
log.success(f'CL {latest}')
|
|
666
|
+
targets = [(latest, 'latest')]
|
|
614
667
|
else:
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
668
|
+
targets = []
|
|
669
|
+
for c in raw:
|
|
670
|
+
if c.lower() == 'head':
|
|
671
|
+
log.heading('Finding latest changelist')
|
|
672
|
+
latest = get_latest_changelist(depot_root, workspace_dir)
|
|
673
|
+
log.success(f'CL {latest}')
|
|
674
|
+
targets.append((latest, 'latest'))
|
|
675
|
+
else:
|
|
676
|
+
try:
|
|
677
|
+
targets.append((int(c), 'specified'))
|
|
678
|
+
except ValueError:
|
|
679
|
+
log.error(f'Invalid changelist number: {c}')
|
|
680
|
+
return 1
|
|
681
|
+
|
|
682
|
+
# Targets are synced in the given order, so they must be strictly
|
|
683
|
+
# increasing: no syncing back and forth.
|
|
684
|
+
numbers = [cl for cl, _ in targets]
|
|
685
|
+
for prev, curr in zip(numbers, numbers[1:]):
|
|
686
|
+
if curr <= prev:
|
|
687
|
+
log.error('Changelists must be strictly increasing, '
|
|
688
|
+
f'got {prev} then {curr}')
|
|
620
689
|
return 1
|
|
621
690
|
|
|
622
691
|
last_changelist = last_sync.changelist if last_sync else None
|
|
623
|
-
if last_changelist == changelist:
|
|
624
|
-
log.info(f'Already at CL {last_changelist}, nothing to do.')
|
|
625
|
-
log.heading('Skipping post-sync hooks')
|
|
626
|
-
return 0
|
|
627
692
|
|
|
628
|
-
#
|
|
629
|
-
|
|
693
|
+
# Syncing to a changelist older than the current one needs --force.
|
|
694
|
+
# Targets are strictly increasing, so the smallest is numbers[0]; an
|
|
695
|
+
# equal-to-current changelist (dropped just below) is not "older".
|
|
696
|
+
if last_sync and numbers[0] < last_sync.changelist:
|
|
630
697
|
if not args.force:
|
|
631
698
|
log.error(
|
|
632
|
-
f'Cannot sync to CL {
|
|
699
|
+
f'Cannot sync to CL {numbers[0]} '
|
|
633
700
|
f'(currently at CL {last_sync.changelist}) without --force.')
|
|
634
701
|
return 1
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
702
|
+
log.warning(
|
|
703
|
+
f'Syncing to older CL {numbers[0]} '
|
|
704
|
+
f'(currently at CL {last_sync.changelist}) with --force')
|
|
705
|
+
|
|
706
|
+
# Drop a target equal to the last synced changelist: we are already
|
|
707
|
+
# there, so there is nothing to sync or commit for it. An explicitly
|
|
708
|
+
# requested *older* changelist is kept and synced as the user asked.
|
|
709
|
+
if last_changelist is not None and last_changelist in numbers:
|
|
710
|
+
log.info(f'Skipping CL {last_changelist} (already synced)')
|
|
711
|
+
targets = [(cl, lbl) for cl, lbl in targets
|
|
712
|
+
if cl != last_changelist]
|
|
713
|
+
|
|
714
|
+
if not targets:
|
|
715
|
+
log.info('Already synced, nothing to do.')
|
|
716
|
+
log.heading('Skipping post-sync hooks')
|
|
717
|
+
return 0
|
|
718
|
+
|
|
719
|
+
# Pre-sync hooks run once, before syncing any changelist (including the
|
|
720
|
+
# catch-up pass), and abort the whole sync if any of them fail.
|
|
721
|
+
if not _run_pre_sync_hooks(workspace_dir, invocation_dir):
|
|
722
|
+
return 1
|
|
639
723
|
|
|
640
|
-
# First sync pass: to last_synced_cl
|
|
641
724
|
all_changed: list[ChangedFile] = []
|
|
642
725
|
all_ignored: list[str] = []
|
|
643
726
|
|
|
727
|
+
# Catch-up pass to the last synced changelist. This makes locally
|
|
728
|
+
# modified (writable) files read-only and stages their content so the
|
|
729
|
+
# post-sync merge can restore local changes; its result is folded into
|
|
730
|
+
# the first target commit.
|
|
644
731
|
if last_changelist is not None:
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
pre_sync_head_commit, temp_root,
|
|
649
|
-
uses_crlf=uses_crlf, clobber=clobber)
|
|
732
|
+
prep = _sync_pass(last_changelist, last_changelist_label,
|
|
733
|
+
depot_root, workspace_dir, pre_sync_head_commit,
|
|
734
|
+
temp_root, uses_crlf, clobber)
|
|
650
735
|
all_changed.extend(prep.changed)
|
|
651
736
|
all_ignored.extend(prep.ignored)
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
if dirty_files:
|
|
671
|
-
add_all_files(workspace_dir)
|
|
672
|
-
|
|
673
|
-
commit_msg = f'git-p4son: p4 sync {depot_root}/...@{changelist}'
|
|
674
|
-
commit(commit_msg, workspace_dir, allow_empty=True)
|
|
675
|
-
log.success(f'Committed {len(dirty_files)} files')
|
|
737
|
+
|
|
738
|
+
# Sync each target changelist in turn, committing pure Perforce state
|
|
739
|
+
# for each. Local changes are merged back once at the very end so the
|
|
740
|
+
# intermediate commits stay clean.
|
|
741
|
+
for changelist, changelist_label in targets:
|
|
742
|
+
prep = _sync_pass(changelist, changelist_label, depot_root,
|
|
743
|
+
workspace_dir, pre_sync_head_commit, temp_root,
|
|
744
|
+
uses_crlf, clobber)
|
|
745
|
+
all_changed.extend(prep.changed)
|
|
746
|
+
all_ignored.extend(prep.ignored)
|
|
747
|
+
|
|
748
|
+
log.heading(f'Committing git changes for CL {changelist}')
|
|
749
|
+
dirty_files = get_dirty_files(workspace_dir)
|
|
750
|
+
if dirty_files:
|
|
751
|
+
add_all_files(workspace_dir)
|
|
752
|
+
commit_msg = f'git-p4son: p4 sync {depot_root}/...@{changelist}'
|
|
753
|
+
commit(commit_msg, workspace_dir, allow_empty=True)
|
|
754
|
+
log.success(f'Committed {len(dirty_files)} files')
|
|
676
755
|
|
|
677
756
|
# Post-commit: merge changed files back. Dedup by filepath in case
|
|
678
|
-
# the same file shows up in
|
|
757
|
+
# the same file shows up in multiple sync passes.
|
|
679
758
|
by_path: dict[str, ChangedFile] = {}
|
|
680
759
|
for cf in all_changed:
|
|
681
760
|
by_path[cf.filepath] = cf
|