forked from mirrors/jj
working_copy: add support for .gitignore files
The project's source of truth is now in Git and I really miss support for anonymous heads and evolution (compared to when the code was in Mercurial). I'm therefore more motivated to make the tool useful for day-to-day work on small repos, so I can use it myself. Until now, I had been more focused on improving performance when it was used as a read-only client for medium-to-large repos. One important feature for my day-to-day work is support for ignores. This commit adds simple and effective, but somewhat hacky support for that. libgit2 requires a repo to check if a file should be ignored (presumably so it can respect `.git/info/excludes`). To work around that, we create a temporary git repo in `/tmp/` whenever the working copy is committed. We set that temporary git repo's working copy to be shared with our own working copy. Due to https://github.com/libgit2/libgit2sharp/issues/1716 (which seems to apply to the non-.NET version as well), this workaround unfortunately leaves a .git file (pointing to the deleted temporary git repo) around in every Jujube repo. That's always ignored by libgit2, so it's not much of a problem.
This commit is contained in:
parent
9ad225b3b5
commit
3b326a942c
2 changed files with 94 additions and 3 deletions
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue