bb_cli/commands/
pr_reviewers.rs1use crate::api::models::{PullRequest, ReviewerRef, ReviewerState, User};
2use crate::commands::pr::Ctx;
3use crate::error::{BbError, Result};
4use crate::output::{self, Format};
5use crate::users::resolve_user;
6
7async fn fetch(ctx: &Ctx, id: u64) -> Result<PullRequest> {
8 ctx.client
9 .get_json(&ctx.path(&format!("/pullrequests/{id}")))
10 .await
11}
12
13fn render(ctx: &Ctx, states: &[ReviewerState]) -> Result<()> {
14 match ctx.format {
15 Format::Json => output::print_json(&states)?,
16 Format::Human => output::print_table(
17 &["NAME", "STATE"],
18 states
19 .iter()
20 .map(|s| {
21 vec![
22 s.name.clone(),
23 serde_json::to_value(s.state)
26 .ok()
27 .and_then(|v| v.as_str().map(str::to_string))
28 .unwrap_or_else(|| "pending".into()),
29 ]
30 })
31 .collect(),
32 ),
33 }
34 Ok(())
35}
36
37pub async fn list(ctx: &Ctx, id: u64) -> Result<()> {
38 let pr = fetch(ctx, id).await?;
39 render(ctx, &pr.reviewer_states())
40}
41
42fn split_names(names: &str) -> Vec<&str> {
43 names
44 .split(',')
45 .map(str::trim)
46 .filter(|s| !s.is_empty())
47 .collect()
48}
49
50async fn resolve_all(ctx: &Ctx, names: &str, pool: &[User]) -> Result<Vec<User>> {
53 let requested = split_names(names);
54 if requested.is_empty() {
55 return Err(BbError::Config("no reviewer name given".into()));
56 }
57 let mut resolved = Vec::new();
58 for name in requested {
59 resolved.push(resolve_user(&ctx.client, &ctx.slug, name, pool).await?);
60 }
61 Ok(resolved)
62}
63
64async fn write_reviewers(
68 ctx: &Ctx,
69 id: u64,
70 pr: &PullRequest,
71 uuids: Vec<String>,
72 success_message: &str,
73) -> Result<()> {
74 let body = serde_json::json!({
75 "title": pr.title.clone().unwrap_or_default(),
76 "reviewers": uuids
77 .into_iter()
78 .map(|uuid| ReviewerRef { uuid })
79 .collect::<Vec<_>>(),
80 });
81 let updated: PullRequest = ctx
82 .client
83 .put_json(&ctx.path(&format!("/pullrequests/{id}")), &body)
84 .await?;
85 if !ctx.format.is_json() {
88 output::success(success_message);
89 }
90 render(ctx, &updated.reviewer_states())
91}
92
93fn current_uuids(pr: &PullRequest) -> Vec<String> {
94 pr.reviewers.iter().filter_map(|r| r.uuid.clone()).collect()
95}
96
97pub async fn add(ctx: &Ctx, id: u64, names: &str) -> Result<()> {
98 let pr = fetch(ctx, id).await?;
99 let resolved = resolve_all(ctx, names, &pr.reviewers).await?;
100
101 let mut uuids = current_uuids(&pr);
102 let mut added: Vec<String> = Vec::new();
103 let mut already: Vec<String> = Vec::new();
104 for user in &resolved {
105 let uuid = user
106 .uuid
107 .clone()
108 .ok_or_else(|| BbError::Config(format!("`{}` has no uuid to tag", user.name())))?;
109 if uuids.contains(&uuid) {
110 already.push(user.name().to_string());
111 } else {
112 uuids.push(uuid);
113 added.push(user.name().to_string());
114 }
115 }
116
117 if added.is_empty() {
118 if !ctx.format.is_json() {
119 output::info(&format!("already a reviewer: {}", already.join(", ")));
120 }
121 return render(ctx, &pr.reviewer_states());
122 }
123
124 if !already.is_empty() && !ctx.format.is_json() {
125 output::info(&format!("already a reviewer: {}", already.join(", ")));
126 }
127 let message = format!("added {}", added.join(", "));
128 write_reviewers(ctx, id, &pr, uuids, &message).await
129}
130
131pub async fn remove(ctx: &Ctx, id: u64, names: &str) -> Result<()> {
132 let pr = fetch(ctx, id).await?;
133 let resolved = resolve_all(ctx, names, &pr.reviewers).await?;
134
135 let mut uuids = current_uuids(&pr);
136 let mut removed: Vec<String> = Vec::new();
137 for user in &resolved {
138 let uuid = user
141 .uuid
142 .as_deref()
143 .filter(|uuid| uuids.iter().any(|u| u == uuid))
144 .ok_or_else(|| {
145 BbError::Config(format!("`{}` is not a reviewer on #{id}", user.name()))
146 })?
147 .to_string();
148 uuids.retain(|u| *u != uuid);
149 removed.push(user.name().to_string());
150 }
151
152 let message = format!("removed {}", removed.join(", "));
153 write_reviewers(ctx, id, &pr, uuids, &message).await
154}