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

working_copy: propagate most errors on checkout

This commit is contained in:
Martin von Zweigbergk 2022-05-23 21:47:45 -07:00 committed by Martin von Zweigbergk
parent 9aa2009320
commit fa4b5aa2c7
2 changed files with 72 additions and 25 deletions

View file

@ -148,9 +148,15 @@ fn sparse_patterns_from_proto(proto: &crate::protos::working_copy::TreeState) ->
sparse_patterns sparse_patterns
} }
fn create_parent_dirs(disk_path: &Path) { fn create_parent_dirs(disk_path: &Path) -> Result<(), CheckoutError> {
fs::create_dir_all(disk_path.parent().unwrap()) fs::create_dir_all(disk_path.parent().unwrap()).map_err(|err| CheckoutError::IoError {
.unwrap_or_else(|_| panic!("failed to create parent directories for {:?}", &disk_path)); message: format!(
"Failed to create parent directories for {}",
&disk_path.display()
),
err,
})?;
Ok(())
} }
fn mtime_from_metadata(metadata: &Metadata) -> MillisSinceEpoch { fn mtime_from_metadata(metadata: &Metadata) -> MillisSinceEpoch {
@ -201,7 +207,7 @@ pub struct CheckoutStats {
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum SnapshotError { pub enum SnapshotError {
#[error("{message}: {err:?}")] #[error("{message}: {err}")]
IoError { IoError {
message: String, message: String,
#[source] #[source]
@ -215,7 +221,7 @@ pub enum SnapshotError {
InternalBackendError(#[from] BackendError), InternalBackendError(#[from] BackendError),
} }
#[derive(Debug, Error, PartialEq, Eq)] #[derive(Debug, Error)]
pub enum CheckoutError { pub enum CheckoutError {
// The current checkout was deleted, maybe by an overly aggressive GC that happened while // The current checkout was deleted, maybe by an overly aggressive GC that happened while
// the current process was running. // the current process was running.
@ -225,10 +231,25 @@ pub enum CheckoutError {
// working copy was read by the current process). // working copy was read by the current process).
#[error("Concurrent checkout")] #[error("Concurrent checkout")]
ConcurrentCheckout, ConcurrentCheckout,
#[error("{message}: {err:?}")]
IoError {
message: String,
#[source]
err: std::io::Error,
},
#[error("Internal error: {0:?}")] #[error("Internal error: {0:?}")]
InternalBackendError(#[from] BackendError), InternalBackendError(#[from] BackendError),
} }
impl CheckoutError {
fn for_stat_error(err: std::io::Error, path: &Path) -> Self {
CheckoutError::IoError {
message: format!("Failed to stat file {}", path.display()),
err,
}
}
}
#[derive(Debug, Error, PartialEq, Eq)] #[derive(Debug, Error, PartialEq, Eq)]
pub enum ResetError { pub enum ResetError {
// The current checkout was deleted, maybe by an overly aggressive GC that happened while // The current checkout was deleted, maybe by an overly aggressive GC that happened while
@ -574,7 +595,7 @@ impl TreeState {
id: &FileId, id: &FileId,
executable: bool, executable: bool,
) -> Result<FileState, CheckoutError> { ) -> Result<FileState, CheckoutError> {
create_parent_dirs(disk_path); create_parent_dirs(disk_path)?;
// TODO: Check that we're not overwriting an un-ignored file here (which might // TODO: Check that we're not overwriting an un-ignored file here (which might
// be created by a concurrent process). // be created by a concurrent process).
let mut file = OpenOptions::new() let mut file = OpenOptions::new()
@ -582,15 +603,23 @@ impl TreeState {
.create(true) .create(true)
.truncate(true) .truncate(true)
.open(disk_path) .open(disk_path)
.unwrap_or_else(|err| panic!("failed to open {:?} for write: {:?}", &disk_path, err)); .map_err(|err| CheckoutError::IoError {
message: format!("Failed to open file {} for writing", disk_path.display()),
err,
})?;
let mut contents = self.store.read_file(path, id)?; let mut contents = self.store.read_file(path, id)?;
std::io::copy(&mut contents, &mut file).unwrap(); std::io::copy(&mut contents, &mut file).map_err(|err| CheckoutError::IoError {
self.set_executable(disk_path, executable); message: format!("Failed to write file {}", disk_path.display()),
err,
})?;
self.set_executable(disk_path, executable)?;
// Read the file state from the file descriptor. That way, know that the file // Read the file state from the file descriptor. That way, know that the file
// exists and is of the expected type, and the stat information is most likely // exists and is of the expected type, and the stat information is most likely
// accurate, except for other processes modifying the file concurrently (The // accurate, except for other processes modifying the file concurrently (The
// mtime is set at write time and won't change when we close the file.) // mtime is set at write time and won't change when we close the file.)
let metadata = file.metadata().unwrap(); let metadata = file
.metadata()
.map_err(|err| CheckoutError::for_stat_error(err, disk_path))?;
let mut file_state = file_state(&metadata); let mut file_state = file_state(&metadata);
// Make sure the state we record is what we tried to set above. This is mostly // Make sure the state we record is what we tried to set above. This is mostly
// for Windows, since the executable bit is not reflected in the file system // for Windows, since the executable bit is not reflected in the file system
@ -606,7 +635,7 @@ impl TreeState {
path: &RepoPath, path: &RepoPath,
id: &SymlinkId, id: &SymlinkId,
) -> Result<FileState, CheckoutError> { ) -> Result<FileState, CheckoutError> {
create_parent_dirs(disk_path); create_parent_dirs(disk_path)?;
#[cfg(windows)] #[cfg(windows)]
{ {
println!("ignoring symlink at {:?}", path); println!("ignoring symlink at {:?}", path);
@ -615,9 +644,18 @@ impl TreeState {
{ {
let target = self.store.read_symlink(path, id)?; let target = self.store.read_symlink(path, id)?;
let target = PathBuf::from(&target); let target = PathBuf::from(&target);
symlink(target, disk_path).unwrap(); symlink(&target, disk_path).map_err(|err| CheckoutError::IoError {
message: format!(
"Failed to create symlink from {} to {}",
disk_path.display(),
target.display()
),
err,
})?;
} }
let metadata = disk_path.symlink_metadata().unwrap(); let metadata = disk_path
.symlink_metadata()
.map_err(|err| CheckoutError::for_stat_error(err, disk_path))?;
Ok(file_state(&metadata)) Ok(file_state(&metadata))
} }
@ -627,7 +665,7 @@ impl TreeState {
path: &RepoPath, path: &RepoPath,
id: &ConflictId, id: &ConflictId,
) -> Result<FileState, CheckoutError> { ) -> Result<FileState, CheckoutError> {
create_parent_dirs(disk_path); create_parent_dirs(disk_path)?;
let conflict = self.store.read_conflict(path, id)?; let conflict = self.store.read_conflict(path, id)?;
// TODO: Check that we're not overwriting an un-ignored file here (which might // TODO: Check that we're not overwriting an un-ignored file here (which might
// be created by a concurrent process). // be created by a concurrent process).
@ -636,27 +674,35 @@ impl TreeState {
.create(true) .create(true)
.truncate(true) .truncate(true)
.open(disk_path) .open(disk_path)
.unwrap_or_else(|err| panic!("failed to open {:?} for write: {:?}", &disk_path, err)); .map_err(|err| CheckoutError::IoError {
materialize_conflict(self.store.as_ref(), path, &conflict, &mut file).unwrap(); message: format!("Failed to open file {} for writing", disk_path.display()),
err,
})?;
materialize_conflict(self.store.as_ref(), path, &conflict, &mut file).map_err(|err| {
CheckoutError::IoError {
message: format!("Failed to write conflict to file {}", disk_path.display()),
err,
}
})?;
// TODO: Set the executable bit correctly (when possible) and preserve that on // TODO: Set the executable bit correctly (when possible) and preserve that on
// Windows like we do with the executable bit for regular files. // Windows like we do with the executable bit for regular files.
let metadata = file.metadata().unwrap(); let metadata = file
.metadata()
.map_err(|err| CheckoutError::for_stat_error(err, disk_path))?;
let mut result = file_state(&metadata); let mut result = file_state(&metadata);
result.file_type = FileType::Conflict { id: id.clone() }; result.file_type = FileType::Conflict { id: id.clone() };
Ok(result) Ok(result)
} }
#[cfg_attr(windows, allow(unused_variables))] #[cfg_attr(windows, allow(unused_variables))]
fn set_executable(&self, disk_path: &Path, executable: bool) { fn set_executable(&self, disk_path: &Path, executable: bool) -> Result<(), CheckoutError> {
#[cfg(windows)]
{
return;
}
#[cfg(unix)] #[cfg(unix)]
{ {
let mode = if executable { 0o755 } else { 0o644 }; let mode = if executable { 0o755 } else { 0o644 };
fs::set_permissions(disk_path, fs::Permissions::from_mode(mode)).unwrap(); fs::set_permissions(disk_path, fs::Permissions::from_mode(mode))
.map_err(|err| CheckoutError::for_stat_error(err, disk_path))?;
} }
Ok(())
} }
pub fn check_out(&mut self, new_tree: &Tree) -> Result<CheckoutStats, CheckoutError> { pub fn check_out(&mut self, new_tree: &Tree) -> Result<CheckoutStats, CheckoutError> {
@ -758,7 +804,7 @@ impl TreeState {
) if id == old_id => { ) if id == old_id => {
// Optimization for when only the executable bit changed // Optimization for when only the executable bit changed
assert_ne!(executable, old_executable); assert_ne!(executable, old_executable);
self.set_executable(&disk_path, executable); self.set_executable(&disk_path, executable)?;
let file_state = self.file_states.get_mut(&path).unwrap(); let file_state = self.file_states.get_mut(&path).unwrap();
file_state.mark_executable(executable); file_state.mark_executable(executable);
stats.updated_files += 1; stats.updated_files += 1;

View file

@ -15,6 +15,7 @@
use std::cmp::max; use std::cmp::max;
use std::thread; use std::thread;
use assert_matches::assert_matches;
use jujutsu_lib::gitignore::GitIgnoreFile; use jujutsu_lib::gitignore::GitIgnoreFile;
use jujutsu_lib::repo_path::RepoPath; use jujutsu_lib::repo_path::RepoPath;
use jujutsu_lib::testutils; use jujutsu_lib::testutils;
@ -63,7 +64,7 @@ fn test_concurrent_checkout(use_git: bool) {
.unwrap(); .unwrap();
// Checking out another tree (via the first repo instance) should now fail. // Checking out another tree (via the first repo instance) should now fail.
assert_eq!( assert_matches!(
wc1.check_out(repo1.op_id().clone(), Some(&tree_id1), &tree3), wc1.check_out(repo1.op_id().clone(), Some(&tree_id1), &tree3),
Err(CheckoutError::ConcurrentCheckout) Err(CheckoutError::ConcurrentCheckout)
); );