~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transform.py

  • Committer: Aaron Bentley
  • Date: 2006-02-22 14:39:42 UTC
  • mto: (2027.1.2 revert-subpath-56549)
  • mto: This revision was merged to the branch mainline in revision 1570.
  • Revision ID: abentley@panoramicfeedback.com-20060222143942-ae72299f2de66767
Fixed build_tree with symlinks

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