use anyhow::{Result, bail};
pub const KEY: &str = "safe.bareRepository";
pub const VALUE: &str = "explicit";
const COUNT: &str = "GIT_CONFIG_COUNT";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Change {
Set(String, String),
Unset(String),
}
pub type Lookup<'a> = &'a dyn Fn(&str) -> Option<String>;
fn key_var(index: usize) -> String {
format!("GIT_CONFIG_KEY_{index}")
}
fn value_var(index: usize) -> String {
format!("GIT_CONFIG_VALUE_{index}")
}
fn count(get: Lookup) -> Result<usize> {
match get(COUNT) {
None => Ok(0),
Some(raw) if raw.is_empty() => Ok(0),
Some(raw) if raw.bytes().all(|b| b.is_ascii_digit()) => match raw.parse() {
Ok(n) => Ok(n),
Err(_) => bail!("{COUNT}={raw} is too large"),
},
Some(raw) => bail!("{COUNT}={raw} is not a number, so git refuses every command"),
}
}
fn is_key(name: Option<String>) -> bool {
name.is_some_and(|k| k.eq_ignore_ascii_case(KEY))
}
pub fn present(get: Lookup) -> bool {
let Ok(n) = count(get) else {
return false;
};
(0..n)
.rev()
.find(|&i| is_key(get(&key_var(i))))
.is_some_and(|i| get(&value_var(i)).as_deref() == Some(VALUE))
}
pub fn add(get: Lookup) -> Result<Vec<(String, String)>> {
let n = count(get)?;
if present(get) {
return Ok(Vec::new());
}
Ok(vec![
(COUNT.to_owned(), (n + 1).to_string()),
(key_var(n), KEY.to_owned()),
(value_var(n), VALUE.to_owned()),
])
}
pub fn remove(get: Lookup) -> Vec<Change> {
let Ok(n) = count(get) else {
return Vec::new();
};
let entries: Vec<(Option<String>, Option<String>)> = (0..n)
.map(|i| (get(&key_var(i)), get(&value_var(i))))
.collect();
let ours = |(k, v): &(Option<String>, Option<String>)| {
is_key(k.clone()) && v.as_deref() == Some(VALUE)
};
if !entries.iter().any(ours) {
return Vec::new();
}
let kept: Vec<_> = entries.iter().filter(|e| !ours(e)).collect();
let mut changes = Vec::new();
let mut put = |var: String, value: &Option<String>| {
changes.push(match value {
Some(v) => Change::Set(var, v.clone()),
None => Change::Unset(var),
});
};
for (i, (k, v)) in kept.iter().enumerate() {
put(key_var(i), k);
put(value_var(i), v);
}
for i in kept.len()..n {
put(key_var(i), &None);
put(value_var(i), &None);
}
let left = (!kept.is_empty()).then(|| kept.len().to_string());
put(COUNT.to_owned(), &left);
changes
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
fn env(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs
.iter()
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
.collect()
}
fn added(map: &mut BTreeMap<String, String>) {
let lookup = map.clone();
for (k, v) in add(&|k| lookup.get(k).cloned()).unwrap() {
map.insert(k, v);
}
}
fn removed(map: &mut BTreeMap<String, String>) {
let lookup = map.clone();
for change in remove(&|k| lookup.get(k).cloned()) {
match change {
Change::Set(k, v) => map.insert(k, v),
Change::Unset(k) => map.remove(&k),
};
}
}
#[test]
fn an_empty_environment_gets_entry_zero() {
let mut map = env(&[]);
added(&mut map);
let expected = env(&[
("GIT_CONFIG_COUNT", "1"),
("GIT_CONFIG_KEY_0", "safe.bareRepository"),
("GIT_CONFIG_VALUE_0", "explicit"),
]);
assert_eq!(map, expected);
added(&mut map);
assert_eq!(map, expected, "adding twice changes nothing");
removed(&mut map);
assert!(map.is_empty(), "{map:?}");
}
#[test]
fn existing_entries_are_kept_and_ours_comes_last() {
let user = env(&[
("GIT_CONFIG_COUNT", "2"),
("GIT_CONFIG_KEY_0", "user.name"),
("GIT_CONFIG_VALUE_0", "me"),
("GIT_CONFIG_KEY_1", "SAFE.bareRepository"),
("GIT_CONFIG_VALUE_1", "all"),
]);
let mut map = user.clone();
added(&mut map);
assert!(!present(&|k| user.get(k).cloned()), "a later `all` wins");
assert_eq!(map["GIT_CONFIG_COUNT"], "3");
assert_eq!(map["GIT_CONFIG_KEY_2"], "safe.bareRepository");
assert_eq!(map["GIT_CONFIG_VALUE_2"], "explicit");
assert!(present(&|k| map.get(k).cloned()));
removed(&mut map);
assert_eq!(map, user);
}
#[test]
fn removing_renumbers_the_entries_after_ours() {
let mut map = env(&[
("GIT_CONFIG_COUNT", "2"),
("GIT_CONFIG_KEY_0", "safe.bareRepository"),
("GIT_CONFIG_VALUE_0", "explicit"),
("GIT_CONFIG_KEY_1", "user.name"),
("GIT_CONFIG_VALUE_1", "me"),
]);
removed(&mut map);
let expected = env(&[
("GIT_CONFIG_COUNT", "1"),
("GIT_CONFIG_KEY_0", "user.name"),
("GIT_CONFIG_VALUE_0", "me"),
]);
assert_eq!(map, expected);
assert_eq!(remove(&|k| map.get(k).cloned()), Vec::<Change>::new());
}
#[test]
fn a_count_git_would_refuse_is_an_error() {
for bad in ["x", "-1", "1 ", "99999999999999999999999"] {
let map = env(&[("GIT_CONFIG_COUNT", bad)]);
let get = |k: &str| map.get(k).cloned();
assert!(add(&get).is_err(), "{bad}");
assert!(!present(&get) && remove(&get).is_empty(), "{bad}");
}
let empty = env(&[("GIT_CONFIG_COUNT", "")]);
assert_eq!(add(&|k| empty.get(k).cloned()).unwrap().len(), 3);
}
}