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
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)
105
70
:param other_path: Path to the file text for the OTHER tree
107
72
self.add_suffix(this_path, ".THIS")
108
self.dump(base_lines, this_path+".BASE")
109
self.dump(other_lines, this_path+".OTHER")
73
self.copy(base_path, this_path+".BASE")
74
self.copy(other_path, this_path+".OTHER")
110
75
os.rename(new_file, this_path)
111
76
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)
119
78
def target_exists(self, entry, target, old_path):
120
79
"""Handle the case when the target file or dir exists"""
121
80
moved_path = self.add_suffix(target, ".moved")
122
81
self.conflict("Moved existing %s to %s" % (target, moved_path))
124
def rmdir_non_empty(self, filename):
125
"""Handle the case where the dir to be removed still has contents"""
126
self.conflict("Directory %s not removed because it is not empty"\
130
83
def finalize(self):
131
84
if not self.ignore_zero:
132
85
print "%d conflicts encountered.\n" % self.conflicts
134
def get_tree(treespec, temp_root, label, local_branch=None):
87
class SourceFile(object):
88
def __init__(self, path, id, present=None, isdir=None):
91
self.present = present
93
self.interesting = True
96
return "SourceFile(%s, %s)" % (self.path, self.id)
98
def get_tree(treespec, temp_root, label):
135
99
location, revno = treespec
136
100
branch = find_branch(location)
137
101
if revno is None:
102
base_tree = branch.working_tree()
139
103
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)
104
base_tree = branch.basis_tree()
106
base_tree = branch.revision_tree(branch.lookup_revision(revno))
155
107
temp_path = os.path.join(temp_root, label)
156
108
os.mkdir(temp_path)
157
return MergeTree(base_tree, temp_path)
109
return branch, MergeTree(base_tree, temp_path)
112
def abspath(tree, file_id):
113
path = tree.inventory.id2path(file_id)
160
118
def file_exists(tree, file_id):
161
119
return tree.has_filename(tree.id2path(file_id))
121
def inventory_map(tree):
123
for file_id in tree.inventory:
124
if not file_exists(tree, file_id):
126
path = abspath(tree, file_id)
127
inventory[path] = SourceFile(path, file_id)
164
131
class MergeTree(object):
165
132
def __init__(self, tree, tempdir):
217
160
def merge(other_revision, base_revision,
218
161
check_clean=True, ignore_zero=False,
219
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
221
163
"""Merge changes into a tree.
224
tuple(path, revision) Base for three-way merge.
166
Base for three-way merge.
226
tuple(path, revision) Other revision for three-way merge.
168
Other revision for three-way merge.
228
170
Directory to merge changes into; '.' by default.
230
172
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
237
175
tempdir = tempfile.mkdtemp(prefix="bzr-")
239
177
if this_dir is None:
241
179
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")
246
181
changes = compare_trees(this_branch.working_tree(),
247
182
this_branch.basis_tree(), False)
248
183
if changes.has_changed():
249
184
raise BzrCommandError("Working tree has uncommitted changes.")
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()
185
other_branch, other_tree = get_tree(other_revision, tempdir, "other")
261
186
if base_revision == [None, None]:
262
base_rev_id = common_ancestor(this_rev_id, other_basis,
264
if base_rev_id is None:
187
if other_revision[1] == -1:
190
o_revno = other_revision[1]
191
base_revno = this_branch.common_ancestor(other_branch,
192
other_revno=o_revno)[0]
193
if base_revno is None:
265
194
raise UnrelatedBranches()
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
283
if file_list is None:
284
interesting_ids = None
286
interesting_ids = set()
287
this_tree = this_branch.working_tree()
288
for fname in file_list:
289
path = this_branch.relpath(fname)
291
for tree in (this_tree, base_tree.tree, other_tree.tree):
292
file_id = tree.inventory.path2id(path)
293
if file_id is not None:
294
interesting_ids.add(file_id)
297
raise BzrCommandError("%s is not a source file in any"
195
base_revision = ['.', base_revno]
196
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
299
197
merge_inner(this_branch, other_tree, base_tree, tempdir,
300
ignore_zero=ignore_zero, backup_files=backup_files,
301
merge_type=merge_type, interesting_ids=interesting_ids)
302
if base_is_ancestor and other_rev_id is not None:
303
this_branch.add_pending_merge(other_rev_id)
198
ignore_zero=ignore_zero)
305
200
shutil.rmtree(tempdir)
308
def set_interesting(inventory_a, inventory_b, interesting_ids):
309
"""Mark files whose ids are in interesting_ids as interesting
311
for inventory in (inventory_a, inventory_b):
312
for path, source_file in inventory.iteritems():
313
source_file.interesting = source_file.id in interesting_ids
316
def generate_cset_optimized(tree_a, tree_b, interesting_ids=None):
317
"""Generate a changeset. If interesting_ids is supplied, only changes
318
to those files will be shown. Metadata changes are stripped.
203
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b):
204
"""Generate a changeset, using the text_id to mark really-changed files.
205
This permits blazing comparisons when text_ids are present. It also
206
disables metadata comparison for files with identical texts.
320
cset = generate_changeset(tree_a, tree_b, interesting_ids)
208
for file_id in tree_a.tree.inventory:
209
if file_id not in tree_b.tree.inventory:
211
entry_a = tree_a.tree.inventory[file_id]
212
entry_b = tree_b.tree.inventory[file_id]
213
if (entry_a.kind, entry_b.kind) != ("file", "file"):
215
if None in (entry_a.text_id, entry_b.text_id):
217
if entry_a.text_id != entry_b.text_id:
219
inventory_a[abspath(tree_a.tree, file_id)].interesting = False
220
inventory_b[abspath(tree_b.tree, file_id)].interesting = False
221
cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
321
222
for entry in cset.entries.itervalues():
322
223
entry.metadata_change = None
326
227
def merge_inner(this_branch, other_tree, base_tree, tempdir,
327
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
328
interesting_ids=None):
330
def merge_factory(file_id, base, other):
331
contents_change = merge_type(file_id, base, other)
333
contents_change = BackupBeforeChange(contents_change)
334
return contents_change
336
229
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
338
231
def get_inventory(tree):
339
return tree.tree.inventory
232
return tree.inventory
341
234
inv_changes = merge_flex(this_tree, base_tree, other_tree,
342
235
generate_cset_optimized, get_inventory,
343
236
MergeConflictHandler(base_tree.root,
344
ignore_zero=ignore_zero),
345
merge_factory=merge_factory,
346
interesting_ids=interesting_ids)
237
ignore_zero=ignore_zero))
349
240
for id, path in inv_changes.iteritems():