reups_lib/
list.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 * Copyright Nate Lust 2018*/
5
6use crate::argparse;
7use crate::db;
8use fnv::{FnvHashMap, FnvHashSet};
9use std::env;
10
11/**
12 * Lists info about products defined in the product database
13 *
14 * This function takes two arguments, one for command specific arguments,
15 * and one for program general options. These arguments are parsed from the
16 * command line, packaged and sent here. The arguments are defined in the
17 * argparse module.
18 */
19pub fn list_command(sub_args: &argparse::ArgMatches, _main_args: &argparse::ArgMatches) {
20    let mut lister = ListImpl::new(sub_args, _main_args);
21    lister.run();
22}
23
24/// This enum controls if the list command shows tags, versions, or both
25#[derive(Clone)]
26enum OnlyPrint {
27    Tags,
28    Versions,
29    All,
30}
31
32/**
33 * The Listimpl structure is responsible for implementing the list subcomand functionality
34 * It is created with argument matche from the command line in the new method. This method
35 * prepopulates the database and some system variables. The sub command is executed with the run
36 * command.
37 */
38struct ListImpl<'a> {
39    sub_args: &'a argparse::ArgMatches<'a>,
40    _main_args: &'a argparse::ArgMatches<'a>,
41    output_string: String,
42    current_products: FnvHashSet<(String, String)>,
43    local_setups: FnvHashMap<String, String>,
44    db: db::DB,
45    tags: Option<Vec<String>>,
46}
47
48impl<'a> ListImpl<'a> {
49    /** Creates a LIstImpl struct given argument matches from the command line
50     */
51    fn new(
52        sub_args: &'a argparse::ArgMatches<'a>,
53        _main_args: &'a argparse::ArgMatches<'a>,
54    ) -> ListImpl<'a> {
55        // Here we will process any of the global arguments in the future but for now there is
56        // nothing so we do nothing but create the database. The global arguments might affect
57        // construction in the future
58
59        // cheat and look at the sub_args here to see if all products are listing all products or not. If
60        // not, dont preload tag files in the database as this will slow things down
61        let preload = if sub_args.is_present("product")
62            || sub_args.is_present("setup")
63            || sub_args.is_present("tags")
64            || sub_args.is_present("onlyTags")
65            || sub_args.is_present("onlyVers")
66        {
67            None
68        } else {
69            // Mark the database to preload all the tag files off disk
70            Some(db::DBLoadControl::Tags)
71        };
72        let db = db::DB::new(None, None, None, preload);
73        // get any products that are currently setup
74        let (current_products, local_setups) = find_setup_products();
75        // String to hold the output
76        let output_string = String::from("");
77        // Hold tag information
78        let tags = None;
79        // create the object
80        ListImpl {
81            sub_args,
82            _main_args,
83            output_string,
84            current_products,
85            local_setups,
86            db,
87            tags,
88        }
89    }
90
91    /// Runs the ListImpl over arguments given on the command line, and information
92    /// gained from environment variables. Its result is the requested information is
93    /// printed out to the user in the console.
94    fn run(&mut self) {
95        // If the user specified a specific product only generate output for that product
96        let mut product_vec = if self.sub_args.is_present("product") {
97            vec![self.sub_args.value_of("product").unwrap().to_string()]
98        }
99        // If the user specifed they want only setup products, get the list of those to display
100        else if self.sub_args.is_present("setup") {
101            self.current_products
102                .iter()
103                .map(|tup| tup.0.clone())
104                .collect()
105        }
106        // If the user wants only products that have been locally setup, get the list of those
107        // products
108        else if self.sub_args.is_present("local") {
109            self.local_setups.keys().map(|k| k.clone()).collect()
110        }
111        // Baring any input from the user, list all products found in the user and system databases
112        else {
113            self.db.get_all_products()
114        };
115
116        // check if we should restrict printing
117        let select_printing = if self.sub_args.is_present("onlyTags") {
118            OnlyPrint::Tags
119        } else if self.sub_args.is_present("onlyVers") {
120            OnlyPrint::Versions
121        } else {
122            OnlyPrint::All
123        };
124
125        // Read any tags the user supplied, to restrict printing to only products
126        // with those tags
127        let mut tags_vec = vec![];
128        if self.sub_args.is_present("tags") {
129            for t in self.sub_args.values_of("tags").unwrap() {
130                tags_vec.push(t.to_string());
131            }
132            self.tags = Some(tags_vec);
133        }
134
135        // Sort the products to be listed so that the results come out deterministically and in
136        // lexographic order
137        product_vec.sort();
138        // Loop over all products and print the information about that product.
139        for product in product_vec.iter() {
140            self.print_product(product, select_printing.clone());
141        }
142        println!("{}", self.output_string.trim_right_matches("\n\n"));
143    }
144
145    /**
146     * Given a product to print, and printing options, this function retrieves
147     * all the information required about the product from the database, formats
148     * it, and appends it to the output string.
149     */
150    fn print_product(&mut self, product: &String, select_printing: OnlyPrint) {
151        // If the user supplied tags, use those when determining the tags and
152        // versions to print, else grab all tags associated with the given product
153        let tags = if self.tags.is_some() {
154            self.tags.as_ref().unwrap().clone()
155        } else {
156            self.db.product_tags(product)
157        };
158
159        // Switch on which printing is to be done, only tags, only versons, or all
160        match select_printing {
161            OnlyPrint::All => {
162                // This builds an association between versions of a product and
163                // what tags point to that version. Unfortunately this must
164                // open and read a lot of files to do this.
165                let mut version_to_tags = FnvHashMap::default();
166                // dont accumulate versions if only locals are to be listed
167                if !self.sub_args.is_present("local") {
168                    for tag in tags.iter() {
169                        if let OnlyPrint::All = select_printing {
170                            let versions = self.db.get_versions_from_tag(product, vec![tag]);
171                            for v in versions {
172                                version_to_tags.entry(v).or_insert(vec![]).push(tag);
173                            }
174                        }
175                    }
176                }
177                // look for any local version that might be setup
178                if let Some(local) = self.local_setups.get(product) {
179                    version_to_tags.entry(local.clone()).or_insert(vec![]);
180                }
181
182                // Turn the hashmap into a vector
183                let mut version_to_tags_vec: Vec<(String, Vec<&String>)> =
184                    version_to_tags.into_iter().collect();
185                // Sort the versions vector by version
186                version_to_tags_vec.sort_by(|tup1, tup2| tup1.0.cmp(&tup2.0));
187                // Iterate over and print results
188                for (ver, tags) in version_to_tags_vec {
189                    self.output_string.push_str(
190                        format!(
191                            "{:25}{:>25}{:10}{}]",
192                            product,
193                            ver,
194                            "",
195                            tags.iter()
196                                .fold(String::from("["), |acc, &x| {
197                                    // if the tag is current, color the string
198                                    let name = if *x == "current" {
199                                        "\x1b[96mcurrent\x1b[0m".to_owned()
200                                    } else {
201                                        (*x).clone()
202                                    };
203                                    acc + &name + ", "
204                                })
205                                .trim_right_matches(", ")
206                        ).as_str()
207                            .trim(),
208                    );
209                    // Check if this product and version match any that are setup,
210                    // and if so add a colored setup string
211                    if self.current_products.contains(&(product.clone(), ver)) {
212                        self.output_string.push_str("    \x1b[92mSetup\x1b[0m");
213                    }
214                    self.output_string.push_str("\n\n");
215                }
216            }
217            OnlyPrint::Tags => {
218                self.output_string.push_str(
219                    format!(
220                        "{:25}{:10}{}]",
221                        product,
222                        "",
223                        tags.iter()
224                            .fold(String::from("["), |acc, x| {
225                                let name = if x == "current" {
226                                    "\x1b[96mcurrent\x1b[0m"
227                                } else {
228                                    &x
229                                };
230                                acc + name + ", "
231                            })
232                            .trim_right_matches(", ")
233                    ).as_str()
234                        .trim(),
235                );
236                self.output_string.push_str("\n\n");
237            }
238            OnlyPrint::Versions => {
239                let mut versions = if !self.sub_args.is_present("local") {
240                    self.db.product_versions(product)
241                } else {
242                    vec![]
243                };
244                if let Some(local) = self.local_setups.get(product) {
245                    versions.push(local.clone());
246                }
247                self.output_string
248                    .push_str(format!("{:25}{:10}", product, "").as_str());
249                self.output_string.push_str("[");
250                for version in versions {
251                    if self
252                        .current_products
253                        .contains(&(product.clone(), version.clone()))
254                    {
255                        self.output_string
256                            .push_str(format!("\x1b[92m{}\x1b[0m", version).as_str());
257                    } else {
258                        self.output_string.push_str(version.as_str());
259                    }
260                    self.output_string.push_str(", ");
261                }
262                // This removes the last space and comma added
263                self.output_string.pop();
264                self.output_string.pop();
265                self.output_string.push_str("]");
266                self.output_string.push_str("\n\n");
267            }
268        }
269    }
270}
271
272/**
273 * Read the environment variable and find all products that have previously been setup.
274 *
275 * Returns a tuple where the first element is a hash set of (product, version) tuples. The second
276 * element is a hashmap of locally setup product names as keys, and their local setup path.
277 */
278fn find_setup_products() -> (FnvHashSet<(String, String)>, FnvHashMap<String, String>) {
279    let mut product_set = FnvHashSet::default();
280    let mut local_products = FnvHashMap::default();
281    for (var, value) in env::vars() {
282        if var.starts_with("SETUP_") {
283            let value_vec: Vec<&str> = value.split(" ").collect();
284            if value_vec.len() < 2 {
285                // The value corresponding to a setup product should at least have
286                // a Name and a version, if not there was an issue with that variable
287                eprintln!("Warning, problem parsing {} skipping", var);
288                continue;
289            }
290            // the first element is the product that is setup, the second is version
291            product_set.insert((value_vec[0].to_string(), value_vec[1].to_string()));
292            // Check if the product is a local setup. Track these differently, as
293            // these versions will be the setup version, but not have a corresponding
294            // version string in any database. This hashmap lets us display or append
295            // local results onto results from databases
296            if value_vec[1].starts_with("LOCAL") {
297                local_products.insert(value_vec[0].to_string(), value_vec[1].to_string());
298            }
299        }
300    }
301    (product_set, local_products)
302}