mirror of
https://github.com/martinvonz/jj.git
synced 2025-01-18 18:27:38 +00:00
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::{CommitId, FileId, MillisSinceEpoch, StoreError, SymlinkId, TreeId, TreeValue};
|
||||||
use crate::store_wrapper::StoreWrapper;
|
use crate::store_wrapper::StoreWrapper;
|
||||||
use crate::trees::TreeValueDiff;
|
use crate::trees::TreeValueDiff;
|
||||||
|
use git2::{Repository, RepositoryInitOptions};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
@ -169,9 +170,11 @@ impl TreeState {
|
||||||
state_path: PathBuf,
|
state_path: PathBuf,
|
||||||
) -> TreeState {
|
) -> TreeState {
|
||||||
let tree_id = store.empty_tree_id().clone();
|
let tree_id = store.empty_tree_id().clone();
|
||||||
|
// Canonicalize the working copy path because "repo/." makes libgit2 think that
|
||||||
|
// everything should be ignored
|
||||||
TreeState {
|
TreeState {
|
||||||
store,
|
store,
|
||||||
working_copy_path,
|
working_copy_path: working_copy_path.canonicalize().unwrap(),
|
||||||
state_path,
|
state_path,
|
||||||
tree_id,
|
tree_id,
|
||||||
file_states: BTreeMap::new(),
|
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.
|
// a new tree from it and return it, and also update the dirstate on disk.
|
||||||
// TODO: respect ignores
|
// TODO: respect ignores
|
||||||
pub fn write_tree(&mut self) -> &TreeId {
|
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 work = vec![(DirRepoPath::root(), self.working_copy_path.clone())];
|
||||||
let mut tree_builder = self.store.tree_builder(self.tree_id.clone());
|
let mut tree_builder = self.store.tree_builder(self.tree_id.clone());
|
||||||
let mut deleted_files: HashSet<&FileRepoPath> = self.file_states.keys().collect();
|
let mut deleted_files: HashSet<&FileRepoPath> = self.file_states.keys().collect();
|
||||||
|
@ -292,16 +303,22 @@ impl TreeState {
|
||||||
work.push((subdir, disk_subdir));
|
work.push((subdir, disk_subdir));
|
||||||
} else {
|
} else {
|
||||||
let file = dir.join(&FileRepoPathComponent::from(name));
|
let file = dir.join(&FileRepoPathComponent::from(name));
|
||||||
|
let disk_file = disk_dir.join(file_name);
|
||||||
deleted_files.remove(&file);
|
deleted_files.remove(&file);
|
||||||
let new_file_state = self.file_state(&entry.path()).unwrap();
|
let new_file_state = self.file_state(&entry.path()).unwrap();
|
||||||
let clean = match self.file_states.get(&file) {
|
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) => {
|
Some(current_entry) => {
|
||||||
current_entry == &new_file_state && current_entry.mtime < self.read_time
|
current_entry == &new_file_state && current_entry.mtime < self.read_time
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if !clean {
|
if !clean {
|
||||||
let disk_file = disk_dir.join(file_name);
|
|
||||||
let file_value = match new_file_state.file_type {
|
let file_value = match new_file_state.file_type {
|
||||||
FileType::Normal | FileType::Executable => {
|
FileType::Normal | FileType::Executable => {
|
||||||
let id = self.write_file_to_store(&file, &disk_file);
|
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;
|
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