From 0d881de56cc5a8bef73e6c5955b6a6b764ddda44 Mon Sep 17 00:00:00 2001 From: Martin von Zweigbergk Date: Wed, 9 Feb 2022 22:42:50 -0800 Subject: [PATCH] working_copy: allow updating sparse patterns (#52) With this patch, we add support for setting the sparse patterns, and we respect it when updating the working copy. --- lib/src/working_copy.rs | 53 +++++++++-- lib/tests/test_working_copy_sparse.rs | 124 ++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 lib/tests/test_working_copy_sparse.rs diff --git a/lib/src/working_copy.rs b/lib/src/working_copy.rs index 1c49453b7..4ebfe5c58 100644 --- a/lib/src/working_copy.rs +++ b/lib/src/working_copy.rs @@ -37,7 +37,7 @@ use crate::backend::{ use crate::conflicts::{materialize_conflict, update_conflict_from_content}; use crate::gitignore::GitIgnoreFile; use crate::lock::FileLock; -use crate::matchers::{EverythingMatcher, Matcher}; +use crate::matchers::{DifferenceMatcher, Matcher, PrefixMatcher}; use crate::op_store::{OperationId, WorkspaceId}; use crate::repo_path::{RepoPath, RepoPathComponent, RepoPathJoin}; use crate::store::Store; @@ -226,6 +226,10 @@ impl TreeState { &self.sparse_patterns } + fn sparse_matcher(&self) -> Box { + Box::new(PrefixMatcher::new(&self.sparse_patterns)) + } + pub fn init(store: Arc, working_copy_path: PathBuf, state_path: PathBuf) -> TreeState { let mut wc = TreeState::empty(store, working_copy_path, state_path); wc.save(); @@ -579,13 +583,39 @@ impl TreeState { BackendError::NotFound => CheckoutError::SourceNotFound, other => CheckoutError::InternalBackendError(other), })?; - let stats = self.update(&old_tree, new_tree, &EverythingMatcher)?; + let stats = self.update(&old_tree, new_tree, self.sparse_matcher().as_ref())?; self.tree_id = new_tree.id().clone(); Ok(stats) } - pub fn set_sparse_patterns(&mut self, sparse_patterns: Vec) { + pub fn set_sparse_patterns( + &mut self, + sparse_patterns: Vec, + ) -> Result { + let tree = self + .store + .get_tree(&RepoPath::root(), &self.tree_id) + .map_err(|err| match err { + BackendError::NotFound => CheckoutError::SourceNotFound, + other => CheckoutError::InternalBackendError(other), + })?; + let old_matcher = PrefixMatcher::new(&self.sparse_patterns); + let new_matcher = PrefixMatcher::new(&sparse_patterns); + let added_matcher = DifferenceMatcher::new(&new_matcher, &old_matcher); + let removed_matcher = DifferenceMatcher::new(&old_matcher, &new_matcher); + let empty_tree = Tree::null(self.store.clone(), RepoPath::root()); + let added_stats = self.update(&empty_tree, &tree, &added_matcher)?; + let removed_stats = self.update(&tree, &empty_tree, &removed_matcher)?; self.sparse_patterns = sparse_patterns; + assert_eq!(added_stats.updated_files, 0); + assert_eq!(added_stats.removed_files, 0); + assert_eq!(removed_stats.updated_files, 0); + assert_eq!(removed_stats.added_files, 0); + Ok(CheckoutStats { + updated_files: 0, + added_files: added_stats.added_files, + removed_files: removed_stats.removed_files, + }) } fn update( @@ -684,7 +714,7 @@ impl TreeState { other => ResetError::InternalBackendError(other), })?; - for (path, diff) in old_tree.diff(new_tree, &EverythingMatcher) { + for (path, diff) in old_tree.diff(new_tree, self.sparse_matcher().as_ref()) { match diff { Diff::Removed(_before) => { self.file_states.remove(&path); @@ -766,6 +796,10 @@ impl WorkingCopy { } } + pub fn working_copy_path(&self) -> &Path { + &self.working_copy_path + } + pub fn state_path(&self) -> &Path { &self.state_path } @@ -925,8 +959,8 @@ impl LockedWorkingCopy<'_> { } pub fn check_out(&mut self, new_tree: &Tree) -> Result { - // TODO: Write a "pending_checkout" file with the old and new TreeIds so we can - // continue an interrupted checkout if we find such a file. + // 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().as_mut().unwrap().check_out(new_tree)?; Ok(stats) } @@ -939,7 +973,12 @@ impl LockedWorkingCopy<'_> { self.wc.sparse_patterns() } - pub fn set_sparse_patterns(&mut self, new_sparse_patterns: Vec) { + pub fn set_sparse_patterns( + &mut self, + new_sparse_patterns: Vec, + ) -> Result { + // TODO: Write a "pending_checkout" file with new sparse patterns so we can + // continue an interrupted update if we find such a file. self.wc .tree_state() .as_mut() diff --git a/lib/tests/test_working_copy_sparse.rs b/lib/tests/test_working_copy_sparse.rs new file mode 100644 index 000000000..f7f5d2041 --- /dev/null +++ b/lib/tests/test_working_copy_sparse.rs @@ -0,0 +1,124 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use itertools::Itertools; +use jujutsu_lib::repo_path::RepoPath; +use jujutsu_lib::testutils; +use jujutsu_lib::working_copy::{CheckoutStats, WorkingCopy}; + +#[test] +fn test_sparse_checkout() { + let settings = testutils::user_settings(); + let mut test_workspace = testutils::init_workspace(&settings, false); + let repo = &test_workspace.repo; + let working_copy_path = test_workspace.workspace.workspace_root().clone(); + + let root_file1_path = RepoPath::from_internal_string("file1"); + let root_file2_path = RepoPath::from_internal_string("file2"); + let dir1_path = RepoPath::from_internal_string("dir1"); + let dir1_file1_path = RepoPath::from_internal_string("dir1/file1"); + let dir1_file2_path = RepoPath::from_internal_string("dir1/file2"); + let dir1_subdir1_path = RepoPath::from_internal_string("dir1/subdir1"); + let dir1_subdir1_file1_path = RepoPath::from_internal_string("dir1/subdir1/file1"); + let dir2_path = RepoPath::from_internal_string("dir2"); + let dir2_file1_path = RepoPath::from_internal_string("dir2/file1"); + + let tree = testutils::create_tree( + repo, + &[ + (&root_file1_path, "contents"), + (&root_file2_path, "contents"), + (&dir1_file1_path, "contents"), + (&dir1_file2_path, "contents"), + (&dir1_subdir1_file1_path, "contents"), + (&dir2_file1_path, "contents"), + ], + ); + + let wc = test_workspace.workspace.working_copy_mut(); + wc.check_out(repo.op_id().clone(), None, &tree).unwrap(); + + // Set sparse patterns to only dir1/ + let mut locked_wc = wc.start_mutation(); + let sparse_patterns = vec![dir1_path]; + let stats = locked_wc + .set_sparse_patterns(sparse_patterns.clone()) + .unwrap(); + assert_eq!( + stats, + CheckoutStats { + updated_files: 0, + added_files: 0, + removed_files: 3 + } + ); + assert_eq!(locked_wc.sparse_patterns(), sparse_patterns); + assert!(!root_file1_path.to_fs_path(&working_copy_path).exists()); + assert!(!root_file2_path.to_fs_path(&working_copy_path).exists()); + assert!(dir1_file1_path.to_fs_path(&working_copy_path).exists()); + assert!(dir1_file2_path.to_fs_path(&working_copy_path).exists()); + assert!(dir1_subdir1_file1_path + .to_fs_path(&working_copy_path) + .exists()); + assert!(!dir2_file1_path.to_fs_path(&working_copy_path).exists()); + + // Write the new state to disk + locked_wc.finish(repo.op_id().clone()); + assert_eq!( + wc.file_states().keys().collect_vec(), + vec![&dir1_file1_path, &dir1_file2_path, &dir1_subdir1_file1_path] + ); + assert_eq!(wc.sparse_patterns(), sparse_patterns); + + // Reload the state to check that it was persisted + let mut wc = WorkingCopy::load( + repo.store().clone(), + wc.working_copy_path().to_path_buf(), + wc.state_path().to_path_buf(), + ); + assert_eq!( + wc.file_states().keys().collect_vec(), + vec![&dir1_file1_path, &dir1_file2_path, &dir1_subdir1_file1_path] + ); + assert_eq!(wc.sparse_patterns(), sparse_patterns); + + // Set sparse patterns to file2, dir1/subdir1/ and dir2/ + let mut locked_wc = wc.start_mutation(); + let sparse_patterns = vec![root_file1_path.clone(), dir1_subdir1_path, dir2_path]; + let stats = locked_wc + .set_sparse_patterns(sparse_patterns.clone()) + .unwrap(); + assert_eq!( + stats, + CheckoutStats { + updated_files: 0, + added_files: 2, + removed_files: 2 + } + ); + assert_eq!(locked_wc.sparse_patterns(), sparse_patterns); + assert!(root_file1_path.to_fs_path(&working_copy_path).exists()); + assert!(!root_file2_path.to_fs_path(&working_copy_path).exists()); + assert!(!dir1_file1_path.to_fs_path(&working_copy_path).exists()); + assert!(!dir1_file2_path.to_fs_path(&working_copy_path).exists()); + assert!(dir1_subdir1_file1_path + .to_fs_path(&working_copy_path) + .exists()); + assert!(dir2_file1_path.to_fs_path(&working_copy_path).exists()); + locked_wc.finish(repo.op_id().clone()); + assert_eq!( + wc.file_states().keys().collect_vec(), + vec![&dir1_subdir1_file1_path, &dir2_file1_path, &root_file1_path] + ); +}