Skip to main content

perforce_cli/cmd/
sync.rs

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