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