/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

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