jj_lib/working_copy.rs
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::dag_walk;
32use crate::fsmonitor::FsmonitorSettings;
33use crate::gitignore::GitIgnoreError;
34use crate::gitignore::GitIgnoreFile;
35use crate::matchers::EverythingMatcher;
36use crate::matchers::Matcher;
37use crate::op_store::OpStoreError;
38use crate::op_store::OperationId;
39use crate::operation::Operation;
40use crate::ref_name::WorkspaceName;
41use crate::ref_name::WorkspaceNameBuf;
42use crate::repo::ReadonlyRepo;
43use crate::repo::Repo as _;
44use crate::repo::RewriteRootCommit;
45use crate::repo_path::InvalidRepoPathError;
46use crate::repo_path::RepoPath;
47use crate::repo_path::RepoPathBuf;
48use crate::settings::UserSettings;
49use crate::store::Store;
50use crate::transaction::TransactionCommitError;
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 settings: &UserSettings,
92 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError>;
93
94 /// Load an existing working copy.
95 fn load_working_copy(
96 &self,
97 store: Arc<Store>,
98 working_copy_path: PathBuf,
99 state_path: PathBuf,
100 settings: &UserSettings,
101 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError>;
102}
103
104/// A working copy that's being modified.
105pub trait LockedWorkingCopy {
106 /// Should return `self`. For down-casting purposes.
107 fn as_any(&self) -> &dyn Any;
108
109 /// Should return `self`. For down-casting purposes.
110 fn as_any_mut(&mut self) -> &mut dyn Any;
111
112 /// The operation at the time the lock was taken
113 fn old_operation_id(&self) -> &OperationId;
114
115 /// The tree at the time the lock was taken
116 fn old_tree_id(&self) -> &MergedTreeId;
117
118 /// Snapshot the working copy. Returns the tree id and stats.
119 fn snapshot(
120 &mut self,
121 options: &SnapshotOptions,
122 ) -> Result<(MergedTreeId, SnapshotStats), SnapshotError>;
123
124 /// Check out the specified commit in the working copy.
125 fn check_out(&mut self, commit: &Commit) -> Result<CheckoutStats, CheckoutError>;
126
127 /// Update the workspace name.
128 fn rename_workspace(&mut self, new_workspace_name: WorkspaceNameBuf);
129
130 /// Update to another commit without touching the files in the working copy.
131 fn reset(&mut self, commit: &Commit) -> Result<(), ResetError>;
132
133 /// Update to another commit without touching the files in the working copy,
134 /// without assuming that the previous tree exists.
135 fn recover(&mut self, commit: &Commit) -> Result<(), ResetError>;
136
137 /// See `WorkingCopy::sparse_patterns()`
138 fn sparse_patterns(&self) -> Result<&[RepoPathBuf], WorkingCopyStateError>;
139
140 /// Updates the patterns that decide which paths from the current tree
141 /// should be checked out in the working copy.
142 // TODO: Use a different error type here so we can include a
143 // `SparseNotSupported` variants for working copies that don't support sparse
144 // checkouts (e.g. because they use a virtual file system so there's no reason
145 // to use sparse).
146 fn set_sparse_patterns(
147 &mut self,
148 new_sparse_patterns: Vec<RepoPathBuf>,
149 ) -> Result<CheckoutStats, CheckoutError>;
150
151 /// Finish the modifications to the working copy by writing the updated
152 /// states to disk. Returns the new (unlocked) working copy.
153 fn finish(
154 self: Box<Self>,
155 operation_id: OperationId,
156 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError>;
157}
158
159/// An error while snapshotting the working copy.
160#[derive(Debug, Error)]
161pub enum SnapshotError {
162 /// A tracked path contained invalid component such as `..`.
163 #[error(transparent)]
164 InvalidRepoPath(#[from] InvalidRepoPathError),
165 /// A path in the working copy was not valid UTF-8.
166 #[error("Working copy path {} is not valid UTF-8", path.to_string_lossy())]
167 InvalidUtf8Path {
168 /// The path with invalid UTF-8.
169 path: OsString,
170 },
171 /// A symlink target in the working copy was not valid UTF-8.
172 #[error("Symlink {path} target is not valid UTF-8")]
173 InvalidUtf8SymlinkTarget {
174 /// The path of the symlink that has a target that's not valid UTF-8.
175 /// This path itself is valid UTF-8.
176 path: PathBuf,
177 },
178 /// Reading or writing from the commit backend failed.
179 #[error(transparent)]
180 BackendError(#[from] BackendError),
181 /// Checking path with ignore patterns failed.
182 #[error(transparent)]
183 GitIgnoreError(#[from] GitIgnoreError),
184 /// Failed to load the working copy state.
185 #[error(transparent)]
186 WorkingCopyStateError(#[from] WorkingCopyStateError),
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}
224
225impl SnapshotOptions<'_> {
226 /// Create an instance for use in tests.
227 pub fn empty_for_test() -> Self {
228 Self {
229 base_ignores: GitIgnoreFile::empty(),
230 fsmonitor_settings: FsmonitorSettings::None,
231 progress: None,
232 start_tracking_matcher: &EverythingMatcher,
233 max_new_file_size: u64::MAX,
234 }
235 }
236}
237
238/// A callback for getting progress updates.
239pub type SnapshotProgress<'a> = dyn Fn(&RepoPath) + 'a + Sync;
240
241/// Stats about a snapshot operation on a working copy.
242#[derive(Clone, Debug, Default)]
243pub struct SnapshotStats {
244 /// List of new (previously untracked) files which are still untracked.
245 pub untracked_paths: BTreeMap<RepoPathBuf, UntrackedReason>,
246}
247
248/// Reason why the new path isn't tracked.
249#[derive(Clone, Debug)]
250pub enum UntrackedReason {
251 /// File was larger than the specified maximum file size.
252 FileTooLarge {
253 /// Actual size of the large file.
254 size: u64,
255 /// Maximum allowed size.
256 max_size: u64,
257 },
258 /// File does not match the fileset specified in snapshot.auto-track.
259 FileNotAutoTracked,
260}
261
262/// Stats about a checkout operation on a working copy. All "files" mentioned
263/// below may also be symlinks or materialized conflicts.
264#[derive(Debug, PartialEq, Eq, Clone, Default)]
265pub struct CheckoutStats {
266 /// The number of files that were updated in the working copy.
267 /// These files existed before and after the checkout.
268 pub updated_files: u32,
269 /// The number of files added in the working copy.
270 pub added_files: u32,
271 /// The number of files removed in the working copy.
272 pub removed_files: u32,
273 /// The number of files that were supposed to be updated or added in the
274 /// working copy but were skipped because there was an untracked (probably
275 /// ignored) file in its place.
276 pub skipped_files: u32,
277}
278
279/// The working-copy checkout failed.
280#[derive(Debug, Error)]
281pub enum CheckoutError {
282 /// The current working-copy commit was deleted, maybe by an overly
283 /// aggressive GC that happened while the current process was running.
284 #[error("Current working-copy commit not found")]
285 SourceNotFound {
286 /// The underlying error.
287 source: Box<dyn std::error::Error + Send + Sync>,
288 },
289 /// Another process checked out a commit while the current process was
290 /// running (after the working copy was read by the current process).
291 #[error("Concurrent checkout")]
292 ConcurrentCheckout,
293 /// Path in the commit contained invalid component such as `..`.
294 #[error(transparent)]
295 InvalidRepoPath(#[from] InvalidRepoPathError),
296 /// Path contained reserved name which cannot be checked out to disk.
297 #[error("Reserved path component {name} in {path}")]
298 ReservedPathComponent {
299 /// The file or directory path.
300 path: PathBuf,
301 /// The reserved path component.
302 name: &'static str,
303 },
304 /// Reading or writing from the commit backend failed.
305 #[error("Internal backend error")]
306 InternalBackendError(#[from] BackendError),
307 /// Failed to load the working copy state.
308 #[error(transparent)]
309 WorkingCopyStateError(#[from] WorkingCopyStateError),
310 /// Some other error happened while checking out the working copy.
311 #[error("{message}")]
312 Other {
313 /// Error message.
314 message: String,
315 /// The underlying error.
316 #[source]
317 err: Box<dyn std::error::Error + Send + Sync>,
318 },
319}
320
321/// An error while resetting the working copy.
322#[derive(Debug, Error)]
323pub enum ResetError {
324 /// The current working-copy commit was deleted, maybe by an overly
325 /// aggressive GC that happened while the current process was running.
326 #[error("Current working-copy commit not found")]
327 SourceNotFound {
328 /// The underlying error.
329 source: Box<dyn std::error::Error + Send + Sync>,
330 },
331 /// Reading or writing from the commit backend failed.
332 #[error("Internal error")]
333 InternalBackendError(#[from] BackendError),
334 /// Failed to load the working copy state.
335 #[error(transparent)]
336 WorkingCopyStateError(#[from] WorkingCopyStateError),
337 /// Some other error happened while resetting the working copy.
338 #[error("{message}")]
339 Other {
340 /// Error message.
341 message: String,
342 /// The underlying error.
343 #[source]
344 err: Box<dyn std::error::Error + Send + Sync>,
345 },
346}
347
348/// Whether the working copy is stale or not.
349#[derive(Clone, Debug, Eq, PartialEq)]
350pub enum WorkingCopyFreshness {
351 /// The working copy isn't stale, and no need to reload the repo.
352 Fresh,
353 /// The working copy was updated since we loaded the repo. The repo must be
354 /// reloaded at the working copy's operation.
355 Updated(Box<Operation>),
356 /// The working copy is behind the latest operation.
357 WorkingCopyStale,
358 /// The working copy is a sibling of the latest operation.
359 SiblingOperation,
360}
361
362impl WorkingCopyFreshness {
363 /// Determine the freshness of the provided working copy relative to the
364 /// target commit.
365 #[instrument(skip_all)]
366 pub fn check_stale(
367 locked_wc: &dyn LockedWorkingCopy,
368 wc_commit: &Commit,
369 repo: &ReadonlyRepo,
370 ) -> Result<Self, OpStoreError> {
371 // Check if the working copy's tree matches the repo's view
372 let wc_tree_id = locked_wc.old_tree_id();
373 if wc_commit.tree_id() == wc_tree_id {
374 // The working copy isn't stale, and no need to reload the repo.
375 Ok(Self::Fresh)
376 } else {
377 let wc_operation = repo.loader().load_operation(locked_wc.old_operation_id())?;
378 let repo_operation = repo.operation();
379 let ancestor_op = dag_walk::closest_common_node_ok(
380 [Ok(wc_operation.clone())],
381 [Ok(repo_operation.clone())],
382 |op: &Operation| op.id().clone(),
383 |op: &Operation| op.parents().collect_vec(),
384 )?
385 .expect("unrelated operations");
386 if ancestor_op.id() == repo_operation.id() {
387 // The working copy was updated since we loaded the repo. The repo must be
388 // reloaded at the working copy's operation.
389 Ok(Self::Updated(Box::new(wc_operation)))
390 } else if ancestor_op.id() == wc_operation.id() {
391 // The working copy was not updated when some repo operation committed,
392 // meaning that it's stale compared to the repo view.
393 Ok(Self::WorkingCopyStale)
394 } else {
395 Ok(Self::SiblingOperation)
396 }
397 }
398 }
399}
400
401/// An error while recovering a stale working copy.
402#[derive(Debug, Error)]
403pub enum RecoverWorkspaceError {
404 /// Backend error.
405 #[error(transparent)]
406 Backend(#[from] BackendError),
407 /// Error during checkout.
408 #[error(transparent)]
409 Reset(#[from] ResetError),
410 /// Checkout attempted to modify the root commit.
411 #[error(transparent)]
412 RewriteRootCommit(#[from] RewriteRootCommit),
413 /// Error during transaction.
414 #[error(transparent)]
415 TransactionCommit(#[from] TransactionCommitError),
416 /// Working copy commit is missing.
417 #[error(r#""{}" doesn't have a working-copy commit"#, .0.as_symbol())]
418 WorkspaceMissingWorkingCopy(WorkspaceNameBuf),
419}
420
421/// Recover this workspace to its last known checkout.
422pub fn create_and_check_out_recovery_commit(
423 locked_wc: &mut dyn LockedWorkingCopy,
424 repo: &Arc<ReadonlyRepo>,
425 workspace_name: WorkspaceNameBuf,
426 description: &str,
427) -> Result<(Arc<ReadonlyRepo>, Commit), RecoverWorkspaceError> {
428 let mut tx = repo.start_transaction();
429 let repo_mut = tx.repo_mut();
430
431 let commit_id = repo
432 .view()
433 .get_wc_commit_id(&workspace_name)
434 .ok_or_else(|| {
435 RecoverWorkspaceError::WorkspaceMissingWorkingCopy(workspace_name.clone())
436 })?;
437 let commit = repo.store().get_commit(commit_id)?;
438 let new_commit = repo_mut
439 .new_commit(vec![commit_id.clone()], commit.tree_id().clone())
440 .set_description(description)
441 .write()?;
442 repo_mut.set_wc_commit(workspace_name, new_commit.id().clone())?;
443
444 let repo = tx.commit("recovery commit")?;
445 locked_wc.recover(&new_commit)?;
446
447 Ok((repo, new_commit))
448}
449
450/// An error while reading the working copy state.
451#[derive(Debug, Error)]
452#[error("{message}")]
453pub struct WorkingCopyStateError {
454 /// Error message.
455 pub message: String,
456 /// The underlying error.
457 #[source]
458 pub err: Box<dyn std::error::Error + Send + Sync>,
459}