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::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 encoded")]
70    EncodeRepoPath(#[source] BadPathEncoding),
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 decoded")]
96    DecodeRepoPath(#[source] BadPathEncoding),
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_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 = Workspace::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        options: &CheckoutOptions,
433    ) -> Result<CheckoutStats, CheckoutError> {
434        let mut locked_ws =
435            self.start_working_copy_mutation()
436                .map_err(|err| CheckoutError::Other {
437                    message: "Failed to start editing the working copy state".to_string(),
438                    err: err.into(),
439                })?;
440        // Check if the current working-copy commit has changed on disk compared to what
441        // the caller expected. It's safe to check out another commit
442        // regardless, but it's probably not what  the caller wanted, so we let
443        // them know.
444        if let Some(old_tree_id) = old_tree_id {
445            if old_tree_id != locked_ws.locked_wc().old_tree_id() {
446                return Err(CheckoutError::ConcurrentCheckout);
447            }
448        }
449        let stats = locked_ws.locked_wc().check_out(commit, options)?;
450        locked_ws
451            .finish(operation_id)
452            .map_err(|err| CheckoutError::Other {
453                message: "Failed to save the working copy state".to_string(),
454                err: err.into(),
455            })?;
456        Ok(stats)
457    }
458}
459
460pub struct LockedWorkspace<'a> {
461    base: &'a mut Workspace,
462    locked_wc: Box<dyn LockedWorkingCopy>,
463}
464
465impl LockedWorkspace<'_> {
466    pub fn locked_wc(&mut self) -> &mut dyn LockedWorkingCopy {
467        self.locked_wc.as_mut()
468    }
469
470    pub fn finish(self, operation_id: OperationId) -> Result<(), WorkingCopyStateError> {
471        let new_wc = self.locked_wc.finish(operation_id)?;
472        self.base.working_copy = new_wc;
473        Ok(())
474    }
475}
476
477// Factory trait to build WorkspaceLoaders given the workspace root.
478pub trait WorkspaceLoaderFactory {
479    fn create(&self, workspace_root: &Path)
480        -> Result<Box<dyn WorkspaceLoader>, WorkspaceLoadError>;
481}
482
483pub fn get_working_copy_factory<'a>(
484    workspace_loader: &dyn WorkspaceLoader,
485    working_copy_factories: &'a WorkingCopyFactories,
486) -> Result<&'a dyn WorkingCopyFactory, StoreLoadError> {
487    let working_copy_type = workspace_loader.get_working_copy_type()?;
488
489    if let Some(factory) = working_copy_factories.get(&working_copy_type) {
490        Ok(factory.as_ref())
491    } else {
492        Err(StoreLoadError::UnsupportedType {
493            store: "working copy",
494            store_type: working_copy_type.to_string(),
495        })
496    }
497}
498
499// Loader assigned to a specific workspace root that knows how to load a
500// Workspace object for that path.
501pub trait WorkspaceLoader {
502    // The root of the Workspace to be loaded.
503    fn workspace_root(&self) -> &Path;
504
505    // The path to the repo/ dir for this Workspace.
506    fn repo_path(&self) -> &Path;
507
508    // Loads the specified Workspace with the provided factories.
509    fn load(
510        &self,
511        user_settings: &UserSettings,
512        store_factories: &StoreFactories,
513        working_copy_factories: &WorkingCopyFactories,
514    ) -> Result<Workspace, WorkspaceLoadError>;
515
516    // Returns the type identifier for the WorkingCopy trait in this Workspace.
517    fn get_working_copy_type(&self) -> Result<String, StoreLoadError>;
518}
519
520pub struct DefaultWorkspaceLoaderFactory;
521
522impl WorkspaceLoaderFactory for DefaultWorkspaceLoaderFactory {
523    fn create(
524        &self,
525        workspace_root: &Path,
526    ) -> Result<Box<dyn WorkspaceLoader>, WorkspaceLoadError> {
527        Ok(Box::new(DefaultWorkspaceLoader::new(workspace_root)?))
528    }
529}
530
531/// Helps create a `Workspace` instance by reading `.jj/repo/` and
532/// `.jj/working_copy/` from the file system.
533#[derive(Clone, Debug)]
534struct DefaultWorkspaceLoader {
535    workspace_root: PathBuf,
536    repo_path: PathBuf,
537    working_copy_state_path: PathBuf,
538}
539
540pub type WorkingCopyFactories = HashMap<String, Box<dyn WorkingCopyFactory>>;
541
542impl DefaultWorkspaceLoader {
543    pub fn new(workspace_root: &Path) -> Result<Self, WorkspaceLoadError> {
544        let jj_dir = workspace_root.join(".jj");
545        if !jj_dir.is_dir() {
546            return Err(WorkspaceLoadError::NoWorkspaceHere(
547                workspace_root.to_owned(),
548            ));
549        }
550        let mut repo_dir = jj_dir.join("repo");
551        // If .jj/repo is a file, then we interpret its contents as a relative path to
552        // the actual repo directory (typically in another workspace).
553        if repo_dir.is_file() {
554            let buf = fs::read(&repo_dir).context(&repo_dir)?;
555            let repo_path =
556                file_util::path_from_bytes(&buf).map_err(WorkspaceLoadError::DecodeRepoPath)?;
557            repo_dir = dunce::canonicalize(jj_dir.join(repo_path)).context(repo_path)?;
558            if !repo_dir.is_dir() {
559                return Err(WorkspaceLoadError::RepoDoesNotExist(repo_dir));
560            }
561        }
562        let working_copy_state_path = jj_dir.join("working_copy");
563        Ok(Self {
564            workspace_root: workspace_root.to_owned(),
565            repo_path: repo_dir,
566            working_copy_state_path,
567        })
568    }
569}
570
571impl WorkspaceLoader for DefaultWorkspaceLoader {
572    fn workspace_root(&self) -> &Path {
573        &self.workspace_root
574    }
575
576    fn repo_path(&self) -> &Path {
577        &self.repo_path
578    }
579
580    fn load(
581        &self,
582        user_settings: &UserSettings,
583        store_factories: &StoreFactories,
584        working_copy_factories: &WorkingCopyFactories,
585    ) -> Result<Workspace, WorkspaceLoadError> {
586        let repo_loader =
587            RepoLoader::init_from_file_system(user_settings, &self.repo_path, store_factories)?;
588        let working_copy_factory = get_working_copy_factory(self, working_copy_factories)?;
589        let working_copy = working_copy_factory.load_working_copy(
590            repo_loader.store().clone(),
591            self.workspace_root.clone(),
592            self.working_copy_state_path.clone(),
593        )?;
594        let workspace = Workspace::new(
595            &self.workspace_root,
596            self.repo_path.clone(),
597            working_copy,
598            repo_loader,
599        )?;
600        Ok(workspace)
601    }
602
603    fn get_working_copy_type(&self) -> Result<String, StoreLoadError> {
604        read_store_type("working copy", self.working_copy_state_path.join("type"))
605    }
606}
607
608pub fn default_working_copy_factories() -> WorkingCopyFactories {
609    let mut factories = WorkingCopyFactories::new();
610    factories.insert(
611        LocalWorkingCopy::name().to_owned(),
612        Box::new(LocalWorkingCopyFactory {}),
613    );
614    factories
615}
616
617pub fn default_working_copy_factory() -> Box<dyn WorkingCopyFactory> {
618    Box::new(LocalWorkingCopyFactory {})
619}