use crate::{ parser::{GitReceivePackArgs, GitUploadPackArgs}, user_info::{get_gid, get_user_groups, get_username}, ShackleError, }; use git2::{ErrorCode, Repository, RepositoryInitMode, RepositoryInitOptions}; use std::{ fs, os::unix::fs::PermissionsExt, 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 { if let Some(group) = &group { if !verify_user_is_in_group(group) { return Err(ShackleError::InvalidGroup); } } let git_prefix = git_dir_prefix(); let collection_dir = match group { Some(group) => group_git_dir(group), None => personal_git_dir()?, }; let path = collection_dir.join(repo_name).with_extension("git"); if !git_prefix.is_dir() { fs::create_dir(&git_prefix)?; } if !collection_dir.is_dir() { fs::create_dir(&collection_dir)?; if let Some(group) = group { let gid = get_gid(&group).expect("User is in group but no group ID?"); nix::unistd::chown(&collection_dir, None, Some(gid))?; } let mut perms = collection_dir.metadata()?.permissions(); perms.set_mode(match group { Some(_) => 0o2770, None => 0o700, }); fs::set_permissions(&collection_dir, perms)?; } let mut init_opts = RepositoryInitOptions::new(); init_opts .bare(true) .mkdir(false) .no_reinit(true) .initial_head(branch); if group.is_some() { init_opts.mode(RepositoryInitMode::SHARED_GROUP); } 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 struct VerboseRepoMetadata { pub path: PathBuf, pub description: String, pub size: u64, } fn get_size(path: impl AsRef) -> Result { let path_metadata = path.as_ref().symlink_metadata()?; if path_metadata.is_dir() { let mut size_in_bytes = path_metadata.len(); for entry in path.as_ref().read_dir()? { let entry = entry?; let entry_metadata = entry.metadata()?; if entry_metadata.is_dir() { size_in_bytes += get_size(entry.path())?; } else { size_in_bytes += entry_metadata.len(); } } Ok(size_in_bytes) } else { Ok(path_metadata.len()) } } 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 list_verbose() -> Result, ShackleError> { list()? .into_iter() .map(|meta| { get_size(&meta.path).map(|size| VerboseRepoMetadata { path: meta.path, description: meta.description, size, }) }) .collect() } 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 housekeeping(directory: &Path) -> Result<(), ShackleError> { if !is_valid_git_repo_path(directory)? { return Err(ShackleError::InvalidDirectory); } Command::new("git") .arg("gc") .arg("--prune=now") .current_dir(directory) .spawn()? .wait()?; Ok(()) } pub fn delete(directory: &Path) -> Result<(), ShackleError> { if !is_valid_git_repo_path(directory)? { return Err(ShackleError::InvalidDirectory); } if Repository::open_bare(directory).is_ok() { fs::remove_dir_all(directory)?; 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(()) }