~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transform.py

  • Committer: Martin Pool
  • Date: 2005-05-09 03:03:55 UTC
  • Revision ID: mbp@sourcefrog.net-20050509030355-ad6ab558d1362959
- Don't give an error if the trace file can't be opened

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