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, 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
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
14
class UnrelatedBranches(BzrCommandError):
16
msg = "Branches have no common ancestor, and no base revision"\
18
BzrCommandError.__init__(self, msg)
42
21
class MergeConflictHandler(ExceptionConflictHandler):
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
22
"""Handle conflicts encountered while merging"""
49
23
def __init__(self, dir, ignore_zero=False):
50
24
ExceptionConflictHandler.__init__(self, dir)
131
90
if not self.ignore_zero:
132
91
print "%d conflicts encountered.\n" % self.conflicts
134
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):
135
105
location, revno = treespec
136
106
branch = find_branch(location)
137
107
if revno is None:
108
base_tree = branch.working_tree()
139
109
elif revno == -1:
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):
148
base_tree = branch.working_tree()
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)
110
base_tree = branch.basis_tree()
112
base_tree = branch.revision_tree(branch.lookup_revision(revno))
155
113
temp_path = os.path.join(temp_root, label)
156
114
os.mkdir(temp_path)
157
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)
160
124
def file_exists(tree, file_id):
161
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)
164
135
class MergeTree(object):
165
136
def __init__(self, tree, tempdir):
168
139
self.root = tree.basedir
142
self.inventory = inventory_map(tree)
172
144
self.tempdir = tempdir
173
145
os.mkdir(os.path.join(self.tempdir, "texts"))
177
return self.tree.__iter__()
179
def __contains__(self, file_id):
180
return file_id in self.tree
182
def get_file(self, file_id):
183
return self.tree.get_file(file_id)
185
def get_file_sha1(self, id):
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)
199
def has_or_had_id(self, file_id):
200
if file_id == self.tree.inventory.root.file_id:
202
return self.tree.inventory.has_id(file_id)
204
148
def readonly_path(self, id):
205
149
if id not in self.tree:
222
166
def merge(other_revision, base_revision,
223
167
check_clean=True, ignore_zero=False,
224
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
168
this_dir=None, backup_files=False, merge_type=ApplyMerge3):
226
169
"""Merge changes into a tree.
229
tuple(path, revision) Base for three-way merge.
172
Base for three-way merge.
231
tuple(path, revision) Other revision for three-way merge.
174
Other revision for three-way merge.
233
176
Directory to merge changes into; '.' by default.
235
178
If true, this_dir must have no uncommitted changes before the
237
all available ancestors of other_revision and base_revision are
238
automatically pulled into the branch.
240
from bzrlib.revision import common_ancestor, MultipleRevisionSources
241
from bzrlib.errors import NoSuchRevision
242
181
tempdir = tempfile.mkdtemp(prefix="bzr-")
244
183
if this_dir is None:
246
185
this_branch = find_branch(this_dir)
247
this_rev_id = this_branch.last_patch()
248
if this_rev_id is None:
249
raise BzrCommandError("This branch has no commits")
251
187
changes = compare_trees(this_branch.working_tree(),
252
188
this_branch.basis_tree(), False)
253
189
if changes.has_changed():
254
190
raise BzrCommandError("Working tree has uncommitted changes.")
255
other_branch, other_tree = get_tree(other_revision, tempdir, "other",
257
if other_revision[1] == -1:
258
other_rev_id = other_branch.last_patch()
259
other_basis = other_rev_id
260
elif other_revision[1] is not None:
261
other_rev_id = other_branch.lookup_revision(other_revision[1])
262
other_basis = other_rev_id
265
other_basis = other_branch.last_patch()
191
other_branch, other_tree = get_tree(other_revision, tempdir, "other")
266
192
if base_revision == [None, None]:
267
base_rev_id = common_ancestor(this_rev_id, other_basis,
269
if base_rev_id is None:
193
if other_revision[1] == -1:
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:
270
200
raise UnrelatedBranches()
271
base_tree = get_revid_tree(this_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
204
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
and other_rev_id not in this_branch.revision_history():
309
this_branch.add_pending_merge(other_rev_id)
205
merge_type=merge_type)
311
207
shutil.rmtree(tempdir)
314
def set_interesting(inventory_a, inventory_b, interesting_ids):
315
"""Mark files whose ids are in interesting_ids as interesting
317
for inventory in (inventory_a, inventory_b):
318
for path, source_file in inventory.iteritems():
319
source_file.interesting = source_file.id in interesting_ids
322
def generate_cset_optimized(tree_a, tree_b, interesting_ids=None):
323
"""Generate a changeset. If interesting_ids is supplied, only changes
324
to those files will be shown. Metadata changes are stripped.
210
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b):
211
"""Generate a changeset, using the text_id to mark really-changed files.
212
This permits blazing comparisons when text_ids are present. It also
213
disables metadata comparison for files with identical texts.
326
cset = generate_changeset(tree_a, tree_b, interesting_ids)
215
for file_id in tree_a.tree.inventory:
216
if file_id not in tree_b.tree.inventory:
218
entry_a = tree_a.tree.inventory[file_id]
219
entry_b = tree_b.tree.inventory[file_id]
220
if (entry_a.kind, entry_b.kind) != ("file", "file"):
222
if None in (entry_a.text_id, entry_b.text_id):
224
if entry_a.text_id != entry_b.text_id:
226
inventory_a[abspath(tree_a.tree, file_id)].interesting = False
227
inventory_b[abspath(tree_b.tree, file_id)].interesting = False
228
cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
327
229
for entry in cset.entries.itervalues():
328
230
entry.metadata_change = None
332
234
def merge_inner(this_branch, other_tree, base_tree, tempdir,
333
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
334
interesting_ids=None):
235
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False):
336
def merge_factory(file_id, base, other):
337
contents_change = merge_type(file_id, base, other)
237
def merge_factory(base_file, other_file):
238
contents_change = merge_type(base_file, other_file)
339
240
contents_change = BackupBeforeChange(contents_change)
340
241
return contents_change