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().unwrap();
159
160 let repository = Repository {
161 id: 0, name: bitbucket_config.repo.clone(),
163 slug: bitbucket_config.repo.clone(),
164 scm_id: "git".to_string(),
165 state: "AVAILABLE".to_string(),
166 status_message: "Available".to_string(),
167 forkable: true,
168 project: Project {
169 id: 0,
170 key: bitbucket_config.project.clone(),
171 name: bitbucket_config.project.clone(),
172 description: None,
173 public: false,
174 project_type: "NORMAL".to_string(),
175 },
176 public: false,
177 };
178
179 let from_ref = PullRequestRef {
180 id: format!("refs/heads/{}", entry.branch),
181 display_id: entry.branch.clone(),
182 latest_commit: entry.commit_hash.clone(),
183 repository: repository.clone(),
184 };
185
186 let to_ref = PullRequestRef {
187 id: format!("refs/heads/{target_branch}"),
188 display_id: target_branch.to_string(),
189 latest_commit: "".to_string(), repository,
191 };
192
193 let title = title.unwrap_or_else(|| entry.message.lines().next().unwrap_or("").to_string());
194
195 let description = description.or_else(|| {
196 if entry.message.lines().count() > 1 {
197 Some(entry.message.lines().skip(1).collect::<Vec<_>>().join("\n"))
198 } else {
199 None
200 }
201 });
202
203 Ok(CreatePullRequestRequest {
204 title,
205 description,
206 from_ref,
207 to_ref,
208 draft: if draft { Some(true) } else { None },
209 })
210 }
211
212 pub async fn update_prs_after_rebase(
215 &mut self,
216 stack_id: &Uuid,
217 branch_mapping: &HashMap<String, String>,
218 ) -> Result<Vec<String>> {
219 info!(
220 "Updating pull requests after rebase for stack {} using smart force push",
221 stack_id
222 );
223
224 let stack = self
225 .stack_manager
226 .get_stack(stack_id)
227 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?
228 .clone();
229
230 let mut updated_branches = Vec::new();
231
232 for entry in &stack.entries {
233 if let (Some(pr_id_str), Some(new_branch)) =
235 (&entry.pull_request_id, branch_mapping.get(&entry.branch))
236 {
237 if let Ok(pr_id) = pr_id_str.parse::<u64>() {
238 info!(
239 "Found existing PR #{} for entry {}, updating branch {} -> {}",
240 pr_id, entry.id, entry.branch, new_branch
241 );
242
243 match self.pr_manager.get_pull_request(pr_id).await {
245 Ok(_existing_pr) => {
246 match self
249 .stack_manager
250 .git_repo()
251 .force_push_branch(&entry.branch, new_branch)
252 {
253 Ok(_) => {
254 info!(
255 "✅ Successfully force-pushed {} to preserve PR #{}",
256 entry.branch, pr_id
257 );
258
259 let rebase_comment = format!(
261 "🔄 **Automatic rebase completed**\n\n\
262 This PR has been automatically rebased to incorporate the latest changes.\n\
263 - Original commits: `{}`\n\
264 - New base: Latest main branch\n\
265 - All review history and comments are preserved\n\n\
266 The changes in this PR remain the same - only the base has been updated.",
267 &entry.commit_hash[..8]
268 );
269
270 if let Err(e) =
271 self.pr_manager.add_comment(pr_id, &rebase_comment).await
272 {
273 warn!(
274 "Failed to add rebase comment to PR #{}: {}",
275 pr_id, e
276 );
277 }
278
279 updated_branches.push(format!(
280 "PR #{}: {} (preserved)",
281 pr_id, entry.branch
282 ));
283 }
284 Err(e) => {
285 error!("Failed to force push {}: {}", entry.branch, e);
286 let error_comment = format!(
288 "⚠️ **Rebase Update Issue**\n\n\
289 The automatic rebase completed, but updating this PR failed.\n\
290 You may need to manually update this branch.\n\
291 Error: {e}"
292 );
293
294 if let Err(e2) =
295 self.pr_manager.add_comment(pr_id, &error_comment).await
296 {
297 warn!(
298 "Failed to add error comment to PR #{}: {}",
299 pr_id, e2
300 );
301 }
302 }
303 }
304 }
305 Err(e) => {
306 warn!("Could not retrieve PR #{}: {}", pr_id, e);
307 }
308 }
309 }
310 } else if branch_mapping.contains_key(&entry.branch) {
311 info!(
313 "Entry {} was remapped but has no PR - no action needed",
314 entry.id
315 );
316 }
317 }
318
319 if !updated_branches.is_empty() {
320 info!(
321 "Successfully updated {} PRs using smart force push strategy",
322 updated_branches.len()
323 );
324 }
325
326 Ok(updated_branches)
327 }
328
329 pub async fn check_enhanced_stack_status(
331 &self,
332 stack_id: &Uuid,
333 ) -> Result<StackSubmissionStatus> {
334 let stack = self
335 .stack_manager
336 .get_stack(stack_id)
337 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
338
339 let mut status = StackSubmissionStatus {
340 stack_name: stack.name.clone(),
341 total_entries: stack.entries.len(),
342 submitted_entries: 0,
343 open_prs: 0,
344 merged_prs: 0,
345 declined_prs: 0,
346 pull_requests: Vec::new(),
347 enhanced_statuses: Vec::new(),
348 };
349
350 for entry in &stack.entries {
351 if let Some(pr_id_str) = &entry.pull_request_id {
352 status.submitted_entries += 1;
353
354 if let Ok(pr_id) = pr_id_str.parse::<u64>() {
355 match self.pr_manager.get_pull_request_status(pr_id).await {
357 Ok(enhanced_status) => {
358 match enhanced_status.pr.state {
359 crate::bitbucket::pull_request::PullRequestState::Open => {
360 status.open_prs += 1
361 }
362 crate::bitbucket::pull_request::PullRequestState::Merged => {
363 status.merged_prs += 1
364 }
365 crate::bitbucket::pull_request::PullRequestState::Declined => {
366 status.declined_prs += 1
367 }
368 }
369 status.pull_requests.push(enhanced_status.pr.clone());
370 status.enhanced_statuses.push(enhanced_status);
371 }
372 Err(e) => {
373 warn!("Failed to get enhanced status for PR #{}: {}", pr_id, e);
374 match self.pr_manager.get_pull_request(pr_id).await {
376 Ok(pr) => {
377 match pr.state {
378 crate::bitbucket::pull_request::PullRequestState::Open => status.open_prs += 1,
379 crate::bitbucket::pull_request::PullRequestState::Merged => status.merged_prs += 1,
380 crate::bitbucket::pull_request::PullRequestState::Declined => status.declined_prs += 1,
381 }
382 status.pull_requests.push(pr);
383 }
384 Err(e2) => {
385 warn!("Failed to get basic PR #{}: {}", pr_id, e2);
386 }
387 }
388 }
389 }
390 }
391 }
392 }
393
394 Ok(status)
395 }
396}
397
398#[derive(Debug)]
400pub struct StackSubmissionStatus {
401 pub stack_name: String,
402 pub total_entries: usize,
403 pub submitted_entries: usize,
404 pub open_prs: usize,
405 pub merged_prs: usize,
406 pub declined_prs: usize,
407 pub pull_requests: Vec<PullRequest>,
408 pub enhanced_statuses: Vec<crate::bitbucket::pull_request::PullRequestStatus>,
409}
410
411impl StackSubmissionStatus {
412 pub fn completion_percentage(&self) -> f64 {
414 if self.total_entries == 0 {
415 0.0
416 } else {
417 (self.merged_prs as f64 / self.total_entries as f64) * 100.0
418 }
419 }
420
421 pub fn all_submitted(&self) -> bool {
423 self.submitted_entries == self.total_entries
424 }
425
426 pub fn all_merged(&self) -> bool {
428 self.merged_prs == self.total_entries
429 }
430}