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