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::transaction::TransactionCommitError;
58use crate::working_copy::CheckoutError;
59use crate::working_copy::CheckoutOptions;
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;
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 interpreted as Unicode text")]
71    NonUnicodePath,
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    Backend(#[from] BackendInitError),
82    #[error(transparent)]
83    SignInit(#[from] SignInitError),
84    #[error(transparent)]
85    TransactionCommit(#[from] TransactionCommitError),
86}
87
88#[derive(Error, Debug)]
89pub enum WorkspaceLoadError {
90    #[error("The repo appears to no longer be at {0}")]
91    RepoDoesNotExist(PathBuf),
92    #[error("There is no Jujutsu repo in {0}")]
93    NoWorkspaceHere(PathBuf),
94    #[error("Cannot read the repo")]
95    StoreLoadError(#[from] StoreLoadError),
96    #[error("Repo path could not be interpreted as Unicode text")]
97    NonUnicodePath,
98    #[error(transparent)]
99    WorkingCopyState(#[from] WorkingCopyStateError),
100    #[error(transparent)]
101    Path(#[from] PathError),
102}
103
104/// The combination of a repo and a working copy.
105///
106/// Represents the combination of a repo and working copy, i.e. what's typically
107/// the .jj/ directory and its parent. See
108/// <https://github.com/jj-vcs/jj/blob/main/docs/working-copy.md#workspaces>
109/// for more information.
110pub struct Workspace {
111    // Path to the workspace root (typically the parent of a .jj/ directory), which is where
112    // working copy files live.
113    workspace_root: PathBuf,
114    repo_path: PathBuf,
115    repo_loader: RepoLoader,
116    working_copy: Box<dyn WorkingCopy>,
117}
118
119fn create_jj_dir(workspace_root: &Path) -> Result<PathBuf, WorkspaceInitError> {
120    let jj_dir = workspace_root.join(".jj");
121    match std::fs::create_dir(&jj_dir).context(&jj_dir) {
122        Ok(()) => Ok(jj_dir),
123        Err(ref e) if e.error.kind() == io::ErrorKind::AlreadyExists => {
124            Err(WorkspaceInitError::DestinationExists(jj_dir))
125        }
126        Err(e) => Err(e.into()),
127    }
128}
129
130fn init_working_copy(
131    repo: &Arc<ReadonlyRepo>,
132    workspace_root: &Path,
133    jj_dir: &Path,
134    working_copy_factory: &dyn WorkingCopyFactory,
135    workspace_name: WorkspaceNameBuf,
136) -> Result<(Box<dyn WorkingCopy>, Arc<ReadonlyRepo>), WorkspaceInitError> {
137    let working_copy_state_path = jj_dir.join("working_copy");
138    std::fs::create_dir(&working_copy_state_path).context(&working_copy_state_path)?;
139
140    let mut tx = repo.start_transaction();
141    tx.repo_mut()
142        .check_out(workspace_name.clone(), &repo.store().root_commit())?;
143    let repo = tx.commit(format!("add workspace '{}'", workspace_name.as_symbol()))?;
144
145    let working_copy = working_copy_factory.init_working_copy(
146        repo.store().clone(),
147        workspace_root.to_path_buf(),
148        working_copy_state_path.clone(),
149        repo.op_id().clone(),
150        workspace_name,
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<Workspace, 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 = Workspace::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_file_path = jj_dir.join("repo");
362        let mut repo_file = File::create(&repo_file_path).context(&repo_file_path)?;
363        repo_file
364            .write_all(
365                repo_dir
366                    .to_str()
367                    .ok_or(WorkspaceInitError::NonUnicodePath)?
368                    .as_bytes(),
369            )
370            .context(&repo_file_path)?;
371
372        let (working_copy, repo) = init_working_copy(
373            repo,
374            workspace_root,
375            &jj_dir,
376            working_copy_factory,
377            workspace_name,
378        )?;
379        let workspace = Workspace::new(
380            workspace_root,
381            repo_dir,
382            working_copy,
383            repo.loader().clone(),
384        )?;
385        Ok((workspace, repo))
386    }
387
388    pub fn load(
389        user_settings: &UserSettings,
390        workspace_path: &Path,
391        store_factories: &StoreFactories,
392        working_copy_factories: &WorkingCopyFactories,
393    ) -> Result<Self, WorkspaceLoadError> {
394        let loader = DefaultWorkspaceLoader::new(workspace_path)?;
395        let workspace = loader.load(user_settings, store_factories, working_copy_factories)?;
396        Ok(workspace)
397    }
398
399    pub fn workspace_root(&self) -> &Path {
400        &self.workspace_root
401    }
402
403    pub fn workspace_name(&self) -> &WorkspaceName {
404        self.working_copy.workspace_name()
405    }
406
407    pub fn repo_path(&self) -> &Path {
408        &self.repo_path
409    }
410
411    pub fn repo_loader(&self) -> &RepoLoader {
412        &self.repo_loader
413    }
414
415    /// Settings for this workspace.
416    pub fn settings(&self) -> &UserSettings {
417        self.repo_loader.settings()
418    }
419
420    pub fn working_copy(&self) -> &dyn WorkingCopy {
421        self.working_copy.as_ref()
422    }
423
424    pub fn start_working_copy_mutation(
425        &mut self,
426    ) -> Result<LockedWorkspace, WorkingCopyStateError> {
427        let locked_wc = self.working_copy.start_mutation()?;
428        Ok(LockedWorkspace {
429            base: self,
430            locked_wc,
431        })
432    }
433
434    pub fn check_out(
435        &mut self,
436        operation_id: OperationId,
437        old_tree_id: Option<&MergedTreeId>,
438        commit: &Commit,
439        options: &CheckoutOptions,
440    ) -> Result<CheckoutStats, CheckoutError> {
441        let mut locked_ws =
442            self.start_working_copy_mutation()
443                .map_err(|err| CheckoutError::Other {
444                    message: "Failed to start editing the working copy state".to_string(),
445                    err: err.into(),
446                })?;
447        // Check if the current working-copy commit has changed on disk compared to what
448        // the caller expected. It's safe to check out another commit
449        // regardless, but it's probably not what  the caller wanted, so we let
450        // them know.
451        if let Some(old_tree_id) = old_tree_id {
452            if old_tree_id != locked_ws.locked_wc().old_tree_id() {
453                return Err(CheckoutError::ConcurrentCheckout);
454            }
455        }
456        let stats = locked_ws.locked_wc().check_out(commit, options)?;
457        locked_ws
458            .finish(operation_id)
459            .map_err(|err| CheckoutError::Other {
460                message: "Failed to save the working copy state".to_string(),
461                err: err.into(),
462            })?;
463        Ok(stats)
464    }
465}
466
467pub struct LockedWorkspace<'a> {
468    base: &'a mut Workspace,
469    locked_wc: Box<dyn LockedWorkingCopy>,
470}
471
472impl LockedWorkspace<'_> {
473    pub fn locked_wc(&mut self) -> &mut dyn LockedWorkingCopy {
474        self.locked_wc.as_mut()
475    }
476
477    pub fn finish(self, operation_id: OperationId) -> Result<(), WorkingCopyStateError> {
478        let new_wc = self.locked_wc.finish(operation_id)?;
479        self.base.working_copy = new_wc;
480        Ok(())
481    }
482}
483
484// Factory trait to build WorkspaceLoaders given the workspace root.
485pub trait WorkspaceLoaderFactory {
486    fn create(&self, workspace_root: &Path)
487        -> Result<Box<dyn WorkspaceLoader>, WorkspaceLoadError>;
488}
489
490pub fn get_working_copy_factory<'a>(
491    workspace_loader: &dyn WorkspaceLoader,
492    working_copy_factories: &'a WorkingCopyFactories,
493) -> Result<&'a dyn WorkingCopyFactory, StoreLoadError> {
494    let working_copy_type = workspace_loader.get_working_copy_type()?;
495
496    if let Some(factory) = working_copy_factories.get(&working_copy_type) {
497        Ok(factory.as_ref())
498    } else {
499        Err(StoreLoadError::UnsupportedType {
500            store: "working copy",
501            store_type: working_copy_type.to_string(),
502        })
503    }
504}
505
506// Loader assigned to a specific workspace root that knows how to load a
507// Workspace object for that path.
508pub trait WorkspaceLoader {
509    // The root of the Workspace to be loaded.
510    fn workspace_root(&self) -> &Path;
511
512    // The path to the repo/ dir for this Workspace.
513    fn repo_path(&self) -> &Path;
514
515    // Loads the specified Workspace with the provided factories.
516    fn load(
517        &self,
518        user_settings: &UserSettings,
519        store_factories: &StoreFactories,
520        working_copy_factories: &WorkingCopyFactories,
521    ) -> Result<Workspace, WorkspaceLoadError>;
522
523    // Returns the type identifier for the WorkingCopy trait in this Workspace.
524    fn get_working_copy_type(&self) -> Result<String, StoreLoadError>;
525
526    // Loads the WorkingCopy trait for this Workspace.
527    fn load_working_copy(
528        &self,
529        store: &Arc<Store>,
530        working_copy_factory: &dyn WorkingCopyFactory,
531    ) -> Result<Box<dyn WorkingCopy>, WorkspaceLoadError>;
532}
533
534pub struct DefaultWorkspaceLoaderFactory;
535
536impl WorkspaceLoaderFactory for DefaultWorkspaceLoaderFactory {
537    fn create(
538        &self,
539        workspace_root: &Path,
540    ) -> Result<Box<dyn WorkspaceLoader>, WorkspaceLoadError> {
541        Ok(Box::new(DefaultWorkspaceLoader::new(workspace_root)?))
542    }
543}
544
545/// Helps create a `Workspace` instance by reading `.jj/repo/` and
546/// `.jj/working_copy/` from the file system.
547#[derive(Clone, Debug)]
548struct DefaultWorkspaceLoader {
549    workspace_root: PathBuf,
550    repo_path: PathBuf,
551    working_copy_state_path: PathBuf,
552}
553
554pub type WorkingCopyFactories = HashMap<String, Box<dyn WorkingCopyFactory>>;
555
556impl DefaultWorkspaceLoader {
557    pub fn new(workspace_root: &Path) -> Result<Self, WorkspaceLoadError> {
558        let jj_dir = workspace_root.join(".jj");
559        if !jj_dir.is_dir() {
560            return Err(WorkspaceLoadError::NoWorkspaceHere(
561                workspace_root.to_owned(),
562            ));
563        }
564        let mut repo_dir = jj_dir.join("repo");
565        // If .jj/repo is a file, then we interpret its contents as a relative path to
566        // the actual repo directory (typically in another workspace).
567        if repo_dir.is_file() {
568            let buf = fs::read(&repo_dir).context(&repo_dir)?;
569            let repo_path_str =
570                String::from_utf8(buf).map_err(|_| WorkspaceLoadError::NonUnicodePath)?;
571            repo_dir = dunce::canonicalize(jj_dir.join(&repo_path_str)).context(&repo_path_str)?;
572            if !repo_dir.is_dir() {
573                return Err(WorkspaceLoadError::RepoDoesNotExist(repo_dir));
574            }
575        }
576        let working_copy_state_path = jj_dir.join("working_copy");
577        Ok(Self {
578            workspace_root: workspace_root.to_owned(),
579            repo_path: repo_dir,
580            working_copy_state_path,
581        })
582    }
583}
584
585impl WorkspaceLoader for DefaultWorkspaceLoader {
586    fn workspace_root(&self) -> &Path {
587        &self.workspace_root
588    }
589
590    fn repo_path(&self) -> &Path {
591        &self.repo_path
592    }
593
594    fn load(
595        &self,
596        user_settings: &UserSettings,
597        store_factories: &StoreFactories,
598        working_copy_factories: &WorkingCopyFactories,
599    ) -> Result<Workspace, WorkspaceLoadError> {
600        let repo_loader =
601            RepoLoader::init_from_file_system(user_settings, &self.repo_path, store_factories)?;
602        let working_copy_factory = get_working_copy_factory(self, working_copy_factories)?;
603        let working_copy = self.load_working_copy(repo_loader.store(), working_copy_factory)?;
604        let workspace = Workspace::new(
605            &self.workspace_root,
606            self.repo_path.clone(),
607            working_copy,
608            repo_loader,
609        )?;
610        Ok(workspace)
611    }
612
613    fn get_working_copy_type(&self) -> Result<String, StoreLoadError> {
614        read_store_type("working copy", self.working_copy_state_path.join("type"))
615    }
616
617    fn load_working_copy(
618        &self,
619        store: &Arc<Store>,
620        working_copy_factory: &dyn WorkingCopyFactory,
621    ) -> Result<Box<dyn WorkingCopy>, WorkspaceLoadError> {
622        Ok(working_copy_factory.load_working_copy(
623            store.clone(),
624            self.workspace_root.clone(),
625            self.working_copy_state_path.clone(),
626        )?)
627    }
628}
629
630pub fn default_working_copy_factories() -> WorkingCopyFactories {
631    let mut factories = WorkingCopyFactories::new();
632    factories.insert(
633        LocalWorkingCopy::name().to_owned(),
634        Box::new(LocalWorkingCopyFactory {}),
635    );
636    factories
637}
638
639pub fn default_working_copy_factory() -> Box<dyn WorkingCopyFactory> {
640    Box::new(LocalWorkingCopyFactory {})
641}