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:
parent
a42b24c014
commit
29a71c619a
2 changed files with 92 additions and 21 deletions
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue