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
19
from StringIO import StringIO
22
from bzrlib import lazy_import
23
lazy_import.lazy_import(globals(), """
25
branch as _mod_branch,
32
revision as _mod_revision,
38
from bzrlib.bundle import (
39
serializer as bundle_serializer,
44
class MergeRequestBodyParams(object):
45
"""Parameter object for the merge_request_body hook."""
47
def __init__(self, body, orig_body, directive, to, basename, subject,
50
self.orig_body = orig_body
51
self.directive = directive
55
self.basename = basename
56
self.subject = subject
59
class MergeDirectiveHooks(hooks.Hooks):
60
"""Hooks for MergeDirective classes."""
63
hooks.Hooks.__init__(self, "bzrlib.merge_directive", "BaseMergeDirective.hooks")
64
self.add_hook('merge_request_body',
65
"Called with a MergeRequestBodyParams when a body is needed for"
66
" a merge request. Callbacks must return a body. If more"
67
" than one callback is registered, the output of one callback is"
68
" provided to the next.", (1, 15, 0))
71
class BaseMergeDirective(object):
72
"""A request to perform a merge into a branch.
74
This is the base class that all merge directive implementations
77
:cvar multiple_output_files: Whether or not this merge directive
78
stores a set of revisions in more than one file
81
hooks = MergeDirectiveHooks()
83
multiple_output_files = False
85
def __init__(self, revision_id, testament_sha1, time, timezone,
86
target_branch, patch=None, source_branch=None,
87
message=None, bundle=None):
90
:param revision_id: The revision to merge
91
:param testament_sha1: The sha1 of the testament of the revision to
93
:param time: The current POSIX timestamp time
94
:param timezone: The timezone offset
95
:param target_branch: Location of branch to apply the merge to
96
:param patch: The text of a diff or bundle
97
:param source_branch: A public location to merge the revision from
98
:param message: The message to use when committing this merge
100
self.revision_id = revision_id
101
self.testament_sha1 = testament_sha1
103
self.timezone = timezone
104
self.target_branch = target_branch
106
self.source_branch = source_branch
107
self.message = message
110
"""Serialize as a list of lines
112
:return: a list of lines
114
raise NotImplementedError(self.to_lines)
117
"""Serialize as a set of files.
119
:return: List of tuples with filename and contents as lines
121
raise NotImplementedError(self.to_files)
123
def get_raw_bundle(self):
124
"""Return the bundle for this merge directive.
126
:return: bundle text or None if there is no bundle
130
def _to_lines(self, base_revision=False):
131
"""Serialize as a list of lines
133
:return: a list of lines
135
time_str = timestamp.format_patch_date(self.time, self.timezone)
136
stanza = rio.Stanza(revision_id=self.revision_id, timestamp=time_str,
137
target_branch=self.target_branch,
138
testament_sha1=self.testament_sha1)
139
for key in ('source_branch', 'message'):
140
if self.__dict__[key] is not None:
141
stanza.add(key, self.__dict__[key])
143
stanza.add('base_revision_id', self.base_revision_id)
144
lines = ['# ' + self._format_string + '\n']
145
lines.extend(rio.to_patch_lines(stanza))
149
def write_to_directory(self, path):
150
"""Write this merge directive to a series of files in a directory.
152
:param path: Filesystem path to write to
154
raise NotImplementedError(self.write_to_directory)
157
def from_objects(klass, repository, revision_id, time, timezone,
158
target_branch, patch_type='bundle',
159
local_target_branch=None, public_branch=None, message=None):
160
"""Generate a merge directive from various objects
162
:param repository: The repository containing the revision
163
:param revision_id: The revision to merge
164
:param time: The POSIX timestamp of the date the request was issued.
165
:param timezone: The timezone of the request
166
:param target_branch: The url of the branch to merge into
167
:param patch_type: 'bundle', 'diff' or None, depending on the type of
169
:param local_target_branch: the submit branch, either itself or a local copy
170
:param public_branch: location of a public branch containing
172
:param message: Message to use when committing the merge
173
:return: The merge directive
175
The public branch is always used if supplied. If the patch_type is
176
not 'bundle', the public branch must be supplied, and will be verified.
178
If the message is not supplied, the message from revision_id will be
181
t_revision_id = revision_id
182
if revision_id == _mod_revision.NULL_REVISION:
184
t = testament.StrictTestament3.from_revision(repository, t_revision_id)
185
if local_target_branch is None:
186
submit_branch = _mod_branch.Branch.open(target_branch)
188
submit_branch = local_target_branch
189
if submit_branch.get_public_branch() is not None:
190
target_branch = submit_branch.get_public_branch()
191
if patch_type is None:
194
submit_revision_id = submit_branch.last_revision()
195
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
196
repository.fetch(submit_branch.repository, submit_revision_id)
197
graph = repository.get_graph()
198
ancestor_id = graph.find_unique_lca(revision_id,
200
type_handler = {'bundle': klass._generate_bundle,
201
'diff': klass._generate_diff,
202
None: lambda x, y, z: None }
203
patch = type_handler[patch_type](repository, revision_id,
206
if public_branch is not None and patch_type != 'bundle':
207
public_branch_obj = _mod_branch.Branch.open(public_branch)
208
if not public_branch_obj.repository.has_revision(revision_id):
209
raise errors.PublicBranchOutOfDate(public_branch,
212
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
213
patch, patch_type, public_branch, message)
215
def get_disk_name(self, branch):
216
"""Generate a suitable basename for storing this directive on disk
218
:param branch: The Branch this merge directive was generated fro
221
revno, revision_id = branch.last_revision_info()
222
if self.revision_id == revision_id:
225
revno = branch.get_revision_id_to_revno_map().get(self.revision_id,
227
nick = re.sub('(\W+)', '-', branch.nick).strip('-')
228
return '%s-%s' % (nick, '.'.join(str(n) for n in revno))
231
def _generate_diff(repository, revision_id, ancestor_id):
232
tree_1 = repository.revision_tree(ancestor_id)
233
tree_2 = repository.revision_tree(revision_id)
235
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
239
def _generate_bundle(repository, revision_id, ancestor_id):
241
bundle_serializer.write_bundle(repository, revision_id,
245
def to_signed(self, branch):
246
"""Serialize as a signed string.
248
:param branch: The source branch, to get the signing strategy
251
my_gpg = gpg.GPGStrategy(branch.get_config_stack())
252
return my_gpg.sign(''.join(self.to_lines()))
254
def to_email(self, mail_to, branch, sign=False):
255
"""Serialize as an email message.
257
:param mail_to: The address to mail the message to
258
:param branch: The source branch, to get the signing strategy and
260
:param sign: If True, gpg-sign the email
261
:return: an email message
263
mail_from = branch.get_config_stack().get('email')
264
if self.message is not None:
265
subject = self.message
267
revision = branch.repository.get_revision(self.revision_id)
268
subject = revision.message
270
body = self.to_signed(branch)
272
body = ''.join(self.to_lines())
273
message = email_message.EmailMessage(mail_from, mail_to, subject,
277
def install_revisions(self, target_repo):
278
"""Install revisions and return the target revision"""
279
if not target_repo.has_revision(self.revision_id):
280
if self.patch_type == 'bundle':
281
info = bundle_serializer.read_bundle(
282
StringIO(self.get_raw_bundle()))
283
# We don't use the bundle's target revision, because
284
# MergeDirective.revision_id is authoritative.
286
info.install_revisions(target_repo, stream_input=False)
287
except errors.RevisionNotPresent:
288
# At least one dependency isn't present. Try installing
289
# missing revisions from the submit branch
292
_mod_branch.Branch.open(self.target_branch)
293
except errors.NotBranchError:
294
raise errors.TargetNotBranch(self.target_branch)
295
missing_revisions = []
296
bundle_revisions = set(r.revision_id for r in
298
for revision in info.real_revisions:
299
for parent_id in revision.parent_ids:
300
if (parent_id not in bundle_revisions and
301
not target_repo.has_revision(parent_id)):
302
missing_revisions.append(parent_id)
303
# reverse missing revisions to try to get heads first
305
unique_missing_set = set()
306
for revision in reversed(missing_revisions):
307
if revision in unique_missing_set:
309
unique_missing.append(revision)
310
unique_missing_set.add(revision)
311
for missing_revision in unique_missing:
312
target_repo.fetch(submit_branch.repository,
314
info.install_revisions(target_repo, stream_input=False)
316
source_branch = _mod_branch.Branch.open(self.source_branch)
317
target_repo.fetch(source_branch.repository, self.revision_id)
318
return self.revision_id
320
def compose_merge_request(self, mail_client, to, body, branch, tree=None):
321
"""Compose a request to merge this directive.
323
:param mail_client: The mail client to use for composing this request.
324
:param to: The address to compose the request to.
325
:param branch: The Branch that was used to produce this directive.
326
:param tree: The Tree (if any) for the Branch used to produce this
329
basename = self.get_disk_name(branch)
331
if self.message is not None:
332
subject += self.message
334
revision = branch.repository.get_revision(self.revision_id)
335
subject += revision.get_summary()
336
if getattr(mail_client, 'supports_body', False):
338
for hook in self.hooks['merge_request_body']:
339
params = MergeRequestBodyParams(body, orig_body, self,
340
to, basename, subject, branch,
343
elif len(self.hooks['merge_request_body']) > 0:
344
trace.warning('Cannot run merge_request_body hooks because mail'
345
' client %s does not support message bodies.',
346
mail_client.__class__.__name__)
347
mail_client.compose_merge_request(to, subject,
348
''.join(self.to_lines()),
352
class MergeDirective(BaseMergeDirective):
354
"""A request to perform a merge into a branch.
356
Designed to be serialized and mailed. It provides all the information
357
needed to perform a merge automatically, by providing at minimum a revision
358
bundle or the location of a branch.
360
The serialization format is robust against certain common forms of
361
deterioration caused by mailing.
363
The format is also designed to be patch-compatible. If the directive
364
includes a diff or revision bundle, it should be possible to apply it
365
directly using the standard patch program.
368
_format_string = 'Bazaar merge directive format 1'
370
def __init__(self, revision_id, testament_sha1, time, timezone,
371
target_branch, patch=None, patch_type=None,
372
source_branch=None, message=None, bundle=None):
375
:param revision_id: The revision to merge
376
:param testament_sha1: The sha1 of the testament of the revision to
378
:param time: The current POSIX timestamp time
379
:param timezone: The timezone offset
380
:param target_branch: Location of the branch to apply the merge to
381
:param patch: The text of a diff or bundle
382
:param patch_type: None, "diff" or "bundle", depending on the contents
384
:param source_branch: A public location to merge the revision from
385
:param message: The message to use when committing this merge
387
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
388
timezone, target_branch, patch, source_branch, message)
389
if patch_type not in (None, 'diff', 'bundle'):
390
raise ValueError(patch_type)
391
if patch_type != 'bundle' and source_branch is None:
392
raise errors.NoMergeSource()
393
if patch_type is not None and patch is None:
394
raise errors.PatchMissing(patch_type)
395
self.patch_type = patch_type
397
def clear_payload(self):
399
self.patch_type = None
401
def get_raw_bundle(self):
405
if self.patch_type == 'bundle':
410
bundle = property(_bundle)
413
def from_lines(klass, lines):
414
"""Deserialize a MergeRequest from an iterable of lines
416
:param lines: An iterable of lines
417
:return: a MergeRequest
419
line_iter = iter(lines)
421
for line in line_iter:
422
if line.startswith('# Bazaar merge directive format '):
423
return _format_registry.get(line[2:].rstrip())._from_lines(
425
firstline = firstline or line.strip()
426
raise errors.NotAMergeDirective(firstline)
429
def _from_lines(klass, line_iter):
430
stanza = rio.read_patch_stanza(line_iter)
431
patch_lines = list(line_iter)
432
if len(patch_lines) == 0:
436
patch = ''.join(patch_lines)
438
bundle_serializer.read_bundle(StringIO(patch))
439
except (errors.NotABundle, errors.BundleNotSupported,
443
patch_type = 'bundle'
444
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
446
for key in ('revision_id', 'testament_sha1', 'target_branch',
447
'source_branch', 'message'):
449
kwargs[key] = stanza.get(key)
452
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
453
return MergeDirective(time=time, timezone=timezone,
454
patch_type=patch_type, patch=patch, **kwargs)
457
lines = self._to_lines()
458
if self.patch is not None:
459
lines.extend(self.patch.splitlines(True))
463
def _generate_bundle(repository, revision_id, ancestor_id):
465
bundle_serializer.write_bundle(repository, revision_id,
466
ancestor_id, s, '0.9')
469
def get_merge_request(self, repository):
470
"""Provide data for performing a merge
472
Returns suggested base, suggested target, and patch verification status
474
return None, self.revision_id, 'inapplicable'
477
class MergeDirective2(BaseMergeDirective):
479
_format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
481
def __init__(self, revision_id, testament_sha1, time, timezone,
482
target_branch, patch=None, source_branch=None, message=None,
483
bundle=None, base_revision_id=None):
484
if source_branch is None and bundle is None:
485
raise errors.NoMergeSource()
486
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
487
timezone, target_branch, patch, source_branch, message)
489
self.base_revision_id = base_revision_id
491
def _patch_type(self):
492
if self.bundle is not None:
494
elif self.patch is not None:
499
patch_type = property(_patch_type)
501
def clear_payload(self):
505
def get_raw_bundle(self):
506
if self.bundle is None:
509
return self.bundle.decode('base-64')
512
def _from_lines(klass, line_iter):
513
stanza = rio.read_patch_stanza(line_iter)
517
start = line_iter.next()
518
except StopIteration:
521
if start.startswith('# Begin patch'):
523
for line in line_iter:
524
if line.startswith('# Begin bundle'):
527
patch_lines.append(line)
530
patch = ''.join(patch_lines)
531
if start is not None:
532
if start.startswith('# Begin bundle'):
533
bundle = ''.join(line_iter)
535
raise errors.IllegalMergeDirectivePayload(start)
536
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
538
for key in ('revision_id', 'testament_sha1', 'target_branch',
539
'source_branch', 'message', 'base_revision_id'):
541
kwargs[key] = stanza.get(key)
544
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
545
kwargs['base_revision_id'] =\
546
kwargs['base_revision_id'].encode('utf-8')
547
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
551
lines = self._to_lines(base_revision=True)
552
if self.patch is not None:
553
lines.append('# Begin patch\n')
554
lines.extend(self.patch.splitlines(True))
555
if self.bundle is not None:
556
lines.append('# Begin bundle\n')
557
lines.extend(self.bundle.splitlines(True))
561
def from_objects(klass, repository, revision_id, time, timezone,
562
target_branch, include_patch=True, include_bundle=True,
563
local_target_branch=None, public_branch=None, message=None,
564
base_revision_id=None):
565
"""Generate a merge directive from various objects
567
:param repository: The repository containing the revision
568
:param revision_id: The revision to merge
569
:param time: The POSIX timestamp of the date the request was issued.
570
:param timezone: The timezone of the request
571
:param target_branch: The url of the branch to merge into
572
:param include_patch: If true, include a preview patch
573
:param include_bundle: If true, include a bundle
574
:param local_target_branch: the target branch, either itself or a local copy
575
:param public_branch: location of a public branch containing
577
:param message: Message to use when committing the merge
578
:return: The merge directive
580
The public branch is always used if supplied. If no bundle is
581
included, the public branch must be supplied, and will be verified.
583
If the message is not supplied, the message from revision_id will be
588
repository.lock_write()
589
locked.append(repository)
590
t_revision_id = revision_id
591
if revision_id == 'null:':
593
t = testament.StrictTestament3.from_revision(repository,
595
if local_target_branch is None:
596
submit_branch = _mod_branch.Branch.open(target_branch)
598
submit_branch = local_target_branch
599
submit_branch.lock_read()
600
locked.append(submit_branch)
601
if submit_branch.get_public_branch() is not None:
602
target_branch = submit_branch.get_public_branch()
603
submit_revision_id = submit_branch.last_revision()
604
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
605
graph = repository.get_graph(submit_branch.repository)
606
ancestor_id = graph.find_unique_lca(revision_id,
608
if base_revision_id is None:
609
base_revision_id = ancestor_id
610
if (include_patch, include_bundle) != (False, False):
611
repository.fetch(submit_branch.repository, submit_revision_id)
613
patch = klass._generate_diff(repository, revision_id,
619
bundle = klass._generate_bundle(repository, revision_id,
620
ancestor_id).encode('base-64')
624
if public_branch is not None and not include_bundle:
625
public_branch_obj = _mod_branch.Branch.open(public_branch)
626
public_branch_obj.lock_read()
627
locked.append(public_branch_obj)
628
if not public_branch_obj.repository.has_revision(
630
raise errors.PublicBranchOutOfDate(public_branch,
632
testament_sha1 = t.as_sha1()
634
for entry in reversed(locked):
636
return klass(revision_id, testament_sha1, time, timezone,
637
target_branch, patch, public_branch, message, bundle,
640
def _verify_patch(self, repository):
641
calculated_patch = self._generate_diff(repository, self.revision_id,
642
self.base_revision_id)
643
# Convert line-endings to UNIX
644
stored_patch = re.sub('\r\n?', '\n', self.patch)
645
calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
646
# Strip trailing whitespace
647
calculated_patch = re.sub(' *\n', '\n', calculated_patch)
648
stored_patch = re.sub(' *\n', '\n', stored_patch)
649
return (calculated_patch == stored_patch)
651
def get_merge_request(self, repository):
652
"""Provide data for performing a merge
654
Returns suggested base, suggested target, and patch verification status
656
verified = self._maybe_verify(repository)
657
return self.base_revision_id, self.revision_id, verified
659
def _maybe_verify(self, repository):
660
if self.patch is not None:
661
if self._verify_patch(repository):
666
return 'inapplicable'
669
class MergeDirectiveFormatRegistry(registry.Registry):
671
def register(self, directive, format_string=None):
672
if format_string is None:
673
format_string = directive._format_string
674
registry.Registry.register(self, format_string, directive)
677
_format_registry = MergeDirectiveFormatRegistry()
678
_format_registry.register(MergeDirective)
679
_format_registry.register(MergeDirective2)
680
# 0.19 never existed. It got renamed to 0.90. But by that point, there were
681
# already merge directives in the wild that used 0.19. Registering with the old
682
# format string to retain compatibility with those merge directives.
683
_format_registry.register(MergeDirective2,
684
'Bazaar merge directive format 2 (Bazaar 0.19)')