pub mod bookkeeping;
use rootle_provider::{
Capabilities, GitRef, Provider, ProviderResult, RepoId, RepoInfo, SearchCodeResult, SearchItem,
Sha, TreeResult,
};
use crate::config::Config;
use std::sync::Arc;
fn name_from_command(command: &[String]) -> String {
command
.first()
.and_then(|c| std::path::Path::new(c).file_stem().and_then(|s| s.to_str()))
.unwrap_or("provider")
.to_string()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Declaration {
pub name: String,
pub repo: String,
pub tag: Option<String>,
pub sha: Option<String>,
}
#[derive(Debug)]
pub enum BuildOutcome {
Ready,
Warn(String),
Health(HealthIssue),
Missing(Declaration),
}
#[derive(Debug, Clone)]
pub struct HealthIssue {
pub name: String,
pub error: String,
pub retryable: bool,
}
pub fn build(config: &Config) -> (Arc<dyn Provider>, BuildOutcome) {
match config.provider.kind.as_str() {
"github" => (
Arc::new(rootle_github::GitHubProvider::new(config.cache.max_mb)),
BuildOutcome::Ready,
),
"stdio" => {
let (provider, warn) = try_spawn_stdio(config, config.provider.command.clone(), None);
match provider {
Ok(p) => (p, warn.map_or(BuildOutcome::Ready, BuildOutcome::Warn)),
Err(e) => (
github_fallback(config),
BuildOutcome::Health(HealthIssue {
name: "stdio".into(),
error: e,
retryable: true,
}),
),
}
}
other => build_declared(config, other),
}
}
fn build_declared(config: &Config, kind: &str) -> (Arc<dyn Provider>, BuildOutcome) {
let r = match rootle_manager::ProviderReference::parse(kind) {
Ok(r) => r,
Err(e) => {
return (
github_fallback(config),
BuildOutcome::Health(HealthIssue {
name: kind.to_string(),
error: e.to_string(),
retryable: false,
}),
);
}
};
if r.tarball.is_some() {
return (
github_fallback(config),
BuildOutcome::Health(HealthIssue {
name: kind.to_string(),
error: "kind names a plain-HTTP tarball — install-and-pin only (run `rootle provider install` with the URL)".into(),
retryable: false,
}),
);
}
let Ok(m) = rootle_manager::Manager::new() else {
return (
github_fallback(config),
BuildOutcome::Warn("no provider data dir; using github".into()),
);
};
match m.current_binary(&r.name) {
Some(bin) => {
let mut argv = vec![bin.display().to_string()];
argv.extend(config.provider.command.iter().cloned());
match try_spawn_stdio(config, argv, Some(r.name.clone())).0 {
Ok(p) => (p, BuildOutcome::Ready),
Err(e) => (
github_fallback(config),
BuildOutcome::Health(HealthIssue {
name: kind.to_string(),
error: e,
retryable: true,
}),
),
}
}
None => (
github_fallback(config),
BuildOutcome::Missing(Declaration {
name: r.name,
repo: r.repo,
tag: config.provider.tag.clone().or(r.tag),
sha: config.provider.sha.clone(),
}),
),
}
}
pub fn spawn_installed(config: &Config, name: &str) -> Result<Arc<dyn Provider>, String> {
let bin = rootle_manager::Manager::new()
.ok()
.and_then(|m| m.current_binary(name))
.ok_or_else(|| format!("{name} installed but its current binary is missing"))?;
let mut argv = vec![bin.display().to_string()];
argv.extend(config.provider.command.iter().cloned());
try_spawn_stdio(config, argv, Some(name.to_string())).0
}
fn github_fallback(config: &Config) -> Arc<dyn Provider> {
Arc::new(rootle_github::GitHubProvider::new(config.cache.max_mb))
}
fn try_spawn_stdio(
config: &Config,
argv: Vec<String>,
cache_name: Option<String>,
) -> (Result<Arc<dyn Provider>, String>, Option<String>) {
let stderr = config.provider.stderr.trim();
let inherit = stderr == "inherit";
let warn = (!inherit && !stderr.is_empty() && stderr != "null").then(|| {
format!(
"provider stderr {stderr:?} not recognized (use \"inherit\" or \"null\"); discarding child stderr"
)
});
let cache_bytes = config.cache.max_mb * 1024 * 1024;
let cache_dir = rootle_provider::paths::cache_dir().map(|d| {
d.join("rootle")
.join("providers")
.join(cache_name.unwrap_or_else(|| name_from_command(&argv)))
});
match rootle_stdio::StdioProvider::spawn_with_cache(
&argv,
std::time::Duration::from_millis(config.provider.timeout_ms),
inherit,
cache_bytes,
cache_dir,
) {
Ok(p) => (Ok(Arc::new(p)), warn),
Err(e) => (Err(e.to_string()), warn),
}
}
pub fn offline() -> Arc<dyn Provider> {
struct Offline;
impl Provider for Offline {
fn name(&self) -> &str {
"offline"
}
fn capabilities(&self) -> Capabilities {
Capabilities {
orgs: false,
code_search: false,
file_search: false,
refs: true,
log: true,
blame: true,
commit: true,
}
}
fn search(&self, _: &str) -> ProviderResult<Vec<SearchItem>> {
Err("offline".into())
}
fn org_repos(&self, _: &str) -> ProviderResult<Vec<RepoInfo>> {
Err("offline".into())
}
fn fetch_tree(&self, _: &RepoId, _: Option<&GitRef>) -> ProviderResult<TreeResult> {
Err("offline".into())
}
fn fetch_blob(&self, _: &RepoId, _: &Sha) -> ProviderResult<Vec<u8>> {
Err("offline".into())
}
fn search_code(&self, _: &str) -> ProviderResult<SearchCodeResult> {
Err("offline".into())
}
fn clone_url(&self, _: &RepoId) -> ProviderResult<String> {
Err("offline".into())
}
fn web_url(
&self,
_: &RepoId,
_: &str,
_: Option<&GitRef>,
_: Option<u32>,
_: Option<u32>,
_: bool,
) -> ProviderResult<String> {
Err("offline".into())
}
fn org_url(&self, _: &str) -> ProviderResult<String> {
Err("offline".into())
}
}
Arc::new(Offline)
}