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, S, I> ParameterizedSpawn<(S,)> for Changes<L>
202where
203 S: IntoIterator<Item = I>,
204 I: AsRef<OsStr>,
205{
206 type Output = Child;
207 type Error = std::io::Error;
208
209 fn spawn_with(&mut self, (files,): (S,)) -> Result<Self::Output, Self::Error> {
216 self.setup_command(&self.bin)
217 .args(files)
218 .stdout(Stdio::piped())
219 .stderr(Stdio::piped())
220 .spawn()
221 }
222}
223
224impl<L: ExclusiveOption> Changes<L> {
225 #[cfg_attr(
230 feature = "lt2014_2",
231 doc = "See the [Global Options](GlobalOpts) section."
232 )]
233 #[cfg_attr(
234 all(feature = "lt2015_1", not(feature = "lt2014_2")),
235 doc = "See the [“Global Options”](GlobalOpts) section."
236 )]
237 #[cfg_attr(
238 all(feature = "lt2017_1", not(feature = "lt2015_1")),
239 doc = "See [“Global Options”](GlobalOpts)."
240 )]
241 #[cfg_attr(
242 all(feature = "lt2018_2", not(feature = "lt2017_1")),
243 doc = "See [Global Options](GlobalOpts)."
244 )]
245 #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
246 pub fn get_global_opts(&self) -> &GlobalOpts {
247 &self.global_opts
248 }
249
250 #[cfg_attr(
255 feature = "lt2014_2",
256 doc = "See the [Global Options](GlobalOpts) section."
257 )]
258 #[cfg_attr(
259 all(feature = "lt2015_1", not(feature = "lt2014_2")),
260 doc = "See the [“Global Options”](GlobalOpts) section."
261 )]
262 #[cfg_attr(
263 all(feature = "lt2017_1", not(feature = "lt2015_1")),
264 doc = "See [“Global Options”](GlobalOpts)."
265 )]
266 #[cfg_attr(
267 all(feature = "lt2018_2", not(feature = "lt2017_1")),
268 doc = "See [Global Options](GlobalOpts)."
269 )]
270 #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
271 pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
272 self.global_opts = v;
273 self
274 }
275
276 #[cfg_attr(
281 feature = "lt2014_2",
282 doc = "See the [Global Options](GlobalOpts) section."
283 )]
284 #[cfg_attr(
285 all(feature = "lt2015_1", not(feature = "lt2014_2")),
286 doc = "See the [“Global Options”](GlobalOpts) section."
287 )]
288 #[cfg_attr(
289 all(feature = "lt2017_1", not(feature = "lt2015_1")),
290 doc = "See [“Global Options”](GlobalOpts)."
291 )]
292 #[cfg_attr(
293 all(feature = "lt2018_2", not(feature = "lt2017_1")),
294 doc = "See [Global Options](GlobalOpts)."
295 )]
296 #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
297 pub fn global_opts(mut self, v: GlobalOpts) -> Self {
298 self.global_opts = v;
299 self
300 }
301
302 pub fn get_filter_clients(&self) -> Option<&[String]> {
309 self.filter_clients.as_deref()
310 }
311
312 pub fn set_filter_client(&mut self, v: impl Into<String>) -> &mut Self {
319 self.filter_clients
320 .get_or_insert_with(Vec::new)
321 .push(v.into());
322 self
323 }
324
325 pub fn filter_client(mut self, v: impl Into<String>) -> Self {
332 self.filter_clients
333 .get_or_insert_with(Vec::new)
334 .push(v.into());
335 self
336 }
337
338 #[cfg(not(feature = "lt2015_2"))]
345 pub fn get_min_change_list(&self) -> Option<&String> {
346 self.min_change_list.as_ref()
347 }
348
349 #[cfg(not(feature = "lt2015_2"))]
356 pub fn set_min_change_list(&mut self, v: impl Into<String>) -> &mut Self {
357 self.min_change_list = Some(v.into());
358 self
359 }
360
361 #[cfg(not(feature = "lt2015_2"))]
368 pub fn min_change_list(mut self, v: impl Into<String>) -> Self {
369 self.min_change_list = Some(v.into());
370 self
371 }
372
373 pub fn get_include_restricted(&self) -> bool {
379 self.include_restricted
380 }
381
382 pub fn set_include_restricted(&mut self, v: bool) -> &mut Self {
388 self.include_restricted = v;
389 self
390 }
391
392 pub fn include_restricted(mut self, v: bool) -> Self {
398 self.include_restricted = v;
399 self
400 }
401
402 pub fn get_include_integrated(&self) -> bool {
409 self.include_integrated
410 }
411
412 pub fn set_include_integrated(&mut self, v: bool) -> &mut Self {
419 self.include_integrated = v;
420 self
421 }
422
423 pub fn include_integrated(mut self, v: bool) -> Self {
430 self.include_integrated = v;
431 self
432 }
433
434 pub fn get_limit(&self) -> Option<u64> {
440 self.limit
441 }
442
443 pub fn set_limit(&mut self, v: u64) -> &mut Self {
449 self.limit = Some(v);
450 self
451 }
452
453 pub fn limit(mut self, v: u64) -> Self {
459 self.limit = Some(v);
460 self
461 }
462
463 #[cfg(not(feature = "lt2017_2"))]
470 pub fn get_reverse_order(&self) -> bool {
471 self.reverse_order
472 }
473
474 #[cfg(not(feature = "lt2017_2"))]
481 pub fn set_reverse_order(&mut self, v: bool) -> &mut Self {
482 self.reverse_order = v;
483 self
484 }
485
486 #[cfg(not(feature = "lt2017_2"))]
493 pub fn reverse_order(mut self, v: bool) -> Self {
494 self.reverse_order = v;
495 self
496 }
497
498 pub fn get_status(&self) -> Option<Status> {
505 self.filter_status
506 }
507
508 pub fn set_status(&mut self, v: Status) -> &mut Self {
515 self.filter_status = Some(v);
516 self
517 }
518
519 pub fn status(mut self, v: Status) -> Self {
526 self.filter_status = Some(v);
527 self
528 }
529
530 pub fn get_include_time(&self) -> bool {
536 self.include_time
537 }
538
539 pub fn set_include_time(&mut self, v: bool) -> &mut Self {
545 self.include_time = v;
546 self
547 }
548
549 pub fn include_time(mut self, v: bool) -> Self {
555 self.include_time = v;
556 self
557 }
558
559 pub fn get_filter_users(&self) -> Option<&[User]> {
567 self.filter_users.as_deref()
568 }
569
570 pub fn set_filter_user(&mut self, v: impl Into<String>) -> &mut Self {
577 self.filter_users
578 .get_or_insert_with(Vec::new)
579 .push(User::User(v.into()));
580 self
581 }
582
583 pub fn filter_user(mut self, v: impl Into<String>) -> Self {
590 self.filter_users
591 .get_or_insert_with(Vec::new)
592 .push(User::User(v.into()));
593 self
594 }
595
596 #[cfg(not(feature = "lt2016_1"))]
602 pub fn set_filter_me(&mut self) -> &mut Self {
603 self.filter_users
604 .get_or_insert_with(Vec::new)
605 .push(User::Me);
606 self
607 }
608
609 #[cfg(not(feature = "lt2016_1"))]
615 pub fn filter_me(mut self) -> Self {
616 self.filter_users
617 .get_or_insert_with(Vec::new)
618 .push(User::Me);
619 self
620 }
621
622 #[cfg(not(feature = "lt2025_1"))]
629 pub fn get_client_case_insensitive(&self) -> bool {
630 self.client_case_insensitive
631 }
632
633 #[cfg(not(feature = "lt2025_1"))]
640 pub fn set_client_case_insensitive(&mut self, v: bool) -> &mut Self {
641 self.client_case_insensitive = v;
642 self
643 }
644
645 #[cfg(not(feature = "lt2025_1"))]
652 pub fn client_case_insensitive(mut self, v: bool) -> Self {
653 self.client_case_insensitive = v;
654 self
655 }
656
657 #[cfg(not(feature = "lt2022_2"))]
665 pub fn get_stream(&self) -> Option<bool> {
666 self.stream
667 }
668
669 #[cfg(not(feature = "lt2022_2"))]
677 pub fn set_stream(&mut self, v: bool) -> &mut Self {
678 self.stream = Some(v);
679 self
680 }
681
682 #[cfg(not(feature = "lt2022_2"))]
690 pub fn stream(mut self, v: bool) -> Self {
691 self.stream = Some(v);
692 self
693 }
694}
695
696impl<L: ExclusiveOption> SubCommand for Changes<L> {
697 fn name(&self) -> &str {
698 "changes"
699 }
700
701 fn inject_local_args(&self, command: &mut std::process::Command) {
702 if let Some(ref clients) = self.filter_clients {
703 for client in clients {
704 command.arg("-c").arg(client);
705 }
706 }
707 #[cfg(not(feature = "lt2025_1"))]
708 if self.client_case_insensitive {
709 command.arg("--client-case-insensitive");
710 }
711 #[cfg(not(feature = "lt2015_2"))]
712 if let Some(ref min_change) = self.min_change_list {
713 command.arg("-e").arg(min_change);
714 }
715 if self.include_restricted {
716 command.arg("-f");
717 }
718 if self.include_integrated {
719 command.arg("-i");
720 }
721 self.long_output.inject_args(command);
722 if let Some(max) = self.limit {
723 command.arg("-m").arg(max.to_string());
724 }
725 #[cfg(not(feature = "lt2017_2"))]
726 if self.reverse_order {
727 command.arg("-r");
728 }
729 if let Some(status) = self.filter_status {
730 command.arg("-s").arg(status.as_str());
731 }
732 if self.include_time {
733 command.arg("-t");
734 }
735 if let Some(ref users) = self.filter_users {
736 for user in users {
737 match user {
738 User::User(name) => {
739 command.arg("-u").arg(name);
740 }
741 #[cfg(not(feature = "lt2016_1"))]
742 User::Me => {
743 command.arg("--me");
744 }
745 }
746 }
747 }
748 #[cfg(not(feature = "lt2022_2"))]
749 if let Some(stream) = self.stream {
750 if stream {
751 command.arg("--stream");
752 } else {
753 command.arg("--nostream");
754 }
755 }
756 }
757
758 fn global_opts(&self) -> Option<&GlobalOpts> {
759 Some(&self.global_opts)
760 }
761}
762
763#[cfg(test)]
764mod tests {
765 use super::*;
766 use crate::cmd::args_of;
767
768 #[test]
769 fn without_options() {
770 let changes = Changes::new("p4", GlobalOpts::default());
771 let cmd = changes.setup_command("p4");
772 assert_eq!(args_of(&cmd), vec!["changes"]);
773 }
774
775 #[test]
776 fn with_files() {
777 let changes = Changes::new("p4", GlobalOpts::default());
778 let mut cmd = changes.setup_command("p4");
779 cmd.arg("//depot/project/...");
780 assert_eq!(args_of(&cmd), vec!["changes", "//depot/project/..."]);
781 }
782
783 #[test]
784 fn client() {
785 let changes = Changes::new("p4", GlobalOpts::default()).filter_client("eds_elm");
786 let cmd = changes.setup_command("p4");
787 assert_eq!(args_of(&cmd), vec!["changes", "-c", "eds_elm"]);
788 }
789
790 #[test]
791 fn multiple_clients() {
792 let changes = Changes::new("p4", GlobalOpts::default())
793 .filter_client("eds_elm")
794 .filter_client("build_ws");
795 let cmd = changes.setup_command("p4");
796 assert_eq!(
797 args_of(&cmd),
798 vec!["changes", "-c", "eds_elm", "-c", "build_ws"]
799 );
800 }
801
802 #[cfg(not(feature = "lt2015_2"))]
803 #[test]
804 fn min_change_list() {
805 let changes = Changes::new("p4", GlobalOpts::default()).min_change_list("800");
806 let cmd = changes.setup_command("p4");
807 assert_eq!(args_of(&cmd), vec!["changes", "-e", "800"]);
808 }
809
810 #[test]
811 fn include_restricted() {
812 let changes = Changes::new("p4", GlobalOpts::default()).include_restricted(true);
813 let cmd = changes.setup_command("p4");
814 assert_eq!(args_of(&cmd), vec!["changes", "-f"]);
815 }
816
817 #[test]
818 fn include_integrated() {
819 let changes = Changes::new("p4", GlobalOpts::default()).include_integrated(true);
820 let cmd = changes.setup_command("p4");
821 assert_eq!(args_of(&cmd), vec!["changes", "-i"]);
822 }
823
824 #[test]
825 fn long_output_full() {
826 let changes = Changes::new("p4", GlobalOpts::default()).long_output_full();
827 let cmd = changes.setup_command("p4");
828 assert_eq!(args_of(&cmd), vec!["changes", "-l"]);
829 }
830
831 #[test]
832 fn long_output_truncated() {
833 let changes = Changes::new("p4", GlobalOpts::default()).long_output_truncated();
834 let cmd = changes.setup_command("p4");
835 assert_eq!(args_of(&cmd), vec!["changes", "-L"]);
836 }
837
838 #[test]
839 fn limit() {
840 let changes = Changes::new("p4", GlobalOpts::default()).limit(5);
841 let cmd = changes.setup_command("p4");
842 assert_eq!(args_of(&cmd), vec!["changes", "-m", "5"]);
843 }
844
845 #[cfg(not(feature = "lt2017_2"))]
846 #[test]
847 fn reverse_order() {
848 let changes = Changes::new("p4", GlobalOpts::default()).reverse_order(true);
849 let cmd = changes.setup_command("p4");
850 assert_eq!(args_of(&cmd), vec!["changes", "-r"]);
851 }
852
853 #[test]
854 fn status_pending() {
855 let changes = Changes::new("p4", GlobalOpts::default()).status(Status::Pending);
856 let cmd = changes.setup_command("p4");
857 assert_eq!(args_of(&cmd), vec!["changes", "-s", "pending"]);
858 }
859
860 #[test]
861 fn status_submitted() {
862 let changes = Changes::new("p4", GlobalOpts::default()).status(Status::Submitted);
863 let cmd = changes.setup_command("p4");
864 assert_eq!(args_of(&cmd), vec!["changes", "-s", "submitted"]);
865 }
866
867 #[test]
868 fn status_shelved() {
869 let changes = Changes::new("p4", GlobalOpts::default()).status(Status::Shelved);
870 let cmd = changes.setup_command("p4");
871 assert_eq!(args_of(&cmd), vec!["changes", "-s", "shelved"]);
872 }
873
874 #[test]
875 fn include_time() {
876 let changes = Changes::new("p4", GlobalOpts::default()).include_time(true);
877 let cmd = changes.setup_command("p4");
878 assert_eq!(args_of(&cmd), vec!["changes", "-t"]);
879 }
880
881 #[test]
882 fn user_name() {
883 let changes = Changes::new("p4", GlobalOpts::default()).filter_user("edk");
884 let cmd = changes.setup_command("p4");
885 assert_eq!(args_of(&cmd), vec!["changes", "-u", "edk"]);
886 }
887
888 #[test]
889 fn multiple_users() {
890 let changes = Changes::new("p4", GlobalOpts::default())
891 .filter_user("maria")
892 .filter_user("edk");
893 let cmd = changes.setup_command("p4");
894 assert_eq!(args_of(&cmd), vec!["changes", "-u", "maria", "-u", "edk"]);
895 }
896
897 #[cfg(not(feature = "lt2016_1"))]
898 #[test]
899 fn user_me() {
900 let changes = Changes::new("p4", GlobalOpts::default()).filter_me();
901 let cmd = changes.setup_command("p4");
902 assert_eq!(args_of(&cmd), vec!["changes", "--me"]);
903 }
904
905 #[cfg(not(feature = "lt2016_1"))]
906 #[test]
907 fn multiple_users_with_me() {
908 let changes = Changes::new("p4", GlobalOpts::default())
909 .filter_user("maria")
910 .filter_me();
911 let cmd = changes.setup_command("p4");
912 assert_eq!(args_of(&cmd), vec!["changes", "-u", "maria", "--me"]);
913 }
914
915 #[cfg(not(feature = "lt2025_1"))]
916 #[test]
917 fn client_case_insensitive() {
918 let changes = Changes::new("p4", GlobalOpts::default())
919 .filter_client("eds_elm")
920 .client_case_insensitive(true);
921 let cmd = changes.setup_command("p4");
922 assert_eq!(
923 args_of(&cmd),
924 vec!["changes", "-c", "eds_elm", "--client-case-insensitive"]
925 );
926 }
927
928 #[cfg(not(feature = "lt2022_2"))]
929 #[test]
930 fn stream_spec() {
931 let changes = Changes::new("p4", GlobalOpts::default()).stream(true);
932 let cmd = changes.setup_command("p4");
933 assert_eq!(args_of(&cmd), vec!["changes", "--stream"]);
934 }
935
936 #[cfg(not(feature = "lt2022_2"))]
937 #[test]
938 fn no_stream_spec() {
939 let changes = Changes::new("p4", GlobalOpts::default()).stream(false);
940 let cmd = changes.setup_command("p4");
941 assert_eq!(args_of(&cmd), vec!["changes", "--nostream"]);
942 }
943
944 #[test]
945 fn all_options_order() {
946 let changes = Changes::new("p4", GlobalOpts::default())
947 .filter_client("eds_elm")
948 .include_restricted(true)
949 .include_integrated(true)
950 .long_output_full()
951 .limit(5)
952 .status(Status::Submitted)
953 .include_time(true)
954 .filter_user("edk");
955 #[cfg(not(feature = "lt2015_2"))]
956 let changes = changes.min_change_list("800");
957 #[cfg(not(feature = "lt2017_2"))]
958 let changes = changes.reverse_order(true);
959 #[cfg(not(feature = "lt2022_2"))]
960 let changes = changes.stream(true);
961 let cmd = changes.setup_command("p4");
962 let mut expected = vec!["changes", "-c", "eds_elm"];
963 #[cfg(not(feature = "lt2015_2"))]
964 expected.extend(["-e", "800"]);
965 expected.extend(["-f", "-i", "-l", "-m", "5"]);
966 #[cfg(not(feature = "lt2017_2"))]
967 expected.push("-r");
968 expected.extend(["-s", "submitted", "-t", "-u", "edk"]);
969 #[cfg(not(feature = "lt2022_2"))]
970 expected.push("--stream");
971 assert_eq!(args_of(&cmd), expected);
972 }
973
974 #[test]
975 fn long_output_full_preserves_other_options() {
976 let changes = Changes::new("p4", GlobalOpts::default())
977 .include_restricted(true)
978 .include_time(true)
979 .long_output_full();
980 let cmd = changes.setup_command("p4");
981 assert_eq!(args_of(&cmd), vec!["changes", "-f", "-l", "-t"]);
982 }
983}