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