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) {
126 "Yes"
127 } else {
128 "No"
129 }
130 .to_string(),
131 updated: r
132 .updated_on
133 .map(|d| d.format("%Y-%m-%d").to_string())
134 .unwrap_or_default(),
135 })
136 .collect();
137
138 let table = Table::new(rows).to_string();
139 println!("{}", table);
140
141 if repos.next.is_some() {
142 println!(
143 "\n{} More repositories available. Use --limit to see more.",
144 "ℹ".blue()
145 );
146 }
147
148 Ok(())
149 }
150
151 RepoCommands::View { repo, web } => {
152 let (workspace, repo_slug) = parse_repo(&repo)?;
153 let client = BitbucketClient::from_stored()?;
154 let repository = client.get_repository(&workspace, &repo_slug).await?;
155
156 if web {
157 if let Some(links) = &repository.links {
158 if let Some(html) = &links.html {
159 open::that(&html.href)?;
160 println!("Opened {} in browser", html.href.cyan());
161 return Ok(());
162 }
163 }
164 anyhow::bail!("Could not find repository URL");
165 }
166
167 println!("{}", repository.full_name.bold());
168 println!("{}", "─".repeat(50));
169
170 if let Some(desc) = &repository.description {
171 if !desc.is_empty() {
172 println!("{}", desc);
173 println!();
174 }
175 }
176
177 println!(
178 "{} {}",
179 "Private:".dimmed(),
180 if repository.is_private.unwrap_or(false) {
181 "Yes"
182 } else {
183 "No"
184 }
185 );
186 println!(
187 "{} {}",
188 "SCM:".dimmed(),
189 repository.scm.as_deref().unwrap_or("unknown")
190 );
191
192 if let Some(lang) = &repository.language {
193 if !lang.is_empty() {
194 println!("{} {}", "Language:".dimmed(), lang);
195 }
196 }
197
198 if let Some(branch) = &repository.mainbranch {
199 println!("{} {}", "Main branch:".dimmed(), branch.name);
200 }
201
202 if let Some(size) = repository.size {
203 let size_mb = size as f64 / (1024.0 * 1024.0);
204 println!("{} {:.2} MB", "Size:".dimmed(), size_mb);
205 }
206
207 if let Some(created) = repository.created_on {
208 println!("{} {}", "Created:".dimmed(), created.format("%Y-%m-%d"));
209 }
210
211 if let Some(updated) = repository.updated_on {
212 println!("{} {}", "Updated:".dimmed(), updated.format("%Y-%m-%d"));
213 }
214
215 if let Some(links) = &repository.links {
216 println!();
217 if let Some(html) = &links.html {
218 println!("{} {}", "Web:".dimmed(), html.href.cyan());
219 }
220 if let Some(clone_links) = &links.clone {
221 for link in clone_links {
222 println!("{} {} ({})", "Clone:".dimmed(), link.href, link.name);
223 }
224 }
225 }
226
227 Ok(())
228 }
229
230 RepoCommands::Clone { repo, dir } => {
231 let (workspace, repo_slug) = parse_repo(&repo)?;
232 let client = BitbucketClient::from_stored()?;
233 let repository = client.get_repository(&workspace, &repo_slug).await?;
234
235 let clone_url = repository
236 .links
237 .as_ref()
238 .and_then(|l| l.clone.as_ref())
239 .and_then(|links| links.iter().find(|l| l.name == "ssh" || l.name == "https"))
240 .map(|l| &l.href)
241 .context("Could not find clone URL")?;
242
243 let target_dir = dir.unwrap_or_else(|| repo_slug.clone());
244
245 println!("Cloning {} into {}...", repo.cyan(), target_dir);
246
247 let status = std::process::Command::new("git")
248 .args(["clone", clone_url, &target_dir])
249 .status()
250 .context("Failed to run git clone")?;
251
252 if status.success() {
253 println!("{} Successfully cloned repository", "✓".green());
254 } else {
255 anyhow::bail!("git clone failed");
256 }
257
258 Ok(())
259 }
260
261 RepoCommands::Create {
262 workspace,
263 name,
264 description,
265 public,
266 project,
267 } => {
268 let client = BitbucketClient::from_stored()?;
269
270 let slug = name.to_lowercase().replace(' ', "-");
271
272 let request = CreateRepositoryRequest {
273 scm: "git".to_string(),
274 name: Some(name.clone()),
275 description,
276 is_private: Some(!public),
277 project: project.map(|key| crate::models::ProjectKey { key }),
278 ..Default::default()
279 };
280
281 let repository = client
282 .create_repository(&workspace, &slug, &request)
283 .await?;
284
285 println!(
286 "{} Created repository {}",
287 "✓".green(),
288 repository.full_name.cyan()
289 );
290
291 if let Some(links) = &repository.links {
292 if let Some(html) = &links.html {
293 println!("{} {}", "URL:".dimmed(), html.href);
294 }
295 }
296
297 Ok(())
298 }
299
300 RepoCommands::Fork {
301 repo,
302 workspace,
303 name,
304 } => {
305 let (src_workspace, src_repo) = parse_repo(&repo)?;
306 let client = BitbucketClient::from_stored()?;
307
308 let forked = client
309 .fork_repository(
310 &src_workspace,
311 &src_repo,
312 workspace.as_deref(),
313 name.as_deref(),
314 )
315 .await?;
316
317 println!("{} Forked to {}", "✓".green(), forked.full_name.cyan());
318
319 Ok(())
320 }
321
322 RepoCommands::Delete { repo, yes } => {
323 let (workspace, repo_slug) = parse_repo(&repo)?;
324
325 if !yes {
326 use dialoguer::Confirm;
327 let confirmed = Confirm::new()
328 .with_prompt(format!(
329 "Are you sure you want to delete {}? This cannot be undone!",
330 repo.red()
331 ))
332 .default(false)
333 .interact()?;
334
335 if !confirmed {
336 println!("Aborted");
337 return Ok(());
338 }
339 }
340
341 let client = BitbucketClient::from_stored()?;
342 client.delete_repository(&workspace, &repo_slug).await?;
343
344 println!("{} Deleted repository {}", "✓".green(), repo);
345
346 Ok(())
347 }
348 }
349 }
350}
351
352fn parse_repo(repo: &str) -> Result<(String, String)> {
353 let parts: Vec<&str> = repo.split('/').collect();
354 if parts.len() != 2 {
355 anyhow::bail!(
356 "Invalid repository format. Expected 'workspace/repo-slug', got '{}'",
357 repo
358 );
359 }
360 Ok((parts[0].to_string(), parts[1].to_string()))
361}