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}