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
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
from itertools import chain
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
from bzrlib.lazy_import import lazy_import
20
lazy_import(globals(), """
23
21
from bzrlib import (
22
branch as _mod_branch,
24
conflicts as _mod_conflicts,
26
27
graph as _mod_graph,
30
31
revision as _mod_revision,
34
from bzrlib.branch import Branch
35
from bzrlib.conflicts import ConflictList, Conflict
36
from bzrlib.errors import (BzrCommandError,
46
WorkingTreeNotRevision,
49
from bzrlib.graph import Graph
50
from bzrlib.merge3 import Merge3
51
from bzrlib.osutils import rename, pathjoin
52
from progress import DummyProgress, ProgressPhase
53
from bzrlib.revision import (NULL_REVISION, ensure_null)
54
from bzrlib.textfile import check_text_lines
55
from bzrlib.trace import mutter, warning, note, is_quiet
56
from bzrlib.transform import (TransformPreview, TreeTransform,
57
resolve_conflicts, cook_conflicts,
58
conflict_pass, FinalPaths, create_from_tree,
59
unique_add, ROOT_PARENT)
60
from bzrlib.versionedfile import PlanWeaveMerge
41
from bzrlib.i18n import gettext
48
from bzrlib.symbol_versioning import (
63
52
# TODO: Report back as changes are merged in
66
55
def transform_tree(from_tree, to_tree, interesting_ids=None):
67
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
68
interesting_ids=interesting_ids, this_tree=from_tree)
56
from_tree.lock_tree_write()
57
operation = cleanup.OperationWithCleanups(merge_inner)
58
operation.add_cleanup(from_tree.unlock)
59
operation.run_simple(from_tree.branch, to_tree, from_tree,
60
ignore_zero=True, interesting_ids=interesting_ids, this_tree=from_tree)
63
class MergeHooks(hooks.Hooks):
66
hooks.Hooks.__init__(self, "bzrlib.merge", "Merger.hooks")
67
self.add_hook('merge_file_content',
68
"Called with a bzrlib.merge.Merger object to create a per file "
69
"merge object when starting a merge. "
70
"Should return either None or a subclass of "
71
"``bzrlib.merge.AbstractPerFileMerger``. "
72
"Such objects will then be called per file "
73
"that needs to be merged (including when one "
74
"side has deleted the file and the other has changed it). "
75
"See the AbstractPerFileMerger API docs for details on how it is "
80
class AbstractPerFileMerger(object):
81
"""PerFileMerger objects are used by plugins extending merge for bzrlib.
83
See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
85
:ivar merger: The Merge3Merger performing the merge.
88
def __init__(self, merger):
89
"""Create a PerFileMerger for use with merger."""
92
def merge_contents(self, merge_params):
93
"""Attempt to merge the contents of a single file.
95
:param merge_params: A bzrlib.merge.MergeHookParams
96
:return: A tuple of (status, chunks), where status is one of
97
'not_applicable', 'success', 'conflicted', or 'delete'. If status
98
is 'success' or 'conflicted', then chunks should be an iterable of
99
strings for the new file contents.
101
return ('not applicable', None)
104
class PerFileMerger(AbstractPerFileMerger):
105
"""Merge individual files when self.file_matches returns True.
107
This class is intended to be subclassed. The file_matches and
108
merge_matching methods should be overridden with concrete implementations.
111
def file_matches(self, params):
112
"""Return True if merge_matching should be called on this file.
114
Only called with merges of plain files with no clear winner.
116
Subclasses must override this.
118
raise NotImplementedError(self.file_matches)
120
def get_filename(self, params, tree):
121
"""Lookup the filename (i.e. basename, not path), given a Tree (e.g.
122
self.merger.this_tree) and a MergeHookParams.
124
return osutils.basename(tree.id2path(params.file_id))
126
def get_filepath(self, params, tree):
127
"""Calculate the path to the file in a tree.
129
:param params: A MergeHookParams describing the file to merge
130
:param tree: a Tree, e.g. self.merger.this_tree.
132
return tree.id2path(params.file_id)
134
def merge_contents(self, params):
135
"""Merge the contents of a single file."""
136
# Check whether this custom merge logic should be used.
138
# OTHER is a straight winner, rely on default merge.
139
params.winner == 'other' or
140
# THIS and OTHER aren't both files.
141
not params.is_file_merge() or
142
# The filename doesn't match *.xml
143
not self.file_matches(params)):
144
return 'not_applicable', None
145
return self.merge_matching(params)
147
def merge_matching(self, params):
148
"""Merge the contents of a single file that has matched the criteria
149
in PerFileMerger.merge_contents (is a conflict, is a file,
150
self.file_matches is True).
152
Subclasses must override this.
154
raise NotImplementedError(self.merge_matching)
157
class ConfigurableFileMerger(PerFileMerger):
158
"""Merge individual files when configured via a .conf file.
160
This is a base class for concrete custom file merging logic. Concrete
161
classes should implement ``merge_text``.
163
See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
165
:ivar affected_files: The configured file paths to merge.
167
:cvar name_prefix: The prefix to use when looking up configuration
168
details. <name_prefix>_merge_files describes the files targeted by the
171
:cvar default_files: The default file paths to merge when no configuration
178
def __init__(self, merger):
179
super(ConfigurableFileMerger, self).__init__(merger)
180
self.affected_files = None
181
self.default_files = self.__class__.default_files or []
182
self.name_prefix = self.__class__.name_prefix
183
if self.name_prefix is None:
184
raise ValueError("name_prefix must be set.")
186
def file_matches(self, params):
187
"""Check whether the file should call the merge hook.
189
<name_prefix>_merge_files configuration variable is a list of files
190
that should use the hook.
192
affected_files = self.affected_files
193
if affected_files is None:
194
config = self.merger.this_branch.get_config()
195
# Until bzr provides a better policy for caching the config, we
196
# just add the part we're interested in to the params to avoid
197
# reading the config files repeatedly (bazaar.conf, location.conf,
199
config_key = self.name_prefix + '_merge_files'
200
affected_files = config.get_user_option_as_list(config_key)
201
if affected_files is None:
202
# If nothing was specified in the config, use the default.
203
affected_files = self.default_files
204
self.affected_files = affected_files
206
filepath = self.get_filepath(params, self.merger.this_tree)
207
if filepath in affected_files:
211
def merge_matching(self, params):
212
return self.merge_text(params)
214
def merge_text(self, params):
215
"""Merge the byte contents of a single file.
217
This is called after checking that the merge should be performed in
218
merge_contents, and it should behave as per
219
``bzrlib.merge.AbstractPerFileMerger.merge_contents``.
221
raise NotImplementedError(self.merge_text)
224
class MergeHookParams(object):
225
"""Object holding parameters passed to merge_file_content hooks.
227
There are some fields hooks can access:
229
:ivar file_id: the file ID of the file being merged
230
:ivar trans_id: the transform ID for the merge of this file
231
:ivar this_kind: kind of file_id in 'this' tree
232
:ivar other_kind: kind of file_id in 'other' tree
233
:ivar winner: one of 'this', 'other', 'conflict'
236
def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
238
self._merger = merger
239
self.file_id = file_id
240
self.trans_id = trans_id
241
self.this_kind = this_kind
242
self.other_kind = other_kind
245
def is_file_merge(self):
246
"""True if this_kind and other_kind are both 'file'."""
247
return self.this_kind == 'file' and self.other_kind == 'file'
249
@decorators.cachedproperty
250
def base_lines(self):
251
"""The lines of the 'base' version of the file."""
252
return self._merger.get_lines(self._merger.base_tree, self.file_id)
254
@decorators.cachedproperty
255
def this_lines(self):
256
"""The lines of the 'this' version of the file."""
257
return self._merger.get_lines(self._merger.this_tree, self.file_id)
259
@decorators.cachedproperty
260
def other_lines(self):
261
"""The lines of the 'other' version of the file."""
262
return self._merger.get_lines(self._merger.other_tree, self.file_id)
71
265
class Merger(object):
72
269
def __init__(self, this_branch, other_tree=None, base_tree=None,
73
this_tree=None, pb=DummyProgress(), change_reporter=None,
270
this_tree=None, pb=None, change_reporter=None,
74
271
recurse='down', revision_graph=None):
75
272
object.__init__(self)
76
273
self.this_branch = this_branch
242
452
if self.other_rev_id is None:
243
453
other_basis_tree = self.revision_tree(self.other_basis)
244
changes = other_basis_tree.changes_from(self.other_tree)
245
if changes.has_changed():
246
raise WorkingTreeNotRevision(self.this_tree)
454
if other_basis_tree.has_changes(self.other_tree):
455
raise errors.WorkingTreeNotRevision(self.this_tree)
247
456
other_rev_id = self.other_basis
248
457
self.other_tree = other_basis_tree
459
@deprecated_method(deprecated_in((2, 1, 0)))
250
460
def file_revisions(self, file_id):
251
461
self.ensure_revision_trees()
252
def get_id(tree, file_id):
253
revision_id = tree.inventory[file_id].revision
255
462
if self.this_rev_id is None:
256
463
if self.this_basis_tree.get_file_sha1(file_id) != \
257
464
self.this_tree.get_file_sha1(file_id):
258
raise WorkingTreeNotRevision(self.this_tree)
465
raise errors.WorkingTreeNotRevision(self.this_tree)
260
467
trees = (self.this_basis_tree, self.other_tree)
261
return [get_id(tree, file_id) for tree in trees]
468
return [tree.get_file_revision(file_id) for tree in trees]
470
@deprecated_method(deprecated_in((2, 1, 0)))
263
471
def check_basis(self, check_clean, require_commits=True):
264
472
if self.this_basis is None and require_commits is True:
265
raise BzrCommandError("This branch has no commits."
266
" (perhaps you would prefer 'bzr pull')")
473
raise errors.BzrCommandError(
474
"This branch has no commits."
475
" (perhaps you would prefer 'bzr pull')")
268
477
self.compare_basis()
269
478
if self.this_basis != self.this_rev_id:
270
479
raise errors.UncommittedChanges(self.this_tree)
481
@deprecated_method(deprecated_in((2, 1, 0)))
272
482
def compare_basis(self):
274
484
basis_tree = self.revision_tree(self.this_tree.last_revision())
275
485
except errors.NoSuchRevision:
276
486
basis_tree = self.this_tree.basis_tree()
277
changes = self.this_tree.changes_from(basis_tree)
278
if not changes.has_changed():
487
if not self.this_tree.has_changes(basis_tree):
279
488
self.this_rev_id = self.this_basis
281
490
def set_interesting_files(self, file_list):
282
491
self.interesting_files = file_list
284
493
def set_pending(self):
285
if not self.base_is_ancestor or not self.base_is_other_ancestor or self.other_rev_id is None:
494
if (not self.base_is_ancestor or not self.base_is_other_ancestor
495
or self.other_rev_id is None):
287
497
self._add_parent()
289
499
def _add_parent(self):
290
500
new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
291
501
new_parent_trees = []
502
operation = cleanup.OperationWithCleanups(
503
self.this_tree.set_parent_trees)
292
504
for revision_id in new_parents:
294
506
tree = self.revision_tree(revision_id)
385
593
self.base_rev_id = self.revision_graph.find_unique_lca(
387
self._is_criss_cross = True
388
if self.base_rev_id == NULL_REVISION:
389
raise UnrelatedBranches()
595
sorted_lca_keys = self.revision_graph.find_merge_order(
597
if self.base_rev_id == _mod_revision.NULL_REVISION:
598
self.base_rev_id = sorted_lca_keys[0]
600
if self.base_rev_id == _mod_revision.NULL_REVISION:
601
raise errors.UnrelatedBranches()
390
602
if self._is_criss_cross:
391
warning('Warning: criss-cross merge encountered. See bzr'
392
' help criss-cross.')
393
mutter('Criss-cross lcas: %r' % lcas)
394
interesting_revision_ids = [self.base_rev_id]
395
interesting_revision_ids.extend(lcas)
603
trace.warning('Warning: criss-cross merge encountered. See bzr'
604
' help criss-cross.')
605
trace.mutter('Criss-cross lcas: %r' % lcas)
606
if self.base_rev_id in lcas:
607
trace.mutter('Unable to find unique lca. '
608
'Fallback %r as best option.' % self.base_rev_id)
609
interesting_revision_ids = set(lcas)
610
interesting_revision_ids.add(self.base_rev_id)
396
611
interesting_trees = dict((t.get_revision_id(), t)
397
612
for t in self.this_branch.repository.revision_trees(
398
613
interesting_revision_ids))
399
614
self._cached_trees.update(interesting_trees)
400
self.base_tree = interesting_trees.pop(self.base_rev_id)
401
sorted_lca_keys = self.revision_graph.find_merge_order(
615
if self.base_rev_id in lcas:
616
self.base_tree = interesting_trees[self.base_rev_id]
618
self.base_tree = interesting_trees.pop(self.base_rev_id)
403
619
self._lca_trees = [interesting_trees[key]
404
620
for key in sorted_lca_keys]
406
622
self.base_tree = self.revision_tree(self.base_rev_id)
407
623
self.base_is_ancestor = True
408
624
self.base_is_other_ancestor = True
409
mutter('Base revid: %r' % self.base_rev_id)
625
trace.mutter('Base revid: %r' % self.base_rev_id)
411
627
def set_base(self, base_revision):
412
628
"""Set the base revision to use for the merge.
414
630
:param base_revision: A 2-list containing a path and revision number.
416
mutter("doing merge() with no base_revision specified")
632
trace.mutter("doing merge() with no base_revision specified")
417
633
if base_revision == [None, None]:
534
748
winner_idx = {"this": 2, "other": 1, "conflict": 1}
535
749
supports_lca_trees = True
537
def __init__(self, working_tree, this_tree, base_tree, other_tree,
751
def __init__(self, working_tree, this_tree, base_tree, other_tree,
538
752
interesting_ids=None, reprocess=False, show_base=False,
539
pb=DummyProgress(), pp=None, change_reporter=None,
753
pb=None, pp=None, change_reporter=None,
540
754
interesting_files=None, do_merge=True,
541
cherrypick=False, lca_trees=None):
755
cherrypick=False, lca_trees=None, this_branch=None):
542
756
"""Initialize the merger object and perform the merge.
544
758
:param working_tree: The working tree to apply the merge to
545
759
:param this_tree: The local tree in the merge operation
546
760
:param base_tree: The common tree in the merge operation
547
:param other_tree: The other other tree to merge changes from
761
:param other_tree: The other tree to merge changes from
762
:param this_branch: The branch associated with this_tree. Defaults to
763
this_tree.branch if not supplied.
548
764
:param interesting_ids: The file_ids of files that should be
549
765
participate in the merge. May not be combined with
550
766
interesting_files.
551
767
:param: reprocess If True, perform conflict-reduction processing.
552
768
:param show_base: If True, show the base revision in text conflicts.
553
769
(incompatible with reprocess)
554
:param pb: A Progress bar
555
771
:param pp: A ProgressPhase object
556
772
:param change_reporter: An object that should report changes made
557
773
:param interesting_files: The tree-relative paths of files that should
583
802
# making sure we haven't missed any corner cases.
584
803
# if lca_trees is None:
585
804
# self._lca_trees = [self.base_tree]
588
805
self.change_reporter = change_reporter
589
806
self.cherrypick = cherrypick
591
self.pp = ProgressPhase("Merge phase", 3, self.pb)
810
warnings.warn("pp argument to Merge3Merger is deprecated")
812
warnings.warn("pb argument to Merge3Merger is deprecated")
595
814
def do_merge(self):
815
operation = cleanup.OperationWithCleanups(self._do_merge)
596
816
self.this_tree.lock_tree_write()
817
operation.add_cleanup(self.this_tree.unlock)
597
818
self.base_tree.lock_read()
819
operation.add_cleanup(self.base_tree.unlock)
598
820
self.other_tree.lock_read()
599
self.tt = TreeTransform(self.this_tree, self.pb)
821
operation.add_cleanup(self.other_tree.unlock)
824
def _do_merge(self, operation):
825
self.tt = transform.TreeTransform(self.this_tree, None)
826
operation.add_cleanup(self.tt.finalize)
827
self._compute_transform()
828
results = self.tt.apply(no_conflicts=True)
829
self.write_modified(results)
602
self._compute_transform()
604
results = self.tt.apply(no_conflicts=True)
605
self.write_modified(results)
607
self.this_tree.add_conflicts(self.cooked_conflicts)
608
except UnsupportedOperation:
612
self.other_tree.unlock()
613
self.base_tree.unlock()
614
self.this_tree.unlock()
831
self.this_tree.add_conflicts(self.cooked_conflicts)
832
except errors.UnsupportedOperation:
617
835
def make_preview_transform(self):
836
operation = cleanup.OperationWithCleanups(self._make_preview_transform)
618
837
self.base_tree.lock_read()
838
operation.add_cleanup(self.base_tree.unlock)
619
839
self.other_tree.lock_read()
620
self.tt = TransformPreview(self.this_tree)
623
self._compute_transform()
626
self.other_tree.unlock()
627
self.base_tree.unlock()
840
operation.add_cleanup(self.other_tree.unlock)
841
return operation.run_simple()
843
def _make_preview_transform(self):
844
self.tt = transform.TransformPreview(self.this_tree)
845
self._compute_transform()
631
848
def _compute_transform(self):
882
1107
other_root = self.tt.trans_id_file_id(other_root_file_id)
883
1108
if other_root == self.tt.root:
886
self.tt.final_kind(other_root)
889
if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
890
# the other tree's root is a non-root in the current tree
892
self.reparent_children(self.other_tree.inventory.root, self.tt.root)
893
self.tt.cancel_creation(other_root)
894
self.tt.cancel_versioning(other_root)
896
def reparent_children(self, ie, target):
897
for thing, child in ie.children.iteritems():
1110
if self.this_tree.inventory.has_id(
1111
self.other_tree.inventory.root.file_id):
1112
# the other tree's root is a non-root in the current tree (as
1113
# when a previously unrelated branch is merged into another)
1115
if self.tt.final_kind(other_root) is not None:
1116
other_root_is_present = True
1118
# other_root doesn't have a physical representation. We still need
1119
# to move any references to the actual root of the tree.
1120
other_root_is_present = False
1121
# 'other_tree.inventory.root' is not present in this tree. We are
1122
# calling adjust_path for children which *want* to be present with a
1123
# correct place to go.
1124
for _, child in self.other_tree.inventory.root.children.iteritems():
898
1125
trans_id = self.tt.trans_id_file_id(child.file_id)
899
self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_id)
1126
if not other_root_is_present:
1127
if self.tt.final_kind(trans_id) is not None:
1128
# The item exist in the final tree and has a defined place
1131
# Move the item into the root
1133
final_name = self.tt.final_name(trans_id)
1134
except errors.NoFinalPath:
1135
# This file is not present anymore, ignore it.
1137
self.tt.adjust_path(final_name, self.tt.root, trans_id)
1138
if other_root_is_present:
1139
self.tt.cancel_creation(other_root)
1140
self.tt.cancel_versioning(other_root)
901
1142
def write_modified(self, results):
902
1143
modified_hashes = {}
1067
1311
parent_id_winner = "other"
1068
1312
if name_winner == "this" and parent_id_winner == "this":
1070
if name_winner == "conflict":
1071
trans_id = self.tt.trans_id_file_id(file_id)
1072
self._raw_conflicts.append(('name conflict', trans_id,
1073
this_name, other_name))
1074
if parent_id_winner == "conflict":
1075
trans_id = self.tt.trans_id_file_id(file_id)
1076
self._raw_conflicts.append(('parent conflict', trans_id,
1077
this_parent, other_parent))
1078
if other_name is None:
1079
# it doesn't matter whether the result was 'other' or
1080
# 'conflict'-- if there's no 'other', we leave it alone.
1314
if name_winner == 'conflict' or parent_id_winner == 'conflict':
1315
# Creating helpers (.OTHER or .THIS) here cause problems down the
1316
# road if a ContentConflict needs to be created so we should not do
1318
trans_id = self.tt.trans_id_file_id(file_id)
1319
self._raw_conflicts.append(('path conflict', trans_id, file_id,
1320
this_parent, this_name,
1321
other_parent, other_name))
1322
if not self.other_tree.has_id(file_id):
1323
# it doesn't matter whether the result was 'other' or
1324
# 'conflict'-- if it has no file id, we leave it alone.
1082
# if we get here, name_winner and parent_winner are set to safe values.
1083
trans_id = self.tt.trans_id_file_id(file_id)
1084
1326
parent_id = parents[self.winner_idx[parent_id_winner]]
1085
if parent_id is not None:
1086
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1087
self.tt.adjust_path(names[self.winner_idx[name_winner]],
1088
parent_trans_id, trans_id)
1327
name = names[self.winner_idx[name_winner]]
1328
if parent_id is not None or name is not None:
1329
# if we get here, name_winner and parent_winner are set to safe
1331
if parent_id is None and name is not None:
1332
# if parent_id is None and name is non-None, current file is
1334
if names[self.winner_idx[parent_id_winner]] != '':
1335
raise AssertionError(
1336
'File looks like a root, but named %s' %
1337
names[self.winner_idx[parent_id_winner]])
1338
parent_trans_id = transform.ROOT_PARENT
1340
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1341
self.tt.adjust_path(name, parent_trans_id,
1342
self.tt.trans_id_file_id(file_id))
1090
def merge_contents(self, file_id):
1344
def _do_merge_contents(self, file_id):
1091
1345
"""Performs a merge on file_id contents."""
1092
1346
def contents_pair(tree):
1093
if file_id not in tree:
1347
if not tree.has_id(file_id):
1094
1348
return (None, None)
1095
1349
kind = tree.kind(file_id)
1096
1350
if kind == "file":
1134
1376
if winner == 'this':
1135
1377
# No interesting changes introduced by OTHER
1136
1378
return "unmodified"
1379
# We have a hypothetical conflict, but if we have files, then we
1380
# can try to merge the content
1137
1381
trans_id = self.tt.trans_id_file_id(file_id)
1138
if winner == 'other':
1382
params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1383
other_pair[0], winner)
1384
hooks = self.active_hooks
1385
hook_status = 'not_applicable'
1387
hook_status, lines = hook.merge_contents(params)
1388
if hook_status != 'not_applicable':
1389
# Don't try any more hooks, this one applies.
1392
if hook_status == 'not_applicable':
1393
# This is a contents conflict, because none of the available
1394
# functions could merge it.
1396
name = self.tt.final_name(trans_id)
1397
parent_id = self.tt.final_parent(trans_id)
1398
if self.this_tree.has_id(file_id):
1399
self.tt.unversion_file(trans_id)
1400
file_group = self._dump_conflicts(name, parent_id, file_id,
1402
self._raw_conflicts.append(('contents conflict', file_group))
1403
elif hook_status == 'success':
1404
self.tt.create_file(lines, trans_id)
1405
elif hook_status == 'conflicted':
1406
# XXX: perhaps the hook should be able to provide
1407
# the BASE/THIS/OTHER files?
1408
self.tt.create_file(lines, trans_id)
1409
self._raw_conflicts.append(('text conflict', trans_id))
1410
name = self.tt.final_name(trans_id)
1411
parent_id = self.tt.final_parent(trans_id)
1412
self._dump_conflicts(name, parent_id, file_id)
1413
elif hook_status == 'delete':
1414
self.tt.unversion_file(trans_id)
1416
elif hook_status == 'done':
1417
# The hook function did whatever it needs to do directly, no
1418
# further action needed here.
1421
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1422
if not self.this_tree.has_id(file_id) and result == "modified":
1423
self.tt.version_file(file_id, trans_id)
1424
# The merge has been performed, so the old contents should not be
1426
self.tt.delete_contents(trans_id)
1429
def _default_other_winner_merge(self, merge_hook_params):
1430
"""Replace this contents with other."""
1431
file_id = merge_hook_params.file_id
1432
trans_id = merge_hook_params.trans_id
1433
file_in_this = self.this_tree.has_id(file_id)
1434
if self.other_tree.has_id(file_id):
1435
# OTHER changed the file
1437
if wt.supports_content_filtering():
1438
# We get the path from the working tree if it exists.
1439
# That fails though when OTHER is adding a file, so
1440
# we fall back to the other tree to find the path if
1441
# it doesn't exist locally.
1443
filter_tree_path = wt.id2path(file_id)
1444
except errors.NoSuchId:
1445
filter_tree_path = self.other_tree.id2path(file_id)
1447
# Skip the id2path lookup for older formats
1448
filter_tree_path = None
1449
transform.create_from_tree(self.tt, trans_id,
1450
self.other_tree, file_id,
1451
filter_tree_path=filter_tree_path)
1454
# OTHER deleted the file
1455
return 'delete', None
1457
raise AssertionError(
1458
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1461
def merge_contents(self, merge_hook_params):
1462
"""Fallback merge logic after user installed hooks."""
1463
# This function is used in merge hooks as the fallback instance.
1464
# Perhaps making this function and the functions it calls be a
1465
# a separate class would be better.
1466
if merge_hook_params.winner == 'other':
1139
1467
# OTHER is a straight winner, so replace this contents with other
1140
file_in_this = file_id in self.this_tree
1142
# Remove any existing contents
1143
self.tt.delete_contents(trans_id)
1144
if file_id in self.other_tree:
1145
# OTHER changed the file
1146
create_from_tree(self.tt, trans_id,
1147
self.other_tree, file_id)
1148
if not file_in_this:
1149
self.tt.version_file(file_id, trans_id)
1152
# OTHER deleted the file
1153
self.tt.unversion_file(trans_id)
1468
return self._default_other_winner_merge(merge_hook_params)
1469
elif merge_hook_params.is_file_merge():
1470
# THIS and OTHER are both files, so text merge. Either
1471
# BASE is a file, or both converted to files, so at least we
1472
# have agreement that output should be a file.
1474
self.text_merge(merge_hook_params.file_id,
1475
merge_hook_params.trans_id)
1476
except errors.BinaryFile:
1477
return 'not_applicable', None
1156
# We have a hypothetical conflict, but if we have files, then we
1157
# can try to merge the content
1158
if this_pair[0] == 'file' and other_pair[0] == 'file':
1159
# THIS and OTHER are both files, so text merge. Either
1160
# BASE is a file, or both converted to files, so at least we
1161
# have agreement that output should be a file.
1163
self.text_merge(file_id, trans_id)
1165
return contents_conflict()
1166
if file_id not in self.this_tree:
1167
self.tt.version_file(file_id, trans_id)
1169
self.tt.tree_kind(trans_id)
1170
self.tt.delete_contents(trans_id)
1175
return contents_conflict()
1480
return 'not_applicable', None
1177
1482
def get_lines(self, tree, file_id):
1178
1483
"""Return the lines in a file, or an empty list."""
1180
return tree.get_file(file_id).readlines()
1484
if tree.has_id(file_id):
1485
return tree.get_file_lines(file_id)
1233
1538
determined automatically. If set_version is true, the .OTHER, .THIS
1234
1539
or .BASE (in that order) will be created as versioned files.
1236
data = [('OTHER', self.other_tree, other_lines),
1541
data = [('OTHER', self.other_tree, other_lines),
1237
1542
('THIS', self.this_tree, this_lines)]
1238
1543
if not no_base:
1239
1544
data.append(('BASE', self.base_tree, base_lines))
1546
# We need to use the actual path in the working tree of the file here,
1547
# ignoring the conflict suffixes
1549
if wt.supports_content_filtering():
1551
filter_tree_path = wt.id2path(file_id)
1552
except errors.NoSuchId:
1553
# file has been deleted
1554
filter_tree_path = None
1556
# Skip the id2path lookup for older formats
1557
filter_tree_path = None
1240
1559
versioned = False
1241
1560
file_group = []
1242
1561
for suffix, tree, lines in data:
1562
if tree.has_id(file_id):
1244
1563
trans_id = self._conflict_file(name, parent_id, tree, file_id,
1564
suffix, lines, filter_tree_path)
1246
1565
file_group.append(trans_id)
1247
1566
if set_version and not versioned:
1248
1567
self.tt.version_file(file_id, trans_id)
1249
1568
versioned = True
1250
1569
return file_group
1252
1571
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
1572
lines=None, filter_tree_path=None):
1254
1573
"""Emit a single conflict file."""
1255
1574
name = name + '.' + suffix
1256
1575
trans_id = self.tt.create_path(name, parent_id)
1257
create_from_tree(self.tt, trans_id, tree, file_id, lines)
1576
transform.create_from_tree(self.tt, trans_id, tree, file_id, lines,
1258
1578
return trans_id
1260
1580
def merge_executable(self, file_id, file_status):
1302
1619
def cook_conflicts(self, fs_conflicts):
1303
1620
"""Convert all conflicts into a form that doesn't depend on trans_id"""
1304
from conflicts import Conflict
1306
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
1307
fp = FinalPaths(self.tt)
1621
content_conflict_file_ids = set()
1622
cooked_conflicts = transform.cook_conflicts(fs_conflicts, self.tt)
1623
fp = transform.FinalPaths(self.tt)
1308
1624
for conflict in self._raw_conflicts:
1309
1625
conflict_type = conflict[0]
1310
if conflict_type in ('name conflict', 'parent conflict'):
1311
trans_id = conflict[1]
1312
conflict_args = conflict[2:]
1313
if trans_id not in name_conflicts:
1314
name_conflicts[trans_id] = {}
1315
unique_add(name_conflicts[trans_id], conflict_type,
1317
if conflict_type == 'contents conflict':
1626
if conflict_type == 'path conflict':
1628
this_parent, this_name,
1629
other_parent, other_name) = conflict[1:]
1630
if this_parent is None or this_name is None:
1631
this_path = '<deleted>'
1633
parent_path = fp.get_path(
1634
self.tt.trans_id_file_id(this_parent))
1635
this_path = osutils.pathjoin(parent_path, this_name)
1636
if other_parent is None or other_name is None:
1637
other_path = '<deleted>'
1639
if other_parent == self.other_tree.get_root_id():
1640
# The tree transform doesn't know about the other root,
1641
# so we special case here to avoid a NoFinalPath
1645
parent_path = fp.get_path(
1646
self.tt.trans_id_file_id(other_parent))
1647
other_path = osutils.pathjoin(parent_path, other_name)
1648
c = _mod_conflicts.Conflict.factory(
1649
'path conflict', path=this_path,
1650
conflict_path=other_path,
1652
elif conflict_type == 'contents conflict':
1318
1653
for trans_id in conflict[1]:
1319
1654
file_id = self.tt.final_file_id(trans_id)
1320
1655
if file_id is not None:
1324
1659
if path.endswith(suffix):
1325
1660
path = path[:-len(suffix)]
1327
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1328
self.cooked_conflicts.append(c)
1329
if conflict_type == 'text conflict':
1662
c = _mod_conflicts.Conflict.factory(conflict_type,
1663
path=path, file_id=file_id)
1664
content_conflict_file_ids.add(file_id)
1665
elif conflict_type == 'text conflict':
1330
1666
trans_id = conflict[1]
1331
1667
path = fp.get_path(trans_id)
1332
1668
file_id = self.tt.final_file_id(trans_id)
1333
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1334
self.cooked_conflicts.append(c)
1669
c = _mod_conflicts.Conflict.factory(conflict_type,
1670
path=path, file_id=file_id)
1672
raise AssertionError('bad conflict type: %r' % (conflict,))
1673
cooked_conflicts.append(c)
1336
for trans_id, conflicts in name_conflicts.iteritems():
1338
this_parent, other_parent = conflicts['parent conflict']
1339
if this_parent == other_parent:
1340
raise AssertionError()
1342
this_parent = other_parent = \
1343
self.tt.final_file_id(self.tt.final_parent(trans_id))
1345
this_name, other_name = conflicts['name conflict']
1346
if this_name == other_name:
1347
raise AssertionError()
1349
this_name = other_name = self.tt.final_name(trans_id)
1350
other_path = fp.get_path(trans_id)
1351
if this_parent is not None and this_name is not None:
1352
this_parent_path = \
1353
fp.get_path(self.tt.trans_id_file_id(this_parent))
1354
this_path = pathjoin(this_parent_path, this_name)
1356
this_path = "<deleted>"
1357
file_id = self.tt.final_file_id(trans_id)
1358
c = Conflict.factory('path conflict', path=this_path,
1359
conflict_path=other_path, file_id=file_id)
1675
self.cooked_conflicts = []
1676
# We want to get rid of path conflicts when a corresponding contents
1677
# conflict exists. This can occur when one branch deletes a file while
1678
# the other renames *and* modifies it. In this case, the content
1679
# conflict is enough.
1680
for c in cooked_conflicts:
1681
if (c.typestring == 'path conflict'
1682
and c.file_id in content_conflict_file_ids):
1360
1684
self.cooked_conflicts.append(c)
1361
self.cooked_conflicts.sort(key=Conflict.sort_key)
1685
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1364
1688
class WeaveMerger(Merge3Merger):
1368
1692
supports_reverse_cherrypick = False
1369
1693
history_based = True
1371
def _merged_lines(self, file_id):
1372
"""Generate the merged lines.
1373
There is no distinction between lines that are meant to contain <<<<<<<
1377
base = self.base_tree
1380
plan = self.this_tree.plan_file_merge(file_id, self.other_tree,
1695
def _generate_merge_plan(self, file_id, base):
1696
return self.this_tree.plan_file_merge(file_id, self.other_tree,
1699
def _merged_lines(self, file_id):
1700
"""Generate the merged lines.
1701
There is no distinction between lines that are meant to contain <<<<<<<
1705
base = self.base_tree
1708
plan = self._generate_merge_plan(file_id, base)
1382
1709
if 'merge' in debug.debug_flags:
1383
1710
plan = list(plan)
1384
1711
trans_id = self.tt.trans_id_file_id(file_id)
1385
1712
name = self.tt.final_name(trans_id) + '.plan'
1386
contents = ('%10s|%s' % l for l in plan)
1713
contents = ('%11s|%s' % l for l in plan)
1387
1714
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1388
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1389
'>>>>>>> MERGE-SOURCE\n')
1390
return textmerge.merge_lines(self.reprocess)
1715
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1716
'>>>>>>> MERGE-SOURCE\n')
1717
lines, conflicts = textmerge.merge_lines(self.reprocess)
1719
base_lines = textmerge.base_from_plan()
1722
return lines, base_lines
1392
1724
def text_merge(self, file_id, trans_id):
1393
1725
"""Perform a (weave) text merge for a given file and file-id.
1394
1726
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1395
1727
and a conflict will be noted.
1397
lines, conflicts = self._merged_lines(file_id)
1729
lines, base_lines = self._merged_lines(file_id)
1398
1730
lines = list(lines)
1399
# Note we're checking whether the OUTPUT is binary in this case,
1731
# Note we're checking whether the OUTPUT is binary in this case,
1400
1732
# because we don't want to get into weave merge guts.
1401
check_text_lines(lines)
1733
textfile.check_text_lines(lines)
1402
1734
self.tt.create_file(lines, trans_id)
1735
if base_lines is not None:
1404
1737
self._raw_conflicts.append(('text conflict', trans_id))
1405
1738
name = self.tt.final_name(trans_id)
1406
1739
parent_id = self.tt.final_parent(trans_id)
1407
file_group = self._dump_conflicts(name, parent_id, file_id,
1740
file_group = self._dump_conflicts(name, parent_id, file_id,
1742
base_lines=base_lines)
1409
1743
file_group.append(trans_id)
1412
1746
class LCAMerger(WeaveMerger):
1414
def _merged_lines(self, file_id):
1415
"""Generate the merged lines.
1416
There is no distinction between lines that are meant to contain <<<<<<<
1420
base = self.base_tree
1423
plan = self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1748
def _generate_merge_plan(self, file_id, base):
1749
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1425
if 'merge' in debug.debug_flags:
1427
trans_id = self.tt.trans_id_file_id(file_id)
1428
name = self.tt.final_name(trans_id) + '.plan'
1429
contents = ('%10s|%s' % l for l in plan)
1430
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1431
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1432
'>>>>>>> MERGE-SOURCE\n')
1433
return textmerge.merge_lines(self.reprocess)
1436
1752
class Diff3Merger(Merge3Merger):
1437
1753
"""Three-way merger using external diff3 for text merging"""
1439
1755
def dump_file(self, temp_dir, name, tree, file_id):
1440
out_path = pathjoin(temp_dir, name)
1756
out_path = osutils.pathjoin(temp_dir, name)
1441
1757
out_file = open(out_path, "wb")
1443
1759
in_file = tree.get_file(file_id)
1476
1792
osutils.rmtree(temp_dir)
1795
class PathNotInTree(errors.BzrError):
1797
_fmt = """Merge-into failed because %(tree)s does not contain %(path)s."""
1799
def __init__(self, path, tree):
1800
errors.BzrError.__init__(self, path=path, tree=tree)
1803
class MergeIntoMerger(Merger):
1804
"""Merger that understands other_tree will be merged into a subdir.
1806
This also changes the Merger api so that it uses real Branch, revision_id,
1807
and RevisonTree objects, rather than using revision specs.
1810
def __init__(self, this_tree, other_branch, other_tree, target_subdir,
1811
source_subpath, other_rev_id=None):
1812
"""Create a new MergeIntoMerger object.
1814
source_subpath in other_tree will be effectively copied to
1815
target_subdir in this_tree.
1817
:param this_tree: The tree that we will be merging into.
1818
:param other_branch: The Branch we will be merging from.
1819
:param other_tree: The RevisionTree object we want to merge.
1820
:param target_subdir: The relative path where we want to merge
1821
other_tree into this_tree
1822
:param source_subpath: The relative path specifying the subtree of
1823
other_tree to merge into this_tree.
1825
# It is assumed that we are merging a tree that is not in our current
1826
# ancestry, which means we are using the "EmptyTree" as our basis.
1827
null_ancestor_tree = this_tree.branch.repository.revision_tree(
1828
_mod_revision.NULL_REVISION)
1829
super(MergeIntoMerger, self).__init__(
1830
this_branch=this_tree.branch,
1831
this_tree=this_tree,
1832
other_tree=other_tree,
1833
base_tree=null_ancestor_tree,
1835
self._target_subdir = target_subdir
1836
self._source_subpath = source_subpath
1837
self.other_branch = other_branch
1838
if other_rev_id is None:
1839
other_rev_id = other_tree.get_revision_id()
1840
self.other_rev_id = self.other_basis = other_rev_id
1841
self.base_is_ancestor = True
1842
self.backup_files = True
1843
self.merge_type = Merge3Merger
1844
self.show_base = False
1845
self.reprocess = False
1846
self.interesting_ids = None
1847
self.merge_type = _MergeTypeParameterizer(MergeIntoMergeType,
1848
target_subdir=self._target_subdir,
1849
source_subpath=self._source_subpath)
1850
if self._source_subpath != '':
1851
# If this isn't a partial merge make sure the revisions will be
1853
self._maybe_fetch(self.other_branch, self.this_branch,
1856
def set_pending(self):
1857
if self._source_subpath != '':
1859
Merger.set_pending(self)
1862
class _MergeTypeParameterizer(object):
1863
"""Wrap a merge-type class to provide extra parameters.
1865
This is hack used by MergeIntoMerger to pass some extra parameters to its
1866
merge_type. Merger.do_merge() sets up its own set of parameters to pass to
1867
the 'merge_type' member. It is difficult override do_merge without
1868
re-writing the whole thing, so instead we create a wrapper which will pass
1869
the extra parameters.
1872
def __init__(self, merge_type, **kwargs):
1873
self._extra_kwargs = kwargs
1874
self._merge_type = merge_type
1876
def __call__(self, *args, **kwargs):
1877
kwargs.update(self._extra_kwargs)
1878
return self._merge_type(*args, **kwargs)
1880
def __getattr__(self, name):
1881
return getattr(self._merge_type, name)
1884
class MergeIntoMergeType(Merge3Merger):
1885
"""Merger that incorporates a tree (or part of a tree) into another."""
1887
def __init__(self, *args, **kwargs):
1888
"""Initialize the merger object.
1890
:param args: See Merge3Merger.__init__'s args.
1891
:param kwargs: See Merge3Merger.__init__'s keyword args, except for
1892
source_subpath and target_subdir.
1893
:keyword source_subpath: The relative path specifying the subtree of
1894
other_tree to merge into this_tree.
1895
:keyword target_subdir: The relative path where we want to merge
1896
other_tree into this_tree
1898
# All of the interesting work happens during Merge3Merger.__init__(),
1899
# so we have have to hack in to get our extra parameters set.
1900
self._source_subpath = kwargs.pop('source_subpath')
1901
self._target_subdir = kwargs.pop('target_subdir')
1902
super(MergeIntoMergeType, self).__init__(*args, **kwargs)
1904
def _compute_transform(self):
1905
child_pb = ui.ui_factory.nested_progress_bar()
1907
entries = self._entries_to_incorporate()
1908
entries = list(entries)
1909
for num, (entry, parent_id) in enumerate(entries):
1910
child_pb.update(gettext('Preparing file merge'), num, len(entries))
1911
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1912
trans_id = transform.new_by_entry(self.tt, entry,
1913
parent_trans_id, self.other_tree)
1916
self._finish_computing_transform()
1918
def _entries_to_incorporate(self):
1919
"""Yields pairs of (inventory_entry, new_parent)."""
1920
other_inv = self.other_tree.inventory
1921
subdir_id = other_inv.path2id(self._source_subpath)
1922
if subdir_id is None:
1923
# XXX: The error would be clearer if it gave the URL of the source
1924
# branch, but we don't have a reference to that here.
1925
raise PathNotInTree(self._source_subpath, "Source tree")
1926
subdir = other_inv[subdir_id]
1927
parent_in_target = osutils.dirname(self._target_subdir)
1928
target_id = self.this_tree.inventory.path2id(parent_in_target)
1929
if target_id is None:
1930
raise PathNotInTree(self._target_subdir, "Target tree")
1931
name_in_target = osutils.basename(self._target_subdir)
1932
merge_into_root = subdir.copy()
1933
merge_into_root.name = name_in_target
1934
if self.this_tree.inventory.has_id(merge_into_root.file_id):
1935
# Give the root a new file-id.
1936
# This can happen fairly easily if the directory we are
1937
# incorporating is the root, and both trees have 'TREE_ROOT' as
1938
# their root_id. Users will expect this to Just Work, so we
1939
# change the file-id here.
1940
# Non-root file-ids could potentially conflict too. That's really
1941
# an edge case, so we don't do anything special for those. We let
1942
# them cause conflicts.
1943
merge_into_root.file_id = generate_ids.gen_file_id(name_in_target)
1944
yield (merge_into_root, target_id)
1945
if subdir.kind != 'directory':
1946
# No children, so we are done.
1948
for ignored_path, entry in other_inv.iter_entries_by_dir(subdir_id):
1949
parent_id = entry.parent_id
1950
if parent_id == subdir.file_id:
1951
# The root's parent ID has changed, so make sure children of
1952
# the root refer to the new ID.
1953
parent_id = merge_into_root.file_id
1954
yield (entry, parent_id)
1479
1957
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
1480
1958
backup_files=False,
1481
1959
merge_type=Merge3Merger,