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::collections::BTreeSet;
21use std::ffi::OsString;
22use std::path::PathBuf;
23use std::sync::Arc;
24
25use async_trait::async_trait;
26use thiserror::Error;
27use tracing::instrument;
28
29use crate::backend::BackendError;
30use crate::commit::Commit;
31use crate::gitignore::GitIgnoreError;
32use crate::gitignore::GitIgnoreFile;
33use crate::matchers::Matcher;
34use crate::merged_tree::MergedTree;
35use crate::op_store::OpStoreError;
36use crate::op_store::OperationId;
37use crate::op_walk;
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.
52#[async_trait(?Send)]
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    async 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 symlink target in the working copy was not valid UTF-8.
177    #[error("Symlink {path} target is not valid UTF-8")]
178    InvalidUtf8SymlinkTarget {
179        /// The path of the symlink that has a target that's not valid UTF-8.
180        /// This path itself is valid UTF-8.
181        path: PathBuf,
182    },
183    /// Reading or writing from the commit backend failed.
184    #[error(transparent)]
185    BackendError(#[from] BackendError),
186    /// Checking path with ignore patterns failed.
187    #[error(transparent)]
188    GitIgnoreError(#[from] GitIgnoreError),
189    /// Failed to load the working copy state.
190    #[error(transparent)]
191    WorkingCopyStateError(#[from] WorkingCopyStateError),
192    /// Some other error happened while snapshotting the working copy.
193    #[error("{message}")]
194    Other {
195        /// Error message.
196        message: String,
197        /// The underlying error.
198        #[source]
199        err: Box<dyn std::error::Error + Send + Sync>,
200    },
201}
202
203/// Options used when snapshotting the working copy. Some of them may be ignored
204/// by some `WorkingCopy` implementations.
205#[derive(Clone)]
206pub struct SnapshotOptions<'a> {
207    /// The `.gitignore`s to use while snapshotting. The typically come from the
208    /// user's configured patterns combined with per-repo patterns.
209    // The base_ignores are passed in here rather than being set on the TreeState
210    // because the TreeState may be long-lived if the library is used in a
211    // long-lived process.
212    pub base_ignores: Arc<GitIgnoreFile>,
213    /// A callback for the UI to display progress.
214    pub progress: Option<&'a SnapshotProgress<'a>>,
215    /// For new files that are not already tracked, start tracking them if they
216    /// match this.
217    pub start_tracking_matcher: &'a dyn Matcher,
218    /// For files that match the ignore patterns or are too large, start
219    /// tracking them anyway if they match this.
220    pub force_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    /// Paths that were skipped because their file names aren't valid UTF-8,
238    /// as (directory, file name) pairs. These paths cannot be represented as
239    /// `RepoPath`s.
240    pub invalid_utf8_paths: BTreeSet<(RepoPathBuf, OsString)>,
241}
242
243/// Reason why the new path isn't tracked.
244#[derive(Clone, Debug)]
245pub enum UntrackedReason {
246    /// File was larger than the specified maximum file size.
247    FileTooLarge {
248        /// Actual size of the large file.
249        size: u64,
250        /// Maximum allowed size.
251        max_size: u64,
252    },
253    /// File does not match the fileset specified in snapshot.auto-track.
254    FileNotAutoTracked,
255}
256
257/// Stats about a checkout operation on a working copy. All "files" mentioned
258/// below may also be symlinks or materialized conflicts.
259#[derive(Debug, PartialEq, Eq, Clone, Default)]
260pub struct CheckoutStats {
261    /// The number of files that were updated in the working copy.
262    /// These files existed before and after the checkout.
263    pub updated_files: u32,
264    /// The number of files added in the working copy.
265    pub added_files: u32,
266    /// The number of files removed in the working copy.
267    pub removed_files: u32,
268    /// The number of files that were supposed to be updated or added in the
269    /// working copy but were skipped because there was an untracked (probably
270    /// ignored) file in its place.
271    pub skipped_files: u32,
272}
273
274/// The working-copy checkout failed.
275#[derive(Debug, Error)]
276pub enum CheckoutError {
277    /// The current working-copy commit was deleted, maybe by an overly
278    /// aggressive GC that happened while the current process was running.
279    #[error("Current working-copy commit not found")]
280    SourceNotFound {
281        /// The underlying error.
282        source: Box<dyn std::error::Error + Send + Sync>,
283    },
284    /// Another process checked out a commit while the current process was
285    /// running (after the working copy was read by the current process).
286    #[error("Concurrent checkout")]
287    ConcurrentCheckout,
288    /// Path in the commit contained invalid component such as `..`.
289    #[error(transparent)]
290    InvalidRepoPath(#[from] InvalidRepoPathError),
291    /// Path contained reserved name which cannot be checked out to disk.
292    #[error("Reserved path component {name} in {path}")]
293    ReservedPathComponent {
294        /// The file or directory path.
295        path: PathBuf,
296        /// The reserved path component.
297        name: &'static str,
298    },
299    /// Reading or writing from the commit backend failed.
300    #[error("Internal backend error")]
301    InternalBackendError(#[from] BackendError),
302    /// Failed to load the working copy state.
303    #[error(transparent)]
304    WorkingCopyStateError(#[from] WorkingCopyStateError),
305    /// Some other error happened while checking out the working copy.
306    #[error("{message}")]
307    Other {
308        /// Error message.
309        message: String,
310        /// The underlying error.
311        #[source]
312        err: Box<dyn std::error::Error + Send + Sync>,
313    },
314}
315
316/// An error while resetting the working copy.
317#[derive(Debug, Error)]
318pub enum ResetError {
319    /// The current working-copy commit was deleted, maybe by an overly
320    /// aggressive GC that happened while the current process was running.
321    #[error("Current working-copy commit not found")]
322    SourceNotFound {
323        /// The underlying error.
324        source: Box<dyn std::error::Error + Send + Sync>,
325    },
326    /// Reading or writing from the commit backend failed.
327    #[error("Internal error")]
328    InternalBackendError(#[from] BackendError),
329    /// Failed to load the working copy state.
330    #[error(transparent)]
331    WorkingCopyStateError(#[from] WorkingCopyStateError),
332    /// Some other error happened while resetting the working copy.
333    #[error("{message}")]
334    Other {
335        /// Error message.
336        message: String,
337        /// The underlying error.
338        #[source]
339        err: Box<dyn std::error::Error + Send + Sync>,
340    },
341}
342
343/// Whether the working copy is stale or not.
344#[derive(Clone, Debug, Eq, PartialEq)]
345pub enum WorkingCopyFreshness {
346    /// The working copy isn't stale, and no need to reload the repo.
347    Fresh,
348    /// The working copy was updated since we loaded the repo. The repo must be
349    /// reloaded at the working copy's operation.
350    Updated(Box<Operation>),
351    /// The working copy is behind the latest operation.
352    WorkingCopyStale,
353    /// The working copy is a sibling of the latest operation.
354    SiblingOperation,
355}
356
357impl WorkingCopyFreshness {
358    /// Determine the freshness of the provided working copy relative to the
359    /// target commit.
360    #[instrument(skip_all)]
361    pub async fn check_stale(
362        locked_wc: &dyn LockedWorkingCopy,
363        wc_commit: &Commit,
364        repo: &ReadonlyRepo,
365    ) -> Result<Self, OpStoreError> {
366        // Check if the working copy's operation matches the repo's operation
367        if locked_wc.old_operation_id() == repo.op_id() {
368            // The working copy isn't stale, and no need to reload the repo.
369            Ok(Self::Fresh)
370        } else {
371            let wc_operation = repo
372                .loader()
373                .load_operation(locked_wc.old_operation_id())
374                .await?;
375            let repo_operation = repo.operation();
376            let ancestor_ops =
377                op_walk::closest_common_ancestors([wc_operation.clone()], [repo_operation.clone()])
378                    .await?;
379            // TODO: test all operations instead of using only a single common operation
380            let ancestor_op = ancestor_ops.into_iter().next().unwrap();
381            if ancestor_op.id() == repo_operation.id() {
382                // The working copy was updated since we loaded the repo. The repo must be
383                // reloaded at the working copy's operation.
384                Ok(Self::Updated(Box::new(wc_operation)))
385            } else if ancestor_op.id() == wc_operation.id() {
386                // The working copy was not updated when some repo operation committed,
387                // meaning that it's stale compared to the repo view.
388                if locked_wc.old_tree().tree_ids_and_labels()
389                    == wc_commit.tree().tree_ids_and_labels()
390                {
391                    // The working copy doesn't require any changes
392                    Ok(Self::Fresh)
393                } else {
394                    Ok(Self::WorkingCopyStale)
395                }
396            } else {
397                Ok(Self::SiblingOperation)
398            }
399        }
400    }
401}
402
403/// An error while recovering a stale working copy.
404#[derive(Debug, Error)]
405pub enum RecoverWorkspaceError {
406    /// Backend error.
407    #[error(transparent)]
408    Backend(#[from] BackendError),
409    /// Error during checkout.
410    #[error(transparent)]
411    Reset(#[from] ResetError),
412    /// Checkout attempted to modify the root commit.
413    #[error(transparent)]
414    RewriteRootCommit(#[from] RewriteRootCommit),
415    /// Error during transaction.
416    #[error(transparent)]
417    TransactionCommit(#[from] TransactionCommitError),
418    /// Working copy commit is missing.
419    #[error(r#""{}" doesn't have a working-copy commit"#, .0.as_symbol())]
420    WorkspaceMissingWorkingCopy(WorkspaceNameBuf),
421}
422
423/// Recover this workspace to its last known checkout.
424pub async fn create_and_check_out_recovery_commit(
425    locked_wc: &mut dyn LockedWorkingCopy,
426    repo: &Arc<ReadonlyRepo>,
427    workspace_name: WorkspaceNameBuf,
428    description: &str,
429) -> Result<(Arc<ReadonlyRepo>, Commit), RecoverWorkspaceError> {
430    let mut tx = repo.start_transaction();
431    let repo_mut = tx.repo_mut();
432
433    let commit_id = repo
434        .view()
435        .get_wc_commit_id(&workspace_name)
436        .ok_or_else(|| {
437            RecoverWorkspaceError::WorkspaceMissingWorkingCopy(workspace_name.clone())
438        })?;
439    let commit = repo.store().get_commit_async(commit_id).await?;
440    let new_commit = repo_mut
441        .new_commit(vec![commit_id.clone()], commit.tree())
442        .set_description(description)
443        .write()
444        .await?;
445    repo_mut.set_wc_commit(workspace_name, new_commit.id().clone())?;
446
447    let repo = tx.commit("recovery commit").await?;
448    locked_wc.recover(&new_commit).await?;
449
450    Ok((repo, new_commit))
451}
452
453/// An error while reading the working copy state.
454#[derive(Debug, Error)]
455#[error("{message}")]
456pub struct WorkingCopyStateError {
457    /// Error message.
458    pub message: String,
459    /// The underlying error.
460    #[source]
461    pub err: Box<dyn std::error::Error + Send + Sync>,
462}