use crate::{ parser::{GitReceivePackArgs, GitUploadPackArgs}, user_info::{get_user_groups, get_username}, ShackleError, }; use git2::{ErrorCode, Repository, RepositoryInitMode, RepositoryInitOptions}; use std::{ fs, path::{Path, PathBuf}, process::Command, }; pub struct GitInitResult { pub path: PathBuf, } fn git_dir_prefix() -> PathBuf { PathBuf::from("git") } fn personal_git_dir() -> Result { let username = get_username().ok_or(ShackleError::UserReadError)?; Ok(git_dir_prefix().join(username)) } fn verify_user_is_in_group(group: &str) -> bool { let user_groups = get_user_groups(); user_groups.iter().any(|g| g == group) } fn group_git_dir(group: &str) -> PathBuf { git_dir_prefix().join(group) } fn is_valid_git_repo_path(path: &Path) -> Result { let prefix = git_dir_prefix(); let relative_path = match path.strip_prefix(&prefix) { Ok(relative_path) => relative_path, Err(_) => { return Ok(false); } }; let mut it = relative_path.iter(); let group = it.next(); let repo_name = it.next(); let end = it.next(); match (group, repo_name, end) { (_, _, Some(_)) | (None, _, _) | (_, None, _) => Ok(false), (Some(group_name), Some(_repo_name), _) => { if relative_path.extension().map(|ext| ext == "git") != Some(true) { Ok(false) } else { let group_name = group_name.to_string_lossy(); let user_name = get_username(); let is_valid_personal_repo_path = user_name .map(|user_name| user_name == group_name) .unwrap_or(false); let user_groups = get_user_groups(); let is_valid_shared_repo_path = user_groups.iter().any(|group| group.as_ref() == group_name); Ok(is_valid_personal_repo_path || is_valid_shared_repo_path) } } } } pub fn init( repo_name: &str, group: &Option, description: &Option, branch: &str, ) -> Result { let mut init_opts = RepositoryInitOptions::new(); init_opts .bare(true) .mkdir(true) .no_reinit(true) .initial_head(branch); let path = match group { Some(group) => { if !verify_user_is_in_group(group) { return Err(ShackleError::InvalidGroup); } init_opts.mode(RepositoryInitMode::SHARED_GROUP); group_git_dir(group).join(repo_name).with_extension("git") } None => personal_git_dir()?.join(repo_name).with_extension("git"), }; Repository::init_opts(&path, &init_opts)?; if let Some(description) = description { // There is an init option for setting the description but it seems to // just do nothing? set_description(&path, description)?; } Ok(GitInitResult { path }) } pub struct RepoMetadata { pub path: PathBuf, pub description: String, } pub fn list() -> Result, ShackleError> { fn add_from_dir( collection_dir: &Path, is_checking_group: bool, ) -> Result, ShackleError> { let mut results = Vec::new(); if !collection_dir.is_dir() { return Ok(results); } for dir in collection_dir.read_dir()? { let path = dir?.path(); let description_path = path.join("description"); let has_git_ext = path.extension().map_or(false, |ext| ext == "git"); if has_git_ext { if let Ok(repo) = Repository::open_bare(&path) { let config = repo.config()?.snapshot()?; let shared_config = config.get_str("core.sharedRepository").or_else(|e| { if e.code() == ErrorCode::NotFound { Ok("") } else { Err(e) } })?; let is_group_shared = [Some("group"), Some("1"), Some("true")].contains(&Some(shared_config)); if is_group_shared == is_checking_group { let description = if description_path.is_file() { fs::read_to_string(description_path)? } else { String::new() }; results.push(RepoMetadata { path, description }); } } } } Ok(results) } let mut results = Vec::new(); results.append(&mut add_from_dir(&personal_git_dir()?, false)?); let groups = get_user_groups(); for group in &groups { results.append(&mut add_from_dir(&group_git_dir(group), true)?); } Ok(results) } pub fn set_description(directory: &Path, description: &str) -> Result<(), ShackleError> { if !is_valid_git_repo_path(directory)? { return Err(ShackleError::InvalidDirectory); } let description_path = directory.join("description"); if description_path.is_file() { fs::write(description_path, description).map_err(|e| e.into()) } else { Err(ShackleError::InvalidDirectory) } } pub fn set_branch(directory: &Path, branch: &str) -> Result<(), ShackleError> { if !is_valid_git_repo_path(directory)? { return Err(ShackleError::InvalidDirectory); } if let Ok(repo) = Repository::open_bare(directory) { repo.reference_symbolic( "HEAD", &format!("refs/heads/{branch}"), true, "shackle set-branch", )?; Ok(()) } else { Err(ShackleError::InvalidDirectory) } } pub fn upload_pack(upload_pack_args: &GitUploadPackArgs) -> Result<(), ShackleError> { if !is_valid_git_repo_path(&upload_pack_args.directory)? { return Err(ShackleError::InvalidDirectory); } let mut command = Command::new("git-upload-pack"); command.arg("--strict"); if let Some(timeout) = upload_pack_args.timeout { command.args(["--timeout", &timeout.to_string()]); } if upload_pack_args.stateless_rpc { command.arg("--stateless-rpc"); } if upload_pack_args.advertise_refs { command.arg("--advertise-refs"); } command.arg(&upload_pack_args.directory); command.spawn()?.wait()?; Ok(()) } pub fn receive_pack(receive_pack_args: &GitReceivePackArgs) -> Result<(), ShackleError> { if !is_valid_git_repo_path(&receive_pack_args.directory)? { return Err(ShackleError::InvalidDirectory); } let mut command = Command::new("git-receive-pack"); command.arg(&receive_pack_args.directory); command.spawn()?.wait()?; Ok(()) }