jujube_lib/
working_copy.rs

1// Copyright 2020 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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    /* TODO: What else do we need here? Git stores a lot of fields.
60     * TODO: Could possibly handle case-insensitive file systems keeping an
61     *       Option<PathBuf> with the actual path here. */
62}
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    // The current checkout was deleted, maybe by an overly aggressive GC that happened while
137    // the current process was running.
138    #[error("Current checkout not found")]
139    SourceNotFound,
140    // Another process checked out a commit while the current process was running (after the
141    // working copy was read by the current process).
142    #[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        // Canonicalize the working copy path because "repo/." makes libgit2 think that
174        // everything should be ignored
175        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        // update read time while we still have the file open for writes, so we know
229        // there is no unknown data in it
230        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    // Look for changes to the working copy. If there are any changes, create
275    // a new tree from it and return it, and also update the dirstate on disk.
276    // TODO: respect ignores
277    pub fn write_tree(&mut self) -> &TreeId {
278        // We create a temporary git repo with the working copy shared with ours only
279        // so we can use libgit2's .gitignore check.
280        // TODO: Do this more cleanly, perhaps by reading .gitignore files ourselves.
281        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                            // untracked
312                            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        // Read the file state while we still have the write lock. That way there is no
374        // race with other processes modifying it. We know that the file exists,
375        // and we know that the stat information is accurate. (The mtime is set
376        // at write time and won't change when we close the file.)
377        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            // TODO: Check that the file has not changed before overwriting/removing it.
428            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                            // Optimization for when only the executable bit changed
475                            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    // cached commit
523    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        // Leave the commit_id empty so a subsequent call to check out the root revision
533        // will have an effect.
534        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    /// The id of the commit that's currently checked out in the working copy.
578    /// Note that the View is the source of truth for which commit *should*
579    /// be checked out. That should be kept up to date within a Transaction.
580    /// The WorkingCopy is only updated at the end.
581    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    /// The commit that's currently checked out in the working copy. Note that
592    /// the View is the source of truth for which commit *should* be checked
593    /// out. That should be kept up to date within a Transaction. The
594    /// WorkingCopy is only updated at the end.
595    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        // TODO: Write a "pending_checkout" file with the old and new TreeIds so we can
643        // continue       an interrupted checkout if we find such a file. Write
644        // access to that file can       also serve as lock so only one process
645        // at once can do a checkout.
646
647        // Check if the current checkout has changed on disk after we read it. It's safe
648        // to check out another commit regardless, but it's probably not what
649        // the caller wanted, so we let them know.
650        //
651        // We could safely add a version of this function without the check if we see a
652        // need for it.
653        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        // TODO: Clear the "pending_checkout" file here.
671        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        // Check if the current checkout has changed on disk after we read it. It's fine
679        // if it has, but we'll want our new commit to be a successor of the one
680        // just created in that case, so we need to reset our state to have the new
681        // commit id.
682        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(), &current_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}