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