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