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