#![cfg_attr(windows, allow(dead_code))] use std::{ io::Write, path::{Path, PathBuf}, process::Command, }; use bstr::{BString, ByteSlice}; use git_config::file::init::{self}; use crate::file::{ cow_str, init::from_paths::{escape_backslashes, includes::conditional::options_with_git_dir}, }; #[derive(Debug)] pub struct GitEnv { tempdir: tempfile::TempDir, root_dir: PathBuf, git_dir: PathBuf, home_dir: PathBuf, } #[derive(Copy, Clone, Eq, PartialEq)] enum ConfigLocation { Repo, User, } #[derive(Copy, Clone)] enum Value { Original, Override, } pub struct Condition { condition: String, value: Option, config_location: ConfigLocation, } impl Condition { pub fn new(condition: impl Into) -> Self { Condition { condition: condition.into(), value: Value::Override.into(), config_location: ConfigLocation::Repo, } } pub fn set_user_config_instead_of_repo_config(mut self) -> Self { self.config_location = ConfigLocation::User; self } pub fn expect_original_value(mut self) -> Self { self.value = Value::Original.into(); self } pub fn expect_no_value(mut self) -> Self { self.value = None; self } } impl GitEnv { pub fn repo_name(repo_name: impl AsRef) -> crate::Result { let tempdir = tempfile::tempdir()?; let root_dir = git_path::realpath(tempdir.path())?; let worktree_dir = root_dir.join(repo_name); std::fs::create_dir_all(&worktree_dir)?; let home_dir = git_path::realpath(tempdir.path())?; Ok(Self { tempdir, root_dir, git_dir: worktree_dir.join(".git"), home_dir, }) } pub fn repo_in_home() -> crate::Result { Self::repo_name("") } } impl GitEnv { pub fn to_init_options(&self) -> init::Options<'_> { let mut opts = options_with_git_dir(self.git_dir()); opts.includes.interpolate.home_dir = Some(self.home_dir()); opts } pub fn git_dir(&self) -> &Path { &self.git_dir } pub fn set_git_dir(&mut self, git_dir: PathBuf) { self.git_dir = git_dir; } pub fn worktree_dir(&self) -> &Path { self.git_dir .parent() .expect("this is .git as subdirectory of the worktree") } pub fn home_dir(&self) -> &Path { &self.home_dir } pub fn root_dir(&self) -> &Path { &self.root_dir } } pub fn assert_section_value( Condition { condition, value: expected, config_location, }: Condition, env: GitEnv, ) -> crate::Result { write_config(condition, &env, config_location)?; let mut paths = vec![env.git_dir().join("config")]; if config_location == ConfigLocation::User { paths.push(env.home_dir().join(".gitconfig")); } let config = git_config::File::from_paths_metadata( paths .into_iter() .map(|path| git_config::file::Metadata::try_from_path(path, git_config::Source::Local).unwrap()), env.to_init_options(), )? .expect("non-empty"); assert_eq!( config.string("section", None, "value"), match expected { Some(Value::Original) => Some(cow_str("base-value")), Some(Value::Override) => Some(cow_str("override-value")), None => None, }, "git-config disagrees with the expected value, {:?} for debugging", env.tempdir.into_path() ); assure_git_agrees(expected, env) } pub fn git_env_with_symlinked_repo() -> crate::Result { let mut env = GitEnv::repo_name("worktree")?; let link_destination = env.root_dir().join("symlink-worktree"); crate::file::init::from_paths::includes::conditional::create_symlink(&link_destination, env.worktree_dir()); let git_dir_through_symlink = link_destination.join(".git"); env.set_git_dir(git_dir_through_symlink); Ok(env) } fn assure_git_agrees(expected: Option, env: GitEnv) -> crate::Result { let output = Command::new("git") .args(["config", "--get", "section.value"]) .env("HOME", env.home_dir()) .env("GIT_DIR", env.git_dir()) .env_remove("GIT_CONFIG_COUNT") .env_remove("XDG_CONFIG_HOME") .current_dir(env.worktree_dir()) .output()?; assert_eq!( output.status.success(), expected.is_some(), "{:?}, {:?} for debugging", output, env.tempdir.into_path() ); let git_output: BString = output.stdout.trim_end().into(); assert_eq!( git_output, match expected { Some(Value::Original) => "base-value", Some(Value::Override) => "override-value", None => "", }, "git disagrees with git-config, {:?} for debugging", env.tempdir.into_path() ); Ok(()) } fn write_config(condition: impl AsRef, env: &GitEnv, overwrite_config_location: ConfigLocation) -> crate::Result { let include_config = write_included_config(env)?; write_main_config(condition, include_config, env, overwrite_config_location) } fn write_included_config(env: &GitEnv) -> crate::Result { let include_path = env.worktree_dir().join("include.path"); write_append_config_value(&include_path, "override-value")?; Ok(include_path) } fn write_append_config_value(path: impl AsRef, value: &str) -> crate::Result { let mut file = std::fs::OpenOptions::new().append(true).create(true).open(path)?; file.write_all( format!( " [section] value = {value}" ) .as_bytes(), )?; Ok(()) } fn write_main_config( condition: impl AsRef, include_file_path: PathBuf, env: &GitEnv, overwrite_config_location: ConfigLocation, ) -> crate::Result { git_repository::init(env.worktree_dir())?; if overwrite_config_location == ConfigLocation::Repo { write_append_config_value(env.git_dir().join("config"), "base-value")?; } let config_file_path = match overwrite_config_location { ConfigLocation::User => env.home_dir().join(".gitconfig"), ConfigLocation::Repo => env.git_dir().join("config"), }; let condition = condition.as_ref(); let condition = { let c = condition .replace("$gitdir", &env.git_dir().to_string_lossy()) .replace("$worktree", &env.worktree_dir().to_string_lossy()); if c == condition { condition.to_owned() } else { escape_backslashes(c) } }; let include_file_path = escape_backslashes(include_file_path); let mut file = std::fs::OpenOptions::new() .append(true) .create(true) .open(config_file_path)?; file.write_all( format!( " [includeIf \"{condition}\"] path = {include_file_path}", ) .as_bytes(), )?; Ok(()) }