Skip to main content

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