Skip to main content

perforce_cli/cmd/
sync.rs

1use std::{
2    ffi::OsStr,
3    path::{Path, PathBuf},
4    process::{Child, Command, Stdio},
5};
6
7use super::{ExclusiveOption, SubCommand, Unselected};
8
9use crate::global::GlobalOpts;
10use crate::spawn::ParameterizedSpawn;
11
12/// Configuration for the `--parallel` option of `p4 sync`.
13///
14/// Controls how files are transferred in parallel. `threads` is required;
15/// all other sub-options are optional and fall back to server defaults when
16/// omitted.
17#[derive(Debug, Clone, Default)]
18pub struct ParallelConfig {
19    /// Number of concurrent network connections (`threads=N`).
20    pub threads: u64,
21
22    /// Number of files in a batch (`batch=N`).
23    pub batch_files: Option<u64>,
24
25    /// Number of bytes in a batch (`batchsize=N`).
26    pub batch_size_bytes: Option<u64>,
27
28    /// Minimum number of files for a parallel sync (`min=N`).
29    pub min_files: Option<u64>,
30
31    /// Minimum number of bytes for a parallel sync (`minsize=N`).
32    pub min_size_bytes: Option<u64>,
33}
34
35impl ParallelConfig {
36    /// Builds the value passed to `--parallel=...`, e.g.
37    /// `threads=4,batch=8,batchsize=512K,min=9,minsize=576K`.
38    pub fn as_arg(&self) -> String {
39        let mut parts = vec![format!("threads={}", self.threads)];
40
41        if let Some(v) = self.batch_files {
42            parts.push(format!("batch={}", v));
43        }
44        if let Some(v) = self.batch_size_bytes {
45            parts.push(format!("batchsize={}", v));
46        }
47        if let Some(v) = self.min_files {
48            parts.push(format!("min={}", v));
49        }
50        if let Some(v) = self.min_size_bytes {
51            parts.push(format!("minsize={}", v));
52        }
53
54        parts.join(",")
55    }
56}
57
58/// The `--use-stream-change` value controlling which stream specification
59/// version is used to generate the client view.
60#[cfg(not(feature = "lt2022_2"))]
61#[derive(Debug, Clone, Copy)]
62pub enum StreamSpecVersion {
63    /// `--use-stream-change` with no value: the maximum change number in the
64    /// file list determines the stream spec version.
65    MaxInFilelists,
66    /// `--use-stream-change=0`: use the current stream spec version.
67    Current,
68    /// `--use-stream-change=N`: use the stream spec version at or before
69    /// change `N`.
70    ChangeNumber(u32),
71}
72
73#[cfg(not(feature = "lt2022_2"))]
74impl StreamSpecVersion {
75    /// `--use-stream-change` with no value: the maximum change number in the
76    /// file list determines the stream spec version.
77    pub fn max_in_filelists() -> Self {
78        StreamSpecVersion::MaxInFilelists
79    }
80
81    /// `--use-stream-change=0`: use the current stream spec version.
82    pub fn current() -> Self {
83        StreamSpecVersion::Current
84    }
85
86    /// `--use-stream-change=N`: use the stream spec version at or before
87    /// change `n`.
88    pub fn at_change(n: u32) -> Self {
89        StreamSpecVersion::ChangeNumber(n)
90    }
91
92    /// Injects the `--use-stream-change` argument(s) into `command`.
93    pub fn inject_arg(&self, command: &mut Command) {
94        match self {
95            StreamSpecVersion::MaxInFilelists => {
96                command.arg("--use-stream-change");
97            }
98            StreamSpecVersion::Current => {
99                command.arg("--use-stream-change=0");
100            }
101            StreamSpecVersion::ChangeNumber(n) => {
102                command.arg(format!("--use-stream-change={}", n));
103            }
104        }
105    }
106}
107
108/// Preview mode of `p4 sync` (`-n`): display the results of the sync without
109/// actually performing the sync.
110///
111/// Entered with [`Sync::preview_result`]. Mutually exclusive with
112/// [`PreviewNetworkTraffic`].
113#[derive(Debug, Clone, Copy, Default)]
114pub struct PreviewResult;
115
116impl ExclusiveOption for PreviewResult {
117    fn inject_args(&self, command: &mut Command) {
118        command.arg("-n");
119    }
120}
121
122/// Preview mode of `p4 sync` (`-N`): display a summary of the expected
123/// network traffic associated with a sync, without performing the sync.
124///
125/// Entered with [`Sync::preview_network_traffic`]. Mutually exclusive with
126/// [`PreviewResult`].
127#[derive(Debug, Clone, Copy, Default)]
128pub struct PreviewNetworkTraffic;
129
130impl ExclusiveOption for PreviewNetworkTraffic {
131    fn inject_args(&self, command: &mut Command) {
132        command.arg("-N");
133    }
134}
135
136/// Force execution sub-mode of [`RegularMode`]: entered when any of `-f`,
137/// `-k`, or `-r` is set.
138///
139/// In this state the command is locked into the regular sync form and can no
140/// longer transition to [`SafeCheckMode`] or [`PopulateMode`].
141#[derive(Debug, Clone, Default)]
142pub struct ForceRegularMode {
143    force: bool,
144
145    metadata_only: bool,
146
147    #[cfg(not(feature = "lt2015_1"))]
148    reopen_moved_files: bool,
149}
150
151impl ExclusiveOption for ForceRegularMode {
152    fn inject_args(&self, command: &mut Command) {
153        if self.force {
154            command.arg("-f");
155        }
156
157        if self.metadata_only {
158            command.arg("-k");
159        }
160
161        #[cfg(not(feature = "lt2015_1"))]
162        if self.reopen_moved_files {
163            command.arg("-r");
164        }
165    }
166}
167
168/// Safe sync sub-mode of [`RegularMode`] (`-s`): compare the content in the
169/// client workspace against what was last synced and do not overwrite files
170/// that were modified outside of P4 Server's control.
171///
172/// Entered with [`Sync::safe_check`].
173#[derive(Debug, Clone, Copy, Default)]
174pub struct SafeCheckMode;
175
176impl ExclusiveOption for SafeCheckMode {
177    fn inject_args(&self, command: &mut Command) {
178        command.arg("-s");
179    }
180}
181
182/// Populate sub-mode of [`RegularMode`] (`-p`): populate a client workspace
183/// but do not update the have list.
184///
185/// Entered with [`Sync::populate`].
186#[derive(Debug, Clone, Copy, Default)]
187pub struct PopulateMode;
188
189impl ExclusiveOption for PopulateMode {
190    fn inject_args(&self, command: &mut Command) {
191        command.arg("-p");
192    }
193}
194
195/// Regular mode of `p4 sync`: the command forms that operate on the client
196/// workspace.
197///
198/// All common options are stored directly on this struct. The `Mode` type
199/// parameter selects the mutually exclusive execution sub-mode:
200///
201/// - [`Unselected`] (default): the first command form, plain regular sync.
202/// - [`ForceRegularMode`]: entered by setting `-f`, `-k`, or `-r`; locks out
203///   further transitions to [`SafeCheckMode`] or [`PopulateMode`].
204/// - [`SafeCheckMode`]: entered by [`Sync::safe_check`] (`-s`).
205/// - [`PopulateMode`]: entered by [`Sync::populate`] (`-p`).
206///
207/// The `P` type parameter tracks the preview mode ([`Unselected`] by default,
208/// [`PreviewResult`] or [`PreviewNetworkTraffic`] otherwise).
209#[derive(Debug, Clone, Default)]
210pub struct RegularMode<Mode = Unselected, P = Unselected> {
211    #[cfg(not(feature = "lt2022_2"))]
212    verify_edge_replication: bool,
213
214    script_list_mode: bool,
215
216    #[cfg(not(feature = "lt2022_1"))]
217    suppress_keyword_expansion: bool,
218
219    quiet_mode: bool,
220
221    limit: Option<u64>,
222
223    parallel: Option<ParallelConfig>,
224
225    #[cfg(not(feature = "lt2022_2"))]
226    stream_spec_version: Option<StreamSpecVersion>,
227
228    mode: Mode,
229
230    preview: P,
231}
232
233impl<Mode: ExclusiveOption, P: ExclusiveOption> ExclusiveOption for RegularMode<Mode, P> {
234    fn inject_args(&self, command: &mut Command) {
235        #[cfg(not(feature = "lt2022_2"))]
236        if self.verify_edge_replication {
237            command.arg("-E");
238        }
239
240        if self.script_list_mode {
241            command.arg("-L");
242        }
243
244        #[cfg(not(feature = "lt2022_1"))]
245        if self.suppress_keyword_expansion {
246            command.arg("-K");
247        }
248
249        if self.quiet_mode {
250            command.arg("-q");
251        }
252
253        self.mode.inject_args(command);
254
255        self.preview.inject_args(command);
256
257        if let Some(max) = self.limit {
258            command.arg("-m").arg(max.to_string());
259        }
260
261        if let Some(parallel) = &self.parallel {
262            command.arg(format!("--parallel={}", parallel.as_arg()));
263        }
264
265        #[cfg(not(feature = "lt2022_2"))]
266        if let Some(version) = &self.stream_spec_version {
267            version.inject_arg(command);
268        }
269    }
270}
271
272/// Sync-time mode of `p4 sync` (`-k --sync-time=N`): update the have list to
273/// reflect the state of the depot at the given time without transferring
274/// files.
275///
276/// Entered with [`Sync::sync_time`]. This mode always implies `-k` (metadata
277/// only), so no separate interface is provided for it.
278#[cfg(not(feature = "lt2025_1"))]
279#[derive(Debug, Clone)]
280pub struct SyncTimeMode {
281    sync_time: String,
282}
283
284#[cfg(not(feature = "lt2025_1"))]
285impl ExclusiveOption for SyncTimeMode {
286    fn inject_args(&self, command: &mut Command) {
287        command
288            .arg("-k")
289            .arg(format!("--sync-time={}", self.sync_time));
290    }
291}
292
293///
294/// Update the client workspace to reflect the contents of the depot.
295///
296/// The `M` type parameter tracks the top-level mode at compile time. The
297/// default [`Unselected`] state syncs files without local options; setting
298/// any regular option or calling a mode-transition method moves into
299/// [`RegularMode`]
300#[cfg_attr(feature = "lt2025_1", doc = ".")]
301#[cfg_attr(
302    not(feature = "lt2025_1"),
303    doc = "; [`Self::sync_time`] moves into [`SyncTimeMode`]."
304)]
305#[derive(Debug, Clone, Default)]
306pub struct Sync<M = Unselected> {
307    bin: PathBuf,
308
309    global_opts: GlobalOpts,
310
311    mode: M,
312}
313
314impl Sync<Unselected> {
315    /// Creates a new `p4 sync` command.
316    ///
317    /// `bin` is the path to the Perforce command-line executable.
318    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
319        Self {
320            bin: bin.into(),
321            global_opts,
322            mode: Unselected,
323        }
324    }
325
326    /// # Description
327    ///
328    /// `-k --sync-time=N`
329    ///
330    /// Update the have list to reflect the state of the depot at the given
331    /// time without transferring files.
332    #[cfg_attr(
333        all(feature = "lt2025_2", not(feature = "lt2025_1")),
334        doc = "The value of `N` can be Unix epoch time or the perforce date",
335        doc = "time format."
336    )]
337    #[cfg_attr(
338        not(feature = "lt2025_2"),
339        doc = "The value of `N` can be Unix epoch time or the Perforce date",
340        doc = "time format."
341    )]
342    ///
343    /// This mode always implies `-k`, so no separate interface is provided
344    /// for it. Transitions this command to the [`SyncTimeMode`] state.
345    #[cfg(not(feature = "lt2025_1"))]
346    pub fn sync_time(self, time: impl Into<String>) -> Sync<SyncTimeMode> {
347        Sync {
348            bin: self.bin,
349            global_opts: self.global_opts,
350            mode: SyncTimeMode {
351                sync_time: time.into(),
352            },
353        }
354    }
355
356    /// # Description
357    ///
358    /// `-s`
359    ///
360    /// Safe sync: compare the content in the client workspace against what
361    /// was last synced.
362    #[cfg_attr(
363        feature = "lt2017_2",
364        doc = "If the file was modified outside of Perforce control, an error",
365        doc = "message is displayed and the file is not overwritten."
366    )]
367    #[cfg_attr(
368        all(feature = "lt2024_1", not(feature = "lt2017_2")),
369        doc = "If the file was modified outside of the control of Helix",
370        doc = "Server, an error message is displayed and the file is not",
371        doc = "overwritten."
372    )]
373    #[cfg_attr(
374        all(feature = "lt2024_2", not(feature = "lt2024_1")),
375        doc = "If the file was modified outside of the control of Helix Core",
376        doc = "Server, an error message is displayed and the file is not",
377        doc = "overwritten."
378    )]
379    #[cfg_attr(
380        not(feature = "lt2024_2"),
381        doc = "If the file was modified outside of the control of P4 Server,",
382        doc = "an error message is displayed and the file is not overwritten."
383    )]
384    ///
385    /// Transitions this command to the [`RegularMode`] state with the
386    /// [`SafeCheckMode`] sub-mode.
387    pub fn enable_safe_check(self) -> Sync<RegularMode<SafeCheckMode>> {
388        Sync {
389            bin: self.bin,
390            global_opts: self.global_opts,
391            mode: RegularMode {
392                mode: SafeCheckMode,
393                ..RegularMode::default()
394            },
395        }
396    }
397
398    /// # Description
399    ///
400    /// `-p`
401    ///
402    /// Populate a client workspace, but do not update the have list. Any
403    /// file that is already synced or opened is bypassed with a warning
404    /// message.
405    ///
406    /// Transitions this command to the [`RegularMode`] state with the
407    /// [`PopulateMode`] sub-mode.
408    pub fn populate_client_workspace(self) -> Sync<RegularMode<PopulateMode>> {
409        Sync {
410            bin: self.bin,
411            global_opts: self.global_opts,
412            mode: RegularMode {
413                mode: PopulateMode,
414                ..RegularMode::default()
415            },
416        }
417    }
418
419    /// # Description
420    ///
421    /// `-E`
422    ///
423    /// For edge servers replicating from a commit or an upstream edge, verify
424    /// that any changelists specified in the revSpec are submitted before
425    /// continuing with the sync.
426    ///
427    /// Transitions this command to the [`RegularMode`] state.
428    #[cfg(not(feature = "lt2022_2"))]
429    pub fn verify_edge_replication(self, v: bool) -> Sync<RegularMode> {
430        Sync {
431            bin: self.bin,
432            global_opts: self.global_opts,
433            mode: RegularMode {
434                verify_edge_replication: v,
435                ..RegularMode::default()
436            },
437        }
438    }
439
440    /// # Description
441    ///
442    /// `-L`
443    ///
444    /// For scripting purposes, perform the sync on a list of valid file
445    /// arguments in full depot syntax with a valid revision number.
446    #[cfg_attr(
447        not(feature = "lt2016_1"),
448        doc = "",
449        doc = "When this flag is used, the arguments are processed together by",
450        doc = "building an internal table similar to a label. This file list",
451        doc = "processing is significantly faster than having to call the",
452        doc = "internal query engine for each individual file argument. However,",
453        doc = "the file argument syntax is strict and the command will not run",
454        doc = "if an error is encountered."
455    )]
456    ///
457    /// Transitions this command to the [`RegularMode`] state.
458    pub fn script_list_mode(self, v: bool) -> Sync<RegularMode> {
459        Sync {
460            bin: self.bin,
461            global_opts: self.global_opts,
462            mode: RegularMode {
463                script_list_mode: v,
464                ..RegularMode::default()
465            },
466        }
467    }
468
469    /// # Description
470    ///
471    /// `-K`
472    ///
473    /// Suppress keyword expansion when updating `+k` type files on the
474    /// client.
475    #[cfg_attr(feature = "lt2024_2", doc = "See File type modifiers.")]
476    #[cfg_attr(
477        not(feature = "lt2024_2"),
478        doc = "To learn more, see File type modifiers."
479    )]
480    ///
481    /// Transitions this command to the [`RegularMode`] state.
482    #[cfg(not(feature = "lt2022_1"))]
483    pub fn suppress_keyword_expansion(self, v: bool) -> Sync<RegularMode> {
484        Sync {
485            bin: self.bin,
486            global_opts: self.global_opts,
487            mode: RegularMode {
488                suppress_keyword_expansion: v,
489                ..RegularMode::default()
490            },
491        }
492    }
493
494    /// # Description
495    ///
496    /// `-q`
497    ///
498    /// Quiet operation: suppress normal output messages. Messages describing
499    /// errors or exceptional conditions are not suppressed.
500    ///
501    /// Transitions this command to the [`RegularMode`] state.
502    pub fn quiet_mode(self, v: bool) -> Sync<RegularMode> {
503        Sync {
504            bin: self.bin,
505            global_opts: self.global_opts,
506            mode: RegularMode {
507                quiet_mode: v,
508                ..RegularMode::default()
509            },
510        }
511    }
512
513    /// # Description
514    ///
515    /// `-m max`
516    ///
517    /// Sync only the first `max` files specified.
518    #[cfg_attr(
519        not(feature = "lt2022_2"),
520        doc = "",
521        doc = "This option is useful in conjunction with tagged output and the",
522        doc = "`-n` flag, to preview how many files will be synced without",
523        doc = "transferring all the file data."
524    )]
525    ///
526    /// Transitions this command to the [`RegularMode`] state.
527    pub fn limit(self, v: u64) -> Sync<RegularMode> {
528        Sync {
529            bin: self.bin,
530            global_opts: self.global_opts,
531            mode: RegularMode {
532                limit: Some(v),
533                ..RegularMode::default()
534            },
535        }
536    }
537
538    /// # Description
539    ///
540    /// `--parallel=threads=N[,batch=N][,batchsize=N][,min=N][,minsize=N]`
541    ///
542    /// Specify options for parallel file transfer.
543    ///
544    /// Transitions this command to the [`RegularMode`] state.
545    pub fn parallel(self, v: ParallelConfig) -> Sync<RegularMode> {
546        Sync {
547            bin: self.bin,
548            global_opts: self.global_opts,
549            mode: RegularMode {
550                parallel: Some(v),
551                ..RegularMode::default()
552            },
553        }
554    }
555
556    /// # Description
557    ///
558    /// `--use-stream-change[N]`
559    ///
560    /// Specify the stream specification version to use for generating the
561    /// client view for sync.
562    ///
563    /// Transitions this command to the [`RegularMode`] state.
564    #[cfg(not(feature = "lt2022_2"))]
565    pub fn stream_spec_version(self, v: StreamSpecVersion) -> Sync<RegularMode> {
566        Sync {
567            bin: self.bin,
568            global_opts: self.global_opts,
569            mode: RegularMode {
570                stream_spec_version: Some(v),
571                ..RegularMode::default()
572            },
573        }
574    }
575
576    /// `--use-stream-change` (no value): the maximum change number in the
577    /// file list determines the stream spec version.
578    ///
579    /// Transitions this command to the [`RegularMode`] state.
580    #[cfg(not(feature = "lt2022_2"))]
581    pub fn sc_max_change_number(self) -> Sync<RegularMode> {
582        self.stream_spec_version(StreamSpecVersion::MaxInFilelists)
583    }
584
585    /// `--use-stream-change=0`: use the current stream spec version.
586    ///
587    /// Transitions this command to the [`RegularMode`] state.
588    #[cfg(not(feature = "lt2022_2"))]
589    pub fn sc_current_stream_spec(self) -> Sync<RegularMode> {
590        self.stream_spec_version(StreamSpecVersion::Current)
591    }
592
593    /// `--use-stream-change=N`: use the stream spec version at or before
594    /// change `n`.
595    ///
596    /// Transitions this command to the [`RegularMode`] state.
597    #[cfg(not(feature = "lt2022_2"))]
598    pub fn sc_change_number(self, n: u32) -> Sync<RegularMode> {
599        self.stream_spec_version(StreamSpecVersion::ChangeNumber(n))
600    }
601
602    /// # Description
603    ///
604    /// `-n`
605    ///
606    #[cfg_attr(
607        feature = "lt2016_1",
608        doc = "Display the results of the sync without actually performing the",
609        doc = "sync.",
610        doc = "",
611        doc = "This lets you make sure that the sync does what you think it",
612        doc = "does before you do it."
613    )]
614    #[cfg_attr(
615        not(feature = "lt2016_1"),
616        doc = "Preview mode: display the results of the sync without actually",
617        doc = "performing the sync."
618    )]
619    ///
620    /// Transitions this command to the [`RegularMode`] state with the
621    /// [`PreviewResult`] preview mode.
622    pub fn preview_result(self) -> Sync<RegularMode<Unselected, PreviewResult>> {
623        Sync {
624            bin: self.bin,
625            global_opts: self.global_opts,
626            mode: RegularMode {
627                preview: PreviewResult,
628                ..RegularMode::default()
629            },
630        }
631    }
632
633    /// # Description
634    ///
635    /// `-N`
636    ///
637    #[cfg_attr(
638        feature = "lt2016_1",
639        doc = "Display a summary of the expected network traffic associated",
640        doc = "with a sync, without performing the sync."
641    )]
642    #[cfg_attr(
643        all(feature = "lt2021_2", not(feature = "lt2016_1")),
644        doc = "Preview mode: display a summary of the expected network traffic",
645        doc = "associated with a sync, without performing the sync."
646    )]
647    #[cfg_attr(
648        not(feature = "lt2021_2"),
649        doc = "Preview mode: display a summary of the expected network traffic",
650        doc = "associated with a sync, without performing the sync.",
651        doc = "",
652        doc = "This tells you how many files are to be added or updated, which",
653        doc = "is useful if there are many large files, limits on bandwidth, or",
654        doc = "limits on disk space."
655    )]
656    ///
657    /// Transitions this command to the [`RegularMode`] state with the
658    /// [`PreviewNetworkTraffic`] preview mode.
659    pub fn preview_network_traffic(self) -> Sync<RegularMode<Unselected, PreviewNetworkTraffic>> {
660        Sync {
661            bin: self.bin,
662            global_opts: self.global_opts,
663            mode: RegularMode {
664                preview: PreviewNetworkTraffic,
665                ..RegularMode::default()
666            },
667        }
668    }
669
670    /// # Description
671    ///
672    /// `-f`
673    ///
674    #[cfg_attr(
675        feature = "lt2014_2",
676        doc = "Force the sync. Perforce performs the sync even if the client",
677        doc = "workspace already has the file at the specified revision. If the",
678        doc = "file is writable, it is overwritten.",
679        doc = "",
680        doc = "This flag does not affect open files, but it does override the",
681        doc = "noclobber client option."
682    )]
683    #[cfg_attr(
684        all(feature = "lt2017_2", not(feature = "lt2014_2")),
685        doc = "Force the sync. Perforce performs the sync even if the client",
686        doc = "workspace already has the file at the specified revision. If the",
687        doc = "file is writable, it is overwritten.",
688        doc = "",
689        doc = "This option does not affect open files, but it does override the",
690        doc = "noclobber client option."
691    )]
692    #[cfg_attr(
693        all(feature = "lt2024_1", not(feature = "lt2017_2")),
694        doc = "Force the sync. Helix Server performs the sync even if the",
695        doc = "client workspace already has the file at the specified",
696        doc = "revision. If the file is writable, it is overwritten.",
697        doc = "",
698        doc = "This option does not affect open files, but it does override the",
699        doc = "noclobber client option (see p4 client)."
700    )]
701    #[cfg_attr(
702        all(feature = "lt2024_2", not(feature = "lt2024_1")),
703        doc = "Force the sync. Helix Core Server performs the sync even if the",
704        doc = "client workspace already has the file at the specified",
705        doc = "revision. If the file is writable, it is overwritten.",
706        doc = "",
707        doc = "This option does not affect open files, but it does override the",
708        doc = "noclobber client option (see p4 client)."
709    )]
710    #[cfg_attr(
711        not(feature = "lt2024_2"),
712        doc = "Force the sync. P4 Server performs the sync even if the client",
713        doc = "workspace already has the file at the specified revision. If the",
714        doc = "file is writable, it is overwritten.",
715        doc = "",
716        doc = "This option does not affect open files, but it does override the",
717        doc = "noclobber client option (see p4 client)."
718    )]
719    ///
720    /// Transitions this command to the [`RegularMode`] state with the
721    /// [`ForceRegularMode`] sub-mode, which prevents further transitions to
722    /// [`SafeCheckMode`] or [`PopulateMode`].
723    pub fn force(self, v: bool) -> Sync<RegularMode<ForceRegularMode>> {
724        Sync {
725            bin: self.bin,
726            global_opts: self.global_opts,
727            mode: RegularMode {
728                mode: ForceRegularMode {
729                    force: v,
730                    ..ForceRegularMode::default()
731                },
732                ..RegularMode::default()
733            },
734        }
735    }
736
737    /// # Description
738    ///
739    /// `-k`
740    ///
741    #[cfg_attr(
742        feature = "lt2022_2",
743        doc = "Keep existing workspace files; update the have list without",
744        doc = "updating the client workspace."
745    )]
746    #[cfg_attr(
747        not(feature = "lt2022_2"),
748        doc = "Update server metadata without syncing files. Keep existing",
749        doc = "workspace files and update the have list without updating the",
750        doc = "client workspace."
751    )]
752    ///
753    /// Transitions this command to the [`RegularMode`] state with the
754    /// [`ForceRegularMode`] sub-mode, which prevents further transitions to
755    /// [`SafeCheckMode`] or [`PopulateMode`].
756    pub fn metadata_only(self, v: bool) -> Sync<RegularMode<ForceRegularMode>> {
757        Sync {
758            bin: self.bin,
759            global_opts: self.global_opts,
760            mode: RegularMode {
761                mode: ForceRegularMode {
762                    metadata_only: v,
763                    ..ForceRegularMode::default()
764                },
765                ..RegularMode::default()
766            },
767        }
768    }
769
770    /// # Description
771    ///
772    /// `-r`
773    ///
774    /// Reopen files that are mapped to new locations in the depot, in the new
775    /// location.
776    ///
777    /// Transitions this command to the [`RegularMode`] state with the
778    /// [`ForceRegularMode`] sub-mode, which prevents further transitions to
779    /// [`SafeCheckMode`] or [`PopulateMode`].
780    #[cfg(not(feature = "lt2015_1"))]
781    pub fn reopen_moved_files(self, v: bool) -> Sync<RegularMode<ForceRegularMode>> {
782        Sync {
783            bin: self.bin,
784            global_opts: self.global_opts,
785            mode: RegularMode {
786                mode: ForceRegularMode {
787                    reopen_moved_files: v,
788                    ..ForceRegularMode::default()
789                },
790                ..RegularMode::default()
791            },
792        }
793    }
794}
795
796// ---- Common option accessors (available in every RegularMode sub-mode) ----
797
798impl<Mode: ExclusiveOption, P: ExclusiveOption> Sync<RegularMode<Mode, P>> {
799    /// Returns whether edge replication is verified (`-E`).
800    #[cfg(not(feature = "lt2022_2"))]
801    pub fn get_verify_edge_replication(&self) -> bool {
802        self.mode.verify_edge_replication
803    }
804
805    /// Sets whether edge replication is verified (`-E`).
806    #[cfg(not(feature = "lt2022_2"))]
807    pub fn set_verify_edge_replication(&mut self, v: bool) -> &mut Self {
808        self.mode.verify_edge_replication = v;
809        self
810    }
811
812    /// Sets whether edge replication is verified (`-E`).
813    #[cfg(not(feature = "lt2022_2"))]
814    pub fn verify_edge_replication(mut self, v: bool) -> Self {
815        self.mode.verify_edge_replication = v;
816        self
817    }
818
819    /// Returns whether script list mode is enabled (`-L`).
820    pub fn get_script_list_mode(&self) -> bool {
821        self.mode.script_list_mode
822    }
823
824    /// Sets whether script list mode is enabled (`-L`).
825    pub fn set_script_list_mode(&mut self, v: bool) -> &mut Self {
826        self.mode.script_list_mode = v;
827        self
828    }
829
830    /// Sets whether script list mode is enabled (`-L`).
831    pub fn script_list_mode(mut self, v: bool) -> Self {
832        self.mode.script_list_mode = v;
833        self
834    }
835
836    /// Returns whether keyword expansion is suppressed (`-K`).
837    #[cfg(not(feature = "lt2022_1"))]
838    pub fn get_suppress_keyword_expansion(&self) -> bool {
839        self.mode.suppress_keyword_expansion
840    }
841
842    /// Sets whether keyword expansion is suppressed (`-K`).
843    #[cfg(not(feature = "lt2022_1"))]
844    pub fn set_suppress_keyword_expansion(&mut self, v: bool) -> &mut Self {
845        self.mode.suppress_keyword_expansion = v;
846        self
847    }
848
849    /// Sets whether keyword expansion is suppressed (`-K`).
850    #[cfg(not(feature = "lt2022_1"))]
851    pub fn suppress_keyword_expansion(mut self, v: bool) -> Self {
852        self.mode.suppress_keyword_expansion = v;
853        self
854    }
855
856    /// Returns whether quiet mode is enabled (`-q`).
857    pub fn get_quiet_mode(&self) -> bool {
858        self.mode.quiet_mode
859    }
860
861    /// Sets whether quiet mode is enabled (`-q`).
862    pub fn set_quiet_mode(&mut self, v: bool) -> &mut Self {
863        self.mode.quiet_mode = v;
864        self
865    }
866
867    /// Sets whether quiet mode is enabled (`-q`).
868    pub fn quiet_mode(mut self, v: bool) -> Self {
869        self.mode.quiet_mode = v;
870        self
871    }
872
873    /// Returns the maximum number of files to sync (`-m max`).
874    pub fn get_limit(&self) -> Option<u64> {
875        self.mode.limit
876    }
877
878    /// Sets the maximum number of files to sync (`-m max`).
879    pub fn set_limit(&mut self, v: u64) -> &mut Self {
880        self.mode.limit = Some(v);
881        self
882    }
883
884    /// Sets the maximum number of files to sync (`-m max`).
885    pub fn limit(mut self, v: u64) -> Self {
886        self.mode.limit = Some(v);
887        self
888    }
889
890    /// Returns the parallel sync configuration (`--parallel`).
891    pub fn get_parallel(&self) -> Option<&ParallelConfig> {
892        self.mode.parallel.as_ref()
893    }
894
895    /// Sets the parallel sync configuration (`--parallel`).
896    pub fn set_parallel(&mut self, v: ParallelConfig) -> &mut Self {
897        self.mode.parallel = Some(v);
898        self
899    }
900
901    /// Sets the parallel sync configuration (`--parallel`).
902    pub fn parallel(mut self, v: ParallelConfig) -> Self {
903        self.mode.parallel = Some(v);
904        self
905    }
906
907    /// Returns the stream spec version (`--use-stream-change`).
908    #[cfg(not(feature = "lt2022_2"))]
909    pub fn get_stream_spec_version(&self) -> Option<StreamSpecVersion> {
910        self.mode.stream_spec_version
911    }
912
913    /// Sets the stream spec version (`--use-stream-change`).
914    #[cfg(not(feature = "lt2022_2"))]
915    pub fn set_stream_spec_version(&mut self, v: StreamSpecVersion) -> &mut Self {
916        self.mode.stream_spec_version = Some(v);
917        self
918    }
919
920    /// Sets the stream spec version (`--use-stream-change`).
921    #[cfg(not(feature = "lt2022_2"))]
922    pub fn stream_spec_version(mut self, v: StreamSpecVersion) -> Self {
923        self.mode.stream_spec_version = Some(v);
924        self
925    }
926
927    /// `--use-stream-change` (no value): the maximum change number in the
928    /// file list determines the stream spec version.
929    #[cfg(not(feature = "lt2022_2"))]
930    pub fn set_sc_max_change_number(&mut self) -> &mut Self {
931        self.mode.stream_spec_version = Some(StreamSpecVersion::MaxInFilelists);
932        self
933    }
934
935    /// `--use-stream-change` (no value): the maximum change number in the
936    /// file list determines the stream spec version.
937    #[cfg(not(feature = "lt2022_2"))]
938    pub fn sc_max_change_number(mut self) -> Self {
939        self.mode.stream_spec_version = Some(StreamSpecVersion::MaxInFilelists);
940        self
941    }
942
943    /// `--use-stream-change=0`: use the current stream spec version.
944    #[cfg(not(feature = "lt2022_2"))]
945    pub fn set_sc_current_stream_spec(&mut self) -> &mut Self {
946        self.mode.stream_spec_version = Some(StreamSpecVersion::Current);
947        self
948    }
949
950    /// `--use-stream-change=0`: use the current stream spec version.
951    #[cfg(not(feature = "lt2022_2"))]
952    pub fn sc_current_stream_spec(mut self) -> Self {
953        self.mode.stream_spec_version = Some(StreamSpecVersion::Current);
954        self
955    }
956
957    /// `--use-stream-change=N`: use the stream spec version at or before
958    /// change `n`.
959    #[cfg(not(feature = "lt2022_2"))]
960    pub fn set_sc_change_number(&mut self, n: u32) -> &mut Self {
961        self.mode.stream_spec_version = Some(StreamSpecVersion::ChangeNumber(n));
962        self
963    }
964
965    /// `--use-stream-change=N`: use the stream spec version at or before
966    /// change `n`.
967    #[cfg(not(feature = "lt2022_2"))]
968    pub fn sc_change_number(mut self, n: u32) -> Self {
969        self.mode.stream_spec_version = Some(StreamSpecVersion::ChangeNumber(n));
970        self
971    }
972}
973
974// ---- Sub-mode transitions (only from the Unselected sub-mode) ----
975
976impl<P: ExclusiveOption> Sync<RegularMode<Unselected, P>> {
977    /// # Description
978    ///
979    /// `-f`
980    ///
981    #[cfg_attr(
982        feature = "lt2014_2",
983        doc = "Force the sync. Perforce performs the sync even if the client",
984        doc = "workspace already has the file at the specified revision. If the",
985        doc = "file is writable, it is overwritten.",
986        doc = "",
987        doc = "This flag does not affect open files, but it does override the",
988        doc = "noclobber client option."
989    )]
990    #[cfg_attr(
991        all(feature = "lt2017_2", not(feature = "lt2014_2")),
992        doc = "Force the sync. Perforce performs the sync even if the client",
993        doc = "workspace already has the file at the specified revision. If the",
994        doc = "file is writable, it is overwritten.",
995        doc = "",
996        doc = "This option does not affect open files, but it does override the",
997        doc = "noclobber client option."
998    )]
999    #[cfg_attr(
1000        all(feature = "lt2024_1", not(feature = "lt2017_2")),
1001        doc = "Force the sync. Helix Server performs the sync even if the",
1002        doc = "client workspace already has the file at the specified",
1003        doc = "revision. If the file is writable, it is overwritten.",
1004        doc = "",
1005        doc = "This option does not affect open files, but it does override the",
1006        doc = "noclobber client option (see p4 client)."
1007    )]
1008    #[cfg_attr(
1009        all(feature = "lt2024_2", not(feature = "lt2024_1")),
1010        doc = "Force the sync. Helix Core Server performs the sync even if the",
1011        doc = "client workspace already has the file at the specified",
1012        doc = "revision. If the file is writable, it is overwritten.",
1013        doc = "",
1014        doc = "This option does not affect open files, but it does override the",
1015        doc = "noclobber client option (see p4 client)."
1016    )]
1017    #[cfg_attr(
1018        not(feature = "lt2024_2"),
1019        doc = "Force the sync. P4 Server performs the sync even if the client",
1020        doc = "workspace already has the file at the specified revision. If the",
1021        doc = "file is writable, it is overwritten.",
1022        doc = "",
1023        doc = "This option does not affect open files, but it does override the",
1024        doc = "noclobber client option (see p4 client)."
1025    )]
1026    ///
1027    /// Transitions the sub-mode to [`ForceRegularMode`], which prevents
1028    /// further transitions to [`SafeCheckMode`] or [`PopulateMode`].
1029    pub fn force(self, v: bool) -> Sync<RegularMode<ForceRegularMode, P>> {
1030        Sync {
1031            bin: self.bin,
1032            global_opts: self.global_opts,
1033            mode: RegularMode {
1034                #[cfg(not(feature = "lt2022_2"))]
1035                verify_edge_replication: self.mode.verify_edge_replication,
1036                script_list_mode: self.mode.script_list_mode,
1037                #[cfg(not(feature = "lt2022_1"))]
1038                suppress_keyword_expansion: self.mode.suppress_keyword_expansion,
1039                quiet_mode: self.mode.quiet_mode,
1040                limit: self.mode.limit,
1041                parallel: self.mode.parallel,
1042                #[cfg(not(feature = "lt2022_2"))]
1043                stream_spec_version: self.mode.stream_spec_version,
1044                mode: ForceRegularMode {
1045                    force: v,
1046                    ..ForceRegularMode::default()
1047                },
1048                preview: self.mode.preview,
1049            },
1050        }
1051    }
1052
1053    /// # Description
1054    ///
1055    /// `-k`
1056    ///
1057    #[cfg_attr(
1058        feature = "lt2022_2",
1059        doc = "Keep existing workspace files; update the have list without",
1060        doc = "updating the client workspace."
1061    )]
1062    #[cfg_attr(
1063        not(feature = "lt2022_2"),
1064        doc = "Update server metadata without syncing files. Keep existing",
1065        doc = "workspace files and update the have list without updating the",
1066        doc = "client workspace."
1067    )]
1068    ///
1069    /// Transitions the sub-mode to [`ForceRegularMode`], which prevents
1070    /// further transitions to [`SafeCheckMode`] or [`PopulateMode`].
1071    pub fn metadata_only(self, v: bool) -> Sync<RegularMode<ForceRegularMode, P>> {
1072        Sync {
1073            bin: self.bin,
1074            global_opts: self.global_opts,
1075            mode: RegularMode {
1076                #[cfg(not(feature = "lt2022_2"))]
1077                verify_edge_replication: self.mode.verify_edge_replication,
1078                script_list_mode: self.mode.script_list_mode,
1079                #[cfg(not(feature = "lt2022_1"))]
1080                suppress_keyword_expansion: self.mode.suppress_keyword_expansion,
1081                quiet_mode: self.mode.quiet_mode,
1082                limit: self.mode.limit,
1083                parallel: self.mode.parallel,
1084                #[cfg(not(feature = "lt2022_2"))]
1085                stream_spec_version: self.mode.stream_spec_version,
1086                mode: ForceRegularMode {
1087                    metadata_only: v,
1088                    ..ForceRegularMode::default()
1089                },
1090                preview: self.mode.preview,
1091            },
1092        }
1093    }
1094
1095    /// # Description
1096    ///
1097    /// `-r`
1098    ///
1099    /// Reopen files that are mapped to new locations in the depot, in the new
1100    /// location.
1101    ///
1102    /// Transitions the sub-mode to [`ForceRegularMode`], which prevents
1103    /// further transitions to [`SafeCheckMode`] or [`PopulateMode`].
1104    #[cfg(not(feature = "lt2015_1"))]
1105    pub fn reopen_moved_files(self, v: bool) -> Sync<RegularMode<ForceRegularMode, P>> {
1106        Sync {
1107            bin: self.bin,
1108            global_opts: self.global_opts,
1109            mode: RegularMode {
1110                #[cfg(not(feature = "lt2022_2"))]
1111                verify_edge_replication: self.mode.verify_edge_replication,
1112                script_list_mode: self.mode.script_list_mode,
1113                #[cfg(not(feature = "lt2022_1"))]
1114                suppress_keyword_expansion: self.mode.suppress_keyword_expansion,
1115                quiet_mode: self.mode.quiet_mode,
1116                limit: self.mode.limit,
1117                parallel: self.mode.parallel,
1118                #[cfg(not(feature = "lt2022_2"))]
1119                stream_spec_version: self.mode.stream_spec_version,
1120                mode: ForceRegularMode {
1121                    reopen_moved_files: v,
1122                    ..ForceRegularMode::default()
1123                },
1124                preview: self.mode.preview,
1125            },
1126        }
1127    }
1128
1129    /// # Description
1130    ///
1131    /// `-s`
1132    ///
1133    /// Safe sync: compare the content in the client workspace against what
1134    /// was last synced.
1135    #[cfg_attr(
1136        feature = "lt2017_2",
1137        doc = "If the file was modified outside of Perforce control, an error",
1138        doc = "message is displayed and the file is not overwritten."
1139    )]
1140    #[cfg_attr(
1141        all(feature = "lt2024_1", not(feature = "lt2017_2")),
1142        doc = "If the file was modified outside of the control of Helix",
1143        doc = "Server, an error message is displayed and the file is not",
1144        doc = "overwritten."
1145    )]
1146    #[cfg_attr(
1147        all(feature = "lt2024_2", not(feature = "lt2024_1")),
1148        doc = "If the file was modified outside of the control of Helix Core",
1149        doc = "Server, an error message is displayed and the file is not",
1150        doc = "overwritten."
1151    )]
1152    #[cfg_attr(
1153        not(feature = "lt2024_2"),
1154        doc = "If the file was modified outside of the control of P4 Server,",
1155        doc = "an error message is displayed and the file is not overwritten."
1156    )]
1157    ///
1158    /// Transitions the sub-mode to [`SafeCheckMode`].
1159    pub fn safe_check(self) -> Sync<RegularMode<SafeCheckMode, P>> {
1160        Sync {
1161            bin: self.bin,
1162            global_opts: self.global_opts,
1163            mode: RegularMode {
1164                #[cfg(not(feature = "lt2022_2"))]
1165                verify_edge_replication: self.mode.verify_edge_replication,
1166                script_list_mode: self.mode.script_list_mode,
1167                #[cfg(not(feature = "lt2022_1"))]
1168                suppress_keyword_expansion: self.mode.suppress_keyword_expansion,
1169                quiet_mode: self.mode.quiet_mode,
1170                limit: self.mode.limit,
1171                parallel: self.mode.parallel,
1172                #[cfg(not(feature = "lt2022_2"))]
1173                stream_spec_version: self.mode.stream_spec_version,
1174                mode: SafeCheckMode,
1175                preview: self.mode.preview,
1176            },
1177        }
1178    }
1179
1180    /// # Description
1181    ///
1182    /// `-p`
1183    ///
1184    /// Populate a client workspace, but do not update the have list. Any
1185    /// file that is already synced or opened is bypassed with a warning
1186    /// message.
1187    ///
1188    /// Transitions the sub-mode to [`PopulateMode`].
1189    pub fn populate(self) -> Sync<RegularMode<PopulateMode, P>> {
1190        Sync {
1191            bin: self.bin,
1192            global_opts: self.global_opts,
1193            mode: RegularMode {
1194                #[cfg(not(feature = "lt2022_2"))]
1195                verify_edge_replication: self.mode.verify_edge_replication,
1196                script_list_mode: self.mode.script_list_mode,
1197                #[cfg(not(feature = "lt2022_1"))]
1198                suppress_keyword_expansion: self.mode.suppress_keyword_expansion,
1199                quiet_mode: self.mode.quiet_mode,
1200                limit: self.mode.limit,
1201                parallel: self.mode.parallel,
1202                #[cfg(not(feature = "lt2022_2"))]
1203                stream_spec_version: self.mode.stream_spec_version,
1204                mode: PopulateMode,
1205                preview: self.mode.preview,
1206            },
1207        }
1208    }
1209}
1210
1211// ---- Preview transitions (only when preview is Unselected) ----
1212
1213impl<Mode: ExclusiveOption> Sync<RegularMode<Mode, Unselected>> {
1214    /// # Description
1215    ///
1216    /// `-n`
1217    ///
1218    #[cfg_attr(
1219        feature = "lt2016_1",
1220        doc = "Display the results of the sync without actually performing the",
1221        doc = "sync.",
1222        doc = "",
1223        doc = "This lets you make sure that the sync does what you think it",
1224        doc = "does before you do it."
1225    )]
1226    #[cfg_attr(
1227        not(feature = "lt2016_1"),
1228        doc = "Preview mode: display the results of the sync without actually",
1229        doc = "performing the sync."
1230    )]
1231    ///
1232    /// Transitions the preview mode to [`PreviewResult`].
1233    pub fn preview_result(self) -> Sync<RegularMode<Mode, PreviewResult>> {
1234        Sync {
1235            bin: self.bin,
1236            global_opts: self.global_opts,
1237            mode: RegularMode {
1238                #[cfg(not(feature = "lt2022_2"))]
1239                verify_edge_replication: self.mode.verify_edge_replication,
1240                script_list_mode: self.mode.script_list_mode,
1241                #[cfg(not(feature = "lt2022_1"))]
1242                suppress_keyword_expansion: self.mode.suppress_keyword_expansion,
1243                quiet_mode: self.mode.quiet_mode,
1244                limit: self.mode.limit,
1245                parallel: self.mode.parallel,
1246                #[cfg(not(feature = "lt2022_2"))]
1247                stream_spec_version: self.mode.stream_spec_version,
1248                mode: self.mode.mode,
1249                preview: PreviewResult,
1250            },
1251        }
1252    }
1253
1254    /// # Description
1255    ///
1256    /// `-N`
1257    ///
1258    #[cfg_attr(
1259        feature = "lt2016_1",
1260        doc = "Display a summary of the expected network traffic associated",
1261        doc = "with a sync, without performing the sync."
1262    )]
1263    #[cfg_attr(
1264        all(feature = "lt2021_2", not(feature = "lt2016_1")),
1265        doc = "Preview mode: display a summary of the expected network traffic",
1266        doc = "associated with a sync, without performing the sync."
1267    )]
1268    #[cfg_attr(
1269        not(feature = "lt2021_2"),
1270        doc = "Preview mode: display a summary of the expected network traffic",
1271        doc = "associated with a sync, without performing the sync.",
1272        doc = "",
1273        doc = "This tells you how many files are to be added or updated, which",
1274        doc = "is useful if there are many large files, limits on bandwidth, or",
1275        doc = "limits on disk space."
1276    )]
1277    ///
1278    /// Transitions the preview mode to [`PreviewNetworkTraffic`].
1279    pub fn preview_network_traffic(self) -> Sync<RegularMode<Mode, PreviewNetworkTraffic>> {
1280        Sync {
1281            bin: self.bin,
1282            global_opts: self.global_opts,
1283            mode: RegularMode {
1284                #[cfg(not(feature = "lt2022_2"))]
1285                verify_edge_replication: self.mode.verify_edge_replication,
1286                script_list_mode: self.mode.script_list_mode,
1287                #[cfg(not(feature = "lt2022_1"))]
1288                suppress_keyword_expansion: self.mode.suppress_keyword_expansion,
1289                quiet_mode: self.mode.quiet_mode,
1290                limit: self.mode.limit,
1291                parallel: self.mode.parallel,
1292                #[cfg(not(feature = "lt2022_2"))]
1293                stream_spec_version: self.mode.stream_spec_version,
1294                mode: self.mode.mode,
1295                preview: PreviewNetworkTraffic,
1296            },
1297        }
1298    }
1299}
1300
1301// ---- Force-only option accessors (only in ForceRegularMode sub-mode) ----
1302
1303impl<P: ExclusiveOption> Sync<RegularMode<ForceRegularMode, P>> {
1304    /// Returns whether the sync is forced (`-f`).
1305    pub fn get_force(&self) -> bool {
1306        self.mode.mode.force
1307    }
1308
1309    /// Sets whether the sync is forced (`-f`).
1310    pub fn set_force(&mut self, v: bool) -> &mut Self {
1311        self.mode.mode.force = v;
1312        self
1313    }
1314
1315    /// Sets whether the sync is forced (`-f`).
1316    pub fn force(mut self, v: bool) -> Self {
1317        self.mode.mode.force = v;
1318        self
1319    }
1320
1321    /// Returns whether only metadata is updated (`-k`).
1322    pub fn get_metadata_only(&self) -> bool {
1323        self.mode.mode.metadata_only
1324    }
1325
1326    /// Sets whether only metadata is updated (`-k`).
1327    pub fn set_metadata_only(&mut self, v: bool) -> &mut Self {
1328        self.mode.mode.metadata_only = v;
1329        self
1330    }
1331
1332    /// Sets whether only metadata is updated (`-k`).
1333    pub fn metadata_only(mut self, v: bool) -> Self {
1334        self.mode.mode.metadata_only = v;
1335        self
1336    }
1337
1338    /// Returns whether moved files are reopened (`-r`).
1339    #[cfg(not(feature = "lt2015_1"))]
1340    pub fn get_reopen_moved_files(&self) -> bool {
1341        self.mode.mode.reopen_moved_files
1342    }
1343
1344    /// Sets whether moved files are reopened (`-r`).
1345    #[cfg(not(feature = "lt2015_1"))]
1346    pub fn set_reopen_moved_files(&mut self, v: bool) -> &mut Self {
1347        self.mode.mode.reopen_moved_files = v;
1348        self
1349    }
1350
1351    /// Sets whether moved files are reopened (`-r`).
1352    #[cfg(not(feature = "lt2015_1"))]
1353    pub fn reopen_moved_files(mut self, v: bool) -> Self {
1354        self.mode.mode.reopen_moved_files = v;
1355        self
1356    }
1357}
1358
1359// ---- SyncTimeMode accessors ----
1360
1361#[cfg(not(feature = "lt2025_1"))]
1362impl Sync<SyncTimeMode> {
1363    /// Returns the sync time value (`--sync-time=N`).
1364    pub fn get_sync_time(&self) -> &str {
1365        &self.mode.sync_time
1366    }
1367
1368    /// Sets the sync time value (`--sync-time=N`). The value can be Unix
1369    /// epoch time or the Perforce date time format.
1370    pub fn set_sync_time(&mut self, v: impl Into<String>) -> &mut Self {
1371        self.mode.sync_time = v.into();
1372        self
1373    }
1374
1375    /// Sets the sync time value (`--sync-time=N`). The value can be Unix
1376    /// epoch time or the Perforce date time format.
1377    pub fn sync_time(mut self, v: impl Into<String>) -> Self {
1378        self.mode.sync_time = v.into();
1379        self
1380    }
1381}
1382
1383// ---- Shared: executors + global opts ----
1384
1385impl<M: ExclusiveOption, S, I> ParameterizedSpawn<(S,)> for Sync<M>
1386where
1387    S: IntoIterator<Item = I>,
1388    I: AsRef<OsStr>,
1389{
1390    type Output = Child;
1391    type Error = std::io::Error;
1392
1393    /// Spawns `p4 sync` for the given files as a child process with piped
1394    /// standard output and error streams; use the returned [`Child`] handle
1395    /// to wait for it or interact with it.
1396    fn spawn_with(&mut self, (files,): (S,)) -> Result<Self::Output, Self::Error> {
1397        self.setup_command(&self.bin)
1398            .args(files)
1399            .stdout(Stdio::piped())
1400            .stderr(Stdio::piped())
1401            .spawn()
1402    }
1403}
1404
1405impl<M: ExclusiveOption> Sync<M> {
1406    /// Get the Perforce command path.
1407    pub fn get_bin(&self) -> &Path {
1408        &self.bin
1409    }
1410
1411    /// Set the Perforce command path.
1412    pub fn set_bin(&mut self, bin: impl Into<PathBuf>) -> &mut Self {
1413        self.bin = bin.into();
1414        self
1415    }
1416
1417    /// Set the Perforce command path.
1418    pub fn bin(mut self, bin: impl Into<PathBuf>) -> Self {
1419        self.set_bin(bin);
1420        self
1421    }
1422
1423    /// # Description
1424    ///
1425    /// g-opts
1426    ///
1427    #[cfg_attr(
1428        feature = "lt2014_2",
1429        doc = "See the [Global Options](GlobalOpts) section."
1430    )]
1431    #[cfg_attr(
1432        all(feature = "lt2015_1", not(feature = "lt2014_2")),
1433        doc = "See the [“Global Options”](GlobalOpts) section."
1434    )]
1435    #[cfg_attr(
1436        all(feature = "lt2017_1", not(feature = "lt2015_1")),
1437        doc = "See [“Global Options”](GlobalOpts)."
1438    )]
1439    #[cfg_attr(
1440        all(feature = "lt2018_2", not(feature = "lt2017_1")),
1441        doc = "See [Global Options](GlobalOpts)."
1442    )]
1443    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
1444    pub fn get_global_opts(&self) -> &GlobalOpts {
1445        &self.global_opts
1446    }
1447
1448    /// # Description
1449    ///
1450    /// g-opts
1451    ///
1452    #[cfg_attr(
1453        feature = "lt2014_2",
1454        doc = "See the [Global Options](GlobalOpts) section."
1455    )]
1456    #[cfg_attr(
1457        all(feature = "lt2015_1", not(feature = "lt2014_2")),
1458        doc = "See the [“Global Options”](GlobalOpts) section."
1459    )]
1460    #[cfg_attr(
1461        all(feature = "lt2017_1", not(feature = "lt2015_1")),
1462        doc = "See [“Global Options”](GlobalOpts)."
1463    )]
1464    #[cfg_attr(
1465        all(feature = "lt2018_2", not(feature = "lt2017_1")),
1466        doc = "See [Global Options](GlobalOpts)."
1467    )]
1468    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
1469    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
1470        self.global_opts = v;
1471        self
1472    }
1473
1474    /// # Description
1475    ///
1476    /// g-opts
1477    ///
1478    #[cfg_attr(
1479        feature = "lt2014_2",
1480        doc = "See the [Global Options](GlobalOpts) section."
1481    )]
1482    #[cfg_attr(
1483        all(feature = "lt2015_1", not(feature = "lt2014_2")),
1484        doc = "See the [“Global Options”](GlobalOpts) section."
1485    )]
1486    #[cfg_attr(
1487        all(feature = "lt2017_1", not(feature = "lt2015_1")),
1488        doc = "See [“Global Options”](GlobalOpts)."
1489    )]
1490    #[cfg_attr(
1491        all(feature = "lt2018_2", not(feature = "lt2017_1")),
1492        doc = "See [Global Options](GlobalOpts)."
1493    )]
1494    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
1495    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
1496        self.global_opts = v;
1497        self
1498    }
1499}
1500
1501impl<M: ExclusiveOption> SubCommand for Sync<M> {
1502    fn name(&self) -> &str {
1503        "sync"
1504    }
1505
1506    fn inject_local_args(&self, command: &mut Command) {
1507        self.mode.inject_args(command);
1508    }
1509
1510    fn global_opts(&self) -> Option<&GlobalOpts> {
1511        Some(&self.global_opts)
1512    }
1513}
1514
1515#[cfg(test)]
1516mod tests {
1517    use super::*;
1518    use crate::cmd::args_of;
1519
1520    #[test]
1521    fn bin_accessors_update_executable_path() {
1522        let mut command = Sync::new("p4", GlobalOpts::new());
1523        assert_eq!(command.get_bin(), Path::new("p4"));
1524
1525        command.set_bin("/usr/local/bin/p4");
1526        assert_eq!(command.get_bin(), Path::new("/usr/local/bin/p4"));
1527
1528        let command = command.bin("p4.exe");
1529        assert_eq!(command.get_bin(), Path::new("p4.exe"));
1530    }
1531
1532    #[test]
1533    fn without_options() {
1534        let sync = Sync::new("p4", GlobalOpts::new());
1535
1536        assert_eq!(args_of(&sync.setup_command("p4")), ["sync"]);
1537    }
1538
1539    #[test]
1540    #[cfg(not(feature = "lt2025_1"))]
1541    fn sync_time_mode() {
1542        let sync = Sync::new("p4", GlobalOpts::new()).sync_time("2024/01/01");
1543
1544        assert_eq!(
1545            args_of(&sync.setup_command("p4")),
1546            ["sync", "-k", "--sync-time=2024/01/01"]
1547        );
1548    }
1549
1550    #[test]
1551    #[cfg(not(feature = "lt2025_1"))]
1552    fn sync_time_mode_epoch() {
1553        let sync = Sync::new("p4", GlobalOpts::new()).sync_time("1700000000");
1554
1555        assert_eq!(
1556            args_of(&sync.setup_command("p4")),
1557            ["sync", "-k", "--sync-time=1700000000"]
1558        );
1559    }
1560
1561    #[test]
1562    #[cfg(not(feature = "lt2025_1"))]
1563    fn sync_time_set_style() {
1564        let mut sync = Sync::new("p4", GlobalOpts::new()).sync_time("2024/01/01");
1565        sync.set_sync_time("2024/06/01");
1566
1567        assert_eq!(sync.get_sync_time(), "2024/06/01");
1568        assert_eq!(
1569            args_of(&sync.setup_command("p4")),
1570            ["sync", "-k", "--sync-time=2024/06/01"]
1571        );
1572    }
1573
1574    #[test]
1575    fn regular_mode_common_options() {
1576        let sync = Sync::new("p4", GlobalOpts::new())
1577            .script_list_mode(true)
1578            .quiet_mode(true)
1579            .limit(5);
1580
1581        #[cfg(not(feature = "lt2022_2"))]
1582        {
1583            let sync = sync.verify_edge_replication(true);
1584            assert_eq!(
1585                args_of(&sync.setup_command("p4")),
1586                ["sync", "-E", "-L", "-q", "-m", "5"]
1587            );
1588        }
1589
1590        #[cfg(feature = "lt2022_2")]
1591        assert_eq!(
1592            args_of(&sync.setup_command("p4")),
1593            ["sync", "-L", "-q", "-m", "5"]
1594        );
1595    }
1596
1597    #[test]
1598    fn regular_mode_force_options() {
1599        let sync = Sync::new("p4", GlobalOpts::new())
1600            .force(true)
1601            .metadata_only(true);
1602
1603        #[cfg(not(feature = "lt2015_1"))]
1604        let sync = sync.reopen_moved_files(true);
1605
1606        #[cfg(not(feature = "lt2015_1"))]
1607        let expected = vec!["sync", "-f", "-k", "-r"];
1608        #[cfg(feature = "lt2015_1")]
1609        let expected = vec!["sync", "-f", "-k"];
1610
1611        assert_eq!(args_of(&sync.setup_command("p4")), expected);
1612    }
1613
1614    #[test]
1615    fn regular_mode_combined() {
1616        let sync = Sync::new("p4", GlobalOpts::new())
1617            .quiet_mode(true)
1618            .force(true)
1619            .limit(10);
1620
1621        assert_eq!(
1622            args_of(&sync.setup_command("p4")),
1623            ["sync", "-q", "-f", "-m", "10"]
1624        );
1625    }
1626
1627    #[test]
1628    fn regular_mode_preview_result() {
1629        let sync = Sync::new("p4", GlobalOpts::new()).preview_result();
1630
1631        assert_eq!(args_of(&sync.setup_command("p4")), ["sync", "-n"]);
1632    }
1633
1634    #[test]
1635    fn regular_mode_preview_network_traffic() {
1636        let sync = Sync::new("p4", GlobalOpts::new()).preview_network_traffic();
1637
1638        assert_eq!(args_of(&sync.setup_command("p4")), ["sync", "-N"]);
1639    }
1640
1641    #[test]
1642    fn regular_mode_preview_with_options() {
1643        let sync = Sync::new("p4", GlobalOpts::new())
1644            .quiet_mode(true)
1645            .preview_result()
1646            .limit(3);
1647
1648        assert_eq!(
1649            args_of(&sync.setup_command("p4")),
1650            ["sync", "-q", "-n", "-m", "3"]
1651        );
1652    }
1653
1654    #[test]
1655    fn regular_mode_parallel() {
1656        let sync = Sync::new("p4", GlobalOpts::new()).parallel(ParallelConfig {
1657            threads: 4,
1658            batch_files: Some(8),
1659            batch_size_bytes: None,
1660            min_files: Some(9),
1661            min_size_bytes: None,
1662        });
1663
1664        assert_eq!(
1665            args_of(&sync.setup_command("p4")),
1666            ["sync", "--parallel=threads=4,batch=8,min=9"]
1667        );
1668    }
1669
1670    #[test]
1671    #[cfg(not(feature = "lt2022_2"))]
1672    fn regular_mode_stream_spec_auto() {
1673        let sync = Sync::new("p4", GlobalOpts::new())
1674            .stream_spec_version(StreamSpecVersion::MaxInFilelists);
1675
1676        assert_eq!(
1677            args_of(&sync.setup_command("p4")),
1678            ["sync", "--use-stream-change"]
1679        );
1680    }
1681
1682    #[test]
1683    #[cfg(not(feature = "lt2022_2"))]
1684    fn regular_mode_stream_spec_current() {
1685        let sync =
1686            Sync::new("p4", GlobalOpts::new()).stream_spec_version(StreamSpecVersion::Current);
1687
1688        assert_eq!(
1689            args_of(&sync.setup_command("p4")),
1690            ["sync", "--use-stream-change=0"]
1691        );
1692    }
1693
1694    #[test]
1695    #[cfg(not(feature = "lt2022_2"))]
1696    fn regular_mode_stream_spec_specific() {
1697        let sync = Sync::new("p4", GlobalOpts::new())
1698            .stream_spec_version(StreamSpecVersion::ChangeNumber(123));
1699
1700        assert_eq!(
1701            args_of(&sync.setup_command("p4")),
1702            ["sync", "--use-stream-change=123"]
1703        );
1704    }
1705
1706    #[test]
1707    fn safe_check_mode() {
1708        let sync = Sync::new("p4", GlobalOpts::new()).enable_safe_check();
1709
1710        assert_eq!(args_of(&sync.setup_command("p4")), ["sync", "-s"]);
1711    }
1712
1713    #[test]
1714    fn safe_check_mode_with_options() {
1715        let sync = Sync::new("p4", GlobalOpts::new())
1716            .enable_safe_check()
1717            .quiet_mode(true)
1718            .limit(5);
1719
1720        assert_eq!(
1721            args_of(&sync.setup_command("p4")),
1722            ["sync", "-q", "-s", "-m", "5"]
1723        );
1724    }
1725
1726    #[test]
1727    fn populate_mode() {
1728        let sync = Sync::new("p4", GlobalOpts::new()).populate_client_workspace();
1729
1730        assert_eq!(args_of(&sync.setup_command("p4")), ["sync", "-p"]);
1731    }
1732
1733    #[test]
1734    fn populate_mode_with_options() {
1735        let sync = Sync::new("p4", GlobalOpts::new())
1736            .populate_client_workspace()
1737            .quiet_mode(true)
1738            .limit(5);
1739
1740        assert_eq!(
1741            args_of(&sync.setup_command("p4")),
1742            ["sync", "-q", "-p", "-m", "5"]
1743        );
1744    }
1745
1746    #[test]
1747    fn transition_to_safe_check_from_regular() {
1748        let sync = Sync::new("p4", GlobalOpts::new())
1749            .quiet_mode(true)
1750            .limit(5)
1751            .safe_check();
1752
1753        assert_eq!(
1754            args_of(&sync.setup_command("p4")),
1755            ["sync", "-q", "-s", "-m", "5"]
1756        );
1757    }
1758
1759    #[test]
1760    fn transition_to_populate_from_regular() {
1761        let sync = Sync::new("p4", GlobalOpts::new())
1762            .quiet_mode(true)
1763            .limit(5)
1764            .populate();
1765
1766        assert_eq!(
1767            args_of(&sync.setup_command("p4")),
1768            ["sync", "-q", "-p", "-m", "5"]
1769        );
1770    }
1771
1772    #[test]
1773    fn transition_preserves_preview() {
1774        let sync = Sync::new("p4", GlobalOpts::new())
1775            .preview_result()
1776            .quiet_mode(true)
1777            .safe_check();
1778
1779        assert_eq!(
1780            args_of(&sync.setup_command("p4")),
1781            ["sync", "-q", "-s", "-n"]
1782        );
1783    }
1784
1785    #[test]
1786    fn force_mode_blocks_safe_check() {
1787        // Once in ForceRegularMode, safe_check/populate are unavailable at
1788        // compile time. This test just exercises force mode injection.
1789        let sync = Sync::new("p4", GlobalOpts::new())
1790            .force(true)
1791            .quiet_mode(true);
1792
1793        assert_eq!(args_of(&sync.setup_command("p4")), ["sync", "-q", "-f"]);
1794    }
1795
1796    #[test]
1797    fn all_regular_options_order() {
1798        let sync = Sync::new("p4", GlobalOpts::new())
1799            .script_list_mode(true)
1800            .quiet_mode(true)
1801            .force(true)
1802            .metadata_only(true)
1803            .limit(5)
1804            .parallel(ParallelConfig {
1805                threads: 2,
1806                batch_files: None,
1807                batch_size_bytes: None,
1808                min_files: None,
1809                min_size_bytes: None,
1810            });
1811
1812        #[cfg(not(feature = "lt2022_2"))]
1813        let sync = sync.verify_edge_replication(true);
1814        #[cfg(not(feature = "lt2022_1"))]
1815        let sync = sync.suppress_keyword_expansion(true);
1816        #[cfg(not(feature = "lt2015_1"))]
1817        let sync = sync.reopen_moved_files(true);
1818        #[cfg(not(feature = "lt2022_2"))]
1819        let sync = sync.stream_spec_version(StreamSpecVersion::Current);
1820
1821        let mut expected: Vec<&str> = vec!["sync", "-L", "-q", "-f", "-k"];
1822        #[cfg(not(feature = "lt2022_2"))]
1823        expected.insert(1, "-E");
1824        #[cfg(not(feature = "lt2022_1"))]
1825        {
1826            let k = expected.iter().position(|&a| a == "-L").unwrap() + 1;
1827            expected.insert(k, "-K");
1828        }
1829        #[cfg(not(feature = "lt2015_1"))]
1830        expected.push("-r");
1831        expected.extend(["-m", "5", "--parallel=threads=2"]);
1832        #[cfg(not(feature = "lt2022_2"))]
1833        expected.push("--use-stream-change=0");
1834
1835        assert_eq!(args_of(&sync.setup_command("p4")), expected);
1836    }
1837}