diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index c73c91ca1..c61bf4caa 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -42,7 +42,7 @@ 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::{ - CheckoutStats, LocalWorkingCopy, LockedLocalWorkingCopy, ResetError, WorkingCopyStateError, + LocalWorkingCopy, LockedLocalWorkingCopy, ResetError, WorkingCopyStateError, }; use jj_lib::matchers::{EverythingMatcher, Matcher, PrefixMatcher, Visit}; use jj_lib::merged_tree::{MergedTree, MergedTreeBuilder}; @@ -62,7 +62,9 @@ use jj_lib::revset::{ use jj_lib::settings::{ConfigResultExt as _, UserSettings}; use jj_lib::transaction::Transaction; use jj_lib::tree::TreeMergeError; -use jj_lib::working_copy::{LockedWorkingCopy, SnapshotError, SnapshotOptions, WorkingCopy}; +use jj_lib::working_copy::{ + CheckoutStats, LockedWorkingCopy, SnapshotError, SnapshotOptions, WorkingCopy, +}; use jj_lib::workspace::{ LockedWorkspace, Workspace, WorkspaceInitError, WorkspaceLoadError, WorkspaceLoader, }; diff --git a/cli/src/merge_tools/external.rs b/cli/src/merge_tools/external.rs index e25878e6a..a4ee7e88a 100644 --- a/cli/src/merge_tools/external.rs +++ b/cli/src/merge_tools/external.rs @@ -10,14 +10,14 @@ use itertools::Itertools; use jj_lib::backend::{FileId, MergedTreeId, TreeValue}; use jj_lib::conflicts::{self, materialize_merge_result}; use jj_lib::gitignore::GitIgnoreFile; -use jj_lib::local_working_copy::{CheckoutError, TreeState, TreeStateError}; +use jj_lib::local_working_copy::{TreeState, TreeStateError}; use jj_lib::matchers::Matcher; use jj_lib::merge::Merge; use jj_lib::merged_tree::{MergedTree, MergedTreeBuilder}; use jj_lib::repo_path::RepoPath; use jj_lib::settings::UserSettings; use jj_lib::store::Store; -use jj_lib::working_copy::SnapshotOptions; +use jj_lib::working_copy::{CheckoutError, SnapshotOptions}; use regex::{Captures, Regex}; use tempfile::TempDir; use thiserror::Error; diff --git a/lib/src/local_working_copy.rs b/lib/src/local_working_copy.rs index b7c906a70..bc60f5811 100644 --- a/lib/src/local_working_copy.rs +++ b/lib/src/local_working_copy.rs @@ -59,7 +59,8 @@ use crate::settings::HumanByteSize; use crate::store::Store; use crate::tree::Tree; use crate::working_copy::{ - LockedWorkingCopy, SnapshotError, SnapshotOptions, SnapshotProgress, WorkingCopy, + CheckoutError, CheckoutStats, LockedWorkingCopy, SnapshotError, SnapshotOptions, + SnapshotProgress, WorkingCopy, }; #[cfg(unix)] @@ -302,45 +303,6 @@ fn file_state(metadata: &Metadata) -> Option { }) } -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct CheckoutStats { - pub updated_files: u32, - pub added_files: u32, - pub removed_files: u32, - pub skipped_files: u32, -} - -#[derive(Debug, Error)] -pub enum CheckoutError { - // The current working-copy commit was deleted, maybe by an overly aggressive GC that happened - // while the current process was running. - #[error("Current working-copy commit not found: {source}")] - SourceNotFound { - source: Box, - }, - // Another process checked out a commit while the current process was running (after the - // working copy was read by the current process). - #[error("Concurrent checkout")] - ConcurrentCheckout, - #[error("Internal backend error: {0}")] - InternalBackendError(#[from] BackendError), - #[error("{message}: {err:?}")] - Other { - message: String, - #[source] - err: Box, - }, -} - -impl CheckoutError { - fn for_stat_error(err: std::io::Error, path: &Path) -> Self { - CheckoutError::Other { - message: format!("Failed to stat file {}", path.display()), - err: err.into(), - } - } -} - struct FsmonitorMatcher { matcher: Option>, watchman_clock: Option, @@ -1068,7 +1030,7 @@ impl TreeState { // mtime is set at write time and won't change when we close the file.) let metadata = file .metadata() - .map_err(|err| CheckoutError::for_stat_error(err, disk_path))?; + .map_err(|err| checkout_error_for_stat_error(err, disk_path))?; Ok(FileState::for_file(executable, size, &metadata)) } @@ -1098,7 +1060,7 @@ impl TreeState { } let metadata = disk_path .symlink_metadata() - .map_err(|err| CheckoutError::for_stat_error(err, disk_path))?; + .map_err(|err| checkout_error_for_stat_error(err, disk_path))?; Ok(FileState::for_symlink(&metadata)) } @@ -1129,7 +1091,7 @@ impl TreeState { // Windows like we do with the executable bit for regular files. let metadata = file .metadata() - .map_err(|err| CheckoutError::for_stat_error(err, disk_path))?; + .map_err(|err| checkout_error_for_stat_error(err, disk_path))?; Ok(FileState::for_file(false, size, &metadata)) } @@ -1139,7 +1101,7 @@ impl TreeState { { let mode = if executable { 0o755 } else { 0o644 }; fs::set_permissions(disk_path, fs::Permissions::from_mode(mode)) - .map_err(|err| CheckoutError::for_stat_error(err, disk_path))?; + .map_err(|err| checkout_error_for_stat_error(err, disk_path))?; } Ok(()) } @@ -1317,6 +1279,13 @@ impl TreeState { } } +fn checkout_error_for_stat_error(err: std::io::Error, path: &Path) -> CheckoutError { + CheckoutError::Other { + message: format!("Failed to stat file {}", path.display()), + err: err.into(), + } +} + #[derive(Debug, Error)] #[error("{message}: {err:?}")] pub struct WorkingCopyStateError { @@ -1565,6 +1534,21 @@ impl LockedWorkingCopy for LockedLocalWorkingCopy { self.tree_state_dirty |= tree_state.snapshot(options)?; Ok(tree_state.current_tree_id().clone()) } + + fn check_out(&mut self, new_tree: &MergedTree) -> Result { + // TODO: Write a "pending_checkout" file with the new TreeId so we can + // continue an interrupted update if we find such a file. + let stats = self + .wc + .tree_state_mut() + .map_err(|err| CheckoutError::Other { + message: "Failed to load the working copy state".to_string(), + err: err.into(), + })? + .check_out(new_tree)?; + self.tree_state_dirty = true; + Ok(stats) + } } impl LockedLocalWorkingCopy { @@ -1580,21 +1564,6 @@ impl LockedLocalWorkingCopy { Ok(()) } - pub fn check_out(&mut self, new_tree: &MergedTree) -> Result { - // TODO: Write a "pending_checkout" file with the new TreeId so we can - // continue an interrupted update if we find such a file. - let stats = self - .wc - .tree_state_mut() - .map_err(|err| CheckoutError::Other { - message: "Failed to load the working copy state".to_string(), - err: err.into(), - })? - .check_out(new_tree)?; - self.tree_state_dirty = true; - Ok(stats) - } - pub fn reset(&mut self, new_tree: &MergedTree) -> Result<(), ResetError> { self.wc .tree_state_mut() diff --git a/lib/src/working_copy.rs b/lib/src/working_copy.rs index a66555f56..c9b98dd2a 100644 --- a/lib/src/working_copy.rs +++ b/lib/src/working_copy.rs @@ -25,6 +25,7 @@ use thiserror::Error; use crate::backend::{BackendError, MergedTreeId}; use crate::fsmonitor::FsmonitorKind; use crate::gitignore::GitIgnoreFile; +use crate::merged_tree::MergedTree; use crate::op_store::{OperationId, WorkspaceId}; use crate::repo_path::RepoPath; use crate::settings::HumanByteSize; @@ -61,6 +62,10 @@ pub trait LockedWorkingCopy { /// Snapshot the working copy and return the tree id. fn snapshot(&mut self, options: SnapshotOptions) -> Result; + + /// Check out the specified tree in the working copy. + // TODO: Pass a Commit object here because some implementations need that. + fn check_out(&mut self, new_tree: &MergedTree) -> Result; } /// An error while snapshotting the working copy. @@ -143,3 +148,48 @@ impl SnapshotOptions<'_> { /// A callback for getting progress updates. pub type SnapshotProgress<'a> = dyn Fn(&RepoPath) + 'a + Sync; + +/// Stats about a checkout operation on a working copy. All "files" mentioned +/// below may also be symlinks or materialized conflicts. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct CheckoutStats { + /// The number of files that were updated in the working copy. + /// These files existed before and after the checkout. + pub updated_files: u32, + /// The number of files added in the working copy. + pub added_files: u32, + /// The number of files removed in the working copy. + pub removed_files: u32, + /// The number of files that were supposed to be updated or added in the + /// working copy but were skipped because there was an untracked (probably + /// ignored) file in its place. + pub skipped_files: u32, +} + +/// The working-copy checkout failed. +#[derive(Debug, Error)] +pub enum CheckoutError { + /// The current working-copy commit was deleted, maybe by an overly + /// aggressive GC that happened while the current process was running. + #[error("Current working-copy commit not found: {source}")] + SourceNotFound { + /// The underlying error. + source: Box, + }, + /// Another process checked out a commit while the current process was + /// running (after the working copy was read by the current process). + #[error("Concurrent checkout")] + ConcurrentCheckout, + /// Reading or writing from the commit backend failed. + #[error("Internal backend error: {0}")] + InternalBackendError(#[from] BackendError), + /// Some other error happened while checking out the working copy. + #[error("{message}: {err:?}")] + Other { + /// Error message. + message: String, + /// The underlying error. + #[source] + err: Box, + }, +} diff --git a/lib/src/workspace.rs b/lib/src/workspace.rs index a41132825..5a46cb561 100644 --- a/lib/src/workspace.rs +++ b/lib/src/workspace.rs @@ -27,9 +27,7 @@ use crate::file_util::{self, IoResultExt as _, PathError}; use crate::git_backend::GitBackend; use crate::index::IndexStore; use crate::local_backend::LocalBackend; -use crate::local_working_copy::{ - CheckoutError, CheckoutStats, LocalWorkingCopy, LockedLocalWorkingCopy, WorkingCopyStateError, -}; +use crate::local_working_copy::{LocalWorkingCopy, LockedLocalWorkingCopy, WorkingCopyStateError}; use crate::merged_tree::MergedTree; use crate::op_heads_store::OpHeadsStore; use crate::op_store::{OpStore, OperationId, WorkspaceId}; @@ -39,7 +37,7 @@ use crate::repo::{ }; use crate::settings::UserSettings; use crate::submodule_store::SubmoduleStore; -use crate::working_copy::{LockedWorkingCopy, WorkingCopy}; +use crate::working_copy::{CheckoutError, CheckoutStats, LockedWorkingCopy, WorkingCopy}; #[derive(Error, Debug)] pub enum WorkspaceInitError { diff --git a/lib/tests/test_local_working_copy.rs b/lib/tests/test_local_working_copy.rs index d11829a08..25f66802a 100644 --- a/lib/tests/test_local_working_copy.rs +++ b/lib/tests/test_local_working_copy.rs @@ -27,14 +27,14 @@ use std::sync::Arc; use itertools::Itertools; use jj_lib::backend::{MergedTreeId, ObjectId, TreeId, TreeValue}; use jj_lib::fsmonitor::FsmonitorKind; -use jj_lib::local_working_copy::{CheckoutStats, LocalWorkingCopy}; +use jj_lib::local_working_copy::LocalWorkingCopy; use jj_lib::merge::Merge; use jj_lib::merged_tree::{MergedTree, MergedTreeBuilder}; 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::{LockedWorkingCopy, SnapshotError, SnapshotOptions}; +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}; diff --git a/lib/tests/test_local_working_copy_concurrent.rs b/lib/tests/test_local_working_copy_concurrent.rs index 15d8c8d42..3e00adcda 100644 --- a/lib/tests/test_local_working_copy_concurrent.rs +++ b/lib/tests/test_local_working_copy_concurrent.rs @@ -16,10 +16,9 @@ use std::cmp::max; use std::thread; use assert_matches::assert_matches; -use jj_lib::local_working_copy::CheckoutError; use jj_lib::repo::Repo; use jj_lib::repo_path::RepoPath; -use jj_lib::working_copy::{LockedWorkingCopy, SnapshotOptions}; +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 e85bca703..5399199ef 100644 --- a/lib/tests/test_local_working_copy_sparse.rs +++ b/lib/tests/test_local_working_copy_sparse.rs @@ -13,11 +13,11 @@ // limitations under the License. use itertools::Itertools; -use jj_lib::local_working_copy::{CheckoutStats, LocalWorkingCopy}; +use jj_lib::local_working_copy::LocalWorkingCopy; use jj_lib::matchers::EverythingMatcher; use jj_lib::repo::Repo; use jj_lib::repo_path::RepoPath; -use jj_lib::working_copy::WorkingCopy; +use jj_lib::working_copy::{CheckoutStats, WorkingCopy}; use testutils::{create_tree, TestWorkspace}; #[test]