use crate::api::ApiError;
use crate::api::browse::WikiPage;
use crate::api::model::RepoId;
use crate::git::repo::Repo;
use crate::diff_job::GitContext;
const HEAD: &str = "refs/ghtui/wiki";
const MARKUPS: &[&str] = &[
"md",
"markdown",
"mediawiki",
"wiki",
"textile",
"rdoc",
"org",
"creole",
"rst",
"asciidoc",
"adoc",
"pod",
];
fn page_file(path: &str) -> Option<(&str, &str)> {
let (title, ext) = path.rsplit_once('.')?;
MARKUPS
.contains(&ext.to_ascii_lowercase().as_str())
.then_some((title, ext))
}
pub fn slug(title: &str) -> String {
title.replace(' ', "-")
}
fn absent(err: &crate::git::GitError) -> bool {
matches!(err, crate::git::GitError::Failed { stderr, .. }
if stderr.contains("fatal: repository '") && stderr.contains("' not found"))
}
pub async fn page(
git: &GitContext,
repo: &RepoId,
page: Option<&str>,
) -> Result<WikiPage, ApiError> {
let unread = |err: &dyn std::fmt::Display| ApiError::Git(format!("{repo}'s wiki: {err}"));
let failed = |err: &crate::git::GitError| {
if absent(err) {
ApiError::NotFound(format!("{repo} has no wiki"))
} else {
unread(err)
}
};
let name = format!("{}.wiki", repo.name);
let url = format!("https://github.com/{}/{name}.git", repo.owner);
let quiet = |_: String| {};
let wiki = Repo::open_cache(
&git.cache_root,
&repo.owner,
&name,
&url,
git.credentials.clone(),
&quiet,
)
.await
.map_err(|e| failed(&e))?;
if let Err(err) = wiki.fetch_head(HEAD, &quiet).await
&& !wiki.has(HEAD).await
{
return Err(failed(&err));
}
let files = wiki.file_names(HEAD).await.map_err(|e| unread(&e))?;
let pages: Vec<(&str, &str, &String)> = files
.iter()
.filter_map(|f| page_file(f).map(|(title, ext)| (title, ext, f)))
.collect();
let title_of = |title: &str| title.rsplit('/').next().unwrap_or(title).to_owned();
let mut titles: Vec<String> = pages
.iter()
.map(|(t, _, _)| title_of(t))
.filter(|t| !t.starts_with('_'))
.collect();
titles.sort_by_key(|t| t.to_lowercase());
let find = |wanted: &str| {
pages
.iter()
.find(|(t, _, _)| slug(&title_of(t)).eq_ignore_ascii_case(&slug(wanted)))
};
let sidebar = match find("_Sidebar") {
Some((_, _, path)) => Some(wiki.file_text(HEAD, path).await.map_err(|e| unread(&e))?),
None => None,
};
let wanted = page.unwrap_or("Home");
let found = if wanted == "_pages" {
None
} else {
find(wanted)
};
let (title, text, markdown) = match found {
Some((t, ext, path)) => {
let text = wiki.file_text(HEAD, path).await.map_err(|e| unread(&e))?;
let markdown = matches!(ext.to_ascii_lowercase().as_str(), "md" | "markdown");
(Some(title_of(t)), Some(text), markdown)
}
None if page.is_none() || wanted == "_pages" => (None, None, false),
None => {
return Err(ApiError::NotFound(format!(
"{repo}'s wiki has no page {wanted}"
)));
}
};
Ok(WikiPage {
title,
text,
markdown,
pages: titles,
sidebar,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pages_are_files_in_wiki_markups() {
assert_eq!(page_file("Home.md"), Some(("Home", "md")));
assert_eq!(
page_file("Doc FAQ.mediawiki"),
Some(("Doc FAQ", "mediawiki"))
);
assert_eq!(page_file("logo.png"), None);
assert_eq!(page_file(".gitignore"), None);
assert_eq!(
slug("Doc continuous integration"),
"Doc-continuous-integration"
);
}
#[tokio::test]
async fn a_wiki_git_cant_read_isnt_taken_for_none() {
let dir = tempfile::tempdir().unwrap();
let cache_root = dir.path().join("a-file");
std::fs::write(&cache_root, "").unwrap();
let git = GitContext {
cache_root,
credentials: crate::git::credentials::Credentials::Ambient,
cwd: dir.path().to_owned(),
};
let err = page(&git, &RepoId::new("o", "r"), None).await.unwrap_err();
assert!(matches!(err, ApiError::Git(_)), "{err:?}");
}
#[test]
fn only_a_missing_repository_is_no_wiki() {
let failed = |stderr: &str| crate::git::GitError::Failed {
args: "fetch".into(),
stderr: stderr.into(),
};
let missing = "remote: Repository not found.\nfatal: repository 'https://github.com/o/r.wiki.git/' not found\n";
assert!(absent(&failed(missing)));
let missing = "Cloning into bare repository 'x'...; remote: Repository not found.; fatal: repository 'https://github.com/o/r.wiki.git/' not found";
assert!(absent(&failed(missing)));
let auth =
"fatal: could not read Username for 'https://github.com': terminal prompts disabled";
assert!(!absent(&failed(auth)));
assert!(!absent(&failed(
"fatal: unable to access 'https://github.com/o/r.wiki.git/': Could not resolve host: github.com"
)));
assert!(!absent(&crate::git::GitError::Spawn(
std::io::ErrorKind::NotFound.into()
)));
}
#[tokio::test]
#[ignore = "clones a wiki from GitHub"]
async fn real_wiki() {
let cache_root = std::env::var_os("GHTUI_WIKI_CACHE")
.expect("GHTUI_WIKI_CACHE")
.into();
let git = GitContext {
cache_root,
credentials: crate::git::credentials::Credentials::Ambient,
cwd: std::env::current_dir().unwrap(),
};
let repo = RepoId::new("rust-lang", "rust");
let home = page(&git, &repo, None).await.unwrap();
assert_eq!(home.title.as_deref(), Some("Home"));
assert!(home.pages.len() > 50, "{:?}", home.pages);
let faq = page(&git, &repo, Some("Doc-language-faq")).await.unwrap();
assert_eq!(faq.title.as_deref(), Some("Doc-language-faq"));
let spaced = page(&git, &repo, Some("Doc-continuous-integration"))
.await
.unwrap();
assert!(spaced.text.is_some());
let list = page(&git, &repo, Some("_pages")).await.unwrap();
assert!(list.text.is_none() && !list.pages.is_empty());
page(&git, &repo, Some("No-such-page")).await.unwrap_err();
}
}