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<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> {
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 {
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(())
}