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.bundle import (
41
serializer as bundle_serializer,
49
class MergeRequestBodyParams(object):
50
"""Parameter object for the merge_request_body hook."""
52
def __init__(self, body, orig_body, directive, to, basename, subject,
55
self.orig_body = orig_body
56
self.directive = directive
60
self.basename = basename
61
self.subject = subject
64
class MergeDirectiveHooks(hooks.Hooks):
65
"""Hooks for MergeDirective classes."""
68
hooks.Hooks.__init__(self, "breezy.merge_directive",
69
"BaseMergeDirective.hooks")
72
"Called with a MergeRequestBodyParams when a body is needed for"
73
" a merge request. Callbacks must return a body. If more"
74
" than one callback is registered, the output of one callback is"
75
" provided to the next.", (1, 15, 0))
78
class BaseMergeDirective(object):
79
"""A request to perform a merge into a branch.
81
This is the base class that all merge directive implementations
84
:cvar multiple_output_files: Whether or not this merge directive
85
stores a set of revisions in more than one file
88
hooks = MergeDirectiveHooks()
90
multiple_output_files = False
92
def __init__(self, revision_id, testament_sha1, time, timezone,
93
target_branch, patch=None, source_branch=None,
94
message=None, bundle=None):
97
:param revision_id: The revision to merge
98
:param testament_sha1: The sha1 of the testament of the revision to
100
:param time: The current POSIX timestamp time
101
:param timezone: The timezone offset
102
:param target_branch: Location of branch to apply the merge to
103
:param patch: The text of a diff or bundle
104
:param source_branch: A public location to merge the revision from
105
:param message: The message to use when committing this merge
107
self.revision_id = revision_id
108
self.testament_sha1 = testament_sha1
110
self.timezone = timezone
111
self.target_branch = target_branch
113
self.source_branch = source_branch
114
self.message = message
117
"""Serialize as a list of lines
119
:return: a list of lines
121
raise NotImplementedError(self.to_lines)
124
"""Serialize as a set of files.
126
:return: List of tuples with filename and contents as lines
128
raise NotImplementedError(self.to_files)
130
def get_raw_bundle(self):
131
"""Return the bundle for this merge directive.
133
:return: bundle text or None if there is no bundle
137
def _to_lines(self, base_revision=False):
138
"""Serialize as a list of lines
140
:return: a list of lines
142
time_str = timestamp.format_patch_date(self.time, self.timezone)
143
stanza = rio.Stanza(revision_id=self.revision_id, timestamp=time_str,
144
target_branch=self.target_branch,
145
testament_sha1=self.testament_sha1)
146
for key in ('source_branch', 'message'):
147
if self.__dict__[key] is not None:
148
stanza.add(key, self.__dict__[key])
150
stanza.add('base_revision_id', self.base_revision_id)
151
lines = [b'# ' + self._format_string + b'\n']
152
lines.extend(rio.to_patch_lines(stanza))
153
lines.append(b'# \n')
156
def write_to_directory(self, path):
157
"""Write this merge directive to a series of files in a directory.
159
:param path: Filesystem path to write to
161
raise NotImplementedError(self.write_to_directory)
164
def from_objects(klass, repository, revision_id, time, timezone,
165
target_branch, patch_type='bundle',
166
local_target_branch=None, public_branch=None, message=None):
167
"""Generate a merge directive from various objects
169
:param repository: The repository containing the revision
170
:param revision_id: The revision to merge
171
:param time: The POSIX timestamp of the date the request was issued.
172
:param timezone: The timezone of the request
173
:param target_branch: The url of the branch to merge into
174
:param patch_type: 'bundle', 'diff' or None, depending on the type of
176
:param local_target_branch: the submit branch, either itself or a local copy
177
:param public_branch: location of a public branch containing
179
:param message: Message to use when committing the merge
180
:return: The merge directive
182
The public branch is always used if supplied. If the patch_type is
183
not 'bundle', the public branch must be supplied, and will be verified.
185
If the message is not supplied, the message from revision_id will be
188
t_revision_id = revision_id
189
if revision_id == _mod_revision.NULL_REVISION:
191
t = testament.StrictTestament3.from_revision(repository, t_revision_id)
192
if local_target_branch is None:
193
submit_branch = _mod_branch.Branch.open(target_branch)
195
submit_branch = local_target_branch
196
if submit_branch.get_public_branch() is not None:
197
target_branch = submit_branch.get_public_branch()
198
if patch_type is None:
201
submit_revision_id = submit_branch.last_revision()
202
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
203
repository.fetch(submit_branch.repository, submit_revision_id)
204
graph = repository.get_graph()
205
ancestor_id = graph.find_unique_lca(revision_id,
207
type_handler = {'bundle': klass._generate_bundle,
208
'diff': klass._generate_diff,
209
None: lambda x, y, z: None}
210
patch = type_handler[patch_type](repository, revision_id,
213
if public_branch is not None and patch_type != 'bundle':
214
public_branch_obj = _mod_branch.Branch.open(public_branch)
215
if not public_branch_obj.repository.has_revision(revision_id):
216
raise errors.PublicBranchOutOfDate(public_branch,
219
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
220
patch, patch_type, public_branch, message)
222
def get_disk_name(self, branch):
223
"""Generate a suitable basename for storing this directive on disk
225
:param branch: The Branch this merge directive was generated fro
228
revno, revision_id = branch.last_revision_info()
229
if self.revision_id == revision_id:
233
revno = branch.revision_id_to_dotted_revno(self.revision_id)
234
except errors.NoSuchRevision:
236
nick = re.sub('(\\W+)', '-', branch.nick).strip('-')
237
return '%s-%s' % (nick, '.'.join(str(n) for n in revno))
240
def _generate_diff(repository, revision_id, ancestor_id):
241
tree_1 = repository.revision_tree(ancestor_id)
242
tree_2 = repository.revision_tree(revision_id)
244
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
248
def _generate_bundle(repository, revision_id, ancestor_id):
250
bundle_serializer.write_bundle(repository, revision_id,
254
def to_signed(self, branch):
255
"""Serialize as a signed string.
257
:param branch: The source branch, to get the signing strategy
260
my_gpg = gpg.GPGStrategy(branch.get_config_stack())
261
return my_gpg.sign(b''.join(self.to_lines()), gpg.MODE_CLEAR)
263
def to_email(self, mail_to, branch, sign=False):
264
"""Serialize as an email message.
266
:param mail_to: The address to mail the message to
267
:param branch: The source branch, to get the signing strategy and
269
:param sign: If True, gpg-sign the email
270
:return: an email message
272
mail_from = branch.get_config_stack().get('email')
273
if self.message is not None:
274
subject = self.message
276
revision = branch.repository.get_revision(self.revision_id)
277
subject = revision.message
279
body = self.to_signed(branch)
281
body = b''.join(self.to_lines())
282
message = email_message.EmailMessage(mail_from, mail_to, subject,
286
def install_revisions(self, target_repo):
287
"""Install revisions and return the target revision"""
288
if not target_repo.has_revision(self.revision_id):
289
if self.patch_type == 'bundle':
290
info = bundle_serializer.read_bundle(
291
BytesIO(self.get_raw_bundle()))
292
# We don't use the bundle's target revision, because
293
# MergeDirective.revision_id is authoritative.
295
info.install_revisions(target_repo, stream_input=False)
296
except errors.RevisionNotPresent:
297
# At least one dependency isn't present. Try installing
298
# missing revisions from the submit branch
301
_mod_branch.Branch.open(self.target_branch)
302
except errors.NotBranchError:
303
raise errors.TargetNotBranch(self.target_branch)
304
missing_revisions = []
305
bundle_revisions = set(r.revision_id for r in
307
for revision in info.real_revisions:
308
for parent_id in revision.parent_ids:
309
if (parent_id not in bundle_revisions
310
and not target_repo.has_revision(parent_id)):
311
missing_revisions.append(parent_id)
312
# reverse missing revisions to try to get heads first
314
unique_missing_set = set()
315
for revision in reversed(missing_revisions):
316
if revision in unique_missing_set:
318
unique_missing.append(revision)
319
unique_missing_set.add(revision)
320
for missing_revision in unique_missing:
321
target_repo.fetch(submit_branch.repository,
323
info.install_revisions(target_repo, stream_input=False)
325
source_branch = _mod_branch.Branch.open(self.source_branch)
326
target_repo.fetch(source_branch.repository, self.revision_id)
327
return self.revision_id
329
def compose_merge_request(self, mail_client, to, body, branch, tree=None):
330
"""Compose a request to merge this directive.
332
:param mail_client: The mail client to use for composing this request.
333
:param to: The address to compose the request to.
334
:param branch: The Branch that was used to produce this directive.
335
:param tree: The Tree (if any) for the Branch used to produce this
338
basename = self.get_disk_name(branch)
340
if self.message is not None:
341
subject += self.message
343
revision = branch.repository.get_revision(self.revision_id)
344
subject += revision.get_summary()
345
if getattr(mail_client, 'supports_body', False):
347
for hook in self.hooks['merge_request_body']:
348
params = MergeRequestBodyParams(body, orig_body, self,
349
to, basename, subject, branch,
352
elif len(self.hooks['merge_request_body']) > 0:
353
trace.warning('Cannot run merge_request_body hooks because mail'
354
' client %s does not support message bodies.',
355
mail_client.__class__.__name__)
356
mail_client.compose_merge_request(to, subject,
357
b''.join(self.to_lines()),
361
class MergeDirective(BaseMergeDirective):
363
"""A request to perform a merge into a branch.
365
Designed to be serialized and mailed. It provides all the information
366
needed to perform a merge automatically, by providing at minimum a revision
367
bundle or the location of a branch.
369
The serialization format is robust against certain common forms of
370
deterioration caused by mailing.
372
The format is also designed to be patch-compatible. If the directive
373
includes a diff or revision bundle, it should be possible to apply it
374
directly using the standard patch program.
377
_format_string = b'Bazaar merge directive format 1'
379
def __init__(self, revision_id, testament_sha1, time, timezone,
380
target_branch, patch=None, patch_type=None,
381
source_branch=None, message=None, bundle=None):
384
:param revision_id: The revision to merge
385
:param testament_sha1: The sha1 of the testament of the revision to
387
:param time: The current POSIX timestamp time
388
:param timezone: The timezone offset
389
:param target_branch: Location of the branch to apply the merge to
390
:param patch: The text of a diff or bundle
391
:param patch_type: None, "diff" or "bundle", depending on the contents
393
:param source_branch: A public location to merge the revision from
394
:param message: The message to use when committing this merge
396
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
397
timezone, target_branch, patch, source_branch, message)
398
if patch_type not in (None, 'diff', 'bundle'):
399
raise ValueError(patch_type)
400
if patch_type != 'bundle' and source_branch is None:
401
raise errors.NoMergeSource()
402
if patch_type is not None and patch is None:
403
raise errors.PatchMissing(patch_type)
404
self.patch_type = patch_type
406
def clear_payload(self):
408
self.patch_type = None
410
def get_raw_bundle(self):
414
if self.patch_type == 'bundle':
419
bundle = property(_bundle)
422
def from_lines(klass, lines):
423
"""Deserialize a MergeRequest from an iterable of lines
425
:param lines: An iterable of lines
426
:return: a MergeRequest
428
line_iter = iter(lines)
430
for line in line_iter:
431
if line.startswith(b'# Bazaar merge directive format '):
432
return _format_registry.get(line[2:].rstrip())._from_lines(
434
firstline = firstline or line.strip()
435
raise errors.NotAMergeDirective(firstline)
438
def _from_lines(klass, line_iter):
439
stanza = rio.read_patch_stanza(line_iter)
440
patch_lines = list(line_iter)
441
if len(patch_lines) == 0:
445
patch = b''.join(patch_lines)
447
bundle_serializer.read_bundle(BytesIO(patch))
448
except (errors.NotABundle, errors.BundleNotSupported,
452
patch_type = 'bundle'
453
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
455
for key in ('revision_id', 'testament_sha1', 'target_branch',
456
'source_branch', 'message'):
458
kwargs[key] = stanza.get(key)
461
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
462
if 'testament_sha1' in kwargs:
463
kwargs['testament_sha1'] = kwargs['testament_sha1'].encode('ascii')
464
return MergeDirective(time=time, timezone=timezone,
465
patch_type=patch_type, patch=patch, **kwargs)
468
lines = self._to_lines()
469
if self.patch is not None:
470
lines.extend(self.patch.splitlines(True))
474
def _generate_bundle(repository, revision_id, ancestor_id):
476
bundle_serializer.write_bundle(repository, revision_id,
477
ancestor_id, s, '0.9')
480
def get_merge_request(self, repository):
481
"""Provide data for performing a merge
483
Returns suggested base, suggested target, and patch verification status
485
return None, self.revision_id, 'inapplicable'
488
class MergeDirective2(BaseMergeDirective):
490
_format_string = b'Bazaar merge directive format 2 (Bazaar 0.90)'
492
def __init__(self, revision_id, testament_sha1, time, timezone,
493
target_branch, patch=None, source_branch=None, message=None,
494
bundle=None, base_revision_id=None):
495
if source_branch is None and bundle is None:
496
raise errors.NoMergeSource()
497
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
498
timezone, target_branch, patch, source_branch, message)
500
self.base_revision_id = base_revision_id
502
def _patch_type(self):
503
if self.bundle is not None:
505
elif self.patch is not None:
510
patch_type = property(_patch_type)
512
def clear_payload(self):
516
def get_raw_bundle(self):
517
if self.bundle is None:
520
return base64.b64decode(self.bundle)
523
def _from_lines(klass, line_iter):
524
stanza = rio.read_patch_stanza(line_iter)
528
start = next(line_iter)
529
except StopIteration:
532
if start.startswith(b'# Begin patch'):
534
for line in line_iter:
535
if line.startswith(b'# Begin bundle'):
538
patch_lines.append(line)
541
patch = b''.join(patch_lines)
542
if start is not None:
543
if start.startswith(b'# Begin bundle'):
544
bundle = b''.join(line_iter)
546
raise errors.IllegalMergeDirectivePayload(start)
547
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
549
for key in ('revision_id', 'testament_sha1', 'target_branch',
550
'source_branch', 'message', 'base_revision_id'):
552
kwargs[key] = stanza.get(key)
555
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
556
kwargs['base_revision_id'] =\
557
kwargs['base_revision_id'].encode('utf-8')
558
if 'testament_sha1' in kwargs:
559
kwargs['testament_sha1'] = kwargs['testament_sha1'].encode('ascii')
560
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
564
lines = self._to_lines(base_revision=True)
565
if self.patch is not None:
566
lines.append(b'# Begin patch\n')
567
lines.extend(self.patch.splitlines(True))
568
if self.bundle is not None:
569
lines.append(b'# Begin bundle\n')
570
lines.extend(self.bundle.splitlines(True))
574
def from_objects(klass, repository, revision_id, time, timezone,
575
target_branch, include_patch=True, include_bundle=True,
576
local_target_branch=None, public_branch=None, message=None,
577
base_revision_id=None):
578
"""Generate a merge directive from various objects
580
:param repository: The repository containing the revision
581
:param revision_id: The revision to merge
582
:param time: The POSIX timestamp of the date the request was issued.
583
:param timezone: The timezone of the request
584
:param target_branch: The url of the branch to merge into
585
:param include_patch: If true, include a preview patch
586
:param include_bundle: If true, include a bundle
587
:param local_target_branch: the target branch, either itself or a local copy
588
:param public_branch: location of a public branch containing
590
:param message: Message to use when committing the merge
591
:return: The merge directive
593
The public branch is always used if supplied. If no bundle is
594
included, the public branch must be supplied, and will be verified.
596
If the message is not supplied, the message from revision_id will be
601
repository.lock_write()
602
locked.append(repository)
603
t_revision_id = revision_id
604
if revision_id == b'null:':
606
t = testament.StrictTestament3.from_revision(repository,
608
if local_target_branch is None:
609
submit_branch = _mod_branch.Branch.open(target_branch)
611
submit_branch = local_target_branch
612
submit_branch.lock_read()
613
locked.append(submit_branch)
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
public_branch_obj.lock_read()
640
locked.append(public_branch_obj)
641
if not public_branch_obj.repository.has_revision(
643
raise errors.PublicBranchOutOfDate(public_branch,
645
testament_sha1 = t.as_sha1()
647
for entry in reversed(locked):
649
return klass(revision_id, testament_sha1, time, timezone,
650
target_branch, patch, public_branch, message, bundle,
653
def _verify_patch(self, repository):
654
calculated_patch = self._generate_diff(repository, self.revision_id,
655
self.base_revision_id)
656
# Convert line-endings to UNIX
657
stored_patch = re.sub(b'\r\n?', b'\n', self.patch)
658
calculated_patch = re.sub(b'\r\n?', b'\n', calculated_patch)
659
# Strip trailing whitespace
660
calculated_patch = re.sub(b' *\n', b'\n', calculated_patch)
661
stored_patch = re.sub(b' *\n', b'\n', stored_patch)
662
return (calculated_patch == stored_patch)
664
def get_merge_request(self, repository):
665
"""Provide data for performing a merge
667
Returns suggested base, suggested target, and patch verification status
669
verified = self._maybe_verify(repository)
670
return self.base_revision_id, self.revision_id, verified
672
def _maybe_verify(self, repository):
673
if self.patch is not None:
674
if self._verify_patch(repository):
679
return 'inapplicable'
682
class MergeDirectiveFormatRegistry(registry.Registry):
684
def register(self, directive, format_string=None):
685
if format_string is None:
686
format_string = directive._format_string
687
registry.Registry.register(self, format_string, directive)
690
_format_registry = MergeDirectiveFormatRegistry()
691
_format_registry.register(MergeDirective)
692
_format_registry.register(MergeDirective2)
693
# 0.19 never existed. It got renamed to 0.90. But by that point, there were
694
# already merge directives in the wild that used 0.19. Registering with the old
695
# format string to retain compatibility with those merge directives.
696
_format_registry.register(MergeDirective2,
697
b'Bazaar merge directive format 2 (Bazaar 0.19)')