use ggen_core::types::codeowners::CodeownersGenerator;
use ggen_utils::error::Result;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CodeownersConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub source_dirs: Vec<String>,
#[serde(default)]
pub base_dirs: Vec<String>,
pub output_path: Option<String>,
#[serde(default)]
pub auto_regenerate: bool,
}
#[derive(Debug, Clone)]
pub struct CodeownersResult {
pub output_path: PathBuf,
pub owners_files_scanned: usize,
pub entries_generated: usize,
}
pub fn generate_codeowners(
config: &CodeownersConfig, project_root: &Path,
) -> Result<CodeownersResult> {
if !config.enabled {
return Err(ggen_utils::error::Error::new(
"CODEOWNERS generation is disabled in ggen.toml. Set [codeowners].enabled = true",
));
}
let source_dirs = if config.source_dirs.is_empty() {
vec!["ontology".to_string()] } else {
config.source_dirs.clone()
};
let mut generator = if config.base_dirs.is_empty() {
CodeownersGenerator::new()
} else {
CodeownersGenerator::new().with_base_dirs(config.base_dirs.clone())
};
for source_dir in &source_dirs {
let dir_path = project_root.join(source_dir);
if dir_path.exists() {
generator.scan_owners_files(&dir_path).map_err(|e| {
ggen_utils::error::Error::new(&format!(
"Failed to scan OWNERS files in {}: {}",
source_dir, e
))
})?;
}
}
let (output_path, entries_generated) = if let Some(ref custom_path) = config.output_path {
let path = project_root.join(custom_path);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
ggen_utils::error::Error::new(&format!("Failed to create output directory: {}", e))
})?;
}
let content = generator.generate();
let entries = content
.lines()
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.count();
std::fs::write(&path, &content).map_err(|e| {
ggen_utils::error::Error::new(&format!("Failed to write CODEOWNERS: {}", e))
})?;
(path, entries)
} else {
let path = generator.write_to_github(project_root).map_err(|e| {
ggen_utils::error::Error::new(&format!("Failed to write CODEOWNERS: {}", e))
})?;
let content = std::fs::read_to_string(&path).unwrap_or_default();
let entries = content
.lines()
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.count();
(path, entries)
};
Ok(CodeownersResult {
output_path,
owners_files_scanned: source_dirs.len(),
entries_generated,
})
}
pub fn should_regenerate(config: &CodeownersConfig) -> bool {
config.enabled && config.auto_regenerate
}
pub fn generate_codeowners_default(
ontology_dir: &Path, project_root: &Path,
) -> Result<CodeownersResult> {
let config = CodeownersConfig {
enabled: true,
source_dirs: vec![ontology_dir
.strip_prefix(project_root)
.unwrap_or(ontology_dir)
.to_string_lossy()
.to_string()],
base_dirs: vec![],
output_path: None,
auto_regenerate: false,
};
generate_codeowners(&config, project_root)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn test_config() -> CodeownersConfig {
CodeownersConfig {
enabled: true,
source_dirs: vec!["ontology".to_string()],
base_dirs: vec!["ontology".to_string(), "src/generated".to_string()],
output_path: None,
auto_regenerate: true,
}
}
#[test]
fn test_generate_codeowners_disabled() {
let temp_dir = TempDir::new().unwrap();
let config = CodeownersConfig {
enabled: false,
..Default::default()
};
let result = generate_codeowners(&config, temp_dir.path());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("disabled"));
}
#[test]
fn test_generate_codeowners_empty_project() {
let temp_dir = TempDir::new().unwrap();
let ontology_dir = temp_dir.path().join("ontology");
fs::create_dir_all(&ontology_dir).unwrap();
let config = test_config();
let result = generate_codeowners(&config, temp_dir.path());
assert!(result.is_ok());
let codeowners_path = temp_dir.path().join(".github/CODEOWNERS");
assert!(codeowners_path.exists());
}
#[test]
fn test_generate_codeowners_with_owners() {
let temp_dir = TempDir::new().unwrap();
let user_dir = temp_dir.path().join("ontology/user");
fs::create_dir_all(&user_dir).unwrap();
fs::write(user_dir.join("OWNERS"), "@company/identity-team\n@john.doe").unwrap();
let config = test_config();
let result = generate_codeowners(&config, temp_dir.path()).unwrap();
assert!(result.output_path.exists());
assert!(result.entries_generated > 0);
let content = fs::read_to_string(&result.output_path).unwrap();
assert!(content.contains("Auto-generated by ggen"));
assert!(content.contains("@company/identity-team"));
}
#[test]
fn test_generate_codeowners_custom_output() {
let temp_dir = TempDir::new().unwrap();
let ontology_dir = temp_dir.path().join("ontology");
fs::create_dir_all(&ontology_dir).unwrap();
let config = CodeownersConfig {
enabled: true,
source_dirs: vec!["ontology".to_string()],
base_dirs: vec![],
output_path: Some("docs/CODEOWNERS".to_string()),
auto_regenerate: false,
};
let result = generate_codeowners(&config, temp_dir.path()).unwrap();
assert_eq!(result.output_path, temp_dir.path().join("docs/CODEOWNERS"));
assert!(result.output_path.exists());
}
#[test]
fn test_should_regenerate() {
let enabled_config = CodeownersConfig {
enabled: true,
auto_regenerate: true,
..Default::default()
};
assert!(should_regenerate(&enabled_config));
let disabled_config = CodeownersConfig {
enabled: true,
auto_regenerate: false,
..Default::default()
};
assert!(!should_regenerate(&disabled_config));
let disabled_all = CodeownersConfig {
enabled: false,
auto_regenerate: true,
..Default::default()
};
assert!(!should_regenerate(&disabled_all));
}
#[test]
fn test_generate_codeowners_default() {
let temp_dir = TempDir::new().unwrap();
let ontology_dir = temp_dir.path().join("ontology");
let user_dir = ontology_dir.join("user");
fs::create_dir_all(&user_dir).unwrap();
fs::write(user_dir.join("OWNERS"), "@team").unwrap();
let result = generate_codeowners_default(&ontology_dir, temp_dir.path()).unwrap();
assert!(result.output_path.exists());
let content = fs::read_to_string(&result.output_path).unwrap();
assert!(content.contains("@team"));
}
}