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
19
from io import BytesIO
22
from . import lazy_import
23
lazy_import.lazy_import(globals(), """
25
branch as _mod_branch,
31
revision as _mod_revision,
36
from breezy.bzr import (
39
from breezy.bzr.bundle import (
40
serializer as bundle_serializer,
48
class MergeRequestBodyParams(object):
49
"""Parameter object for the merge_request_body hook."""
51
def __init__(self, body, orig_body, directive, to, basename, subject,
54
self.orig_body = orig_body
55
self.directive = directive
59
self.basename = basename
60
self.subject = subject
63
class MergeDirectiveHooks(hooks.Hooks):
64
"""Hooks for MergeDirective classes."""
67
hooks.Hooks.__init__(self, "breezy.merge_directive",
68
"BaseMergeDirective.hooks")
71
"Called with a MergeRequestBodyParams when a body is needed for"
72
" a merge request. Callbacks must return a body. If more"
73
" than one callback is registered, the output of one callback is"
74
" provided to the next.", (1, 15, 0))
77
class BaseMergeDirective(object):
78
"""A request to perform a merge into a branch.
80
This is the base class that all merge directive implementations
83
:cvar multiple_output_files: Whether or not this merge directive
84
stores a set of revisions in more than one file
87
hooks = MergeDirectiveHooks()
89
multiple_output_files = False
91
def __init__(self, revision_id, testament_sha1, time, timezone,
92
target_branch, patch=None, source_branch=None,
93
message=None, bundle=None):
96
:param revision_id: The revision to merge
97
:param testament_sha1: The sha1 of the testament of the revision to
99
:param time: The current POSIX timestamp time
100
:param timezone: The timezone offset
101
:param target_branch: Location of branch to apply the merge to
102
:param patch: The text of a diff or bundle
103
:param source_branch: A public location to merge the revision from
104
:param message: The message to use when committing this merge
106
self.revision_id = revision_id
107
self.testament_sha1 = testament_sha1
109
self.timezone = timezone
110
self.target_branch = target_branch
112
self.source_branch = source_branch
113
self.message = message
116
"""Serialize as a list of lines
118
:return: a list of lines
120
raise NotImplementedError(self.to_lines)
123
"""Serialize as a set of files.
125
:return: List of tuples with filename and contents as lines
127
raise NotImplementedError(self.to_files)
129
def get_raw_bundle(self):
130
"""Return the bundle for this merge directive.
132
:return: bundle text or None if there is no bundle
136
def _to_lines(self, base_revision=False):
137
"""Serialize as a list of lines
139
:return: a list of lines
141
time_str = timestamp.format_patch_date(self.time, self.timezone)
142
stanza = rio.Stanza(revision_id=self.revision_id, timestamp=time_str,
143
target_branch=self.target_branch,
144
testament_sha1=self.testament_sha1)
145
for key in ('source_branch', 'message'):
146
if self.__dict__[key] is not None:
147
stanza.add(key, self.__dict__[key])
149
stanza.add('base_revision_id', self.base_revision_id)
150
lines = [b'# ' + self._format_string + b'\n']
151
lines.extend(rio.to_patch_lines(stanza))
152
lines.append(b'# \n')
155
def write_to_directory(self, path):
156
"""Write this merge directive to a series of files in a directory.
158
:param path: Filesystem path to write to
160
raise NotImplementedError(self.write_to_directory)
163
def from_objects(klass, repository, revision_id, time, timezone,
164
target_branch, patch_type='bundle',
165
local_target_branch=None, public_branch=None, message=None):
166
"""Generate a merge directive from various objects
168
:param repository: The repository containing the revision
169
:param revision_id: The revision to merge
170
:param time: The POSIX timestamp of the date the request was issued.
171
:param timezone: The timezone of the request
172
:param target_branch: The url of the branch to merge into
173
:param patch_type: 'bundle', 'diff' or None, depending on the type of
175
:param local_target_branch: the submit branch, either itself or a local copy
176
:param public_branch: location of a public branch containing
178
:param message: Message to use when committing the merge
179
:return: The merge directive
181
The public branch is always used if supplied. If the patch_type is
182
not 'bundle', the public branch must be supplied, and will be verified.
184
If the message is not supplied, the message from revision_id will be
187
t_revision_id = revision_id
188
if revision_id == _mod_revision.NULL_REVISION:
190
t = testament.StrictTestament3.from_revision(repository, t_revision_id)
191
if local_target_branch is None:
192
submit_branch = _mod_branch.Branch.open(target_branch)
194
submit_branch = local_target_branch
195
if submit_branch.get_public_branch() is not None:
196
target_branch = submit_branch.get_public_branch()
197
if patch_type is None:
200
submit_revision_id = submit_branch.last_revision()
201
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
202
repository.fetch(submit_branch.repository, submit_revision_id)
203
graph = repository.get_graph()
204
ancestor_id = graph.find_unique_lca(revision_id,
206
type_handler = {'bundle': klass._generate_bundle,
207
'diff': klass._generate_diff,
208
None: lambda x, y, z: None}
209
patch = type_handler[patch_type](repository, revision_id,
212
if public_branch is not None and patch_type != 'bundle':
213
public_branch_obj = _mod_branch.Branch.open(public_branch)
214
if not public_branch_obj.repository.has_revision(revision_id):
215
raise errors.PublicBranchOutOfDate(public_branch,
218
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
219
patch, patch_type, public_branch, message)
221
def get_disk_name(self, branch):
222
"""Generate a suitable basename for storing this directive on disk
224
:param branch: The Branch this merge directive was generated fro
227
revno, revision_id = branch.last_revision_info()
228
if self.revision_id == revision_id:
232
revno = branch.revision_id_to_dotted_revno(self.revision_id)
233
except errors.NoSuchRevision:
235
nick = re.sub('(\\W+)', '-', branch.nick).strip('-')
236
return '%s-%s' % (nick, '.'.join(str(n) for n in revno))
239
def _generate_diff(repository, revision_id, ancestor_id):
240
tree_1 = repository.revision_tree(ancestor_id)
241
tree_2 = repository.revision_tree(revision_id)
243
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
247
def _generate_bundle(repository, revision_id, ancestor_id):
249
bundle_serializer.write_bundle(repository, revision_id,
253
def to_signed(self, branch):
254
"""Serialize as a signed string.
256
:param branch: The source branch, to get the signing strategy
259
my_gpg = gpg.GPGStrategy(branch.get_config_stack())
260
return my_gpg.sign(b''.join(self.to_lines()), gpg.MODE_CLEAR)
262
def to_email(self, mail_to, branch, sign=False):
263
"""Serialize as an email message.
265
:param mail_to: The address to mail the message to
266
:param branch: The source branch, to get the signing strategy and
268
:param sign: If True, gpg-sign the email
269
:return: an email message
271
mail_from = branch.get_config_stack().get('email')
272
if self.message is not None:
273
subject = self.message
275
revision = branch.repository.get_revision(self.revision_id)
276
subject = revision.message
278
body = self.to_signed(branch)
280
body = b''.join(self.to_lines())
281
message = email_message.EmailMessage(mail_from, mail_to, subject,
285
def install_revisions(self, target_repo):
286
"""Install revisions and return the target revision"""
287
if not target_repo.has_revision(self.revision_id):
288
if self.patch_type == 'bundle':
289
info = bundle_serializer.read_bundle(
290
BytesIO(self.get_raw_bundle()))
291
# We don't use the bundle's target revision, because
292
# MergeDirective.revision_id is authoritative.
294
info.install_revisions(target_repo, stream_input=False)
295
except errors.RevisionNotPresent:
296
# At least one dependency isn't present. Try installing
297
# missing revisions from the submit branch
300
_mod_branch.Branch.open(self.target_branch)
301
except errors.NotBranchError:
302
raise errors.TargetNotBranch(self.target_branch)
303
missing_revisions = []
304
bundle_revisions = set(r.revision_id for r in
306
for revision in info.real_revisions:
307
for parent_id in revision.parent_ids:
308
if (parent_id not in bundle_revisions
309
and not target_repo.has_revision(parent_id)):
310
missing_revisions.append(parent_id)
311
# reverse missing revisions to try to get heads first
313
unique_missing_set = set()
314
for revision in reversed(missing_revisions):
315
if revision in unique_missing_set:
317
unique_missing.append(revision)
318
unique_missing_set.add(revision)
319
for missing_revision in unique_missing:
320
target_repo.fetch(submit_branch.repository,
322
info.install_revisions(target_repo, stream_input=False)
324
source_branch = _mod_branch.Branch.open(self.source_branch)
325
target_repo.fetch(source_branch.repository, self.revision_id)
326
return self.revision_id
328
def compose_merge_request(self, mail_client, to, body, branch, tree=None):
329
"""Compose a request to merge this directive.
331
:param mail_client: The mail client to use for composing this request.
332
:param to: The address to compose the request to.
333
:param branch: The Branch that was used to produce this directive.
334
:param tree: The Tree (if any) for the Branch used to produce this
337
basename = self.get_disk_name(branch)
339
if self.message is not None:
340
subject += self.message
342
revision = branch.repository.get_revision(self.revision_id)
343
subject += revision.get_summary()
344
if getattr(mail_client, 'supports_body', False):
346
for hook in self.hooks['merge_request_body']:
347
params = MergeRequestBodyParams(body, orig_body, self,
348
to, basename, subject, branch,
351
elif len(self.hooks['merge_request_body']) > 0:
352
trace.warning('Cannot run merge_request_body hooks because mail'
353
' client %s does not support message bodies.',
354
mail_client.__class__.__name__)
355
mail_client.compose_merge_request(to, subject,
356
b''.join(self.to_lines()),
360
class MergeDirective(BaseMergeDirective):
362
"""A request to perform a merge into a branch.
364
Designed to be serialized and mailed. It provides all the information
365
needed to perform a merge automatically, by providing at minimum a revision
366
bundle or the location of a branch.
368
The serialization format is robust against certain common forms of
369
deterioration caused by mailing.
371
The format is also designed to be patch-compatible. If the directive
372
includes a diff or revision bundle, it should be possible to apply it
373
directly using the standard patch program.
376
_format_string = b'Bazaar merge directive format 1'
378
def __init__(self, revision_id, testament_sha1, time, timezone,
379
target_branch, patch=None, patch_type=None,
380
source_branch=None, message=None, bundle=None):
383
:param revision_id: The revision to merge
384
:param testament_sha1: The sha1 of the testament of the revision to
386
:param time: The current POSIX timestamp time
387
:param timezone: The timezone offset
388
:param target_branch: Location of the branch to apply the merge to
389
:param patch: The text of a diff or bundle
390
:param patch_type: None, "diff" or "bundle", depending on the contents
392
:param source_branch: A public location to merge the revision from
393
:param message: The message to use when committing this merge
395
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
396
timezone, target_branch, patch, source_branch, message)
397
if patch_type not in (None, 'diff', 'bundle'):
398
raise ValueError(patch_type)
399
if patch_type != 'bundle' and source_branch is None:
400
raise errors.NoMergeSource()
401
if patch_type is not None and patch is None:
402
raise errors.PatchMissing(patch_type)
403
self.patch_type = patch_type
405
def clear_payload(self):
407
self.patch_type = None
409
def get_raw_bundle(self):
413
if self.patch_type == 'bundle':
418
bundle = property(_bundle)
421
def from_lines(klass, lines):
422
"""Deserialize a MergeRequest from an iterable of lines
424
:param lines: An iterable of lines
425
:return: a MergeRequest
427
line_iter = iter(lines)
429
for line in line_iter:
430
if line.startswith(b'# Bazaar merge directive format '):
431
return _format_registry.get(line[2:].rstrip())._from_lines(
433
firstline = firstline or line.strip()
434
raise errors.NotAMergeDirective(firstline)
437
def _from_lines(klass, line_iter):
438
stanza = rio.read_patch_stanza(line_iter)
439
patch_lines = list(line_iter)
440
if len(patch_lines) == 0:
444
patch = b''.join(patch_lines)
446
bundle_serializer.read_bundle(BytesIO(patch))
447
except (errors.NotABundle, errors.BundleNotSupported,
451
patch_type = 'bundle'
452
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
454
for key in ('revision_id', 'testament_sha1', 'target_branch',
455
'source_branch', 'message'):
457
kwargs[key] = stanza.get(key)
460
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
461
if 'testament_sha1' in kwargs:
462
kwargs['testament_sha1'] = kwargs['testament_sha1'].encode('ascii')
463
return MergeDirective(time=time, timezone=timezone,
464
patch_type=patch_type, patch=patch, **kwargs)
467
lines = self._to_lines()
468
if self.patch is not None:
469
lines.extend(self.patch.splitlines(True))
473
def _generate_bundle(repository, revision_id, ancestor_id):
475
bundle_serializer.write_bundle(repository, revision_id,
476
ancestor_id, s, '0.9')
479
def get_merge_request(self, repository):
480
"""Provide data for performing a merge
482
Returns suggested base, suggested target, and patch verification status
484
return None, self.revision_id, 'inapplicable'
487
class MergeDirective2(BaseMergeDirective):
489
_format_string = b'Bazaar merge directive format 2 (Bazaar 0.90)'
491
def __init__(self, revision_id, testament_sha1, time, timezone,
492
target_branch, patch=None, source_branch=None, message=None,
493
bundle=None, base_revision_id=None):
494
if source_branch is None and bundle is None:
495
raise errors.NoMergeSource()
496
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
497
timezone, target_branch, patch, source_branch, message)
499
self.base_revision_id = base_revision_id
501
def _patch_type(self):
502
if self.bundle is not None:
504
elif self.patch is not None:
509
patch_type = property(_patch_type)
511
def clear_payload(self):
515
def get_raw_bundle(self):
516
if self.bundle is None:
519
return base64.b64decode(self.bundle)
522
def _from_lines(klass, line_iter):
523
stanza = rio.read_patch_stanza(line_iter)
527
start = next(line_iter)
528
except StopIteration:
531
if start.startswith(b'# Begin patch'):
533
for line in line_iter:
534
if line.startswith(b'# Begin bundle'):
537
patch_lines.append(line)
540
patch = b''.join(patch_lines)
541
if start is not None:
542
if start.startswith(b'# Begin bundle'):
543
bundle = b''.join(line_iter)
545
raise errors.IllegalMergeDirectivePayload(start)
546
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
548
for key in ('revision_id', 'testament_sha1', 'target_branch',
549
'source_branch', 'message', 'base_revision_id'):
551
kwargs[key] = stanza.get(key)
554
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
555
kwargs['base_revision_id'] =\
556
kwargs['base_revision_id'].encode('utf-8')
557
if 'testament_sha1' in kwargs:
558
kwargs['testament_sha1'] = kwargs['testament_sha1'].encode('ascii')
559
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
563
lines = self._to_lines(base_revision=True)
564
if self.patch is not None:
565
lines.append(b'# Begin patch\n')
566
lines.extend(self.patch.splitlines(True))
567
if self.bundle is not None:
568
lines.append(b'# Begin bundle\n')
569
lines.extend(self.bundle.splitlines(True))
573
def from_objects(klass, repository, revision_id, time, timezone,
574
target_branch, include_patch=True, include_bundle=True,
575
local_target_branch=None, public_branch=None, message=None,
576
base_revision_id=None):
577
"""Generate a merge directive from various objects
579
:param repository: The repository containing the revision
580
:param revision_id: The revision to merge
581
:param time: The POSIX timestamp of the date the request was issued.
582
:param timezone: The timezone of the request
583
:param target_branch: The url of the branch to merge into
584
:param include_patch: If true, include a preview patch
585
:param include_bundle: If true, include a bundle
586
:param local_target_branch: the target branch, either itself or a local copy
587
:param public_branch: location of a public branch containing
589
:param message: Message to use when committing the merge
590
:return: The merge directive
592
The public branch is always used if supplied. If no bundle is
593
included, the public branch must be supplied, and will be verified.
595
If the message is not supplied, the message from revision_id will be
598
with contextlib.ExitStack() as exit_stack:
599
exit_stack.enter_context(repository.lock_write())
600
t_revision_id = revision_id
601
if revision_id == b'null:':
603
t = testament.StrictTestament3.from_revision(repository,
605
if local_target_branch is None:
606
submit_branch = _mod_branch.Branch.open(target_branch)
608
submit_branch = local_target_branch
609
exit_stack.enter_context(submit_branch.lock_read())
610
if submit_branch.get_public_branch() is not None:
611
target_branch = submit_branch.get_public_branch()
612
submit_revision_id = submit_branch.last_revision()
613
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
614
graph = repository.get_graph(submit_branch.repository)
615
ancestor_id = graph.find_unique_lca(revision_id,
617
if base_revision_id is None:
618
base_revision_id = ancestor_id
619
if (include_patch, include_bundle) != (False, False):
620
repository.fetch(submit_branch.repository, submit_revision_id)
622
patch = klass._generate_diff(repository, revision_id,
628
bundle = base64.b64encode(klass._generate_bundle(repository, revision_id,
633
if public_branch is not None and not include_bundle:
634
public_branch_obj = _mod_branch.Branch.open(public_branch)
635
exit_stack.enter_context(public_branch_obj.lock_read())
636
if not public_branch_obj.repository.has_revision(
638
raise errors.PublicBranchOutOfDate(public_branch,
640
testament_sha1 = t.as_sha1()
641
return klass(revision_id, testament_sha1, time, timezone,
642
target_branch, patch, public_branch, message, bundle,
645
def _verify_patch(self, repository):
646
calculated_patch = self._generate_diff(repository, self.revision_id,
647
self.base_revision_id)
648
# Convert line-endings to UNIX
649
stored_patch = re.sub(b'\r\n?', b'\n', self.patch)
650
calculated_patch = re.sub(b'\r\n?', b'\n', calculated_patch)
651
# Strip trailing whitespace
652
calculated_patch = re.sub(b' *\n', b'\n', calculated_patch)
653
stored_patch = re.sub(b' *\n', b'\n', stored_patch)
654
return (calculated_patch == stored_patch)
656
def get_merge_request(self, repository):
657
"""Provide data for performing a merge
659
Returns suggested base, suggested target, and patch verification status
661
verified = self._maybe_verify(repository)
662
return self.base_revision_id, self.revision_id, verified
664
def _maybe_verify(self, repository):
665
if self.patch is not None:
666
if self._verify_patch(repository):
671
return 'inapplicable'
674
class MergeDirectiveFormatRegistry(registry.Registry):
676
def register(self, directive, format_string=None):
677
if format_string is None:
678
format_string = directive._format_string
679
registry.Registry.register(self, format_string, directive)
682
_format_registry = MergeDirectiveFormatRegistry()
683
_format_registry.register(MergeDirective)
684
_format_registry.register(MergeDirective2)
685
# 0.19 never existed. It got renamed to 0.90. But by that point, there were
686
# already merge directives in the wild that used 0.19. Registering with the old
687
# format string to retain compatibility with those merge directives.
688
_format_registry.register(MergeDirective2,
689
b'Bazaar merge directive format 2 (Bazaar 0.19)')