git-p4son 0.3.3__tar.gz → 0.3.4__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.3 → git_p4son-0.3.4}/PKG-INFO +9 -1
- {git_p4son-0.3.3 → git_p4son-0.3.4}/README.md +8 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/__init__.py +1 -1
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/perforce.py +10 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/sync.py +141 -54
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/sync_split.py +1 -1
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son.egg-info/PKG-INFO +9 -1
- {git_p4son-0.3.3 → git_p4son-0.3.4}/pyproject.toml +1 -1
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_perforce.py +30 -1
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_sync.py +177 -8
- {git_p4son-0.3.3 → git_p4son-0.3.4}/LICENSE +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/__main__.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/alias.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/changelist_store.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/cli.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/common.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/complete.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/completions/_git-p4son +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/completions/git-p4son.bash +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/completions/git-p4son.ps1 +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/config.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/git.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/hooks.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/init.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/lib.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/list_changes.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/log.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/new.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/review.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/state.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son/update.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son.egg-info/SOURCES.txt +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son.egg-info/dependency_links.txt +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son.egg-info/entry_points.txt +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/git_p4son.egg-info/top_level.txt +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/setup.cfg +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_alias.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_changelist_store.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_cli.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_common.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_complete.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_config.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_git.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_hooks.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_init.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_lib_changelist.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_lib_edit.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_lib_review.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_list_changes.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_log.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_new.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_review.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_state.py +0 -0
- {git_p4son-0.3.3 → git_p4son-0.3.4}/tests/test_sync_split.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.4
|
|
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
|
|
@@ -231,6 +231,14 @@ be managed manually or via Perforce directly.
|
|
|
231
231
|
After syncing, git-p4son prints a summary of how each writable file was handled, and instructs you to review and
|
|
232
232
|
commit any files that need attention.
|
|
233
233
|
|
|
234
|
+
**Workspaces with `allwrite`** - When your client spec has the `allwrite` option, every file is writable by design,
|
|
235
|
+
so the read-only flag carries no information about local changes. git-p4son classifies files exactly as above (the
|
|
236
|
+
verdict comes from git, not from the flag), but leaves the write bit alone on files it finds unchanged: with
|
|
237
|
+
`noclobber` Perforce compares digests before refusing to overwrite a writable file, and unchanged files pass that
|
|
238
|
+
check. Only files with local changes are made read-only for the duration of the sync, since those are the ones
|
|
239
|
+
Perforce would otherwise refuse to overwrite. The same digest check applies to git-ignored files: unchanged ones sync
|
|
240
|
+
normally, and after the sync git-p4son lists the ones Perforce skipped because they were modified locally.
|
|
241
|
+
|
|
234
242
|
## Usage
|
|
235
243
|
|
|
236
244
|
git-p4son provides nine commands: `init`, `sync`, `sync-split`, `new`, `update`, `review`, `list-changes`,
|
|
@@ -208,6 +208,14 @@ be managed manually or via Perforce directly.
|
|
|
208
208
|
After syncing, git-p4son prints a summary of how each writable file was handled, and instructs you to review and
|
|
209
209
|
commit any files that need attention.
|
|
210
210
|
|
|
211
|
+
**Workspaces with `allwrite`** - When your client spec has the `allwrite` option, every file is writable by design,
|
|
212
|
+
so the read-only flag carries no information about local changes. git-p4son classifies files exactly as above (the
|
|
213
|
+
verdict comes from git, not from the flag), but leaves the write bit alone on files it finds unchanged: with
|
|
214
|
+
`noclobber` Perforce compares digests before refusing to overwrite a writable file, and unchanged files pass that
|
|
215
|
+
check. Only files with local changes are made read-only for the duration of the sync, since those are the ones
|
|
216
|
+
Perforce would otherwise refuse to overwrite. The same digest check applies to git-ignored files: unchanged ones sync
|
|
217
|
+
normally, and after the sync git-p4son lists the ones Perforce skipped because they were modified locally.
|
|
218
|
+
|
|
211
219
|
## Usage
|
|
212
220
|
|
|
213
221
|
git-p4son provides nine commands: `init`, `sync`, `sync-split`, `new`, `update`, `review`, `list-changes`,
|
|
@@ -77,6 +77,10 @@ class P4ClientSpec:
|
|
|
77
77
|
def clobber(self) -> bool:
|
|
78
78
|
return 'clobber' in self.options
|
|
79
79
|
|
|
80
|
+
@property
|
|
81
|
+
def allwrite(self) -> bool:
|
|
82
|
+
return 'allwrite' in self.options
|
|
83
|
+
|
|
80
84
|
@property
|
|
81
85
|
def uses_crlf(self) -> bool:
|
|
82
86
|
if self.line_end == 'win':
|
|
@@ -472,6 +476,12 @@ def is_binary_file_type(head_type: str) -> bool:
|
|
|
472
476
|
return base_type in ('binary', 'ubinary')
|
|
473
477
|
|
|
474
478
|
|
|
479
|
+
def is_always_writable_file_type(head_type: str) -> bool:
|
|
480
|
+
"""Check if a Perforce headType carries the +w (always writable) modifier."""
|
|
481
|
+
_, _, modifiers = head_type.partition('+')
|
|
482
|
+
return 'w' in modifiers
|
|
483
|
+
|
|
484
|
+
|
|
475
485
|
@dataclass
|
|
476
486
|
class P4SyncPreviewFile:
|
|
477
487
|
"""A file a sync would affect, from p4 sync -n output.
|
|
@@ -26,6 +26,7 @@ from .perforce import (
|
|
|
26
26
|
get_client_spec,
|
|
27
27
|
get_latest_changelist,
|
|
28
28
|
get_writable_files,
|
|
29
|
+
is_always_writable_file_type,
|
|
29
30
|
is_binary_file_type,
|
|
30
31
|
p4_fstat_file_info,
|
|
31
32
|
p4_get_opened_files,
|
|
@@ -101,6 +102,11 @@ class WritableSyncFileSet:
|
|
|
101
102
|
"""Writable files found during sync preview, classified."""
|
|
102
103
|
changed: list[ChangedFile] = field(default_factory=list)
|
|
103
104
|
ignored: list[str] = field(default_factory=list)
|
|
105
|
+
always_writable: list[str] = field(default_factory=list)
|
|
106
|
+
# Ignored files p4 actually refused to overwrite, known only after the
|
|
107
|
+
# sync. Without allwrite that is all of ignored; with it, only the
|
|
108
|
+
# locally modified ones.
|
|
109
|
+
not_synced: list[str] = field(default_factory=list)
|
|
104
110
|
|
|
105
111
|
|
|
106
112
|
def _stage_temp_content(temp_root: str, rel_path: str, suffix: str,
|
|
@@ -154,12 +160,63 @@ def _stage_changed_file(meta: _ChangedFileMeta, pre_sync_head_commit: str,
|
|
|
154
160
|
is_binary=is_binary, added_both=meta.added_both)
|
|
155
161
|
|
|
156
162
|
|
|
163
|
+
def _log_prepare_summary(result: WritableSyncFileSet, workspace_dir: str,
|
|
164
|
+
clobber: bool, unchanged_count: int = 0,
|
|
165
|
+
allwrite: bool = False) -> None:
|
|
166
|
+
"""Log what the writable-file classification found."""
|
|
167
|
+
log.heading('Prepare sync summary')
|
|
168
|
+
if unchanged_count:
|
|
169
|
+
label = 'file' if unchanged_count == 1 else 'files'
|
|
170
|
+
log.success(
|
|
171
|
+
f'{unchanged_count} writable {label} unchanged, '
|
|
172
|
+
'skipping merge')
|
|
173
|
+
|
|
174
|
+
if result.always_writable:
|
|
175
|
+
count = len(result.always_writable)
|
|
176
|
+
label = 'file is' if count == 1 else 'files are'
|
|
177
|
+
log.success(
|
|
178
|
+
f'{count} git-ignored {label} always writable (+w), '
|
|
179
|
+
'p4 syncs them normally')
|
|
180
|
+
|
|
181
|
+
if result.changed:
|
|
182
|
+
count = len(result.changed)
|
|
183
|
+
label = 'file has' if count == 1 else 'files have'
|
|
184
|
+
log.warning(f'{count} {label} local changes, will merge after sync')
|
|
185
|
+
for cf in result.changed:
|
|
186
|
+
log.info(os.path.relpath(cf.filepath, workspace_dir))
|
|
187
|
+
|
|
188
|
+
if result.ignored:
|
|
189
|
+
count = len(result.ignored)
|
|
190
|
+
label = 'file' if count == 1 else 'files'
|
|
191
|
+
if clobber:
|
|
192
|
+
# These files are left writable, so with clobber enabled p4
|
|
193
|
+
# overwrites them during sync rather than refusing to.
|
|
194
|
+
log.warning(
|
|
195
|
+
f'{count} git-ignored writable {label} will be overwritten '
|
|
196
|
+
'by p4 (clobber is enabled on the workspace)')
|
|
197
|
+
elif allwrite:
|
|
198
|
+
# With allwrite every file is writable, and p4 digest-compares
|
|
199
|
+
# before refusing to clobber one: unchanged files sync, modified
|
|
200
|
+
# ones are skipped. Which is which is only known after the sync,
|
|
201
|
+
# which reports the skipped ones, so they are not listed here.
|
|
202
|
+
log.info(
|
|
203
|
+
f'{count} git-ignored writable {label} will be synced '
|
|
204
|
+
'unless modified locally')
|
|
205
|
+
return
|
|
206
|
+
else:
|
|
207
|
+
log.warning(
|
|
208
|
+
f'{count} git-ignored writable {label} will not be synced')
|
|
209
|
+
for f in result.ignored:
|
|
210
|
+
log.info(os.path.relpath(f, workspace_dir))
|
|
211
|
+
|
|
212
|
+
|
|
157
213
|
def prepare_writable_files(preview_files: list[P4SyncPreviewFile],
|
|
158
214
|
workspace_dir: str,
|
|
159
215
|
pre_sync_head_commit: str,
|
|
160
216
|
temp_root: str,
|
|
161
217
|
uses_crlf: bool = False,
|
|
162
|
-
clobber: bool = False
|
|
218
|
+
clobber: bool = False,
|
|
219
|
+
allwrite: bool = False) -> WritableSyncFileSet:
|
|
163
220
|
"""Check which preview files are writable on disk and prepare them for sync.
|
|
164
221
|
|
|
165
222
|
For tracked writable files, queries Perforce for the file type (binary
|
|
@@ -172,7 +229,12 @@ def prepare_writable_files(preview_files: list[P4SyncPreviewFile],
|
|
|
172
229
|
clobber reflects the workspace clobber option. git-ignored writable files
|
|
173
230
|
are left writable and rely on p4 refusing to overwrite them, which only
|
|
174
231
|
holds when clobber is off; with clobber on p4 overwrites them, so the
|
|
175
|
-
reported outcome differs.
|
|
232
|
+
reported outcome differs. Ignored files whose Perforce type is +w
|
|
233
|
+
(always writable) are exempt: p4 overwrites those regardless, so they
|
|
234
|
+
sync normally and are reported separately.
|
|
235
|
+
|
|
236
|
+
allwrite reflects the workspace allwrite option, which narrows how many
|
|
237
|
+
files have their write bit stripped (see _clear_write_bits).
|
|
176
238
|
"""
|
|
177
239
|
result = WritableSyncFileSet()
|
|
178
240
|
|
|
@@ -203,7 +265,27 @@ def prepare_writable_files(preview_files: list[P4SyncPreviewFile],
|
|
|
203
265
|
result.ignored = [f for f in writable if f not in tracked_set]
|
|
204
266
|
log.success(f'{len(tracked)} tracked, {len(result.ignored)} ignored')
|
|
205
267
|
|
|
268
|
+
if result.ignored:
|
|
269
|
+
# A file whose Perforce type carries the +w modifier is meant to be
|
|
270
|
+
# writable in the workspace. p4 overwrites it on sync without a
|
|
271
|
+
# clobber error, so it is not at risk of being skipped and must not
|
|
272
|
+
# be reported as such.
|
|
273
|
+
log.heading('Checking ignored files for the +w (always writable) type')
|
|
274
|
+
ignored_info = p4_fstat_file_info(result.ignored, workspace_dir)
|
|
275
|
+
always_writable = set()
|
|
276
|
+
for f in result.ignored:
|
|
277
|
+
info = ignored_info.get(f)
|
|
278
|
+
if info and is_always_writable_file_type(info.head_type):
|
|
279
|
+
always_writable.add(f)
|
|
280
|
+
result.always_writable = [f for f in result.ignored
|
|
281
|
+
if f in always_writable]
|
|
282
|
+
result.ignored = [f for f in result.ignored
|
|
283
|
+
if f not in always_writable]
|
|
284
|
+
log.success(f'{len(result.always_writable)} always writable (+w)')
|
|
285
|
+
|
|
206
286
|
if not tracked:
|
|
287
|
+
_log_prepare_summary(result, workspace_dir, clobber,
|
|
288
|
+
allwrite=allwrite)
|
|
207
289
|
return result
|
|
208
290
|
|
|
209
291
|
# Pass 1: decide which files the user modified since their baseline by
|
|
@@ -212,10 +294,12 @@ def prepare_writable_files(preview_files: list[P4SyncPreviewFile],
|
|
|
212
294
|
# one cat-file); with hundreds of writable files after a branch switch,
|
|
213
295
|
# per-file process spawning dominated the entire sync.
|
|
214
296
|
log.heading('Detecting modified tracked writable files')
|
|
215
|
-
|
|
216
|
-
#
|
|
217
|
-
|
|
218
|
-
|
|
297
|
+
if not allwrite:
|
|
298
|
+
# Without allwrite, noclobber makes p4 refuse to overwrite any
|
|
299
|
+
# writable file, so every tracked file goes read-only whether it
|
|
300
|
+
# changed or not. The modified ones get their local content back
|
|
301
|
+
# from the post-sync merge.
|
|
302
|
+
_clear_write_bits(tracked)
|
|
219
303
|
|
|
220
304
|
rel_paths = {f: os.path.relpath(f, workspace_dir) for f in tracked}
|
|
221
305
|
candidates = [f for f in tracked if f not in added_upstream]
|
|
@@ -263,6 +347,15 @@ def prepare_writable_files(preview_files: list[P4SyncPreviewFile],
|
|
|
263
347
|
|
|
264
348
|
log.success(f'{len(metas)} changed, {unchanged_count} unchanged')
|
|
265
349
|
|
|
350
|
+
if allwrite:
|
|
351
|
+
# An allwrite workspace is writable by design, so the write bit is
|
|
352
|
+
# left alone on files p4 will overwrite cleanly: with noclobber it
|
|
353
|
+
# digest-compares first and these still match the have revision,
|
|
354
|
+
# and with clobber it overwrites regardless. Only the modified
|
|
355
|
+
# files, the ones noclobber would refuse on, are stripped; the
|
|
356
|
+
# post-sync merge makes them writable again.
|
|
357
|
+
_clear_write_bits([m.filepath for m in metas])
|
|
358
|
+
|
|
266
359
|
# Pass 2: query Perforce for file type only on the changed subset. The
|
|
267
360
|
# binary verdict must be known before staging so text content can be
|
|
268
361
|
# written once in the workspace line ending (its only authoritative
|
|
@@ -283,37 +376,17 @@ def prepare_writable_files(preview_files: list[P4SyncPreviewFile],
|
|
|
283
376
|
is_binary, uses_crlf))
|
|
284
377
|
log.success('')
|
|
285
378
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
label = 'file' if unchanged_count == 1 else 'files'
|
|
290
|
-
log.success(
|
|
291
|
-
f'{unchanged_count} writable {label} unchanged, '
|
|
292
|
-
'skipping merge')
|
|
379
|
+
_log_prepare_summary(result, workspace_dir, clobber, unchanged_count,
|
|
380
|
+
allwrite)
|
|
381
|
+
return result
|
|
293
382
|
|
|
294
|
-
if result.changed:
|
|
295
|
-
count = len(result.changed)
|
|
296
|
-
label = 'file has' if count == 1 else 'files have'
|
|
297
|
-
log.warning(f'{count} {label} local changes, will merge after sync')
|
|
298
|
-
for cf in result.changed:
|
|
299
|
-
log.info(os.path.relpath(cf.filepath, workspace_dir))
|
|
300
383
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
log.warning(
|
|
308
|
-
f'{count} git-ignored writable {label} will be overwritten '
|
|
309
|
-
'by p4 (clobber is enabled on the workspace)')
|
|
310
|
-
else:
|
|
311
|
-
log.warning(
|
|
312
|
-
f'{count} git-ignored writable {label} will not be synced')
|
|
313
|
-
for f in result.ignored:
|
|
314
|
-
log.info(os.path.relpath(f, workspace_dir))
|
|
315
|
-
|
|
316
|
-
return result
|
|
384
|
+
def _clear_write_bits(filepaths: list[str]) -> None:
|
|
385
|
+
"""Remove user write permission from each file, making p4 willing to
|
|
386
|
+
overwrite it."""
|
|
387
|
+
for filepath in filepaths:
|
|
388
|
+
mode = os.stat(filepath).st_mode
|
|
389
|
+
os.chmod(filepath, mode & ~stat.S_IWUSR)
|
|
317
390
|
|
|
318
391
|
|
|
319
392
|
def _make_writable(filepath: str) -> None:
|
|
@@ -464,12 +537,12 @@ def _merge_changed_files(changed_files: list[ChangedFile],
|
|
|
464
537
|
|
|
465
538
|
def p4_sync(changelist: int, label: str, depot_root: str,
|
|
466
539
|
workspace_dir: str,
|
|
467
|
-
expected_clobber: set[str] | None = None) ->
|
|
540
|
+
expected_clobber: set[str] | None = None) -> list[str]:
|
|
468
541
|
"""Sync files from Perforce.
|
|
469
542
|
|
|
470
543
|
If expected_clobber is provided, clobber errors for those files are
|
|
471
|
-
tolerated (they are git-ignored writable files)
|
|
472
|
-
errors cause a raise.
|
|
544
|
+
tolerated (they are git-ignored writable files) and the files are
|
|
545
|
+
returned. Unexpected clobber errors cause a raise.
|
|
473
546
|
"""
|
|
474
547
|
log.heading(f'Syncing to {label} CL ({changelist})')
|
|
475
548
|
|
|
@@ -481,6 +554,7 @@ def p4_sync(changelist: int, label: str, depot_root: str,
|
|
|
481
554
|
if result.elapsed:
|
|
482
555
|
log.elapsed(result.elapsed)
|
|
483
556
|
log.success(output_processor.get_summary())
|
|
557
|
+
return []
|
|
484
558
|
except RunError as e:
|
|
485
559
|
writable_files = get_writable_files(e.stderr)
|
|
486
560
|
if not writable_files:
|
|
@@ -499,6 +573,7 @@ def p4_sync(changelist: int, label: str, depot_root: str,
|
|
|
499
573
|
log.warning(
|
|
500
574
|
f'{len(writable_files)} expected clobber errors (git-ignored files)'
|
|
501
575
|
)
|
|
576
|
+
return writable_files
|
|
502
577
|
|
|
503
578
|
|
|
504
579
|
def _handle_clobber_warning(clobber: bool, workspace_dir: str) -> bool:
|
|
@@ -601,7 +676,8 @@ def sync_preflight(depot_root: str, workspace_dir: str, invocation_dir: str,
|
|
|
601
676
|
|
|
602
677
|
def _sync_pass(changelist: int, label: str, depot_root: str,
|
|
603
678
|
workspace_dir: str, pre_sync_head_commit: str, temp_root: str,
|
|
604
|
-
uses_crlf: bool, clobber: bool
|
|
679
|
+
uses_crlf: bool, clobber: bool,
|
|
680
|
+
allwrite: bool) -> WritableSyncFileSet:
|
|
605
681
|
"""Run one sync pass: preview, prepare writable files, and p4 sync.
|
|
606
682
|
|
|
607
683
|
Returns the classified writable file set. p4 sync is skipped when the
|
|
@@ -610,10 +686,11 @@ def _sync_pass(changelist: int, label: str, depot_root: str,
|
|
|
610
686
|
preview = p4_sync_preview(changelist, depot_root, workspace_dir)
|
|
611
687
|
prep = prepare_writable_files(preview, workspace_dir, pre_sync_head_commit,
|
|
612
688
|
temp_root, uses_crlf=uses_crlf,
|
|
613
|
-
clobber=clobber)
|
|
689
|
+
clobber=clobber, allwrite=allwrite)
|
|
614
690
|
if preview:
|
|
615
|
-
p4_sync(changelist, label, depot_root,
|
|
616
|
-
|
|
691
|
+
prep.not_synced = p4_sync(changelist, label, depot_root,
|
|
692
|
+
workspace_dir,
|
|
693
|
+
expected_clobber=set(prep.ignored))
|
|
617
694
|
return prep
|
|
618
695
|
|
|
619
696
|
|
|
@@ -774,6 +851,7 @@ def sync_command(args: argparse.Namespace) -> int:
|
|
|
774
851
|
# (client_spec was fetched above to resolve the depot root.)
|
|
775
852
|
uses_crlf = bool(client_spec and client_spec.uses_crlf)
|
|
776
853
|
clobber = bool(client_spec and client_spec.clobber)
|
|
854
|
+
allwrite = bool(client_spec and client_spec.allwrite)
|
|
777
855
|
|
|
778
856
|
# Prompted before the preflight so declining here costs nothing: no
|
|
779
857
|
# workspace queries, and no hooks fired for a sync that is abandoned.
|
|
@@ -797,12 +875,13 @@ def sync_command(args: argparse.Namespace) -> int:
|
|
|
797
875
|
if resync_last_synced:
|
|
798
876
|
_sync_pass(last_sync.changelist, LAST_SYNCED_LABEL, depot_root,
|
|
799
877
|
workspace_dir, pre_sync_head_commit, temp_root,
|
|
800
|
-
uses_crlf, clobber)
|
|
878
|
+
uses_crlf, clobber, allwrite)
|
|
801
879
|
run_hooks('post-sync', workspace_dir, invocation_dir)
|
|
802
880
|
return 0
|
|
803
881
|
|
|
804
882
|
all_changed: list[ChangedFile] = []
|
|
805
883
|
all_ignored: list[str] = []
|
|
884
|
+
all_not_synced: list[str] = []
|
|
806
885
|
last_changelist = last_sync.changelist if last_sync else None
|
|
807
886
|
|
|
808
887
|
# Catch-up pass to the last synced changelist. This makes locally
|
|
@@ -812,9 +891,10 @@ def sync_command(args: argparse.Namespace) -> int:
|
|
|
812
891
|
if last_changelist is not None:
|
|
813
892
|
prep = _sync_pass(last_changelist, LAST_SYNCED_LABEL,
|
|
814
893
|
depot_root, workspace_dir, pre_sync_head_commit,
|
|
815
|
-
temp_root, uses_crlf, clobber)
|
|
894
|
+
temp_root, uses_crlf, clobber, allwrite)
|
|
816
895
|
all_changed.extend(prep.changed)
|
|
817
896
|
all_ignored.extend(prep.ignored)
|
|
897
|
+
all_not_synced.extend(prep.not_synced)
|
|
818
898
|
|
|
819
899
|
# Sync each target changelist in turn, committing pure Perforce state
|
|
820
900
|
# for each. Local changes are merged back once at the very end so the
|
|
@@ -822,9 +902,10 @@ def sync_command(args: argparse.Namespace) -> int:
|
|
|
822
902
|
for changelist, changelist_label in targets:
|
|
823
903
|
prep = _sync_pass(changelist, changelist_label, depot_root,
|
|
824
904
|
workspace_dir, pre_sync_head_commit, temp_root,
|
|
825
|
-
uses_crlf, clobber)
|
|
905
|
+
uses_crlf, clobber, allwrite)
|
|
826
906
|
all_changed.extend(prep.changed)
|
|
827
907
|
all_ignored.extend(prep.ignored)
|
|
908
|
+
all_not_synced.extend(prep.not_synced)
|
|
828
909
|
|
|
829
910
|
log.heading(f'Committing git changes for CL {changelist}')
|
|
830
911
|
dirty_files = get_dirty_files(workspace_dir)
|
|
@@ -844,15 +925,21 @@ def sync_command(args: argparse.Namespace) -> int:
|
|
|
844
925
|
|
|
845
926
|
# Report git-ignored writable files. Left writable, they are only
|
|
846
927
|
# preserved because p4 refuses to clobber them; with clobber on p4
|
|
847
|
-
# overwrites them during the sync instead.
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
928
|
+
# overwrites them during the sync instead. With allwrite p4 refuses
|
|
929
|
+
# only the locally modified ones, so report what it actually skipped.
|
|
930
|
+
if clobber:
|
|
931
|
+
reported = all_ignored
|
|
932
|
+
heading = ('Git-ignored writable files overwritten by p4 '
|
|
933
|
+
'(clobber enabled)')
|
|
934
|
+
elif allwrite:
|
|
935
|
+
reported = all_not_synced
|
|
936
|
+
heading = 'Files not synced (git-ignored and locally modified)'
|
|
937
|
+
else:
|
|
938
|
+
reported = all_ignored
|
|
939
|
+
heading = 'Files not synced (git-ignored and writable)'
|
|
940
|
+
if reported:
|
|
941
|
+
log.heading(heading)
|
|
942
|
+
for f in sorted(set(reported)):
|
|
856
943
|
log.info(os.path.relpath(f, workspace_dir))
|
|
857
944
|
|
|
858
945
|
run_hooks('post-sync', workspace_dir, invocation_dir)
|
|
@@ -145,7 +145,7 @@ def sync_split_command(args: argparse.Namespace) -> int:
|
|
|
145
145
|
label = 'changelist' if len(matched) == 1 else 'changelists'
|
|
146
146
|
log.success(f'{len(matched)} {label} to split out')
|
|
147
147
|
else:
|
|
148
|
-
log.
|
|
148
|
+
log.success('None found, syncing to the target changelist only')
|
|
149
149
|
|
|
150
150
|
targets = build_sync_targets(changes, users, last_synced, upper)
|
|
151
151
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-p4son
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
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
|
|
@@ -231,6 +231,14 @@ be managed manually or via Perforce directly.
|
|
|
231
231
|
After syncing, git-p4son prints a summary of how each writable file was handled, and instructs you to review and
|
|
232
232
|
commit any files that need attention.
|
|
233
233
|
|
|
234
|
+
**Workspaces with `allwrite`** - When your client spec has the `allwrite` option, every file is writable by design,
|
|
235
|
+
so the read-only flag carries no information about local changes. git-p4son classifies files exactly as above (the
|
|
236
|
+
verdict comes from git, not from the flag), but leaves the write bit alone on files it finds unchanged: with
|
|
237
|
+
`noclobber` Perforce compares digests before refusing to overwrite a writable file, and unchanged files pass that
|
|
238
|
+
check. Only files with local changes are made read-only for the duration of the sync, since those are the ones
|
|
239
|
+
Perforce would otherwise refuse to overwrite. The same digest check applies to git-ignored files: unchanged ones sync
|
|
240
|
+
normally, and after the sync git-p4son lists the ones Perforce skipped because they were modified locally.
|
|
241
|
+
|
|
234
242
|
## Usage
|
|
235
243
|
|
|
236
244
|
git-p4son provides nine commands: `init`, `sync`, `sync-split`, `new`, `update`, `review`, `list-changes`,
|
|
@@ -5,7 +5,8 @@ from unittest import mock
|
|
|
5
5
|
|
|
6
6
|
from git_p4son.perforce import (
|
|
7
7
|
P4ClientSpec, P4FileInfo, P4SyncPreviewFile, get_client_spec,
|
|
8
|
-
get_p4_user, get_submitted_changes,
|
|
8
|
+
get_p4_user, get_submitted_changes, is_always_writable_file_type,
|
|
9
|
+
is_binary_file_type,
|
|
9
10
|
p4_fstat_file_info, p4_sync_preview, parse_ztag_multi_output,
|
|
10
11
|
parse_ztag_output,
|
|
11
12
|
)
|
|
@@ -102,6 +103,20 @@ class TestP4ClientSpec(unittest.TestCase):
|
|
|
102
103
|
line_end='local')
|
|
103
104
|
self.assertTrue(spec.clobber)
|
|
104
105
|
|
|
106
|
+
def test_allwrite_enabled(self):
|
|
107
|
+
spec = P4ClientSpec(
|
|
108
|
+
name='ws', root='/ws',
|
|
109
|
+
options=['allwrite', 'noclobber', 'nocompress'], stream=None,
|
|
110
|
+
line_end='local')
|
|
111
|
+
self.assertTrue(spec.allwrite)
|
|
112
|
+
|
|
113
|
+
def test_allwrite_disabled(self):
|
|
114
|
+
spec = P4ClientSpec(
|
|
115
|
+
name='ws', root='/ws',
|
|
116
|
+
options=['noallwrite', 'noclobber', 'nocompress'], stream=None,
|
|
117
|
+
line_end='local')
|
|
118
|
+
self.assertFalse(spec.allwrite)
|
|
119
|
+
|
|
105
120
|
def test_clobber_disabled(self):
|
|
106
121
|
spec = P4ClientSpec(
|
|
107
122
|
name='ws', root='/ws',
|
|
@@ -195,6 +210,20 @@ class TestIsBinaryFileType(unittest.TestCase):
|
|
|
195
210
|
self.assertTrue(is_binary_file_type('ubinary+x'))
|
|
196
211
|
|
|
197
212
|
|
|
213
|
+
class TestIsAlwaysWritableFileType(unittest.TestCase):
|
|
214
|
+
def test_without_w_modifier(self):
|
|
215
|
+
self.assertFalse(is_always_writable_file_type('text'))
|
|
216
|
+
self.assertFalse(is_always_writable_file_type('binary'))
|
|
217
|
+
self.assertFalse(is_always_writable_file_type('binary+x'))
|
|
218
|
+
self.assertFalse(is_always_writable_file_type('text+kx'))
|
|
219
|
+
|
|
220
|
+
def test_with_w_modifier(self):
|
|
221
|
+
self.assertTrue(is_always_writable_file_type('text+w'))
|
|
222
|
+
self.assertTrue(is_always_writable_file_type('binary+w'))
|
|
223
|
+
self.assertTrue(is_always_writable_file_type('binary+Swl'))
|
|
224
|
+
self.assertTrue(is_always_writable_file_type('binary+xw'))
|
|
225
|
+
|
|
226
|
+
|
|
198
227
|
class TestP4FstatFileInfo(unittest.TestCase):
|
|
199
228
|
@mock.patch('git_p4son.perforce.run')
|
|
200
229
|
def test_returns_head_type(self, mock_run):
|
|
@@ -433,8 +433,15 @@ class TestP4Sync(unittest.TestCase):
|
|
|
433
433
|
'p4 sync failed', returncode=1,
|
|
434
434
|
stderr=["Can't clobber writable file /ws/a.txt",
|
|
435
435
|
"Can't clobber writable file /ws/b.txt"])
|
|
436
|
-
p4_sync(12345, 'test', '//myclient', '/ws',
|
|
437
|
-
|
|
436
|
+
refused = p4_sync(12345, 'test', '//myclient', '/ws',
|
|
437
|
+
expected_clobber={'/ws/a.txt', '/ws/b.txt'})
|
|
438
|
+
self.assertEqual(sorted(refused), ['/ws/a.txt', '/ws/b.txt'])
|
|
439
|
+
|
|
440
|
+
@mock.patch('git_p4son.sync.run_with_output')
|
|
441
|
+
def test_clean_sync_refuses_nothing(self, mock_rwo):
|
|
442
|
+
mock_rwo.return_value = mock.Mock(elapsed=None)
|
|
443
|
+
self.assertEqual(
|
|
444
|
+
p4_sync(12345, 'test', '//myclient', '/ws'), [])
|
|
438
445
|
|
|
439
446
|
@mock.patch('git_p4son.sync.run_with_output')
|
|
440
447
|
def test_unexpected_clobber_raises(self, mock_rwo):
|
|
@@ -540,6 +547,57 @@ class TestPrepareWritableFiles(unittest.TestCase):
|
|
|
540
547
|
mode = os.stat(path).st_mode
|
|
541
548
|
self.assertFalse(mode & stat.S_IWUSR)
|
|
542
549
|
|
|
550
|
+
@mock.patch('git_p4son.sync.get_blob_oids',
|
|
551
|
+
return_value={('head123', 'a.txt'): 'same_oid',
|
|
552
|
+
('sync456', 'a.txt'): 'same_oid'})
|
|
553
|
+
@mock.patch('git_p4son.sync.get_file_at_commit')
|
|
554
|
+
@mock.patch('git_p4son.sync.find_base_commits',
|
|
555
|
+
return_value={'a.txt': 'sync456'})
|
|
556
|
+
@mock.patch('git_p4son.sync.p4_fstat_file_info')
|
|
557
|
+
@mock.patch('git_p4son.sync.get_tracked_files',
|
|
558
|
+
side_effect=lambda paths, ws: set(paths))
|
|
559
|
+
def test_allwrite_keeps_unchanged_file_writable(
|
|
560
|
+
self, _tracked, mock_fstat, _find_base, _get_file, _oids):
|
|
561
|
+
"""An allwrite workspace is writable by design, and p4 overwrites
|
|
562
|
+
an unchanged file regardless of clobber, so the write bit stays."""
|
|
563
|
+
from git_p4son.perforce import P4FileInfo
|
|
564
|
+
with tempfile.TemporaryDirectory() as ws:
|
|
565
|
+
path = self._make_file(ws, 'a.txt')
|
|
566
|
+
mock_fstat.return_value = {path: P4FileInfo(head_type='text')}
|
|
567
|
+
|
|
568
|
+
result = prepare_writable_files([_upd(path)], ws, 'head123',
|
|
569
|
+
self.temp_root, allwrite=True)
|
|
570
|
+
|
|
571
|
+
self.assertEqual(result.changed, [])
|
|
572
|
+
mode = os.stat(path).st_mode
|
|
573
|
+
self.assertTrue(mode & stat.S_IWUSR)
|
|
574
|
+
|
|
575
|
+
@mock.patch('git_p4son.sync.get_blob_oids')
|
|
576
|
+
@mock.patch('git_p4son.sync.get_file_at_commit',
|
|
577
|
+
return_value=b'head content')
|
|
578
|
+
@mock.patch('git_p4son.sync.find_base_commits',
|
|
579
|
+
return_value={'a.txt': 'sync456'})
|
|
580
|
+
@mock.patch('git_p4son.sync.p4_fstat_file_info')
|
|
581
|
+
@mock.patch('git_p4son.sync.get_tracked_files',
|
|
582
|
+
side_effect=lambda paths, ws: set(paths))
|
|
583
|
+
def test_allwrite_makes_changed_file_read_only(
|
|
584
|
+
self, _tracked, mock_fstat, _find_base, _get_file, mock_oids):
|
|
585
|
+
"""With allwrite plus noclobber p4 digest-compares and would refuse
|
|
586
|
+
on exactly the modified files, so those still go read-only."""
|
|
587
|
+
from git_p4son.perforce import P4FileInfo
|
|
588
|
+
with tempfile.TemporaryDirectory() as ws:
|
|
589
|
+
path = self._make_file(ws, 'a.txt')
|
|
590
|
+
mock_fstat.return_value = {path: P4FileInfo(head_type='text')}
|
|
591
|
+
mock_oids.return_value = {('head123', 'a.txt'): 'oid_head',
|
|
592
|
+
('sync456', 'a.txt'): 'oid_base'}
|
|
593
|
+
|
|
594
|
+
result = prepare_writable_files([_upd(path)], ws, 'head123',
|
|
595
|
+
self.temp_root, allwrite=True)
|
|
596
|
+
|
|
597
|
+
self.assertEqual([cf.filepath for cf in result.changed], [path])
|
|
598
|
+
mode = os.stat(path).st_mode
|
|
599
|
+
self.assertFalse(mode & stat.S_IWUSR)
|
|
600
|
+
|
|
543
601
|
@mock.patch('git_p4son.sync.get_file_at_commit',
|
|
544
602
|
return_value=b'head content')
|
|
545
603
|
@mock.patch('git_p4son.sync.find_base_commits',
|
|
@@ -726,33 +784,86 @@ class TestPrepareWritableFiles(unittest.TestCase):
|
|
|
726
784
|
path = self._make_file(ws, 'build.log')
|
|
727
785
|
|
|
728
786
|
with mock.patch('git_p4son.sync.get_tracked_files',
|
|
729
|
-
return_value=set())
|
|
787
|
+
return_value=set()), \
|
|
788
|
+
mock.patch('git_p4son.sync.p4_fstat_file_info',
|
|
789
|
+
return_value={}):
|
|
730
790
|
result = prepare_writable_files([_upd(path)], ws, 'head123',
|
|
731
791
|
self.temp_root)
|
|
732
792
|
|
|
733
793
|
self.assertEqual(result.changed, [])
|
|
734
794
|
self.assertEqual(result.ignored, [path])
|
|
795
|
+
self.assertEqual(result.always_writable, [])
|
|
735
796
|
mode = os.stat(path).st_mode
|
|
736
797
|
self.assertTrue(mode & stat.S_IWUSR)
|
|
737
798
|
|
|
738
|
-
def
|
|
799
|
+
def test_always_writable_ignored_file_not_reported_as_unsynced(self):
|
|
800
|
+
"""A git-ignored file whose Perforce type is +w is meant to be
|
|
801
|
+
writable; p4 overwrites it on sync, so no warning is due."""
|
|
802
|
+
from git_p4son.perforce import P4FileInfo
|
|
803
|
+
with tempfile.TemporaryDirectory() as ws:
|
|
804
|
+
path = self._make_file(ws, 'Tool.exe')
|
|
805
|
+
|
|
806
|
+
with mock.patch('git_p4son.sync.get_tracked_files',
|
|
807
|
+
return_value=set()), \
|
|
808
|
+
mock.patch(
|
|
809
|
+
'git_p4son.sync.p4_fstat_file_info',
|
|
810
|
+
return_value={
|
|
811
|
+
path: P4FileInfo(head_type='binary+w')}), \
|
|
812
|
+
mock.patch('git_p4son.sync.log') as mock_log:
|
|
813
|
+
result = prepare_writable_files([_upd(path)], ws, 'head123',
|
|
814
|
+
self.temp_root)
|
|
815
|
+
|
|
816
|
+
self.assertEqual(result.ignored, [])
|
|
817
|
+
self.assertEqual(result.always_writable, [path])
|
|
818
|
+
warnings = ' '.join(
|
|
819
|
+
str(c.args[0]) for c in mock_log.warning.call_args_list)
|
|
820
|
+
self.assertNotIn('will not be synced', warnings)
|
|
821
|
+
mode = os.stat(path).st_mode
|
|
822
|
+
self.assertTrue(mode & stat.S_IWUSR)
|
|
823
|
+
|
|
824
|
+
def test_always_writable_files_excluded_from_expected_clobber(self):
|
|
825
|
+
"""+w files must not be classified as ignored: a clobber error on
|
|
826
|
+
one is unexpected and should still surface."""
|
|
827
|
+
from git_p4son.perforce import P4FileInfo
|
|
828
|
+
with tempfile.TemporaryDirectory() as ws:
|
|
829
|
+
always = self._make_file(ws, 'Tool.exe')
|
|
830
|
+
ignored = self._make_file(ws, 'build.log')
|
|
831
|
+
|
|
832
|
+
with mock.patch('git_p4son.sync.get_tracked_files',
|
|
833
|
+
return_value=set()), \
|
|
834
|
+
mock.patch(
|
|
835
|
+
'git_p4son.sync.p4_fstat_file_info',
|
|
836
|
+
return_value={
|
|
837
|
+
always: P4FileInfo(head_type='binary+w'),
|
|
838
|
+
ignored: P4FileInfo(head_type='text')}):
|
|
839
|
+
result = prepare_writable_files(
|
|
840
|
+
[_upd(always), _upd(ignored)], ws, 'head123',
|
|
841
|
+
self.temp_root)
|
|
842
|
+
|
|
843
|
+
self.assertEqual(result.ignored, [ignored])
|
|
844
|
+
self.assertEqual(result.always_writable, [always])
|
|
845
|
+
|
|
846
|
+
def _run_with_tracked_and_ignored(self, ws, clobber, allwrite=False,
|
|
847
|
+
level='warning'):
|
|
739
848
|
"""Prepare one unchanged tracked file plus one ignored file so the
|
|
740
849
|
summary block (which reports ignored files) is reached, and return
|
|
741
|
-
the concatenated
|
|
850
|
+
the concatenated messages logged at level."""
|
|
742
851
|
tracked = self._make_file(ws, 'a.txt')
|
|
743
852
|
ignored = self._make_file(ws, 'build.log')
|
|
744
853
|
with mock.patch('git_p4son.sync.get_tracked_files',
|
|
745
854
|
return_value={tracked}), \
|
|
855
|
+
mock.patch('git_p4son.sync.p4_fstat_file_info',
|
|
856
|
+
return_value={}), \
|
|
746
857
|
mock.patch('git_p4son.sync.find_base_commits',
|
|
747
858
|
return_value={'a.txt': 'head123'}), \
|
|
748
859
|
mock.patch('git_p4son.sync.get_blob_oids', return_value={}), \
|
|
749
860
|
mock.patch('git_p4son.sync.log') as mock_log:
|
|
750
861
|
result = prepare_writable_files(
|
|
751
862
|
[_upd(tracked), _upd(ignored)], ws, 'head123',
|
|
752
|
-
self.temp_root, clobber=clobber)
|
|
863
|
+
self.temp_root, clobber=clobber, allwrite=allwrite)
|
|
753
864
|
self.assertEqual(result.ignored, [ignored])
|
|
754
|
-
return ' '.join(
|
|
755
|
-
|
|
865
|
+
return ' '.join(str(c.args[0]) for c in
|
|
866
|
+
getattr(mock_log, level).call_args_list)
|
|
756
867
|
|
|
757
868
|
def test_ignored_message_without_clobber(self):
|
|
758
869
|
with tempfile.TemporaryDirectory() as ws:
|
|
@@ -768,6 +879,26 @@ class TestPrepareWritableFiles(unittest.TestCase):
|
|
|
768
879
|
self.assertIn('overwritten', warnings)
|
|
769
880
|
self.assertNotIn('will not be synced', warnings)
|
|
770
881
|
|
|
882
|
+
def test_ignored_message_with_allwrite(self):
|
|
883
|
+
"""With allwrite p4 digest-compares before refusing to clobber, so
|
|
884
|
+
unchanged ignored files do sync: the summary must not claim none
|
|
885
|
+
will, and cannot yet say which will not."""
|
|
886
|
+
with tempfile.TemporaryDirectory() as ws:
|
|
887
|
+
warnings = self._run_with_tracked_and_ignored(
|
|
888
|
+
ws, clobber=False, allwrite=True)
|
|
889
|
+
self.assertNotIn('will not be synced', warnings)
|
|
890
|
+
infos = self._run_with_tracked_and_ignored(
|
|
891
|
+
ws, clobber=False, allwrite=True, level='info')
|
|
892
|
+
self.assertIn('unless modified locally', infos)
|
|
893
|
+
self.assertNotIn('build.log', infos)
|
|
894
|
+
|
|
895
|
+
def test_ignored_message_with_allwrite_and_clobber(self):
|
|
896
|
+
"""clobber overwrites regardless, so it wins over allwrite."""
|
|
897
|
+
with tempfile.TemporaryDirectory() as ws:
|
|
898
|
+
warnings = self._run_with_tracked_and_ignored(
|
|
899
|
+
ws, clobber=True, allwrite=True)
|
|
900
|
+
self.assertIn('overwritten', warnings)
|
|
901
|
+
|
|
771
902
|
def test_nonexistent_files_skipped(self):
|
|
772
903
|
result = prepare_writable_files(
|
|
773
904
|
[_upd('/ws/noexist.txt')], '/ws', 'head123', self.temp_root)
|
|
@@ -816,6 +947,43 @@ class TestSyncCommand(unittest.TestCase):
|
|
|
816
947
|
rc = sync_command(args)
|
|
817
948
|
self.assertEqual(rc, 0)
|
|
818
949
|
|
|
950
|
+
@mock.patch('git_p4son.sync.log')
|
|
951
|
+
@mock.patch('git_p4son.sync._merge_changed_files')
|
|
952
|
+
@mock.patch('git_p4son.sync.commit')
|
|
953
|
+
@mock.patch('git_p4son.sync.add_all_files')
|
|
954
|
+
@mock.patch('git_p4son.sync.get_dirty_files', return_value=[])
|
|
955
|
+
@mock.patch('git_p4son.sync.p4_sync', return_value=['/ws/edited.log'])
|
|
956
|
+
@mock.patch('git_p4son.sync.prepare_writable_files')
|
|
957
|
+
@mock.patch('git_p4son.sync.p4_sync_preview',
|
|
958
|
+
return_value=[_upd('/ws/edited.log')])
|
|
959
|
+
@mock.patch('git_p4son.sync.get_head_commit', return_value='def456')
|
|
960
|
+
@mock.patch('git_p4son.sync.git_last_sync')
|
|
961
|
+
@mock.patch('git_p4son.sync.p4_get_opened_files', return_value=[])
|
|
962
|
+
@mock.patch('git_p4son.sync.get_client_spec')
|
|
963
|
+
@mock.patch('git_p4son.sync.get_depot_root', return_value='//myclient')
|
|
964
|
+
def test_allwrite_reports_only_refused_ignored_files(
|
|
965
|
+
self, _depot, mock_spec, _p4clean, mock_last_sync, _head,
|
|
966
|
+
_preview, mock_prep, _p4sync, _git_clean, _add, _commit, _merge,
|
|
967
|
+
mock_log):
|
|
968
|
+
"""With allwrite p4 syncs unchanged ignored files and refuses only
|
|
969
|
+
the modified ones, so the report lists what it actually skipped."""
|
|
970
|
+
spec = mock.Mock(uses_crlf=False, clobber=False, allwrite=True)
|
|
971
|
+
spec.name = 'myclient'
|
|
972
|
+
mock_spec.return_value = spec
|
|
973
|
+
mock_last_sync.return_value = self._last_sync
|
|
974
|
+
mock_prep.return_value = WritableSyncFileSet(
|
|
975
|
+
ignored=['/ws/pristine.log', '/ws/edited.log'])
|
|
976
|
+
args = mock.Mock(changelist=['12345'], force=False,
|
|
977
|
+
workspace_dir='/ws')
|
|
978
|
+
self.assertEqual(sync_command(args), 0)
|
|
979
|
+
|
|
980
|
+
headings = [str(c.args[0]) for c in mock_log.heading.call_args_list]
|
|
981
|
+
self.assertIn('Files not synced (git-ignored and locally modified)',
|
|
982
|
+
headings)
|
|
983
|
+
infos = [str(c.args[0]) for c in mock_log.info.call_args_list]
|
|
984
|
+
self.assertIn('edited.log', infos)
|
|
985
|
+
self.assertNotIn('pristine.log', infos)
|
|
986
|
+
|
|
819
987
|
@mock.patch('git_p4son.sync.get_depot_root', return_value=None)
|
|
820
988
|
def test_no_depot_root_aborts(self, _depot):
|
|
821
989
|
args = mock.Mock(changelist=['100'], force=False, workspace_dir='/ws')
|
|
@@ -843,6 +1011,7 @@ class TestSyncCommand(unittest.TestCase):
|
|
|
843
1011
|
spec.name = 'real-client'
|
|
844
1012
|
spec.uses_crlf = False
|
|
845
1013
|
spec.clobber = False
|
|
1014
|
+
spec.allwrite = False
|
|
846
1015
|
mock_spec.return_value = spec
|
|
847
1016
|
mock_last_sync.return_value = self._last_sync
|
|
848
1017
|
mock_prep.return_value = self._empty_prep()
|
|
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
|