git_branch_stash/
config.rs

1#[derive(Default, Clone, Debug)]
2pub struct RepoConfig {
3    pub protected_branches: Option<Vec<String>>,
4    pub capacity: Option<usize>,
5}
6
7static STACK_FIELD: &str = "stack.stack";
8static PROTECTED_STACK_FIELD: &str = "stack.protected-branch";
9static BACKUP_CAPACITY_FIELD: &str = "branch-stash.capacity";
10
11static DEFAULT_PROTECTED_BRANCHES: [&str; 4] = ["main", "master", "dev", "stable"];
12const DEFAULT_CAPACITY: usize = 30;
13
14impl RepoConfig {
15    pub fn from_all(repo: &git2::Repository) -> eyre::Result<Self> {
16        log::trace!("Loading gitconfig");
17        let default_config = match git2::Config::open_default() {
18            Ok(config) => Some(config),
19            Err(err) => {
20                log::debug!("Failed to load git config: {err}");
21                None
22            }
23        };
24        let config = Self::from_defaults_internal(default_config.as_ref());
25        let config = if let Some(default_config) = default_config.as_ref() {
26            config.update(Self::from_gitconfig(default_config))
27        } else {
28            config
29        };
30        let config = config.update(Self::from_workdir(repo)?);
31        let config = config.update(Self::from_repo(repo)?);
32        let config = config.update(Self::from_env());
33        Ok(config)
34    }
35
36    pub fn from_repo(repo: &git2::Repository) -> eyre::Result<Self> {
37        let config_path = git_dir_config(repo);
38        log::trace!("Loading {}", config_path.display());
39        if config_path.exists() {
40            match git2::Config::open(&config_path) {
41                Ok(config) => Ok(Self::from_gitconfig(&config)),
42                Err(err) => {
43                    log::debug!("Failed to load git config: {err}");
44                    Ok(Default::default())
45                }
46            }
47        } else {
48            Ok(Default::default())
49        }
50    }
51
52    pub fn from_workdir(repo: &git2::Repository) -> eyre::Result<Self> {
53        let workdir = repo
54            .workdir()
55            .ok_or_else(|| eyre::eyre!("Cannot read config in bare repository."))?;
56        let config_path = workdir.join(".gitconfig");
57        log::trace!("Loading {}", config_path.display());
58        if config_path.exists() {
59            match git2::Config::open(&config_path) {
60                Ok(config) => Ok(Self::from_gitconfig(&config)),
61                Err(err) => {
62                    log::debug!("Failed to load git config: {err}");
63                    Ok(Default::default())
64                }
65            }
66        } else {
67            Ok(Default::default())
68        }
69    }
70
71    pub fn from_env() -> Self {
72        let mut config = Self::default();
73
74        let params = git_config_env::ConfigParameters::new();
75        config = config.update(Self::from_env_iter(params.iter()));
76
77        let params = git_config_env::ConfigEnv::new();
78        config = config.update(Self::from_env_iter(
79            params.iter().map(|(k, v)| (k, Some(v))),
80        ));
81
82        config
83    }
84
85    fn from_env_iter<'s>(
86        iter: impl Iterator<Item = (std::borrow::Cow<'s, str>, Option<std::borrow::Cow<'s, str>>)>,
87    ) -> Self {
88        let mut config = Self::default();
89
90        for (key, value) in iter {
91            log::trace!("Env config: {key}={value:?}");
92            if key == PROTECTED_STACK_FIELD {
93                if let Some(value) = value {
94                    config
95                        .protected_branches
96                        .get_or_insert_with(Vec::new)
97                        .push(value.into_owned());
98                }
99            } else if key == BACKUP_CAPACITY_FIELD {
100                config.capacity = value.as_deref().and_then(|s| s.parse::<usize>().ok());
101            } else {
102                log::warn!(
103                    "Unsupported config: {}={}",
104                    key,
105                    value.as_deref().unwrap_or("")
106                );
107            }
108        }
109
110        config
111    }
112
113    pub fn from_defaults() -> Self {
114        log::trace!("Loading gitconfig");
115        let config = match git2::Config::open_default() {
116            Ok(config) => Some(config),
117            Err(err) => {
118                log::debug!("Failed to load git config: {err}");
119                None
120            }
121        };
122        Self::from_defaults_internal(config.as_ref())
123    }
124
125    fn from_defaults_internal(config: Option<&git2::Config>) -> Self {
126        let mut conf = Self::default();
127
128        let mut protected_branches: Vec<String> = Vec::new();
129        if let Some(config) = config {
130            let default_branch = default_branch(config);
131            let default_branch_ignore = default_branch.to_owned();
132            protected_branches.push(default_branch_ignore);
133        }
134        // Don't bother with removing duplicates if `default_branch` is the same as one of our
135        // default protected branches
136        protected_branches.extend(DEFAULT_PROTECTED_BRANCHES.iter().map(|s| (*s).to_owned()));
137        conf.protected_branches = Some(protected_branches);
138
139        conf.capacity = Some(DEFAULT_CAPACITY);
140
141        conf
142    }
143
144    pub fn from_gitconfig(config: &git2::Config) -> Self {
145        let protected_branches = config
146            .multivar(PROTECTED_STACK_FIELD, None)
147            .map(|entries| {
148                let mut protected_branches = Vec::new();
149                entries
150                    .for_each(|entry| {
151                        if let Some(value) = entry.value() {
152                            protected_branches.push(value.to_owned());
153                        }
154                    })
155                    .unwrap();
156                if protected_branches.is_empty() {
157                    None
158                } else {
159                    Some(protected_branches)
160                }
161            })
162            .unwrap_or(None);
163
164        let capacity = config
165            .get_i64(BACKUP_CAPACITY_FIELD)
166            .map(|i| i as usize)
167            .ok();
168
169        Self {
170            protected_branches,
171            capacity,
172        }
173    }
174
175    pub fn write_repo(&self, repo: &git2::Repository) -> eyre::Result<()> {
176        let config_path = git_dir_config(repo);
177        log::trace!("Loading {}", config_path.display());
178        let mut config = git2::Config::open(&config_path)?;
179        log::info!("Writing {}", config_path.display());
180        self.to_gitconfig(&mut config)?;
181        Ok(())
182    }
183
184    pub fn to_gitconfig(&self, config: &mut git2::Config) -> eyre::Result<()> {
185        if let Some(protected_branches) = self.protected_branches.as_ref() {
186            // Ignore errors if there aren't keys to remove
187            let _ = config.remove_multivar(PROTECTED_STACK_FIELD, ".*");
188            for branch in protected_branches {
189                config.set_multivar(PROTECTED_STACK_FIELD, "^$", branch)?;
190            }
191        }
192        Ok(())
193    }
194
195    pub fn update(mut self, other: Self) -> Self {
196        match (&mut self.protected_branches, other.protected_branches) {
197            (Some(lhs), Some(rhs)) => lhs.extend(rhs),
198            (None, Some(rhs)) => self.protected_branches = Some(rhs),
199            (_, _) => (),
200        }
201        self.capacity = other.capacity.or(self.capacity);
202
203        self
204    }
205
206    pub fn protected_branches(&self) -> &[String] {
207        self.protected_branches.as_deref().unwrap_or(&[])
208    }
209
210    pub fn capacity(&self) -> Option<usize> {
211        let capacity = self.capacity.unwrap_or(DEFAULT_CAPACITY);
212        (capacity != 0).then_some(capacity)
213    }
214}
215
216impl std::fmt::Display for RepoConfig {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        writeln!(f, "[{}]", STACK_FIELD.split_once('.').unwrap().0)?;
219        for branch in self.protected_branches() {
220            writeln!(
221                f,
222                "\t{}={}",
223                PROTECTED_STACK_FIELD.split_once('.').unwrap().1,
224                branch
225            )?;
226        }
227        writeln!(f, "[{}]", BACKUP_CAPACITY_FIELD.split_once('.').unwrap().0)?;
228        writeln!(
229            f,
230            "\t{}={}",
231            BACKUP_CAPACITY_FIELD.split_once('.').unwrap().1,
232            self.capacity().unwrap_or(0)
233        )?;
234        Ok(())
235    }
236}
237
238fn git_dir_config(repo: &git2::Repository) -> std::path::PathBuf {
239    repo.path().join("config")
240}
241
242fn default_branch(config: &git2::Config) -> &str {
243    config.get_str("init.defaultBranch").ok().unwrap_or("main")
244}