1use std::ffi::OsStr;
2use std::io::Write;
3#[cfg(not(feature = "lt2024_2"))]
4use std::path::Path;
5use std::path::PathBuf;
6use std::process::{Child, Command, Output, Stdio};
7
8use super::{ExclusiveOption, SubCommand, Unselected};
9
10use crate::global::GlobalOpts;
11use crate::spawn::ParameterizedSpawn;
12
13#[derive(Debug, Clone, Default)]
19pub struct Standard {
20 values: Option<Vec<(String, Option<String>)>>,
21
22 hex: bool,
23}
24
25impl ExclusiveOption for Standard {
26 fn inject_args(&self, command: &mut Command) {
27 if self.hex {
28 command.arg("-e");
29 }
30
31 if let Some(pairs) = &self.values {
32 for (name, value) in pairs {
33 command.arg("-n").arg(name);
34
35 if let Some(value) = value {
36 command.arg("-v").arg(value);
37 }
38 }
39 }
40 }
41}
42
43#[derive(Debug, Clone, Default)]
48pub struct FromStdin {
49 hex: bool,
50
51 name: String,
52}
53
54impl ExclusiveOption for FromStdin {
55 fn inject_args(&self, command: &mut Command) {
56 if self.hex {
57 command.arg("-e");
58 }
59
60 command.arg("-i").arg("-n").arg(&self.name);
61 }
62}
63
64#[cfg(not(feature = "lt2024_2"))]
69#[derive(Debug, Clone, Default)]
70pub struct FromFile {
71 name: String,
72
73 file: PathBuf,
74}
75
76#[cfg(not(feature = "lt2024_2"))]
77impl ExclusiveOption for FromFile {
78 fn inject_args(&self, command: &mut Command) {
79 command.arg("-I").arg(&self.file).arg("-n").arg(&self.name);
80 }
81}
82
83#[cfg(not(feature = "lt2023_2"))]
86pub mod storage {
87 #[derive(Debug, Clone, Copy, Default)]
89 pub struct DatabaseTraits;
90
91 #[derive(Debug, Clone, Copy, Default)]
93 pub struct TraitDepot;
94}
95
96#[cfg(not(feature = "lt2023_2"))]
97impl ExclusiveOption for storage::DatabaseTraits {
98 fn inject_args(&self, command: &mut Command) {
99 command.arg("-T0");
100 }
101}
102
103#[cfg(not(feature = "lt2023_2"))]
104impl ExclusiveOption for storage::TraitDepot {
105 fn inject_args(&self, command: &mut Command) {
106 command.arg("-T1");
107 }
108}
109
110#[cfg_attr(
111 feature = "lt2023_2",
112 doc = "`p4 [g-opts] attribute [-e -f -p] -n name [-v value] files ...`: set per-revision attributes on revisions.\n\n`p4 [g-opts] attribute [-e -f -p] -i -n name file`"
113)]
114#[cfg_attr(
115 all(feature = "lt2024_2", not(feature = "lt2023_2")),
116 doc = "`p4 [g-opts] attribute [-e -f -p] -n name [-v value [-T0 | -T1]] files ...`: set per-revision attributes on revisions.\n\n`p4 [g-opts] attribute [-e -f -p [-T0 | -T1]] -i -n name file`"
117)]
118#[cfg_attr(
119 not(feature = "lt2024_2"),
120 doc = "`p4 [g-opts] attribute [-e -f -p] -n name [-v value [-T0 | -T1]] files ...`: set per-revision attributes on revisions.\n\n`p4 [g-opts] attribute [-e -f -p [-T0 | -T1]] -i -n name file`\n\n`p4 attribute [-f -p [-T0 | -T1]] -I filename -n name file`"
121)]
122#[cfg_attr(
126 not(feature = "lt2024_2"),
127 doc = "[`FromFile`] for the `-I filename` form, and"
128)]
129#[derive(Debug, Clone, Default)]
131pub struct Attribute<S = Standard, T = Unselected> {
132 bin: PathBuf,
133
134 global_opts: GlobalOpts,
135
136 source: S,
137
138 storage: T,
139
140 on_submitted_files: bool,
141
142 propagating: bool,
143}
144
145impl<S: ExclusiveOption, T: ExclusiveOption> SubCommand for Attribute<S, T> {
146 fn name(&self) -> &str {
147 "attribute"
148 }
149
150 fn inject_local_args(&self, command: &mut Command) {
151 if self.on_submitted_files {
152 command.arg("-f");
153 }
154
155 if self.propagating {
156 command.arg("-p");
157 }
158
159 self.storage.inject_args(command);
160
161 self.source.inject_args(command);
162 }
163
164 fn global_opts(&self) -> Option<&GlobalOpts> {
165 Some(&self.global_opts)
166 }
167}
168
169impl Attribute<Standard, Unselected> {
170 pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
174 Self {
175 bin: bin.into(),
176 global_opts,
177 on_submitted_files: false,
178 propagating: false,
179 source: Standard::default(),
180 storage: Unselected,
181 }
182 }
183}
184
185impl<T: ExclusiveOption, S, I> ParameterizedSpawn<(S,)> for Attribute<Standard, T>
186where
187 S: IntoIterator<Item = I>,
188 I: AsRef<OsStr>,
189{
190 type Output = Child;
191 type Error = std::io::Error;
192
193 fn spawn_with(&mut self, (files,): (S,)) -> Result<Self::Output, Self::Error> {
197 self.setup_command(&self.bin)
198 .args(files)
199 .stdout(Stdio::piped())
200 .stderr(Stdio::piped())
201 .spawn()
202 }
203}
204
205impl<T: ExclusiveOption> Attribute<Standard, T> {
206 #[cfg_attr(not(feature = "lt2024_2"), doc = "The supplied value must be text.")]
212 pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
213 self.source
214 .values
215 .get_or_insert_with(Vec::new)
216 .push((name.into(), Some(value.into())));
217 self
218 }
219
220 pub fn clear(&mut self, name: impl Into<String>) -> &mut Self {
227 self.source
228 .values
229 .get_or_insert_with(Vec::new)
230 .push((name.into(), None));
231 self
232 }
233
234 pub fn get_hex(&self) -> bool {
240 self.source.hex
241 }
242
243 pub fn set_hex(&mut self, v: bool) -> &mut Self {
249 self.source.hex = v;
250 self
251 }
252
253 pub fn hex(mut self, v: bool) -> Self {
259 self.source.hex = v;
260 self
261 }
262
263 #[cfg_attr(
268 feature = "lt2024_2",
269 doc = "Read an attribute value from the standard input. Only one file argument is allowed when using this option."
270 )]
271 #[cfg_attr(
272 not(feature = "lt2024_2"),
273 doc = "Read an attribute value from the standard input. Only one file argument is allowed when using this option. This option supports both textual and binary content."
274 )]
275 pub fn read_from_stdin(self, name: String) -> Attribute<FromStdin, T> {
280 Attribute {
281 bin: self.bin,
282 global_opts: self.global_opts,
283 on_submitted_files: self.on_submitted_files,
284 propagating: self.propagating,
285 source: FromStdin {
286 hex: self.source.hex,
287 name,
288 },
289 storage: self.storage,
290 }
291 }
292
293 #[cfg(not(feature = "lt2024_2"))]
314 pub fn read_from_file(self, name: String, file: PathBuf) -> Attribute<FromFile, T> {
315 Attribute {
316 bin: self.bin,
317 global_opts: self.global_opts,
318 on_submitted_files: self.on_submitted_files,
319 propagating: self.propagating,
320 source: FromFile { name, file },
321 storage: self.storage,
322 }
323 }
324}
325
326impl<T: ExclusiveOption> Attribute<FromStdin, T> {
327 pub fn get_hex(&self) -> bool {
333 self.source.hex
334 }
335
336 pub fn set_hex(&mut self, v: bool) -> &mut Self {
342 self.source.hex = v;
343 self
344 }
345
346 pub fn hex(mut self, v: bool) -> Self {
352 self.source.hex = v;
353 self
354 }
355
356 pub fn get_name(&self) -> &str {
362 &self.source.name
363 }
364
365 pub fn set_name(&mut self, v: impl Into<String>) -> &mut Self {
371 self.source.name = v.into();
372 self
373 }
374
375 pub fn name(mut self, v: impl Into<String>) -> Self {
381 self.source.name = v.into();
382 self
383 }
384
385 pub fn output<S, V>(&self, file: S, value: V) -> Result<Output, std::io::Error>
394 where
395 S: AsRef<OsStr>,
396 V: AsRef<[u8]>,
397 {
398 let mut child = self
399 .setup_command(&self.bin)
400 .arg(file)
401 .stdin(Stdio::piped())
402 .stdout(Stdio::piped())
403 .stderr(Stdio::piped())
404 .spawn()?;
405
406 if let Some(ref mut stdin) = child.stdin {
407 stdin.write_all(value.as_ref())?;
408 }
409 drop(child.stdin.take());
410
411 child.wait_with_output()
412 }
413}
414
415impl<T: ExclusiveOption, I> ParameterizedSpawn<(I, Stdio)> for Attribute<FromStdin, T>
416where
417 I: AsRef<OsStr>,
418{
419 type Output = Child;
420 type Error = std::io::Error;
421
422 fn spawn_with(&mut self, input: (I, Stdio)) -> Result<Self::Output, Self::Error> {
432 self.setup_command(&self.bin)
433 .arg(input.0)
434 .stdin(input.1)
435 .stdout(Stdio::piped())
436 .stderr(Stdio::piped())
437 .spawn()
438 }
439}
440
441impl<T: ExclusiveOption, I> ParameterizedSpawn<(I,)> for Attribute<FromStdin, T>
442where
443 I: AsRef<OsStr>,
444{
445 type Output = Child;
446 type Error = std::io::Error;
447
448 fn spawn_with(&mut self, (input,): (I,)) -> Result<Self::Output, Self::Error> {
453 self.setup_command(&self.bin)
454 .arg(input)
455 .stdin(Stdio::piped())
456 .stdout(Stdio::piped())
457 .stderr(Stdio::piped())
458 .spawn()
459 }
460}
461
462#[cfg(not(feature = "lt2024_2"))]
463impl<T: ExclusiveOption> Attribute<FromFile, T> {
464 pub fn get_name(&self) -> &str {
470 &self.source.name
471 }
472
473 pub fn set_name(&mut self, v: impl Into<String>) -> &mut Self {
479 self.source.name = v.into();
480 self
481 }
482
483 pub fn name(mut self, v: impl Into<String>) -> Self {
489 self.source.name = v.into();
490 self
491 }
492
493 pub fn get_file(&self) -> &Path {
499 &self.source.file
500 }
501}
502
503#[cfg(not(feature = "lt2024_2"))]
504impl<T: ExclusiveOption, I> ParameterizedSpawn<(I,)> for Attribute<FromFile, T>
505where
506 I: AsRef<OsStr>,
507{
508 type Output = Child;
509 type Error = std::io::Error;
510
511 fn spawn_with(&mut self, (file,): (I,)) -> Result<Self::Output, Self::Error> {
517 self.setup_command(&self.bin)
518 .arg(file)
519 .stdout(Stdio::piped())
520 .stderr(Stdio::piped())
521 .spawn()
522 }
523}
524
525impl<S: ExclusiveOption, T: ExclusiveOption> Attribute<S, T> {
526 #[cfg_attr(
531 feature = "lt2014_2",
532 doc = "See the [Global Options](GlobalOpts) section."
533 )]
534 #[cfg_attr(
535 all(feature = "lt2015_1", not(feature = "lt2014_2")),
536 doc = "See the [“Global Options”](GlobalOpts) section."
537 )]
538 #[cfg_attr(
539 all(feature = "lt2017_1", not(feature = "lt2015_1")),
540 doc = "See [“Global Options”](GlobalOpts)."
541 )]
542 #[cfg_attr(
543 all(feature = "lt2018_2", not(feature = "lt2017_1")),
544 doc = "See [Global Options](GlobalOpts)."
545 )]
546 #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
547 pub fn get_global_opts(&self) -> &GlobalOpts {
548 &self.global_opts
549 }
550
551 #[cfg_attr(
556 feature = "lt2014_2",
557 doc = "See the [Global Options](GlobalOpts) section."
558 )]
559 #[cfg_attr(
560 all(feature = "lt2015_1", not(feature = "lt2014_2")),
561 doc = "See the [“Global Options”](GlobalOpts) section."
562 )]
563 #[cfg_attr(
564 all(feature = "lt2017_1", not(feature = "lt2015_1")),
565 doc = "See [“Global Options”](GlobalOpts)."
566 )]
567 #[cfg_attr(
568 all(feature = "lt2018_2", not(feature = "lt2017_1")),
569 doc = "See [Global Options](GlobalOpts)."
570 )]
571 #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
572 pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
573 self.global_opts = v;
574 self
575 }
576
577 #[cfg_attr(
582 feature = "lt2014_2",
583 doc = "See the [Global Options](GlobalOpts) section."
584 )]
585 #[cfg_attr(
586 all(feature = "lt2015_1", not(feature = "lt2014_2")),
587 doc = "See the [“Global Options”](GlobalOpts) section."
588 )]
589 #[cfg_attr(
590 all(feature = "lt2017_1", not(feature = "lt2015_1")),
591 doc = "See [“Global Options”](GlobalOpts)."
592 )]
593 #[cfg_attr(
594 all(feature = "lt2018_2", not(feature = "lt2017_1")),
595 doc = "See [Global Options](GlobalOpts)."
596 )]
597 #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
598 pub fn global_opts(mut self, v: GlobalOpts) -> Self {
599 self.global_opts = v;
600 self
601 }
602
603 pub fn get_on_submitted_files(&self) -> bool {
611 self.on_submitted_files
612 }
613
614 pub fn set_on_submitted_files(&mut self, v: bool) -> &mut Self {
622 self.on_submitted_files = v;
623 self
624 }
625
626 pub fn on_submitted_files(mut self, v: bool) -> Self {
634 self.on_submitted_files = v;
635 self
636 }
637
638 #[cfg_attr(
643 feature = "lt2021_2",
644 doc = "Create a propagating attribute: an attribute whose value is propagated to subsequent revisions whenever the file is opened with `p4 add`, `p4 edit`, or `p4 delete`."
645 )]
646 #[cfg_attr(
647 not(feature = "lt2021_2"),
648 doc = "Create a *propagating attribute*: an attribute whose value is propagated to subsequent revisions whenever the file is opened. Relevant commands include `p4 copy`, `p4 delete`, `p4 edit`, `p4 integrate`, `p4 reconcile`, `p4 resolve`, `p4 shelve`, `p4 submit`, and `p4 unshelve`."
649 )]
650 pub fn get_propagating(&self) -> bool {
651 self.propagating
652 }
653
654 #[cfg_attr(
659 feature = "lt2021_2",
660 doc = "Create a propagating attribute: an attribute whose value is propagated to subsequent revisions whenever the file is opened with `p4 add`, `p4 edit`, or `p4 delete`."
661 )]
662 #[cfg_attr(
663 not(feature = "lt2021_2"),
664 doc = "Create a *propagating attribute*: an attribute whose value is propagated to subsequent revisions whenever the file is opened. Relevant commands include `p4 copy`, `p4 delete`, `p4 edit`, `p4 integrate`, `p4 reconcile`, `p4 resolve`, `p4 shelve`, `p4 submit`, and `p4 unshelve`."
665 )]
666 pub fn set_propagating(&mut self, v: bool) -> &mut Self {
667 self.propagating = v;
668 self
669 }
670
671 #[cfg_attr(
676 feature = "lt2021_2",
677 doc = "Create a propagating attribute: an attribute whose value is propagated to subsequent revisions whenever the file is opened with `p4 add`, `p4 edit`, or `p4 delete`."
678 )]
679 #[cfg_attr(
680 not(feature = "lt2021_2"),
681 doc = "Create a *propagating attribute*: an attribute whose value is propagated to subsequent revisions whenever the file is opened. Relevant commands include `p4 copy`, `p4 delete`, `p4 edit`, `p4 integrate`, `p4 reconcile`, `p4 resolve`, `p4 shelve`, `p4 submit`, and `p4 unshelve`."
682 )]
683 pub fn propagating(mut self, v: bool) -> Self {
684 self.propagating = v;
685 self
686 }
687}
688
689#[cfg(not(feature = "lt2023_2"))]
690impl<S: ExclusiveOption, T: ExclusiveOption> Attribute<S, T> {
691 pub fn store_in_database_traits(self) -> Attribute<S, storage::DatabaseTraits> {
698 Attribute {
699 bin: self.bin,
700 global_opts: self.global_opts,
701 on_submitted_files: self.on_submitted_files,
702 propagating: self.propagating,
703 source: self.source,
704 storage: storage::DatabaseTraits,
705 }
706 }
707
708 pub fn store_in_trait_depot(self) -> Attribute<S, storage::TraitDepot> {
718 Attribute {
719 bin: self.bin,
720 global_opts: self.global_opts,
721 on_submitted_files: self.on_submitted_files,
722 propagating: self.propagating,
723 source: self.source,
724 storage: storage::TraitDepot,
725 }
726 }
727}
728
729#[cfg(test)]
730mod tests {
731 use super::*;
732 use crate::cmd::args_of;
733
734 #[test]
737 fn without_options() {
738 let attr = Attribute::new("p4", GlobalOpts::new());
739
740 assert_eq!(args_of(&attr.setup_command("p4")), ["attribute"]);
741 }
742
743 #[test]
744 fn set_attribute_with_value() {
745 let mut attr = Attribute::new("p4", GlobalOpts::new());
746 attr.set("status", "approved");
747
748 assert_eq!(
749 args_of(&attr.setup_command("p4")),
750 ["attribute", "-n", "status", "-v", "approved"]
751 );
752 }
753
754 #[test]
755 fn clear_attribute_omits_value() {
756 let mut attr = Attribute::new("p4", GlobalOpts::new());
757 attr.clear("status");
758
759 assert_eq!(
760 args_of(&attr.setup_command("p4")),
761 ["attribute", "-n", "status"]
762 );
763 }
764
765 #[test]
766 fn multiple_set_and_clear_pairs_keep_order() {
767 let mut attr = Attribute::new("p4", GlobalOpts::new());
768 attr.set("color", "red")
769 .clear("status")
770 .set("owner", "alice");
771
772 assert_eq!(
773 args_of(&attr.setup_command("p4")),
774 [
775 "attribute",
776 "-n",
777 "color",
778 "-v",
779 "red",
780 "-n",
781 "status",
782 "-n",
783 "owner",
784 "-v",
785 "alice"
786 ]
787 );
788 }
789
790 #[test]
791 fn hex_submitted_propagating() {
792 let mut attr = Attribute::new("p4", GlobalOpts::new());
793 attr.set_hex(true)
794 .set_on_submitted_files(true)
795 .set_propagating(true)
796 .set("thumb", "deadbeef");
797
798 assert_eq!(
799 args_of(&attr.setup_command("p4")),
800 [
801 "attribute",
802 "-f",
803 "-p",
804 "-e",
805 "-n",
806 "thumb",
807 "-v",
808 "deadbeef"
809 ]
810 );
811 }
812
813 #[test]
814 fn read_from_stdin_state() {
815 let attr = Attribute::new("p4", GlobalOpts::new()).read_from_stdin("thumb".to_string());
816
817 assert_eq!(attr.get_name(), "thumb");
818 assert_eq!(
819 args_of(&attr.setup_command("p4")),
820 ["attribute", "-i", "-n", "thumb"]
821 );
822 }
823
824 #[test]
825 fn stdin_name_can_be_replaced() {
826 let mut attr = Attribute::new("p4", GlobalOpts::new()).read_from_stdin("old".to_string());
827 attr.set_name("thumb");
828
829 assert_eq!(attr.get_name(), "thumb");
830
831 let attr = attr.name("icon");
832 assert_eq!(attr.get_name(), "icon");
833 assert_eq!(
834 args_of(&attr.setup_command("p4")),
835 ["attribute", "-i", "-n", "icon"]
836 );
837 }
838
839 #[test]
840 fn stdin_preserves_flags_and_hex() {
841 let mut attr = Attribute::new("p4", GlobalOpts::new());
842 attr.set_hex(true)
843 .set_on_submitted_files(true)
844 .set_propagating(true)
845 .set("discarded", "value");
846
847 let attr = attr.read_from_stdin("thumb".to_string());
848
849 assert!(attr.get_hex());
850 assert_eq!(
851 args_of(&attr.setup_command("p4")),
852 ["attribute", "-f", "-p", "-e", "-i", "-n", "thumb"]
853 );
854 }
855
856 #[cfg(not(feature = "lt2023_2"))]
857 #[test]
858 fn store_in_database_traits() {
859 let mut attr = Attribute::new("p4", GlobalOpts::new());
860 attr.set("thumb", "data");
861
862 let attr = attr.store_in_database_traits();
863
864 assert_eq!(
865 args_of(&attr.setup_command("p4")),
866 ["attribute", "-T0", "-n", "thumb", "-v", "data"]
867 );
868 }
869
870 #[cfg(not(feature = "lt2023_2"))]
871 #[test]
872 fn store_in_trait_depot() {
873 let mut attr = Attribute::new("p4", GlobalOpts::new());
874 attr.set("thumb", "data");
875
876 let attr = attr.store_in_trait_depot();
877
878 assert_eq!(
879 args_of(&attr.setup_command("p4")),
880 ["attribute", "-T1", "-n", "thumb", "-v", "data"]
881 );
882 }
883
884 #[cfg(not(feature = "lt2023_2"))]
885 #[test]
886 fn stdin_with_trait_depot() {
887 let attr = Attribute::new("p4", GlobalOpts::new())
888 .read_from_stdin("thumb".to_string())
889 .store_in_trait_depot();
890
891 assert_eq!(
892 args_of(&attr.setup_command("p4")),
893 ["attribute", "-T1", "-i", "-n", "thumb"]
894 );
895 }
896
897 #[cfg(not(feature = "lt2024_2"))]
898 #[test]
899 fn read_from_file_state() {
900 let attr = Attribute::new("p4", GlobalOpts::new())
901 .read_from_file("thumb".to_string(), PathBuf::from("/tmp/thumb.bin"));
902
903 assert_eq!(attr.get_name(), "thumb");
904 assert_eq!(attr.get_file(), Path::new("/tmp/thumb.bin"));
905 assert_eq!(
906 args_of(&attr.setup_command("p4")),
907 ["attribute", "-I", "/tmp/thumb.bin", "-n", "thumb"]
908 );
909 }
910
911 #[cfg(not(feature = "lt2024_2"))]
912 #[test]
913 fn all_modern_options() {
914 let mut attr = Attribute::new("p4", GlobalOpts::new());
915 attr.set_on_submitted_files(true).set_propagating(true);
916
917 let attr = attr
918 .store_in_trait_depot()
919 .read_from_file("thumb".to_string(), PathBuf::from("/tmp/data.bin"));
920
921 assert_eq!(
922 args_of(&attr.setup_command("p4")),
923 [
924 "attribute",
925 "-f",
926 "-p",
927 "-T1",
928 "-I",
929 "/tmp/data.bin",
930 "-n",
931 "thumb"
932 ]
933 );
934 }
935
936 #[test]
937 fn set_style_with_global_opts() {
938 let mut attr = Attribute::new("p4", GlobalOpts::new().port("localhost:1666"));
939 attr.set("status", "approved");
940
941 assert_eq!(
942 args_of(&attr.setup_command("p4")),
943 [
944 "-p",
945 "localhost:1666",
946 "attribute",
947 "-n",
948 "status",
949 "-v",
950 "approved"
951 ]
952 );
953 }
954}