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
1
from merge_core import merge_flex
2
from changeset import generate_changeset, ExceptionConflictHandler
3
from changeset import Inventory
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
24
import bzrlib.revision
25
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
26
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
27
from bzrlib.changeset import Inventory, Diff3Merge
28
from bzrlib.branch import find_branch
29
from bzrlib.errors import BzrCommandError, UnrelatedBranches
30
from bzrlib.delta import compare_trees
31
from bzrlib.trace import mutter, warning
32
from bzrlib.fetch import greedy_fetch
33
from bzrlib.revision import is_ancestor
35
# comments from abentley on irc: merge happens in two stages, each
36
# of which generates a changeset object
38
# stage 1: generate OLD->OTHER,
39
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
14
class UnrelatedBranches(BzrCommandError):
16
msg = "Branches have no common ancestor, and no base revision"\
18
BzrCommandError.__init__(self, msg)
41
21
class MergeConflictHandler(ExceptionConflictHandler):
42
"""Handle conflicts encountered while merging.
44
This subclasses ExceptionConflictHandler, so that any types of
45
conflict that are not explicitly handled cause an exception and
22
"""Handle conflicts encountered while merging"""
48
23
def __init__(self, dir, ignore_zero=False):
49
24
ExceptionConflictHandler.__init__(self, dir)
130
90
if not self.ignore_zero:
131
91
print "%d conflicts encountered.\n" % self.conflicts
133
def get_tree(treespec, temp_root, label, local_branch=None):
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
105
location, revno = treespec
135
106
branch = find_branch(location)
136
107
if revno is None:
108
base_tree = branch.working_tree()
138
109
elif revno == -1:
139
revision = branch.last_patch()
141
revision = branch.lookup_revision(revno)
142
return branch, get_revid_tree(branch, revision, temp_root, label,
145
def get_revid_tree(branch, revision, temp_root, label, local_branch):
147
base_tree = branch.working_tree()
149
if local_branch is not None:
150
greedy_fetch(local_branch, branch, revision)
151
base_tree = local_branch.revision_tree(revision)
153
base_tree = branch.revision_tree(revision)
110
base_tree = branch.basis_tree()
112
base_tree = branch.revision_tree(branch.lookup_revision(revno))
154
113
temp_path = os.path.join(temp_root, label)
155
114
os.mkdir(temp_path)
156
return MergeTree(base_tree, temp_path)
115
return branch, MergeTree(base_tree, temp_path)
118
def abspath(tree, file_id):
119
path = tree.inventory.id2path(file_id)
159
124
def file_exists(tree, file_id):
160
125
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)
163
135
class MergeTree(object):
164
136
def __init__(self, tree, tempdir):
229
178
If true, this_dir must have no uncommitted changes before the
231
all available ancestors of other_revision and base_revision are
232
automatically pulled into the branch.
234
from bzrlib.revision import common_ancestor, MultipleRevisionSources
235
from bzrlib.errors import NoSuchRevision
236
181
tempdir = tempfile.mkdtemp(prefix="bzr-")
238
183
if this_dir is None:
240
185
this_branch = find_branch(this_dir)
241
this_rev_id = this_branch.last_patch()
242
if this_rev_id is None:
243
raise BzrCommandError("This branch has no commits")
245
187
changes = compare_trees(this_branch.working_tree(),
246
188
this_branch.basis_tree(), False)
247
189
if changes.has_changed():
248
190
raise BzrCommandError("Working tree has uncommitted changes.")
249
other_branch, other_tree = get_tree(other_revision, tempdir, "other",
251
if other_revision[1] == -1:
252
other_rev_id = other_branch.last_patch()
253
other_basis = other_rev_id
254
elif other_revision[1] is not None:
255
other_rev_id = other_branch.lookup_revision(other_revision[1])
256
other_basis = other_rev_id
259
other_basis = other_branch.last_patch()
191
other_branch, other_tree = get_tree(other_revision, tempdir, "other")
260
192
if base_revision == [None, None]:
261
193
if other_revision[1] == -1:
264
196
o_revno = other_revision[1]
197
base_revno = this_branch.common_ancestor(other_branch,
198
other_revno=o_revno)[0]
199
if base_revno is None:
265
200
raise UnrelatedBranches()
267
base_revision = this_branch.get_revision(base_rev_id)
268
base_branch = this_branch
269
except NoSuchRevision:
270
base_branch = other_branch
271
base_tree = get_revid_tree(base_branch, base_rev_id, tempdir,
273
base_is_ancestor = True
275
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
276
if base_revision[1] == -1:
277
base_rev_id = base_branch.last_patch()
278
elif base_revision[1] is None:
281
base_rev_id = base_branch.lookup_revision(base_revision[1])
282
if base_rev_id is not None:
283
base_is_ancestor = is_ancestor(this_rev_id, base_rev_id,
284
MultipleRevisionSources(this_branch,
287
base_is_ancestor = False
288
if file_list is None:
289
interesting_ids = None
291
interesting_ids = set()
292
this_tree = this_branch.working_tree()
293
for fname in file_list:
294
path = this_branch.relpath(fname)
296
for tree in (this_tree, base_tree.tree, other_tree.tree):
297
file_id = tree.inventory.path2id(path)
298
if file_id is not None:
299
interesting_ids.add(file_id)
302
raise BzrCommandError("%s is not a source file in any"
201
base_revision = ['.', base_revno]
202
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
304
203
merge_inner(this_branch, other_tree, base_tree, tempdir,
305
ignore_zero=ignore_zero, backup_files=backup_files,
306
merge_type=merge_type, interesting_ids=interesting_ids)
307
if base_is_ancestor and other_rev_id is not None:
308
this_branch.add_pending_merge(other_rev_id)
204
ignore_zero=ignore_zero)
310
206
shutil.rmtree(tempdir)
313
def set_interesting(inventory_a, inventory_b, interesting_ids):
314
"""Mark files whose ids are in interesting_ids as interesting
316
for inventory in (inventory_a, inventory_b):
317
for path, source_file in inventory.iteritems():
318
source_file.interesting = source_file.id in interesting_ids
321
def generate_cset_optimized(tree_a, tree_b, interesting_ids=None):
322
"""Generate a changeset. If interesting_ids is supplied, only changes
323
to those files will be shown. Metadata changes are stripped.
209
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b):
210
"""Generate a changeset, using the text_id to mark really-changed files.
211
This permits blazing comparisons when text_ids are present. It also
212
disables metadata comparison for files with identical texts.
325
cset = generate_changeset(tree_a, tree_b, interesting_ids)
214
for file_id in tree_a.tree.inventory:
215
if file_id not in tree_b.tree.inventory:
217
entry_a = tree_a.tree.inventory[file_id]
218
entry_b = tree_b.tree.inventory[file_id]
219
if (entry_a.kind, entry_b.kind) != ("file", "file"):
221
if None in (entry_a.text_id, entry_b.text_id):
223
if entry_a.text_id != entry_b.text_id:
225
inventory_a[abspath(tree_a.tree, file_id)].interesting = False
226
inventory_b[abspath(tree_b.tree, file_id)].interesting = False
227
cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
326
228
for entry in cset.entries.itervalues():
327
229
entry.metadata_change = None
331
233
def merge_inner(this_branch, other_tree, base_tree, tempdir,
332
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
333
interesting_ids=None):
335
def merge_factory(file_id, base, other):
336
contents_change = merge_type(file_id, base, other)
338
contents_change = BackupBeforeChange(contents_change)
339
return contents_change
341
235
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
343
237
def get_inventory(tree):
344
return tree.tree.inventory
238
return tree.inventory
346
240
inv_changes = merge_flex(this_tree, base_tree, other_tree,
347
241
generate_cset_optimized, get_inventory,
348
242
MergeConflictHandler(base_tree.root,
349
ignore_zero=ignore_zero),
350
merge_factory=merge_factory,
351
interesting_ids=interesting_ids)
243
ignore_zero=ignore_zero))
354
246
for id, path in inv_changes.iteritems():
359
assert path.startswith('./'), "path is %s" % path
251
assert path.startswith('./')
361
253
adjust_ids.append((path, id))
362
if len(adjust_ids) > 0:
363
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root,
254
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root, adjust_ids))
367
257
def regen_inventory(this_branch, root, new_entries):
368
258
old_entries = this_branch.read_working_inventory()
369
259
new_inventory = {}
372
for path, file_id in new_entries:
375
new_entries_map[file_id] = path
377
def id2path(file_id):
378
path = new_entries_map.get(file_id)
381
entry = old_entries[file_id]
382
if entry.parent_id is None:
384
return os.path.join(id2path(entry.parent_id), entry.name)
386
261
for file_id in old_entries:
387
262
entry = old_entries[file_id]
388
path = id2path(file_id)
263
path = old_entries.id2path(file_id)
389
264
new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
390
265
by_path[path] = file_id