1
# Copyright (C) 2006 Canonical Ltd
 
 
3
# This program is free software; you can redistribute it and/or modify
 
 
4
# it under the terms of the GNU General Public License as published by
 
 
5
# the Free Software Foundation; either version 2 of the License, or
 
 
6
# (at your option) any later version.
 
 
8
# This program is distributed in the hope that it will be useful,
 
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
 
11
# GNU General Public License for more details.
 
 
13
# You should have received a copy of the GNU General Public License
 
 
14
# along with this program; if not, write to the Free Software
 
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 
19
from stat import S_ISREG
 
 
21
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
 
 
22
                           ReusingTransform, NotVersionedError, CantMoveRoot,
 
 
23
                           ExistingLimbo, ImmortalLimbo)
 
 
24
from bzrlib.inventory import InventoryEntry
 
 
25
from bzrlib.osutils import (file_kind, supports_executable, pathjoin, lexists,
 
 
27
from bzrlib.progress import DummyProgress, ProgressPhase
 
 
28
from bzrlib.trace import mutter, warning
 
 
32
ROOT_PARENT = "root-parent"
 
 
35
def unique_add(map, key, value):
 
 
37
        raise DuplicateKey(key=key)
 
 
41
class _TransformResults(object):
 
 
42
    def __init__(self, modified_paths):
 
 
44
        self.modified_paths = modified_paths
 
 
47
class TreeTransform(object):
 
 
48
    """Represent a tree transformation.
 
 
50
    This object is designed to support incremental generation of the transform,
 
 
53
    It is easy to produce malformed transforms, but they are generally
 
 
54
    harmless.  Attempting to apply a malformed transform will cause an
 
 
55
    exception to be raised before any modifications are made to the tree.  
 
 
57
    Many kinds of malformed transforms can be corrected with the 
 
 
58
    resolve_conflicts function.  The remaining ones indicate programming error,
 
 
59
    such as trying to create a file with no path.
 
 
61
    Two sets of file creation methods are supplied.  Convenience methods are:
 
 
66
    These are composed of the low-level methods:
 
 
68
     * create_file or create_directory or create_symlink
 
 
72
    def __init__(self, tree, pb=DummyProgress()):
 
 
73
        """Note: a write lock is taken on the tree.
 
 
75
        Use TreeTransform.finalize() to release the lock
 
 
79
        self._tree.lock_write()
 
 
81
            control_files = self._tree._control_files
 
 
82
            self._limbodir = control_files.controlfilename('limbo')
 
 
84
                os.mkdir(self._limbodir)
 
 
86
                if e.errno == errno.EEXIST:
 
 
87
                    raise ExistingLimbo(self._limbodir)
 
 
95
        self._new_contents = {}
 
 
96
        self._removed_contents = set()
 
 
97
        self._new_executability = {}
 
 
99
        self._non_present_ids = {}
 
 
101
        self._removed_id = set()
 
 
102
        self._tree_path_ids = {}
 
 
103
        self._tree_id_paths = {}
 
 
105
        # Cache of realpath results, to speed up canonical_path
 
 
107
        # Cache of relpath results, to speed up canonical_path
 
 
108
        self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
 
 
112
    def __get_root(self):
 
 
113
        return self._new_root
 
 
115
    root = property(__get_root)
 
 
118
        """Release the working tree lock, if held, clean up limbo dir."""
 
 
119
        if self._tree is None:
 
 
122
            for trans_id, kind in self._new_contents.iteritems():
 
 
123
                path = self._limbo_name(trans_id)
 
 
124
                if kind == "directory":
 
 
129
                os.rmdir(self._limbodir)
 
 
131
                # We don't especially care *why* the dir is immortal.
 
 
132
                raise ImmortalLimbo(self._limbodir)
 
 
137
    def _assign_id(self):
 
 
138
        """Produce a new tranform id"""
 
 
139
        new_id = "new-%s" % self._id_number
 
 
143
    def create_path(self, name, parent):
 
 
144
        """Assign a transaction id to a new path"""
 
 
145
        trans_id = self._assign_id()
 
 
146
        unique_add(self._new_name, trans_id, name)
 
 
147
        unique_add(self._new_parent, trans_id, parent)
 
 
150
    def adjust_path(self, name, parent, trans_id):
 
 
151
        """Change the path that is assigned to a transaction id."""
 
 
152
        if trans_id == self._new_root:
 
 
154
        self._new_name[trans_id] = name
 
 
155
        self._new_parent[trans_id] = parent
 
 
157
    def adjust_root_path(self, name, parent):
 
 
158
        """Emulate moving the root by moving all children, instead.
 
 
160
        We do this by undoing the association of root's transaction id with the
 
 
161
        current tree.  This allows us to create a new directory with that
 
 
162
        transaction id.  We unversion the root directory and version the 
 
 
163
        physically new directory, and hope someone versions the tree root
 
 
166
        old_root = self._new_root
 
 
167
        old_root_file_id = self.final_file_id(old_root)
 
 
168
        # force moving all children of root
 
 
169
        for child_id in self.iter_tree_children(old_root):
 
 
170
            if child_id != parent:
 
 
171
                self.adjust_path(self.final_name(child_id), 
 
 
172
                                 self.final_parent(child_id), child_id)
 
 
173
            file_id = self.final_file_id(child_id)
 
 
174
            if file_id is not None:
 
 
175
                self.unversion_file(child_id)
 
 
176
            self.version_file(file_id, child_id)
 
 
178
        # the physical root needs a new transaction id
 
 
179
        self._tree_path_ids.pop("")
 
 
180
        self._tree_id_paths.pop(old_root)
 
 
181
        self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
 
 
182
        if parent == old_root:
 
 
183
            parent = self._new_root
 
 
184
        self.adjust_path(name, parent, old_root)
 
 
185
        self.create_directory(old_root)
 
 
186
        self.version_file(old_root_file_id, old_root)
 
 
187
        self.unversion_file(self._new_root)
 
 
189
    def trans_id_tree_file_id(self, inventory_id):
 
 
190
        """Determine the transaction id of a working tree file.
 
 
192
        This reflects only files that already exist, not ones that will be
 
 
193
        added by transactions.
 
 
195
        path = self._tree.inventory.id2path(inventory_id)
 
 
196
        return self.trans_id_tree_path(path)
 
 
198
    def trans_id_file_id(self, file_id):
 
 
199
        """Determine or set the transaction id associated with a file ID.
 
 
200
        A new id is only created for file_ids that were never present.  If
 
 
201
        a transaction has been unversioned, it is deliberately still returned.
 
 
202
        (this will likely lead to an unversioned parent conflict.)
 
 
204
        if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
 
 
205
            return self._r_new_id[file_id]
 
 
206
        elif file_id in self._tree.inventory:
 
 
207
            return self.trans_id_tree_file_id(file_id)
 
 
208
        elif file_id in self._non_present_ids:
 
 
209
            return self._non_present_ids[file_id]
 
 
211
            trans_id = self._assign_id()
 
 
212
            self._non_present_ids[file_id] = trans_id
 
 
215
    def canonical_path(self, path):
 
 
216
        """Get the canonical tree-relative path"""
 
 
217
        # don't follow final symlinks
 
 
218
        abs = self._tree.abspath(path)
 
 
219
        if abs in self._relpaths:
 
 
220
            return self._relpaths[abs]
 
 
221
        dirname, basename = os.path.split(abs)
 
 
222
        if dirname not in self._realpaths:
 
 
223
            self._realpaths[dirname] = os.path.realpath(dirname)
 
 
224
        dirname = self._realpaths[dirname]
 
 
225
        abs = pathjoin(dirname, basename)
 
 
226
        if dirname in self._relpaths:
 
 
227
            relpath = pathjoin(self._relpaths[dirname], basename)
 
 
228
            relpath = relpath.rstrip('/\\')
 
 
230
            relpath = self._tree.relpath(abs)
 
 
231
        self._relpaths[abs] = relpath
 
 
234
    def trans_id_tree_path(self, path):
 
 
235
        """Determine (and maybe set) the transaction ID for a tree path."""
 
 
236
        path = self.canonical_path(path)
 
 
237
        if path not in self._tree_path_ids:
 
 
238
            self._tree_path_ids[path] = self._assign_id()
 
 
239
            self._tree_id_paths[self._tree_path_ids[path]] = path
 
 
240
        return self._tree_path_ids[path]
 
 
242
    def get_tree_parent(self, trans_id):
 
 
243
        """Determine id of the parent in the tree."""
 
 
244
        path = self._tree_id_paths[trans_id]
 
 
247
        return self.trans_id_tree_path(os.path.dirname(path))
 
 
249
    def create_file(self, contents, trans_id, mode_id=None):
 
 
250
        """Schedule creation of a new file.
 
 
254
        Contents is an iterator of strings, all of which will be written
 
 
255
        to the target destination.
 
 
257
        New file takes the permissions of any existing file with that id,
 
 
258
        unless mode_id is specified.
 
 
260
        f = file(self._limbo_name(trans_id), 'wb')
 
 
261
        unique_add(self._new_contents, trans_id, 'file')
 
 
262
        for segment in contents:
 
 
265
        self._set_mode(trans_id, mode_id, S_ISREG)
 
 
267
    def _set_mode(self, trans_id, mode_id, typefunc):
 
 
268
        """Set the mode of new file contents.
 
 
269
        The mode_id is the existing file to get the mode from (often the same
 
 
270
        as trans_id).  The operation is only performed if there's a mode match
 
 
271
        according to typefunc.
 
 
276
            old_path = self._tree_id_paths[mode_id]
 
 
280
            mode = os.stat(old_path).st_mode
 
 
282
            if e.errno == errno.ENOENT:
 
 
287
            os.chmod(self._limbo_name(trans_id), mode)
 
 
289
    def create_directory(self, trans_id):
 
 
290
        """Schedule creation of a new directory.
 
 
292
        See also new_directory.
 
 
294
        os.mkdir(self._limbo_name(trans_id))
 
 
295
        unique_add(self._new_contents, trans_id, 'directory')
 
 
297
    def create_symlink(self, target, trans_id):
 
 
298
        """Schedule creation of a new symbolic link.
 
 
300
        target is a bytestring.
 
 
301
        See also new_symlink.
 
 
303
        os.symlink(target, self._limbo_name(trans_id))
 
 
304
        unique_add(self._new_contents, trans_id, 'symlink')
 
 
306
    def cancel_creation(self, trans_id):
 
 
307
        """Cancel the creation of new file contents."""
 
 
308
        del self._new_contents[trans_id]
 
 
309
        delete_any(self._limbo_name(trans_id))
 
 
311
    def delete_contents(self, trans_id):
 
 
312
        """Schedule the contents of a path entry for deletion"""
 
 
313
        self.tree_kind(trans_id)
 
 
314
        self._removed_contents.add(trans_id)
 
 
316
    def cancel_deletion(self, trans_id):
 
 
317
        """Cancel a scheduled deletion"""
 
 
318
        self._removed_contents.remove(trans_id)
 
 
320
    def unversion_file(self, trans_id):
 
 
321
        """Schedule a path entry to become unversioned"""
 
 
322
        self._removed_id.add(trans_id)
 
 
324
    def delete_versioned(self, trans_id):
 
 
325
        """Delete and unversion a versioned file"""
 
 
326
        self.delete_contents(trans_id)
 
 
327
        self.unversion_file(trans_id)
 
 
329
    def set_executability(self, executability, trans_id):
 
 
330
        """Schedule setting of the 'execute' bit
 
 
331
        To unschedule, set to None
 
 
333
        if executability is None:
 
 
334
            del self._new_executability[trans_id]
 
 
336
            unique_add(self._new_executability, trans_id, executability)
 
 
338
    def version_file(self, file_id, trans_id):
 
 
339
        """Schedule a file to become versioned."""
 
 
340
        assert file_id is not None
 
 
341
        unique_add(self._new_id, trans_id, file_id)
 
 
342
        unique_add(self._r_new_id, file_id, trans_id)
 
 
344
    def cancel_versioning(self, trans_id):
 
 
345
        """Undo a previous versioning of a file"""
 
 
346
        file_id = self._new_id[trans_id]
 
 
347
        del self._new_id[trans_id]
 
 
348
        del self._r_new_id[file_id]
 
 
351
        """Determine the paths of all new and changed files"""
 
 
353
        fp = FinalPaths(self)
 
 
354
        for id_set in (self._new_name, self._new_parent, self._new_contents,
 
 
355
                       self._new_id, self._new_executability):
 
 
356
            new_ids.update(id_set)
 
 
357
        new_paths = [(fp.get_path(t), t) for t in new_ids]
 
 
361
    def tree_kind(self, trans_id):
 
 
362
        """Determine the file kind in the working tree.
 
 
364
        Raises NoSuchFile if the file does not exist
 
 
366
        path = self._tree_id_paths.get(trans_id)
 
 
368
            raise NoSuchFile(None)
 
 
370
            return file_kind(self._tree.abspath(path))
 
 
372
            if e.errno != errno.ENOENT:
 
 
375
                raise NoSuchFile(path)
 
 
377
    def final_kind(self, trans_id):
 
 
378
        """Determine the final file kind, after any changes applied.
 
 
380
        Raises NoSuchFile if the file does not exist/has no contents.
 
 
381
        (It is conceivable that a path would be created without the
 
 
382
        corresponding contents insertion command)
 
 
384
        if trans_id in self._new_contents:
 
 
385
            return self._new_contents[trans_id]
 
 
386
        elif trans_id in self._removed_contents:
 
 
387
            raise NoSuchFile(None)
 
 
389
            return self.tree_kind(trans_id)
 
 
391
    def tree_file_id(self, trans_id):
 
 
392
        """Determine the file id associated with the trans_id in the tree"""
 
 
394
            path = self._tree_id_paths[trans_id]
 
 
396
            # the file is a new, unversioned file, or invalid trans_id
 
 
398
        # the file is old; the old id is still valid
 
 
399
        if self._new_root == trans_id:
 
 
400
            return self._tree.inventory.root.file_id
 
 
401
        return self._tree.inventory.path2id(path)
 
 
403
    def final_file_id(self, trans_id):
 
 
404
        """Determine the file id after any changes are applied, or None.
 
 
406
        None indicates that the file will not be versioned after changes are
 
 
410
            # there is a new id for this file
 
 
411
            assert self._new_id[trans_id] is not None
 
 
412
            return self._new_id[trans_id]
 
 
414
            if trans_id in self._removed_id:
 
 
416
        return self.tree_file_id(trans_id)
 
 
418
    def inactive_file_id(self, trans_id):
 
 
419
        """Return the inactive file_id associated with a transaction id.
 
 
420
        That is, the one in the tree or in non_present_ids.
 
 
421
        The file_id may actually be active, too.
 
 
423
        file_id = self.tree_file_id(trans_id)
 
 
424
        if file_id is not None:
 
 
426
        for key, value in self._non_present_ids.iteritems():
 
 
427
            if value == trans_id:
 
 
430
    def final_parent(self, trans_id):
 
 
431
        """Determine the parent file_id, after any changes are applied.
 
 
433
        ROOT_PARENT is returned for the tree root.
 
 
436
            return self._new_parent[trans_id]
 
 
438
            return self.get_tree_parent(trans_id)
 
 
440
    def final_name(self, trans_id):
 
 
441
        """Determine the final filename, after all changes are applied."""
 
 
443
            return self._new_name[trans_id]
 
 
445
            return os.path.basename(self._tree_id_paths[trans_id])
 
 
448
        """Return a map of parent: children for known parents.
 
 
450
        Only new paths and parents of tree files with assigned ids are used.
 
 
453
        items = list(self._new_parent.iteritems())
 
 
454
        items.extend((t, self.final_parent(t)) for t in 
 
 
455
                      self._tree_id_paths.keys())
 
 
456
        for trans_id, parent_id in items:
 
 
457
            if parent_id not in by_parent:
 
 
458
                by_parent[parent_id] = set()
 
 
459
            by_parent[parent_id].add(trans_id)
 
 
462
    def path_changed(self, trans_id):
 
 
463
        """Return True if a trans_id's path has changed."""
 
 
464
        return trans_id in self._new_name or trans_id in self._new_parent
 
 
466
    def find_conflicts(self):
 
 
467
        """Find any violations of inventory or filesystem invariants"""
 
 
468
        if self.__done is True:
 
 
469
            raise ReusingTransform()
 
 
471
        # ensure all children of all existent parents are known
 
 
472
        # all children of non-existent parents are known, by definition.
 
 
473
        self._add_tree_children()
 
 
474
        by_parent = self.by_parent()
 
 
475
        conflicts.extend(self._unversioned_parents(by_parent))
 
 
476
        conflicts.extend(self._parent_loops())
 
 
477
        conflicts.extend(self._duplicate_entries(by_parent))
 
 
478
        conflicts.extend(self._duplicate_ids())
 
 
479
        conflicts.extend(self._parent_type_conflicts(by_parent))
 
 
480
        conflicts.extend(self._improper_versioning())
 
 
481
        conflicts.extend(self._executability_conflicts())
 
 
482
        conflicts.extend(self._overwrite_conflicts())
 
 
485
    def _add_tree_children(self):
 
 
486
        """Add all the children of all active parents to the known paths.
 
 
488
        Active parents are those which gain children, and those which are
 
 
489
        removed.  This is a necessary first step in detecting conflicts.
 
 
491
        parents = self.by_parent().keys()
 
 
492
        parents.extend([t for t in self._removed_contents if 
 
 
493
                        self.tree_kind(t) == 'directory'])
 
 
494
        for trans_id in self._removed_id:
 
 
495
            file_id = self.tree_file_id(trans_id)
 
 
496
            if self._tree.inventory[file_id].kind in ('directory', 
 
 
498
                parents.append(trans_id)
 
 
500
        for parent_id in parents:
 
 
501
            # ensure that all children are registered with the transaction
 
 
502
            list(self.iter_tree_children(parent_id))
 
 
504
    def iter_tree_children(self, parent_id):
 
 
505
        """Iterate through the entry's tree children, if any"""
 
 
507
            path = self._tree_id_paths[parent_id]
 
 
511
            children = os.listdir(self._tree.abspath(path))
 
 
513
            if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
 
 
517
        for child in children:
 
 
518
            childpath = joinpath(path, child)
 
 
519
            if self._tree.is_control_filename(childpath):
 
 
521
            yield self.trans_id_tree_path(childpath)
 
 
523
    def has_named_child(self, by_parent, parent_id, name):
 
 
525
            children = by_parent[parent_id]
 
 
528
        for child in children:
 
 
529
            if self.final_name(child) == name:
 
 
532
            path = self._tree_id_paths[parent_id]
 
 
535
        childpath = joinpath(path, name)
 
 
536
        child_id = self._tree_path_ids.get(childpath)
 
 
538
            return lexists(self._tree.abspath(childpath))
 
 
540
            if tt.final_parent(child_id) != parent_id:
 
 
542
            if child_id in tt._removed_contents:
 
 
543
                # XXX What about dangling file-ids?
 
 
548
    def _parent_loops(self):
 
 
549
        """No entry should be its own ancestor"""
 
 
551
        for trans_id in self._new_parent:
 
 
554
            while parent_id is not ROOT_PARENT:
 
 
556
                parent_id = self.final_parent(parent_id)
 
 
557
                if parent_id == trans_id:
 
 
558
                    conflicts.append(('parent loop', trans_id))
 
 
559
                if parent_id in seen:
 
 
563
    def _unversioned_parents(self, by_parent):
 
 
564
        """If parent directories are versioned, children must be versioned."""
 
 
566
        for parent_id, children in by_parent.iteritems():
 
 
567
            if parent_id is ROOT_PARENT:
 
 
569
            if self.final_file_id(parent_id) is not None:
 
 
571
            for child_id in children:
 
 
572
                if self.final_file_id(child_id) is not None:
 
 
573
                    conflicts.append(('unversioned parent', parent_id))
 
 
577
    def _improper_versioning(self):
 
 
578
        """Cannot version a file with no contents, or a bad type.
 
 
580
        However, existing entries with no contents are okay.
 
 
583
        for trans_id in self._new_id.iterkeys():
 
 
585
                kind = self.final_kind(trans_id)
 
 
587
                conflicts.append(('versioning no contents', trans_id))
 
 
589
            if not InventoryEntry.versionable_kind(kind):
 
 
590
                conflicts.append(('versioning bad kind', trans_id, kind))
 
 
593
    def _executability_conflicts(self):
 
 
594
        """Check for bad executability changes.
 
 
596
        Only versioned files may have their executability set, because
 
 
597
        1. only versioned entries can have executability under windows
 
 
598
        2. only files can be executable.  (The execute bit on a directory
 
 
599
           does not indicate searchability)
 
 
602
        for trans_id in self._new_executability:
 
 
603
            if self.final_file_id(trans_id) is None:
 
 
604
                conflicts.append(('unversioned executability', trans_id))
 
 
607
                    non_file = self.final_kind(trans_id) != "file"
 
 
611
                    conflicts.append(('non-file executability', trans_id))
 
 
614
    def _overwrite_conflicts(self):
 
 
615
        """Check for overwrites (not permitted on Win32)"""
 
 
617
        for trans_id in self._new_contents:
 
 
619
                self.tree_kind(trans_id)
 
 
622
            if trans_id not in self._removed_contents:
 
 
623
                conflicts.append(('overwrite', trans_id,
 
 
624
                                 self.final_name(trans_id)))
 
 
627
    def _duplicate_entries(self, by_parent):
 
 
628
        """No directory may have two entries with the same name."""
 
 
630
        for children in by_parent.itervalues():
 
 
631
            name_ids = [(self.final_name(t), t) for t in children]
 
 
635
            for name, trans_id in name_ids:
 
 
636
                if name == last_name:
 
 
637
                    conflicts.append(('duplicate', last_trans_id, trans_id,
 
 
640
                    kind = self.final_kind(trans_id)
 
 
643
                file_id = self.final_file_id(trans_id)
 
 
644
                if kind is not None or file_id is not None:
 
 
646
                    last_trans_id = trans_id
 
 
649
    def _duplicate_ids(self):
 
 
650
        """Each inventory id may only be used once"""
 
 
652
        removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
 
 
654
        active_tree_ids = set((f for f in self._tree.inventory if
 
 
655
                               f not in removed_tree_ids))
 
 
656
        for trans_id, file_id in self._new_id.iteritems():
 
 
657
            if file_id in active_tree_ids:
 
 
658
                old_trans_id = self.trans_id_tree_file_id(file_id)
 
 
659
                conflicts.append(('duplicate id', old_trans_id, trans_id))
 
 
662
    def _parent_type_conflicts(self, by_parent):
 
 
663
        """parents must have directory 'contents'."""
 
 
665
        for parent_id, children in by_parent.iteritems():
 
 
666
            if parent_id is ROOT_PARENT:
 
 
668
            if not self._any_contents(children):
 
 
670
            for child in children:
 
 
672
                    self.final_kind(child)
 
 
676
                kind = self.final_kind(parent_id)
 
 
680
                conflicts.append(('missing parent', parent_id))
 
 
681
            elif kind != "directory":
 
 
682
                conflicts.append(('non-directory parent', parent_id))
 
 
685
    def _any_contents(self, trans_ids):
 
 
686
        """Return true if any of the trans_ids, will have contents."""
 
 
687
        for trans_id in trans_ids:
 
 
689
                kind = self.final_kind(trans_id)
 
 
696
        """Apply all changes to the inventory and filesystem.
 
 
698
        If filesystem or inventory conflicts are present, MalformedTransform
 
 
701
        conflicts = self.find_conflicts()
 
 
702
        if len(conflicts) != 0:
 
 
703
            raise MalformedTransform(conflicts=conflicts)
 
 
705
        inv = self._tree.inventory
 
 
706
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
 
708
            child_pb.update('Apply phase', 0, 2)
 
 
709
            self._apply_removals(inv, limbo_inv)
 
 
710
            child_pb.update('Apply phase', 1, 2)
 
 
711
            modified_paths = self._apply_insertions(inv, limbo_inv)
 
 
714
        self._tree._write_inventory(inv)
 
 
717
        return _TransformResults(modified_paths)
 
 
719
    def _limbo_name(self, trans_id):
 
 
720
        """Generate the limbo name of a file"""
 
 
721
        return pathjoin(self._limbodir, trans_id)
 
 
723
    def _apply_removals(self, inv, limbo_inv):
 
 
724
        """Perform tree operations that remove directory/inventory names.
 
 
726
        That is, delete files that are to be deleted, and put any files that
 
 
727
        need renaming into limbo.  This must be done in strict child-to-parent
 
 
730
        tree_paths = list(self._tree_path_ids.iteritems())
 
 
731
        tree_paths.sort(reverse=True)
 
 
732
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
 
734
            for num, data in enumerate(tree_paths):
 
 
735
                path, trans_id = data
 
 
736
                child_pb.update('removing file', num, len(tree_paths))
 
 
737
                full_path = self._tree.abspath(path)
 
 
738
                if trans_id in self._removed_contents:
 
 
739
                    delete_any(full_path)
 
 
740
                elif trans_id in self._new_name or trans_id in \
 
 
743
                        os.rename(full_path, self._limbo_name(trans_id))
 
 
745
                        if e.errno != errno.ENOENT:
 
 
747
                if trans_id in self._removed_id:
 
 
748
                    if trans_id == self._new_root:
 
 
749
                        file_id = self._tree.inventory.root.file_id
 
 
751
                        file_id = self.tree_file_id(trans_id)
 
 
753
                elif trans_id in self._new_name or trans_id in self._new_parent:
 
 
754
                    file_id = self.tree_file_id(trans_id)
 
 
755
                    if file_id is not None:
 
 
756
                        limbo_inv[trans_id] = inv[file_id]
 
 
761
    def _apply_insertions(self, inv, limbo_inv):
 
 
762
        """Perform tree operations that insert directory/inventory names.
 
 
764
        That is, create any files that need to be created, and restore from
 
 
765
        limbo any files that needed renaming.  This must be done in strict
 
 
766
        parent-to-child order.
 
 
768
        new_paths = self.new_paths()
 
 
770
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
 
772
            for num, (path, trans_id) in enumerate(new_paths):
 
 
773
                child_pb.update('adding file', num, len(new_paths))
 
 
775
                    kind = self._new_contents[trans_id]
 
 
777
                    kind = contents = None
 
 
778
                if trans_id in self._new_contents or \
 
 
779
                    self.path_changed(trans_id):
 
 
780
                    full_path = self._tree.abspath(path)
 
 
782
                        os.rename(self._limbo_name(trans_id), full_path)
 
 
784
                        # We may be renaming a dangling inventory id
 
 
785
                        if e.errno != errno.ENOENT:
 
 
787
                    if trans_id in self._new_contents:
 
 
788
                        modified_paths.append(full_path)
 
 
789
                        del self._new_contents[trans_id]
 
 
791
                if trans_id in self._new_id:
 
 
793
                        kind = file_kind(self._tree.abspath(path))
 
 
794
                    inv.add_path(path, kind, self._new_id[trans_id])
 
 
795
                elif trans_id in self._new_name or trans_id in\
 
 
797
                    entry = limbo_inv.get(trans_id)
 
 
798
                    if entry is not None:
 
 
799
                        entry.name = self.final_name(trans_id)
 
 
800
                        parent_path = os.path.dirname(path)
 
 
802
                            self._tree.inventory.path2id(parent_path)
 
 
805
                # requires files and inventory entries to be in place
 
 
806
                if trans_id in self._new_executability:
 
 
807
                    self._set_executability(path, inv, trans_id)
 
 
810
        return modified_paths
 
 
812
    def _set_executability(self, path, inv, trans_id):
 
 
813
        """Set the executability of versioned files """
 
 
814
        file_id = inv.path2id(path)
 
 
815
        new_executability = self._new_executability[trans_id]
 
 
816
        inv[file_id].executable = new_executability
 
 
817
        if supports_executable():
 
 
818
            abspath = self._tree.abspath(path)
 
 
819
            current_mode = os.stat(abspath).st_mode
 
 
820
            if new_executability:
 
 
823
                to_mode = current_mode | (0100 & ~umask)
 
 
824
                # Enable x-bit for others only if they can read it.
 
 
825
                if current_mode & 0004:
 
 
826
                    to_mode |= 0001 & ~umask
 
 
827
                if current_mode & 0040:
 
 
828
                    to_mode |= 0010 & ~umask
 
 
830
                to_mode = current_mode & ~0111
 
 
831
            os.chmod(abspath, to_mode)
 
 
833
    def _new_entry(self, name, parent_id, file_id):
 
 
834
        """Helper function to create a new filesystem entry."""
 
 
835
        trans_id = self.create_path(name, parent_id)
 
 
836
        if file_id is not None:
 
 
837
            self.version_file(file_id, trans_id)
 
 
840
    def new_file(self, name, parent_id, contents, file_id=None, 
 
 
842
        """Convenience method to create files.
 
 
844
        name is the name of the file to create.
 
 
845
        parent_id is the transaction id of the parent directory of the file.
 
 
846
        contents is an iterator of bytestrings, which will be used to produce
 
 
848
        file_id is the inventory ID of the file, if it is to be versioned.
 
 
850
        trans_id = self._new_entry(name, parent_id, file_id)
 
 
851
        self.create_file(contents, trans_id)
 
 
852
        if executable is not None:
 
 
853
            self.set_executability(executable, trans_id)
 
 
856
    def new_directory(self, name, parent_id, file_id=None):
 
 
857
        """Convenience method to create directories.
 
 
859
        name is the name of the directory to create.
 
 
860
        parent_id is the transaction id of the parent directory of the
 
 
862
        file_id is the inventory ID of the directory, if it is to be versioned.
 
 
864
        trans_id = self._new_entry(name, parent_id, file_id)
 
 
865
        self.create_directory(trans_id)
 
 
868
    def new_symlink(self, name, parent_id, target, file_id=None):
 
 
869
        """Convenience method to create symbolic link.
 
 
871
        name is the name of the symlink to create.
 
 
872
        parent_id is the transaction id of the parent directory of the symlink.
 
 
873
        target is a bytestring of the target of the symlink.
 
 
874
        file_id is the inventory ID of the file, if it is to be versioned.
 
 
876
        trans_id = self._new_entry(name, parent_id, file_id)
 
 
877
        self.create_symlink(target, trans_id)
 
 
880
def joinpath(parent, child):
 
 
881
    """Join tree-relative paths, handling the tree root specially"""
 
 
882
    if parent is None or parent == "":
 
 
885
        return pathjoin(parent, child)
 
 
888
class FinalPaths(object):
 
 
889
    """Make path calculation cheap by memoizing paths.
 
 
891
    The underlying tree must not be manipulated between calls, or else
 
 
892
    the results will likely be incorrect.
 
 
894
    def __init__(self, transform):
 
 
895
        object.__init__(self)
 
 
896
        self._known_paths = {}
 
 
897
        self.transform = transform
 
 
899
    def _determine_path(self, trans_id):
 
 
900
        if trans_id == self.transform.root:
 
 
902
        name = self.transform.final_name(trans_id)
 
 
903
        parent_id = self.transform.final_parent(trans_id)
 
 
904
        if parent_id == self.transform.root:
 
 
907
            return pathjoin(self.get_path(parent_id), name)
 
 
909
    def get_path(self, trans_id):
 
 
910
        """Find the final path associated with a trans_id"""
 
 
911
        if trans_id not in self._known_paths:
 
 
912
            self._known_paths[trans_id] = self._determine_path(trans_id)
 
 
913
        return self._known_paths[trans_id]
 
 
915
def topology_sorted_ids(tree):
 
 
916
    """Determine the topological order of the ids in a tree"""
 
 
917
    file_ids = list(tree)
 
 
918
    file_ids.sort(key=tree.id2path)
 
 
921
def build_tree(tree, wt):
 
 
922
    """Create working tree for a branch, using a Transaction."""
 
 
924
    top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
 
925
    pp = ProgressPhase("Build phase", 2, top_pb)
 
 
926
    tt = TreeTransform(wt)
 
 
929
        file_trans_id[wt.get_root_id()] = tt.trans_id_tree_file_id(wt.get_root_id())
 
 
930
        file_ids = topology_sorted_ids(tree)
 
 
931
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
 
933
            for num, file_id in enumerate(file_ids):
 
 
934
                pb.update("Building tree", num, len(file_ids))
 
 
935
                entry = tree.inventory[file_id]
 
 
936
                if entry.parent_id is None:
 
 
938
                if entry.parent_id not in file_trans_id:
 
 
939
                    raise repr(entry.parent_id)
 
 
940
                parent_id = file_trans_id[entry.parent_id]
 
 
941
                file_trans_id[file_id] = new_by_entry(tt, entry, parent_id, 
 
 
951
def new_by_entry(tt, entry, parent_id, tree):
 
 
952
    """Create a new file according to its inventory entry"""
 
 
956
        contents = tree.get_file(entry.file_id).readlines()
 
 
957
        executable = tree.is_executable(entry.file_id)
 
 
958
        return tt.new_file(name, parent_id, contents, entry.file_id, 
 
 
960
    elif kind == 'directory':
 
 
961
        return tt.new_directory(name, parent_id, entry.file_id)
 
 
962
    elif kind == 'symlink':
 
 
963
        target = tree.get_symlink_target(entry.file_id)
 
 
964
        return tt.new_symlink(name, parent_id, target, entry.file_id)
 
 
966
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
 
 
967
    """Create new file contents according to an inventory entry."""
 
 
968
    if entry.kind == "file":
 
 
970
            lines = tree.get_file(entry.file_id).readlines()
 
 
971
        tt.create_file(lines, trans_id, mode_id=mode_id)
 
 
972
    elif entry.kind == "symlink":
 
 
973
        tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
 
 
974
    elif entry.kind == "directory":
 
 
975
        tt.create_directory(trans_id)
 
 
977
def create_entry_executability(tt, entry, trans_id):
 
 
978
    """Set the executability of a trans_id according to an inventory entry"""
 
 
979
    if entry.kind == "file":
 
 
980
        tt.set_executability(entry.executable, trans_id)
 
 
983
def find_interesting(working_tree, target_tree, filenames):
 
 
984
    """Find the ids corresponding to specified filenames."""
 
 
986
        interesting_ids = None
 
 
988
        interesting_ids = set()
 
 
989
        for tree_path in filenames:
 
 
991
            for tree in (working_tree, target_tree):
 
 
992
                file_id = tree.inventory.path2id(tree_path)
 
 
993
                if file_id is not None:
 
 
994
                    interesting_ids.add(file_id)
 
 
997
                raise NotVersionedError(path=tree_path)
 
 
998
    return interesting_ids
 
 
1001
def change_entry(tt, file_id, working_tree, target_tree, 
 
 
1002
                 trans_id_file_id, backups, trans_id, by_parent):
 
 
1003
    """Replace a file_id's contents with those from a target tree."""
 
 
1004
    e_trans_id = trans_id_file_id(file_id)
 
 
1005
    entry = target_tree.inventory[file_id]
 
 
1006
    has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry, 
 
 
1009
        mode_id = e_trans_id
 
 
1012
                tt.delete_contents(e_trans_id)
 
 
1014
                parent_trans_id = trans_id_file_id(entry.parent_id)
 
 
1015
                backup_name = get_backup_name(entry, by_parent,
 
 
1016
                                              parent_trans_id, tt)
 
 
1017
                tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
 
 
1018
                tt.unversion_file(e_trans_id)
 
 
1019
                e_trans_id = tt.create_path(entry.name, parent_trans_id)
 
 
1020
                tt.version_file(file_id, e_trans_id)
 
 
1021
                trans_id[file_id] = e_trans_id
 
 
1022
        create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
 
 
1023
        create_entry_executability(tt, entry, e_trans_id)
 
 
1026
        tt.set_executability(entry.executable, e_trans_id)
 
 
1027
    if tt.final_name(e_trans_id) != entry.name:
 
 
1030
        parent_id = tt.final_parent(e_trans_id)
 
 
1031
        parent_file_id = tt.final_file_id(parent_id)
 
 
1032
        if parent_file_id != entry.parent_id:
 
 
1037
        parent_trans_id = trans_id_file_id(entry.parent_id)
 
 
1038
        tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
 
 
1041
def get_backup_name(entry, by_parent, parent_trans_id, tt):
 
 
1042
    """Produce a backup-style name that appears to be available"""
 
 
1046
            yield "%s.~%d~" % (entry.name, counter)
 
 
1048
    for name in name_gen():
 
 
1049
        if not tt.has_named_child(by_parent, parent_trans_id, name):
 
 
1052
def _entry_changes(file_id, entry, working_tree):
 
 
1053
    """Determine in which ways the inventory entry has changed.
 
 
1055
    Returns booleans: has_contents, content_mod, meta_mod
 
 
1056
    has_contents means there are currently contents, but they differ
 
 
1057
    contents_mod means contents need to be modified
 
 
1058
    meta_mod means the metadata needs to be modified
 
 
1060
    cur_entry = working_tree.inventory[file_id]
 
 
1062
        working_kind = working_tree.kind(file_id)
 
 
1065
        if e.errno != errno.ENOENT:
 
 
1067
        has_contents = False
 
 
1070
    if has_contents is True:
 
 
1071
        real_e_kind = entry.kind
 
 
1072
        if real_e_kind == 'root_directory':
 
 
1073
            real_e_kind = 'directory'
 
 
1074
        if real_e_kind != working_kind:
 
 
1075
            contents_mod, meta_mod = True, False
 
 
1077
            cur_entry._read_tree_state(working_tree.id2path(file_id), 
 
 
1079
            contents_mod, meta_mod = entry.detect_changes(cur_entry)
 
 
1080
            cur_entry._forget_tree_state()
 
 
1081
    return has_contents, contents_mod, meta_mod
 
 
1084
def revert(working_tree, target_tree, filenames, backups=False, 
 
 
1085
           pb=DummyProgress()):
 
 
1086
    """Revert a working tree's contents to those of a target tree."""
 
 
1087
    interesting_ids = find_interesting(working_tree, target_tree, filenames)
 
 
1088
    def interesting(file_id):
 
 
1089
        return interesting_ids is None or file_id in interesting_ids
 
 
1091
    tt = TreeTransform(working_tree, pb)
 
 
1093
        merge_modified = working_tree.merge_modified()
 
 
1095
        def trans_id_file_id(file_id):
 
 
1097
                return trans_id[file_id]
 
 
1099
                return tt.trans_id_tree_file_id(file_id)
 
 
1101
        pp = ProgressPhase("Revert phase", 4, pb)
 
 
1103
        sorted_interesting = [i for i in topology_sorted_ids(target_tree) if
 
 
1105
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
 
1107
            by_parent = tt.by_parent()
 
 
1108
            for id_num, file_id in enumerate(sorted_interesting):
 
 
1109
                child_pb.update("Reverting file", id_num+1, 
 
 
1110
                                len(sorted_interesting))
 
 
1111
                if file_id not in working_tree.inventory:
 
 
1112
                    entry = target_tree.inventory[file_id]
 
 
1113
                    parent_id = trans_id_file_id(entry.parent_id)
 
 
1114
                    e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
 
 
1115
                    trans_id[file_id] = e_trans_id
 
 
1117
                    backup_this = backups
 
 
1118
                    if file_id in merge_modified:
 
 
1120
                        del merge_modified[file_id]
 
 
1121
                    change_entry(tt, file_id, working_tree, target_tree, 
 
 
1122
                                 trans_id_file_id, backup_this, trans_id,
 
 
1127
        wt_interesting = [i for i in working_tree.inventory if interesting(i)]
 
 
1128
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
 
1130
            for id_num, file_id in enumerate(wt_interesting):
 
 
1131
                child_pb.update("New file check", id_num+1, 
 
 
1132
                                len(sorted_interesting))
 
 
1133
                if file_id not in target_tree:
 
 
1134
                    trans_id = tt.trans_id_tree_file_id(file_id)
 
 
1135
                    tt.unversion_file(trans_id)
 
 
1136
                    if file_id in merge_modified:
 
 
1137
                        tt.delete_contents(trans_id)
 
 
1138
                        del merge_modified[file_id]
 
 
1142
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
 
1144
            raw_conflicts = resolve_conflicts(tt, child_pb)
 
 
1147
        conflicts = cook_conflicts(raw_conflicts, tt)
 
 
1148
        for conflict in conflicts:
 
 
1152
        working_tree.set_merge_modified({})
 
 
1159
def resolve_conflicts(tt, pb=DummyProgress()):
 
 
1160
    """Make many conflict-resolution attempts, but die if they fail"""
 
 
1161
    new_conflicts = set()
 
 
1164
            pb.update('Resolution pass', n+1, 10)
 
 
1165
            conflicts = tt.find_conflicts()
 
 
1166
            if len(conflicts) == 0:
 
 
1167
                return new_conflicts
 
 
1168
            new_conflicts.update(conflict_pass(tt, conflicts))
 
 
1169
        raise MalformedTransform(conflicts=conflicts)
 
 
1174
def conflict_pass(tt, conflicts):
 
 
1175
    """Resolve some classes of conflicts."""
 
 
1176
    new_conflicts = set()
 
 
1177
    for c_type, conflict in ((c[0], c) for c in conflicts):
 
 
1178
        if c_type == 'duplicate id':
 
 
1179
            tt.unversion_file(conflict[1])
 
 
1180
            new_conflicts.add((c_type, 'Unversioned existing file',
 
 
1181
                               conflict[1], conflict[2], ))
 
 
1182
        elif c_type == 'duplicate':
 
 
1183
            # files that were renamed take precedence
 
 
1184
            new_name = tt.final_name(conflict[1])+'.moved'
 
 
1185
            final_parent = tt.final_parent(conflict[1])
 
 
1186
            if tt.path_changed(conflict[1]):
 
 
1187
                tt.adjust_path(new_name, final_parent, conflict[2])
 
 
1188
                new_conflicts.add((c_type, 'Moved existing file to', 
 
 
1189
                                   conflict[2], conflict[1]))
 
 
1191
                tt.adjust_path(new_name, final_parent, conflict[1])
 
 
1192
                new_conflicts.add((c_type, 'Moved existing file to', 
 
 
1193
                                  conflict[1], conflict[2]))
 
 
1194
        elif c_type == 'parent loop':
 
 
1195
            # break the loop by undoing one of the ops that caused the loop
 
 
1197
            while not tt.path_changed(cur):
 
 
1198
                cur = tt.final_parent(cur)
 
 
1199
            new_conflicts.add((c_type, 'Cancelled move', cur,
 
 
1200
                               tt.final_parent(cur),))
 
 
1201
            tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
 
 
1203
        elif c_type == 'missing parent':
 
 
1204
            trans_id = conflict[1]
 
 
1206
                tt.cancel_deletion(trans_id)
 
 
1207
                new_conflicts.add((c_type, 'Not deleting', trans_id))
 
 
1209
                tt.create_directory(trans_id)
 
 
1210
                new_conflicts.add((c_type, 'Created directory.', trans_id))
 
 
1211
        elif c_type == 'unversioned parent':
 
 
1212
            tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
 
 
1213
            new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
 
 
1214
    return new_conflicts
 
 
1217
def cook_conflicts(raw_conflicts, tt):
 
 
1218
    """Generate a list of cooked conflicts, sorted by file path"""
 
 
1219
    from bzrlib.conflicts import Conflict
 
 
1220
    conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
 
 
1221
    return sorted(conflict_iter, key=Conflict.sort_key)
 
 
1224
def iter_cook_conflicts(raw_conflicts, tt):
 
 
1225
    from bzrlib.conflicts import Conflict
 
 
1227
    for conflict in raw_conflicts:
 
 
1228
        c_type = conflict[0]
 
 
1229
        action = conflict[1]
 
 
1230
        modified_path = fp.get_path(conflict[2])
 
 
1231
        modified_id = tt.final_file_id(conflict[2])
 
 
1232
        if len(conflict) == 3:
 
 
1233
            yield Conflict.factory(c_type, action=action, path=modified_path,
 
 
1234
                                     file_id=modified_id)
 
 
1237
            conflicting_path = fp.get_path(conflict[3])
 
 
1238
            conflicting_id = tt.final_file_id(conflict[3])
 
 
1239
            yield Conflict.factory(c_type, action=action, path=modified_path,
 
 
1240
                                   file_id=modified_id, 
 
 
1241
                                   conflict_path=conflicting_path,
 
 
1242
                                   conflict_file_id=conflicting_id)