repo_utils/
repo_project_selector.rs

1use anyhow::{anyhow, bail, Result};
2use serde::Deserialize;
3use serde_xml_rs::from_reader;
4use std::env;
5use std::fs;
6use std::fs::File;
7use std::io::BufRead;
8use std::io::BufReader;
9use std::path::Path;
10use std::path::PathBuf;
11
12/// The repo-tool keeps a list of synced projects at
13/// .repo/project.list
14/// This function can filter the list of projects by groups
15/// and/or manifest files. If a group *and* manifest filter
16/// are given, the list will contain the intersection.
17/// Additionally the function can include the manifest repo
18/// itsself into the list (.repo/manifests).
19pub fn select_projects(
20    include_manifest_repo: bool,
21    filter_by_groups: Option<Vec<String>>,
22    filter_by_manifest_files: Option<Vec<PathBuf>>,
23) -> Result<Vec<String>> {
24    let projects_on_disk = lines_from_file(find_project_list()?)?;
25    let mut selected_projects = projects_on_disk;
26
27    if let Some(groups) = filter_by_groups {
28        let manifest = parse_manifest(&find_repo_folder()?.join("manifest.xml"))?;
29        selected_projects = selected_projects
30            .drain(..)
31            .filter(|path| {
32                let project = manifest.find_project(path);
33                project.is_some() && project.unwrap().in_any_given_group(&groups)
34            })
35            .collect();
36    }
37
38    if let Some(manifest_files) = filter_by_manifest_files {
39        let repo_manifests_folder = find_repo_manifests_folder()?;
40        let mut aggregated_manifest = Manifest::empty();
41        for manifest_file in manifest_files {
42            let manifest = parse_manifest(&repo_manifests_folder.join(&manifest_file))?;
43            aggregated_manifest.append(&manifest);
44        }
45        selected_projects = selected_projects
46            .drain(..)
47            .filter(|p| aggregated_manifest.contains_project(p))
48            .collect();
49    }
50
51    if include_manifest_repo {
52        selected_projects.push(".repo/manifests".to_string());
53    }
54
55    Ok(selected_projects)
56}
57
58fn lines_from_file(filename: impl AsRef<Path>) -> Result<Vec<String>> {
59    BufReader::new(File::open(filename)?)
60        .lines()
61        .collect::<Result<Vec<_>, _>>()
62        .map_err(anyhow::Error::msg)
63}
64
65/// returns a path pointing to he project.list file in
66/// the .repo folder, or an io::Error in case the file
67/// couldn't been found.
68pub fn find_project_list() -> Result<PathBuf> {
69    let find_project_list = find_repo_folder()?.join("project.list");
70    match find_project_list.is_file() {
71        true => Ok(find_project_list),
72        false => Err(anyhow!("no project.list in .repo found")),
73    }
74}
75
76/// returns a path pointing to the .repo folder,
77/// or Error in case the .repo folder couldn't been
78/// found in the cwd or any of its parent folders.
79pub fn find_repo_folder() -> Result<PathBuf> {
80    let base_folder = find_repo_root_folder()?;
81    Ok(base_folder.join(".repo"))
82}
83
84/// returns a path pointing to the .repo/manifests folder,
85/// or Error in case the .repo folder couldn't been
86/// found in the cwd or any of its parent folders.
87pub fn find_repo_manifests_folder() -> Result<PathBuf> {
88    let base_folder = find_repo_folder()?;
89    Ok(base_folder.join("manifests"))
90}
91
92/// returns a path pointing to the folder containing .repo,
93/// or io::Error in case the .repo folder couldn't been
94/// found in the cwd or any of its parent folders.
95pub fn find_repo_root_folder() -> Result<PathBuf> {
96    let cwd = env::current_dir()?;
97    for parent in cwd.ancestors() {
98        for entry in fs::read_dir(&parent)? {
99            let entry = entry?;
100            if entry.path().is_dir() && entry.file_name() == ".repo" {
101                return Ok(parent.to_path_buf());
102            }
103        }
104    }
105    bail!("no .repo folder found")
106}
107
108pub fn parse_manifest(path: &Path) -> Result<Manifest> {
109    let file = File::open(path).map_err(|e| anyhow!("Unable to open {:?}: {}", path, e))?;
110    let reader = BufReader::new(file);
111    let mut manifest: Manifest = from_reader(reader)?;
112    let includes: Vec<String> = manifest.includes.iter().map(|i| i.name.clone()).collect();
113    for include in &includes {
114        let path = find_repo_manifests_folder()?.join(include);
115        let child = parse(&path).map_err(|e| anyhow!("Failed to parse {}: {}", include, e))?;
116        manifest.append(&child);
117    }
118    Ok(manifest)
119}
120
121pub fn parse(path: &Path) -> Result<Manifest> {
122    let file = File::open(path)?;
123    let reader = BufReader::new(file);
124    let mut manifest: Manifest = from_reader(reader)?;
125    let includes: Vec<String> = manifest.includes.iter().map(|i| i.name.clone()).collect();
126    for include in &includes {
127        let path = path.with_file_name(include);
128        let child = parse(&path).map_err(|e| anyhow!("Failed to parse {}: {}", include, e))?;
129        manifest.append(&child);
130    }
131    Ok(manifest)
132}
133
134/// OO representation of a repo-tool's manifest xml element
135#[derive(Debug, Deserialize)]
136pub struct Manifest {
137    #[serde(rename = "project", default)]
138    pub projects: Vec<Project>,
139    #[serde(rename = "include", default)]
140    pub includes: Vec<Include>,
141}
142
143impl Manifest {
144    pub fn empty() -> Self {
145        Manifest {
146            projects: vec![],
147            includes: vec![],
148        }
149    }
150
151    pub fn append(&mut self, manifest: &Manifest) {
152        let projects = &manifest.projects;
153        self.projects.extend(projects.iter().cloned());
154    }
155
156    pub fn contains_project(&self, local_path: &str) -> bool {
157        self.projects.iter().any(|p| p.path == local_path)
158    }
159
160    pub fn find_project(&self, local_path: &str) -> Option<&Project> {
161        self.projects.iter().find(|p| p.path == local_path)
162    }
163}
164
165/// OO representation of a repo-tool's project xml element
166#[derive(Debug, Deserialize, Clone)]
167pub struct Project {
168    pub name: String,
169    pub path: String,
170    pub groups: Option<String>,
171}
172
173impl Project {
174    pub fn in_any_given_group(&self, test_for_groups: &[String]) -> bool {
175        let project_groups: Vec<String> = self
176            .groups
177            .as_ref()
178            .unwrap_or(&String::new())
179            .split(&[',', ' '][..])
180            .map(|s| s.to_string())
181            .collect();
182        project_groups
183            .iter()
184            .any(|g| test_for_groups.iter().any(|other| g == other))
185    }
186}
187
188/// OO representation of a repo-tool's include xml element
189#[derive(Debug, Deserialize, Clone)]
190pub struct Include {
191    pub name: String,
192}