~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

Hid raw conflicts

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Canonical Ltd
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
# TODO: build_working_dir can be built on something simpler than merge()
 
18
 
 
19
import os
 
20
import errno
 
21
 
 
22
import bzrlib
 
23
from bzrlib.branch import Branch
 
24
from bzrlib.delta import compare_trees
 
25
from bzrlib.errors import (BzrCommandError,
 
26
                           BzrError,
 
27
                           NoCommonAncestor,
 
28
                           NoCommits,
 
29
                           NoSuchRevision,
 
30
                           NotBranchError,
 
31
                           NotVersionedError,
 
32
                           UnrelatedBranches,
 
33
                           WorkingTreeNotRevision,
 
34
                           )
 
35
from bzrlib.fetch import greedy_fetch, fetch
 
36
import bzrlib.osutils
 
37
from bzrlib.osutils import rename, pathjoin
 
38
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
 
39
from bzrlib.transform import (Merge3Merger as ApplyMerge3, WeaveMerger, 
 
40
                              Diff3Merger)
 
41
from bzrlib.trace import mutter, warning, note
 
42
from bzrlib.workingtree import WorkingTree
 
43
 
 
44
# TODO: Report back as changes are merged in
 
45
 
 
46
# comments from abentley on irc: merge happens in two stages, each
 
47
# of which generates a changeset object
 
48
 
 
49
# stage 1: generate OLD->OTHER,
 
50
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
 
51
 
 
52
def _get_tree(treespec, local_branch=None):
 
53
    location, revno = treespec
 
54
    branch = Branch.open_containing(location)[0]
 
55
    if revno is None:
 
56
        revision = None
 
57
    elif revno == -1:
 
58
        revision = branch.last_revision()
 
59
    else:
 
60
        revision = branch.get_rev_id(revno)
 
61
        if revision is None:
 
62
            revision = NULL_REVISION
 
63
    return branch, _get_revid_tree(branch, revision, local_branch)
 
64
 
 
65
 
 
66
def _get_revid_tree(branch, revision, local_branch):
 
67
    if revision is None:
 
68
        base_tree = branch.working_tree()
 
69
    else:
 
70
        if local_branch is not None:
 
71
            if local_branch.base != branch.base:
 
72
                greedy_fetch(local_branch, branch, revision)
 
73
            base_tree = local_branch.repository.revision_tree(revision)
 
74
        else:
 
75
            base_tree = branch.repository.revision_tree(revision)
 
76
    return base_tree
 
77
 
 
78
 
 
79
def build_working_dir(to_dir):
 
80
    """Build a working directory in an empty directory.
 
81
 
 
82
    to_dir is a directory containing branch metadata but no working files,
 
83
    typically constructed by cloning an existing branch. 
 
84
 
 
85
    This is split out as a special idiomatic case of merge.  It could
 
86
    eventually be done by just building the tree directly calling into 
 
87
    lower-level code (e.g. constructing a changeset).
 
88
    """
 
89
    # RBC 20051019 is this not just 'export' ?
 
90
    # AB Well, export doesn't take care of inventory...
 
91
    from transform import build_tree
 
92
    this_branch = Branch.open_containing(to_dir)[0]
 
93
    build_tree(this_branch, this_branch.basis_tree())
 
94
 
 
95
 
 
96
def transform_tree(from_tree, to_tree, interesting_ids=None):
 
97
    merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
 
98
                interesting_ids=interesting_ids)
 
99
 
 
100
 
 
101
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
 
102
                backup_files=False, 
 
103
                merge_type=ApplyMerge3, 
 
104
                interesting_ids=None, 
 
105
                show_base=False, 
 
106
                reprocess=False, 
 
107
                other_rev_id=None,
 
108
                interesting_files=None,
 
109
                this_tree=None):
 
110
    """Primary interface for merging. 
 
111
 
 
112
        typical use is probably 
 
113
        'merge_inner(branch, branch.get_revision_tree(other_revision),
 
114
                     branch.get_revision_tree(base_revision))'
 
115
        """
 
116
    if this_tree is None:
 
117
        this_tree = this_branch.working_tree()
 
118
    merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree)
 
119
    merger.backup_files = backup_files
 
120
    merger.merge_type = merge_type
 
121
    merger.interesting_ids = interesting_ids
 
122
    if interesting_files:
 
123
        assert not interesting_ids, ('Only supply interesting_ids'
 
124
                                     ' or interesting_files')
 
125
        merger._set_interesting_files(interesting_files)
 
126
    merger.show_base = show_base 
 
127
    merger.reprocess = reprocess
 
128
    merger.other_rev_id = other_rev_id
 
129
    merger.other_basis = other_rev_id
 
130
    return merger.do_merge()
 
131
 
 
132
 
 
133
class Merger(object):
 
134
    def __init__(self, this_branch, other_tree=None, base_tree=None, this_tree=None):
 
135
        object.__init__(self)
 
136
        assert this_tree is not None, "this_tree is required"
 
137
        self.this_branch = this_branch
 
138
        self.this_basis = this_branch.last_revision()
 
139
        self.this_rev_id = None
 
140
        self.this_tree = this_tree
 
141
        self.this_revision_tree = None
 
142
        self.this_basis_tree = None
 
143
        self.other_tree = other_tree
 
144
        self.base_tree = base_tree
 
145
        self.ignore_zero = False
 
146
        self.backup_files = False
 
147
        self.interesting_ids = None
 
148
        self.show_base = False
 
149
        self.reprocess = False
 
150
 
 
151
    def revision_tree(self, revision_id):
 
152
        return self.this_branch.repository.revision_tree(revision_id)
 
153
 
 
154
    def ensure_revision_trees(self):
 
155
        if self.this_revision_tree is None:
 
156
            self.this_basis_tree = self.this_branch.repository.revision_tree(
 
157
                self.this_basis)
 
158
            if self.this_basis == self.this_rev_id:
 
159
                self.this_revision_tree = self.this_basis_tree
 
160
 
 
161
        if self.other_rev_id is None:
 
162
            other_basis_tree = self.revision_tree(self.other_basis)
 
163
            changes = compare_trees(self.other_tree, other_basis_tree)
 
164
            if changes.has_changed():
 
165
                raise WorkingTreeNotRevision(self.this_tree)
 
166
            other_rev_id = other_basis
 
167
            self.other_tree = other_basis_tree
 
168
 
 
169
    def file_revisions(self, file_id):
 
170
        self.ensure_revision_trees()
 
171
        def get_id(tree, file_id):
 
172
            revision_id = tree.inventory[file_id].revision
 
173
            assert revision_id is not None
 
174
            return revision_id
 
175
        if self.this_rev_id is None:
 
176
            if self.this_basis_tree.get_file_sha1(file_id) != \
 
177
                self.this_tree.get_file_sha1(file_id):
 
178
                raise WorkingTreeNotRevision(self.this_tree)
 
179
 
 
180
        trees = (self.this_basis_tree, self.other_tree)
 
181
        return [get_id(tree, file_id) for tree in trees]
 
182
 
 
183
    def merge_factory(self, file_id, base, other):
 
184
        if self.merge_type.history_based:
 
185
            if self.show_base is True:
 
186
                raise BzrError("Cannot show base for hisory-based merges")
 
187
            if self.reprocess is True:
 
188
                raise BzrError("Cannot reprocess history-based merges")
 
189
                
 
190
            t_revid, o_revid = self.file_revisions(file_id)
 
191
            weave = self.this_basis_tree.get_weave(file_id)
 
192
            contents_change = self.merge_type(weave, t_revid, o_revid)
 
193
        else:
 
194
            if self.show_base is True or self.reprocess is True:
 
195
                contents_change = self.merge_type(file_id, base, other, 
 
196
                                                  show_base=self.show_base, 
 
197
                                                  reprocess=self.reprocess)
 
198
            else:
 
199
                contents_change = self.merge_type(file_id, base, other)
 
200
        if self.backup_files:
 
201
            contents_change = BackupBeforeChange(contents_change)
 
202
        return contents_change
 
203
 
 
204
    def check_basis(self, check_clean):
 
205
        if self.this_basis is None:
 
206
            raise BzrCommandError("This branch has no commits")
 
207
        if check_clean:
 
208
            self.compare_basis()
 
209
            if self.this_basis != self.this_rev_id:
 
210
                raise BzrCommandError("Working tree has uncommitted changes.")
 
211
 
 
212
    def compare_basis(self):
 
213
        changes = compare_trees(self.this_tree, 
 
214
                                self.this_branch.basis_tree(), False)
 
215
        if not changes.has_changed():
 
216
            self.this_rev_id = self.this_basis
 
217
 
 
218
    def set_interesting_files(self, file_list):
 
219
        try:
 
220
            self._set_interesting_files(file_list)
 
221
        except NotVersionedError, e:
 
222
            raise BzrCommandError("%s is not a source file in any"
 
223
                                      " tree." % e.path)
 
224
 
 
225
    def _set_interesting_files(self, file_list):
 
226
        """Set the list of interesting ids from a list of files."""
 
227
        if file_list is None:
 
228
            self.interesting_ids = None
 
229
            return
 
230
 
 
231
        interesting_ids = set()
 
232
        for path in file_list:
 
233
            found_id = False
 
234
            for tree in (self.this_tree, self.base_tree, self.other_tree):
 
235
                file_id = tree.inventory.path2id(path)
 
236
                if file_id is not None:
 
237
                    interesting_ids.add(file_id)
 
238
                    found_id = True
 
239
            if not found_id:
 
240
                raise NotVersionedError(path=path)
 
241
        self.interesting_ids = interesting_ids
 
242
 
 
243
    def set_pending(self):
 
244
        if not self.base_is_ancestor:
 
245
            return
 
246
        if self.other_rev_id is None:
 
247
            return
 
248
        ancestry = self.this_branch.repository.get_ancestry(self.this_basis)
 
249
        if self.other_rev_id in ancestry:
 
250
            return
 
251
        self.this_tree.add_pending_merge(self.other_rev_id)
 
252
 
 
253
    def set_other(self, other_revision):
 
254
        other_branch, self.other_tree = _get_tree(other_revision, 
 
255
                                                  self.this_branch)
 
256
        if other_revision[1] == -1:
 
257
            self.other_rev_id = other_branch.last_revision()
 
258
            if self.other_rev_id is None:
 
259
                raise NoCommits(other_branch)
 
260
            self.other_basis = self.other_rev_id
 
261
        elif other_revision[1] is not None:
 
262
            self.other_rev_id = other_branch.get_rev_id(other_revision[1])
 
263
            self.other_basis = self.other_rev_id
 
264
        else:
 
265
            self.other_rev_id = None
 
266
            self.other_basis = other_branch.last_revision()
 
267
            if self.other_basis is None:
 
268
                raise NoCommits(other_branch)
 
269
        if other_branch.base != self.this_branch.base:
 
270
            fetch(from_branch=other_branch, to_branch=self.this_branch, 
 
271
                  last_revision=self.other_basis)
 
272
 
 
273
    def set_base(self, base_revision):
 
274
        mutter("doing merge() with no base_revision specified")
 
275
        if base_revision == [None, None]:
 
276
            try:
 
277
                self.base_rev_id = common_ancestor(self.this_basis, 
 
278
                                                   self.other_basis, 
 
279
                                                   self.this_branch.repository)
 
280
            except NoCommonAncestor:
 
281
                raise UnrelatedBranches()
 
282
            self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
 
283
                                            None)
 
284
            self.base_is_ancestor = True
 
285
        else:
 
286
            base_branch, self.base_tree = _get_tree(base_revision)
 
287
            if base_revision[1] == -1:
 
288
                self.base_rev_id = base_branch.last_revision()
 
289
            elif base_revision[1] is None:
 
290
                self.base_rev_id = None
 
291
            else:
 
292
                self.base_rev_id = base_branch.get_rev_id(base_revision[1])
 
293
            fetch(from_branch=base_branch, to_branch=self.this_branch)
 
294
            self.base_is_ancestor = is_ancestor(self.this_basis, 
 
295
                                                self.base_rev_id,
 
296
                                                self.this_branch)
 
297
 
 
298
    def do_merge(self):
 
299
        kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree, 
 
300
                  'other_tree': self.other_tree}
 
301
        if self.merge_type.requires_base:
 
302
            kwargs['base_tree'] = self.base_tree
 
303
        if self.reprocess and not self.merge_type.supports_reprocess:
 
304
            raise BzrError("Reprocess is not supported for this merge"
 
305
                                  " type. %s" % merge_type)
 
306
        else:
 
307
            kwargs['reprocess'] = self.reprocess
 
308
        if self.show_base and not self.merge_type.supports_show_base:
 
309
            raise BzrError("Showing base is not supported for this"
 
310
                                  " merge type. %s" % self.merge_type)
 
311
        else:
 
312
            kwargs['show_base'] = self.show_base
 
313
        merge = self.merge_type(**kwargs)
 
314
        return len(merge.cooked_conflicts)
 
315
 
 
316
    def regen_inventory(self, new_entries):
 
317
        old_entries = self.this_tree.read_working_inventory()
 
318
        new_inventory = {}
 
319
        by_path = {}
 
320
        new_entries_map = {} 
 
321
        for path, file_id in new_entries:
 
322
            if path is None:
 
323
                continue
 
324
            new_entries_map[file_id] = path
 
325
 
 
326
        def id2path(file_id):
 
327
            path = new_entries_map.get(file_id)
 
328
            if path is not None:
 
329
                return path
 
330
            entry = old_entries[file_id]
 
331
            if entry.parent_id is None:
 
332
                return entry.name
 
333
            return pathjoin(id2path(entry.parent_id), entry.name)
 
334
            
 
335
        for file_id in old_entries:
 
336
            entry = old_entries[file_id]
 
337
            path = id2path(file_id)
 
338
            new_inventory[file_id] = (path, file_id, entry.parent_id, 
 
339
                                      entry.kind)
 
340
            by_path[path] = file_id
 
341
        
 
342
        deletions = 0
 
343
        insertions = 0
 
344
        new_path_list = []
 
345
        for path, file_id in new_entries:
 
346
            if path is None:
 
347
                del new_inventory[file_id]
 
348
                deletions += 1
 
349
            else:
 
350
                new_path_list.append((path, file_id))
 
351
                if file_id not in old_entries:
 
352
                    insertions += 1
 
353
        # Ensure no file is added before its parent
 
354
        new_path_list.sort()
 
355
        for path, file_id in new_path_list:
 
356
            if path == '':
 
357
                parent = None
 
358
            else:
 
359
                parent = by_path[os.path.dirname(path)]
 
360
            abspath = pathjoin(self.this_tree.basedir, path)
 
361
            kind = bzrlib.osutils.file_kind(abspath)
 
362
            new_inventory[file_id] = (path, file_id, parent, kind)
 
363
            by_path[path] = file_id 
 
364
 
 
365
        # Get a list in insertion order
 
366
        new_inventory_list = new_inventory.values()
 
367
        mutter ("""Inventory regeneration:
 
368
    old length: %i insertions: %i deletions: %i new_length: %i"""\
 
369
            % (len(old_entries), insertions, deletions, 
 
370
               len(new_inventory_list)))
 
371
        assert len(new_inventory_list) == len(old_entries) + insertions\
 
372
            - deletions
 
373
        new_inventory_list.sort()
 
374
        return new_inventory_list
 
375
 
 
376
 
 
377
merge_types = {     "merge3": (ApplyMerge3, "Native diff3-style merge"), 
 
378
                     "diff3": (Diff3Merger,  "Merge using external diff3"),
 
379
                     'weave': (WeaveMerger, "Weave-based merge")
 
380
              }