use std::collections::HashMap;
use std::sync::OnceLock;
pub fn branch_name_from_config_file(
_directory: &std::path::Path,
config_file: &std::path::Path,
) -> String {
match std::fs::read(config_file) {
Ok(bytes) => String::from_utf8_lossy(&bytes).trim().to_string(),
Err(_) => "default".to_string(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileStatus {
M,
A,
R,
D,
U,
I,
Clean,
}
impl FileStatus {
pub fn as_char(&self) -> &'static str {
match self {
FileStatus::M => "M",
FileStatus::A => "A",
FileStatus::R => "R",
FileStatus::D => "D",
FileStatus::U => "U",
FileStatus::I => "I",
FileStatus::Clean => "",
}
}
}
pub fn statuses() -> &'static HashMap<u8, (FileStatus, u8)> {
static M: OnceLock<HashMap<u8, (FileStatus, u8)>> = OnceLock::new();
M.get_or_init(|| {
let mut t = HashMap::new();
t.insert(b'M', (FileStatus::M, 1));
t.insert(b'A', (FileStatus::A, 1));
t.insert(b'R', (FileStatus::R, 1));
t.insert(b'!', (FileStatus::D, 1));
t.insert(b'?', (FileStatus::U, 2));
t.insert(b'I', (FileStatus::I, 0));
t.insert(b'C', (FileStatus::Clean, 0));
t
})
}
pub const REPO_STATUSES_STR: [Option<&str>; 4] = [None, Some("D "), Some(" U"), Some("DU")];
pub struct Repository {
pub directory: std::path::PathBuf,
pub create_watcher: (),
}
impl Repository {
pub fn new(directory: impl AsRef<std::path::Path>, create_watcher: ()) -> Self {
let abs = std::fs::canonicalize(directory.as_ref())
.unwrap_or_else(|_| directory.as_ref().to_path_buf());
Self {
directory: abs,
create_watcher,
}
}
pub fn _repo<'a>(&'a self, _directory: &std::path::Path) -> &'a std::path::Path {
&self.directory
}
pub fn status(&self, _path: Option<&str>) -> Option<String> {
None
}
pub fn do_status(&self, _directory: &std::path::Path, _path: Option<&str>) -> Option<String> {
None
}
pub fn branch(&self) -> String {
let config_file = self.directory.join(".hg").join("branch");
branch_name_from_config_file(&self.directory, &config_file)
}
pub fn aggregate_repo_status(codes: impl IntoIterator<Item = u8>) -> Option<&'static str> {
let mut bits: u8 = 0;
let table = statuses();
for c in codes {
if let Some((_, bit)) = table.get(&c) {
bits |= bit;
}
}
REPO_STATUSES_STR[bits as usize % 4]
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn tmp_dir() -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut p = std::env::temp_dir();
p.push(format!(
"powerliners-hg-{}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos(),
COUNTER.fetch_add(1, Ordering::SeqCst)
));
std::fs::create_dir_all(&p).unwrap();
p
}
#[test]
fn statuses_table_matches_upstream() {
let t = statuses();
assert_eq!(t.get(&b'M'), Some(&(FileStatus::M, 1)));
assert_eq!(t.get(&b'A'), Some(&(FileStatus::A, 1)));
assert_eq!(t.get(&b'R'), Some(&(FileStatus::R, 1)));
assert_eq!(t.get(&b'!'), Some(&(FileStatus::D, 1)));
assert_eq!(t.get(&b'?'), Some(&(FileStatus::U, 2)));
assert_eq!(t.get(&b'I'), Some(&(FileStatus::I, 0)));
assert_eq!(t.get(&b'C'), Some(&(FileStatus::Clean, 0)));
}
#[test]
fn repo_statuses_str_matches_upstream() {
assert_eq!(REPO_STATUSES_STR[0], None);
assert_eq!(REPO_STATUSES_STR[1], Some("D "));
assert_eq!(REPO_STATUSES_STR[2], Some(" U"));
assert_eq!(REPO_STATUSES_STR[3], Some("DU"));
}
#[test]
fn file_status_as_char_matches_upstream() {
assert_eq!(FileStatus::M.as_char(), "M");
assert_eq!(FileStatus::A.as_char(), "A");
assert_eq!(FileStatus::R.as_char(), "R");
assert_eq!(FileStatus::D.as_char(), "D");
assert_eq!(FileStatus::U.as_char(), "U");
assert_eq!(FileStatus::I.as_char(), "I");
assert_eq!(FileStatus::Clean.as_char(), "");
}
#[test]
fn branch_name_from_existing_file_returns_trimmed_content() {
let d = tmp_dir();
let f = d.join("branch");
let mut h = std::fs::File::create(&f).unwrap();
h.write_all(b"feature-xyz\n").unwrap();
let name = branch_name_from_config_file(&d, &f);
assert_eq!(name, "feature-xyz");
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn branch_name_missing_file_returns_default() {
let d = tmp_dir();
let f = d.join("does-not-exist");
let name = branch_name_from_config_file(&d, &f);
assert_eq!(name, "default");
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn branch_name_empty_file_returns_empty_string_after_strip() {
let d = tmp_dir();
let f = d.join("branch");
std::fs::File::create(&f).unwrap();
let name = branch_name_from_config_file(&d, &f);
assert_eq!(name, "");
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn repository_new_canonicalizes_directory() {
let d = tmp_dir();
let repo = Repository::new(&d, ());
assert!(repo.directory.is_absolute());
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn repository_branch_reads_hg_branch_file() {
let d = tmp_dir();
let hg = d.join(".hg");
std::fs::create_dir_all(&hg).unwrap();
let f = hg.join("branch");
let mut h = std::fs::File::create(&f).unwrap();
h.write_all(b"main\n").unwrap();
let repo = Repository::new(&d, ());
assert_eq!(repo.branch(), "main");
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn repository_branch_missing_returns_default() {
let d = tmp_dir();
let repo = Repository::new(&d, ());
assert_eq!(repo.branch(), "default");
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn repository_status_repo_level_stub_returns_none() {
let d = tmp_dir();
let repo = Repository::new(&d, ());
assert_eq!(repo.status(None), None);
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn repository_do_status_stub_returns_none() {
let d = tmp_dir();
let repo = Repository::new(&d, ());
assert_eq!(repo.do_status(&d, None), None);
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn aggregate_repo_status_clean_returns_none() {
let r = Repository::aggregate_repo_status(std::iter::empty());
assert_eq!(r, None);
}
#[test]
fn aggregate_repo_status_modified_returns_dirty() {
let r = Repository::aggregate_repo_status([b'M']);
assert_eq!(r, Some("D "));
}
#[test]
fn aggregate_repo_status_unknown_returns_untracked() {
let r = Repository::aggregate_repo_status([b'?']);
assert_eq!(r, Some(" U"));
}
#[test]
fn aggregate_repo_status_dirty_plus_untracked_returns_both() {
let r = Repository::aggregate_repo_status([b'M', b'?']);
assert_eq!(r, Some("DU"));
}
#[test]
fn aggregate_repo_status_clean_codes_ignored() {
let r = Repository::aggregate_repo_status([b'I', b'C']);
assert_eq!(r, None);
}
}