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