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.delta import compare_trees
8
from trace import mutter, warning
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
14
class UnrelatedBranches(BzrCommandError):
16
msg = "Branches have no common ancestor, and no base revision"\
18
BzrCommandError.__init__(self, msg)
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 find_branch
29
from bzrlib.errors import BzrCommandError, UnrelatedBranches
30
from bzrlib.delta import compare_trees
31
from bzrlib.trace import mutter, warning
32
from bzrlib.fetch import greedy_fetch
33
from bzrlib.revision import is_ancestor
35
# comments from abentley on irc: merge happens in two stages, each
36
# of which generates a changeset object
38
# stage 1: generate OLD->OTHER,
39
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
21
41
class MergeConflictHandler(ExceptionConflictHandler):
22
"""Handle conflicts encountered while merging"""
42
"""Handle conflicts encountered while merging.
44
This subclasses ExceptionConflictHandler, so that any types of
45
conflict that are not explicitly handled cause an exception and
23
48
def __init__(self, dir, ignore_zero=False):
24
49
ExceptionConflictHandler.__init__(self, dir)
37
62
os.chmod(dest, 0777 & os.stat(source).st_mode)
64
def dump(self, lines, dest):
65
"""Copy the text and mode of a file
66
:param source: The path of the file to copy
67
:param dest: The distination file to create
69
d_file = file(dest, "wb")
39
73
def add_suffix(self, name, suffix, last_new_name=None):
40
74
"""Rename a file to append a suffix. If the new name exists, the
41
75
suffix is added repeatedly until a non-existant name is found
60
94
self.conflicts += 1
63
def merge_conflict(self, new_file, this_path, base_path, other_path):
97
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
65
99
Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The
66
100
main file will be a version with diff3 conflicts.
70
104
:param other_path: Path to the file text for the OTHER tree
72
106
self.add_suffix(this_path, ".THIS")
73
self.copy(base_path, this_path+".BASE")
74
self.copy(other_path, this_path+".OTHER")
107
self.dump(base_lines, this_path+".BASE")
108
self.dump(other_lines, this_path+".OTHER")
75
109
os.rename(new_file, this_path)
76
110
self.conflict("Diff3 conflict encountered in %s" % this_path)
112
def new_contents_conflict(self, filename, other_contents):
113
"""Conflicting contents for newly added file."""
114
self.copy(other_contents, filename + ".OTHER")
115
self.conflict("Conflict in newly added file %s" % filename)
78
118
def target_exists(self, entry, target, old_path):
79
119
"""Handle the case when the target file or dir exists"""
80
120
moved_path = self.add_suffix(target, ".moved")
90
130
if not self.ignore_zero:
91
131
print "%d conflicts encountered.\n" % self.conflicts
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):
133
def get_tree(treespec, temp_root, label, local_branch=None):
105
134
location, revno = treespec
106
135
branch = find_branch(location)
107
136
if revno is None:
139
revision = branch.last_patch()
141
revision = branch.lookup_revision(revno)
142
return branch, get_revid_tree(branch, revision, temp_root, label,
145
def get_revid_tree(branch, revision, temp_root, label, local_branch):
108
147
base_tree = branch.working_tree()
110
base_tree = branch.basis_tree()
112
base_tree = branch.revision_tree(branch.lookup_revision(revno))
149
if local_branch is not None:
150
greedy_fetch(local_branch, branch, revision)
151
base_tree = local_branch.revision_tree(revision)
153
base_tree = branch.revision_tree(revision)
113
154
temp_path = os.path.join(temp_root, label)
114
155
os.mkdir(temp_path)
115
return branch, MergeTree(base_tree, temp_path)
118
def abspath(tree, file_id):
119
path = tree.inventory.id2path(file_id)
156
return MergeTree(base_tree, temp_path)
124
159
def file_exists(tree, file_id):
125
160
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)
135
163
class MergeTree(object):
136
164
def __init__(self, tree, tempdir):
139
167
self.root = tree.basedir
142
self.inventory = inventory_map(tree)
144
171
self.tempdir = tempdir
145
172
os.mkdir(os.path.join(self.tempdir, "texts"))
176
return self.tree.__iter__()
178
def __contains__(self, file_id):
179
return file_id in self.tree
181
def get_file(self, file_id):
182
return self.tree.get_file(file_id)
184
def get_file_sha1(self, id):
185
return self.tree.get_file_sha1(id)
187
def id2path(self, file_id):
188
return self.tree.id2path(file_id)
190
def has_id(self, file_id):
191
return self.tree.has_id(file_id)
193
def has_or_had_id(self, file_id):
194
if file_id == self.tree.inventory.root.file_id:
196
return self.tree.inventory.has_id(file_id)
148
198
def readonly_path(self, id):
149
199
if id not in self.tree:
179
229
If true, this_dir must have no uncommitted changes before the
231
all available ancestors of other_revision and base_revision are
232
automatically pulled into the branch.
234
from bzrlib.revision import common_ancestor, MultipleRevisionSources
235
from bzrlib.errors import NoSuchRevision
182
236
tempdir = tempfile.mkdtemp(prefix="bzr-")
184
238
if this_dir is None:
186
240
this_branch = find_branch(this_dir)
241
this_rev_id = this_branch.last_patch()
242
if this_rev_id is None:
243
raise BzrCommandError("This branch has no commits")
188
245
changes = compare_trees(this_branch.working_tree(),
189
246
this_branch.basis_tree(), False)
190
247
if changes.has_changed():
191
248
raise BzrCommandError("Working tree has uncommitted changes.")
192
other_branch, other_tree = get_tree(other_revision, tempdir, "other")
249
other_branch, other_tree = get_tree(other_revision, tempdir, "other",
251
if other_revision[1] == -1:
252
other_rev_id = other_branch.last_patch()
253
other_basis = other_rev_id
254
elif other_revision[1] is not None:
255
other_rev_id = other_branch.lookup_revision(other_revision[1])
256
other_basis = other_rev_id
259
other_basis = other_branch.last_patch()
193
260
if base_revision == [None, None]:
194
261
if other_revision[1] == -1:
197
264
o_revno = other_revision[1]
198
base_revno = this_branch.common_ancestor(other_branch,
199
other_revno=o_revno)[0]
200
if base_revno is None:
201
265
raise UnrelatedBranches()
202
base_revision = ['.', base_revno]
203
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
267
base_revision = this_branch.get_revision(base_rev_id)
268
base_branch = this_branch
269
except NoSuchRevision:
270
base_branch = other_branch
271
base_tree = get_revid_tree(base_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
204
288
if file_list is None:
205
289
interesting_ids = None
220
304
merge_inner(this_branch, other_tree, base_tree, tempdir,
221
305
ignore_zero=ignore_zero, backup_files=backup_files,
222
306
merge_type=merge_type, interesting_ids=interesting_ids)
307
if base_is_ancestor and other_rev_id is not None:
308
this_branch.add_pending_merge(other_rev_id)
224
310
shutil.rmtree(tempdir)
232
318
source_file.interesting = source_file.id in interesting_ids
235
def set_optimized(tree_a, tree_b, inventory_a, inventory_b):
236
"""Mark files that have changed texts as interesting
238
for file_id in tree_a.tree.inventory:
239
if file_id not in tree_b.tree.inventory:
241
entry_a = tree_a.tree.inventory[file_id]
242
entry_b = tree_b.tree.inventory[file_id]
243
if (entry_a.kind, entry_b.kind) != ("file", "file"):
245
if None in (entry_a.text_id, entry_b.text_id):
247
if entry_a.text_id != entry_b.text_id:
249
inventory_a[abspath(tree_a.tree, file_id)].interesting = False
250
inventory_b[abspath(tree_b.tree, file_id)].interesting = False
253
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
254
interesting_ids=None):
255
"""Generate a changeset, with preprocessing to select interesting files.
256
using the text_id to mark really-changed files.
257
This permits blazing comparisons when text_ids are present. It also
258
disables metadata comparison for files with identical texts.
321
def generate_cset_optimized(tree_a, tree_b, interesting_ids=None):
322
"""Generate a changeset. If interesting_ids is supplied, only changes
323
to those files will be shown. Metadata changes are stripped.
260
if interesting_ids is None:
261
set_optimized(tree_a, tree_b, inventory_a, inventory_b)
263
set_interesting(inventory_a, inventory_b, interesting_ids)
264
cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
325
cset = generate_changeset(tree_a, tree_b, interesting_ids)
265
326
for entry in cset.entries.itervalues():
266
327
entry.metadata_change = None
271
332
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
272
333
interesting_ids=None):
274
def merge_factory(base_file, other_file):
275
contents_change = merge_type(base_file, other_file)
335
def merge_factory(file_id, base, other):
336
contents_change = merge_type(file_id, base, other)
277
338
contents_change = BackupBeforeChange(contents_change)
278
339
return contents_change
280
def generate_cset(tree_a, tree_b, inventory_a, inventory_b):
281
return generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
284
341
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
286
343
def get_inventory(tree):
287
return tree.inventory
344
return tree.tree.inventory
289
346
inv_changes = merge_flex(this_tree, base_tree, other_tree,
290
generate_cset, get_inventory,
347
generate_cset_optimized, get_inventory,
291
348
MergeConflictHandler(base_tree.root,
292
349
ignore_zero=ignore_zero),
293
merge_factory=merge_factory)
350
merge_factory=merge_factory,
351
interesting_ids=interesting_ids)
296
354
for id, path in inv_changes.iteritems():
301
assert path.startswith('./')
359
assert path.startswith('./'), "path is %s" % path
303
361
adjust_ids.append((path, id))
304
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root, adjust_ids))
362
if len(adjust_ids) > 0:
363
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root,
307
367
def regen_inventory(this_branch, root, new_entries):
308
368
old_entries = this_branch.read_working_inventory()
309
369
new_inventory = {}
372
for path, file_id in new_entries:
375
new_entries_map[file_id] = path
377
def id2path(file_id):
378
path = new_entries_map.get(file_id)
381
entry = old_entries[file_id]
382
if entry.parent_id is None:
384
return os.path.join(id2path(entry.parent_id), entry.name)
311
386
for file_id in old_entries:
312
387
entry = old_entries[file_id]
313
path = old_entries.id2path(file_id)
388
path = id2path(file_id)
314
389
new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
315
390
by_path[path] = file_id