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