#![allow(dead_code)]
use anyhow::Result;
use git2::Repository as Git2Repository;
use gix::open::Options;
use std::path::PathBuf;
pub fn get_current_git2_repository(
repo: Option<Git2Repository>,
) -> Result<Git2Repository, anyhow::Error> {
if let Some(r) = repo {
Ok(r)
} else {
let rep = Git2Repository::discover(".")
.map_err(|e| anyhow::anyhow!("Failed to discover repository: {e}"))?;
if rep.is_bare() {
return Err(anyhow::anyhow!("Bare repositories are not supported"));
}
Ok(rep)
}
}
pub fn repo_from_path(path: &PathBuf) -> Result<gix::Repository, anyhow::Error> {
let options = Options::isolated();
gix::ThreadSafeRepository::open_opts(path, options)
.map(|repo| repo.to_thread_local())
.map_err(|e| anyhow::anyhow!("Failed to open repository at {}: {e}", path.display()))
}
pub fn get_current_repository() -> Result<gix::Repository, anyhow::Error> {
let options = Options::isolated();
Ok(gix::ThreadSafeRepository::open_opts(".", options)?.to_thread_local())
}
pub fn get_cwd() -> Result<PathBuf, anyhow::Error> {
std::env::current_dir()
.map_err(|e| anyhow::anyhow!("Failed to get current working directory: {e}"))
}
#[must_use]
pub fn get_thread_local_repo(repo: &gix::Repository) -> gix::Repository {
repo.to_owned().into_sync().to_thread_local()
}
pub fn get_main_repo(repo: Option<&gix::Repository>) -> Result<gix::Repository, anyhow::Error> {
let repo = match repo {
Some(r) => r.to_owned(),
None => get_current_repository()?,
};
let super_repo = repo
.main_repo()
.map_err(|e| anyhow::anyhow!("Failed to get main repository: {e}"))?;
Ok(super_repo)
}
pub fn get_main_root(repo: Option<&gix::Repository>) -> Result<PathBuf, anyhow::Error> {
let repo = get_main_repo(repo)?;
let path = repo.path().to_path_buf();
if path.is_dir() {
Ok(path)
} else {
Err(anyhow::anyhow!(
"Failed to get main repository root: {}",
path.display()
))
}
}
pub fn get_current_branch(repo: Option<&gix::Repository>) -> Result<String, anyhow::Error> {
fn branch_from_repo(repo: &gix::Repository) -> Result<String, anyhow::Error> {
let head = repo.head()?;
if let Some(reference) = head.referent_name() {
return Ok(reference.as_bstr().to_string());
}
Err(anyhow::anyhow!("Failed to get current branch name"))
}
if let Some(r) = repo {
branch_from_repo(r)
} else {
let owned = get_current_repository()?;
branch_from_repo(&owned)
}
}
pub fn get_current_working_directory() -> Result<PathBuf, anyhow::Error> {
std::env::current_dir()
.map_err(|e| anyhow::anyhow!("Failed to get current working directory: {e}"))
}
pub fn path_to_string(path: &std::path::Path) -> Result<String, anyhow::Error> {
path.to_str()
.map(std::string::ToString::to_string)
.ok_or_else(|| anyhow::anyhow!("Path is not valid UTF-8"))
}
#[must_use]
pub fn path_to_string_lossy(path: &std::path::Path) -> String {
if let Some(s) = path.to_str() {
return s.to_string();
}
let lossy = path.to_string_lossy();
eprintln!("Warning: Path contains non-UTF-8 characters, using lossy conversion: {lossy}");
lossy.to_string()
}
#[must_use]
pub fn path_to_os_string(path: &std::path::Path) -> std::ffi::OsString {
path.as_os_str().to_owned()
}
#[must_use]
#[allow(clippy::option_if_let_else)]
pub fn set_path(path: std::ffi::OsString) -> String {
if let Some(s) = path.to_str() {
s.to_string()
} else {
path_to_string_lossy(&PathBuf::from(path))
}
}
pub fn name_from_url(url: &str) -> Result<String, anyhow::Error> {
if url.is_empty() {
return Err(anyhow::anyhow!("URL cannot be empty"));
}
let cleaned_url = url.trim_end_matches('/').trim_end_matches(".git");
cleaned_url
.split('/')
.next_back()
.map(std::string::ToString::to_string)
.ok_or_else(|| anyhow::anyhow!("Failed to extract name from URL"))
}
pub fn name_from_osstring(os_string: std::ffi::OsString) -> Result<String, anyhow::Error> {
osstring_to_string(os_string).and_then(|s| {
if s.contains('\0') {
return Err(anyhow::anyhow!("Name cannot contain null bytes"));
}
if s.trim().is_empty() {
return Err(anyhow::anyhow!("Name cannot be empty or whitespace-only"));
}
let sep = std::path::MAIN_SEPARATOR.to_string();
s.trim()
.split(&sep)
.last()
.map(std::string::ToString::to_string)
.ok_or_else(|| anyhow::anyhow!("Failed to extract name from OsString"))
})
}
pub fn osstring_to_string(os_string: std::ffi::OsString) -> Result<String, anyhow::Error> {
os_string
.into_string()
.map_err(|_| anyhow::anyhow!("Failed to convert OsString to String"))
}
pub fn get_sparse_paths(
sparse_paths: Option<Vec<String>>,
) -> Result<Option<Vec<String>>, anyhow::Error> {
let sparse_paths_vec = match sparse_paths {
Some(paths) => {
for path in &paths {
if path.contains('\0') {
return Err(anyhow::anyhow!(
"Invalid sparse path pattern: contains null byte"
));
}
}
Some(paths)
}
None => None,
};
Ok(sparse_paths_vec)
}
pub fn get_name(
name: Option<String>,
url: Option<String>,
path: Option<std::ffi::OsString>,
) -> Result<String, anyhow::Error> {
if let Some(name) = name {
let trimmed_name = name.trim().to_string();
if trimmed_name.is_empty() {
get_name(None, url, path)
} else {
Ok(trimmed_name)
}
} else if let Some(path) = path {
name_from_osstring(path)
} else if let Some(url) = url {
name_from_url(&url)
} else {
Err(anyhow::anyhow!("No valid name source provided"))
}
}
pub fn validate_submodule_path(
repo_root: &std::path::Path,
path: &std::path::Path,
) -> Result<(), anyhow::Error> {
if path.is_absolute() {
return Err(anyhow::anyhow!("Submodule path cannot be absolute"));
}
let repo_root = repo_root
.canonicalize()
.unwrap_or_else(|_| repo_root.to_path_buf());
let mut current = repo_root.clone();
for component in path.components() {
match component {
std::path::Component::Normal(c) => {
current.push(c);
if current.is_symlink() {
let target = std::fs::read_link(¤t)?;
let resolved = if target.is_absolute() {
target
} else {
current.parent().unwrap().join(target)
};
let canonical_resolved = resolved
.canonicalize()
.unwrap_or_else(|_| normalize_path_only(&resolved));
if !canonical_resolved.starts_with(&repo_root) {
return Err(anyhow::anyhow!(
"Submodule path escapes repository root via symlink: {}",
canonical_resolved.display()
));
}
current = canonical_resolved;
}
}
std::path::Component::ParentDir => {
current.pop();
if !current.starts_with(&repo_root) {
return Err(anyhow::anyhow!("Submodule path escapes repository root"));
}
}
std::path::Component::CurDir => {}
std::path::Component::Prefix(_) | std::path::Component::RootDir => {
return Err(anyhow::anyhow!(
"Submodule path cannot contain root or prefix components"
));
}
}
}
let final_canonical = current
.canonicalize()
.unwrap_or_else(|_| normalize_path_only(¤t));
if !final_canonical.starts_with(&repo_root) {
return Err(anyhow::anyhow!(
"Submodule path resolves outside repository root"
));
}
Ok(())
}
fn normalize_path_only(path: &std::path::Path) -> std::path::PathBuf {
use std::path::{Component, PathBuf};
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
Component::ParentDir => {
normalized.pop();
}
Component::CurDir => {}
Component::Normal(c) => {
normalized.push(c);
}
Component::RootDir => {
normalized.push(Component::RootDir);
}
Component::Prefix(p) => {
normalized.push(Component::Prefix(p));
}
}
}
normalized
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn test_get_name_valid_name() {
assert_eq!(
get_name(Some("my-repo".to_string()), None, None).unwrap(),
"my-repo"
);
assert_eq!(
get_name(Some(" spaced-repo ".to_string()), None, None).unwrap(),
"spaced-repo"
);
}
#[test]
fn test_get_name_empty_name_fallback_url() {
assert_eq!(
get_name(
Some(" ".to_string()),
Some("https://github.com/user/repo.git".to_string()),
None
)
.unwrap(),
"repo"
);
}
#[test]
fn test_get_name_empty_name_fallback_path() {
assert_eq!(
get_name(
Some(String::new()),
None,
Some(PathBuf::from_iter(["path", "to", "my-module"]).into_os_string())
)
.unwrap(),
"my-module"
);
}
#[test]
fn test_get_name_empty_name_no_fallback() {
assert!(get_name(Some(" ".to_string()), None, None).is_err());
}
#[test]
fn test_get_name_no_name_valid_path() {
assert_eq!(
get_name(None, None, Some(std::ffi::OsString::from("another-path"))).unwrap(),
"another-path"
);
}
#[test]
fn test_get_name_no_name_no_path_valid_url() {
assert_eq!(
get_name(
None,
Some("git@github.com:user/another-repo.git".to_string()),
None
)
.unwrap(),
"another-repo"
);
}
#[test]
fn test_get_name_all_none() {
assert!(get_name(None, None, None).is_err());
}
#[test]
fn test_name_from_url_standard() {
assert_eq!(
name_from_url("https://github.com/user/repo.git").unwrap(),
"repo"
);
assert_eq!(
name_from_url("https://github.com/user/repo").unwrap(),
"repo"
);
}
#[test]
fn test_name_from_url_trailing_slashes() {
assert_eq!(
name_from_url("https://github.com/user/repo/").unwrap(),
"repo"
);
assert_eq!(
name_from_url("https://github.com/user/repo///").unwrap(),
"repo"
);
}
#[test]
fn test_name_from_url_ssh_format() {
assert_eq!(
name_from_url("git@github.com:user/mylib.git").unwrap(),
"mylib"
);
}
#[test]
fn test_name_from_url_file_url() {
assert_eq!(name_from_url("file:///path/to/repo.git").unwrap(), "repo");
}
#[test]
fn test_name_from_url_simple_name() {
assert_eq!(name_from_url("repo").unwrap(), "repo");
assert_eq!(name_from_url("my-lib.git").unwrap(), "my-lib");
}
#[test]
fn test_name_from_url_empty() {
assert!(name_from_url("").is_err());
}
#[test]
fn test_name_from_osstring_simple() {
assert_eq!(
name_from_osstring(std::ffi::OsString::from("my-repo")).unwrap(),
"my-repo"
);
}
#[test]
fn test_name_from_osstring_path() {
let path = PathBuf::from_iter(["path", "to", "module"]);
assert_eq!(name_from_osstring(path.into_os_string()).unwrap(), "module");
}
#[test]
fn test_name_from_osstring_empty() {
assert!(name_from_osstring(std::ffi::OsString::from("")).is_err());
assert!(name_from_osstring(std::ffi::OsString::from(" ")).is_err());
}
#[test]
fn test_name_from_osstring_null_byte() {
assert!(name_from_osstring(std::ffi::OsString::from("foo\0bar")).is_err());
}
#[test]
fn test_osstring_to_string_valid() {
assert_eq!(
osstring_to_string(std::ffi::OsString::from("hello")).unwrap(),
"hello"
);
}
#[test]
fn test_path_to_string_valid() {
let path = std::path::Path::new("/home/user/repo");
assert_eq!(path_to_string(path).unwrap(), "/home/user/repo");
}
#[test]
fn test_path_to_os_string_roundtrip() {
let path = std::path::Path::new("/some/path");
let os = path_to_os_string(path);
assert_eq!(os, std::ffi::OsString::from("/some/path"));
}
#[test]
fn test_set_path_valid_utf8() {
let os = std::ffi::OsString::from("/valid/path");
assert_eq!(set_path(os), "/valid/path");
}
#[test]
fn test_get_sparse_paths_valid() {
let paths = Some(vec!["src/".to_string(), "docs/".to_string()]);
let result = get_sparse_paths(paths).unwrap();
assert_eq!(result, Some(vec!["src/".to_string(), "docs/".to_string()]));
}
#[test]
fn test_get_sparse_paths_none() {
let result = get_sparse_paths(None).unwrap();
assert!(result.is_none());
}
#[test]
fn test_get_sparse_paths_null_byte() {
let paths = Some(vec!["src/\0bad".to_string()]);
assert!(get_sparse_paths(paths).is_err());
}
#[test]
fn test_get_sparse_paths_empty_vec() {
let result = get_sparse_paths(Some(vec![])).unwrap();
assert_eq!(result, Some(vec![]));
}
#[test]
fn test_path_to_string_lossy_valid() {
let path = std::path::Path::new("valid_utf8");
assert_eq!(path_to_string_lossy(path), "valid_utf8");
}
#[test]
#[cfg(unix)]
fn test_path_to_string_lossy_invalid() {
use std::os::unix::ffi::OsStringExt;
let bytes = vec![0x61, 0xFF, 0x62]; let os_string = std::ffi::OsString::from_vec(bytes);
let path = std::path::Path::new(&os_string);
let result = path_to_string_lossy(path);
assert_eq!(result, format!("a{}b", std::char::REPLACEMENT_CHARACTER));
}
}