summaryrefslogtreecommitdiff
path: root/src/vcs
diff options
context:
space:
mode:
Diffstat (limited to 'src/vcs')
-rw-r--r--src/vcs/git.rs323
-rw-r--r--src/vcs/pijul.rs1
2 files changed, 324 insertions, 0 deletions
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<PathBuf, ShackleError> {
+ 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<bool, ShackleError> {
+ 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<String>,
+ description: &Option<String>,
+ branch: &str,
+) -> Result<GitInitResult, ShackleError> {
+ 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<Path>) -> Result<u64, ShackleError> {
+ 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<Vec<RepoMetadata>, ShackleError> {
+ fn add_from_dir(
+ collection_dir: &Path,
+ is_checking_group: bool,
+ ) -> Result<Vec<RepoMetadata>, 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<Vec<VerboseRepoMetadata>, 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 @@
+