just playing with tangled
at ig/vimdiffwarn 472 lines 18 kB view raw
1// Copyright 2023 The Jujutsu Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15//! Defines the interface for the working copy. See `LocalWorkingCopy` for the 16//! default local-disk implementation. 17 18use std::any::Any; 19use std::collections::BTreeMap; 20use std::ffi::OsString; 21use std::path::PathBuf; 22use std::sync::Arc; 23 24use itertools::Itertools as _; 25use thiserror::Error; 26use tracing::instrument; 27 28use crate::backend::BackendError; 29use crate::backend::MergedTreeId; 30use crate::commit::Commit; 31use crate::conflicts::ConflictMarkerStyle; 32use crate::dag_walk; 33use crate::fsmonitor::FsmonitorSettings; 34use crate::gitignore::GitIgnoreError; 35use crate::gitignore::GitIgnoreFile; 36use crate::matchers::EverythingMatcher; 37use crate::matchers::Matcher; 38use crate::op_heads_store::OpHeadsStoreError; 39use crate::op_store::OpStoreError; 40use crate::op_store::OperationId; 41use crate::operation::Operation; 42use crate::ref_name::WorkspaceName; 43use crate::ref_name::WorkspaceNameBuf; 44use crate::repo::ReadonlyRepo; 45use crate::repo::Repo as _; 46use crate::repo::RewriteRootCommit; 47use crate::repo_path::InvalidRepoPathError; 48use crate::repo_path::RepoPath; 49use crate::repo_path::RepoPathBuf; 50use crate::store::Store; 51 52/// The trait all working-copy implementations must implement. 53pub trait WorkingCopy: Send { 54 /// Should return `self`. For down-casting purposes. 55 fn as_any(&self) -> &dyn Any; 56 57 /// The name/id of the implementation. Used for choosing the right 58 /// implementation when loading a working copy. 59 fn name(&self) -> &str; 60 61 /// The working copy's workspace name (or identifier.) 62 fn workspace_name(&self) -> &WorkspaceName; 63 64 /// The operation this working copy was most recently updated to. 65 fn operation_id(&self) -> &OperationId; 66 67 /// The ID of the tree this working copy was most recently updated to. 68 fn tree_id(&self) -> Result<&MergedTreeId, WorkingCopyStateError>; 69 70 /// Patterns that decide which paths from the current tree should be checked 71 /// out in the working copy. An empty list means that no paths should be 72 /// checked out in the working copy. A single `RepoPath::root()` entry means 73 /// that all files should be checked out. 74 fn sparse_patterns(&self) -> Result<&[RepoPathBuf], WorkingCopyStateError>; 75 76 /// Locks the working copy and returns an instance with methods for updating 77 /// the working copy files and state. 78 fn start_mutation(&self) -> Result<Box<dyn LockedWorkingCopy>, WorkingCopyStateError>; 79} 80 81/// The factory which creates and loads a specific type of working copy. 82pub trait WorkingCopyFactory { 83 /// Create a new working copy from scratch. 84 fn init_working_copy( 85 &self, 86 store: Arc<Store>, 87 working_copy_path: PathBuf, 88 state_path: PathBuf, 89 operation_id: OperationId, 90 workspace_name: WorkspaceNameBuf, 91 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError>; 92 93 /// Load an existing working copy. 94 fn load_working_copy( 95 &self, 96 store: Arc<Store>, 97 working_copy_path: PathBuf, 98 state_path: PathBuf, 99 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError>; 100} 101 102/// A working copy that's being modified. 103pub trait LockedWorkingCopy { 104 /// Should return `self`. For down-casting purposes. 105 fn as_any(&self) -> &dyn Any; 106 107 /// Should return `self`. For down-casting purposes. 108 fn as_any_mut(&mut self) -> &mut dyn Any; 109 110 /// The operation at the time the lock was taken 111 fn old_operation_id(&self) -> &OperationId; 112 113 /// The tree at the time the lock was taken 114 fn old_tree_id(&self) -> &MergedTreeId; 115 116 /// Snapshot the working copy. Returns the tree id and stats. 117 fn snapshot( 118 &mut self, 119 options: &SnapshotOptions, 120 ) -> Result<(MergedTreeId, SnapshotStats), SnapshotError>; 121 122 /// Check out the specified commit in the working copy. 123 fn check_out( 124 &mut self, 125 commit: &Commit, 126 options: &CheckoutOptions, 127 ) -> Result<CheckoutStats, CheckoutError>; 128 129 /// Update the workspace name. 130 fn rename_workspace(&mut self, new_workspace_name: WorkspaceNameBuf); 131 132 /// Update to another commit without touching the files in the working copy. 133 fn reset(&mut self, commit: &Commit) -> Result<(), ResetError>; 134 135 /// Update to another commit without touching the files in the working copy, 136 /// without assuming that the previous tree exists. 137 fn recover(&mut self, commit: &Commit) -> Result<(), ResetError>; 138 139 /// See `WorkingCopy::sparse_patterns()` 140 fn sparse_patterns(&self) -> Result<&[RepoPathBuf], WorkingCopyStateError>; 141 142 /// Updates the patterns that decide which paths from the current tree 143 /// should be checked out in the working copy. 144 // TODO: Use a different error type here so we can include a 145 // `SparseNotSupported` variants for working copies that don't support sparse 146 // checkouts (e.g. because they use a virtual file system so there's no reason 147 // to use sparse). 148 fn set_sparse_patterns( 149 &mut self, 150 new_sparse_patterns: Vec<RepoPathBuf>, 151 options: &CheckoutOptions, 152 ) -> Result<CheckoutStats, CheckoutError>; 153 154 /// Finish the modifications to the working copy by writing the updated 155 /// states to disk. Returns the new (unlocked) working copy. 156 fn finish( 157 self: Box<Self>, 158 operation_id: OperationId, 159 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError>; 160} 161 162/// An error while snapshotting the working copy. 163#[derive(Debug, Error)] 164pub enum SnapshotError { 165 /// A tracked path contained invalid component such as `..`. 166 #[error(transparent)] 167 InvalidRepoPath(#[from] InvalidRepoPathError), 168 /// A path in the working copy was not valid UTF-8. 169 #[error("Working copy path {} is not valid UTF-8", path.to_string_lossy())] 170 InvalidUtf8Path { 171 /// The path with invalid UTF-8. 172 path: OsString, 173 }, 174 /// A symlink target in the working copy was not valid UTF-8. 175 #[error("Symlink {path} target is not valid UTF-8")] 176 InvalidUtf8SymlinkTarget { 177 /// The path of the symlink that has a target that's not valid UTF-8. 178 /// This path itself is valid UTF-8. 179 path: PathBuf, 180 }, 181 /// Reading or writing from the commit backend failed. 182 #[error(transparent)] 183 BackendError(#[from] BackendError), 184 /// Checking path with ignore patterns failed. 185 #[error(transparent)] 186 GitIgnoreError(#[from] GitIgnoreError), 187 /// Some other error happened while snapshotting the working copy. 188 #[error("{message}")] 189 Other { 190 /// Error message. 191 message: String, 192 /// The underlying error. 193 #[source] 194 err: Box<dyn std::error::Error + Send + Sync>, 195 }, 196} 197 198/// Options used when snapshotting the working copy. Some of them may be ignored 199/// by some `WorkingCopy` implementations. 200#[derive(Clone)] 201pub struct SnapshotOptions<'a> { 202 /// The `.gitignore`s to use while snapshotting. The typically come from the 203 /// user's configured patterns combined with per-repo patterns. 204 // The base_ignores are passed in here rather than being set on the TreeState 205 // because the TreeState may be long-lived if the library is used in a 206 // long-lived process. 207 pub base_ignores: Arc<GitIgnoreFile>, 208 /// The fsmonitor (e.g. Watchman) to use, if any. 209 // TODO: Should we make this a field on `LocalWorkingCopy` instead since it's quite specific to 210 // that implementation? 211 pub fsmonitor_settings: FsmonitorSettings, 212 /// A callback for the UI to display progress. 213 pub progress: Option<&'a SnapshotProgress<'a>>, 214 /// For new files that are not already tracked, start tracking them if they 215 /// match this. 216 pub start_tracking_matcher: &'a dyn Matcher, 217 /// The size of the largest file that should be allowed to become tracked 218 /// (already tracked files are always snapshotted). If there are larger 219 /// files in the working copy, then `LockedWorkingCopy::snapshot()` may 220 /// (depending on implementation) 221 /// return `SnapshotError::NewFileTooLarge`. 222 pub max_new_file_size: u64, 223 /// Expected conflict marker style for checking for changed files. 224 pub conflict_marker_style: ConflictMarkerStyle, 225} 226 227impl SnapshotOptions<'_> { 228 /// Create an instance for use in tests. 229 pub fn empty_for_test() -> Self { 230 SnapshotOptions { 231 base_ignores: GitIgnoreFile::empty(), 232 fsmonitor_settings: FsmonitorSettings::None, 233 progress: None, 234 start_tracking_matcher: &EverythingMatcher, 235 max_new_file_size: u64::MAX, 236 conflict_marker_style: ConflictMarkerStyle::default(), 237 } 238 } 239} 240 241/// A callback for getting progress updates. 242pub type SnapshotProgress<'a> = dyn Fn(&RepoPath) + 'a + Sync; 243 244/// Stats about a snapshot operation on a working copy. 245#[derive(Clone, Debug, Default)] 246pub struct SnapshotStats { 247 /// List of new (previously untracked) files which are still untracked. 248 pub untracked_paths: BTreeMap<RepoPathBuf, UntrackedReason>, 249} 250 251/// Reason why the new path isn't tracked. 252#[derive(Clone, Debug)] 253pub enum UntrackedReason { 254 /// File was larger than the specified maximum file size. 255 FileTooLarge { 256 /// Actual size of the large file. 257 size: u64, 258 /// Maximum allowed size. 259 max_size: u64, 260 }, 261 /// File does not match the fileset specified in snapshot.auto-track. 262 FileNotAutoTracked, 263} 264 265/// Options used when checking out a tree in the working copy. 266#[derive(Clone)] 267pub struct CheckoutOptions { 268 /// Conflict marker style to use when materializing files 269 pub conflict_marker_style: ConflictMarkerStyle, 270} 271 272impl CheckoutOptions { 273 /// Create an instance for use in tests. 274 pub fn empty_for_test() -> Self { 275 CheckoutOptions { 276 conflict_marker_style: ConflictMarkerStyle::default(), 277 } 278 } 279} 280 281/// Stats about a checkout operation on a working copy. All "files" mentioned 282/// below may also be symlinks or materialized conflicts. 283#[derive(Debug, PartialEq, Eq, Clone, Default)] 284pub struct CheckoutStats { 285 /// The number of files that were updated in the working copy. 286 /// These files existed before and after the checkout. 287 pub updated_files: u32, 288 /// The number of files added in the working copy. 289 pub added_files: u32, 290 /// The number of files removed in the working copy. 291 pub removed_files: u32, 292 /// The number of files that were supposed to be updated or added in the 293 /// working copy but were skipped because there was an untracked (probably 294 /// ignored) file in its place. 295 pub skipped_files: u32, 296} 297 298/// The working-copy checkout failed. 299#[derive(Debug, Error)] 300pub enum CheckoutError { 301 /// The current working-copy commit was deleted, maybe by an overly 302 /// aggressive GC that happened while the current process was running. 303 #[error("Current working-copy commit not found")] 304 SourceNotFound { 305 /// The underlying error. 306 source: Box<dyn std::error::Error + Send + Sync>, 307 }, 308 /// Another process checked out a commit while the current process was 309 /// running (after the working copy was read by the current process). 310 #[error("Concurrent checkout")] 311 ConcurrentCheckout, 312 /// Path in the commit contained invalid component such as `..`. 313 #[error(transparent)] 314 InvalidRepoPath(#[from] InvalidRepoPathError), 315 /// Path contained reserved name which cannot be checked out to disk. 316 #[error("Reserved path component {name} in {path}")] 317 ReservedPathComponent { 318 /// The file or directory path. 319 path: PathBuf, 320 /// The reserved path component. 321 name: &'static str, 322 }, 323 /// Reading or writing from the commit backend failed. 324 #[error("Internal backend error")] 325 InternalBackendError(#[from] BackendError), 326 /// Some other error happened while checking out the working copy. 327 #[error("{message}")] 328 Other { 329 /// Error message. 330 message: String, 331 /// The underlying error. 332 #[source] 333 err: Box<dyn std::error::Error + Send + Sync>, 334 }, 335} 336 337/// An error while resetting the working copy. 338#[derive(Debug, Error)] 339pub enum ResetError { 340 /// The current working-copy commit was deleted, maybe by an overly 341 /// aggressive GC that happened while the current process was running. 342 #[error("Current working-copy commit not found")] 343 SourceNotFound { 344 /// The underlying error. 345 source: Box<dyn std::error::Error + Send + Sync>, 346 }, 347 /// Reading or writing from the commit backend failed. 348 #[error("Internal error")] 349 InternalBackendError(#[from] BackendError), 350 /// Some other error happened while checking out the working copy. 351 #[error("{message}")] 352 Other { 353 /// Error message. 354 message: String, 355 /// The underlying error. 356 #[source] 357 err: Box<dyn std::error::Error + Send + Sync>, 358 }, 359} 360 361/// Whether the working copy is stale or not. 362#[derive(Clone, Debug, Eq, PartialEq)] 363pub enum WorkingCopyFreshness { 364 /// The working copy isn't stale, and no need to reload the repo. 365 Fresh, 366 /// The working copy was updated since we loaded the repo. The repo must be 367 /// reloaded at the working copy's operation. 368 Updated(Box<Operation>), 369 /// The working copy is behind the latest operation. 370 WorkingCopyStale, 371 /// The working copy is a sibling of the latest operation. 372 SiblingOperation, 373} 374 375impl WorkingCopyFreshness { 376 /// Determine the freshness of the provided working copy relative to the 377 /// target commit. 378 #[instrument(skip_all)] 379 pub fn check_stale( 380 locked_wc: &dyn LockedWorkingCopy, 381 wc_commit: &Commit, 382 repo: &ReadonlyRepo, 383 ) -> Result<Self, OpStoreError> { 384 // Check if the working copy's tree matches the repo's view 385 let wc_tree_id = locked_wc.old_tree_id(); 386 if wc_commit.tree_id() == wc_tree_id { 387 // The working copy isn't stale, and no need to reload the repo. 388 Ok(Self::Fresh) 389 } else { 390 let wc_operation = repo.loader().load_operation(locked_wc.old_operation_id())?; 391 let repo_operation = repo.operation(); 392 let ancestor_op = dag_walk::closest_common_node_ok( 393 [Ok(wc_operation.clone())], 394 [Ok(repo_operation.clone())], 395 |op: &Operation| op.id().clone(), 396 |op: &Operation| op.parents().collect_vec(), 397 )? 398 .expect("unrelated operations"); 399 if ancestor_op.id() == repo_operation.id() { 400 // The working copy was updated since we loaded the repo. The repo must be 401 // reloaded at the working copy's operation. 402 Ok(Self::Updated(Box::new(wc_operation))) 403 } else if ancestor_op.id() == wc_operation.id() { 404 // The working copy was not updated when some repo operation committed, 405 // meaning that it's stale compared to the repo view. 406 Ok(Self::WorkingCopyStale) 407 } else { 408 Ok(Self::SiblingOperation) 409 } 410 } 411 } 412} 413 414/// An error while recovering a stale working copy. 415#[derive(Debug, Error)] 416pub enum RecoverWorkspaceError { 417 /// Backend error. 418 #[error(transparent)] 419 Backend(#[from] BackendError), 420 /// Error during transaction. 421 #[error(transparent)] 422 OpHeadsStore(#[from] OpHeadsStoreError), 423 /// Error during checkout. 424 #[error(transparent)] 425 Reset(#[from] ResetError), 426 /// Checkout attempted to modify the root commit. 427 #[error(transparent)] 428 RewriteRootCommit(#[from] RewriteRootCommit), 429 /// Working copy commit is missing. 430 #[error(r#""{}" doesn't have a working-copy commit"#, .0.as_symbol())] 431 WorkspaceMissingWorkingCopy(WorkspaceNameBuf), 432} 433 434/// Recover this workspace to its last known checkout. 435pub fn create_and_check_out_recovery_commit( 436 locked_wc: &mut dyn LockedWorkingCopy, 437 repo: &Arc<ReadonlyRepo>, 438 workspace_name: WorkspaceNameBuf, 439 description: &str, 440) -> Result<(Arc<ReadonlyRepo>, Commit), RecoverWorkspaceError> { 441 let mut tx = repo.start_transaction(); 442 let repo_mut = tx.repo_mut(); 443 444 let commit_id = repo 445 .view() 446 .get_wc_commit_id(&workspace_name) 447 .ok_or_else(|| { 448 RecoverWorkspaceError::WorkspaceMissingWorkingCopy(workspace_name.clone()) 449 })?; 450 let commit = repo.store().get_commit(commit_id)?; 451 let new_commit = repo_mut 452 .new_commit(vec![commit_id.clone()], commit.tree_id().clone()) 453 .set_description(description) 454 .write()?; 455 repo_mut.set_wc_commit(workspace_name, new_commit.id().clone())?; 456 457 let repo = tx.commit("recovery commit")?; 458 locked_wc.recover(&new_commit)?; 459 460 Ok((repo, new_commit)) 461} 462 463/// An error while reading the working copy state. 464#[derive(Debug, Error)] 465#[error("{message}")] 466pub struct WorkingCopyStateError { 467 /// Error message. 468 pub message: String, 469 /// The underlying error. 470 #[source] 471 pub err: Box<dyn std::error::Error + Send + Sync>, 472}