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::backend::MergedTreeId;
29use crate::commit::Commit;
30use crate::file_util;
31use crate::file_util::BadPathEncoding;
32use crate::file_util::IoResultExt as _;
33use crate::file_util::PathError;
34use crate::local_working_copy::LocalWorkingCopy;
35use crate::local_working_copy::LocalWorkingCopyFactory;
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;
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.source.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        repo.settings(),
151    )?;
152    let working_copy_type_path = working_copy_state_path.join("type");
153    fs::write(&working_copy_type_path, working_copy.name()).context(&working_copy_type_path)?;
154    Ok((working_copy, repo))
155}
156
157impl Workspace {
158    pub fn new(
159        workspace_root: &Path,
160        repo_path: PathBuf,
161        working_copy: Box<dyn WorkingCopy>,
162        repo_loader: RepoLoader,
163    ) -> Result<Self, PathError> {
164        let workspace_root = dunce::canonicalize(workspace_root).context(workspace_root)?;
165        Ok(Self::new_no_canonicalize(
166            workspace_root,
167            repo_path,
168            working_copy,
169            repo_loader,
170        ))
171    }
172
173    pub fn new_no_canonicalize(
174        workspace_root: PathBuf,
175        repo_path: PathBuf,
176        working_copy: Box<dyn WorkingCopy>,
177        repo_loader: RepoLoader,
178    ) -> Self {
179        Self {
180            workspace_root,
181            repo_path,
182            repo_loader,
183            working_copy,
184        }
185    }
186
187    pub fn init_simple(
188        user_settings: &UserSettings,
189        workspace_root: &Path,
190    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
191        let backend_initializer: &BackendInitializer =
192            &|_settings, store_path| Ok(Box::new(SimpleBackend::init(store_path)));
193        let signer = Signer::from_settings(user_settings)?;
194        Self::init_with_backend(user_settings, workspace_root, backend_initializer, signer)
195    }
196
197    /// Initializes a workspace with a new Git backend and bare Git repo in
198    /// `.jj/repo/store/git`.
199    #[cfg(feature = "git")]
200    pub fn init_internal_git(
201        user_settings: &UserSettings,
202        workspace_root: &Path,
203    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
204        let backend_initializer: &BackendInitializer = &|settings, store_path| {
205            Ok(Box::new(crate::git_backend::GitBackend::init_internal(
206                settings, store_path,
207            )?))
208        };
209        let signer = Signer::from_settings(user_settings)?;
210        Self::init_with_backend(user_settings, workspace_root, backend_initializer, signer)
211    }
212
213    /// Initializes a workspace with a new Git backend and Git repo that shares
214    /// the same working copy.
215    #[cfg(feature = "git")]
216    pub fn init_colocated_git(
217        user_settings: &UserSettings,
218        workspace_root: &Path,
219    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
220        let backend_initializer = |settings: &UserSettings,
221                                   store_path: &Path|
222         -> Result<Box<dyn crate::backend::Backend>, _> {
223            // TODO: Clean up path normalization. store_path is canonicalized by
224            // ReadonlyRepo::init(). workspace_root will be canonicalized by
225            // Workspace::new(), but it's not yet here.
226            let store_relative_workspace_root =
227                if let Ok(workspace_root) = dunce::canonicalize(workspace_root) {
228                    crate::file_util::relative_path(store_path, &workspace_root)
229                } else {
230                    workspace_root.to_owned()
231                };
232            let backend = crate::git_backend::GitBackend::init_colocated(
233                settings,
234                store_path,
235                &store_relative_workspace_root,
236            )?;
237            Ok(Box::new(backend))
238        };
239        let signer = Signer::from_settings(user_settings)?;
240        Self::init_with_backend(user_settings, workspace_root, &backend_initializer, signer)
241    }
242
243    /// Initializes a workspace with an existing Git repo at the specified path.
244    ///
245    /// The `git_repo_path` usually ends with `.git`. It's the path to the Git
246    /// repo directory, not the working directory.
247    #[cfg(feature = "git")]
248    pub fn init_external_git(
249        user_settings: &UserSettings,
250        workspace_root: &Path,
251        git_repo_path: &Path,
252    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
253        let backend_initializer = |settings: &UserSettings,
254                                   store_path: &Path|
255         -> Result<Box<dyn crate::backend::Backend>, _> {
256            // If the git repo is inside the workspace, use a relative path to it so the
257            // whole workspace can be moved without breaking.
258            // TODO: Clean up path normalization. store_path is canonicalized by
259            // ReadonlyRepo::init(). workspace_root will be canonicalized by
260            // Workspace::new(), but it's not yet here.
261            let store_relative_git_repo_path = match (
262                dunce::canonicalize(workspace_root),
263                crate::git_backend::canonicalize_git_repo_path(git_repo_path),
264            ) {
265                (Ok(workspace_root), Ok(git_repo_path))
266                    if git_repo_path.starts_with(&workspace_root) =>
267                {
268                    crate::file_util::relative_path(store_path, &git_repo_path)
269                }
270                _ => git_repo_path.to_owned(),
271            };
272            let backend = crate::git_backend::GitBackend::init_external(
273                settings,
274                store_path,
275                &store_relative_git_repo_path,
276            )?;
277            Ok(Box::new(backend))
278        };
279        let signer = Signer::from_settings(user_settings)?;
280        Self::init_with_backend(user_settings, workspace_root, &backend_initializer, signer)
281    }
282
283    #[expect(clippy::too_many_arguments)]
284    pub fn init_with_factories(
285        user_settings: &UserSettings,
286        workspace_root: &Path,
287        backend_initializer: &BackendInitializer,
288        signer: Signer,
289        op_store_initializer: &OpStoreInitializer,
290        op_heads_store_initializer: &OpHeadsStoreInitializer,
291        index_store_initializer: &IndexStoreInitializer,
292        submodule_store_initializer: &SubmoduleStoreInitializer,
293        working_copy_factory: &dyn WorkingCopyFactory,
294        workspace_name: WorkspaceNameBuf,
295    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
296        let jj_dir = create_jj_dir(workspace_root)?;
297        (|| {
298            let repo_dir = jj_dir.join("repo");
299            std::fs::create_dir(&repo_dir).context(&repo_dir)?;
300            let repo = ReadonlyRepo::init(
301                user_settings,
302                &repo_dir,
303                backend_initializer,
304                signer,
305                op_store_initializer,
306                op_heads_store_initializer,
307                index_store_initializer,
308                submodule_store_initializer,
309            )
310            .map_err(|repo_init_err| match repo_init_err {
311                RepoInitError::Backend(err) => WorkspaceInitError::Backend(err),
312                RepoInitError::OpHeadsStore(err) => WorkspaceInitError::OpHeadsStore(err),
313                RepoInitError::Path(err) => WorkspaceInitError::Path(err),
314            })?;
315            let (working_copy, repo) = init_working_copy(
316                &repo,
317                workspace_root,
318                &jj_dir,
319                working_copy_factory,
320                workspace_name,
321            )?;
322            let repo_loader = repo.loader().clone();
323            let workspace = Self::new(workspace_root, repo_dir, working_copy, repo_loader)?;
324            Ok((workspace, repo))
325        })()
326        .inspect_err(|_err| {
327            let _ = std::fs::remove_dir_all(jj_dir);
328        })
329    }
330
331    pub fn init_with_backend(
332        user_settings: &UserSettings,
333        workspace_root: &Path,
334        backend_initializer: &BackendInitializer,
335        signer: Signer,
336    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
337        Self::init_with_factories(
338            user_settings,
339            workspace_root,
340            backend_initializer,
341            signer,
342            ReadonlyRepo::default_op_store_initializer(),
343            ReadonlyRepo::default_op_heads_store_initializer(),
344            ReadonlyRepo::default_index_store_initializer(),
345            ReadonlyRepo::default_submodule_store_initializer(),
346            &*default_working_copy_factory(),
347            WorkspaceName::DEFAULT.to_owned(),
348        )
349    }
350
351    pub fn init_workspace_with_existing_repo(
352        workspace_root: &Path,
353        repo_path: &Path,
354        repo: &Arc<ReadonlyRepo>,
355        working_copy_factory: &dyn WorkingCopyFactory,
356        workspace_name: WorkspaceNameBuf,
357    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
358        let jj_dir = create_jj_dir(workspace_root)?;
359
360        let repo_dir = dunce::canonicalize(repo_path).context(repo_path)?;
361        let repo_dir_bytes =
362            file_util::path_to_bytes(&repo_dir).map_err(WorkspaceInitError::EncodeRepoPath)?;
363        let repo_file_path = jj_dir.join("repo");
364        fs::write(&repo_file_path, repo_dir_bytes).context(&repo_file_path)?;
365
366        let (working_copy, repo) = init_working_copy(
367            repo,
368            workspace_root,
369            &jj_dir,
370            working_copy_factory,
371            workspace_name,
372        )?;
373        let workspace = Self::new(
374            workspace_root,
375            repo_dir,
376            working_copy,
377            repo.loader().clone(),
378        )?;
379        Ok((workspace, repo))
380    }
381
382    pub fn load(
383        user_settings: &UserSettings,
384        workspace_path: &Path,
385        store_factories: &StoreFactories,
386        working_copy_factories: &WorkingCopyFactories,
387    ) -> Result<Self, WorkspaceLoadError> {
388        let loader = DefaultWorkspaceLoader::new(workspace_path)?;
389        let workspace = loader.load(user_settings, store_factories, working_copy_factories)?;
390        Ok(workspace)
391    }
392
393    pub fn workspace_root(&self) -> &Path {
394        &self.workspace_root
395    }
396
397    pub fn workspace_name(&self) -> &WorkspaceName {
398        self.working_copy.workspace_name()
399    }
400
401    pub fn repo_path(&self) -> &Path {
402        &self.repo_path
403    }
404
405    pub fn repo_loader(&self) -> &RepoLoader {
406        &self.repo_loader
407    }
408
409    /// Settings for this workspace.
410    pub fn settings(&self) -> &UserSettings {
411        self.repo_loader.settings()
412    }
413
414    pub fn working_copy(&self) -> &dyn WorkingCopy {
415        self.working_copy.as_ref()
416    }
417
418    pub fn start_working_copy_mutation(
419        &mut self,
420    ) -> Result<LockedWorkspace<'_>, WorkingCopyStateError> {
421        let locked_wc = self.working_copy.start_mutation()?;
422        Ok(LockedWorkspace {
423            base: self,
424            locked_wc,
425        })
426    }
427
428    pub fn check_out(
429        &mut self,
430        operation_id: OperationId,
431        old_tree_id: Option<&MergedTreeId>,
432        commit: &Commit,
433    ) -> Result<CheckoutStats, CheckoutError> {
434        let mut locked_ws = self.start_working_copy_mutation()?;
435        // Check if the current working-copy commit has changed on disk compared to what
436        // the caller expected. It's safe to check out another commit
437        // regardless, but it's probably not what  the caller wanted, so we let
438        // them know.
439        if let Some(old_tree_id) = old_tree_id
440            && old_tree_id != locked_ws.locked_wc().old_tree_id()
441        {
442            return Err(CheckoutError::ConcurrentCheckout);
443        }
444        let stats = locked_ws.locked_wc().check_out(commit).block_on()?;
445        locked_ws
446            .finish(operation_id)
447            .map_err(|err| CheckoutError::Other {
448                message: "Failed to save the working copy state".to_string(),
449                err: err.into(),
450            })?;
451        Ok(stats)
452    }
453}
454
455pub struct LockedWorkspace<'a> {
456    base: &'a mut Workspace,
457    locked_wc: Box<dyn LockedWorkingCopy>,
458}
459
460impl LockedWorkspace<'_> {
461    pub fn locked_wc(&mut self) -> &mut dyn LockedWorkingCopy {
462        self.locked_wc.as_mut()
463    }
464
465    pub fn finish(self, operation_id: OperationId) -> Result<(), WorkingCopyStateError> {
466        let new_wc = self.locked_wc.finish(operation_id).block_on()?;
467        self.base.working_copy = new_wc;
468        Ok(())
469    }
470}
471
472// Factory trait to build WorkspaceLoaders given the workspace root.
473pub trait WorkspaceLoaderFactory {
474    fn create(&self, workspace_root: &Path)
475    -> Result<Box<dyn WorkspaceLoader>, WorkspaceLoadError>;
476}
477
478pub fn get_working_copy_factory<'a>(
479    workspace_loader: &dyn WorkspaceLoader,
480    working_copy_factories: &'a WorkingCopyFactories,
481) -> Result<&'a dyn WorkingCopyFactory, StoreLoadError> {
482    let working_copy_type = workspace_loader.get_working_copy_type()?;
483
484    if let Some(factory) = working_copy_factories.get(&working_copy_type) {
485        Ok(factory.as_ref())
486    } else {
487        Err(StoreLoadError::UnsupportedType {
488            store: "working copy",
489            store_type: working_copy_type.clone(),
490        })
491    }
492}
493
494// Loader assigned to a specific workspace root that knows how to load a
495// Workspace object for that path.
496pub trait WorkspaceLoader {
497    // The root of the Workspace to be loaded.
498    fn workspace_root(&self) -> &Path;
499
500    // The path to the repo/ dir for this Workspace.
501    fn repo_path(&self) -> &Path;
502
503    // Loads the specified Workspace with the provided factories.
504    fn load(
505        &self,
506        user_settings: &UserSettings,
507        store_factories: &StoreFactories,
508        working_copy_factories: &WorkingCopyFactories,
509    ) -> Result<Workspace, WorkspaceLoadError>;
510
511    // Returns the type identifier for the WorkingCopy trait in this Workspace.
512    fn get_working_copy_type(&self) -> Result<String, StoreLoadError>;
513}
514
515pub struct DefaultWorkspaceLoaderFactory;
516
517impl WorkspaceLoaderFactory for DefaultWorkspaceLoaderFactory {
518    fn create(
519        &self,
520        workspace_root: &Path,
521    ) -> Result<Box<dyn WorkspaceLoader>, WorkspaceLoadError> {
522        Ok(Box::new(DefaultWorkspaceLoader::new(workspace_root)?))
523    }
524}
525
526/// Helps create a `Workspace` instance by reading `.jj/repo/` and
527/// `.jj/working_copy/` from the file system.
528#[derive(Clone, Debug)]
529struct DefaultWorkspaceLoader {
530    workspace_root: PathBuf,
531    repo_path: PathBuf,
532    working_copy_state_path: PathBuf,
533}
534
535pub type WorkingCopyFactories = HashMap<String, Box<dyn WorkingCopyFactory>>;
536
537impl DefaultWorkspaceLoader {
538    pub fn new(workspace_root: &Path) -> Result<Self, WorkspaceLoadError> {
539        let jj_dir = workspace_root.join(".jj");
540        if !jj_dir.is_dir() {
541            return Err(WorkspaceLoadError::NoWorkspaceHere(
542                workspace_root.to_owned(),
543            ));
544        }
545        let mut repo_dir = jj_dir.join("repo");
546        // If .jj/repo is a file, then we interpret its contents as a relative path to
547        // the actual repo directory (typically in another workspace).
548        if repo_dir.is_file() {
549            let buf = fs::read(&repo_dir).context(&repo_dir)?;
550            let repo_path =
551                file_util::path_from_bytes(&buf).map_err(WorkspaceLoadError::DecodeRepoPath)?;
552            repo_dir = dunce::canonicalize(jj_dir.join(repo_path)).context(repo_path)?;
553            if !repo_dir.is_dir() {
554                return Err(WorkspaceLoadError::RepoDoesNotExist(repo_dir));
555            }
556        }
557        let working_copy_state_path = jj_dir.join("working_copy");
558        Ok(Self {
559            workspace_root: workspace_root.to_owned(),
560            repo_path: repo_dir,
561            working_copy_state_path,
562        })
563    }
564}
565
566impl WorkspaceLoader for DefaultWorkspaceLoader {
567    fn workspace_root(&self) -> &Path {
568        &self.workspace_root
569    }
570
571    fn repo_path(&self) -> &Path {
572        &self.repo_path
573    }
574
575    fn load(
576        &self,
577        user_settings: &UserSettings,
578        store_factories: &StoreFactories,
579        working_copy_factories: &WorkingCopyFactories,
580    ) -> Result<Workspace, WorkspaceLoadError> {
581        let repo_loader =
582            RepoLoader::init_from_file_system(user_settings, &self.repo_path, store_factories)?;
583        let working_copy_factory = get_working_copy_factory(self, working_copy_factories)?;
584        let working_copy = working_copy_factory.load_working_copy(
585            repo_loader.store().clone(),
586            self.workspace_root.clone(),
587            self.working_copy_state_path.clone(),
588            user_settings,
589        )?;
590        let workspace = Workspace::new(
591            &self.workspace_root,
592            self.repo_path.clone(),
593            working_copy,
594            repo_loader,
595        )?;
596        Ok(workspace)
597    }
598
599    fn get_working_copy_type(&self) -> Result<String, StoreLoadError> {
600        read_store_type("working copy", self.working_copy_state_path.join("type"))
601    }
602}
603
604pub fn default_working_copy_factories() -> WorkingCopyFactories {
605    let mut factories = WorkingCopyFactories::new();
606    factories.insert(
607        LocalWorkingCopy::name().to_owned(),
608        Box::new(LocalWorkingCopyFactory {}),
609    );
610    factories
611}
612
613pub fn default_working_copy_factory() -> Box<dyn WorkingCopyFactory> {
614    Box::new(LocalWorkingCopyFactory {})
615}