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