src/commands/update/repo/tree.rs
use crate::commands::update::hub::RepositorySummary;
use std::{
borrow::Cow,
collections::{BTreeMap, BTreeSet, btree_map::Entry},
path::{Path, PathBuf},
};
use askama::Template;
use jj_cli::{
cli_util::CommandHelper,
command_error::{CommandError, internal_error_with_message, user_error_with_message},
ui::Ui,
};
use jj_lib::{backend::TreeValue, commit::Commit, repo::Repo, revset::ResolvedRevsetExpression};
use tokio::io::AsyncReadExt;
#[derive(Debug, Clone, Default)]
pub struct LanguageContent {
pub language: Option<&'static str>,
pub file: String,
}
#[derive(Debug, Clone, Default)]
pub struct DirectoryContent {
pub files: BTreeMap<String, LanguageContent>,
pub directories: BTreeSet<String>,
}
#[derive(Debug, Clone, Template)]
#[template(path = "repository/tree.html")]
struct TreeTemplate<'a> {
public: &'a str,
summary: &'a RepositorySummary,
bookmark: &'a str,
bookmarks: &'a [String],
current_dir: &'a Path,
current_name: &'a str,
adjacent_directories: &'a BTreeSet<String>,
adjacent_files: &'a BTreeMap<String, LanguageContent>,
content: Option<Cow<'a, LanguageContent>>,
}
pub async fn write_frontend(
output: &Path,
ui: &mut Ui,
ch: &CommandHelper,
name: &str,
public: &str,
bookmark: &str,
bookmarks: &[String],
commit: Commit,
) -> Result<(), CommandError> {
std::fs::create_dir_all(&output)?;
let wch = ch.workspace_helper(ui)?;
let tree = read_tree(&commit).await?;
let commits = ResolvedRevsetExpression::ancestors(&ResolvedRevsetExpression::commits(vec![
commit.id().clone(),
]))
.evaluate(wch.repo().base_repo())?
.count_estimate()?
.0;
let latest = commit
.author()
.timestamp
.to_datetime()
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
.map_err(|e| user_error_with_message("Couldn't format date properly", e))?;
let description = tree
.get(Path::new(""))
.and_then(|dir| dir.files.get("DESCRIPTION").map(|f| f.file.clone()))
.unwrap_or_else(|| String::from("<no_description>"));
let summary = RepositorySummary {
name: name.to_string(),
commits,
latest,
description,
};
for (path, directory) in &tree {
let mut template = TreeTemplate {
public,
summary: &summary,
current_dir: path,
current_name: "",
bookmark,
bookmarks,
adjacent_directories: &directory.directories,
adjacent_files: &directory.files,
content: directory.files.get("README.md").map(|f| {
Cow::Owned(LanguageContent {
language: Some("markdown"),
file: f.file.clone(),
})
}),
};
std::fs::create_dir_all(output.join(&path))?;
std::fs::write(
output.join(&path).join("index.html"),
template
.render()
.map_err(|e| internal_error_with_message("Couldn't render html", e))?,
)?;
for (adj_file, content) in &directory.files {
template.current_name = &adj_file;
template.content = Some(Cow::Borrowed(content));
std::fs::write(
output.join(&path).join(format!("{}.html", adj_file)),
template
.render()
.map_err(|e| internal_error_with_message("Couldn't render html", e))?,
)?;
}
}
Ok(())
}
async fn read_tree(commit: &Commit) -> Result<BTreeMap<PathBuf, DirectoryContent>, CommandError> {
let mut content = BTreeMap::<PathBuf, DirectoryContent>::new();
let root = PathBuf::new();
for (path, merge_tree_value) in commit.tree().entries() {
for tree_value in merge_tree_value? {
let Some(tree_value) = tree_value else {
continue;
};
let TreeValue::File { id, .. } = tree_value else {
continue;
};
let Some((parent, filename)) = path.split() else {
continue;
};
let Ok(parent_path) = parent.to_fs_path(&root) else {
continue;
};
let name = filename.as_internal_str().to_string();
let (read, language) = match classify_file(&path.to_fs_path_unchecked(&root)) {
None => (false, None),
Some(language) => (true, language),
};
let file = if read {
let mut buf = String::new();
commit
.store()
.read_file(&path, &id)
.await?
.read_to_string(&mut buf)
.await?;
buf
} else {
String::from("<no readable content>")
};
let entry = content.entry(parent_path);
let walked = matches!(entry, Entry::Occupied(_));
entry
.or_default()
.files
.insert(name, LanguageContent { language, file });
if walked {
continue;
}
let mut current = parent.to_owned();
while let Some((ancestor, dir_name)) = current.split() {
let Ok(ancestor_path) = ancestor.to_fs_path(&root) else {
break;
};
let dir_name = dir_name.as_internal_str().to_string();
content
.entry(ancestor_path)
.or_default()
.directories
.insert(dir_name);
current = ancestor.to_owned();
}
}
}
Ok(content)
}
fn classify_file(path: &Path) -> Option<Option<&'static str>> {
let name = path.file_name()?.to_str()?;
match name {
"Cargo.lock" | "package-lock.json" | "yarn.lock" => return Some(Some("toml")),
"Makefile" | "makefile" | "GNUmakefile" => return Some(Some("makefile")),
"Dockerfile" => return Some(Some("dockerfile")),
"LICENSE" | "README" | "CHANGELOG" | "AUTHORS" | "NOTICE" => return Some(None),
".gitignore" | ".gitattributes" | ".editorconfig" => return Some(None),
_ => {}
}
if let Some(ext) = path.extension() {
let ext = ext.to_str()?.to_ascii_lowercase();
let lang = match ext.as_str() {
"rs" => Some("rust"),
"py" => Some("python"),
"js" | "mjs" | "cjs" => Some("javascript"),
"ts" => Some("typescript"),
"jsx" => Some("javascript"),
"tsx" => Some("typescript"),
"go" => Some("go"),
"c" | "h" => Some("c"),
"cpp" | "cc" | "cxx" | "hpp" | "hh" | "hxx" => Some("cpp"),
"java" => Some("java"),
"kt" | "kts" => Some("kotlin"),
"scala" => Some("scala"),
"rb" => Some("ruby"),
"php" => Some("php"),
"swift" => Some("swift"),
"cs" => Some("csharp"),
"sh" | "bash" | "zsh" => Some("bash"),
"ps1" => Some("powershell"),
"lua" => Some("lua"),
"r" => Some("r"),
"sql" => Some("sql"),
"html" | "htm" => Some("xml"),
"xml" | "svg" => Some("xml"),
"css" => Some("css"),
"scss" | "sass" => Some("scss"),
"less" => Some("less"),
"json" => Some("json"),
"yaml" | "yml" => Some("yaml"),
"toml" => Some("toml"),
"ini" | "cfg" | "conf" => Some("ini"),
"md" | "markdown" => Some("markdown"),
"tex" => Some("latex"),
"diff" | "patch" => Some("diff"),
"dockerfile" => Some("dockerfile"),
"vim" => Some("vim"),
"el" => Some("lisp"),
"clj" | "cljs" => Some("clojure"),
"hs" => Some("haskell"),
"ml" | "mli" => Some("ocaml"),
"ex" | "exs" => Some("elixir"),
"erl" => Some("erlang"),
"dart" => Some("dart"),
"nim" => Some("nim"),
"zig" => Some("zig"),
"nix" => Some("nix"),
"nu" => Some("nu"),
"txt" | "log" | "csv" | "tsv" => return Some(None),
"png" | "jpg" | "jpeg" | "gif" | "bmp" | "ico" | "webp" | "tiff" | "pdf" | "zip"
| "tar" | "gz" | "bz2" | "xz" | "7z" | "rar" | "exe" | "dll" | "so" | "dylib" | "o"
| "a" | "lib" | "class" | "jar" | "wasm" | "mp3" | "mp4" | "wav" | "flac" | "ogg"
| "avi" | "mov" | "mkv" | "ttf" | "otf" | "woff" | "woff2" | "eot" => return None,
_ => return None,
};
Some(lang)
} else {
Some(None)
}
}