use crate::io::api::{Endpoint, Params, RemoteResource, TreeEntryType};
use crate::io::ApiResult;
use color_eyre::eyre::eyre;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TreeEntry {
pub path: String,
pub mode: String,
#[serde(rename = "type")]
pub entry_type: TreeEntryType,
pub sha: String,
pub size: Option<u64>,
pub url: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TreeResponse {
pub sha: String,
pub url: String,
pub tree: Vec<TreeEntry>,
pub truncated: bool,
}
impl TreeEntry {
pub fn path(self) -> String {
self.path
}
pub fn is_blob(&self) -> bool {
self.entry_type.eq(&TreeEntryType::Blob)
}
}
pub(crate) async fn tree_paths(host: impl Into<String>, path: impl Into<String>, branch: impl Into<String>) -> ApiResult<Vec<String>> {
let endpoint = match Endpoint::from_template("github::api").map(|e| e.with_domain(host)) {
| Ok(endpoint) => endpoint,
| Err(why) => return Err(why),
};
let path = path.into();
let branch = branch.into();
let recursive_response = match fetch_tree(&endpoint, path.as_str(), branch.as_str(), true).await {
| Ok(data) => data,
| Err(why) => return Err(why),
};
if !recursive_response.truncated {
let (paths, _) = collect_tree(recursive_response.tree, "");
return Ok(paths);
}
let root_response = match fetch_tree(&endpoint, path.as_str(), branch.as_str(), false).await {
| Ok(data) if !data.truncated => data,
| Ok(_) => return Err(eyre!("Failed to fetch complete GitHub tree for {path}")),
| Err(why) => return Err(why),
};
let (mut all_paths, mut pending) = collect_tree(root_response.tree, "");
while let Some((sha, prefix)) = pending.pop() {
let subtree_response = match fetch_tree(&endpoint, path.as_str(), sha.as_str(), false).await {
| Ok(data) if !data.truncated => data,
| Ok(_) => return Err(eyre!("Failed to fetch complete GitHub tree for {path} at subtree {prefix}")),
| Err(why) => return Err(why),
};
let (paths, subtrees) = collect_tree(subtree_response.tree, prefix.as_str());
all_paths.extend(paths);
pending.extend(subtrees);
}
Ok(all_paths)
}
pub(crate) fn collect_tree(entries: Vec<TreeEntry>, prefix: &str) -> (Vec<String>, Vec<(String, String)>) {
entries.into_iter().fold((vec![], vec![]), |(mut paths, mut pending), entry| {
let is_blob = entry.is_blob();
let TreeEntry { path, sha, .. } = entry;
let full_path = path_for(prefix, path.as_str());
if is_blob {
paths.push(full_path);
} else {
pending.push((sha, full_path));
}
(paths, pending)
})
}
async fn fetch_tree(endpoint: &Endpoint, path: &str, branch: &str, recursive: bool) -> ApiResult<TreeResponse> {
let params = Params::new()
.with_template("path", Some(path))
.with_template("branch", Some(branch))
.with_keyvalue("recursive", recursive.then_some("1"))
.build();
let response = endpoint.invoke("tree", Some(params)).await;
endpoint.handle::<TreeResponse>(response)
}
pub(crate) fn path_for(prefix: &str, path: &str) -> String {
if prefix.is_empty() {
path.to_string()
} else {
format!("{prefix}/{path}")
}
}