Skip to main content

perforce_cli/cmd/
filelog.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/// 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    /// # Description
205    ///
206    /// g-opts
207    ///
208    #[cfg_attr(
209        feature = "lt2014_2",
210        doc = "See the [Global Options](GlobalOpts) section."
211    )]
212    #[cfg_attr(
213        all(feature = "lt2015_1", not(feature = "lt2014_2")),
214        doc = "See the [“Global Options”](GlobalOpts) section."
215    )]
216    #[cfg_attr(
217        all(feature = "lt2017_1", not(feature = "lt2015_1")),
218        doc = "See [“Global Options”](GlobalOpts)."
219    )]
220    #[cfg_attr(
221        all(feature = "lt2018_2", not(feature = "lt2017_1")),
222        doc = "See [Global Options](GlobalOpts)."
223    )]
224    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
225    pub fn get_global_opts(&self) -> &GlobalOpts {
226        &self.global_opts
227    }
228
229    /// # Description
230    ///
231    /// g-opts
232    ///
233    #[cfg_attr(
234        feature = "lt2014_2",
235        doc = "See the [Global Options](GlobalOpts) section."
236    )]
237    #[cfg_attr(
238        all(feature = "lt2015_1", not(feature = "lt2014_2")),
239        doc = "See the [“Global Options”](GlobalOpts) section."
240    )]
241    #[cfg_attr(
242        all(feature = "lt2017_1", not(feature = "lt2015_1")),
243        doc = "See [“Global Options”](GlobalOpts)."
244    )]
245    #[cfg_attr(
246        all(feature = "lt2018_2", not(feature = "lt2017_1")),
247        doc = "See [Global Options](GlobalOpts)."
248    )]
249    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
250    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
251        self.global_opts = v;
252        self
253    }
254
255    /// # Description
256    ///
257    /// g-opts
258    ///
259    #[cfg_attr(
260        feature = "lt2014_2",
261        doc = "See the [Global Options](GlobalOpts) section."
262    )]
263    #[cfg_attr(
264        all(feature = "lt2015_1", not(feature = "lt2014_2")),
265        doc = "See the [“Global Options”](GlobalOpts) section."
266    )]
267    #[cfg_attr(
268        all(feature = "lt2017_1", not(feature = "lt2015_1")),
269        doc = "See [“Global Options”](GlobalOpts)."
270    )]
271    #[cfg_attr(
272        all(feature = "lt2018_2", not(feature = "lt2017_1")),
273        doc = "See [Global Options](GlobalOpts)."
274    )]
275    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
276    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
277        self.global_opts = v;
278        self
279    }
280
281    /// # Description
282    ///
283    /// `-c change`
284    ///
285    /// Display only files submitted at the specified changelist number.
286    pub fn get_changelist(&self) -> Option<&String> {
287        self.changelist.as_ref()
288    }
289
290    /// # Description
291    ///
292    /// `-c change`
293    ///
294    /// Display only files submitted at the specified changelist number.
295    pub fn set_changelist(&mut self, v: impl Into<String>) -> &mut Self {
296        self.changelist = Some(v.into());
297        self
298    }
299
300    /// # Description
301    ///
302    /// `-c change`
303    ///
304    /// Display only files submitted at the specified changelist number.
305    pub fn changelist(mut self, v: impl Into<String>) -> Self {
306        self.changelist = Some(v.into());
307        self
308    }
309
310    /// # Description
311    ///
312    /// `-i`
313    ///
314    /// Follow file history across branches.
315    pub fn get_follow_branches(&self) -> bool {
316        self.follow_branches
317    }
318
319    /// # Description
320    ///
321    /// `-i`
322    ///
323    /// Follow file history across branches.
324    pub fn set_follow_branches(&mut self, v: bool) -> &mut Self {
325        self.follow_branches = v;
326        self
327    }
328
329    /// # Description
330    ///
331    /// `-i`
332    ///
333    /// Follow file history across branches.
334    pub fn follow_branches(mut self, v: bool) -> Self {
335        self.follow_branches = v;
336        self
337    }
338
339    /// # Description
340    ///
341    /// `-m max`
342    ///
343    /// List only the first `max` changes per file output.
344    pub fn get_limit(&self) -> Option<u64> {
345        self.limit
346    }
347
348    /// # Description
349    ///
350    /// `-m max`
351    ///
352    /// List only the first `max` changes per file output.
353    pub fn set_limit(&mut self, v: u64) -> &mut Self {
354        self.limit = Some(v);
355        self
356    }
357
358    /// # Description
359    ///
360    /// `-m max`
361    ///
362    /// List only the first `max` changes per file output.
363    pub fn limit(mut self, v: u64) -> Self {
364        self.limit = Some(v);
365        self
366    }
367
368    /// # Description
369    ///
370    /// `-s`
371    ///
372    /// Display a shortened form of output by ignoring non-contributory
373    /// integrations.
374    pub fn get_ignore_non_contributory(&self) -> bool {
375        self.ignore_non_contributory
376    }
377
378    /// # Description
379    ///
380    /// `-s`
381    ///
382    /// Display a shortened form of output by ignoring non-contributory
383    /// integrations.
384    pub fn set_ignore_non_contributory(&mut self, v: bool) -> &mut Self {
385        self.ignore_non_contributory = v;
386        self
387    }
388
389    /// # Description
390    ///
391    /// `-s`
392    ///
393    /// Display a shortened form of output by ignoring non-contributory
394    /// integrations.
395    pub fn ignore_non_contributory(mut self, v: bool) -> Self {
396        self.ignore_non_contributory = v;
397        self
398    }
399
400    /// # Description
401    ///
402    /// `-t`
403    ///
404    /// Display the time as well as the date.
405    pub fn get_include_time(&self) -> bool {
406        self.include_time
407    }
408
409    /// # Description
410    ///
411    /// `-t`
412    ///
413    /// Display the time as well as the date.
414    pub fn set_include_time(&mut self, v: bool) -> &mut Self {
415        self.include_time = v;
416        self
417    }
418
419    /// # Description
420    ///
421    /// `-t`
422    ///
423    /// Display the time as well as the date.
424    pub fn include_time(mut self, v: bool) -> Self {
425        self.include_time = v;
426        self
427    }
428}
429
430impl<L: ExclusiveOption> FileLog<L, DisplayContentHistory> {
431    /// # Description
432    ///
433    /// -p
434    ///
435    /// When used with the `-h` option, do not follow content of promoted task
436    /// streams.
437    pub fn get_skip_promoted_tasks(&self) -> bool {
438        self.content_history.skip_promoted_tasks
439    }
440
441    /// # Description
442    ///
443    /// -p
444    ///
445    /// When used with the `-h` option, do not follow content of promoted task
446    /// streams.
447    pub fn set_skip_promoted_tasks(&mut self, v: bool) -> &mut Self {
448        self.content_history.skip_promoted_tasks = v;
449        self
450    }
451
452    /// # Description
453    ///
454    /// -p
455    ///
456    /// When used with the `-h` option, do not follow content of promoted task
457    /// streams.
458    pub fn skip_promoted_tasks(mut self, v: bool) -> Self {
459        self.content_history.skip_promoted_tasks = v;
460        self
461    }
462}
463
464impl<L: ExclusiveOption, H: ExclusiveOption> SubCommand for FileLog<L, H> {
465    fn name(&self) -> &str {
466        "filelog"
467    }
468
469    fn inject_local_args(&self, command: &mut Command) {
470        if let Some(changelist) = &self.changelist {
471            command.arg("-c").arg(changelist);
472        }
473
474        self.content_history.inject_args(command);
475
476        if self.follow_branches {
477            command.arg("-i");
478        }
479
480        self.long_output.inject_args(command);
481
482        if let Some(max) = self.limit {
483            command.arg("-m").arg(max.to_string());
484        }
485
486        if self.ignore_non_contributory {
487            command.arg("-s");
488        }
489
490        if self.include_time {
491            command.arg("-t");
492        }
493    }
494
495    fn global_opts(&self) -> Option<&GlobalOpts> {
496        Some(&self.global_opts)
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503    use crate::cmd::args_of;
504
505    #[test]
506    fn with_files() {
507        let filelog = FileLog::new("p4", GlobalOpts::default());
508        let mut cmd = filelog.setup_command("p4");
509        cmd.arg("//depot/project/...");
510        assert_eq!(args_of(&cmd), vec!["filelog", "//depot/project/..."]);
511    }
512
513    #[test]
514    fn changelist() {
515        let filelog = FileLog::new("p4", GlobalOpts::default()).changelist("100");
516        let cmd = filelog.setup_command("p4");
517        assert_eq!(args_of(&cmd), vec!["filelog", "-c", "100"]);
518    }
519
520    #[test]
521    fn content_history() {
522        let filelog = FileLog::new("p4", GlobalOpts::default()).content_history();
523        let cmd = filelog.setup_command("p4");
524        assert_eq!(args_of(&cmd), vec!["filelog", "-h"]);
525    }
526
527    #[test]
528    fn follow_branches() {
529        let filelog = FileLog::new("p4", GlobalOpts::default()).follow_branches(true);
530        let cmd = filelog.setup_command("p4");
531        assert_eq!(args_of(&cmd), vec!["filelog", "-i"]);
532    }
533
534    #[test]
535    fn full_description() {
536        let filelog = FileLog::new("p4", GlobalOpts::default()).full_description();
537        let cmd = filelog.setup_command("p4");
538        assert_eq!(args_of(&cmd), vec!["filelog", "-l"]);
539    }
540
541    #[test]
542    fn truncated_description() {
543        let filelog = FileLog::new("p4", GlobalOpts::default()).truncated_description();
544        let cmd = filelog.setup_command("p4");
545        assert_eq!(args_of(&cmd), vec!["filelog", "-L"]);
546    }
547
548    #[test]
549    fn limit() {
550        let filelog = FileLog::new("p4", GlobalOpts::default()).limit(5);
551        let cmd = filelog.setup_command("p4");
552        assert_eq!(args_of(&cmd), vec!["filelog", "-m", "5"]);
553    }
554
555    #[test]
556    fn skip_promoted_tasks() {
557        let filelog = FileLog::new("p4", GlobalOpts::default())
558            .content_history()
559            .skip_promoted_tasks(true);
560        let cmd = filelog.setup_command("p4");
561        assert_eq!(args_of(&cmd), vec!["filelog", "-h", "-p"]);
562    }
563
564    #[test]
565    fn skip_promoted_tasks_accessors() {
566        let mut filelog = FileLog::new("p4", GlobalOpts::default())
567            .full_description()
568            .content_history();
569        filelog.set_skip_promoted_tasks(true);
570
571        assert!(filelog.get_skip_promoted_tasks());
572        assert_eq!(
573            args_of(&filelog.setup_command("p4")),
574            ["filelog", "-h", "-p", "-l"]
575        );
576    }
577
578    #[test]
579    fn ignore_non_contributory() {
580        let filelog = FileLog::new("p4", GlobalOpts::default()).ignore_non_contributory(true);
581        let cmd = filelog.setup_command("p4");
582        assert_eq!(args_of(&cmd), vec!["filelog", "-s"]);
583    }
584
585    #[test]
586    fn include_time() {
587        let filelog = FileLog::new("p4", GlobalOpts::default()).include_time(true);
588        let cmd = filelog.setup_command("p4");
589        assert_eq!(args_of(&cmd), vec!["filelog", "-t"]);
590    }
591
592    #[test]
593    fn all_options_order() {
594        let filelog = FileLog::new("p4", GlobalOpts::default())
595            .changelist("100")
596            .content_history()
597            .follow_branches(true)
598            .full_description()
599            .limit(5)
600            .skip_promoted_tasks(true)
601            .ignore_non_contributory(true)
602            .include_time(true);
603        let cmd = filelog.setup_command("p4");
604        assert_eq!(
605            args_of(&cmd),
606            vec![
607                "filelog", "-c", "100", "-h", "-p", "-i", "-l", "-m", "5", "-s", "-t",
608            ]
609        );
610    }
611}