1
# Copyright (C) 2005 Canonical Ltd
1
# Copyright (C) 2005, 2006 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20
from tempfile import mkdtemp
23
import bzrlib.revision
24
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
25
from bzrlib.merge_core import WeaveMerge
26
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
27
from bzrlib.changeset import Inventory, Diff3Merge, ReplaceContents
28
23
from bzrlib.branch import Branch
24
from bzrlib.conflicts import ConflictList, Conflict
29
25
from bzrlib.errors import (BzrCommandError,
33
WorkingTreeNotRevision,
37
from bzrlib.delta import compare_trees
35
WorkingTreeNotRevision,
38
from bzrlib.merge3 import Merge3
40
from bzrlib.osutils import rename, pathjoin, rmtree
41
from progress import DummyProgress, ProgressPhase
42
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
43
from bzrlib.textfile import check_text_lines
38
44
from bzrlib.trace import mutter, warning, note
39
from bzrlib.fetch import greedy_fetch, fetch
40
from bzrlib.revision import is_ancestor, NULL_REVISION
41
from bzrlib.osutils import rename, pathjoin
42
from bzrlib.revision import common_ancestor, MultipleRevisionSources
43
from bzrlib.errors import NoSuchRevision
45
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
46
FinalPaths, create_by_entry, unique_add)
47
from bzrlib.versionedfile import WeaveMerge
45
50
# TODO: Report back as changes are merged in
47
# TODO: build_working_dir can be built on something simpler than merge()
49
# FIXME: merge() parameters seem oriented towards the command line
50
# NOTABUG: merge is a helper for commandline functions. merge_inner is the
51
# the core functionality.
53
# comments from abentley on irc: merge happens in two stages, each
54
# of which generates a changeset object
56
# stage 1: generate OLD->OTHER,
57
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
59
class MergeConflictHandler(ExceptionConflictHandler):
60
"""Handle conflicts encountered while merging.
62
This subclasses ExceptionConflictHandler, so that any types of
63
conflict that are not explicitly handled cause an exception and
66
def __init__(self, this_tree, base_tree, other_tree, ignore_zero=False):
67
ExceptionConflictHandler.__init__(self)
69
self.ignore_zero = ignore_zero
70
self.this_tree = this_tree
71
self.base_tree = base_tree
72
self.other_tree = other_tree
74
def copy(self, source, dest):
75
"""Copy the text and mode of a file
76
:param source: The path of the file to copy
77
:param dest: The distination file to create
79
s_file = file(source, "rb")
80
d_file = file(dest, "wb")
83
os.chmod(dest, 0777 & os.stat(source).st_mode)
85
def dump(self, lines, dest):
86
"""Copy the text and mode of a file
87
:param source: The path of the file to copy
88
:param dest: The distination file to create
90
d_file = file(dest, "wb")
94
def add_suffix(self, name, suffix, last_new_name=None, fix_inventory=True):
95
"""Rename a file to append a suffix. If the new name exists, the
96
suffix is added repeatedly until a non-existant name is found
98
:param name: The path of the file
99
:param suffix: The suffix to append
100
:param last_new_name: (used for recursive calls) the last name tried
102
if last_new_name is None:
104
new_name = last_new_name+suffix
106
rename(name, new_name)
107
if fix_inventory is True:
109
relpath = self.this_tree.relpath(name)
110
except NotBranchError:
112
if relpath is not None:
113
file_id = self.this_tree.path2id(relpath)
114
if file_id is not None:
115
new_path = self.this_tree.relpath(new_name)
116
rename(new_name, name)
117
self.this_tree.rename_one(relpath, new_path)
118
assert self.this_tree.id2path(file_id) == new_path
120
if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
122
return self.add_suffix(name, suffix, last_new_name=new_name,
123
fix_inventory=fix_inventory)
126
def conflict(self, text):
131
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
133
Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The
134
main file will be a version with diff3 conflicts.
135
:param new_file: Path to the output file with diff3 markers
136
:param this_path: Path to the file text for the THIS tree
137
:param base_path: Path to the file text for the BASE tree
138
:param other_path: Path to the file text for the OTHER tree
140
self.add_suffix(this_path, ".THIS", fix_inventory=False)
141
self.dump(base_lines, this_path+".BASE")
142
self.dump(other_lines, this_path+".OTHER")
143
rename(new_file, this_path)
144
self.conflict("Diff3 conflict encountered in %s" % this_path)
146
def weave_merge_conflict(self, filename, weave, other_i, out_file):
148
Handle weave conflicts by producing a .THIS, and .OTHER. The
149
main file will be a version with diff3-style conflicts.
151
self.add_suffix(filename, ".THIS", fix_inventory=False)
153
self.dump(weave.get_iter(other_i), filename+".OTHER")
154
self.conflict("Text conflict encountered in %s" % filename)
156
def new_contents_conflict(self, filename, other_contents):
157
"""Conflicting contents for newly added file."""
158
other_contents(filename + ".OTHER", self, False)
159
self.conflict("Conflict in newly added file %s" % filename)
162
def target_exists(self, entry, target, old_path):
163
"""Handle the case when the target file or dir exists"""
164
moved_path = self.add_suffix(target, ".moved")
165
self.conflict("Moved existing %s to %s" % (target, moved_path))
167
def rmdir_non_empty(self, filename):
168
"""Handle the case where the dir to be removed still has contents"""
169
self.conflict("Directory %s not removed because it is not empty"\
173
def rem_contents_conflict(self, filename, this_contents, base_contents):
174
base_contents(filename+".BASE", self)
175
this_contents(filename+".THIS", self)
176
self.conflict("Other branch deleted locally modified file %s" %
178
return ReplaceContents(this_contents, None)
180
def abs_this_path(self, file_id):
181
"""Return the absolute path for a file_id in the this tree."""
182
return self.this_tree.id2abspath(file_id)
184
def add_missing_parents(self, file_id, tree):
185
"""If some of the parents for file_id are missing, add them."""
186
entry = tree.inventory[file_id]
187
if entry.parent_id not in self.this_tree:
188
return self.create_all_missing(entry.parent_id, tree)
190
return self.abs_this_path(entry.parent_id)
192
def create_all_missing(self, file_id, tree):
193
"""Add contents for a file_id and all its parents to a tree."""
194
entry = tree.inventory[file_id]
195
if entry.parent_id is not None and entry.parent_id not in self.this_tree:
196
abspath = self.create_all_missing(entry.parent_id, tree)
198
abspath = self.abs_this_path(entry.parent_id)
199
entry_path = pathjoin(abspath, entry.name)
200
if not os.path.isdir(entry_path):
201
self.create(file_id, entry_path, tree)
204
def create(self, file_id, path, tree):
205
"""Uses tree data to create a filesystem object for the file_id"""
206
from changeset import get_contents
207
get_contents(tree, file_id)(path, self)
209
def missing_for_merge(self, file_id, other_path):
210
"""The file_id doesn't exist in THIS, but does in OTHER and BASE"""
211
self.conflict("Other branch modified locally deleted file %s" %
213
parent_dir = self.add_missing_parents(file_id, self.other_tree)
214
stem = pathjoin(parent_dir, os.path.basename(other_path))
215
self.create(file_id, stem+".OTHER", self.other_tree)
216
self.create(file_id, stem+".BASE", self.base_tree)
218
def threeway_contents_conflict(filename, this_contents, base_contents,
220
self.conflict("Three-way conflict merging %s" % filename)
223
if self.conflicts == 0:
224
if not self.ignore_zero:
225
note("All changes applied successfully.")
227
note("%d conflicts encountered." % self.conflicts)
229
def get_tree(treespec, local_branch=None):
52
def _get_tree(treespec, local_branch=None):
53
from bzrlib import workingtree
230
54
location, revno = treespec
56
tree = workingtree.WorkingTree.open_containing(location)[0]
57
return tree.branch, tree
231
58
branch = Branch.open_containing(location)[0]
235
60
revision = branch.last_revision()
237
62
revision = branch.get_rev_id(revno)
238
63
if revision is None:
239
64
revision = NULL_REVISION
240
return branch, get_revid_tree(branch, revision, local_branch)
242
def get_revid_tree(branch, revision, local_branch):
65
return branch, _get_revid_tree(branch, revision, local_branch)
68
def _get_revid_tree(branch, revision, local_branch):
243
69
if revision is None:
244
base_tree = branch.working_tree()
70
base_tree = branch.bzrdir.open_workingtree()
246
72
if local_branch is not None:
247
greedy_fetch(local_branch, branch, revision)
248
base_tree = local_branch.revision_tree(revision)
73
if local_branch.base != branch.base:
74
local_branch.fetch(branch, revision)
75
base_tree = local_branch.repository.revision_tree(revision)
250
base_tree = branch.revision_tree(revision)
77
base_tree = branch.repository.revision_tree(revision)
254
def file_exists(tree, file_id):
255
return tree.has_filename(tree.id2path(file_id))
258
def build_working_dir(to_dir):
259
"""Build a working directory in an empty directory.
261
to_dir is a directory containing branch metadata but no working files,
262
typically constructed by cloning an existing branch.
264
This is split out as a special idiomatic case of merge. It could
265
eventually be done by just building the tree directly calling into
266
lower-level code (e.g. constructing a changeset).
268
# RBC 20051019 is this not just 'export' ?
269
# AB Well, export doesn't take care of inventory...
270
this_branch = Branch.open_containing(to_dir)[0]
271
transform_tree(this_branch.working_tree(), this_branch.basis_tree())
274
81
def transform_tree(from_tree, to_tree, interesting_ids=None):
275
82
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
276
interesting_ids=interesting_ids)
279
def merge(other_revision, base_revision,
280
check_clean=True, ignore_zero=False,
281
this_dir=None, backup_files=False, merge_type=ApplyMerge3,
282
file_list=None, show_base=False, reprocess=False):
283
"""Merge changes into a tree.
286
list(path, revno) Base for three-way merge.
287
If [None, None] then a base will be automatically determined.
289
list(path, revno) Other revision for three-way merge.
291
Directory to merge changes into; '.' by default.
293
If true, this_dir must have no uncommitted changes before the
295
ignore_zero - If true, suppress the "zero conflicts" message when
296
there are no conflicts; should be set when doing something we expect
297
to complete perfectly.
298
file_list - If supplied, merge only changes to selected files.
300
All available ancestors of other_revision and base_revision are
301
automatically pulled into the branch.
303
The revno may be -1 to indicate the last revision on the branch, which is
306
This function is intended for use from the command line; programmatic
307
clients might prefer to call merge_inner(), which has less magic behavior.
311
this_branch = Branch.open_containing(this_dir)[0]
312
if show_base and not merge_type is ApplyMerge3:
313
raise BzrCommandError("Show-base is not supported for this merge"
314
" type. %s" % merge_type)
315
if reprocess and not merge_type is ApplyMerge3:
316
raise BzrCommandError("Reprocess is not supported for this merge"
317
" type. %s" % merge_type)
318
if reprocess and show_base:
319
raise BzrCommandError("Cannot reprocess and show base.")
320
merger = Merger(this_branch)
321
merger.check_basis(check_clean)
322
merger.set_other(other_revision)
323
merger.set_base(base_revision)
324
if merger.base_rev_id == merger.other_rev_id:
325
note('Nothing to do.')
327
merger.backup_files = backup_files
328
merger.merge_type = merge_type
329
merger.set_interesting_files(file_list)
330
merger.show_base = show_base
331
merger.reprocess = reprocess
332
merger.conflict_handler = MergeConflictHandler(merger.this_tree,
335
ignore_zero=ignore_zero)
336
conflicts = merger.do_merge()
340
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
342
merge_type=ApplyMerge3,
343
interesting_ids=None,
347
interesting_files=None):
348
"""Primary interface for merging.
350
typical use is probably
351
'merge_inner(branch, branch.get_revision_tree(other_revision),
352
branch.get_revision_tree(base_revision))'
354
merger = Merger(this_branch, other_tree, base_tree)
355
merger.backup_files = backup_files
356
merger.merge_type = merge_type
357
merger.interesting_ids = interesting_ids
358
if interesting_files:
359
assert not interesting_ids, ('Only supply interesting_ids'
360
' or interesting_files')
361
merger._set_interesting_files(interesting_files)
362
merger.show_base = show_base
363
merger.reprocess = reprocess
364
merger.conflict_handler = MergeConflictHandler(merger.this_tree, base_tree,
366
ignore_zero=ignore_zero)
367
merger.other_rev_id = other_rev_id
368
merger.other_basis = other_rev_id
369
return merger.do_merge()
83
interesting_ids=interesting_ids, this_tree=from_tree)
372
86
class Merger(object):
373
def __init__(self, this_branch, other_tree=None, base_tree=None):
87
def __init__(self, this_branch, other_tree=None, base_tree=None,
88
this_tree=None, pb=DummyProgress()):
374
89
object.__init__(self)
90
assert this_tree is not None, "this_tree is required"
375
91
self.this_branch = this_branch
376
92
self.this_basis = this_branch.last_revision()
377
93
self.this_rev_id = None
378
self.this_tree = this_branch.working_tree()
94
self.this_tree = this_tree
379
95
self.this_revision_tree = None
380
96
self.this_basis_tree = None
381
97
self.other_tree = other_tree
623
333
new_inventory_list.sort()
624
334
return new_inventory_list
626
merge_types = { "merge3": (ApplyMerge3, "Native diff3-style merge"),
627
"diff3": (Diff3Merge, "Merge using external diff3"),
628
'weave': (WeaveMerge, "Weave-based merge")
337
class Merge3Merger(object):
338
"""Three-way merger that uses the merge3 text merger"""
340
supports_reprocess = True
341
supports_show_base = True
342
history_based = False
344
def __init__(self, working_tree, this_tree, base_tree, other_tree,
345
interesting_ids=None, reprocess=False, show_base=False,
346
pb=DummyProgress(), pp=None):
347
"""Initialize the merger object and perform the merge."""
348
object.__init__(self)
349
self.this_tree = working_tree
350
self.base_tree = base_tree
351
self.other_tree = other_tree
352
self._raw_conflicts = []
353
self.cooked_conflicts = []
354
self.reprocess = reprocess
355
self.show_base = show_base
359
self.pp = ProgressPhase("Merge phase", 3, self.pb)
361
if interesting_ids is not None:
362
all_ids = interesting_ids
364
all_ids = set(base_tree)
365
all_ids.update(other_tree)
366
working_tree.lock_write()
367
self.tt = TreeTransform(working_tree, self.pb)
370
child_pb = ui.ui_factory.nested_progress_bar()
372
for num, file_id in enumerate(all_ids):
373
child_pb.update('Preparing file merge', num, len(all_ids))
374
self.merge_names(file_id)
375
file_status = self.merge_contents(file_id)
376
self.merge_executable(file_id, file_status)
381
child_pb = ui.ui_factory.nested_progress_bar()
383
fs_conflicts = resolve_conflicts(self.tt, child_pb)
386
self.cook_conflicts(fs_conflicts)
387
for conflict in self.cooked_conflicts:
390
results = self.tt.apply()
391
self.write_modified(results)
393
working_tree.add_conflicts(self.cooked_conflicts)
394
except UnsupportedOperation:
398
working_tree.unlock()
401
def write_modified(self, results):
403
for path in results.modified_paths:
404
file_id = self.this_tree.path2id(self.this_tree.relpath(path))
407
hash = self.this_tree.get_file_sha1(file_id)
410
modified_hashes[file_id] = hash
411
self.this_tree.set_merge_modified(modified_hashes)
414
def parent(entry, file_id):
415
"""Determine the parent for a file_id (used as a key method)"""
418
return entry.parent_id
421
def name(entry, file_id):
422
"""Determine the name for a file_id (used as a key method)"""
428
def contents_sha1(tree, file_id):
429
"""Determine the sha1 of the file contents (used as a key method)."""
430
if file_id not in tree:
432
return tree.get_file_sha1(file_id)
435
def executable(tree, file_id):
436
"""Determine the executability of a file-id (used as a key method)."""
437
if file_id not in tree:
439
if tree.kind(file_id) != "file":
441
return tree.is_executable(file_id)
444
def kind(tree, file_id):
445
"""Determine the kind of a file-id (used as a key method)."""
446
if file_id not in tree:
448
return tree.kind(file_id)
451
def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
452
"""Do a three-way test on a scalar.
453
Return "this", "other" or "conflict", depending whether a value wins.
455
key_base = key(base_tree, file_id)
456
key_other = key(other_tree, file_id)
457
#if base == other, either they all agree, or only THIS has changed.
458
if key_base == key_other:
460
key_this = key(this_tree, file_id)
461
if key_this not in (key_base, key_other):
463
# "Ambiguous clean merge"
464
elif key_this == key_other:
467
assert key_this == key_base
470
def merge_names(self, file_id):
471
"""Perform a merge on file_id names and parents"""
473
if file_id in tree.inventory:
474
return tree.inventory[file_id]
477
this_entry = get_entry(self.this_tree)
478
other_entry = get_entry(self.other_tree)
479
base_entry = get_entry(self.base_tree)
480
name_winner = self.scalar_three_way(this_entry, base_entry,
481
other_entry, file_id, self.name)
482
parent_id_winner = self.scalar_three_way(this_entry, base_entry,
483
other_entry, file_id,
485
if this_entry is None:
486
if name_winner == "this":
487
name_winner = "other"
488
if parent_id_winner == "this":
489
parent_id_winner = "other"
490
if name_winner == "this" and parent_id_winner == "this":
492
if name_winner == "conflict":
493
trans_id = self.tt.trans_id_file_id(file_id)
494
self._raw_conflicts.append(('name conflict', trans_id,
495
self.name(this_entry, file_id),
496
self.name(other_entry, file_id)))
497
if parent_id_winner == "conflict":
498
trans_id = self.tt.trans_id_file_id(file_id)
499
self._raw_conflicts.append(('parent conflict', trans_id,
500
self.parent(this_entry, file_id),
501
self.parent(other_entry, file_id)))
502
if other_entry is None:
503
# it doesn't matter whether the result was 'other' or
504
# 'conflict'-- if there's no 'other', we leave it alone.
506
# if we get here, name_winner and parent_winner are set to safe values.
507
winner_entry = {"this": this_entry, "other": other_entry,
508
"conflict": other_entry}
509
trans_id = self.tt.trans_id_file_id(file_id)
510
parent_id = winner_entry[parent_id_winner].parent_id
511
parent_trans_id = self.tt.trans_id_file_id(parent_id)
512
self.tt.adjust_path(winner_entry[name_winner].name, parent_trans_id,
515
def merge_contents(self, file_id):
516
"""Performa a merge on file_id contents."""
517
def contents_pair(tree):
518
if file_id not in tree:
520
kind = tree.kind(file_id)
522
contents = tree.get_file_sha1(file_id)
523
elif kind == "symlink":
524
contents = tree.get_symlink_target(file_id)
527
return kind, contents
529
def contents_conflict():
530
trans_id = self.tt.trans_id_file_id(file_id)
531
name = self.tt.final_name(trans_id)
532
parent_id = self.tt.final_parent(trans_id)
533
if file_id in self.this_tree.inventory:
534
self.tt.unversion_file(trans_id)
535
self.tt.delete_contents(trans_id)
536
file_group = self._dump_conflicts(name, parent_id, file_id,
538
self._raw_conflicts.append(('contents conflict', file_group))
540
# See SPOT run. run, SPOT, run.
541
# So we're not QUITE repeating ourselves; we do tricky things with
543
base_pair = contents_pair(self.base_tree)
544
other_pair = contents_pair(self.other_tree)
545
if base_pair == other_pair:
546
# OTHER introduced no changes
548
this_pair = contents_pair(self.this_tree)
549
if this_pair == other_pair:
550
# THIS and OTHER introduced the same changes
553
trans_id = self.tt.trans_id_file_id(file_id)
554
if this_pair == base_pair:
555
# only OTHER introduced changes
556
if file_id in self.this_tree:
557
# Remove any existing contents
558
self.tt.delete_contents(trans_id)
559
if file_id in self.other_tree:
560
# OTHER changed the file
561
create_by_entry(self.tt,
562
self.other_tree.inventory[file_id],
563
self.other_tree, trans_id)
564
if file_id not in self.this_tree.inventory:
565
self.tt.version_file(file_id, trans_id)
567
elif file_id in self.this_tree.inventory:
568
# OTHER deleted the file
569
self.tt.unversion_file(trans_id)
571
#BOTH THIS and OTHER introduced changes; scalar conflict
572
elif this_pair[0] == "file" and other_pair[0] == "file":
573
# THIS and OTHER are both files, so text merge. Either
574
# BASE is a file, or both converted to files, so at least we
575
# have agreement that output should be a file.
577
self.text_merge(file_id, trans_id)
579
return contents_conflict()
580
if file_id not in self.this_tree.inventory:
581
self.tt.version_file(file_id, trans_id)
583
self.tt.tree_kind(trans_id)
584
self.tt.delete_contents(trans_id)
589
# Scalar conflict, can't text merge. Dump conflicts
590
return contents_conflict()
592
def get_lines(self, tree, file_id):
593
"""Return the lines in a file, or an empty list."""
595
return tree.get_file(file_id).readlines()
599
def text_merge(self, file_id, trans_id):
600
"""Perform a three-way text merge on a file_id"""
601
# it's possible that we got here with base as a different type.
602
# if so, we just want two-way text conflicts.
603
if file_id in self.base_tree and \
604
self.base_tree.kind(file_id) == "file":
605
base_lines = self.get_lines(self.base_tree, file_id)
608
other_lines = self.get_lines(self.other_tree, file_id)
609
this_lines = self.get_lines(self.this_tree, file_id)
610
m3 = Merge3(base_lines, this_lines, other_lines)
611
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
612
if self.show_base is True:
613
base_marker = '|' * 7
617
def iter_merge3(retval):
618
retval["text_conflicts"] = False
619
for line in m3.merge_lines(name_a = "TREE",
620
name_b = "MERGE-SOURCE",
621
name_base = "BASE-REVISION",
622
start_marker=start_marker,
623
base_marker=base_marker,
624
reprocess=self.reprocess):
625
if line.startswith(start_marker):
626
retval["text_conflicts"] = True
627
yield line.replace(start_marker, '<' * 7)
631
merge3_iterator = iter_merge3(retval)
632
self.tt.create_file(merge3_iterator, trans_id)
633
if retval["text_conflicts"] is True:
634
self._raw_conflicts.append(('text conflict', trans_id))
635
name = self.tt.final_name(trans_id)
636
parent_id = self.tt.final_parent(trans_id)
637
file_group = self._dump_conflicts(name, parent_id, file_id,
638
this_lines, base_lines,
640
file_group.append(trans_id)
642
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
643
base_lines=None, other_lines=None, set_version=False,
645
"""Emit conflict files.
646
If this_lines, base_lines, or other_lines are omitted, they will be
647
determined automatically. If set_version is true, the .OTHER, .THIS
648
or .BASE (in that order) will be created as versioned files.
650
data = [('OTHER', self.other_tree, other_lines),
651
('THIS', self.this_tree, this_lines)]
653
data.append(('BASE', self.base_tree, base_lines))
656
for suffix, tree, lines in data:
658
trans_id = self._conflict_file(name, parent_id, tree, file_id,
660
file_group.append(trans_id)
661
if set_version and not versioned:
662
self.tt.version_file(file_id, trans_id)
666
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
668
"""Emit a single conflict file."""
669
name = name + '.' + suffix
670
trans_id = self.tt.create_path(name, parent_id)
671
entry = tree.inventory[file_id]
672
create_by_entry(self.tt, entry, tree, trans_id, lines)
675
def merge_executable(self, file_id, file_status):
676
"""Perform a merge on the execute bit."""
677
if file_status == "deleted":
679
trans_id = self.tt.trans_id_file_id(file_id)
681
if self.tt.final_kind(trans_id) != "file":
685
winner = self.scalar_three_way(self.this_tree, self.base_tree,
686
self.other_tree, file_id,
688
if winner == "conflict":
689
# There must be a None in here, if we have a conflict, but we
690
# need executability since file status was not deleted.
691
if self.executable(self.other_tree, file_id) is None:
696
if file_status == "modified":
697
executability = self.this_tree.is_executable(file_id)
698
if executability is not None:
699
trans_id = self.tt.trans_id_file_id(file_id)
700
self.tt.set_executability(executability, trans_id)
702
assert winner == "other"
703
if file_id in self.other_tree:
704
executability = self.other_tree.is_executable(file_id)
705
elif file_id in self.this_tree:
706
executability = self.this_tree.is_executable(file_id)
707
elif file_id in self.base_tree:
708
executability = self.base_tree.is_executable(file_id)
709
if executability is not None:
710
trans_id = self.tt.trans_id_file_id(file_id)
711
self.tt.set_executability(executability, trans_id)
713
def cook_conflicts(self, fs_conflicts):
714
"""Convert all conflicts into a form that doesn't depend on trans_id"""
715
from conflicts import Conflict
717
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
718
fp = FinalPaths(self.tt)
719
for conflict in self._raw_conflicts:
720
conflict_type = conflict[0]
721
if conflict_type in ('name conflict', 'parent conflict'):
722
trans_id = conflict[1]
723
conflict_args = conflict[2:]
724
if trans_id not in name_conflicts:
725
name_conflicts[trans_id] = {}
726
unique_add(name_conflicts[trans_id], conflict_type,
728
if conflict_type == 'contents conflict':
729
for trans_id in conflict[1]:
730
file_id = self.tt.final_file_id(trans_id)
731
if file_id is not None:
733
path = fp.get_path(trans_id)
734
for suffix in ('.BASE', '.THIS', '.OTHER'):
735
if path.endswith(suffix):
736
path = path[:-len(suffix)]
738
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
739
self.cooked_conflicts.append(c)
740
if conflict_type == 'text conflict':
741
trans_id = conflict[1]
742
path = fp.get_path(trans_id)
743
file_id = self.tt.final_file_id(trans_id)
744
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
745
self.cooked_conflicts.append(c)
747
for trans_id, conflicts in name_conflicts.iteritems():
749
this_parent, other_parent = conflicts['parent conflict']
750
assert this_parent != other_parent
752
this_parent = other_parent = \
753
self.tt.final_file_id(self.tt.final_parent(trans_id))
755
this_name, other_name = conflicts['name conflict']
756
assert this_name != other_name
758
this_name = other_name = self.tt.final_name(trans_id)
759
other_path = fp.get_path(trans_id)
760
if this_parent is not None:
762
fp.get_path(self.tt.trans_id_file_id(this_parent))
763
this_path = pathjoin(this_parent_path, this_name)
765
this_path = "<deleted>"
766
file_id = self.tt.final_file_id(trans_id)
767
c = Conflict.factory('path conflict', path=this_path,
768
conflict_path=other_path, file_id=file_id)
769
self.cooked_conflicts.append(c)
770
self.cooked_conflicts.sort(key=Conflict.sort_key)
773
class WeaveMerger(Merge3Merger):
774
"""Three-way tree merger, text weave merger."""
775
supports_reprocess = True
776
supports_show_base = False
778
def __init__(self, working_tree, this_tree, base_tree, other_tree,
779
interesting_ids=None, pb=DummyProgress(), pp=None,
781
self.this_revision_tree = self._get_revision_tree(this_tree)
782
self.other_revision_tree = self._get_revision_tree(other_tree)
783
super(WeaveMerger, self).__init__(working_tree, this_tree,
784
base_tree, other_tree,
785
interesting_ids=interesting_ids,
786
pb=pb, pp=pp, reprocess=reprocess)
788
def _get_revision_tree(self, tree):
789
"""Return a revision tree related to this tree.
790
If the tree is a WorkingTree, the basis will be returned.
792
if getattr(tree, 'get_weave', False) is False:
793
# If we have a WorkingTree, try using the basis
794
return tree.branch.basis_tree()
798
def _check_file(self, file_id):
799
"""Check that the revision tree's version of the file matches."""
800
for tree, rt in ((self.this_tree, self.this_revision_tree),
801
(self.other_tree, self.other_revision_tree)):
804
if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
805
raise WorkingTreeNotRevision(self.this_tree)
807
def _merged_lines(self, file_id):
808
"""Generate the merged lines.
809
There is no distinction between lines that are meant to contain <<<<<<<
812
weave = self.this_revision_tree.get_weave(file_id)
813
this_revision_id = self.this_revision_tree.inventory[file_id].revision
814
other_revision_id = \
815
self.other_revision_tree.inventory[file_id].revision
816
wm = WeaveMerge(weave, this_revision_id, other_revision_id,
817
'<<<<<<< TREE\n', '>>>>>>> MERGE-SOURCE\n')
818
return wm.merge_lines(self.reprocess)
820
def text_merge(self, file_id, trans_id):
821
"""Perform a (weave) text merge for a given file and file-id.
822
If conflicts are encountered, .THIS and .OTHER files will be emitted,
823
and a conflict will be noted.
825
self._check_file(file_id)
826
lines, conflicts = self._merged_lines(file_id)
828
# Note we're checking whether the OUTPUT is binary in this case,
829
# because we don't want to get into weave merge guts.
830
check_text_lines(lines)
831
self.tt.create_file(lines, trans_id)
833
self._raw_conflicts.append(('text conflict', trans_id))
834
name = self.tt.final_name(trans_id)
835
parent_id = self.tt.final_parent(trans_id)
836
file_group = self._dump_conflicts(name, parent_id, file_id,
838
file_group.append(trans_id)
841
class Diff3Merger(Merge3Merger):
842
"""Three-way merger using external diff3 for text merging"""
844
def dump_file(self, temp_dir, name, tree, file_id):
845
out_path = pathjoin(temp_dir, name)
846
out_file = open(out_path, "wb")
848
in_file = tree.get_file(file_id)
855
def text_merge(self, file_id, trans_id):
856
"""Perform a diff3 merge using a specified file-id and trans-id.
857
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
858
will be dumped, and a will be conflict noted.
861
temp_dir = mkdtemp(prefix="bzr-")
863
new_file = pathjoin(temp_dir, "new")
864
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
865
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
866
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
867
status = bzrlib.patch.diff3(new_file, this, base, other)
868
if status not in (0, 1):
869
raise BzrError("Unhandled diff3 exit code")
870
f = open(new_file, 'rb')
872
self.tt.create_file(f, trans_id)
876
name = self.tt.final_name(trans_id)
877
parent_id = self.tt.final_parent(trans_id)
878
self._dump_conflicts(name, parent_id, file_id)
879
self._raw_conflicts.append(('text conflict', trans_id))
884
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
886
merge_type=Merge3Merger,
887
interesting_ids=None,
891
interesting_files=None,
894
"""Primary interface for merging.
896
typical use is probably
897
'merge_inner(branch, branch.get_revision_tree(other_revision),
898
branch.get_revision_tree(base_revision))'
900
if this_tree is None:
901
warnings.warn("bzrlib.merge.merge_inner requires a this_tree parameter as of "
902
"bzrlib version 0.8.",
905
this_tree = this_branch.bzrdir.open_workingtree()
906
merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree,
908
merger.backup_files = backup_files
909
merger.merge_type = merge_type
910
merger.interesting_ids = interesting_ids
911
merger.ignore_zero = ignore_zero
912
if interesting_files:
913
assert not interesting_ids, ('Only supply interesting_ids'
914
' or interesting_files')
915
merger._set_interesting_files(interesting_files)
916
merger.show_base = show_base
917
merger.reprocess = reprocess
918
merger.other_rev_id = other_rev_id
919
merger.other_basis = other_rev_id
920
return merger.do_merge()
923
merge_types = { "merge3": (Merge3Merger, "Native diff3-style merge"),
924
"diff3": (Diff3Merger, "Merge using external diff3"),
925
'weave': (WeaveMerger, "Weave-based merge")
929
def merge_type_help():
930
templ = '%s%%7s: %%s' % (' '*12)
931
lines = [templ % (f[0], f[1][1]) for f in merge_types.iteritems()]
932
return '\n'.join(lines)