git_wok/
config.rs

1use anyhow::*;
2use serde::{Deserialize, Serialize};
3use std::{fs, path};
4
5const CONFIG_CURRENT_VERSION: &str = "1.0";
6
7#[derive(Serialize, Deserialize, Debug, Clone)]
8#[serde(deny_unknown_fields)]
9pub struct Repo {
10    pub path: path::PathBuf,
11    pub head: String,
12    #[serde(default, skip_serializing_if = "Vec::is_empty")]
13    pub skip_for: Vec<String>,
14}
15
16/// Config schema for `wok.toml`
17///
18/// A repository containing `wok.toml` file serves as an "umbrella" repo for a
19/// workspace containing several repos.
20#[derive(Serialize, Deserialize, Debug)]
21#[serde(deny_unknown_fields)]
22pub struct Config {
23    pub version: String,
24    #[serde(rename = "repo")]
25    pub repos: Vec<Repo>,
26}
27
28impl Config {
29    pub fn new() -> Self {
30        Config {
31            version: String::from(CONFIG_CURRENT_VERSION),
32            repos: vec![],
33        }
34    }
35
36    pub fn add_repo(&mut self, path: &path::Path, head: &str) -> bool {
37        assert!(!path.is_absolute());
38
39        if self.has_repo_path(path) {
40            return false;
41        }
42
43        self.repos.push(Repo {
44            path: path::PathBuf::from(path),
45            head: String::from(head),
46            skip_for: vec![],
47        });
48        true
49    }
50
51    pub fn remove_repo(&mut self, path: &path::Path) -> bool {
52        assert!(!path.is_absolute());
53
54        let mut removed = false;
55        self.repos.retain(|r| {
56            if r.path != path {
57                return true;
58            }
59            removed = true;
60            false
61        });
62        removed
63    }
64
65    pub fn set_repo_head(&mut self, path: &path::Path, head: &String) -> bool {
66        assert!(!path.is_absolute());
67
68        let mut updated = false;
69        self.repos = self
70            .repos
71            .iter()
72            .map(|r| -> Repo {
73                let mut r = r.to_owned();
74                if r.path != path {
75                    return r;
76                }
77                if r.head == *head {
78                    return r;
79                }
80                r.head = head.to_owned();
81                updated = true;
82                r
83            })
84            .collect();
85        updated
86    }
87
88    /// Loads the workspace config from a file at the `config_path`.
89    pub fn load(config_path: &path::Path) -> Result<Config> {
90        let mut config: Config = toml::from_str(&Self::read(config_path)?)
91            .context("Cannot parse the wok file")?;
92
93        // Migrate from 1.0-experimental to 1.0
94        if config.version == "1.0-experimental" {
95            config.version = String::from("1.0");
96            config
97                .save(config_path)
98                .context("Cannot save migrated wok file")?;
99        }
100
101        Ok(config)
102    }
103
104    /// Reads the config file into a string (useful mainly for testing).
105    pub fn read(config_path: &path::Path) -> Result<String> {
106        fs::read_to_string(config_path).context("Cannot read the wok file")
107    }
108
109    /// Saves the workspace config to a file.
110    pub fn save(&self, config_path: &path::Path) -> Result<()> {
111        fs::write(config_path, self.dump()?).context("Cannot save the wok file")?;
112        Ok(())
113    }
114
115    /// Returns config as TOML string (useful mainly for testing).
116    pub fn dump(&self) -> Result<String> {
117        Ok(toml::to_string(self).context("Cannot serialize config")?)
118    }
119
120    fn has_repo_path(&self, path: &path::Path) -> bool {
121        assert!(!path.is_absolute());
122        self.repos.iter().any(|r| r.path == path)
123    }
124}
125
126impl Repo {
127    pub fn is_skipped_for(&self, command: &str) -> bool {
128        self.skip_for
129            .iter()
130            .any(|skip| skip.eq_ignore_ascii_case(command))
131    }
132}
133
134impl Default for Config {
135    fn default() -> Self {
136        Config::new()
137    }
138}