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> ParameterizedSpawn for Attribute<Standard, T> {
186 type Input<'a> = &'a [&'a OsStr];
187 type Output<'a> = Child;
188 type Error = std::io::Error;
189
190 fn spawn_with<'a>(&mut self, files: Self::Input<'a>) -> Result<Self::Output<'a>, Self::Error> {
194 self.setup_command(&self.bin)
195 .args(files)
196 .stdout(Stdio::piped())
197 .stderr(Stdio::piped())
198 .spawn()
199 }
200}
201
202impl<T: ExclusiveOption> Attribute<Standard, T> {
203 #[cfg_attr(not(feature = "lt2024_2"), doc = "The supplied value must be text.")]
209 pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
210 self.source
211 .values
212 .get_or_insert_with(Vec::new)
213 .push((name.into(), Some(value.into())));
214 self
215 }
216
217 pub fn clear(&mut self, name: impl Into<String>) -> &mut Self {
224 self.source
225 .values
226 .get_or_insert_with(Vec::new)
227 .push((name.into(), None));
228 self
229 }
230
231 pub fn get_hex(&self) -> bool {
237 self.source.hex
238 }
239
240 pub fn set_hex(&mut self, v: bool) -> &mut Self {
246 self.source.hex = v;
247 self
248 }
249
250 pub fn hex(mut self, v: bool) -> Self {
256 self.source.hex = v;
257 self
258 }
259
260 #[cfg_attr(
265 feature = "lt2024_2",
266 doc = "Read an attribute value from the standard input. Only one file argument is allowed when using this option."
267 )]
268 #[cfg_attr(
269 not(feature = "lt2024_2"),
270 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."
271 )]
272 pub fn read_from_stdin(self, name: String) -> Attribute<FromStdin, T> {
277 Attribute {
278 bin: self.bin,
279 global_opts: self.global_opts,
280 on_submitted_files: self.on_submitted_files,
281 propagating: self.propagating,
282 source: FromStdin {
283 hex: self.source.hex,
284 name,
285 },
286 storage: self.storage,
287 }
288 }
289
290 #[cfg(not(feature = "lt2024_2"))]
311 pub fn read_from_file(self, name: String, file: PathBuf) -> Attribute<FromFile, T> {
312 Attribute {
313 bin: self.bin,
314 global_opts: self.global_opts,
315 on_submitted_files: self.on_submitted_files,
316 propagating: self.propagating,
317 source: FromFile { name, file },
318 storage: self.storage,
319 }
320 }
321}
322
323impl<T: ExclusiveOption> Attribute<FromStdin, T> {
324 pub fn get_hex(&self) -> bool {
330 self.source.hex
331 }
332
333 pub fn set_hex(&mut self, v: bool) -> &mut Self {
339 self.source.hex = v;
340 self
341 }
342
343 pub fn hex(mut self, v: bool) -> Self {
349 self.source.hex = v;
350 self
351 }
352
353 pub fn get_name(&self) -> &str {
359 &self.source.name
360 }
361
362 pub fn set_name(&mut self, v: impl Into<String>) -> &mut Self {
368 self.source.name = v.into();
369 self
370 }
371
372 pub fn name(mut self, v: impl Into<String>) -> Self {
378 self.source.name = v.into();
379 self
380 }
381
382 pub fn output<S, V>(&self, file: S, value: V) -> Result<Output, std::io::Error>
391 where
392 S: AsRef<OsStr>,
393 V: AsRef<[u8]>,
394 {
395 let mut child = self
396 .setup_command(&self.bin)
397 .arg(file)
398 .stdin(Stdio::piped())
399 .stdout(Stdio::piped())
400 .stderr(Stdio::piped())
401 .spawn()?;
402
403 if let Some(ref mut stdin) = child.stdin {
404 stdin.write_all(value.as_ref())?;
405 }
406 drop(child.stdin.take());
407
408 child.wait_with_output()
409 }
410}
411
412impl<T: ExclusiveOption> ParameterizedSpawn for Attribute<FromStdin, T> {
413 type Input<'a> = (&'a OsStr, Stdio);
414 type Output<'a> = Child;
415 type Error = std::io::Error;
416
417 fn spawn_with<'a>(&mut self, input: Self::Input<'a>) -> Result<Self::Output<'a>, Self::Error> {
427 self.setup_command(&self.bin)
428 .arg(input.0)
429 .stdin(input.1)
430 .stdout(Stdio::piped())
431 .stderr(Stdio::piped())
432 .spawn()
433 }
434}
435
436#[cfg(not(feature = "lt2024_2"))]
437impl<T: ExclusiveOption> Attribute<FromFile, T> {
438 pub fn get_name(&self) -> &str {
444 &self.source.name
445 }
446
447 pub fn set_name(&mut self, v: impl Into<String>) -> &mut Self {
453 self.source.name = v.into();
454 self
455 }
456
457 pub fn name(mut self, v: impl Into<String>) -> Self {
463 self.source.name = v.into();
464 self
465 }
466
467 pub fn get_file(&self) -> &Path {
473 &self.source.file
474 }
475}
476
477#[cfg(not(feature = "lt2024_2"))]
478impl<T: ExclusiveOption> ParameterizedSpawn for Attribute<FromFile, T> {
479 type Input<'a> = &'a OsStr;
480 type Output<'a> = Child;
481 type Error = std::io::Error;
482
483 fn spawn_with<'a>(&mut self, file: Self::Input<'a>) -> Result<Self::Output<'a>, Self::Error> {
489 self.setup_command(&self.bin)
490 .arg(file)
491 .stdout(Stdio::piped())
492 .stderr(Stdio::piped())
493 .spawn()
494 }
495}
496
497impl<S: ExclusiveOption, T: ExclusiveOption> Attribute<S, T> {
498 #[cfg_attr(
503 feature = "lt2014_2",
504 doc = "See the [Global Options](GlobalOpts) section."
505 )]
506 #[cfg_attr(
507 all(feature = "lt2015_1", not(feature = "lt2014_2")),
508 doc = "See the [“Global Options”](GlobalOpts) section."
509 )]
510 #[cfg_attr(
511 all(feature = "lt2017_1", not(feature = "lt2015_1")),
512 doc = "See [“Global Options”](GlobalOpts)."
513 )]
514 #[cfg_attr(
515 all(feature = "lt2018_2", not(feature = "lt2017_1")),
516 doc = "See [Global Options](GlobalOpts)."
517 )]
518 #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
519 pub fn get_global_opts(&self) -> &GlobalOpts {
520 &self.global_opts
521 }
522
523 #[cfg_attr(
528 feature = "lt2014_2",
529 doc = "See the [Global Options](GlobalOpts) section."
530 )]
531 #[cfg_attr(
532 all(feature = "lt2015_1", not(feature = "lt2014_2")),
533 doc = "See the [“Global Options”](GlobalOpts) section."
534 )]
535 #[cfg_attr(
536 all(feature = "lt2017_1", not(feature = "lt2015_1")),
537 doc = "See [“Global Options”](GlobalOpts)."
538 )]
539 #[cfg_attr(
540 all(feature = "lt2018_2", not(feature = "lt2017_1")),
541 doc = "See [Global Options](GlobalOpts)."
542 )]
543 #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
544 pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
545 self.global_opts = v;
546 self
547 }
548
549 #[cfg_attr(
554 feature = "lt2014_2",
555 doc = "See the [Global Options](GlobalOpts) section."
556 )]
557 #[cfg_attr(
558 all(feature = "lt2015_1", not(feature = "lt2014_2")),
559 doc = "See the [“Global Options”](GlobalOpts) section."
560 )]
561 #[cfg_attr(
562 all(feature = "lt2017_1", not(feature = "lt2015_1")),
563 doc = "See [“Global Options”](GlobalOpts)."
564 )]
565 #[cfg_attr(
566 all(feature = "lt2018_2", not(feature = "lt2017_1")),
567 doc = "See [Global Options](GlobalOpts)."
568 )]
569 #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
570 pub fn global_opts(mut self, v: GlobalOpts) -> Self {
571 self.global_opts = v;
572 self
573 }
574
575 pub fn get_on_submitted_files(&self) -> bool {
583 self.on_submitted_files
584 }
585
586 pub fn set_on_submitted_files(&mut self, v: bool) -> &mut Self {
594 self.on_submitted_files = v;
595 self
596 }
597
598 pub fn on_submitted_files(mut self, v: bool) -> Self {
606 self.on_submitted_files = v;
607 self
608 }
609
610 #[cfg_attr(
615 feature = "lt2021_2",
616 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`."
617 )]
618 #[cfg_attr(
619 not(feature = "lt2021_2"),
620 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`."
621 )]
622 pub fn get_propagating(&self) -> bool {
623 self.propagating
624 }
625
626 #[cfg_attr(
631 feature = "lt2021_2",
632 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`."
633 )]
634 #[cfg_attr(
635 not(feature = "lt2021_2"),
636 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`."
637 )]
638 pub fn set_propagating(&mut self, v: bool) -> &mut Self {
639 self.propagating = v;
640 self
641 }
642
643 #[cfg_attr(
648 feature = "lt2021_2",
649 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`."
650 )]
651 #[cfg_attr(
652 not(feature = "lt2021_2"),
653 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`."
654 )]
655 pub fn propagating(mut self, v: bool) -> Self {
656 self.propagating = v;
657 self
658 }
659}
660
661#[cfg(not(feature = "lt2023_2"))]
662impl<S: ExclusiveOption, T: ExclusiveOption> Attribute<S, T> {
663 pub fn store_in_database_traits(self) -> Attribute<S, storage::DatabaseTraits> {
670 Attribute {
671 bin: self.bin,
672 global_opts: self.global_opts,
673 on_submitted_files: self.on_submitted_files,
674 propagating: self.propagating,
675 source: self.source,
676 storage: storage::DatabaseTraits,
677 }
678 }
679
680 pub fn store_in_trait_depot(self) -> Attribute<S, storage::TraitDepot> {
690 Attribute {
691 bin: self.bin,
692 global_opts: self.global_opts,
693 on_submitted_files: self.on_submitted_files,
694 propagating: self.propagating,
695 source: self.source,
696 storage: storage::TraitDepot,
697 }
698 }
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704 use crate::cmd::args_of;
705
706 #[test]
709 fn without_options() {
710 let attr = Attribute::new("p4", GlobalOpts::new());
711
712 assert_eq!(args_of(&attr.setup_command("p4")), ["attribute"]);
713 }
714
715 #[test]
716 fn set_attribute_with_value() {
717 let mut attr = Attribute::new("p4", GlobalOpts::new());
718 attr.set("status", "approved");
719
720 assert_eq!(
721 args_of(&attr.setup_command("p4")),
722 ["attribute", "-n", "status", "-v", "approved"]
723 );
724 }
725
726 #[test]
727 fn clear_attribute_omits_value() {
728 let mut attr = Attribute::new("p4", GlobalOpts::new());
729 attr.clear("status");
730
731 assert_eq!(
732 args_of(&attr.setup_command("p4")),
733 ["attribute", "-n", "status"]
734 );
735 }
736
737 #[test]
738 fn multiple_set_and_clear_pairs_keep_order() {
739 let mut attr = Attribute::new("p4", GlobalOpts::new());
740 attr.set("color", "red")
741 .clear("status")
742 .set("owner", "alice");
743
744 assert_eq!(
745 args_of(&attr.setup_command("p4")),
746 [
747 "attribute",
748 "-n",
749 "color",
750 "-v",
751 "red",
752 "-n",
753 "status",
754 "-n",
755 "owner",
756 "-v",
757 "alice"
758 ]
759 );
760 }
761
762 #[test]
763 fn hex_submitted_propagating() {
764 let mut attr = Attribute::new("p4", GlobalOpts::new());
765 attr.set_hex(true)
766 .set_on_submitted_files(true)
767 .set_propagating(true)
768 .set("thumb", "deadbeef");
769
770 assert_eq!(
771 args_of(&attr.setup_command("p4")),
772 [
773 "attribute",
774 "-f",
775 "-p",
776 "-e",
777 "-n",
778 "thumb",
779 "-v",
780 "deadbeef"
781 ]
782 );
783 }
784
785 #[test]
786 fn read_from_stdin_state() {
787 let attr = Attribute::new("p4", GlobalOpts::new()).read_from_stdin("thumb".to_string());
788
789 assert_eq!(attr.get_name(), "thumb");
790 assert_eq!(
791 args_of(&attr.setup_command("p4")),
792 ["attribute", "-i", "-n", "thumb"]
793 );
794 }
795
796 #[test]
797 fn stdin_name_can_be_replaced() {
798 let mut attr = Attribute::new("p4", GlobalOpts::new()).read_from_stdin("old".to_string());
799 attr.set_name("thumb");
800
801 assert_eq!(attr.get_name(), "thumb");
802
803 let attr = attr.name("icon");
804 assert_eq!(attr.get_name(), "icon");
805 assert_eq!(
806 args_of(&attr.setup_command("p4")),
807 ["attribute", "-i", "-n", "icon"]
808 );
809 }
810
811 #[test]
812 fn stdin_preserves_flags_and_hex() {
813 let mut attr = Attribute::new("p4", GlobalOpts::new());
814 attr.set_hex(true)
815 .set_on_submitted_files(true)
816 .set_propagating(true)
817 .set("discarded", "value");
818
819 let attr = attr.read_from_stdin("thumb".to_string());
820
821 assert!(attr.get_hex());
822 assert_eq!(
823 args_of(&attr.setup_command("p4")),
824 ["attribute", "-f", "-p", "-e", "-i", "-n", "thumb"]
825 );
826 }
827
828 #[cfg(not(feature = "lt2023_2"))]
829 #[test]
830 fn store_in_database_traits() {
831 let mut attr = Attribute::new("p4", GlobalOpts::new());
832 attr.set("thumb", "data");
833
834 let attr = attr.store_in_database_traits();
835
836 assert_eq!(
837 args_of(&attr.setup_command("p4")),
838 ["attribute", "-T0", "-n", "thumb", "-v", "data"]
839 );
840 }
841
842 #[cfg(not(feature = "lt2023_2"))]
843 #[test]
844 fn store_in_trait_depot() {
845 let mut attr = Attribute::new("p4", GlobalOpts::new());
846 attr.set("thumb", "data");
847
848 let attr = attr.store_in_trait_depot();
849
850 assert_eq!(
851 args_of(&attr.setup_command("p4")),
852 ["attribute", "-T1", "-n", "thumb", "-v", "data"]
853 );
854 }
855
856 #[cfg(not(feature = "lt2023_2"))]
857 #[test]
858 fn stdin_with_trait_depot() {
859 let attr = Attribute::new("p4", GlobalOpts::new())
860 .read_from_stdin("thumb".to_string())
861 .store_in_trait_depot();
862
863 assert_eq!(
864 args_of(&attr.setup_command("p4")),
865 ["attribute", "-T1", "-i", "-n", "thumb"]
866 );
867 }
868
869 #[cfg(not(feature = "lt2024_2"))]
870 #[test]
871 fn read_from_file_state() {
872 let attr = Attribute::new("p4", GlobalOpts::new())
873 .read_from_file("thumb".to_string(), PathBuf::from("/tmp/thumb.bin"));
874
875 assert_eq!(attr.get_name(), "thumb");
876 assert_eq!(attr.get_file(), Path::new("/tmp/thumb.bin"));
877 assert_eq!(
878 args_of(&attr.setup_command("p4")),
879 ["attribute", "-I", "/tmp/thumb.bin", "-n", "thumb"]
880 );
881 }
882
883 #[cfg(not(feature = "lt2024_2"))]
884 #[test]
885 fn all_modern_options() {
886 let mut attr = Attribute::new("p4", GlobalOpts::new());
887 attr.set_on_submitted_files(true).set_propagating(true);
888
889 let attr = attr
890 .store_in_trait_depot()
891 .read_from_file("thumb".to_string(), PathBuf::from("/tmp/data.bin"));
892
893 assert_eq!(
894 args_of(&attr.setup_command("p4")),
895 [
896 "attribute",
897 "-f",
898 "-p",
899 "-T1",
900 "-I",
901 "/tmp/data.bin",
902 "-n",
903 "thumb"
904 ]
905 );
906 }
907
908 #[test]
909 fn set_style_with_global_opts() {
910 let mut attr = Attribute::new("p4", GlobalOpts::new().port("localhost:1666"));
911 attr.set("status", "approved");
912
913 assert_eq!(
914 args_of(&attr.setup_command("p4")),
915 [
916 "-p",
917 "localhost:1666",
918 "attribute",
919 "-n",
920 "status",
921 "-v",
922 "approved"
923 ]
924 );
925 }
926}