use std::path::Path;
use anyhow::Result;
use crate::source::Checkout;
pub mod git;
pub mod jj;
pub trait Vcs {
fn ensure_commit(&self, repo: &Path, refspec_or_sha: &str) -> Result<()>;
fn add_workspace(
&self,
repo: &Path,
ws: &Path,
checkout: &Checkout,
name: &str,
version: &str,
) -> Result<String>;
fn update_workspace(
&self,
repo: &Path,
ws: &Path,
checkout: &Checkout,
name: &str,
version: &str,
) -> Result<String>;
fn is_dirty(&self, ws: &Path, expected_head: &str) -> Result<bool>;
fn position(&self, ws: &Path, commit: &str) -> Result<()>;
fn commits(&self, ws: &Path, tip: &str, limit: usize) -> Result<Vec<(String, String)>>;
fn remove_workspace(&self, repo: &Path, ws: &Path, name: &str) -> Result<()>;
}
pub(crate) fn parse_commit_records(raw: &str) -> Vec<(String, String)> {
raw.split('\x1e')
.filter_map(|rec| {
let (id, msg) = rec.trim_start_matches('\n').split_once('\x1f')?;
Some((id.trim().to_string(), msg.to_string()))
})
.collect()
}
pub(crate) fn clean_output(raw: &[u8]) -> String {
let text = String::from_utf8_lossy(raw);
let mut lines = Vec::new();
let mut line: Vec<char> = Vec::new();
let mut col: usize = 0;
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\n' => {
lines.push(std::mem::take(&mut line).into_iter().collect::<String>());
col = 0;
}
'\r' => col = 0,
'\u{8}' => col = col.saturating_sub(1),
'\u{1b}' => {
if chars.peek() == Some(&'[') {
chars.next();
for n in chars.by_ref() {
if ('\u{40}'..='\u{7e}').contains(&n) {
break;
}
}
}
}
c if c.is_control() || c == '\u{fffd}' => {}
c => {
if col < line.len() {
line[col] = c;
} else {
line.push(c);
}
col += 1;
}
}
}
lines.push(line.into_iter().collect());
lines
.iter()
.map(|l| l.trim_end())
.collect::<Vec<_>>()
.join("\n")
.trim()
.to_string()
}
#[cfg(test)]
mod clean_output_tests {
use super::clean_output;
#[test]
fn applies_overwrites_and_strips_escapes() {
let raw = b"Starting up.. -\x08\\\x08|\nFetching\rDone!!!!\n\x1b[31mred\x1b[0m\n";
assert_eq!(clean_output(raw), "Starting up.. |\nDone!!!!\nred");
}
}