pub mod links;
pub mod notes;
pub mod refs;
pub use links::Link;
pub use notes::Note;
pub use refs::{ProjectId, Ref};
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::Serialize;
pub(crate) fn write_file(path: &Path, text: &str) -> Result<()> {
let parent = path.parent().with_context(|| format!("{} has no folder", path.display()))?;
std::fs::create_dir_all(parent).with_context(|| format!("cannot create {}", parent.display()))?;
let staged = parent.join(format!(
".{}.partial",
path.file_name().map(|name| name.to_string_lossy()).unwrap_or_default()
));
std::fs::write(&staged, text).with_context(|| format!("cannot write {}", staged.display()))?;
std::fs::rename(&staged, path).with_context(|| format!("cannot write {}", path.display()))?;
Ok(())
}
pub(crate) fn files_with_extension(dir: &Path, extension: &str) -> Result<Vec<(String, PathBuf)>> {
let entries = match std::fs::read_dir(dir) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(e).with_context(|| format!("cannot read {}", dir.display())),
};
let mut files = Vec::new();
for entry in entries {
let entry = entry.with_context(|| format!("cannot read {}", dir.display()))?;
let path = entry.path();
let name = entry.file_name().to_string_lossy().into_owned();
match name.strip_suffix(&format!(".{extension}")) {
Some(stem) if entry.file_type().map(|t| t.is_file()).unwrap_or(false) => {
files.push((stem.to_string(), path))
}
_ => anyhow::bail!(
"{} is not a .{extension} file, and nothing else belongs in {}",
path.display(),
dir.display()
),
}
}
files.sort();
Ok(files)
}
pub fn remove_note(root: &Path, id: &str) -> Result<PathBuf> {
let path = notes::path_of(root, ¬es::check_id(id)?);
std::fs::remove_file(&path).with_context(|| format!("there is no note {id} in {}", root.display()))?;
Ok(path)
}
pub fn remove_link(root: &Path, link: &Link) -> Result<PathBuf> {
let path = links::path_of(root, &link.id());
std::fs::remove_file(&path).with_context(|| {
format!("there is no link {} → {} in {}", link.from, link.to, root.display())
})?;
Ok(path)
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub struct Place {
pub workspace: String,
pub worktree: String,
pub root: PathBuf,
}
#[derive(Debug, Clone, Serialize)]
pub struct HeldLink {
pub id: String,
#[serde(flatten)]
pub link: Link,
pub places: Vec<Place>,
}
#[derive(Debug, Clone, Serialize)]
pub struct HeldNote {
#[serde(flatten)]
pub note: Note,
pub places: Vec<Place>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct Index {
pub notes: Vec<HeldNote>,
pub links: Vec<HeldLink>,
pub problems: Vec<String>,
}
impl Index {
pub fn gather(workshop: &wsb_prj::Workshop) -> Result<Self> {
let mut places = Vec::new();
for workspace in workshop.workspaces()? {
for worktree in workspace.worktrees()? {
places.push(Place {
workspace: workspace.name().to_string(),
worktree: worktree.name,
root: worktree.path,
});
}
}
Ok(Self::of(places))
}
pub fn of(places: Vec<Place>) -> Self {
let mut links: BTreeMap<String, (Link, Vec<Place>)> = BTreeMap::new();
let mut notes: BTreeMap<(String, String), (Note, Vec<Place>)> = BTreeMap::new();
let mut problems = Vec::new();
for place in places {
match Link::all(&place.root) {
Ok(held) => {
for link in held {
links.entry(link.id()).or_insert_with(|| (link, Vec::new())).1.push(place.clone());
}
}
Err(e) => problems.push(format!("{}/{}: {e:#}", place.workspace, place.worktree)),
}
match Note::all(&place.root) {
Ok(held) => {
for note in held {
notes
.entry((note.id.clone(), note.render()))
.or_insert_with(|| (note, Vec::new()))
.1
.push(place.clone());
}
}
Err(e) => problems.push(format!("{}/{}: {e:#}", place.workspace, place.worktree)),
}
}
Self {
links: links
.into_iter()
.map(|(id, (link, places))| HeldLink { id, link, places })
.collect(),
notes: notes.into_values().map(|(note, places)| HeldNote { note, places }).collect(),
problems,
}
}
pub fn links_at<'a>(&'a self, end: &'a Ref) -> impl Iterator<Item = &'a HeldLink> + 'a {
self.links.iter().filter(move |held| held.link.other_end(end).is_some())
}
pub fn note(&self, id: &str) -> Vec<&HeldNote> {
self.notes.iter().filter(|held| held.note.id == id).collect()
}
pub fn named_projects(&self) -> BTreeSet<ProjectId> {
self.links
.iter()
.flat_map(|held| [&held.link.from, &held.link.to])
.filter_map(|end| end.project().cloned())
.collect()
}
}
#[cfg(test)]
mod tests;