diff --git a/lib/src/working_copy.rs b/lib/src/working_copy.rs index 4ebfe5c58..e90a97125 100644 --- a/lib/src/working_copy.rs +++ b/lib/src/working_copy.rs @@ -327,6 +327,7 @@ impl TreeState { // Look for changes to the working copy. If there are any changes, create // a new tree from it and return it, and also update the dirstate on disk. pub fn write_tree(&mut self, base_ignores: Arc) -> TreeId { + let sparse_matcher = self.sparse_matcher(); let mut work = vec![( RepoPath::root(), self.working_copy_path.clone(), @@ -336,6 +337,9 @@ impl TreeState { let mut deleted_files: HashSet<_> = self.file_states.keys().cloned().collect(); while !work.is_empty() { let (dir, disk_dir, git_ignore) = work.pop().unwrap(); + if sparse_matcher.visit(&dir).is_nothing() { + continue; + } let git_ignore = git_ignore .chain_with_file(&dir.to_internal_dir_string(), disk_dir.join(".gitignore")); for maybe_entry in disk_dir.read_dir().unwrap() { @@ -367,12 +371,14 @@ impl TreeState { work.push((sub_path, entry.path(), git_ignore.clone())); } else { deleted_files.remove(&sub_path); - self.update_file_state( - sub_path, - entry.path(), - git_ignore.as_ref(), - &mut tree_builder, - ); + if sparse_matcher.matches(&sub_path) { + self.update_file_state( + sub_path, + entry.path(), + git_ignore.as_ref(), + &mut tree_builder, + ); + } } } } diff --git a/lib/tests/test_working_copy_sparse.rs b/lib/tests/test_working_copy_sparse.rs index f7f5d2041..2eb0424d6 100644 --- a/lib/tests/test_working_copy_sparse.rs +++ b/lib/tests/test_working_copy_sparse.rs @@ -13,6 +13,8 @@ // limitations under the License. use itertools::Itertools; +use jujutsu_lib::gitignore::GitIgnoreFile; +use jujutsu_lib::matchers::EverythingMatcher; use jujutsu_lib::repo_path::RepoPath; use jujutsu_lib::testutils; use jujutsu_lib::working_copy::{CheckoutStats, WorkingCopy}; @@ -122,3 +124,121 @@ fn test_sparse_checkout() { vec![&dir1_subdir1_file1_path, &dir2_file1_path, &root_file1_path] ); } + +/// Test that sparse patterns are respected on commit +#[test] +fn test_sparse_commit() { + 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 dir1_path = RepoPath::from_internal_string("dir1"); + let dir1_file1_path = RepoPath::from_internal_string("dir1/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"), + (&dir1_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.clone()]; + locked_wc.set_sparse_patterns(sparse_patterns).unwrap(); + locked_wc.finish(repo.op_id().clone()); + + // Write modified version of all files, including files that are not in the + // sparse patterns. + std::fs::write(root_file1_path.to_fs_path(&working_copy_path), "modified").unwrap(); + std::fs::write(dir1_file1_path.to_fs_path(&working_copy_path), "modified").unwrap(); + std::fs::create_dir(dir2_path.to_fs_path(&working_copy_path)).unwrap(); + std::fs::write(dir2_file1_path.to_fs_path(&working_copy_path), "modified").unwrap(); + + // Create a tree from the working copy. Only dir1/file1 should be updated in the + // tree. + let mut locked_wc = wc.start_mutation(); + let modified_tree_id = locked_wc.write_tree(GitIgnoreFile::empty()); + locked_wc.finish(repo.op_id().clone()); + let modified_tree = repo + .store() + .get_tree(&RepoPath::root(), &modified_tree_id) + .unwrap(); + let diff = tree.diff(&modified_tree, &EverythingMatcher).collect_vec(); + assert_eq!(diff.len(), 1); + assert_eq!(diff[0].0, dir1_file1_path); + + // Set sparse patterns to also include dir2/ + let mut locked_wc = wc.start_mutation(); + let sparse_patterns = vec![dir1_path, dir2_path]; + locked_wc.set_sparse_patterns(sparse_patterns).unwrap(); + locked_wc.finish(repo.op_id().clone()); + // Write out a modified version of dir2/file1 again because it was overwritten + // when we added dir2/ to the sparse patterns. + // TODO: We shouldn't overwrite files when updating (there's already a TODO + // about that in `TreeState::write_file()`). + std::fs::write(dir2_file1_path.to_fs_path(&working_copy_path), "modified").unwrap(); + + // Create a tree from the working copy. Only dir1/file1 and dir2/file1 should be + // updated in the tree. + let mut locked_wc = wc.start_mutation(); + let modified_tree_id = locked_wc.write_tree(GitIgnoreFile::empty()); + locked_wc.finish(repo.op_id().clone()); + let modified_tree = repo + .store() + .get_tree(&RepoPath::root(), &modified_tree_id) + .unwrap(); + let diff = tree.diff(&modified_tree, &EverythingMatcher).collect_vec(); + assert_eq!(diff.len(), 2); + assert_eq!(diff[0].0, dir1_file1_path); + assert_eq!(diff[1].0, dir2_file1_path); +} + +#[test] +fn test_sparse_commit_gitignore() { + // Test that (untracked) .gitignore files in parent directories are respected + 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 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 wc = test_workspace.workspace.working_copy_mut(); + + // Set sparse patterns to only dir1/ + let mut locked_wc = wc.start_mutation(); + let sparse_patterns = vec![dir1_path.clone()]; + locked_wc.set_sparse_patterns(sparse_patterns).unwrap(); + locked_wc.finish(repo.op_id().clone()); + + // Write dir1/file1 and dir1/file2 and a .gitignore saying to ignore dir1/file1 + std::fs::write(working_copy_path.join(".gitignore"), "dir1/file1").unwrap(); + std::fs::create_dir(dir1_path.to_fs_path(&working_copy_path)).unwrap(); + std::fs::write(dir1_file1_path.to_fs_path(&working_copy_path), "contents").unwrap(); + std::fs::write(dir1_file2_path.to_fs_path(&working_copy_path), "contents").unwrap(); + + // Create a tree from the working copy. Only dir1/file2 should be updated in the + // tree because dir1/file1 is ignored. + let mut locked_wc = wc.start_mutation(); + let modified_tree_id = locked_wc.write_tree(GitIgnoreFile::empty()); + locked_wc.finish(repo.op_id().clone()); + let modified_tree = repo + .store() + .get_tree(&RepoPath::root(), &modified_tree_id) + .unwrap(); + let entries = modified_tree.entries().collect_vec(); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].0, dir1_file2_path); +}