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 thiserror::Error;
25
26use crate::backend::BackendInitError;
27use crate::commit::Commit;
28use crate::file_util;
29use crate::file_util::BadPathEncoding;
30use crate::file_util::IoResultExt as _;
31use crate::file_util::PathError;
32use crate::local_working_copy::LocalWorkingCopy;
33use crate::local_working_copy::LocalWorkingCopyFactory;
34use crate::merged_tree::MergedTree;
35use crate::op_heads_store::OpHeadsStoreError;
36use crate::op_store::OperationId;
37use crate::ref_name::WorkspaceName;
38use crate::ref_name::WorkspaceNameBuf;
39use crate::repo::BackendInitializer;
40use crate::repo::CheckOutCommitError;
41use crate::repo::IndexStoreInitializer;
42use crate::repo::OpHeadsStoreInitializer;
43use crate::repo::OpStoreInitializer;
44use crate::repo::ReadonlyRepo;
45use crate::repo::Repo as _;
46use crate::repo::RepoInitError;
47use crate::repo::RepoLoader;
48use crate::repo::StoreFactories;
49use crate::repo::StoreLoadError;
50use crate::repo::SubmoduleStoreInitializer;
51use crate::repo::read_store_type;
52use crate::settings::UserSettings;
53use crate::signing::SignInitError;
54use crate::signing::Signer;
55use crate::simple_backend::SimpleBackend;
56use crate::transaction::TransactionCommitError;
57use crate::working_copy::CheckoutError;
58use crate::working_copy::CheckoutStats;
59use crate::working_copy::LockedWorkingCopy;
60use crate::working_copy::WorkingCopy;
61use crate::working_copy::WorkingCopyFactory;
62use crate::working_copy::WorkingCopyStateError;
63use crate::workspace_store::SimpleWorkspaceStore;
64use crate::workspace_store::WorkspaceStore as _;
65use crate::workspace_store::WorkspaceStoreError;
66
67#[derive(Error, Debug)]
68pub enum WorkspaceInitError {
69 #[error("The destination repo ({0}) already exists")]
70 DestinationExists(PathBuf),
71 #[error("Repo path could not be encoded")]
72 EncodeRepoPath(#[source] BadPathEncoding),
73 #[error(transparent)]
74 CheckOutCommit(#[from] CheckOutCommitError),
75 #[error(transparent)]
76 WorkingCopyState(#[from] WorkingCopyStateError),
77 #[error(transparent)]
78 Path(#[from] PathError),
79 #[error(transparent)]
80 OpHeadsStore(OpHeadsStoreError),
81 #[error(transparent)]
82 WorkspaceStore(#[from] WorkspaceStoreError),
83 #[error(transparent)]
84 Backend(#[from] BackendInitError),
85 #[error(transparent)]
86 SignInit(#[from] SignInitError),
87 #[error(transparent)]
88 TransactionCommit(#[from] TransactionCommitError),
89}
90
91#[derive(Error, Debug)]
92pub enum WorkspaceLoadError {
93 #[error("The repo appears to no longer be at {0}")]
94 RepoDoesNotExist(PathBuf),
95 #[error("There is no Jujutsu repo in {0}")]
96 NoWorkspaceHere(PathBuf),
97 #[error("Cannot read the repo")]
98 StoreLoadError(#[from] StoreLoadError),
99 #[error("Repo path could not be decoded")]
100 DecodeRepoPath(#[source] BadPathEncoding),
101 #[error(transparent)]
102 WorkingCopyState(#[from] WorkingCopyStateError),
103 #[error(transparent)]
104 Path(#[from] PathError),
105}
106
107pub struct Workspace {
114 workspace_root: PathBuf,
117 repo_path: PathBuf,
118 repo_loader: RepoLoader,
119 working_copy: Box<dyn WorkingCopy>,
120}
121
122fn create_jj_dir(workspace_root: &Path) -> Result<PathBuf, WorkspaceInitError> {
123 let jj_dir = workspace_root.join(".jj");
124 match std::fs::create_dir(&jj_dir).context(&jj_dir) {
125 Ok(()) => Ok(jj_dir),
126 Err(e) if e.source.kind() == io::ErrorKind::AlreadyExists => {
127 Err(WorkspaceInitError::DestinationExists(jj_dir))
128 }
129 Err(e) => Err(e.into()),
130 }
131}
132
133async fn init_working_copy(
134 repo: &Arc<ReadonlyRepo>,
135 workspace_root: &Path,
136 jj_dir: &Path,
137 working_copy_factory: &dyn WorkingCopyFactory,
138 workspace_name: WorkspaceNameBuf,
139) -> Result<(Box<dyn WorkingCopy>, Arc<ReadonlyRepo>), WorkspaceInitError> {
140 let working_copy_state_path = jj_dir.join("working_copy");
141 std::fs::create_dir(&working_copy_state_path).context(&working_copy_state_path)?;
142
143 let mut tx = repo.start_transaction();
144 tx.repo_mut()
145 .check_out(workspace_name.clone(), &repo.store().root_commit())
146 .await?;
147 let repo = tx
148 .commit(format!("add workspace '{}'", workspace_name.as_symbol()))
149 .await?;
150
151 let working_copy = working_copy_factory.init_working_copy(
152 repo.store().clone(),
153 workspace_root.to_path_buf(),
154 working_copy_state_path.clone(),
155 repo.op_id().clone(),
156 workspace_name,
157 repo.settings(),
158 )?;
159 let working_copy_type_path = working_copy_state_path.join("type");
160 fs::write(&working_copy_type_path, working_copy.name()).context(&working_copy_type_path)?;
161 Ok((working_copy, repo))
162}
163
164impl Workspace {
165 pub fn new(
166 workspace_root: &Path,
167 repo_path: PathBuf,
168 working_copy: Box<dyn WorkingCopy>,
169 repo_loader: RepoLoader,
170 ) -> Result<Self, PathError> {
171 let workspace_root = dunce::canonicalize(workspace_root).context(workspace_root)?;
172 Ok(Self::new_no_canonicalize(
173 workspace_root,
174 repo_path,
175 working_copy,
176 repo_loader,
177 ))
178 }
179
180 pub fn new_no_canonicalize(
181 workspace_root: PathBuf,
182 repo_path: PathBuf,
183 working_copy: Box<dyn WorkingCopy>,
184 repo_loader: RepoLoader,
185 ) -> Self {
186 Self {
187 workspace_root,
188 repo_path,
189 repo_loader,
190 working_copy,
191 }
192 }
193
194 pub async fn init_simple(
195 user_settings: &UserSettings,
196 workspace_root: &Path,
197 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
198 let backend_initializer: &BackendInitializer =
199 &|_settings, store_path| Ok(Box::new(SimpleBackend::init(store_path)));
200 let signer = Signer::from_settings(user_settings)?;
201 Self::init_with_backend(user_settings, workspace_root, backend_initializer, signer).await
202 }
203
204 #[cfg(feature = "git")]
207 pub async fn init_internal_git(
208 user_settings: &UserSettings,
209 workspace_root: &Path,
210 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
211 let backend_initializer: &BackendInitializer = &|settings, store_path| {
212 Ok(Box::new(crate::git_backend::GitBackend::init_internal(
213 settings, store_path,
214 )?))
215 };
216 let signer = Signer::from_settings(user_settings)?;
217 Self::init_with_backend(user_settings, workspace_root, backend_initializer, signer).await
218 }
219
220 #[cfg(feature = "git")]
223 pub async fn init_colocated_git(
224 user_settings: &UserSettings,
225 workspace_root: &Path,
226 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
227 let backend_initializer = |settings: &UserSettings,
228 store_path: &Path|
229 -> Result<Box<dyn crate::backend::Backend>, _> {
230 let store_relative_workspace_root =
234 if let Ok(workspace_root) = dunce::canonicalize(workspace_root) {
235 crate::file_util::relative_path(store_path, &workspace_root)
236 } else {
237 workspace_root.to_owned()
238 };
239 let backend = crate::git_backend::GitBackend::init_colocated(
240 settings,
241 store_path,
242 &store_relative_workspace_root,
243 )?;
244 Ok(Box::new(backend))
245 };
246 let signer = Signer::from_settings(user_settings)?;
247 Self::init_with_backend(user_settings, workspace_root, &backend_initializer, signer).await
248 }
249
250 #[cfg(feature = "git")]
255 pub async fn init_external_git(
256 user_settings: &UserSettings,
257 workspace_root: &Path,
258 git_repo_path: &Path,
259 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
260 let backend_initializer = |settings: &UserSettings,
261 store_path: &Path|
262 -> Result<Box<dyn crate::backend::Backend>, _> {
263 let store_relative_git_repo_path = match (
269 dunce::canonicalize(workspace_root),
270 crate::git_backend::canonicalize_git_repo_path(git_repo_path),
271 ) {
272 (Ok(workspace_root), Ok(git_repo_path))
273 if git_repo_path.starts_with(&workspace_root) =>
274 {
275 crate::file_util::relative_path(store_path, &git_repo_path)
276 }
277 _ => git_repo_path.to_owned(),
278 };
279 let backend = crate::git_backend::GitBackend::init_external(
280 settings,
281 store_path,
282 &store_relative_git_repo_path,
283 )?;
284 Ok(Box::new(backend))
285 };
286 let signer = Signer::from_settings(user_settings)?;
287 Self::init_with_backend(user_settings, workspace_root, &backend_initializer, signer).await
288 }
289
290 #[expect(clippy::too_many_arguments)]
291 pub async fn init_with_factories(
292 user_settings: &UserSettings,
293 workspace_root: &Path,
294 backend_initializer: &BackendInitializer<'_>,
295 signer: Signer,
296 op_store_initializer: &OpStoreInitializer<'_>,
297 op_heads_store_initializer: &OpHeadsStoreInitializer<'_>,
298 index_store_initializer: &IndexStoreInitializer<'_>,
299 submodule_store_initializer: &SubmoduleStoreInitializer<'_>,
300 working_copy_factory: &dyn WorkingCopyFactory,
301 workspace_name: WorkspaceNameBuf,
302 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
303 let jj_dir = create_jj_dir(workspace_root)?;
304 async {
305 let repo_dir = jj_dir.join("repo");
306 std::fs::create_dir(&repo_dir).context(&repo_dir)?;
307 let repo = ReadonlyRepo::init(
308 user_settings,
309 &repo_dir,
310 backend_initializer,
311 signer,
312 op_store_initializer,
313 op_heads_store_initializer,
314 index_store_initializer,
315 submodule_store_initializer,
316 )
317 .await
318 .map_err(|repo_init_err| match repo_init_err {
319 RepoInitError::Backend(err) => WorkspaceInitError::Backend(err),
320 RepoInitError::OpHeadsStore(err) => WorkspaceInitError::OpHeadsStore(err),
321 RepoInitError::Path(err) => WorkspaceInitError::Path(err),
322 })?;
323 let workspace_store = SimpleWorkspaceStore::load(&repo_dir)?;
324 let (working_copy, repo) = init_working_copy(
325 &repo,
326 workspace_root,
327 &jj_dir,
328 working_copy_factory,
329 workspace_name,
330 )
331 .await?;
332 let repo_loader = repo.loader().clone();
333 let repo_dir = dunce::canonicalize(&repo_dir).context(&repo_dir)?;
334 let workspace = Self::new(workspace_root, repo_dir, working_copy, repo_loader)?;
335 workspace_store.add(workspace.workspace_name(), workspace.workspace_root())?;
336 Ok((workspace, repo))
337 }
338 .await
339 .inspect_err(|_err| {
340 std::fs::remove_dir_all(jj_dir).ok();
341 })
342 }
343
344 pub async fn init_with_backend(
345 user_settings: &UserSettings,
346 workspace_root: &Path,
347 backend_initializer: &BackendInitializer<'_>,
348 signer: Signer,
349 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
350 Self::init_with_factories(
351 user_settings,
352 workspace_root,
353 backend_initializer,
354 signer,
355 ReadonlyRepo::default_op_store_initializer(),
356 ReadonlyRepo::default_op_heads_store_initializer(),
357 ReadonlyRepo::default_index_store_initializer(),
358 ReadonlyRepo::default_submodule_store_initializer(),
359 &*default_working_copy_factory(),
360 WorkspaceName::DEFAULT.to_owned(),
361 )
362 .await
363 }
364
365 pub async fn init_workspace_with_existing_repo(
366 workspace_root: &Path,
367 repo_path: &Path,
368 repo: &Arc<ReadonlyRepo>,
369 working_copy_factory: &dyn WorkingCopyFactory,
370 workspace_name: WorkspaceNameBuf,
371 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
372 let jj_dir = create_jj_dir(workspace_root)?;
373
374 let repo_dir = dunce::canonicalize(repo_path).context(repo_path)?;
375 let jj_dir_abs = dunce::canonicalize(&jj_dir).context(&jj_dir)?;
376 let path_to_store = file_util::relative_path(&jj_dir_abs, &repo_dir);
377 let path_to_store = if path_to_store.is_relative() {
378 file_util::slash_path(&path_to_store).into_owned()
379 } else {
380 path_to_store
381 };
382 let repo_dir_bytes =
383 file_util::path_to_bytes(&path_to_store).map_err(WorkspaceInitError::EncodeRepoPath)?;
384 let repo_file_path = jj_dir.join("repo");
385 fs::write(&repo_file_path, repo_dir_bytes).context(&repo_file_path)?;
386
387 let workspace_store = SimpleWorkspaceStore::load(repo_path)?;
388 let (working_copy, repo) = init_working_copy(
389 repo,
390 workspace_root,
391 &jj_dir,
392 working_copy_factory,
393 workspace_name,
394 )
395 .await?;
396 let workspace = Self::new(
397 workspace_root,
398 repo_dir,
399 working_copy,
400 repo.loader().clone(),
401 )?;
402 workspace_store.add(workspace.workspace_name(), workspace.workspace_root())?;
403 Ok((workspace, repo))
404 }
405
406 pub fn load(
407 user_settings: &UserSettings,
408 workspace_path: &Path,
409 store_factories: &StoreFactories,
410 working_copy_factories: &WorkingCopyFactories,
411 ) -> Result<Self, WorkspaceLoadError> {
412 let loader = DefaultWorkspaceLoader::new(workspace_path)?;
413 let workspace = loader.load(user_settings, store_factories, working_copy_factories)?;
414 Ok(workspace)
415 }
416
417 pub fn workspace_root(&self) -> &Path {
418 &self.workspace_root
419 }
420
421 pub fn workspace_name(&self) -> &WorkspaceName {
422 self.working_copy.workspace_name()
423 }
424
425 pub fn repo_path(&self) -> &Path {
426 &self.repo_path
427 }
428
429 pub fn repo_loader(&self) -> &RepoLoader {
430 &self.repo_loader
431 }
432
433 pub fn settings(&self) -> &UserSettings {
435 self.repo_loader.settings()
436 }
437
438 pub fn working_copy(&self) -> &dyn WorkingCopy {
439 self.working_copy.as_ref()
440 }
441
442 pub fn start_working_copy_mutation(
443 &mut self,
444 ) -> Result<LockedWorkspace<'_>, WorkingCopyStateError> {
445 let locked_wc = self.working_copy.start_mutation()?;
446 Ok(LockedWorkspace {
447 base: self,
448 locked_wc,
449 })
450 }
451
452 pub async fn check_out(
453 &mut self,
454 operation_id: OperationId,
455 old_tree: Option<&MergedTree>,
456 commit: &Commit,
457 ) -> Result<CheckoutStats, CheckoutError> {
458 let mut locked_ws = self.start_working_copy_mutation()?;
459 if let Some(old_tree) = old_tree
464 && old_tree.tree_ids_and_labels()
465 != locked_ws.locked_wc().old_tree().tree_ids_and_labels()
466 {
467 return Err(CheckoutError::ConcurrentCheckout);
468 }
469 let stats = locked_ws.locked_wc().check_out(commit).await?;
470 locked_ws
471 .finish(operation_id)
472 .await
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 async fn finish(self, operation_id: OperationId) -> Result<(), WorkingCopyStateError> {
492 let new_wc = self.locked_wc.finish(operation_id).await?;
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}