use crate::{Res, config_git, error::Error};
use std::{collections::BTreeMap, path::Path, process::Command};
pub(crate) fn set_feature_keys(dir: &Path, keys: &BTreeMap<String, char>) -> Res<()> {
set_feature_keys_with(&git_in(dir), keys)
}
pub(crate) fn set_theme(dir: &Path, theme: Option<&str>) -> Res<()> {
set_theme_with(&git_in(dir), theme)
}
fn git_in(dir: &Path) -> impl Fn() -> Command {
move || {
let mut git = Command::new("git");
git.current_dir(dir);
git
}
}
fn set_theme_with(git: &dyn Fn() -> Command, theme: Option<&str>) -> Res<()> {
let name = config_git::THEME;
let file = file_for(git, &format!("^{}$", name.replace('.', "\\.")))?;
match theme {
Some(theme) => run(git().arg("config").args(&file).args([name, theme]))?,
None => run(git().arg("config").args(&file).args(["--unset-all", name]))?,
};
Ok(())
}
fn file_for(git: &dyn Fn() -> Command, regex: &str) -> Res<Vec<String>> {
Ok(
match origin(git, regex)?.or(origin(git, &format!("^{}\\.", config_git::SECTION))?) {
Some(file) => vec!["--file".to_owned(), file],
None => vec!["--global".to_owned()],
},
)
}
fn set_feature_keys_with(git: &dyn Fn() -> Command, keys: &BTreeMap<String, char>) -> Res<()> {
let prefix = format!("{}.", config_git::FEATURE_KEYS_SECTION);
let regex = format!("^{}", prefix.replace('.', "\\."));
let file = file_for(git, ®ex)?;
let held = entries(git, &file, ®ex)?;
for (name, value) in &held {
let feature = &name[prefix.len()..];
if keys.get(feature).map(char::to_string).as_ref() != Some(value) {
run(git()
.args(["config"])
.args(&file)
.args(["--unset-all", name]))?;
}
}
for (feature, key) in keys {
let name = format!("{prefix}{feature}");
let key = key.to_string();
if !held.contains(&(name.clone(), key.clone())) {
run(git().args(["config"]).args(&file).args([&name, &key]))?;
}
}
Ok(())
}
fn origin(git: &dyn Fn() -> Command, regex: &str) -> Res<Option<String>> {
let output = run(git().args(["config", "--show-origin", "--null", "--get-regexp", regex]))?;
let fields: Vec<&str> = output.split('\0').collect();
Ok(fields
.chunks(2)
.filter_map(|entry| entry[0].strip_prefix("file:"))
.next_back()
.map(str::to_owned))
}
fn entries(git: &dyn Fn() -> Command, file: &[String], regex: &str) -> Res<Vec<(String, String)>> {
let output = run(git()
.arg("config")
.args(file)
.args(["--null", "--get-regexp", regex]))?;
Ok(output
.split('\0')
.filter_map(|entry| entry.split_once('\n'))
.map(|(name, value)| (name.to_owned(), value.to_owned()))
.collect())
}
fn run(git: &mut Command) -> Res<String> {
let output = git.output().map_err(|e| Error::EditConfig(e.to_string()))?;
if !output.status.success() && output.status.code() != Some(1) {
return Err(Error::EditConfig(
String::from_utf8_lossy(&output.stderr).trim().to_owned(),
));
}
String::from_utf8(output.stdout).map_err(|e| Error::EditConfig(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::init_config_from;
use std::{fs, path::PathBuf};
use temp_dir::TempDir;
fn keys(keys: &[(&str, char)]) -> BTreeMap<String, char> {
keys.iter()
.map(|(feature, key)| ((*feature).to_owned(), *key))
.collect()
}
struct Repo {
dir: TempDir,
}
impl Repo {
fn new(global: &str) -> Self {
let dir = TempDir::new().unwrap();
fs::create_dir(dir.path().join("repo")).unwrap();
fs::write(dir.path().join("global.gitconfig"), global).unwrap();
let repo = Self { dir };
assert!(repo.git().arg("init").output().unwrap().status.success());
repo
}
fn path(&self, name: &str) -> PathBuf {
self.dir.path().join(name)
}
fn git(&self) -> Command {
let mut git = Command::new("git");
git.current_dir(self.path("repo"))
.env("GIT_CONFIG_GLOBAL", self.path("global.gitconfig"))
.env("GIT_CONFIG_NOSYSTEM", "1");
git
}
fn set_feature_keys(&self, keys: &BTreeMap<String, char>) {
set_feature_keys_with(&|| self.git(), keys).unwrap();
}
fn feature_keys(&self, name: &str) -> Vec<(String, char)> {
let git_config = git2::Config::open(&self.path(name)).unwrap();
init_config_from(self.path("none.toml"), Some(&git_config))
.unwrap()
.general
.diff_renderer
.feature_keys
.into_iter()
.collect()
}
fn read(&self, name: &str) -> String {
fs::read_to_string(self.path(name)).unwrap()
}
}
#[test]
fn keys_go_in_the_global_git_config_by_default() {
let repo = Repo::new("");
repo.set_feature_keys(&keys(&[("side-by-side", 'x')]));
assert_eq!(
repo.feature_keys("global.gitconfig"),
[("side-by-side".into(), 'x')]
);
}
#[test]
fn keys_go_in_the_file_holding_the_delta_tui_section() {
let repo = Repo::new("[include]\n\tpath = tui.gitconfig\n");
fs::write(
repo.path("tui.gitconfig"),
"[delta-tui]\n\tstart-folded = true\n",
)
.unwrap();
repo.set_feature_keys(&keys(&[("side-by-side", 'x')]));
assert_eq!(
repo.read("global.gitconfig"),
"[include]\n\tpath = tui.gitconfig\n"
);
assert_eq!(
repo.feature_keys("tui.gitconfig"),
[("side-by-side".into(), 'x')]
);
}
#[test]
fn everything_else_in_the_file_is_kept_as_written() {
let mine = "# My settings\n\
[delta-tui]\n\
\t# Offered by -.\n\
\tvisit-context-lines = true # mine\n\
[delta-tui \"general.diff-renderer.feature-keys\"]\n\
\tline-numbers = w # mine\n";
let repo = Repo::new(mine);
repo.set_feature_keys(&keys(&[("line-numbers", 'w'), ("side-by-side", 'x')]));
let written = repo.read("global.gitconfig");
assert!(written.starts_with(mine), "{written}");
assert_eq!(
repo.feature_keys("global.gitconfig"),
[("line-numbers".into(), 'w'), ("side-by-side".into(), 'x')]
);
}
#[test]
fn a_feature_left_out_loses_its_key() {
let repo = Repo::new("");
repo.set_feature_keys(&keys(&[("side-by-side", 'x'), ("line-numbers", 'w')]));
repo.set_feature_keys(&keys(&[("line-numbers", 'w')]));
assert_eq!(
repo.feature_keys("global.gitconfig"),
[("line-numbers".into(), 'w')]
);
}
#[test]
fn a_key_moves_to_the_feature_it_is_set_for() {
let repo = Repo::new("");
repo.set_feature_keys(&keys(&[("side-by-side", 'x'), ("line-numbers", 'w')]));
repo.set_feature_keys(&keys(&[("line-numbers", 'x')]));
assert_eq!(
repo.feature_keys("global.gitconfig"),
[("line-numbers".into(), 'x')]
);
}
#[test]
fn a_linked_config_file_is_written_through_the_link() {
let repo = Repo::new("[include]\n\tpath = tui.gitconfig\n");
fs::write(
repo.path("dotfiles.gitconfig"),
"[delta-tui]\n\tstart-folded = true\n",
)
.unwrap();
std::os::unix::fs::symlink(repo.path("dotfiles.gitconfig"), repo.path("tui.gitconfig"))
.unwrap();
repo.set_feature_keys(&keys(&[("side-by-side", 'x')]));
assert!(
fs::symlink_metadata(repo.path("tui.gitconfig"))
.unwrap()
.is_symlink()
);
assert_eq!(
repo.feature_keys("dotfiles.gitconfig"),
[("side-by-side".into(), 'x')]
);
}
}