Skip to main content

perforce_cli/cmd/
filelog.rs

1use std::{
2    ffi::OsStr,
3    path::{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/// Full description output of `p4 filelog` (`-l`): list long output, with
13/// the full text of each changelist description.
14///
15/// Entered with [`FileLog::full_description`].
16#[derive(Debug, Clone, Copy, Default)]
17pub struct FullDescription;
18
19impl ExclusiveOption for FullDescription {
20    fn inject_args(&self, command: &mut Command) {
21        command.arg("-l");
22    }
23}
24
25/// Truncated description output of `p4 filelog` (`-L`): list long output,
26/// with the full text of each changelist description truncated at 250
27/// characters.
28///
29/// Entered with [`FileLog::truncated_description`].
30#[derive(Debug, Clone, Copy, Default)]
31pub struct TruncatedDescription;
32
33impl ExclusiveOption for TruncatedDescription {
34    fn inject_args(&self, command: &mut Command) {
35        command.arg("-L");
36    }
37}
38
39/// Content history mode of `p4 filelog` (`-h`): display file content
40/// history instead of file name history.
41///
42/// This is the only state in which the `-p` option
43/// ([`skip_promoted_tasks`](FileLog::get_skip_promoted_tasks)) is
44/// meaningful. Entered with [`FileLog::content_history`].
45#[derive(Debug, Clone, Copy, Default)]
46pub struct DisplayContentHistory {
47    skip_promoted_tasks: bool,
48}
49
50impl ExclusiveOption for DisplayContentHistory {
51    fn inject_args(&self, command: &mut Command) {
52        command.arg("-h");
53
54        if self.skip_promoted_tasks {
55            command.arg("-p");
56        }
57    }
58}
59
60///
61/// Print detailed information about the revisions of files.
62///
63/// The `L` type parameter tracks the changelist description output at
64/// compile time: [`Self::full_description`] transitions to the
65/// [`FullDescription`] state and [`Self::truncated_description`]
66/// transitions to the [`TruncatedDescription`] state. The `H` type
67/// parameter tracks whether file content history is displayed:
68/// [`Self::content_history`] transitions to the [`DisplayContentHistory`]
69/// state.
70#[derive(Debug, Clone, Default)]
71pub struct FileLog<L = Unselected, H = Unselected> {
72    bin: PathBuf,
73
74    global_opts: GlobalOpts,
75
76    changelist: Option<String>,
77
78    content_history: H,
79
80    follow_branches: bool,
81
82    long_output: L,
83
84    limit: Option<u64>,
85
86    ignore_non_contributory: bool,
87
88    include_time: bool,
89}
90
91impl FileLog<Unselected, Unselected> {
92    /// Creates a new `p4 filelog` command.
93    ///
94    /// `bin` is the path to the Perforce command-line executable.
95    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
96        Self {
97            bin: bin.into(),
98            global_opts,
99            changelist: None,
100            content_history: Unselected,
101            follow_branches: false,
102            long_output: Unselected,
103            limit: None,
104            ignore_non_contributory: false,
105            include_time: false,
106        }
107    }
108}
109
110impl<H: ExclusiveOption> FileLog<Unselected, H> {
111    /// # Description
112    ///
113    /// -l
114    ///
115    /// List long output, with the full text of each changelist description.
116    ///
117    /// Transitions this command to the [`FullDescription`] state.
118    pub fn full_description(self) -> FileLog<FullDescription, H> {
119        FileLog {
120            bin: self.bin,
121            global_opts: self.global_opts,
122            changelist: self.changelist,
123            content_history: self.content_history,
124            follow_branches: self.follow_branches,
125            long_output: FullDescription,
126            limit: self.limit,
127            ignore_non_contributory: self.ignore_non_contributory,
128            include_time: self.include_time,
129        }
130    }
131
132    /// # Description
133    ///
134    /// -L
135    ///
136    /// List long output, with the full text of each changelist description
137    /// truncated at 250 characters.
138    ///
139    /// Transitions this command to the [`TruncatedDescription`] state.
140    pub fn truncated_description(self) -> FileLog<TruncatedDescription, H> {
141        FileLog {
142            bin: self.bin,
143            global_opts: self.global_opts,
144            changelist: self.changelist,
145            content_history: self.content_history,
146            follow_branches: self.follow_branches,
147            long_output: TruncatedDescription,
148            limit: self.limit,
149            ignore_non_contributory: self.ignore_non_contributory,
150            include_time: self.include_time,
151        }
152    }
153}
154
155impl<L: ExclusiveOption> FileLog<L, Unselected> {
156    /// # Description
157    ///
158    /// -h
159    ///
160    /// Display file content history instead of file name history.
161    ///
162    /// Transitions this command to the [`DisplayContentHistory`] state,
163    /// which unlocks the `-p` option.
164    pub fn content_history(self) -> FileLog<L, DisplayContentHistory> {
165        FileLog {
166            bin: self.bin,
167            global_opts: self.global_opts,
168            changelist: self.changelist,
169            content_history: DisplayContentHistory {
170                skip_promoted_tasks: false,
171            },
172            follow_branches: self.follow_branches,
173            long_output: self.long_output,
174            limit: self.limit,
175            ignore_non_contributory: self.ignore_non_contributory,
176            include_time: self.include_time,
177        }
178    }
179}
180
181impl<L: ExclusiveOption, H: ExclusiveOption, S, I> ParameterizedSpawn<(S,)> for FileLog<L, H>
182where
183    S: IntoIterator<Item = I>,
184    I: AsRef<OsStr>,
185{
186    type Output = Child;
187    type Error = std::io::Error;
188
189    /// Spawns `p4 filelog` for the given files as a child process with piped
190    /// standard output and error streams; use the returned [`Child`] handle
191    /// to wait for it or interact with it.
192    ///
193    /// At least one file or file pattern must be provided.
194    fn spawn_with(&mut self, (files,): (S,)) -> Result<Self::Output, Self::Error> {
195        self.setup_command(&self.bin)
196            .args(files)
197            .stdout(Stdio::piped())
198            .stderr(Stdio::piped())
199            .spawn()
200    }
201}
202
203impl<L: ExclusiveOption, H: ExclusiveOption> FileLog<L, H> {
204    /// Get the Perforce command path.
205    pub fn get_bin(&self) -> &Path {
206        &self.bin
207    }
208
209    /// Set the Perforce command path.
210    pub fn set_bin(&mut self, bin: impl Into<PathBuf>) -> &mut Self {
211        self.bin = bin.into();
212        self
213    }
214
215    /// Set the Perforce command path.
216    pub fn bin(mut self, bin: impl Into<PathBuf>) -> Self {
217        self.set_bin(bin);
218        self
219    }
220
221    /// # Description
222    ///
223    /// g-opts
224    ///
225    #[cfg_attr(
226        feature = "lt2014_2",
227        doc = "See the [Global Options](GlobalOpts) section."
228    )]
229    #[cfg_attr(
230        all(feature = "lt2015_1", not(feature = "lt2014_2")),
231        doc = "See the [“Global Options”](GlobalOpts) section."
232    )]
233    #[cfg_attr(
234        all(feature = "lt2017_1", not(feature = "lt2015_1")),
235        doc = "See [“Global Options”](GlobalOpts)."
236    )]
237    #[cfg_attr(
238        all(feature = "lt2018_2", not(feature = "lt2017_1")),
239        doc = "See [Global Options](GlobalOpts)."
240    )]
241    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
242    pub fn get_global_opts(&self) -> &GlobalOpts {
243        &self.global_opts
244    }
245
246    /// # Description
247    ///
248    /// g-opts
249    ///
250    #[cfg_attr(
251        feature = "lt2014_2",
252        doc = "See the [Global Options](GlobalOpts) section."
253    )]
254    #[cfg_attr(
255        all(feature = "lt2015_1", not(feature = "lt2014_2")),
256        doc = "See the [“Global Options”](GlobalOpts) section."
257    )]
258    #[cfg_attr(
259        all(feature = "lt2017_1", not(feature = "lt2015_1")),
260        doc = "See [“Global Options”](GlobalOpts)."
261    )]
262    #[cfg_attr(
263        all(feature = "lt2018_2", not(feature = "lt2017_1")),
264        doc = "See [Global Options](GlobalOpts)."
265    )]
266    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
267    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
268        self.global_opts = v;
269        self
270    }
271
272    /// # Description
273    ///
274    /// g-opts
275    ///
276    #[cfg_attr(
277        feature = "lt2014_2",
278        doc = "See the [Global Options](GlobalOpts) section."
279    )]
280    #[cfg_attr(
281        all(feature = "lt2015_1", not(feature = "lt2014_2")),
282        doc = "See the [“Global Options”](GlobalOpts) section."
283    )]
284    #[cfg_attr(
285        all(feature = "lt2017_1", not(feature = "lt2015_1")),
286        doc = "See [“Global Options”](GlobalOpts)."
287    )]
288    #[cfg_attr(
289        all(feature = "lt2018_2", not(feature = "lt2017_1")),
290        doc = "See [Global Options](GlobalOpts)."
291    )]
292    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
293    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
294        self.global_opts = v;
295        self
296    }
297
298    /// # Description
299    ///
300    /// `-c change`
301    ///
302    /// Display only files submitted at the specified changelist number.
303    pub fn get_changelist(&self) -> Option<&String> {
304        self.changelist.as_ref()
305    }
306
307    /// # Description
308    ///
309    /// `-c change`
310    ///
311    /// Display only files submitted at the specified changelist number.
312    pub fn set_changelist(&mut self, v: impl Into<String>) -> &mut Self {
313        self.changelist = Some(v.into());
314        self
315    }
316
317    /// # Description
318    ///
319    /// `-c change`
320    ///
321    /// Display only files submitted at the specified changelist number.
322    pub fn changelist(mut self, v: impl Into<String>) -> Self {
323        self.changelist = Some(v.into());
324        self
325    }
326
327    /// # Description
328    ///
329    /// `-i`
330    ///
331    /// Follow file history across branches.
332    pub fn get_follow_branches(&self) -> bool {
333        self.follow_branches
334    }
335
336    /// # Description
337    ///
338    /// `-i`
339    ///
340    /// Follow file history across branches.
341    pub fn set_follow_branches(&mut self, v: bool) -> &mut Self {
342        self.follow_branches = v;
343        self
344    }
345
346    /// # Description
347    ///
348    /// `-i`
349    ///
350    /// Follow file history across branches.
351    pub fn follow_branches(mut self, v: bool) -> Self {
352        self.follow_branches = v;
353        self
354    }
355
356    /// # Description
357    ///
358    /// `-m max`
359    ///
360    /// List only the first `max` changes per file output.
361    pub fn get_limit(&self) -> Option<u64> {
362        self.limit
363    }
364
365    /// # Description
366    ///
367    /// `-m max`
368    ///
369    /// List only the first `max` changes per file output.
370    pub fn set_limit(&mut self, v: u64) -> &mut Self {
371        self.limit = Some(v);
372        self
373    }
374
375    /// # Description
376    ///
377    /// `-m max`
378    ///
379    /// List only the first `max` changes per file output.
380    pub fn limit(mut self, v: u64) -> Self {
381        self.limit = Some(v);
382        self
383    }
384
385    /// # Description
386    ///
387    /// `-s`
388    ///
389    /// Display a shortened form of output by ignoring non-contributory
390    /// integrations.
391    pub fn get_ignore_non_contributory(&self) -> bool {
392        self.ignore_non_contributory
393    }
394
395    /// # Description
396    ///
397    /// `-s`
398    ///
399    /// Display a shortened form of output by ignoring non-contributory
400    /// integrations.
401    pub fn set_ignore_non_contributory(&mut self, v: bool) -> &mut Self {
402        self.ignore_non_contributory = v;
403        self
404    }
405
406    /// # Description
407    ///
408    /// `-s`
409    ///
410    /// Display a shortened form of output by ignoring non-contributory
411    /// integrations.
412    pub fn ignore_non_contributory(mut self, v: bool) -> Self {
413        self.ignore_non_contributory = v;
414        self
415    }
416
417    /// # Description
418    ///
419    /// `-t`
420    ///
421    /// Display the time as well as the date.
422    pub fn get_include_time(&self) -> bool {
423        self.include_time
424    }
425
426    /// # Description
427    ///
428    /// `-t`
429    ///
430    /// Display the time as well as the date.
431    pub fn set_include_time(&mut self, v: bool) -> &mut Self {
432        self.include_time = v;
433        self
434    }
435
436    /// # Description
437    ///
438    /// `-t`
439    ///
440    /// Display the time as well as the date.
441    pub fn include_time(mut self, v: bool) -> Self {
442        self.include_time = v;
443        self
444    }
445}
446
447impl<L: ExclusiveOption> FileLog<L, DisplayContentHistory> {
448    /// # Description
449    ///
450    /// -p
451    ///
452    /// When used with the `-h` option, do not follow content of promoted task
453    /// streams.
454    pub fn get_skip_promoted_tasks(&self) -> bool {
455        self.content_history.skip_promoted_tasks
456    }
457
458    /// # Description
459    ///
460    /// -p
461    ///
462    /// When used with the `-h` option, do not follow content of promoted task
463    /// streams.
464    pub fn set_skip_promoted_tasks(&mut self, v: bool) -> &mut Self {
465        self.content_history.skip_promoted_tasks = v;
466        self
467    }
468
469    /// # Description
470    ///
471    /// -p
472    ///
473    /// When used with the `-h` option, do not follow content of promoted task
474    /// streams.
475    pub fn skip_promoted_tasks(mut self, v: bool) -> Self {
476        self.content_history.skip_promoted_tasks = v;
477        self
478    }
479}
480
481impl<L: ExclusiveOption, H: ExclusiveOption> SubCommand for FileLog<L, H> {
482    fn name(&self) -> &str {
483        "filelog"
484    }
485
486    fn inject_local_args(&self, command: &mut Command) {
487        if let Some(changelist) = &self.changelist {
488            command.arg("-c").arg(changelist);
489        }
490
491        self.content_history.inject_args(command);
492
493        if self.follow_branches {
494            command.arg("-i");
495        }
496
497        self.long_output.inject_args(command);
498
499        if let Some(max) = self.limit {
500            command.arg("-m").arg(max.to_string());
501        }
502
503        if self.ignore_non_contributory {
504            command.arg("-s");
505        }
506
507        if self.include_time {
508            command.arg("-t");
509        }
510    }
511
512    fn global_opts(&self) -> Option<&GlobalOpts> {
513        Some(&self.global_opts)
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520    use crate::cmd::args_of;
521
522    #[test]
523    fn bin_accessors_update_executable_path() {
524        let mut command = FileLog::new("p4", GlobalOpts::new());
525        assert_eq!(command.get_bin(), Path::new("p4"));
526
527        command.set_bin("/usr/local/bin/p4");
528        assert_eq!(command.get_bin(), Path::new("/usr/local/bin/p4"));
529
530        let command = command.bin("p4.exe");
531        assert_eq!(command.get_bin(), Path::new("p4.exe"));
532    }
533
534    #[test]
535    fn with_files() {
536        let filelog = FileLog::new("p4", GlobalOpts::default());
537        let mut cmd = filelog.setup_command("p4");
538        cmd.arg("//depot/project/...");
539        assert_eq!(args_of(&cmd), vec!["filelog", "//depot/project/..."]);
540    }
541
542    #[test]
543    fn changelist() {
544        let filelog = FileLog::new("p4", GlobalOpts::default()).changelist("100");
545        let cmd = filelog.setup_command("p4");
546        assert_eq!(args_of(&cmd), vec!["filelog", "-c", "100"]);
547    }
548
549    #[test]
550    fn content_history() {
551        let filelog = FileLog::new("p4", GlobalOpts::default()).content_history();
552        let cmd = filelog.setup_command("p4");
553        assert_eq!(args_of(&cmd), vec!["filelog", "-h"]);
554    }
555
556    #[test]
557    fn follow_branches() {
558        let filelog = FileLog::new("p4", GlobalOpts::default()).follow_branches(true);
559        let cmd = filelog.setup_command("p4");
560        assert_eq!(args_of(&cmd), vec!["filelog", "-i"]);
561    }
562
563    #[test]
564    fn full_description() {
565        let filelog = FileLog::new("p4", GlobalOpts::default()).full_description();
566        let cmd = filelog.setup_command("p4");
567        assert_eq!(args_of(&cmd), vec!["filelog", "-l"]);
568    }
569
570    #[test]
571    fn truncated_description() {
572        let filelog = FileLog::new("p4", GlobalOpts::default()).truncated_description();
573        let cmd = filelog.setup_command("p4");
574        assert_eq!(args_of(&cmd), vec!["filelog", "-L"]);
575    }
576
577    #[test]
578    fn limit() {
579        let filelog = FileLog::new("p4", GlobalOpts::default()).limit(5);
580        let cmd = filelog.setup_command("p4");
581        assert_eq!(args_of(&cmd), vec!["filelog", "-m", "5"]);
582    }
583
584    #[test]
585    fn skip_promoted_tasks() {
586        let filelog = FileLog::new("p4", GlobalOpts::default())
587            .content_history()
588            .skip_promoted_tasks(true);
589        let cmd = filelog.setup_command("p4");
590        assert_eq!(args_of(&cmd), vec!["filelog", "-h", "-p"]);
591    }
592
593    #[test]
594    fn skip_promoted_tasks_accessors() {
595        let mut filelog = FileLog::new("p4", GlobalOpts::default())
596            .full_description()
597            .content_history();
598        filelog.set_skip_promoted_tasks(true);
599
600        assert!(filelog.get_skip_promoted_tasks());
601        assert_eq!(
602            args_of(&filelog.setup_command("p4")),
603            ["filelog", "-h", "-p", "-l"]
604        );
605    }
606
607    #[test]
608    fn ignore_non_contributory() {
609        let filelog = FileLog::new("p4", GlobalOpts::default()).ignore_non_contributory(true);
610        let cmd = filelog.setup_command("p4");
611        assert_eq!(args_of(&cmd), vec!["filelog", "-s"]);
612    }
613
614    #[test]
615    fn include_time() {
616        let filelog = FileLog::new("p4", GlobalOpts::default()).include_time(true);
617        let cmd = filelog.setup_command("p4");
618        assert_eq!(args_of(&cmd), vec!["filelog", "-t"]);
619    }
620
621    #[test]
622    fn all_options_order() {
623        let filelog = FileLog::new("p4", GlobalOpts::default())
624            .changelist("100")
625            .content_history()
626            .follow_branches(true)
627            .full_description()
628            .limit(5)
629            .skip_promoted_tasks(true)
630            .ignore_non_contributory(true)
631            .include_time(true);
632        let cmd = filelog.setup_command("p4");
633        assert_eq!(
634            args_of(&cmd),
635            vec![
636                "filelog", "-c", "100", "-h", "-p", "-i", "-l", "-m", "5", "-s", "-t",
637            ]
638        );
639    }
640}