use regex::bytes::Regex as ByteRegex;
use std::sync::OnceLock;
pub fn nick_pat() -> &'static ByteRegex {
static R: OnceLock<ByteRegex> = OnceLock::new();
R.get_or_init(|| ByteRegex::new(r"^nickname\s*=\s*(.+)$").unwrap())
}
pub struct CoerceIO {
pub buffer: String,
}
impl Default for CoerceIO {
fn default() -> Self {
Self::new()
}
}
impl CoerceIO {
pub fn new() -> Self {
Self {
buffer: String::new(),
}
}
pub fn write(&mut self, arg: &[u8]) -> usize {
let s = String::from_utf8_lossy(arg);
let n = s.len();
self.buffer.push_str(&s);
n
}
}
pub fn branch_name_from_config_file(
directory: &std::path::Path,
config_file: &std::path::Path,
) -> String {
if let Ok(bytes) = std::fs::read(config_file) {
for line in bytes.split(|&b| b == b'\n') {
if let Some(c) = nick_pat().captures(line) {
if let Some(m) = c.get(1) {
let s = String::from_utf8_lossy(m.as_bytes()).trim().to_string();
if !s.is_empty() {
return s;
}
}
}
}
}
directory
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default()
}
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 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 _status(&self, _directory: &std::path::Path, _path: Option<&str>) -> Option<String> {
None
}
pub fn branch(&self) -> String {
let config_file = self
.directory
.join(".bzr")
.join("branch")
.join("branch.conf");
branch_name_from_config_file(&self.directory, &config_file)
}
pub fn aggregate_short_status(raw: &str) -> Option<String> {
if raw.trim().is_empty() {
return None;
}
let mut dirtied: char = ' ';
let mut untracked: char = ' ';
for line in raw.lines() {
let bytes = line.as_bytes();
if bytes.len() > 1 && b"ACDMRIN".contains(&bytes[1]) {
dirtied = 'D';
}
if !bytes.is_empty() && bytes[0] == b'?' {
untracked = 'U';
}
}
let ans: String = format!("{}{}", dirtied, untracked);
if ans.trim().is_empty() {
None
} else {
Some(ans)
}
}
pub fn extract_file_status(raw: &str) -> Option<String> {
if raw.trim().is_empty() {
return None;
}
let ans: String = raw.chars().take(2).collect();
if ans == "I " {
None
} else {
Some(ans)
}
}
}
#[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-bzr-{}-{}-{}",
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 nick_pat_matches_simple_nickname_line() {
let m = nick_pat().captures(b"nickname = main-branch").unwrap();
assert_eq!(&m[1], b"main-branch");
}
#[test]
fn nick_pat_matches_with_extra_whitespace() {
let m = nick_pat()
.captures(b"nickname = my-feature ")
.unwrap();
assert_eq!(&m[1], b"my-feature ");
}
#[test]
fn nick_pat_does_not_match_unrelated_line() {
assert!(nick_pat().captures(b"# comment").is_none());
assert!(nick_pat().captures(b"other = value").is_none());
}
#[test]
fn coerce_io_write_decodes_bytes() {
let mut io = CoerceIO::new();
io.write(b"hello ");
io.write(b"world");
assert_eq!(io.buffer, "hello world");
}
#[test]
fn coerce_io_write_handles_invalid_utf8() {
let mut io = CoerceIO::new();
io.write(&[b'a', 0xff, b'b']);
assert!(io.buffer.contains('a'));
assert!(io.buffer.contains('b'));
}
#[test]
fn branch_name_extracts_nickname_from_config() {
let d = tmp_dir();
let f = d.join("branch.conf");
let mut h = std::fs::File::create(&f).unwrap();
h.write_all(b"# header\nnickname = feature-x\nother = ignored\n")
.unwrap();
let name = branch_name_from_config_file(&d, &f);
assert_eq!(name, "feature-x");
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn branch_name_falls_back_to_directory_basename() {
let d = tmp_dir();
let basename = d.file_name().unwrap().to_string_lossy().to_string();
let f = d.join("does-not-exist");
let name = branch_name_from_config_file(&d, &f);
assert_eq!(name, basename);
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn branch_name_falls_back_when_no_nickname_line() {
let d = tmp_dir();
let basename = d.file_name().unwrap().to_string_lossy().to_string();
let f = d.join("branch.conf");
let mut h = std::fs::File::create(&f).unwrap();
h.write_all(b"# only a comment\n").unwrap();
let name = branch_name_from_config_file(&d, &f);
assert_eq!(name, basename);
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_branch_conf() {
let d = tmp_dir();
let branch_dir = d.join(".bzr").join("branch");
std::fs::create_dir_all(&branch_dir).unwrap();
let f = branch_dir.join("branch.conf");
let mut h = std::fs::File::create(&f).unwrap();
h.write_all(b"nickname = lp:foo\n").unwrap();
let repo = Repository::new(&d, ());
assert_eq!(repo.branch(), "lp:foo");
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn repository_branch_falls_back_to_basename_when_no_conf() {
let d = tmp_dir();
let basename = d.file_name().unwrap().to_string_lossy().to_string();
let repo = Repository::new(&d, ());
let expected = repo
.directory
.file_name()
.unwrap()
.to_string_lossy()
.to_string();
let actual = repo.branch();
assert!(actual == expected || actual == basename);
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn repository_status_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 aggregate_short_status_empty_returns_none() {
assert_eq!(Repository::aggregate_short_status(""), None);
assert_eq!(Repository::aggregate_short_status(" \n "), None);
}
#[test]
fn aggregate_short_status_modified_returns_d() {
let raw = " M file.txt\n";
assert_eq!(
Repository::aggregate_short_status(raw),
Some("D ".to_string())
);
}
#[test]
fn aggregate_short_status_untracked_returns_u() {
let raw = "? newfile.txt\n";
assert_eq!(
Repository::aggregate_short_status(raw),
Some(" U".to_string())
);
}
#[test]
fn aggregate_short_status_both_returns_du() {
let raw = " M a.txt\n? b.txt\n";
assert_eq!(
Repository::aggregate_short_status(raw),
Some("DU".to_string())
);
}
#[test]
fn aggregate_short_status_only_clean_chars_returns_none() {
let raw = " \n \n";
assert_eq!(Repository::aggregate_short_status(raw), None);
}
#[test]
fn extract_file_status_takes_first_two_chars() {
assert_eq!(
Repository::extract_file_status(" M file.txt\n"),
Some(" M".to_string())
);
assert_eq!(
Repository::extract_file_status("? file.txt\n"),
Some("? ".to_string())
);
}
#[test]
fn extract_file_status_ignored_returns_none() {
assert_eq!(Repository::extract_file_status("I file.txt\n"), None);
}
#[test]
fn extract_file_status_empty_returns_none() {
assert_eq!(Repository::extract_file_status(""), None);
assert_eq!(Repository::extract_file_status(" "), None);
}
}