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 if let Err(integrity_error) = stack.validate_git_integrity(self.stack_manager.git_repo()) {
60 return Err(CascadeError::validation(format!(
61 "Cannot submit entry from corrupted stack '{}':\n{}",
62 stack.name, integrity_error
63 )));
64 }
65
66 let git_repo = self.stack_manager.git_repo();
68 info!("Ensuring branch '{}' is pushed to remote", entry.branch);
69
70 match git_repo.push(&entry.branch) {
71 Ok(_) => {
72 info!("✅ Successfully pushed branch '{}' to remote", entry.branch);
73
74 if let Some(commit_meta) = self
76 .stack_manager
77 .get_repository_metadata()
78 .commits
79 .get(&entry.commit_hash)
80 {
81 let mut updated_meta = commit_meta.clone();
82 updated_meta.mark_pushed();
83 }
86 }
87 Err(e) => {
88 warn!("Failed to push branch '{}': {}", entry.branch, e);
90
91 info!("Attempting to create PR anyway (branch may already exist remotely)");
93 }
94 }
95
96 let target_branch = self.get_target_branch(stack, entry)?;
98
99 if target_branch != stack.base_branch {
101 info!(
102 "Ensuring target branch '{}' is pushed to remote",
103 target_branch
104 );
105 match git_repo.push(&target_branch) {
106 Ok(_) => {
107 info!(
108 "✅ Successfully pushed target branch '{}' to remote",
109 target_branch
110 );
111 }
112 Err(e) => {
113 warn!("Failed to push target branch '{}': {}", target_branch, e);
114 info!("Continuing anyway (target branch may already exist remotely)");
115 }
116 }
117 }
118
119 let pr_request =
121 self.create_pr_request(stack, entry, &target_branch, title, description, draft)?;
122
123 let pr = match self.pr_manager.create_pull_request(pr_request).await {
124 Ok(pr) => pr,
125 Err(e) => {
126 return Err(CascadeError::bitbucket(format!(
127 "Failed to create pull request for branch '{}' -> '{}': {}. \
128 Ensure both branches exist in the remote repository. \
129 You can manually push with: git push origin {}",
130 entry.branch, target_branch, e, entry.branch
131 )));
132 }
133 };
134
135 self.stack_manager
137 .submit_entry(stack_id, entry_id, pr.id.to_string())?;
138
139 info!("Created pull request #{} for entry {}", pr.id, entry_id);
140 Ok(pr)
141 }
142
143 pub async fn check_stack_status(&self, stack_id: &Uuid) -> Result<StackSubmissionStatus> {
145 let stack = self
146 .stack_manager
147 .get_stack(stack_id)
148 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
149
150 let mut status = StackSubmissionStatus {
151 stack_name: stack.name.clone(),
152 total_entries: stack.entries.len(),
153 submitted_entries: 0,
154 open_prs: 0,
155 merged_prs: 0,
156 declined_prs: 0,
157 pull_requests: Vec::new(),
158 enhanced_statuses: Vec::new(),
159 };
160
161 for entry in &stack.entries {
162 if let Some(pr_id_str) = &entry.pull_request_id {
163 status.submitted_entries += 1;
164
165 if let Ok(pr_id) = pr_id_str.parse::<u64>() {
166 match self.pr_manager.get_pull_request(pr_id).await {
167 Ok(pr) => {
168 match pr.state {
169 PullRequestState::Open => status.open_prs += 1,
170 PullRequestState::Merged => status.merged_prs += 1,
171 PullRequestState::Declined => status.declined_prs += 1,
172 }
173 status.pull_requests.push(pr);
174 }
175 Err(e) => {
176 warn!("Failed to get pull request #{}: {}", pr_id, e);
177 }
178 }
179 }
180 }
181 }
182
183 Ok(status)
184 }
185
186 pub async fn list_pull_requests(
188 &self,
189 state: Option<PullRequestState>,
190 ) -> Result<crate::bitbucket::pull_request::PullRequestPage> {
191 self.pr_manager.list_pull_requests(state).await
192 }
193
194 fn get_target_branch(&self, stack: &Stack, entry: &StackEntry) -> Result<String> {
196 if let Some(first_entry) = stack.entries.first() {
198 if entry.id == first_entry.id {
199 return Ok(stack.base_branch.clone());
200 }
201 }
202
203 let entry_index = stack
205 .entries
206 .iter()
207 .position(|e| e.id == entry.id)
208 .ok_or_else(|| CascadeError::config("Entry not found in stack"))?;
209
210 if entry_index == 0 {
211 Ok(stack.base_branch.clone())
212 } else {
213 Ok(stack.entries[entry_index - 1].branch.clone())
214 }
215 }
216
217 fn create_pr_request(
219 &self,
220 _stack: &Stack,
221 entry: &StackEntry,
222 target_branch: &str,
223 title: Option<String>,
224 description: Option<String>,
225 draft: bool,
226 ) -> Result<CreatePullRequestRequest> {
227 let bitbucket_config = self.config.bitbucket.as_ref()
228 .ok_or_else(|| CascadeError::config("Bitbucket configuration is missing. Run 'ca setup' to configure Bitbucket integration."))?;
229
230 let repository = Repository {
231 id: 0, name: bitbucket_config.repo.clone(),
233 slug: bitbucket_config.repo.clone(),
234 scm_id: "git".to_string(),
235 state: "AVAILABLE".to_string(),
236 status_message: "Available".to_string(),
237 forkable: true,
238 project: Project {
239 id: 0,
240 key: bitbucket_config.project.clone(),
241 name: bitbucket_config.project.clone(),
242 description: None,
243 public: false,
244 project_type: "NORMAL".to_string(),
245 },
246 public: false,
247 };
248
249 let from_ref = PullRequestRef {
250 id: format!("refs/heads/{}", entry.branch),
251 display_id: entry.branch.clone(),
252 latest_commit: entry.commit_hash.clone(),
253 repository: repository.clone(),
254 };
255
256 let to_ref = PullRequestRef {
257 id: format!("refs/heads/{target_branch}"),
258 display_id: target_branch.to_string(),
259 latest_commit: "".to_string(), repository,
261 };
262
263 let title = title.unwrap_or_else(|| entry.message.lines().next().unwrap_or("").to_string());
264
265 let description = description.or_else(|| {
266 if entry.message.lines().count() > 1 {
267 Some(entry.message.lines().skip(1).collect::<Vec<_>>().join("\n"))
268 } else {
269 None
270 }
271 });
272
273 Ok(CreatePullRequestRequest {
274 title,
275 description,
276 from_ref,
277 to_ref,
278 draft: if draft { Some(true) } else { None },
279 })
280 }
281
282 pub async fn update_prs_after_rebase(
285 &mut self,
286 stack_id: &Uuid,
287 branch_mapping: &HashMap<String, String>,
288 ) -> Result<Vec<String>> {
289 info!(
290 "Updating pull requests after rebase for stack {} using smart force push",
291 stack_id
292 );
293
294 let stack = self
295 .stack_manager
296 .get_stack(stack_id)
297 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?
298 .clone();
299
300 let mut updated_branches = Vec::new();
301
302 for entry in &stack.entries {
303 if let (Some(pr_id_str), Some(new_branch)) =
305 (&entry.pull_request_id, branch_mapping.get(&entry.branch))
306 {
307 if let Ok(pr_id) = pr_id_str.parse::<u64>() {
308 info!(
309 "Found existing PR #{} for entry {}, updating branch {} -> {}",
310 pr_id, entry.id, entry.branch, new_branch
311 );
312
313 match self.pr_manager.get_pull_request(pr_id).await {
315 Ok(_existing_pr) => {
316 match self
319 .stack_manager
320 .git_repo()
321 .force_push_branch(&entry.branch, new_branch)
322 {
323 Ok(_) => {
324 info!(
325 "✅ Successfully force-pushed {} to preserve PR #{}",
326 entry.branch, pr_id
327 );
328
329 let rebase_comment = format!(
331 "🔄 **Automatic rebase completed**\n\n\
332 This PR has been automatically rebased to incorporate the latest changes.\n\
333 - Original commits: `{}`\n\
334 - New base: Latest main branch\n\
335 - All review history and comments are preserved\n\n\
336 The changes in this PR remain the same - only the base has been updated.",
337 &entry.commit_hash[..8]
338 );
339
340 if let Err(e) =
341 self.pr_manager.add_comment(pr_id, &rebase_comment).await
342 {
343 warn!(
344 "Failed to add rebase comment to PR #{}: {}",
345 pr_id, e
346 );
347 }
348
349 updated_branches.push(format!(
350 "PR #{}: {} (preserved)",
351 pr_id, entry.branch
352 ));
353 }
354 Err(e) => {
355 error!("Failed to force push {}: {}", entry.branch, e);
356 let error_comment = format!(
358 "⚠️ **Rebase Update Issue**\n\n\
359 The automatic rebase completed, but updating this PR failed.\n\
360 You may need to manually update this branch.\n\
361 Error: {e}"
362 );
363
364 if let Err(e2) =
365 self.pr_manager.add_comment(pr_id, &error_comment).await
366 {
367 warn!(
368 "Failed to add error comment to PR #{}: {}",
369 pr_id, e2
370 );
371 }
372 }
373 }
374 }
375 Err(e) => {
376 warn!("Could not retrieve PR #{}: {}", pr_id, e);
377 }
378 }
379 }
380 } else if branch_mapping.contains_key(&entry.branch) {
381 info!(
383 "Entry {} was remapped but has no PR - no action needed",
384 entry.id
385 );
386 }
387 }
388
389 if !updated_branches.is_empty() {
390 info!(
391 "Successfully updated {} PRs using smart force push strategy",
392 updated_branches.len()
393 );
394 }
395
396 Ok(updated_branches)
397 }
398
399 pub async fn check_enhanced_stack_status(
401 &self,
402 stack_id: &Uuid,
403 ) -> Result<StackSubmissionStatus> {
404 let stack = self
405 .stack_manager
406 .get_stack(stack_id)
407 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
408
409 let mut status = StackSubmissionStatus {
410 stack_name: stack.name.clone(),
411 total_entries: stack.entries.len(),
412 submitted_entries: 0,
413 open_prs: 0,
414 merged_prs: 0,
415 declined_prs: 0,
416 pull_requests: Vec::new(),
417 enhanced_statuses: Vec::new(),
418 };
419
420 for entry in &stack.entries {
421 if let Some(pr_id_str) = &entry.pull_request_id {
422 status.submitted_entries += 1;
423
424 if let Ok(pr_id) = pr_id_str.parse::<u64>() {
425 match self.pr_manager.get_pull_request_status(pr_id).await {
427 Ok(enhanced_status) => {
428 match enhanced_status.pr.state {
429 crate::bitbucket::pull_request::PullRequestState::Open => {
430 status.open_prs += 1
431 }
432 crate::bitbucket::pull_request::PullRequestState::Merged => {
433 status.merged_prs += 1
434 }
435 crate::bitbucket::pull_request::PullRequestState::Declined => {
436 status.declined_prs += 1
437 }
438 }
439 status.pull_requests.push(enhanced_status.pr.clone());
440 status.enhanced_statuses.push(enhanced_status);
441 }
442 Err(e) => {
443 warn!("Failed to get enhanced status for PR #{}: {}", pr_id, e);
444 match self.pr_manager.get_pull_request(pr_id).await {
446 Ok(pr) => {
447 match pr.state {
448 crate::bitbucket::pull_request::PullRequestState::Open => status.open_prs += 1,
449 crate::bitbucket::pull_request::PullRequestState::Merged => status.merged_prs += 1,
450 crate::bitbucket::pull_request::PullRequestState::Declined => status.declined_prs += 1,
451 }
452 status.pull_requests.push(pr);
453 }
454 Err(e2) => {
455 warn!("Failed to get basic PR #{}: {}", pr_id, e2);
456 }
457 }
458 }
459 }
460 }
461 }
462 }
463
464 Ok(status)
465 }
466}
467
468#[derive(Debug)]
470pub struct StackSubmissionStatus {
471 pub stack_name: String,
472 pub total_entries: usize,
473 pub submitted_entries: usize,
474 pub open_prs: usize,
475 pub merged_prs: usize,
476 pub declined_prs: usize,
477 pub pull_requests: Vec<PullRequest>,
478 pub enhanced_statuses: Vec<crate::bitbucket::pull_request::PullRequestStatus>,
479}
480
481impl StackSubmissionStatus {
482 pub fn completion_percentage(&self) -> f64 {
484 if self.total_entries == 0 {
485 0.0
486 } else {
487 (self.merged_prs as f64 / self.total_entries as f64) * 100.0
488 }
489 }
490
491 pub fn all_submitted(&self) -> bool {
493 self.submitted_entries == self.total_entries
494 }
495
496 pub fn all_merged(&self) -> bool {
498 self.merged_prs == self.total_entries
499 }
500}