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::default_backend_factories::default_workspace_loader_factory;
30use crate::file_util;
31use crate::file_util::BadPathEncoding;
32use crate::file_util::IoResultExt as _;
33use crate::file_util::PathError;
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::WorkspaceStoreInitializer;
52use crate::repo::read_store_type;
53use crate::settings::UserSettings;
54use crate::signing::Signer;
55use crate::signing_factory::SignInitError;
56use crate::signing_factory::signer_from_settings;
57use crate::simple_backend::SimpleBackend;
58use crate::transaction::TransactionCommitError;
59use crate::working_copy::CheckoutError;
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;
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 object_hash: gix::hash::Kind,
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,
215 store_path,
216 object_hash,
217 )?))
218 };
219 let signer = signer_from_settings(user_settings)?;
220 Self::init_with_backend(user_settings, workspace_root, backend_initializer, signer).await
221 }
222
223 #[cfg(feature = "git")]
226 pub async fn init_colocated_git(
227 user_settings: &UserSettings,
228 workspace_root: &Path,
229 object_hash: gix::hash::Kind,
230 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
231 let backend_initializer = |settings: &UserSettings,
232 store_path: &Path|
233 -> Result<Box<dyn crate::backend::Backend>, _> {
234 let store_relative_workspace_root =
238 if let Ok(workspace_root) = dunce::canonicalize(workspace_root) {
239 crate::file_util::relative_path(store_path, &workspace_root)
240 } else {
241 workspace_root.to_owned()
242 };
243 let backend = crate::git_backend::GitBackend::init_colocated(
244 settings,
245 store_path,
246 &store_relative_workspace_root,
247 object_hash,
248 )?;
249 Ok(Box::new(backend))
250 };
251 let signer = signer_from_settings(user_settings)?;
252 Self::init_with_backend(user_settings, workspace_root, &backend_initializer, signer).await
253 }
254
255 #[cfg(feature = "git")]
260 pub async fn init_external_git(
261 user_settings: &UserSettings,
262 workspace_root: &Path,
263 git_repo_path: &Path,
264 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
265 let backend_initializer = |settings: &UserSettings,
266 store_path: &Path|
267 -> Result<Box<dyn crate::backend::Backend>, _> {
268 let store_relative_git_repo_path = match (
274 dunce::canonicalize(workspace_root),
275 crate::git_backend::canonicalize_git_repo_path(git_repo_path),
276 ) {
277 (Ok(workspace_root), Ok(git_repo_path))
278 if git_repo_path.starts_with(&workspace_root) =>
279 {
280 crate::file_util::relative_path(store_path, &git_repo_path)
281 }
282 _ => git_repo_path.to_owned(),
283 };
284 let backend = crate::git_backend::GitBackend::init_external(
285 settings,
286 store_path,
287 &store_relative_git_repo_path,
288 )?;
289 Ok(Box::new(backend))
290 };
291 let signer = signer_from_settings(user_settings)?;
292 Self::init_with_backend(user_settings, workspace_root, &backend_initializer, signer).await
293 }
294
295 #[expect(clippy::too_many_arguments)]
296 pub async fn init_with_factories(
297 user_settings: &UserSettings,
298 workspace_root: &Path,
299 backend_initializer: &BackendInitializer<'_>,
300 signer: Signer,
301 workspace_store_initializer: &WorkspaceStoreInitializer<'_>,
302 op_store_initializer: &OpStoreInitializer<'_>,
303 op_heads_store_initializer: &OpHeadsStoreInitializer<'_>,
304 index_store_initializer: &IndexStoreInitializer<'_>,
305 submodule_store_initializer: &SubmoduleStoreInitializer<'_>,
306 working_copy_factory: &dyn WorkingCopyFactory,
307 workspace_name: WorkspaceNameBuf,
308 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
309 let jj_dir = create_jj_dir(workspace_root)?;
310 async {
311 let repo_dir = jj_dir.join("repo");
312 std::fs::create_dir(&repo_dir).context(&repo_dir)?;
313 let repo = ReadonlyRepo::init(
314 user_settings,
315 &repo_dir,
316 backend_initializer,
317 signer,
318 workspace_store_initializer,
319 op_store_initializer,
320 op_heads_store_initializer,
321 index_store_initializer,
322 submodule_store_initializer,
323 )
324 .await
325 .map_err(|repo_init_err| match repo_init_err {
326 RepoInitError::Backend(err) => WorkspaceInitError::Backend(err),
327 RepoInitError::OpHeadsStore(err) => WorkspaceInitError::OpHeadsStore(err),
328 RepoInitError::Path(err) => WorkspaceInitError::Path(err),
329 })?;
330 let (working_copy, repo) = init_working_copy(
331 &repo,
332 workspace_root,
333 &jj_dir,
334 working_copy_factory,
335 workspace_name,
336 )
337 .await?;
338 let repo_loader = repo.loader().clone();
339 let repo_dir = dunce::canonicalize(&repo_dir).context(&repo_dir)?;
340 let workspace = Self::new(workspace_root, repo_dir, working_copy, repo_loader)?;
341 repo.loader()
342 .workspace_store()
343 .add(workspace.workspace_name(), workspace.workspace_root())?;
344 Ok((workspace, repo))
345 }
346 .await
347 .inspect_err(|_err| {
348 std::fs::remove_dir_all(jj_dir).ok();
349 })
350 }
351
352 pub async fn init_with_backend(
353 user_settings: &UserSettings,
354 workspace_root: &Path,
355 backend_initializer: &BackendInitializer<'_>,
356 signer: Signer,
357 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
358 Self::init_with_factories(
359 user_settings,
360 workspace_root,
361 backend_initializer,
362 signer,
363 ReadonlyRepo::default_workspace_store_initializer(),
364 ReadonlyRepo::default_op_store_initializer(),
365 ReadonlyRepo::default_op_heads_store_initializer(),
366 ReadonlyRepo::default_index_store_initializer(),
367 ReadonlyRepo::default_submodule_store_initializer(),
368 &*default_working_copy_factory(),
369 WorkspaceName::DEFAULT.to_owned(),
370 )
371 .await
372 }
373
374 pub async fn init_workspace_with_existing_repo(
375 workspace_root: &Path,
376 repo_path: &Path,
377 repo: &Arc<ReadonlyRepo>,
378 working_copy_factory: &dyn WorkingCopyFactory,
379 workspace_name: WorkspaceNameBuf,
380 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
381 let jj_dir = create_jj_dir(workspace_root)?;
382
383 let repo_dir = dunce::canonicalize(repo_path).context(repo_path)?;
384 let jj_dir_abs = dunce::canonicalize(&jj_dir).context(&jj_dir)?;
385 let path_to_store = file_util::relative_path(&jj_dir_abs, &repo_dir);
386 let path_to_store = if path_to_store.is_relative() {
387 file_util::slash_path(&path_to_store).into_owned()
388 } else {
389 path_to_store
390 };
391 let repo_dir_bytes =
392 file_util::path_to_bytes(&path_to_store).map_err(WorkspaceInitError::EncodeRepoPath)?;
393 let repo_file_path = jj_dir.join("repo");
394 fs::write(&repo_file_path, repo_dir_bytes).context(&repo_file_path)?;
395
396 let workspace_store = repo.loader().workspace_store();
397 let (working_copy, repo) = init_working_copy(
398 repo,
399 workspace_root,
400 &jj_dir,
401 working_copy_factory,
402 workspace_name,
403 )
404 .await?;
405 let workspace = Self::new(
406 workspace_root,
407 repo_dir,
408 working_copy,
409 repo.loader().clone(),
410 )?;
411 workspace_store.add(workspace.workspace_name(), workspace.workspace_root())?;
412 Ok((workspace, repo))
413 }
414
415 pub fn load(
416 user_settings: &UserSettings,
417 workspace_path: &Path,
418 store_factories: &StoreFactories,
419 working_copy_factories: &WorkingCopyFactories,
420 ) -> Result<Self, WorkspaceLoadError> {
421 let loader = default_workspace_loader_factory().create(workspace_path)?;
422 let workspace = loader.load(user_settings, store_factories, working_copy_factories)?;
423 Ok(workspace)
424 }
425
426 pub fn workspace_root(&self) -> &Path {
427 &self.workspace_root
428 }
429
430 pub fn workspace_name(&self) -> &WorkspaceName {
431 self.working_copy.workspace_name()
432 }
433
434 pub fn repo_path(&self) -> &Path {
435 &self.repo_path
436 }
437
438 pub fn repo_loader(&self) -> &RepoLoader {
439 &self.repo_loader
440 }
441
442 pub fn settings(&self) -> &UserSettings {
444 self.repo_loader.settings()
445 }
446
447 pub fn working_copy(&self) -> &dyn WorkingCopy {
448 self.working_copy.as_ref()
449 }
450
451 pub async fn start_working_copy_mutation(
452 &mut self,
453 ) -> Result<LockedWorkspace<'_>, WorkingCopyStateError> {
454 let locked_wc = self.working_copy.start_mutation().await?;
455 Ok(LockedWorkspace {
456 base: self,
457 locked_wc,
458 })
459 }
460
461 pub async fn check_out(
462 &mut self,
463 operation_id: OperationId,
464 old_tree: Option<&MergedTree>,
465 commit: &Commit,
466 ) -> Result<CheckoutStats, CheckoutError> {
467 let mut locked_ws = self.start_working_copy_mutation().await?;
468 if let Some(old_tree) = old_tree
473 && old_tree.tree_ids_and_labels()
474 != locked_ws.locked_wc().old_tree().tree_ids_and_labels()
475 {
476 return Err(CheckoutError::ConcurrentCheckout);
477 }
478 let stats = locked_ws.locked_wc().check_out(commit).await?;
479 locked_ws
480 .finish(operation_id)
481 .await
482 .map_err(|err| CheckoutError::Other {
483 message: "Failed to save the working copy state".to_string(),
484 err: err.into(),
485 })?;
486 Ok(stats)
487 }
488}
489
490pub struct LockedWorkspace<'a> {
491 base: &'a mut Workspace,
492 locked_wc: Box<dyn LockedWorkingCopy>,
493}
494
495impl LockedWorkspace<'_> {
496 pub fn locked_wc(&mut self) -> &mut dyn LockedWorkingCopy {
497 self.locked_wc.as_mut()
498 }
499
500 pub async fn finish(self, operation_id: OperationId) -> Result<(), WorkingCopyStateError> {
501 let new_wc = self.locked_wc.finish(operation_id).await?;
502 self.base.working_copy = new_wc;
503 Ok(())
504 }
505}
506
507pub trait WorkspaceLoaderFactory {
509 fn create(&self, workspace_root: &Path)
510 -> Result<Box<dyn WorkspaceLoader>, WorkspaceLoadError>;
511}
512
513pub fn get_working_copy_factory<'a>(
514 workspace_loader: &dyn WorkspaceLoader,
515 working_copy_factories: &'a WorkingCopyFactories,
516) -> Result<&'a dyn WorkingCopyFactory, StoreLoadError> {
517 let working_copy_type = workspace_loader.get_working_copy_type()?;
518
519 if let Some(factory) = working_copy_factories.get(&working_copy_type) {
520 Ok(factory.as_ref())
521 } else {
522 Err(StoreLoadError::UnsupportedType {
523 store: "working copy",
524 store_type: working_copy_type.clone(),
525 })
526 }
527}
528
529pub trait WorkspaceLoader {
532 fn workspace_root(&self) -> &Path;
534
535 fn repo_path(&self) -> &Path;
537
538 fn load(
540 &self,
541 user_settings: &UserSettings,
542 store_factories: &StoreFactories,
543 working_copy_factories: &WorkingCopyFactories,
544 ) -> Result<Workspace, WorkspaceLoadError>;
545
546 fn get_working_copy_type(&self) -> Result<String, StoreLoadError>;
548}
549
550pub struct DefaultWorkspaceLoaderFactory;
551
552impl WorkspaceLoaderFactory for DefaultWorkspaceLoaderFactory {
553 fn create(
554 &self,
555 workspace_root: &Path,
556 ) -> Result<Box<dyn WorkspaceLoader>, WorkspaceLoadError> {
557 Ok(Box::new(DefaultWorkspaceLoader::new(workspace_root)?))
558 }
559}
560
561#[derive(Clone, Debug)]
564struct DefaultWorkspaceLoader {
565 workspace_root: PathBuf,
566 repo_path: PathBuf,
567 working_copy_state_path: PathBuf,
568}
569
570pub type WorkingCopyFactories = HashMap<String, Box<dyn WorkingCopyFactory>>;
571
572impl DefaultWorkspaceLoader {
573 pub fn new(workspace_root: &Path) -> Result<Self, WorkspaceLoadError> {
574 let jj_dir = workspace_root.join(".jj");
575 if !jj_dir.is_dir() {
576 return Err(WorkspaceLoadError::NoWorkspaceHere(
577 workspace_root.to_owned(),
578 ));
579 }
580 let mut repo_dir = jj_dir.join("repo");
581 if repo_dir.is_file() {
584 let buf = fs::read(&repo_dir).context(&repo_dir)?;
585 let repo_path =
586 file_util::path_from_bytes(&buf).map_err(WorkspaceLoadError::DecodeRepoPath)?;
587 repo_dir = dunce::canonicalize(jj_dir.join(repo_path)).context(repo_path)?;
588 if !repo_dir.is_dir() {
589 return Err(WorkspaceLoadError::RepoDoesNotExist(repo_dir));
590 }
591 }
592 let working_copy_state_path = jj_dir.join("working_copy");
593 Ok(Self {
594 workspace_root: workspace_root.to_owned(),
595 repo_path: repo_dir,
596 working_copy_state_path,
597 })
598 }
599}
600
601impl WorkspaceLoader for DefaultWorkspaceLoader {
602 fn workspace_root(&self) -> &Path {
603 &self.workspace_root
604 }
605
606 fn repo_path(&self) -> &Path {
607 &self.repo_path
608 }
609
610 fn load(
611 &self,
612 user_settings: &UserSettings,
613 store_factories: &StoreFactories,
614 working_copy_factories: &WorkingCopyFactories,
615 ) -> Result<Workspace, WorkspaceLoadError> {
616 let repo_loader =
617 RepoLoader::init_from_file_system(user_settings, &self.repo_path, store_factories)?;
618 let working_copy_factory = get_working_copy_factory(self, working_copy_factories)?;
619 let working_copy = working_copy_factory.load_working_copy(
620 repo_loader.store().clone(),
621 self.workspace_root.clone(),
622 self.working_copy_state_path.clone(),
623 user_settings,
624 )?;
625 let workspace = Workspace::new(
626 &self.workspace_root,
627 self.repo_path.clone(),
628 working_copy,
629 repo_loader,
630 )?;
631 Ok(workspace)
632 }
633
634 fn get_working_copy_type(&self) -> Result<String, StoreLoadError> {
635 read_store_type("working copy", self.working_copy_state_path.join("type"))
636 }
637}