1use std::collections::{BTreeMap, BTreeSet};
2
3use atelier_sdk_diff::{Diff, diff_listings};
4use futures::{AsyncReadExt, StreamExt};
5use jj_lib::backend::{CommitId, Signature, Timestamp, TreeValue};
6use jj_lib::config::{ConfigLayer, ConfigSource, StackedConfig};
7use jj_lib::default_backend_factories::{
8 default_backend_factories, default_working_copy_factories, default_working_copy_factory,
9};
10use jj_lib::git::{self, GitImportOptions};
11use jj_lib::gitignore::GitIgnoreFile;
12use jj_lib::matchers::{EverythingMatcher, NothingMatcher};
13use jj_lib::merged_tree::MergedTree;
14use jj_lib::object_id::ObjectId;
15use jj_lib::op_store::RefTarget;
16use jj_lib::ref_name::{RefName, WorkspaceNameBuf};
17use jj_lib::repo::{ReadonlyRepo, Repo};
18use jj_lib::repo_path::RepoPath;
19use jj_lib::rewrite::rebase_commit;
20use jj_lib::settings::UserSettings;
21use jj_lib::working_copy::SnapshotOptions;
22use jj_lib::workspace::{LockedWorkspace, Workspace as JjWorkspace};
23use pollster::block_on;
24use std::fs;
25use std::os::unix::fs::PermissionsExt;
26use std::path::{Path, PathBuf};
27use std::sync::Arc;
28
29use crate::config::Actor;
30use crate::error::{Error, config_err, engine_err};
31use crate::workspace::SKIP_NAMES;
32
33const NEW_FILE_SIZE_MAX: u64 = 50 * 1024 * 1024;
34
35pub(crate) const LADDER_FILE_SIZE_MAX: u64 = 8 * 1024 * 1024;
39const _: () = assert!(LADDER_FILE_SIZE_MAX <= NEW_FILE_SIZE_MAX);
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Snapshot {
45 pub id: String,
47 pub actor: String,
49 pub at_ms: i64,
51 pub parents: Vec<String>,
53}
54
55pub(crate) struct DiffSides {
58 before: MergedTree,
59 after: MergedTree,
60}
61
62pub(crate) struct FileBlob {
64 pub id: String,
65 pub bytes: Vec<u8>,
66}
67
68pub(crate) enum Side {
70 Absent,
72 TooLarge,
75 Blob(FileBlob),
76}
77
78pub(crate) enum StepBack {
80 Stepped { restored: String },
82 AlreadyStepped,
85 LineMoved { head: String },
87}
88
89pub(crate) enum LandOutcome {
91 Landed {
92 snapshot: String,
93 },
94 Conflicted,
97}
98
99enum SnapshotStyle {
102 Stack,
103 Amend,
104}
105
106pub(crate) struct Engine {
108 jj: JjWorkspace,
109 repo: Arc<ReadonlyRepo>,
110 _settings: UserSettings,
111 boundary: Vec<String>,
114}
115
116impl Engine {
117 pub fn init(root: &Path, actor: &Actor, boundary: &[String]) -> Result<Self, Error> {
120 let settings = build_settings(actor)?;
121 let (jj, repo) = block_on(JjWorkspace::init_colocated_git(
122 &settings,
123 root,
124 gix_hash::Kind::Sha1,
125 ))
126 .map_err(engine_err)?;
127 Ok(Self {
128 jj,
129 repo,
130 _settings: settings,
131 boundary: boundary.to_vec(),
132 })
133 }
134
135 pub fn open(root: &Path, actor: &Actor, boundary: &[String]) -> Result<Self, Error> {
137 let settings = build_settings(actor)?;
138 let jj = JjWorkspace::load(
139 &settings,
140 root,
141 &default_backend_factories(),
142 &default_working_copy_factories(),
143 )
144 .map_err(engine_err)?;
145 let repo = block_on(jj.repo_loader().load_at_head()).map_err(engine_err)?;
146 Ok(Self {
147 jj,
148 repo,
149 _settings: settings,
150 boundary: boundary.to_vec(),
151 })
152 }
153
154 pub fn refresh(&mut self) -> Result<(), Error> {
158 self.repo = block_on(self.jj.repo_loader().load_at_head()).map_err(engine_err)?;
159 Ok(())
160 }
161
162 pub fn adopt_git(root: &Path, actor: &Actor, boundary: &[String]) -> Result<Self, Error> {
167 block_on(Self::adopt_git_async(root, actor, boundary))
168 }
169
170 async fn adopt_git_async(
171 root: &Path,
172 actor: &Actor,
173 boundary: &[String],
174 ) -> Result<Self, Error> {
175 let settings = build_settings(actor)?;
176 let (mut jj, repo) = JjWorkspace::init_external_git(&settings, root, &root.join(".git"))
177 .await
178 .map_err(engine_err)?;
179 let mut tx = repo.start_transaction();
180 git::import_head(tx.repo_mut()).await.map_err(engine_err)?;
181 let options = GitImportOptions {
182 abandon_unreachable_commits: false,
183 record_synthetic_predecessors: false,
184 remote_auto_track_bookmarks: std::collections::HashMap::new(),
185 };
186 git::import_refs(tx.repo_mut(), &options)
187 .await
188 .map_err(engine_err)?;
189 let head = tx.repo_mut().view().git_head().as_normal().cloned();
190 let name = jj.workspace_name().to_owned();
191 let wc_commit = match head {
192 Some(head_id) => {
195 let head = tx
196 .repo_mut()
197 .store()
198 .get_commit(&head_id)
199 .map_err(engine_err)?;
200 let wc_commit = tx
201 .repo_mut()
202 .new_commit(vec![head_id], head.tree())
203 .set_author(signature(actor))
204 .write()
205 .await
206 .map_err(engine_err)?;
207 tx.repo_mut()
208 .set_wc_commit(name, wc_commit.id().clone())
209 .map_err(engine_err)?;
210 tx.repo_mut()
211 .rebase_descendants()
212 .await
213 .map_err(engine_err)?;
214 Some(wc_commit)
215 }
216 None => None,
218 };
219 if let Some(wc_commit) = &wc_commit {
220 git::reset_head(tx.repo_mut(), wc_commit)
221 .await
222 .map_err(engine_err)?;
223 }
224 let repo = tx.commit("adopt git repo").await.map_err(engine_err)?;
225 if let Some(wc_commit) = &wc_commit {
226 jj.check_out(repo.op_id().clone(), None, wc_commit)
227 .await
228 .map_err(engine_err)?;
229 }
230 Ok(Self {
231 jj,
232 repo,
233 _settings: settings,
234 boundary: boundary.to_vec(),
235 })
236 }
237
238 pub fn snapshot(&mut self) -> Result<Option<String>, Error> {
241 block_on(self.snapshot_with(&SnapshotStyle::Stack))
242 }
243
244 pub fn snapshot_amend(&mut self) -> Result<Option<String>, Error> {
247 block_on(self.snapshot_with(&SnapshotStyle::Amend))
248 }
249
250 async fn snapshot_with(&mut self, style: &SnapshotStyle) -> Result<Option<String>, Error> {
251 let name = self.jj.workspace_name().to_owned();
252 let wc_id = match self.repo.view().get_wc_commit_id(&name) {
253 Some(id) => id.clone(),
254 None => return Err(Error::Engine("no working-copy commit".to_owned())),
255 };
256 let options = snapshot_options(base_ignores(&self.boundary)?);
257
258 let mut locked = self
259 .jj
260 .start_working_copy_mutation()
261 .await
262 .map_err(engine_err)?;
263
264 let (new_tree, stats) = match locked.locked_wc().snapshot(&options).await {
265 Ok(result) => result,
266 Err(err) => {
267 release_at_old_operation(locked).await?;
268 return Err(engine_err(err));
269 }
270 };
271 if !stats.invalid_utf8_paths.is_empty() {
272 release_at_old_operation(locked).await?;
273 return Err(Error::Engine(
274 "working copy has paths with invalid utf-8 names".to_owned(),
275 ));
276 }
277
278 let wc_commit = self.repo.store().get_commit(&wc_id).map_err(engine_err)?;
279 if new_tree.tree_ids() == wc_commit.tree_ids() {
280 release_at_old_operation(locked).await?;
281 return Ok(None);
282 }
283
284 let mut tx = self.repo.start_transaction();
285 tx.set_is_snapshot(true);
286 let new_commit = match style {
287 SnapshotStyle::Stack => tx
288 .repo_mut()
289 .new_commit(vec![wc_id], new_tree)
290 .write()
291 .await
292 .map_err(engine_err)?,
293 SnapshotStyle::Amend => tx
294 .repo_mut()
295 .rewrite_commit(&wc_commit)
296 .set_tree(new_tree)
297 .write()
298 .await
299 .map_err(engine_err)?,
300 };
301 let new_id = new_commit.id().clone();
302 tx.repo_mut()
303 .set_wc_commit(name, new_id.clone())
304 .map_err(engine_err)?;
305 tx.repo_mut()
306 .rebase_descendants()
307 .await
308 .map_err(engine_err)?;
309 if let SnapshotStyle::Stack = style {
314 git::reset_head(tx.repo_mut(), &new_commit)
315 .await
316 .map_err(engine_err)?;
317 }
318 let repo = tx.commit("snapshot").await.map_err(engine_err)?;
319 locked
320 .finish(repo.op_id().clone())
321 .await
322 .map_err(engine_err)?;
323 self.repo = repo;
324 Ok(Some(new_id.hex()))
325 }
326
327 pub fn log(&self, limit: usize) -> Result<Vec<Snapshot>, Error> {
329 let name = self.jj.workspace_name().to_owned();
330 let wc_id = match self.repo.view().get_wc_commit_id(&name) {
331 Some(id) => id.clone(),
332 None => return Err(Error::Engine("no working-copy commit".to_owned())),
333 };
334 let root = self.repo.store().root_commit_id().clone();
335 let mut out = Vec::new();
336 let mut current = Some(wc_id);
337 while let Some(id) = current {
338 if out.len() >= limit {
339 break;
340 }
341 let commit = self.repo.store().get_commit(&id).map_err(engine_err)?;
342 let parents: Vec<String> = commit
343 .parent_ids()
344 .iter()
345 .filter(|parent| **parent != root)
346 .map(ObjectId::hex)
347 .collect();
348 let author = commit.author();
349 out.push(Snapshot {
350 id: id.hex(),
351 actor: author.name.clone(),
352 at_ms: author.timestamp.timestamp.0,
353 parents,
354 });
355 current = commit
356 .parent_ids()
357 .iter()
358 .find(|parent| **parent != root)
359 .cloned();
360 }
361 Ok(out)
362 }
363
364 pub fn head(&self) -> Result<String, Error> {
366 Ok(self.wc_commit_id()?.hex())
367 }
368
369 pub fn parent_of(&self, id: &str) -> Result<String, Error> {
372 let commit = self.commit_at(id)?;
373 match commit.parent_ids().first() {
374 Some(parent) => Ok(parent.hex()),
375 None => Err(Error::Engine(format!("snapshot {id} has no parent"))),
376 }
377 }
378
379 pub fn create_session_workspace(
383 &mut self,
384 root: &Path,
385 name: &str,
386 actor: &Actor,
387 ) -> Result<String, Error> {
388 block_on(self.create_session_workspace_async(root, name, actor))
389 }
390
391 async fn create_session_workspace_async(
392 &mut self,
393 root: &Path,
394 name: &str,
395 actor: &Actor,
396 ) -> Result<String, Error> {
397 let head_id = self.wc_commit_id()?;
398 let head = self.repo.store().get_commit(&head_id).map_err(engine_err)?;
399 fs::create_dir_all(root)?;
400 let (mut session_ws, repo) = JjWorkspace::init_workspace_with_existing_repo(
401 root,
402 self.jj.repo_path(),
403 &self.repo,
404 &*default_working_copy_factory(),
405 WorkspaceNameBuf::from(name),
406 )
407 .await
408 .map_err(engine_err)?;
409 let mut tx = repo.start_transaction();
410 let wc_commit = tx
411 .repo_mut()
412 .new_commit(vec![head_id], head.tree())
413 .set_author(signature(actor))
414 .write()
415 .await
416 .map_err(engine_err)?;
417 tx.repo_mut()
418 .edit(WorkspaceNameBuf::from(name), &wc_commit)
419 .await
420 .map_err(engine_err)?;
421 tx.repo_mut()
425 .rebase_descendants()
426 .await
427 .map_err(engine_err)?;
428 let repo = tx.commit("open session").await.map_err(engine_err)?;
429 session_ws
430 .check_out(repo.op_id().clone(), None, &wc_commit)
431 .await
432 .map_err(engine_err)?;
433 self.repo = repo;
434 Ok(wc_commit.change_id().hex())
435 }
436
437 pub fn land(&mut self, tip: &str, bookmark: &str) -> Result<LandOutcome, Error> {
441 block_on(self.land_async(tip, bookmark))
442 }
443
444 async fn land_async(&mut self, tip: &str, bookmark: &str) -> Result<LandOutcome, Error> {
445 let name = self.jj.workspace_name().to_owned();
446 let head_id = self.wc_commit_id()?;
447 let tip_commit = self.commit_at(tip)?;
448 let mut tx = self.repo.start_transaction();
449 let rebased = rebase_commit(tx.repo_mut(), tip_commit, vec![head_id])
450 .await
451 .map_err(engine_err)?;
452 if rebased.has_conflict() {
453 return Ok(LandOutcome::Conflicted);
454 }
455 tx.repo_mut()
456 .set_wc_commit(name, rebased.id().clone())
457 .map_err(engine_err)?;
458 tx.repo_mut()
459 .rebase_descendants()
460 .await
461 .map_err(engine_err)?;
462 git::reset_head(tx.repo_mut(), &rebased)
464 .await
465 .map_err(engine_err)?;
466 tx.repo_mut().set_local_bookmark_target(
471 RefName::new(bookmark),
472 RefTarget::normal(rebased.id().clone()),
473 );
474 let exported = git::export_refs(tx.repo_mut()).map_err(engine_err)?;
475 if !exported.failed_bookmarks.is_empty() {
476 return Err(Error::Engine(format!(
477 "bookmark {bookmark:?} failed to export: {:?}",
478 exported.failed_bookmarks
479 )));
480 }
481 let repo = tx.commit("land").await.map_err(engine_err)?;
482 self.repo = repo;
483 self.jj
484 .check_out(self.repo.op_id().clone(), None, &rebased)
485 .await
486 .map_err(engine_err)?;
487 Ok(LandOutcome::Landed {
488 snapshot: rebased.id().hex(),
489 })
490 }
491
492 pub fn step_back(&mut self, landed: &str, bookmark: &str) -> Result<StepBack, Error> {
498 block_on(self.step_back_async(landed, bookmark))
499 }
500
501 async fn step_back_async(&mut self, landed: &str, bookmark: &str) -> Result<StepBack, Error> {
502 let name = self.jj.workspace_name().to_owned();
503 let head = self.wc_commit_id()?;
504 let landed_commit = self.commit_at(landed)?;
505 let Some(parent_id) = landed_commit.parent_ids().first() else {
506 return Err(Error::Engine(format!(
507 "the landed snapshot {landed} has no parent to step back to"
508 )));
509 };
510 if head == *parent_id {
511 return Ok(StepBack::AlreadyStepped);
512 }
513 if head.hex() != landed {
514 return Ok(StepBack::LineMoved { head: head.hex() });
515 }
516 let parent = self
517 .repo
518 .store()
519 .get_commit(parent_id)
520 .map_err(engine_err)?;
521 let mut tx = self.repo.start_transaction();
522 tx.repo_mut()
523 .set_wc_commit(name, parent_id.clone())
524 .map_err(engine_err)?;
525 git::reset_head(tx.repo_mut(), &parent)
528 .await
529 .map_err(engine_err)?;
530 tx.repo_mut().set_local_bookmark_target(
531 RefName::new(bookmark),
532 RefTarget::normal(parent_id.clone()),
533 );
534 let exported = git::export_refs(tx.repo_mut()).map_err(engine_err)?;
535 if !exported.failed_bookmarks.is_empty() {
536 return Err(Error::Engine(format!(
537 "bookmark {bookmark:?} failed to export: {:?}",
538 exported.failed_bookmarks
539 )));
540 }
541 let repo = tx.commit("undo").await.map_err(engine_err)?;
542 self.repo = repo;
543 self.jj
544 .check_out(self.repo.op_id().clone(), None, &parent)
545 .await
546 .map_err(engine_err)?;
547 Ok(StepBack::Stepped {
548 restored: parent_id.hex(),
549 })
550 }
551
552 pub fn tree_changed(&self, id: &str) -> Result<bool, Error> {
555 let commit = self.commit_at(id)?;
556 let Some(parent_id) = commit.parent_ids().first() else {
557 return Ok(true);
558 };
559 let parent = self
560 .repo
561 .store()
562 .get_commit(parent_id)
563 .map_err(engine_err)?;
564 Ok(commit.tree_ids() != parent.tree_ids())
565 }
566
567 fn wc_commit_id(&self) -> Result<CommitId, Error> {
568 let name = self.jj.workspace_name().to_owned();
569 match self.repo.view().get_wc_commit_id(&name) {
570 Some(id) => Ok(id.clone()),
571 None => Err(Error::Engine("no working-copy commit".to_owned())),
572 }
573 }
574
575 fn commit_at(&self, id: &str) -> Result<jj_lib::commit::Commit, Error> {
576 let Some(commit_id) = CommitId::try_from_hex(id) else {
577 return Err(Error::Engine(format!("not a snapshot id: {id}")));
578 };
579 self.repo.store().get_commit(&commit_id).map_err(engine_err)
580 }
581
582 pub fn diff_latest(&self) -> Result<(Diff, DiffSides), Error> {
585 block_on(self.diff_latest_async())
586 }
587
588 async fn diff_latest_async(&self) -> Result<(Diff, DiffSides), Error> {
589 let name = self.jj.workspace_name().to_owned();
590 let wc_id = match self.repo.view().get_wc_commit_id(&name) {
591 Some(id) => id.clone(),
592 None => return Err(Error::Engine("no working-copy commit".to_owned())),
593 };
594 let root = self.repo.store().root_commit_id().clone();
595 let commit = self.repo.store().get_commit(&wc_id).map_err(engine_err)?;
596 let new_tree = commit.tree();
597 let parent = commit
598 .parent_ids()
599 .iter()
600 .find(|parent| **parent != root)
601 .cloned();
602 let old_tree = match parent {
603 Some(parent_id) => self
604 .repo
605 .store()
606 .get_commit(&parent_id)
607 .map_err(engine_err)?
608 .tree(),
609 None => self.empty_tree()?,
610 };
611 self.tree_diff(old_tree, new_tree).await
612 }
613
614 pub fn diff_between(&self, before: &str, after: &str) -> Result<(Diff, DiffSides), Error> {
617 block_on(self.diff_between_async(before, after))
618 }
619
620 async fn diff_between_async(
621 &self,
622 before: &str,
623 after: &str,
624 ) -> Result<(Diff, DiffSides), Error> {
625 let old_tree = self.tree_at(before)?;
626 let new_tree = self.tree_at(after)?;
627 self.tree_diff(old_tree, new_tree).await
628 }
629
630 pub fn read_file_sides(&self, sides: &DiffSides, path: &str) -> Result<(Side, Side), Error> {
632 block_on(async {
633 let path = RepoPath::from_internal_string(path).map_err(engine_err)?;
634 let before = self.file_blob(&sides.before, path).await?;
635 let after = self.file_blob(&sides.after, path).await?;
636 Ok((before, after))
637 })
638 }
639
640 async fn file_blob(&self, tree: &MergedTree, path: &RepoPath) -> Result<Side, Error> {
641 let value = tree.path_value(path).await.map_err(engine_err)?;
642 let Some(Some(TreeValue::File { id, .. })) = value.as_resolved() else {
643 return Ok(Side::Absent);
644 };
645 let reader = self
646 .repo
647 .store()
648 .read_file(path, id)
649 .await
650 .map_err(engine_err)?;
651 let mut bytes = Vec::new();
652 reader
653 .take(LADDER_FILE_SIZE_MAX + 1)
654 .read_to_end(&mut bytes)
655 .await
656 .map_err(engine_err)?;
657 if bytes.len() as u64 > LADDER_FILE_SIZE_MAX {
658 return Ok(Side::TooLarge);
659 }
660 Ok(Side::Blob(FileBlob {
661 id: id.hex(),
662 bytes,
663 }))
664 }
665
666 async fn tree_diff(
667 &self,
668 old_tree: MergedTree,
669 new_tree: MergedTree,
670 ) -> Result<(Diff, DiffSides), Error> {
671 let mut before = BTreeMap::new();
672 let mut after = BTreeMap::new();
673 let mut stream = old_tree.diff_stream(&new_tree, &EverythingMatcher);
674 while let Some(entry) = stream.next().await {
675 let path = entry.path.as_internal_file_string().to_owned();
676 let values = entry.values.map_err(engine_err)?;
677 if values.before.is_present() {
678 before.insert(path.clone(), format!("{:?}", values.before));
679 }
680 if values.after.is_present() {
681 after.insert(path, format!("{:?}", values.after));
682 }
683 }
684 drop(stream);
685 let diff = diff_listings(&before, &after);
686 Ok((
687 diff,
688 DiffSides {
689 before: old_tree,
690 after: new_tree,
691 },
692 ))
693 }
694
695 fn tree_at(&self, id: &str) -> Result<MergedTree, Error> {
696 Ok(self.commit_at(id)?.tree())
697 }
698
699 pub fn export_tree(&self, id: &str, dest: &Path) -> Result<(), Error> {
704 block_on(self.export_tree_async(id, dest))
705 }
706
707 async fn export_tree_async(&self, id: &str, dest: &Path) -> Result<(), Error> {
708 let empty = self.empty_tree()?;
709 let tree = self.tree_at(id)?;
710 let mut kept: BTreeSet<String> = BTreeSet::new();
711 let mut stream = empty.diff_stream(&tree, &EverythingMatcher);
712 while let Some(entry) = stream.next().await {
713 let values = entry.values.map_err(engine_err)?;
714 let rel = entry.path.as_internal_file_string().to_owned();
715 let Some(value) = values.after.as_resolved() else {
716 return Err(Error::Engine(format!("conflicted tree entry at {rel}")));
717 };
718 let Some(value) = value else {
719 continue;
720 };
721 let target = dest.join(&rel);
722 if let Some(parent) = target.parent() {
723 fs::create_dir_all(parent)?;
724 }
725 match value {
726 TreeValue::File { id, executable, .. } => {
727 let mut reader = self
728 .repo
729 .store()
730 .read_file(&entry.path, id)
731 .await
732 .map_err(engine_err)?;
733 let mut bytes = Vec::new();
734 reader.read_to_end(&mut bytes).await.map_err(engine_err)?;
735 fs::write(&target, &bytes)?;
736 if *executable {
737 let mut permissions = fs::metadata(&target)?.permissions();
738 permissions.set_mode(0o755);
739 fs::set_permissions(&target, permissions)?;
740 }
741 }
742 TreeValue::Symlink(id) => {
743 let link = self
744 .repo
745 .store()
746 .read_symlink(&entry.path, id)
747 .await
748 .map_err(engine_err)?;
749 if target.symlink_metadata().is_ok() {
750 fs::remove_file(&target)?;
751 }
752 std::os::unix::fs::symlink(&link, &target)?;
753 }
754 other => {
755 return Err(Error::Engine(format!("cannot export {rel}: {other:?}")));
756 }
757 }
758 kept.insert(rel);
759 }
760 drop(stream);
761 remove_unkept(dest, &kept)
762 }
763
764 fn empty_tree(&self) -> Result<MergedTree, Error> {
765 let root = self.repo.store().root_commit_id().clone();
766 let commit = self.repo.store().get_commit(&root).map_err(engine_err)?;
767 Ok(commit.tree())
768 }
769}
770
771fn build_settings(actor: &Actor) -> Result<UserSettings, Error> {
772 #[derive(serde::Serialize)]
773 struct UserConfig<'a> {
774 user: UserSection<'a>,
775 }
776
777 #[derive(serde::Serialize)]
778 struct UserSection<'a> {
779 name: &'a str,
780 email: String,
781 }
782
783 let mut config = StackedConfig::with_defaults();
784 let text = toml::to_string(&UserConfig {
785 user: UserSection {
786 name: &actor.name,
787 email: format!("{}@atelier.local", actor.name),
788 },
789 })
790 .map_err(config_err)?;
791 let layer = ConfigLayer::parse(ConfigSource::User, &text).map_err(config_err)?;
792 config.add_layer(layer);
793 UserSettings::from_config(config).map_err(config_err)
794}
795
796fn signature(actor: &Actor) -> Signature {
800 Signature {
801 name: actor.name.clone(),
802 email: format!("{}@atelier.local", actor.name),
803 timestamp: Timestamp::now(),
804 }
805}
806
807async fn release_at_old_operation(mut locked: LockedWorkspace<'_>) -> Result<(), Error> {
813 let operation = locked.locked_wc().old_operation_id().clone();
814 locked.finish(operation).await.map_err(engine_err)?;
815 Ok(())
816}
817
818fn snapshot_options(base_ignores: Arc<GitIgnoreFile>) -> SnapshotOptions<'static> {
821 SnapshotOptions {
822 base_ignores,
823 progress: None,
824 start_tracking_matcher: &EverythingMatcher,
825 force_tracking_matcher: &NothingMatcher,
826 max_new_file_size: NEW_FILE_SIZE_MAX,
827 }
828}
829
830fn base_ignores(boundary: &[String]) -> Result<Arc<GitIgnoreFile>, Error> {
831 let mut rules = String::from(".atelier/\n.git/\n.jj/\n");
832 for name in boundary {
833 rules.push('/');
834 rules.push_str(name);
835 rules.push_str("/\n");
836 }
837 GitIgnoreFile::empty()
838 .chain(RepoPath::root(), Path::new(".gitignore"), rules.as_bytes())
839 .map_err(engine_err)
840}
841
842fn remove_unkept(root: &Path, kept: &BTreeSet<String>) -> Result<(), Error> {
846 let mut directories: Vec<PathBuf> = Vec::new();
850 let mut pending = vec![root.to_path_buf()];
851 while let Some(dir) = pending.pop() {
852 for entry in fs::read_dir(&dir)? {
853 let entry = entry?;
854 let name = entry.file_name();
855 let Some(name) = name.to_str() else {
856 return Err(Error::Engine(format!(
857 "cannot mirror over a non-utf8 name at {}",
858 entry.path().display()
859 )));
860 };
861 if SKIP_NAMES.contains(&name) {
862 continue;
863 }
864 let path = entry.path();
865 if entry.file_type()?.is_dir() {
866 directories.push(path.clone());
867 pending.push(path);
868 } else {
869 let rel = path
870 .strip_prefix(root)
871 .map_err(engine_err)?
872 .components()
873 .map(|component| component.as_os_str().to_string_lossy())
874 .collect::<Vec<_>>()
875 .join("/");
876 if !kept.contains(&rel) {
877 fs::remove_file(&path)?;
878 }
879 }
880 }
881 }
882 directories.sort_by_key(|dir| std::cmp::Reverse(dir.components().count()));
883 for dir in directories {
884 if fs::read_dir(&dir)?.next().is_none() {
885 fs::remove_dir(&dir)?;
886 }
887 }
888 Ok(())
889}