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,
38
from breezy.bundle import (
39
serializer as bundle_serializer,
47
class MergeRequestBodyParams(object):
48
"""Parameter object for the merge_request_body hook."""
50
def __init__(self, body, orig_body, directive, to, basename, subject,
53
self.orig_body = orig_body
54
self.directive = directive
58
self.basename = basename
59
self.subject = subject
62
class MergeDirectiveHooks(hooks.Hooks):
63
"""Hooks for MergeDirective classes."""
66
hooks.Hooks.__init__(self, "breezy.merge_directive",
67
"BaseMergeDirective.hooks")
70
"Called with a MergeRequestBodyParams when a body is needed for"
71
" a merge request. Callbacks must return a body. If more"
72
" than one callback is registered, the output of one callback is"
73
" provided to the next.", (1, 15, 0))
76
class BaseMergeDirective(object):
77
"""A request to perform a merge into a branch.
79
This is the base class that all merge directive implementations
82
:cvar multiple_output_files: Whether or not this merge directive
83
stores a set of revisions in more than one file
86
hooks = MergeDirectiveHooks()
88
multiple_output_files = False
90
def __init__(self, revision_id, testament_sha1, time, timezone,
91
target_branch, patch=None, source_branch=None,
92
message=None, bundle=None):
95
:param revision_id: The revision to merge
96
:param testament_sha1: The sha1 of the testament of the revision to
98
:param time: The current POSIX timestamp time
99
:param timezone: The timezone offset
100
:param target_branch: Location of branch to apply the merge to
101
:param patch: The text of a diff or bundle
102
:param source_branch: A public location to merge the revision from
103
:param message: The message to use when committing this merge
105
self.revision_id = revision_id
106
self.testament_sha1 = testament_sha1
108
self.timezone = timezone
109
self.target_branch = target_branch
111
self.source_branch = source_branch
112
self.message = message
115
"""Serialize as a list of lines
117
:return: a list of lines
119
raise NotImplementedError(self.to_lines)
122
"""Serialize as a set of files.
124
:return: List of tuples with filename and contents as lines
126
raise NotImplementedError(self.to_files)
128
def get_raw_bundle(self):
129
"""Return the bundle for this merge directive.
131
:return: bundle text or None if there is no bundle
135
def _to_lines(self, base_revision=False):
136
"""Serialize as a list of lines
138
:return: a list of lines
140
time_str = timestamp.format_patch_date(self.time, self.timezone)
141
stanza = rio.Stanza(revision_id=self.revision_id, timestamp=time_str,
142
target_branch=self.target_branch,
143
testament_sha1=self.testament_sha1)
144
for key in ('source_branch', 'message'):
145
if self.__dict__[key] is not None:
146
stanza.add(key, self.__dict__[key])
148
stanza.add('base_revision_id', self.base_revision_id)
149
lines = [b'# ' + self._format_string + b'\n']
150
lines.extend(rio.to_patch_lines(stanza))
151
lines.append(b'# \n')
154
def write_to_directory(self, path):
155
"""Write this merge directive to a series of files in a directory.
157
:param path: Filesystem path to write to
159
raise NotImplementedError(self.write_to_directory)
162
def from_objects(klass, repository, revision_id, time, timezone,
163
target_branch, patch_type='bundle',
164
local_target_branch=None, public_branch=None, message=None):
165
"""Generate a merge directive from various objects
167
:param repository: The repository containing the revision
168
:param revision_id: The revision to merge
169
:param time: The POSIX timestamp of the date the request was issued.
170
:param timezone: The timezone of the request
171
:param target_branch: The url of the branch to merge into
172
:param patch_type: 'bundle', 'diff' or None, depending on the type of
174
:param local_target_branch: the submit branch, either itself or a local copy
175
:param public_branch: location of a public branch containing
177
:param message: Message to use when committing the merge
178
:return: The merge directive
180
The public branch is always used if supplied. If the patch_type is
181
not 'bundle', the public branch must be supplied, and will be verified.
183
If the message is not supplied, the message from revision_id will be
186
t_revision_id = revision_id
187
if revision_id == _mod_revision.NULL_REVISION:
189
t = testament.StrictTestament3.from_revision(repository, t_revision_id)
190
if local_target_branch is None:
191
submit_branch = _mod_branch.Branch.open(target_branch)
193
submit_branch = local_target_branch
194
if submit_branch.get_public_branch() is not None:
195
target_branch = submit_branch.get_public_branch()
196
if patch_type is None:
199
submit_revision_id = submit_branch.last_revision()
200
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
201
repository.fetch(submit_branch.repository, submit_revision_id)
202
graph = repository.get_graph()
203
ancestor_id = graph.find_unique_lca(revision_id,
205
type_handler = {'bundle': klass._generate_bundle,
206
'diff': klass._generate_diff,
207
None: lambda x, y, z: None}
208
patch = type_handler[patch_type](repository, revision_id,
211
if public_branch is not None and patch_type != 'bundle':
212
public_branch_obj = _mod_branch.Branch.open(public_branch)
213
if not public_branch_obj.repository.has_revision(revision_id):
214
raise errors.PublicBranchOutOfDate(public_branch,
217
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
218
patch, patch_type, public_branch, message)
220
def get_disk_name(self, branch):
221
"""Generate a suitable basename for storing this directive on disk
223
:param branch: The Branch this merge directive was generated fro
226
revno, revision_id = branch.last_revision_info()
227
if self.revision_id == revision_id:
231
revno = branch.revision_id_to_dotted_revno(self.revision_id)
232
except errors.NoSuchRevision:
234
nick = re.sub('(\\W+)', '-', branch.nick).strip('-')
235
return '%s-%s' % (nick, '.'.join(str(n) for n in revno))
238
def _generate_diff(repository, revision_id, ancestor_id):
239
tree_1 = repository.revision_tree(ancestor_id)
240
tree_2 = repository.revision_tree(revision_id)
242
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
246
def _generate_bundle(repository, revision_id, ancestor_id):
248
bundle_serializer.write_bundle(repository, revision_id,
252
def to_signed(self, branch):
253
"""Serialize as a signed string.
255
:param branch: The source branch, to get the signing strategy
258
my_gpg = gpg.GPGStrategy(branch.get_config_stack())
259
return my_gpg.sign(b''.join(self.to_lines()), gpg.MODE_CLEAR)
261
def to_email(self, mail_to, branch, sign=False):
262
"""Serialize as an email message.
264
:param mail_to: The address to mail the message to
265
:param branch: The source branch, to get the signing strategy and
267
:param sign: If True, gpg-sign the email
268
:return: an email message
270
mail_from = branch.get_config_stack().get('email')
271
if self.message is not None:
272
subject = self.message
274
revision = branch.repository.get_revision(self.revision_id)
275
subject = revision.message
277
body = self.to_signed(branch)
279
body = b''.join(self.to_lines())
280
message = email_message.EmailMessage(mail_from, mail_to, subject,
284
def install_revisions(self, target_repo):
285
"""Install revisions and return the target revision"""
286
if not target_repo.has_revision(self.revision_id):
287
if self.patch_type == 'bundle':
288
info = bundle_serializer.read_bundle(
289
BytesIO(self.get_raw_bundle()))
290
# We don't use the bundle's target revision, because
291
# MergeDirective.revision_id is authoritative.
293
info.install_revisions(target_repo, stream_input=False)
294
except errors.RevisionNotPresent:
295
# At least one dependency isn't present. Try installing
296
# missing revisions from the submit branch
299
_mod_branch.Branch.open(self.target_branch)
300
except errors.NotBranchError:
301
raise errors.TargetNotBranch(self.target_branch)
302
missing_revisions = []
303
bundle_revisions = set(r.revision_id for r in
305
for revision in info.real_revisions:
306
for parent_id in revision.parent_ids:
307
if (parent_id not in bundle_revisions
308
and not target_repo.has_revision(parent_id)):
309
missing_revisions.append(parent_id)
310
# reverse missing revisions to try to get heads first
312
unique_missing_set = set()
313
for revision in reversed(missing_revisions):
314
if revision in unique_missing_set:
316
unique_missing.append(revision)
317
unique_missing_set.add(revision)
318
for missing_revision in unique_missing:
319
target_repo.fetch(submit_branch.repository,
321
info.install_revisions(target_repo, stream_input=False)
323
source_branch = _mod_branch.Branch.open(self.source_branch)
324
target_repo.fetch(source_branch.repository, self.revision_id)
325
return self.revision_id
327
def compose_merge_request(self, mail_client, to, body, branch, tree=None):
328
"""Compose a request to merge this directive.
330
:param mail_client: The mail client to use for composing this request.
331
:param to: The address to compose the request to.
332
:param branch: The Branch that was used to produce this directive.
333
:param tree: The Tree (if any) for the Branch used to produce this
336
basename = self.get_disk_name(branch)
338
if self.message is not None:
339
subject += self.message
341
revision = branch.repository.get_revision(self.revision_id)
342
subject += revision.get_summary()
343
if getattr(mail_client, 'supports_body', False):
345
for hook in self.hooks['merge_request_body']:
346
params = MergeRequestBodyParams(body, orig_body, self,
347
to, basename, subject, branch,
350
elif len(self.hooks['merge_request_body']) > 0:
351
trace.warning('Cannot run merge_request_body hooks because mail'
352
' client %s does not support message bodies.',
353
mail_client.__class__.__name__)
354
mail_client.compose_merge_request(to, subject,
355
b''.join(self.to_lines()),
359
class MergeDirective(BaseMergeDirective):
361
"""A request to perform a merge into a branch.
363
Designed to be serialized and mailed. It provides all the information
364
needed to perform a merge automatically, by providing at minimum a revision
365
bundle or the location of a branch.
367
The serialization format is robust against certain common forms of
368
deterioration caused by mailing.
370
The format is also designed to be patch-compatible. If the directive
371
includes a diff or revision bundle, it should be possible to apply it
372
directly using the standard patch program.
375
_format_string = b'Bazaar merge directive format 1'
377
def __init__(self, revision_id, testament_sha1, time, timezone,
378
target_branch, patch=None, patch_type=None,
379
source_branch=None, message=None, bundle=None):
382
:param revision_id: The revision to merge
383
:param testament_sha1: The sha1 of the testament of the revision to
385
:param time: The current POSIX timestamp time
386
:param timezone: The timezone offset
387
:param target_branch: Location of the branch to apply the merge to
388
:param patch: The text of a diff or bundle
389
:param patch_type: None, "diff" or "bundle", depending on the contents
391
:param source_branch: A public location to merge the revision from
392
:param message: The message to use when committing this merge
394
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
395
timezone, target_branch, patch, source_branch, message)
396
if patch_type not in (None, 'diff', 'bundle'):
397
raise ValueError(patch_type)
398
if patch_type != 'bundle' and source_branch is None:
399
raise errors.NoMergeSource()
400
if patch_type is not None and patch is None:
401
raise errors.PatchMissing(patch_type)
402
self.patch_type = patch_type
404
def clear_payload(self):
406
self.patch_type = None
408
def get_raw_bundle(self):
412
if self.patch_type == 'bundle':
417
bundle = property(_bundle)
420
def from_lines(klass, lines):
421
"""Deserialize a MergeRequest from an iterable of lines
423
:param lines: An iterable of lines
424
:return: a MergeRequest
426
line_iter = iter(lines)
428
for line in line_iter:
429
if line.startswith(b'# Bazaar merge directive format '):
430
return _format_registry.get(line[2:].rstrip())._from_lines(
432
firstline = firstline or line.strip()
433
raise errors.NotAMergeDirective(firstline)
436
def _from_lines(klass, line_iter):
437
stanza = rio.read_patch_stanza(line_iter)
438
patch_lines = list(line_iter)
439
if len(patch_lines) == 0:
443
patch = b''.join(patch_lines)
445
bundle_serializer.read_bundle(BytesIO(patch))
446
except (errors.NotABundle, errors.BundleNotSupported,
450
patch_type = 'bundle'
451
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
453
for key in ('revision_id', 'testament_sha1', 'target_branch',
454
'source_branch', 'message'):
456
kwargs[key] = stanza.get(key)
459
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
460
if 'testament_sha1' in kwargs:
461
kwargs['testament_sha1'] = kwargs['testament_sha1'].encode('ascii')
462
return MergeDirective(time=time, timezone=timezone,
463
patch_type=patch_type, patch=patch, **kwargs)
466
lines = self._to_lines()
467
if self.patch is not None:
468
lines.extend(self.patch.splitlines(True))
472
def _generate_bundle(repository, revision_id, ancestor_id):
474
bundle_serializer.write_bundle(repository, revision_id,
475
ancestor_id, s, '0.9')
478
def get_merge_request(self, repository):
479
"""Provide data for performing a merge
481
Returns suggested base, suggested target, and patch verification status
483
return None, self.revision_id, 'inapplicable'
486
class MergeDirective2(BaseMergeDirective):
488
_format_string = b'Bazaar merge directive format 2 (Bazaar 0.90)'
490
def __init__(self, revision_id, testament_sha1, time, timezone,
491
target_branch, patch=None, source_branch=None, message=None,
492
bundle=None, base_revision_id=None):
493
if source_branch is None and bundle is None:
494
raise errors.NoMergeSource()
495
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
496
timezone, target_branch, patch, source_branch, message)
498
self.base_revision_id = base_revision_id
500
def _patch_type(self):
501
if self.bundle is not None:
503
elif self.patch is not None:
508
patch_type = property(_patch_type)
510
def clear_payload(self):
514
def get_raw_bundle(self):
515
if self.bundle is None:
518
return base64.b64decode(self.bundle)
521
def _from_lines(klass, line_iter):
522
stanza = rio.read_patch_stanza(line_iter)
526
start = next(line_iter)
527
except StopIteration:
530
if start.startswith(b'# Begin patch'):
532
for line in line_iter:
533
if line.startswith(b'# Begin bundle'):
536
patch_lines.append(line)
539
patch = b''.join(patch_lines)
540
if start is not None:
541
if start.startswith(b'# Begin bundle'):
542
bundle = b''.join(line_iter)
544
raise errors.IllegalMergeDirectivePayload(start)
545
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
547
for key in ('revision_id', 'testament_sha1', 'target_branch',
548
'source_branch', 'message', 'base_revision_id'):
550
kwargs[key] = stanza.get(key)
553
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
554
kwargs['base_revision_id'] =\
555
kwargs['base_revision_id'].encode('utf-8')
556
if 'testament_sha1' in kwargs:
557
kwargs['testament_sha1'] = kwargs['testament_sha1'].encode('ascii')
558
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
562
lines = self._to_lines(base_revision=True)
563
if self.patch is not None:
564
lines.append(b'# Begin patch\n')
565
lines.extend(self.patch.splitlines(True))
566
if self.bundle is not None:
567
lines.append(b'# Begin bundle\n')
568
lines.extend(self.bundle.splitlines(True))
572
def from_objects(klass, repository, revision_id, time, timezone,
573
target_branch, include_patch=True, include_bundle=True,
574
local_target_branch=None, public_branch=None, message=None,
575
base_revision_id=None):
576
"""Generate a merge directive from various objects
578
:param repository: The repository containing the revision
579
:param revision_id: The revision to merge
580
:param time: The POSIX timestamp of the date the request was issued.
581
:param timezone: The timezone of the request
582
:param target_branch: The url of the branch to merge into
583
:param include_patch: If true, include a preview patch
584
:param include_bundle: If true, include a bundle
585
:param local_target_branch: the target branch, either itself or a local copy
586
:param public_branch: location of a public branch containing
588
:param message: Message to use when committing the merge
589
:return: The merge directive
591
The public branch is always used if supplied. If no bundle is
592
included, the public branch must be supplied, and will be verified.
594
If the message is not supplied, the message from revision_id will be
599
repository.lock_write()
600
locked.append(repository)
601
t_revision_id = revision_id
602
if revision_id == b'null:':
604
t = testament.StrictTestament3.from_revision(repository,
606
if local_target_branch is None:
607
submit_branch = _mod_branch.Branch.open(target_branch)
609
submit_branch = local_target_branch
610
submit_branch.lock_read()
611
locked.append(submit_branch)
612
if submit_branch.get_public_branch() is not None:
613
target_branch = submit_branch.get_public_branch()
614
submit_revision_id = submit_branch.last_revision()
615
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
616
graph = repository.get_graph(submit_branch.repository)
617
ancestor_id = graph.find_unique_lca(revision_id,
619
if base_revision_id is None:
620
base_revision_id = ancestor_id
621
if (include_patch, include_bundle) != (False, False):
622
repository.fetch(submit_branch.repository, submit_revision_id)
624
patch = klass._generate_diff(repository, revision_id,
630
bundle = base64.b64encode(klass._generate_bundle(repository, revision_id,
635
if public_branch is not None and not include_bundle:
636
public_branch_obj = _mod_branch.Branch.open(public_branch)
637
public_branch_obj.lock_read()
638
locked.append(public_branch_obj)
639
if not public_branch_obj.repository.has_revision(
641
raise errors.PublicBranchOutOfDate(public_branch,
643
testament_sha1 = t.as_sha1()
645
for entry in reversed(locked):
647
return klass(revision_id, testament_sha1, time, timezone,
648
target_branch, patch, public_branch, message, bundle,
651
def _verify_patch(self, repository):
652
calculated_patch = self._generate_diff(repository, self.revision_id,
653
self.base_revision_id)
654
# Convert line-endings to UNIX
655
stored_patch = re.sub(b'\r\n?', b'\n', self.patch)
656
calculated_patch = re.sub(b'\r\n?', b'\n', calculated_patch)
657
# Strip trailing whitespace
658
calculated_patch = re.sub(b' *\n', b'\n', calculated_patch)
659
stored_patch = re.sub(b' *\n', b'\n', stored_patch)
660
return (calculated_patch == stored_patch)
662
def get_merge_request(self, repository):
663
"""Provide data for performing a merge
665
Returns suggested base, suggested target, and patch verification status
667
verified = self._maybe_verify(repository)
668
return self.base_revision_id, self.revision_id, verified
670
def _maybe_verify(self, repository):
671
if self.patch is not None:
672
if self._verify_patch(repository):
677
return 'inapplicable'
680
class MergeDirectiveFormatRegistry(registry.Registry):
682
def register(self, directive, format_string=None):
683
if format_string is None:
684
format_string = directive._format_string
685
registry.Registry.register(self, format_string, directive)
688
_format_registry = MergeDirectiveFormatRegistry()
689
_format_registry.register(MergeDirective)
690
_format_registry.register(MergeDirective2)
691
# 0.19 never existed. It got renamed to 0.90. But by that point, there were
692
# already merge directives in the wild that used 0.19. Registering with the old
693
# format string to retain compatibility with those merge directives.
694
_format_registry.register(MergeDirective2,
695
b'Bazaar merge directive format 2 (Bazaar 0.19)')