jujutsu_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
15use std::fs::File;
16use std::io::{self, Read, Write};
17use std::path::{Path, PathBuf};
18use std::sync::Arc;
19
20use thiserror::Error;
21
22use crate::backend::Backend;
23use crate::git_backend::GitBackend;
24use crate::local_backend::LocalBackend;
25use crate::op_heads_store::OpHeadsStore;
26use crate::op_store::{self, OpStore, OperationMetadata, WorkspaceId};
27use crate::operation::Operation;
28use crate::repo::{
29    CheckOutCommitError, IoResultExt, PathError, ReadonlyRepo, Repo, RepoLoader, StoreFactories,
30};
31use crate::settings::UserSettings;
32use crate::working_copy::WorkingCopy;
33
34#[derive(Error, Debug)]
35pub enum WorkspaceInitError {
36    #[error("The destination repo ({0}) already exists")]
37    DestinationExists(PathBuf),
38    #[error("Repo path could not be interpreted as Unicode text")]
39    NonUnicodePath,
40    #[error(transparent)]
41    CheckOutCommit(#[from] CheckOutCommitError),
42    #[error(transparent)]
43    Path(#[from] PathError),
44}
45
46#[derive(Error, Debug)]
47pub enum WorkspaceLoadError {
48    #[error("The repo appears to no longer be at {0}")]
49    RepoDoesNotExist(PathBuf),
50    #[error("There is no Jujutsu repo in {0}")]
51    NoWorkspaceHere(PathBuf),
52    #[error("Repo path could not be interpreted as Unicode text")]
53    NonUnicodePath,
54    #[error(transparent)]
55    Path(#[from] PathError),
56}
57
58/// Represents a workspace, i.e. what's typically the .jj/ directory and its
59/// parent.
60pub struct Workspace {
61    // Path to the workspace root (typically the parent of a .jj/ directory), which is where
62    // working copy files live.
63    workspace_root: PathBuf,
64    repo_loader: RepoLoader,
65    working_copy: WorkingCopy,
66}
67
68fn create_jj_dir(workspace_root: &Path) -> Result<PathBuf, WorkspaceInitError> {
69    let jj_dir = workspace_root.join(".jj");
70    match std::fs::create_dir(&jj_dir).context(&jj_dir) {
71        Ok(()) => Ok(jj_dir),
72        Err(ref e) if e.error.kind() == io::ErrorKind::AlreadyExists => {
73            Err(WorkspaceInitError::DestinationExists(jj_dir))
74        }
75        Err(e) => Err(e.into()),
76    }
77}
78
79fn init_working_copy(
80    user_settings: &UserSettings,
81    repo: &Arc<ReadonlyRepo>,
82    workspace_root: &Path,
83    jj_dir: &Path,
84    workspace_id: WorkspaceId,
85) -> Result<(WorkingCopy, Arc<ReadonlyRepo>), WorkspaceInitError> {
86    let working_copy_state_path = jj_dir.join("working_copy");
87    std::fs::create_dir(&working_copy_state_path).context(&working_copy_state_path)?;
88
89    let mut tx = repo.start_transaction(
90        user_settings,
91        &format!("add workspace '{}'", workspace_id.as_str()),
92    );
93    tx.mut_repo().check_out(
94        workspace_id.clone(),
95        user_settings,
96        &repo.store().root_commit(),
97    )?;
98    let repo = tx.commit();
99
100    let working_copy = WorkingCopy::init(
101        repo.store().clone(),
102        workspace_root.to_path_buf(),
103        working_copy_state_path,
104        repo.op_id().clone(),
105        workspace_id,
106    );
107    Ok((working_copy, repo))
108}
109
110impl Workspace {
111    fn new(
112        workspace_root: &Path,
113        working_copy: WorkingCopy,
114        repo_loader: RepoLoader,
115    ) -> Result<Workspace, PathError> {
116        let workspace_root = workspace_root.canonicalize().context(workspace_root)?;
117        Ok(Workspace {
118            workspace_root,
119            repo_loader,
120            working_copy,
121        })
122    }
123
124    pub fn init_local(
125        user_settings: &UserSettings,
126        workspace_root: &Path,
127    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
128        Self::init_with_backend(user_settings, workspace_root, |store_path| {
129            Box::new(LocalBackend::init(store_path))
130        })
131    }
132
133    /// Initializes a workspace with a new Git backend in .jj/git/ (bare Git
134    /// repo)
135    pub fn init_internal_git(
136        user_settings: &UserSettings,
137        workspace_root: &Path,
138    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
139        Self::init_with_backend(user_settings, workspace_root, |store_path| {
140            Box::new(GitBackend::init_internal(store_path))
141        })
142    }
143
144    /// Initializes a workspace with an existing Git backend at the specified
145    /// path
146    pub fn init_external_git(
147        user_settings: &UserSettings,
148        workspace_root: &Path,
149        git_repo_path: &Path,
150    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
151        Self::init_with_backend(user_settings, workspace_root, |store_path| {
152            Box::new(GitBackend::init_external(store_path, git_repo_path))
153        })
154    }
155
156    pub fn init_with_factories(
157        user_settings: &UserSettings,
158        workspace_root: &Path,
159        backend_factory: impl FnOnce(&Path) -> Box<dyn Backend>,
160        op_store_factory: impl FnOnce(&Path) -> Box<dyn OpStore>,
161        op_heads_store_factory: impl FnOnce(
162            &Path,
163            &Arc<dyn OpStore>,
164            &op_store::View,
165            OperationMetadata,
166        ) -> (Box<dyn OpHeadsStore>, Operation),
167    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
168        let jj_dir = create_jj_dir(workspace_root)?;
169        let repo_dir = jj_dir.join("repo");
170        std::fs::create_dir(&repo_dir).context(&repo_dir)?;
171        let repo = ReadonlyRepo::init(
172            user_settings,
173            &repo_dir,
174            backend_factory,
175            op_store_factory,
176            op_heads_store_factory,
177        )?;
178        let (working_copy, repo) = init_working_copy(
179            user_settings,
180            &repo,
181            workspace_root,
182            &jj_dir,
183            WorkspaceId::default(),
184        )?;
185        let repo_loader = repo.loader();
186        let workspace = Workspace::new(workspace_root, working_copy, repo_loader)?;
187        Ok((workspace, repo))
188    }
189
190    pub fn init_with_backend(
191        user_settings: &UserSettings,
192        workspace_root: &Path,
193        backend_factory: impl FnOnce(&Path) -> Box<dyn Backend>,
194    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
195        Self::init_with_factories(
196            user_settings,
197            workspace_root,
198            backend_factory,
199            ReadonlyRepo::default_op_store_factory(),
200            ReadonlyRepo::default_op_heads_store_factory(),
201        )
202    }
203
204    pub fn init_workspace_with_existing_repo(
205        user_settings: &UserSettings,
206        workspace_root: &Path,
207        repo: &Arc<ReadonlyRepo>,
208        workspace_id: WorkspaceId,
209    ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
210        let jj_dir = create_jj_dir(workspace_root)?;
211
212        let repo_dir = repo.repo_path().canonicalize().context(repo.repo_path())?;
213        let repo_file_path = jj_dir.join("repo");
214        let mut repo_file = File::create(&repo_file_path).context(&repo_file_path)?;
215        repo_file
216            .write_all(
217                repo_dir
218                    .to_str()
219                    .ok_or(WorkspaceInitError::NonUnicodePath)?
220                    .as_bytes(),
221            )
222            .context(&repo_file_path)?;
223
224        let (working_copy, repo) =
225            init_working_copy(user_settings, repo, workspace_root, &jj_dir, workspace_id)?;
226        let workspace = Workspace::new(workspace_root, working_copy, repo.loader())?;
227        Ok((workspace, repo))
228    }
229
230    pub fn load(
231        user_settings: &UserSettings,
232        workspace_path: &Path,
233        store_factories: &StoreFactories,
234    ) -> Result<Self, WorkspaceLoadError> {
235        let loader = WorkspaceLoader::init(workspace_path)?;
236        Ok(loader.load(user_settings, store_factories)?)
237    }
238
239    pub fn workspace_root(&self) -> &PathBuf {
240        &self.workspace_root
241    }
242
243    pub fn workspace_id(&self) -> &WorkspaceId {
244        self.working_copy.workspace_id()
245    }
246
247    pub fn repo_path(&self) -> &PathBuf {
248        self.repo_loader.repo_path()
249    }
250
251    pub fn repo_loader(&self) -> &RepoLoader {
252        &self.repo_loader
253    }
254
255    pub fn working_copy(&self) -> &WorkingCopy {
256        &self.working_copy
257    }
258
259    pub fn working_copy_mut(&mut self) -> &mut WorkingCopy {
260        &mut self.working_copy
261    }
262}
263
264#[derive(Clone)]
265pub struct WorkspaceLoader {
266    workspace_root: PathBuf,
267    repo_dir: PathBuf,
268    working_copy_state_path: PathBuf,
269}
270
271impl WorkspaceLoader {
272    pub fn init(workspace_root: &Path) -> Result<Self, WorkspaceLoadError> {
273        let jj_dir = workspace_root.join(".jj");
274        if !jj_dir.is_dir() {
275            return Err(WorkspaceLoadError::NoWorkspaceHere(
276                workspace_root.to_owned(),
277            ));
278        }
279        let mut repo_dir = jj_dir.join("repo");
280        // If .jj/repo is a file, then we interpret its contents as a relative path to
281        // the actual repo directory (typically in another workspace).
282        if repo_dir.is_file() {
283            let mut repo_file = File::open(&repo_dir).context(&repo_dir)?;
284            let mut buf = Vec::new();
285            repo_file.read_to_end(&mut buf).context(&repo_dir)?;
286            let repo_path_str =
287                String::from_utf8(buf).map_err(|_| WorkspaceLoadError::NonUnicodePath)?;
288            repo_dir = jj_dir
289                .join(&repo_path_str)
290                .canonicalize()
291                .context(&repo_path_str)?;
292            if !repo_dir.is_dir() {
293                return Err(WorkspaceLoadError::RepoDoesNotExist(repo_dir));
294            }
295        }
296        let working_copy_state_path = jj_dir.join("working_copy");
297        Ok(WorkspaceLoader {
298            workspace_root: workspace_root.to_owned(),
299            repo_dir,
300            working_copy_state_path,
301        })
302    }
303
304    pub fn workspace_root(&self) -> &Path {
305        &self.workspace_root
306    }
307
308    pub fn repo_path(&self) -> &Path {
309        &self.repo_dir
310    }
311
312    pub fn load(
313        &self,
314        user_settings: &UserSettings,
315        store_factories: &StoreFactories,
316    ) -> Result<Workspace, PathError> {
317        let repo_loader = RepoLoader::init(user_settings, &self.repo_dir, store_factories);
318        let working_copy = WorkingCopy::load(
319            repo_loader.store().clone(),
320            self.workspace_root.clone(),
321            self.working_copy_state_path.clone(),
322        );
323        Workspace::new(&self.workspace_root, working_copy, repo_loader)
324    }
325}