use std::fs::File;
use std::path::{Path, PathBuf};
use nix::fcntl::{Flock, FlockArg};
use crate::config::Config;
use crate::error::{Error, IoResultExt, Result};
use crate::namespace::{current_gid_map, current_uid_map, NsConfig};
pub struct Repo {
path: PathBuf,
config: Config,
}
impl Repo {
pub fn init(path: &Path) -> Result<Self> {
let config_path = path.join("config.toml");
if config_path.exists() {
return Err(Error::RepoExists(path.to_path_buf()));
}
std::fs::create_dir_all(path.join("objects/blobs")).with_path(path)?;
std::fs::create_dir_all(path.join("objects/trees")).with_path(path)?;
std::fs::create_dir_all(path.join("objects/commits")).with_path(path)?;
std::fs::create_dir_all(path.join("refs/heads")).with_path(path)?;
std::fs::create_dir_all(path.join("refs/tags")).with_path(path)?;
std::fs::create_dir_all(path.join("tmp")).with_path(path)?;
let uid_map = current_uid_map()?;
let gid_map = current_gid_map()?;
let config = Config::new(NsConfig { uid_map, gid_map });
config.save(&config_path)?;
Ok(Self {
path: path.to_path_buf(),
config,
})
}
pub fn open(path: &Path) -> Result<Self> {
let config_path = path.join("config.toml");
if !config_path.exists() {
return Err(Error::NoRepo(path.to_path_buf()));
}
let config = Config::load(&config_path)?;
Ok(Self {
path: path.to_path_buf(),
config,
})
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn config_mut(&mut self) -> &mut Config {
&mut self.config
}
pub fn save_config(&self) -> Result<()> {
self.config.save(&self.config_path())
}
pub fn config_path(&self) -> PathBuf {
self.path.join("config.toml")
}
pub fn objects_path(&self) -> PathBuf {
self.path.join("objects")
}
pub fn blobs_path(&self) -> PathBuf {
self.objects_path().join("blobs")
}
pub fn trees_path(&self) -> PathBuf {
self.objects_path().join("trees")
}
pub fn commits_path(&self) -> PathBuf {
self.objects_path().join("commits")
}
pub fn refs_path(&self) -> PathBuf {
self.path.join("refs/heads")
}
pub fn tags_path(&self) -> PathBuf {
self.path.join("refs/tags")
}
pub fn tmp_path(&self) -> PathBuf {
self.path.join("tmp")
}
pub fn lock_path(&self) -> PathBuf {
self.path.join(".lock")
}
pub fn lock(&self) -> Result<RepoLock> {
let lock_path = self.lock_path();
let file = File::create(&lock_path).with_path(&lock_path)?;
let flock = Flock::lock(file, FlockArg::LockExclusiveNonblock)
.map_err(|_| Error::LockContention)?;
Ok(RepoLock { flock })
}
pub fn try_lock(&self) -> Result<Option<RepoLock>> {
let lock_path = self.lock_path();
let file = File::create(&lock_path).with_path(&lock_path)?;
match Flock::lock(file, FlockArg::LockExclusiveNonblock) {
Ok(flock) => Ok(Some(RepoLock { flock })),
Err((_, nix::errno::Errno::EWOULDBLOCK)) => Ok(None),
Err(_) => Err(Error::LockContention),
}
}
}
pub struct RepoLock {
#[allow(dead_code)]
flock: Flock<File>,
}
#[allow(dead_code)]
pub fn with_lock<T, F>(repo: &Repo, f: F) -> Result<T>
where
F: FnOnce() -> Result<T>,
{
let _lock = repo.lock()?;
f()
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_repo_init() {
let dir = tempdir().unwrap();
let repo_path = dir.path().join("test-repo");
let repo = Repo::init(&repo_path).unwrap();
assert!(repo_path.join("objects/blobs").is_dir());
assert!(repo_path.join("objects/trees").is_dir());
assert!(repo_path.join("objects/commits").is_dir());
assert!(repo_path.join("refs/heads").is_dir());
assert!(repo_path.join("refs/tags").is_dir());
assert!(repo_path.join("tmp").is_dir());
assert!(repo_path.join("config.toml").is_file());
assert!(!repo.config().namespace.uid_map.is_empty());
}
#[test]
fn test_repo_init_already_exists() {
let dir = tempdir().unwrap();
let repo_path = dir.path().join("test-repo");
Repo::init(&repo_path).unwrap();
let result = Repo::init(&repo_path);
assert!(matches!(result, Err(Error::RepoExists(_))));
}
#[test]
fn test_repo_open() {
let dir = tempdir().unwrap();
let repo_path = dir.path().join("test-repo");
Repo::init(&repo_path).unwrap();
let repo = Repo::open(&repo_path).unwrap();
assert_eq!(repo.path(), repo_path);
}
#[test]
fn test_repo_open_not_found() {
let dir = tempdir().unwrap();
let repo_path = dir.path().join("nonexistent");
let result = Repo::open(&repo_path);
assert!(matches!(result, Err(Error::NoRepo(_))));
}
#[test]
fn test_repo_paths() {
let dir = tempdir().unwrap();
let repo_path = dir.path().join("test-repo");
let repo = Repo::init(&repo_path).unwrap();
assert_eq!(repo.blobs_path(), repo_path.join("objects/blobs"));
assert_eq!(repo.trees_path(), repo_path.join("objects/trees"));
assert_eq!(repo.commits_path(), repo_path.join("objects/commits"));
assert_eq!(repo.refs_path(), repo_path.join("refs/heads"));
assert_eq!(repo.tmp_path(), repo_path.join("tmp"));
}
#[test]
fn test_repo_lock() {
let dir = tempdir().unwrap();
let repo_path = dir.path().join("test-repo");
let repo = Repo::init(&repo_path).unwrap();
let lock = repo.lock().unwrap();
let result = repo.try_lock().unwrap();
assert!(result.is_none());
drop(lock);
let lock2 = repo.try_lock().unwrap();
assert!(lock2.is_some());
}
#[test]
fn test_config_modification() {
let dir = tempdir().unwrap();
let repo_path = dir.path().join("test-repo");
let mut repo = Repo::init(&repo_path).unwrap();
repo.config_mut()
.add_remote("origin", "ssh://server/repo")
.unwrap();
repo.save_config().unwrap();
let repo2 = Repo::open(&repo_path).unwrap();
assert_eq!(repo2.config().remotes.len(), 1);
assert_eq!(repo2.config().remotes[0].name, "origin");
}
}