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 ParameterizedSpawn for Edit<Unselected> {
225    type Input<'a> = &'a [&'a OsStr];
226    type Output<'a> = Child;
227    type Error = std::io::Error;
228
229    /// Spawns `p4 edit` for the given files as a child process with piped
230    /// standard output and error streams; use the returned [`Child`] handle
231    /// to wait for it or interact with it.
232    ///
233    /// This corresponds to the file form of the command:
234    /// `p4 edit [options] file ...`.
235    fn spawn_with<'a>(&mut self, files: Self::Input<'a>) -> Result<Self::Output<'a>, Self::Error> {
236        self.setup_command(&self.bin)
237            .args(files)
238            .stdout(Stdio::piped())
239            .stderr(Stdio::piped())
240            .spawn()
241    }
242}
243
244impl ParameterizedSpawn for Edit<FileEditMode> {
245    type Input<'a> = &'a [&'a OsStr];
246    type Output<'a> = Child;
247    type Error = std::io::Error;
248
249    /// Spawns `p4 edit` for the given files as a child process with piped
250    /// standard output and error streams; use the returned [`Child`] handle
251    /// to wait for it or interact with it.
252    ///
253    /// This corresponds to the file form of the command:
254    /// `p4 edit [options] file ...`.
255    fn spawn_with<'a>(&mut self, files: Self::Input<'a>) -> Result<Self::Output<'a>, Self::Error> {
256        self.setup_command(&self.bin)
257            .args(files)
258            .stdout(Stdio::piped())
259            .stderr(Stdio::piped())
260            .spawn()
261    }
262}
263
264impl Edit<FileEditMode> {
265    /// # Description
266    ///
267    /// -k
268    ///
269    /// Keep existing workspace files; mark the file as open for edit even if
270    /// the file is not in the client view. Use `p4 edit -k` only in the
271    /// context of reconciling work performed while disconnected from the
272    /// shared versioning service.
273    pub fn get_keep_workspace(&self) -> bool {
274        self.mode.keep_workspace
275    }
276
277    /// # Description
278    ///
279    /// -k
280    ///
281    /// Keep existing workspace files; mark the file as open for edit even if
282    /// the file is not in the client view. Use `p4 edit -k` only in the
283    /// context of reconciling work performed while disconnected from the
284    /// shared versioning service.
285    pub fn set_keep_workspace(&mut self, v: bool) -> &mut Self {
286        self.mode.keep_workspace = v;
287        self
288    }
289
290    /// # Description
291    ///
292    /// -k
293    ///
294    /// Keep existing workspace files; mark the file as open for edit even if
295    /// the file is not in the client view. Use `p4 edit -k` only in the
296    /// context of reconciling work performed while disconnected from the
297    /// shared versioning service.
298    pub fn keep_workspace(mut self, v: bool) -> Self {
299        self.mode.keep_workspace = v;
300        self
301    }
302
303    /// # Description
304    ///
305    /// -n
306    ///
307    /// Preview which files would be opened for edit, without actually changing
308    /// any files or metadata.
309    pub fn get_preview(&self) -> bool {
310        self.mode.preview
311    }
312
313    /// # Description
314    ///
315    /// -n
316    ///
317    /// Preview which files would be opened for edit, without actually changing
318    /// any files or metadata.
319    pub fn set_preview(&mut self, v: bool) -> &mut Self {
320        self.mode.preview = v;
321        self
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 preview(mut self, v: bool) -> Self {
331        self.mode.preview = v;
332        self
333    }
334
335    /// # Description
336    ///
337    /// `--remote=remote`
338    ///
339    /// Opens the file for edit in your personal server, and additionally — if
340    /// the file is of type `+l` — takes a global exclusive lock on the file in
341    /// the shared server from which you cloned the file.
342    pub fn get_remote_server(&self) -> Option<&String> {
343        self.mode.remote_server.as_ref()
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 set_remote_server(&mut self, v: impl Into<String>) -> &mut Self {
354        self.mode.remote_server = Some(v.into());
355        self
356    }
357
358    /// # Description
359    ///
360    /// `--remote=remote`
361    ///
362    /// Opens the file for edit in your personal server, and additionally — if
363    /// the file is of type `+l` — takes a global exclusive lock on the file in
364    /// the shared server from which you cloned the file.
365    pub fn remote_server(mut self, v: impl Into<String>) -> Self {
366        self.mode.remote_server = Some(v.into());
367        self
368    }
369
370    /// # Description
371    ///
372    /// `-t type`
373    ///
374    /// Stores the new file revision as the specified type, overriding the file
375    /// type of the previous revision of the same file. To forcibly re-detect a
376    /// file's filetype upon editing a file, use `p4 edit -t auto`. This assigns
377    /// a file type as if the file were being newly added.
378    ///
379    #[cfg_attr(feature = "lt2024_1", doc = "See File types for a list of file types.")]
380    #[cfg_attr(
381        not(feature = "lt2024_1"),
382        doc = "See File types as well as the lbr.autocompress configurable."
383    )]
384    pub fn get_filetype(&self) -> Option<&String> {
385        self.mode.filetype.as_ref()
386    }
387
388    /// # Description
389    ///
390    /// `-t type`
391    ///
392    /// Stores the new file revision as the specified type, overriding the file
393    /// type of the previous revision of the same file. To forcibly re-detect a
394    /// file's filetype upon editing a file, use `p4 edit -t auto`. This assigns
395    /// a file type as if the file were being newly added.
396    ///
397    #[cfg_attr(feature = "lt2024_1", doc = "See File types for a list of file types.")]
398    #[cfg_attr(
399        not(feature = "lt2024_1"),
400        doc = "See File types as well as the lbr.autocompress configurable."
401    )]
402    pub fn set_filetype(&mut self, v: impl Into<String>) -> &mut Self {
403        self.mode.filetype = Some(v.into());
404        self
405    }
406
407    /// # Description
408    ///
409    /// `-t type`
410    ///
411    /// Stores the new file revision as the specified type, overriding the file
412    /// type of the previous revision of the same file. To forcibly re-detect a
413    /// file's filetype upon editing a file, use `p4 edit -t auto`. This assigns
414    /// a file type as if the file were being newly added.
415    ///
416    #[cfg_attr(feature = "lt2024_1", doc = "See File types for a list of file types.")]
417    #[cfg_attr(
418        not(feature = "lt2024_1"),
419        doc = "See File types as well as the lbr.autocompress configurable."
420    )]
421    pub fn filetype(mut self, v: impl Into<String>) -> Self {
422        self.mode.filetype = Some(v.into());
423        self
424    }
425}
426
427#[cfg(not(feature = "lt2019_1"))]
428impl ParameterizedSpawn for Edit<StreamSpecEditMode> {
429    type Input<'a> = ();
430    type Output<'a> = Child;
431    type Error = std::io::Error;
432
433    /// Spawns `p4 edit -So` to open the current stream spec for edit as a
434    /// child process with piped standard output and error streams; use the
435    /// returned [`Child`] handle to wait for it or interact with it.
436    ///
437    /// This corresponds to the stream spec form of the command:
438    /// `p4 edit -So [-c changelist]`, which takes no file arguments.
439    fn spawn_with<'a>(&mut self, (): Self::Input<'a>) -> Result<Self::Output<'a>, Self::Error> {
440        self.setup_command(&self.bin)
441            .stdout(Stdio::piped())
442            .stderr(Stdio::piped())
443            .spawn()
444    }
445}
446
447impl<M: ExclusiveOption> Edit<M> {
448    /// # Description
449    ///
450    /// g-opts
451    ///
452    #[cfg_attr(
453        feature = "lt2014_2",
454        doc = "See the [Global Options](GlobalOpts) section."
455    )]
456    #[cfg_attr(
457        all(feature = "lt2015_1", not(feature = "lt2014_2")),
458        doc = "See the [“Global Options”](GlobalOpts) section."
459    )]
460    #[cfg_attr(
461        all(feature = "lt2017_1", not(feature = "lt2015_1")),
462        doc = "See [“Global Options”](GlobalOpts)."
463    )]
464    #[cfg_attr(
465        all(feature = "lt2018_2", not(feature = "lt2017_1")),
466        doc = "See [Global Options](GlobalOpts)."
467    )]
468    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
469    pub fn get_global_opts(&self) -> &GlobalOpts {
470        &self.global_opts
471    }
472
473    /// # Description
474    ///
475    /// g-opts
476    ///
477    #[cfg_attr(
478        feature = "lt2014_2",
479        doc = "See the [Global Options](GlobalOpts) section."
480    )]
481    #[cfg_attr(
482        all(feature = "lt2015_1", not(feature = "lt2014_2")),
483        doc = "See the [“Global Options”](GlobalOpts) section."
484    )]
485    #[cfg_attr(
486        all(feature = "lt2017_1", not(feature = "lt2015_1")),
487        doc = "See [“Global Options”](GlobalOpts)."
488    )]
489    #[cfg_attr(
490        all(feature = "lt2018_2", not(feature = "lt2017_1")),
491        doc = "See [Global Options](GlobalOpts)."
492    )]
493    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
494    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
495        self.global_opts = v;
496        self
497    }
498
499    /// # Description
500    ///
501    /// g-opts
502    ///
503    #[cfg_attr(
504        feature = "lt2014_2",
505        doc = "See the [Global Options](GlobalOpts) section."
506    )]
507    #[cfg_attr(
508        all(feature = "lt2015_1", not(feature = "lt2014_2")),
509        doc = "See the [“Global Options”](GlobalOpts) section."
510    )]
511    #[cfg_attr(
512        all(feature = "lt2017_1", not(feature = "lt2015_1")),
513        doc = "See [“Global Options”](GlobalOpts)."
514    )]
515    #[cfg_attr(
516        all(feature = "lt2018_2", not(feature = "lt2017_1")),
517        doc = "See [Global Options](GlobalOpts)."
518    )]
519    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
520    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
521        self.global_opts = v;
522        self
523    }
524
525    /// # Description
526    ///
527    /// `-c changelist`
528    ///
529    /// Opens the files for edit within the specified changelist. If this
530    /// option is not provided, the files are linked to the default changelist.
531    pub fn get_change_list(&self) -> Option<&String> {
532        self.change_list.as_ref()
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 set_change_list(&mut self, v: impl Into<String>) -> &mut Self {
542        self.change_list = Some(v.into());
543        self
544    }
545
546    /// # Description
547    ///
548    /// `-c changelist`
549    ///
550    /// Opens the files for edit within the specified changelist. If this
551    /// option is not provided, the files are linked to the default changelist.
552    pub fn change_list(mut self, v: impl Into<String>) -> Self {
553        self.change_list = Some(v.into());
554        self
555    }
556}
557
558impl<M: ExclusiveOption> SubCommand for Edit<M> {
559    fn name(&self) -> &str {
560        "edit"
561    }
562
563    fn inject_local_args(&self, command: &mut Command) {
564        if let Some(change_list) = &self.change_list {
565            command.arg("-c").arg(change_list);
566        }
567
568        self.mode.inject_args(command);
569    }
570
571    fn global_opts(&self) -> Option<&GlobalOpts> {
572        Some(&self.global_opts)
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579    use crate::cmd::args_of;
580
581    #[test]
582    fn without_options() {
583        let edit = Edit::new("p4", GlobalOpts::default());
584        let mut cmd = edit.setup_command("p4");
585        cmd.arg("//depot/file.txt");
586        assert_eq!(args_of(&cmd), vec!["edit", "//depot/file.txt"]);
587    }
588
589    #[test]
590    fn change_list() {
591        let edit = Edit::new("p4", GlobalOpts::default()).change_list("14");
592        let mut cmd = edit.setup_command("p4");
593        cmd.arg("//depot/file.txt");
594        assert_eq!(args_of(&cmd), vec!["edit", "-c", "14", "//depot/file.txt"]);
595    }
596
597    #[test]
598    fn keep_workspace_and_preview() {
599        let edit = Edit::new("p4", GlobalOpts::default())
600            .keep_workspace(true)
601            .preview(true);
602        let mut cmd = edit.setup_command("p4");
603        cmd.arg("//depot/file.txt");
604        assert_eq!(args_of(&cmd), vec!["edit", "-k", "-n", "//depot/file.txt"]);
605    }
606
607    #[test]
608    fn remote_option_uses_equals_sign() {
609        let edit = Edit::new("p4", GlobalOpts::default()).remote_server("origin");
610        let mut cmd = edit.setup_command("p4");
611        cmd.arg("//depot/file.txt");
612        assert_eq!(
613            args_of(&cmd),
614            vec!["edit", "--remote=origin", "//depot/file.txt"]
615        );
616    }
617
618    #[test]
619    fn filetype() {
620        let edit = Edit::new("p4", GlobalOpts::default()).filetype("text+k");
621        let mut cmd = edit.setup_command("p4");
622        cmd.arg("//depot/file.txt");
623        assert_eq!(
624            args_of(&cmd),
625            vec!["edit", "-t", "text+k", "//depot/file.txt"]
626        );
627    }
628
629    #[test]
630    fn file_mode_transition_preserves_change_list() {
631        let edit = Edit::new("p4", GlobalOpts::default())
632            .change_list("14")
633            .keep_workspace(true);
634        let mut cmd = edit.setup_command("p4");
635        cmd.arg("//depot/file.txt");
636        assert_eq!(
637            args_of(&cmd),
638            vec!["edit", "-c", "14", "-k", "//depot/file.txt"]
639        );
640    }
641
642    #[test]
643    fn file_mode_accessors() {
644        let mut edit = Edit::new("p4", GlobalOpts::default()).keep_workspace(true);
645        edit.set_remote_server("origin").set_preview(true);
646        assert!(edit.get_keep_workspace());
647        assert!(edit.get_preview());
648        assert_eq!(edit.get_remote_server(), Some(&"origin".to_string()));
649        assert_eq!(edit.get_filetype(), None);
650
651        let mut cmd = edit.setup_command("p4");
652        cmd.arg("//depot/file.txt");
653        assert_eq!(
654            args_of(&cmd),
655            vec!["edit", "-k", "-n", "--remote=origin", "//depot/file.txt"]
656        );
657    }
658
659    #[cfg(not(feature = "lt2019_1"))]
660    #[test]
661    fn stream_spec() {
662        let edit = Edit::new("p4", GlobalOpts::default()).edit_stream_spec();
663        let cmd = edit.setup_command("p4");
664        assert_eq!(args_of(&cmd), vec!["edit", "-So"]);
665    }
666
667    #[cfg(not(feature = "lt2019_1"))]
668    #[test]
669    fn stream_spec_with_change_list() {
670        let edit = Edit::new("p4", GlobalOpts::default())
671            .change_list("14")
672            .edit_stream_spec();
673        let cmd = edit.setup_command("p4");
674        assert_eq!(args_of(&cmd), vec!["edit", "-c", "14", "-So"]);
675    }
676
677    #[cfg(not(feature = "lt2019_1"))]
678    #[test]
679    fn stream_spec_change_list_after_transition() {
680        let edit = Edit::new("p4", GlobalOpts::default())
681            .edit_stream_spec()
682            .change_list("14");
683        let cmd = edit.setup_command("p4");
684        assert_eq!(args_of(&cmd), vec!["edit", "-c", "14", "-So"]);
685    }
686
687    #[test]
688    fn all_options_order() {
689        let edit = Edit::new("p4", GlobalOpts::default())
690            .change_list("14")
691            .keep_workspace(true)
692            .preview(true)
693            .remote_server("origin")
694            .filetype("binary");
695        let mut cmd = edit.setup_command("p4");
696        cmd.arg("//depot/file.txt");
697        assert_eq!(
698            args_of(&cmd),
699            vec![
700                "edit",
701                "-c",
702                "14",
703                "-k",
704                "-n",
705                "--remote=origin",
706                "-t",
707                "binary",
708                "//depot/file.txt"
709            ]
710        );
711    }
712
713    #[test]
714    fn set_style_with_global_opts() {
715        let mut edit = Edit::new("p4", GlobalOpts::default());
716        edit.set_change_list("14");
717        let mut edit = edit.filetype("binary");
718        edit.set_preview(true);
719        let mut cmd = edit.setup_command("p4");
720        cmd.arg("//depot/file.txt");
721        assert_eq!(
722            args_of(&cmd),
723            vec!["edit", "-c", "14", "-n", "-t", "binary", "//depot/file.txt"]
724        );
725    }
726}