Skip to main content

jj_lib/
workspace.rs

1// Copyright 2021 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#![expect(missing_docs)]
16
17use std::collections::HashMap;
18use std::fs;
19use std::io;
20use std::path::Path;
21use std::path::PathBuf;
22use std::sync::Arc;
23
24use thiserror::Error;
25
26use crate::backend::BackendInitError;
27use crate::commit::Commit;
28use crate::file_util;
29use crate::file_util::BadPathEncoding;
30use crate::file_util::IoResultExt as _;
31use crate::file_util::PathError;
32use crate::local_working_copy::LocalWorkingCopy;
33use crate::local_working_copy::LocalWorkingCopyFactory;
34use crate::merged_tree::MergedTree;
35use crate::op_heads_store::OpHeadsStoreError;
36use crate::op_store::OperationId;
37use crate::ref_name::WorkspaceName;
38use crate::ref_name::WorkspaceNameBuf;
39use crate::repo::BackendInitializer;
40use crate::repo::CheckOutCommitError;
41use crate::repo::IndexStoreInitializer;
42use crate::repo::OpHeadsStoreInitializer;
43use crate::repo::OpStoreInitializer;
44use crate::repo::ReadonlyRepo;
45use crate::repo::Repo as _;
46use crate::repo::RepoInitError;
47use crate::repo::RepoLoader;
48use crate::repo::StoreFactories;
49use crate::repo::StoreLoadError;
50use crate::repo::SubmoduleStoreInitializer;
51use crate::repo::read_store_type;
52use crate::settings::UserSettings;
53use crate::signing::SignInitError;
54use crate::signing::Signer;
55use crate::simple_backend::SimpleBackend;
56use crate::transaction::TransactionCommitError;
57use crate::working_copy::CheckoutError;
58use crate::working_copy::CheckoutStats;
59use crate::working_copy::LockedWorkingCopy;
60use crate::working_copy::WorkingCopy;
61use crate::working_copy::WorkingCopyFactory;
62use crate::working_copy::WorkingCopyStateError;
63use crate::workspace_store::SimpleWorkspaceStore;
64use crate::workspace_store::WorkspaceStore as _;
65use crate::workspace_store::WorkspaceStoreError;
66
67#[derive(Error, Debug)]
68pub enum WorkspaceInitError {
69    #[error("The destination repo ({0}) already exists")]
70    DestinationExists(PathBuf),
71    #[error("Repo path could not be encoded")]
72    EncodeRepoPath(#[source] BadPathEncoding),
73    #[error(transparent)]
74    CheckOutCommit(#[from] CheckOutCommitError),
75    #[error(transparent)]
76    WorkingCopyState(#[from] WorkingCopyStateError),
77    #[error(transparent)]
78    Path(#[from] PathError),
79    #[error(transparent)]
80    OpHeadsStore(OpHeadsStoreError),
81    #[error(transparent)]
82    WorkspaceStore(#[from] WorkspaceStoreError),
83    #[error(transparent)]
84    Backend(#[from] BackendInitError),
85    #[error(transparent)]
86    SignInit(#[from] SignInitError),
87    #[error(transparent)]
88    TransactionCommit(#[from] TransactionCommitError),
89}
90
91#[derive(Error, Debug)]
92pub enum WorkspaceLoadError {
93    #[error("The repo appears to no longer be at {0}")]
94    RepoDoesNotExist(PathBuf),
95    #[error("There is no Jujutsu repo in {0}")]
96    NoWorkspaceHere(PathBuf),
97    #[error("Cannot read the repo")]
98    StoreLoadError(#[from] StoreLoadError),
99    #[error("Repo path could not be decoded")]
100    DecodeRepoPath(#[source] BadPathEncoding),
101    #[error(transparent)]
102    WorkingCopyState(#[from] WorkingCopyStateError),
103    #[error(transparent)]
104    Path(#[from] PathError),
105}
106
107/// The combination of a repo and a working copy.
108///
109/// Represents the combination of a repo and working copy, i.e. what's typically
110/// the .jj/ directory and its parent. See
111/// <https://github.com/jj-vcs/jj/blob/main/docs/working-copy.md#workspaces>
112/// for more information.
113pub struct Workspace {
114    // Path to the workspace root (typically the parent of a .jj/ directory), which is where
115    // working copy files live.
116    workspace_root: PathBuf,
117    repo_path: PathBuf,
118    repo_loader: RepoLoader,
119    working_copy: Box<dyn WorkingCopy>,
120}
121
122fn create_jj_dir(workspace_root: &Path) -> Result<PathBuf, WorkspaceInitError> {
123    let jj_dir = workspace_root.join(".jj");
124    match std::fs::create_dir(&jj_dir).context(&jj_dir) {
125        Ok(()) => Ok(jj_dir),
126        Err(e) if e.source.kind() == io::ErrorKind::AlreadyExists => {
127            Err(WorkspaceInitError::DestinationExists(jj_dir))
128        }
129        Err(e) => Err(e.into()),
130    }
131}
132
133async fn init_working_copy(
134    repo: &Arc<ReadonlyRepo>,
135    workspace_root: &Path,
136    jj_dir: &Path,
137    working_copy_factory: &dyn WorkingCopyFactory,
138    workspace_name: WorkspaceNameBuf,
139) -> Result<(Box<dyn WorkingCopy>, Arc<ReadonlyRepo>), WorkspaceInitError> {
140    let working_copy_state_path = jj_dir.join("working_copy");
141    std::fs::create_dir(&working_copy_state_path).context(&working_copy_state_path)?;
142
143    let mut tx = repo.start_transaction();
144    tx.repo_mut()
145        .check_out(workspace_name.clone(), &repo.store().root_commit())
146        .await?;
147    let repo = tx
148        .commit(format!("add workspace '{}'", workspace_name.as_symbol()))
149        .await?;
150
151    let working_copy = working_copy_factory.init_working_copy(
152        repo.store().clone(),
153        workspace_root.to_path_buf(),
154        working_copy_state_path.clone(),
155        repo.op_id().clone(),
156        workspace_name,
157        repo.settings(),
158    )?;
159    let working_copy_type_path = working_copy_state_path.join("type");
160    fs::write(&working_copy_type_path, working_copy.name()).context(&working_copy_type_path)?;
161    Ok((working_copy, repo))
162}
163
164impl Workspace {
165    pub fn new(
166        workspace_root: &Path,
167        repo_path: PathBuf,
168        working_copy: Box<dyn WorkingCopy>,
169        repo_loader: RepoLoader,
170    ) -> Result<Self, PathError> {
171        let workspace_root = dunce::canonicalize(workspace_root).context(workspace_root)?;
172        Ok(Self::new_no_canonicalize(
173            workspace_root,
174            repo_path,
175            working_copy,
176            repo_loader,
177        ))
178    }
179
180    pub fn new_no_canonicalize(
181        workspace_root: PathBuf,
182        repo_path: PathBuf,
183        working_copy: Box<dyn WorkingCopy>,
184        repo_loader: RepoLoader,
185    ) -> Self {
186        Self {
187            workspace_root,
188            repo_path,
189            repo_loader,
190            working_copy,
191        }
192    }
193
194    pub async fn init_simple(
195        user_settings: &UserSettings,
196        workspace_root: &Path,
197    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
198        let backend_initializer: &BackendInitializer =
199            &|_settings, store_path| Ok(Box::new(SimpleBackend::init(store_path)));
200        let signer = Signer::from_settings(user_settings)?;
201        Self::init_with_backend(user_settings, workspace_root, backend_initializer, signer).await
202    }
203
204    /// Initializes a workspace with a new Git backend and bare Git repo in
205    /// `.jj/repo/store/git`.
206    #[cfg(feature = "git")]
207    pub async fn init_internal_git(
208        user_settings: &UserSettings,
209        workspace_root: &Path,
210    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
211        let backend_initializer: &BackendInitializer = &|settings, store_path| {
212            Ok(Box::new(crate::git_backend::GitBackend::init_internal(
213                settings, store_path,
214            )?))
215        };
216        let signer = Signer::from_settings(user_settings)?;
217        Self::init_with_backend(user_settings, workspace_root, backend_initializer, signer).await
218    }
219
220    /// Initializes a workspace with a new Git backend and Git repo that shares
221    /// the same working copy.
222    #[cfg(feature = "git")]
223    pub async fn init_colocated_git(
224        user_settings: &UserSettings,
225        workspace_root: &Path,
226    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
227        let backend_initializer = |settings: &UserSettings,
228                                   store_path: &Path|
229         -> Result<Box<dyn crate::backend::Backend>, _> {
230            // TODO: Clean up path normalization. store_path is canonicalized by
231            // ReadonlyRepo::init(). workspace_root will be canonicalized by
232            // Workspace::new(), but it's not yet here.
233            let store_relative_workspace_root =
234                if let Ok(workspace_root) = dunce::canonicalize(workspace_root) {
235                    crate::file_util::relative_path(store_path, &workspace_root)
236                } else {
237                    workspace_root.to_owned()
238                };
239            let backend = crate::git_backend::GitBackend::init_colocated(
240                settings,
241                store_path,
242                &store_relative_workspace_root,
243            )?;
244            Ok(Box::new(backend))
245        };
246        let signer = Signer::from_settings(user_settings)?;
247        Self::init_with_backend(user_settings, workspace_root, &backend_initializer, signer).await
248    }
249
250    /// Initializes a workspace with an existing Git repo at the specified path.
251    ///
252    /// The `git_repo_path` usually ends with `.git`. It's the path to the Git
253    /// repo directory, not the working directory.
254    #[cfg(feature = "git")]
255    pub async fn init_external_git(
256        user_settings: &UserSettings,
257        workspace_root: &Path,
258        git_repo_path: &Path,
259    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
260        let backend_initializer = |settings: &UserSettings,
261                                   store_path: &Path|
262         -> Result<Box<dyn crate::backend::Backend>, _> {
263            // If the git repo is inside the workspace, use a relative path to it so the
264            // whole workspace can be moved without breaking.
265            // TODO: Clean up path normalization. store_path is canonicalized by
266            // ReadonlyRepo::init(). workspace_root will be canonicalized by
267            // Workspace::new(), but it's not yet here.
268            let store_relative_git_repo_path = match (
269                dunce::canonicalize(workspace_root),
270                crate::git_backend::canonicalize_git_repo_path(git_repo_path),
271            ) {
272                (Ok(workspace_root), Ok(git_repo_path))
273                    if git_repo_path.starts_with(&workspace_root) =>
274                {
275                    crate::file_util::relative_path(store_path, &git_repo_path)
276                }
277                _ => git_repo_path.to_owned(),
278            };
279            let backend = crate::git_backend::GitBackend::init_external(
280                settings,
281                store_path,
282                &store_relative_git_repo_path,
283            )?;
284            Ok(Box::new(backend))
285        };
286        let signer = Signer::from_settings(user_settings)?;
287        Self::init_with_backend(user_settings, workspace_root, &backend_initializer, signer).await
288    }
289
290    #[expect(clippy::too_many_arguments)]
291    pub async fn init_with_factories(
292        user_settings: &UserSettings,
293        workspace_root: &Path,
294        backend_initializer: &BackendInitializer<'_>,
295        signer: Signer,
296        op_store_initializer: &OpStoreInitializer<'_>,
297        op_heads_store_initializer: &OpHeadsStoreInitializer<'_>,
298        index_store_initializer: &IndexStoreInitializer<'_>,
299        submodule_store_initializer: &SubmoduleStoreInitializer<'_>,
300        working_copy_factory: &dyn WorkingCopyFactory,
301        workspace_name: WorkspaceNameBuf,
302    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
303        let jj_dir = create_jj_dir(workspace_root)?;
304        async {
305            let repo_dir = jj_dir.join("repo");
306            std::fs::create_dir(&repo_dir).context(&repo_dir)?;
307            let repo = ReadonlyRepo::init(
308                user_settings,
309                &repo_dir,
310                backend_initializer,
311                signer,
312                op_store_initializer,
313                op_heads_store_initializer,
314                index_store_initializer,
315                submodule_store_initializer,
316            )
317            .await
318            .map_err(|repo_init_err| match repo_init_err {
319                RepoInitError::Backend(err) => WorkspaceInitError::Backend(err),
320                RepoInitError::OpHeadsStore(err) => WorkspaceInitError::OpHeadsStore(err),
321                RepoInitError::Path(err) => WorkspaceInitError::Path(err),
322            })?;
323            let workspace_store = SimpleWorkspaceStore::load(&repo_dir)?;
324            let (working_copy, repo) = init_working_copy(
325                &repo,
326                workspace_root,
327                &jj_dir,
328                working_copy_factory,
329                workspace_name,
330            )
331            .await?;
332            let repo_loader = repo.loader().clone();
333            let repo_dir = dunce::canonicalize(&repo_dir).context(&repo_dir)?;
334            let workspace = Self::new(workspace_root, repo_dir, working_copy, repo_loader)?;
335            workspace_store.add(workspace.workspace_name(), workspace.workspace_root())?;
336            Ok((workspace, repo))
337        }
338        .await
339        .inspect_err(|_err| {
340            std::fs::remove_dir_all(jj_dir).ok();
341        })
342    }
343
344    pub async fn init_with_backend(
345        user_settings: &UserSettings,
346        workspace_root: &Path,
347        backend_initializer: &BackendInitializer<'_>,
348        signer: Signer,
349    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
350        Self::init_with_factories(
351            user_settings,
352            workspace_root,
353            backend_initializer,
354            signer,
355            ReadonlyRepo::default_op_store_initializer(),
356            ReadonlyRepo::default_op_heads_store_initializer(),
357            ReadonlyRepo::default_index_store_initializer(),
358            ReadonlyRepo::default_submodule_store_initializer(),
359            &*default_working_copy_factory(),
360            WorkspaceName::DEFAULT.to_owned(),
361        )
362        .await
363    }
364
365    pub async fn init_workspace_with_existing_repo(
366        workspace_root: &Path,
367        repo_path: &Path,
368        repo: &Arc<ReadonlyRepo>,
369        working_copy_factory: &dyn WorkingCopyFactory,
370        workspace_name: WorkspaceNameBuf,
371    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
372        let jj_dir = create_jj_dir(workspace_root)?;
373
374        let repo_dir = dunce::canonicalize(repo_path).context(repo_path)?;
375        let jj_dir_abs = dunce::canonicalize(&jj_dir).context(&jj_dir)?;
376        let path_to_store = file_util::relative_path(&jj_dir_abs, &repo_dir);
377        let path_to_store = if path_to_store.is_relative() {
378            file_util::slash_path(&path_to_store).into_owned()
379        } else {
380            path_to_store
381        };
382        let repo_dir_bytes =
383            file_util::path_to_bytes(&path_to_store).map_err(WorkspaceInitError::EncodeRepoPath)?;
384        let repo_file_path = jj_dir.join("repo");
385        fs::write(&repo_file_path, repo_dir_bytes).context(&repo_file_path)?;
386
387        let workspace_store = SimpleWorkspaceStore::load(repo_path)?;
388        let (working_copy, repo) = init_working_copy(
389            repo,
390            workspace_root,
391            &jj_dir,
392            working_copy_factory,
393            workspace_name,
394        )
395        .await?;
396        let workspace = Self::new(
397            workspace_root,
398            repo_dir,
399            working_copy,
400            repo.loader().clone(),
401        )?;
402        workspace_store.add(workspace.workspace_name(), workspace.workspace_root())?;
403        Ok((workspace, repo))
404    }
405
406    pub fn load(
407        user_settings: &UserSettings,
408        workspace_path: &Path,
409        store_factories: &StoreFactories,
410        working_copy_factories: &WorkingCopyFactories,
411    ) -> Result<Self, WorkspaceLoadError> {
412        let loader = DefaultWorkspaceLoader::new(workspace_path)?;
413        let workspace = loader.load(user_settings, store_factories, working_copy_factories)?;
414        Ok(workspace)
415    }
416
417    pub fn workspace_root(&self) -> &Path {
418        &self.workspace_root
419    }
420
421    pub fn workspace_name(&self) -> &WorkspaceName {
422        self.working_copy.workspace_name()
423    }
424
425    pub fn repo_path(&self) -> &Path {
426        &self.repo_path
427    }
428
429    pub fn repo_loader(&self) -> &RepoLoader {
430        &self.repo_loader
431    }
432
433    /// Settings for this workspace.
434    pub fn settings(&self) -> &UserSettings {
435        self.repo_loader.settings()
436    }
437
438    pub fn working_copy(&self) -> &dyn WorkingCopy {
439        self.working_copy.as_ref()
440    }
441
442    pub fn start_working_copy_mutation(
443        &mut self,
444    ) -> Result<LockedWorkspace<'_>, WorkingCopyStateError> {
445        let locked_wc = self.working_copy.start_mutation()?;
446        Ok(LockedWorkspace {
447            base: self,
448            locked_wc,
449        })
450    }
451
452    pub async fn check_out(
453        &mut self,
454        operation_id: OperationId,
455        old_tree: Option<&MergedTree>,
456        commit: &Commit,
457    ) -> Result<CheckoutStats, CheckoutError> {
458        let mut locked_ws = self.start_working_copy_mutation()?;
459        // Check if the current working-copy commit has changed on disk compared to what
460        // the caller expected. It's safe to check out another commit
461        // regardless, but it's probably not what  the caller wanted, so we let
462        // them know.
463        if let Some(old_tree) = old_tree
464            && old_tree.tree_ids_and_labels()
465                != locked_ws.locked_wc().old_tree().tree_ids_and_labels()
466        {
467            return Err(CheckoutError::ConcurrentCheckout);
468        }
469        let stats = locked_ws.locked_wc().check_out(commit).await?;
470        locked_ws
471            .finish(operation_id)
472            .await
473            .map_err(|err| CheckoutError::Other {
474                message: "Failed to save the working copy state".to_string(),
475                err: err.into(),
476            })?;
477        Ok(stats)
478    }
479}
480
481pub struct LockedWorkspace<'a> {
482    base: &'a mut Workspace,
483    locked_wc: Box<dyn LockedWorkingCopy>,
484}
485
486impl LockedWorkspace<'_> {
487    pub fn locked_wc(&mut self) -> &mut dyn LockedWorkingCopy {
488        self.locked_wc.as_mut()
489    }
490
491    pub async fn finish(self, operation_id: OperationId) -> Result<(), WorkingCopyStateError> {
492        let new_wc = self.locked_wc.finish(operation_id).await?;
493        self.base.working_copy = new_wc;
494        Ok(())
495    }
496}
497
498// Factory trait to build WorkspaceLoaders given the workspace root.
499pub trait WorkspaceLoaderFactory {
500    fn create(&self, workspace_root: &Path)
501    -> Result<Box<dyn WorkspaceLoader>, WorkspaceLoadError>;
502}
503
504pub fn get_working_copy_factory<'a>(
505    workspace_loader: &dyn WorkspaceLoader,
506    working_copy_factories: &'a WorkingCopyFactories,
507) -> Result<&'a dyn WorkingCopyFactory, StoreLoadError> {
508    let working_copy_type = workspace_loader.get_working_copy_type()?;
509
510    if let Some(factory) = working_copy_factories.get(&working_copy_type) {
511        Ok(factory.as_ref())
512    } else {
513        Err(StoreLoadError::UnsupportedType {
514            store: "working copy",
515            store_type: working_copy_type.clone(),
516        })
517    }
518}
519
520// Loader assigned to a specific workspace root that knows how to load a
521// Workspace object for that path.
522pub trait WorkspaceLoader {
523    // The root of the Workspace to be loaded.
524    fn workspace_root(&self) -> &Path;
525
526    // The path to the repo/ dir for this Workspace.
527    fn repo_path(&self) -> &Path;
528
529    // Loads the specified Workspace with the provided factories.
530    fn load(
531        &self,
532        user_settings: &UserSettings,
533        store_factories: &StoreFactories,
534        working_copy_factories: &WorkingCopyFactories,
535    ) -> Result<Workspace, WorkspaceLoadError>;
536
537    // Returns the type identifier for the WorkingCopy trait in this Workspace.
538    fn get_working_copy_type(&self) -> Result<String, StoreLoadError>;
539}
540
541pub struct DefaultWorkspaceLoaderFactory;
542
543impl WorkspaceLoaderFactory for DefaultWorkspaceLoaderFactory {
544    fn create(
545        &self,
546        workspace_root: &Path,
547    ) -> Result<Box<dyn WorkspaceLoader>, WorkspaceLoadError> {
548        Ok(Box::new(DefaultWorkspaceLoader::new(workspace_root)?))
549    }
550}
551
552/// Helps create a `Workspace` instance by reading `.jj/repo/` and
553/// `.jj/working_copy/` from the file system.
554#[derive(Clone, Debug)]
555struct DefaultWorkspaceLoader {
556    workspace_root: PathBuf,
557    repo_path: PathBuf,
558    working_copy_state_path: PathBuf,
559}
560
561pub type WorkingCopyFactories = HashMap<String, Box<dyn WorkingCopyFactory>>;
562
563impl DefaultWorkspaceLoader {
564    pub fn new(workspace_root: &Path) -> Result<Self, WorkspaceLoadError> {
565        let jj_dir = workspace_root.join(".jj");
566        if !jj_dir.is_dir() {
567            return Err(WorkspaceLoadError::NoWorkspaceHere(
568                workspace_root.to_owned(),
569            ));
570        }
571        let mut repo_dir = jj_dir.join("repo");
572        // If .jj/repo is a file, then we interpret its contents as a relative path to
573        // the actual repo directory (typically in another workspace).
574        if repo_dir.is_file() {
575            let buf = fs::read(&repo_dir).context(&repo_dir)?;
576            let repo_path =
577                file_util::path_from_bytes(&buf).map_err(WorkspaceLoadError::DecodeRepoPath)?;
578            repo_dir = dunce::canonicalize(jj_dir.join(repo_path)).context(repo_path)?;
579            if !repo_dir.is_dir() {
580                return Err(WorkspaceLoadError::RepoDoesNotExist(repo_dir));
581            }
582        }
583        let working_copy_state_path = jj_dir.join("working_copy");
584        Ok(Self {
585            workspace_root: workspace_root.to_owned(),
586            repo_path: repo_dir,
587            working_copy_state_path,
588        })
589    }
590}
591
592impl WorkspaceLoader for DefaultWorkspaceLoader {
593    fn workspace_root(&self) -> &Path {
594        &self.workspace_root
595    }
596
597    fn repo_path(&self) -> &Path {
598        &self.repo_path
599    }
600
601    fn load(
602        &self,
603        user_settings: &UserSettings,
604        store_factories: &StoreFactories,
605        working_copy_factories: &WorkingCopyFactories,
606    ) -> Result<Workspace, WorkspaceLoadError> {
607        let repo_loader =
608            RepoLoader::init_from_file_system(user_settings, &self.repo_path, store_factories)?;
609        let working_copy_factory = get_working_copy_factory(self, working_copy_factories)?;
610        let working_copy = working_copy_factory.load_working_copy(
611            repo_loader.store().clone(),
612            self.workspace_root.clone(),
613            self.working_copy_state_path.clone(),
614            user_settings,
615        )?;
616        let workspace = Workspace::new(
617            &self.workspace_root,
618            self.repo_path.clone(),
619            working_copy,
620            repo_loader,
621        )?;
622        Ok(workspace)
623    }
624
625    fn get_working_copy_type(&self) -> Result<String, StoreLoadError> {
626        read_store_type("working copy", self.working_copy_state_path.join("type"))
627    }
628}
629
630pub fn default_working_copy_factories() -> WorkingCopyFactories {
631    let mut factories = WorkingCopyFactories::new();
632    factories.insert(
633        LocalWorkingCopy::name().to_owned(),
634        Box::new(LocalWorkingCopyFactory {}),
635    );
636    factories
637}
638
639pub fn default_working_copy_factory() -> Box<dyn WorkingCopyFactory> {
640    Box::new(LocalWorkingCopyFactory {})
641}