~bzr-pqm/bzr/bzr.dev

493 by Martin Pool
- Merge aaron's merge command
1
import changeset
2
from changeset import Inventory, apply_changeset, invert_dict
3
import os.path
974.1.8 by Aaron Bentley
Added default backups for merge-revert
4
from osutils import backup_file
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
5
from merge3 import Merge3
6
7
class ApplyMerge3:
8
    """Contents-change wrapper around merge3.Merge3"""
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
9
    def __init__(self, file_id, base, other):
10
        self.file_id = file_id
11
        self.base = base
12
        self.other = other
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
13
 
14
    def __eq__(self, other):
15
        if not isinstance(other, ApplyMerge3):
16
            return False
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
17
        return (self.base == other.base and 
18
                self.other == other.other and self.file_id == other.file_id)
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
19
20
    def __ne__(self, other):
21
        return not (self == other)
22
23
24
    def apply(self, filename, conflict_handler, reverse=False):
25
        new_file = filename+".new" 
26
        if not reverse:
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
27
            base = self.base
28
            other = self.other
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
29
        else:
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
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)
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
40
41
        new_conflicts = False
42
        output_file = file(new_file, "wb")
43
        start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
44
        for line in m3.merge_lines(name_a = "TREE", name_b = "MERGE-SOURCE", 
45
                       start_marker=start_marker):
46
            if line.startswith(start_marker):
47
                new_conflicts = True
974.1.45 by aaron.bentley at utoronto
Shortened conflict markers to 7 characters, to please smerge
48
                output_file.write(line.replace(start_marker, '<' * 7))
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
49
            else:
50
                output_file.write(line)
51
        output_file.close()
52
        if not new_conflicts:
53
            os.chmod(new_file, os.stat(filename).st_mode)
54
            os.rename(new_file, filename)
55
            return
56
        else:
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
57
            conflict_handler.merge_conflict(new_file, filename, base_lines,
58
                                            other_lines)
493 by Martin Pool
- Merge aaron's merge command
59
974.1.8 by Aaron Bentley
Added default backups for merge-revert
60
61
class BackupBeforeChange:
62
    """Contents-change wrapper to back up file first"""
63
    def __init__(self, contents_change):
64
        self.contents_change = contents_change
65
 
66
    def __eq__(self, other):
67
        if not isinstance(other, BackupBeforeChange):
68
            return False
69
        return (self.contents_change == other.contents_change)
70
71
    def __ne__(self, other):
72
        return not (self == other)
73
74
    def apply(self, filename, conflict_handler, reverse=False):
75
        backup_file(filename)
76
        self.contents_change.apply(filename, conflict_handler, reverse)
77
78
493 by Martin Pool
- Merge aaron's merge command
79
def invert_invent(inventory):
80
    invert_invent = {}
974.1.19 by Aaron Bentley
Removed MergeTree.inventory
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
493 by Martin Pool
- Merge aaron's merge command
88
    return invert_invent
89
90
91
def merge_flex(this, base, other, changeset_function, inventory_function,
974.1.17 by Aaron Bentley
Switched to using a set of interesting file_ids instead of SourceFile attribute
92
               conflict_handler, merge_factory, interesting_ids):
974.1.19 by Aaron Bentley
Removed MergeTree.inventory
93
    cset = changeset_function(base, other, interesting_ids)
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
94
    new_cset = make_merge_changeset(cset, this, base, other, 
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
95
                                    conflict_handler, merge_factory)
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
96
    result = apply_changeset(new_cset, invert_invent(this.tree.inventory),
622 by Martin Pool
Updated merge patch from Aaron
97
                             this.root, conflict_handler, False)
98
    conflict_handler.finalize()
99
    return result
493 by Martin Pool
- Merge aaron's merge command
100
101
    
102
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
103
def make_merge_changeset(cset, this, base, other, 
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
104
                         conflict_handler, merge_factory):
493 by Martin Pool
- Merge aaron's merge command
105
    new_cset = changeset.Changeset()
106
    def get_this_contents(id):
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
107
        path = this.readonly_path(id)
493 by Martin Pool
- Merge aaron's merge command
108
        if os.path.isdir(path):
109
            return changeset.dir_create
110
        else:
111
            return changeset.FileCreate(file(path, "rb").read())
112
113
    for entry in cset.entries.itervalues():
114
        if entry.is_boring():
115
            new_cset.add_entry(entry)
116
        else:
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
117
            new_entry = make_merged_entry(entry, this, base, other, 
118
                                          conflict_handler)
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
119
            new_contents = make_merged_contents(entry, this, base, other, 
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
120
                                                conflict_handler,
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
121
                                                merge_factory)
850 by Martin Pool
- Merge merge updates from aaron
122
            new_entry.contents_change = new_contents
123
            new_entry.metadata_change = make_merged_metadata(entry, base, other)
124
            new_cset.add_entry(new_entry)
125
493 by Martin Pool
- Merge aaron's merge command
126
    return new_cset
127
974.1.22 by Aaron Bentley
Refactored code
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
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
149
def make_merged_entry(entry, this, base, other, conflict_handler):
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
150
    from bzrlib.trace import mutter
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
151
    def entry_data(file_id, tree):
152
        assert hasattr(tree, "__contains__"), "%s" % tree
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
153
        if not tree.has_or_had_id(file_id):
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
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)
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
163
    mutter("Dirs: this, base, other %r %r %r" % (this_dir, base_dir, other_dir))
164
    mutter("Names: this, base, other %r %r %r" % (this_name, base_name, other_name))
974.1.22 by Aaron Bentley
Refactored code
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):
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
179
        if name is not None:
180
            if name == "":
181
                assert parent is None
182
                return './.'
974.1.22 by Aaron Bentley
Refactored code
183
            parent_dir = {this_parent: this_dir, other_parent: other_dir, 
184
                          base_parent: base_dir}
185
            directory = parent_dir[parent]
186
            return os.path.join(directory, name)
187
        else:
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
188
            assert parent is None
974.1.22 by Aaron Bentley
Refactored code
189
            return None
190
191
    old_path = get_path(old_name, old_parent)
192
        
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
193
    new_entry = changeset.ChangesetEntry(entry.id, old_parent, old_path)
974.1.22 by Aaron Bentley
Refactored code
194
    new_entry.new_path = get_path(new_name, new_parent)
493 by Martin Pool
- Merge aaron's merge command
195
    new_entry.new_parent = new_parent
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
196
    mutter(repr(new_entry))
850 by Martin Pool
- Merge merge updates from aaron
197
    return new_entry
198
199
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
200
def get_contents(entry, tree):
201
    """Get a contents change element suitable for use with ReplaceContents
202
    """
203
    tree_entry = tree.tree.inventory[entry.id]
204
    if tree_entry.kind == "file":
205
        return changeset.FileCreate(tree.get_file(entry.id).read())
206
    else:
207
        assert tree_entry.kind in ("root_directory", "directory")
208
        return changeset.dir_create
209
210
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
211
def make_merged_contents(entry, this, base, other, conflict_handler,
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
212
                         merge_factory):
850 by Martin Pool
- Merge merge updates from aaron
213
    contents = entry.contents_change
214
    if contents is None:
215
        return None
216
    this_path = this.readonly_path(entry.id)
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
217
    def make_merge():
850 by Martin Pool
- Merge merge updates from aaron
218
        if this_path is None:
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
219
            return conflict_handler.missing_for_merge(entry.id, 
220
                                                      other.id2path(entry.id))
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
221
        return merge_factory(entry.id, base, other)
850 by Martin Pool
- Merge merge updates from aaron
222
223
    if isinstance(contents, changeset.ReplaceContents):
224
        if contents.old_contents is None and contents.new_contents is None:
225
            return None
226
        if contents.new_contents is None:
227
            if this_path is not None and os.path.exists(this_path):
228
                return contents
229
            else:
230
                return None
231
        elif contents.old_contents is None:
232
            if this_path is None or not os.path.exists(this_path):
233
                return contents
234
            else:
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
235
                this_contents = get_contents(entry, this)
850 by Martin Pool
- Merge merge updates from aaron
236
                if this_contents == contents.new_contents:
237
                    return None
238
                else:
239
                    other_path = other.readonly_path(entry.id)    
240
                    conflict_handler.new_contents_conflict(this_path, 
241
                                                           other_path)
242
        elif isinstance(contents.old_contents, changeset.FileCreate) and \
243
            isinstance(contents.new_contents, changeset.FileCreate):
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
244
            return make_merge()
850 by Martin Pool
- Merge merge updates from aaron
245
        else:
246
            raise Exception("Unhandled merge scenario")
247
248
def make_merged_metadata(entry, base, other):
249
    if entry.metadata_change is not None:
250
        base_path = base.readonly_path(entry.id)
251
        other_path = other.readonly_path(entry.id)    
252
        return PermissionsMerge(base_path, other_path)
493 by Martin Pool
- Merge aaron's merge command
253
    
254
558 by Martin Pool
- All top-level classes inherit from object
255
class PermissionsMerge(object):
493 by Martin Pool
- Merge aaron's merge command
256
    def __init__(self, base_path, other_path):
257
        self.base_path = base_path
258
        self.other_path = other_path
259
260
    def apply(self, filename, conflict_handler, reverse=False):
261
        if not reverse:
262
            base = self.base_path
263
            other = self.other_path
264
        else:
265
            base = self.other_path
266
            other = self.base_path
267
        base_stat = os.stat(base).st_mode
268
        other_stat = os.stat(other).st_mode
269
        this_stat = os.stat(filename).st_mode
270
        if base_stat &0777 == other_stat &0777:
271
            return
272
        elif this_stat &0777 == other_stat &0777:
273
            return
274
        elif this_stat &0777 == base_stat &0777:
275
            os.chmod(filename, other_stat)
276
        else:
277
            conflict_handler.permission_conflict(filename, base, other)