diff --git a/lib/src/working_copy.rs b/lib/src/working_copy.rs index 0f157b6b1..dd4487875 100644 --- a/lib/src/working_copy.rs +++ b/lib/src/working_copy.rs @@ -41,6 +41,7 @@ use crate::settings::UserSettings; use crate::store::{CommitId, FileId, MillisSinceEpoch, StoreError, SymlinkId, TreeId, TreeValue}; use crate::store_wrapper::StoreWrapper; use crate::trees::TreeValueDiff; +use git2::{Repository, RepositoryInitOptions}; use std::sync::Arc; #[derive(Debug, PartialEq, Eq, Clone)] @@ -169,9 +170,11 @@ impl TreeState { state_path: PathBuf, ) -> TreeState { let tree_id = store.empty_tree_id().clone(); + // Canonicalize the working copy path because "repo/." makes libgit2 think that + // everything should be ignored TreeState { store, - working_copy_path, + working_copy_path: working_copy_path.canonicalize().unwrap(), state_path, tree_id, file_states: BTreeMap::new(), @@ -272,6 +275,14 @@ impl TreeState { // a new tree from it and return it, and also update the dirstate on disk. // TODO: respect ignores pub fn write_tree(&mut self) -> &TreeId { + // We create a temporary git repo with the working copy shared with ours only + // so we can use libgit2's .gitignore check. + // TODO: Do this more cleanly, perhaps by reading .gitignore files ourselves. + let git_repo_dir = tempfile::tempdir().unwrap(); + let mut git_repo_options = RepositoryInitOptions::new(); + git_repo_options.workdir_path(&self.working_copy_path); + let git_repo = Repository::init_opts(git_repo_dir.path(), &git_repo_options).unwrap(); + let mut work = vec![(DirRepoPath::root(), self.working_copy_path.clone())]; let mut tree_builder = self.store.tree_builder(self.tree_id.clone()); let mut deleted_files: HashSet<&FileRepoPath> = self.file_states.keys().collect(); @@ -292,16 +303,22 @@ impl TreeState { work.push((subdir, disk_subdir)); } else { let file = dir.join(&FileRepoPathComponent::from(name)); + let disk_file = disk_dir.join(file_name); deleted_files.remove(&file); let new_file_state = self.file_state(&entry.path()).unwrap(); let clean = match self.file_states.get(&file) { - None => false, // untracked + None => { + // untracked + if git_repo.status_should_ignore(&disk_file).unwrap() { + continue; + } + false + } Some(current_entry) => { current_entry == &new_file_state && current_entry.mtime < self.read_time } }; if !clean { - let disk_file = disk_dir.join(file_name); let file_value = match new_file_state.file_type { FileType::Normal | FileType::Executable => { let id = self.write_file_to_store(&file, &disk_file); diff --git a/lib/tests/test_working_copy.rs b/lib/tests/test_working_copy.rs index 3cbd35e4d..5575c68d1 100644 --- a/lib/tests/test_working_copy.rs +++ b/lib/tests/test_working_copy.rs @@ -265,3 +265,77 @@ fn test_commit_racy_timestamps(use_git: bool) { previous_tree_id = new_tree_id; } } + +#[test_case(false ; "local store")] +#[test_case(true ; "git store")] +fn test_gitignores(use_git: bool) { + // Tests that .gitignore files are respected. + + let settings = testutils::user_settings(); + let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git); + + let gitignore_path = FileRepoPath::from(".gitignore"); + let added_path = FileRepoPath::from("added"); + let modified_path = FileRepoPath::from("modified"); + let removed_path = FileRepoPath::from("removed"); + let ignored_path = FileRepoPath::from("ignored"); + let subdir_modified_path = FileRepoPath::from("dir/modified"); + let subdir_ignored_path = FileRepoPath::from("dir/ignored"); + + testutils::write_working_copy_file(&repo, &gitignore_path, "ignored"); + testutils::write_working_copy_file(&repo, &modified_path, "1"); + testutils::write_working_copy_file(&repo, &removed_path, "1"); + std::fs::create_dir(repo.working_copy_path().join("dir")).unwrap(); + testutils::write_working_copy_file(&repo, &subdir_modified_path, "1"); + + let wc = repo.working_copy().clone(); + let commit1 = wc + .lock() + .unwrap() + .commit(&settings, Arc::get_mut(&mut repo).unwrap()); + let files1: Vec<_> = commit1 + .tree() + .entries() + .map(|(name, _value)| name) + .collect(); + assert_eq!( + files1, + vec![ + gitignore_path.to_repo_path(), + subdir_modified_path.to_repo_path(), + modified_path.to_repo_path(), + removed_path.to_repo_path() + ] + ); + + testutils::write_working_copy_file(&repo, &added_path, "2"); + testutils::write_working_copy_file(&repo, &modified_path, "2"); + std::fs::remove_file( + repo.working_copy_path() + .join(removed_path.to_internal_string()), + ) + .unwrap(); + testutils::write_working_copy_file(&repo, &ignored_path, "2"); + testutils::write_working_copy_file(&repo, &subdir_modified_path, "2"); + testutils::write_working_copy_file(&repo, &subdir_ignored_path, "2"); + + let wc = repo.working_copy().clone(); + let commit2 = wc + .lock() + .unwrap() + .commit(&settings, Arc::get_mut(&mut repo).unwrap()); + let files2: Vec<_> = commit2 + .tree() + .entries() + .map(|(name, _value)| name) + .collect(); + assert_eq!( + files2, + vec![ + gitignore_path.to_repo_path(), + added_path.to_repo_path(), + subdir_modified_path.to_repo_path(), + modified_path.to_repo_path() + ] + ); +}