/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/plan_merge.py

  • Committer: Aaron Bentley
  • Date: 2007-12-02 18:23:12 UTC
  • mto: This revision was merged to the branch mainline in revision 3073.
  • Revision ID: aaron.bentley@utoronto.ca-20071202182312-1red2zttrs87iamc
Clean up names and add documentation

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2007 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
from bzrlib import errors, patiencediff, revision
 
18
 
 
19
 
 
20
class PlanMerge(object):
 
21
    """Plan an annotate merge using on-the-fly annotation"""
 
22
 
 
23
    def __init__(self, a_rev, b_rev, vf):
 
24
        """Contructor.
 
25
 
 
26
        :param a_rev: Revision-id of one revision to merge
 
27
        :param b_rev: Revision-id of the other revision to merge
 
28
        :param vf: A versionedfile containing both revisions
 
29
        """
 
30
        self.a_rev = a_rev
 
31
        self.b_rev = b_rev
 
32
        self.lines_a = vf.get_lines(a_rev)
 
33
        self.lines_b = vf.get_lines(b_rev)
 
34
        self.vf = vf
 
35
        a_ancestry = set(vf.get_ancestry(a_rev))
 
36
        b_ancestry = set(vf.get_ancestry(b_rev))
 
37
        self.uncommon = a_ancestry.symmetric_difference(b_ancestry)
 
38
 
 
39
    def plan_merge(self):
 
40
        """Generate a 'plan' for merging the two revisions.
 
41
 
 
42
        This involves comparing their texts and determining the cause of
 
43
        differences.  If text A has a line and text B does not, then either the
 
44
        line was added to text A, or it was deleted from B.  Once the causes
 
45
        are combined, they are written out in the format described in
 
46
        VersionedFile.plan_merge
 
47
        """
 
48
        blocks = self._get_matching_blocks(self.a_rev, self.b_rev)
 
49
        new_a = self._find_new(self.a_rev)
 
50
        new_b = self._find_new(self.b_rev)
 
51
        last_i = 0
 
52
        last_j = 0
 
53
        a_lines = self.vf.get_lines(self.a_rev)
 
54
        b_lines = self.vf.get_lines(self.b_rev)
 
55
        for i, j, n in blocks:
 
56
            # determine why lines aren't common
 
57
            for a_index in range(last_i, i):
 
58
                if a_index in new_a:
 
59
                    cause = 'new-a'
 
60
                else:
 
61
                    cause = 'killed-b'
 
62
                yield cause, a_lines[a_index]
 
63
            for b_index in range(last_j, j):
 
64
                if b_index in new_b:
 
65
                    cause = 'new-b'
 
66
                else:
 
67
                    cause = 'killed-a'
 
68
                yield cause, b_lines[b_index]
 
69
            # handle common lines
 
70
            for a_index in range(i, i+n):
 
71
                yield 'unchanged', a_lines[a_index]
 
72
            last_i = i+n
 
73
            last_j = j+n
 
74
 
 
75
    def _get_matching_blocks(self, left_revision, right_revision):
 
76
        """Return a description of which sections of two revisions match.
 
77
 
 
78
        See SequenceMatcher.get_matching_blocks
 
79
        """
 
80
        left_lines = self.vf.get_lines(left_revision)
 
81
        right_lines = self.vf.get_lines(right_revision)
 
82
        matcher = patiencediff.PatienceSequenceMatcher(None, left_lines,
 
83
                                                       right_lines)
 
84
        return matcher.get_matching_blocks()
 
85
 
 
86
    def _unique_lines(self, matching_blocks):
 
87
        """Analyse matching_blocks to determine which lines are unique
 
88
 
 
89
        :return: a tuple of (unique_left, unique_right), where the values are
 
90
            sets of line numbers of unique lines.
 
91
        """
 
92
        last_i = 0
 
93
        last_j = 0
 
94
        unique_left = []
 
95
        unique_right = []
 
96
        for i, j, n in matching_blocks:
 
97
            unique_left.extend(range(last_i, i))
 
98
            unique_right.extend(range(last_j, j))
 
99
            last_i = i + n
 
100
            last_j = j + n
 
101
        return unique_left, unique_right
 
102
 
 
103
    def _find_new(self, version_id):
 
104
        """Determine which lines are new in the ancestry of this version.
 
105
 
 
106
        If a lines is present in this version, and not present in any
 
107
        common ancestor, it is considered new.
 
108
        """
 
109
        if version_id not in self.uncommon:
 
110
            return set()
 
111
        parents = self.vf.get_parents(version_id)
 
112
        if len(parents) == 0:
 
113
            return set(range(len(self.vf.get_lines(version_id))))
 
114
        new = None
 
115
        for parent in parents:
 
116
            blocks = self._get_matching_blocks(version_id, parent)
 
117
            result, unused = self._unique_lines(blocks)
 
118
            parent_new = self._find_new(parent)
 
119
            for i, j, n in blocks:
 
120
                for ii, jj in [(i+r, j+r) for r in range(n)]:
 
121
                    if jj in parent_new:
 
122
                        result.append(ii)
 
123
            if new is None:
 
124
                new = set(result)
 
125
            else:
 
126
                new.intersection_update(result)
 
127
        return new
 
128
 
 
129
 
 
130
class _PlanMergeVersionedFile(object):
 
131
    """A VersionedFile for uncommitted and committed texts.
 
132
 
 
133
    It is intended to allow merges to be planned with working tree texts.
 
134
    It implements only the small part of the VersionedFile interface used by
 
135
    PlanMerge.  It falls back to multiple versionedfiles for data not stored in
 
136
    _PlanMergeVersionedFile itself.
 
137
    """
 
138
 
 
139
    def __init__(self, file_id, fallback_versionedfiles=None):
 
140
        """Constuctor
 
141
 
 
142
        :param file_id: Used when raising exceptions.
 
143
        :param fallback_versionedfiles: If supplied, the set of fallbacks to
 
144
            use.  Otherwise, _PlanMergeVersionedFile.fallback_versionedfiles
 
145
            can be appended to later.
 
146
        """
 
147
        self._file_id = file_id
 
148
        if fallback_versionedfiles is None:
 
149
            self.fallback_versionedfiles = []
 
150
        else:
 
151
            self.fallback_versionedfiles = fallback_versionedfiles
 
152
        self._parents = {}
 
153
        self._lines = {}
 
154
 
 
155
    def add_lines(self, version_id, parents, lines):
 
156
        """See VersionedFile.add_lines
 
157
 
 
158
        Lines are added locally, not fallback versionedfiles.  Also, ghosts are
 
159
        permitted.  Only reserved ids are permitted.
 
160
        """
 
161
        if not revision.is_reserved_id(version_id):
 
162
            raise ValueError('Only reserved ids may be used')
 
163
        if parents is None:
 
164
            raise ValueError('Parents may not be None')
 
165
        if lines is None:
 
166
            raise ValueError('Lines may not be None')
 
167
        self._parents[version_id] = parents
 
168
        self._lines[version_id] = lines
 
169
 
 
170
    def get_lines(self, version_id):
 
171
        """See VersionedFile.get_ancestry"""
 
172
        lines = self._lines.get(version_id)
 
173
        if lines is not None:
 
174
            return lines
 
175
        for versionedfile in self.fallback_versionedfiles:
 
176
            try:
 
177
                return versionedfile.get_lines(version_id)
 
178
            except errors.RevisionNotPresent:
 
179
                continue
 
180
        else:
 
181
            raise errors.RevisionNotPresent(version_id, self._file_id)
 
182
 
 
183
    def get_ancestry(self, version_id):
 
184
        """See VersionedFile.get_ancestry.
 
185
 
 
186
        Note that this implementation assumes that if a VersionedFile can
 
187
        answer get_ancestry at all, it can give an authoritative answer.  In
 
188
        fact, ghosts can invalidate this assumption.  But it's good enough
 
189
        99% of the time, and far cheaper/simpler.
 
190
 
 
191
        Also note that the results of this version are never topologically
 
192
        sorted, and are a set.
 
193
        """
 
194
        parents = self._parents.get(version_id)
 
195
        if parents is None:
 
196
            for vf in self.fallback_versionedfiles:
 
197
                try:
 
198
                    return vf.get_ancestry(version_id)
 
199
                except errors.RevisionNotPresent:
 
200
                    continue
 
201
            else:
 
202
                raise errors.RevisionNotPresent(version_id, self._file_id)
 
203
        ancestry = set([version_id])
 
204
        for parent in parents:
 
205
            ancestry.update(self.get_ancestry(parent))
 
206
        return ancestry
 
207
 
 
208
    def get_parents(self, version_id):
 
209
        """See VersionedFile.get_parents"""
 
210
        parents = self._parents.get(version_id)
 
211
        if parents is not None:
 
212
            return parents
 
213
        for versionedfile in self.fallback_versionedfiles:
 
214
            try:
 
215
                return versionedfile.get_parents(version_id)
 
216
            except errors.RevisionNotPresent:
 
217
                continue
 
218
        else:
 
219
            raise errors.RevisionNotPresent(version_id, self._file_id)