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