1use anyhow::{Context, Result};
2use clap::Subcommand;
3use colored::Colorize;
4use tabled::{Table, Tabled};
5
6use crate::api::BitbucketClient;
7use crate::models::CreateRepositoryRequest;
8
9#[derive(Subcommand)]
10pub enum RepoCommands {
11 List {
13 workspace: String,
15
16 #[arg(short, long, default_value = "25")]
18 limit: u32,
19 },
20
21 View {
23 repo: String,
25
26 #[arg(short, long)]
28 web: bool,
29 },
30
31 Clone {
33 repo: String,
35
36 #[arg(short, long)]
38 dir: Option<String>,
39 },
40
41 Create {
43 workspace: String,
45
46 name: String,
48
49 #[arg(short, long)]
51 description: Option<String>,
52
53 #[arg(long)]
55 public: bool,
56
57 #[arg(short, long)]
59 project: Option<String>,
60 },
61
62 Fork {
64 repo: String,
66
67 #[arg(short, long)]
69 workspace: Option<String>,
70
71 #[arg(short, long)]
73 name: Option<String>,
74 },
75
76 Delete {
78 repo: String,
80
81 #[arg(short, long)]
83 yes: bool,
84 },
85}
86
87#[derive(Tabled)]
88struct RepoRow {
89 #[tabled(rename = "NAME")]
90 name: String,
91 #[tabled(rename = "DESCRIPTION")]
92 description: String,
93 #[tabled(rename = "PRIVATE")]
94 private: String,
95 #[tabled(rename = "UPDATED")]
96 updated: String,
97}
98
99impl RepoCommands {
100 pub async fn run(self) -> Result<()> {
101 match self {
102 RepoCommands::List { workspace, limit } => {
103 let client = BitbucketClient::from_stored()?;
104 let repos = client
105 .list_repositories(&workspace, None, Some(limit))
106 .await?;
107
108 if repos.values.is_empty() {
109 println!("No repositories found in workspace '{}'", workspace);
110 return Ok(());
111 }
112
113 let rows: Vec<RepoRow> = repos
114 .values
115 .iter()
116 .map(|r| RepoRow {
117 name: r.full_name.clone(),
118 description: r
119 .description
120 .clone()
121 .unwrap_or_default()
122 .chars()
123 .take(40)
124 .collect::<String>(),
125 private: if r.is_private.unwrap_or(false) { "Yes" } else { "No" }.to_string(),
126 updated: r
127 .updated_on
128 .map(|d| d.format("%Y-%m-%d").to_string())
129 .unwrap_or_default(),
130 })
131 .collect();
132
133 let table = Table::new(rows).to_string();
134 println!("{}", table);
135
136 if repos.next.is_some() {
137 println!(
138 "\n{} More repositories available. Use --limit to see more.",
139 "ℹ".blue()
140 );
141 }
142
143 Ok(())
144 }
145
146 RepoCommands::View { repo, web } => {
147 let (workspace, repo_slug) = parse_repo(&repo)?;
148 let client = BitbucketClient::from_stored()?;
149 let repository = client.get_repository(&workspace, &repo_slug).await?;
150
151 if web {
152 if let Some(links) = &repository.links {
153 if let Some(html) = &links.html {
154 open::that(&html.href)?;
155 println!("Opened {} in browser", html.href.cyan());
156 return Ok(());
157 }
158 }
159 anyhow::bail!("Could not find repository URL");
160 }
161
162 println!("{}", repository.full_name.bold());
163 println!("{}", "─".repeat(50));
164
165 if let Some(desc) = &repository.description {
166 if !desc.is_empty() {
167 println!("{}", desc);
168 println!();
169 }
170 }
171
172 println!(
173 "{} {}",
174 "Private:".dimmed(),
175 if repository.is_private.unwrap_or(false) { "Yes" } else { "No" }
176 );
177 println!("{} {}", "SCM:".dimmed(), repository.scm.as_deref().unwrap_or("unknown"));
178
179 if let Some(lang) = &repository.language {
180 if !lang.is_empty() {
181 println!("{} {}", "Language:".dimmed(), lang);
182 }
183 }
184
185 if let Some(branch) = &repository.mainbranch {
186 println!("{} {}", "Main branch:".dimmed(), branch.name);
187 }
188
189 if let Some(size) = repository.size {
190 let size_mb = size as f64 / (1024.0 * 1024.0);
191 println!("{} {:.2} MB", "Size:".dimmed(), size_mb);
192 }
193
194 if let Some(created) = repository.created_on {
195 println!("{} {}", "Created:".dimmed(), created.format("%Y-%m-%d"));
196 }
197
198 if let Some(updated) = repository.updated_on {
199 println!("{} {}", "Updated:".dimmed(), updated.format("%Y-%m-%d"));
200 }
201
202 if let Some(links) = &repository.links {
203 println!();
204 if let Some(html) = &links.html {
205 println!("{} {}", "Web:".dimmed(), html.href.cyan());
206 }
207 if let Some(clone_links) = &links.clone {
208 for link in clone_links {
209 println!("{} {} ({})", "Clone:".dimmed(), link.href, link.name);
210 }
211 }
212 }
213
214 Ok(())
215 }
216
217 RepoCommands::Clone { repo, dir } => {
218 let (workspace, repo_slug) = parse_repo(&repo)?;
219 let client = BitbucketClient::from_stored()?;
220 let repository = client.get_repository(&workspace, &repo_slug).await?;
221
222 let clone_url = repository
223 .links
224 .as_ref()
225 .and_then(|l| l.clone.as_ref())
226 .and_then(|links| links.iter().find(|l| l.name == "ssh" || l.name == "https"))
227 .map(|l| &l.href)
228 .context("Could not find clone URL")?;
229
230 let target_dir = dir.unwrap_or_else(|| repo_slug.clone());
231
232 println!("Cloning {} into {}...", repo.cyan(), target_dir);
233
234 let status = std::process::Command::new("git")
235 .args(["clone", clone_url, &target_dir])
236 .status()
237 .context("Failed to run git clone")?;
238
239 if status.success() {
240 println!("{} Successfully cloned repository", "✓".green());
241 } else {
242 anyhow::bail!("git clone failed");
243 }
244
245 Ok(())
246 }
247
248 RepoCommands::Create {
249 workspace,
250 name,
251 description,
252 public,
253 project,
254 } => {
255 let client = BitbucketClient::from_stored()?;
256
257 let slug = name.to_lowercase().replace(' ', "-");
258
259 let request = CreateRepositoryRequest {
260 scm: "git".to_string(),
261 name: Some(name.clone()),
262 description,
263 is_private: Some(!public),
264 project: project.map(|key| crate::models::ProjectKey { key }),
265 ..Default::default()
266 };
267
268 let repository = client
269 .create_repository(&workspace, &slug, &request)
270 .await?;
271
272 println!(
273 "{} Created repository {}",
274 "✓".green(),
275 repository.full_name.cyan()
276 );
277
278 if let Some(links) = &repository.links {
279 if let Some(html) = &links.html {
280 println!("{} {}", "URL:".dimmed(), html.href);
281 }
282 }
283
284 Ok(())
285 }
286
287 RepoCommands::Fork {
288 repo,
289 workspace,
290 name,
291 } => {
292 let (src_workspace, src_repo) = parse_repo(&repo)?;
293 let client = BitbucketClient::from_stored()?;
294
295 let forked = client
296 .fork_repository(
297 &src_workspace,
298 &src_repo,
299 workspace.as_deref(),
300 name.as_deref(),
301 )
302 .await?;
303
304 println!("{} Forked to {}", "✓".green(), forked.full_name.cyan());
305
306 Ok(())
307 }
308
309 RepoCommands::Delete { repo, yes } => {
310 let (workspace, repo_slug) = parse_repo(&repo)?;
311
312 if !yes {
313 use dialoguer::Confirm;
314 let confirmed = Confirm::new()
315 .with_prompt(format!(
316 "Are you sure you want to delete {}? This cannot be undone!",
317 repo.red()
318 ))
319 .default(false)
320 .interact()?;
321
322 if !confirmed {
323 println!("Aborted");
324 return Ok(());
325 }
326 }
327
328 let client = BitbucketClient::from_stored()?;
329 client.delete_repository(&workspace, &repo_slug).await?;
330
331 println!("{} Deleted repository {}", "✓".green(), repo);
332
333 Ok(())
334 }
335 }
336 }
337}
338
339fn parse_repo(repo: &str) -> Result<(String, String)> {
340 let parts: Vec<&str> = repo.split('/').collect();
341 if parts.len() != 2 {
342 anyhow::bail!(
343 "Invalid repository format. Expected 'workspace/repo-slug', got '{}'",
344 repo
345 );
346 }
347 Ok((parts[0].to_string(), parts[1].to_string()))
348}