working_copy: add a method for untracking specified paths

This commit is contained in:
Martin von Zweigbergk 2021-11-14 22:00:46 -08:00
parent 41860692e9
commit 287602966e
2 changed files with 107 additions and 1 deletions

View file

@ -38,7 +38,7 @@ use crate::commit::Commit;
use crate::conflicts::{materialize_conflict, update_conflict_from_content};
use crate::gitignore::GitIgnoreFile;
use crate::lock::FileLock;
use crate::matchers::EverythingMatcher;
use crate::matchers::{EverythingMatcher, Matcher};
use crate::repo_path::{RepoPath, RepoPathComponent, RepoPathJoin};
use crate::store::Store;
use crate::tree::Diff;
@ -189,6 +189,16 @@ pub enum CheckoutError {
InternalBackendError(BackendError),
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum UntrackError {
// The current checkout was deleted, maybe by an overly aggressive GC that happened while
// the current process was running.
#[error("Current checkout not found")]
SourceNotFound,
#[error("Internal error: {0:?}")]
InternalBackendError(BackendError),
}
impl TreeState {
pub fn current_tree_id(&self) -> &TreeId {
&self.tree_id
@ -662,6 +672,25 @@ impl TreeState {
self.save();
Ok(stats)
}
pub fn untrack(&mut self, matcher: &dyn Matcher) -> Result<(), UntrackError> {
let tree = self
.store
.get_tree(&RepoPath::root(), &self.tree_id)
.map_err(|err| match err {
BackendError::NotFound => UntrackError::SourceNotFound,
other => UntrackError::InternalBackendError(other),
})?;
let mut tree_builder = self.store.tree_builder(self.tree_id.clone());
for (path, _value) in tree.entries_matching(matcher) {
self.file_states.remove(&path);
tree_builder.remove(path);
}
self.tree_id = tree_builder.write_tree();
self.save();
Ok(())
}
}
pub struct WorkingCopy {
@ -831,6 +860,19 @@ impl WorkingCopy {
closed: false,
}
}
pub fn untrack(&self, matcher: &dyn Matcher) -> Result<LockedWorkingCopy, UntrackError> {
let lock_path = self.state_path.join("working_copy.lock");
let lock = FileLock::lock(lock_path);
self.tree_state().as_mut().unwrap().untrack(matcher)?;
Ok(LockedWorkingCopy {
wc: self,
lock,
closed: false,
})
}
}
// A working copy that's locked on disk. The tree state has already been

View file

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::HashSet;
use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
#[cfg(unix)]
@ -21,6 +22,7 @@ use std::sync::Arc;
use itertools::Itertools;
use jujutsu_lib::backend::{Conflict, ConflictPart, TreeValue};
use jujutsu_lib::commit_builder::CommitBuilder;
use jujutsu_lib::matchers::FilesMatcher;
use jujutsu_lib::repo::ReadonlyRepo;
use jujutsu_lib::repo_path::{RepoPath, RepoPathComponent};
use jujutsu_lib::settings::UserSettings;
@ -294,6 +296,68 @@ fn test_checkout_file_transitions(use_git: bool) {
}
}
#[test]
fn test_untrack() {
let settings = testutils::user_settings();
let (_temp_dir, repo) = testutils::init_repo(&settings, false);
let wanted_path = RepoPath::from_internal_string("wanted");
let unwanted_path = RepoPath::from_internal_string("unwanted");
let gitignore_path = RepoPath::from_internal_string(".gitignore");
// First create a commit where one of the files is unwanted.
let initial_tree = testutils::create_tree(
&repo,
&[
(&wanted_path, "code"),
(&unwanted_path, "garbage"),
(&gitignore_path, "unwanted\n"),
],
);
let mut tx = repo.start_transaction("test");
let initial_commit = CommitBuilder::for_open_commit(
&settings,
repo.store(),
repo.store().root_commit_id().clone(),
initial_tree.id().clone(),
)
.write_to_repo(tx.mut_repo());
let repo = tx.commit();
let working_copy = repo.working_copy().clone();
let locked_working_copy = working_copy.lock().unwrap();
locked_working_copy
.check_out(initial_commit.clone())
.unwrap();
// Now we untrack the file called "unwanted"
let mut tx = repo.start_transaction("test");
let matcher = FilesMatcher::new(HashSet::from([unwanted_path.clone()]));
let unfinished_write = locked_working_copy.untrack(&matcher).unwrap();
let new_commit = CommitBuilder::for_rewrite_from(&settings, repo.store(), &initial_commit)
.set_tree(unfinished_write.new_tree_id())
.write_to_repo(tx.mut_repo());
unfinished_write.finish(new_commit.clone());
let repo = tx.commit();
// The file should still exist in the working copy.
assert!(unwanted_path.to_fs_path(repo.working_copy_path()).is_file());
// It should not be in the new tree.
let tracked_paths = new_commit
.tree()
.entries()
.map(|(path, _)| path)
.collect_vec();
assert_eq!(tracked_paths, vec![gitignore_path, wanted_path]);
// It should not get re-added if we write a new tree since we also added it
// to the .gitignore file.
let unfinished_write = locked_working_copy.write_tree();
let new_tree_id = unfinished_write.new_tree_id();
assert_eq!(new_tree_id, *new_commit.tree().id());
unfinished_write.discard();
}
#[test_case(false ; "local backend")]
#[test_case(true ; "git backend")]
fn test_commit_racy_timestamps(use_git: bool) {