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