1use std::{
2 ffi::OsStr,
3 path::PathBuf,
4 process::{Child, Stdio},
5};
6
7use super::{ExclusiveOption, SubCommand, Unselected};
8
9use crate::global::GlobalOpts;
10use crate::spawn::ParameterizedSpawn;
11
12#[derive(Debug, Clone, Copy, Default)]
14pub struct FullDescription;
15
16impl ExclusiveOption for FullDescription {
17 fn inject_args(&self, command: &mut std::process::Command) {
18 command.arg("-l");
19 }
20}
21
22#[derive(Debug, Clone, Copy, Default)]
24pub struct TruncatedDescription;
25
26impl ExclusiveOption for TruncatedDescription {
27 fn inject_args(&self, command: &mut std::process::Command) {
28 command.arg("-L");
29 }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum User {
35 User(String),
37 #[cfg(not(feature = "lt2016_1"))]
39 Me,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Status {
45 Pending,
47 Submitted,
49 Shelved,
51}
52
53impl Status {
54 pub fn as_str(&self) -> &'static str {
56 match self {
57 Status::Pending => "pending",
58 Status::Submitted => "submitted",
59 Status::Shelved => "shelved",
60 }
61 }
62}
63
64#[cfg_attr(
65 feature = "lt2015_2",
66 doc = "`p4 [g-opts] changes [-i -t -l -L -f] [-c client] [-m max] [-s status] [-u user] [file[RevRange] ...]`"
67)]
68#[cfg_attr(
69 all(feature = "lt2016_1", not(feature = "lt2015_2")),
70 doc = "`p4 [g-opts] changes [-i -t -l -L -f] [-c client] [ -e changelist#][-m max] [-s status] [-u user][file[RevRange] ...]`"
71)]
72#[cfg_attr(
73 all(feature = "lt2017_2", not(feature = "lt2016_1")),
74 doc = "`p4 [g-opts] changes [-i -t -l -L -f] [-c client] [ -e changelist#][-m max] [-s status] [-u user | --me][file[RevRange] ...]`"
75)]
76#[cfg_attr(
77 all(feature = "lt2022_2", not(feature = "lt2017_2")),
78 doc = "`p4 [g-opts] changes [-i -t -l -L -f] [-c client] [ -e changelist#][-m max] [-r] [-s status] [-u user | --me] [file[RevRange] ...]`"
79)]
80#[cfg_attr(
81 not(feature = "lt2022_2"),
82 doc = "`p4 [g-opts] changes [-i -t -l -L -f] [-c client] [ -e changelist#][-m max] [-r] [-s status] [-u user | --me] [file[RevRange] ...] [--stream | --nostream]`"
83)]
84#[derive(Debug, Clone, Default)]
93pub struct Changes<L = Unselected> {
94 bin: PathBuf,
95
96 global_opts: GlobalOpts,
97
98 #[cfg(not(feature = "lt2015_2"))]
99 min_change_list: Option<String>,
100
101 include_restricted: bool,
102
103 include_integrated: bool,
104
105 include_time: bool,
106
107 long_output: L,
108
109 limit: Option<u64>,
110
111 #[cfg(not(feature = "lt2017_2"))]
112 reverse_order: bool,
113
114 filter_status: Option<Status>,
115
116 filter_users: Option<Vec<User>>,
117
118 filter_clients: Option<Vec<String>>,
119
120 #[cfg(not(feature = "lt2025_1"))]
121 client_case_insensitive: bool,
122
123 #[cfg(not(feature = "lt2022_2"))]
124 stream: Option<bool>,
125}
126
127impl Changes<Unselected> {
128 pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
132 Self {
133 bin: bin.into(),
134 global_opts,
135 ..Self::default()
136 }
137 }
138
139 pub fn long_output_full(self) -> Changes<FullDescription> {
147 Changes {
148 bin: self.bin,
149 global_opts: self.global_opts,
150 #[cfg(not(feature = "lt2015_2"))]
151 min_change_list: self.min_change_list,
152 include_restricted: self.include_restricted,
153 include_integrated: self.include_integrated,
154 include_time: self.include_time,
155 long_output: FullDescription,
156 limit: self.limit,
157 #[cfg(not(feature = "lt2017_2"))]
158 reverse_order: self.reverse_order,
159 filter_status: self.filter_status,
160 filter_users: self.filter_users,
161 filter_clients: self.filter_clients,
162 #[cfg(not(feature = "lt2025_1"))]
163 client_case_insensitive: self.client_case_insensitive,
164 #[cfg(not(feature = "lt2022_2"))]
165 stream: self.stream,
166 }
167 }
168
169 pub fn long_output_truncated(self) -> Changes<TruncatedDescription> {
178 Changes {
179 bin: self.bin,
180 global_opts: self.global_opts,
181 #[cfg(not(feature = "lt2015_2"))]
182 min_change_list: self.min_change_list,
183 include_restricted: self.include_restricted,
184 include_integrated: self.include_integrated,
185 include_time: self.include_time,
186 long_output: TruncatedDescription,
187 limit: self.limit,
188 #[cfg(not(feature = "lt2017_2"))]
189 reverse_order: self.reverse_order,
190 filter_status: self.filter_status,
191 filter_users: self.filter_users,
192 filter_clients: self.filter_clients,
193 #[cfg(not(feature = "lt2025_1"))]
194 client_case_insensitive: self.client_case_insensitive,
195 #[cfg(not(feature = "lt2022_2"))]
196 stream: self.stream,
197 }
198 }
199}
200
201impl<L: ExclusiveOption> ParameterizedSpawn for Changes<L> {
202 type Input<'a> = &'a [&'a OsStr];
203 type Output<'a> = Child;
204 type Error = std::io::Error;
205
206 fn spawn_with<'a>(&mut self, files: Self::Input<'a>) -> Result<Self::Output<'a>, Self::Error> {
213 self.setup_command(&self.bin)
214 .args(files)
215 .stdout(Stdio::piped())
216 .stderr(Stdio::piped())
217 .spawn()
218 }
219}
220
221impl<L: ExclusiveOption> Changes<L> {
222 #[cfg_attr(
227 feature = "lt2014_2",
228 doc = "See the [Global Options](GlobalOpts) section."
229 )]
230 #[cfg_attr(
231 all(feature = "lt2015_1", not(feature = "lt2014_2")),
232 doc = "See the [“Global Options”](GlobalOpts) section."
233 )]
234 #[cfg_attr(
235 all(feature = "lt2017_1", not(feature = "lt2015_1")),
236 doc = "See [“Global Options”](GlobalOpts)."
237 )]
238 #[cfg_attr(
239 all(feature = "lt2018_2", not(feature = "lt2017_1")),
240 doc = "See [Global Options](GlobalOpts)."
241 )]
242 #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
243 pub fn get_global_opts(&self) -> &GlobalOpts {
244 &self.global_opts
245 }
246
247 #[cfg_attr(
252 feature = "lt2014_2",
253 doc = "See the [Global Options](GlobalOpts) section."
254 )]
255 #[cfg_attr(
256 all(feature = "lt2015_1", not(feature = "lt2014_2")),
257 doc = "See the [“Global Options”](GlobalOpts) section."
258 )]
259 #[cfg_attr(
260 all(feature = "lt2017_1", not(feature = "lt2015_1")),
261 doc = "See [“Global Options”](GlobalOpts)."
262 )]
263 #[cfg_attr(
264 all(feature = "lt2018_2", not(feature = "lt2017_1")),
265 doc = "See [Global Options](GlobalOpts)."
266 )]
267 #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
268 pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
269 self.global_opts = v;
270 self
271 }
272
273 #[cfg_attr(
278 feature = "lt2014_2",
279 doc = "See the [Global Options](GlobalOpts) section."
280 )]
281 #[cfg_attr(
282 all(feature = "lt2015_1", not(feature = "lt2014_2")),
283 doc = "See the [“Global Options”](GlobalOpts) section."
284 )]
285 #[cfg_attr(
286 all(feature = "lt2017_1", not(feature = "lt2015_1")),
287 doc = "See [“Global Options”](GlobalOpts)."
288 )]
289 #[cfg_attr(
290 all(feature = "lt2018_2", not(feature = "lt2017_1")),
291 doc = "See [Global Options](GlobalOpts)."
292 )]
293 #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
294 pub fn global_opts(mut self, v: GlobalOpts) -> Self {
295 self.global_opts = v;
296 self
297 }
298
299 pub fn get_filter_clients(&self) -> Option<&[String]> {
306 self.filter_clients.as_deref()
307 }
308
309 pub fn set_filter_client(&mut self, v: impl Into<String>) -> &mut Self {
316 self.filter_clients
317 .get_or_insert_with(Vec::new)
318 .push(v.into());
319 self
320 }
321
322 pub fn filter_client(mut self, v: impl Into<String>) -> Self {
329 self.filter_clients
330 .get_or_insert_with(Vec::new)
331 .push(v.into());
332 self
333 }
334
335 #[cfg(not(feature = "lt2015_2"))]
342 pub fn get_min_change_list(&self) -> Option<&String> {
343 self.min_change_list.as_ref()
344 }
345
346 #[cfg(not(feature = "lt2015_2"))]
353 pub fn set_min_change_list(&mut self, v: impl Into<String>) -> &mut Self {
354 self.min_change_list = Some(v.into());
355 self
356 }
357
358 #[cfg(not(feature = "lt2015_2"))]
365 pub fn min_change_list(mut self, v: impl Into<String>) -> Self {
366 self.min_change_list = Some(v.into());
367 self
368 }
369
370 pub fn get_include_restricted(&self) -> bool {
376 self.include_restricted
377 }
378
379 pub fn set_include_restricted(&mut self, v: bool) -> &mut Self {
385 self.include_restricted = v;
386 self
387 }
388
389 pub fn include_restricted(mut self, v: bool) -> Self {
395 self.include_restricted = v;
396 self
397 }
398
399 pub fn get_include_integrated(&self) -> bool {
406 self.include_integrated
407 }
408
409 pub fn set_include_integrated(&mut self, v: bool) -> &mut Self {
416 self.include_integrated = v;
417 self
418 }
419
420 pub fn include_integrated(mut self, v: bool) -> Self {
427 self.include_integrated = v;
428 self
429 }
430
431 pub fn get_limit(&self) -> Option<u64> {
437 self.limit
438 }
439
440 pub fn set_limit(&mut self, v: u64) -> &mut Self {
446 self.limit = Some(v);
447 self
448 }
449
450 pub fn limit(mut self, v: u64) -> Self {
456 self.limit = Some(v);
457 self
458 }
459
460 #[cfg(not(feature = "lt2017_2"))]
467 pub fn get_reverse_order(&self) -> bool {
468 self.reverse_order
469 }
470
471 #[cfg(not(feature = "lt2017_2"))]
478 pub fn set_reverse_order(&mut self, v: bool) -> &mut Self {
479 self.reverse_order = v;
480 self
481 }
482
483 #[cfg(not(feature = "lt2017_2"))]
490 pub fn reverse_order(mut self, v: bool) -> Self {
491 self.reverse_order = v;
492 self
493 }
494
495 pub fn get_status(&self) -> Option<Status> {
502 self.filter_status
503 }
504
505 pub fn set_status(&mut self, v: Status) -> &mut Self {
512 self.filter_status = Some(v);
513 self
514 }
515
516 pub fn status(mut self, v: Status) -> Self {
523 self.filter_status = Some(v);
524 self
525 }
526
527 pub fn get_include_time(&self) -> bool {
533 self.include_time
534 }
535
536 pub fn set_include_time(&mut self, v: bool) -> &mut Self {
542 self.include_time = v;
543 self
544 }
545
546 pub fn include_time(mut self, v: bool) -> Self {
552 self.include_time = v;
553 self
554 }
555
556 pub fn get_filter_users(&self) -> Option<&[User]> {
564 self.filter_users.as_deref()
565 }
566
567 pub fn set_filter_user(&mut self, v: impl Into<String>) -> &mut Self {
574 self.filter_users
575 .get_or_insert_with(Vec::new)
576 .push(User::User(v.into()));
577 self
578 }
579
580 pub fn filter_user(mut self, v: impl Into<String>) -> Self {
587 self.filter_users
588 .get_or_insert_with(Vec::new)
589 .push(User::User(v.into()));
590 self
591 }
592
593 #[cfg(not(feature = "lt2016_1"))]
599 pub fn set_filter_me(&mut self) -> &mut Self {
600 self.filter_users
601 .get_or_insert_with(Vec::new)
602 .push(User::Me);
603 self
604 }
605
606 #[cfg(not(feature = "lt2016_1"))]
612 pub fn filter_me(mut self) -> Self {
613 self.filter_users
614 .get_or_insert_with(Vec::new)
615 .push(User::Me);
616 self
617 }
618
619 #[cfg(not(feature = "lt2025_1"))]
626 pub fn get_client_case_insensitive(&self) -> bool {
627 self.client_case_insensitive
628 }
629
630 #[cfg(not(feature = "lt2025_1"))]
637 pub fn set_client_case_insensitive(&mut self, v: bool) -> &mut Self {
638 self.client_case_insensitive = v;
639 self
640 }
641
642 #[cfg(not(feature = "lt2025_1"))]
649 pub fn client_case_insensitive(mut self, v: bool) -> Self {
650 self.client_case_insensitive = v;
651 self
652 }
653
654 #[cfg(not(feature = "lt2022_2"))]
662 pub fn get_stream(&self) -> Option<bool> {
663 self.stream
664 }
665
666 #[cfg(not(feature = "lt2022_2"))]
674 pub fn set_stream(&mut self, v: bool) -> &mut Self {
675 self.stream = Some(v);
676 self
677 }
678
679 #[cfg(not(feature = "lt2022_2"))]
687 pub fn stream(mut self, v: bool) -> Self {
688 self.stream = Some(v);
689 self
690 }
691}
692
693impl<L: ExclusiveOption> SubCommand for Changes<L> {
694 fn name(&self) -> &str {
695 "changes"
696 }
697
698 fn inject_local_args(&self, command: &mut std::process::Command) {
699 if let Some(ref clients) = self.filter_clients {
700 for client in clients {
701 command.arg("-c").arg(client);
702 }
703 }
704 #[cfg(not(feature = "lt2025_1"))]
705 if self.client_case_insensitive {
706 command.arg("--client-case-insensitive");
707 }
708 #[cfg(not(feature = "lt2015_2"))]
709 if let Some(ref min_change) = self.min_change_list {
710 command.arg("-e").arg(min_change);
711 }
712 if self.include_restricted {
713 command.arg("-f");
714 }
715 if self.include_integrated {
716 command.arg("-i");
717 }
718 self.long_output.inject_args(command);
719 if let Some(max) = self.limit {
720 command.arg("-m").arg(max.to_string());
721 }
722 #[cfg(not(feature = "lt2017_2"))]
723 if self.reverse_order {
724 command.arg("-r");
725 }
726 if let Some(status) = self.filter_status {
727 command.arg("-s").arg(status.as_str());
728 }
729 if self.include_time {
730 command.arg("-t");
731 }
732 if let Some(ref users) = self.filter_users {
733 for user in users {
734 match user {
735 User::User(name) => {
736 command.arg("-u").arg(name);
737 }
738 #[cfg(not(feature = "lt2016_1"))]
739 User::Me => {
740 command.arg("--me");
741 }
742 }
743 }
744 }
745 #[cfg(not(feature = "lt2022_2"))]
746 if let Some(stream) = self.stream {
747 if stream {
748 command.arg("--stream");
749 } else {
750 command.arg("--nostream");
751 }
752 }
753 }
754
755 fn global_opts(&self) -> Option<&GlobalOpts> {
756 Some(&self.global_opts)
757 }
758}
759
760#[cfg(test)]
761mod tests {
762 use super::*;
763 use crate::cmd::args_of;
764
765 #[test]
766 fn without_options() {
767 let changes = Changes::new("p4", GlobalOpts::default());
768 let cmd = changes.setup_command("p4");
769 assert_eq!(args_of(&cmd), vec!["changes"]);
770 }
771
772 #[test]
773 fn with_files() {
774 let changes = Changes::new("p4", GlobalOpts::default());
775 let mut cmd = changes.setup_command("p4");
776 cmd.arg("//depot/project/...");
777 assert_eq!(args_of(&cmd), vec!["changes", "//depot/project/..."]);
778 }
779
780 #[test]
781 fn client() {
782 let changes = Changes::new("p4", GlobalOpts::default()).filter_client("eds_elm");
783 let cmd = changes.setup_command("p4");
784 assert_eq!(args_of(&cmd), vec!["changes", "-c", "eds_elm"]);
785 }
786
787 #[test]
788 fn multiple_clients() {
789 let changes = Changes::new("p4", GlobalOpts::default())
790 .filter_client("eds_elm")
791 .filter_client("build_ws");
792 let cmd = changes.setup_command("p4");
793 assert_eq!(
794 args_of(&cmd),
795 vec!["changes", "-c", "eds_elm", "-c", "build_ws"]
796 );
797 }
798
799 #[cfg(not(feature = "lt2015_2"))]
800 #[test]
801 fn min_change_list() {
802 let changes = Changes::new("p4", GlobalOpts::default()).min_change_list("800");
803 let cmd = changes.setup_command("p4");
804 assert_eq!(args_of(&cmd), vec!["changes", "-e", "800"]);
805 }
806
807 #[test]
808 fn include_restricted() {
809 let changes = Changes::new("p4", GlobalOpts::default()).include_restricted(true);
810 let cmd = changes.setup_command("p4");
811 assert_eq!(args_of(&cmd), vec!["changes", "-f"]);
812 }
813
814 #[test]
815 fn include_integrated() {
816 let changes = Changes::new("p4", GlobalOpts::default()).include_integrated(true);
817 let cmd = changes.setup_command("p4");
818 assert_eq!(args_of(&cmd), vec!["changes", "-i"]);
819 }
820
821 #[test]
822 fn long_output_full() {
823 let changes = Changes::new("p4", GlobalOpts::default()).long_output_full();
824 let cmd = changes.setup_command("p4");
825 assert_eq!(args_of(&cmd), vec!["changes", "-l"]);
826 }
827
828 #[test]
829 fn long_output_truncated() {
830 let changes = Changes::new("p4", GlobalOpts::default()).long_output_truncated();
831 let cmd = changes.setup_command("p4");
832 assert_eq!(args_of(&cmd), vec!["changes", "-L"]);
833 }
834
835 #[test]
836 fn limit() {
837 let changes = Changes::new("p4", GlobalOpts::default()).limit(5);
838 let cmd = changes.setup_command("p4");
839 assert_eq!(args_of(&cmd), vec!["changes", "-m", "5"]);
840 }
841
842 #[cfg(not(feature = "lt2017_2"))]
843 #[test]
844 fn reverse_order() {
845 let changes = Changes::new("p4", GlobalOpts::default()).reverse_order(true);
846 let cmd = changes.setup_command("p4");
847 assert_eq!(args_of(&cmd), vec!["changes", "-r"]);
848 }
849
850 #[test]
851 fn status_pending() {
852 let changes = Changes::new("p4", GlobalOpts::default()).status(Status::Pending);
853 let cmd = changes.setup_command("p4");
854 assert_eq!(args_of(&cmd), vec!["changes", "-s", "pending"]);
855 }
856
857 #[test]
858 fn status_submitted() {
859 let changes = Changes::new("p4", GlobalOpts::default()).status(Status::Submitted);
860 let cmd = changes.setup_command("p4");
861 assert_eq!(args_of(&cmd), vec!["changes", "-s", "submitted"]);
862 }
863
864 #[test]
865 fn status_shelved() {
866 let changes = Changes::new("p4", GlobalOpts::default()).status(Status::Shelved);
867 let cmd = changes.setup_command("p4");
868 assert_eq!(args_of(&cmd), vec!["changes", "-s", "shelved"]);
869 }
870
871 #[test]
872 fn include_time() {
873 let changes = Changes::new("p4", GlobalOpts::default()).include_time(true);
874 let cmd = changes.setup_command("p4");
875 assert_eq!(args_of(&cmd), vec!["changes", "-t"]);
876 }
877
878 #[test]
879 fn user_name() {
880 let changes = Changes::new("p4", GlobalOpts::default()).filter_user("edk");
881 let cmd = changes.setup_command("p4");
882 assert_eq!(args_of(&cmd), vec!["changes", "-u", "edk"]);
883 }
884
885 #[test]
886 fn multiple_users() {
887 let changes = Changes::new("p4", GlobalOpts::default())
888 .filter_user("maria")
889 .filter_user("edk");
890 let cmd = changes.setup_command("p4");
891 assert_eq!(args_of(&cmd), vec!["changes", "-u", "maria", "-u", "edk"]);
892 }
893
894 #[cfg(not(feature = "lt2016_1"))]
895 #[test]
896 fn user_me() {
897 let changes = Changes::new("p4", GlobalOpts::default()).filter_me();
898 let cmd = changes.setup_command("p4");
899 assert_eq!(args_of(&cmd), vec!["changes", "--me"]);
900 }
901
902 #[cfg(not(feature = "lt2016_1"))]
903 #[test]
904 fn multiple_users_with_me() {
905 let changes = Changes::new("p4", GlobalOpts::default())
906 .filter_user("maria")
907 .filter_me();
908 let cmd = changes.setup_command("p4");
909 assert_eq!(args_of(&cmd), vec!["changes", "-u", "maria", "--me"]);
910 }
911
912 #[cfg(not(feature = "lt2025_1"))]
913 #[test]
914 fn client_case_insensitive() {
915 let changes = Changes::new("p4", GlobalOpts::default())
916 .filter_client("eds_elm")
917 .client_case_insensitive(true);
918 let cmd = changes.setup_command("p4");
919 assert_eq!(
920 args_of(&cmd),
921 vec!["changes", "-c", "eds_elm", "--client-case-insensitive"]
922 );
923 }
924
925 #[cfg(not(feature = "lt2022_2"))]
926 #[test]
927 fn stream_spec() {
928 let changes = Changes::new("p4", GlobalOpts::default()).stream(true);
929 let cmd = changes.setup_command("p4");
930 assert_eq!(args_of(&cmd), vec!["changes", "--stream"]);
931 }
932
933 #[cfg(not(feature = "lt2022_2"))]
934 #[test]
935 fn no_stream_spec() {
936 let changes = Changes::new("p4", GlobalOpts::default()).stream(false);
937 let cmd = changes.setup_command("p4");
938 assert_eq!(args_of(&cmd), vec!["changes", "--nostream"]);
939 }
940
941 #[test]
942 fn all_options_order() {
943 let changes = Changes::new("p4", GlobalOpts::default())
944 .filter_client("eds_elm")
945 .include_restricted(true)
946 .include_integrated(true)
947 .long_output_full()
948 .limit(5)
949 .status(Status::Submitted)
950 .include_time(true)
951 .filter_user("edk");
952 #[cfg(not(feature = "lt2015_2"))]
953 let changes = changes.min_change_list("800");
954 #[cfg(not(feature = "lt2017_2"))]
955 let changes = changes.reverse_order(true);
956 #[cfg(not(feature = "lt2022_2"))]
957 let changes = changes.stream(true);
958 let cmd = changes.setup_command("p4");
959 let mut expected = vec!["changes", "-c", "eds_elm"];
960 #[cfg(not(feature = "lt2015_2"))]
961 expected.extend(["-e", "800"]);
962 expected.extend(["-f", "-i", "-l", "-m", "5"]);
963 #[cfg(not(feature = "lt2017_2"))]
964 expected.push("-r");
965 expected.extend(["-s", "submitted", "-t", "-u", "edk"]);
966 #[cfg(not(feature = "lt2022_2"))]
967 expected.push("--stream");
968 assert_eq!(args_of(&cmd), expected);
969 }
970
971 #[test]
972 fn long_output_full_preserves_other_options() {
973 let changes = Changes::new("p4", GlobalOpts::default())
974 .include_restricted(true)
975 .include_time(true)
976 .long_output_full();
977 let cmd = changes.setup_command("p4");
978 assert_eq!(args_of(&cmd), vec!["changes", "-f", "-l", "-t"]);
979 }
980}