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