1use crate::api::models::Repository;
2use crate::error::{BbError, Result};
3use crate::output::{self, Format};
4use crate::workspace::{projects, WorkspaceCtx};
5use serde::Serialize;
6
7#[derive(Debug, Serialize)]
8struct RepoRow {
9 name: String,
10 project: String,
11 access: String,
12 updated: String,
13}
14
15pub async fn list(
16 ctx: &WorkspaceCtx,
17 project: Option<String>,
18 name: Option<String>,
19 limit: usize,
20) -> Result<()> {
21 let query = match &project {
25 Some(key) => format!(
26 "?pagelen=100&sort=-updated_on&q={}",
27 urlencoding::encode(&format!("project.key=\"{key}\""))
28 ),
29 None => "?pagelen=100&sort=-updated_on".to_string(),
30 };
31
32 let spinner = output::spinner("fetching repositories");
33 let repos: Vec<Repository> = ctx.client.paginate(&ctx.repos_path(&query)).await?;
34 spinner.finish_and_clear();
35
36 let needle = name.map(|n| n.to_lowercase());
37 let rows: Vec<RepoRow> = repos
38 .iter()
39 .filter(|r| match &needle {
40 Some(needle) => r.display_name().to_lowercase().contains(needle),
41 None => true,
42 })
43 .take(limit)
44 .map(|r| RepoRow {
45 name: r.display_name().to_string(),
46 project: r.project_key().to_string(),
47 access: r.access().to_string(),
48 updated: r
49 .updated_on
50 .as_deref()
51 .map(output::relative_time)
52 .unwrap_or_else(|| "-".into()),
53 })
54 .collect();
55
56 match ctx.format {
57 Format::Json => output::print_json(&rows)?,
58 Format::Human => output::print_table(
59 &["NAME", "PROJECT", "ACCESS", "UPDATED"],
60 rows.iter()
61 .map(|r| {
62 vec![
63 r.name.clone(),
64 r.project.clone(),
65 r.access.clone(),
66 r.updated.clone(),
67 ]
68 })
69 .collect(),
70 ),
71 }
72 Ok(())
73}
74
75#[derive(Debug, Serialize)]
81struct CreateBody {
82 is_private: bool,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 project: Option<ProjectKey>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 description: Option<String>,
90}
91
92#[derive(Debug, Serialize)]
93struct ProjectKey {
94 key: String,
95}
96
97async fn resolve_project(ctx: &WorkspaceCtx, explicit: Option<String>) -> Result<String> {
104 if let Some(key) = explicit {
105 return Ok(key);
106 }
107 if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
108 return Err(BbError::Config(
109 "a project is required — pass --project KEY".into(),
110 ));
111 }
112
113 let spinner = output::spinner("fetching projects");
114 let projects = projects(ctx).await?;
115 spinner.finish_and_clear();
116
117 let choices: Vec<String> = projects
118 .iter()
119 .filter(|p| p.key.is_some())
120 .map(|p| format!("{} — {}", p.key_or_dash(), p.name_or_dash()))
121 .collect();
122 if choices.is_empty() {
123 return Err(BbError::Config(format!(
124 "workspace {} has no projects you can see — pass --project KEY",
125 ctx.workspace
126 )));
127 }
128
129 let picked = inquire::Select::new("which project?", choices)
131 .prompt()
132 .map_err(|e| BbError::Config(format!("no project chosen: {e}")))?;
133 Ok(picked
134 .split_once(" — ")
135 .map(|(key, _)| key.to_string())
136 .unwrap_or(picked))
137}
138
139pub async fn create(
140 ctx: &WorkspaceCtx,
141 name: String,
142 project: Option<String>,
143 description: Option<String>,
144 public: bool,
145) -> Result<()> {
146 let key = resolve_project(ctx, project).await?;
147
148 let body = CreateBody {
149 is_private: !public,
150 project: Some(ProjectKey { key }),
151 description,
152 };
153
154 let path = format!("/{}", urlencoding::encode(&name));
158 let spinner = output::spinner("creating repository");
159 let repo: Repository = ctx
160 .client
161 .post_json(&ctx.repos_path(&path), &body)
162 .await
163 .inspect_err(|_| spinner.finish_and_clear())?;
164 spinner.finish_and_clear();
165
166 match ctx.format {
167 Format::Json => output::print_json(&repo)?,
168 Format::Human => {
169 output::success(&format!(
170 "created {} in project {}",
171 repo.full_name.as_deref().unwrap_or(repo.display_name()),
172 repo.project_key()
173 ));
174 if let Some(url) = repo.html_url() {
177 println!(" {url}");
178 }
179 if let Some(url) = repo.clone_url() {
180 println!(" {url}");
181 }
182 }
183 }
184 Ok(())
185}