Refactor `PathState` handling to be updated incrementally
Dependencies
- [2]
WFWTKCJNCreate initial Visual Studio Code extension - [3]
TIPGJCUWConsistently use `EditorWorkingCopy` in `pijul-extension` - [4]
DUZBRB3URender correspoding hunk on inline credit hover - [5]
37JT3GCXLazily load channel state instead of storing in memory - [6]
GQ52YLWHRemove unnecessary debug logs - [7]
72K45XKDRefactor inline credit to improve hover messages
Change contents
- file deletion: untracked.rs
// 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)} - file move: modified_paths.rs → path_state.rs
- edit in pijul-extension/src/path_state.rs at line 2
use std::collections::hash_map::Entry;use std::num::NonZeroUsize; - replacement in pijul-extension/src/path_state.rs at line 5
use camino::Utf8PathBuf;use libpijul::change::BaseHunk;use camino::{Utf8Path, Utf8PathBuf};use canonical_path::CanonicalPathBuf;use libpijul::change::{BaseHunk, Hunk}; - replacement in pijul-extension/src/path_state.rs at line 11
use libpijul::{ArcTxn, ChannelRef, RecordBuilder};use libpijul::{ArcTxn, ChannelRef, RecordBuilder, TxnTExt}; - replacement in pijul-extension/src/path_state.rs at line 13
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),} - replacement in pijul-extension/src/path_state.rs at line 28
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), - edit in pijul-extension/src/path_state.rs at line 32
}#[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,}} - replacement in pijul-extension/src/path_state.rs at line 75
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),} - replacement in pijul-extension/src/path_state.rs at line 81
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)?; - replacement in pijul-extension/src/path_state.rs at line 100
let unrecorded_state = unrecorded_changes.finish();let mut modified_paths = HashMap::new();Ok(path_states)} - replacement in pijul-extension/src/path_state.rs at line 103
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, - replacement in pijul-extension/src/path_state.rs at line 113
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); - replacement in pijul-extension/src/path_state.rs at line 154
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); - edit in pijul-extension/src/path_state.rs at line 206
// TODO: FileUndel// TODO: conflicts_ => continue, - edit in pijul-extension/src/path_state.rs at line 207
Ok(()) - replacement in pijul-extension/src/path_state.rs at line 211
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(())} - edit in pijul-extension/src/lib.rs at line 15
use std::collections::{HashMap, HashSet}; - replacement in pijul-extension/src/lib.rs at line 23
use libpijul::{ArcTxn, Base32, ChangeId, ChannelRef, GraphTxnT, TxnT, Vertex};use libpijul::{ArcTxn, ChangeId, ChannelRef, GraphTxnT, TxnT, Vertex}; - replacement in pijul-extension/src/lib.rs at line 35
use crate::modified_paths::ModifiedPathsError;use crate::path_state::{PathState, PathStates, PathStatesError}; - replacement in pijul-extension/src/lib.rs at line 39
pub mod modified_paths;pub mod untracked;pub mod path_state; - edit in pijul-extension/src/lib.rs at line 48
Untracked(#[from] untracked::UntrackedError),#[error("failed to get local authors: {0:#?}")] - replacement in pijul-extension/src/lib.rs at line 49
#[error("unable to get modified paths: {0:#?}")]Modified(#[from] ModifiedPathsError<C, W>),#[error("unable to get path states: {0:#?}")]PathStates(#[from] PathStatesError<C, W>), - edit in pijul-extension/src/lib.rs at line 90
}#[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>), - edit in pijul-extension/src/lib.rs at line 143
}#[derive(Clone, Copy, Debug)]pub enum TrackedState {Added,Removed,Modified,Moved,ModifiedAndMoved,}#[derive(Debug)]pub enum PathState {Untracked,Tracked(TrackedState), - replacement in pijul-extension/src/lib.rs at line 155
// TODO: use a triepub untracked: HashSet<Utf8PathBuf>,pub modified_paths: HashMap<Utf8PathBuf, TrackedState>,pub path_states: PathStates, - edit in pijul-extension/src/lib.rs at line 173
let working_copy = EditorWorkingCopy::new(file_system); - replacement in pijul-extension/src/lib.rs at line 175
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); - replacement in pijul-extension/src/lib.rs at line 183
untracked,modified_paths,path_states, - replacement in pijul-extension/src/lib.rs at line 373
if self.untracked.contains(path) {Some(PathState::Untracked)} else {self.modified_paths.get(path).map(|path_state| PathState::Tracked(*path_state))}self.path_states.get_path_state(path) - replacement in pijul-extension/src/lib.rs at line 376
pub fn is_untracked(&self, path: &Utf8Path) -> bool {self.untracked.contains(path)pub fn iter_path_states(&self) -> impl Iterator<Item = (&Utf8Path, PathState)> {self.path_states.states.iter().map(|(path, state)| (path.as_path(), *state)) - replacement in pijul-extension/src/lib.rs at line 383
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,)?; - replacement in pijul-extension/src/lib.rs at line 397
pub fn iter_untracked(&self) -> impl Iterator<Item = &Utf8PathBuf> {self.untracked.iter()Ok(()) - replacement in pijul-extension/src/lib.rs at line 435
// Since `FileCredits::new()` reads the file state from the working copy,// Since `FileCredits::new()` reads the file contents from the working copy, - replacement in pijul-extension/src/lib.rs at line 463
// Since the credits depend on file state already stored in the working copy,// Since the credits depend on file contents already stored in the working copy, - edit in pijul-extension/src/file_system/changes/unrecorded.rs at line 64
debug_assert_eq!(globalized_hunk.path(), path.as_str()); - edit in extensions/vscode/src/repository/open_repository.rs at line 5
use pijul_extension::path_state::PathState; - replacement in extensions/vscode/src/repository/open_repository.rs at line 44[6.28]→[2.100529:100726](∅→∅),[2.100529]→[2.100529:100726](∅→∅),[2.100771]→[2.100771:100827](∅→∅),[2.100869]→[2.100869:101335](∅→∅)
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")?; - replacement in extensions/vscode/src/repository/open_repository.rs at line 47[2.101336]→[2.101336:101550](∅→∅),[2.101620]→[2.101620:101790](∅→∅),[2.101791]→[2.101791:101934](∅→∅),[2.101979]→[2.101979:101997](∅→∅)
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 { - replacement in extensions/vscode/src/repository/open_repository.rs at line 53
})};open_repository.update_resource_states(env, &repository_path)?;Ok(open_repository) - edit in extensions/vscode/src/repository/open_repository.rs at line 96
#[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(())} - replacement in extensions/vscode/src/lib.rs at line 21
use pijul_extension::{PathState, TrackedState};use pijul_extension::path_state::{PathState, TrackedState};