p4cmd 2.8.6__tar.gz → 2.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- p4cmd-2.9.0/PKG-INFO +493 -0
- p4cmd-2.9.0/README.md +469 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/__init__.py +1 -1
- {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/p4cmd.py +215 -221
- p4cmd-2.9.0/p4cmd/p4errors.py +25 -0
- p4cmd-2.9.0/p4cmd/p4file.py +298 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/utils.py +21 -22
- p4cmd-2.9.0/p4cmd.egg-info/PKG-INFO +493 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd.egg-info/SOURCES.txt +8 -1
- p4cmd-2.9.0/p4cmd.egg-info/requires.txt +3 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd.egg-info/top_level.txt +1 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/setup.py +1 -0
- p4cmd-2.9.0/tests/__init__.py +0 -0
- p4cmd-2.9.0/tests/conftest.py +32 -0
- p4cmd-2.9.0/tests/test_integration.py +480 -0
- p4cmd-2.9.0/tests/test_p4client.py +573 -0
- p4cmd-2.9.0/tests/test_p4file.py +388 -0
- p4cmd-2.9.0/tests/test_utils.py +119 -0
- p4cmd-2.8.6/PKG-INFO +0 -211
- p4cmd-2.8.6/README.md +0 -190
- p4cmd-2.8.6/p4cmd/p4errors.py +0 -15
- p4cmd-2.8.6/p4cmd/p4file.py +0 -242
- p4cmd-2.8.6/p4cmd.egg-info/PKG-INFO +0 -211
- {p4cmd-2.8.6 → p4cmd-2.9.0}/LICENSE +0 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/__init__.py +0 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_DEPOT_ONLY.png +0 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_MOVED.png +0 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_NEED_SYNC.png +0 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_OPEN_FOR_ADD.png +0 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_OPEN_FOR_DELETE.png +0 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_OPEN_FOR_EDIT.png +0 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_UNKNOWN.png +0 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_UNTRACKED.png +0 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_UP_TO_DATE.png +0 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd.egg-info/dependency_links.txt +0 -0
- {p4cmd-2.8.6 → p4cmd-2.9.0}/setup.cfg +0 -0
p4cmd-2.9.0/PKG-INFO
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: p4cmd
|
|
3
|
+
Version: 2.9.0
|
|
4
|
+
Summary: Simple P4 python module
|
|
5
|
+
Home-page: https://github.com/nielsvaes/p4cmd
|
|
6
|
+
Author: Niels Vaes
|
|
7
|
+
Author-email: nielsvaes@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
14
|
+
Dynamic: author
|
|
15
|
+
Dynamic: author-email
|
|
16
|
+
Dynamic: classifier
|
|
17
|
+
Dynamic: description
|
|
18
|
+
Dynamic: description-content-type
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: license
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
Dynamic: provides-extra
|
|
23
|
+
Dynamic: summary
|
|
24
|
+
|
|
25
|
+
### P4CMD 🌴
|
|
26
|
+
|
|
27
|
+
A Python Perforce package that doesn't bring in any other packages to work.
|
|
28
|
+
Relies on the `p4` CLI installed on the system.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install p4cmd
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Getting started
|
|
35
|
+
|
|
36
|
+
### Creating a client
|
|
37
|
+
|
|
38
|
+
Pass the root path of your Perforce workspace, or use the `P4ROOT` environment variable:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from p4cmd import p4cmd
|
|
42
|
+
|
|
43
|
+
# Direct path
|
|
44
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
45
|
+
|
|
46
|
+
# Or with explicit credentials
|
|
47
|
+
p4 = p4cmd.P4Client("~/p4/MyGame", user="my.user", client="my-workspace", server="ssl:perforce:1666")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import os
|
|
52
|
+
from p4cmd import p4cmd
|
|
53
|
+
|
|
54
|
+
os.environ["P4ROOT"] = "~/p4/MyGame"
|
|
55
|
+
p4 = p4cmd.P4Client.from_env()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If the path you pass doesn't contain a `.p4config` file, P4Client will walk up the directory tree until it finds one and use that as `perforce_root`.
|
|
59
|
+
|
|
60
|
+
## P4Client API
|
|
61
|
+
|
|
62
|
+
### Connection & settings
|
|
63
|
+
|
|
64
|
+
| Method | Description |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `host_online(timeout=2.0)` | Check if the Perforce server is reachable |
|
|
67
|
+
| `get_ticket_expiration()` | Seconds until the authentication ticket expires |
|
|
68
|
+
| `get_p4_setting(setting)` | Read a Perforce setting (e.g. `"P4CLIENT"`) |
|
|
69
|
+
| `find_p4_client()` | Return the current workspace name |
|
|
70
|
+
| `find_p4_port()` | Return the server address |
|
|
71
|
+
| `set_workspace(workspace)` | Switch the active workspace |
|
|
72
|
+
| `set_perforce_root(root)` | Change the workspace root directory |
|
|
73
|
+
| `set_max_parallel_connections(value)` | Set max parallel connections for sync/submit (default 4) |
|
|
74
|
+
| `get_all_workspaces()` | List all workspaces for the current user |
|
|
75
|
+
|
|
76
|
+
### File operations
|
|
77
|
+
|
|
78
|
+
| Method | Description |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `add_or_edit_files(file_list, changelist="default")` | Add new files or check out existing ones — auto-detects which |
|
|
81
|
+
| `add_or_edit_folders(folders, include_subfolders=True, changelist="default")` | Same as above, for entire folders |
|
|
82
|
+
| `add_files(file_list, changelist="default")` | Mark files for add |
|
|
83
|
+
| `edit_files(file_list, changelist="default")` | Mark files for edit |
|
|
84
|
+
| `delete_files(file_list, changelist="default")` | Mark files for delete |
|
|
85
|
+
| `rename_file(old_path, new_path, changelist="default")` | P4 move/rename a file |
|
|
86
|
+
| `copy_file(original_path, copied_path, changelist="default")` | P4 copy a file |
|
|
87
|
+
| `revert_files(file_list, unchanged_only=False)` | Revert files |
|
|
88
|
+
| `revert_folders(folder_list, unchanged_only=False)` | Revert all files in folders |
|
|
89
|
+
| `sync_files(file_list, revision=-1, verify=True, force=False)` | Sync files to a specific revision |
|
|
90
|
+
| `sync_folders(folder_list)` | Sync entire folders |
|
|
91
|
+
| `reconcile_offline_files(file_list, add=True, edit=True, delete=True, changelist="default")` | Reconcile offline changes |
|
|
92
|
+
| `reconcile_offline_folders(folder_list, add=True, edit=True, delete=True, changelist="default")` | Reconcile offline changes in folders |
|
|
93
|
+
|
|
94
|
+
### Changelists
|
|
95
|
+
|
|
96
|
+
| Method | Description |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `make_new_changelist(description)` | Create a new numbered changelist |
|
|
99
|
+
| `get_or_make_changelist(description, case_sensitive=False)` | Find a CL by description, or create it |
|
|
100
|
+
| `changelist_exists(changelist)` | Check if a CL exists (by number or description) |
|
|
101
|
+
| `update_changelist_description(changelist, new_description)` | Update a CL's description |
|
|
102
|
+
| `delete_changelist(changelist, ...)` | Delete a CL (with options to obliterate, delete shelved files) |
|
|
103
|
+
| `get_pending_changelists(description_filter="", ...)` | List pending CLs, optionally filtered by description |
|
|
104
|
+
| `get_files_in_changelist(changelist="default")` | List depot paths in a CL |
|
|
105
|
+
| `get_all_files_in_all_changelists()` | List depot paths across all pending CLs |
|
|
106
|
+
| `get_changelist_for_file(depot_path)` | Find which CL contains a file |
|
|
107
|
+
| `move_files_to_changelist(file_list, changelist="default")` | Move files between changelists |
|
|
108
|
+
| `combine_changelists(source_changelists, target_changelist)` | Merge multiple CLs into one |
|
|
109
|
+
| `revert_changelist(unchanged_only=False, changelist="default")` | Revert all files in a CL |
|
|
110
|
+
| `submit_changelist(changelist, revert_unchanged_files=True)` | Submit a CL |
|
|
111
|
+
|
|
112
|
+
### Shelving
|
|
113
|
+
|
|
114
|
+
| Method | Description |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `shelve_files(changelist, file_list=None, revert_after_shelve=False, force=False)` | Shelve files |
|
|
117
|
+
| `unshelve_files(changelist, file_list=None, target_changelist="default", force=False, delete_shelved_files=False)` | Unshelve files |
|
|
118
|
+
| `get_shelved_files()` | List all shelved files and their CLs |
|
|
119
|
+
| `delete_shelf(changelist)` | Delete shelved files without deleting the CL |
|
|
120
|
+
|
|
121
|
+
### Path conversion & history
|
|
122
|
+
|
|
123
|
+
| Method | Description |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `get_depot_paths(paths)` | Convert local paths to depot paths |
|
|
126
|
+
| `get_local_paths(paths)` | Convert depot paths to local paths |
|
|
127
|
+
| `get_history(paths)` | Get change history for files |
|
|
128
|
+
|
|
129
|
+
### P4File conversion
|
|
130
|
+
|
|
131
|
+
| Method | Description |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `files_to_p4files(file_list, allow_invalid_files=False)` | Convert file paths to `P4File` objects with full status info |
|
|
134
|
+
| `folder_to_p4files(folder, include_subfolders=True, allow_invalid_files=False, specific_file_filter="")` | Same, for all files in a folder |
|
|
135
|
+
|
|
136
|
+
## P4File
|
|
137
|
+
|
|
138
|
+
`P4File` represents a single file and its Perforce status. You get these from `files_to_p4files` or `folder_to_p4files`. All attributes are available as both properties and legacy `get_`/`set_` methods.
|
|
139
|
+
|
|
140
|
+
### Properties
|
|
141
|
+
|
|
142
|
+
| Property | Type | Description |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `local_file_path` | `str` | Local filesystem path |
|
|
145
|
+
| `depot_file_path` | `str` | Depot path (`//depot/...`) |
|
|
146
|
+
| `action` | `str` | Current open action (`add`, `edit`, `delete`, `move/add`, `move/delete`) |
|
|
147
|
+
| `head_action` | `str` | Head revision action |
|
|
148
|
+
| `have_revision` | `int` | Local revision number |
|
|
149
|
+
| `head_revision` | `int` | Latest depot revision number |
|
|
150
|
+
| `last_submit_time` | `str` | Formatted timestamp of last submission |
|
|
151
|
+
| `last_submitted_by` | `str` | User who last submitted |
|
|
152
|
+
| `checked_out_by` | `list` | Users who have the file checked out |
|
|
153
|
+
| `file_size` | `str` | Raw file size from depot |
|
|
154
|
+
| `raw_data` | `str` | Raw fstat data |
|
|
155
|
+
|
|
156
|
+
### Status predicates
|
|
157
|
+
|
|
158
|
+
| Method | Description |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `get_status()` | Returns a `Status` constant (see below) |
|
|
161
|
+
| `is_open_for_add()` | File is marked for add |
|
|
162
|
+
| `is_open_for_edit()` | File is marked for edit |
|
|
163
|
+
| `is_marked_for_delete()` | File is marked for delete |
|
|
164
|
+
| `is_checked_out()` | File has any open action |
|
|
165
|
+
| `is_untracked()` / `is_local_only()` | File exists locally but not in depot |
|
|
166
|
+
| `is_depot_only()` | File exists in depot but was never synced |
|
|
167
|
+
| `is_deleted()` | Head revision is a delete |
|
|
168
|
+
| `is_moved_deleted()` | File was moved away |
|
|
169
|
+
| `is_moved_added()` | File was moved here |
|
|
170
|
+
| `is_up_to_date()` | Local and head revisions match |
|
|
171
|
+
| `is_under_client_root()` | File is within the workspace mapping |
|
|
172
|
+
| `is_valid()` | Has at least one path set |
|
|
173
|
+
| `needs_syncing()` | Behind head revision and needs sync |
|
|
174
|
+
| `get_file_size(in_megabyte=True)` | File size in MB or bytes |
|
|
175
|
+
|
|
176
|
+
### Status constants
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from p4cmd.p4file import Status
|
|
180
|
+
|
|
181
|
+
Status.OPEN_FOR_ADD
|
|
182
|
+
Status.OPEN_FOR_EDIT
|
|
183
|
+
Status.OPEN_FOR_DELETE
|
|
184
|
+
Status.NEED_SYNC
|
|
185
|
+
Status.DEPOT_ONLY
|
|
186
|
+
Status.UP_TO_DATE
|
|
187
|
+
Status.UNTRACKED
|
|
188
|
+
Status.MOVED
|
|
189
|
+
Status.DELETED
|
|
190
|
+
Status.MOVED_DELETED
|
|
191
|
+
Status.UNKNOWN
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Other methods
|
|
195
|
+
|
|
196
|
+
| Method | Description |
|
|
197
|
+
|---|---|
|
|
198
|
+
| `update_self(p4client)` | Refresh all attributes from the server |
|
|
199
|
+
| `update_last_submitted_by(p4client)` | Refresh just the `last_submitted_by` field |
|
|
200
|
+
|
|
201
|
+
## Usage examples
|
|
202
|
+
|
|
203
|
+
### Check out or add files
|
|
204
|
+
|
|
205
|
+
You can mix local and depot paths. If you pass a changelist description that doesn't exist, it will be created automatically.
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
209
|
+
|
|
210
|
+
files = ["~/p4/MyGame/Raw/Characters/info_file.json",
|
|
211
|
+
"//MyGame/Main/Templates/morefiles.json"]
|
|
212
|
+
|
|
213
|
+
p4.add_or_edit_files(files, changelist="My new changelist")
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Inspect files in bulk
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
220
|
+
p4files = p4.folder_to_p4files("~/p4/MyGame/Animations")
|
|
221
|
+
|
|
222
|
+
files_to_sync = []
|
|
223
|
+
for pf in p4files:
|
|
224
|
+
if pf.checked_out_by:
|
|
225
|
+
print(f"depot: {pf.depot_file_path}")
|
|
226
|
+
print(f"local: {pf.local_file_path}")
|
|
227
|
+
print(f"status: {pf.get_status()}")
|
|
228
|
+
print(f"checked out by: {pf.checked_out_by}")
|
|
229
|
+
if pf.needs_syncing():
|
|
230
|
+
files_to_sync.append(pf.local_file_path)
|
|
231
|
+
|
|
232
|
+
p4.sync_files(files_to_sync)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Changelists
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
239
|
+
|
|
240
|
+
# List all pending changelists
|
|
241
|
+
all_cls = p4.get_pending_changelists()
|
|
242
|
+
# [35272, 33160, 32756, 30872, 27277, 'default']
|
|
243
|
+
|
|
244
|
+
# Filter by description
|
|
245
|
+
houdini_cls = p4.get_pending_changelists(description_filter="houdini")
|
|
246
|
+
# [35272, 33160, 'default']
|
|
247
|
+
|
|
248
|
+
# Exact match
|
|
249
|
+
exact = p4.get_pending_changelists(
|
|
250
|
+
description_filter="[houdini tools]",
|
|
251
|
+
perfect_match_only=True,
|
|
252
|
+
case_sensitive=True,
|
|
253
|
+
)
|
|
254
|
+
# [33160, 'default']
|
|
255
|
+
|
|
256
|
+
# Get or create a changelist
|
|
257
|
+
cl = p4.get_or_make_changelist("My tools CL")
|
|
258
|
+
|
|
259
|
+
# List files in a changelist (by number or description)
|
|
260
|
+
files = p4.get_files_in_changelist(33160)
|
|
261
|
+
files = p4.get_files_in_changelist("[houdini tools]")
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Submit
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
268
|
+
p4.submit_changelist("Character files", revert_unchanged_files=False)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Sync with parallel connections
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
275
|
+
p4.set_max_parallel_connections(2)
|
|
276
|
+
p4.sync_folders(["//Content/Basketball/Players/"])
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Revert files
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
283
|
+
|
|
284
|
+
# Revert specific files
|
|
285
|
+
p4.revert_files(["~/p4/MyGame/Raw/Characters/info_file.json"])
|
|
286
|
+
|
|
287
|
+
# Revert only unchanged files (keep actual edits)
|
|
288
|
+
p4.revert_files(["~/p4/MyGame/Raw/Characters/info_file.json"], unchanged_only=True)
|
|
289
|
+
|
|
290
|
+
# Revert everything in a folder
|
|
291
|
+
p4.revert_folders(["~/p4/MyGame/Raw/Characters"])
|
|
292
|
+
|
|
293
|
+
# Revert all files in a changelist
|
|
294
|
+
p4.revert_changelist(changelist=12345)
|
|
295
|
+
p4.revert_changelist(changelist=12345, unchanged_only=True)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Rename and copy files
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
302
|
+
|
|
303
|
+
# Rename / move a file
|
|
304
|
+
p4.rename_file(
|
|
305
|
+
"//MyGame/Main/Characters/old_name.fbx",
|
|
306
|
+
"//MyGame/Main/Characters/new_name.fbx",
|
|
307
|
+
changelist="Rename characters",
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Copy a file (branching)
|
|
311
|
+
p4.copy_file(
|
|
312
|
+
"//MyGame/Main/Templates/base_config.json",
|
|
313
|
+
"//MyGame/Main/Templates/new_config.json",
|
|
314
|
+
changelist="Copy config template",
|
|
315
|
+
)
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Delete files
|
|
319
|
+
|
|
320
|
+
```python
|
|
321
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
322
|
+
p4.delete_files(
|
|
323
|
+
["~/p4/MyGame/Raw/Characters/unused_file.json"],
|
|
324
|
+
changelist="Clean up unused files",
|
|
325
|
+
)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Move files between changelists
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
332
|
+
|
|
333
|
+
# Move files from default to a named changelist
|
|
334
|
+
p4.move_files_to_changelist(
|
|
335
|
+
["//MyGame/Main/Characters/hero.fbx"],
|
|
336
|
+
changelist="Character updates",
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Merge multiple changelists into one
|
|
340
|
+
p4.combine_changelists(
|
|
341
|
+
source_changelists=[33160, 33161],
|
|
342
|
+
target_changelist=33162,
|
|
343
|
+
)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Manage changelist descriptions
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
350
|
+
|
|
351
|
+
# Update a changelist description (supports multi-line)
|
|
352
|
+
p4.update_changelist_description(
|
|
353
|
+
12345,
|
|
354
|
+
"Updated character models\nReviewed by: artist@studio",
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# Check if a changelist exists
|
|
358
|
+
if p4.changelist_exists("My tools CL"):
|
|
359
|
+
print("CL exists")
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Convert between depot and local paths
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
366
|
+
|
|
367
|
+
# Local paths -> depot paths
|
|
368
|
+
depot_paths = p4.get_depot_paths(["~/p4/MyGame/Raw/Characters"])
|
|
369
|
+
# ['//MyGame/Main/Raw/Characters']
|
|
370
|
+
|
|
371
|
+
# Depot paths -> local paths
|
|
372
|
+
local_paths = p4.get_local_paths(["//MyGame/Main/Raw/Characters"])
|
|
373
|
+
# ['~/p4/MyGame/Raw/Characters']
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### File history
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
380
|
+
history = p4.get_history(["//MyGame/Main/Characters/hero.fbx"])
|
|
381
|
+
for entry in history:
|
|
382
|
+
print(entry)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Shelving
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
389
|
+
|
|
390
|
+
# Shelve and optionally revert
|
|
391
|
+
p4.shelve_files(changelist=12345, revert_after_shelve=True)
|
|
392
|
+
|
|
393
|
+
# Unshelve into a different changelist
|
|
394
|
+
p4.unshelve_files(12345, target_changelist=67890)
|
|
395
|
+
|
|
396
|
+
# List all your shelved files
|
|
397
|
+
shelved = p4.get_shelved_files()
|
|
398
|
+
for depot_path, cl_number in shelved:
|
|
399
|
+
print(f"{depot_path} in CL {cl_number}")
|
|
400
|
+
|
|
401
|
+
# Delete shelved files without deleting the changelist
|
|
402
|
+
p4.delete_shelf(12345)
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Reconcile offline work
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
409
|
+
p4.reconcile_offline_folders(
|
|
410
|
+
["~/p4/MyGame/Raw/Characters"],
|
|
411
|
+
add=True, edit=True, delete=False,
|
|
412
|
+
changelist="Offline reconcile",
|
|
413
|
+
)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Check server connection
|
|
417
|
+
|
|
418
|
+
```python
|
|
419
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
420
|
+
|
|
421
|
+
if p4.host_online():
|
|
422
|
+
expiry = p4.get_ticket_expiration()
|
|
423
|
+
if expiry is not None:
|
|
424
|
+
print(f"Ticket expires in {expiry} seconds")
|
|
425
|
+
else:
|
|
426
|
+
print("Server unreachable")
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Find which changelist a file is in
|
|
430
|
+
|
|
431
|
+
```python
|
|
432
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
433
|
+
cl = p4.get_changelist_for_file("//MyGame/Main/Characters/hero.fbx")
|
|
434
|
+
if cl != -1:
|
|
435
|
+
print(f"File is in CL {cl}")
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Filter P4Files by status
|
|
439
|
+
|
|
440
|
+
```python
|
|
441
|
+
from p4cmd.p4file import Status
|
|
442
|
+
|
|
443
|
+
p4 = p4cmd.P4Client("~/p4/MyGame")
|
|
444
|
+
p4files = p4.folder_to_p4files("~/p4/MyGame/Raw/Characters")
|
|
445
|
+
|
|
446
|
+
# Group files by status
|
|
447
|
+
for pf in p4files:
|
|
448
|
+
status = pf.get_status()
|
|
449
|
+
if status == Status.NEED_SYNC:
|
|
450
|
+
print(f"Needs sync: {pf.local_file_path}")
|
|
451
|
+
elif status == Status.DEPOT_ONLY:
|
|
452
|
+
print(f"Never synced: {pf.depot_file_path}")
|
|
453
|
+
elif status == Status.OPEN_FOR_EDIT:
|
|
454
|
+
print(f"Being edited: {pf.local_file_path}")
|
|
455
|
+
|
|
456
|
+
# Get file sizes
|
|
457
|
+
large_files = [
|
|
458
|
+
pf for pf in p4files
|
|
459
|
+
if pf.get_file_size(in_megabyte=True) and pf.get_file_size(in_megabyte=True) > 100
|
|
460
|
+
]
|
|
461
|
+
print(f"{len(large_files)} files over 100 MB")
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Development setup
|
|
465
|
+
|
|
466
|
+
### Running tests
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
pip install pytest
|
|
470
|
+
pytest tests/ -v
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Integration tests
|
|
474
|
+
|
|
475
|
+
Integration tests communicate with a real Perforce server. Copy `tests/.env.example`
|
|
476
|
+
to `tests/.env` and fill in your details, then run:
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
pytest tests/test_integration.py -v
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Pre-commit hook
|
|
483
|
+
|
|
484
|
+
A pre-commit hook that runs the unit test suite before every commit is included.
|
|
485
|
+
To install it:
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
cp scripts/pre-commit .git/hooks/pre-commit
|
|
489
|
+
chmod +x .git/hooks/pre-commit
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
The CI pipeline also runs all tests before publishing to PyPI, so a release
|
|
493
|
+
will never be created from a failing build.
|