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