diff --git a/rust/treedirstate/src/dirstate.rs b/rust/treedirstate/src/dirstate.rs --- a/rust/treedirstate/src/dirstate.rs +++ b/rust/treedirstate/src/dirstate.rs @@ -189,12 +189,21 @@ /// Visit all tracked files with a visitor. pub fn visit_tracked(&mut self, visitor: &mut F) -> Result<()> where - F: FnMut(&Vec, &T) -> Result<()>, + F: FnMut(&Vec, &mut T) -> Result<()>, { self.store.cache()?; self.tracked.visit(self.store.store_view(), visitor) } + /// Visit all tracked files in changed nodes with a visitor. + pub fn visit_changed_tracked(&mut self, visitor: &mut F) -> Result<()> + where + F: FnMut(&Vec, &mut T) -> Result<()>, + { + self.store.cache()?; + self.tracked.visit_changed(self.store.store_view(), visitor) + } + /// Get the name and state of the first file in the tracked tree. pub fn get_first_tracked<'a>(&'a mut self) -> Result> { self.store.cache()?; @@ -218,7 +227,7 @@ /// Visit all removed files with a visitor. pub fn visit_removed(&mut self, visitor: &mut F) -> Result<()> where - F: FnMut(&Vec, &T) -> Result<()>, + F: FnMut(&Vec, &mut T) -> Result<()>, { self.removed.visit(self.store.store_view(), visitor) } diff --git a/rust/treedirstate/src/python.rs b/rust/treedirstate/src/python.rs --- a/rust/treedirstate/src/python.rs +++ b/rust/treedirstate/src/python.rs @@ -89,17 +89,45 @@ Ok(None) } - def write(&self, name: &str) -> PyResult> { + def write(&self, name: &str, fsnow: i32, nonnorm: PyObject) -> PyResult> { let mut dirstate = self.dirstate(py).borrow_mut(); + // Mark files with an mtime of `fsnow` as being out of date. See + // mercurial/pure/parsers.py:pack_dirstate in core Mercurial for why this is done. + let mut filter = |filepath: &Vec, state: &mut FileState| { + if state.state == b'n' && state.mtime == fsnow { + state.mtime = -1; + let filename = PyBytes::new(py, &filepath.concat()).into_object(); + nonnorm.call(py, (filename,), None).map_err(|e| callback_error(py, e))?; + } + Ok(()) + }; + dirstate + .visit_tracked(&mut filter) + .map_err(|e| PyErr::new::(py, e.description()))?; dirstate .write_full(self.repodir(py).join(name)) .map_err(|e| PyErr::new::(py, e.description()))?; Ok(None) } - def writedelta(&self) -> PyResult> { + def writedelta(&self, fsnow: i32, nonnorm: PyObject) -> PyResult> { let mut dirstate = self.dirstate(py).borrow_mut(); - dirstate.write_delta().map_err(|e| PyErr::new::(py, e.description()))?; + // Mark files with an mtime of `fsnow` as being out of date. See + // mercurial/pure/parsers.py:pack_dirstate in core Mercurial for why this is done. + let mut filter = |filepath: &Vec, state: &mut FileState| { + if state.state == b'n' && state.mtime == fsnow { + state.mtime = -1; + let filename = PyBytes::new(py, &filepath.concat()).into_object(); + nonnorm.call(py, (filename,), None).map_err(|e| callback_error(py, e))?; + } + Ok(()) + }; + dirstate + .visit_changed_tracked(&mut filter) + .map_err(|e| PyErr::new::(py, e.description()))?; + dirstate + .write_delta() + .map_err(|e| PyErr::new::(py, e.description()))?; Ok(None) } @@ -183,7 +211,7 @@ def visittrackedfiles(&self, target: PyObject) -> PyResult { let mut dirstate = self.dirstate(py).borrow_mut(); - let mut visitor = |filepath: &Vec, _state: &FileState| { + let mut visitor = |filepath: &Vec, _state: &mut FileState| { let filename = PyBytes::new(py, &filepath.concat()).into_object(); target.call(py, (filename,), None).map_err(|e| callback_error(py, e))?; Ok(()) @@ -196,7 +224,7 @@ def visitremovedfiles(&self, target: PyObject) -> PyResult { let mut dirstate = self.dirstate(py).borrow_mut(); - let mut visitor = |filepath: &Vec, _state: &FileState| { + let mut visitor = |filepath: &Vec, _state: &mut FileState| { let filename = PyBytes::new(py, &filepath.concat()).into_object(); target.call(py, (filename,), None).map_err(|e| callback_error(py, e))?; Ok(()) diff --git a/rust/treedirstate/src/tree.rs b/rust/treedirstate/src/tree.rs --- a/rust/treedirstate/src/tree.rs +++ b/rust/treedirstate/src/tree.rs @@ -232,7 +232,7 @@ visitor: &mut F, ) -> Result<()> where - F: FnMut(&Vec, &T) -> Result<()>, + F: FnMut(&Vec, &mut T) -> Result<()>, { for (name, entry) in self.load_entries(store)?.iter_mut() { path.push(name); @@ -240,7 +240,7 @@ &mut NodeEntry::Directory(ref mut node) => { node.visit(store, path, visitor)?; } - &mut NodeEntry::File(ref file) => { + &mut NodeEntry::File(ref mut file) => { visitor(path, file)?; } } @@ -249,6 +249,37 @@ Ok(()) } + // Visit all of the files in changed nodes under this node, by calling the visitor function + // on each one. + fn visit_changed<'a, F>( + &'a mut self, + store: &StoreView, + path: &mut Vec>, + visitor: &mut F, + ) -> Result<()> + where + F: FnMut(&Vec, &mut T) -> Result<()>, + { + if self.id.is_none() { + // This node has been modified. The entries list must already have been populated when + // the node was modified, so no need to load it here. + if let Some(ref mut entries) = self.entries { + for (name, entry) in entries.iter_mut() { + path.push(name); + match entry { + &mut NodeEntry::Directory(ref mut node) => { + node.visit_changed(store, path, visitor)?; + } + &mut NodeEntry::File(ref mut file) => { + visitor(path, file)?; + } + } + path.pop(); + } + } + } + Ok(()) + } /// Get the first file in the subtree under this node. If the subtree is not empty, returns a /// pair containing the path to the file as a reversed vector of key references for each path /// element, and a reference to the file. @@ -490,12 +521,20 @@ pub fn visit(&mut self, store: &StoreView, visitor: &mut F) -> Result<()> where - F: FnMut(&Vec, &T) -> Result<()>, + F: FnMut(&Vec, &mut T) -> Result<()>, { let mut path = Vec::new(); self.root.visit(store, &mut path, visitor) } + pub fn visit_changed(&mut self, store: &StoreView, visitor: &mut F) -> Result<()> + where + F: FnMut(&Vec, &mut T) -> Result<()>, + { + let mut path = Vec::new(); + self.root.visit_changed(store, &mut path, visitor) + } + pub fn get_first<'a>(&'a mut self, store: &StoreView) -> Result> { Ok(self.root.get_first(store)?.map(|(mut path, file)| { path.reverse(); @@ -707,7 +746,7 @@ populate(&mut t, &ms); let mut files = Vec::new(); { - let mut v = |path: &Vec, _fs: &FileState| { + let mut v = |path: &Vec, _fs: &mut FileState| { files.push(path.concat()); Ok(()) }; @@ -721,4 +760,38 @@ .collect::>>() ); } + + #[test] + fn visit_changed() { + let ns = NullStore::new(); + let mut ms = MapStore::new(); + let mut t = Tree::new(); + populate(&mut t, &ms); + t.write_full(&mut ms, &ns).expect("can write full"); + + // Touch file5. This file, and any file in an ancestor directory (file4, file5 and file16) + // will be in directories marked as changed. + t.add( + &ms, + b"dirB/subdira/subsubdirx/file5", + &FileState::new('m', 0o644, 200, 2000), + ).expect("can add"); + + let mut files = Vec::new(); + { + let mut v = |path: &Vec, _fs: &mut FileState| { + files.push(path.concat()); + Ok(()) + }; + t.visit_changed(&mut ms, &mut v).expect("can visit_changed"); + } + assert_eq!( + files, + vec![ + b"dirB/subdira/file4".to_vec(), + b"dirB/subdira/subsubdirx/file5".to_vec(), + b"file16".to_vec(), + ] + ); + } } diff --git a/treedirstate/__init__.py b/treedirstate/__init__.py --- a/treedirstate/__init__.py +++ b/treedirstate/__init__.py @@ -232,8 +232,16 @@ return self._rmap.dropfile(f) def clearambiguoustimes(self, files, now): - ### TODO - pass + """Mark files with an mtime of `now` as being out of date. + + See mercurial/pure/parsers.py:pack_dirstate in core Mercurial for why + this is done. + """ + for f in files: + e = self.gettracked(f) + if e is not None and e[0] == 'n' and e[3] == now: + self._rmap.addfile(f, e[0], e[0], e[1], e[2], -1) + self.nonnormalset.add(f) def parents(self): """ @@ -338,9 +346,9 @@ """Write the dirstate to the filehandle st.""" if self._treeid is None: self._treeid = '000' - self._rmap.write('dirstate.tree.000') + self._rmap.write('dirstate.tree.000', now, self._nonnormalset.add) else: - self._rmap.writedelta() + self._rmap.writedelta(now, self._nonnormalset.add) st.write(self._genrootdata()) st.close() self._dirtyparents = False