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::io;
20use std::path::Path;
21use std::path::PathBuf;
22use std::sync::Arc;
23
24use thiserror::Error;
25
26use crate::backend::BackendInitError;
27use crate::backend::MergedTreeId;
28use crate::commit::Commit;
29use crate::file_util;
30use crate::file_util::BadPathEncoding;
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::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;
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 encoded")]
69    EncodeRepoPath(#[source] BadPathEncoding),
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(OpHeadsStoreError),
78    #[error(transparent)]
79    Backend(#[from] BackendInitError),
80    #[error(transparent)]
81    SignInit(#[from] SignInitError),
82    #[error(transparent)]
83    TransactionCommit(#[from] TransactionCommitError),
84}
85
86#[derive(Error, Debug)]
87pub enum WorkspaceLoadError {
88    #[error("The repo appears to no longer be at {0}")]
89    RepoDoesNotExist(PathBuf),
90    #[error("There is no Jujutsu repo in {0}")]
91    NoWorkspaceHere(PathBuf),
92    #[error("Cannot read the repo")]
93    StoreLoadError(#[from] StoreLoadError),
94    #[error("Repo path could not be decoded")]
95    DecodeRepoPath(#[source] BadPathEncoding),
96    #[error(transparent)]
97    WorkingCopyState(#[from] WorkingCopyStateError),
98    #[error(transparent)]
99    Path(#[from] PathError),
100}
101
102/// The combination of a repo and a working copy.
103///
104/// Represents the combination of a repo and working copy, i.e. what's typically
105/// the .jj/ directory and its parent. See
106/// <https://github.com/jj-vcs/jj/blob/main/docs/working-copy.md#workspaces>
107/// for more information.
108pub struct Workspace {
109    // Path to the workspace root (typically the parent of a .jj/ directory), which is where
110    // working copy files live.
111    workspace_root: PathBuf,
112    repo_path: PathBuf,
113    repo_loader: RepoLoader,
114    working_copy: Box<dyn WorkingCopy>,
115}
116
117fn create_jj_dir(workspace_root: &Path) -> Result<PathBuf, WorkspaceInitError> {
118    let jj_dir = workspace_root.join(".jj");
119    match std::fs::create_dir(&jj_dir).context(&jj_dir) {
120        Ok(()) => Ok(jj_dir),
121        Err(ref e) if e.source.kind() == io::ErrorKind::AlreadyExists => {
122            Err(WorkspaceInitError::DestinationExists(jj_dir))
123        }
124        Err(e) => Err(e.into()),
125    }
126}
127
128fn init_working_copy(
129    repo: &Arc<ReadonlyRepo>,
130    workspace_root: &Path,
131    jj_dir: &Path,
132    working_copy_factory: &dyn WorkingCopyFactory,
133    workspace_name: WorkspaceNameBuf,
134) -> Result<(Box<dyn WorkingCopy>, Arc<ReadonlyRepo>), WorkspaceInitError> {
135    let working_copy_state_path = jj_dir.join("working_copy");
136    std::fs::create_dir(&working_copy_state_path).context(&working_copy_state_path)?;
137
138    let mut tx = repo.start_transaction();
139    tx.repo_mut()
140        .check_out(workspace_name.clone(), &repo.store().root_commit())?;
141    let repo = tx.commit(format!("add workspace '{}'", workspace_name.as_symbol()))?;
142
143    let working_copy = working_copy_factory.init_working_copy(
144        repo.store().clone(),
145        workspace_root.to_path_buf(),
146        working_copy_state_path.clone(),
147        repo.op_id().clone(),
148        workspace_name,
149        repo.settings(),
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<Self, 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 = Self::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_dir_bytes =
361            file_util::path_to_bytes(&repo_dir).map_err(WorkspaceInitError::EncodeRepoPath)?;
362        let repo_file_path = jj_dir.join("repo");
363        fs::write(&repo_file_path, repo_dir_bytes).context(&repo_file_path)?;
364
365        let (working_copy, repo) = init_working_copy(
366            repo,
367            workspace_root,
368            &jj_dir,
369            working_copy_factory,
370            workspace_name,
371        )?;
372        let workspace = Self::new(
373            workspace_root,
374            repo_dir,
375            working_copy,
376            repo.loader().clone(),
377        )?;
378        Ok((workspace, repo))
379    }
380
381    pub fn load(
382        user_settings: &UserSettings,
383        workspace_path: &Path,
384        store_factories: &StoreFactories,
385        working_copy_factories: &WorkingCopyFactories,
386    ) -> Result<Self, WorkspaceLoadError> {
387        let loader = DefaultWorkspaceLoader::new(workspace_path)?;
388        let workspace = loader.load(user_settings, store_factories, working_copy_factories)?;
389        Ok(workspace)
390    }
391
392    pub fn workspace_root(&self) -> &Path {
393        &self.workspace_root
394    }
395
396    pub fn workspace_name(&self) -> &WorkspaceName {
397        self.working_copy.workspace_name()
398    }
399
400    pub fn repo_path(&self) -> &Path {
401        &self.repo_path
402    }
403
404    pub fn repo_loader(&self) -> &RepoLoader {
405        &self.repo_loader
406    }
407
408    /// Settings for this workspace.
409    pub fn settings(&self) -> &UserSettings {
410        self.repo_loader.settings()
411    }
412
413    pub fn working_copy(&self) -> &dyn WorkingCopy {
414        self.working_copy.as_ref()
415    }
416
417    pub fn start_working_copy_mutation(
418        &mut self,
419    ) -> Result<LockedWorkspace<'_>, WorkingCopyStateError> {
420        let locked_wc = self.working_copy.start_mutation()?;
421        Ok(LockedWorkspace {
422            base: self,
423            locked_wc,
424        })
425    }
426
427    pub fn check_out(
428        &mut self,
429        operation_id: OperationId,
430        old_tree_id: Option<&MergedTreeId>,
431        commit: &Commit,
432    ) -> Result<CheckoutStats, CheckoutError> {
433        let mut locked_ws = self.start_working_copy_mutation()?;
434        // Check if the current working-copy commit has changed on disk compared to what
435        // the caller expected. It's safe to check out another commit
436        // regardless, but it's probably not what  the caller wanted, so we let
437        // them know.
438        if let Some(old_tree_id) = old_tree_id {
439            if old_tree_id != locked_ws.locked_wc().old_tree_id() {
440                return Err(CheckoutError::ConcurrentCheckout);
441            }
442        }
443        let stats = locked_ws.locked_wc().check_out(commit)?;
444        locked_ws
445            .finish(operation_id)
446            .map_err(|err| CheckoutError::Other {
447                message: "Failed to save the working copy state".to_string(),
448                err: err.into(),
449            })?;
450        Ok(stats)
451    }
452}
453
454pub struct LockedWorkspace<'a> {
455    base: &'a mut Workspace,
456    locked_wc: Box<dyn LockedWorkingCopy>,
457}
458
459impl LockedWorkspace<'_> {
460    pub fn locked_wc(&mut self) -> &mut dyn LockedWorkingCopy {
461        self.locked_wc.as_mut()
462    }
463
464    pub fn finish(self, operation_id: OperationId) -> Result<(), WorkingCopyStateError> {
465        let new_wc = self.locked_wc.finish(operation_id)?;
466        self.base.working_copy = new_wc;
467        Ok(())
468    }
469}
470
471// Factory trait to build WorkspaceLoaders given the workspace root.
472pub trait WorkspaceLoaderFactory {
473    fn create(&self, workspace_root: &Path)
474    -> Result<Box<dyn WorkspaceLoader>, WorkspaceLoadError>;
475}
476
477pub fn get_working_copy_factory<'a>(
478    workspace_loader: &dyn WorkspaceLoader,
479    working_copy_factories: &'a WorkingCopyFactories,
480) -> Result<&'a dyn WorkingCopyFactory, StoreLoadError> {
481    let working_copy_type = workspace_loader.get_working_copy_type()?;
482
483    if let Some(factory) = working_copy_factories.get(&working_copy_type) {
484        Ok(factory.as_ref())
485    } else {
486        Err(StoreLoadError::UnsupportedType {
487            store: "working copy",
488            store_type: working_copy_type.clone(),
489        })
490    }
491}
492
493// Loader assigned to a specific workspace root that knows how to load a
494// Workspace object for that path.
495pub trait WorkspaceLoader {
496    // The root of the Workspace to be loaded.
497    fn workspace_root(&self) -> &Path;
498
499    // The path to the repo/ dir for this Workspace.
500    fn repo_path(&self) -> &Path;
501
502    // Loads the specified Workspace with the provided factories.
503    fn load(
504        &self,
505        user_settings: &UserSettings,
506        store_factories: &StoreFactories,
507        working_copy_factories: &WorkingCopyFactories,
508    ) -> Result<Workspace, WorkspaceLoadError>;
509
510    // Returns the type identifier for the WorkingCopy trait in this Workspace.
511    fn get_working_copy_type(&self) -> Result<String, StoreLoadError>;
512}
513
514pub struct DefaultWorkspaceLoaderFactory;
515
516impl WorkspaceLoaderFactory for DefaultWorkspaceLoaderFactory {
517    fn create(
518        &self,
519        workspace_root: &Path,
520    ) -> Result<Box<dyn WorkspaceLoader>, WorkspaceLoadError> {
521        Ok(Box::new(DefaultWorkspaceLoader::new(workspace_root)?))
522    }
523}
524
525/// Helps create a `Workspace` instance by reading `.jj/repo/` and
526/// `.jj/working_copy/` from the file system.
527#[derive(Clone, Debug)]
528struct DefaultWorkspaceLoader {
529    workspace_root: PathBuf,
530    repo_path: PathBuf,
531    working_copy_state_path: PathBuf,
532}
533
534pub type WorkingCopyFactories = HashMap<String, Box<dyn WorkingCopyFactory>>;
535
536impl DefaultWorkspaceLoader {
537    pub fn new(workspace_root: &Path) -> Result<Self, WorkspaceLoadError> {
538        let jj_dir = workspace_root.join(".jj");
539        if !jj_dir.is_dir() {
540            return Err(WorkspaceLoadError::NoWorkspaceHere(
541                workspace_root.to_owned(),
542            ));
543        }
544        let mut repo_dir = jj_dir.join("repo");
545        // If .jj/repo is a file, then we interpret its contents as a relative path to
546        // the actual repo directory (typically in another workspace).
547        if repo_dir.is_file() {
548            let buf = fs::read(&repo_dir).context(&repo_dir)?;
549            let repo_path =
550                file_util::path_from_bytes(&buf).map_err(WorkspaceLoadError::DecodeRepoPath)?;
551            repo_dir = dunce::canonicalize(jj_dir.join(repo_path)).context(repo_path)?;
552            if !repo_dir.is_dir() {
553                return Err(WorkspaceLoadError::RepoDoesNotExist(repo_dir));
554            }
555        }
556        let working_copy_state_path = jj_dir.join("working_copy");
557        Ok(Self {
558            workspace_root: workspace_root.to_owned(),
559            repo_path: repo_dir,
560            working_copy_state_path,
561        })
562    }
563}
564
565impl WorkspaceLoader for DefaultWorkspaceLoader {
566    fn workspace_root(&self) -> &Path {
567        &self.workspace_root
568    }
569
570    fn repo_path(&self) -> &Path {
571        &self.repo_path
572    }
573
574    fn load(
575        &self,
576        user_settings: &UserSettings,
577        store_factories: &StoreFactories,
578        working_copy_factories: &WorkingCopyFactories,
579    ) -> Result<Workspace, WorkspaceLoadError> {
580        let repo_loader =
581            RepoLoader::init_from_file_system(user_settings, &self.repo_path, store_factories)?;
582        let working_copy_factory = get_working_copy_factory(self, working_copy_factories)?;
583        let working_copy = working_copy_factory.load_working_copy(
584            repo_loader.store().clone(),
585            self.workspace_root.clone(),
586            self.working_copy_state_path.clone(),
587            user_settings,
588        )?;
589        let workspace = Workspace::new(
590            &self.workspace_root,
591            self.repo_path.clone(),
592            working_copy,
593            repo_loader,
594        )?;
595        Ok(workspace)
596    }
597
598    fn get_working_copy_type(&self) -> Result<String, StoreLoadError> {
599        read_store_type("working copy", self.working_copy_state_path.join("type"))
600    }
601}
602
603pub fn default_working_copy_factories() -> WorkingCopyFactories {
604    let mut factories = WorkingCopyFactories::new();
605    factories.insert(
606        LocalWorkingCopy::name().to_owned(),
607        Box::new(LocalWorkingCopyFactory {}),
608    );
609    factories
610}
611
612pub fn default_working_copy_factory() -> Box<dyn WorkingCopyFactory> {
613    Box::new(LocalWorkingCopyFactory {})
614}