use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use super::utils::BitbucketContext;
#[derive(Deserialize)]
struct RepositoryList {
values: Vec<Repository>,
}
#[derive(Deserialize)]
struct Repository {
slug: String,
name: String,
#[serde(default)]
updated_on: Option<String>,
}
#[derive(Deserialize)]
struct BranchList {
values: Vec<Branch>,
}
#[derive(Deserialize)]
struct Branch {
name: String,
#[serde(default)]
target: Option<Target>,
}
#[derive(Deserialize)]
struct Target {
#[serde(default)]
date: Option<String>,
}
pub async fn archive_stale_repos(
ctx: &BitbucketContext<'_>,
workspace: &str,
days_threshold: i64,
dry_run: bool,
) -> Result<()> {
let path = format!("/2.0/repositories/{workspace}?pagelen=100");
let response: RepositoryList = ctx
.client
.get(&path)
.await
.with_context(|| format!("Failed to list repositories in workspace {workspace}"))?;
let now = chrono::Utc::now();
let threshold = chrono::Duration::days(days_threshold);
#[derive(Serialize)]
struct StaleRepo<'a> {
slug: &'a str,
name: &'a str,
last_updated: &'a str,
action: &'a str,
}
let mut stale_repos = Vec::new();
for repo in &response.values {
if let Some(updated) = &repo.updated_on {
if let Ok(updated_date) = chrono::DateTime::parse_from_rfc3339(updated) {
let age = now.signed_duration_since(updated_date);
if age > threshold {
stale_repos.push(StaleRepo {
slug: repo.slug.as_str(),
name: repo.name.as_str(),
last_updated: updated,
action: if dry_run { "would archive" } else { "archived" },
});
if !dry_run {
let update_path = format!("/2.0/repositories/{workspace}/{}", repo.slug);
let payload = serde_json::json!({
"has_issues": false,
"has_wiki": false,
});
let _: serde_json::Value = ctx
.client
.put(&update_path, &payload)
.await
.with_context(|| {
format!("Failed to archive repository {}", repo.slug)
})?;
tracing::info!(
repo_slug = repo.slug.as_str(),
workspace,
"Repository archived"
);
}
}
}
}
}
if dry_run {
println!(
"DRY RUN - No changes made. Found {} stale repositories:",
stale_repos.len()
);
}
ctx.renderer.render_list_or_empty(
&stale_repos,
"No stale repositories found (threshold: {days_threshold} days)",
)
}
pub async fn delete_merged_branches(
ctx: &BitbucketContext<'_>,
workspace: &str,
repo_slug: &str,
exclude_patterns: Vec<String>,
dry_run: bool,
) -> Result<()> {
let path = format!("/2.0/repositories/{workspace}/{repo_slug}/refs/branches?pagelen=100");
let response: BranchList = ctx
.client
.get(&path)
.await
.with_context(|| format!("Failed to list branches for {workspace}/{repo_slug}"))?;
#[derive(Serialize)]
struct MergedBranch<'a> {
name: &'a str,
last_commit: &'a str,
action: &'a str,
}
let mut merged_branches = Vec::new();
let protected = ["main", "master", "develop", "development"];
for branch in &response.values {
let is_protected = protected.contains(&branch.name.as_str())
|| exclude_patterns
.iter()
.any(|pattern| branch.name.contains(pattern));
if !is_protected {
merged_branches.push(MergedBranch {
name: branch.name.as_str(),
last_commit: branch
.target
.as_ref()
.and_then(|t| t.date.as_deref())
.unwrap_or(""),
action: if dry_run { "would delete" } else { "deleted" },
});
if !dry_run {
let delete_path = format!(
"/2.0/repositories/{workspace}/{repo_slug}/refs/branches/{}",
branch.name
);
let _: serde_json::Value =
ctx.client.delete(&delete_path).await.with_context(|| {
format!(
"Failed to delete branch {} from {workspace}/{repo_slug}",
branch.name
)
})?;
tracing::info!(
branch_name = branch.name.as_str(),
workspace,
repo_slug,
"Branch deleted"
);
}
}
}
if dry_run {
println!(
"DRY RUN - No changes made. Found {} branches to delete:",
merged_branches.len()
);
}
ctx.renderer.render_list_or_empty(
&merged_branches,
"No branches to delete (excluding protected patterns)",
)
}