pub mod github;
pub mod manager;
pub mod stdio;
pub mod ui;
use std::sync::Arc;
pub const RENDER_BUDGET: usize = 500;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RepoInfo {
pub name: String,
pub description: Option<String>,
pub private: bool,
pub archived: bool,
pub pushed_at: Option<String>,
}
impl RepoInfo {
pub fn bare(name: impl Into<String>) -> Self {
RepoInfo {
name: name.into(),
..Default::default()
}
}
}
impl From<String> for RepoInfo {
fn from(name: String) -> Self {
RepoInfo::bare(name)
}
}
impl From<&str> for RepoInfo {
fn from(name: &str) -> Self {
RepoInfo::bare(name)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderError {
pub kind: ErrorKind,
pub message: String,
pub retry_after: Option<std::time::Duration>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
Auth,
RateLimited,
NotFound,
Network,
Timeout,
Provider,
Other,
}
impl ProviderError {
pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
ProviderError {
kind,
message: message.into(),
retry_after: None,
}
}
pub fn other(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Other, message)
}
pub fn with_retry_after(mut self, retry: std::time::Duration) -> Self {
self.retry_after = Some(retry);
self
}
}
impl std::fmt::Display for ProviderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for ProviderError {}
impl From<String> for ProviderError {
fn from(message: String) -> Self {
Self::other(message)
}
}
impl From<&str> for ProviderError {
fn from(message: &str) -> Self {
Self::other(message)
}
}
pub type ProviderResult<T> = std::result::Result<T, ProviderError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Capabilities {
pub orgs: bool,
pub code_search: bool,
pub file_search: bool,
pub refs: bool,
pub log: bool,
pub blame: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefInfo {
pub name: String,
pub sha: String,
pub is_default: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RepoRefs {
pub branches: Vec<RefInfo>,
pub tags: Vec<RefInfo>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
pub struct LogEntry {
pub sha: String,
pub subject: String,
pub author: String,
pub date: String,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
pub struct BlameRange {
pub start_line: u32,
pub end_line: u32,
pub sha: String,
pub author: String,
pub date: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SearchItem {
Repo(String),
Org(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeNode {
pub path: String,
pub is_dir: bool,
pub sha: String,
pub size: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct TreeResult {
pub entries: Vec<TreeNode>,
pub truncated: bool,
pub branch: String,
}
#[derive(Debug, Clone)]
pub struct CodeMatch {
pub repo: String,
pub path: String,
pub sha: String,
pub branch: String,
pub matches: Vec<String>,
pub located: bool,
pub line: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct SearchCodeResult {
pub hits: Vec<CodeMatch>,
pub truncated: bool,
pub index_as_of: Option<String>,
}
pub trait Provider: Send + Sync {
fn name(&self) -> &str;
fn capabilities(&self) -> Capabilities;
fn default_orgs(&self) -> Vec<String> {
Vec::new()
}
fn search(&self, query: &str) -> ProviderResult<Vec<SearchItem>>;
fn org_repos(&self, org: &str) -> ProviderResult<Vec<RepoInfo>>;
fn fetch_tree(&self, repo: &str, ref_: Option<&str>) -> ProviderResult<TreeResult>;
fn fetch_blob(&self, repo: &str, sha: &str) -> ProviderResult<Vec<u8>>;
fn refs(&self, repo: &str) -> ProviderResult<RepoRefs> {
let _ = repo;
Err(ProviderError::new(
ErrorKind::Provider,
"provider has no revision listing",
))
}
fn log(
&self,
repo: &str,
path: Option<&str>,
ref_: Option<&str>,
limit: Option<usize>,
) -> ProviderResult<(Vec<LogEntry>, bool)> {
let _ = (repo, path, ref_, limit);
Err(ProviderError::new(
ErrorKind::Provider,
"provider has no commit log",
))
}
fn blob_at(
&self,
repo: &str,
path: &str,
ref_: Option<&str>,
) -> ProviderResult<(Vec<u8>, String)> {
let _ = (repo, path, ref_);
Err(ProviderError::new(
ErrorKind::Provider,
"provider cannot serve blobs at a ref",
))
}
fn blame(&self, repo: &str, path: &str, ref_: Option<&str>) -> ProviderResult<Vec<BlameRange>> {
let _ = (repo, path, ref_);
Err(ProviderError::new(
ErrorKind::Provider,
"provider has no blame",
))
}
fn search_code(&self, q: &str) -> ProviderResult<SearchCodeResult>;
fn icon(&self) -> Option<String> {
None
}
fn search_code_progressive(
&self,
q: &str,
on_hits: &(dyn Fn(&[CodeMatch]) + Send + Sync),
) -> ProviderResult<SearchCodeResult> {
let result = self.search_code(q)?;
on_hits(&result.hits);
Ok(SearchCodeResult {
hits: Vec::new(),
truncated: result.truncated,
index_as_of: result.index_as_of,
})
}
fn advise_cancel(&self) {}
fn take_notice(&self) -> Option<String> {
None
}
fn cache_usage(&self) -> Option<u64> {
None
}
fn clone_url(&self, repo: &str) -> ProviderResult<String>;
fn web_url(
&self,
repo: &str,
path: &str,
branch: &str,
line: Option<u32>,
end: Option<u32>,
is_file: bool,
) -> ProviderResult<String>;
fn org_url(&self, org: &str) -> ProviderResult<String>;
}
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()
}
pub fn build(config: &crate::config::Config) -> (Arc<dyn Provider>, Option<String>) {
match config.provider.kind.as_str() {
"github" => (
Arc::new(github::GitHubProvider::new(config.cache.max_mb)),
None,
),
"stdio" => {
let stderr = config.provider.stderr.trim();
let inherit = stderr == "inherit";
let warn = if inherit || stderr.is_empty() || stderr == "null" {
None
} else {
Some(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 = dirs::cache_dir().map(|d| {
d.join("rootle")
.join("providers")
.join(name_from_command(&config.provider.command))
});
match stdio::StdioProvider::spawn_with_cache(
&config.provider.command,
std::time::Duration::from_millis(config.provider.timeout_ms),
inherit,
cache_bytes,
cache_dir,
) {
Ok(p) => (Arc::new(p), warn),
Err(e) => (
Arc::new(github::GitHubProvider::new(config.cache.max_mb)),
Some(format!("provider stdio failed ({e}); fell back to github")),
),
}
}
other => (
Arc::new(github::GitHubProvider::new(config.cache.max_mb)),
Some(format!("unknown provider kind {other:?}; using github")),
),
}
}
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,
}
}
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, _: &str, _: Option<&str>) -> ProviderResult<TreeResult> {
Err("offline".into())
}
fn fetch_blob(&self, _: &str, _: &str) -> ProviderResult<Vec<u8>> {
Err("offline".into())
}
fn search_code(&self, _: &str) -> ProviderResult<SearchCodeResult> {
Err("offline".into())
}
fn clone_url(&self, _: &str) -> ProviderResult<String> {
Err("offline".into())
}
fn web_url(
&self,
_: &str,
_: &str,
_: &str,
_: Option<u32>,
_: Option<u32>,
_: bool,
) -> ProviderResult<String> {
Err("offline".into())
}
fn org_url(&self, _: &str) -> ProviderResult<String> {
Err("offline".into())
}
}
Arc::new(Offline)
}