use crate::api::models::User;
use crate::api::{repo_path, Client};
use crate::error::{BbError, Result};
use crate::output;
use crate::repo::RepoSlug;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Membership {
user: Option<User>,
}
#[derive(Debug, Deserialize)]
struct RepoPermission {
user: Option<User>,
}
pub async fn current_user(client: &Client) -> Result<User> {
client.get_json("/user").await
}
async fn candidates(client: &Client, slug: &RepoSlug, extra: &[User]) -> Result<(Vec<User>, bool)> {
let mut pool: Vec<User> = Vec::new();
let mut pool_incomplete = false;
match client
.paginate::<Membership>(&format!(
"/workspaces/{}/members?pagelen=100",
slug.workspace
))
.await
{
Ok(memberships) => pool.extend(memberships.into_iter().filter_map(|m| m.user)),
Err(BbError::Api { status: 403, .. }) | Err(BbError::Auth) => {
pool_incomplete = true;
}
Err(other) => return Err(other),
}
match client
.paginate::<RepoPermission>(&repo_path(slug, "/permissions-config/users?pagelen=100"))
.await
{
Ok(permissions) => pool.extend(permissions.into_iter().filter_map(|p| p.user)),
Err(BbError::Api { status: 403, .. }) | Err(BbError::Auth) | Err(BbError::NotFound) => {
pool_incomplete = true;
}
Err(other) => return Err(other),
}
let defaults: Vec<User> = client
.paginate(&repo_path(slug, "/default-reviewers?pagelen=100"))
.await?;
pool.extend(defaults);
for user in extra {
pool.push(User {
uuid: user.uuid.clone(),
account_id: user.account_id.clone(),
display_name: user.display_name.clone(),
nickname: user.nickname.clone(),
});
}
let mut seen: Vec<String> = Vec::new();
pool.retain(|user| match user.uuid.as_deref() {
Some(uuid) => {
let fresh = !seen.iter().any(|s| s == uuid);
if fresh {
seen.push(uuid.to_string());
}
fresh
}
None => true,
});
Ok((pool, pool_incomplete))
}
fn matches(user: &User, needle: &str) -> bool {
[user.display_name.as_deref(), user.nickname.as_deref()]
.into_iter()
.flatten()
.any(|field| field.to_lowercase().contains(needle))
}
fn is_exact(user: &User, needle: &str) -> bool {
[user.display_name.as_deref(), user.nickname.as_deref()]
.into_iter()
.flatten()
.any(|field| field.to_lowercase() == needle)
}
pub async fn resolve_user(
client: &Client,
slug: &RepoSlug,
query: &str,
extra: &[User],
) -> Result<User> {
let query = query.trim();
if query.is_empty() {
return Err(BbError::Config("empty user name".into()));
}
if query.starts_with('{') && query.ends_with('}') {
return Ok(User {
uuid: Some(query.to_string()),
account_id: None,
display_name: None,
nickname: None,
});
}
let needle = query.to_lowercase();
let (pool, pool_incomplete) = candidates(client, slug, extra).await?;
let mut found: Vec<User> = pool.into_iter().filter(|u| matches(u, &needle)).collect();
if found.iter().any(|u| is_exact(u, &needle)) {
found.retain(|u| is_exact(u, &needle));
}
match found.len() {
1 => Ok(found.remove(0)),
0 => {
if pool_incomplete {
output::warn(
"some user lists could not be read, so the name search may be \
incomplete — pass a `{uuid}` to be exact",
);
}
Err(BbError::Config(format!(
"no user matching `{query}` — pass a `{{uuid}}` to be exact"
)))
}
_ => {
let names: Vec<&str> = found.iter().map(|u| u.name()).collect();
Err(BbError::Config(format!(
"`{query}` matches {} people: {} — pass a `{{uuid}}` to be exact",
names.len(),
names.join(", ")
)))
}
}
}