Skip to main content

perforce_cli/cmd/
attribute.rs

1use std::ffi::OsStr;
2use std::io::Write;
3use std::path::Path;
4use std::path::PathBuf;
5use std::process::{Child, Command, Output, Stdio};
6
7use super::{ExclusiveOption, SubCommand, Unselected};
8
9use crate::global::GlobalOpts;
10use crate::spawn::ParameterizedSpawn;
11
12/// Default value source of `p4 attribute`: set or clear attributes with
13/// `-n name [-v value]` pairs.
14///
15/// Each entry is an attribute name together with the value to set, or `None`
16/// to clear the attribute by omitting `-v`.
17#[derive(Debug, Clone, Default)]
18pub struct Standard {
19    values: Option<Vec<(String, Option<String>)>>,
20
21    hex: bool,
22}
23
24impl ExclusiveOption for Standard {
25    fn inject_args(&self, command: &mut Command) {
26        if self.hex {
27            command.arg("-e");
28        }
29
30        if let Some(pairs) = &self.values {
31            for (name, value) in pairs {
32                command.arg("-n").arg(name);
33
34                if let Some(value) = value {
35                    command.arg("-v").arg(value);
36                }
37            }
38        }
39    }
40}
41
42/// Read the attribute value from standard input (`-i`).
43///
44/// Entered with [`Attribute::read_from_stdin`]; only one file argument is
45/// allowed in this state.
46#[derive(Debug, Clone, Default)]
47pub struct FromStdin {
48    hex: bool,
49
50    name: String,
51}
52
53impl ExclusiveOption for FromStdin {
54    fn inject_args(&self, command: &mut Command) {
55        if self.hex {
56            command.arg("-e");
57        }
58
59        command.arg("-i").arg("-n").arg(&self.name);
60    }
61}
62
63/// Read the attribute value from a file (`-I filename`).
64///
65/// Entered with [`Attribute::read_from_file`]; `-e` is not available and only
66/// one file argument is allowed in this state.
67#[cfg(not(feature = "lt2024_2"))]
68#[derive(Debug, Clone, Default)]
69pub struct FromFile {
70    name: String,
71
72    file: PathBuf,
73}
74
75#[cfg(not(feature = "lt2024_2"))]
76impl ExclusiveOption for FromFile {
77    fn inject_args(&self, command: &mut Command) {
78        command.arg("-I").arg(&self.file).arg("-n").arg(&self.name);
79    }
80}
81
82/// Trait storage location of the `-T0` / `-T1` option group of
83/// `p4 attribute`.
84#[cfg(not(feature = "lt2023_2"))]
85pub mod storage {
86    /// `-T0`: store the attribute value in the `db.traits` table.
87    #[derive(Debug, Clone, Copy, Default)]
88    pub struct DatabaseTraits;
89
90    /// `-T1`: store the attribute value in the `trait` depot.
91    #[derive(Debug, Clone, Copy, Default)]
92    pub struct TraitDepot;
93}
94
95#[cfg(not(feature = "lt2023_2"))]
96impl ExclusiveOption for storage::DatabaseTraits {
97    fn inject_args(&self, command: &mut Command) {
98        command.arg("-T0");
99    }
100}
101
102#[cfg(not(feature = "lt2023_2"))]
103impl ExclusiveOption for storage::TraitDepot {
104    fn inject_args(&self, command: &mut Command) {
105        command.arg("-T1");
106    }
107}
108
109#[cfg_attr(
110    feature = "lt2023_2",
111    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`"
112)]
113#[cfg_attr(
114    all(feature = "lt2024_2", not(feature = "lt2023_2")),
115    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`"
116)]
117#[cfg_attr(
118    not(feature = "lt2024_2"),
119    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`"
120)]
121///
122/// The `S` type parameter tracks the value source: [`Standard`] for the
123/// default `-n`/`-v` set-or-clear form, [`FromStdin`] for the `-i` form,
124#[cfg_attr(
125    not(feature = "lt2024_2"),
126    doc = "[`FromFile`] for the `-I filename` form, and"
127)]
128/// and the `T` type parameter tracks the `-T0`/`-T1` trait storage location.
129#[derive(Debug, Clone, Default)]
130pub struct Attribute<S = Standard, T = Unselected> {
131    bin: PathBuf,
132
133    global_opts: GlobalOpts,
134
135    source: S,
136
137    storage: T,
138
139    on_submitted_files: bool,
140
141    propagating: bool,
142}
143
144impl<S: ExclusiveOption, T: ExclusiveOption> SubCommand for Attribute<S, T> {
145    fn name(&self) -> &str {
146        "attribute"
147    }
148
149    fn inject_local_args(&self, command: &mut Command) {
150        if self.on_submitted_files {
151            command.arg("-f");
152        }
153
154        if self.propagating {
155            command.arg("-p");
156        }
157
158        self.storage.inject_args(command);
159
160        self.source.inject_args(command);
161    }
162
163    fn global_opts(&self) -> Option<&GlobalOpts> {
164        Some(&self.global_opts)
165    }
166}
167
168impl Attribute<Standard, Unselected> {
169    /// Creates a new `p4 attribute` command.
170    ///
171    /// `bin` is the path to the Perforce command-line executable.
172    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
173        Self {
174            bin: bin.into(),
175            global_opts,
176            on_submitted_files: false,
177            propagating: false,
178            source: Standard::default(),
179            storage: Unselected,
180        }
181    }
182}
183
184impl<T: ExclusiveOption, S, I> ParameterizedSpawn<(S,)> for Attribute<Standard, T>
185where
186    S: IntoIterator<Item = I>,
187    I: AsRef<OsStr>,
188{
189    type Output = Child;
190    type Error = std::io::Error;
191
192    /// Spawns `p4 attribute` for the given files as a child process with
193    /// piped standard output and error streams; use the returned [`Child`]
194    /// handle to wait for it or interact with it.
195    fn spawn_with(&mut self, (files,): (S,)) -> Result<Self::Output, Self::Error> {
196        self.setup_command(&self.bin)
197            .args(files)
198            .stdout(Stdio::piped())
199            .stderr(Stdio::piped())
200            .spawn()
201    }
202}
203
204impl<T: ExclusiveOption> Attribute<Standard, T> {
205    /// # Description
206    ///
207    /// -n name -v value
208    ///
209    /// Sets the attribute `name` to `value` on the given files.
210    #[cfg_attr(not(feature = "lt2024_2"), doc = "The supplied value must be text.")]
211    pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
212        self.source
213            .values
214            .get_or_insert_with(Vec::new)
215            .push((name.into(), Some(value.into())));
216        self
217    }
218
219    /// # Description
220    ///
221    /// -n name
222    ///
223    /// Clears the attribute `name` on the given files by omitting the `-v`
224    /// option.
225    pub fn clear(&mut self, name: impl Into<String>) -> &mut Self {
226        self.source
227            .values
228            .get_or_insert_with(Vec::new)
229            .push((name.into(), None));
230        self
231    }
232
233    /// # Description
234    ///
235    /// -e
236    ///
237    /// Indicates that the value is specified in hex.
238    pub fn get_hex(&self) -> bool {
239        self.source.hex
240    }
241
242    /// # Description
243    ///
244    /// -e
245    ///
246    /// Indicates that the value is specified in hex.
247    pub fn set_hex(&mut self, v: bool) -> &mut Self {
248        self.source.hex = v;
249        self
250    }
251
252    /// # Description
253    ///
254    /// -e
255    ///
256    /// Indicates that the value is specified in hex.
257    pub fn hex(mut self, v: bool) -> Self {
258        self.source.hex = v;
259        self
260    }
261
262    /// # Description
263    ///
264    /// -i
265    ///
266    #[cfg_attr(
267        feature = "lt2024_2",
268        doc = "Read an attribute value from the standard input. Only one file argument is allowed when using this option."
269    )]
270    #[cfg_attr(
271        not(feature = "lt2024_2"),
272        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."
273    )]
274    ///
275    /// Transitions this command to the [`FromStdin`] state; any `-n`/`-v`
276    /// pairs set with [`Self::set`] or [`Self::clear`] are discarded, while
277    /// `-e` is preserved.
278    pub fn read_from_stdin(self, name: String) -> Attribute<FromStdin, T> {
279        Attribute {
280            bin: self.bin,
281            global_opts: self.global_opts,
282            on_submitted_files: self.on_submitted_files,
283            propagating: self.propagating,
284            source: FromStdin {
285                hex: self.source.hex,
286                name,
287            },
288            storage: self.storage,
289        }
290    }
291
292    /// # Description
293    ///
294    /// -I filename
295    ///
296    /// Read the attribute value from a file. The file can have textual or
297    /// binary content. Use this when the attribute data exceeds 250
298    /// megabytes, which might cause the command to fail with the `Rpc buffer
299    /// too big` error. The following are not allowed with this option:
300    ///
301    /// - Using the `-e` option to specify the value as hex.
302    /// - More than one file argument.
303    /// - Setting more than one trait value.
304    ///
305    /// To display attributes set with this option, use the `p4 print -T`
306    /// command instead of the `p4 fstat -Oa` command because `p4 print -T`
307    /// can handle larger non-encoded binary data.
308    ///
309    /// Transitions this command to the [`FromFile`] state; any `-n`/`-v`
310    /// pairs set with [`Self::set`] or [`Self::clear`], as well as `-e`, are
311    /// discarded.
312    #[cfg(not(feature = "lt2024_2"))]
313    pub fn read_from_file(self, name: String, file: PathBuf) -> Attribute<FromFile, T> {
314        Attribute {
315            bin: self.bin,
316            global_opts: self.global_opts,
317            on_submitted_files: self.on_submitted_files,
318            propagating: self.propagating,
319            source: FromFile { name, file },
320            storage: self.storage,
321        }
322    }
323}
324
325impl<T: ExclusiveOption> Attribute<FromStdin, T> {
326    /// # Description
327    ///
328    /// -e
329    ///
330    /// Indicates that the value is specified in hex.
331    pub fn get_hex(&self) -> bool {
332        self.source.hex
333    }
334
335    /// # Description
336    ///
337    /// -e
338    ///
339    /// Indicates that the value is specified in hex.
340    pub fn set_hex(&mut self, v: bool) -> &mut Self {
341        self.source.hex = v;
342        self
343    }
344
345    /// # Description
346    ///
347    /// -e
348    ///
349    /// Indicates that the value is specified in hex.
350    pub fn hex(mut self, v: bool) -> Self {
351        self.source.hex = v;
352        self
353    }
354
355    /// # Description
356    ///
357    /// -n name
358    ///
359    /// The name of the attribute read from standard input.
360    pub fn get_name(&self) -> &str {
361        &self.source.name
362    }
363
364    /// # Description
365    ///
366    /// -n name
367    ///
368    /// The name of the attribute read from standard input.
369    pub fn set_name(&mut self, v: impl Into<String>) -> &mut Self {
370        self.source.name = v.into();
371        self
372    }
373
374    /// # Description
375    ///
376    /// -n name
377    ///
378    /// The name of the attribute read from standard input.
379    pub fn name(mut self, v: impl Into<String>) -> Self {
380        self.source.name = v.into();
381        self
382    }
383
384    /// Runs `p4 attribute -i` for a single file, writing `value` to the
385    /// child's standard input, and captures the command's output.
386    ///
387    /// Unlike [`ParameterizedSpawn::spawn_with`], this method handles writing
388    /// the attribute value to the child's standard input itself, so it is the
389    /// most convenient way to submit values through standard input.
390    ///
391    /// Only one file argument is allowed in the [`FromStdin`] state.
392    pub fn output<S, V>(&self, file: S, value: V) -> Result<Output, std::io::Error>
393    where
394        S: AsRef<OsStr>,
395        V: AsRef<[u8]>,
396    {
397        let mut child = self
398            .setup_command(&self.bin)
399            .arg(file)
400            .stdin(Stdio::piped())
401            .stdout(Stdio::piped())
402            .stderr(Stdio::piped())
403            .spawn()?;
404
405        if let Some(ref mut stdin) = child.stdin {
406            stdin.write_all(value.as_ref())?;
407        }
408        drop(child.stdin.take());
409
410        child.wait_with_output()
411    }
412}
413
414impl<T: ExclusiveOption, I> ParameterizedSpawn<(I, Stdio)> for Attribute<FromStdin, T>
415where
416    I: AsRef<OsStr>,
417{
418    type Output = Child;
419    type Error = std::io::Error;
420
421    /// Spawns `p4 attribute -i` for a single file as a child process with the
422    /// given standard input configuration and piped standard output and error
423    /// streams; use the returned [`Child`] handle to wait for it or interact
424    /// with it.
425    ///
426    /// Pass `Stdio::piped()` to obtain a writable `child.stdin` handle and
427    /// write the attribute value yourself, then drop it before waiting on
428    /// the child. Only one file argument is allowed in the [`FromStdin`]
429    /// state.
430    fn spawn_with(&mut self, input: (I, Stdio)) -> Result<Self::Output, Self::Error> {
431        self.setup_command(&self.bin)
432            .arg(input.0)
433            .stdin(input.1)
434            .stdout(Stdio::piped())
435            .stderr(Stdio::piped())
436            .spawn()
437    }
438}
439
440impl<T: ExclusiveOption, I> ParameterizedSpawn<(I,)> for Attribute<FromStdin, T>
441where
442    I: AsRef<OsStr>,
443{
444    type Output = Child;
445    type Error = std::io::Error;
446
447    /// Spawns `p4 attribute -i` for a single file as a child process with the
448    /// given standard input configuration and piped standard output and error
449    /// streams; use the returned [`Child`] handle to wait for it or interact
450    /// with it.
451    fn spawn_with(&mut self, (input,): (I,)) -> Result<Self::Output, Self::Error> {
452        self.setup_command(&self.bin)
453            .arg(input)
454            .stdin(Stdio::piped())
455            .stdout(Stdio::piped())
456            .stderr(Stdio::piped())
457            .spawn()
458    }
459}
460
461#[cfg(not(feature = "lt2024_2"))]
462impl<T: ExclusiveOption> Attribute<FromFile, T> {
463    /// # Description
464    ///
465    /// -n name
466    ///
467    /// The name of the attribute read from the file.
468    pub fn get_name(&self) -> &str {
469        &self.source.name
470    }
471
472    /// # Description
473    ///
474    /// -n name
475    ///
476    /// The name of the attribute read from the file.
477    pub fn set_name(&mut self, v: impl Into<String>) -> &mut Self {
478        self.source.name = v.into();
479        self
480    }
481
482    /// # Description
483    ///
484    /// -n name
485    ///
486    /// The name of the attribute read from the file.
487    pub fn name(mut self, v: impl Into<String>) -> Self {
488        self.source.name = v.into();
489        self
490    }
491
492    /// # Description
493    ///
494    /// -I filename
495    ///
496    /// The file the attribute value is read from.
497    pub fn get_file(&self) -> &Path {
498        &self.source.file
499    }
500}
501
502#[cfg(not(feature = "lt2024_2"))]
503impl<T: ExclusiveOption, I> ParameterizedSpawn<(I,)> for Attribute<FromFile, T>
504where
505    I: AsRef<OsStr>,
506{
507    type Output = Child;
508    type Error = std::io::Error;
509
510    /// Spawns `p4 attribute -I` for a single file as a child process with
511    /// piped standard output and error streams; use the returned [`Child`]
512    /// handle to wait for it or interact with it.
513    ///
514    /// Only one file argument is allowed in the [`FromFile`] state.
515    fn spawn_with(&mut self, (file,): (I,)) -> Result<Self::Output, Self::Error> {
516        self.setup_command(&self.bin)
517            .arg(file)
518            .stdout(Stdio::piped())
519            .stderr(Stdio::piped())
520            .spawn()
521    }
522}
523
524impl<S: ExclusiveOption, T: ExclusiveOption> Attribute<S, T> {
525    /// Get the Perforce command path.
526    pub fn get_bin(&self) -> &Path {
527        &self.bin
528    }
529
530    /// Set the Perforce command path.
531    pub fn set_bin(&mut self, bin: impl Into<PathBuf>) -> &mut Self {
532        self.bin = bin.into();
533        self
534    }
535
536    /// Set the Perforce command path.
537    pub fn bin(mut self, bin: impl Into<PathBuf>) -> Self {
538        self.set_bin(bin);
539        self
540    }
541
542    /// # Description
543    ///
544    /// g-opts
545    ///
546    #[cfg_attr(
547        feature = "lt2014_2",
548        doc = "See the [Global Options](GlobalOpts) section."
549    )]
550    #[cfg_attr(
551        all(feature = "lt2015_1", not(feature = "lt2014_2")),
552        doc = "See the [“Global Options”](GlobalOpts) section."
553    )]
554    #[cfg_attr(
555        all(feature = "lt2017_1", not(feature = "lt2015_1")),
556        doc = "See [“Global Options”](GlobalOpts)."
557    )]
558    #[cfg_attr(
559        all(feature = "lt2018_2", not(feature = "lt2017_1")),
560        doc = "See [Global Options](GlobalOpts)."
561    )]
562    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
563    pub fn get_global_opts(&self) -> &GlobalOpts {
564        &self.global_opts
565    }
566
567    /// # Description
568    ///
569    /// g-opts
570    ///
571    #[cfg_attr(
572        feature = "lt2014_2",
573        doc = "See the [Global Options](GlobalOpts) section."
574    )]
575    #[cfg_attr(
576        all(feature = "lt2015_1", not(feature = "lt2014_2")),
577        doc = "See the [“Global Options”](GlobalOpts) section."
578    )]
579    #[cfg_attr(
580        all(feature = "lt2017_1", not(feature = "lt2015_1")),
581        doc = "See [“Global Options”](GlobalOpts)."
582    )]
583    #[cfg_attr(
584        all(feature = "lt2018_2", not(feature = "lt2017_1")),
585        doc = "See [Global Options](GlobalOpts)."
586    )]
587    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
588    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
589        self.global_opts = v;
590        self
591    }
592
593    /// # Description
594    ///
595    /// g-opts
596    ///
597    #[cfg_attr(
598        feature = "lt2014_2",
599        doc = "See the [Global Options](GlobalOpts) section."
600    )]
601    #[cfg_attr(
602        all(feature = "lt2015_1", not(feature = "lt2014_2")),
603        doc = "See the [“Global Options”](GlobalOpts) section."
604    )]
605    #[cfg_attr(
606        all(feature = "lt2017_1", not(feature = "lt2015_1")),
607        doc = "See [“Global Options”](GlobalOpts)."
608    )]
609    #[cfg_attr(
610        all(feature = "lt2018_2", not(feature = "lt2017_1")),
611        doc = "See [Global Options](GlobalOpts)."
612    )]
613    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
614    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
615        self.global_opts = v;
616        self
617    }
618
619    /// # Description
620    ///
621    /// -f
622    ///
623    /// Set the attribute on submitted files. If a propagating trait is set
624    /// on a submitted file, a revision specifier cannot be used, and the
625    /// file must not be currently open in any workspace.
626    pub fn get_on_submitted_files(&self) -> bool {
627        self.on_submitted_files
628    }
629
630    /// # Description
631    ///
632    /// -f
633    ///
634    /// Set the attribute on submitted files. If a propagating trait is set
635    /// on a submitted file, a revision specifier cannot be used, and the
636    /// file must not be currently open in any workspace.
637    pub fn set_on_submitted_files(&mut self, v: bool) -> &mut Self {
638        self.on_submitted_files = v;
639        self
640    }
641
642    /// # Description
643    ///
644    /// -f
645    ///
646    /// Set the attribute on submitted files. If a propagating trait is set
647    /// on a submitted file, a revision specifier cannot be used, and the
648    /// file must not be currently open in any workspace.
649    pub fn on_submitted_files(mut self, v: bool) -> Self {
650        self.on_submitted_files = v;
651        self
652    }
653
654    /// # Description
655    ///
656    /// -p
657    ///
658    #[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 get_propagating(&self) -> bool {
667        self.propagating
668    }
669
670    /// # Description
671    ///
672    /// -p
673    ///
674    #[cfg_attr(
675        feature = "lt2021_2",
676        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`."
677    )]
678    #[cfg_attr(
679        not(feature = "lt2021_2"),
680        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`."
681    )]
682    pub fn set_propagating(&mut self, v: bool) -> &mut Self {
683        self.propagating = v;
684        self
685    }
686
687    /// # Description
688    ///
689    /// -p
690    ///
691    #[cfg_attr(
692        feature = "lt2021_2",
693        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`."
694    )]
695    #[cfg_attr(
696        not(feature = "lt2021_2"),
697        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`."
698    )]
699    pub fn propagating(mut self, v: bool) -> Self {
700        self.propagating = v;
701        self
702    }
703}
704
705#[cfg(not(feature = "lt2023_2"))]
706impl<S: ExclusiveOption, T: ExclusiveOption> Attribute<S, T> {
707    /// # Description
708    ///
709    /// -T0
710    ///
711    /// Causes the value to be stored in the `db.traits` table, which is the
712    /// implicit default.
713    pub fn store_in_database_traits(self) -> Attribute<S, storage::DatabaseTraits> {
714        Attribute {
715            bin: self.bin,
716            global_opts: self.global_opts,
717            on_submitted_files: self.on_submitted_files,
718            propagating: self.propagating,
719            source: self.source,
720            storage: storage::DatabaseTraits,
721        }
722    }
723
724    /// # Description
725    ///
726    /// -T1
727    ///
728    /// Causes the value to be stored in the `trait` depot instead of the
729    /// `db.traits` table, even if the size of the attribute value is less
730    /// than the size specified by the `trait.storagedepot.min` configurable.
731    /// However, if `trait.storagedepot.min` is unset or set to `0`, the
732    /// attribute value is stored in the `db.traits` table.
733    pub fn store_in_trait_depot(self) -> Attribute<S, storage::TraitDepot> {
734        Attribute {
735            bin: self.bin,
736            global_opts: self.global_opts,
737            on_submitted_files: self.on_submitted_files,
738            propagating: self.propagating,
739            source: self.source,
740            storage: storage::TraitDepot,
741        }
742    }
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748    use crate::cmd::args_of;
749
750    #[test]
751    fn bin_accessors_update_executable_path() {
752        let mut command = Attribute::new("p4", GlobalOpts::new());
753        assert_eq!(command.get_bin(), Path::new("p4"));
754
755        command.set_bin("/usr/local/bin/p4");
756        assert_eq!(command.get_bin(), Path::new("/usr/local/bin/p4"));
757
758        let command = command.bin("p4.exe");
759        assert_eq!(command.get_bin(), Path::new("p4.exe"));
760    }
761
762    /// Dry-run checks of the assembled `p4 attribute` command line; no
763    /// process is spawned.
764    #[test]
765    fn without_options() {
766        let attr = Attribute::new("p4", GlobalOpts::new());
767
768        assert_eq!(args_of(&attr.setup_command("p4")), ["attribute"]);
769    }
770
771    #[test]
772    fn set_attribute_with_value() {
773        let mut attr = Attribute::new("p4", GlobalOpts::new());
774        attr.set("status", "approved");
775
776        assert_eq!(
777            args_of(&attr.setup_command("p4")),
778            ["attribute", "-n", "status", "-v", "approved"]
779        );
780    }
781
782    #[test]
783    fn clear_attribute_omits_value() {
784        let mut attr = Attribute::new("p4", GlobalOpts::new());
785        attr.clear("status");
786
787        assert_eq!(
788            args_of(&attr.setup_command("p4")),
789            ["attribute", "-n", "status"]
790        );
791    }
792
793    #[test]
794    fn multiple_set_and_clear_pairs_keep_order() {
795        let mut attr = Attribute::new("p4", GlobalOpts::new());
796        attr.set("color", "red")
797            .clear("status")
798            .set("owner", "alice");
799
800        assert_eq!(
801            args_of(&attr.setup_command("p4")),
802            [
803                "attribute",
804                "-n",
805                "color",
806                "-v",
807                "red",
808                "-n",
809                "status",
810                "-n",
811                "owner",
812                "-v",
813                "alice"
814            ]
815        );
816    }
817
818    #[test]
819    fn hex_submitted_propagating() {
820        let mut attr = Attribute::new("p4", GlobalOpts::new());
821        attr.set_hex(true)
822            .set_on_submitted_files(true)
823            .set_propagating(true)
824            .set("thumb", "deadbeef");
825
826        assert_eq!(
827            args_of(&attr.setup_command("p4")),
828            [
829                "attribute",
830                "-f",
831                "-p",
832                "-e",
833                "-n",
834                "thumb",
835                "-v",
836                "deadbeef"
837            ]
838        );
839    }
840
841    #[test]
842    fn read_from_stdin_state() {
843        let attr = Attribute::new("p4", GlobalOpts::new()).read_from_stdin("thumb".to_string());
844
845        assert_eq!(attr.get_name(), "thumb");
846        assert_eq!(
847            args_of(&attr.setup_command("p4")),
848            ["attribute", "-i", "-n", "thumb"]
849        );
850    }
851
852    #[test]
853    fn stdin_name_can_be_replaced() {
854        let mut attr = Attribute::new("p4", GlobalOpts::new()).read_from_stdin("old".to_string());
855        attr.set_name("thumb");
856
857        assert_eq!(attr.get_name(), "thumb");
858
859        let attr = attr.name("icon");
860        assert_eq!(attr.get_name(), "icon");
861        assert_eq!(
862            args_of(&attr.setup_command("p4")),
863            ["attribute", "-i", "-n", "icon"]
864        );
865    }
866
867    #[test]
868    fn stdin_preserves_flags_and_hex() {
869        let mut attr = Attribute::new("p4", GlobalOpts::new());
870        attr.set_hex(true)
871            .set_on_submitted_files(true)
872            .set_propagating(true)
873            .set("discarded", "value");
874
875        let attr = attr.read_from_stdin("thumb".to_string());
876
877        assert!(attr.get_hex());
878        assert_eq!(
879            args_of(&attr.setup_command("p4")),
880            ["attribute", "-f", "-p", "-e", "-i", "-n", "thumb"]
881        );
882    }
883
884    #[cfg(not(feature = "lt2023_2"))]
885    #[test]
886    fn store_in_database_traits() {
887        let mut attr = Attribute::new("p4", GlobalOpts::new());
888        attr.set("thumb", "data");
889
890        let attr = attr.store_in_database_traits();
891
892        assert_eq!(
893            args_of(&attr.setup_command("p4")),
894            ["attribute", "-T0", "-n", "thumb", "-v", "data"]
895        );
896    }
897
898    #[cfg(not(feature = "lt2023_2"))]
899    #[test]
900    fn store_in_trait_depot() {
901        let mut attr = Attribute::new("p4", GlobalOpts::new());
902        attr.set("thumb", "data");
903
904        let attr = attr.store_in_trait_depot();
905
906        assert_eq!(
907            args_of(&attr.setup_command("p4")),
908            ["attribute", "-T1", "-n", "thumb", "-v", "data"]
909        );
910    }
911
912    #[cfg(not(feature = "lt2023_2"))]
913    #[test]
914    fn stdin_with_trait_depot() {
915        let attr = Attribute::new("p4", GlobalOpts::new())
916            .read_from_stdin("thumb".to_string())
917            .store_in_trait_depot();
918
919        assert_eq!(
920            args_of(&attr.setup_command("p4")),
921            ["attribute", "-T1", "-i", "-n", "thumb"]
922        );
923    }
924
925    #[cfg(not(feature = "lt2024_2"))]
926    #[test]
927    fn read_from_file_state() {
928        let attr = Attribute::new("p4", GlobalOpts::new())
929            .read_from_file("thumb".to_string(), PathBuf::from("/tmp/thumb.bin"));
930
931        assert_eq!(attr.get_name(), "thumb");
932        assert_eq!(attr.get_file(), Path::new("/tmp/thumb.bin"));
933        assert_eq!(
934            args_of(&attr.setup_command("p4")),
935            ["attribute", "-I", "/tmp/thumb.bin", "-n", "thumb"]
936        );
937    }
938
939    #[cfg(not(feature = "lt2024_2"))]
940    #[test]
941    fn all_modern_options() {
942        let mut attr = Attribute::new("p4", GlobalOpts::new());
943        attr.set_on_submitted_files(true).set_propagating(true);
944
945        let attr = attr
946            .store_in_trait_depot()
947            .read_from_file("thumb".to_string(), PathBuf::from("/tmp/data.bin"));
948
949        assert_eq!(
950            args_of(&attr.setup_command("p4")),
951            [
952                "attribute",
953                "-f",
954                "-p",
955                "-T1",
956                "-I",
957                "/tmp/data.bin",
958                "-n",
959                "thumb"
960            ]
961        );
962    }
963
964    #[test]
965    fn set_style_with_global_opts() {
966        let mut attr = Attribute::new("p4", GlobalOpts::new().port("localhost:1666"));
967        attr.set("status", "approved");
968
969        assert_eq!(
970            args_of(&attr.setup_command("p4")),
971            [
972                "-p",
973                "localhost:1666",
974                "attribute",
975                "-n",
976                "status",
977                "-v",
978                "approved"
979            ]
980        );
981    }
982}