use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use crate::gitcmd;
use crate::names;
pub struct Store {
pub root: PathBuf,
}
impl Store {
pub fn new(root: impl AsRef<Path>) -> Result<Self> {
let root = root.as_ref();
std::fs::create_dir_all(root).with_context(|| format!("create repo root {root:?}"))?;
let root = root.canonicalize().with_context(|| format!("resolve repo root {root:?}"))?;
Ok(Store { root })
}
pub fn path(&self, name: &str) -> Result<PathBuf> {
names::validate_repo_path(name)?;
Ok(self.root.join(format!("{name}.git")))
}
pub async fn exists(&self, name: &str) -> bool {
match self.path(name) {
Ok(path) => tokio::fs::try_exists(&path).await.unwrap_or(false),
Err(_) => false,
}
}
pub fn list(&self) -> Result<Vec<String>> {
let mut names = Vec::new();
list_under(&self.root, "", &mut names)?;
names.retain(|n| names::validate_repo_path(n).is_ok());
Ok(names)
}
pub async fn init_bare(&self, name: &str) -> Result<PathBuf> {
let path = self.path(name)?;
if path.exists() {
bail!("repo {name:?} already exists");
}
tokio::fs::create_dir_all(&path).await?;
gitcmd::run(&path, &["init", "--bare", "."]).await?;
let storaged_bin = std::env::current_exe().context("resolve nugitea-storaged binary path")?;
install_hooks(&path, &storaged_bin).await?;
Ok(path)
}
}
fn list_under(dir: &Path, prefix: &str, out: &mut Vec<String>) -> Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
let Some(file_name) = entry.file_name().to_str().map(str::to_string) else { continue };
match file_name.strip_suffix(".git") {
Some(name) => out.push(format!("{prefix}{name}")),
None => list_under(&entry.path(), &format!("{prefix}{file_name}/"), out)?,
}
}
Ok(())
}
const HOOK_NAMES: [&str; 3] = ["pre-receive", "update", "post-receive"];
async fn install_hooks(repo_path: &Path, storaged_bin: &Path) -> Result<()> {
let hooks_dir = repo_path.join("hooks");
tokio::fs::create_dir_all(&hooks_dir).await?;
let bin_quoted = shell_quote_single(&storaged_bin.to_string_lossy());
for name in HOOK_NAMES {
let script =
format!("#!/bin/sh\n# AUTO GENERATED BY NUGITEA, DO NOT MODIFY\nexec {bin_quoted} hook {name} \"$@\"\n");
let path = hooks_dir.join(name);
tokio::fs::write(&path, script).await.with_context(|| format!("write hook {name}"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
tokio::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).await?;
}
}
Ok(())
}
fn shell_quote_single(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn nested_layout() {
let root = std::env::temp_dir().join(format!("nugitea-storage-test-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let store = Store::new(&root).unwrap();
assert_eq!(store.path("acme/platform/api").unwrap(), store.root.join("acme/platform/api.git"));
assert!(store.path("../x").is_err());
std::fs::create_dir_all(store.root.join("acme/platform/api.git")).unwrap();
std::fs::create_dir_all(store.root.join("alice/dotfiles.git")).unwrap();
std::fs::create_dir_all(store.root.join("legacy.git")).unwrap();
let mut names = store.list().unwrap();
names.sort();
assert_eq!(names, ["acme/platform/api", "alice/dotfiles"]);
assert!(store.exists("acme/platform/api").await);
std::fs::remove_dir_all(&root).unwrap();
}
}