#![allow(clippy::result_large_err)]
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use thiserror::Error;
use crate::tool_config::{RepoConfig, ToolConfig, ToolConfigError};
#[derive(Debug, Error)]
pub enum InitError {
#[error("must provide URL or --bare")]
MissingUrl,
#[error("repo path already exists: {path:?} (use --force to overwrite)")]
RepoExists {
path: PathBuf,
},
#[error("preparing clone of {url:?}: {source}")]
GixClonePrepare {
url: String,
#[source]
source: Box<gix::clone::Error>,
},
#[error("fetching during clone of {url:?}: {source}")]
GixCloneFetch {
url: String,
#[source]
source: Box<gix::clone::fetch::Error>,
},
#[error("checking out clone of {url:?}: {source}")]
GixCloneCheckout {
url: String,
#[source]
source: Box<gix::clone::checkout::main_worktree::Error>,
},
#[error("io: {0}")]
Io(#[from] io::Error),
#[error("writing tool config: {0}")]
ToolConfig(#[from] ToolConfigError),
}
pub struct InitOpts {
pub url: Option<String>,
pub repo_path: PathBuf,
pub tool_config_path: PathBuf,
pub bare: bool,
pub force: bool,
}
#[derive(Debug)]
pub struct InitReport {
pub repo_path: PathBuf,
pub tool_config_path: PathBuf,
}
pub fn init(opts: &InitOpts) -> Result<InitReport, InitError> {
if opts.url.is_none() && !opts.bare {
return Err(InitError::MissingUrl);
}
prepare_repo_path(&opts.repo_path, opts.force)?;
if opts.bare {
init_bare(&opts.repo_path)?;
} else {
gix_clone(opts.url.as_deref().unwrap(), &opts.repo_path)?;
}
let cfg = ToolConfig {
repo: RepoConfig {
path: opts.repo_path.clone(),
url: if opts.bare { None } else { opts.url.clone() },
},
};
cfg.save(&opts.tool_config_path)?;
Ok(InitReport {
repo_path: opts.repo_path.clone(),
tool_config_path: opts.tool_config_path.clone(),
})
}
fn prepare_repo_path(path: &Path, force: bool) -> Result<(), InitError> {
if path.exists() {
let non_empty = path
.read_dir()
.map(|mut d| d.next().is_some())
.unwrap_or(false);
if non_empty {
if force {
fs::remove_dir_all(path)?;
} else {
return Err(InitError::RepoExists {
path: path.to_path_buf(),
});
}
}
}
Ok(())
}
fn gix_clone(url: &str, dest: &Path) -> Result<(), InitError> {
let interrupt = AtomicBool::new(false);
let (mut checkout, _fetch_outcome) = gix::prepare_clone(url, dest)
.map_err(|e| InitError::GixClonePrepare {
url: url.to_owned(),
source: Box::new(e),
})?
.fetch_then_checkout(gix::progress::Discard, &interrupt)
.map_err(|e| InitError::GixCloneFetch {
url: url.to_owned(),
source: Box::new(e),
})?;
checkout
.main_worktree(gix::progress::Discard, &interrupt)
.map_err(|e| InitError::GixCloneCheckout {
url: url.to_owned(),
source: Box::new(e),
})?;
Ok(())
}
fn init_bare(path: &Path) -> Result<(), InitError> {
fs::create_dir_all(path)?;
let stub = path.join(".krypt.toml");
fs::write(
&stub,
concat!(
"# krypt dotfiles manifest\n",
"# See https://github.com/kryptic-sh/krypt for schema reference.\n",
"\n",
"# [[link]]\n",
"# src = \".gitconfig\"\n",
"# dst = \"${HOME}/.gitconfig\"\n",
),
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn tool_config_path(dir: &tempfile::TempDir) -> PathBuf {
dir.path().join("krypt").join("config.toml")
}
#[test]
fn missing_url_no_bare_errors() {
let repo_dir = tempdir().unwrap();
let cfg_dir = tempdir().unwrap();
let err = init(&InitOpts {
url: None,
repo_path: repo_dir.path().join("repo"),
tool_config_path: tool_config_path(&cfg_dir),
bare: false,
force: false,
})
.unwrap_err();
assert!(matches!(err, InitError::MissingUrl));
}
#[test]
fn bare_creates_stub_and_tool_config() {
let repo_dir = tempdir().unwrap();
let cfg_dir = tempdir().unwrap();
let repo_path = repo_dir.path().join("repo");
let tc_path = tool_config_path(&cfg_dir);
init(&InitOpts {
url: None,
repo_path: repo_path.clone(),
tool_config_path: tc_path.clone(),
bare: true,
force: false,
})
.unwrap();
assert!(repo_path.join(".krypt.toml").exists());
let tc = ToolConfig::load(&tc_path).unwrap().unwrap();
assert_eq!(tc.repo.path, repo_path);
assert!(tc.repo.url.is_none());
}
#[test]
fn existing_repo_without_force_errors() {
let repo_dir = tempdir().unwrap();
let cfg_dir = tempdir().unwrap();
let repo_path = repo_dir.path().join("repo");
fs::create_dir_all(&repo_path).unwrap();
fs::write(repo_path.join("existing"), b"data").unwrap();
let err = init(&InitOpts {
url: None,
repo_path,
tool_config_path: tool_config_path(&cfg_dir),
bare: true,
force: false,
})
.unwrap_err();
assert!(matches!(err, InitError::RepoExists { .. }));
}
#[test]
fn existing_repo_with_force_succeeds() {
let repo_dir = tempdir().unwrap();
let cfg_dir = tempdir().unwrap();
let repo_path = repo_dir.path().join("repo");
fs::create_dir_all(&repo_path).unwrap();
fs::write(repo_path.join("old_file"), b"old").unwrap();
init(&InitOpts {
url: None,
repo_path: repo_path.clone(),
tool_config_path: tool_config_path(&cfg_dir),
bare: true,
force: true,
})
.unwrap();
assert!(!repo_path.join("old_file").exists());
assert!(repo_path.join(".krypt.toml").exists());
}
#[test]
fn clone_from_local_file_url() {
let origin_dir = tempdir().unwrap();
let repo_dir = tempdir().unwrap();
let cfg_dir = tempdir().unwrap();
let origin_repo = gix::init(origin_dir.path()).expect("gix::init origin");
write_initial_gix_commit(&origin_repo);
let url = format!("file://{}", origin_dir.path().display());
let repo_path = repo_dir.path().join("repo");
let tc_path = tool_config_path(&cfg_dir);
init(&InitOpts {
url: Some(url.clone()),
repo_path: repo_path.clone(),
tool_config_path: tc_path.clone(),
bare: false,
force: false,
})
.unwrap();
assert!(repo_path.exists());
let tc = ToolConfig::load(&tc_path).unwrap().unwrap();
assert_eq!(tc.repo.path, repo_path);
assert_eq!(tc.repo.url.as_deref(), Some(url.as_str()));
}
fn write_initial_gix_commit(repo: &gix::Repository) {
let sig = gix::actor::SignatureRef::from_bytes(b"Test <test@test.test> 0 +0000")
.expect("valid sig");
let empty_tree = gix::objs::Tree::empty();
let tree_id = repo.write_object(&empty_tree).expect("write tree").detach();
let parents: Vec<gix::hash::ObjectId> = vec![];
repo.commit_as(sig, sig, "HEAD", "initial", tree_id, parents)
.expect("write initial commit");
}
}