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