1
# Copyright (C) 2007-2010 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
18
from StringIO import StringIO
22
branch as _mod_branch,
28
revision as _mod_revision,
34
from bzrlib.bundle import (
35
serializer as bundle_serializer,
37
from bzrlib.email_message import EmailMessage
40
class MergeRequestBodyParams(object):
41
"""Parameter object for the merge_request_body hook."""
43
def __init__(self, body, orig_body, directive, to, basename, subject,
46
self.orig_body = orig_body
47
self.directive = directive
51
self.basename = basename
52
self.subject = subject
55
class MergeDirectiveHooks(hooks.Hooks):
56
"""Hooks for MergeDirective classes."""
59
hooks.Hooks.__init__(self)
60
self.create_hook(hooks.HookPoint('merge_request_body',
61
"Called with a MergeRequestBodyParams when a body is needed for"
62
" a merge request. Callbacks must return a body. If more"
63
" than one callback is registered, the output of one callback is"
64
" provided to the next.", (1, 15, 0), False))
67
class BaseMergeDirective(object):
69
hooks = MergeDirectiveHooks()
71
def __init__(self, revision_id, testament_sha1, time, timezone,
72
target_branch, patch=None, source_branch=None, message=None,
76
:param revision_id: The revision to merge
77
:param testament_sha1: The sha1 of the testament of the revision to
79
:param time: The current POSIX timestamp time
80
:param timezone: The timezone offset
81
:param target_branch: The branch to apply the merge to
82
:param patch: The text of a diff or bundle
83
:param source_branch: A public location to merge the revision from
84
:param message: The message to use when committing this merge
86
self.revision_id = revision_id
87
self.testament_sha1 = testament_sha1
89
self.timezone = timezone
90
self.target_branch = target_branch
92
self.source_branch = source_branch
93
self.message = message
96
"""Serialize as a list of lines
98
:return: a list of lines
100
raise NotImplementedError(self.to_lines)
102
def get_raw_bundle(self):
103
"""Return the bundle for this merge directive.
105
:return: bundle text or None if there is no bundle
109
def _to_lines(self, base_revision=False):
110
"""Serialize as a list of lines
112
:return: a list of lines
114
time_str = timestamp.format_patch_date(self.time, self.timezone)
115
stanza = rio.Stanza(revision_id=self.revision_id, timestamp=time_str,
116
target_branch=self.target_branch,
117
testament_sha1=self.testament_sha1)
118
for key in ('source_branch', 'message'):
119
if self.__dict__[key] is not None:
120
stanza.add(key, self.__dict__[key])
122
stanza.add('base_revision_id', self.base_revision_id)
123
lines = ['# ' + self._format_string + '\n']
124
lines.extend(rio.to_patch_lines(stanza))
129
def from_objects(klass, repository, revision_id, time, timezone,
130
target_branch, patch_type='bundle',
131
local_target_branch=None, public_branch=None, message=None):
132
"""Generate a merge directive from various objects
134
:param repository: The repository containing the revision
135
:param revision_id: The revision to merge
136
:param time: The POSIX timestamp of the date the request was issued.
137
:param timezone: The timezone of the request
138
:param target_branch: The url of the branch to merge into
139
:param patch_type: 'bundle', 'diff' or None, depending on the type of
141
:param local_target_branch: a local copy of the target branch
142
:param public_branch: location of a public branch containing the target
144
:param message: Message to use when committing the merge
145
:return: The merge directive
147
The public branch is always used if supplied. If the patch_type is
148
not 'bundle', the public branch must be supplied, and will be verified.
150
If the message is not supplied, the message from revision_id will be
153
t_revision_id = revision_id
154
if revision_id == _mod_revision.NULL_REVISION:
156
t = testament.StrictTestament3.from_revision(repository, t_revision_id)
157
submit_branch = _mod_branch.Branch.open(target_branch)
158
if submit_branch.get_public_branch() is not None:
159
target_branch = submit_branch.get_public_branch()
160
if patch_type is None:
163
submit_revision_id = submit_branch.last_revision()
164
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
165
repository.fetch(submit_branch.repository, submit_revision_id)
166
graph = repository.get_graph()
167
ancestor_id = graph.find_unique_lca(revision_id,
169
type_handler = {'bundle': klass._generate_bundle,
170
'diff': klass._generate_diff,
171
None: lambda x, y, z: None }
172
patch = type_handler[patch_type](repository, revision_id,
175
if public_branch is not None and patch_type != 'bundle':
176
public_branch_obj = _mod_branch.Branch.open(public_branch)
177
if not public_branch_obj.repository.has_revision(revision_id):
178
raise errors.PublicBranchOutOfDate(public_branch,
181
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
182
patch, patch_type, public_branch, message)
184
def get_disk_name(self, branch):
185
"""Generate a suitable basename for storing this directive on disk
187
:param branch: The Branch this merge directive was generated fro
190
revno, revision_id = branch.last_revision_info()
191
if self.revision_id == revision_id:
194
revno = branch.get_revision_id_to_revno_map().get(self.revision_id,
196
nick = re.sub('(\W+)', '-', branch.nick).strip('-')
197
return '%s-%s' % (nick, '.'.join(str(n) for n in revno))
200
def _generate_diff(repository, revision_id, ancestor_id):
201
tree_1 = repository.revision_tree(ancestor_id)
202
tree_2 = repository.revision_tree(revision_id)
204
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
208
def _generate_bundle(repository, revision_id, ancestor_id):
210
bundle_serializer.write_bundle(repository, revision_id,
214
def to_signed(self, branch):
215
"""Serialize as a signed string.
217
:param branch: The source branch, to get the signing strategy
220
my_gpg = gpg.GPGStrategy(branch.get_config())
221
return my_gpg.sign(''.join(self.to_lines()))
223
def to_email(self, mail_to, branch, sign=False):
224
"""Serialize as an email message.
226
:param mail_to: The address to mail the message to
227
:param branch: The source branch, to get the signing strategy and
229
:param sign: If True, gpg-sign the email
230
:return: an email message
232
mail_from = branch.get_config().username()
233
if self.message is not None:
234
subject = self.message
236
revision = branch.repository.get_revision(self.revision_id)
237
subject = revision.message
239
body = self.to_signed(branch)
241
body = ''.join(self.to_lines())
242
message = EmailMessage(mail_from, mail_to, subject, body)
245
def install_revisions(self, target_repo):
246
"""Install revisions and return the target revision"""
247
if not target_repo.has_revision(self.revision_id):
248
if self.patch_type == 'bundle':
249
info = bundle_serializer.read_bundle(
250
StringIO(self.get_raw_bundle()))
251
# We don't use the bundle's target revision, because
252
# MergeDirective.revision_id is authoritative.
254
info.install_revisions(target_repo, stream_input=False)
255
except errors.RevisionNotPresent:
256
# At least one dependency isn't present. Try installing
257
# missing revisions from the submit branch
260
_mod_branch.Branch.open(self.target_branch)
261
except errors.NotBranchError:
262
raise errors.TargetNotBranch(self.target_branch)
263
missing_revisions = []
264
bundle_revisions = set(r.revision_id for r in
266
for revision in info.real_revisions:
267
for parent_id in revision.parent_ids:
268
if (parent_id not in bundle_revisions and
269
not target_repo.has_revision(parent_id)):
270
missing_revisions.append(parent_id)
271
# reverse missing revisions to try to get heads first
273
unique_missing_set = set()
274
for revision in reversed(missing_revisions):
275
if revision in unique_missing_set:
277
unique_missing.append(revision)
278
unique_missing_set.add(revision)
279
for missing_revision in unique_missing:
280
target_repo.fetch(submit_branch.repository,
282
info.install_revisions(target_repo, stream_input=False)
284
source_branch = _mod_branch.Branch.open(self.source_branch)
285
target_repo.fetch(source_branch.repository, self.revision_id)
286
return self.revision_id
288
def compose_merge_request(self, mail_client, to, body, branch, tree=None):
289
"""Compose a request to merge this directive.
291
:param mail_client: The mail client to use for composing this request.
292
:param to: The address to compose the request to.
293
:param branch: The Branch that was used to produce this directive.
294
:param tree: The Tree (if any) for the Branch used to produce this
297
basename = self.get_disk_name(branch)
299
if self.message is not None:
300
subject += self.message
302
revision = branch.repository.get_revision(self.revision_id)
303
subject += revision.get_summary()
304
if getattr(mail_client, 'supports_body', False):
306
for hook in self.hooks['merge_request_body']:
307
params = MergeRequestBodyParams(body, orig_body, self,
308
to, basename, subject, branch,
311
elif len(self.hooks['merge_request_body']) > 0:
312
trace.warning('Cannot run merge_request_body hooks because mail'
313
' client %s does not support message bodies.',
314
mail_client.__class__.__name__)
315
mail_client.compose_merge_request(to, subject,
316
''.join(self.to_lines()),
320
class MergeDirective(BaseMergeDirective):
322
"""A request to perform a merge into a branch.
324
Designed to be serialized and mailed. It provides all the information
325
needed to perform a merge automatically, by providing at minimum a revision
326
bundle or the location of a branch.
328
The serialization format is robust against certain common forms of
329
deterioration caused by mailing.
331
The format is also designed to be patch-compatible. If the directive
332
includes a diff or revision bundle, it should be possible to apply it
333
directly using the standard patch program.
336
_format_string = 'Bazaar merge directive format 1'
338
def __init__(self, revision_id, testament_sha1, time, timezone,
339
target_branch, patch=None, patch_type=None,
340
source_branch=None, message=None, bundle=None):
343
:param revision_id: The revision to merge
344
:param testament_sha1: The sha1 of the testament of the revision to
346
:param time: The current POSIX timestamp time
347
:param timezone: The timezone offset
348
:param target_branch: The branch to apply the merge to
349
:param patch: The text of a diff or bundle
350
:param patch_type: None, "diff" or "bundle", depending on the contents
352
:param source_branch: A public location to merge the revision from
353
:param message: The message to use when committing this merge
355
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
356
timezone, target_branch, patch, source_branch, message)
357
if patch_type not in (None, 'diff', 'bundle'):
358
raise ValueError(patch_type)
359
if patch_type != 'bundle' and source_branch is None:
360
raise errors.NoMergeSource()
361
if patch_type is not None and patch is None:
362
raise errors.PatchMissing(patch_type)
363
self.patch_type = patch_type
365
def clear_payload(self):
367
self.patch_type = None
369
def get_raw_bundle(self):
373
if self.patch_type == 'bundle':
378
bundle = property(_bundle)
381
def from_lines(klass, lines):
382
"""Deserialize a MergeRequest from an iterable of lines
384
:param lines: An iterable of lines
385
:return: a MergeRequest
387
line_iter = iter(lines)
389
for line in line_iter:
390
if line.startswith('# Bazaar merge directive format '):
391
return _format_registry.get(line[2:].rstrip())._from_lines(
393
firstline = firstline or line.strip()
394
raise errors.NotAMergeDirective(firstline)
397
def _from_lines(klass, line_iter):
398
stanza = rio.read_patch_stanza(line_iter)
399
patch_lines = list(line_iter)
400
if len(patch_lines) == 0:
404
patch = ''.join(patch_lines)
406
bundle_serializer.read_bundle(StringIO(patch))
407
except (errors.NotABundle, errors.BundleNotSupported,
411
patch_type = 'bundle'
412
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
414
for key in ('revision_id', 'testament_sha1', 'target_branch',
415
'source_branch', 'message'):
417
kwargs[key] = stanza.get(key)
420
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
421
return MergeDirective(time=time, timezone=timezone,
422
patch_type=patch_type, patch=patch, **kwargs)
425
lines = self._to_lines()
426
if self.patch is not None:
427
lines.extend(self.patch.splitlines(True))
431
def _generate_bundle(repository, revision_id, ancestor_id):
433
bundle_serializer.write_bundle(repository, revision_id,
434
ancestor_id, s, '0.9')
437
def get_merge_request(self, repository):
438
"""Provide data for performing a merge
440
Returns suggested base, suggested target, and patch verification status
442
return None, self.revision_id, 'inapplicable'
445
class MergeDirective2(BaseMergeDirective):
447
_format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
449
def __init__(self, revision_id, testament_sha1, time, timezone,
450
target_branch, patch=None, source_branch=None, message=None,
451
bundle=None, base_revision_id=None):
452
if source_branch is None and bundle is None:
453
raise errors.NoMergeSource()
454
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
455
timezone, target_branch, patch, source_branch, message)
457
self.base_revision_id = base_revision_id
459
def _patch_type(self):
460
if self.bundle is not None:
462
elif self.patch is not None:
467
patch_type = property(_patch_type)
469
def clear_payload(self):
473
def get_raw_bundle(self):
474
if self.bundle is None:
477
return self.bundle.decode('base-64')
480
def _from_lines(klass, line_iter):
481
stanza = rio.read_patch_stanza(line_iter)
485
start = line_iter.next()
486
except StopIteration:
489
if start.startswith('# Begin patch'):
491
for line in line_iter:
492
if line.startswith('# Begin bundle'):
495
patch_lines.append(line)
498
patch = ''.join(patch_lines)
499
if start is not None:
500
if start.startswith('# Begin bundle'):
501
bundle = ''.join(line_iter)
503
raise errors.IllegalMergeDirectivePayload(start)
504
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
506
for key in ('revision_id', 'testament_sha1', 'target_branch',
507
'source_branch', 'message', 'base_revision_id'):
509
kwargs[key] = stanza.get(key)
512
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
513
kwargs['base_revision_id'] =\
514
kwargs['base_revision_id'].encode('utf-8')
515
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
519
lines = self._to_lines(base_revision=True)
520
if self.patch is not None:
521
lines.append('# Begin patch\n')
522
lines.extend(self.patch.splitlines(True))
523
if self.bundle is not None:
524
lines.append('# Begin bundle\n')
525
lines.extend(self.bundle.splitlines(True))
529
def from_objects(klass, repository, revision_id, time, timezone,
530
target_branch, include_patch=True, include_bundle=True,
531
local_target_branch=None, public_branch=None, message=None,
532
base_revision_id=None):
533
"""Generate a merge directive from various objects
535
:param repository: The repository containing the revision
536
:param revision_id: The revision to merge
537
:param time: The POSIX timestamp of the date the request was issued.
538
:param timezone: The timezone of the request
539
:param target_branch: The url of the branch to merge into
540
:param include_patch: If true, include a preview patch
541
:param include_bundle: If true, include a bundle
542
:param local_target_branch: a local copy of the target branch
543
:param public_branch: location of a public branch containing the target
545
:param message: Message to use when committing the merge
546
:return: The merge directive
548
The public branch is always used if supplied. If no bundle is
549
included, the public branch must be supplied, and will be verified.
551
If the message is not supplied, the message from revision_id will be
556
repository.lock_write()
557
locked.append(repository)
558
t_revision_id = revision_id
559
if revision_id == 'null:':
561
t = testament.StrictTestament3.from_revision(repository,
563
submit_branch = _mod_branch.Branch.open(target_branch)
564
submit_branch.lock_read()
565
locked.append(submit_branch)
566
if submit_branch.get_public_branch() is not None:
567
target_branch = submit_branch.get_public_branch()
568
submit_revision_id = submit_branch.last_revision()
569
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
570
graph = repository.get_graph(submit_branch.repository)
571
ancestor_id = graph.find_unique_lca(revision_id,
573
if base_revision_id is None:
574
base_revision_id = ancestor_id
575
if (include_patch, include_bundle) != (False, False):
576
repository.fetch(submit_branch.repository, submit_revision_id)
578
patch = klass._generate_diff(repository, revision_id,
584
bundle = klass._generate_bundle(repository, revision_id,
585
ancestor_id).encode('base-64')
589
if public_branch is not None and not include_bundle:
590
public_branch_obj = _mod_branch.Branch.open(public_branch)
591
public_branch_obj.lock_read()
592
locked.append(public_branch_obj)
593
if not public_branch_obj.repository.has_revision(
595
raise errors.PublicBranchOutOfDate(public_branch,
597
testament_sha1 = t.as_sha1()
599
for entry in reversed(locked):
601
return klass(revision_id, testament_sha1, time, timezone,
602
target_branch, patch, public_branch, message, bundle,
605
def _verify_patch(self, repository):
606
calculated_patch = self._generate_diff(repository, self.revision_id,
607
self.base_revision_id)
608
# Convert line-endings to UNIX
609
stored_patch = re.sub('\r\n?', '\n', self.patch)
610
calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
611
# Strip trailing whitespace
612
calculated_patch = re.sub(' *\n', '\n', calculated_patch)
613
stored_patch = re.sub(' *\n', '\n', stored_patch)
614
return (calculated_patch == stored_patch)
616
def get_merge_request(self, repository):
617
"""Provide data for performing a merge
619
Returns suggested base, suggested target, and patch verification status
621
verified = self._maybe_verify(repository)
622
return self.base_revision_id, self.revision_id, verified
624
def _maybe_verify(self, repository):
625
if self.patch is not None:
626
if self._verify_patch(repository):
631
return 'inapplicable'
634
class MergeDirectiveFormatRegistry(registry.Registry):
636
def register(self, directive, format_string=None):
637
if format_string is None:
638
format_string = directive._format_string
639
registry.Registry.register(self, format_string, directive)
642
_format_registry = MergeDirectiveFormatRegistry()
643
_format_registry.register(MergeDirective)
644
_format_registry.register(MergeDirective2)
645
# 0.19 never existed. It got renamed to 0.90. But by that point, there were
646
# already merge directives in the wild that used 0.19. Registering with the old
647
# format string to retain compatibility with those merge directives.
648
_format_registry.register(MergeDirective2,
649
'Bazaar merge directive format 2 (Bazaar 0.19)')