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