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
36
from bzrlib.osutils import rename
39
# comments from abentley on irc: merge happens in two stages, each
40
# of which generates a changeset object
42
# stage 1: generate OLD->OTHER,
43
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
12
45
class MergeConflictHandler(ExceptionConflictHandler):
13
"""Handle conflicts encountered while merging"""
46
"""Handle conflicts encountered while merging.
48
This subclasses ExceptionConflictHandler, so that any types of
49
conflict that are not explicitly handled cause an exception and
52
def __init__(self, ignore_zero=False):
53
ExceptionConflictHandler.__init__(self)
55
self.ignore_zero = ignore_zero
14
57
def copy(self, source, dest):
15
58
"""Copy the text and mode of a file
16
59
:param source: The path of the file to copy
50
108
:param other_path: Path to the file text for the OTHER tree
52
110
self.add_suffix(this_path, ".THIS")
53
self.copy(base_path, this_path+".BASE")
54
self.copy(other_path, this_path+".OTHER")
55
os.rename(new_file, this_path)
111
self.dump(base_lines, this_path+".BASE")
112
self.dump(other_lines, this_path+".OTHER")
113
rename(new_file, this_path)
114
self.conflict("Diff3 conflict encountered in %s" % this_path)
116
def new_contents_conflict(self, filename, other_contents):
117
"""Conflicting contents for newly added file."""
118
self.copy(other_contents, filename + ".OTHER")
119
self.conflict("Conflict in newly added file %s" % filename)
57
122
def target_exists(self, entry, target, old_path):
58
123
"""Handle the case when the target file or dir exists"""
59
self.add_suffix(target, ".moved")
124
moved_path = self.add_suffix(target, ".moved")
125
self.conflict("Moved existing %s to %s" % (target, moved_path))
127
def rmdir_non_empty(self, filename):
128
"""Handle the case where the dir to be removed still has contents"""
129
self.conflict("Directory %s not removed because it is not empty"\
134
if not self.ignore_zero:
135
print "%d conflicts encountered.\n" % self.conflicts
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):
137
def get_tree(treespec, temp_root, label, local_branch=None):
138
location, revno = treespec
139
branch = Branch.open_containing(location)
143
revision = branch.last_patch()
145
revision = branch.get_rev_id(revno)
146
return branch, get_revid_tree(branch, revision, temp_root, label,
149
def get_revid_tree(branch, revision, temp_root, label, local_branch):
76
151
base_tree = branch.working_tree()
78
base_tree = branch.basis_tree()
80
base_tree = branch.revision_tree(branch.lookup_revision(revno))
153
if local_branch is not None:
154
greedy_fetch(local_branch, branch, revision)
155
base_tree = local_branch.revision_tree(revision)
157
base_tree = branch.revision_tree(revision)
81
158
temp_path = os.path.join(temp_root, label)
82
159
os.mkdir(temp_path)
83
160
return MergeTree(base_tree, temp_path)
86
def abspath(tree, file_id):
87
path = tree.inventory.id2path(file_id)
92
163
def file_exists(tree, file_id):
93
164
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
167
class MergeTree(object):
106
168
def __init__(self, tree, tempdir):
109
171
self.root = tree.basedir
112
self.inventory = inventory_map(tree)
114
175
self.tempdir = tempdir
115
176
os.mkdir(os.path.join(self.tempdir, "texts"))
180
return self.tree.__iter__()
182
def __contains__(self, file_id):
183
return file_id in self.tree
185
def get_file(self, file_id):
186
return self.tree.get_file(file_id)
188
def get_file_sha1(self, id):
189
return self.tree.get_file_sha1(id)
191
def id2path(self, file_id):
192
return self.tree.id2path(file_id)
194
def has_id(self, file_id):
195
return self.tree.has_id(file_id)
197
def has_or_had_id(self, file_id):
198
if file_id == self.tree.inventory.root.file_id:
200
return self.tree.inventory.has_id(file_id)
202
def has_or_had_id(self, file_id):
203
if file_id == self.tree.inventory.root.file_id:
205
return self.tree.inventory.has_id(file_id)
118
207
def readonly_path(self, id):
208
if id not in self.tree:
119
210
if self.root is not None:
120
211
return self.tree.abspath(self.tree.id2path(id))
129
220
self.cached[id] = path
130
221
return self.cached[id]
132
def merge(other_revision, base_revision):
225
def merge(other_revision, base_revision,
226
check_clean=True, ignore_zero=False,
227
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
229
"""Merge changes into a tree.
232
tuple(path, revision) Base for three-way merge.
234
tuple(path, revision) Other revision for three-way merge.
236
Directory to merge changes into; '.' by default.
238
If true, this_dir must have no uncommitted changes before the
240
all available ancestors of other_revision and base_revision are
241
automatically pulled into the branch.
243
from bzrlib.revision import common_ancestor, MultipleRevisionSources
244
from bzrlib.errors import NoSuchRevision
133
245
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)
249
this_branch = Branch.open_containing(this_dir)
250
this_rev_id = this_branch.last_patch()
251
if this_rev_id is None:
252
raise BzrCommandError("This branch has no commits")
254
changes = compare_trees(this_branch.working_tree(),
255
this_branch.basis_tree(), False)
256
if changes.has_changed():
257
raise BzrCommandError("Working tree has uncommitted changes.")
258
other_branch, other_tree = get_tree(other_revision, tempdir, "other",
260
if other_revision[1] == -1:
261
other_rev_id = other_branch.last_patch()
262
if other_rev_id is None:
263
raise NoCommits(other_branch)
264
other_basis = other_rev_id
265
elif other_revision[1] is not None:
266
other_rev_id = other_branch.get_rev_id(other_revision[1])
267
other_basis = other_rev_id
270
other_basis = other_branch.last_patch()
271
if other_basis is None:
272
raise NoCommits(other_branch)
273
if base_revision == [None, None]:
275
base_rev_id = common_ancestor(this_rev_id, other_basis,
277
except NoCommonAncestor:
278
raise UnrelatedBranches()
279
base_tree = get_revid_tree(this_branch, base_rev_id, tempdir,
281
base_is_ancestor = True
283
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
284
if base_revision[1] == -1:
285
base_rev_id = base_branch.last_patch()
286
elif base_revision[1] is None:
289
base_rev_id = base_branch.get_rev_id(base_revision[1])
290
multi_source = MultipleRevisionSources(this_branch, base_branch)
291
base_is_ancestor = is_ancestor(this_rev_id, base_rev_id,
293
if file_list is None:
294
interesting_ids = None
296
interesting_ids = set()
297
this_tree = this_branch.working_tree()
298
for fname in file_list:
299
path = this_branch.relpath(fname)
301
for tree in (this_tree, base_tree.tree, other_tree.tree):
302
file_id = tree.inventory.path2id(path)
303
if file_id is not None:
304
interesting_ids.add(file_id)
307
raise BzrCommandError("%s is not a source file in any"
309
merge_inner(this_branch, other_tree, base_tree, tempdir,
310
ignore_zero=ignore_zero, backup_files=backup_files,
311
merge_type=merge_type, interesting_ids=interesting_ids)
312
if base_is_ancestor and other_rev_id is not None\
313
and other_rev_id not in this_branch.revision_history():
314
this_branch.add_pending_merge(other_rev_id)
140
316
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.
319
def set_interesting(inventory_a, inventory_b, interesting_ids):
320
"""Mark files whose ids are in interesting_ids as interesting
322
for inventory in (inventory_a, inventory_b):
323
for path, source_file in inventory.iteritems():
324
source_file.interesting = source_file.id in interesting_ids
327
def generate_cset_optimized(tree_a, tree_b, interesting_ids=None):
328
"""Generate a changeset. If interesting_ids is supplied, only changes
329
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)
331
cset = generate_changeset(tree_a, tree_b, interesting_ids)
162
332
for entry in cset.entries.itervalues():
163
333
entry.metadata_change = None
167
def merge_inner(this_branch, other_tree, base_tree, tempdir):
168
this_tree = get_tree(('.', None), tempdir, "this")
337
def merge_inner(this_branch, other_tree, base_tree, tempdir,
338
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
339
interesting_ids=None):
341
def merge_factory(file_id, base, other):
342
contents_change = merge_type(file_id, base, other)
344
contents_change = BackupBeforeChange(contents_change)
345
return contents_change
347
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
170
349
def get_inventory(tree):
171
return tree.inventory
350
return tree.tree.inventory
173
352
inv_changes = merge_flex(this_tree, base_tree, other_tree,
174
353
generate_cset_optimized, get_inventory,
175
MergeConflictHandler(base_tree.root))
354
MergeConflictHandler(ignore_zero=ignore_zero),
355
merge_factory=merge_factory,
356
interesting_ids=interesting_ids)
178
359
for id, path in inv_changes.iteritems():