This enables extensions to keep track of modified files as they change over time, and should make it easier to eventually switch over to the message-based model in the future.
LQJG2LGQMNJ35Y5H6XWCU3JWVIMOS5S7UQQQPIO5TCAXMLU3ORUQC // TODO: rewrite to be generic over filesystem implementationsuse std::collections::HashSet;use camino::{Utf8Path, Utf8PathBuf};use canonical_path::CanonicalPathBuf;use libpijul::pristine::sanakirja::{MutTxn, SanakirjaError};use libpijul::{ArcTxn, TxnTExt};#[derive(Debug, thiserror::Error)]pub enum UntrackedItemError {#[error("unable to get untracked path from filesystem: {0:#?}")]IO(std::io::Error),#[error("unable to check if path is tracked: {0:#?}")]IsTracked(SanakirjaError),#[error("unable to convert `{invalid_path:#?}` to UTF-8: {conversion_error:#?}")]Utf8Path {invalid_path: std::path::PathBuf,conversion_error: camino::FromPathBufError,},}#[derive(Debug, thiserror::Error)]#[error(transparent)]pub enum UntrackedError {#[error("unable to get available parallelism: {0:#?}")]AvailableParallelism(std::io::Error),#[error("unable to canonicalize root path `{root_path}`: {io_error:#?}")]CanonicalRoot {root_path: Utf8PathBuf,io_error: std::io::Error,},#[error("unable to iterate untracked files: {0:#?}")]Iteration(std::io::Error),#[error(transparent)]IterationItem(#[from] UntrackedItemError),}pub fn file_system(root: &Utf8Path,file_system: &libpijul::working_copy::FileSystem,transaction: ArcTxn<MutTxn<()>>,) -> Result<HashSet<Utf8PathBuf>, UntrackedError> {let untracked_items_transaction = transaction.clone();let canonical_path =CanonicalPathBuf::canonicalize(root).map_err(|io_error| UntrackedError::CanonicalRoot {root_path: root.to_path_buf(),io_error,})?;let untracked_files: HashSet<Utf8PathBuf> = file_system.iterate_prefix_rec(canonical_path.clone(),canonical_path,false,std::thread::available_parallelism().map_err(UntrackedError::AvailableParallelism)?.get(),// Follow all paths|_path, _is_directory| true,).map_err(UntrackedError::Iteration)?// Handle any errors in iteration.map(|filesystem_result| match filesystem_result {Ok((path, _is_directory)) => {Utf8PathBuf::try_from(path.clone()).map_err(|conversion_error| {UntrackedItemError::Utf8Path {invalid_path: path,conversion_error,}})}Err(io_error) => Err(UntrackedItemError::IO(io_error)),})// Filter out tracked paths.filter_map(|path_result| match path_result {Ok(path) => match untracked_items_transaction.read().is_tracked(path.as_str()) {Ok(true) => None,Ok(false) => Some(Ok(path)),Err(error) => Some(Err(UntrackedItemError::IsTracked(error))),},Err(error) => Some(Err(error)),}).collect::<Result<_, UntrackedItemError>>()?;Ok(untracked_files)}
use crate::TrackedState;
#[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),}
pub enum ModifiedPathsError<C: std::error::Error + 'static, W: std::error::Error> {Globalize(#[from] SanakirjaError),
pub enum PathStatesError<C: std::error::Error + 'static, W: std::error::Error> {Create(#[from] CreatePathStatesError),Sanakirja(#[from] SanakirjaError),
}#[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:#?}"),},// TODO: FileUndel// TODO: conflicts_ => state,}}
pub fn modified_paths<C, W>(transaction: &ArcTxn<MutTxn<()>>,channel: &ChannelRef<MutTxn<()>>,working_copy: &W,change_store: &C,) -> Result<HashMap<Utf8PathBuf, TrackedState>, ModifiedPathsError<C::Error, W::Error>>whereC: ChangeStore + Clone + Send + 'static,W: WorkingCopy + Clone + Send + Sync + 'static,{let mut unrecorded_changes = RecordBuilder::new();
#[derive(Clone, Copy, Debug)]pub enum PathState {Untracked,Tracked(TrackedState),}
unrecorded_changes.record(transaction.clone(),libpijul::Algorithm::default(),false, // TODO: check and document&libpijul::DEFAULT_SEPARATOR,channel.clone(),working_copy,change_store,"",1, // TODO: figure out concurrency model)?;
pub struct PathStates {// TODO: use a triepub states: HashMap<Utf8PathBuf, 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>>whereC: 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)?;
for hunk in unrecorded_state.actions {match hunk {BaseHunk::FileMove { path, .. } => {modified_paths.entry(Utf8PathBuf::from(path)).and_modify(|path_state| match path_state {TrackedState::Modified => *path_state = TrackedState::ModifiedAndMoved,_ => unreachable!("{path_state:#?}"),}).or_insert(TrackedState::Moved);
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,
BaseHunk::FileDel { path, .. } => {modified_paths.try_insert(Utf8PathBuf::from(path), TrackedState::Removed).unwrap();
})?;let file_system_iterator = file_system.iterate_prefix_rec(canonical_path.clone(),canonical_path,false,std::thread::available_parallelism().unwrap_or(NonZeroUsize::MIN).get(),// Follow all paths|_path, _is_directory| true,).map_err(CreatePathStatesError::Iteration)?;let mut untracked_states = HashMap::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, PathState::Untracked);
BaseHunk::FileAdd { path, .. } => {modified_paths.try_insert(Utf8PathBuf::from(path), TrackedState::Added).unwrap();}BaseHunk::Edit { local, .. } | BaseHunk::Replacement { local, .. } => {modified_paths.entry(Utf8PathBuf::from(local.path)).and_modify(|path_state| match path_state {TrackedState::Modified => (),TrackedState::Moved => *path_state = TrackedState::ModifiedAndMoved,_ => unreachable!("{path_state:#?}"),}).or_insert(TrackedState::Modified);
}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>>whereC: 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, // TODO: check and document&libpijul::DEFAULT_SEPARATOR,channel.clone(),working_copy,change_store,prefix,1, // TODO: figure out concurrency model)?;let unrecorded_state = unrecorded_changes.finish();for hunk in unrecorded_state.actions {let globalized_hunk = hunk.globalize(&*transaction.read())?;let entry = self.states.entry(Utf8PathBuf::from(globalized_hunk.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, &globalized_hunk){entry.insert_entry(PathState::Tracked(updated_state));} else {tracing::info!(message = "Skipping unrecorded hunk", ?globalized_hunk);
Ok(modified_paths)
pub fn get_path_state(&self, path: &Utf8Path) -> Option<PathState> {self.states.get(path).copied()}// TODO: handle transitions:// - Added -> unrecorded// - Modified -> unmodifiedpub 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>>whereC: 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, PathState::Untracked);}Ok(())}
}#[derive(Debug, thiserror::Error)]pub enum UpdatePathStateError<C: std::error::Error + 'static, W: std::error::Error + 'static> {#[error("Unable to begin transaction: {0}")]BeginTransaction(#[from] BeginTransactionError),#[error(transparent)]PathStates(#[from] PathStatesError<C, W>),
let untracked =untracked::file_system(root, &working_copy.working_copy, transaction.clone())?;let modified_paths = modified_paths::modified_paths(&transaction,&channel,&working_copy.working_copy,&change_store,)?;
let path_states =PathStates::new(root, &transaction, &channel, &file_system, &change_store)?;let working_copy = EditorWorkingCopy::new(file_system);
pub fn iter_modified(&self) -> impl Iterator<Item = &Utf8PathBuf> {self.modified_paths.keys()}
pub fn update_path_state(&mut self,path: Utf8PathBuf,) -> Result<(), UpdatePathStateError<C::Error, W::Error>> {let (transaction, channel) = begin_transaction(&self.pristine)?;self.path_states.update_path_state(path,&transaction,&channel,&self.working_copy.working_copy,&self.change_store,)?;
let mut unrecorded_changes = source_control.create_resource_group("changes", "Changes")?;let mut untracked_paths = source_control.create_resource_group("untracked", "Untracked")?;let mut modified_resource_states = Vec::new();for relative_modified_path in repository.iter_modified() {let absolute_modified_path = repository_path.join(relative_modified_path);let resource_uri = Uri::file(env, absolute_modified_path.as_str())?;let resource_state = SourceControlResourceState::new(env, &resource_uri)?;modified_resource_states.push(resource_state)}unrecorded_changes.set_resource_states(modified_resource_states)?;
let unrecorded_changes = source_control.create_resource_group("changes", "Changes")?;let untracked_paths = source_control.create_resource_group("untracked", "Untracked")?;
let mut untracked_resource_states = Vec::new();for relative_untracked_path in repository.iter_untracked() {let absolute_untracked_path = repository_path.join(relative_untracked_path);let resource_uri = Uri::file(env, absolute_untracked_path.as_str())?;let resource_state = SourceControlResourceState::new(env, &resource_uri)?;untracked_resource_states.push(resource_state)}untracked_paths.set_resource_states(untracked_resource_states)?;Ok(Self {
let open_repository = Self {
#[tracing::instrument(skip_all)]pub fn update_resource_states(&self,env: &napi::Env,// TODO: ideally caller wouldn't have to provide repository pathrepository_path: &Utf8Path,) -> Result<(), napi::Error> {let mut modified_resource_states = Vec::new();let mut untracked_resource_states = Vec::new();for (relative_path, path_state) in self.repository.iter_path_states() {let absolute_path = repository_path.join(relative_path);let resource_uri = Uri::file(env, absolute_path.as_str())?;let resource_state = SourceControlResourceState::new(env, &resource_uri)?;match path_state {PathState::Untracked => untracked_resource_states.push(resource_state),PathState::Tracked(_tracked_state) => modified_resource_states.push(resource_state),}}self.unrecorded_changes.get_inner(env)?.set_resource_states(modified_resource_states)?;self.untracked_paths.get_inner(env)?.set_resource_states(untracked_resource_states)?;Ok(())}