~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transform.py

Handled simultaneous renames of parent and child better

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006 Canonical Ltd
 
2
 
 
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.
 
7
 
 
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.
 
12
 
 
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
 
16
 
 
17
import os
 
18
import errno
 
19
from stat import S_ISREG
 
20
 
 
21
from bzrlib import BZRDIR
 
22
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
 
23
                           ReusingTransform, NotVersionedError, CantMoveRoot)
 
24
from bzrlib.inventory import InventoryEntry
 
25
from bzrlib.osutils import file_kind, supports_executable, pathjoin
 
26
from bzrlib.trace import mutter
 
27
 
 
28
ROOT_PARENT = "root-parent"
 
29
 
 
30
def unique_add(map, key, value):
 
31
    if key in map:
 
32
        raise DuplicateKey(key=key)
 
33
    map[key] = value
 
34
 
 
35
class TreeTransform(object):
 
36
    """Represent a tree transformation."""
 
37
    def __init__(self, tree):
 
38
        """Note: a write lock is taken on the tree.
 
39
        
 
40
        Use TreeTransform.finalize() to release the lock
 
41
        """
 
42
        object.__init__(self)
 
43
        self._tree = tree
 
44
        self._tree.lock_write()
 
45
        self._id_number = 0
 
46
        self._new_name = {}
 
47
        self._new_parent = {}
 
48
        self._new_contents = {}
 
49
        self._removed_contents = set()
 
50
        self._new_executability = {}
 
51
        self._new_id = {}
 
52
        self._non_present_ids = {}
 
53
        self._r_new_id = {}
 
54
        self._removed_id = set()
 
55
        self._tree_path_ids = {}
 
56
        self._tree_id_paths = {}
 
57
        self._new_root = self.get_id_tree(tree.get_root_id())
 
58
        self.__done = False
 
59
        # XXX use the WorkingTree LockableFiles, when available
 
60
        control_files = self._tree.branch.control_files
 
61
        self._limbodir = control_files.controlfilename('limbo')
 
62
        os.mkdir(self._limbodir)
 
63
 
 
64
    def __get_root(self):
 
65
        return self._new_root
 
66
 
 
67
    root = property(__get_root)
 
68
 
 
69
    def finalize(self):
 
70
        """Release the working tree lock, if held."""
 
71
        if self._tree is None:
 
72
            return
 
73
        for trans_id, kind in self._new_contents.iteritems():
 
74
            path = self._limbo_name(trans_id)
 
75
            if kind == "directory":
 
76
                os.rmdir(path)
 
77
            else:
 
78
                os.unlink(path)
 
79
        os.rmdir(self._limbodir)
 
80
        self._tree.unlock()
 
81
        self._tree = None
 
82
 
 
83
    def _assign_id(self):
 
84
        """Produce a new tranform id"""
 
85
        new_id = "new-%s" % self._id_number
 
86
        self._id_number +=1
 
87
        return new_id
 
88
 
 
89
    def create_path(self, name, parent):
 
90
        """Assign a transaction id to a new path"""
 
91
        trans_id = self._assign_id()
 
92
        unique_add(self._new_name, trans_id, name)
 
93
        unique_add(self._new_parent, trans_id, parent)
 
94
        return trans_id
 
95
 
 
96
    def adjust_path(self, name, parent, trans_id):
 
97
        """Change the path that is assigned to a transaction id."""
 
98
        if trans_id == self._new_root:
 
99
            raise CantMoveRoot
 
100
        self._new_name[trans_id] = name
 
101
        self._new_parent[trans_id] = parent
 
102
 
 
103
    def adjust_root_path(self, name, parent):
 
104
        """Emulate moving the root by moving all children, instead.
 
105
        
 
106
        We do this by undoing the association of root's transaction id with the
 
107
        current tree.  This allows us to create a new directory with that
 
108
        transaction id.  We unversion the root directory and version the 
 
109
        physically new directory, and hope someone versions the tree root
 
110
        later.
 
111
        """
 
112
        old_root = self._new_root
 
113
        old_root_file_id = self.final_file_id(old_root)
 
114
        # force moving all children of root
 
115
        for child_id in self.iter_tree_children(old_root):
 
116
            if child_id != parent:
 
117
                self.adjust_path(self.final_name(child_id), 
 
118
                                 self.final_parent(child_id), child_id)
 
119
            file_id = self.final_file_id(child_id)
 
120
            if file_id is not None:
 
121
                self.unversion_file(child_id)
 
122
            self.version_file(file_id, child_id)
 
123
        
 
124
        # the physical root needs a new transaction id
 
125
        self._tree_path_ids.pop("")
 
126
        self._tree_id_paths.pop(old_root)
 
127
        self._new_root = self.get_id_tree(self._tree.get_root_id())
 
128
        if parent == old_root:
 
129
            parent = self._new_root
 
130
        self.adjust_path(name, parent, old_root)
 
131
        self.create_directory(old_root)
 
132
        self.version_file(old_root_file_id, old_root)
 
133
        self.unversion_file(self._new_root)
 
134
 
 
135
    def get_id_tree(self, inventory_id):
 
136
        """Determine the transaction id of a working tree file.
 
137
        
 
138
        This reflects only files that already exist, not ones that will be
 
139
        added by transactions.
 
140
        """
 
141
        path = self._tree.inventory.id2path(inventory_id)
 
142
        return self.get_tree_path_id(path)
 
143
 
 
144
    def get_trans_id(self, file_id):
 
145
        """\
 
146
        Determine or set the transaction id associated with a file ID.
 
147
        A new id is only created for file_ids that were never present.  If
 
148
        a transaction has been unversioned, it is deliberately still returned.
 
149
        (this will likely lead to an unversioned parent conflict.)
 
150
        """
 
151
        if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
 
152
            return self._r_new_id[file_id]
 
153
        elif file_id in self._tree.inventory:
 
154
            return self.get_id_tree(file_id)
 
155
        elif file_id in self._non_present_ids:
 
156
            return self._non_present_ids[file_id]
 
157
        else:
 
158
            trans_id = self._assign_id()
 
159
            self._non_present_ids[file_id] = trans_id
 
160
            return trans_id
 
161
 
 
162
    def canonical_path(self, path):
 
163
        """Get the canonical tree-relative path"""
 
164
        # don't follow final symlinks
 
165
        dirname, basename = os.path.split(self._tree.abspath(path))
 
166
        dirname = os.path.realpath(dirname)
 
167
        return self._tree.relpath(os.path.join(dirname, basename))
 
168
 
 
169
    def get_tree_path_id(self, path):
 
170
        """Determine (and maybe set) the transaction ID for a tree path."""
 
171
        path = self.canonical_path(path)
 
172
        if path not in self._tree_path_ids:
 
173
            self._tree_path_ids[path] = self._assign_id()
 
174
            self._tree_id_paths[self._tree_path_ids[path]] = path
 
175
        return self._tree_path_ids[path]
 
176
 
 
177
    def get_tree_parent(self, trans_id):
 
178
        """Determine id of the parent in the tree."""
 
179
        path = self._tree_id_paths[trans_id]
 
180
        if path == "":
 
181
            return ROOT_PARENT
 
182
        return self.get_tree_path_id(os.path.dirname(path))
 
183
 
 
184
    def create_file(self, contents, trans_id, mode_id=None):
 
185
        """Schedule creation of a new file.
 
186
 
 
187
        See also new_file.
 
188
        
 
189
        Contents is an iterator of strings, all of which will be written
 
190
        to the target destination.
 
191
 
 
192
        New file takes the permissions of any existing file with that id,
 
193
        unless mode_id is specified.
 
194
        """
 
195
        f = file(self._limbo_name(trans_id), 'wb')
 
196
        unique_add(self._new_contents, trans_id, 'file')
 
197
        for segment in contents:
 
198
            f.write(segment)
 
199
        f.close()
 
200
        self._set_mode(trans_id, mode_id, S_ISREG)
 
201
 
 
202
    def _set_mode(self, trans_id, mode_id, typefunc):
 
203
        if mode_id is None:
 
204
            mode_id = trans_id
 
205
        try:
 
206
            old_path = self._tree_id_paths[mode_id]
 
207
        except KeyError:
 
208
            return
 
209
        try:
 
210
            mode = os.stat(old_path).st_mode
 
211
        except OSError, e:
 
212
            if e.errno == errno.ENOENT:
 
213
                return
 
214
            else:
 
215
                raise
 
216
        if typefunc(mode):
 
217
            os.chmod(self._limbo_name(trans_id), mode)
 
218
 
 
219
    def create_directory(self, trans_id):
 
220
        """Schedule creation of a new directory.
 
221
        
 
222
        See also new_directory.
 
223
        """
 
224
        os.mkdir(self._limbo_name(trans_id))
 
225
        unique_add(self._new_contents, trans_id, 'directory')
 
226
 
 
227
    def create_symlink(self, target, trans_id):
 
228
        """Schedule creation of a new symbolic link.
 
229
 
 
230
        target is a bytestring.
 
231
        See also new_symlink.
 
232
        """
 
233
        os.symlink(target, self._limbo_name(trans_id))
 
234
        unique_add(self._new_contents, trans_id, 'symlink')
 
235
 
 
236
    @staticmethod
 
237
    def delete_any(full_path):
 
238
        try:
 
239
            os.unlink(full_path)
 
240
        except OSError, e:
 
241
        # We may be renaming a dangling inventory id
 
242
            if e.errno != errno.EISDIR and e.errno != errno.EACCES:
 
243
                raise
 
244
            os.rmdir(full_path)
 
245
 
 
246
    def cancel_creation(self, trans_id):
 
247
        del self._new_contents[trans_id]
 
248
        self.delete_any(self._limbo_name(trans_id))
 
249
 
 
250
    def delete_contents(self, trans_id):
 
251
        """Schedule the contents of a path entry for deletion"""
 
252
        self.tree_kind(trans_id)
 
253
        self._removed_contents.add(trans_id)
 
254
 
 
255
    def cancel_deletion(self, trans_id):
 
256
        """Cancel a scheduled deletion"""
 
257
        self._removed_contents.remove(trans_id)
 
258
 
 
259
    def unversion_file(self, trans_id):
 
260
        """Schedule a path entry to become unversioned"""
 
261
        self._removed_id.add(trans_id)
 
262
 
 
263
    def delete_versioned(self, trans_id):
 
264
        """Delete and unversion a versioned file"""
 
265
        self.delete_contents(trans_id)
 
266
        self.unversion_file(trans_id)
 
267
 
 
268
    def set_executability(self, executability, trans_id):
 
269
        """Schedule setting of the 'execute' bit"""
 
270
        if executability is None:
 
271
            del self._new_executability[trans_id]
 
272
        else:
 
273
            unique_add(self._new_executability, trans_id, executability)
 
274
 
 
275
    def version_file(self, file_id, trans_id):
 
276
        """Schedule a file to become versioned."""
 
277
        assert file_id is not None
 
278
        unique_add(self._new_id, trans_id, file_id)
 
279
        unique_add(self._r_new_id, file_id, trans_id)
 
280
 
 
281
    def cancel_versioning(self, trans_id):
 
282
        """Undo a previous versioning of a file"""
 
283
        file_id = self._new_id[trans_id]
 
284
        del self._new_id[trans_id]
 
285
        del self._r_new_id[file_id]
 
286
 
 
287
    def new_paths(self):
 
288
        """Determine the paths of all new and changed files"""
 
289
        new_ids = set()
 
290
        fp = FinalPaths(self)
 
291
        for id_set in (self._new_name, self._new_parent, self._new_contents,
 
292
                       self._new_id, self._new_executability):
 
293
            new_ids.update(id_set)
 
294
        new_paths = [(fp.get_path(t), t) for t in new_ids]
 
295
        new_paths.sort()
 
296
        return new_paths
 
297
 
 
298
    def tree_kind(self, trans_id):
 
299
        """Determine the file kind in the working tree.
 
300
 
 
301
        Raises NoSuchFile if the file does not exist
 
302
        """
 
303
        path = self._tree_id_paths.get(trans_id)
 
304
        if path is None:
 
305
            raise NoSuchFile(None)
 
306
        try:
 
307
            return file_kind(self._tree.abspath(path))
 
308
        except OSError, e:
 
309
            if e.errno != errno.ENOENT:
 
310
                raise
 
311
            else:
 
312
                raise NoSuchFile(path)
 
313
 
 
314
    def final_kind(self, trans_id):
 
315
        """\
 
316
        Determine the final file kind, after any changes applied.
 
317
        
 
318
        Raises NoSuchFile if the file does not exist/has no contents.
 
319
        (It is conceivable that a path would be created without the
 
320
        corresponding contents insertion command)
 
321
        """
 
322
        if trans_id in self._new_contents:
 
323
            return self._new_contents[trans_id]
 
324
        elif trans_id in self._removed_contents:
 
325
            raise NoSuchFile(None)
 
326
        else:
 
327
            return self.tree_kind(trans_id)
 
328
 
 
329
    def get_tree_file_id(self, trans_id):
 
330
        """Determine the file id associated with the trans_id in the tree"""
 
331
        try:
 
332
            path = self._tree_id_paths[trans_id]
 
333
        except KeyError:
 
334
            # the file is a new, unversioned file, or invalid trans_id
 
335
            return None
 
336
        # the file is old; the old id is still valid
 
337
        if self._new_root == trans_id:
 
338
            return self._tree.inventory.root.file_id
 
339
        return self._tree.inventory.path2id(path)
 
340
 
 
341
    def final_file_id(self, trans_id):
 
342
        """\
 
343
        Determine the file id after any changes are applied, or None.
 
344
        
 
345
        None indicates that the file will not be versioned after changes are
 
346
        applied.
 
347
        """
 
348
        try:
 
349
            # there is a new id for this file
 
350
            assert self._new_id[trans_id] is not None
 
351
            return self._new_id[trans_id]
 
352
        except KeyError:
 
353
            if trans_id in self._removed_id:
 
354
                return None
 
355
        return self.get_tree_file_id(trans_id)
 
356
 
 
357
 
 
358
    def inactive_file_id(self, trans_id):
 
359
        """Returns the inactive file_id associated with a transaction id.
 
360
        That is, the one in the tree or in non_present_ids.
 
361
        The file_id may actually be active, too.
 
362
        """
 
363
        file_id = self.get_tree_file_id(trans_id)
 
364
        if file_id is not None:
 
365
            return file_id
 
366
        for key, value in self._non_present_ids.iteritems():
 
367
            if value == trans_id:
 
368
                return key
 
369
 
 
370
 
 
371
    def final_parent(self, trans_id):
 
372
        """\
 
373
        Determine the parent file_id, after any changes are applied.
 
374
 
 
375
        ROOT_PARENT is returned for the tree root.
 
376
        """
 
377
        try:
 
378
            return self._new_parent[trans_id]
 
379
        except KeyError:
 
380
            return self.get_tree_parent(trans_id)
 
381
 
 
382
    def final_name(self, trans_id):
 
383
        """Determine the final filename, after all changes are applied."""
 
384
        try:
 
385
            return self._new_name[trans_id]
 
386
        except KeyError:
 
387
            return os.path.basename(self._tree_id_paths[trans_id])
 
388
 
 
389
    def _by_parent(self):
 
390
        """Return a map of parent: children for known parents.
 
391
        
 
392
        Only new paths and parents of tree files with assigned ids are used.
 
393
        """
 
394
        by_parent = {}
 
395
        items = list(self._new_parent.iteritems())
 
396
        items.extend((t, self.final_parent(t)) for t in 
 
397
                      self._tree_id_paths.keys())
 
398
        for trans_id, parent_id in items:
 
399
            if parent_id not in by_parent:
 
400
                by_parent[parent_id] = set()
 
401
            by_parent[parent_id].add(trans_id)
 
402
        return by_parent
 
403
 
 
404
    def path_changed(self, trans_id):
 
405
        return trans_id in self._new_name or trans_id in self._new_parent
 
406
 
 
407
    def find_conflicts(self):
 
408
        """Find any violations of inventory or filesystem invariants"""
 
409
        if self.__done is True:
 
410
            raise ReusingTransform()
 
411
        conflicts = []
 
412
        # ensure all children of all existent parents are known
 
413
        # all children of non-existent parents are known, by definition.
 
414
        self._add_tree_children()
 
415
        by_parent = self._by_parent()
 
416
        conflicts.extend(self._unversioned_parents(by_parent))
 
417
        conflicts.extend(self._parent_loops())
 
418
        conflicts.extend(self._duplicate_entries(by_parent))
 
419
        conflicts.extend(self._duplicate_ids())
 
420
        conflicts.extend(self._parent_type_conflicts(by_parent))
 
421
        conflicts.extend(self._improper_versioning())
 
422
        conflicts.extend(self._executability_conflicts())
 
423
        return conflicts
 
424
 
 
425
    def _add_tree_children(self):
 
426
        """\
 
427
        Add all the children of all active parents to the known paths.
 
428
 
 
429
        Active parents are those which gain children, and those which are
 
430
        removed.  This is a necessary first step in detecting conflicts.
 
431
        """
 
432
        parents = self._by_parent().keys()
 
433
        parents.extend([t for t in self._removed_contents if 
 
434
                        self.tree_kind(t) == 'directory'])
 
435
        for trans_id in self._removed_id:
 
436
            file_id = self.get_tree_file_id(trans_id)
 
437
            if self._tree.inventory[file_id].kind in ('directory', 
 
438
                                                      'root_directory'):
 
439
                parents.append(trans_id)
 
440
 
 
441
        for parent_id in parents:
 
442
            # ensure that all children are registered with the transaction
 
443
            list(self.iter_tree_children(parent_id))
 
444
 
 
445
    def iter_tree_children(self, parent_id):
 
446
        """Iterate through the entry's tree children, if any"""
 
447
        try:
 
448
            path = self._tree_id_paths[parent_id]
 
449
        except KeyError:
 
450
            return
 
451
        try:
 
452
            children = os.listdir(self._tree.abspath(path))
 
453
        except OSError, e:
 
454
            if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
 
455
                raise
 
456
            return
 
457
            
 
458
        for child in children:
 
459
            childpath = joinpath(path, child)
 
460
            if childpath == BZRDIR:
 
461
                continue
 
462
            yield self.get_tree_path_id(childpath)
 
463
 
 
464
    def _parent_loops(self):
 
465
        """No entry should be its own ancestor"""
 
466
        conflicts = []
 
467
        for trans_id in self._new_parent:
 
468
            seen = set()
 
469
            parent_id = trans_id
 
470
            while parent_id is not ROOT_PARENT:
 
471
                seen.add(parent_id)
 
472
                parent_id = self.final_parent(parent_id)
 
473
                if parent_id == trans_id:
 
474
                    conflicts.append(('parent loop', trans_id))
 
475
                if parent_id in seen:
 
476
                    break
 
477
        return conflicts
 
478
 
 
479
    def _unversioned_parents(self, by_parent):
 
480
        """If parent directories are versioned, children must be versioned."""
 
481
        conflicts = []
 
482
        for parent_id, children in by_parent.iteritems():
 
483
            if parent_id is ROOT_PARENT:
 
484
                continue
 
485
            if self.final_file_id(parent_id) is not None:
 
486
                continue
 
487
            for child_id in children:
 
488
                if self.final_file_id(child_id) is not None:
 
489
                    conflicts.append(('unversioned parent', parent_id))
 
490
                    break;
 
491
        return conflicts
 
492
 
 
493
    def _improper_versioning(self):
 
494
        """\
 
495
        Cannot version a file with no contents, or a bad type.
 
496
        
 
497
        However, existing entries with no contents are okay.
 
498
        """
 
499
        conflicts = []
 
500
        for trans_id in self._new_id.iterkeys():
 
501
            try:
 
502
                kind = self.final_kind(trans_id)
 
503
            except NoSuchFile:
 
504
                conflicts.append(('versioning no contents', trans_id))
 
505
                continue
 
506
            if not InventoryEntry.versionable_kind(kind):
 
507
                conflicts.append(('versioning bad kind', trans_id, kind))
 
508
        return conflicts
 
509
 
 
510
    def _executability_conflicts(self):
 
511
        """Check for bad executability changes.
 
512
        
 
513
        Only versioned files may have their executability set, because
 
514
        1. only versioned entries can have executability under windows
 
515
        2. only files can be executable.  (The execute bit on a directory
 
516
           does not indicate searchability)
 
517
        """
 
518
        conflicts = []
 
519
        for trans_id in self._new_executability:
 
520
            if self.final_file_id(trans_id) is None:
 
521
                conflicts.append(('unversioned executability', trans_id))
 
522
            else:
 
523
                try:
 
524
                    non_file = self.final_kind(trans_id) != "file"
 
525
                except NoSuchFile:
 
526
                    non_file = True
 
527
                if non_file is True:
 
528
                    conflicts.append(('non-file executability', trans_id))
 
529
        return conflicts
 
530
 
 
531
    def _duplicate_entries(self, by_parent):
 
532
        """No directory may have two entries with the same name."""
 
533
        conflicts = []
 
534
        for children in by_parent.itervalues():
 
535
            name_ids = [(self.final_name(t), t) for t in children]
 
536
            name_ids.sort()
 
537
            last_name = None
 
538
            last_trans_id = None
 
539
            for name, trans_id in name_ids:
 
540
                if name == last_name:
 
541
                    conflicts.append(('duplicate', last_trans_id, trans_id,
 
542
                    name))
 
543
                last_name = name
 
544
                last_trans_id = trans_id
 
545
        return conflicts
 
546
 
 
547
    def _duplicate_ids(self):
 
548
        """Each inventory id may only be used once"""
 
549
        conflicts = []
 
550
        removed_tree_ids = set((self.get_tree_file_id(trans_id) for trans_id in
 
551
                                self._removed_id))
 
552
        active_tree_ids = set((f for f in self._tree.inventory if
 
553
                               f not in removed_tree_ids))
 
554
        for trans_id, file_id in self._new_id.iteritems():
 
555
            if file_id in active_tree_ids:
 
556
                old_trans_id = self.get_id_tree(file_id)
 
557
                conflicts.append(('duplicate id', old_trans_id, trans_id))
 
558
        return conflicts
 
559
 
 
560
    def _parent_type_conflicts(self, by_parent):
 
561
        """parents must have directory 'contents'."""
 
562
        conflicts = []
 
563
        for parent_id, children in by_parent.iteritems():
 
564
            if parent_id is ROOT_PARENT:
 
565
                continue
 
566
            if not self._any_contents(children):
 
567
                continue
 
568
            for child in children:
 
569
                try:
 
570
                    self.final_kind(child)
 
571
                except NoSuchFile:
 
572
                    continue
 
573
            try:
 
574
                kind = self.final_kind(parent_id)
 
575
            except NoSuchFile:
 
576
                kind = None
 
577
            if kind is None:
 
578
                conflicts.append(('missing parent', parent_id))
 
579
            elif kind != "directory":
 
580
                conflicts.append(('non-directory parent', parent_id))
 
581
        return conflicts
 
582
 
 
583
    def _any_contents(self, trans_ids):
 
584
        """Return true if any of the trans_ids, will have contents."""
 
585
        for trans_id in trans_ids:
 
586
            try:
 
587
                kind = self.final_kind(trans_id)
 
588
            except NoSuchFile:
 
589
                continue
 
590
            return True
 
591
        return False
 
592
            
 
593
    def apply(self):
 
594
        """\
 
595
        Apply all changes to the inventory and filesystem.
 
596
        
 
597
        If filesystem or inventory conflicts are present, MalformedTransform
 
598
        will be thrown.
 
599
        """
 
600
        conflicts = self.find_conflicts()
 
601
        if len(conflicts) != 0:
 
602
            raise MalformedTransform(conflicts=conflicts)
 
603
        limbo_inv = {}
 
604
        inv = self._tree.inventory
 
605
        self._apply_removals(inv, limbo_inv)
 
606
        self._apply_insertions(inv, limbo_inv)
 
607
        self._tree._write_inventory(inv)
 
608
        self.__done = True
 
609
        self.finalize()
 
610
 
 
611
    def _limbo_name(self, trans_id):
 
612
        """Generate the limbo name of a file"""
 
613
        return os.path.join(self._limbodir, trans_id)
 
614
 
 
615
    def _apply_removals(self, inv, limbo_inv):
 
616
        """Perform tree operations that remove directory/inventory names.
 
617
        
 
618
        That is, delete files that are to be deleted, and put any files that
 
619
        need renaming into limbo.  This must be done in strict child-to-parent
 
620
        order.
 
621
        """
 
622
        tree_paths = list(self._tree_path_ids.iteritems())
 
623
        tree_paths.sort(reverse=True)
 
624
        for path, trans_id in tree_paths:
 
625
            full_path = self._tree.abspath(path)
 
626
            if trans_id in self._removed_contents:
 
627
                self.delete_any(full_path)
 
628
            elif trans_id in self._new_name or trans_id in self._new_parent:
 
629
                try:
 
630
                    os.rename(full_path, self._limbo_name(trans_id))
 
631
                except OSError, e:
 
632
                    if e.errno != errno.ENOENT:
 
633
                        raise
 
634
            if trans_id in self._removed_id:
 
635
                if trans_id == self._new_root:
 
636
                    file_id = self._tree.inventory.root.file_id
 
637
                else:
 
638
                    file_id = self.get_tree_file_id(trans_id)
 
639
                del inv[file_id]
 
640
            elif trans_id in self._new_name or trans_id in self._new_parent:
 
641
                file_id = self.get_tree_file_id(trans_id)
 
642
                if file_id is not None:
 
643
                    limbo_inv[trans_id] = inv[file_id]
 
644
                    del inv[file_id]
 
645
 
 
646
    def _apply_insertions(self, inv, limbo_inv):
 
647
        """Perform tree operations that insert directory/inventory names.
 
648
        
 
649
        That is, create any files that need to be created, and restore from
 
650
        limbo any files that needed renaming.  This must be done in strict
 
651
        parent-to-child order.
 
652
        """
 
653
        for path, trans_id in self.new_paths():
 
654
            try:
 
655
                kind = self._new_contents[trans_id]
 
656
            except KeyError:
 
657
                kind = contents = None
 
658
            if trans_id in self._new_contents or self.path_changed(trans_id):
 
659
                full_path = self._tree.abspath(path)
 
660
                try:
 
661
                    os.rename(self._limbo_name(trans_id), full_path)
 
662
                except OSError, e:
 
663
                    # We may be renaming a dangling inventory id
 
664
                    if e.errno != errno.ENOENT:
 
665
                        raise
 
666
                if trans_id in self._new_contents:
 
667
                    del self._new_contents[trans_id]
 
668
 
 
669
            if trans_id in self._new_id:
 
670
                if kind is None:
 
671
                    kind = file_kind(self._tree.abspath(path))
 
672
                inv.add_path(path, kind, self._new_id[trans_id])
 
673
            elif trans_id in self._new_name or trans_id in self._new_parent:
 
674
                entry = limbo_inv.get(trans_id)
 
675
                if entry is not None:
 
676
                    entry.name = self.final_name(trans_id)
 
677
                    parent_path = os.path.dirname(path)
 
678
                    entry.parent_id = self._tree.inventory.path2id(parent_path)
 
679
                    inv.add(entry)
 
680
 
 
681
            # requires files and inventory entries to be in place
 
682
            if trans_id in self._new_executability:
 
683
                self._set_executability(path, inv, trans_id)
 
684
 
 
685
    def _set_executability(self, path, inv, trans_id):
 
686
        """Set the executability of versioned files """
 
687
        file_id = inv.path2id(path)
 
688
        new_executability = self._new_executability[trans_id]
 
689
        inv[file_id].executable = new_executability
 
690
        if supports_executable():
 
691
            abspath = self._tree.abspath(path)
 
692
            current_mode = os.stat(abspath).st_mode
 
693
            if new_executability:
 
694
                umask = os.umask(0)
 
695
                os.umask(umask)
 
696
                to_mode = current_mode | (0100 & ~umask)
 
697
                # Enable x-bit for others only if they can read it.
 
698
                if current_mode & 0004:
 
699
                    to_mode |= 0001 & ~umask
 
700
                if current_mode & 0040:
 
701
                    to_mode |= 0010 & ~umask
 
702
            else:
 
703
                to_mode = current_mode & ~0111
 
704
            os.chmod(abspath, to_mode)
 
705
 
 
706
    def _new_entry(self, name, parent_id, file_id):
 
707
        """Helper function to create a new filesystem entry."""
 
708
        trans_id = self.create_path(name, parent_id)
 
709
        if file_id is not None:
 
710
            self.version_file(file_id, trans_id)
 
711
        return trans_id
 
712
 
 
713
    def new_file(self, name, parent_id, contents, file_id=None, 
 
714
                 executable=None):
 
715
        """\
 
716
        Convenience method to create files.
 
717
        
 
718
        name is the name of the file to create.
 
719
        parent_id is the transaction id of the parent directory of the file.
 
720
        contents is an iterator of bytestrings, which will be used to produce
 
721
        the file.
 
722
        file_id is the inventory ID of the file, if it is to be versioned.
 
723
        """
 
724
        trans_id = self._new_entry(name, parent_id, file_id)
 
725
        self.create_file(contents, trans_id)
 
726
        if executable is not None:
 
727
            self.set_executability(executable, trans_id)
 
728
        return trans_id
 
729
 
 
730
    def new_directory(self, name, parent_id, file_id=None):
 
731
        """\
 
732
        Convenience method to create directories.
 
733
 
 
734
        name is the name of the directory to create.
 
735
        parent_id is the transaction id of the parent directory of the
 
736
        directory.
 
737
        file_id is the inventory ID of the directory, if it is to be versioned.
 
738
        """
 
739
        trans_id = self._new_entry(name, parent_id, file_id)
 
740
        self.create_directory(trans_id)
 
741
        return trans_id 
 
742
 
 
743
    def new_symlink(self, name, parent_id, target, file_id=None):
 
744
        """\
 
745
        Convenience method to create symbolic link.
 
746
        
 
747
        name is the name of the symlink to create.
 
748
        parent_id is the transaction id of the parent directory of the symlink.
 
749
        target is a bytestring of the target of the symlink.
 
750
        file_id is the inventory ID of the file, if it is to be versioned.
 
751
        """
 
752
        trans_id = self._new_entry(name, parent_id, file_id)
 
753
        self.create_symlink(target, trans_id)
 
754
        return trans_id
 
755
 
 
756
def joinpath(parent, child):
 
757
    """Join tree-relative paths, handling the tree root specially"""
 
758
    if parent is None or parent == "":
 
759
        return child
 
760
    else:
 
761
        return os.path.join(parent, child)
 
762
 
 
763
class FinalPaths(object):
 
764
    """\
 
765
    Make path calculation cheap by memoizing paths.
 
766
 
 
767
    The underlying tree must not be manipulated between calls, or else
 
768
    the results will likely be incorrect.
 
769
    """
 
770
    def __init__(self, transform):
 
771
        object.__init__(self)
 
772
        self._known_paths = {}
 
773
        self.transform = transform
 
774
 
 
775
    def _determine_path(self, trans_id):
 
776
        if trans_id == self.transform.root:
 
777
            return ""
 
778
        name = self.transform.final_name(trans_id)
 
779
        parent_id = self.transform.final_parent(trans_id)
 
780
        if parent_id == self.transform.root:
 
781
            return name
 
782
        else:
 
783
            return os.path.join(self.get_path(parent_id), name)
 
784
 
 
785
    def get_path(self, trans_id):
 
786
        if trans_id not in self._known_paths:
 
787
            self._known_paths[trans_id] = self._determine_path(trans_id)
 
788
        return self._known_paths[trans_id]
 
789
 
 
790
def topology_sorted_ids(tree):
 
791
    """Determine the topological order of the ids in a tree"""
 
792
    file_ids = list(tree)
 
793
    file_ids.sort(key=tree.id2path)
 
794
    return file_ids
 
795
 
 
796
def build_tree(branch, tree):
 
797
    """Create working tree for a branch, using a Transaction."""
 
798
    file_trans_id = {}
 
799
    wt = branch.working_tree()
 
800
    tt = TreeTransform(wt)
 
801
    try:
 
802
        file_trans_id[wt.get_root_id()] = tt.get_id_tree(wt.get_root_id())
 
803
        file_ids = topology_sorted_ids(tree)
 
804
        for file_id in file_ids:
 
805
            entry = tree.inventory[file_id]
 
806
            if entry.parent_id is None:
 
807
                continue
 
808
            if entry.parent_id not in file_trans_id:
 
809
                raise repr(entry.parent_id)
 
810
            parent_id = file_trans_id[entry.parent_id]
 
811
            file_trans_id[file_id] = new_by_entry(tt, entry, parent_id, tree)
 
812
        tt.apply()
 
813
    finally:
 
814
        tt.finalize()
 
815
 
 
816
def new_by_entry(tt, entry, parent_id, tree):
 
817
    name = entry.name
 
818
    kind = entry.kind
 
819
    if kind == 'file':
 
820
        contents = tree.get_file(entry.file_id).readlines()
 
821
        executable = tree.is_executable(entry.file_id)
 
822
        return tt.new_file(name, parent_id, contents, entry.file_id, 
 
823
                           executable)
 
824
    elif kind == 'directory':
 
825
        return tt.new_directory(name, parent_id, entry.file_id)
 
826
    elif kind == 'symlink':
 
827
        target = entry.get_symlink_target(file_id)
 
828
        return tt.new_symlink(name, parent_id, target, file_id)
 
829
 
 
830
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
 
831
    if entry.kind == "file":
 
832
        if lines == None:
 
833
            lines = tree.get_file(entry.file_id).readlines()
 
834
        tt.create_file(lines, trans_id, mode_id=mode_id)
 
835
    elif entry.kind == "symlink":
 
836
        tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
 
837
    elif entry.kind == "directory":
 
838
        tt.create_directory(trans_id)
 
839
 
 
840
def create_entry_executability(tt, entry, trans_id):
 
841
    if entry.kind == "file":
 
842
        tt.set_executability(entry.executable, trans_id)
 
843
 
 
844
def find_interesting(working_tree, target_tree, filenames):
 
845
    if not filenames:
 
846
        interesting_ids = None
 
847
    else:
 
848
        interesting_ids = set()
 
849
        for tree_path in filenames:
 
850
            for tree in (working_tree, target_tree):
 
851
                not_found = True
 
852
                file_id = tree.inventory.path2id(tree_path)
 
853
                if file_id is not None:
 
854
                    interesting_ids.add(file_id)
 
855
                    not_found = False
 
856
                if not_found:
 
857
                    raise NotVersionedError(path=tree_path)
 
858
    return interesting_ids
 
859
 
 
860
 
 
861
def change_entry(tt, file_id, working_tree, target_tree, 
 
862
                 get_trans_id, backups, trans_id):
 
863
    e_trans_id = get_trans_id(file_id)
 
864
    entry = target_tree.inventory[file_id]
 
865
    has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry, 
 
866
                                                           working_tree)
 
867
    if contents_mod:
 
868
        mode_id = e_trans_id
 
869
        if has_contents:
 
870
            if not backups:
 
871
                tt.delete_contents(e_trans_id)
 
872
            else:
 
873
                parent_trans_id = get_trans_id(entry.parent_id)
 
874
                tt.adjust_path(entry.name+"~", parent_trans_id, e_trans_id)
 
875
                tt.unversion_file(e_trans_id)
 
876
                e_trans_id = tt.create_path(entry.name, parent_trans_id)
 
877
                tt.version_file(file_id, e_trans_id)
 
878
                trans_id[file_id] = e_trans_id
 
879
        create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
 
880
        create_entry_executability(tt, entry, e_trans_id)
 
881
 
 
882
    elif meta_mod:
 
883
        tt.set_executability(entry.executable, e_trans_id)
 
884
    if tt.final_name(e_trans_id) != entry.name:
 
885
        adjust_path  = True
 
886
    else:
 
887
        parent_id = tt.final_parent(e_trans_id)
 
888
        parent_file_id = tt.final_file_id(parent_id)
 
889
        if parent_file_id != entry.parent_id:
 
890
            adjust_path = True
 
891
        else:
 
892
            adjust_path = False
 
893
    if adjust_path:
 
894
        parent_trans_id = get_trans_id(entry.parent_id)
 
895
        tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
 
896
 
 
897
 
 
898
def _entry_changes(file_id, entry, working_tree):
 
899
    """\
 
900
    Determine in which ways the inventory entry has changed.
 
901
 
 
902
    Returns booleans: has_contents, content_mod, meta_mod
 
903
    has_contents means there are currently contents, but they differ
 
904
    contents_mod means contents need to be modified
 
905
    meta_mod means the metadata needs to be modified
 
906
    """
 
907
    cur_entry = working_tree.inventory[file_id]
 
908
    try:
 
909
        working_kind = working_tree.kind(file_id)
 
910
        has_contents = True
 
911
    except OSError, e:
 
912
        if e.errno != errno.ENOENT:
 
913
            raise
 
914
        has_contents = False
 
915
        contents_mod = True
 
916
        meta_mod = False
 
917
    if has_contents is True:
 
918
        real_e_kind = entry.kind
 
919
        if real_e_kind == 'root_directory':
 
920
            real_e_kind = 'directory'
 
921
        if real_e_kind != working_kind:
 
922
            contents_mod, meta_mod = True, False
 
923
        else:
 
924
            cur_entry._read_tree_state(working_tree.id2path(file_id), 
 
925
                                       working_tree)
 
926
            contents_mod, meta_mod = entry.detect_changes(cur_entry)
 
927
    return has_contents, contents_mod, meta_mod
 
928
 
 
929
 
 
930
def revert(working_tree, target_tree, filenames, backups=False):
 
931
    interesting_ids = find_interesting(working_tree, target_tree, filenames)
 
932
    def interesting(file_id):
 
933
        return interesting_ids is None or file_id in interesting_ids
 
934
 
 
935
    tt = TreeTransform(working_tree)
 
936
    try:
 
937
        trans_id = {}
 
938
        def get_trans_id(file_id):
 
939
            try:
 
940
                return trans_id[file_id]
 
941
            except KeyError:
 
942
                return tt.get_id_tree(file_id)
 
943
 
 
944
        for file_id in topology_sorted_ids(target_tree):
 
945
            if not interesting(file_id):
 
946
                continue
 
947
            if file_id not in working_tree.inventory:
 
948
                entry = target_tree.inventory[file_id]
 
949
                parent_id = get_trans_id(entry.parent_id)
 
950
                e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
 
951
                trans_id[file_id] = e_trans_id
 
952
            else:
 
953
                change_entry(tt, file_id, working_tree, target_tree, 
 
954
                             get_trans_id, backups, trans_id)
 
955
        for file_id in working_tree:
 
956
            if not interesting(file_id):
 
957
                continue
 
958
            if file_id not in target_tree:
 
959
                tt.unversion_file(tt.get_id_tree(file_id))
 
960
        resolve_conflicts(tt)
 
961
        tt.apply()
 
962
    finally:
 
963
        tt.finalize()
 
964
 
 
965
 
 
966
def resolve_conflicts(tt):
 
967
    """Make many conflict-resolution attempts, but die if they fail"""
 
968
    for n in range(10):
 
969
        conflicts = tt.find_conflicts()
 
970
        if len(conflicts) == 0:
 
971
            return
 
972
        conflict_pass(tt, conflicts)
 
973
    raise MalformedTransform(conflicts=conflicts)
 
974
 
 
975
 
 
976
def conflict_pass(tt, conflicts):
 
977
    for c_type, conflict in ((c[0], c) for c in conflicts):
 
978
        if c_type == 'duplicate id':
 
979
            tt.unversion_file(conflict[1])
 
980
        elif c_type == 'duplicate':
 
981
            # files that were renamed take precedence
 
982
            new_name = tt.final_name(conflict[1])+'.moved'
 
983
            final_parent = tt.final_parent(conflict[1])
 
984
            if tt.path_changed(conflict[1]):
 
985
                tt.adjust_path(new_name, final_parent, conflict[2])
 
986
            else:
 
987
                tt.adjust_path(new_name, final_parent, conflict[1])
 
988
        elif c_type == 'parent loop':
 
989
            # break the loop by undoing one of the ops that caused the loop
 
990
            cur = conflict[1]
 
991
            while not tt.path_changed(cur):
 
992
                cur = tt.final_parent(cur)
 
993
            tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
 
994
        elif c_type == 'missing parent':
 
995
            trans_id = conflict[1]
 
996
            try:
 
997
                tt.cancel_deletion(trans_id)
 
998
            except KeyError:
 
999
                tt.create_directory(trans_id)
 
1000
        elif c_type == 'unversioned parent':
 
1001
            tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])