From 5bab4fbbf4c18e801691acbfcffe4b8ad7a08ace Mon Sep 17 00:00:00 2001 From: Justin Wernick Date: Thu, 14 Sep 2023 21:06:02 +0200 Subject: Mod structure for pijul support coming in --- src/vcs/git.rs | 323 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 src/vcs/git.rs (limited to 'src/vcs/git.rs') diff --git a/src/vcs/git.rs b/src/vcs/git.rs new file mode 100644 index 0000000..ea55f2c --- /dev/null +++ b/src/vcs/git.rs @@ -0,0 +1,323 @@ +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(()) +} -- cgit v1.2.3