/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/transform.py

Switched to build_tree instead of revert

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