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