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