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/git.rs | 323 ------------------------------------------------------- src/lib.rs | 3 +- src/vcs.rs | 1 + src/vcs/git.rs | 323 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/vcs/pijul.rs | 1 + 5 files changed, 327 insertions(+), 324 deletions(-) delete mode 100644 src/git.rs create mode 100644 src/vcs.rs create mode 100644 src/vcs/git.rs create mode 100644 src/vcs/pijul.rs diff --git a/src/git.rs b/src/git.rs deleted file mode 100644 index ea55f2c..0000000 --- a/src/git.rs +++ /dev/null @@ -1,323 +0,0 @@ -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(()) -} diff --git a/src/lib.rs b/src/lib.rs index fded885..2a0d3b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,8 @@ -pub mod git; mod parser; pub mod user_info; +pub mod vcs; +use crate::vcs::git; use comfy_table::Table; use humansize::{format_size, BINARY}; use parser::*; diff --git a/src/vcs.rs b/src/vcs.rs new file mode 100644 index 0000000..c2bf1c3 --- /dev/null +++ b/src/vcs.rs @@ -0,0 +1 @@ +pub mod git; 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(()) +} diff --git a/src/vcs/pijul.rs b/src/vcs/pijul.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/vcs/pijul.rs @@ -0,0 +1 @@ + -- cgit v1.2.3