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