1
from merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
2
from changeset import generate_changeset, ExceptionConflictHandler
3
from changeset import Inventory, Diff3Merge
4
from bzrlib import find_branch
6
from bzrlib.errors import BzrCommandError
7
from bzrlib.diff import compare_trees
8
from trace import mutter, warning
1
# Copyright (C) 2005 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
14
class UnrelatedBranches(BzrCommandError):
16
msg = "Branches have no common ancestor, and no base revision"\
18
BzrCommandError.__init__(self, msg)
22
from fetch import greedy_fetch
25
import bzrlib.revision
26
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
27
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
28
from bzrlib.changeset import Inventory, Diff3Merge
29
from bzrlib.branch import find_branch
30
from bzrlib.errors import BzrCommandError, UnrelatedBranches
31
from bzrlib.delta import compare_trees
32
from bzrlib.trace import mutter, warning
33
from bzrlib.fetch import greedy_fetch
34
from bzrlib.revision import is_ancestor
36
# comments from abentley on irc: merge happens in two stages, each
37
# of which generates a changeset object
39
# stage 1: generate OLD->OTHER,
40
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
21
42
class MergeConflictHandler(ExceptionConflictHandler):
22
"""Handle conflicts encountered while merging"""
43
"""Handle conflicts encountered while merging.
45
This subclasses ExceptionConflictHandler, so that any types of
46
conflict that are not explicitly handled cause an exception and
23
49
def __init__(self, dir, ignore_zero=False):
24
50
ExceptionConflictHandler.__init__(self, dir)
37
63
os.chmod(dest, 0777 & os.stat(source).st_mode)
65
def dump(self, lines, dest):
66
"""Copy the text and mode of a file
67
:param source: The path of the file to copy
68
:param dest: The distination file to create
70
d_file = file(dest, "wb")
39
74
def add_suffix(self, name, suffix, last_new_name=None):
40
75
"""Rename a file to append a suffix. If the new name exists, the
41
76
suffix is added repeatedly until a non-existant name is found
70
105
:param other_path: Path to the file text for the OTHER tree
72
107
self.add_suffix(this_path, ".THIS")
73
self.copy(base_path, this_path+".BASE")
74
self.copy(other_path, this_path+".OTHER")
108
self.dump(base_lines, this_path+".BASE")
109
self.dump(other_lines, this_path+".OTHER")
75
110
os.rename(new_file, this_path)
76
111
self.conflict("Diff3 conflict encountered in %s" % this_path)
113
def new_contents_conflict(self, filename, other_contents):
114
"""Conflicting contents for newly added file."""
115
self.copy(other_contents, filename + ".OTHER")
116
self.conflict("Conflict in newly added file %s" % filename)
78
119
def target_exists(self, entry, target, old_path):
79
120
"""Handle the case when the target file or dir exists"""
80
121
moved_path = self.add_suffix(target, ".moved")
90
131
if not self.ignore_zero:
91
132
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
def get_tree(treespec, temp_root, label):
134
def get_tree(treespec, temp_root, label, local_branch=None):
105
135
location, revno = treespec
106
136
branch = find_branch(location)
107
137
if revno is None:
140
revision = branch.last_patch()
142
revision = branch.lookup_revision(revno)
143
return branch, get_revid_tree(branch, revision, temp_root, label,
146
def get_revid_tree(branch, revision, temp_root, label, local_branch):
108
148
base_tree = branch.working_tree()
110
base_tree = branch.basis_tree()
112
base_tree = branch.revision_tree(branch.lookup_revision(revno))
150
if local_branch is not None:
151
greedy_fetch(local_branch, branch, revision)
152
base_tree = local_branch.revision_tree(revision)
154
base_tree = branch.revision_tree(revision)
113
155
temp_path = os.path.join(temp_root, label)
114
156
os.mkdir(temp_path)
115
return branch, MergeTree(base_tree, temp_path)
118
def abspath(tree, file_id):
119
path = tree.inventory.id2path(file_id)
157
return MergeTree(base_tree, temp_path)
124
160
def file_exists(tree, file_id):
125
161
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
164
class MergeTree(object):
136
165
def __init__(self, tree, tempdir):
139
168
self.root = tree.basedir
142
self.inventory = inventory_map(tree)
144
172
self.tempdir = tempdir
145
173
os.mkdir(os.path.join(self.tempdir, "texts"))
177
return self.tree.__iter__()
148
179
def __contains__(self, file_id):
149
180
return file_id in self.tree
182
def get_file(self, file_id):
183
return self.tree.get_file(file_id)
151
185
def get_file_sha1(self, id):
152
186
return self.tree.get_file_sha1(id)
188
def id2path(self, file_id):
189
return self.tree.id2path(file_id)
191
def has_id(self, file_id):
192
return self.tree.has_id(file_id)
194
def has_or_had_id(self, file_id):
195
if file_id == self.tree.inventory.root.file_id:
197
return self.tree.inventory.has_id(file_id)
154
199
def readonly_path(self, id):
155
200
if id not in self.tree:
176
221
"""Merge changes into a tree.
179
Base for three-way merge.
224
tuple(path, revision) Base for three-way merge.
181
Other revision for three-way merge.
226
tuple(path, revision) Other revision for three-way merge.
183
228
Directory to merge changes into; '.' by default.
185
230
If true, this_dir must have no uncommitted changes before the
232
all available ancestors of other_revision and base_revision are
233
automatically pulled into the branch.
235
from bzrlib.revision import common_ancestor, MultipleRevisionSources
236
from bzrlib.errors import NoSuchRevision
188
237
tempdir = tempfile.mkdtemp(prefix="bzr-")
190
239
if this_dir is None:
192
241
this_branch = find_branch(this_dir)
242
this_rev_id = this_branch.last_patch()
243
if this_rev_id is None:
244
raise BzrCommandError("This branch has no commits")
194
246
changes = compare_trees(this_branch.working_tree(),
195
247
this_branch.basis_tree(), False)
196
248
if changes.has_changed():
197
249
raise BzrCommandError("Working tree has uncommitted changes.")
198
other_branch, other_tree = get_tree(other_revision, tempdir, "other")
250
other_branch, other_tree = get_tree(other_revision, tempdir, "other",
252
if other_revision[1] == -1:
253
other_rev_id = other_branch.last_patch()
254
other_basis = other_rev_id
255
elif other_revision[1] is not None:
256
other_rev_id = other_branch.lookup_revision(other_revision[1])
257
other_basis = other_rev_id
260
other_basis = other_branch.last_patch()
199
261
if base_revision == [None, None]:
200
if other_revision[1] == -1:
203
o_revno = other_revision[1]
204
base_revno = this_branch.common_ancestor(other_branch,
205
other_revno=o_revno)[0]
206
if base_revno is None:
262
base_rev_id = common_ancestor(this_rev_id, other_basis,
264
if base_rev_id is None:
207
265
raise UnrelatedBranches()
208
base_revision = ['.', base_revno]
209
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
266
base_tree = get_revid_tree(this_branch, base_rev_id, tempdir,
268
base_is_ancestor = True
270
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
271
if base_revision[1] == -1:
272
base_rev_id = base_branch.last_patch()
273
elif base_revision[1] is None:
276
base_rev_id = base_branch.lookup_revision(base_revision[1])
277
if base_rev_id is not None:
278
base_is_ancestor = is_ancestor(this_rev_id, base_rev_id,
279
MultipleRevisionSources(this_branch,
282
base_is_ancestor = False
210
283
if file_list is None:
211
284
interesting_ids = None
238
313
source_file.interesting = source_file.id in interesting_ids
241
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
242
interesting_ids=None):
316
def generate_cset_optimized(tree_a, tree_b, interesting_ids=None):
243
317
"""Generate a changeset. If interesting_ids is supplied, only changes
244
318
to those files will be shown. Metadata changes are stripped.
246
if interesting_ids is not None:
247
set_interesting(inventory_a, inventory_b, interesting_ids)
248
cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
320
cset = generate_changeset(tree_a, tree_b, interesting_ids)
249
321
for entry in cset.entries.itervalues():
250
322
entry.metadata_change = None
255
327
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
256
328
interesting_ids=None):
258
def merge_factory(base_file, other_file):
259
contents_change = merge_type(base_file, other_file)
330
def merge_factory(file_id, base, other):
331
contents_change = merge_type(file_id, base, other)
261
333
contents_change = BackupBeforeChange(contents_change)
262
334
return contents_change
264
def generate_cset(tree_a, tree_b, inventory_a, inventory_b):
265
return generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
268
336
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
270
338
def get_inventory(tree):
271
return tree.inventory
339
return tree.tree.inventory
273
341
inv_changes = merge_flex(this_tree, base_tree, other_tree,
274
generate_cset, get_inventory,
342
generate_cset_optimized, get_inventory,
275
343
MergeConflictHandler(base_tree.root,
276
344
ignore_zero=ignore_zero),
277
merge_factory=merge_factory)
345
merge_factory=merge_factory,
346
interesting_ids=interesting_ids)
280
349
for id, path in inv_changes.iteritems():
294
363
old_entries = this_branch.read_working_inventory()
295
364
new_inventory = {}
367
for path, file_id in new_entries:
370
new_entries_map[file_id] = path
372
def id2path(file_id):
373
path = new_entries_map.get(file_id)
376
entry = old_entries[file_id]
377
if entry.parent_id is None:
379
return os.path.join(id2path(entry.parent_id), entry.name)
297
381
for file_id in old_entries:
298
382
entry = old_entries[file_id]
299
path = old_entries.id2path(file_id)
383
path = id2path(file_id)
300
384
new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
301
385
by_path[path] = file_id