1
from merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
2
from changeset import generate_changeset, ExceptionConflictHandler
3
from changeset import Inventory, Diff3Merge
1
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
2
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
3
from bzrlib.changeset import Inventory, Diff3Merge
4
4
from bzrlib import find_branch
5
5
import bzrlib.osutils
6
6
from bzrlib.errors import BzrCommandError
15
# comments from abentley on irc: merge happens in two stages, each
16
# of which generates a changeset object
18
# stage 1: generate OLD->OTHER,
19
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
14
21
class UnrelatedBranches(BzrCommandError):
15
22
def __init__(self):
16
23
msg = "Branches have no common ancestor, and no base revision"\
21
28
class MergeConflictHandler(ExceptionConflictHandler):
22
"""Handle conflicts encountered while merging"""
29
"""Handle conflicts encountered while merging.
31
This subclasses ExceptionConflictHandler, so that any types of
32
conflict that are not explicitly handled cause an exception and
23
35
def __init__(self, dir, ignore_zero=False):
24
36
ExceptionConflictHandler.__init__(self, dir)
37
49
os.chmod(dest, 0777 & os.stat(source).st_mode)
51
def dump(self, lines, dest):
52
"""Copy the text and mode of a file
53
:param source: The path of the file to copy
54
:param dest: The distination file to create
56
d_file = file(dest, "wb")
39
60
def add_suffix(self, name, suffix, last_new_name=None):
40
61
"""Rename a file to append a suffix. If the new name exists, the
41
62
suffix is added repeatedly until a non-existant name is found
60
81
self.conflicts += 1
63
def merge_conflict(self, new_file, this_path, base_path, other_path):
84
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
65
86
Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The
66
87
main file will be a version with diff3 conflicts.
70
91
:param other_path: Path to the file text for the OTHER tree
72
93
self.add_suffix(this_path, ".THIS")
73
self.copy(base_path, this_path+".BASE")
74
self.copy(other_path, this_path+".OTHER")
94
self.dump(base_lines, this_path+".BASE")
95
self.dump(other_lines, this_path+".OTHER")
75
96
os.rename(new_file, this_path)
76
97
self.conflict("Diff3 conflict encountered in %s" % this_path)
99
def new_contents_conflict(self, filename, other_contents):
100
"""Conflicting contents for newly added file."""
101
self.copy(other_contents, filename + ".OTHER")
102
self.conflict("Conflict in newly added file %s" % filename)
78
105
def target_exists(self, entry, target, old_path):
79
106
"""Handle the case when the target file or dir exists"""
80
107
moved_path = self.add_suffix(target, ".moved")
90
117
if not self.ignore_zero:
91
118
print "%d conflicts encountered.\n" % self.conflicts
93
class SourceFile(object):
94
def __init__(self, path, id, present=None, isdir=None):
97
self.present = present
99
self.interesting = True
102
return "SourceFile(%s, %s)" % (self.path, self.id)
104
120
def get_tree(treespec, temp_root, label):
105
121
location, revno = treespec
106
122
branch = find_branch(location)
115
131
return branch, MergeTree(base_tree, temp_path)
118
def abspath(tree, file_id):
119
path = tree.inventory.id2path(file_id)
124
134
def file_exists(tree, file_id):
125
135
return tree.has_filename(tree.id2path(file_id))
127
def inventory_map(tree):
129
for file_id in tree.inventory:
130
path = abspath(tree, file_id)
131
inventory[path] = SourceFile(path, file_id)
135
138
class MergeTree(object):
136
139
def __init__(self, tree, tempdir):
139
142
self.root = tree.basedir
142
self.inventory = inventory_map(tree)
144
146
self.tempdir = tempdir
145
147
os.mkdir(os.path.join(self.tempdir, "texts"))
151
return self.tree.__iter__()
153
def __contains__(self, file_id):
154
return file_id in self.tree
156
def get_file(self, file_id):
157
return self.tree.get_file(file_id)
159
def get_file_sha1(self, id):
160
return self.tree.get_file_sha1(id)
162
def id2path(self, file_id):
163
return self.tree.id2path(file_id)
165
def has_id(self, file_id):
166
return self.tree.has_id(file_id)
148
168
def readonly_path(self, id):
149
169
if id not in self.tree:
232
252
source_file.interesting = source_file.id in interesting_ids
235
def set_optimized(tree_a, tree_b, inventory_a, inventory_b):
236
"""Mark files that have changed texts as interesting
238
for file_id in tree_a.tree.inventory:
239
if file_id not in tree_b.tree.inventory:
241
entry_a = tree_a.tree.inventory[file_id]
242
entry_b = tree_b.tree.inventory[file_id]
243
if (entry_a.kind, entry_b.kind) != ("file", "file"):
245
if None in (entry_a.text_id, entry_b.text_id):
247
if entry_a.text_id != entry_b.text_id:
249
inventory_a[abspath(tree_a.tree, file_id)].interesting = False
250
inventory_b[abspath(tree_b.tree, file_id)].interesting = False
253
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
254
interesting_ids=None):
255
"""Generate a changeset, with preprocessing to select interesting files.
256
using the text_id to mark really-changed files.
257
This permits blazing comparisons when text_ids are present. It also
258
disables metadata comparison for files with identical texts.
255
def generate_cset_optimized(tree_a, tree_b, interesting_ids=None):
256
"""Generate a changeset. If interesting_ids is supplied, only changes
257
to those files will be shown. Metadata changes are stripped.
260
if interesting_ids is None:
261
set_optimized(tree_a, tree_b, inventory_a, inventory_b)
263
set_interesting(inventory_a, inventory_b, interesting_ids)
264
cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
259
cset = generate_changeset(tree_a, tree_b, interesting_ids)
265
260
for entry in cset.entries.itervalues():
266
261
entry.metadata_change = None
271
266
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
272
267
interesting_ids=None):
274
def merge_factory(base_file, other_file):
275
contents_change = merge_type(base_file, other_file)
269
def merge_factory(file_id, base, other):
270
contents_change = merge_type(file_id, base, other)
277
272
contents_change = BackupBeforeChange(contents_change)
278
273
return contents_change
280
def generate_cset(tree_a, tree_b, inventory_a, inventory_b):
281
return generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
284
275
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
286
277
def get_inventory(tree):
287
return tree.inventory
278
return tree.tree.inventory
289
280
inv_changes = merge_flex(this_tree, base_tree, other_tree,
290
generate_cset, get_inventory,
281
generate_cset_optimized, get_inventory,
291
282
MergeConflictHandler(base_tree.root,
292
283
ignore_zero=ignore_zero),
293
merge_factory=merge_factory)
284
merge_factory=merge_factory,
285
interesting_ids=interesting_ids)
296
288
for id, path in inv_changes.iteritems():
301
assert path.startswith('./')
293
assert path.startswith('./'), "path is %s" % path
303
295
adjust_ids.append((path, id))
304
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root, adjust_ids))
296
if len(adjust_ids) > 0:
297
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root,
307
301
def regen_inventory(this_branch, root, new_entries):
308
302
old_entries = this_branch.read_working_inventory()
309
303
new_inventory = {}
306
for path, file_id in new_entries:
309
new_entries_map[file_id] = path
311
def id2path(file_id):
312
path = new_entries_map.get(file_id)
315
entry = old_entries[file_id]
316
if entry.parent_id is None:
318
return os.path.join(id2path(entry.parent_id), entry.name)
311
320
for file_id in old_entries:
312
321
entry = old_entries[file_id]
313
path = old_entries.id2path(file_id)
322
path = id2path(file_id)
314
323
new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
315
324
by_path[path] = file_id