~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: Vincent Ladeuil
  • Date: 2008-01-03 08:49:38 UTC
  • mfrom: (3111.1.31 175524)
  • mto: This revision was merged to the branch mainline in revision 3158.
  • Revision ID: v.ladeuil+lp@free.fr-20080103084938-7kvurk5uvde2ui54
Fix bug #175524, http test servers are 1.1 compliant

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
 
from email import Message
19
18
from StringIO import StringIO
 
19
import re
20
20
 
21
21
from bzrlib import (
22
22
    branch as _mod_branch,
23
23
    diff,
24
24
    errors,
25
25
    gpg,
 
26
    registry,
26
27
    revision as _mod_revision,
27
28
    rio,
28
29
    testament,
31
32
from bzrlib.bundle import (
32
33
    serializer as bundle_serializer,
33
34
    )
34
 
 
35
 
 
36
 
class MergeDirective(object):
37
 
 
38
 
    """A request to perform a merge into a branch.
39
 
 
40
 
    Designed to be serialized and mailed.  It provides all the information
41
 
    needed to perform a merge automatically, by providing at minimum a revision
42
 
    bundle or the location of a branch.
43
 
 
44
 
    The serialization format is robust against certain common forms of
45
 
    deterioration caused by mailing.
46
 
 
47
 
    The format is also designed to be patch-compatible.  If the directive
48
 
    includes a diff or revision bundle, it should be possible to apply it
49
 
    directly using the standard patch program.
50
 
    """
51
 
 
52
 
    _format_string = 'Bazaar merge directive format 1'
 
35
from bzrlib.email_message import EmailMessage
 
36
 
 
37
 
 
38
class _BaseMergeDirective(object):
53
39
 
54
40
    def __init__(self, revision_id, testament_sha1, time, timezone,
55
 
                 target_branch, patch=None, patch_type=None,
56
 
                 source_branch=None, message=None):
 
41
                 target_branch, patch=None, source_branch=None, message=None,
 
42
                 bundle=None):
57
43
        """Constructor.
58
44
 
59
45
        :param revision_id: The revision to merge
63
49
        :param timezone: The timezone offset
64
50
        :param target_branch: The branch to apply the merge to
65
51
        :param patch: The text of a diff or bundle
66
 
        :param patch_type: None, "diff" or "bundle", depending on the contents
67
 
            of patch
68
52
        :param source_branch: A public location to merge the revision from
69
53
        :param message: The message to use when committing this merge
70
54
        """
71
 
        assert patch_type in (None, 'diff', 'bundle')
72
 
        if patch_type != 'bundle' and source_branch is None:
73
 
            raise errors.NoMergeSource()
74
 
        if patch_type is not None and patch is None:
75
 
            raise errors.PatchMissing(patch_type)
76
55
        self.revision_id = revision_id
77
56
        self.testament_sha1 = testament_sha1
78
57
        self.time = time
79
58
        self.timezone = timezone
80
59
        self.target_branch = target_branch
81
60
        self.patch = patch
82
 
        self.patch_type = patch_type
83
61
        self.source_branch = source_branch
84
62
        self.message = message
85
63
 
86
 
    @classmethod
87
 
    def from_lines(klass, lines):
88
 
        """Deserialize a MergeRequest from an iterable of lines
89
 
 
90
 
        :param lines: An iterable of lines
91
 
        :return: a MergeRequest
92
 
        """
93
 
        line_iter = iter(lines)
94
 
        for line in line_iter:
95
 
            if line.startswith('# ' + klass._format_string):
96
 
                break
97
 
        else:
98
 
            if len(lines) > 0:
99
 
                raise errors.NotAMergeDirective(lines[0])
100
 
            else:
101
 
                raise errors.NotAMergeDirective('')
102
 
        stanza = rio.read_patch_stanza(line_iter)
103
 
        patch_lines = list(line_iter)
104
 
        if len(patch_lines) == 0:
105
 
            patch = None
106
 
            patch_type = None
107
 
        else:
108
 
            patch = ''.join(patch_lines)
109
 
            try:
110
 
                bundle_serializer.read_bundle(StringIO(patch))
111
 
            except errors.NotABundle:
112
 
                patch_type = 'diff'
113
 
            else:
114
 
                patch_type = 'bundle'
115
 
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
116
 
        kwargs = {}
117
 
        for key in ('revision_id', 'testament_sha1', 'target_branch',
118
 
                    'source_branch', 'message'):
119
 
            try:
120
 
                kwargs[key] = stanza.get(key)
121
 
            except KeyError:
122
 
                pass
123
 
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
124
 
        return MergeDirective(time=time, timezone=timezone,
125
 
                              patch_type=patch_type, patch=patch, **kwargs)
126
 
 
127
 
    def to_lines(self):
 
64
    def _to_lines(self, base_revision=False):
128
65
        """Serialize as a list of lines
129
66
 
130
67
        :return: a list of lines
136
73
        for key in ('source_branch', 'message'):
137
74
            if self.__dict__[key] is not None:
138
75
                stanza.add(key, self.__dict__[key])
 
76
        if base_revision:
 
77
            stanza.add('base_revision_id', self.base_revision_id)
139
78
        lines = ['# ' + self._format_string + '\n']
140
79
        lines.extend(rio.to_patch_lines(stanza))
141
80
        lines.append('# \n')
142
 
        if self.patch is not None:
143
 
            lines.extend(self.patch.splitlines(True))
144
81
        return lines
145
82
 
 
83
    @classmethod
 
84
    def from_objects(klass, repository, revision_id, time, timezone,
 
85
                 target_branch, patch_type='bundle',
 
86
                 local_target_branch=None, public_branch=None, message=None):
 
87
        """Generate a merge directive from various objects
 
88
 
 
89
        :param repository: The repository containing the revision
 
90
        :param revision_id: The revision to merge
 
91
        :param time: The POSIX timestamp of the date the request was issued.
 
92
        :param timezone: The timezone of the request
 
93
        :param target_branch: The url of the branch to merge into
 
94
        :param patch_type: 'bundle', 'diff' or None, depending on the type of
 
95
            patch desired.
 
96
        :param local_target_branch: a local copy of the target branch
 
97
        :param public_branch: location of a public branch containing the target
 
98
            revision.
 
99
        :param message: Message to use when committing the merge
 
100
        :return: The merge directive
 
101
 
 
102
        The public branch is always used if supplied.  If the patch_type is
 
103
        not 'bundle', the public branch must be supplied, and will be verified.
 
104
 
 
105
        If the message is not supplied, the message from revision_id will be
 
106
        used for the commit.
 
107
        """
 
108
        t_revision_id = revision_id
 
109
        if revision_id == _mod_revision.NULL_REVISION:
 
110
            t_revision_id = None
 
111
        t = testament.StrictTestament3.from_revision(repository, t_revision_id)
 
112
        submit_branch = _mod_branch.Branch.open(target_branch)
 
113
        if submit_branch.get_public_branch() is not None:
 
114
            target_branch = submit_branch.get_public_branch()
 
115
        if patch_type is None:
 
116
            patch = None
 
117
        else:
 
118
            submit_revision_id = submit_branch.last_revision()
 
119
            submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
 
120
            repository.fetch(submit_branch.repository, submit_revision_id)
 
121
            graph = repository.get_graph()
 
122
            ancestor_id = graph.find_unique_lca(revision_id,
 
123
                                                submit_revision_id)
 
124
            type_handler = {'bundle': klass._generate_bundle,
 
125
                            'diff': klass._generate_diff,
 
126
                            None: lambda x, y, z: None }
 
127
            patch = type_handler[patch_type](repository, revision_id,
 
128
                                             ancestor_id)
 
129
 
 
130
        if public_branch is not None and patch_type != 'bundle':
 
131
            public_branch_obj = _mod_branch.Branch.open(public_branch)
 
132
            if not public_branch_obj.repository.has_revision(revision_id):
 
133
                raise errors.PublicBranchOutOfDate(public_branch,
 
134
                                                   revision_id)
 
135
 
 
136
        return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
 
137
            patch, patch_type, public_branch, message)
 
138
 
 
139
    @staticmethod
 
140
    def _generate_diff(repository, revision_id, ancestor_id):
 
141
        tree_1 = repository.revision_tree(ancestor_id)
 
142
        tree_2 = repository.revision_tree(revision_id)
 
143
        s = StringIO()
 
144
        diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
 
145
        return s.getvalue()
 
146
 
 
147
    @staticmethod
 
148
    def _generate_bundle(repository, revision_id, ancestor_id):
 
149
        s = StringIO()
 
150
        bundle_serializer.write_bundle(repository, revision_id,
 
151
                                       ancestor_id, s)
 
152
        return s.getvalue()
 
153
 
146
154
    def to_signed(self, branch):
147
155
        """Serialize as a signed string.
148
156
 
162
170
        :return: an email message
163
171
        """
164
172
        mail_from = branch.get_config().username()
165
 
        message = Message.Message()
166
 
        message['To'] = mail_to
167
 
        message['From'] = mail_from
168
173
        if self.message is not None:
169
 
            message['Subject'] = self.message
 
174
            subject = self.message
170
175
        else:
171
176
            revision = branch.repository.get_revision(self.revision_id)
172
 
            message['Subject'] = revision.message
 
177
            subject = revision.message
173
178
        if sign:
174
179
            body = self.to_signed(branch)
175
180
        else:
176
181
            body = ''.join(self.to_lines())
177
 
        message.set_payload(body)
 
182
        message = EmailMessage(mail_from, mail_to, subject, body)
178
183
        return message
179
184
 
180
 
    @classmethod
181
 
    def from_objects(klass, repository, revision_id, time, timezone,
182
 
                 target_branch, patch_type='bundle',
183
 
                 local_target_branch=None, public_branch=None, message=None):
184
 
        """Generate a merge directive from various objects
185
 
 
186
 
        :param repository: The repository containing the revision
187
 
        :param revision_id: The revision to merge
188
 
        :param time: The POSIX timestamp of the date the request was issued.
189
 
        :param timezone: The timezone of the request
190
 
        :param target_branch: The url of the branch to merge into
191
 
        :param patch_type: 'bundle', 'diff' or None, depending on the type of
192
 
            patch desired.
193
 
        :param local_target_branch: a local copy of the target branch
194
 
        :param public_branch: location of a public branch containing the target
195
 
            revision.
196
 
        :param message: Message to use when committing the merge
197
 
        :return: The merge directive
198
 
 
199
 
        The public branch is always used if supplied.  If the patch_type is
200
 
        not 'bundle', the public branch must be supplied, and will be verified.
201
 
 
202
 
        If the message is not supplied, the message from revision_id will be
203
 
        used for the commit.
204
 
        """
205
 
        t = testament.StrictTestament3.from_revision(repository, revision_id)
206
 
        submit_branch = _mod_branch.Branch.open(target_branch)
207
 
        if submit_branch.get_public_branch() is not None:
208
 
            target_branch = submit_branch.get_public_branch()
209
 
        if patch_type is None:
210
 
            patch = None
211
 
        else:
212
 
            submit_revision_id = submit_branch.last_revision()
213
 
            repository.fetch(submit_branch.repository, submit_revision_id)
214
 
            ancestor_id = _mod_revision.common_ancestor(revision_id,
215
 
                                                        submit_revision_id,
216
 
                                                        repository)
217
 
            type_handler = {'bundle': klass._generate_bundle,
218
 
                            'diff': klass._generate_diff,
219
 
                            None: lambda x, y, z: None }
220
 
            patch = type_handler[patch_type](repository, revision_id,
221
 
                                             ancestor_id)
222
 
            if patch_type == 'bundle':
223
 
                s = StringIO()
224
 
                bundle_serializer.write_bundle(repository, revision_id,
225
 
                                               ancestor_id, s)
226
 
                patch = s.getvalue()
227
 
            elif patch_type == 'diff':
228
 
                patch = klass._generate_diff(repository, revision_id,
229
 
                                             ancestor_id)
230
 
 
231
 
            if public_branch is not None and patch_type != 'bundle':
232
 
                public_branch_obj = _mod_branch.Branch.open(public_branch)
233
 
                if not public_branch_obj.repository.has_revision(revision_id):
234
 
                    raise errors.PublicBranchOutOfDate(public_branch,
235
 
                                                       revision_id)
236
 
 
237
 
        return MergeDirective(revision_id, t.as_sha1(), time, timezone,
238
 
                              target_branch, patch, patch_type, public_branch,
239
 
                              message)
240
 
 
241
 
    @staticmethod
242
 
    def _generate_diff(repository, revision_id, ancestor_id):
243
 
        tree_1 = repository.revision_tree(ancestor_id)
244
 
        tree_2 = repository.revision_tree(revision_id)
245
 
        s = StringIO()
246
 
        diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
247
 
        return s.getvalue()
248
 
 
249
 
    @staticmethod
250
 
    def _generate_bundle(repository, revision_id, ancestor_id):
251
 
        s = StringIO()
252
 
        bundle_serializer.write_bundle(repository, revision_id,
253
 
                                       ancestor_id, s)
254
 
        return s.getvalue()
255
 
 
256
185
    def install_revisions(self, target_repo):
257
186
        """Install revisions and return the target revision"""
258
187
        if not target_repo.has_revision(self.revision_id):
259
188
            if self.patch_type == 'bundle':
260
 
                info = bundle_serializer.read_bundle(StringIO(self.patch))
 
189
                info = bundle_serializer.read_bundle(
 
190
                    StringIO(self.get_raw_bundle()))
261
191
                # We don't use the bundle's target revision, because
262
192
                # MergeDirective.revision_id is authoritative.
263
 
                info.install_revisions(target_repo)
 
193
                try:
 
194
                    info.install_revisions(target_repo, stream_input=False)
 
195
                except errors.RevisionNotPresent:
 
196
                    # At least one dependency isn't present.  Try installing
 
197
                    # missing revisions from the submit branch
 
198
                    submit_branch = _mod_branch.Branch.open(self.target_branch)
 
199
                    missing_revisions = []
 
200
                    bundle_revisions = set(r.revision_id for r in
 
201
                                           info.real_revisions)
 
202
                    for revision in info.real_revisions:
 
203
                        for parent_id in revision.parent_ids:
 
204
                            if (parent_id not in bundle_revisions and
 
205
                                not target_repo.has_revision(parent_id)):
 
206
                                missing_revisions.append(parent_id)
 
207
                    # reverse missing revisions to try to get heads first
 
208
                    unique_missing = []
 
209
                    unique_missing_set = set()
 
210
                    for revision in reversed(missing_revisions):
 
211
                        if revision in unique_missing_set:
 
212
                            continue
 
213
                        unique_missing.append(revision)
 
214
                        unique_missing_set.add(revision)
 
215
                    for missing_revision in unique_missing:
 
216
                        target_repo.fetch(submit_branch.repository,
 
217
                                          missing_revision)
 
218
                    info.install_revisions(target_repo, stream_input=False)
264
219
            else:
265
220
                source_branch = _mod_branch.Branch.open(self.source_branch)
266
221
                target_repo.fetch(source_branch.repository, self.revision_id)
267
222
        return self.revision_id
 
223
 
 
224
 
 
225
class MergeDirective(_BaseMergeDirective):
 
226
 
 
227
    """A request to perform a merge into a branch.
 
228
 
 
229
    Designed to be serialized and mailed.  It provides all the information
 
230
    needed to perform a merge automatically, by providing at minimum a revision
 
231
    bundle or the location of a branch.
 
232
 
 
233
    The serialization format is robust against certain common forms of
 
234
    deterioration caused by mailing.
 
235
 
 
236
    The format is also designed to be patch-compatible.  If the directive
 
237
    includes a diff or revision bundle, it should be possible to apply it
 
238
    directly using the standard patch program.
 
239
    """
 
240
 
 
241
    _format_string = 'Bazaar merge directive format 1'
 
242
 
 
243
    def __init__(self, revision_id, testament_sha1, time, timezone,
 
244
                 target_branch, patch=None, patch_type=None,
 
245
                 source_branch=None, message=None, bundle=None):
 
246
        """Constructor.
 
247
 
 
248
        :param revision_id: The revision to merge
 
249
        :param testament_sha1: The sha1 of the testament of the revision to
 
250
            merge.
 
251
        :param time: The current POSIX timestamp time
 
252
        :param timezone: The timezone offset
 
253
        :param target_branch: The branch to apply the merge to
 
254
        :param patch: The text of a diff or bundle
 
255
        :param patch_type: None, "diff" or "bundle", depending on the contents
 
256
            of patch
 
257
        :param source_branch: A public location to merge the revision from
 
258
        :param message: The message to use when committing this merge
 
259
        """
 
260
        _BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
 
261
            timezone, target_branch, patch, source_branch, message)
 
262
        assert patch_type in (None, 'diff', 'bundle'), patch_type
 
263
        if patch_type != 'bundle' and source_branch is None:
 
264
            raise errors.NoMergeSource()
 
265
        if patch_type is not None and patch is None:
 
266
            raise errors.PatchMissing(patch_type)
 
267
        self.patch_type = patch_type
 
268
 
 
269
    def clear_payload(self):
 
270
        self.patch = None
 
271
        self.patch_type = None
 
272
 
 
273
    def get_raw_bundle(self):
 
274
        return self.bundle
 
275
 
 
276
    def _bundle(self):
 
277
        if self.patch_type == 'bundle':
 
278
            return self.patch
 
279
        else:
 
280
            return None
 
281
 
 
282
    bundle = property(_bundle)
 
283
 
 
284
    @classmethod
 
285
    def from_lines(klass, lines):
 
286
        """Deserialize a MergeRequest from an iterable of lines
 
287
 
 
288
        :param lines: An iterable of lines
 
289
        :return: a MergeRequest
 
290
        """
 
291
        line_iter = iter(lines)
 
292
        for line in line_iter:
 
293
            if line.startswith('# Bazaar merge directive format '):
 
294
                break
 
295
        else:
 
296
            if len(lines) > 0:
 
297
                raise errors.NotAMergeDirective(lines[0])
 
298
            else:
 
299
                raise errors.NotAMergeDirective('')
 
300
        return _format_registry.get(line[2:].rstrip())._from_lines(line_iter)
 
301
 
 
302
    @classmethod
 
303
    def _from_lines(klass, line_iter):
 
304
        stanza = rio.read_patch_stanza(line_iter)
 
305
        patch_lines = list(line_iter)
 
306
        if len(patch_lines) == 0:
 
307
            patch = None
 
308
            patch_type = None
 
309
        else:
 
310
            patch = ''.join(patch_lines)
 
311
            try:
 
312
                bundle_serializer.read_bundle(StringIO(patch))
 
313
            except (errors.NotABundle, errors.BundleNotSupported,
 
314
                    errors.BadBundle):
 
315
                patch_type = 'diff'
 
316
            else:
 
317
                patch_type = 'bundle'
 
318
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
319
        kwargs = {}
 
320
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
321
                    'source_branch', 'message'):
 
322
            try:
 
323
                kwargs[key] = stanza.get(key)
 
324
            except KeyError:
 
325
                pass
 
326
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
327
        return MergeDirective(time=time, timezone=timezone,
 
328
                              patch_type=patch_type, patch=patch, **kwargs)
 
329
 
 
330
    def to_lines(self):
 
331
        lines = self._to_lines()
 
332
        if self.patch is not None:
 
333
            lines.extend(self.patch.splitlines(True))
 
334
        return lines
 
335
 
 
336
    @staticmethod
 
337
    def _generate_bundle(repository, revision_id, ancestor_id):
 
338
        s = StringIO()
 
339
        bundle_serializer.write_bundle(repository, revision_id,
 
340
                                       ancestor_id, s, '0.9')
 
341
        return s.getvalue()
 
342
 
 
343
    def get_merge_request(self, repository):
 
344
        """Provide data for performing a merge
 
345
 
 
346
        Returns suggested base, suggested target, and patch verification status
 
347
        """
 
348
        return None, self.revision_id, 'inapplicable'
 
349
 
 
350
 
 
351
class MergeDirective2(_BaseMergeDirective):
 
352
 
 
353
    _format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
 
354
 
 
355
    def __init__(self, revision_id, testament_sha1, time, timezone,
 
356
                 target_branch, patch=None, source_branch=None, message=None,
 
357
                 bundle=None, base_revision_id=None):
 
358
        if source_branch is None and bundle is None:
 
359
            raise errors.NoMergeSource()
 
360
        _BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
 
361
            timezone, target_branch, patch, source_branch, message)
 
362
        self.bundle = bundle
 
363
        self.base_revision_id = base_revision_id
 
364
 
 
365
    def _patch_type(self):
 
366
        if self.bundle is not None:
 
367
            return 'bundle'
 
368
        elif self.patch is not None:
 
369
            return 'diff'
 
370
        else:
 
371
            return None
 
372
 
 
373
    patch_type = property(_patch_type)
 
374
 
 
375
    def clear_payload(self):
 
376
        self.patch = None
 
377
        self.bundle = None
 
378
 
 
379
    def get_raw_bundle(self):
 
380
        if self.bundle is None:
 
381
            return None
 
382
        else:
 
383
            return self.bundle.decode('base-64')
 
384
 
 
385
    @classmethod
 
386
    def _from_lines(klass, line_iter):
 
387
        stanza = rio.read_patch_stanza(line_iter)
 
388
        patch = None
 
389
        bundle = None
 
390
        try:
 
391
            start = line_iter.next()
 
392
        except StopIteration:
 
393
            pass
 
394
        else:
 
395
            if start.startswith('# Begin patch'):
 
396
                patch_lines = []
 
397
                for line in line_iter:
 
398
                    if line.startswith('# Begin bundle'):
 
399
                        start = line
 
400
                        break
 
401
                    patch_lines.append(line)
 
402
                else:
 
403
                    start = None
 
404
                patch = ''.join(patch_lines)
 
405
            if start is not None:
 
406
                if start.startswith('# Begin bundle'):
 
407
                    bundle = ''.join(line_iter)
 
408
                else:
 
409
                    raise errors.IllegalMergeDirectivePayload(start)
 
410
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
411
        kwargs = {}
 
412
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
413
                    'source_branch', 'message', 'base_revision_id'):
 
414
            try:
 
415
                kwargs[key] = stanza.get(key)
 
416
            except KeyError:
 
417
                pass
 
418
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
419
        kwargs['base_revision_id'] =\
 
420
            kwargs['base_revision_id'].encode('utf-8')
 
421
        return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
 
422
                     **kwargs)
 
423
 
 
424
    def to_lines(self):
 
425
        lines = self._to_lines(base_revision=True)
 
426
        if self.patch is not None:
 
427
            lines.append('# Begin patch\n')
 
428
            lines.extend(self.patch.splitlines(True))
 
429
        if self.bundle is not None:
 
430
            lines.append('# Begin bundle\n')
 
431
            lines.extend(self.bundle.splitlines(True))
 
432
        return lines
 
433
 
 
434
    @classmethod
 
435
    def from_objects(klass, repository, revision_id, time, timezone,
 
436
                 target_branch, include_patch=True, include_bundle=True,
 
437
                 local_target_branch=None, public_branch=None, message=None,
 
438
                 base_revision_id=None):
 
439
        """Generate a merge directive from various objects
 
440
 
 
441
        :param repository: The repository containing the revision
 
442
        :param revision_id: The revision to merge
 
443
        :param time: The POSIX timestamp of the date the request was issued.
 
444
        :param timezone: The timezone of the request
 
445
        :param target_branch: The url of the branch to merge into
 
446
        :param include_patch: If true, include a preview patch
 
447
        :param include_bundle: If true, include a bundle
 
448
        :param local_target_branch: a local copy of the target branch
 
449
        :param public_branch: location of a public branch containing the target
 
450
            revision.
 
451
        :param message: Message to use when committing the merge
 
452
        :return: The merge directive
 
453
 
 
454
        The public branch is always used if supplied.  If no bundle is
 
455
        included, the public branch must be supplied, and will be verified.
 
456
 
 
457
        If the message is not supplied, the message from revision_id will be
 
458
        used for the commit.
 
459
        """
 
460
        locked = []
 
461
        try:
 
462
            repository.lock_write()
 
463
            locked.append(repository)
 
464
            t_revision_id = revision_id
 
465
            if revision_id == 'null:':
 
466
                t_revision_id = None
 
467
            t = testament.StrictTestament3.from_revision(repository,
 
468
                t_revision_id)
 
469
            submit_branch = _mod_branch.Branch.open(target_branch)
 
470
            submit_branch.lock_read()
 
471
            locked.append(submit_branch)
 
472
            if submit_branch.get_public_branch() is not None:
 
473
                target_branch = submit_branch.get_public_branch()
 
474
            submit_revision_id = submit_branch.last_revision()
 
475
            submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
 
476
            graph = repository.get_graph(submit_branch.repository)
 
477
            ancestor_id = graph.find_unique_lca(revision_id,
 
478
                                                submit_revision_id)
 
479
            if base_revision_id is None:
 
480
                base_revision_id = ancestor_id
 
481
            if (include_patch, include_bundle) != (False, False):
 
482
                repository.fetch(submit_branch.repository, submit_revision_id)
 
483
            if include_patch:
 
484
                patch = klass._generate_diff(repository, revision_id,
 
485
                                             base_revision_id)
 
486
            else:
 
487
                patch = None
 
488
 
 
489
            if include_bundle:
 
490
                bundle = klass._generate_bundle(repository, revision_id,
 
491
                    ancestor_id).encode('base-64')
 
492
            else:
 
493
                bundle = None
 
494
 
 
495
            if public_branch is not None and not include_bundle:
 
496
                public_branch_obj = _mod_branch.Branch.open(public_branch)
 
497
                public_branch_obj.lock_read()
 
498
                locked.append(public_branch_obj)
 
499
                if not public_branch_obj.repository.has_revision(
 
500
                    revision_id):
 
501
                    raise errors.PublicBranchOutOfDate(public_branch,
 
502
                                                       revision_id)
 
503
        finally:
 
504
            for entry in reversed(locked):
 
505
                entry.unlock()
 
506
        return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
 
507
            patch, public_branch, message, bundle, base_revision_id)
 
508
 
 
509
    def _verify_patch(self, repository):
 
510
        calculated_patch = self._generate_diff(repository, self.revision_id,
 
511
                                               self.base_revision_id)
 
512
        # Convert line-endings to UNIX
 
513
        stored_patch = re.sub('\r\n?', '\n', self.patch)
 
514
        calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
 
515
        # Strip trailing whitespace
 
516
        calculated_patch = re.sub(' *\n', '\n', calculated_patch)
 
517
        stored_patch = re.sub(' *\n', '\n', stored_patch)
 
518
        return (calculated_patch == stored_patch)
 
519
 
 
520
    def get_merge_request(self, repository):
 
521
        """Provide data for performing a merge
 
522
 
 
523
        Returns suggested base, suggested target, and patch verification status
 
524
        """
 
525
        verified = self._maybe_verify(repository)
 
526
        return self.base_revision_id, self.revision_id, verified
 
527
 
 
528
    def _maybe_verify(self, repository):
 
529
        if self.patch is not None:
 
530
            if self._verify_patch(repository):
 
531
                return 'verified'
 
532
            else:
 
533
                return 'failed'
 
534
        else:
 
535
            return 'inapplicable'
 
536
 
 
537
 
 
538
class MergeDirectiveFormatRegistry(registry.Registry):
 
539
 
 
540
    def register(self, directive, format_string=None):
 
541
        if format_string is None:
 
542
            format_string = directive._format_string
 
543
        registry.Registry.register(self, format_string, directive)
 
544
 
 
545
 
 
546
_format_registry = MergeDirectiveFormatRegistry()
 
547
_format_registry.register(MergeDirective)
 
548
_format_registry.register(MergeDirective2)
 
549
_format_registry.register(MergeDirective2,
 
550
                          'Bazaar merge directive format 2 (Bazaar 0.19)')