[MERGE] TreeTransform rolls back on exception (#67699)

Kuno Meyer kuno.meyer at gmx.ch
Thu Aug 23 20:24:31 BST 2007


It's really great news to see a TreeTransform rollback. I played around 
a bit, and I've got the following points:

On 21.08.2007 17:42, Aaron Bentley wrote:

[skip]
> === modified file 'bzrlib/transform.py'
> --- bzrlib/transform.py	2007-08-16 05:37:08 +0000
> +++ bzrlib/transform.py	2007-08-21 15:26:59 +0000
> @@ -108,6 +108,9 @@
>              except OSError, e:
>                  if e.errno == errno.EEXIST:
>                      raise ExistingLimbo(self._limbodir)
> +            self._deletiondir = urlutils.local_path_from_url(
> +                control_files.controlfilename('pending-deletion'))
> +            os.mkdir(self._deletiondir)

Why not complain about an already existing pending-deletion dir? As far 
as I can see, the reasons for complaining are not that different than in 
the case of the already existing limbo dir: if pending-deletion exists, 
then a tree transformation has unexpectedly terminated and name 
conflicts during further operation are very likely to occur.

>          except: 
>              self._tree.unlock()
>              raise
> @@ -170,6 +173,7 @@
>              except OSError:
>                  # We don't especially care *why* the dir is immortal.
>                  raise ImmortalLimbo(self._limbodir)
> +            os.rmdir(self._deletiondir)

Why not use osutils.rmtree()? It would deal with write-protected files 
which can be moved, but not deleted (on Windows).

>          finally:
>              self._tree.unlock()
>              self._tree = None
> @@ -789,7 +793,7 @@
>              return True
>          return False
>              
> -    def apply(self, no_conflicts=False):
> +    def apply(self, no_conflicts=False, _mover=None):
>          """Apply all changes to the inventory and filesystem.
>          
>          If filesystem or inventory conflicts are present, MalformedTransform
> @@ -799,6 +803,7 @@
>  
>          :param no_conflicts: if True, the caller guarantees there are no
>              conflicts, so no check is made.
> +        :param _mover: Supply an alternate FileMover, for testing
>          """
>          if not no_conflicts:
>              conflicts = self.find_conflicts()
> @@ -808,10 +813,21 @@
>          inventory_delta = []
>          child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
>          try:
> -            child_pb.update('Apply phase', 0, 2)
> -            self._apply_removals(inv, inventory_delta)
> -            child_pb.update('Apply phase', 1, 2)
> -            modified_paths = self._apply_insertions(inv, inventory_delta)
> +            if _mover is None:
> +                mover = _FileMover()
> +            else:
> +                mover = _mover
> +            try:
> +                child_pb.update('Apply phase', 0, 2)
> +                self._apply_removals(inv, inventory_delta, mover)
> +                child_pb.update('Apply phase', 1, 2)
> +                modified_paths = self._apply_insertions(inv, inventory_delta,
> +                                                        mover)
> +            except:
> +                mover.rollback()
> +                raise
> +            else:
> +                mover.apply_deletions()

It would be a big gain in user-friendliness if bazaar would tell the 
user which file causes the conflict, for example by a warning message 
before re-raising exceptions at

- transform.py:898 (in _apply_removals())
- transform.py:939 (in _apply_insertions())

Currently, all what the user receives is a "ERROR: [Errno 13] Permission 
denied".

>          finally:
>              child_pb.finished()
>          self._tree.apply_inventory_delta(inventory_delta)
> @@ -852,7 +868,7 @@
>          self._limbo_files[trans_id] = limbo_name
>          return limbo_name
>  
> -    def _apply_removals(self, inv, inventory_delta):
> +    def _apply_removals(self, inv, inventory_delta, mover):
>          """Perform tree operations that remove directory/inventory names.
>          
>          That is, delete files that are to be deleted, and put any files that
> @@ -868,11 +884,12 @@
>                  child_pb.update('removing file', num, len(tree_paths))
>                  full_path = self._tree.abspath(path)
>                  if trans_id in self._removed_contents:
> -                    delete_any(full_path)
> +                    mover.pre_delete(full_path, os.path.join(self._deletiondir,
> +                                     trans_id))
>                  elif trans_id in self._new_name or trans_id in \
>                      self._new_parent:
>                      try:
> -                        os.rename(full_path, self._limbo_name(trans_id))
> +                        mover.rename(full_path, self._limbo_name(trans_id))
>                      except OSError, e:
>                          if e.errno != errno.ENOENT:
>                              raise
> @@ -888,7 +905,7 @@
>          finally:
>              child_pb.finished()
>  
> -    def _apply_insertions(self, inv, inventory_delta):
> +    def _apply_insertions(self, inv, inventory_delta, mover):
>          """Perform tree operations that insert directory/inventory names.
>          
>          That is, create any files that need to be created, and restore from
> @@ -911,7 +928,7 @@
>                      full_path = self._tree.abspath(path)
>                      if trans_id in self._needs_rename:
>                          try:
> -                            os.rename(self._limbo_name(trans_id), full_path)
> +                            mover.rename(self._limbo_name(trans_id), full_path)
>                          except OSError, e:
>                              # We may be renaming a dangling inventory id
>                              if e.errno != errno.ENOENT:
> @@ -1763,3 +1780,36 @@
>                                     file_id=modified_id, 
>                                     conflict_path=conflicting_path,
>                                     conflict_file_id=conflicting_id)
> +
> +
> +class _FileMover(object):
> +    """Moves and deletes files for TreeTransform, tracking operations"""
> +
> +    def __init__(self):
> +        self.past_renames = []
> +        self.pending_deletions = []
> +
> +    def rename(self, from_, to):
> +        """Rename a file from one path to another.  Functions like os.rename"""
> +        os.rename(from_, to)
> +        self.past_renames.append((from_, to))
> +
> +    def pre_delete(self, from_, to):
> +        """Rename a file out of the way and mark it for deletion.
> +
> +        Unlike os.unlink, this works equally well for files and directories.
> +        :param from_: The current file path
> +        :param to: A temporary path for the file
> +        """
> +        self.rename(from_, to)
> +        self.pending_deletions.append(to)
> +
> +    def rollback(self):
> +        """Reverse all renames that have been performed"""
> +        for from_, to in reversed(self.past_renames):
> +            os.rename(to, from_)
> +
> +    def apply_deletions(self):
> +        """Apply all marked deletions"""
> +        for path in self.pending_deletions:
> +            delete_any(path)



More information about the bazaar mailing list