1use crate::bitbucket::client::BitbucketClient;
2use crate::bitbucket::pull_request::{
3 CreatePullRequestRequest, Project, PullRequest, PullRequestManager, PullRequestRef,
4 PullRequestState, Repository,
5};
6use crate::config::CascadeConfig;
7use crate::errors::{CascadeError, Result};
8use crate::stack::{Stack, StackEntry, StackManager};
9use std::collections::HashMap;
10use tracing::{error, info, warn};
11use uuid::Uuid;
12
13pub struct BitbucketIntegration {
15 stack_manager: StackManager,
16 pr_manager: PullRequestManager,
17 config: CascadeConfig,
18}
19
20impl BitbucketIntegration {
21 pub fn new(stack_manager: StackManager, config: CascadeConfig) -> Result<Self> {
23 let bitbucket_config = config
24 .bitbucket
25 .as_ref()
26 .ok_or_else(|| CascadeError::config("Bitbucket configuration not found"))?;
27
28 let client = BitbucketClient::new(bitbucket_config)?;
29 let pr_manager = PullRequestManager::new(client);
30
31 Ok(Self {
32 stack_manager,
33 pr_manager,
34 config,
35 })
36 }
37
38 pub async fn submit_entry(
40 &mut self,
41 stack_id: &Uuid,
42 entry_id: &Uuid,
43 title: Option<String>,
44 description: Option<String>,
45 draft: bool,
46 ) -> Result<PullRequest> {
47 let stack = self
48 .stack_manager
49 .get_stack(stack_id)
50 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
51
52 let entry = stack
53 .get_entry(entry_id)
54 .ok_or_else(|| CascadeError::config(format!("Entry {entry_id} not found in stack")))?;
55
56 info!("Submitting stack entry {} as pull request", entry_id);
57
58 let target_branch = self.get_target_branch(stack, entry)?;
60
61 let pr_request =
63 self.create_pr_request(stack, entry, &target_branch, title, description, draft)?;
64 let pr = self.pr_manager.create_pull_request(pr_request).await?;
65
66 self.stack_manager
68 .submit_entry(stack_id, entry_id, pr.id.to_string())?;
69
70 info!("Created pull request #{} for entry {}", pr.id, entry_id);
71 Ok(pr)
72 }
73
74 pub async fn check_stack_status(&self, stack_id: &Uuid) -> Result<StackSubmissionStatus> {
76 let stack = self
77 .stack_manager
78 .get_stack(stack_id)
79 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
80
81 let mut status = StackSubmissionStatus {
82 stack_name: stack.name.clone(),
83 total_entries: stack.entries.len(),
84 submitted_entries: 0,
85 open_prs: 0,
86 merged_prs: 0,
87 declined_prs: 0,
88 pull_requests: Vec::new(),
89 enhanced_statuses: Vec::new(),
90 };
91
92 for entry in &stack.entries {
93 if let Some(pr_id_str) = &entry.pull_request_id {
94 status.submitted_entries += 1;
95
96 if let Ok(pr_id) = pr_id_str.parse::<u64>() {
97 match self.pr_manager.get_pull_request(pr_id).await {
98 Ok(pr) => {
99 match pr.state {
100 PullRequestState::Open => status.open_prs += 1,
101 PullRequestState::Merged => status.merged_prs += 1,
102 PullRequestState::Declined => status.declined_prs += 1,
103 }
104 status.pull_requests.push(pr);
105 }
106 Err(e) => {
107 warn!("Failed to get pull request #{}: {}", pr_id, e);
108 }
109 }
110 }
111 }
112 }
113
114 Ok(status)
115 }
116
117 pub async fn list_pull_requests(
119 &self,
120 state: Option<PullRequestState>,
121 ) -> Result<crate::bitbucket::pull_request::PullRequestPage> {
122 self.pr_manager.list_pull_requests(state).await
123 }
124
125 fn get_target_branch(&self, stack: &Stack, entry: &StackEntry) -> Result<String> {
127 if let Some(first_entry) = stack.entries.first() {
129 if entry.id == first_entry.id {
130 return Ok(stack.base_branch.clone());
131 }
132 }
133
134 let entry_index = stack
136 .entries
137 .iter()
138 .position(|e| e.id == entry.id)
139 .ok_or_else(|| CascadeError::config("Entry not found in stack"))?;
140
141 if entry_index == 0 {
142 Ok(stack.base_branch.clone())
143 } else {
144 Ok(stack.entries[entry_index - 1].branch.clone())
145 }
146 }
147
148 fn create_pr_request(
150 &self,
151 _stack: &Stack,
152 entry: &StackEntry,
153 target_branch: &str,
154 title: Option<String>,
155 description: Option<String>,
156 draft: bool,
157 ) -> Result<CreatePullRequestRequest> {
158 let bitbucket_config = self.config.bitbucket.as_ref()
159 .ok_or_else(|| CascadeError::config("Bitbucket configuration is missing. Run 'ca setup' to configure Bitbucket integration."))?;
160
161 let repository = Repository {
162 id: 0, name: bitbucket_config.repo.clone(),
164 slug: bitbucket_config.repo.clone(),
165 scm_id: "git".to_string(),
166 state: "AVAILABLE".to_string(),
167 status_message: "Available".to_string(),
168 forkable: true,
169 project: Project {
170 id: 0,
171 key: bitbucket_config.project.clone(),
172 name: bitbucket_config.project.clone(),
173 description: None,
174 public: false,
175 project_type: "NORMAL".to_string(),
176 },
177 public: false,
178 };
179
180 let from_ref = PullRequestRef {
181 id: format!("refs/heads/{}", entry.branch),
182 display_id: entry.branch.clone(),
183 latest_commit: entry.commit_hash.clone(),
184 repository: repository.clone(),
185 };
186
187 let to_ref = PullRequestRef {
188 id: format!("refs/heads/{target_branch}"),
189 display_id: target_branch.to_string(),
190 latest_commit: "".to_string(), repository,
192 };
193
194 let title = title.unwrap_or_else(|| entry.message.lines().next().unwrap_or("").to_string());
195
196 let description = description.or_else(|| {
197 if entry.message.lines().count() > 1 {
198 Some(entry.message.lines().skip(1).collect::<Vec<_>>().join("\n"))
199 } else {
200 None
201 }
202 });
203
204 Ok(CreatePullRequestRequest {
205 title,
206 description,
207 from_ref,
208 to_ref,
209 draft: if draft { Some(true) } else { None },
210 })
211 }
212
213 pub async fn update_prs_after_rebase(
216 &mut self,
217 stack_id: &Uuid,
218 branch_mapping: &HashMap<String, String>,
219 ) -> Result<Vec<String>> {
220 info!(
221 "Updating pull requests after rebase for stack {} using smart force push",
222 stack_id
223 );
224
225 let stack = self
226 .stack_manager
227 .get_stack(stack_id)
228 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?
229 .clone();
230
231 let mut updated_branches = Vec::new();
232
233 for entry in &stack.entries {
234 if let (Some(pr_id_str), Some(new_branch)) =
236 (&entry.pull_request_id, branch_mapping.get(&entry.branch))
237 {
238 if let Ok(pr_id) = pr_id_str.parse::<u64>() {
239 info!(
240 "Found existing PR #{} for entry {}, updating branch {} -> {}",
241 pr_id, entry.id, entry.branch, new_branch
242 );
243
244 match self.pr_manager.get_pull_request(pr_id).await {
246 Ok(_existing_pr) => {
247 match self
250 .stack_manager
251 .git_repo()
252 .force_push_branch(&entry.branch, new_branch)
253 {
254 Ok(_) => {
255 info!(
256 "✅ Successfully force-pushed {} to preserve PR #{}",
257 entry.branch, pr_id
258 );
259
260 let rebase_comment = format!(
262 "🔄 **Automatic rebase completed**\n\n\
263 This PR has been automatically rebased to incorporate the latest changes.\n\
264 - Original commits: `{}`\n\
265 - New base: Latest main branch\n\
266 - All review history and comments are preserved\n\n\
267 The changes in this PR remain the same - only the base has been updated.",
268 &entry.commit_hash[..8]
269 );
270
271 if let Err(e) =
272 self.pr_manager.add_comment(pr_id, &rebase_comment).await
273 {
274 warn!(
275 "Failed to add rebase comment to PR #{}: {}",
276 pr_id, e
277 );
278 }
279
280 updated_branches.push(format!(
281 "PR #{}: {} (preserved)",
282 pr_id, entry.branch
283 ));
284 }
285 Err(e) => {
286 error!("Failed to force push {}: {}", entry.branch, e);
287 let error_comment = format!(
289 "⚠️ **Rebase Update Issue**\n\n\
290 The automatic rebase completed, but updating this PR failed.\n\
291 You may need to manually update this branch.\n\
292 Error: {e}"
293 );
294
295 if let Err(e2) =
296 self.pr_manager.add_comment(pr_id, &error_comment).await
297 {
298 warn!(
299 "Failed to add error comment to PR #{}: {}",
300 pr_id, e2
301 );
302 }
303 }
304 }
305 }
306 Err(e) => {
307 warn!("Could not retrieve PR #{}: {}", pr_id, e);
308 }
309 }
310 }
311 } else if branch_mapping.contains_key(&entry.branch) {
312 info!(
314 "Entry {} was remapped but has no PR - no action needed",
315 entry.id
316 );
317 }
318 }
319
320 if !updated_branches.is_empty() {
321 info!(
322 "Successfully updated {} PRs using smart force push strategy",
323 updated_branches.len()
324 );
325 }
326
327 Ok(updated_branches)
328 }
329
330 pub async fn check_enhanced_stack_status(
332 &self,
333 stack_id: &Uuid,
334 ) -> Result<StackSubmissionStatus> {
335 let stack = self
336 .stack_manager
337 .get_stack(stack_id)
338 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
339
340 let mut status = StackSubmissionStatus {
341 stack_name: stack.name.clone(),
342 total_entries: stack.entries.len(),
343 submitted_entries: 0,
344 open_prs: 0,
345 merged_prs: 0,
346 declined_prs: 0,
347 pull_requests: Vec::new(),
348 enhanced_statuses: Vec::new(),
349 };
350
351 for entry in &stack.entries {
352 if let Some(pr_id_str) = &entry.pull_request_id {
353 status.submitted_entries += 1;
354
355 if let Ok(pr_id) = pr_id_str.parse::<u64>() {
356 match self.pr_manager.get_pull_request_status(pr_id).await {
358 Ok(enhanced_status) => {
359 match enhanced_status.pr.state {
360 crate::bitbucket::pull_request::PullRequestState::Open => {
361 status.open_prs += 1
362 }
363 crate::bitbucket::pull_request::PullRequestState::Merged => {
364 status.merged_prs += 1
365 }
366 crate::bitbucket::pull_request::PullRequestState::Declined => {
367 status.declined_prs += 1
368 }
369 }
370 status.pull_requests.push(enhanced_status.pr.clone());
371 status.enhanced_statuses.push(enhanced_status);
372 }
373 Err(e) => {
374 warn!("Failed to get enhanced status for PR #{}: {}", pr_id, e);
375 match self.pr_manager.get_pull_request(pr_id).await {
377 Ok(pr) => {
378 match pr.state {
379 crate::bitbucket::pull_request::PullRequestState::Open => status.open_prs += 1,
380 crate::bitbucket::pull_request::PullRequestState::Merged => status.merged_prs += 1,
381 crate::bitbucket::pull_request::PullRequestState::Declined => status.declined_prs += 1,
382 }
383 status.pull_requests.push(pr);
384 }
385 Err(e2) => {
386 warn!("Failed to get basic PR #{}: {}", pr_id, e2);
387 }
388 }
389 }
390 }
391 }
392 }
393 }
394
395 Ok(status)
396 }
397}
398
399#[derive(Debug)]
401pub struct StackSubmissionStatus {
402 pub stack_name: String,
403 pub total_entries: usize,
404 pub submitted_entries: usize,
405 pub open_prs: usize,
406 pub merged_prs: usize,
407 pub declined_prs: usize,
408 pub pull_requests: Vec<PullRequest>,
409 pub enhanced_statuses: Vec<crate::bitbucket::pull_request::PullRequestStatus>,
410}
411
412impl StackSubmissionStatus {
413 pub fn completion_percentage(&self) -> f64 {
415 if self.total_entries == 0 {
416 0.0
417 } else {
418 (self.merged_prs as f64 / self.total_entries as f64) * 100.0
419 }
420 }
421
422 pub fn all_submitted(&self) -> bool {
424 self.submitted_entries == self.total_entries
425 }
426
427 pub fn all_merged(&self) -> bool {
429 self.merged_prs == self.total_entries
430 }
431}