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
21
from . import lazy_import
22
lazy_import.lazy_import(globals(), """
24
branch as _mod_branch,
31
revision as _mod_revision,
37
from breezy.bundle import (
38
serializer as bundle_serializer,
46
class MergeRequestBodyParams(object):
47
"""Parameter object for the merge_request_body hook."""
49
def __init__(self, body, orig_body, directive, to, basename, subject,
52
self.orig_body = orig_body
53
self.directive = directive
57
self.basename = basename
58
self.subject = subject
61
class MergeDirectiveHooks(hooks.Hooks):
62
"""Hooks for MergeDirective classes."""
65
hooks.Hooks.__init__(self, "breezy.merge_directive", "BaseMergeDirective.hooks")
66
self.add_hook('merge_request_body',
67
"Called with a MergeRequestBodyParams when a body is needed for"
68
" a merge request. Callbacks must return a body. If more"
69
" than one callback is registered, the output of one callback is"
70
" provided to the next.", (1, 15, 0))
73
class BaseMergeDirective(object):
74
"""A request to perform a merge into a branch.
76
This is the base class that all merge directive implementations
79
:cvar multiple_output_files: Whether or not this merge directive
80
stores a set of revisions in more than one file
83
hooks = MergeDirectiveHooks()
85
multiple_output_files = False
87
def __init__(self, revision_id, testament_sha1, time, timezone,
88
target_branch, patch=None, source_branch=None,
89
message=None, bundle=None):
92
:param revision_id: The revision to merge
93
:param testament_sha1: The sha1 of the testament of the revision to
95
:param time: The current POSIX timestamp time
96
:param timezone: The timezone offset
97
:param target_branch: Location of branch to apply the merge to
98
:param patch: The text of a diff or bundle
99
:param source_branch: A public location to merge the revision from
100
:param message: The message to use when committing this merge
102
self.revision_id = revision_id
103
self.testament_sha1 = testament_sha1
105
self.timezone = timezone
106
self.target_branch = target_branch
108
self.source_branch = source_branch
109
self.message = message
112
"""Serialize as a list of lines
114
:return: a list of lines
116
raise NotImplementedError(self.to_lines)
119
"""Serialize as a set of files.
121
:return: List of tuples with filename and contents as lines
123
raise NotImplementedError(self.to_files)
125
def get_raw_bundle(self):
126
"""Return the bundle for this merge directive.
128
:return: bundle text or None if there is no bundle
132
def _to_lines(self, base_revision=False):
133
"""Serialize as a list of lines
135
:return: a list of lines
137
time_str = timestamp.format_patch_date(self.time, self.timezone)
138
stanza = rio.Stanza(revision_id=self.revision_id, timestamp=time_str,
139
target_branch=self.target_branch,
140
testament_sha1=self.testament_sha1)
141
for key in ('source_branch', 'message'):
142
if self.__dict__[key] is not None:
143
stanza.add(key, self.__dict__[key])
145
stanza.add('base_revision_id', self.base_revision_id)
146
lines = ['# ' + self._format_string + '\n']
147
lines.extend(rio.to_patch_lines(stanza))
151
def write_to_directory(self, path):
152
"""Write this merge directive to a series of files in a directory.
154
:param path: Filesystem path to write to
156
raise NotImplementedError(self.write_to_directory)
159
def from_objects(klass, repository, revision_id, time, timezone,
160
target_branch, patch_type='bundle',
161
local_target_branch=None, public_branch=None, message=None):
162
"""Generate a merge directive from various objects
164
:param repository: The repository containing the revision
165
:param revision_id: The revision to merge
166
:param time: The POSIX timestamp of the date the request was issued.
167
:param timezone: The timezone of the request
168
:param target_branch: The url of the branch to merge into
169
:param patch_type: 'bundle', 'diff' or None, depending on the type of
171
:param local_target_branch: the submit branch, either itself or a local copy
172
:param public_branch: location of a public branch containing
174
:param message: Message to use when committing the merge
175
:return: The merge directive
177
The public branch is always used if supplied. If the patch_type is
178
not 'bundle', the public branch must be supplied, and will be verified.
180
If the message is not supplied, the message from revision_id will be
183
t_revision_id = revision_id
184
if revision_id == _mod_revision.NULL_REVISION:
186
t = testament.StrictTestament3.from_revision(repository, t_revision_id)
187
if local_target_branch is None:
188
submit_branch = _mod_branch.Branch.open(target_branch)
190
submit_branch = local_target_branch
191
if submit_branch.get_public_branch() is not None:
192
target_branch = submit_branch.get_public_branch()
193
if patch_type is None:
196
submit_revision_id = submit_branch.last_revision()
197
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
198
repository.fetch(submit_branch.repository, submit_revision_id)
199
graph = repository.get_graph()
200
ancestor_id = graph.find_unique_lca(revision_id,
202
type_handler = {'bundle': klass._generate_bundle,
203
'diff': klass._generate_diff,
204
None: lambda x, y, z: None }
205
patch = type_handler[patch_type](repository, revision_id,
208
if public_branch is not None and patch_type != 'bundle':
209
public_branch_obj = _mod_branch.Branch.open(public_branch)
210
if not public_branch_obj.repository.has_revision(revision_id):
211
raise errors.PublicBranchOutOfDate(public_branch,
214
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
215
patch, patch_type, public_branch, message)
217
def get_disk_name(self, branch):
218
"""Generate a suitable basename for storing this directive on disk
220
:param branch: The Branch this merge directive was generated fro
223
revno, revision_id = branch.last_revision_info()
224
if self.revision_id == revision_id:
227
revno = branch.get_revision_id_to_revno_map().get(self.revision_id,
229
nick = re.sub('(\\W+)', '-', branch.nick).strip('-')
230
return '%s-%s' % (nick, '.'.join(str(n) for n in revno))
233
def _generate_diff(repository, revision_id, ancestor_id):
234
tree_1 = repository.revision_tree(ancestor_id)
235
tree_2 = repository.revision_tree(revision_id)
237
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
241
def _generate_bundle(repository, revision_id, ancestor_id):
243
bundle_serializer.write_bundle(repository, revision_id,
247
def to_signed(self, branch):
248
"""Serialize as a signed string.
250
:param branch: The source branch, to get the signing strategy
253
my_gpg = gpg.GPGStrategy(branch.get_config_stack())
254
return my_gpg.sign(''.join(self.to_lines()), gpg.MODE_CLEAR)
256
def to_email(self, mail_to, branch, sign=False):
257
"""Serialize as an email message.
259
:param mail_to: The address to mail the message to
260
:param branch: The source branch, to get the signing strategy and
262
:param sign: If True, gpg-sign the email
263
:return: an email message
265
mail_from = branch.get_config_stack().get('email')
266
if self.message is not None:
267
subject = self.message
269
revision = branch.repository.get_revision(self.revision_id)
270
subject = revision.message
272
body = self.to_signed(branch)
274
body = ''.join(self.to_lines())
275
message = email_message.EmailMessage(mail_from, mail_to, subject,
279
def install_revisions(self, target_repo):
280
"""Install revisions and return the target revision"""
281
if not target_repo.has_revision(self.revision_id):
282
if self.patch_type == 'bundle':
283
info = bundle_serializer.read_bundle(
284
BytesIO(self.get_raw_bundle()))
285
# We don't use the bundle's target revision, because
286
# MergeDirective.revision_id is authoritative.
288
info.install_revisions(target_repo, stream_input=False)
289
except errors.RevisionNotPresent:
290
# At least one dependency isn't present. Try installing
291
# missing revisions from the submit branch
294
_mod_branch.Branch.open(self.target_branch)
295
except errors.NotBranchError:
296
raise errors.TargetNotBranch(self.target_branch)
297
missing_revisions = []
298
bundle_revisions = set(r.revision_id for r in
300
for revision in info.real_revisions:
301
for parent_id in revision.parent_ids:
302
if (parent_id not in bundle_revisions and
303
not target_repo.has_revision(parent_id)):
304
missing_revisions.append(parent_id)
305
# reverse missing revisions to try to get heads first
307
unique_missing_set = set()
308
for revision in reversed(missing_revisions):
309
if revision in unique_missing_set:
311
unique_missing.append(revision)
312
unique_missing_set.add(revision)
313
for missing_revision in unique_missing:
314
target_repo.fetch(submit_branch.repository,
316
info.install_revisions(target_repo, stream_input=False)
318
source_branch = _mod_branch.Branch.open(self.source_branch)
319
target_repo.fetch(source_branch.repository, self.revision_id)
320
return self.revision_id
322
def compose_merge_request(self, mail_client, to, body, branch, tree=None):
323
"""Compose a request to merge this directive.
325
:param mail_client: The mail client to use for composing this request.
326
:param to: The address to compose the request to.
327
:param branch: The Branch that was used to produce this directive.
328
:param tree: The Tree (if any) for the Branch used to produce this
331
basename = self.get_disk_name(branch)
333
if self.message is not None:
334
subject += self.message
336
revision = branch.repository.get_revision(self.revision_id)
337
subject += revision.get_summary()
338
if getattr(mail_client, 'supports_body', False):
340
for hook in self.hooks['merge_request_body']:
341
params = MergeRequestBodyParams(body, orig_body, self,
342
to, basename, subject, branch,
345
elif len(self.hooks['merge_request_body']) > 0:
346
trace.warning('Cannot run merge_request_body hooks because mail'
347
' client %s does not support message bodies.',
348
mail_client.__class__.__name__)
349
mail_client.compose_merge_request(to, subject,
350
''.join(self.to_lines()),
354
class MergeDirective(BaseMergeDirective):
356
"""A request to perform a merge into a branch.
358
Designed to be serialized and mailed. It provides all the information
359
needed to perform a merge automatically, by providing at minimum a revision
360
bundle or the location of a branch.
362
The serialization format is robust against certain common forms of
363
deterioration caused by mailing.
365
The format is also designed to be patch-compatible. If the directive
366
includes a diff or revision bundle, it should be possible to apply it
367
directly using the standard patch program.
370
_format_string = 'Bazaar merge directive format 1'
372
def __init__(self, revision_id, testament_sha1, time, timezone,
373
target_branch, patch=None, patch_type=None,
374
source_branch=None, message=None, bundle=None):
377
:param revision_id: The revision to merge
378
:param testament_sha1: The sha1 of the testament of the revision to
380
:param time: The current POSIX timestamp time
381
:param timezone: The timezone offset
382
:param target_branch: Location of the branch to apply the merge to
383
:param patch: The text of a diff or bundle
384
:param patch_type: None, "diff" or "bundle", depending on the contents
386
:param source_branch: A public location to merge the revision from
387
:param message: The message to use when committing this merge
389
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
390
timezone, target_branch, patch, source_branch, message)
391
if patch_type not in (None, 'diff', 'bundle'):
392
raise ValueError(patch_type)
393
if patch_type != 'bundle' and source_branch is None:
394
raise errors.NoMergeSource()
395
if patch_type is not None and patch is None:
396
raise errors.PatchMissing(patch_type)
397
self.patch_type = patch_type
399
def clear_payload(self):
401
self.patch_type = None
403
def get_raw_bundle(self):
407
if self.patch_type == 'bundle':
412
bundle = property(_bundle)
415
def from_lines(klass, lines):
416
"""Deserialize a MergeRequest from an iterable of lines
418
:param lines: An iterable of lines
419
:return: a MergeRequest
421
line_iter = iter(lines)
423
for line in line_iter:
424
if line.startswith('# Bazaar merge directive format '):
425
return _format_registry.get(line[2:].rstrip())._from_lines(
427
firstline = firstline or line.strip()
428
raise errors.NotAMergeDirective(firstline)
431
def _from_lines(klass, line_iter):
432
stanza = rio.read_patch_stanza(line_iter)
433
patch_lines = list(line_iter)
434
if len(patch_lines) == 0:
438
patch = ''.join(patch_lines)
440
bundle_serializer.read_bundle(BytesIO(patch))
441
except (errors.NotABundle, errors.BundleNotSupported,
445
patch_type = 'bundle'
446
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
448
for key in ('revision_id', 'testament_sha1', 'target_branch',
449
'source_branch', 'message'):
451
kwargs[key] = stanza.get(key)
454
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
455
return MergeDirective(time=time, timezone=timezone,
456
patch_type=patch_type, patch=patch, **kwargs)
459
lines = self._to_lines()
460
if self.patch is not None:
461
lines.extend(self.patch.splitlines(True))
465
def _generate_bundle(repository, revision_id, ancestor_id):
467
bundle_serializer.write_bundle(repository, revision_id,
468
ancestor_id, s, '0.9')
471
def get_merge_request(self, repository):
472
"""Provide data for performing a merge
474
Returns suggested base, suggested target, and patch verification status
476
return None, self.revision_id, 'inapplicable'
479
class MergeDirective2(BaseMergeDirective):
481
_format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
483
def __init__(self, revision_id, testament_sha1, time, timezone,
484
target_branch, patch=None, source_branch=None, message=None,
485
bundle=None, base_revision_id=None):
486
if source_branch is None and bundle is None:
487
raise errors.NoMergeSource()
488
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
489
timezone, target_branch, patch, source_branch, message)
491
self.base_revision_id = base_revision_id
493
def _patch_type(self):
494
if self.bundle is not None:
496
elif self.patch is not None:
501
patch_type = property(_patch_type)
503
def clear_payload(self):
507
def get_raw_bundle(self):
508
if self.bundle is None:
511
return self.bundle.decode('base-64')
514
def _from_lines(klass, line_iter):
515
stanza = rio.read_patch_stanza(line_iter)
519
start = next(line_iter)
520
except StopIteration:
523
if start.startswith('# Begin patch'):
525
for line in line_iter:
526
if line.startswith('# Begin bundle'):
529
patch_lines.append(line)
532
patch = ''.join(patch_lines)
533
if start is not None:
534
if start.startswith('# Begin bundle'):
535
bundle = ''.join(line_iter)
537
raise errors.IllegalMergeDirectivePayload(start)
538
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
540
for key in ('revision_id', 'testament_sha1', 'target_branch',
541
'source_branch', 'message', 'base_revision_id'):
543
kwargs[key] = stanza.get(key)
546
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
547
kwargs['base_revision_id'] =\
548
kwargs['base_revision_id'].encode('utf-8')
549
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
553
lines = self._to_lines(base_revision=True)
554
if self.patch is not None:
555
lines.append('# Begin patch\n')
556
lines.extend(self.patch.splitlines(True))
557
if self.bundle is not None:
558
lines.append('# Begin bundle\n')
559
lines.extend(self.bundle.splitlines(True))
563
def from_objects(klass, repository, revision_id, time, timezone,
564
target_branch, include_patch=True, include_bundle=True,
565
local_target_branch=None, public_branch=None, message=None,
566
base_revision_id=None):
567
"""Generate a merge directive from various objects
569
:param repository: The repository containing the revision
570
:param revision_id: The revision to merge
571
:param time: The POSIX timestamp of the date the request was issued.
572
:param timezone: The timezone of the request
573
:param target_branch: The url of the branch to merge into
574
:param include_patch: If true, include a preview patch
575
:param include_bundle: If true, include a bundle
576
:param local_target_branch: the target branch, either itself or a local copy
577
:param public_branch: location of a public branch containing
579
:param message: Message to use when committing the merge
580
:return: The merge directive
582
The public branch is always used if supplied. If no bundle is
583
included, the public branch must be supplied, and will be verified.
585
If the message is not supplied, the message from revision_id will be
590
repository.lock_write()
591
locked.append(repository)
592
t_revision_id = revision_id
593
if revision_id == 'null:':
595
t = testament.StrictTestament3.from_revision(repository,
597
if local_target_branch is None:
598
submit_branch = _mod_branch.Branch.open(target_branch)
600
submit_branch = local_target_branch
601
submit_branch.lock_read()
602
locked.append(submit_branch)
603
if submit_branch.get_public_branch() is not None:
604
target_branch = submit_branch.get_public_branch()
605
submit_revision_id = submit_branch.last_revision()
606
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
607
graph = repository.get_graph(submit_branch.repository)
608
ancestor_id = graph.find_unique_lca(revision_id,
610
if base_revision_id is None:
611
base_revision_id = ancestor_id
612
if (include_patch, include_bundle) != (False, False):
613
repository.fetch(submit_branch.repository, submit_revision_id)
615
patch = klass._generate_diff(repository, revision_id,
621
bundle = klass._generate_bundle(repository, revision_id,
622
ancestor_id).encode('base-64')
626
if public_branch is not None and not include_bundle:
627
public_branch_obj = _mod_branch.Branch.open(public_branch)
628
public_branch_obj.lock_read()
629
locked.append(public_branch_obj)
630
if not public_branch_obj.repository.has_revision(
632
raise errors.PublicBranchOutOfDate(public_branch,
634
testament_sha1 = t.as_sha1()
636
for entry in reversed(locked):
638
return klass(revision_id, testament_sha1, time, timezone,
639
target_branch, patch, public_branch, message, bundle,
642
def _verify_patch(self, repository):
643
calculated_patch = self._generate_diff(repository, self.revision_id,
644
self.base_revision_id)
645
# Convert line-endings to UNIX
646
stored_patch = re.sub('\r\n?', '\n', self.patch)
647
calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
648
# Strip trailing whitespace
649
calculated_patch = re.sub(' *\n', '\n', calculated_patch)
650
stored_patch = re.sub(' *\n', '\n', stored_patch)
651
return (calculated_patch == stored_patch)
653
def get_merge_request(self, repository):
654
"""Provide data for performing a merge
656
Returns suggested base, suggested target, and patch verification status
658
verified = self._maybe_verify(repository)
659
return self.base_revision_id, self.revision_id, verified
661
def _maybe_verify(self, repository):
662
if self.patch is not None:
663
if self._verify_patch(repository):
668
return 'inapplicable'
671
class MergeDirectiveFormatRegistry(registry.Registry):
673
def register(self, directive, format_string=None):
674
if format_string is None:
675
format_string = directive._format_string
676
registry.Registry.register(self, format_string, directive)
679
_format_registry = MergeDirectiveFormatRegistry()
680
_format_registry.register(MergeDirective)
681
_format_registry.register(MergeDirective2)
682
# 0.19 never existed. It got renamed to 0.90. But by that point, there were
683
# already merge directives in the wild that used 0.19. Registering with the old
684
# format string to retain compatibility with those merge directives.
685
_format_registry.register(MergeDirective2,
686
'Bazaar merge directive format 2 (Bazaar 0.19)')