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