~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_core.py

  • Committer: Martin Pool
  • Date: 2005-08-17 08:32:56 UTC
  • Revision ID: mbp@sourcefrog.net-20050817083255-894936d5aebf7a95
- merge merge improvements from aaron

  revisions to merge are now specified by the -r parameter; the 
  /@ syntax is no longer needed

  abentley@panoramicfeedback.com-20050811205739-dc1988c004f9503e

Show diffs side-by-side

added added

removed removed

Lines of Context:
6
6
 
7
7
class ApplyMerge3:
8
8
    """Contents-change wrapper around merge3.Merge3"""
9
 
    def __init__(self, base_file, other_file):
10
 
        self.base_file = base_file
11
 
        self.other_file = other_file
 
9
    def __init__(self, file_id, base, other):
 
10
        self.file_id = file_id
 
11
        self.base = base
 
12
        self.other = other
12
13
 
13
14
    def __eq__(self, other):
14
15
        if not isinstance(other, ApplyMerge3):
15
16
            return False
16
 
        return (self.base_file == other.base_file and 
17
 
                self.other_file == other.other_file)
 
17
        return (self.base == other.base and 
 
18
                self.other == other.other and self.file_id == other.file_id)
18
19
 
19
20
    def __ne__(self, other):
20
21
        return not (self == other)
23
24
    def apply(self, filename, conflict_handler, reverse=False):
24
25
        new_file = filename+".new" 
25
26
        if not reverse:
26
 
            base = self.base_file
27
 
            other = self.other_file
 
27
            base = self.base
 
28
            other = self.other
28
29
        else:
29
 
            base = self.other_file
30
 
            other = self.base_file
31
 
        m3 = Merge3(file(base, "rb").readlines(), 
32
 
                    file(filename, "rb").readlines(), 
33
 
                    file(other, "rb").readlines())
 
30
            base = self.other
 
31
            other = self.base
 
32
        def get_lines(tree):
 
33
            if self.file_id not in tree:
 
34
                raise Exception("%s not in tree" % self.file_id)
 
35
                return ()
 
36
            return tree.get_file(self.file_id).readlines()
 
37
        base_lines = get_lines(base)
 
38
        other_lines = get_lines(other)
 
39
        m3 = Merge3(base_lines, file(filename, "rb").readlines(), other_lines)
34
40
 
35
41
        new_conflicts = False
36
42
        output_file = file(new_file, "wb")
48
54
            os.rename(new_file, filename)
49
55
            return
50
56
        else:
51
 
            conflict_handler.merge_conflict(new_file, filename, base, other)
 
57
            conflict_handler.merge_conflict(new_file, filename, base_lines,
 
58
                                            other_lines)
52
59
 
53
60
 
54
61
class BackupBeforeChange:
69
76
        self.contents_change.apply(filename, conflict_handler, reverse)
70
77
 
71
78
 
72
 
class ThreewayInventory(object):
73
 
    def __init__(self, this_inventory, base_inventory, other_inventory):
74
 
        self.this = this_inventory
75
 
        self.base = base_inventory
76
 
        self.other = other_inventory
77
79
def invert_invent(inventory):
78
80
    invert_invent = {}
79
 
    for key, value in inventory.iteritems():
80
 
        invert_invent[value.id] = key
 
81
    for file_id in inventory:
 
82
        path = inventory.id2path(file_id)
 
83
        if path == '':
 
84
            path = './.'
 
85
        else:
 
86
            path = './' + path
 
87
        invert_invent[file_id] = path
81
88
    return invert_invent
82
89
 
83
 
def make_inv(inventory):
84
 
    return Inventory(invert_invent(inventory))
85
 
        
86
90
 
87
91
def merge_flex(this, base, other, changeset_function, inventory_function,
88
 
               conflict_handler, merge_factory):
89
 
    this_inventory = inventory_function(this)
90
 
    base_inventory = inventory_function(base)
91
 
    other_inventory = inventory_function(other)
92
 
    inventory = ThreewayInventory(make_inv(this_inventory),
93
 
                                  make_inv(base_inventory), 
94
 
                                  make_inv(other_inventory))
95
 
    cset = changeset_function(base, other, base_inventory, other_inventory)
96
 
    new_cset = make_merge_changeset(cset, inventory, this, base, other, 
 
92
               conflict_handler, merge_factory, interesting_ids):
 
93
    cset = changeset_function(base, other, interesting_ids)
 
94
    new_cset = make_merge_changeset(cset, this, base, other, 
97
95
                                    conflict_handler, merge_factory)
98
 
    result = apply_changeset(new_cset, invert_invent(this_inventory),
 
96
    result = apply_changeset(new_cset, invert_invent(this.tree.inventory),
99
97
                             this.root, conflict_handler, False)
100
98
    conflict_handler.finalize()
101
99
    return result
102
100
 
103
101
    
104
102
 
105
 
def make_merge_changeset(cset, inventory, this, base, other, 
 
103
def make_merge_changeset(cset, this, base, other, 
106
104
                         conflict_handler, merge_factory):
107
105
    new_cset = changeset.Changeset()
108
106
    def get_this_contents(id):
109
 
        path = os.path.join(this.root, inventory.this.get_path(id))
 
107
        path = this.readonly_path(id)
110
108
        if os.path.isdir(path):
111
109
            return changeset.dir_create
112
110
        else:
116
114
        if entry.is_boring():
117
115
            new_cset.add_entry(entry)
118
116
        else:
119
 
            new_entry = make_merged_entry(entry, inventory, conflict_handler)
 
117
            new_entry = make_merged_entry(entry, this, base, other, 
 
118
                                          conflict_handler)
120
119
            new_contents = make_merged_contents(entry, this, base, other, 
121
 
                                                inventory, conflict_handler, 
 
120
                                                conflict_handler,
122
121
                                                merge_factory)
123
122
            new_entry.contents_change = new_contents
124
123
            new_entry.metadata_change = make_merged_metadata(entry, base, other)
126
125
 
127
126
    return new_cset
128
127
 
129
 
def make_merged_entry(entry, inventory, conflict_handler):
 
128
class ThreeWayConflict(Exception):
 
129
    def __init__(self, this, base, other):
 
130
        self.this = this
 
131
        self.base = base
 
132
        self.other = other
 
133
        msg = "Conflict merging %s %s and %s" % (this, base, other)
 
134
        Exception.__init__(self, msg)
 
135
 
 
136
def threeway_select(this, base, other):
 
137
    """Returns a value selected by the three-way algorithm.
 
138
    Raises ThreewayConflict if the algorithm yields a conflict"""
 
139
    if base == other:
 
140
        return this
 
141
    elif base == this:
 
142
        return other
 
143
    elif other == this:
 
144
        return this
 
145
    else:
 
146
        raise ThreeWayConflict(this, base, other)
 
147
 
 
148
 
 
149
def make_merged_entry(entry, this, base, other, conflict_handler):
130
150
    from bzrlib.trace import mutter
131
 
    this_name = inventory.this.get_name(entry.id)
132
 
    this_parent = inventory.this.get_parent(entry.id)
133
 
    this_dir = inventory.this.get_dir(entry.id)
134
 
    if this_dir is None:
135
 
        this_dir = ""
136
 
 
137
 
    base_name = inventory.base.get_name(entry.id)
138
 
    base_parent = inventory.base.get_parent(entry.id)
139
 
    base_dir = inventory.base.get_dir(entry.id)
140
 
    if base_dir is None:
141
 
        base_dir = ""
142
 
    other_name = inventory.other.get_name(entry.id)
143
 
    other_parent = inventory.other.get_parent(entry.id)
144
 
    other_dir = inventory.base.get_dir(entry.id)
145
 
    if other_dir is None:
146
 
        other_dir = ""
 
151
    def entry_data(file_id, tree):
 
152
        assert hasattr(tree, "__contains__"), "%s" % tree
 
153
        if file_id not in tree:
 
154
            return (None, None, "")
 
155
        entry = tree.tree.inventory[file_id]
 
156
        my_dir = tree.id2path(entry.parent_id)
 
157
        if my_dir is None:
 
158
            my_dir = ""
 
159
        return entry.name, entry.parent_id, my_dir 
 
160
    this_name, this_parent, this_dir = entry_data(entry.id, this)
 
161
    base_name, base_parent, base_dir = entry_data(entry.id, base)
 
162
    other_name, other_parent, other_dir = entry_data(entry.id, other)
147
163
    mutter("Dirs: this, base, other %r %r %r" % (this_dir, base_dir, other_dir))
148
164
    mutter("Names: this, base, other %r %r %r" % (this_name, base_name, other_name))
149
 
    if base_name == other_name:
150
 
        old_name = this_name
151
 
        new_name = this_name
152
 
    else:
153
 
        if this_name != base_name and this_name != other_name:
154
 
            conflict_handler.rename_conflict(entry.id, this_name, base_name,
155
 
                                             other_name)
156
 
        else:
157
 
            old_name = this_name
158
 
            new_name = other_name
159
 
 
160
 
    if base_parent == other_parent:
161
 
        old_parent = this_parent
162
 
        new_parent = this_parent
163
 
        old_dir = this_dir
164
 
        new_dir = this_dir
165
 
    else:
166
 
        if this_parent != base_parent and this_parent != other_parent:
167
 
            conflict_handler.move_conflict(entry.id, inventory)
168
 
        else:
169
 
            old_parent = this_parent
170
 
            old_dir = this_dir
171
 
            new_parent = other_parent
172
 
            new_dir = other_dir
173
 
    if old_name is not None and old_parent is not None:
174
 
        old_path = os.path.join(old_dir, old_name)
175
 
    else:
176
 
        old_path = None
 
165
    old_name = this_name
 
166
    try:
 
167
        new_name = threeway_select(this_name, base_name, other_name)
 
168
    except ThreeWayConflict:
 
169
        new_name = conflict_handler.rename_conflict(entry.id, this_name, 
 
170
                                                    base_name, other_name)
 
171
 
 
172
    old_parent = this_parent
 
173
    try:
 
174
        new_parent = threeway_select(this_parent, base_parent, other_parent)
 
175
    except ThreeWayConflict:
 
176
        new_parent = conflict_handler.move_conflict(entry.id, this_dir,
 
177
                                                    base_dir, other_dir)
 
178
    def get_path(name, parent):
 
179
        if name is not None and parent is not None:
 
180
            parent_dir = {this_parent: this_dir, other_parent: other_dir, 
 
181
                          base_parent: base_dir}
 
182
            directory = parent_dir[parent]
 
183
            return os.path.join(directory, name)
 
184
        else:
 
185
            assert name is None and parent is None
 
186
            return None
 
187
 
 
188
    old_path = get_path(old_name, old_parent)
 
189
        
177
190
    new_entry = changeset.ChangesetEntry(entry.id, old_parent, old_path)
178
 
    if new_name is not None and new_parent is not None:
179
 
        new_entry.new_path = os.path.join(new_dir, new_name)
180
 
    else:
181
 
        new_entry.new_path = None
 
191
    new_entry.new_path = get_path(new_name, new_parent)
182
192
    new_entry.new_parent = new_parent
183
193
    mutter(repr(new_entry))
184
194
    return new_entry
185
195
 
186
196
 
187
 
def make_merged_contents(entry, this, base, other, inventory, conflict_handler,
 
197
def make_merged_contents(entry, this, base, other, conflict_handler,
188
198
                         merge_factory):
189
199
    contents = entry.contents_change
190
200
    if contents is None:
192
202
    this_path = this.readonly_path(entry.id)
193
203
    def make_merge():
194
204
        if this_path is None:
195
 
            return conflict_handler.missing_for_merge(entry.id, inventory)
196
 
        base_path = base.readonly_path(entry.id)
197
 
        other_path = other.readonly_path(entry.id)    
198
 
        return merge_factory(base_path, other_path)
 
205
            return conflict_handler.missing_for_merge(entry.id, 
 
206
                                                      other.id2path(entry.id))
 
207
        return merge_factory(entry.id, base, other)
199
208
 
200
 
    if isinstance(contents, changeset.PatchApply):
201
 
        return make_merge()
202
209
    if isinstance(contents, changeset.ReplaceContents):
203
210
        if contents.old_contents is None and contents.new_contents is None:
204
211
            return None
211
218
            if this_path is None or not os.path.exists(this_path):
212
219
                return contents
213
220
            else:
214
 
                this_contents = file(this_path, "rb").read()
 
221
                this_contents = changeset.FileCreate(file(this_path, 
 
222
                                                     "rb").read())
215
223
                if this_contents == contents.new_contents:
216
224
                    return None
217
225
                else:
259
267
import unittest
260
268
import tempfile
261
269
import shutil
 
270
from bzrlib.inventory import InventoryEntry, RootEntry
 
271
from osutils import file_kind
 
272
class FalseTree(object):
 
273
    def __init__(self, realtree):
 
274
        self._realtree = realtree
 
275
        self.inventory = self
 
276
 
 
277
    def __getitem__(self, file_id):
 
278
        entry = self.make_inventory_entry(file_id)
 
279
        if entry is None:
 
280
            raise KeyError(file_id)
 
281
        return entry
 
282
        
 
283
    def make_inventory_entry(self, file_id):
 
284
        path = self._realtree.inventory.get(file_id)
 
285
        if path is None:
 
286
            return None
 
287
        if path == "":
 
288
            return RootEntry(file_id)
 
289
        dir, name = os.path.split(path)
 
290
        kind = file_kind(self._realtree.abs_path(path))
 
291
        for parent_id, path in self._realtree.inventory.iteritems():
 
292
            if path == dir:
 
293
                break
 
294
        if path != dir:
 
295
            raise Exception("Can't find parent for %s" % name)
 
296
        return InventoryEntry(file_id, name, kind, parent_id)
 
297
 
 
298
 
262
299
class MergeTree(object):
263
300
    def __init__(self, dir):
264
301
        self.dir = dir;
265
302
        os.mkdir(dir)
266
303
        self.inventory = {'0': ""}
 
304
        self.tree = FalseTree(self)
267
305
    
268
306
    def child_path(self, parent, name):
269
307
        return os.path.join(self.inventory[parent], name)
301
339
    def readonly_path(self, id):
302
340
        return self.full_path(id)
303
341
 
 
342
    def __contains__(self, file_id):
 
343
        return file_id in self.inventory
 
344
 
 
345
    def get_file(self, file_id):
 
346
        path = self.readonly_path(file_id)
 
347
        return file(path, "rb")
 
348
 
 
349
    def id2path(self, file_id):
 
350
        return self.inventory[file_id]
 
351
 
304
352
    def change_path(self, id, path):
305
353
        new = os.path.join(self.dir, self.inventory[id])
306
354
        os.rename(self.abs_path(self.inventory[id]), self.abs_path(path))
336
384
                tree.remove_file(id)
337
385
            if other or base:
338
386
                change = self.cset.entries[id].contents_change
339
 
                assert isinstance(change, changeset.ReplaceContents)
 
387
                if change is None:
 
388
                    change = changeset.ReplaceContents(None, None)
 
389
                    self.cset.entries[id].contents_change = change
 
390
                    def create_file(tree):
 
391
                        return changeset.FileCreate(tree.get_file(id).read())
 
392
                    if not other:
 
393
                        change.new_contents = create_file(self.other)
 
394
                    if not base:
 
395
                        change.old_contents = create_file(self.base)
 
396
                else:
 
397
                    assert isinstance(change, changeset.ReplaceContents)
340
398
                if other:
341
399
                    change.new_contents=None
342
400
                if base:
432
490
        os.chmod(tree.full_path(id), mode)
433
491
 
434
492
    def merge_changeset(self, merge_factory):
435
 
        all_inventory = ThreewayInventory(Inventory(self.this.inventory),
436
 
                                          Inventory(self.base.inventory), 
437
 
                                          Inventory(self.other.inventory))
438
493
        conflict_handler = changeset.ExceptionConflictHandler(self.this.dir)
439
 
        return make_merge_changeset(self.cset, all_inventory, self.this,
440
 
                                    self.base, self.other, conflict_handler,
 
494
        return make_merge_changeset(self.cset, self.this, self.base,
 
495
                                    self.other, conflict_handler,
441
496
                                    merge_factory)
442
497
 
443
498
    def apply_inv_change(self, inventory_change, orig_inventory):
570
625
 
571
626
    def test_contents_merge3(self):
572
627
        """Test diff3 merging"""
573
 
        def backup_merge(base_file, other_file):
574
 
            return BackupBeforeChange(ApplyMerge3(base_file, other_file))
 
628
        def backup_merge(file_id, base, other):
 
629
            return BackupBeforeChange(ApplyMerge3(file_id, base, other))
575
630
        builder = self.contents_test_success(backup_merge)
576
631
        def backup_exists(file_id):
577
632
            return os.path.exists(builder.this.full_path(file_id)+"~")
594
649
        builder.add_file("2", "0", "name3", "text2", 0655)
595
650
        builder.change_contents("2", base="text5")
596
651
        builder.add_file("3", "0", "name5", "text3", 0744)
 
652
        builder.add_file("4", "0", "name6", "text4", 0744)
 
653
        builder.remove_file("4", base=True)
 
654
        assert not builder.cset.entries["4"].is_boring()
597
655
        builder.change_contents("3", this="text6")
598
656
        cset = builder.merge_changeset(merge_factory)
599
657
        assert(cset.entries["1"].contents_change is not None)
603
661
            assert(isinstance(cset.entries["2"].contents_change,
604
662
                          merge_factory))
605
663
        assert(cset.entries["3"].is_boring())
 
664
        assert(cset.entries["4"].is_boring())
606
665
        builder.apply_changeset(cset)
607
666
        assert(file(builder.this.full_path("1"), "rb").read() == "text4" )
608
667
        assert(file(builder.this.full_path("2"), "rb").read() == "text2" )