#![feature(map_try_insert)]
#[macro_use]
extern crate napi_derive;
use std::path::{Path, PathBuf};
use anyhow::bail;
use canonical_path::{CanonicalPath, CanonicalPathBuf};
use libpijul::change::{BaseHunk, Change};
use libpijul::changestore::ChangeStore;
use libpijul::vertex_buffer::VertexBuffer;
use libpijul::{
change::{ChangeHeader, LocalChange},
pristine::TxnT,
MutTxnT, RecordBuilder,
};
use libpijul::{Base32, ChangeId, GraphTxnT, Hash, MutTxnTExt, TxnTExt};
use pijul_repository::Repository;
use walkdir::WalkDir;
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
pub enum Annotation {
Modified,
}
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
pub enum State {
Tracked(Vec<Annotation>),
Added,
Untracked,
Ignored,
}
impl State {
fn badge(&self) -> String {
match self {
Self::Tracked(annotations) => annotations
.iter()
.map(|annotation| {
match annotation {
Annotation::Modified => "M",
}
.to_string()
})
.collect::<Vec<String>>()
.join(", "),
Self::Added => String::from("A"),
Self::Untracked => String::from("U"),
Self::Ignored => String::new(),
}
}
fn tooltip(&self) -> String {
match self {
Self::Tracked(annotations) => annotations
.iter()
.map(|annotation| {
match annotation {
Annotation::Modified => "Modified",
}
.to_string()
})
.collect::<Vec<String>>()
.join(", "),
Self::Added => String::from("Added"),
Self::Untracked => String::from("Untracked"),
Self::Ignored => String::new(),
}
}
}
#[napi]
pub fn resolve_repositories(paths: Vec<String>) -> Result<Vec<String>, napi::Error> {
let mut repositories = Vec::new();
for path in paths {
let path_buf = PathBuf::from(path);
let potential_root = Repository::find_root(Some(path_buf));
if let Ok(root) = potential_root {
let path_str = root.path.to_string_lossy().to_string();
if repositories.iter().all(|repo_path| repo_path != &path_str) {
repositories.push(path_str);
}
}
}
Ok(repositories)
}
#[napi]
pub fn ignored(path: String) -> Result<bool, anyhow::Error> {
let root = find_closest_root(path.clone())?;
let root = CanonicalPath::new(&root).unwrap();
let path = CanonicalPath::new(Path::new(&path)).unwrap();
Ok(!libpijul::working_copy::filesystem::filter_ignore(
root,
path,
root.join(path)?.is_dir(),
))
}
#[napi]
fn find_closest_root(path: String) -> Result<String, anyhow::Error> {
let mut path = PathBuf::from(path).join(libpijul::DOT_DIR);
while let Some(parent) = path.parent() {
if parent.join(libpijul::DOT_DIR).is_dir() {
return Ok(parent.to_string_lossy().to_string());
}
path.pop();
}
bail!("No path found");
}
pub struct InlineCreditBuffer {
hashes: Vec<(ChangeId, u64)>,
}
impl InlineCreditBuffer {
pub fn new() -> Result<Self, anyhow::Error> {
Ok(Self { hashes: Vec::new() })
}
}
impl VertexBuffer for InlineCreditBuffer {
fn output_line<E, F>(
&mut self,
key: libpijul::Vertex<libpijul::ChangeId>,
contents_writer: F,
) -> Result<(), E>
where
E: From<std::io::Error>,
F: FnOnce(&mut [u8]) -> Result<(), E>,
{
if key.change.is_root() {
return Ok(());
}
let mut buf = vec![0; key.end - key.start];
contents_writer(&mut buf)?;
for _line in String::from_utf8(buf).expect("Non-utf8 change").lines() {
if let Some((last_change, lines)) = self.hashes.last_mut() {
if last_change.to_owned() == key.change {
*lines += 1;
continue;
}
}
self.hashes.push((key.change, 1));
}
Ok(())
}
fn output_conflict_marker<C: libpijul::changestore::ChangeStore>(
&mut self,
_s: &str,
_id: usize,
_sides: Option<(&C, &[&Hash])>,
) -> Result<(), std::io::Error> {
unimplemented!();
}
}
#[napi]
pub struct FileState {
path: PathBuf,
current_state: Change,
recorded_changes: Vec<Change>,
hashes: Vec<(Hash, u64)>,
}
#[napi]
impl FileState {
#[napi(constructor)]
pub fn new(path: String) -> Result<Self, anyhow::Error> {
let path = PathBuf::from(path);
let repo = Repository::find_root(Some(path.to_owned())).expect("Could not find root");
let txn = repo.pristine.arc_txn_begin()?;
let channel_name = txn
.read()
.current_channel()
.unwrap_or(libpijul::DEFAULT_CHANNEL)
.to_string();
let channel = txn.write().open_or_create_channel(&channel_name)?;
let prefix = path.strip_prefix(&repo.path)?.to_string_lossy().to_string();
let mut record_builder = RecordBuilder::new();
record_builder.record(
txn.clone(),
libpijul::Algorithm::default(),
false,
&libpijul::DEFAULT_SEPARATOR,
channel.clone(),
&repo.working_copy,
&repo.changes,
&prefix,
std::thread::available_parallelism()?.into(),
)?;
let recorded = record_builder.finish();
let mut write_txn = txn.write();
let actions: Vec<_> = recorded
.actions
.into_iter()
.map(|rec| rec.globalize(&*write_txn).unwrap())
.collect();
let contents = if let Ok(cont) = std::sync::Arc::try_unwrap(recorded.contents) {
cont.into_inner()
} else {
unreachable!()
};
let mut change = LocalChange::make_change(
&*write_txn,
&channel,
actions,
contents,
ChangeHeader::default(),
Vec::new(),
)?;
let (oldest_change, _ambiguous) =
write_txn.follow_oldest_path(&repo.changes, &channel, prefix.as_str())?;
repo.changes
.save_change(&mut change, |_, _| Ok::<_, anyhow::Error>(()))?;
write_txn.apply_change(&repo.changes, &mut channel.write(), &change.hash()?)?;
std::mem::drop(write_txn);
let mut credit_buffer = InlineCreditBuffer::new()?;
libpijul::output::output_file(
&repo.changes,
&txn,
&channel,
oldest_change,
&mut credit_buffer,
)?;
let mut hashes = Vec::new();
let mut changes: Vec<Change> = Vec::new();
for (change_id, lines) in credit_buffer.hashes {
let hash = Hash::from(txn.read().get_external(&change_id)?.unwrap().to_owned());
let change = repo.changes.get_change(&hash)?;
if !changes.iter().any(|c| c.hash().unwrap() == hash) {
changes.push(change);
}
hashes.push((hash, lines));
}
repo.changes.del_change(&change.hash()?)?;
Ok(Self {
path,
current_state: change,
recorded_changes: changes,
hashes,
})
}
fn state(&self) -> State {
let mut annotations = Vec::new();
for change in &self.current_state.changes {
match change {
BaseHunk::Edit { .. } | BaseHunk::Replacement { .. } => {
annotations.push(Annotation::Modified);
}
BaseHunk::FileAdd { .. } => {
return State::Added;
}
_ => todo!(),
}
}
State::Tracked(annotations)
}
#[napi]
pub fn badge(&self) -> String {
self.state().badge()
}
#[napi]
pub fn tooltip(&self) -> String {
self.state().tooltip()
}
#[napi]
pub fn untracked(&self) -> bool {
false
}
#[napi]
pub fn path(&self) -> String {
self.path.to_string_lossy().to_string()
}
fn change_at<'a>(&'a self, target_line: u32) -> Result<&'a Change, anyhow::Error> {
let mut current_line: u64 = 0;
for (hash, line_count) in &self.hashes {
if (current_line..current_line + line_count).contains(&(target_line as u64)) {
if hash == &self.current_state.hash()? {
return Ok(&self.current_state);
} else {
for change in &self.recorded_changes {
if &change.hash()? == hash {
return Ok(&change);
}
}
}
}
current_line += line_count;
}
bail!("Line outside of valid range");
}
#[napi]
pub fn hash_at(&self, target_line: u32) -> Result<String, anyhow::Error> {
let change = self.change_at(target_line)?;
Ok(change.hash()?.to_base32())
}
#[napi]
pub fn authors_at(&self, target_line: u32) -> Result<String, anyhow::Error> {
let change = self.change_at(target_line)?;
if change == &self.current_state {
Ok(String::from("You"))
} else {
Ok(String::from("You"))
}
}
#[napi]
pub fn message_at(&self, target_line: u32) -> Result<String, anyhow::Error> {
let change = self.change_at(target_line)?;
if change == &self.current_state {
Ok(String::from("Unrecorded changes"))
} else {
Ok(change.header.message.clone())
}
}
}
#[napi]
pub fn all_files(path_name: String) -> Result<Vec<FileState>, anyhow::Error> {
let path = PathBuf::from(&path_name);
println!("Path: {path:?}");
if path.is_file() {
return Ok(vec![FileState::new(path_name)?]);
}
let root = match Repository::find_root(Some(path)) {
Ok(repo) => CanonicalPathBuf::new(repo.path)?,
Err(_) => return Ok(Vec::new()),
};
let mut children = Vec::new();
let walker = WalkDir::new(&root).into_iter();
for entry in walker.filter_entry(|entry| {
let current = CanonicalPathBuf::new(entry.path()).unwrap();
libpijul::working_copy::filesystem::filter_ignore(
root.as_canonical_path(),
current.as_canonical_path(),
current.is_dir(),
)
}) {
let path = entry?.path().to_path_buf();
if path.is_file() {
let name = path.to_string_lossy().to_string();
println!("Creating file state with name: {name}");
children.push(FileState::new(name)?);
}
}
Ok(children)
}