1
# Copyright (C) 2007-2011 Canonical Ltd
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.
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.
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from __future__ import absolute_import
22
from . import lazy_import
23
lazy_import.lazy_import(globals(), """
25
branch as _mod_branch,
32
revision as _mod_revision,
37
from breezy.bzr import (
40
from breezy.bzr.bundle import (
41
serializer as bundle_serializer,
52
class MergeRequestBodyParams(object):
53
"""Parameter object for the merge_request_body hook."""
55
def __init__(self, body, orig_body, directive, to, basename, subject,
58
self.orig_body = orig_body
59
self.directive = directive
63
self.basename = basename
64
self.subject = subject
67
class MergeDirectiveHooks(hooks.Hooks):
68
"""Hooks for MergeDirective classes."""
71
hooks.Hooks.__init__(self, "breezy.merge_directive",
72
"BaseMergeDirective.hooks")
75
"Called with a MergeRequestBodyParams when a body is needed for"
76
" a merge request. Callbacks must return a body. If more"
77
" than one callback is registered, the output of one callback is"
78
" provided to the next.", (1, 15, 0))
81
class BaseMergeDirective(object):
82
"""A request to perform a merge into a branch.
84
This is the base class that all merge directive implementations
87
:cvar multiple_output_files: Whether or not this merge directive
88
stores a set of revisions in more than one file
91
hooks = MergeDirectiveHooks()
93
multiple_output_files = False
95
def __init__(self, revision_id, testament_sha1, time, timezone,
96
target_branch, patch=None, source_branch=None,
97
message=None, bundle=None):
100
:param revision_id: The revision to merge
101
:param testament_sha1: The sha1 of the testament of the revision to
103
:param time: The current POSIX timestamp time
104
:param timezone: The timezone offset
105
:param target_branch: Location of branch to apply the merge to
106
:param patch: The text of a diff or bundle
107
:param source_branch: A public location to merge the revision from
108
:param message: The message to use when committing this merge
110
self.revision_id = revision_id
111
self.testament_sha1 = testament_sha1
113
self.timezone = timezone
114
self.target_branch = target_branch
116
self.source_branch = source_branch
117
self.message = message
120
"""Serialize as a list of lines
122
:return: a list of lines
124
raise NotImplementedError(self.to_lines)
127
"""Serialize as a set of files.
129
:return: List of tuples with filename and contents as lines
131
raise NotImplementedError(self.to_files)
133
def get_raw_bundle(self):
134
"""Return the bundle for this merge directive.
136
:return: bundle text or None if there is no bundle
140
def _to_lines(self, base_revision=False):
141
"""Serialize as a list of lines
143
:return: a list of lines
145
time_str = timestamp.format_patch_date(self.time, self.timezone)
146
stanza = rio.Stanza(revision_id=self.revision_id, timestamp=time_str,
147
target_branch=self.target_branch,
148
testament_sha1=self.testament_sha1)
149
for key in ('source_branch', 'message'):
150
if self.__dict__[key] is not None:
151
stanza.add(key, self.__dict__[key])
153
stanza.add('base_revision_id', self.base_revision_id)
154
lines = [b'# ' + self._format_string + b'\n']
155
lines.extend(rio.to_patch_lines(stanza))
156
lines.append(b'# \n')
159
def write_to_directory(self, path):
160
"""Write this merge directive to a series of files in a directory.
162
:param path: Filesystem path to write to
164
raise NotImplementedError(self.write_to_directory)
167
def from_objects(klass, repository, revision_id, time, timezone,
168
target_branch, patch_type='bundle',
169
local_target_branch=None, public_branch=None, message=None):
170
"""Generate a merge directive from various objects
172
:param repository: The repository containing the revision
173
:param revision_id: The revision to merge
174
:param time: The POSIX timestamp of the date the request was issued.
175
:param timezone: The timezone of the request
176
:param target_branch: The url of the branch to merge into
177
:param patch_type: 'bundle', 'diff' or None, depending on the type of
179
:param local_target_branch: the submit branch, either itself or a local copy
180
:param public_branch: location of a public branch containing
182
:param message: Message to use when committing the merge
183
:return: The merge directive
185
The public branch is always used if supplied. If the patch_type is
186
not 'bundle', the public branch must be supplied, and will be verified.
188
If the message is not supplied, the message from revision_id will be
191
t_revision_id = revision_id
192
if revision_id == _mod_revision.NULL_REVISION:
194
t = testament.StrictTestament3.from_revision(repository, t_revision_id)
195
if local_target_branch is None:
196
submit_branch = _mod_branch.Branch.open(target_branch)
198
submit_branch = local_target_branch
199
if submit_branch.get_public_branch() is not None:
200
target_branch = submit_branch.get_public_branch()
201
if patch_type is None:
204
submit_revision_id = submit_branch.last_revision()
205
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
206
repository.fetch(submit_branch.repository, submit_revision_id)
207
graph = repository.get_graph()
208
ancestor_id = graph.find_unique_lca(revision_id,
210
type_handler = {'bundle': klass._generate_bundle,
211
'diff': klass._generate_diff,
212
None: lambda x, y, z: None}
213
patch = type_handler[patch_type](repository, revision_id,
216
if public_branch is not None and patch_type != 'bundle':
217
public_branch_obj = _mod_branch.Branch.open(public_branch)
218
if not public_branch_obj.repository.has_revision(revision_id):
219
raise errors.PublicBranchOutOfDate(public_branch,
222
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
223
patch, patch_type, public_branch, message)
225
def get_disk_name(self, branch):
226
"""Generate a suitable basename for storing this directive on disk
228
:param branch: The Branch this merge directive was generated fro
231
revno, revision_id = branch.last_revision_info()
232
if self.revision_id == revision_id:
236
revno = branch.revision_id_to_dotted_revno(self.revision_id)
237
except errors.NoSuchRevision:
239
nick = re.sub('(\\W+)', '-', branch.nick).strip('-')
240
return '%s-%s' % (nick, '.'.join(str(n) for n in revno))
243
def _generate_diff(repository, revision_id, ancestor_id):
244
tree_1 = repository.revision_tree(ancestor_id)
245
tree_2 = repository.revision_tree(revision_id)
247
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
251
def _generate_bundle(repository, revision_id, ancestor_id):
253
bundle_serializer.write_bundle(repository, revision_id,
257
def to_signed(self, branch):
258
"""Serialize as a signed string.
260
:param branch: The source branch, to get the signing strategy
263
my_gpg = gpg.GPGStrategy(branch.get_config_stack())
264
return my_gpg.sign(b''.join(self.to_lines()), gpg.MODE_CLEAR)
266
def to_email(self, mail_to, branch, sign=False):
267
"""Serialize as an email message.
269
:param mail_to: The address to mail the message to
270
:param branch: The source branch, to get the signing strategy and
272
:param sign: If True, gpg-sign the email
273
:return: an email message
275
mail_from = branch.get_config_stack().get('email')
276
if self.message is not None:
277
subject = self.message
279
revision = branch.repository.get_revision(self.revision_id)
280
subject = revision.message
282
body = self.to_signed(branch)
284
body = b''.join(self.to_lines())
285
message = email_message.EmailMessage(mail_from, mail_to, subject,
289
def install_revisions(self, target_repo):
290
"""Install revisions and return the target revision"""
291
if not target_repo.has_revision(self.revision_id):
292
if self.patch_type == 'bundle':
293
info = bundle_serializer.read_bundle(
294
BytesIO(self.get_raw_bundle()))
295
# We don't use the bundle's target revision, because
296
# MergeDirective.revision_id is authoritative.
298
info.install_revisions(target_repo, stream_input=False)
299
except errors.RevisionNotPresent:
300
# At least one dependency isn't present. Try installing
301
# missing revisions from the submit branch
304
_mod_branch.Branch.open(self.target_branch)
305
except errors.NotBranchError:
306
raise errors.TargetNotBranch(self.target_branch)
307
missing_revisions = []
308
bundle_revisions = set(r.revision_id for r in
310
for revision in info.real_revisions:
311
for parent_id in revision.parent_ids:
312
if (parent_id not in bundle_revisions
313
and not target_repo.has_revision(parent_id)):
314
missing_revisions.append(parent_id)
315
# reverse missing revisions to try to get heads first
317
unique_missing_set = set()
318
for revision in reversed(missing_revisions):
319
if revision in unique_missing_set:
321
unique_missing.append(revision)
322
unique_missing_set.add(revision)
323
for missing_revision in unique_missing:
324
target_repo.fetch(submit_branch.repository,
326
info.install_revisions(target_repo, stream_input=False)
328
source_branch = _mod_branch.Branch.open(self.source_branch)
329
target_repo.fetch(source_branch.repository, self.revision_id)
330
return self.revision_id
332
def compose_merge_request(self, mail_client, to, body, branch, tree=None):
333
"""Compose a request to merge this directive.
335
:param mail_client: The mail client to use for composing this request.
336
:param to: The address to compose the request to.
337
:param branch: The Branch that was used to produce this directive.
338
:param tree: The Tree (if any) for the Branch used to produce this
341
basename = self.get_disk_name(branch)
343
if self.message is not None:
344
subject += self.message
346
revision = branch.repository.get_revision(self.revision_id)
347
subject += revision.get_summary()
348
if getattr(mail_client, 'supports_body', False):
350
for hook in self.hooks['merge_request_body']:
351
params = MergeRequestBodyParams(body, orig_body, self,
352
to, basename, subject, branch,
355
elif len(self.hooks['merge_request_body']) > 0:
356
trace.warning('Cannot run merge_request_body hooks because mail'
357
' client %s does not support message bodies.',
358
mail_client.__class__.__name__)
359
mail_client.compose_merge_request(to, subject,
360
b''.join(self.to_lines()),
364
class MergeDirective(BaseMergeDirective):
366
"""A request to perform a merge into a branch.
368
Designed to be serialized and mailed. It provides all the information
369
needed to perform a merge automatically, by providing at minimum a revision
370
bundle or the location of a branch.
372
The serialization format is robust against certain common forms of
373
deterioration caused by mailing.
375
The format is also designed to be patch-compatible. If the directive
376
includes a diff or revision bundle, it should be possible to apply it
377
directly using the standard patch program.
380
_format_string = b'Bazaar merge directive format 1'
382
def __init__(self, revision_id, testament_sha1, time, timezone,
383
target_branch, patch=None, patch_type=None,
384
source_branch=None, message=None, bundle=None):
387
:param revision_id: The revision to merge
388
:param testament_sha1: The sha1 of the testament of the revision to
390
:param time: The current POSIX timestamp time
391
:param timezone: The timezone offset
392
:param target_branch: Location of the branch to apply the merge to
393
:param patch: The text of a diff or bundle
394
:param patch_type: None, "diff" or "bundle", depending on the contents
396
:param source_branch: A public location to merge the revision from
397
:param message: The message to use when committing this merge
399
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
400
timezone, target_branch, patch, source_branch, message)
401
if patch_type not in (None, 'diff', 'bundle'):
402
raise ValueError(patch_type)
403
if patch_type != 'bundle' and source_branch is None:
404
raise errors.NoMergeSource()
405
if patch_type is not None and patch is None:
406
raise errors.PatchMissing(patch_type)
407
self.patch_type = patch_type
409
def clear_payload(self):
411
self.patch_type = None
413
def get_raw_bundle(self):
417
if self.patch_type == 'bundle':
422
bundle = property(_bundle)
425
def from_lines(klass, lines):
426
"""Deserialize a MergeRequest from an iterable of lines
428
:param lines: An iterable of lines
429
:return: a MergeRequest
431
line_iter = iter(lines)
433
for line in line_iter:
434
if line.startswith(b'# Bazaar merge directive format '):
435
return _format_registry.get(line[2:].rstrip())._from_lines(
437
firstline = firstline or line.strip()
438
raise errors.NotAMergeDirective(firstline)
441
def _from_lines(klass, line_iter):
442
stanza = rio.read_patch_stanza(line_iter)
443
patch_lines = list(line_iter)
444
if len(patch_lines) == 0:
448
patch = b''.join(patch_lines)
450
bundle_serializer.read_bundle(BytesIO(patch))
451
except (errors.NotABundle, errors.BundleNotSupported,
455
patch_type = 'bundle'
456
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
458
for key in ('revision_id', 'testament_sha1', 'target_branch',
459
'source_branch', 'message'):
461
kwargs[key] = stanza.get(key)
464
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
465
if 'testament_sha1' in kwargs:
466
kwargs['testament_sha1'] = kwargs['testament_sha1'].encode('ascii')
467
return MergeDirective(time=time, timezone=timezone,
468
patch_type=patch_type, patch=patch, **kwargs)
471
lines = self._to_lines()
472
if self.patch is not None:
473
lines.extend(self.patch.splitlines(True))
477
def _generate_bundle(repository, revision_id, ancestor_id):
479
bundle_serializer.write_bundle(repository, revision_id,
480
ancestor_id, s, '0.9')
483
def get_merge_request(self, repository):
484
"""Provide data for performing a merge
486
Returns suggested base, suggested target, and patch verification status
488
return None, self.revision_id, 'inapplicable'
491
class MergeDirective2(BaseMergeDirective):
493
_format_string = b'Bazaar merge directive format 2 (Bazaar 0.90)'
495
def __init__(self, revision_id, testament_sha1, time, timezone,
496
target_branch, patch=None, source_branch=None, message=None,
497
bundle=None, base_revision_id=None):
498
if source_branch is None and bundle is None:
499
raise errors.NoMergeSource()
500
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
501
timezone, target_branch, patch, source_branch, message)
503
self.base_revision_id = base_revision_id
505
def _patch_type(self):
506
if self.bundle is not None:
508
elif self.patch is not None:
513
patch_type = property(_patch_type)
515
def clear_payload(self):
519
def get_raw_bundle(self):
520
if self.bundle is None:
523
return base64.b64decode(self.bundle)
526
def _from_lines(klass, line_iter):
527
stanza = rio.read_patch_stanza(line_iter)
531
start = next(line_iter)
532
except StopIteration:
535
if start.startswith(b'# Begin patch'):
537
for line in line_iter:
538
if line.startswith(b'# Begin bundle'):
541
patch_lines.append(line)
544
patch = b''.join(patch_lines)
545
if start is not None:
546
if start.startswith(b'# Begin bundle'):
547
bundle = b''.join(line_iter)
549
raise errors.IllegalMergeDirectivePayload(start)
550
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
552
for key in ('revision_id', 'testament_sha1', 'target_branch',
553
'source_branch', 'message', 'base_revision_id'):
555
kwargs[key] = stanza.get(key)
558
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
559
kwargs['base_revision_id'] =\
560
kwargs['base_revision_id'].encode('utf-8')
561
if 'testament_sha1' in kwargs:
562
kwargs['testament_sha1'] = kwargs['testament_sha1'].encode('ascii')
563
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
567
lines = self._to_lines(base_revision=True)
568
if self.patch is not None:
569
lines.append(b'# Begin patch\n')
570
lines.extend(self.patch.splitlines(True))
571
if self.bundle is not None:
572
lines.append(b'# Begin bundle\n')
573
lines.extend(self.bundle.splitlines(True))
577
def from_objects(klass, repository, revision_id, time, timezone,
578
target_branch, include_patch=True, include_bundle=True,
579
local_target_branch=None, public_branch=None, message=None,
580
base_revision_id=None):
581
"""Generate a merge directive from various objects
583
:param repository: The repository containing the revision
584
:param revision_id: The revision to merge
585
:param time: The POSIX timestamp of the date the request was issued.
586
:param timezone: The timezone of the request
587
:param target_branch: The url of the branch to merge into
588
:param include_patch: If true, include a preview patch
589
:param include_bundle: If true, include a bundle
590
:param local_target_branch: the target branch, either itself or a local copy
591
:param public_branch: location of a public branch containing
593
:param message: Message to use when committing the merge
594
:return: The merge directive
596
The public branch is always used if supplied. If no bundle is
597
included, the public branch must be supplied, and will be verified.
599
If the message is not supplied, the message from revision_id will be
602
with cleanup.ExitStack() as exit_stack:
603
exit_stack.enter_context(repository.lock_write())
604
t_revision_id = revision_id
605
if revision_id == b'null:':
607
t = testament.StrictTestament3.from_revision(repository,
609
if local_target_branch is None:
610
submit_branch = _mod_branch.Branch.open(target_branch)
612
submit_branch = local_target_branch
613
exit_stack.enter_context(submit_branch.lock_read())
614
if submit_branch.get_public_branch() is not None:
615
target_branch = submit_branch.get_public_branch()
616
submit_revision_id = submit_branch.last_revision()
617
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
618
graph = repository.get_graph(submit_branch.repository)
619
ancestor_id = graph.find_unique_lca(revision_id,
621
if base_revision_id is None:
622
base_revision_id = ancestor_id
623
if (include_patch, include_bundle) != (False, False):
624
repository.fetch(submit_branch.repository, submit_revision_id)
626
patch = klass._generate_diff(repository, revision_id,
632
bundle = base64.b64encode(klass._generate_bundle(repository, revision_id,
637
if public_branch is not None and not include_bundle:
638
public_branch_obj = _mod_branch.Branch.open(public_branch)
639
exit_stack.enter_context(public_branch_obj.lock_read())
640
if not public_branch_obj.repository.has_revision(
642
raise errors.PublicBranchOutOfDate(public_branch,
644
testament_sha1 = t.as_sha1()
645
return klass(revision_id, testament_sha1, time, timezone,
646
target_branch, patch, public_branch, message, bundle,
649
def _verify_patch(self, repository):
650
calculated_patch = self._generate_diff(repository, self.revision_id,
651
self.base_revision_id)
652
# Convert line-endings to UNIX
653
stored_patch = re.sub(b'\r\n?', b'\n', self.patch)
654
calculated_patch = re.sub(b'\r\n?', b'\n', calculated_patch)
655
# Strip trailing whitespace
656
calculated_patch = re.sub(b' *\n', b'\n', calculated_patch)
657
stored_patch = re.sub(b' *\n', b'\n', stored_patch)
658
return (calculated_patch == stored_patch)
660
def get_merge_request(self, repository):
661
"""Provide data for performing a merge
663
Returns suggested base, suggested target, and patch verification status
665
verified = self._maybe_verify(repository)
666
return self.base_revision_id, self.revision_id, verified
668
def _maybe_verify(self, repository):
669
if self.patch is not None:
670
if self._verify_patch(repository):
675
return 'inapplicable'
678
class MergeDirectiveFormatRegistry(registry.Registry):
680
def register(self, directive, format_string=None):
681
if format_string is None:
682
format_string = directive._format_string
683
registry.Registry.register(self, format_string, directive)
686
_format_registry = MergeDirectiveFormatRegistry()
687
_format_registry.register(MergeDirective)
688
_format_registry.register(MergeDirective2)
689
# 0.19 never existed. It got renamed to 0.90. But by that point, there were
690
# already merge directives in the wild that used 0.19. Registering with the old
691
# format string to retain compatibility with those merge directives.
692
_format_registry.register(MergeDirective2,
693
b'Bazaar merge directive format 2 (Bazaar 0.19)')