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;
use libpijul::pristine::sanakirja::{MutTxn, SanakirjaError};
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<()>>),
}
#[derive(Clone, Copy, Debug)]
pub enum TrackedState {
Added,
Removed,
Modified,
Moved,
ModifiedAndMoved,
}
impl TrackedState {
pub fn join_hunk(
state: Option<Self>,
hunk: &Hunk<Option<libpijul::Hash>, libpijul::change::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) => 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 mut updated_states = HashMap::new();
for hunk in unrecorded_state.actions {
let globalized_hunk = hunk.globalize(&*transaction.read())?;
let entry = updated_states.entry(globalized_hunk.path().to_string());
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, &globalized_hunk)
{
entry.insert_entry(PathState::Tracked(updated_state));
} else {
tracing::info!(message = "Skipping unrecorded hunk", ?globalized_hunk);
}
}
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);
}
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(())
}
}