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<F>(&mut self, visitor: &mut F) -> Result<()>
     where
-        F: FnMut(&Vec<KeyRef>, &T) -> Result<()>,
+        F: FnMut(&Vec<KeyRef>, &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<F>(&mut self, visitor: &mut F) -> Result<()>
+    where
+        F: FnMut(&Vec<KeyRef>, &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<Option<(Key, &'a T)>> {
         self.store.cache()?;
@@ -218,7 +227,7 @@
     /// Visit all removed files with a visitor.
     pub fn visit_removed<F>(&mut self, visitor: &mut F) -> Result<()>
     where
-        F: FnMut(&Vec<KeyRef>, &T) -> Result<()>,
+        F: FnMut(&Vec<KeyRef>, &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<Option<PyObject>> {
+    def write(&self, name: &str, fsnow: i32, nonnorm: PyObject) -> PyResult<Option<PyObject>> {
         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<KeyRef>, 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::<exc::IOError, _>(py, e.description()))?;
         dirstate
             .write_full(self.repodir(py).join(name))
             .map_err(|e| PyErr::new::<exc::IOError, _>(py, e.description()))?;
         Ok(None)
     }
 
-    def writedelta(&self) -> PyResult<Option<PyObject>> {
+    def writedelta(&self, fsnow: i32, nonnorm: PyObject) -> PyResult<Option<PyObject>> {
         let mut dirstate = self.dirstate(py).borrow_mut();
-        dirstate.write_delta().map_err(|e| PyErr::new::<exc::IOError, _>(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<KeyRef>, 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::<exc::IOError, _>(py, e.description()))?;
+        dirstate
+            .write_delta()
+            .map_err(|e| PyErr::new::<exc::IOError, _>(py, e.description()))?;
         Ok(None)
     }
 
@@ -183,7 +211,7 @@
 
     def visittrackedfiles(&self, target: PyObject) -> PyResult<PyObject> {
         let mut dirstate = self.dirstate(py).borrow_mut();
-        let mut visitor = |filepath: &Vec<KeyRef>, _state: &FileState| {
+        let mut visitor = |filepath: &Vec<KeyRef>, _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<PyObject> {
         let mut dirstate = self.dirstate(py).borrow_mut();
-        let mut visitor = |filepath: &Vec<KeyRef>, _state: &FileState| {
+        let mut visitor = |filepath: &Vec<KeyRef>, _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<KeyRef>, &T) -> Result<()>,
+        F: FnMut(&Vec<KeyRef>, &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<KeyRef<'a>>,
+        visitor: &mut F,
+    ) -> Result<()>
+    where
+        F: FnMut(&Vec<KeyRef>, &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<F>(&mut self, store: &StoreView, visitor: &mut F) -> Result<()>
     where
-        F: FnMut(&Vec<KeyRef>, &T) -> Result<()>,
+        F: FnMut(&Vec<KeyRef>, &mut T) -> Result<()>,
     {
         let mut path = Vec::new();
         self.root.visit(store, &mut path, visitor)
     }
 
+    pub fn visit_changed<F>(&mut self, store: &StoreView, visitor: &mut F) -> Result<()>
+    where
+        F: FnMut(&Vec<KeyRef>, &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<Option<(Key, &'a T)>> {
         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<KeyRef>, _fs: &FileState| {
+            let mut v = |path: &Vec<KeyRef>, _fs: &mut FileState| {
                 files.push(path.concat());
                 Ok(())
             };
@@ -721,4 +760,38 @@
                 .collect::<Vec<Vec<u8>>>()
         );
     }
+
+    #[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(b'm', 0o644, 200, 2000),
+        ).expect("can add");
+
+        let mut files = Vec::new();
+        {
+            let mut v = |path: &Vec<KeyRef>, _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