Skip to main content

perforce_cli/cmd/
edit.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/// File edit mode of `p4 edit`: the file form carrying `-k`, `-n`,
13/// `--remote`, and `-t`.
14///
15/// Entered with [`Edit::keep_workspace`], [`Edit::preview`],
16/// [`Edit::remote_server`], or [`Edit::filetype`].
17#[derive(Debug, Clone, Default)]
18pub struct FileEditMode {
19    keep_workspace: bool,
20
21    preview: bool,
22
23    remote_server: Option<String>,
24
25    filetype: Option<String>,
26}
27
28impl ExclusiveOption for FileEditMode {
29    fn inject_args(&self, command: &mut Command) {
30        if self.keep_workspace {
31            command.arg("-k");
32        }
33
34        if self.preview {
35            command.arg("-n");
36        }
37
38        if let Some(remote) = &self.remote_server {
39            command.arg(format!("--remote={remote}"));
40        }
41
42        if let Some(filetype) = &self.filetype {
43            command.arg("-t").arg(filetype);
44        }
45    }
46}
47
48/// Stream spec edit mode of `p4 edit` (`-So`): opens the current stream
49/// spec for edit.
50///
51/// No list of files is allowed, and `-So` may only be combined with
52/// `-c changelist`. Entered with [`Edit::edit_stream_spec`].
53#[cfg(not(feature = "lt2019_1"))]
54#[derive(Debug, Clone, Copy, Default)]
55pub struct StreamSpecEditMode;
56
57#[cfg(not(feature = "lt2019_1"))]
58impl ExclusiveOption for StreamSpecEditMode {
59    fn inject_args(&self, command: &mut Command) {
60        command.arg("-So");
61    }
62}
63
64#[cfg_attr(
65    feature = "lt2019_1",
66    doc = "`p4 [g-opts] edit [-c changelist] [-k -n] [-t type] [--remote=remote] file ...`"
67)]
68#[cfg_attr(
69    not(feature = "lt2019_1"),
70    doc = "`p4 [g-opts] edit [-c changelist] [-k -n] [-t type] [--remote=remote] file ...`\n\n\
71           `p4 [g-opts] edit -So [-c changelist]`"
72)]
73///
74/// Opens files in a client workspace for edit, or open the current stream
75/// spec.
76///
77/// The `M` type parameter tracks the command form at compile time. The
78/// default [`Unselected`] state opens plain files with no edit-mode options;
79/// [`Self::keep_workspace`], [`Self::preview`], [`Self::remote_server`], and
80/// [`Self::filetype`] transition to the [`FileEditMode`] state
81#[cfg_attr(feature = "lt2019_1", doc = ".")]
82#[cfg_attr(
83    not(feature = "lt2019_1"),
84    doc = ", while [`Self::edit_stream_spec`] transitions to the [`StreamSpecEditMode`] state."
85)]
86#[derive(Debug, Clone, Default)]
87pub struct Edit<M = Unselected> {
88    bin: PathBuf,
89
90    global_opts: GlobalOpts,
91
92    change_list: Option<String>,
93
94    mode: M,
95}
96
97impl Edit<Unselected> {
98    /// Creates a new `p4 edit` command.
99    ///
100    /// `bin` is the path to the Perforce command-line executable.
101    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
102        Self {
103            bin: bin.into(),
104            global_opts,
105            change_list: None,
106            mode: Unselected,
107        }
108    }
109
110    /// # Description
111    ///
112    /// -k
113    ///
114    /// Keep existing workspace files; mark the file as open for edit even if
115    /// the file is not in the client view. Use `p4 edit -k` only in the
116    /// context of reconciling work performed while disconnected from the
117    /// shared versioning service.
118    ///
119    /// Transitions this command to the [`FileEditMode`] state.
120    pub fn keep_workspace(self, v: bool) -> Edit<FileEditMode> {
121        Edit {
122            bin: self.bin,
123            global_opts: self.global_opts,
124            change_list: self.change_list,
125            mode: FileEditMode {
126                keep_workspace: v,
127                preview: false,
128                remote_server: None,
129                filetype: None,
130            },
131        }
132    }
133
134    /// # Description
135    ///
136    /// -n
137    ///
138    /// Preview which files would be opened for edit, without actually changing
139    /// any files or metadata.
140    ///
141    /// Transitions this command to the [`FileEditMode`] state.
142    pub fn preview(self, v: bool) -> Edit<FileEditMode> {
143        Edit {
144            bin: self.bin,
145            global_opts: self.global_opts,
146            change_list: self.change_list,
147            mode: FileEditMode {
148                keep_workspace: false,
149                preview: v,
150                remote_server: None,
151                filetype: None,
152            },
153        }
154    }
155
156    /// # Description
157    ///
158    /// `--remote=remote`
159    ///
160    /// Opens the file for edit in your personal server, and additionally — if
161    /// the file is of type `+l` — takes a global exclusive lock on the file in
162    /// the shared server from which you cloned the file.
163    ///
164    /// Transitions this command to the [`FileEditMode`] state.
165    pub fn remote_server(self, v: impl Into<String>) -> Edit<FileEditMode> {
166        Edit {
167            bin: self.bin,
168            global_opts: self.global_opts,
169            change_list: self.change_list,
170            mode: FileEditMode {
171                keep_workspace: false,
172                preview: false,
173                remote_server: Some(v.into()),
174                filetype: None,
175            },
176        }
177    }
178
179    /// # Description
180    ///
181    /// `-t type`
182    ///
183    /// Stores the new file revision as the specified type, overriding the file
184    /// type of the previous revision of the same file. To forcibly re-detect a
185    /// file's filetype upon editing a file, use `p4 edit -t auto`. This assigns
186    /// a file type as if the file were being newly added.
187    ///
188    #[cfg_attr(feature = "lt2024_1", doc = "See File types for a list of file types.")]
189    #[cfg_attr(
190        not(feature = "lt2024_1"),
191        doc = "See File types as well as the lbr.autocompress configurable."
192    )]
193    ///
194    /// Transitions this command to the [`FileEditMode`] state.
195    pub fn filetype(self, v: impl Into<String>) -> Edit<FileEditMode> {
196        Edit {
197            bin: self.bin,
198            global_opts: self.global_opts,
199            change_list: self.change_list,
200            mode: FileEditMode {
201                keep_workspace: false,
202                preview: false,
203                remote_server: None,
204                filetype: Some(v.into()),
205            },
206        }
207    }
208
209    /// # Description
210    ///
211    /// `-So`
212    ///
213    /// Can be used with `-c changelist` to open the client's stream spec for
214    /// edit. No list of files is allowed. `p4 edit -So` is an alias for
215    /// `p4 stream edit` (see also `p4 help streamcmds`).
216    ///
217    /// Transitions this command to the [`StreamSpecEditMode`] state.
218    #[cfg(not(feature = "lt2019_1"))]
219    pub fn edit_stream_spec(self) -> Edit<StreamSpecEditMode> {
220        Edit {
221            bin: self.bin,
222            global_opts: self.global_opts,
223            change_list: self.change_list,
224            mode: StreamSpecEditMode,
225        }
226    }
227}
228
229impl<S, I> ParameterizedSpawn<(S,)> for Edit<Unselected>
230where
231    S: IntoIterator<Item = I>,
232    I: AsRef<OsStr>,
233{
234    type Output = Child;
235    type Error = std::io::Error;
236
237    /// Spawns `p4 edit` for the given files as a child process with piped
238    /// standard output and error streams; use the returned [`Child`] handle
239    /// to wait for it or interact with it.
240    ///
241    /// This corresponds to the file form of the command:
242    /// `p4 edit [options] file ...`.
243    fn spawn_with(&mut self, (files,): (S,)) -> Result<Self::Output, Self::Error> {
244        self.setup_command(&self.bin)
245            .args(files)
246            .stdout(Stdio::piped())
247            .stderr(Stdio::piped())
248            .spawn()
249    }
250}
251
252impl<S, I> ParameterizedSpawn<(S,)> for Edit<FileEditMode>
253where
254    S: IntoIterator<Item = I>,
255    I: AsRef<OsStr>,
256{
257    type Output = Child;
258    type Error = std::io::Error;
259
260    /// Spawns `p4 edit` for the given files as a child process with piped
261    /// standard output and error streams; use the returned [`Child`] handle
262    /// to wait for it or interact with it.
263    ///
264    /// This corresponds to the file form of the command:
265    /// `p4 edit [options] file ...`.
266    fn spawn_with(&mut self, (files,): (S,)) -> Result<Self::Output, Self::Error> {
267        self.setup_command(&self.bin)
268            .args(files)
269            .stdout(Stdio::piped())
270            .stderr(Stdio::piped())
271            .spawn()
272    }
273}
274
275impl Edit<FileEditMode> {
276    /// # Description
277    ///
278    /// -k
279    ///
280    /// Keep existing workspace files; mark the file as open for edit even if
281    /// the file is not in the client view. Use `p4 edit -k` only in the
282    /// context of reconciling work performed while disconnected from the
283    /// shared versioning service.
284    pub fn get_keep_workspace(&self) -> bool {
285        self.mode.keep_workspace
286    }
287
288    /// # Description
289    ///
290    /// -k
291    ///
292    /// Keep existing workspace files; mark the file as open for edit even if
293    /// the file is not in the client view. Use `p4 edit -k` only in the
294    /// context of reconciling work performed while disconnected from the
295    /// shared versioning service.
296    pub fn set_keep_workspace(&mut self, v: bool) -> &mut Self {
297        self.mode.keep_workspace = v;
298        self
299    }
300
301    /// # Description
302    ///
303    /// -k
304    ///
305    /// Keep existing workspace files; mark the file as open for edit even if
306    /// the file is not in the client view. Use `p4 edit -k` only in the
307    /// context of reconciling work performed while disconnected from the
308    /// shared versioning service.
309    pub fn keep_workspace(mut self, v: bool) -> Self {
310        self.mode.keep_workspace = v;
311        self
312    }
313
314    /// # Description
315    ///
316    /// -n
317    ///
318    /// Preview which files would be opened for edit, without actually changing
319    /// any files or metadata.
320    pub fn get_preview(&self) -> bool {
321        self.mode.preview
322    }
323
324    /// # Description
325    ///
326    /// -n
327    ///
328    /// Preview which files would be opened for edit, without actually changing
329    /// any files or metadata.
330    pub fn set_preview(&mut self, v: bool) -> &mut Self {
331        self.mode.preview = v;
332        self
333    }
334
335    /// # Description
336    ///
337    /// -n
338    ///
339    /// Preview which files would be opened for edit, without actually changing
340    /// any files or metadata.
341    pub fn preview(mut self, v: bool) -> Self {
342        self.mode.preview = v;
343        self
344    }
345
346    /// # Description
347    ///
348    /// `--remote=remote`
349    ///
350    /// Opens the file for edit in your personal server, and additionally — if
351    /// the file is of type `+l` — takes a global exclusive lock on the file in
352    /// the shared server from which you cloned the file.
353    pub fn get_remote_server(&self) -> Option<&String> {
354        self.mode.remote_server.as_ref()
355    }
356
357    /// # Description
358    ///
359    /// `--remote=remote`
360    ///
361    /// Opens the file for edit in your personal server, and additionally — if
362    /// the file is of type `+l` — takes a global exclusive lock on the file in
363    /// the shared server from which you cloned the file.
364    pub fn set_remote_server(&mut self, v: impl Into<String>) -> &mut Self {
365        self.mode.remote_server = Some(v.into());
366        self
367    }
368
369    /// # Description
370    ///
371    /// `--remote=remote`
372    ///
373    /// Opens the file for edit in your personal server, and additionally — if
374    /// the file is of type `+l` — takes a global exclusive lock on the file in
375    /// the shared server from which you cloned the file.
376    pub fn remote_server(mut self, v: impl Into<String>) -> Self {
377        self.mode.remote_server = Some(v.into());
378        self
379    }
380
381    /// # Description
382    ///
383    /// `-t type`
384    ///
385    /// Stores the new file revision as the specified type, overriding the file
386    /// type of the previous revision of the same file. To forcibly re-detect a
387    /// file's filetype upon editing a file, use `p4 edit -t auto`. This assigns
388    /// a file type as if the file were being newly added.
389    ///
390    #[cfg_attr(feature = "lt2024_1", doc = "See File types for a list of file types.")]
391    #[cfg_attr(
392        not(feature = "lt2024_1"),
393        doc = "See File types as well as the lbr.autocompress configurable."
394    )]
395    pub fn get_filetype(&self) -> Option<&String> {
396        self.mode.filetype.as_ref()
397    }
398
399    /// # Description
400    ///
401    /// `-t type`
402    ///
403    /// Stores the new file revision as the specified type, overriding the file
404    /// type of the previous revision of the same file. To forcibly re-detect a
405    /// file's filetype upon editing a file, use `p4 edit -t auto`. This assigns
406    /// a file type as if the file were being newly added.
407    ///
408    #[cfg_attr(feature = "lt2024_1", doc = "See File types for a list of file types.")]
409    #[cfg_attr(
410        not(feature = "lt2024_1"),
411        doc = "See File types as well as the lbr.autocompress configurable."
412    )]
413    pub fn set_filetype(&mut self, v: impl Into<String>) -> &mut Self {
414        self.mode.filetype = Some(v.into());
415        self
416    }
417
418    /// # Description
419    ///
420    /// `-t type`
421    ///
422    /// Stores the new file revision as the specified type, overriding the file
423    /// type of the previous revision of the same file. To forcibly re-detect a
424    /// file's filetype upon editing a file, use `p4 edit -t auto`. This assigns
425    /// a file type as if the file were being newly added.
426    ///
427    #[cfg_attr(feature = "lt2024_1", doc = "See File types for a list of file types.")]
428    #[cfg_attr(
429        not(feature = "lt2024_1"),
430        doc = "See File types as well as the lbr.autocompress configurable."
431    )]
432    pub fn filetype(mut self, v: impl Into<String>) -> Self {
433        self.mode.filetype = Some(v.into());
434        self
435    }
436}
437
438#[cfg(not(feature = "lt2019_1"))]
439impl ParameterizedSpawn<()> for Edit<StreamSpecEditMode> {
440    type Output = Child;
441    type Error = std::io::Error;
442
443    /// Spawns `p4 edit -So` to open the current stream spec for edit as a
444    /// child process with piped standard output and error streams; use the
445    /// returned [`Child`] handle to wait for it or interact with it.
446    ///
447    /// This corresponds to the stream spec form of the command:
448    /// `p4 edit -So [-c changelist]`, which takes no file arguments.
449    fn spawn_with(&mut self, (): ()) -> Result<Self::Output, Self::Error> {
450        self.setup_command(&self.bin)
451            .stdout(Stdio::piped())
452            .stderr(Stdio::piped())
453            .spawn()
454    }
455}
456
457impl<M: ExclusiveOption> Edit<M> {
458    /// # Description
459    ///
460    /// g-opts
461    ///
462    #[cfg_attr(
463        feature = "lt2014_2",
464        doc = "See the [Global Options](GlobalOpts) section."
465    )]
466    #[cfg_attr(
467        all(feature = "lt2015_1", not(feature = "lt2014_2")),
468        doc = "See the [“Global Options”](GlobalOpts) section."
469    )]
470    #[cfg_attr(
471        all(feature = "lt2017_1", not(feature = "lt2015_1")),
472        doc = "See [“Global Options”](GlobalOpts)."
473    )]
474    #[cfg_attr(
475        all(feature = "lt2018_2", not(feature = "lt2017_1")),
476        doc = "See [Global Options](GlobalOpts)."
477    )]
478    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
479    pub fn get_global_opts(&self) -> &GlobalOpts {
480        &self.global_opts
481    }
482
483    /// # Description
484    ///
485    /// g-opts
486    ///
487    #[cfg_attr(
488        feature = "lt2014_2",
489        doc = "See the [Global Options](GlobalOpts) section."
490    )]
491    #[cfg_attr(
492        all(feature = "lt2015_1", not(feature = "lt2014_2")),
493        doc = "See the [“Global Options”](GlobalOpts) section."
494    )]
495    #[cfg_attr(
496        all(feature = "lt2017_1", not(feature = "lt2015_1")),
497        doc = "See [“Global Options”](GlobalOpts)."
498    )]
499    #[cfg_attr(
500        all(feature = "lt2018_2", not(feature = "lt2017_1")),
501        doc = "See [Global Options](GlobalOpts)."
502    )]
503    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
504    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
505        self.global_opts = v;
506        self
507    }
508
509    /// # Description
510    ///
511    /// g-opts
512    ///
513    #[cfg_attr(
514        feature = "lt2014_2",
515        doc = "See the [Global Options](GlobalOpts) section."
516    )]
517    #[cfg_attr(
518        all(feature = "lt2015_1", not(feature = "lt2014_2")),
519        doc = "See the [“Global Options”](GlobalOpts) section."
520    )]
521    #[cfg_attr(
522        all(feature = "lt2017_1", not(feature = "lt2015_1")),
523        doc = "See [“Global Options”](GlobalOpts)."
524    )]
525    #[cfg_attr(
526        all(feature = "lt2018_2", not(feature = "lt2017_1")),
527        doc = "See [Global Options](GlobalOpts)."
528    )]
529    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
530    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
531        self.global_opts = v;
532        self
533    }
534
535    /// # Description
536    ///
537    /// `-c changelist`
538    ///
539    /// Opens the files for edit within the specified changelist. If this
540    /// option is not provided, the files are linked to the default changelist.
541    pub fn get_change_list(&self) -> Option<&String> {
542        self.change_list.as_ref()
543    }
544
545    /// # Description
546    ///
547    /// `-c changelist`
548    ///
549    /// Opens the files for edit within the specified changelist. If this
550    /// option is not provided, the files are linked to the default changelist.
551    pub fn set_change_list(&mut self, v: impl Into<String>) -> &mut Self {
552        self.change_list = Some(v.into());
553        self
554    }
555
556    /// # Description
557    ///
558    /// `-c changelist`
559    ///
560    /// Opens the files for edit within the specified changelist. If this
561    /// option is not provided, the files are linked to the default changelist.
562    pub fn change_list(mut self, v: impl Into<String>) -> Self {
563        self.change_list = Some(v.into());
564        self
565    }
566}
567
568impl<M: ExclusiveOption> SubCommand for Edit<M> {
569    fn name(&self) -> &str {
570        "edit"
571    }
572
573    fn inject_local_args(&self, command: &mut Command) {
574        if let Some(change_list) = &self.change_list {
575            command.arg("-c").arg(change_list);
576        }
577
578        self.mode.inject_args(command);
579    }
580
581    fn global_opts(&self) -> Option<&GlobalOpts> {
582        Some(&self.global_opts)
583    }
584}
585
586#[cfg(test)]
587mod tests {
588    use super::*;
589    use crate::cmd::args_of;
590
591    #[test]
592    fn without_options() {
593        let edit = Edit::new("p4", GlobalOpts::default());
594        let mut cmd = edit.setup_command("p4");
595        cmd.arg("//depot/file.txt");
596        assert_eq!(args_of(&cmd), vec!["edit", "//depot/file.txt"]);
597    }
598
599    #[test]
600    fn change_list() {
601        let edit = Edit::new("p4", GlobalOpts::default()).change_list("14");
602        let mut cmd = edit.setup_command("p4");
603        cmd.arg("//depot/file.txt");
604        assert_eq!(args_of(&cmd), vec!["edit", "-c", "14", "//depot/file.txt"]);
605    }
606
607    #[test]
608    fn keep_workspace_and_preview() {
609        let edit = Edit::new("p4", GlobalOpts::default())
610            .keep_workspace(true)
611            .preview(true);
612        let mut cmd = edit.setup_command("p4");
613        cmd.arg("//depot/file.txt");
614        assert_eq!(args_of(&cmd), vec!["edit", "-k", "-n", "//depot/file.txt"]);
615    }
616
617    #[test]
618    fn remote_option_uses_equals_sign() {
619        let edit = Edit::new("p4", GlobalOpts::default()).remote_server("origin");
620        let mut cmd = edit.setup_command("p4");
621        cmd.arg("//depot/file.txt");
622        assert_eq!(
623            args_of(&cmd),
624            vec!["edit", "--remote=origin", "//depot/file.txt"]
625        );
626    }
627
628    #[test]
629    fn filetype() {
630        let edit = Edit::new("p4", GlobalOpts::default()).filetype("text+k");
631        let mut cmd = edit.setup_command("p4");
632        cmd.arg("//depot/file.txt");
633        assert_eq!(
634            args_of(&cmd),
635            vec!["edit", "-t", "text+k", "//depot/file.txt"]
636        );
637    }
638
639    #[test]
640    fn file_mode_transition_preserves_change_list() {
641        let edit = Edit::new("p4", GlobalOpts::default())
642            .change_list("14")
643            .keep_workspace(true);
644        let mut cmd = edit.setup_command("p4");
645        cmd.arg("//depot/file.txt");
646        assert_eq!(
647            args_of(&cmd),
648            vec!["edit", "-c", "14", "-k", "//depot/file.txt"]
649        );
650    }
651
652    #[test]
653    fn file_mode_accessors() {
654        let mut edit = Edit::new("p4", GlobalOpts::default()).keep_workspace(true);
655        edit.set_remote_server("origin").set_preview(true);
656        assert!(edit.get_keep_workspace());
657        assert!(edit.get_preview());
658        assert_eq!(edit.get_remote_server(), Some(&"origin".to_string()));
659        assert_eq!(edit.get_filetype(), None);
660
661        let mut cmd = edit.setup_command("p4");
662        cmd.arg("//depot/file.txt");
663        assert_eq!(
664            args_of(&cmd),
665            vec!["edit", "-k", "-n", "--remote=origin", "//depot/file.txt"]
666        );
667    }
668
669    #[cfg(not(feature = "lt2019_1"))]
670    #[test]
671    fn stream_spec() {
672        let edit = Edit::new("p4", GlobalOpts::default()).edit_stream_spec();
673        let cmd = edit.setup_command("p4");
674        assert_eq!(args_of(&cmd), vec!["edit", "-So"]);
675    }
676
677    #[cfg(not(feature = "lt2019_1"))]
678    #[test]
679    fn stream_spec_with_change_list() {
680        let edit = Edit::new("p4", GlobalOpts::default())
681            .change_list("14")
682            .edit_stream_spec();
683        let cmd = edit.setup_command("p4");
684        assert_eq!(args_of(&cmd), vec!["edit", "-c", "14", "-So"]);
685    }
686
687    #[cfg(not(feature = "lt2019_1"))]
688    #[test]
689    fn stream_spec_change_list_after_transition() {
690        let edit = Edit::new("p4", GlobalOpts::default())
691            .edit_stream_spec()
692            .change_list("14");
693        let cmd = edit.setup_command("p4");
694        assert_eq!(args_of(&cmd), vec!["edit", "-c", "14", "-So"]);
695    }
696
697    #[test]
698    fn all_options_order() {
699        let edit = Edit::new("p4", GlobalOpts::default())
700            .change_list("14")
701            .keep_workspace(true)
702            .preview(true)
703            .remote_server("origin")
704            .filetype("binary");
705        let mut cmd = edit.setup_command("p4");
706        cmd.arg("//depot/file.txt");
707        assert_eq!(
708            args_of(&cmd),
709            vec![
710                "edit",
711                "-c",
712                "14",
713                "-k",
714                "-n",
715                "--remote=origin",
716                "-t",
717                "binary",
718                "//depot/file.txt"
719            ]
720        );
721    }
722
723    #[test]
724    fn set_style_with_global_opts() {
725        let mut edit = Edit::new("p4", GlobalOpts::default());
726        edit.set_change_list("14");
727        let mut edit = edit.filetype("binary");
728        edit.set_preview(true);
729        let mut cmd = edit.setup_command("p4");
730        cmd.arg("//depot/file.txt");
731        assert_eq!(
732            args_of(&cmd),
733            vec!["edit", "-c", "14", "-n", "-t", "binary", "//depot/file.txt"]
734        );
735    }
736}