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#[derive(Debug, Clone, Copy)]
61pub enum StreamSpecVersion {
62    /// `--use-stream-change` with no value: the maximum change number in the
63    /// file list determines the stream spec version.
64    MaxInFilelists,
65    /// `--use-stream-change=0`: use the current stream spec version.
66    Current,
67    /// `--use-stream-change=N`: use the stream spec version at or before
68    /// change `N`.
69    ChangeNumber(u32),
70}
71
72impl StreamSpecVersion {
73    /// `--use-stream-change` with no value: the maximum change number in the
74    /// file list determines the stream spec version.
75    pub fn max_in_filelists() -> Self {
76        StreamSpecVersion::MaxInFilelists
77    }
78
79    /// `--use-stream-change=0`: use the current stream spec version.
80    pub fn current() -> Self {
81        StreamSpecVersion::Current
82    }
83
84    /// `--use-stream-change=N`: use the stream spec version at or before
85    /// change `n`.
86    pub fn at_change(n: u32) -> Self {
87        StreamSpecVersion::ChangeNumber(n)
88    }
89
90    /// Injects the `--use-stream-change` argument(s) into `command`.
91    pub fn inject_arg(&self, command: &mut Command) {
92        match self {
93            StreamSpecVersion::MaxInFilelists => {
94                command.arg("--use-stream-change");
95            }
96            StreamSpecVersion::Current => {
97                command.arg("--use-stream-change=0");
98            }
99            StreamSpecVersion::ChangeNumber(n) => {
100                command.arg(format!("--use-stream-change={}", n));
101            }
102        }
103    }
104}
105
106/// Preview mode of `p4 sync` (`-n`): display the results of the sync without
107/// actually performing the sync.
108///
109/// Entered with [`Sync::preview_result`]. Mutually exclusive with
110/// [`PreviewNetworkTraffic`].
111#[derive(Debug, Clone, Copy, Default)]
112pub struct PreviewResult;
113
114impl ExclusiveOption for PreviewResult {
115    fn inject_args(&self, command: &mut Command) {
116        command.arg("-n");
117    }
118}
119
120/// Preview mode of `p4 sync` (`-N`): display a summary of the expected
121/// network traffic associated with a sync, without performing the sync.
122///
123/// Entered with [`Sync::preview_network_traffic`]. Mutually exclusive with
124/// [`PreviewResult`].
125#[derive(Debug, Clone, Copy, Default)]
126pub struct PreviewNetworkTraffic;
127
128impl ExclusiveOption for PreviewNetworkTraffic {
129    fn inject_args(&self, command: &mut Command) {
130        command.arg("-N");
131    }
132}
133
134/// Force execution sub-mode of [`RegularMode`]: entered when any of `-f`,
135/// `-k`, or `-r` is set.
136///
137/// In this state the command is locked into the regular sync form and can no
138/// longer transition to [`SafeCheckMode`] or [`PopulateMode`].
139#[derive(Debug, Clone, Default)]
140pub struct ForceRegularMode {
141    force: bool,
142
143    metadata_only: bool,
144
145    reopen_moved_files: bool,
146}
147
148impl ExclusiveOption for ForceRegularMode {
149    fn inject_args(&self, command: &mut Command) {
150        if self.force {
151            command.arg("-f");
152        }
153
154        if self.metadata_only {
155            command.arg("-k");
156        }
157
158        if self.reopen_moved_files {
159            command.arg("-r");
160        }
161    }
162}
163
164/// Safe sync sub-mode of [`RegularMode`] (`-s`): compare the content in the
165/// client workspace against what was last synced and do not overwrite files
166/// that were modified outside of P4 Server's control.
167///
168/// Entered with [`Sync::safe_check`].
169#[derive(Debug, Clone, Copy, Default)]
170pub struct SafeCheckMode;
171
172impl ExclusiveOption for SafeCheckMode {
173    fn inject_args(&self, command: &mut Command) {
174        command.arg("-s");
175    }
176}
177
178/// Populate sub-mode of [`RegularMode`] (`-p`): populate a client workspace
179/// but do not update the have list.
180///
181/// Entered with [`Sync::populate`].
182#[derive(Debug, Clone, Copy, Default)]
183pub struct PopulateMode;
184
185impl ExclusiveOption for PopulateMode {
186    fn inject_args(&self, command: &mut Command) {
187        command.arg("-p");
188    }
189}
190
191/// Regular mode of `p4 sync`: the command forms that operate on the client
192/// workspace.
193///
194/// All common options are stored directly on this struct. The `Mode` type
195/// parameter selects the mutually exclusive execution sub-mode:
196///
197/// - [`Unselected`] (default): the first command form, plain regular sync.
198/// - [`ForceRegularMode`]: entered by setting `-f`, `-k`, or `-r`; locks out
199///   further transitions to [`SafeCheckMode`] or [`PopulateMode`].
200/// - [`SafeCheckMode`]: entered by [`Sync::safe_check`] (`-s`).
201/// - [`PopulateMode`]: entered by [`Sync::populate`] (`-p`).
202///
203/// The `P` type parameter tracks the preview mode ([`Unselected`] by default,
204/// [`PreviewResult`] or [`PreviewNetworkTraffic`] otherwise).
205#[derive(Debug, Clone, Default)]
206pub struct RegularMode<Mode = Unselected, P = Unselected> {
207    verify_edge_replication: bool,
208
209    script_list_mode: bool,
210
211    suppress_keyword_expansion: bool,
212
213    quiet_mode: bool,
214
215    limit: Option<u64>,
216
217    parallel: Option<ParallelConfig>,
218
219    stream_spec_version: Option<StreamSpecVersion>,
220
221    mode: Mode,
222
223    preview: P,
224}
225
226impl<Mode: ExclusiveOption, P: ExclusiveOption> ExclusiveOption for RegularMode<Mode, P> {
227    fn inject_args(&self, command: &mut Command) {
228        if self.verify_edge_replication {
229            command.arg("-E");
230        }
231
232        if self.script_list_mode {
233            command.arg("-L");
234        }
235
236        if self.suppress_keyword_expansion {
237            command.arg("-K");
238        }
239
240        if self.quiet_mode {
241            command.arg("-q");
242        }
243
244        self.mode.inject_args(command);
245
246        self.preview.inject_args(command);
247
248        if let Some(max) = self.limit {
249            command.arg("-m").arg(max.to_string());
250        }
251
252        if let Some(parallel) = &self.parallel {
253            command.arg(format!("--parallel={}", parallel.as_arg()));
254        }
255
256        if let Some(version) = &self.stream_spec_version {
257            version.inject_arg(command);
258        }
259    }
260}
261
262/// Sync-time mode of `p4 sync` (`-k --sync-time=N`): update the have list to
263/// reflect the state of the depot at the given time without transferring
264/// files.
265///
266/// Entered with [`Sync::sync_time`]. This mode always implies `-k` (metadata
267/// only), so no separate interface is provided for it.
268#[derive(Debug, Clone)]
269pub struct SyncTimeMode {
270    sync_time: String,
271}
272
273impl ExclusiveOption for SyncTimeMode {
274    fn inject_args(&self, command: &mut Command) {
275        command
276            .arg("-k")
277            .arg(format!("--sync-time={}", self.sync_time));
278    }
279}
280
281///
282/// Update the client workspace to reflect the contents of the depot.
283///
284/// The `M` type parameter tracks the top-level mode at compile time. The
285/// default [`Unselected`] state syncs files without local options; setting
286/// any regular option or calling a mode-transition method moves into
287/// [`RegularMode`]; [`Self::sync_time`] moves into [`SyncTimeMode`].
288#[derive(Debug, Clone, Default)]
289pub struct Sync<M = Unselected> {
290    bin: PathBuf,
291
292    global_opts: GlobalOpts,
293
294    mode: M,
295}
296
297impl Sync<Unselected> {
298    /// Creates a new `p4 sync` command.
299    ///
300    /// `bin` is the path to the Perforce command-line executable.
301    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
302        Self {
303            bin: bin.into(),
304            global_opts,
305            mode: Unselected,
306        }
307    }
308
309    /// # Description
310    ///
311    /// `-k --sync-time=N`
312    ///
313    /// Update the have list to reflect the state of the depot at the given
314    /// time without transferring files. The value of `N` can be Unix epoch
315    /// time or the Perforce date time format.
316    ///
317    /// This mode always implies `-k`, so no separate interface is provided
318    /// for it. Transitions this command to the [`SyncTimeMode`] state.
319    pub fn sync_time(self, time: impl Into<String>) -> Sync<SyncTimeMode> {
320        Sync {
321            bin: self.bin,
322            global_opts: self.global_opts,
323            mode: SyncTimeMode {
324                sync_time: time.into(),
325            },
326        }
327    }
328
329    /// # Description
330    ///
331    /// `-s`
332    ///
333    /// Safe sync: compare the content in the client workspace against what
334    /// was last synced. If the file was modified outside of the control of
335    /// P4 Server, an error message is displayed and the file is not
336    /// overwritten.
337    ///
338    /// Transitions this command to the [`RegularMode`] state with the
339    /// [`SafeCheckMode`] sub-mode.
340    pub fn enable_safe_check(self) -> Sync<RegularMode<SafeCheckMode>> {
341        Sync {
342            bin: self.bin,
343            global_opts: self.global_opts,
344            mode: RegularMode {
345                mode: SafeCheckMode,
346                ..RegularMode::default()
347            },
348        }
349    }
350
351    /// # Description
352    ///
353    /// `-p`
354    ///
355    /// Populate a client workspace, but do not update the have list. Any
356    /// file that is already synced or opened is bypassed with a warning
357    /// message.
358    ///
359    /// Transitions this command to the [`RegularMode`] state with the
360    /// [`PopulateMode`] sub-mode.
361    pub fn populate_client_workspace(self) -> Sync<RegularMode<PopulateMode>> {
362        Sync {
363            bin: self.bin,
364            global_opts: self.global_opts,
365            mode: RegularMode {
366                mode: PopulateMode,
367                ..RegularMode::default()
368            },
369        }
370    }
371
372    /// # Description
373    ///
374    /// `-E`
375    ///
376    /// For edge servers replicating from a commit or an upstream edge, verify
377    /// that any changelists specified in the revSpec are submitted before
378    /// continuing with the sync.
379    ///
380    /// Transitions this command to the [`RegularMode`] state.
381    pub fn verify_edge_replication(self, v: bool) -> Sync<RegularMode> {
382        Sync {
383            bin: self.bin,
384            global_opts: self.global_opts,
385            mode: RegularMode {
386                verify_edge_replication: v,
387                ..RegularMode::default()
388            },
389        }
390    }
391
392    /// # Description
393    ///
394    /// `-L`
395    ///
396    /// For scripting purposes, perform the sync on a list of valid file
397    /// arguments in full depot syntax with a valid revision number.
398    ///
399    /// Transitions this command to the [`RegularMode`] state.
400    pub fn script_list_mode(self, v: bool) -> Sync<RegularMode> {
401        Sync {
402            bin: self.bin,
403            global_opts: self.global_opts,
404            mode: RegularMode {
405                script_list_mode: v,
406                ..RegularMode::default()
407            },
408        }
409    }
410
411    /// # Description
412    ///
413    /// `-K`
414    ///
415    /// Suppress keyword expansion when updating `+k` type files on the
416    /// client.
417    ///
418    /// Transitions this command to the [`RegularMode`] state.
419    pub fn suppress_keyword_expansion(self, v: bool) -> Sync<RegularMode> {
420        Sync {
421            bin: self.bin,
422            global_opts: self.global_opts,
423            mode: RegularMode {
424                suppress_keyword_expansion: v,
425                ..RegularMode::default()
426            },
427        }
428    }
429
430    /// # Description
431    ///
432    /// `-q`
433    ///
434    /// Quiet operation: suppress normal output messages. Messages describing
435    /// errors or exceptional conditions are not suppressed.
436    ///
437    /// Transitions this command to the [`RegularMode`] state.
438    pub fn quiet_mode(self, v: bool) -> Sync<RegularMode> {
439        Sync {
440            bin: self.bin,
441            global_opts: self.global_opts,
442            mode: RegularMode {
443                quiet_mode: v,
444                ..RegularMode::default()
445            },
446        }
447    }
448
449    /// # Description
450    ///
451    /// `-m max`
452    ///
453    /// Sync only the first `max` files specified.
454    ///
455    /// Transitions this command to the [`RegularMode`] state.
456    pub fn limit(self, v: u64) -> Sync<RegularMode> {
457        Sync {
458            bin: self.bin,
459            global_opts: self.global_opts,
460            mode: RegularMode {
461                limit: Some(v),
462                ..RegularMode::default()
463            },
464        }
465    }
466
467    /// # Description
468    ///
469    /// `--parallel=threads=N[,batch=N][,batchsize=N][,min=N][,minsize=N]`
470    ///
471    /// Specify options for parallel file transfer.
472    ///
473    /// Transitions this command to the [`RegularMode`] state.
474    pub fn parallel(self, v: ParallelConfig) -> Sync<RegularMode> {
475        Sync {
476            bin: self.bin,
477            global_opts: self.global_opts,
478            mode: RegularMode {
479                parallel: Some(v),
480                ..RegularMode::default()
481            },
482        }
483    }
484
485    /// # Description
486    ///
487    /// `--use-stream-change[N]`
488    ///
489    /// Specify the stream specification version to use for generating the
490    /// client view for sync.
491    ///
492    /// Transitions this command to the [`RegularMode`] state.
493    pub fn stream_spec_version(self, v: StreamSpecVersion) -> Sync<RegularMode> {
494        Sync {
495            bin: self.bin,
496            global_opts: self.global_opts,
497            mode: RegularMode {
498                stream_spec_version: Some(v),
499                ..RegularMode::default()
500            },
501        }
502    }
503
504    /// `--use-stream-change` (no value): the maximum change number in the
505    /// file list determines the stream spec version.
506    ///
507    /// Transitions this command to the [`RegularMode`] state.
508    pub fn sc_max_change_number(self) -> Sync<RegularMode> {
509        self.stream_spec_version(StreamSpecVersion::MaxInFilelists)
510    }
511
512    /// `--use-stream-change=0`: use the current stream spec version.
513    ///
514    /// Transitions this command to the [`RegularMode`] state.
515    pub fn sc_current_stream_spec(self) -> Sync<RegularMode> {
516        self.stream_spec_version(StreamSpecVersion::Current)
517    }
518
519    /// `--use-stream-change=N`: use the stream spec version at or before
520    /// change `n`.
521    ///
522    /// Transitions this command to the [`RegularMode`] state.
523    pub fn sc_change_number(self, n: u32) -> Sync<RegularMode> {
524        self.stream_spec_version(StreamSpecVersion::ChangeNumber(n))
525    }
526
527    /// # Description
528    ///
529    /// `-n`
530    ///
531    /// Preview mode: display the results of the sync without actually
532    /// performing the sync.
533    ///
534    /// Transitions this command to the [`RegularMode`] state with the
535    /// [`PreviewResult`] preview mode.
536    pub fn preview_result(self) -> Sync<RegularMode<Unselected, PreviewResult>> {
537        Sync {
538            bin: self.bin,
539            global_opts: self.global_opts,
540            mode: RegularMode {
541                preview: PreviewResult,
542                ..RegularMode::default()
543            },
544        }
545    }
546
547    /// # Description
548    ///
549    /// `-N`
550    ///
551    /// Preview mode: display a summary of the expected network traffic
552    /// associated with a sync, without performing the sync.
553    ///
554    /// Transitions this command to the [`RegularMode`] state with the
555    /// [`PreviewNetworkTraffic`] preview mode.
556    pub fn preview_network_traffic(self) -> Sync<RegularMode<Unselected, PreviewNetworkTraffic>> {
557        Sync {
558            bin: self.bin,
559            global_opts: self.global_opts,
560            mode: RegularMode {
561                preview: PreviewNetworkTraffic,
562                ..RegularMode::default()
563            },
564        }
565    }
566
567    /// # Description
568    ///
569    /// `-f`
570    ///
571    /// Force the sync. P4 Server performs the sync even if the client
572    /// workspace already has the file at the specified revision.
573    ///
574    /// Transitions this command to the [`RegularMode`] state with the
575    /// [`ForceRegularMode`] sub-mode, which prevents further transitions to
576    /// [`SafeCheckMode`] or [`PopulateMode`].
577    pub fn force(self, v: bool) -> Sync<RegularMode<ForceRegularMode>> {
578        Sync {
579            bin: self.bin,
580            global_opts: self.global_opts,
581            mode: RegularMode {
582                mode: ForceRegularMode {
583                    force: v,
584                    ..ForceRegularMode::default()
585                },
586                ..RegularMode::default()
587            },
588        }
589    }
590
591    /// # Description
592    ///
593    /// `-k`
594    ///
595    /// Update server metadata without syncing files. Keep existing workspace
596    /// files and update the have list without updating the client workspace.
597    ///
598    /// Transitions this command to the [`RegularMode`] state with the
599    /// [`ForceRegularMode`] sub-mode, which prevents further transitions to
600    /// [`SafeCheckMode`] or [`PopulateMode`].
601    pub fn metadata_only(self, v: bool) -> Sync<RegularMode<ForceRegularMode>> {
602        Sync {
603            bin: self.bin,
604            global_opts: self.global_opts,
605            mode: RegularMode {
606                mode: ForceRegularMode {
607                    metadata_only: v,
608                    ..ForceRegularMode::default()
609                },
610                ..RegularMode::default()
611            },
612        }
613    }
614
615    /// # Description
616    ///
617    /// `-r`
618    ///
619    /// Reopen files that are mapped to new locations in the depot, in the new
620    /// location.
621    ///
622    /// Transitions this command to the [`RegularMode`] state with the
623    /// [`ForceRegularMode`] sub-mode, which prevents further transitions to
624    /// [`SafeCheckMode`] or [`PopulateMode`].
625    pub fn reopen_moved_files(self, v: bool) -> Sync<RegularMode<ForceRegularMode>> {
626        Sync {
627            bin: self.bin,
628            global_opts: self.global_opts,
629            mode: RegularMode {
630                mode: ForceRegularMode {
631                    reopen_moved_files: v,
632                    ..ForceRegularMode::default()
633                },
634                ..RegularMode::default()
635            },
636        }
637    }
638}
639
640// ---- Common option accessors (available in every RegularMode sub-mode) ----
641
642impl<Mode: ExclusiveOption, P: ExclusiveOption> Sync<RegularMode<Mode, P>> {
643    /// Returns whether edge replication is verified (`-E`).
644    pub fn get_verify_edge_replication(&self) -> bool {
645        self.mode.verify_edge_replication
646    }
647
648    /// Sets whether edge replication is verified (`-E`).
649    pub fn set_verify_edge_replication(&mut self, v: bool) -> &mut Self {
650        self.mode.verify_edge_replication = v;
651        self
652    }
653
654    /// Sets whether edge replication is verified (`-E`).
655    pub fn verify_edge_replication(mut self, v: bool) -> Self {
656        self.mode.verify_edge_replication = v;
657        self
658    }
659
660    /// Returns whether script list mode is enabled (`-L`).
661    pub fn get_script_list_mode(&self) -> bool {
662        self.mode.script_list_mode
663    }
664
665    /// Sets whether script list mode is enabled (`-L`).
666    pub fn set_script_list_mode(&mut self, v: bool) -> &mut Self {
667        self.mode.script_list_mode = v;
668        self
669    }
670
671    /// Sets whether script list mode is enabled (`-L`).
672    pub fn script_list_mode(mut self, v: bool) -> Self {
673        self.mode.script_list_mode = v;
674        self
675    }
676
677    /// Returns whether keyword expansion is suppressed (`-K`).
678    pub fn get_suppress_keyword_expansion(&self) -> bool {
679        self.mode.suppress_keyword_expansion
680    }
681
682    /// Sets whether keyword expansion is suppressed (`-K`).
683    pub fn set_suppress_keyword_expansion(&mut self, v: bool) -> &mut Self {
684        self.mode.suppress_keyword_expansion = v;
685        self
686    }
687
688    /// Sets whether keyword expansion is suppressed (`-K`).
689    pub fn suppress_keyword_expansion(mut self, v: bool) -> Self {
690        self.mode.suppress_keyword_expansion = v;
691        self
692    }
693
694    /// Returns whether quiet mode is enabled (`-q`).
695    pub fn get_quiet_mode(&self) -> bool {
696        self.mode.quiet_mode
697    }
698
699    /// Sets whether quiet mode is enabled (`-q`).
700    pub fn set_quiet_mode(&mut self, v: bool) -> &mut Self {
701        self.mode.quiet_mode = v;
702        self
703    }
704
705    /// Sets whether quiet mode is enabled (`-q`).
706    pub fn quiet_mode(mut self, v: bool) -> Self {
707        self.mode.quiet_mode = v;
708        self
709    }
710
711    /// Returns the maximum number of files to sync (`-m max`).
712    pub fn get_limit(&self) -> Option<u64> {
713        self.mode.limit
714    }
715
716    /// Sets the maximum number of files to sync (`-m max`).
717    pub fn set_limit(&mut self, v: u64) -> &mut Self {
718        self.mode.limit = Some(v);
719        self
720    }
721
722    /// Sets the maximum number of files to sync (`-m max`).
723    pub fn limit(mut self, v: u64) -> Self {
724        self.mode.limit = Some(v);
725        self
726    }
727
728    /// Returns the parallel sync configuration (`--parallel`).
729    pub fn get_parallel(&self) -> Option<&ParallelConfig> {
730        self.mode.parallel.as_ref()
731    }
732
733    /// Sets the parallel sync configuration (`--parallel`).
734    pub fn set_parallel(&mut self, v: ParallelConfig) -> &mut Self {
735        self.mode.parallel = Some(v);
736        self
737    }
738
739    /// Sets the parallel sync configuration (`--parallel`).
740    pub fn parallel(mut self, v: ParallelConfig) -> Self {
741        self.mode.parallel = Some(v);
742        self
743    }
744
745    /// Returns the stream spec version (`--use-stream-change`).
746    pub fn get_stream_spec_version(&self) -> Option<StreamSpecVersion> {
747        self.mode.stream_spec_version
748    }
749
750    /// Sets the stream spec version (`--use-stream-change`).
751    pub fn set_stream_spec_version(&mut self, v: StreamSpecVersion) -> &mut Self {
752        self.mode.stream_spec_version = Some(v);
753        self
754    }
755
756    /// Sets the stream spec version (`--use-stream-change`).
757    pub fn stream_spec_version(mut self, v: StreamSpecVersion) -> Self {
758        self.mode.stream_spec_version = Some(v);
759        self
760    }
761
762    /// `--use-stream-change` (no value): the maximum change number in the
763    /// file list determines the stream spec version.
764    pub fn set_sc_max_change_number(&mut self) -> &mut Self {
765        self.mode.stream_spec_version = Some(StreamSpecVersion::MaxInFilelists);
766        self
767    }
768
769    /// `--use-stream-change` (no value): the maximum change number in the
770    /// file list determines the stream spec version.
771    pub fn sc_max_change_number(mut self) -> Self {
772        self.mode.stream_spec_version = Some(StreamSpecVersion::MaxInFilelists);
773        self
774    }
775
776    /// `--use-stream-change=0`: use the current stream spec version.
777    pub fn set_sc_current_stream_spec(&mut self) -> &mut Self {
778        self.mode.stream_spec_version = Some(StreamSpecVersion::Current);
779        self
780    }
781
782    /// `--use-stream-change=0`: use the current stream spec version.
783    pub fn sc_current_stream_spec(mut self) -> Self {
784        self.mode.stream_spec_version = Some(StreamSpecVersion::Current);
785        self
786    }
787
788    /// `--use-stream-change=N`: use the stream spec version at or before
789    /// change `n`.
790    pub fn set_sc_change_number(&mut self, n: u32) -> &mut Self {
791        self.mode.stream_spec_version = Some(StreamSpecVersion::ChangeNumber(n));
792        self
793    }
794
795    /// `--use-stream-change=N`: use the stream spec version at or before
796    /// change `n`.
797    pub fn sc_change_number(mut self, n: u32) -> Self {
798        self.mode.stream_spec_version = Some(StreamSpecVersion::ChangeNumber(n));
799        self
800    }
801}
802
803// ---- Sub-mode transitions (only from the Unselected sub-mode) ----
804
805impl<P: ExclusiveOption> Sync<RegularMode<Unselected, P>> {
806    /// # Description
807    ///
808    /// `-f`
809    ///
810    /// Force the sync. P4 Server performs the sync even if the client
811    /// workspace already has the file at the specified revision.
812    ///
813    /// Transitions the sub-mode to [`ForceRegularMode`], which prevents
814    /// further transitions to [`SafeCheckMode`] or [`PopulateMode`].
815    pub fn force(self, v: bool) -> Sync<RegularMode<ForceRegularMode, P>> {
816        Sync {
817            bin: self.bin,
818            global_opts: self.global_opts,
819            mode: RegularMode {
820                verify_edge_replication: self.mode.verify_edge_replication,
821                script_list_mode: self.mode.script_list_mode,
822                suppress_keyword_expansion: self.mode.suppress_keyword_expansion,
823                quiet_mode: self.mode.quiet_mode,
824                limit: self.mode.limit,
825                parallel: self.mode.parallel,
826                stream_spec_version: self.mode.stream_spec_version,
827                mode: ForceRegularMode {
828                    force: v,
829                    ..ForceRegularMode::default()
830                },
831                preview: self.mode.preview,
832            },
833        }
834    }
835
836    /// # Description
837    ///
838    /// `-k`
839    ///
840    /// Update server metadata without syncing files. Keep existing workspace
841    /// files and update the have list without updating the client workspace.
842    ///
843    /// Transitions the sub-mode to [`ForceRegularMode`], which prevents
844    /// further transitions to [`SafeCheckMode`] or [`PopulateMode`].
845    pub fn metadata_only(self, v: bool) -> Sync<RegularMode<ForceRegularMode, P>> {
846        Sync {
847            bin: self.bin,
848            global_opts: self.global_opts,
849            mode: RegularMode {
850                verify_edge_replication: self.mode.verify_edge_replication,
851                script_list_mode: self.mode.script_list_mode,
852                suppress_keyword_expansion: self.mode.suppress_keyword_expansion,
853                quiet_mode: self.mode.quiet_mode,
854                limit: self.mode.limit,
855                parallel: self.mode.parallel,
856                stream_spec_version: self.mode.stream_spec_version,
857                mode: ForceRegularMode {
858                    metadata_only: v,
859                    ..ForceRegularMode::default()
860                },
861                preview: self.mode.preview,
862            },
863        }
864    }
865
866    /// # Description
867    ///
868    /// `-r`
869    ///
870    /// Reopen files that are mapped to new locations in the depot, in the new
871    /// location.
872    ///
873    /// Transitions the sub-mode to [`ForceRegularMode`], which prevents
874    /// further transitions to [`SafeCheckMode`] or [`PopulateMode`].
875    pub fn reopen_moved_files(self, v: bool) -> Sync<RegularMode<ForceRegularMode, P>> {
876        Sync {
877            bin: self.bin,
878            global_opts: self.global_opts,
879            mode: RegularMode {
880                verify_edge_replication: self.mode.verify_edge_replication,
881                script_list_mode: self.mode.script_list_mode,
882                suppress_keyword_expansion: self.mode.suppress_keyword_expansion,
883                quiet_mode: self.mode.quiet_mode,
884                limit: self.mode.limit,
885                parallel: self.mode.parallel,
886                stream_spec_version: self.mode.stream_spec_version,
887                mode: ForceRegularMode {
888                    reopen_moved_files: v,
889                    ..ForceRegularMode::default()
890                },
891                preview: self.mode.preview,
892            },
893        }
894    }
895
896    /// # Description
897    ///
898    /// `-s`
899    ///
900    /// Safe sync: compare the content in the client workspace against what
901    /// was last synced. If the file was modified outside of the control of
902    /// P4 Server, an error message is displayed and the file is not
903    /// overwritten.
904    ///
905    /// Transitions the sub-mode to [`SafeCheckMode`].
906    pub fn safe_check(self) -> Sync<RegularMode<SafeCheckMode, P>> {
907        Sync {
908            bin: self.bin,
909            global_opts: self.global_opts,
910            mode: RegularMode {
911                verify_edge_replication: self.mode.verify_edge_replication,
912                script_list_mode: self.mode.script_list_mode,
913                suppress_keyword_expansion: self.mode.suppress_keyword_expansion,
914                quiet_mode: self.mode.quiet_mode,
915                limit: self.mode.limit,
916                parallel: self.mode.parallel,
917                stream_spec_version: self.mode.stream_spec_version,
918                mode: SafeCheckMode,
919                preview: self.mode.preview,
920            },
921        }
922    }
923
924    /// # Description
925    ///
926    /// `-p`
927    ///
928    /// Populate a client workspace, but do not update the have list. Any
929    /// file that is already synced or opened is bypassed with a warning
930    /// message.
931    ///
932    /// Transitions the sub-mode to [`PopulateMode`].
933    pub fn populate(self) -> Sync<RegularMode<PopulateMode, P>> {
934        Sync {
935            bin: self.bin,
936            global_opts: self.global_opts,
937            mode: RegularMode {
938                verify_edge_replication: self.mode.verify_edge_replication,
939                script_list_mode: self.mode.script_list_mode,
940                suppress_keyword_expansion: self.mode.suppress_keyword_expansion,
941                quiet_mode: self.mode.quiet_mode,
942                limit: self.mode.limit,
943                parallel: self.mode.parallel,
944                stream_spec_version: self.mode.stream_spec_version,
945                mode: PopulateMode,
946                preview: self.mode.preview,
947            },
948        }
949    }
950}
951
952// ---- Preview transitions (only when preview is Unselected) ----
953
954impl<Mode: ExclusiveOption> Sync<RegularMode<Mode, Unselected>> {
955    /// # Description
956    ///
957    /// `-n`
958    ///
959    /// Preview mode: display the results of the sync without actually
960    /// performing the sync.
961    pub fn preview_result(self) -> Sync<RegularMode<Mode, PreviewResult>> {
962        Sync {
963            bin: self.bin,
964            global_opts: self.global_opts,
965            mode: RegularMode {
966                verify_edge_replication: self.mode.verify_edge_replication,
967                script_list_mode: self.mode.script_list_mode,
968                suppress_keyword_expansion: self.mode.suppress_keyword_expansion,
969                quiet_mode: self.mode.quiet_mode,
970                limit: self.mode.limit,
971                parallel: self.mode.parallel,
972                stream_spec_version: self.mode.stream_spec_version,
973                mode: self.mode.mode,
974                preview: PreviewResult,
975            },
976        }
977    }
978
979    /// # Description
980    ///
981    /// `-N`
982    ///
983    /// Preview mode: display a summary of the expected network traffic
984    /// associated with a sync, without performing the sync.
985    pub fn preview_network_traffic(self) -> Sync<RegularMode<Mode, PreviewNetworkTraffic>> {
986        Sync {
987            bin: self.bin,
988            global_opts: self.global_opts,
989            mode: RegularMode {
990                verify_edge_replication: self.mode.verify_edge_replication,
991                script_list_mode: self.mode.script_list_mode,
992                suppress_keyword_expansion: self.mode.suppress_keyword_expansion,
993                quiet_mode: self.mode.quiet_mode,
994                limit: self.mode.limit,
995                parallel: self.mode.parallel,
996                stream_spec_version: self.mode.stream_spec_version,
997                mode: self.mode.mode,
998                preview: PreviewNetworkTraffic,
999            },
1000        }
1001    }
1002}
1003
1004// ---- Force-only option accessors (only in ForceRegularMode sub-mode) ----
1005
1006impl<P: ExclusiveOption> Sync<RegularMode<ForceRegularMode, P>> {
1007    /// Returns whether the sync is forced (`-f`).
1008    pub fn get_force(&self) -> bool {
1009        self.mode.mode.force
1010    }
1011
1012    /// Sets whether the sync is forced (`-f`).
1013    pub fn set_force(&mut self, v: bool) -> &mut Self {
1014        self.mode.mode.force = v;
1015        self
1016    }
1017
1018    /// Sets whether the sync is forced (`-f`).
1019    pub fn force(mut self, v: bool) -> Self {
1020        self.mode.mode.force = v;
1021        self
1022    }
1023
1024    /// Returns whether only metadata is updated (`-k`).
1025    pub fn get_metadata_only(&self) -> bool {
1026        self.mode.mode.metadata_only
1027    }
1028
1029    /// Sets whether only metadata is updated (`-k`).
1030    pub fn set_metadata_only(&mut self, v: bool) -> &mut Self {
1031        self.mode.mode.metadata_only = v;
1032        self
1033    }
1034
1035    /// Sets whether only metadata is updated (`-k`).
1036    pub fn metadata_only(mut self, v: bool) -> Self {
1037        self.mode.mode.metadata_only = v;
1038        self
1039    }
1040
1041    /// Returns whether moved files are reopened (`-r`).
1042    pub fn get_reopen_moved_files(&self) -> bool {
1043        self.mode.mode.reopen_moved_files
1044    }
1045
1046    /// Sets whether moved files are reopened (`-r`).
1047    pub fn set_reopen_moved_files(&mut self, v: bool) -> &mut Self {
1048        self.mode.mode.reopen_moved_files = v;
1049        self
1050    }
1051
1052    /// Sets whether moved files are reopened (`-r`).
1053    pub fn reopen_moved_files(mut self, v: bool) -> Self {
1054        self.mode.mode.reopen_moved_files = v;
1055        self
1056    }
1057}
1058
1059// ---- SyncTimeMode accessors ----
1060
1061impl Sync<SyncTimeMode> {
1062    /// Returns the sync time value (`--sync-time=N`).
1063    pub fn get_sync_time(&self) -> &str {
1064        &self.mode.sync_time
1065    }
1066
1067    /// Sets the sync time value (`--sync-time=N`). The value can be Unix
1068    /// epoch time or the Perforce date time format.
1069    pub fn set_sync_time(&mut self, v: impl Into<String>) -> &mut Self {
1070        self.mode.sync_time = v.into();
1071        self
1072    }
1073
1074    /// Sets the sync time value (`--sync-time=N`). The value can be Unix
1075    /// epoch time or the Perforce date time format.
1076    pub fn sync_time(mut self, v: impl Into<String>) -> Self {
1077        self.mode.sync_time = v.into();
1078        self
1079    }
1080}
1081
1082// ---- Shared: executors + global opts ----
1083
1084impl<M: ExclusiveOption> ParameterizedSpawn for Sync<M> {
1085    type Input<'a> = &'a [&'a OsStr];
1086    type Output<'a> = Child;
1087    type Error = std::io::Error;
1088
1089    /// Spawns `p4 sync` for the given files as a child process with piped
1090    /// standard output and error streams; use the returned [`Child`] handle
1091    /// to wait for it or interact with it.
1092    fn spawn_with<'a>(&mut self, files: Self::Input<'a>) -> Result<Self::Output<'a>, Self::Error> {
1093        self.setup_command(&self.bin)
1094            .args(files)
1095            .stdout(Stdio::piped())
1096            .stderr(Stdio::piped())
1097            .spawn()
1098    }
1099}
1100
1101impl<M: ExclusiveOption> Sync<M> {
1102    /// # Description
1103    ///
1104    /// g-opts
1105    ///
1106    /// See [Global options](GlobalOpts).
1107    pub fn get_global_opts(&self) -> &GlobalOpts {
1108        &self.global_opts
1109    }
1110
1111    /// # Description
1112    ///
1113    /// g-opts
1114    ///
1115    /// See [Global options](GlobalOpts).
1116    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
1117        self.global_opts = v;
1118        self
1119    }
1120
1121    /// # Description
1122    ///
1123    /// g-opts
1124    ///
1125    /// See [Global options](GlobalOpts).
1126    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
1127        self.global_opts = v;
1128        self
1129    }
1130}
1131
1132impl<M: ExclusiveOption> SubCommand for Sync<M> {
1133    fn name(&self) -> &str {
1134        "sync"
1135    }
1136
1137    fn inject_local_args(&self, command: &mut Command) {
1138        self.mode.inject_args(command);
1139    }
1140
1141    fn global_opts(&self) -> Option<&GlobalOpts> {
1142        Some(&self.global_opts)
1143    }
1144}
1145
1146#[cfg(test)]
1147mod tests {
1148    use super::*;
1149    use crate::cmd::args_of;
1150
1151    #[test]
1152    fn without_options() {
1153        let sync = Sync::new("p4", GlobalOpts::new());
1154
1155        assert_eq!(args_of(&sync.setup_command("p4")), ["sync"]);
1156    }
1157
1158    #[test]
1159    fn sync_time_mode() {
1160        let sync = Sync::new("p4", GlobalOpts::new()).sync_time("2024/01/01");
1161
1162        assert_eq!(
1163            args_of(&sync.setup_command("p4")),
1164            ["sync", "-k", "--sync-time=2024/01/01"]
1165        );
1166    }
1167
1168    #[test]
1169    fn sync_time_mode_epoch() {
1170        let sync = Sync::new("p4", GlobalOpts::new()).sync_time("1700000000");
1171
1172        assert_eq!(
1173            args_of(&sync.setup_command("p4")),
1174            ["sync", "-k", "--sync-time=1700000000"]
1175        );
1176    }
1177
1178    #[test]
1179    fn sync_time_set_style() {
1180        let mut sync = Sync::new("p4", GlobalOpts::new()).sync_time("2024/01/01");
1181        sync.set_sync_time("2024/06/01");
1182
1183        assert_eq!(sync.get_sync_time(), "2024/06/01");
1184        assert_eq!(
1185            args_of(&sync.setup_command("p4")),
1186            ["sync", "-k", "--sync-time=2024/06/01"]
1187        );
1188    }
1189
1190    #[test]
1191    fn regular_mode_common_options() {
1192        let sync = Sync::new("p4", GlobalOpts::new())
1193            .verify_edge_replication(true)
1194            .script_list_mode(true)
1195            .suppress_keyword_expansion(true)
1196            .quiet_mode(true)
1197            .limit(5);
1198
1199        assert_eq!(
1200            args_of(&sync.setup_command("p4")),
1201            ["sync", "-E", "-L", "-K", "-q", "-m", "5"]
1202        );
1203    }
1204
1205    #[test]
1206    fn regular_mode_force_options() {
1207        let sync = Sync::new("p4", GlobalOpts::new())
1208            .force(true)
1209            .metadata_only(true)
1210            .reopen_moved_files(true);
1211
1212        assert_eq!(
1213            args_of(&sync.setup_command("p4")),
1214            ["sync", "-f", "-k", "-r"]
1215        );
1216    }
1217
1218    #[test]
1219    fn regular_mode_combined() {
1220        let sync = Sync::new("p4", GlobalOpts::new())
1221            .quiet_mode(true)
1222            .force(true)
1223            .limit(10);
1224
1225        assert_eq!(
1226            args_of(&sync.setup_command("p4")),
1227            ["sync", "-q", "-f", "-m", "10"]
1228        );
1229    }
1230
1231    #[test]
1232    fn regular_mode_preview_result() {
1233        let sync = Sync::new("p4", GlobalOpts::new()).preview_result();
1234
1235        assert_eq!(args_of(&sync.setup_command("p4")), ["sync", "-n"]);
1236    }
1237
1238    #[test]
1239    fn regular_mode_preview_network_traffic() {
1240        let sync = Sync::new("p4", GlobalOpts::new()).preview_network_traffic();
1241
1242        assert_eq!(args_of(&sync.setup_command("p4")), ["sync", "-N"]);
1243    }
1244
1245    #[test]
1246    fn regular_mode_preview_with_options() {
1247        let sync = Sync::new("p4", GlobalOpts::new())
1248            .quiet_mode(true)
1249            .preview_result()
1250            .limit(3);
1251
1252        assert_eq!(
1253            args_of(&sync.setup_command("p4")),
1254            ["sync", "-q", "-n", "-m", "3"]
1255        );
1256    }
1257
1258    #[test]
1259    fn regular_mode_parallel() {
1260        let sync = Sync::new("p4", GlobalOpts::new()).parallel(ParallelConfig {
1261            threads: 4,
1262            batch_files: Some(8),
1263            batch_size_bytes: None,
1264            min_files: Some(9),
1265            min_size_bytes: None,
1266        });
1267
1268        assert_eq!(
1269            args_of(&sync.setup_command("p4")),
1270            ["sync", "--parallel=threads=4,batch=8,min=9"]
1271        );
1272    }
1273
1274    #[test]
1275    fn regular_mode_stream_spec_auto() {
1276        let sync = Sync::new("p4", GlobalOpts::new())
1277            .stream_spec_version(StreamSpecVersion::MaxInFilelists);
1278
1279        assert_eq!(
1280            args_of(&sync.setup_command("p4")),
1281            ["sync", "--use-stream-change"]
1282        );
1283    }
1284
1285    #[test]
1286    fn regular_mode_stream_spec_current() {
1287        let sync =
1288            Sync::new("p4", GlobalOpts::new()).stream_spec_version(StreamSpecVersion::Current);
1289
1290        assert_eq!(
1291            args_of(&sync.setup_command("p4")),
1292            ["sync", "--use-stream-change=0"]
1293        );
1294    }
1295
1296    #[test]
1297    fn regular_mode_stream_spec_specific() {
1298        let sync = Sync::new("p4", GlobalOpts::new())
1299            .stream_spec_version(StreamSpecVersion::ChangeNumber(123));
1300
1301        assert_eq!(
1302            args_of(&sync.setup_command("p4")),
1303            ["sync", "--use-stream-change=123"]
1304        );
1305    }
1306
1307    #[test]
1308    fn safe_check_mode() {
1309        let sync = Sync::new("p4", GlobalOpts::new()).enable_safe_check();
1310
1311        assert_eq!(args_of(&sync.setup_command("p4")), ["sync", "-s"]);
1312    }
1313
1314    #[test]
1315    fn safe_check_mode_with_options() {
1316        let sync = Sync::new("p4", GlobalOpts::new())
1317            .enable_safe_check()
1318            .quiet_mode(true)
1319            .limit(5);
1320
1321        assert_eq!(
1322            args_of(&sync.setup_command("p4")),
1323            ["sync", "-q", "-s", "-m", "5"]
1324        );
1325    }
1326
1327    #[test]
1328    fn populate_mode() {
1329        let sync = Sync::new("p4", GlobalOpts::new()).populate_client_workspace();
1330
1331        assert_eq!(args_of(&sync.setup_command("p4")), ["sync", "-p"]);
1332    }
1333
1334    #[test]
1335    fn populate_mode_with_options() {
1336        let sync = Sync::new("p4", GlobalOpts::new())
1337            .populate_client_workspace()
1338            .quiet_mode(true)
1339            .limit(5);
1340
1341        assert_eq!(
1342            args_of(&sync.setup_command("p4")),
1343            ["sync", "-q", "-p", "-m", "5"]
1344        );
1345    }
1346
1347    #[test]
1348    fn transition_to_safe_check_from_regular() {
1349        let sync = Sync::new("p4", GlobalOpts::new())
1350            .quiet_mode(true)
1351            .limit(5)
1352            .safe_check();
1353
1354        assert_eq!(
1355            args_of(&sync.setup_command("p4")),
1356            ["sync", "-q", "-s", "-m", "5"]
1357        );
1358    }
1359
1360    #[test]
1361    fn transition_to_populate_from_regular() {
1362        let sync = Sync::new("p4", GlobalOpts::new())
1363            .quiet_mode(true)
1364            .limit(5)
1365            .populate();
1366
1367        assert_eq!(
1368            args_of(&sync.setup_command("p4")),
1369            ["sync", "-q", "-p", "-m", "5"]
1370        );
1371    }
1372
1373    #[test]
1374    fn transition_preserves_preview() {
1375        let sync = Sync::new("p4", GlobalOpts::new())
1376            .preview_result()
1377            .quiet_mode(true)
1378            .safe_check();
1379
1380        assert_eq!(
1381            args_of(&sync.setup_command("p4")),
1382            ["sync", "-q", "-s", "-n"]
1383        );
1384    }
1385
1386    #[test]
1387    fn force_mode_blocks_safe_check() {
1388        // Once in ForceRegularMode, safe_check/populate are unavailable at
1389        // compile time. This test just exercises force mode injection.
1390        let sync = Sync::new("p4", GlobalOpts::new())
1391            .force(true)
1392            .quiet_mode(true);
1393
1394        assert_eq!(args_of(&sync.setup_command("p4")), ["sync", "-q", "-f"]);
1395    }
1396
1397    #[test]
1398    fn all_regular_options_order() {
1399        let sync = Sync::new("p4", GlobalOpts::new())
1400            .verify_edge_replication(true)
1401            .script_list_mode(true)
1402            .suppress_keyword_expansion(true)
1403            .quiet_mode(true)
1404            .force(true)
1405            .metadata_only(true)
1406            .reopen_moved_files(true)
1407            .limit(5)
1408            .parallel(ParallelConfig {
1409                threads: 2,
1410                batch_files: None,
1411                batch_size_bytes: None,
1412                min_files: None,
1413                min_size_bytes: None,
1414            })
1415            .stream_spec_version(StreamSpecVersion::Current);
1416
1417        assert_eq!(
1418            args_of(&sync.setup_command("p4")),
1419            [
1420                "sync",
1421                "-E",
1422                "-L",
1423                "-K",
1424                "-q",
1425                "-f",
1426                "-k",
1427                "-r",
1428                "-m",
1429                "5",
1430                "--parallel=threads=2",
1431                "--use-stream-change=0",
1432            ]
1433        );
1434    }
1435}