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