1
# Copyright (C) 2005, 2006 Canonical Ltd
1
# Copyright (C) 2005 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 shutil import rmtree
20
21
from tempfile import mkdtemp
23
24
from bzrlib.branch import Branch
24
from bzrlib.conflicts import ConflictList, Conflict
25
25
from bzrlib.delta import compare_trees
26
26
from bzrlib.errors import (BzrCommandError,
36
35
WorkingTreeNotRevision,
39
37
from bzrlib.merge3 import Merge3
40
38
import bzrlib.osutils
41
from bzrlib.osutils import rename, pathjoin, rmtree
42
from progress import DummyProgress, ProgressPhase
39
from bzrlib.osutils import rename, pathjoin
40
from progress import DummyProgress
43
41
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
44
from bzrlib.textfile import check_text_lines
42
from bzrlib.symbol_versioning import *
45
43
from bzrlib.trace import mutter, warning, note
46
44
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
47
FinalPaths, create_by_entry, unique_add)
48
from bzrlib.versionedfile import WeaveMerge
45
conflicts_strings, FinalPaths, create_by_entry,
51
48
# TODO: Report back as changes are merged in
119
114
changes = compare_trees(self.other_tree, other_basis_tree)
120
115
if changes.has_changed():
121
116
raise WorkingTreeNotRevision(self.this_tree)
122
other_rev_id = self.other_basis
117
other_rev_id = other_basis
123
118
self.other_tree = other_basis_tree
125
120
def file_revisions(self, file_id):
136
131
trees = (self.this_basis_tree, self.other_tree)
137
132
return [get_id(tree, file_id) for tree in trees]
139
def check_basis(self, check_clean, require_commits=True):
140
if self.this_basis is None and require_commits is True:
134
def check_basis(self, check_clean):
135
if self.this_basis is None:
141
136
raise BzrCommandError("This branch has no commits")
143
138
self.compare_basis()
204
199
if other_branch.base != self.this_branch.base:
205
200
self.this_branch.fetch(other_branch, last_revision=self.other_basis)
208
self.set_base([None, None])
210
202
def set_base(self, base_revision):
211
203
mutter("doing merge() with no base_revision specified")
212
204
if base_revision == [None, None]:
214
pb = bzrlib.ui.ui_factory.nested_progress_bar()
216
this_repo = self.this_branch.repository
217
self.base_rev_id = common_ancestor(self.this_basis,
206
self.base_rev_id = common_ancestor(self.this_basis,
208
self.this_branch.repository,
222
210
except NoCommonAncestor:
223
211
raise UnrelatedBranches()
224
212
self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
232
220
self.base_rev_id = None
234
222
self.base_rev_id = base_branch.get_rev_id(base_revision[1])
235
if self.this_branch.base != base_branch.base:
236
self.this_branch.fetch(base_branch)
223
self.this_branch.fetch(base_branch)
237
224
self.base_is_ancestor = is_ancestor(self.this_basis,
238
225
self.base_rev_id,
239
226
self.this_branch)
241
228
def do_merge(self):
242
229
kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree,
243
'other_tree': self.other_tree,
244
'interesting_ids': self.interesting_ids,
230
'other_tree': self.other_tree}
246
231
if self.merge_type.requires_base:
247
232
kwargs['base_tree'] = self.base_tree
248
233
if self.merge_type.supports_reprocess:
249
234
kwargs['reprocess'] = self.reprocess
250
235
elif self.reprocess:
251
raise BzrError("Conflict reduction is not supported for merge"
252
" type %s." % self.merge_type)
236
raise BzrError("Reprocess is not supported for this merge"
237
" type. %s" % merge_type)
253
238
if self.merge_type.supports_show_base:
254
239
kwargs['show_base'] = self.show_base
255
240
elif self.show_base:
286
271
for file_id in old_entries:
287
272
entry = old_entries[file_id]
288
273
path = id2path(file_id)
289
if file_id in self.base_tree.inventory:
290
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
292
executable = getattr(entry, 'executable', False)
293
274
new_inventory[file_id] = (path, file_id, entry.parent_id,
294
entry.kind, executable)
296
276
by_path[path] = file_id
315
295
parent = by_path[os.path.dirname(path)]
316
296
abspath = pathjoin(self.this_tree.basedir, path)
317
297
kind = bzrlib.osutils.file_kind(abspath)
318
if file_id in self.base_tree.inventory:
319
executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
322
new_inventory[file_id] = (path, file_id, parent, kind, executable)
298
new_inventory[file_id] = (path, file_id, parent, kind)
323
299
by_path[path] = file_id
325
301
# Get a list in insertion order
342
318
history_based = False
344
320
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):
321
reprocess=False, show_base=False, pb=DummyProgress()):
347
322
"""Initialize the merger object and perform the merge."""
348
323
object.__init__(self)
349
324
self.this_tree = working_tree
354
329
self.reprocess = reprocess
355
330
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()
333
all_ids = set(base_tree)
334
all_ids.update(other_tree)
367
335
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)
337
for num, file_id in enumerate(all_ids):
338
self.pb.update('Preparing file merge', num+1, len(all_ids))
339
self.merge_names(file_id)
340
file_status = self.merge_contents(file_id)
341
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)
344
fs_conflicts = resolve_conflicts(self.tt, self.pb)
386
345
self.cook_conflicts(fs_conflicts)
387
for conflict in self.cooked_conflicts:
390
results = self.tt.apply()
391
self.write_modified(results)
346
for line in conflicts_strings(self.cooked_conflicts):
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
356
def parent(entry, file_id):
415
357
"""Determine the parent for a file_id (used as a key method)"""
529
471
return kind, contents
531
def contents_conflict():
532
trans_id = self.tt.trans_id_file_id(file_id)
533
name = self.tt.final_name(trans_id)
534
parent_id = self.tt.final_parent(trans_id)
535
if file_id in self.this_tree.inventory:
536
self.tt.unversion_file(trans_id)
537
self.tt.delete_contents(trans_id)
538
file_group = self._dump_conflicts(name, parent_id, file_id,
540
self._raw_conflicts.append(('contents conflict', file_group))
542
472
# See SPOT run. run, SPOT, run.
543
473
# So we're not QUITE repeating ourselves; we do tricky things with
575
505
# THIS and OTHER are both files, so text merge. Either
576
506
# BASE is a file, or both converted to files, so at least we
577
507
# have agreement that output should be a file.
579
self.text_merge(file_id, trans_id)
581
return contents_conflict()
582
508
if file_id not in self.this_tree.inventory:
583
509
self.tt.version_file(file_id, trans_id)
510
self.text_merge(file_id, trans_id)
585
512
self.tt.tree_kind(trans_id)
586
513
self.tt.delete_contents(trans_id)
589
516
return "modified"
591
518
# Scalar conflict, can't text merge. Dump conflicts
592
return contents_conflict()
519
trans_id = self.tt.trans_id_file_id(file_id)
520
name = self.tt.final_name(trans_id)
521
parent_id = self.tt.final_parent(trans_id)
522
if file_id in self.this_tree.inventory:
523
self.tt.unversion_file(trans_id)
524
self.tt.delete_contents(trans_id)
525
file_group = self._dump_conflicts(name, parent_id, file_id,
527
self._raw_conflicts.append(('contents conflict', file_group))
594
529
def get_lines(self, tree, file_id):
595
530
"""Return the lines in a file, or an empty list."""
690
625
if winner == "conflict":
691
626
# There must be a None in here, if we have a conflict, but we
692
627
# need executability since file status was not deleted.
693
if self.executable(self.other_tree, file_id) is None:
628
if self.other_tree.is_executable(file_id) is None:
737
671
if path.endswith(suffix):
738
672
path = path[:-len(suffix)]
740
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
741
self.cooked_conflicts.append(c)
674
self.cooked_conflicts.append((conflict_type, file_id, path))
742
675
if conflict_type == 'text conflict':
743
676
trans_id = conflict[1]
744
677
path = fp.get_path(trans_id)
745
678
file_id = self.tt.final_file_id(trans_id)
746
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
747
self.cooked_conflicts.append(c)
679
self.cooked_conflicts.append((conflict_type, file_id, path))
749
681
for trans_id, conflicts in name_conflicts.iteritems():
767
699
this_path = "<deleted>"
768
700
file_id = self.tt.final_file_id(trans_id)
769
c = Conflict.factory('path conflict', path=this_path,
770
conflict_path=other_path, file_id=file_id)
771
self.cooked_conflicts.append(c)
772
self.cooked_conflicts.sort(key=Conflict.sort_key)
701
self.cooked_conflicts.append(('path conflict', file_id, this_path,
775
705
class WeaveMerger(Merge3Merger):
776
706
"""Three-way tree merger, text weave merger."""
777
supports_reprocess = True
707
supports_reprocess = False
778
708
supports_show_base = False
780
710
def __init__(self, working_tree, this_tree, base_tree, other_tree,
781
interesting_ids=None, pb=DummyProgress(), pp=None,
783
712
self.this_revision_tree = self._get_revision_tree(this_tree)
784
713
self.other_revision_tree = self._get_revision_tree(other_tree)
785
714
super(WeaveMerger, self).__init__(working_tree, this_tree,
786
base_tree, other_tree,
787
interesting_ids=interesting_ids,
788
pb=pb, pp=pp, reprocess=reprocess)
715
base_tree, other_tree, pb=pb)
790
717
def _get_revision_tree(self, tree):
791
"""Return a revision tree related to this tree.
718
"""Return a revision tree releated to this tree.
792
719
If the tree is a WorkingTree, the basis will be returned.
794
721
if getattr(tree, 'get_weave', False) is False:
815
742
this_revision_id = self.this_revision_tree.inventory[file_id].revision
816
743
other_revision_id = \
817
744
self.other_revision_tree.inventory[file_id].revision
818
wm = WeaveMerge(weave, this_revision_id, other_revision_id,
819
'<<<<<<< TREE\n', '>>>>>>> MERGE-SOURCE\n')
820
return wm.merge_lines(self.reprocess)
745
this_i = weave.lookup(this_revision_id)
746
other_i = weave.lookup(other_revision_id)
747
plan = weave.plan_merge(this_i, other_i)
748
return weave.weave_merge(plan, '<<<<<<< TREE\n',
749
'>>>>>>> MERGE-SOURCE\n')
822
751
def text_merge(self, file_id, trans_id):
823
752
"""Perform a (weave) text merge for a given file and file-id.
825
754
and a conflict will be noted.
827
756
self._check_file(file_id)
828
lines, conflicts = self._merged_lines(file_id)
830
# Note we're checking whether the OUTPUT is binary in this case,
831
# because we don't want to get into weave merge guts.
832
check_text_lines(lines)
757
lines = self._merged_lines(file_id)
758
conflicts = '<<<<<<< TREE\n' in lines
833
759
self.tt.create_file(lines, trans_id)
835
761
self._raw_conflicts.append(('text conflict', trans_id))
843
769
class Diff3Merger(Merge3Merger):
844
770
"""Three-way merger using external diff3 for text merging"""
846
771
def dump_file(self, temp_dir, name, tree, file_id):
847
772
out_path = pathjoin(temp_dir, name)
848
out_file = open(out_path, "wb")
850
in_file = tree.get_file(file_id)
773
out_file = file(out_path, "wb")
774
in_file = tree.get_file(file_id)
857
779
def text_merge(self, file_id, trans_id):
869
791
status = bzrlib.patch.diff3(new_file, this, base, other)
870
792
if status not in (0, 1):
871
793
raise BzrError("Unhandled diff3 exit code")
872
f = open(new_file, 'rb')
874
self.tt.create_file(f, trans_id)
794
self.tt.create_file(file(new_file, "rb"), trans_id)
878
796
name = self.tt.final_name(trans_id)
879
797
parent_id = self.tt.final_parent(trans_id)
926
843
"diff3": (Diff3Merger, "Merge using external diff3"),
927
844
'weave': (WeaveMerger, "Weave-based merge")
931
def merge_type_help():
932
templ = '%s%%7s: %%s' % (' '*12)
933
lines = [templ % (f[0], f[1][1]) for f in merge_types.iteritems()]
934
return '\n'.join(lines)