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, 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
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, 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
14
class UnrelatedBranches(BzrCommandError):
16
msg = "Branches have no common ancestor, and no base revision"\
18
BzrCommandError.__init__(self, msg)
43
21
class MergeConflictHandler(ExceptionConflictHandler):
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
22
"""Handle conflicts encountered while merging"""
50
23
def __init__(self, dir, ignore_zero=False):
51
24
ExceptionConflictHandler.__init__(self, dir)
64
37
os.chmod(dest, 0777 & os.stat(source).st_mode)
66
def dump(self, lines, dest):
67
"""Copy the text and mode of a file
68
:param source: The path of the file to copy
69
:param dest: The distination file to create
71
d_file = file(dest, "wb")
75
39
def add_suffix(self, name, suffix, last_new_name=None):
76
40
"""Rename a file to append a suffix. If the new name exists, the
77
41
suffix is added repeatedly until a non-existant name is found
106
70
:param other_path: Path to the file text for the OTHER tree
108
72
self.add_suffix(this_path, ".THIS")
109
self.dump(base_lines, this_path+".BASE")
110
self.dump(other_lines, this_path+".OTHER")
73
self.copy(base_path, this_path+".BASE")
74
self.copy(other_path, this_path+".OTHER")
111
75
os.rename(new_file, this_path)
112
76
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)
120
78
def target_exists(self, entry, target, old_path):
121
79
"""Handle the case when the target file or dir exists"""
122
80
moved_path = self.add_suffix(target, ".moved")
132
90
if not self.ignore_zero:
133
91
print "%d conflicts encountered.\n" % self.conflicts
135
def get_tree(treespec, temp_root, label, local_branch=None):
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):
136
105
location, revno = treespec
137
106
branch = find_branch(location)
138
107
if revno is None:
108
base_tree = branch.working_tree()
140
109
elif revno == -1:
141
revision = branch.last_patch()
143
revision = branch.lookup_revision(revno)
144
return branch, get_revid_tree(branch, revision, temp_root, label,
147
def get_revid_tree(branch, revision, temp_root, label, local_branch):
149
base_tree = branch.working_tree()
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)
110
base_tree = branch.basis_tree()
112
base_tree = branch.revision_tree(branch.lookup_revision(revno))
156
113
temp_path = os.path.join(temp_root, label)
157
114
os.mkdir(temp_path)
158
return MergeTree(base_tree, temp_path)
115
return branch, MergeTree(base_tree, temp_path)
118
def abspath(tree, file_id):
119
path = tree.inventory.id2path(file_id)
161
124
def file_exists(tree, file_id):
162
125
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)
165
135
class MergeTree(object):
166
136
def __init__(self, tree, tempdir):
169
139
self.root = tree.basedir
142
self.inventory = inventory_map(tree)
173
144
self.tempdir = tempdir
174
145
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)
205
148
def readonly_path(self, id):
206
149
if id not in self.tree:
227
170
"""Merge changes into a tree.
230
tuple(path, revision) Base for three-way merge.
173
Base for three-way merge.
232
tuple(path, revision) Other revision for three-way merge.
175
Other revision for three-way merge.
234
177
Directory to merge changes into; '.' by default.
236
179
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
243
182
tempdir = tempfile.mkdtemp(prefix="bzr-")
245
184
if this_dir is None:
247
186
this_branch = find_branch(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
188
changes = compare_trees(this_branch.working_tree(),
253
189
this_branch.basis_tree(), False)
254
190
if changes.has_changed():
255
191
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.lookup_revision(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)
192
other_branch, other_tree = get_tree(other_revision, tempdir, "other")
271
193
if base_revision == [None, None]:
273
base_rev_id = common_ancestor(this_rev_id, other_basis,
275
except NoCommonAncestor:
194
if other_revision[1] == -1:
197
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:
276
201
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.lookup_revision(base_revision[1])
288
if base_rev_id is not None:
289
base_is_ancestor = is_ancestor(this_rev_id, base_rev_id,
290
MultipleRevisionSources(this_branch,
293
base_is_ancestor = False
202
base_revision = ['.', base_revno]
203
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
294
204
if file_list is None:
295
205
interesting_ids = None
310
220
merge_inner(this_branch, other_tree, base_tree, tempdir,
311
221
ignore_zero=ignore_zero, backup_files=backup_files,
312
222
merge_type=merge_type, interesting_ids=interesting_ids)
313
if base_is_ancestor and other_rev_id is not None\
314
and other_rev_id not in this_branch.revision_history():
315
this_branch.add_pending_merge(other_rev_id)
317
224
shutil.rmtree(tempdir)
325
232
source_file.interesting = source_file.id in interesting_ids
328
def generate_cset_optimized(tree_a, tree_b, interesting_ids=None):
329
"""Generate a changeset. If interesting_ids is supplied, only changes
330
to those files will be shown. Metadata changes are stripped.
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.
332
cset = generate_changeset(tree_a, tree_b, interesting_ids)
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)
333
265
for entry in cset.entries.itervalues():
334
266
entry.metadata_change = None
339
271
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
340
272
interesting_ids=None):
342
def merge_factory(file_id, base, other):
343
contents_change = merge_type(file_id, base, other)
274
def merge_factory(base_file, other_file):
275
contents_change = merge_type(base_file, other_file)
345
277
contents_change = BackupBeforeChange(contents_change)
346
278
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,
348
284
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
350
286
def get_inventory(tree):
351
return tree.tree.inventory
287
return tree.inventory
353
289
inv_changes = merge_flex(this_tree, base_tree, other_tree,
354
generate_cset_optimized, get_inventory,
290
generate_cset, get_inventory,
355
291
MergeConflictHandler(base_tree.root,
356
292
ignore_zero=ignore_zero),
357
merge_factory=merge_factory,
358
interesting_ids=interesting_ids)
293
merge_factory=merge_factory)
361
296
for id, path in inv_changes.iteritems():
366
assert path.startswith('.' + os.sep), "path is %s" % path
301
assert path.startswith('./')
368
303
adjust_ids.append((path, id))
369
if len(adjust_ids) > 0:
370
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root,
304
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root, adjust_ids))
374
307
def regen_inventory(this_branch, root, new_entries):
375
308
old_entries = this_branch.read_working_inventory()
376
309
new_inventory = {}
379
for path, file_id in new_entries:
382
new_entries_map[file_id] = path
384
def id2path(file_id):
385
path = new_entries_map.get(file_id)
388
entry = old_entries[file_id]
389
if entry.parent_id is None:
391
return os.path.join(id2path(entry.parent_id), entry.name)
393
311
for file_id in old_entries:
394
312
entry = old_entries[file_id]
395
path = id2path(file_id)
313
path = old_entries.id2path(file_id)
396
314
new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
397
315
by_path[path] = file_id