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