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