20
from tempfile import mkdtemp
24
import bzrlib.revision
25
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
26
from bzrlib.merge_core import WeaveMerge
27
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
28
from bzrlib.changeset import Inventory, Diff3Merge, ReplaceContents
23
29
from bzrlib.branch import Branch
24
from bzrlib.conflicts import ConflictList, Conflict
25
from bzrlib.delta import compare_trees
26
30
from bzrlib.errors import (BzrCommandError,
34
WorkingTreeNotRevision,
36
WorkingTreeNotRevision,
39
from bzrlib.merge3 import Merge3
41
from bzrlib.osutils import rename, pathjoin, rmtree
42
from progress import DummyProgress, ProgressPhase
43
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
44
from bzrlib.symbol_versioning import *
45
from bzrlib.textfile import check_text_lines
38
from bzrlib.delta import compare_trees
46
39
from bzrlib.trace import mutter, warning, note
47
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
48
FinalPaths, create_by_entry, unique_add)
49
from bzrlib.versionedfile import WeaveMerge
40
from bzrlib.fetch import greedy_fetch, fetch
41
from bzrlib.revision import is_ancestor, NULL_REVISION
42
from bzrlib.osutils import rename
43
from bzrlib.revision import common_ancestor, MultipleRevisionSources
44
from bzrlib.errors import NoSuchRevision
52
46
# TODO: Report back as changes are merged in
54
def _get_tree(treespec, local_branch=None):
48
# TODO: build_working_dir can be built on something simpler than merge()
50
# FIXME: merge() parameters seem oriented towards the command line
51
# NOTABUG: merge is a helper for commandline functions. merge_inner is the
52
# the core functionality.
54
# comments from abentley on irc: merge happens in two stages, each
55
# of which generates a changeset object
57
# stage 1: generate OLD->OTHER,
58
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
60
class MergeConflictHandler(ExceptionConflictHandler):
61
"""Handle conflicts encountered while merging.
63
This subclasses ExceptionConflictHandler, so that any types of
64
conflict that are not explicitly handled cause an exception and
67
def __init__(self, this_tree, base_tree, other_tree, ignore_zero=False):
68
ExceptionConflictHandler.__init__(self)
70
self.ignore_zero = ignore_zero
71
self.this_tree = this_tree
72
self.base_tree = base_tree
73
self.other_tree = other_tree
75
def copy(self, source, dest):
76
"""Copy the text and mode of a file
77
:param source: The path of the file to copy
78
:param dest: The distination file to create
80
s_file = file(source, "rb")
81
d_file = file(dest, "wb")
84
os.chmod(dest, 0777 & os.stat(source).st_mode)
86
def dump(self, lines, dest):
87
"""Copy the text and mode of a file
88
:param source: The path of the file to copy
89
:param dest: The distination file to create
91
d_file = file(dest, "wb")
95
def add_suffix(self, name, suffix, last_new_name=None, fix_inventory=True):
96
"""Rename a file to append a suffix. If the new name exists, the
97
suffix is added repeatedly until a non-existant name is found
99
:param name: The path of the file
100
:param suffix: The suffix to append
101
:param last_new_name: (used for recursive calls) the last name tried
103
if last_new_name is None:
105
new_name = last_new_name+suffix
107
rename(name, new_name)
108
if fix_inventory is True:
110
relpath = self.this_tree.relpath(name)
111
except NotBranchError:
113
if relpath is not None:
114
file_id = self.this_tree.path2id(relpath)
115
if file_id is not None:
116
new_path = self.this_tree.relpath(new_name)
117
rename(new_name, name)
118
self.this_tree.rename_one(relpath, new_path)
119
assert self.this_tree.id2path(file_id) == new_path
121
if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
123
return self.add_suffix(name, suffix, last_new_name=new_name,
124
fix_inventory=fix_inventory)
127
def conflict(self, text):
132
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
134
Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The
135
main file will be a version with diff3 conflicts.
136
:param new_file: Path to the output file with diff3 markers
137
:param this_path: Path to the file text for the THIS tree
138
:param base_path: Path to the file text for the BASE tree
139
:param other_path: Path to the file text for the OTHER tree
141
self.add_suffix(this_path, ".THIS", fix_inventory=False)
142
self.dump(base_lines, this_path+".BASE")
143
self.dump(other_lines, this_path+".OTHER")
144
rename(new_file, this_path)
145
self.conflict("Diff3 conflict encountered in %s" % this_path)
147
def weave_merge_conflict(self, filename, weave, other_i, out_file):
149
Handle weave conflicts by producing a .THIS, and .OTHER. The
150
main file will be a version with diff3-style conflicts.
152
self.add_suffix(filename, ".THIS", fix_inventory=False)
154
self.dump(weave.get_iter(other_i), filename+".OTHER")
155
self.conflict("Text conflict encountered in %s" % filename)
157
def new_contents_conflict(self, filename, other_contents):
158
"""Conflicting contents for newly added file."""
159
other_contents(filename + ".OTHER", self, False)
160
self.conflict("Conflict in newly added file %s" % filename)
163
def target_exists(self, entry, target, old_path):
164
"""Handle the case when the target file or dir exists"""
165
moved_path = self.add_suffix(target, ".moved")
166
self.conflict("Moved existing %s to %s" % (target, moved_path))
168
def rmdir_non_empty(self, filename):
169
"""Handle the case where the dir to be removed still has contents"""
170
self.conflict("Directory %s not removed because it is not empty"\
174
def rem_contents_conflict(self, filename, this_contents, base_contents):
175
base_contents(filename+".BASE", self, False)
176
this_contents(filename+".THIS", self, False)
177
return ReplaceContents(this_contents, None)
179
def rem_contents_conflict(self, filename, this_contents, base_contents):
180
base_contents(filename+".BASE", self, False)
181
this_contents(filename+".THIS", self, False)
182
self.conflict("Other branch deleted locally modified file %s" %
184
return ReplaceContents(this_contents, None)
186
def abs_this_path(self, file_id):
187
"""Return the absolute path for a file_id in the this tree."""
188
return self.this_tree.id2abspath(file_id)
190
def add_missing_parents(self, file_id, tree):
191
"""If some of the parents for file_id are missing, add them."""
192
entry = tree.inventory[file_id]
193
if entry.parent_id not in self.this_tree:
194
return self.create_all_missing(entry.parent_id, tree)
196
return self.abs_this_path(entry.parent_id)
198
def create_all_missing(self, file_id, tree):
199
"""Add contents for a file_id and all its parents to a tree."""
200
entry = tree.inventory[file_id]
201
if entry.parent_id is not None and entry.parent_id not in self.this_tree:
202
abspath = self.create_all_missing(entry.parent_id, tree)
204
abspath = self.abs_this_path(entry.parent_id)
205
entry_path = os.path.join(abspath, entry.name)
206
if not os.path.isdir(entry_path):
207
self.create(file_id, entry_path, tree)
210
def create(self, file_id, path, tree, reverse=False):
211
"""Uses tree data to create a filesystem object for the file_id"""
212
from changeset import get_contents
213
get_contents(tree, file_id)(path, self, reverse)
215
def missing_for_merge(self, file_id, other_path):
216
"""The file_id doesn't exist in THIS, but does in OTHER and BASE"""
217
self.conflict("Other branch modified locally deleted file %s" %
219
parent_dir = self.add_missing_parents(file_id, self.other_tree)
220
stem = os.path.join(parent_dir, os.path.basename(other_path))
221
self.create(file_id, stem+".OTHER", self.other_tree)
222
self.create(file_id, stem+".BASE", self.base_tree)
224
def threeway_contents_conflict(filename, this_contents, base_contents,
226
self.conflict("Three-way conflict merging %s" % filename)
229
if not self.ignore_zero:
230
note("%d conflicts encountered." % self.conflicts)
232
def get_tree(treespec, local_branch=None):
55
233
location, revno = treespec
56
234
branch = Branch.open_containing(location)[0]
62
240
revision = branch.get_rev_id(revno)
63
241
if revision is None:
64
242
revision = NULL_REVISION
65
return branch, _get_revid_tree(branch, revision, local_branch)
68
def _get_revid_tree(branch, revision, local_branch):
243
return branch, get_revid_tree(branch, revision, local_branch)
245
def get_revid_tree(branch, revision, local_branch):
69
246
if revision is None:
70
base_tree = branch.bzrdir.open_workingtree()
247
base_tree = branch.working_tree()
72
249
if local_branch is not None:
73
if local_branch.base != branch.base:
74
local_branch.fetch(branch, revision)
75
base_tree = local_branch.repository.revision_tree(revision)
250
greedy_fetch(local_branch, branch, revision)
251
base_tree = local_branch.revision_tree(revision)
77
base_tree = branch.repository.revision_tree(revision)
253
base_tree = branch.revision_tree(revision)
257
def file_exists(tree, file_id):
258
return tree.has_filename(tree.id2path(file_id))
261
def build_working_dir(to_dir):
262
"""Build a working directory in an empty directory.
264
to_dir is a directory containing branch metadata but no working files,
265
typically constructed by cloning an existing branch.
267
This is split out as a special idiomatic case of merge. It could
268
eventually be done by just building the tree directly calling into
269
lower-level code (e.g. constructing a changeset).
271
# RBC 20051019 is this not just 'export' ?
272
# AB Well, export doesn't take care of inventory...
273
this_branch = Branch.open_containing(to_dir)[0]
274
transform_tree(this_branch.working_tree(), this_branch.basis_tree())
81
277
def transform_tree(from_tree, to_tree, interesting_ids=None):
82
278
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
83
interesting_ids=interesting_ids, this_tree=from_tree)
279
interesting_ids=interesting_ids)
282
def merge(other_revision, base_revision,
283
check_clean=True, ignore_zero=False,
284
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
285
file_list=None, show_base=False, reprocess=False):
286
"""Merge changes into a tree.
289
list(path, revno) Base for three-way merge.
290
If [None, None] then a base will be automatically determined.
292
list(path, revno) Other revision for three-way merge.
294
Directory to merge changes into; '.' by default.
296
If true, this_dir must have no uncommitted changes before the
298
ignore_zero - If true, suppress the "zero conflicts" message when
299
there are no conflicts; should be set when doing something we expect
300
to complete perfectly.
301
file_list - If supplied, merge only changes to selected files.
303
All available ancestors of other_revision and base_revision are
304
automatically pulled into the branch.
306
The revno may be -1 to indicate the last revision on the branch, which is
309
This function is intended for use from the command line; programmatic
310
clients might prefer to call merge_inner(), which has less magic behavior.
314
this_branch = Branch.open_containing(this_dir)[0]
315
if show_base and not merge_type is ApplyMerge3:
316
raise BzrCommandError("Show-base is not supported for this merge"
317
" type. %s" % merge_type)
318
if reprocess and not merge_type is ApplyMerge3:
319
raise BzrCommandError("Reprocess is not supported for this merge"
320
" type. %s" % merge_type)
321
if reprocess and show_base:
322
raise BzrCommandError("Cannot reprocess and show base.")
323
merger = Merger(this_branch)
324
merger.check_basis(check_clean)
325
merger.set_other(other_revision)
326
merger.set_base(base_revision)
327
if merger.base_rev_id == merger.other_rev_id:
328
note('Nothing to do.')
330
merger.backup_files = backup_files
331
merger.merge_type = merge_type
332
merger.set_interesting_files(file_list)
333
merger.show_base = show_base
334
merger.reprocess = reprocess
335
merger.conflict_handler = MergeConflictHandler(merger.this_tree,
338
ignore_zero=ignore_zero)
339
conflicts = merger.do_merge()
343
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
345
merge_type=ApplyMerge3,
346
interesting_ids=None,
350
interesting_files=None):
351
"""Primary interface for merging.
353
typical use is probably
354
'merge_inner(branch, branch.get_revision_tree(other_revision),
355
branch.get_revision_tree(base_revision))'
357
merger = Merger(this_branch, other_tree, base_tree)
358
merger.backup_files = backup_files
359
merger.merge_type = merge_type
360
merger.interesting_ids = interesting_ids
361
if interesting_files:
362
assert not interesting_ids, ('Only supply interesting_ids'
363
' or interesting_files')
364
merger._set_interesting_files(interesting_files)
365
merger.show_base = show_base
366
merger.reprocess = reprocess
367
merger.conflict_handler = MergeConflictHandler(merger.this_tree, base_tree,
369
ignore_zero=ignore_zero)
370
merger.other_rev_id = other_rev_id
371
merger.other_basis = other_rev_id
372
return merger.do_merge()
86
375
class Merger(object):
87
def __init__(self, this_branch, other_tree=None, base_tree=None,
88
this_tree=None, pb=DummyProgress()):
376
def __init__(self, this_branch, other_tree=None, base_tree=None):
89
377
object.__init__(self)
90
assert this_tree is not None, "this_tree is required"
91
378
self.this_branch = this_branch
92
379
self.this_basis = this_branch.last_revision()
93
380
self.this_rev_id = None
94
self.this_tree = this_tree
381
self.this_tree = this_branch.working_tree()
95
382
self.this_revision_tree = None
96
383
self.this_basis_tree = None
97
384
self.other_tree = other_tree
334
626
new_inventory_list.sort()
335
627
return new_inventory_list
338
class Merge3Merger(object):
339
"""Three-way merger that uses the merge3 text merger"""
341
supports_reprocess = True
342
supports_show_base = True
343
history_based = False
345
def __init__(self, working_tree, this_tree, base_tree, other_tree,
346
interesting_ids=None, reprocess=False, show_base=False,
347
pb=DummyProgress(), pp=None):
348
"""Initialize the merger object and perform the merge."""
349
object.__init__(self)
350
self.this_tree = working_tree
351
self.base_tree = base_tree
352
self.other_tree = other_tree
353
self._raw_conflicts = []
354
self.cooked_conflicts = []
355
self.reprocess = reprocess
356
self.show_base = show_base
360
self.pp = ProgressPhase("Merge phase", 3, self.pb)
362
if interesting_ids is not None:
363
all_ids = interesting_ids
365
all_ids = set(base_tree)
366
all_ids.update(other_tree)
367
working_tree.lock_write()
368
self.tt = TreeTransform(working_tree, self.pb)
371
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
373
for num, file_id in enumerate(all_ids):
374
child_pb.update('Preparing file merge', num, len(all_ids))
375
self.merge_names(file_id)
376
file_status = self.merge_contents(file_id)
377
self.merge_executable(file_id, file_status)
382
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
384
fs_conflicts = resolve_conflicts(self.tt, child_pb)
387
self.cook_conflicts(fs_conflicts)
388
for conflict in self.cooked_conflicts:
391
results = self.tt.apply()
392
self.write_modified(results)
394
working_tree.set_conflicts(ConflictList(self.cooked_conflicts))
395
except UnsupportedOperation:
402
working_tree.unlock()
405
def write_modified(self, results):
407
for path in results.modified_paths:
408
file_id = self.this_tree.path2id(self.this_tree.relpath(path))
411
hash = self.this_tree.get_file_sha1(file_id)
414
modified_hashes[file_id] = hash
415
self.this_tree.set_merge_modified(modified_hashes)
418
def parent(entry, file_id):
419
"""Determine the parent for a file_id (used as a key method)"""
422
return entry.parent_id
425
def name(entry, file_id):
426
"""Determine the name for a file_id (used as a key method)"""
432
def contents_sha1(tree, file_id):
433
"""Determine the sha1 of the file contents (used as a key method)."""
434
if file_id not in tree:
436
return tree.get_file_sha1(file_id)
439
def executable(tree, file_id):
440
"""Determine the executability of a file-id (used as a key method)."""
441
if file_id not in tree:
443
if tree.kind(file_id) != "file":
445
return tree.is_executable(file_id)
448
def kind(tree, file_id):
449
"""Determine the kind of a file-id (used as a key method)."""
450
if file_id not in tree:
452
return tree.kind(file_id)
455
def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
456
"""Do a three-way test on a scalar.
457
Return "this", "other" or "conflict", depending whether a value wins.
459
key_base = key(base_tree, file_id)
460
key_other = key(other_tree, file_id)
461
#if base == other, either they all agree, or only THIS has changed.
462
if key_base == key_other:
464
key_this = key(this_tree, file_id)
465
if key_this not in (key_base, key_other):
467
# "Ambiguous clean merge"
468
elif key_this == key_other:
471
assert key_this == key_base
474
def merge_names(self, file_id):
475
"""Perform a merge on file_id names and parents"""
477
if file_id in tree.inventory:
478
return tree.inventory[file_id]
481
this_entry = get_entry(self.this_tree)
482
other_entry = get_entry(self.other_tree)
483
base_entry = get_entry(self.base_tree)
484
name_winner = self.scalar_three_way(this_entry, base_entry,
485
other_entry, file_id, self.name)
486
parent_id_winner = self.scalar_three_way(this_entry, base_entry,
487
other_entry, file_id,
489
if this_entry is None:
490
if name_winner == "this":
491
name_winner = "other"
492
if parent_id_winner == "this":
493
parent_id_winner = "other"
494
if name_winner == "this" and parent_id_winner == "this":
496
if name_winner == "conflict":
497
trans_id = self.tt.trans_id_file_id(file_id)
498
self._raw_conflicts.append(('name conflict', trans_id,
499
self.name(this_entry, file_id),
500
self.name(other_entry, file_id)))
501
if parent_id_winner == "conflict":
502
trans_id = self.tt.trans_id_file_id(file_id)
503
self._raw_conflicts.append(('parent conflict', trans_id,
504
self.parent(this_entry, file_id),
505
self.parent(other_entry, file_id)))
506
if other_entry is None:
507
# it doesn't matter whether the result was 'other' or
508
# 'conflict'-- if there's no 'other', we leave it alone.
510
# if we get here, name_winner and parent_winner are set to safe values.
511
winner_entry = {"this": this_entry, "other": other_entry,
512
"conflict": other_entry}
513
trans_id = self.tt.trans_id_file_id(file_id)
514
parent_id = winner_entry[parent_id_winner].parent_id
515
parent_trans_id = self.tt.trans_id_file_id(parent_id)
516
self.tt.adjust_path(winner_entry[name_winner].name, parent_trans_id,
519
def merge_contents(self, file_id):
520
"""Performa a merge on file_id contents."""
521
def contents_pair(tree):
522
if file_id not in tree:
524
kind = tree.kind(file_id)
525
if kind == "root_directory":
528
contents = tree.get_file_sha1(file_id)
529
elif kind == "symlink":
530
contents = tree.get_symlink_target(file_id)
533
return kind, contents
535
def contents_conflict():
536
trans_id = self.tt.trans_id_file_id(file_id)
537
name = self.tt.final_name(trans_id)
538
parent_id = self.tt.final_parent(trans_id)
539
if file_id in self.this_tree.inventory:
540
self.tt.unversion_file(trans_id)
541
self.tt.delete_contents(trans_id)
542
file_group = self._dump_conflicts(name, parent_id, file_id,
544
self._raw_conflicts.append(('contents conflict', file_group))
546
# See SPOT run. run, SPOT, run.
547
# So we're not QUITE repeating ourselves; we do tricky things with
549
base_pair = contents_pair(self.base_tree)
550
other_pair = contents_pair(self.other_tree)
551
if base_pair == other_pair:
552
# OTHER introduced no changes
554
this_pair = contents_pair(self.this_tree)
555
if this_pair == other_pair:
556
# THIS and OTHER introduced the same changes
559
trans_id = self.tt.trans_id_file_id(file_id)
560
if this_pair == base_pair:
561
# only OTHER introduced changes
562
if file_id in self.this_tree:
563
# Remove any existing contents
564
self.tt.delete_contents(trans_id)
565
if file_id in self.other_tree:
566
# OTHER changed the file
567
create_by_entry(self.tt,
568
self.other_tree.inventory[file_id],
569
self.other_tree, trans_id)
570
if file_id not in self.this_tree.inventory:
571
self.tt.version_file(file_id, trans_id)
573
elif file_id in self.this_tree.inventory:
574
# OTHER deleted the file
575
self.tt.unversion_file(trans_id)
577
#BOTH THIS and OTHER introduced changes; scalar conflict
578
elif this_pair[0] == "file" and other_pair[0] == "file":
579
# THIS and OTHER are both files, so text merge. Either
580
# BASE is a file, or both converted to files, so at least we
581
# have agreement that output should be a file.
583
self.text_merge(file_id, trans_id)
585
return contents_conflict()
586
if file_id not in self.this_tree.inventory:
587
self.tt.version_file(file_id, trans_id)
589
self.tt.tree_kind(trans_id)
590
self.tt.delete_contents(trans_id)
595
# Scalar conflict, can't text merge. Dump conflicts
596
return contents_conflict()
598
def get_lines(self, tree, file_id):
599
"""Return the lines in a file, or an empty list."""
601
return tree.get_file(file_id).readlines()
605
def text_merge(self, file_id, trans_id):
606
"""Perform a three-way text merge on a file_id"""
607
# it's possible that we got here with base as a different type.
608
# if so, we just want two-way text conflicts.
609
if file_id in self.base_tree and \
610
self.base_tree.kind(file_id) == "file":
611
base_lines = self.get_lines(self.base_tree, file_id)
614
other_lines = self.get_lines(self.other_tree, file_id)
615
this_lines = self.get_lines(self.this_tree, file_id)
616
m3 = Merge3(base_lines, this_lines, other_lines)
617
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
618
if self.show_base is True:
619
base_marker = '|' * 7
623
def iter_merge3(retval):
624
retval["text_conflicts"] = False
625
for line in m3.merge_lines(name_a = "TREE",
626
name_b = "MERGE-SOURCE",
627
name_base = "BASE-REVISION",
628
start_marker=start_marker,
629
base_marker=base_marker,
630
reprocess=self.reprocess):
631
if line.startswith(start_marker):
632
retval["text_conflicts"] = True
633
yield line.replace(start_marker, '<' * 7)
637
merge3_iterator = iter_merge3(retval)
638
self.tt.create_file(merge3_iterator, trans_id)
639
if retval["text_conflicts"] is True:
640
self._raw_conflicts.append(('text conflict', trans_id))
641
name = self.tt.final_name(trans_id)
642
parent_id = self.tt.final_parent(trans_id)
643
file_group = self._dump_conflicts(name, parent_id, file_id,
644
this_lines, base_lines,
646
file_group.append(trans_id)
648
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
649
base_lines=None, other_lines=None, set_version=False,
651
"""Emit conflict files.
652
If this_lines, base_lines, or other_lines are omitted, they will be
653
determined automatically. If set_version is true, the .OTHER, .THIS
654
or .BASE (in that order) will be created as versioned files.
656
data = [('OTHER', self.other_tree, other_lines),
657
('THIS', self.this_tree, this_lines)]
659
data.append(('BASE', self.base_tree, base_lines))
662
for suffix, tree, lines in data:
664
trans_id = self._conflict_file(name, parent_id, tree, file_id,
666
file_group.append(trans_id)
667
if set_version and not versioned:
668
self.tt.version_file(file_id, trans_id)
672
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
674
"""Emit a single conflict file."""
675
name = name + '.' + suffix
676
trans_id = self.tt.create_path(name, parent_id)
677
entry = tree.inventory[file_id]
678
create_by_entry(self.tt, entry, tree, trans_id, lines)
681
def merge_executable(self, file_id, file_status):
682
"""Perform a merge on the execute bit."""
683
if file_status == "deleted":
685
trans_id = self.tt.trans_id_file_id(file_id)
687
if self.tt.final_kind(trans_id) != "file":
691
winner = self.scalar_three_way(self.this_tree, self.base_tree,
692
self.other_tree, file_id,
694
if winner == "conflict":
695
# There must be a None in here, if we have a conflict, but we
696
# need executability since file status was not deleted.
697
if self.other_tree.is_executable(file_id) is None:
702
if file_status == "modified":
703
executability = self.this_tree.is_executable(file_id)
704
if executability is not None:
705
trans_id = self.tt.trans_id_file_id(file_id)
706
self.tt.set_executability(executability, trans_id)
708
assert winner == "other"
709
if file_id in self.other_tree:
710
executability = self.other_tree.is_executable(file_id)
711
elif file_id in self.this_tree:
712
executability = self.this_tree.is_executable(file_id)
713
elif file_id in self.base_tree:
714
executability = self.base_tree.is_executable(file_id)
715
if executability is not None:
716
trans_id = self.tt.trans_id_file_id(file_id)
717
self.tt.set_executability(executability, trans_id)
719
def cook_conflicts(self, fs_conflicts):
720
"""Convert all conflicts into a form that doesn't depend on trans_id"""
721
from conflicts import Conflict
723
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
724
fp = FinalPaths(self.tt)
725
for conflict in self._raw_conflicts:
726
conflict_type = conflict[0]
727
if conflict_type in ('name conflict', 'parent conflict'):
728
trans_id = conflict[1]
729
conflict_args = conflict[2:]
730
if trans_id not in name_conflicts:
731
name_conflicts[trans_id] = {}
732
unique_add(name_conflicts[trans_id], conflict_type,
734
if conflict_type == 'contents conflict':
735
for trans_id in conflict[1]:
736
file_id = self.tt.final_file_id(trans_id)
737
if file_id is not None:
739
path = fp.get_path(trans_id)
740
for suffix in ('.BASE', '.THIS', '.OTHER'):
741
if path.endswith(suffix):
742
path = path[:-len(suffix)]
744
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
745
self.cooked_conflicts.append(c)
746
if conflict_type == 'text conflict':
747
trans_id = conflict[1]
748
path = fp.get_path(trans_id)
749
file_id = self.tt.final_file_id(trans_id)
750
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
751
self.cooked_conflicts.append(c)
753
for trans_id, conflicts in name_conflicts.iteritems():
755
this_parent, other_parent = conflicts['parent conflict']
756
assert this_parent != other_parent
758
this_parent = other_parent = \
759
self.tt.final_file_id(self.tt.final_parent(trans_id))
761
this_name, other_name = conflicts['name conflict']
762
assert this_name != other_name
764
this_name = other_name = self.tt.final_name(trans_id)
765
other_path = fp.get_path(trans_id)
766
if this_parent is not None:
768
fp.get_path(self.tt.trans_id_file_id(this_parent))
769
this_path = pathjoin(this_parent_path, this_name)
771
this_path = "<deleted>"
772
file_id = self.tt.final_file_id(trans_id)
773
c = Conflict.factory('path conflict', path=this_path,
774
conflict_path=other_path, file_id=file_id)
775
self.cooked_conflicts.append(c)
776
self.cooked_conflicts.sort(key=Conflict.sort_key)
779
class WeaveMerger(Merge3Merger):
780
"""Three-way tree merger, text weave merger."""
781
supports_reprocess = True
782
supports_show_base = False
784
def __init__(self, working_tree, this_tree, base_tree, other_tree,
785
interesting_ids=None, pb=DummyProgress(), pp=None,
787
self.this_revision_tree = self._get_revision_tree(this_tree)
788
self.other_revision_tree = self._get_revision_tree(other_tree)
789
super(WeaveMerger, self).__init__(working_tree, this_tree,
790
base_tree, other_tree,
791
interesting_ids=interesting_ids,
792
pb=pb, pp=pp, reprocess=reprocess)
794
def _get_revision_tree(self, tree):
795
"""Return a revision tree related to this tree.
796
If the tree is a WorkingTree, the basis will be returned.
798
if getattr(tree, 'get_weave', False) is False:
799
# If we have a WorkingTree, try using the basis
800
return tree.branch.basis_tree()
804
def _check_file(self, file_id):
805
"""Check that the revision tree's version of the file matches."""
806
for tree, rt in ((self.this_tree, self.this_revision_tree),
807
(self.other_tree, self.other_revision_tree)):
810
if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
811
raise WorkingTreeNotRevision(self.this_tree)
813
def _merged_lines(self, file_id):
814
"""Generate the merged lines.
815
There is no distinction between lines that are meant to contain <<<<<<<
818
weave = self.this_revision_tree.get_weave(file_id)
819
this_revision_id = self.this_revision_tree.inventory[file_id].revision
820
other_revision_id = \
821
self.other_revision_tree.inventory[file_id].revision
822
wm = WeaveMerge(weave, this_revision_id, other_revision_id,
823
'<<<<<<< TREE\n', '>>>>>>> MERGE-SOURCE\n')
824
return wm.merge_lines(self.reprocess)
826
def text_merge(self, file_id, trans_id):
827
"""Perform a (weave) text merge for a given file and file-id.
828
If conflicts are encountered, .THIS and .OTHER files will be emitted,
829
and a conflict will be noted.
831
self._check_file(file_id)
832
lines, conflicts = self._merged_lines(file_id)
834
# Note we're checking whether the OUTPUT is binary in this case,
835
# because we don't want to get into weave merge guts.
836
check_text_lines(lines)
837
self.tt.create_file(lines, trans_id)
839
self._raw_conflicts.append(('text conflict', trans_id))
840
name = self.tt.final_name(trans_id)
841
parent_id = self.tt.final_parent(trans_id)
842
file_group = self._dump_conflicts(name, parent_id, file_id,
844
file_group.append(trans_id)
847
class Diff3Merger(Merge3Merger):
848
"""Three-way merger using external diff3 for text merging"""
849
def dump_file(self, temp_dir, name, tree, file_id):
850
out_path = pathjoin(temp_dir, name)
851
out_file = file(out_path, "wb")
852
in_file = tree.get_file(file_id)
857
def text_merge(self, file_id, trans_id):
858
"""Perform a diff3 merge using a specified file-id and trans-id.
859
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
860
will be dumped, and a will be conflict noted.
863
temp_dir = mkdtemp(prefix="bzr-")
865
new_file = pathjoin(temp_dir, "new")
866
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
867
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
868
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
869
status = bzrlib.patch.diff3(new_file, this, base, other)
870
if status not in (0, 1):
871
raise BzrError("Unhandled diff3 exit code")
872
self.tt.create_file(file(new_file, "rb"), trans_id)
874
name = self.tt.final_name(trans_id)
875
parent_id = self.tt.final_parent(trans_id)
876
self._dump_conflicts(name, parent_id, file_id)
877
self._raw_conflicts.append(('text conflict', trans_id))
882
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
884
merge_type=Merge3Merger,
885
interesting_ids=None,
889
interesting_files=None,
892
"""Primary interface for merging.
894
typical use is probably
895
'merge_inner(branch, branch.get_revision_tree(other_revision),
896
branch.get_revision_tree(base_revision))'
898
if this_tree is None:
899
warn("bzrlib.merge.merge_inner requires a this_tree parameter as of "
900
"bzrlib version 0.8.",
903
this_tree = this_branch.bzrdir.open_workingtree()
904
merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree,
906
merger.backup_files = backup_files
907
merger.merge_type = merge_type
908
merger.interesting_ids = interesting_ids
909
merger.ignore_zero = ignore_zero
910
if interesting_files:
911
assert not interesting_ids, ('Only supply interesting_ids'
912
' or interesting_files')
913
merger._set_interesting_files(interesting_files)
914
merger.show_base = show_base
915
merger.reprocess = reprocess
916
merger.other_rev_id = other_rev_id
917
merger.other_basis = other_rev_id
918
return merger.do_merge()
921
merge_types = { "merge3": (Merge3Merger, "Native diff3-style merge"),
922
"diff3": (Diff3Merger, "Merge using external diff3"),
923
'weave': (WeaveMerger, "Weave-based merge")
629
merge_types = { "merge3": (ApplyMerge3, "Native diff3-style merge"),
630
"diff3": (Diff3Merge, "Merge using external diff3"),
631
'weave': (WeaveMerge, "Weave-based merge")
927
def merge_type_help():
928
templ = '%s%%7s: %%s' % (' '*12)
929
lines = [templ % (f[0], f[1][1]) for f in merge_types.iteritems()]
930
return '\n'.join(lines)