/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

More conflict handling, test porting

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