use std::path::Path;
use aristo_core::auth::store::{load_store, CredentialEntry, CredentialStore};
use aristo_core::auth::{
data_plane_base, derive_repo_full_name, fetch_org_repos, repo_segment_for, AuthError, OrgRepo,
ResolvedCreds, Token, SERVER_ENV_VAR,
};
use crate::workspace::Workspace;
pub(crate) struct Target {
pub base_url: String,
pub repo: String,
}
pub(crate) fn github_repo_for(start: &Path) -> Result<String, AuthError> {
if let Some(r) = std::env::var("ARISTO_REPO")
.ok()
.filter(|v| !v.trim().is_empty())
{
return Ok(r.trim().to_string());
}
derive_repo_full_name(start)
}
pub(crate) fn resolve_target(creds: &ResolvedCreds, start: &Path) -> Result<Target, AuthError> {
warn_if_instance_url_set();
let base_url = data_plane_base(std::env::var(SERVER_ENV_VAR).ok().as_deref(), &creds.server);
let github_repo = github_repo_for(start)?;
let repos =
fetch_org_repos(&base_url, &creds.token).map_err(|e| e.for_checkout(&github_repo))?;
let repo = repo_segment_for(&repos, &github_repo)
.ok_or_else(|| AuthError::RepoNotInOrg {
github_repo: github_repo.clone(),
server: base_url.clone(),
})?
.to_string();
Ok(Target { base_url, repo })
}
pub(crate) fn resolve_creds_and_target(start: &Path) -> Result<(ResolvedCreds, Target), AuthError> {
match aristo_core::auth::resolve_full() {
Ok(creds) => {
let t = resolve_target(&creds, start)?;
Ok((creds, t))
}
Err(AuthError::SeveralServers { path, entries }) => {
warn_if_instance_url_set();
let Ok(github_repo) = github_repo_for(start) else {
return Err(AuthError::SeveralServers { path, entries });
};
let store = load_store()?;
match select_by_directory(&store, &github_repo, fetch_org_repos) {
Some((entry, target)) => Ok((
ResolvedCreds {
token: entry.token.clone(),
server: entry.server.clone(),
user_login: entry.user_login.clone(),
user_id: entry.user_id,
},
target,
)),
None => Err(AuthError::SeveralServers { path, entries }),
}
}
Err(e) => Err(e),
}
}
fn select_by_directory<'a>(
store: &'a CredentialStore,
github_repo: &str,
lookup: impl Fn(&str, &Token) -> Result<Vec<OrgRepo>, AuthError>,
) -> Option<(&'a CredentialEntry, Target)> {
let mut hits = store.entries.iter().filter_map(|e| {
let base_url = data_plane_base(None, &e.server);
let repos = lookup(&base_url, &e.token).ok()?;
let repo = repo_segment_for(&repos, github_repo)?.to_string();
Some((e, Target { base_url, repo }))
});
let first = hits.next()?;
if hits.next().is_some() {
return None;
}
Some(first)
}
pub(crate) fn checkout_start() -> std::path::PathBuf {
Workspace::find(None)
.map(|ws| ws.root)
.or_else(|_| std::env::current_dir())
.unwrap_or_else(|_| std::path::PathBuf::from("."))
}
fn warn_if_instance_url_set() {
let Some(url) = Workspace::find(None)
.ok()
.and_then(|ws| ws.load_config().instance.url)
else {
return;
};
eprintln!(
"warning: aristo.toml `[instance] url = \"{url}\"` is ignored — the data plane is \
the credential's server, and {SERVER_ENV_VAR} overrides it. Remove the section."
);
}
#[cfg(test)]
mod tests {
use super::*;
use aristo_core::auth::ServerUrl;
fn store(servers: &[&str]) -> CredentialStore {
CredentialStore {
entries: servers
.iter()
.map(|s| {
CredentialEntry::bare(Token::new(format!("arta_{s}")), ServerUrl::parse(s))
})
.collect(),
}
}
fn repo(name: &str, github_repo: &str) -> OrgRepo {
OrgRepo {
name: name.into(),
github_repo: github_repo.into(),
}
}
fn directory(base: &str, _token: &Token) -> Result<Vec<OrgRepo>, AuthError> {
match base {
"https://a.example" => Ok(vec![repo("widgets", "acme/widgets")]),
"https://b.example" => Ok(vec![repo("gadgets", "acme/gadgets")]),
other => Err(AuthError::Unreachable {
server: other.into(),
reason: "no such org".into(),
}),
}
}
#[test]
fn the_one_org_listing_the_checkout_wins() {
let s = store(&["https://a.example", "https://b.example"]);
let (entry, target) = select_by_directory(&s, "acme/widgets", directory).expect("a match");
assert_eq!(entry.server.as_str(), "https://a.example");
assert_eq!(target.base_url, "https://a.example");
assert_eq!(target.repo, "widgets");
let (entry, target) =
select_by_directory(&s, "Acme/Gadgets.git", directory).expect("a match");
assert_eq!(entry.server.as_str(), "https://b.example");
assert_eq!(target.repo, "gadgets");
}
#[test]
fn no_org_listing_it_is_none() {
let s = store(&["https://a.example", "https://b.example"]);
assert!(select_by_directory(&s, "acme/nope", directory).is_none());
}
#[test]
fn several_orgs_listing_it_is_none() {
let s = store(&["https://a.example", "https://b.example"]);
let both = |_: &str, _: &Token| Ok(vec![repo("widgets", "acme/widgets")]);
assert!(select_by_directory(&s, "acme/widgets", both).is_none());
}
#[test]
fn an_unreachable_directory_counts_as_not_listing() {
let s = store(&["https://a.example", "https://down.example"]);
let (entry, _) = select_by_directory(&s, "acme/widgets", directory).expect("a match");
assert_eq!(entry.server.as_str(), "https://a.example");
}
}