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(ref e) if e.source.kind() == io::ErrorKind::AlreadyExists => {
128 Err(WorkspaceInitError::DestinationExists(jj_dir))
129 }
130 Err(e) => Err(e.into()),
131 }
132}
133
134fn 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 let repo = tx.commit(format!("add workspace '{}'", workspace_name.as_symbol()))?;
148
149 let working_copy = working_copy_factory.init_working_copy(
150 repo.store().clone(),
151 workspace_root.to_path_buf(),
152 working_copy_state_path.clone(),
153 repo.op_id().clone(),
154 workspace_name,
155 repo.settings(),
156 )?;
157 let working_copy_type_path = working_copy_state_path.join("type");
158 fs::write(&working_copy_type_path, working_copy.name()).context(&working_copy_type_path)?;
159 Ok((working_copy, repo))
160}
161
162impl Workspace {
163 pub fn new(
164 workspace_root: &Path,
165 repo_path: PathBuf,
166 working_copy: Box<dyn WorkingCopy>,
167 repo_loader: RepoLoader,
168 ) -> Result<Self, PathError> {
169 let workspace_root = dunce::canonicalize(workspace_root).context(workspace_root)?;
170 Ok(Self::new_no_canonicalize(
171 workspace_root,
172 repo_path,
173 working_copy,
174 repo_loader,
175 ))
176 }
177
178 pub fn new_no_canonicalize(
179 workspace_root: PathBuf,
180 repo_path: PathBuf,
181 working_copy: Box<dyn WorkingCopy>,
182 repo_loader: RepoLoader,
183 ) -> Self {
184 Self {
185 workspace_root,
186 repo_path,
187 repo_loader,
188 working_copy,
189 }
190 }
191
192 pub fn init_simple(
193 user_settings: &UserSettings,
194 workspace_root: &Path,
195 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
196 let backend_initializer: &BackendInitializer =
197 &|_settings, store_path| Ok(Box::new(SimpleBackend::init(store_path)));
198 let signer = Signer::from_settings(user_settings)?;
199 Self::init_with_backend(user_settings, workspace_root, backend_initializer, signer)
200 }
201
202 #[cfg(feature = "git")]
205 pub fn init_internal_git(
206 user_settings: &UserSettings,
207 workspace_root: &Path,
208 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
209 let backend_initializer: &BackendInitializer = &|settings, store_path| {
210 Ok(Box::new(crate::git_backend::GitBackend::init_internal(
211 settings, store_path,
212 )?))
213 };
214 let signer = Signer::from_settings(user_settings)?;
215 Self::init_with_backend(user_settings, workspace_root, backend_initializer, signer)
216 }
217
218 #[cfg(feature = "git")]
221 pub fn init_colocated_git(
222 user_settings: &UserSettings,
223 workspace_root: &Path,
224 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
225 let backend_initializer = |settings: &UserSettings,
226 store_path: &Path|
227 -> Result<Box<dyn crate::backend::Backend>, _> {
228 let store_relative_workspace_root =
232 if let Ok(workspace_root) = dunce::canonicalize(workspace_root) {
233 crate::file_util::relative_path(store_path, &workspace_root)
234 } else {
235 workspace_root.to_owned()
236 };
237 let backend = crate::git_backend::GitBackend::init_colocated(
238 settings,
239 store_path,
240 &store_relative_workspace_root,
241 )?;
242 Ok(Box::new(backend))
243 };
244 let signer = Signer::from_settings(user_settings)?;
245 Self::init_with_backend(user_settings, workspace_root, &backend_initializer, signer)
246 }
247
248 #[cfg(feature = "git")]
253 pub fn init_external_git(
254 user_settings: &UserSettings,
255 workspace_root: &Path,
256 git_repo_path: &Path,
257 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
258 let backend_initializer = |settings: &UserSettings,
259 store_path: &Path|
260 -> Result<Box<dyn crate::backend::Backend>, _> {
261 let store_relative_git_repo_path = match (
267 dunce::canonicalize(workspace_root),
268 crate::git_backend::canonicalize_git_repo_path(git_repo_path),
269 ) {
270 (Ok(workspace_root), Ok(git_repo_path))
271 if git_repo_path.starts_with(&workspace_root) =>
272 {
273 crate::file_util::relative_path(store_path, &git_repo_path)
274 }
275 _ => git_repo_path.to_owned(),
276 };
277 let backend = crate::git_backend::GitBackend::init_external(
278 settings,
279 store_path,
280 &store_relative_git_repo_path,
281 )?;
282 Ok(Box::new(backend))
283 };
284 let signer = Signer::from_settings(user_settings)?;
285 Self::init_with_backend(user_settings, workspace_root, &backend_initializer, signer)
286 }
287
288 #[expect(clippy::too_many_arguments)]
289 pub fn init_with_factories(
290 user_settings: &UserSettings,
291 workspace_root: &Path,
292 backend_initializer: &BackendInitializer,
293 signer: Signer,
294 op_store_initializer: &OpStoreInitializer,
295 op_heads_store_initializer: &OpHeadsStoreInitializer,
296 index_store_initializer: &IndexStoreInitializer,
297 submodule_store_initializer: &SubmoduleStoreInitializer,
298 working_copy_factory: &dyn WorkingCopyFactory,
299 workspace_name: WorkspaceNameBuf,
300 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
301 let jj_dir = create_jj_dir(workspace_root)?;
302 (|| {
303 let repo_dir = jj_dir.join("repo");
304 std::fs::create_dir(&repo_dir).context(&repo_dir)?;
305 let repo = ReadonlyRepo::init(
306 user_settings,
307 &repo_dir,
308 backend_initializer,
309 signer,
310 op_store_initializer,
311 op_heads_store_initializer,
312 index_store_initializer,
313 submodule_store_initializer,
314 )
315 .map_err(|repo_init_err| match repo_init_err {
316 RepoInitError::Backend(err) => WorkspaceInitError::Backend(err),
317 RepoInitError::OpHeadsStore(err) => WorkspaceInitError::OpHeadsStore(err),
318 RepoInitError::Path(err) => WorkspaceInitError::Path(err),
319 })?;
320 let workspace_store = SimpleWorkspaceStore::load(&repo_dir)?;
321 let (working_copy, repo) = init_working_copy(
322 &repo,
323 workspace_root,
324 &jj_dir,
325 working_copy_factory,
326 workspace_name,
327 )?;
328 let repo_loader = repo.loader().clone();
329 let workspace = Self::new(workspace_root, repo_dir, working_copy, repo_loader)?;
330 workspace_store.add(workspace.workspace_name(), workspace.workspace_root())?;
331 Ok((workspace, repo))
332 })()
333 .inspect_err(|_err| {
334 std::fs::remove_dir_all(jj_dir).ok();
335 })
336 }
337
338 pub fn init_with_backend(
339 user_settings: &UserSettings,
340 workspace_root: &Path,
341 backend_initializer: &BackendInitializer,
342 signer: Signer,
343 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
344 Self::init_with_factories(
345 user_settings,
346 workspace_root,
347 backend_initializer,
348 signer,
349 ReadonlyRepo::default_op_store_initializer(),
350 ReadonlyRepo::default_op_heads_store_initializer(),
351 ReadonlyRepo::default_index_store_initializer(),
352 ReadonlyRepo::default_submodule_store_initializer(),
353 &*default_working_copy_factory(),
354 WorkspaceName::DEFAULT.to_owned(),
355 )
356 }
357
358 pub fn init_workspace_with_existing_repo(
359 workspace_root: &Path,
360 repo_path: &Path,
361 repo: &Arc<ReadonlyRepo>,
362 working_copy_factory: &dyn WorkingCopyFactory,
363 workspace_name: WorkspaceNameBuf,
364 ) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
365 let jj_dir = create_jj_dir(workspace_root)?;
366
367 let repo_dir = dunce::canonicalize(repo_path).context(repo_path)?;
368 let repo_dir_bytes =
369 file_util::path_to_bytes(&repo_dir).map_err(WorkspaceInitError::EncodeRepoPath)?;
370 let repo_file_path = jj_dir.join("repo");
371 fs::write(&repo_file_path, repo_dir_bytes).context(&repo_file_path)?;
372
373 let workspace_store = SimpleWorkspaceStore::load(repo_path)?;
374 let (working_copy, repo) = init_working_copy(
375 repo,
376 workspace_root,
377 &jj_dir,
378 working_copy_factory,
379 workspace_name,
380 )?;
381 let workspace = Self::new(
382 workspace_root,
383 repo_dir,
384 working_copy,
385 repo.loader().clone(),
386 )?;
387 workspace_store.add(workspace.workspace_name(), workspace.workspace_root())?;
388 Ok((workspace, repo))
389 }
390
391 pub fn load(
392 user_settings: &UserSettings,
393 workspace_path: &Path,
394 store_factories: &StoreFactories,
395 working_copy_factories: &WorkingCopyFactories,
396 ) -> Result<Self, WorkspaceLoadError> {
397 let loader = DefaultWorkspaceLoader::new(workspace_path)?;
398 let workspace = loader.load(user_settings, store_factories, working_copy_factories)?;
399 Ok(workspace)
400 }
401
402 pub fn workspace_root(&self) -> &Path {
403 &self.workspace_root
404 }
405
406 pub fn workspace_name(&self) -> &WorkspaceName {
407 self.working_copy.workspace_name()
408 }
409
410 pub fn repo_path(&self) -> &Path {
411 &self.repo_path
412 }
413
414 pub fn repo_loader(&self) -> &RepoLoader {
415 &self.repo_loader
416 }
417
418 pub fn settings(&self) -> &UserSettings {
420 self.repo_loader.settings()
421 }
422
423 pub fn working_copy(&self) -> &dyn WorkingCopy {
424 self.working_copy.as_ref()
425 }
426
427 pub fn start_working_copy_mutation(
428 &mut self,
429 ) -> Result<LockedWorkspace<'_>, WorkingCopyStateError> {
430 let locked_wc = self.working_copy.start_mutation()?;
431 Ok(LockedWorkspace {
432 base: self,
433 locked_wc,
434 })
435 }
436
437 pub fn check_out(
438 &mut self,
439 operation_id: OperationId,
440 old_tree: Option<&MergedTree>,
441 commit: &Commit,
442 ) -> Result<CheckoutStats, CheckoutError> {
443 let mut locked_ws = self.start_working_copy_mutation()?;
444 if let Some(old_tree) = old_tree
449 && old_tree.tree_ids_and_labels()
450 != locked_ws.locked_wc().old_tree().tree_ids_and_labels()
451 {
452 return Err(CheckoutError::ConcurrentCheckout);
453 }
454 let stats = locked_ws.locked_wc().check_out(commit).block_on()?;
455 locked_ws
456 .finish(operation_id)
457 .map_err(|err| CheckoutError::Other {
458 message: "Failed to save the working copy state".to_string(),
459 err: err.into(),
460 })?;
461 Ok(stats)
462 }
463}
464
465pub struct LockedWorkspace<'a> {
466 base: &'a mut Workspace,
467 locked_wc: Box<dyn LockedWorkingCopy>,
468}
469
470impl LockedWorkspace<'_> {
471 pub fn locked_wc(&mut self) -> &mut dyn LockedWorkingCopy {
472 self.locked_wc.as_mut()
473 }
474
475 pub fn finish(self, operation_id: OperationId) -> Result<(), WorkingCopyStateError> {
476 let new_wc = self.locked_wc.finish(operation_id).block_on()?;
477 self.base.working_copy = new_wc;
478 Ok(())
479 }
480}
481
482pub trait WorkspaceLoaderFactory {
484 fn create(&self, workspace_root: &Path)
485 -> Result<Box<dyn WorkspaceLoader>, WorkspaceLoadError>;
486}
487
488pub fn get_working_copy_factory<'a>(
489 workspace_loader: &dyn WorkspaceLoader,
490 working_copy_factories: &'a WorkingCopyFactories,
491) -> Result<&'a dyn WorkingCopyFactory, StoreLoadError> {
492 let working_copy_type = workspace_loader.get_working_copy_type()?;
493
494 if let Some(factory) = working_copy_factories.get(&working_copy_type) {
495 Ok(factory.as_ref())
496 } else {
497 Err(StoreLoadError::UnsupportedType {
498 store: "working copy",
499 store_type: working_copy_type.clone(),
500 })
501 }
502}
503
504pub trait WorkspaceLoader {
507 fn workspace_root(&self) -> &Path;
509
510 fn repo_path(&self) -> &Path;
512
513 fn load(
515 &self,
516 user_settings: &UserSettings,
517 store_factories: &StoreFactories,
518 working_copy_factories: &WorkingCopyFactories,
519 ) -> Result<Workspace, WorkspaceLoadError>;
520
521 fn get_working_copy_type(&self) -> Result<String, StoreLoadError>;
523}
524
525pub struct DefaultWorkspaceLoaderFactory;
526
527impl WorkspaceLoaderFactory for DefaultWorkspaceLoaderFactory {
528 fn create(
529 &self,
530 workspace_root: &Path,
531 ) -> Result<Box<dyn WorkspaceLoader>, WorkspaceLoadError> {
532 Ok(Box::new(DefaultWorkspaceLoader::new(workspace_root)?))
533 }
534}
535
536#[derive(Clone, Debug)]
539struct DefaultWorkspaceLoader {
540 workspace_root: PathBuf,
541 repo_path: PathBuf,
542 working_copy_state_path: PathBuf,
543}
544
545pub type WorkingCopyFactories = HashMap<String, Box<dyn WorkingCopyFactory>>;
546
547impl DefaultWorkspaceLoader {
548 pub fn new(workspace_root: &Path) -> Result<Self, WorkspaceLoadError> {
549 let jj_dir = workspace_root.join(".jj");
550 if !jj_dir.is_dir() {
551 return Err(WorkspaceLoadError::NoWorkspaceHere(
552 workspace_root.to_owned(),
553 ));
554 }
555 let mut repo_dir = jj_dir.join("repo");
556 if repo_dir.is_file() {
559 let buf = fs::read(&repo_dir).context(&repo_dir)?;
560 let repo_path =
561 file_util::path_from_bytes(&buf).map_err(WorkspaceLoadError::DecodeRepoPath)?;
562 repo_dir = dunce::canonicalize(jj_dir.join(repo_path)).context(repo_path)?;
563 if !repo_dir.is_dir() {
564 return Err(WorkspaceLoadError::RepoDoesNotExist(repo_dir));
565 }
566 }
567 let working_copy_state_path = jj_dir.join("working_copy");
568 Ok(Self {
569 workspace_root: workspace_root.to_owned(),
570 repo_path: repo_dir,
571 working_copy_state_path,
572 })
573 }
574}
575
576impl WorkspaceLoader for DefaultWorkspaceLoader {
577 fn workspace_root(&self) -> &Path {
578 &self.workspace_root
579 }
580
581 fn repo_path(&self) -> &Path {
582 &self.repo_path
583 }
584
585 fn load(
586 &self,
587 user_settings: &UserSettings,
588 store_factories: &StoreFactories,
589 working_copy_factories: &WorkingCopyFactories,
590 ) -> Result<Workspace, WorkspaceLoadError> {
591 let repo_loader =
592 RepoLoader::init_from_file_system(user_settings, &self.repo_path, store_factories)?;
593 let working_copy_factory = get_working_copy_factory(self, working_copy_factories)?;
594 let working_copy = working_copy_factory.load_working_copy(
595 repo_loader.store().clone(),
596 self.workspace_root.clone(),
597 self.working_copy_state_path.clone(),
598 user_settings,
599 )?;
600 let workspace = Workspace::new(
601 &self.workspace_root,
602 self.repo_path.clone(),
603 working_copy,
604 repo_loader,
605 )?;
606 Ok(workspace)
607 }
608
609 fn get_working_copy_type(&self) -> Result<String, StoreLoadError> {
610 read_store_type("working copy", self.working_copy_state_path.join("type"))
611 }
612}
613
614pub fn default_working_copy_factories() -> WorkingCopyFactories {
615 let mut factories = WorkingCopyFactories::new();
616 factories.insert(
617 LocalWorkingCopy::name().to_owned(),
618 Box::new(LocalWorkingCopyFactory {}),
619 );
620 factories
621}
622
623pub fn default_working_copy_factory() -> Box<dyn WorkingCopyFactory> {
624 Box::new(LocalWorkingCopyFactory {})
625}