ok/jj
1
0
Fork 0
forked from mirrors/jj

working_copy: ignore special files

This patch makes us treat special files (e.g. Unix sockets) as absent
when snapshotting the working copy. We can consider later reporting
such files back to the caller (possibly via callback) so it can inform
the user about them.

Closes #258
This commit is contained in:
Martin von Zweigbergk 2022-06-17 13:24:55 -07:00 committed by Martin von Zweigbergk
parent a42b24c014
commit 29a71c619a
2 changed files with 92 additions and 21 deletions

View file

@ -199,30 +199,34 @@ fn mtime_from_metadata(metadata: &Metadata) -> MillisSinceEpoch {
)
}
fn file_state(metadata: &Metadata) -> FileState {
let mtime = mtime_from_metadata(metadata);
let size = metadata.len();
fn file_state(metadata: &Metadata) -> Option<FileState> {
let metadata_file_type = metadata.file_type();
let file_type = if metadata_file_type.is_dir() {
panic!("expected file, not directory");
} else if metadata_file_type.is_symlink() {
FileType::Symlink
} else {
Some(FileType::Symlink)
} else if metadata_file_type.is_file() {
#[cfg(unix)]
let mode = metadata.permissions().mode();
#[cfg(windows)]
let mode = 0;
if mode & 0o111 != 0 {
FileType::Normal { executable: true }
Some(FileType::Normal { executable: true })
} else {
FileType::Normal { executable: false }
Some(FileType::Normal { executable: false })
}
} else {
None
};
FileState {
file_type,
mtime,
size,
}
file_type.map(|file_type| {
let mtime = mtime_from_metadata(metadata);
let size = metadata.len();
FileState {
file_type,
mtime,
size,
}
})
}
#[derive(Debug, PartialEq, Eq, Clone)]
@ -518,16 +522,24 @@ impl TreeState {
message: format!("Failed to stat file {}", disk_path.display()),
err,
})?;
let mut new_file_state = file_state(&metadata);
match maybe_current_file_state {
None => {
let maybe_new_file_state = file_state(&metadata);
match (maybe_current_file_state, maybe_new_file_state) {
(None, None) => {
// Untracked Unix socket or such
}
(Some(_), None) => {
// Tracked file replaced by Unix socket or such
self.file_states.remove(&repo_path);
tree_builder.remove(repo_path);
}
(None, Some(new_file_state)) => {
// untracked
let file_type = new_file_state.file_type.clone();
self.file_states.insert(repo_path.clone(), new_file_state);
let file_value = self.write_path_to_store(&repo_path, &disk_path, file_type)?;
tree_builder.set(repo_path, file_value);
}
Some(current_file_state) => {
(Some(current_file_state), Some(mut new_file_state)) => {
#[cfg(windows)]
{
// On Windows, we preserve the state we had recorded

View file

@ -16,12 +16,14 @@ use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[cfg(unix)]
use std::os::unix::net::UnixListener;
use std::sync::Arc;
use itertools::Itertools;
use jujutsu_lib::backend::{Conflict, ConflictPart, TreeValue};
use jujutsu_lib::gitignore::GitIgnoreFile;
use jujutsu_lib::op_store::WorkspaceId;
use jujutsu_lib::op_store::{OperationId, WorkspaceId};
use jujutsu_lib::repo::ReadonlyRepo;
use jujutsu_lib::repo_path::{RepoPath, RepoPathComponent, RepoPathJoin};
use jujutsu_lib::settings::UserSettings;
@ -386,7 +388,7 @@ fn test_checkout_discard() {
#[test_case(false ; "local backend")]
#[test_case(true ; "git backend")]
fn test_commit_racy_timestamps(use_git: bool) {
fn test_snapshot_racy_timestamps(use_git: bool) {
// Tests that file modifications are detected even if they happen the same
// millisecond as the updated working copy state.
let _home_dir = testutils::new_user_home();
@ -416,6 +418,63 @@ fn test_commit_racy_timestamps(use_git: bool) {
}
}
#[cfg(unix)]
#[test]
fn test_snapshot_special_file() {
// Tests that we ignore when special files (such as sockets and pipes) exist on
// disk.
let _home_dir = testutils::new_user_home();
let settings = testutils::user_settings();
let mut test_workspace = TestWorkspace::init(&settings, false);
let workspace_root = test_workspace.workspace.workspace_root().clone();
let store = test_workspace.repo.store();
let file1_path = RepoPath::from_internal_string("file1");
let file1_disk_path = file1_path.to_fs_path(&workspace_root);
std::fs::write(&file1_disk_path, "contents".as_bytes()).unwrap();
let file2_path = RepoPath::from_internal_string("file2");
let file2_disk_path = file2_path.to_fs_path(&workspace_root);
std::fs::write(&file2_disk_path, "contents".as_bytes()).unwrap();
let socket_disk_path = workspace_root.join("socket");
UnixListener::bind(&socket_disk_path).unwrap();
// Test the setup
assert!(socket_disk_path.exists());
assert!(!socket_disk_path.is_file());
// Snapshot the working copy with the socket file
let wc = test_workspace.workspace.working_copy_mut();
let mut locked_wc = wc.start_mutation();
let tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap();
locked_wc.finish(OperationId::from_hex("abc123"));
let tree = store.get_tree(&RepoPath::root(), &tree_id).unwrap();
// Only the regular files should be in the tree
assert_eq!(
tree.entries().map(|(path, _value)| path).collect_vec(),
vec![file1_path.clone(), file2_path.clone()]
);
assert_eq!(
wc.file_states().keys().cloned().collect_vec(),
vec![file1_path, file2_path.clone()]
);
// Replace a regular file by a socket and snapshot the working copy again
std::fs::remove_file(&file1_disk_path).unwrap();
UnixListener::bind(&file1_disk_path).unwrap();
let mut locked_wc = wc.start_mutation();
let tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap();
locked_wc.finish(OperationId::from_hex("abc123"));
let tree = store.get_tree(&RepoPath::root(), &tree_id).unwrap();
// Only the regular file should be in the tree
assert_eq!(
tree.entries().map(|(path, _value)| path).collect_vec(),
vec![file2_path.clone()]
);
assert_eq!(
wc.file_states().keys().cloned().collect_vec(),
vec![file2_path]
);
}
#[test_case(false ; "local backend")]
#[test_case(true ; "git backend")]
fn test_gitignores(use_git: bool) {
@ -531,7 +590,7 @@ fn test_gitignores_checkout_overwrites_ignored(use_git: bool) {
file.read_to_end(&mut buf).unwrap();
assert_eq!(buf, b"contents");
// Check that the file is in the tree created by committing the working copy
// Check that the file is in the tree created by snapshotting the working copy
let mut locked_wc = wc.start_mutation();
let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap();
locked_wc.discard();
@ -548,7 +607,7 @@ fn test_gitignores_checkout_overwrites_ignored(use_git: bool) {
#[test_case(true ; "git backend")]
fn test_gitignores_ignored_directory_already_tracked(use_git: bool) {
// Tests that a .gitignore'd directory that already has a tracked file in it
// does not get removed when committing the working directory.
// does not get removed when snapshotting the working directory.
let _home_dir = testutils::new_user_home();
let settings = testutils::user_settings();
@ -576,7 +635,7 @@ fn test_gitignores_ignored_directory_already_tracked(use_git: bool) {
let wc = test_workspace.workspace.working_copy_mut();
wc.check_out(repo.op_id().clone(), None, &tree).unwrap();
// Check that the file is still in the tree created by committing the working
// Check that the file is still in the tree created by snapshotting the working
// copy (that it didn't get removed because the directory is ignored)
let mut locked_wc = wc.start_mutation();
let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap();