use std::path::{Path, PathBuf};
use crate::core::manifest::{
Manifest, ManifestRepoConfig, PlatformType, RepoAgentConfig, RepoConfig,
};
use crate::core::manifest_paths;
#[derive(Debug, Clone)]
pub struct RepoInfo {
pub name: String,
pub url: String,
pub path: String,
pub absolute_path: PathBuf,
pub default_branch: String,
pub owner: String,
pub repo: String,
pub platform_type: PlatformType,
pub platform_base_url: Option<String>,
pub project: Option<String>,
pub reference: bool,
pub groups: Vec<String>,
pub agent: Option<RepoAgentConfig>,
}
impl RepoInfo {
pub fn from_config(name: &str, config: &RepoConfig, workspace_root: &PathBuf) -> Option<Self> {
let parsed = parse_git_url(&config.url)?;
let absolute_path = workspace_root.join(&config.path);
let platform_type = config
.platform
.as_ref()
.map(|p| p.platform_type)
.unwrap_or_else(|| detect_platform(&config.url));
let platform_base_url = config.platform.as_ref().and_then(|p| p.base_url.clone());
Some(Self {
name: name.to_string(),
url: config.url.clone(),
path: config.path.clone(),
absolute_path,
default_branch: config.default_branch.clone(),
owner: parsed.owner,
repo: parsed.repo,
platform_type,
platform_base_url,
project: parsed.project,
reference: config.reference,
groups: config.groups.clone(),
agent: config.agent.clone(),
})
}
pub fn exists(&self) -> bool {
self.absolute_path.join(".git").exists()
}
}
struct ParsedUrl {
owner: String,
repo: String,
project: Option<String>,
}
fn parse_git_url(url: &str) -> Option<ParsedUrl> {
if url.starts_with("git@") {
let parts: Vec<&str> = url.splitn(2, ':').collect();
if parts.len() != 2 {
return None;
}
let path = parts[1].trim_end_matches(".git");
if url.contains("dev.azure.com") || url.contains("visualstudio.com") {
let segments: Vec<&str> = path.split('/').collect();
if segments.len() >= 4 && segments[0] == "v3" {
return Some(ParsedUrl {
owner: segments[1].to_string(),
repo: segments[3].to_string(),
project: Some(segments[2].to_string()),
});
}
}
let segments: Vec<&str> = path.split('/').collect();
if segments.len() >= 2 {
return Some(ParsedUrl {
owner: segments[0].to_string(),
repo: segments[segments.len() - 1].to_string(),
project: None,
});
}
}
if url.starts_with("https://") || url.starts_with("http://") {
let url_without_proto = url
.trim_start_matches("https://")
.trim_start_matches("http://");
let path = url_without_proto
.split_once('/')?
.1
.trim_end_matches(".git");
if url.contains("dev.azure.com") {
let segments: Vec<&str> = path.split('/').collect();
if segments.len() >= 4 && segments[2] == "_git" {
return Some(ParsedUrl {
owner: segments[0].to_string(),
repo: segments[3].to_string(),
project: Some(segments[1].to_string()),
});
}
}
if url.contains("visualstudio.com") {
let host_and_path: Vec<&str> = url_without_proto.splitn(2, '/').collect();
if host_and_path.len() < 2 {
return None;
}
let host = host_and_path[0];
let org = host.split('.').next()?;
let segments: Vec<&str> = path.split('/').collect();
if segments.len() >= 3 && segments[1] == "_git" {
return Some(ParsedUrl {
owner: org.to_string(),
repo: segments[2].to_string(),
project: Some(segments[0].to_string()),
});
}
}
let segments: Vec<&str> = path.split('/').collect();
if segments.len() >= 2 {
return Some(ParsedUrl {
owner: segments[0].to_string(),
repo: segments[segments.len() - 1].to_string(),
project: None,
});
}
}
if url.starts_with("file://") {
let path = url.trim_start_matches("file://").trim_end_matches(".git");
if let Some(name) = path.rsplit('/').next() {
return Some(ParsedUrl {
owner: "local".to_string(),
repo: name.to_string(),
project: None,
});
}
}
None
}
pub fn filter_repos(
manifest: &Manifest,
workspace_root: &PathBuf,
repos_filter: Option<&[String]>,
group_filter: Option<&[String]>,
include_reference: bool,
) -> Vec<RepoInfo> {
manifest
.repos
.iter()
.filter_map(|(name, config)| RepoInfo::from_config(name, config, workspace_root))
.filter(|r| include_reference || !r.reference)
.filter(|r| {
repos_filter
.map(|filter| filter.iter().any(|f| f == &r.name))
.unwrap_or(true)
})
.filter(|r| {
group_filter
.map(|groups| r.groups.iter().any(|g| groups.contains(g)))
.unwrap_or(true)
})
.collect()
}
pub fn get_manifest_repo_info(manifest: &Manifest, workspace_root: &Path) -> Option<RepoInfo> {
let manifest_config = manifest.manifest.as_ref()?;
let manifests_dir = manifest_paths::resolve_manifest_repo_dir(workspace_root)?;
if !manifests_dir.join(".git").exists() {
return None;
}
create_manifest_repo_info(manifest_config, workspace_root)
}
fn create_manifest_repo_info(
config: &ManifestRepoConfig,
workspace_root: &Path,
) -> Option<RepoInfo> {
let repo_dir = manifest_paths::resolve_manifest_repo_dir(workspace_root)
.unwrap_or_else(|| manifest_paths::main_space_dir(workspace_root));
let path = repo_dir
.strip_prefix(workspace_root)
.ok()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| manifest_paths::MAIN_SPACE_DIR.to_string());
RepoInfo::from_config(
"manifest",
&RepoConfig {
url: config.url.clone(),
path,
default_branch: config.default_branch.clone(),
copyfile: config.copyfile.clone(),
linkfile: config.linkfile.clone(),
platform: config.platform.clone(),
reference: false,
groups: Vec::new(),
agent: None,
},
&workspace_root.to_path_buf(),
)
}
fn detect_platform(url: &str) -> PlatformType {
if url.contains("github.com") {
return PlatformType::GitHub;
}
if url.contains("dev.azure.com") || url.contains("visualstudio.com") {
return PlatformType::AzureDevOps;
}
if url.contains("bitbucket.org") || url.contains("bitbucket.") {
return PlatformType::Bitbucket;
}
if url.contains("gitlab.com") || url.contains("gitlab.") {
return PlatformType::GitLab;
}
PlatformType::GitHub
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_github_ssh() {
let parsed = parse_git_url("git@github.com:user/repo.git").unwrap();
assert_eq!(parsed.owner, "user");
assert_eq!(parsed.repo, "repo");
assert!(parsed.project.is_none());
}
#[test]
fn test_parse_github_https() {
let parsed = parse_git_url("https://github.com/user/repo.git").unwrap();
assert_eq!(parsed.owner, "user");
assert_eq!(parsed.repo, "repo");
}
#[test]
fn test_parse_azure_https() {
let parsed = parse_git_url("https://dev.azure.com/org/project/_git/repo").unwrap();
assert_eq!(parsed.owner, "org");
assert_eq!(parsed.repo, "repo");
assert_eq!(parsed.project, Some("project".to_string()));
}
#[test]
fn test_parse_azure_ssh() {
let parsed = parse_git_url("git@ssh.dev.azure.com:v3/org/project/repo").unwrap();
assert_eq!(parsed.owner, "org");
assert_eq!(parsed.repo, "repo");
assert_eq!(parsed.project, Some("project".to_string()));
}
#[test]
fn test_parse_file_url() {
let parsed = parse_git_url("file:///tmp/remotes/myrepo.git").unwrap();
assert_eq!(parsed.owner, "local");
assert_eq!(parsed.repo, "myrepo");
assert!(parsed.project.is_none());
}
#[test]
fn test_parse_file_url_no_extension() {
let parsed = parse_git_url("file:///tmp/repos/test-repo").unwrap();
assert_eq!(parsed.owner, "local");
assert_eq!(parsed.repo, "test-repo");
}
#[test]
fn test_detect_github() {
assert_eq!(
detect_platform("git@github.com:user/repo.git"),
PlatformType::GitHub
);
}
#[test]
fn test_detect_gitlab() {
assert_eq!(
detect_platform("git@gitlab.com:user/repo.git"),
PlatformType::GitLab
);
}
#[test]
fn test_detect_azure() {
assert_eq!(
detect_platform("https://dev.azure.com/org/project/_git/repo"),
PlatformType::AzureDevOps
);
}
#[test]
fn test_get_manifest_repo_info_no_manifest() {
use crate::core::manifest::Manifest;
use std::collections::HashMap;
use tempfile::TempDir;
let temp = TempDir::new().unwrap();
let manifest = Manifest {
version: 1,
gripspaces: None,
manifest: None,
repos: HashMap::new(),
settings: Default::default(),
workspace: None,
};
let result = get_manifest_repo_info(&manifest, temp.path());
assert!(result.is_none());
}
#[test]
fn test_get_manifest_repo_info_no_git_dir() {
use crate::core::manifest::{Manifest, ManifestRepoConfig};
use std::collections::HashMap;
use tempfile::TempDir;
let temp = TempDir::new().unwrap();
let manifest = Manifest {
version: 1,
gripspaces: None,
manifest: Some(ManifestRepoConfig {
url: "git@github.com:user/manifest.git".to_string(),
default_branch: "main".to_string(),
copyfile: None,
linkfile: None,
composefile: None,
platform: None,
}),
repos: HashMap::new(),
settings: Default::default(),
workspace: None,
};
let result = get_manifest_repo_info(&manifest, temp.path());
assert!(result.is_none());
}
#[test]
fn test_get_manifest_repo_info_with_git_dir() {
use crate::core::manifest::{Manifest, ManifestRepoConfig};
use std::collections::HashMap;
use std::fs;
use tempfile::TempDir;
let temp = TempDir::new().unwrap();
let manifests_dir = temp.path().join(".gitgrip").join("spaces").join("main");
fs::create_dir_all(manifests_dir.join(".git")).unwrap();
let manifest = Manifest {
version: 1,
gripspaces: None,
manifest: Some(ManifestRepoConfig {
url: "git@github.com:user/manifest.git".to_string(),
default_branch: "main".to_string(),
copyfile: None,
linkfile: None,
composefile: None,
platform: None,
}),
repos: HashMap::new(),
settings: Default::default(),
workspace: None,
};
let result = get_manifest_repo_info(&manifest, temp.path());
assert!(result.is_some());
let info = result.unwrap();
assert_eq!(info.name, "manifest");
assert_eq!(info.path, ".gitgrip/spaces/main");
assert_eq!(info.default_branch, "main");
assert!(!info.reference);
}
}