1
from merge_core import merge_flex
2
from changeset import generate_changeset, ExceptionConflictHandler
3
from changeset import Inventory
4
from bzrlib import find_branch
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
18
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
19
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
20
from bzrlib.changeset import Inventory, Diff3Merge
21
from bzrlib.branch import find_branch
5
22
import bzrlib.osutils
6
from bzrlib.errors import BzrCommandError
7
from bzrlib.diff import compare_trees
23
from bzrlib.errors import BzrCommandError, UnrelatedBranches
24
from bzrlib.delta import compare_trees
8
25
from trace import mutter, warning
14
class UnrelatedBranches(BzrCommandError):
16
msg = "Branches have no common ancestor, and no base revision"\
18
BzrCommandError.__init__(self, msg)
30
from fetch import greedy_fetch
33
# comments from abentley on irc: merge happens in two stages, each
34
# of which generates a changeset object
36
# stage 1: generate OLD->OTHER,
37
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
21
39
class MergeConflictHandler(ExceptionConflictHandler):
22
"""Handle conflicts encountered while merging"""
40
"""Handle conflicts encountered while merging.
42
This subclasses ExceptionConflictHandler, so that any types of
43
conflict that are not explicitly handled cause an exception and
23
46
def __init__(self, dir, ignore_zero=False):
24
47
ExceptionConflictHandler.__init__(self, dir)
70
102
:param other_path: Path to the file text for the OTHER tree
72
104
self.add_suffix(this_path, ".THIS")
73
self.copy(base_path, this_path+".BASE")
74
self.copy(other_path, this_path+".OTHER")
105
self.dump(base_lines, this_path+".BASE")
106
self.dump(other_lines, this_path+".OTHER")
75
107
os.rename(new_file, this_path)
76
108
self.conflict("Diff3 conflict encountered in %s" % this_path)
110
def new_contents_conflict(self, filename, other_contents):
111
"""Conflicting contents for newly added file."""
112
self.copy(other_contents, filename + ".OTHER")
113
self.conflict("Conflict in newly added file %s" % filename)
78
116
def target_exists(self, entry, target, old_path):
79
117
"""Handle the case when the target file or dir exists"""
80
118
moved_path = self.add_suffix(target, ".moved")
81
119
self.conflict("Moved existing %s to %s" % (target, moved_path))
121
def rmdir_non_empty(self, filename):
122
"""Handle the case where the dir to be removed still has contents"""
123
self.conflict("Directory %s not removed because it is not empty"\
83
127
def finalize(self):
84
128
if not self.ignore_zero:
85
129
print "%d conflicts encountered.\n" % self.conflicts
87
class SourceFile(object):
88
def __init__(self, path, id, present=None, isdir=None):
91
self.present = present
93
self.interesting = True
96
return "SourceFile(%s, %s)" % (self.path, self.id)
98
def get_tree(treespec, temp_root, label):
131
def get_tree(treespec, temp_root, label, local_branch=None):
99
132
location, revno = treespec
100
133
branch = find_branch(location)
101
134
if revno is None:
137
revision = branch.last_patch()
139
revision = branch.lookup_revision(revno)
140
return branch, get_revid_tree(branch, revision, temp_root, label,
143
def get_revid_tree(branch, revision, temp_root, label, local_branch):
102
145
base_tree = branch.working_tree()
104
base_tree = branch.basis_tree()
106
base_tree = branch.revision_tree(branch.lookup_revision(revno))
147
if local_branch is not None:
148
greedy_fetch(local_branch, branch, revision)
149
base_tree = local_branch.revision_tree(revision)
151
base_tree = branch.revision_tree(revision)
107
152
temp_path = os.path.join(temp_root, label)
108
153
os.mkdir(temp_path)
109
return branch, MergeTree(base_tree, temp_path)
112
def abspath(tree, file_id):
113
path = tree.inventory.id2path(file_id)
154
return MergeTree(base_tree, temp_path)
118
157
def file_exists(tree, file_id):
119
158
return tree.has_filename(tree.id2path(file_id))
121
def inventory_map(tree):
123
for file_id in tree.inventory:
124
if not file_exists(tree, file_id):
126
path = abspath(tree, file_id)
127
inventory[path] = SourceFile(path, file_id)
131
161
class MergeTree(object):
132
162
def __init__(self, tree, tempdir):
172
227
If true, this_dir must have no uncommitted changes before the
229
all available ancestors of other_revision and base_revision are
230
automatically pulled into the branch.
232
from bzrlib.revision import common_ancestor, MultipleRevisionSources
233
from bzrlib.errors import NoSuchRevision
175
234
tempdir = tempfile.mkdtemp(prefix="bzr-")
177
236
if this_dir is None:
179
238
this_branch = find_branch(this_dir)
239
this_rev_id = this_branch.last_patch()
240
if this_rev_id is None:
241
raise BzrCommandError("This branch has no commits")
181
243
changes = compare_trees(this_branch.working_tree(),
182
244
this_branch.basis_tree(), False)
183
245
if changes.has_changed():
184
246
raise BzrCommandError("Working tree has uncommitted changes.")
185
other_branch, other_tree = get_tree(other_revision, tempdir, "other")
247
other_branch, other_tree = get_tree(other_revision, tempdir, "other",
249
if other_revision[1] == -1:
250
other_rev_id = other_branch.last_patch()
251
other_basis = other_rev_id
252
elif other_revision[1] is not None:
253
other_rev_id = other_branch.lookup_revision(other_revision[1])
254
other_basis = other_rev_id
257
other_basis = other_branch.last_patch()
186
258
if base_revision == [None, None]:
187
259
if other_revision[1] == -1:
190
262
o_revno = other_revision[1]
191
base_revno = this_branch.common_ancestor(other_branch,
192
other_revno=o_revno)[0]
193
if base_revno is None:
194
263
raise UnrelatedBranches()
195
base_revision = ['.', base_revno]
196
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
265
base_revision = this_branch.get_revision(base_rev_id)
266
base_branch = this_branch
267
except NoSuchRevision:
268
base_branch = other_branch
269
base_tree = get_revid_tree(base_branch, base_rev_id, tempdir,
271
base_is_ancestor = True
273
base_branch, base_tree = get_tree(base_revision, tempdir, "base")
274
if base_revision[1] == -1:
275
base_rev_id = base_branch.last_patch()
276
elif base_revision[1] is None:
279
base_rev_id = base_branch.lookup_revision(base_revision[1])
280
if base_rev_id is not None:
281
base_is_ancestor = is_ancestor(this_rev_id, base_rev_id,
282
MultipleRevisionSources(
286
base_is_ancestor = False
287
if file_list is None:
288
interesting_ids = None
290
interesting_ids = set()
291
this_tree = this_branch.working_tree()
292
for fname in file_list:
293
path = this_branch.relpath(fname)
295
for tree in (this_tree, base_tree.tree, other_tree.tree):
296
file_id = tree.inventory.path2id(path)
297
if file_id is not None:
298
interesting_ids.add(file_id)
301
raise BzrCommandError("%s is not a source file in any"
197
303
merge_inner(this_branch, other_tree, base_tree, tempdir,
198
ignore_zero=ignore_zero)
304
ignore_zero=ignore_zero, backup_files=backup_files,
305
merge_type=merge_type, interesting_ids=interesting_ids)
306
if base_is_ancestor and other_rev_id is not None:
307
this_branch.add_pending_merge(other_rev_id)
200
309
shutil.rmtree(tempdir)
203
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b):
204
"""Generate a changeset, using the text_id to mark really-changed files.
205
This permits blazing comparisons when text_ids are present. It also
206
disables metadata comparison for files with identical texts.
312
def set_interesting(inventory_a, inventory_b, interesting_ids):
313
"""Mark files whose ids are in interesting_ids as interesting
315
for inventory in (inventory_a, inventory_b):
316
for path, source_file in inventory.iteritems():
317
source_file.interesting = source_file.id in interesting_ids
320
def generate_cset_optimized(tree_a, tree_b, interesting_ids=None):
321
"""Generate a changeset. If interesting_ids is supplied, only changes
322
to those files will be shown. Metadata changes are stripped.
208
for file_id in tree_a.tree.inventory:
209
if file_id not in tree_b.tree.inventory:
211
entry_a = tree_a.tree.inventory[file_id]
212
entry_b = tree_b.tree.inventory[file_id]
213
if (entry_a.kind, entry_b.kind) != ("file", "file"):
215
if None in (entry_a.text_id, entry_b.text_id):
217
if entry_a.text_id != entry_b.text_id:
219
inventory_a[abspath(tree_a.tree, file_id)].interesting = False
220
inventory_b[abspath(tree_b.tree, file_id)].interesting = False
221
cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
324
cset = generate_changeset(tree_a, tree_b, interesting_ids)
222
325
for entry in cset.entries.itervalues():
223
326
entry.metadata_change = None
227
330
def merge_inner(this_branch, other_tree, base_tree, tempdir,
331
ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
332
interesting_ids=None):
334
def merge_factory(file_id, base, other):
335
contents_change = merge_type(file_id, base, other)
337
contents_change = BackupBeforeChange(contents_change)
338
return contents_change
229
340
this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
231
342
def get_inventory(tree):
232
return tree.inventory
343
return tree.tree.inventory
234
345
inv_changes = merge_flex(this_tree, base_tree, other_tree,
235
346
generate_cset_optimized, get_inventory,
236
347
MergeConflictHandler(base_tree.root,
237
ignore_zero=ignore_zero))
348
ignore_zero=ignore_zero),
349
merge_factory=merge_factory,
350
interesting_ids=interesting_ids)
240
353
for id, path in inv_changes.iteritems():
245
assert path.startswith('./')
358
assert path.startswith('./'), "path is %s" % path
247
360
adjust_ids.append((path, id))
248
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root, adjust_ids))
361
if len(adjust_ids) > 0:
362
this_branch.set_inventory(regen_inventory(this_branch, this_tree.root,
251
366
def regen_inventory(this_branch, root, new_entries):
252
367
old_entries = this_branch.read_working_inventory()
253
368
new_inventory = {}
371
for path, file_id in new_entries:
374
new_entries_map[file_id] = path
376
def id2path(file_id):
377
path = new_entries_map.get(file_id)
380
entry = old_entries[file_id]
381
if entry.parent_id is None:
383
return os.path.join(id2path(entry.parent_id), entry.name)
255
385
for file_id in old_entries:
256
386
entry = old_entries[file_id]
257
path = old_entries.id2path(file_id)
387
path = id2path(file_id)
258
388
new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
259
389
by_path[path] = file_id