use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::num::NonZeroUsize;
use camino::{Utf8Path, Utf8PathBuf};
use canonical_path::CanonicalPathBuf;
use libpijul::change::{BaseHunk, Hunk};
use libpijul::changestore::{ChangeStore, FileMetadata};
use libpijul::pristine::sanakirja::{MutTxn, SanakirjaError};
use libpijul::pristine::{ChangePosition, Position};
use libpijul::working_copy::WorkingCopy;
use libpijul::{ArcTxn, ChannelRef, RecordBuilder, TxnTExt};
use patricia_tree::GenericPatriciaMap;
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub enum CreatePathStatesError {
#[error("Unable to canonicalize root path `{root_path}`: {io_error:#?}")]
CanonicalRoot {
root_path: Utf8PathBuf,
io_error: std::io::Error,
},
#[error("Failed to iterate through workspace: {0}")]
Iteration(std::io::Error),
#[error("Failed to check if path is tracked: {0}")]
IsTracked(SanakirjaError),
}
#[derive(Debug, thiserror::Error)]
pub enum PathStatesError<C: std::error::Error + 'static, W: std::error::Error> {
Create(#[from] CreatePathStatesError),
Sanakirja(#[from] SanakirjaError),
Record(#[from] libpijul::record::RecordError<C, W, MutTxn<()>>),
FindPath(#[from] libpijul::output::FileError<C, MutTxn<()>>),
}
#[derive(Clone, Copy, Debug)]
pub enum TrackedState {
Added,
Removed,
Modified,
Moved,
ModifiedAndMoved,
}
impl TrackedState {
pub fn join_hunk<Hash: std::fmt::Debug, Local: std::fmt::Debug>(
state: Option<Self>,
hunk: &Hunk<Option<Hash>, Local>,
) -> Option<Self> {
match hunk {
BaseHunk::FileMove { .. } => match state {
Some(TrackedState::Modified) => Some(TrackedState::ModifiedAndMoved),
None => Some(TrackedState::Moved),
Some(_) => unreachable!("{hunk:#?}"),
},
BaseHunk::FileDel { .. } => match state {
None => Some(TrackedState::Removed),
Some(_existing_state) => unreachable!("{hunk:#?}"),
},
BaseHunk::FileAdd { .. } => match state {
None => Some(TrackedState::Added),
Some(_existing_state) => unreachable!("{hunk:#?}"),
},
BaseHunk::Edit { .. } | BaseHunk::Replacement { .. } => match state {
Some(TrackedState::Modified) => Some(TrackedState::Modified),
Some(TrackedState::Moved | TrackedState::ModifiedAndMoved) => {
Some(TrackedState::ModifiedAndMoved)
}
None => Some(TrackedState::Modified),
Some(_) => unreachable!("{hunk:#?}"),
},
_ => state,
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum PathState {
Untracked,
Tracked(TrackedState),
}
pub struct PathStates {
states: GenericPatriciaMap<String, PathState>,
}
impl PathStates {
pub fn new<C>(
root: &Utf8Path,
transaction: &ArcTxn<MutTxn<()>>,
channel: &ChannelRef<MutTxn<()>>,
file_system: &libpijul::working_copy::FileSystem,
change_store: &C,
) -> Result<Self, PathStatesError<C::Error, std::io::Error>>
where
C: ChangeStore + Clone + Send + 'static,
{
let mut path_states = Self::from_untracked_states(root, file_system, transaction)?;
path_states.update_tracked_states("", transaction, channel, file_system, change_store)?;
Ok(path_states)
}
fn from_untracked_states(
root: &Utf8Path,
file_system: &libpijul::working_copy::FileSystem,
transaction: &ArcTxn<MutTxn<()>>,
) -> Result<Self, CreatePathStatesError> {
let canonical_path = CanonicalPathBuf::canonicalize(root).map_err(|io_error| {
CreatePathStatesError::CanonicalRoot {
root_path: root.to_path_buf(),
io_error,
}
})?;
let file_system_iterator = file_system
.iterate_prefix_rec(
canonical_path.clone(),
canonical_path,
false,
std::thread::available_parallelism()
.unwrap_or(NonZeroUsize::MIN)
.get(),
|_path, _is_directory| true,
)
.map_err(CreatePathStatesError::Iteration)?;
let mut untracked_states = GenericPatriciaMap::new();
let read_transaction = transaction.read();
for entry in file_system_iterator {
let (path, _is_directory) = match entry {
Ok((path, is_directory)) => (path, is_directory),
Err(error) => {
tracing::error!(message = "Error traversing file system", %error);
continue;
}
};
let utf8_path = match Utf8PathBuf::from_path_buf(path) {
Ok(utf8_path) => utf8_path,
Err(path) => {
tracing::error!(message = "Unable to convert PathBuf to Utf8PathBuf", ?path);
continue;
}
};
if !read_transaction
.is_tracked(utf8_path.as_str())
.map_err(CreatePathStatesError::IsTracked)?
{
untracked_states.insert(utf8_path.into_string(), PathState::Untracked);
}
}
Ok(Self {
states: untracked_states,
})
}
fn update_tracked_states<C, W>(
&mut self,
prefix: &str,
transaction: &ArcTxn<MutTxn<()>>,
channel: &ChannelRef<MutTxn<()>>,
working_copy: &W,
change_store: &C,
) -> Result<(), PathStatesError<C::Error, W::Error>>
where
C: ChangeStore + Clone + Send + 'static,
W: WorkingCopy + Clone + Send + Sync + 'static,
{
let mut unrecorded_changes = RecordBuilder::new();
unrecorded_changes.record(
transaction.clone(),
libpijul::Algorithm::default(),
false, &libpijul::DEFAULT_SEPARATOR,
channel.clone(),
working_copy,
change_store,
prefix,
1, )?;
let unrecorded_state = unrecorded_changes.finish();
let change_contents = unrecorded_state.contents.lock();
let new_inodes: HashMap<ChangePosition, Utf8PathBuf> = unrecorded_state
.actions
.iter()
.filter_map(|hunk| {
if let Hunk::FileAdd {
add_inode, path, ..
} = hunk
{
Some((add_inode.as_newvertex().start, Utf8PathBuf::from(path)))
} else {
None
}
})
.collect();
let mut updated_states = HashMap::new();
for hunk in unrecorded_state.actions {
let path = if let Hunk::FileMove { add, .. } = &hunk {
let add_vertex = add.as_newvertex();
let file_metadata = FileMetadata::read(
&change_contents[add_vertex.start.0.as_usize()..add_vertex.end.0.as_usize()],
);
let parent_inode = add_vertex.inode;
let current_parent_path = if let Some(change) = parent_inode.change {
let (path, _is_alive) = libpijul::fs::find_path(
change_store,
&*transaction.read(),
&*channel.read(),
true,
Position {
change,
pos: parent_inode.pos,
},
)?
.unwrap();
Utf8PathBuf::from(path)
} else {
new_inodes.get(&parent_inode.pos).unwrap().to_path_buf()
};
current_parent_path.join(file_metadata.basename).to_string()
} else {
let globalized_hunk = hunk.clone().globalize(&*transaction.read())?;
globalized_hunk.path().to_string()
};
let entry = updated_states.entry(path);
let existing_tracked_state = match &entry {
Entry::Occupied(occupied_entry) => match occupied_entry.get() {
PathState::Untracked => None,
PathState::Tracked(tracked_state) => Some(*tracked_state),
},
Entry::Vacant(_vacant_entry) => None,
};
if let Some(updated_state) = TrackedState::join_hunk(existing_tracked_state, &hunk) {
entry.insert_entry(PathState::Tracked(updated_state));
} else {
tracing::info!(message = "Skipping unrecorded hunk", ?hunk);
}
}
tracing::debug!(?updated_states);
let mut paths_to_remove = Vec::new();
for (path, existing_state) in self.states.iter_prefix_mut(prefix) {
match updated_states.remove(&path) {
Some(updated_state) => {
*existing_state = updated_state;
}
None => {
paths_to_remove.push(path);
}
}
}
let read_transaction = transaction.read();
for outdated_path in paths_to_remove {
if read_transaction.is_tracked(&outdated_path)? {
self.states.remove(outdated_path);
} else {
self.states.insert(outdated_path, PathState::Untracked);
}
}
for (path, updated_state) in updated_states {
self.states.insert(path, updated_state);
}
tracing::debug!(?self.states);
Ok(())
}
pub fn get_path_state(&self, path: &Utf8Path) -> Option<PathState> {
self.states.get(path.as_str()).copied()
}
pub fn iter_path_states(&self) -> impl Iterator<Item = (Utf8PathBuf, PathState)> {
self.states
.iter()
.map(|(path, state)| (Utf8PathBuf::from(path), *state))
}
pub fn update_path_state<C, W>(
&mut self,
path: Utf8PathBuf,
transaction: &ArcTxn<MutTxn<()>>,
channel: &ChannelRef<MutTxn<()>>,
working_copy: &W,
change_store: &C,
) -> Result<(), PathStatesError<C::Error, W::Error>>
where
C: ChangeStore + Clone + Send + 'static,
W: WorkingCopy + Clone + Send + Sync + 'static,
{
if transaction.read().is_tracked(path.as_str())? {
self.update_tracked_states(
path.as_str(),
transaction,
channel,
working_copy,
change_store,
)?;
} else {
self.states.insert(path.into_string(), PathState::Untracked);
}
Ok(())
}
}