use std::{
path::{Path, PathBuf},
sync::OnceLock,
};
use anyhow::{Result, anyhow};
use assert2::assert;
use config_file2::LoadConfigFile;
use log::{info, warn};
use crate::{
config::{CONFIG_FILE_NAME, Config},
utils::prompt_password,
};
pub const GIT_CONFIG_PREFIX: &str =
const_str::replace!(concat!(env!("CARGO_CRATE_NAME"), "."), "_", "-");
#[derive(Debug, Clone, Default)]
pub struct Repo {
pub path: PathBuf,
pub conf: Config,
pub key_sha: OnceLock<Box<[u8]>>,
}
impl Repo {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
debug_assert!(path.as_ref().is_absolute(), "given path must be absolute");
let mut repo_path = path.as_ref().to_path_buf();
assert!(
repo_path.exists(),
"Repo not found: {}",
repo_path.display()
);
assert!(
repo_path.is_dir(),
"Not a directory: {}",
repo_path.display()
);
if repo_path
.file_name()
.ok_or_else(|| anyhow!("Filename not found"))?
== ".git"
{
repo_path.pop();
}
info!("Open repo: {}", repo_path.display());
let config_file_path = repo_path.join(CONFIG_FILE_NAME);
if !config_file_path.exists() {
warn!(
"Config file not found: `{}`, using default config instead...",
config_file_path.display()
);
}
let conf = Config::load_or_default(&config_file_path)?.with_repo_path(&repo_path);
Ok(Self {
path: repo_path,
conf,
key_sha: OnceLock::new(),
})
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn to_absolute_path(&self, path: impl AsRef<Path>) -> PathBuf {
self.path.join(path.as_ref())
}
pub fn get_key(&self) -> String {
self.get_config("key")
.expect("Key not found, please exec `git-se p` first.")
}
pub fn set_key_interactive(&self) -> Result<()> {
let key = prompt_password("Please input your key: ")?;
self.set_config("key", &key)?;
info!("Set key: `{key}`");
Ok(())
}
}
pub trait GitCommand {
fn run(&self, args: &[&str]) -> Result<()>;
fn run_with_output(&self, args: &[&str]) -> Result<String>;
fn set_config(&self, key: &str, value: &str) -> Result<()>;
fn get_config(&self, key: &str) -> Result<String>;
}
impl GitCommand for Repo {
fn run(&self, args: &[&str]) -> Result<()> {
let output = std::process::Command::new("git")
.current_dir(&self.path)
.args(args)
.output()?;
if !output.status.success() {
return Err(anyhow!(
"Git command failed: {}",
String::from_utf8_lossy(&output.stderr)
));
}
Ok(())
}
fn run_with_output(&self, args: &[&str]) -> Result<String> {
let mut cmd = std::process::Command::new("git");
if cfg!(test) {
cmd.env("LC_ALL", "C.UTF-8").env("LANGUAGE", "C.UTF-8");
}
let output = cmd.current_dir(&self.path).args(args).output()?;
if !output.status.success() {
return Err(anyhow!(
"Git command failed: {}",
String::from_utf8_lossy(&output.stderr)
));
}
Ok(String::from_utf8(output.stdout)?)
}
fn set_config(&self, key: &str, value: &str) -> Result<()> {
let temp = String::from(GIT_CONFIG_PREFIX) + key;
self.run(&["config", "--local", &temp, value.trim()])
}
fn get_config(&self, key: &str) -> Result<String> {
let temp = String::from(GIT_CONFIG_PREFIX) + key;
self.run_with_output(&["config", "--get", &temp])
.map(|x| x.trim().to_string())
}
}
#[cfg(test)]
mod tests {
use path_absolutize::Absolutize;
use super::*;
#[test]
fn test_repo_open() -> Result<()> {
let repo = Repo::open(Path::new(".").absolutize()?)?;
assert_eq!(repo.path().file_name().unwrap(), "git-simple-encrypt");
let repo = Repo::open(Path::new("./.git").absolutize()?)?;
assert_eq!(repo.path().file_name().unwrap(), "git-simple-encrypt");
Ok(())
}
}