/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

Roundtrip tree roots in bundles

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