diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index e4e8d403f..39fab8903 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -41,7 +41,7 @@ use jj_lib::git_backend::GitBackend; use jj_lib::gitignore::GitIgnoreFile; use jj_lib::hex_util::to_reverse_hex; use jj_lib::id_prefix::IdPrefixContext; -use jj_lib::local_working_copy::{LocalWorkingCopy, LockedLocalWorkingCopy}; +use jj_lib::local_working_copy::LockedLocalWorkingCopy; use jj_lib::matchers::{EverythingMatcher, Matcher, PrefixMatcher, Visit}; use jj_lib::merged_tree::{MergedTree, MergedTreeBuilder}; use jj_lib::op_heads_store::{self, OpHeadResolutionError, OpHeadsStore}; @@ -853,7 +853,7 @@ impl WorkspaceCommandHelper { &self.user_repo.repo } - pub fn working_copy(&self) -> &LocalWorkingCopy { + pub fn working_copy(&self) -> &dyn WorkingCopy { self.workspace.working_copy() } diff --git a/cli/src/commands/debug.rs b/cli/src/commands/debug.rs index c0c5867b6..60860f79d 100644 --- a/cli/src/commands/debug.rs +++ b/cli/src/commands/debug.rs @@ -12,14 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::any::Any; use std::fmt::Debug; use std::io::Write as _; use clap::Subcommand; use jj_lib::backend::ObjectId; use jj_lib::default_index_store::{DefaultIndexStore, ReadonlyIndexWrapper}; +use jj_lib::local_working_copy::{LocalWorkingCopy, LockedLocalWorkingCopy}; use jj_lib::revset; -use jj_lib::working_copy::WorkingCopy; +use jj_lib::working_copy::{LockedWorkingCopy, WorkingCopy}; use crate::cli_util::{resolve_op_for_load, user_error, CommandError, CommandHelper}; use crate::template_parser; @@ -103,7 +105,7 @@ pub fn cmd_debug( DebugCommands::Revset(args) => cmd_debug_revset(ui, command, args)?, DebugCommands::WorkingCopy(_wc_matches) => { let workspace_command = command.workspace_helper(ui)?; - let wc = workspace_command.working_copy(); + let wc = check_local_disk_wc(workspace_command.working_copy().as_any())?; writeln!(ui.stdout(), "Current operation: {:?}", wc.operation_id())?; writeln!(ui.stdout(), "Current tree: {:?}", wc.tree_id()?)?; for (file, state) in wc.file_states()? { @@ -250,16 +252,25 @@ fn cmd_debug_watchman( let repo = workspace_command.repo().clone(); match subcommand { DebugWatchmanSubcommand::QueryClock => { - let (clock, _changed_files) = workspace_command.working_copy().query_watchman()?; + let wc = check_local_disk_wc(workspace_command.working_copy().as_any())?; + let (clock, _changed_files) = wc.query_watchman()?; writeln!(ui.stdout(), "Clock: {clock:?}")?; } DebugWatchmanSubcommand::QueryChangedFiles => { - let (_clock, changed_files) = workspace_command.working_copy().query_watchman()?; + let wc = check_local_disk_wc(workspace_command.working_copy().as_any())?; + let (_clock, changed_files) = wc.query_watchman()?; writeln!(ui.stdout(), "Changed files: {changed_files:?}")?; } DebugWatchmanSubcommand::ResetClock => { let (mut locked_ws, _commit) = workspace_command.start_working_copy_mutation()?; - locked_ws.locked_wc().reset_watchman()?; + let Some(locked_local_wc): Option<&mut LockedLocalWorkingCopy> = + locked_ws.locked_wc().as_any_mut().downcast_mut() + else { + return Err(user_error( + "This command requires a standard local-disk working copy", + )); + }; + locked_local_wc.reset_watchman()?; locked_ws.finish(repo.op_id().clone())?; writeln!(ui.stderr(), "Reset Watchman clock")?; } @@ -277,3 +288,8 @@ fn cmd_debug_watchman( "Cannot query Watchman because jj was not compiled with the `watchman` feature", )) } + +fn check_local_disk_wc(x: &dyn Any) -> Result<&LocalWorkingCopy, CommandError> { + x.downcast_ref() + .ok_or_else(|| user_error("This command requires a standard local-disk working copy")) +} diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 2a087e570..7f6a61d29 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -47,7 +47,7 @@ use jj_lib::revset_graph::{ }; use jj_lib::rewrite::{back_out_commit, merge_commit_trees, rebase_commit, DescendantRebaser}; use jj_lib::settings::UserSettings; -use jj_lib::working_copy::{LockedWorkingCopy, SnapshotOptions, WorkingCopy}; +use jj_lib::working_copy::{LockedWorkingCopy, SnapshotOptions}; use jj_lib::workspace::Workspace; use jj_lib::{conflicts, file_util, revset}; use maplit::{hashmap, hashset}; diff --git a/lib/src/local_working_copy.rs b/lib/src/local_working_copy.rs index 7d1e63a67..f86abd081 100644 --- a/lib/src/local_working_copy.rs +++ b/lib/src/local_working_copy.rs @@ -1490,6 +1490,10 @@ impl LockedWorkingCopy for LockedLocalWorkingCopy { self } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn old_operation_id(&self) -> &OperationId { &self.old_operation_id } @@ -1563,7 +1567,7 @@ impl LockedWorkingCopy for LockedLocalWorkingCopy { fn finish( mut self, operation_id: OperationId, - ) -> Result { + ) -> Result, WorkingCopyStateError> { assert!(self.tree_state_dirty || &self.old_tree_id == self.wc.tree_id()?); if self.tree_state_dirty { self.wc @@ -1579,7 +1583,7 @@ impl LockedWorkingCopy for LockedLocalWorkingCopy { self.wc.save(); } // TODO: Clear the "pending_checkout" file here. - Ok(self.wc) + Ok(Box::new(self.wc)) } } diff --git a/lib/src/working_copy.rs b/lib/src/working_copy.rs index 0447daa99..8407ba0d8 100644 --- a/lib/src/working_copy.rs +++ b/lib/src/working_copy.rs @@ -25,7 +25,7 @@ use thiserror::Error; use crate::backend::{BackendError, MergedTreeId}; use crate::fsmonitor::FsmonitorKind; use crate::gitignore::GitIgnoreFile; -use crate::local_working_copy::{LocalWorkingCopy, LockedLocalWorkingCopy}; +use crate::local_working_copy::LockedLocalWorkingCopy; use crate::merged_tree::MergedTree; use crate::op_store::{OperationId, WorkspaceId}; use crate::repo_path::RepoPath; @@ -69,6 +69,9 @@ pub trait LockedWorkingCopy { /// Should return `self`. For down-casting purposes. fn as_any(&self) -> &dyn Any; + /// Should return `self`. For down-casting purposes. + fn as_any_mut(&mut self) -> &mut dyn Any; + /// The operation at the time the lock was taken fn old_operation_id(&self) -> &OperationId; @@ -101,8 +104,10 @@ pub trait LockedWorkingCopy { /// Finish the modifications to the working copy by writing the updated /// states to disk. Returns the new (unlocked) working copy. - // TODO: return a `Box` instead - fn finish(self, operation_id: OperationId) -> Result; + fn finish( + self, + operation_id: OperationId, + ) -> Result, WorkingCopyStateError>; } /// An error while snapshotting the working copy. diff --git a/lib/src/workspace.rs b/lib/src/workspace.rs index 4dbf634dc..9460c9a33 100644 --- a/lib/src/workspace.rs +++ b/lib/src/workspace.rs @@ -78,7 +78,7 @@ pub struct Workspace { // working copy files live. workspace_root: PathBuf, repo_loader: RepoLoader, - working_copy: LocalWorkingCopy, + working_copy: Box, } fn create_jj_dir(workspace_root: &Path) -> Result { @@ -98,7 +98,7 @@ fn init_working_copy( workspace_root: &Path, jj_dir: &Path, workspace_id: WorkspaceId, -) -> Result<(LocalWorkingCopy, Arc), WorkspaceInitError> { +) -> Result<(Box, Arc), WorkspaceInitError> { let working_copy_state_path = jj_dir.join("working_copy"); std::fs::create_dir(&working_copy_state_path).context(&working_copy_state_path)?; @@ -120,13 +120,13 @@ fn init_working_copy( repo.op_id().clone(), workspace_id, )?; - Ok((working_copy, repo)) + Ok((Box::new(working_copy), repo)) } impl Workspace { fn new( workspace_root: &Path, - working_copy: LocalWorkingCopy, + working_copy: Box, repo_loader: RepoLoader, ) -> Result { let workspace_root = workspace_root.canonicalize().context(workspace_root)?; @@ -295,8 +295,8 @@ impl Workspace { &self.repo_loader } - pub fn working_copy(&self) -> &LocalWorkingCopy { - &self.working_copy + pub fn working_copy(&self) -> &dyn WorkingCopy { + self.working_copy.as_ref() } pub fn start_working_copy_mutation( @@ -415,7 +415,7 @@ impl WorkspaceLoader { self.workspace_root.clone(), self.working_copy_state_path.clone(), ); - let workspace = Workspace::new(&self.workspace_root, working_copy, repo_loader)?; + let workspace = Workspace::new(&self.workspace_root, Box::new(working_copy), repo_loader)?; Ok(workspace) } } diff --git a/lib/tests/test_local_working_copy.rs b/lib/tests/test_local_working_copy.rs index 63d149cd3..07548a6b0 100644 --- a/lib/tests/test_local_working_copy.rs +++ b/lib/tests/test_local_working_copy.rs @@ -34,9 +34,7 @@ use jj_lib::op_store::{OperationId, WorkspaceId}; use jj_lib::repo::{ReadonlyRepo, Repo}; use jj_lib::repo_path::{RepoPath, RepoPathComponent, RepoPathJoin}; use jj_lib::settings::UserSettings; -use jj_lib::working_copy::{ - CheckoutStats, LockedWorkingCopy, SnapshotError, SnapshotOptions, WorkingCopy, -}; +use jj_lib::working_copy::{CheckoutStats, LockedWorkingCopy, SnapshotError, SnapshotOptions}; use jj_lib::workspace::LockedWorkspace; use test_case::test_case; use testutils::{create_tree, write_random_commit, TestRepoBackend, TestWorkspace}; @@ -417,7 +415,7 @@ fn test_reset() { // Test the setup: the file should exist on disk and in the tree state. assert!(ignored_path.to_fs_path(&workspace_root).is_file()); - let wc = ws.working_copy(); + let wc: &LocalWorkingCopy = ws.working_copy().as_any().downcast_ref().unwrap(); assert!(wc.file_states().unwrap().contains_key(&ignored_path)); // After we reset to the commit without the file, it should still exist on disk, @@ -427,7 +425,7 @@ fn test_reset() { locked_ws.locked_wc().reset(&tree_without_file).unwrap(); locked_ws.finish(op_id.clone()).unwrap(); assert!(ignored_path.to_fs_path(&workspace_root).is_file()); - let wc = ws.working_copy(); + let wc: &LocalWorkingCopy = ws.working_copy().as_any().downcast_ref().unwrap(); assert!(!wc.file_states().unwrap().contains_key(&ignored_path)); let new_tree = test_workspace.snapshot().unwrap(); assert_eq!(new_tree.id(), tree_without_file.id()); @@ -439,7 +437,7 @@ fn test_reset() { locked_ws.locked_wc().reset(&tree_with_file).unwrap(); locked_ws.finish(op_id.clone()).unwrap(); assert!(ignored_path.to_fs_path(&workspace_root).is_file()); - let wc = ws.working_copy(); + let wc: &LocalWorkingCopy = ws.working_copy().as_any().downcast_ref().unwrap(); assert!(wc.file_states().unwrap().contains_key(&ignored_path)); let new_tree = test_workspace.snapshot().unwrap(); assert_eq!(new_tree.id(), tree_with_file.id()); @@ -464,11 +462,12 @@ fn test_checkout_discard() { let ws = &mut test_workspace.workspace; ws.check_out(repo.op_id().clone(), None, &tree1).unwrap(); - let state_path = ws.working_copy().state_path().to_path_buf(); + let wc: &LocalWorkingCopy = ws.working_copy().as_any().downcast_ref().unwrap(); + let state_path = wc.state_path().to_path_buf(); // Test the setup: the file should exist on disk and in the tree state. assert!(file1_path.to_fs_path(&workspace_root).is_file()); - let wc = ws.working_copy(); + let wc: &LocalWorkingCopy = ws.working_copy().as_any().downcast_ref().unwrap(); assert!(wc.file_states().unwrap().contains_key(&file1_path)); // Start a checkout @@ -484,7 +483,7 @@ fn test_checkout_discard() { drop(locked_ws); // The change should remain in the working copy, but not in memory and not saved - let wc = ws.working_copy(); + let wc: &LocalWorkingCopy = ws.working_copy().as_any().downcast_ref().unwrap(); assert!(wc.file_states().unwrap().contains_key(&file1_path)); assert!(!wc.file_states().unwrap().contains_key(&file2_path)); assert!(!file1_path.to_fs_path(&workspace_root).is_file()); @@ -538,6 +537,7 @@ fn test_snapshot_special_file() { let mut test_workspace = TestWorkspace::init(&settings); let workspace_root = test_workspace.workspace.workspace_root().clone(); let store = test_workspace.repo.store(); + let ws = &mut test_workspace.workspace; let file1_path = RepoPath::from_internal_string("file1"); let file1_disk_path = file1_path.to_fs_path(&workspace_root); @@ -552,10 +552,7 @@ fn test_snapshot_special_file() { assert!(!socket_disk_path.is_file()); // Snapshot the working copy with the socket file - let mut locked_ws = test_workspace - .workspace - .start_working_copy_mutation() - .unwrap(); + let mut locked_ws = ws.start_working_copy_mutation().unwrap(); let tree_id = locked_ws .locked_wc() .snapshot(SnapshotOptions::empty_for_test()) @@ -567,7 +564,7 @@ fn test_snapshot_special_file() { tree.entries().map(|(path, _value)| path).collect_vec(), vec![file1_path.clone(), file2_path.clone()] ); - let wc = test_workspace.workspace.working_copy(); + let wc: &LocalWorkingCopy = ws.working_copy().as_any().downcast_ref().unwrap(); assert_eq!( wc.file_states().unwrap().keys().cloned().collect_vec(), vec![file1_path, file2_path.clone()] @@ -582,7 +579,8 @@ fn test_snapshot_special_file() { tree.entries().map(|(path, _value)| path).collect_vec(), vec![file2_path.clone()] ); - let wc = test_workspace.workspace.working_copy(); + let ws = &mut test_workspace.workspace; + let wc: &LocalWorkingCopy = ws.working_copy().as_any().downcast_ref().unwrap(); assert_eq!( wc.file_states().unwrap().keys().cloned().collect_vec(), vec![file2_path] diff --git a/lib/tests/test_local_working_copy_concurrent.rs b/lib/tests/test_local_working_copy_concurrent.rs index 87398cb15..ac65f0dfd 100644 --- a/lib/tests/test_local_working_copy_concurrent.rs +++ b/lib/tests/test_local_working_copy_concurrent.rs @@ -18,7 +18,7 @@ use std::thread; use assert_matches::assert_matches; use jj_lib::repo::Repo; use jj_lib::repo_path::RepoPath; -use jj_lib::working_copy::{CheckoutError, LockedWorkingCopy, SnapshotOptions, WorkingCopy}; +use jj_lib::working_copy::{CheckoutError, LockedWorkingCopy, SnapshotOptions}; use jj_lib::workspace::Workspace; use testutils::{create_tree, write_working_copy_file, TestRepo, TestWorkspace}; diff --git a/lib/tests/test_local_working_copy_sparse.rs b/lib/tests/test_local_working_copy_sparse.rs index bab907e0a..ac7c08676 100644 --- a/lib/tests/test_local_working_copy_sparse.rs +++ b/lib/tests/test_local_working_copy_sparse.rs @@ -86,7 +86,7 @@ fn test_sparse_checkout() { // Write the new state to disk locked_ws.finish(repo.op_id().clone()).unwrap(); - let wc = ws.working_copy(); + let wc: &LocalWorkingCopy = ws.working_copy().as_any().downcast_ref().unwrap(); assert_eq!( wc.file_states().unwrap().keys().collect_vec(), vec![&dir1_file1_path, &dir1_file2_path, &dir1_subdir1_file1_path] @@ -130,6 +130,7 @@ fn test_sparse_checkout() { .exists()); assert!(dir2_file1_path.to_fs_path(&working_copy_path).exists()); let wc = locked_wc.finish(repo.op_id().clone()).unwrap(); + let wc: &LocalWorkingCopy = wc.as_any().downcast_ref().unwrap(); assert_eq!( wc.file_states().unwrap().keys().collect_vec(), vec![&dir1_subdir1_file1_path, &dir2_file1_path, &root_file1_path]