1use std::cell::{RefCell, RefMut};
16use std::collections::{BTreeMap, HashSet};
17use std::convert::TryInto;
18use std::fs;
19use std::fs::{File, OpenOptions};
20#[cfg(not(windows))]
21use std::os::unix::fs::symlink;
22#[cfg(not(windows))]
23use std::os::unix::fs::PermissionsExt;
24#[cfg(windows)]
25use std::os::windows::fs::symlink_file;
26use std::path::PathBuf;
27use std::time::UNIX_EPOCH;
28
29use protobuf::Message;
30use tempfile::NamedTempFile;
31use thiserror::Error;
32
33use crate::commit::Commit;
34use crate::commit_builder::CommitBuilder;
35use crate::lock::FileLock;
36use crate::repo::ReadonlyRepo;
37use crate::repo_path::{
38 DirRepoPath, DirRepoPathComponent, FileRepoPath, FileRepoPathComponent, RepoPathJoin,
39};
40use crate::settings::UserSettings;
41use crate::store::{CommitId, FileId, MillisSinceEpoch, StoreError, SymlinkId, TreeId, TreeValue};
42use crate::store_wrapper::StoreWrapper;
43use crate::trees::TreeValueDiff;
44use git2::{Repository, RepositoryInitOptions};
45use std::sync::Arc;
46
47#[derive(Debug, PartialEq, Eq, Clone)]
48pub enum FileType {
49 Normal,
50 Executable,
51 Symlink,
52}
53
54#[derive(Debug, PartialEq, Eq, Clone)]
55pub struct FileState {
56 pub file_type: FileType,
57 pub mtime: MillisSinceEpoch,
58 pub size: u64,
59 }
63
64impl FileState {
65 fn null() -> FileState {
66 FileState {
67 file_type: FileType::Normal,
68 mtime: MillisSinceEpoch(0),
69 size: 0,
70 }
71 }
72}
73
74pub struct TreeState {
75 store: Arc<StoreWrapper>,
76 working_copy_path: PathBuf,
77 state_path: PathBuf,
78 tree_id: TreeId,
79 file_states: BTreeMap<FileRepoPath, FileState>,
80 read_time: MillisSinceEpoch,
81}
82
83fn file_state_from_proto(proto: &crate::protos::working_copy::FileState) -> FileState {
84 let file_type = match proto.file_type {
85 crate::protos::working_copy::FileType::Normal => FileType::Normal,
86 crate::protos::working_copy::FileType::Symlink => FileType::Symlink,
87 crate::protos::working_copy::FileType::Executable => FileType::Executable,
88 };
89 FileState {
90 file_type,
91 mtime: MillisSinceEpoch(proto.mtime_millis_since_epoch),
92 size: proto.size,
93 }
94}
95
96fn file_state_to_proto(file_state: &FileState) -> crate::protos::working_copy::FileState {
97 let mut proto = crate::protos::working_copy::FileState::new();
98 let file_type = match &file_state.file_type {
99 FileType::Normal => crate::protos::working_copy::FileType::Normal,
100 FileType::Symlink => crate::protos::working_copy::FileType::Symlink,
101 FileType::Executable => crate::protos::working_copy::FileType::Executable,
102 };
103 proto.file_type = file_type;
104 proto.mtime_millis_since_epoch = file_state.mtime.0;
105 proto.size = file_state.size;
106 proto
107}
108
109fn file_states_from_proto(
110 proto: &crate::protos::working_copy::TreeState,
111) -> BTreeMap<FileRepoPath, FileState> {
112 let mut file_states = BTreeMap::new();
113 for (path_str, proto_file_state) in &proto.file_states {
114 let path = FileRepoPath::from(path_str.as_str());
115 file_states.insert(path, file_state_from_proto(&proto_file_state));
116 }
117 file_states
118}
119
120fn create_parent_dirs(disk_path: &PathBuf) {
121 fs::create_dir_all(disk_path.parent().unwrap())
122 .unwrap_or_else(|_| panic!("failed to create parent directories for {:?}", &disk_path));
123}
124
125#[derive(Debug, PartialEq, Eq, Clone)]
126pub struct CheckoutStats {
127 pub updated_files: u32,
128 pub added_files: u32,
129 pub removed_files: u32,
130}
131
132#[derive(Debug, Error, PartialEq, Eq)]
133pub enum CheckoutError {
134 #[error("Update target not found")]
135 TargetNotFound,
136 #[error("Current checkout not found")]
139 SourceNotFound,
140 #[error("Concurrent checkout")]
143 ConcurrentCheckout,
144 #[error("Internal error: {0:?}")]
145 InternalStoreError(StoreError),
146}
147
148impl TreeState {
149 pub fn current_tree_id(&self) -> &TreeId {
150 &self.tree_id
151 }
152
153 pub fn file_states(&self) -> &BTreeMap<FileRepoPath, FileState> {
154 &self.file_states
155 }
156
157 pub fn init(
158 store: Arc<StoreWrapper>,
159 working_copy_path: PathBuf,
160 state_path: PathBuf,
161 ) -> TreeState {
162 let mut wc = TreeState::empty(store, working_copy_path, state_path);
163 wc.save();
164 wc
165 }
166
167 fn empty(
168 store: Arc<StoreWrapper>,
169 working_copy_path: PathBuf,
170 state_path: PathBuf,
171 ) -> TreeState {
172 let tree_id = store.empty_tree_id().clone();
173 TreeState {
176 store,
177 working_copy_path: working_copy_path.canonicalize().unwrap(),
178 state_path,
179 tree_id,
180 file_states: BTreeMap::new(),
181 read_time: MillisSinceEpoch(0),
182 }
183 }
184
185 pub fn load(
186 store: Arc<StoreWrapper>,
187 working_copy_path: PathBuf,
188 state_path: PathBuf,
189 ) -> TreeState {
190 let maybe_file = File::open(state_path.join("tree_state"));
191 let file = match maybe_file {
192 Err(ref err) if err.kind() == std::io::ErrorKind::NotFound => {
193 return TreeState::init(store, working_copy_path, state_path);
194 }
195 result => result.unwrap(),
196 };
197
198 let mut wc = TreeState::empty(store, working_copy_path, state_path);
199 wc.read(file);
200 wc
201 }
202
203 fn update_read_time(&mut self) {
204 let own_file_state = self
205 .file_state(&self.state_path.join("tree_state"))
206 .unwrap_or_else(FileState::null);
207 self.read_time = own_file_state.mtime;
208 }
209
210 fn read(&mut self, mut file: File) {
211 self.update_read_time();
212 let proto: crate::protos::working_copy::TreeState =
213 protobuf::parse_from_reader(&mut file).unwrap();
214 self.tree_id = TreeId(proto.tree_id.clone());
215 self.file_states = file_states_from_proto(&proto);
216 }
217
218 fn save(&mut self) {
219 let mut proto = crate::protos::working_copy::TreeState::new();
220 proto.tree_id = self.tree_id.0.clone();
221 for (file, file_state) in &self.file_states {
222 proto
223 .file_states
224 .insert(file.to_internal_string(), file_state_to_proto(file_state));
225 }
226
227 let mut temp_file = NamedTempFile::new_in(&self.state_path).unwrap();
228 self.update_read_time();
231 proto.write_to_writer(temp_file.as_file_mut()).unwrap();
232 temp_file
233 .persist(self.state_path.join("tree_state"))
234 .unwrap();
235 }
236
237 fn file_state(&self, path: &PathBuf) -> Option<FileState> {
238 let metadata = path.symlink_metadata().ok()?;
239 let time = metadata.modified().unwrap();
240 let since_epoch = time.duration_since(UNIX_EPOCH).unwrap();
241 let mtime = MillisSinceEpoch(since_epoch.as_millis().try_into().unwrap());
242 let size = metadata.len();
243 let metadata_file_type = metadata.file_type();
244 let file_type = if metadata_file_type.is_dir() {
245 panic!("expected file, not directory: {:?}", path);
246 } else if metadata_file_type.is_symlink() {
247 FileType::Symlink
248 } else {
249 let mode = metadata.permissions().mode();
250 if mode & 0o111 != 0 {
251 FileType::Executable
252 } else {
253 FileType::Normal
254 }
255 };
256 Some(FileState {
257 file_type,
258 mtime,
259 size,
260 })
261 }
262
263 fn write_file_to_store(&self, path: &FileRepoPath, disk_path: &PathBuf) -> FileId {
264 let file = File::open(disk_path).unwrap();
265 self.store.write_file(path, &mut Box::new(file)).unwrap()
266 }
267
268 fn write_symlink_to_store(&self, path: &FileRepoPath, disk_path: &PathBuf) -> SymlinkId {
269 let target = disk_path.read_link().unwrap();
270 let str_target = target.to_str().unwrap();
271 self.store.write_symlink(path, str_target).unwrap()
272 }
273
274 pub fn write_tree(&mut self) -> &TreeId {
278 let git_repo_dir = tempfile::tempdir().unwrap();
282 let mut git_repo_options = RepositoryInitOptions::new();
283 git_repo_options.workdir_path(&self.working_copy_path);
284 let git_repo = Repository::init_opts(git_repo_dir.path(), &git_repo_options).unwrap();
285
286 let mut work = vec![(DirRepoPath::root(), self.working_copy_path.clone())];
287 let mut tree_builder = self.store.tree_builder(self.tree_id.clone());
288 let mut deleted_files: HashSet<&FileRepoPath> = self.file_states.keys().collect();
289 let mut modified_files = BTreeMap::new();
290 while !work.is_empty() {
291 let (dir, disk_dir) = work.pop().unwrap();
292 for maybe_entry in disk_dir.read_dir().unwrap() {
293 let entry = maybe_entry.unwrap();
294 let file_type = entry.file_type().unwrap();
295 let file_name = entry.file_name();
296 let name = file_name.to_str().unwrap();
297 if name == ".jj" {
298 continue;
299 }
300 if file_type.is_dir() {
301 let subdir = dir.join(&DirRepoPathComponent::from(name));
302 let disk_subdir = disk_dir.join(file_name);
303 work.push((subdir, disk_subdir));
304 } else {
305 let file = dir.join(&FileRepoPathComponent::from(name));
306 let disk_file = disk_dir.join(file_name);
307 deleted_files.remove(&file);
308 let new_file_state = self.file_state(&entry.path()).unwrap();
309 let clean = match self.file_states.get(&file) {
310 None => {
311 if git_repo.status_should_ignore(&disk_file).unwrap() {
313 continue;
314 }
315 false
316 }
317 Some(current_entry) => {
318 current_entry == &new_file_state && current_entry.mtime < self.read_time
319 }
320 };
321 if !clean {
322 let file_value = match new_file_state.file_type {
323 FileType::Normal | FileType::Executable => {
324 let id = self.write_file_to_store(&file, &disk_file);
325 TreeValue::Normal {
326 id,
327 executable: new_file_state.file_type == FileType::Executable,
328 }
329 }
330 FileType::Symlink => {
331 let id = self.write_symlink_to_store(&file, &disk_file);
332 TreeValue::Symlink(id)
333 }
334 };
335 tree_builder.set(file.to_repo_path(), file_value);
336 modified_files.insert(file, new_file_state);
337 }
338 }
339 }
340 }
341
342 let deleted_files: Vec<FileRepoPath> = deleted_files.iter().cloned().cloned().collect();
343
344 for file in &deleted_files {
345 self.file_states.remove(file);
346 tree_builder.remove(file.to_repo_path());
347 }
348 for (file, file_state) in modified_files {
349 self.file_states.insert(file, file_state);
350 }
351 self.tree_id = tree_builder.write_tree();
352 self.save();
353 &self.tree_id
354 }
355
356 fn write_file(
357 &self,
358 disk_path: &PathBuf,
359 path: &FileRepoPath,
360 id: &FileId,
361 executable: bool,
362 ) -> FileState {
363 create_parent_dirs(disk_path);
364 let mut file = OpenOptions::new()
365 .write(true)
366 .create_new(true)
367 .truncate(true)
368 .open(disk_path)
369 .unwrap_or_else(|_| panic!("failed to open {:?} for write", &disk_path));
370 let mut contents = self.store.read_file(path, id).unwrap();
371 std::io::copy(&mut contents, &mut file).unwrap();
372 self.set_executable(disk_path, executable);
373 self.file_state(&disk_path).unwrap()
378 }
379
380 fn write_symlink(&self, disk_path: &PathBuf, path: &FileRepoPath, id: &SymlinkId) -> FileState {
381 create_parent_dirs(disk_path);
382 #[cfg(windows)]
383 {
384 unimplemented!();
385 }
386 #[cfg(not(windows))]
387 {
388 let target = self.store.read_symlink(path, id).unwrap();
389 let target = PathBuf::from(&target);
390 symlink(target, disk_path).unwrap();
391 }
392 self.file_state(&disk_path).unwrap()
393 }
394
395 fn set_executable(&self, disk_path: &PathBuf, executable: bool) {
396 let mode = if executable { 0o755 } else { 0o644 };
397 fs::set_permissions(disk_path, fs::Permissions::from_mode(mode)).unwrap();
398 }
399
400 pub fn check_out(&mut self, tree_id: TreeId) -> Result<CheckoutStats, CheckoutError> {
401 let old_tree = self
402 .store
403 .get_tree(&DirRepoPath::root(), &self.tree_id)
404 .map_err(|err| match err {
405 StoreError::NotFound => CheckoutError::SourceNotFound,
406 other => CheckoutError::InternalStoreError(other),
407 })?;
408 let new_tree = self
409 .store
410 .get_tree(&DirRepoPath::root(), &tree_id)
411 .map_err(|err| match err {
412 StoreError::NotFound => CheckoutError::TargetNotFound,
413 other => CheckoutError::InternalStoreError(other),
414 })?;
415
416 let mut stats = CheckoutStats {
417 updated_files: 0,
418 added_files: 0,
419 removed_files: 0,
420 };
421
422 old_tree.diff(&new_tree, &mut |path, diff| {
423 let disk_path = self
424 .working_copy_path
425 .join(PathBuf::from(path.to_internal_string()));
426
427 match diff {
429 TreeValueDiff::Removed(_before) => {
430 fs::remove_file(&disk_path).ok();
431 let mut parent_dir = disk_path.parent().unwrap();
432 loop {
433 if fs::remove_dir(&parent_dir).is_err() {
434 break;
435 }
436 parent_dir = parent_dir.parent().unwrap();
437 }
438 self.file_states.remove(&path);
439 stats.removed_files += 1;
440 }
441 TreeValueDiff::Added(after) => {
442 let file_state = match after {
443 TreeValue::Normal { id, executable } => {
444 self.write_file(&disk_path, path, id, *executable)
445 }
446 TreeValue::Symlink(id) => self.write_symlink(&disk_path, path, id),
447 TreeValue::GitSubmodule(_id) => {
448 println!("ignoring git submodule at {:?}", path);
449 return;
450 }
451 TreeValue::Tree(_id) => {
452 panic!("unexpected tree entry in diff at {:?}", path);
453 }
454 TreeValue::Conflict(_id) => {
455 panic!(
456 "conflicts cannot be represented in the working copy: {:?}",
457 path
458 );
459 }
460 };
461 self.file_states.insert(path.clone(), file_state);
462 stats.added_files += 1;
463 }
464 TreeValueDiff::Modified(before, after) => {
465 fs::remove_file(&disk_path).ok();
466 let file_state = match (before, after) {
467 (
468 TreeValue::Normal {
469 id: old_id,
470 executable: old_executable,
471 },
472 TreeValue::Normal { id, executable },
473 ) if id == old_id => {
474 assert_ne!(executable, old_executable);
476 self.set_executable(&disk_path, *executable);
477 let mut file_state = self.file_states.get(&path).unwrap().clone();
478 file_state.file_type = if *executable {
479 FileType::Executable
480 } else {
481 FileType::Normal
482 };
483 file_state
484 }
485 (_, TreeValue::Normal { id, executable }) => {
486 self.write_file(&disk_path, path, id, *executable)
487 }
488 (_, TreeValue::Symlink(id)) => self.write_symlink(&disk_path, path, id),
489 (_, TreeValue::GitSubmodule(_id)) => {
490 println!("ignoring git submodule at {:?}", path);
491 self.file_states.remove(path);
492 return;
493 }
494 (_, TreeValue::Tree(_id)) => {
495 panic!("unexpected tree entry in diff at {:?}", path);
496 }
497 (_, TreeValue::Conflict(_id)) => {
498 panic!(
499 "conflicts cannot be represented in the working copy: {:?}",
500 path
501 );
502 }
503 };
504
505 self.file_states.insert(path.clone(), file_state);
506 stats.updated_files += 1;
507 }
508 }
509 });
510 self.tree_id = tree_id;
511 self.save();
512 Ok(stats)
513 }
514}
515
516pub struct WorkingCopy {
517 store: Arc<StoreWrapper>,
518 working_copy_path: PathBuf,
519 state_path: PathBuf,
520 commit_id: RefCell<Option<CommitId>>,
521 tree_state: RefCell<Option<TreeState>>,
522 commit: RefCell<Option<Commit>>,
524}
525
526impl WorkingCopy {
527 pub fn init(
528 store: Arc<StoreWrapper>,
529 working_copy_path: PathBuf,
530 state_path: PathBuf,
531 ) -> WorkingCopy {
532 let proto = crate::protos::working_copy::Checkout::new();
535 let mut file = OpenOptions::new()
536 .create_new(true)
537 .write(true)
538 .open(state_path.join("checkout"))
539 .unwrap();
540 proto.write_to_writer(&mut file).unwrap();
541 WorkingCopy {
542 store,
543 working_copy_path,
544 state_path,
545 commit_id: RefCell::new(None),
546 tree_state: RefCell::new(None),
547 commit: RefCell::new(None),
548 }
549 }
550
551 pub fn load(
552 store: Arc<StoreWrapper>,
553 working_copy_path: PathBuf,
554 state_path: PathBuf,
555 ) -> WorkingCopy {
556 WorkingCopy {
557 store,
558 working_copy_path,
559 state_path,
560 commit_id: RefCell::new(None),
561 tree_state: RefCell::new(None),
562 commit: RefCell::new(None),
563 }
564 }
565
566 fn write_proto(&self, proto: crate::protos::working_copy::Checkout) {
567 let mut temp_file = NamedTempFile::new_in(&self.state_path).unwrap();
568 proto.write_to_writer(temp_file.as_file_mut()).unwrap();
569 temp_file.persist(self.state_path.join("checkout")).unwrap();
570 }
571
572 fn read_proto(&self) -> crate::protos::working_copy::Checkout {
573 let mut file = File::open(self.state_path.join("checkout")).unwrap();
574 protobuf::parse_from_reader(&mut file).unwrap()
575 }
576
577 pub fn current_commit_id(&self) -> CommitId {
582 if self.commit_id.borrow().is_none() {
583 let proto = self.read_proto();
584 let commit_id = CommitId(proto.commit_id);
585 self.commit_id.replace(Some(commit_id));
586 }
587
588 self.commit_id.borrow().as_ref().unwrap().clone()
589 }
590
591 pub fn current_commit(&self) -> Commit {
596 let commit_id = self.current_commit_id();
597 let stale = match self.commit.borrow().as_ref() {
598 None => true,
599 Some(value) => value.id() != &commit_id,
600 };
601 if stale {
602 self.commit
603 .replace(Some(self.store.get_commit(&commit_id).unwrap()));
604 }
605 self.commit.borrow().as_ref().unwrap().clone()
606 }
607
608 fn tree_state(&self) -> RefMut<Option<TreeState>> {
609 if self.tree_state.borrow().is_none() {
610 self.tree_state.replace(Some(TreeState::load(
611 self.store.clone(),
612 self.working_copy_path.clone(),
613 self.state_path.clone(),
614 )));
615 }
616 self.tree_state.borrow_mut()
617 }
618
619 pub fn current_tree_id(&self) -> TreeId {
620 self.tree_state()
621 .as_ref()
622 .unwrap()
623 .current_tree_id()
624 .clone()
625 }
626
627 pub fn file_states(&self) -> BTreeMap<FileRepoPath, FileState> {
628 self.tree_state().as_ref().unwrap().file_states().clone()
629 }
630
631 fn save(&self) {
632 let mut proto = crate::protos::working_copy::Checkout::new();
633 proto.commit_id = self.current_commit_id().0;
634 self.write_proto(proto);
635 }
636
637 pub fn check_out(&self, commit: Commit) -> Result<CheckoutStats, CheckoutError> {
638 assert!(commit.is_open());
639 let lock_path = self.state_path.join("working_copy.lock");
640 let _lock = FileLock::lock(lock_path);
641
642 let current_proto = self.read_proto();
654 if let Some(commit_id_at_read_time) = self.commit_id.borrow().as_ref() {
655 if current_proto.commit_id != commit_id_at_read_time.0 {
656 return Err(CheckoutError::ConcurrentCheckout);
657 }
658 }
659
660 let stats = self
661 .tree_state()
662 .as_mut()
663 .unwrap()
664 .check_out(commit.tree().id().clone())?;
665
666 self.commit_id.replace(Some(commit.id().clone()));
667 self.commit.replace(Some(commit));
668
669 self.save();
670 Ok(stats)
672 }
673
674 pub fn commit(&self, settings: &UserSettings, repo: &mut ReadonlyRepo) -> Commit {
675 let lock_path = self.state_path.join("working_copy.lock");
676 let _lock = FileLock::lock(lock_path);
677
678 let current_proto = self.read_proto();
683 self.commit_id
684 .replace(Some(CommitId(current_proto.commit_id)));
685 let current_commit = self.current_commit();
686
687 let new_tree_id = self.tree_state().as_mut().unwrap().write_tree().clone();
688 if &new_tree_id != current_commit.tree().id() {
689 let mut tx = repo.start_transaction("commit working copy");
690 let commit = CommitBuilder::for_rewrite_from(settings, repo.store(), ¤t_commit)
691 .set_tree(new_tree_id)
692 .write_to_transaction(&mut tx);
693 tx.set_checkout(commit.id().clone());
694 let operation = tx.commit();
695 repo.reload_at(&operation);
696
697 self.commit_id.replace(Some(commit.id().clone()));
698 self.commit.replace(Some(commit));
699 self.save();
700 }
701 self.commit.borrow().as_ref().unwrap().clone()
702 }
703}