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
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 Branch
29
from bzrlib.errors import BzrCommandError, UnrelatedBranches, NoCommonAncestor
30
from bzrlib.errors import NoCommits
31
from bzrlib.delta import compare_trees
32
from bzrlib.trace import mutter, warning
33
from bzrlib.fetch import greedy_fetch, fetch
34
from bzrlib.revision import is_ancestor
35
from bzrlib.osutils import rename
36
from bzrlib.revision import common_ancestor, MultipleRevisionSources
37
from bzrlib.errors import NoSuchRevision
40
# comments from abentley on irc: merge happens in two stages, each
41
# of which generates a changeset object
43
# stage 1: generate OLD->OTHER,
44
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
12
46
class MergeConflictHandler(ExceptionConflictHandler):
13
"""Handle conflicts encountered while merging"""
47
"""Handle conflicts encountered while merging.
49
This subclasses ExceptionConflictHandler, so that any types of
50
conflict that are not explicitly handled cause an exception and
53
def __init__(self, ignore_zero=False):
54
ExceptionConflictHandler.__init__(self)
56
self.ignore_zero = ignore_zero
14
58
def copy(self, source, dest):
15
59
"""Copy the text and mode of a file
16
60
:param source: The path of the file to copy
50
109
:param other_path: Path to the file text for the OTHER tree
52
111
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)
112
self.dump(base_lines, this_path+".BASE")
113
self.dump(other_lines, this_path+".OTHER")
114
rename(new_file, this_path)
115
self.conflict("Diff3 conflict encountered in %s" % this_path)
117
def new_contents_conflict(self, filename, other_contents):
118
"""Conflicting contents for newly added file."""
119
self.copy(other_contents, filename + ".OTHER")
120
self.conflict("Conflict in newly added file %s" % filename)
57
123
def target_exists(self, entry, target, old_path):
58
124
"""Handle the case when the target file or dir exists"""
59
self.add_suffix(target, ".moved")
125
moved_path = self.add_suffix(target, ".moved")
126
self.conflict("Moved existing %s to %s" % (target, moved_path))
128
def rmdir_non_empty(self, filename):
129
"""Handle the case where the dir to be removed still has contents"""
130
self.conflict("Directory %s not removed because it is not empty"\
135
if not self.ignore_zero:
136
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):
138
def get_tree(treespec, temp_root, label, local_branch=None):
139
location, revno = treespec
140
branch = Branch.open_containing(location)
144
revision = branch.last_revision()
146
revision = branch.get_rev_id(revno)
147
return branch, get_revid_tree(branch, revision, temp_root, label,
150
def get_revid_tree(branch, revision, temp_root, label, local_branch):
76
152
base_tree = branch.working_tree()
78
base_tree = branch.basis_tree()
80
base_tree = branch.revision_tree(branch.lookup_revision(revno))
154
if local_branch is not None:
155
greedy_fetch(local_branch, branch, revision)
156
base_tree = local_branch.revision_tree(revision)
158
base_tree = branch.revision_tree(revision)
81
159
temp_path = os.path.join(temp_root, label)
82
160
os.mkdir(temp_path)
83
161
return MergeTree(base_tree, temp_path)
86
def abspath(tree, file_id):
87
path = tree.inventory.id2path(file_id)
92
164
def file_exists(tree, file_id):
93
165
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
168
class MergeTree(object):
106
169
def __init__(self, tree, tempdir):
109
172
self.root = tree.basedir
112
self.inventory = inventory_map(tree)
114
176
self.tempdir = tempdir
115
177
os.mkdir(os.path.join(self.tempdir, "texts"))
181
return self.tree.__iter__()
183
def __contains__(self, file_id):
184
return file_id in self.tree
186
def get_file(self, file_id):
187
return self.tree.get_file(file_id)
189
def get_file_sha1(self, id):
190
return self.tree.get_file_sha1(id)
192
def id2path(self, file_id):
193
return self.tree.id2path(file_id)
195
def has_id(self, file_id):
196
return self.tree.has_id(file_id)
198
def has_or_had_id(self, file_id):
199
if file_id == self.tree.inventory.root.file_id:
201
return self.tree.inventory.has_id(file_id)
203
def has_or_had_id(self, file_id):
204
if file_id == self.tree.inventory.root.file_id:
206
return self.tree.inventory.has_id(file_id)
118
208
def readonly_path(self, id):
209
if id not in self.tree:
119
211
if self.root is not None:
120
212
return self.tree.abspath(self.tree.id2path(id))
129
221
self.cached[id] = path
130
222
return self.cached[id]
132
def merge(other_revision, base_revision):
226
def merge(other_revision, base_revision,
227
check_clean=True, ignore_zero=False,
228
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
230
"""Merge changes into a tree.
233
tuple(path, revision) Base for three-way merge.
235
tuple(path, revision) Other revision for three-way merge.
237
Directory to merge changes into; '.' by default.
239
If true, this_dir must have no uncommitted changes before the
242
All available ancestors of other_revision and base_revision are
243
automatically pulled into the branch.
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_revision()
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_revision()
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_revision()
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_revision()
286
elif base_revision[1] is None:
289
base_rev_id = base_branch.get_rev_id(base_revision[1])
290
fetch(from_branch=base_branch, to_branch=this_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():