~bzr-pqm/bzr/bzr.dev

1092.2.24 by Robert Collins
merge from martins newformat branch - brings in transport abstraction
1
import os.path
2
493 by Martin Pool
- Merge aaron's merge command
3
import changeset
4
from changeset import Inventory, apply_changeset, invert_dict
1092.2.24 by Robert Collins
merge from martins newformat branch - brings in transport abstraction
5
from bzrlib.osutils import backup_file, rename
6
from bzrlib.merge3 import Merge3
7
import bzrlib
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
8
9
class ApplyMerge3:
10
    """Contents-change wrapper around merge3.Merge3"""
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
11
    def __init__(self, file_id, base, other):
12
        self.file_id = file_id
13
        self.base = base
14
        self.other = other
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
15
 
16
    def __eq__(self, other):
17
        if not isinstance(other, ApplyMerge3):
18
            return False
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
19
        return (self.base == other.base and 
20
                self.other == other.other and self.file_id == other.file_id)
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
21
22
    def __ne__(self, other):
23
        return not (self == other)
24
25
    def apply(self, filename, conflict_handler, reverse=False):
26
        new_file = filename+".new" 
27
        if not reverse:
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
28
            base = self.base
29
            other = self.other
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
30
        else:
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
31
            base = self.other
32
            other = self.base
33
        def get_lines(tree):
34
            if self.file_id not in tree:
35
                raise Exception("%s not in tree" % self.file_id)
36
                return ()
37
            return tree.get_file(self.file_id).readlines()
1448 by Robert Collins
revert symlinks correctly
38
        ### garh. 
39
        other_entry = other.tree.inventory[self.file_id]
40
        if other_entry.kind == 'symlink':
41
            self.apply_symlink(other_entry, base, other, filename)
42
            return
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
43
        base_lines = get_lines(base)
44
        other_lines = get_lines(other)
45
        m3 = Merge3(base_lines, file(filename, "rb").readlines(), other_lines)
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
46
47
        new_conflicts = False
48
        output_file = file(new_file, "wb")
49
        start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
50
        for line in m3.merge_lines(name_a = "TREE", name_b = "MERGE-SOURCE", 
51
                       start_marker=start_marker):
52
            if line.startswith(start_marker):
53
                new_conflicts = True
974.1.45 by aaron.bentley at utoronto
Shortened conflict markers to 7 characters, to please smerge
54
                output_file.write(line.replace(start_marker, '<' * 7))
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
55
            else:
56
                output_file.write(line)
57
        output_file.close()
58
        if not new_conflicts:
59
            os.chmod(new_file, os.stat(filename).st_mode)
1185.1.40 by Robert Collins
Merge what applied of Alexander Belchenko's win32 patch.
60
            rename(new_file, filename)
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
61
            return
62
        else:
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
63
            conflict_handler.merge_conflict(new_file, filename, base_lines,
64
                                            other_lines)
493 by Martin Pool
- Merge aaron's merge command
65
1448 by Robert Collins
revert symlinks correctly
66
    def apply_symlink(self, other_entry, base, other, filename):
67
        if self.file_id in base:
68
            base_entry = base.tree.inventory[self.file_id]
69
            base_entry._read_tree_state(base.tree)
70
        else:
71
            base_entry = None
72
        other_entry._read_tree_state(other.tree)
73
        if not base_entry or other_entry.detect_changes(base_entry):
74
            other_change = True
75
        else:
76
            other_change = False
77
        this_link = os.readlink(filename)
78
        if not base_entry or base_entry.symlink_target != this_link:
79
            this_change = True
80
        else:
81
            this_change = False
82
        if this_change and not other_change:
83
            pass
84
        elif not this_change and other_change:
85
            os.unlink(filename)
86
            os.symlink(other_entry.symlink_target, filename)
87
        elif this_change and other_change:
88
            # conflict
89
            os.unlink(filename)
90
            os.symlink(other_entry.symlink_target, filename + '.OTHER')
91
            os.symlink(this_link, filename + '.THIS')
92
            if base_entry is not None:
93
                os.symlink(other_entry.symlink_target, filename + '.BASE')
94
            note("merge3 conflict in '%s'.\n", filename)
95
974.1.8 by Aaron Bentley
Added default backups for merge-revert
96
97
class BackupBeforeChange:
98
    """Contents-change wrapper to back up file first"""
99
    def __init__(self, contents_change):
100
        self.contents_change = contents_change
101
 
102
    def __eq__(self, other):
103
        if not isinstance(other, BackupBeforeChange):
104
            return False
105
        return (self.contents_change == other.contents_change)
106
107
    def __ne__(self, other):
108
        return not (self == other)
109
110
    def apply(self, filename, conflict_handler, reverse=False):
111
        backup_file(filename)
112
        self.contents_change.apply(filename, conflict_handler, reverse)
113
114
493 by Martin Pool
- Merge aaron's merge command
115
def invert_invent(inventory):
116
    invert_invent = {}
974.1.19 by Aaron Bentley
Removed MergeTree.inventory
117
    for file_id in inventory:
118
        path = inventory.id2path(file_id)
119
        if path == '':
120
            path = './.'
121
        else:
122
            path = './' + path
123
        invert_invent[file_id] = path
493 by Martin Pool
- Merge aaron's merge command
124
    return invert_invent
125
126
127
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
128
               conflict_handler, merge_factory, interesting_ids):
974.1.19 by Aaron Bentley
Removed MergeTree.inventory
129
    cset = changeset_function(base, other, interesting_ids)
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
130
    new_cset = make_merge_changeset(cset, this, base, other, 
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
131
                                    conflict_handler, merge_factory)
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
132
    result = apply_changeset(new_cset, invert_invent(this.tree.inventory),
622 by Martin Pool
Updated merge patch from Aaron
133
                             this.root, conflict_handler, False)
134
    conflict_handler.finalize()
135
    return result
493 by Martin Pool
- Merge aaron's merge command
136
137
    
138
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
139
def make_merge_changeset(cset, this, base, other, 
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
140
                         conflict_handler, merge_factory):
493 by Martin Pool
- Merge aaron's merge command
141
    new_cset = changeset.Changeset()
142
    def get_this_contents(id):
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
143
        path = this.readonly_path(id)
493 by Martin Pool
- Merge aaron's merge command
144
        if os.path.isdir(path):
145
            return changeset.dir_create
146
        else:
147
            return changeset.FileCreate(file(path, "rb").read())
148
149
    for entry in cset.entries.itervalues():
150
        if entry.is_boring():
151
            new_cset.add_entry(entry)
152
        else:
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
153
            new_entry = make_merged_entry(entry, this, base, other, 
154
                                          conflict_handler)
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
155
            new_contents = make_merged_contents(entry, this, base, other, 
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
156
                                                conflict_handler,
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
157
                                                merge_factory)
850 by Martin Pool
- Merge merge updates from aaron
158
            new_entry.contents_change = new_contents
159
            new_entry.metadata_change = make_merged_metadata(entry, base, other)
160
            new_cset.add_entry(new_entry)
161
493 by Martin Pool
- Merge aaron's merge command
162
    return new_cset
163
974.1.22 by Aaron Bentley
Refactored code
164
class ThreeWayConflict(Exception):
165
    def __init__(self, this, base, other):
166
        self.this = this
167
        self.base = base
168
        self.other = other
169
        msg = "Conflict merging %s %s and %s" % (this, base, other)
170
        Exception.__init__(self, msg)
171
172
def threeway_select(this, base, other):
173
    """Returns a value selected by the three-way algorithm.
174
    Raises ThreewayConflict if the algorithm yields a conflict"""
175
    if base == other:
176
        return this
177
    elif base == this:
178
        return other
179
    elif other == this:
180
        return this
181
    else:
182
        raise ThreeWayConflict(this, base, other)
183
184
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
185
def make_merged_entry(entry, this, base, other, conflict_handler):
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
186
    from bzrlib.trace import mutter
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
187
    def entry_data(file_id, tree):
188
        assert hasattr(tree, "__contains__"), "%s" % tree
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
189
        if not tree.has_or_had_id(file_id):
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
190
            return (None, None, "")
191
        entry = tree.tree.inventory[file_id]
192
        my_dir = tree.id2path(entry.parent_id)
193
        if my_dir is None:
194
            my_dir = ""
195
        return entry.name, entry.parent_id, my_dir 
196
    this_name, this_parent, this_dir = entry_data(entry.id, this)
197
    base_name, base_parent, base_dir = entry_data(entry.id, base)
198
    other_name, other_parent, other_dir = entry_data(entry.id, other)
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
199
    mutter("Dirs: this, base, other %r %r %r" % (this_dir, base_dir, other_dir))
200
    mutter("Names: this, base, other %r %r %r" % (this_name, base_name, other_name))
974.1.22 by Aaron Bentley
Refactored code
201
    old_name = this_name
202
    try:
203
        new_name = threeway_select(this_name, base_name, other_name)
204
    except ThreeWayConflict:
205
        new_name = conflict_handler.rename_conflict(entry.id, this_name, 
206
                                                    base_name, other_name)
207
208
    old_parent = this_parent
209
    try:
210
        new_parent = threeway_select(this_parent, base_parent, other_parent)
211
    except ThreeWayConflict:
212
        new_parent = conflict_handler.move_conflict(entry.id, this_dir,
213
                                                    base_dir, other_dir)
214
    def get_path(name, parent):
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
215
        if name is not None:
216
            if name == "":
217
                assert parent is None
218
                return './.'
974.1.22 by Aaron Bentley
Refactored code
219
            parent_dir = {this_parent: this_dir, other_parent: other_dir, 
220
                          base_parent: base_dir}
221
            directory = parent_dir[parent]
222
            return os.path.join(directory, name)
223
        else:
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
224
            assert parent is None
974.1.22 by Aaron Bentley
Refactored code
225
            return None
226
227
    old_path = get_path(old_name, old_parent)
228
        
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
229
    new_entry = changeset.ChangesetEntry(entry.id, old_parent, old_path)
974.1.22 by Aaron Bentley
Refactored code
230
    new_entry.new_path = get_path(new_name, new_parent)
493 by Martin Pool
- Merge aaron's merge command
231
    new_entry.new_parent = new_parent
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
232
    mutter(repr(new_entry))
850 by Martin Pool
- Merge merge updates from aaron
233
    return new_entry
234
235
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
236
def get_contents(entry, tree):
1185.10.10 by Aaron Bentley
Handled modified files missing from THIS
237
    return get_id_contents(entry.id, tree)
238
239
def get_id_contents(file_id, tree):
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
240
    """Get a contents change element suitable for use with ReplaceContents
241
    """
1185.10.10 by Aaron Bentley
Handled modified files missing from THIS
242
    tree_entry = tree.tree.inventory[file_id]
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
243
    if tree_entry.kind == "file":
1185.10.10 by Aaron Bentley
Handled modified files missing from THIS
244
        return changeset.FileCreate(tree.get_file(file_id).read())
1092.3.3 by Robert Collins
abently suggested a patch to symlink support
245
    elif tree_entry.kind == "symlink":
1092.2.24 by Robert Collins
merge from martins newformat branch - brings in transport abstraction
246
        return changeset.SymlinkCreate(tree.get_symlink_target(file_id))
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
247
    else:
248
        assert tree_entry.kind in ("root_directory", "directory")
249
        return changeset.dir_create
250
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
251
def make_merged_contents(entry, this, base, other, conflict_handler,
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
252
                         merge_factory):
850 by Martin Pool
- Merge merge updates from aaron
253
    contents = entry.contents_change
254
    if contents is None:
255
        return None
256
    this_path = this.readonly_path(entry.id)
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
257
    def make_merge():
850 by Martin Pool
- Merge merge updates from aaron
258
        if this_path is None:
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
259
            return conflict_handler.missing_for_merge(entry.id, 
260
                                                      other.id2path(entry.id))
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
261
        return merge_factory(entry.id, base, other)
850 by Martin Pool
- Merge merge updates from aaron
262
263
    if isinstance(contents, changeset.ReplaceContents):
264
        if contents.old_contents is None and contents.new_contents is None:
265
            return None
266
        if contents.new_contents is None:
1185.10.8 by Aaron Bentley
Conflict handling where OTHER is deleted
267
            this_contents = get_contents(entry, this)
1092.2.24 by Robert Collins
merge from martins newformat branch - brings in transport abstraction
268
            if this_path is not None and bzrlib.osutils.lexists(this_path):
1185.10.8 by Aaron Bentley
Conflict handling where OTHER is deleted
269
                if this_contents != contents.old_contents:
270
                    return conflict_handler.rem_contents_conflict(this_path, 
271
                        this_contents, contents.old_contents)
850 by Martin Pool
- Merge merge updates from aaron
272
                return contents
273
            else:
274
                return None
275
        elif contents.old_contents is None:
1092.2.24 by Robert Collins
merge from martins newformat branch - brings in transport abstraction
276
            if this_path is None or not bzrlib.osutils.lexists(this_path):
850 by Martin Pool
- Merge merge updates from aaron
277
                return contents
278
            else:
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
279
                this_contents = get_contents(entry, this)
850 by Martin Pool
- Merge merge updates from aaron
280
                if this_contents == contents.new_contents:
281
                    return None
282
                else:
283
                    other_path = other.readonly_path(entry.id)    
284
                    conflict_handler.new_contents_conflict(this_path, 
285
                                                           other_path)
1448 by Robert Collins
revert symlinks correctly
286
        elif (isinstance(contents.old_contents, changeset.FileCreate)
287
              and isinstance(contents.new_contents, changeset.FileCreate)):
288
            return make_merge()
289
        elif (isinstance(contents.old_contents, changeset.SymlinkCreate)
290
              and isinstance(contents.new_contents, changeset.SymlinkCreate)):
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
291
            return make_merge()
850 by Martin Pool
- Merge merge updates from aaron
292
        else:
293
            raise Exception("Unhandled merge scenario")
294
295
def make_merged_metadata(entry, base, other):
1398 by Robert Collins
integrate in Gustavos x-bit patch
296
    metadata = entry.metadata_change
297
    if metadata is None:
298
        return None
1434 by Robert Collins
merge Gustavos executable2 patch
299
    if isinstance(metadata, changeset.ChangeExecFlag):
300
        if metadata.new_exec_flag is None:
1398 by Robert Collins
integrate in Gustavos x-bit patch
301
            return None
1434 by Robert Collins
merge Gustavos executable2 patch
302
        elif metadata.old_exec_flag is None:
1398 by Robert Collins
integrate in Gustavos x-bit patch
303
            return metadata
304
        else:
305
            base_path = base.readonly_path(entry.id)
306
            other_path = other.readonly_path(entry.id)    
1434 by Robert Collins
merge Gustavos executable2 patch
307
            return ExecFlagMerge(base_path, other_path)
493 by Martin Pool
- Merge aaron's merge command
308
    
309
1434 by Robert Collins
merge Gustavos executable2 patch
310
class ExecFlagMerge(object):
493 by Martin Pool
- Merge aaron's merge command
311
    def __init__(self, base_path, other_path):
312
        self.base_path = base_path
313
        self.other_path = other_path
314
315
    def apply(self, filename, conflict_handler, reverse=False):
316
        if not reverse:
317
            base = self.base_path
318
            other = self.other_path
319
        else:
320
            base = self.other_path
321
            other = self.base_path
1434 by Robert Collins
merge Gustavos executable2 patch
322
        base_mode = os.stat(base).st_mode
323
        base_exec_flag = bool(base_mode & 0111)
324
        other_mode = os.stat(other).st_mode
325
        other_exec_flag = bool(other_mode & 0111)
326
        this_mode = os.stat(filename).st_mode
327
        this_exec_flag = bool(this_mode & 0111)
328
        if (base_exec_flag != other_exec_flag and
329
            this_exec_flag != other_exec_flag):
330
            assert this_exec_flag == base_exec_flag
331
            current_mode = os.stat(filename).st_mode
332
            if other_exec_flag:
333
                umask = os.umask(0)
334
                os.umask(umask)
335
                to_mode = current_mode | (0100 & ~umask)
336
                # Enable x-bit for others only if they can read it.
337
                if current_mode & 0004:
338
                    to_mode |= 0001 & ~umask
339
                if current_mode & 0040:
340
                    to_mode |= 0010 & ~umask
341
            else:
342
                to_mode = current_mode & ~0111
343
            os.chmod(filename, to_mode)
344