mod err;
use std::{
env,
io::{self, BufRead, BufReader},
path::{Path, PathBuf},
process::Command
};
use hashbrown::{HashMap, HashSet};
pub use err::Error;
pub fn detect_repo(dir: Option<&Path>) -> Result<bool, Error> {
let magic_files = &[".fslckout", ".fos", "_FOSSIL_"];
let curdir = if let Some(dir) = dir {
dir.to_path_buf()
} else {
env::current_dir()?
};
let mut checkdir: &Path = curdir.as_ref();
let found = 'outer: loop {
for n in magic_files {
let fname = checkdir.join(n);
if fname.exists() {
break 'outer true;
}
}
let Some(parentdir) = checkdir.parent() else {
break false;
};
checkdir = parentdir;
};
Ok(found)
}
pub fn infomap(dir: Option<&Path>) -> Result<HashMap<String, String>, Error> {
let mut cmd = Command::new("fossil");
cmd.arg("info");
if let Some(ref dir) = dir {
cmd.current_dir(dir);
}
let output = cmd.output()?;
let reader = std::io::Cursor::new(output.stdout);
let reader = BufReader::new(reader);
let mut infomap = HashMap::new();
for line in reader.lines() {
let Ok(line) = line else {
continue;
};
let Some((key, value)) = line.split_once(':') else {
continue;
};
let key = key.trim();
let value = value.trim();
match key {
"project-name" | "project-code" | "repository" | "local-root"
| "config-db" | "tags" | "comment" => {
infomap.insert(key.to_string(), value.to_string());
}
"checkout" | "parent" => {
let (hash, timestamp) = parse_checkout(value);
let k = format!("{key}-hash");
infomap.insert(k, hash.to_string());
let k = format!("{key}-ts");
infomap.insert(k, timestamp.to_string());
}
_ => {}
}
}
Ok(infomap)
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Status {
pub prjname: String,
pub prjcode: String,
pub local_root: PathBuf,
pub checkout_hash: String,
pub checkout_ts: String,
pub tags: HashSet<String>,
pub uncommitted: bool,
pub untracked: bool
}
pub fn status(dir: Option<&Path>) -> Result<Status, Error> {
let mut infomap = infomap(dir)?;
let prjname = infomap
.remove("project-name")
.ok_or_else(|| Error::missing("project-name"))?;
let prjcode = infomap
.remove("project-code")
.ok_or_else(|| Error::missing("project-code"))?;
let local_root = infomap
.remove("local-root")
.map(PathBuf::from)
.ok_or_else(|| Error::missing("local-root"))?;
let checkout_hash = infomap
.remove("checkout-hash")
.ok_or_else(|| Error::missing("checkout-hash"))?;
let checkout_ts = infomap
.remove("checkout-ts")
.ok_or_else(|| Error::missing("checkout-ts"))?;
let tags = infomap.remove("tags").map_or_else(HashSet::new, |tags| {
tags.split(',').map(|x| x.trim().to_string()).collect()
});
let uncommitted = has_changes(dir)?;
let untracked = has_extras(dir)?;
Ok(Status {
prjname,
prjcode,
local_root,
checkout_hash,
checkout_ts,
tags,
uncommitted,
untracked
})
}
fn parse_checkout(value: &str) -> (&str, &str) {
let (hash, timestamp) = value
.split_once(' ')
.expect("Unable to split hash/timestamp");
(hash.trim(), timestamp.trim())
}
pub fn has_changes(dir: Option<&Path>) -> Result<bool, Error> {
let mut cmd = Command::new("fossil");
cmd.arg("changes");
if let Some(ref dir) = dir {
cmd.current_dir(dir);
}
let output = cmd.output()?;
let buf = std::str::from_utf8(&output.stdout)
.map_err(|_| Error::encoding("`fossil changes` output is not utf-8"))?;
let cursor = io::Cursor::new(buf);
let lines_iter = cursor.lines().map_while(Result::ok);
let lines = lines_iter.count();
Ok(lines != 0)
}
pub fn has_extras(dir: Option<&Path>) -> Result<bool, Error> {
let mut cmd = Command::new("fossil");
cmd.arg("extras");
if let Some(ref dir) = dir {
cmd.current_dir(dir);
}
let output = cmd.output()?;
let buf = std::str::from_utf8(&output.stdout)
.map_err(|_| Error::encoding("`fossil extras` output is not utf-8"))?;
let cursor = io::Cursor::new(buf);
let lines_iter = cursor.lines().map_while(Result::ok);
let lines = lines_iter.count();
Ok(lines != 0)
}