1
from merge_core import merge_flex
2
from changeset import generate_changeset, ExceptionConflictHandler
3
from changeset import Inventory
4
from bzrlib import Branch
6
from trace import mutter
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
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 Branch
30
from bzrlib.errors import BzrCommandError, UnrelatedBranches, NoCommonAncestor
31
from bzrlib.errors import NoCommits
32
from bzrlib.delta import compare_trees
33
from bzrlib.trace import mutter, warning
34
from bzrlib.fetch import greedy_fetch
35
from bzrlib.revision import is_ancestor
37
# comments from abentley on irc: merge happens in two stages, each
38
# of which generates a changeset object
40
# stage 1: generate OLD->OTHER,
41
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
12
43
class MergeConflictHandler(ExceptionConflictHandler):
13
"""Handle conflicts encountered while merging"""
44
"""Handle conflicts encountered while merging.
46
This subclasses ExceptionConflictHandler, so that any types of
47
conflict that are not explicitly handled cause an exception and
50
def __init__(self, ignore_zero=False):
51
ExceptionConflictHandler.__init__(self)
53
self.ignore_zero = ignore_zero
14
55
def copy(self, source, dest):
15
56
"""Copy the text and mode of a file
16
57
:param source: The path of the file to copy
50
106
:param other_path: Path to the file text for the OTHER tree
52
108
self.add_suffix(this_path, ".THIS")
53
self.copy(base_path, this_path+".BASE")
54
self.copy(other_path, this_path+".OTHER")
109
self.dump(base_lines, this_path+".BASE")
110
self.dump(other_lines, this_path+".OTHER")
55
111
os.rename(new_file, this_path)
112
self.conflict("Diff3 conflict encountered in %s" % this_path)
114
def new_contents_conflict(self, filename, other_contents):
115
"""Conflicting contents for newly added file."""
116
self.copy(other_contents, filename + ".OTHER")
117
self.conflict("Conflict in newly added file %s" % filename)
57
120
def target_exists(self, entry, target, old_path):
58
121
"""Handle the case when the target file or dir exists"""
59
self.add_suffix(target, ".moved")
122
moved_path = self.add_suffix(target, ".moved")
123
self.conflict("Moved existing %s to %s" % (target, moved_path))
125
def rmdir_non_empty(self, filename):
126
"""Handle the case where the dir to be removed still has contents"""
127
self.conflict("Directory %s not removed because it is not empty"\
132
if not self.ignore_zero:
133
print "%d conflicts encountered.\n" % self.conflicts
61
class SourceFile(object):
62
def __init__(self, path, id, present=None, isdir=None):
65
self.present = present
67
self.interesting = True
70
return "SourceFile(%s, %s)" % (self.path, self.id)
72
def get_tree(treespec, temp_root, label):
135
def get_tree(treespec, temp_root, label, local_branch=None):
136
location, revno = treespec
137
branch = Branch.open(location)
141
revision = branch.last_patch()
143
revision = branch.get_rev_id(revno)
144
return branch, get_revid_tree(branch, revision, temp_root, label,
147
def get_revid_tree(branch, revision, temp_root, label, local_branch):
76
149
base_tree = branch.working_tree()
78
base_tree = branch.basis_tree()
80
base_tree = branch.revision_tree(branch.lookup_revision(revno))
151
if local_branch is not None:
152
greedy_fetch(local_branch, branch, revision)
153
base_tree = local_branch.revision_tree(revision)
155
base_tree = branch.revision_tree(revision)
81
156
temp_path = os.path.join(temp_root, label)
82
157
os.mkdir(temp_path)
83
158
return MergeTree(base_tree, temp_path)
86
def abspath(tree, file_id):
87
path = tree.inventory.id2path(file_id)
92
161
def file_exists(tree, file_id):
93
162
return tree.has_filename(tree.id2path(file_id))
95
def inventory_map(tree):
97
for file_id in tree.inventory:
98
if not file_exists(tree, file_id):
100
path = abspath(tree, file_id)
101
inventory[path] = SourceFile(path, file_id)
105
165
class MergeTree(object):
106
166
def __init__(self, tree, tempdir):
109
169
self.root = tree.basedir
112
self.inventory = inventory_map(tree)
114
173
self.tempdir = tempdir
115
174
os.mkdir(os.path.join(self.tempdir, "texts"))
178
return self.tree.__iter__()
180
def __contains__(self, file_id):
181
return file_id in self.tree
183
def get_file(self, file_id):
184
return self.tree.get_file(file_id)
186
def get_file_sha1(self, id):
187
return self.tree.get_file_sha1(id)
189
def id2path(self, file_id):
190
return self.tree.id2path(file_id)
192
def has_id(self, file_id):
193
return self.tree.has_id(file_id)
195
def has_or_had_id(self, file_id):
196
if file_id == self.tree.inventory.root.file_id:
198
return self.tree.inventory.has_id(file_id)
200
def has_or_had_id(self, file_id):
201
if file_id == self.tree.inventory.root.file_id:
203
return self.tree.inventory.has_id(file_id)
118
205
def readonly_path(self, id):
206
if id not in self.tree:
119
208
if self.root is not None:
120
209
return self.tree.abspath(self.tree.id2path(id))
129
218
self.cached[id] = path
130
219
return self.cached[id]
132
def merge(other_revision, base_revision):
223
def merge(other_revision, base_revision,
224
check_clean=True, ignore_zero=False,
225
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
227
"""Merge changes into a tree.
230
tuple(path, revision) Base for three-way merge.
232
tuple(path, revision) Other revision for three-way merge.
234
Directory to merge changes into; '.' by default.
236
If true, this_dir must have no uncommitted changes before the
238
all available ancestors of other_revision and base_revision are
239
automatically pulled into the branch.
241
from bzrlib.revision import common_ancestor, MultipleRevisionSources
242
from bzrlib.errors import NoSuchRevision
133
243
tempdir = tempfile.mkdtemp(prefix="bzr-")
135
this_branch = Branch('.')
136
other_tree = get_tree(other_revision, tempdir, "other")
137
base_tree = get_tree(base_revision, tempdir, "base")
138
merge_inner(this_branch, other_tree, base_tree, tempdir)
247
this_branch = Branch.open_containing(this_dir)
248
this_rev_id = this_branch.last_patch()
249
if this_rev_id is None:
250
raise BzrCommandError("This branch has no commits")
252
changes = compare_trees(this_branch.working_tree(),
253
this_branch.basis_tree(), False)
254
if changes.has_changed():
255
raise BzrCommandError("Working tree has uncommitted changes.")
256
other_branch, other_tree = get_tree(other_revision, tempdir, "other",
258
if other_revision[1] == -1:
259
other_rev_id = other_branch.last_patch()
260
if other_rev_id is None:
261
raise NoCommits(other_branch)
262
other_basis = other_rev_id
263
elif other_revision[1] is not None:
264
other_rev_id = other_branch.get_rev_id(other_revision[1])
265
other_basis = other_rev_id
268
other_basis = other_branch.last_patch()
269
if other_basis is None:
270
raise NoCommits(other_branch)
271
if base_revision == [None, None]:
273
base_rev_id = common_ancestor(this_rev_id, other_basis,
275
except NoCommonAncestor:
276
raise UnrelatedBranches()
277
base_tree = get_revid_tree(this_branch, base_rev_id, tempdir,
279
base_is_ancestor = True
281
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
282
if base_revision[1] == -1:
283
base_rev_id = base_branch.last_patch()
284
elif base_revision[1] is None:
287
base_rev_id = base_branch.get_rev_id(base_revision[1])
288
multi_source = MultipleRevisionSources(this_branch, base_branch)
289
base_is_ancestor = is_ancestor(this_rev_id, base_rev_id,
291
if file_list is None:
292
interesting_ids = None
294
interesting_ids = set()
295
this_tree = this_branch.working_tree()
296
for fname in file_list:
297
path = this_branch.relpath(fname)
299
for tree in (this_tree, base_tree.tree, other_tree.tree):
300
file_id = tree.inventory.path2id(path)
301
if file_id is not None:
302
interesting_ids.add(file_id)
305
raise BzrCommandError("%s is not a source file in any"
307
merge_inner(this_branch, other_tree, base_tree, tempdir,
308
ignore_zero=ignore_zero, backup_files=backup_files,
309
merge_type=merge_type, interesting_ids=interesting_ids)
310
if base_is_ancestor and other_rev_id is not None\
311
and other_rev_id not in this_branch.revision_history():
312
this_branch.add_pending_merge(other_rev_id)
140
314
shutil.rmtree(tempdir)
143
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b):
144
"""Generate a changeset, using the text_id to mark really-changed files.
145
This permits blazing comparisons when text_ids are present. It also
146
disables metadata comparison for files with identical texts.
317
def set_interesting(inventory_a, inventory_b, interesting_ids):
318
"""Mark files whose ids are in interesting_ids as interesting
320
for inventory in (inventory_a, inventory_b):
321
for path, source_file in inventory.iteritems():
322
source_file.interesting = source_file.id in interesting_ids
325
def generate_cset_optimized(tree_a, tree_b, interesting_ids=None):
326
"""Generate a changeset. If interesting_ids is supplied, only changes
327
to those files will be shown. Metadata changes are stripped.
148
for file_id in tree_a.tree.inventory:
149
if file_id not in tree_b.tree.inventory:
151
entry_a = tree_a.tree.inventory[file_id]
152
entry_b = tree_b.tree.inventory[file_id]
153
if (entry_a.kind, entry_b.kind) != ("file", "file"):
155
if None in (entry_a.text_id, entry_b.text_id):
157
if entry_a.text_id != entry_b.text_id:
159
inventory_a[abspath(tree_a.tree, file_id)].interesting = False
160
inventory_b[abspath(tree_b.tree, file_id)].interesting = False
161
cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
329
cset = generate_changeset(tree_a, tree_b, interesting_ids)
162
330
for entry in cset.entries.itervalues():
163
331
entry.metadata_change = None
167
def merge_inner(this_branch, other_tree, base_tree, tempdir):
168
this_tree = get_tree(('.', None), tempdir, "this")
335
def merge_inner(this_branch, other_tree, base_tree, tempdir,
336
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
337
interesting_ids=None):
339
def merge_factory(file_id, base, other):
340
contents_change = merge_type(file_id, base, other)
342
contents_change = BackupBeforeChange(contents_change)
343
return contents_change
345
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
170
347
def get_inventory(tree):
171
return tree.inventory
348
return tree.tree.inventory
173
350
inv_changes = merge_flex(this_tree, base_tree, other_tree,
174
351
generate_cset_optimized, get_inventory,
175
MergeConflictHandler(base_tree.root))
352
MergeConflictHandler(ignore_zero=ignore_zero),
353
merge_factory=merge_factory,
354
interesting_ids=interesting_ids)
178
357
for id, path in inv_changes.iteritems():