/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

Merged John Meinel's integration

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