cuenv_gitlab/
codeowners.rs1use cuenv_codeowners::Platform;
14use cuenv_codeowners::provider::{
15 CheckResult, CodeownersProvider, ProjectOwners, ProviderError, Result, SyncResult,
16 generate_aggregated_content, write_codeowners_file,
17};
18use std::fs;
19use std::path::Path;
20
21#[derive(Debug, Clone, Copy, Default)]
26pub struct GitLabCodeownersProvider;
27
28impl CodeownersProvider for GitLabCodeownersProvider {
29 fn platform(&self) -> Platform {
30 Platform::Gitlab
31 }
32
33 fn sync(
34 &self,
35 repo_root: &Path,
36 projects: &[ProjectOwners],
37 dry_run: bool,
38 ) -> Result<SyncResult> {
39 if projects.is_empty() {
40 return Err(ProviderError::Configuration(
41 "No projects with ownership configuration provided".to_string(),
42 ));
43 }
44
45 let content = generate_aggregated_content(Platform::Gitlab, projects, None);
47
48 let output_path = repo_root.join("CODEOWNERS");
50
51 let status = write_codeowners_file(&output_path, &content, dry_run)?;
53
54 Ok(SyncResult {
55 path: output_path,
56 status,
57 content,
58 })
59 }
60
61 fn check(&self, repo_root: &Path, projects: &[ProjectOwners]) -> Result<CheckResult> {
62 if projects.is_empty() {
63 return Err(ProviderError::Configuration(
64 "No projects with ownership configuration provided".to_string(),
65 ));
66 }
67
68 let expected = generate_aggregated_content(Platform::Gitlab, projects, None);
70
71 let output_path = repo_root.join("CODEOWNERS");
72
73 let actual = if output_path.exists() {
75 Some(fs::read_to_string(&output_path)?)
76 } else {
77 None
78 };
79
80 let normalize = |s: &str| -> String {
82 s.replace("\r\n", "\n")
83 .lines()
84 .map(str::trim_end)
85 .collect::<Vec<_>>()
86 .join("\n")
87 };
88
89 let in_sync = actual
90 .as_ref()
91 .is_some_and(|a| normalize(a) == normalize(&expected));
92
93 Ok(CheckResult {
94 path: output_path,
95 in_sync,
96 expected,
97 actual,
98 })
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use cuenv_codeowners::Rule;
106 use cuenv_codeowners::provider::SyncStatus;
107 use tempfile::tempdir;
108
109 #[test]
110 fn test_gitlab_provider_platform() {
111 let provider = GitLabCodeownersProvider;
112 assert_eq!(provider.platform(), Platform::Gitlab);
113 }
114
115 #[test]
116 fn test_gitlab_sync_creates_file() {
117 let temp = tempdir().unwrap();
118 let provider = GitLabCodeownersProvider;
119
120 let projects = vec![ProjectOwners::new(
121 "services/api",
122 "services/api",
123 vec![Rule::new("*.rs", ["@backend-team"])],
124 )];
125
126 let result = provider.sync(temp.path(), &projects, false).unwrap();
127
128 assert_eq!(result.status, SyncStatus::Created);
129 assert!(result.path.ends_with("CODEOWNERS"));
131 assert!(!result.path.to_string_lossy().contains(".github"));
132 assert!(result.content.contains("/services/api/*.rs @backend-team"));
133
134 let file_content = fs::read_to_string(&result.path).unwrap();
136 assert_eq!(file_content, result.content);
137 }
138
139 #[test]
140 fn test_gitlab_uses_section_syntax() {
141 let temp = tempdir().unwrap();
142 let provider = GitLabCodeownersProvider;
143
144 let projects = vec![
145 ProjectOwners::new(
146 "services/api",
147 "services/api",
148 vec![Rule::new("*.rs", ["@backend-team"])],
149 ),
150 ProjectOwners::new(
151 "services/web",
152 "services/web",
153 vec![Rule::new("*.ts", ["@frontend-team"])],
154 ),
155 ];
156
157 let result = provider.sync(temp.path(), &projects, false).unwrap();
158
159 assert!(
161 result.content.contains("[services/api]"),
162 "Should use [Section] syntax, got:\n{}",
163 result.content
164 );
165 assert!(
166 result.content.contains("[services/web]"),
167 "Should use [Section] syntax"
168 );
169 }
172
173 #[test]
174 fn test_gitlab_sync_dry_run() {
175 let temp = tempdir().unwrap();
176 let provider = GitLabCodeownersProvider;
177
178 let projects = vec![ProjectOwners::new(
179 "services/api",
180 "services/api",
181 vec![Rule::new("*.rs", ["@backend-team"])],
182 )];
183
184 let result = provider.sync(temp.path(), &projects, true).unwrap();
185
186 assert_eq!(result.status, SyncStatus::WouldCreate);
187 assert!(!result.path.exists());
188 }
189
190 #[test]
191 fn test_gitlab_check_in_sync() {
192 let temp = tempdir().unwrap();
193 let provider = GitLabCodeownersProvider;
194
195 let projects = vec![ProjectOwners::new(
196 "services/api",
197 "services/api",
198 vec![Rule::new("*.rs", ["@backend-team"])],
199 )];
200
201 provider.sync(temp.path(), &projects, false).unwrap();
203
204 let result = provider.check(temp.path(), &projects).unwrap();
206 assert!(result.in_sync);
207 }
208
209 #[test]
210 fn test_gitlab_check_out_of_sync() {
211 let temp = tempdir().unwrap();
212 let provider = GitLabCodeownersProvider;
213
214 fs::write(temp.path().join("CODEOWNERS"), "# Different content\n").unwrap();
216
217 let projects = vec![ProjectOwners::new(
218 "services/api",
219 "services/api",
220 vec![Rule::new("*.rs", ["@backend-team"])],
221 )];
222
223 let result = provider.check(temp.path(), &projects).unwrap();
224 assert!(!result.in_sync);
225 }
226
227 #[test]
228 fn test_gitlab_empty_projects_error() {
229 let temp = tempdir().unwrap();
230 let provider = GitLabCodeownersProvider;
231
232 let result = provider.sync(temp.path(), &[], false);
233 assert!(result.is_err());
234 }
235}