~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-07-19 16:09:34 UTC
  • mfrom: (2520.4.135 bzr.mpbundle)
  • Revision ID: pqm@pqm.ubuntu.com-20070719160934-d51fyijw69oto88p
Add new bundle and merge-directive formats

Show diffs side-by-side

added added

removed removed

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