254
198
return s.getvalue()
200
def to_signed(self, branch):
201
"""Serialize as a signed string.
203
:param branch: The source branch, to get the signing strategy
206
my_gpg = gpg.GPGStrategy(branch.get_config())
207
return my_gpg.sign(''.join(self.to_lines()))
209
def to_email(self, mail_to, branch, sign=False):
210
"""Serialize as an email message.
212
:param mail_to: The address to mail the message to
213
:param branch: The source branch, to get the signing strategy and
215
:param sign: If True, gpg-sign the email
216
:return: an email message
218
mail_from = branch.get_config().username()
219
if self.message is not None:
220
subject = self.message
222
revision = branch.repository.get_revision(self.revision_id)
223
subject = revision.message
225
body = self.to_signed(branch)
227
body = ''.join(self.to_lines())
228
message = EmailMessage(mail_from, mail_to, subject, body)
256
231
def install_revisions(self, target_repo):
257
232
"""Install revisions and return the target revision"""
258
233
if not target_repo.has_revision(self.revision_id):
259
234
if self.patch_type == 'bundle':
260
info = bundle_serializer.read_bundle(StringIO(self.patch))
235
info = bundle_serializer.read_bundle(
236
StringIO(self.get_raw_bundle()))
261
237
# We don't use the bundle's target revision, because
262
238
# MergeDirective.revision_id is authoritative.
263
info.install_revisions(target_repo)
240
info.install_revisions(target_repo, stream_input=False)
241
except errors.RevisionNotPresent:
242
# At least one dependency isn't present. Try installing
243
# missing revisions from the submit branch
246
_mod_branch.Branch.open(self.target_branch)
247
except errors.NotBranchError:
248
raise errors.TargetNotBranch(self.target_branch)
249
missing_revisions = []
250
bundle_revisions = set(r.revision_id for r in
252
for revision in info.real_revisions:
253
for parent_id in revision.parent_ids:
254
if (parent_id not in bundle_revisions and
255
not target_repo.has_revision(parent_id)):
256
missing_revisions.append(parent_id)
257
# reverse missing revisions to try to get heads first
259
unique_missing_set = set()
260
for revision in reversed(missing_revisions):
261
if revision in unique_missing_set:
263
unique_missing.append(revision)
264
unique_missing_set.add(revision)
265
for missing_revision in unique_missing:
266
target_repo.fetch(submit_branch.repository,
268
info.install_revisions(target_repo, stream_input=False)
265
270
source_branch = _mod_branch.Branch.open(self.source_branch)
266
271
target_repo.fetch(source_branch.repository, self.revision_id)
267
272
return self.revision_id
274
def compose_merge_request(self, mail_client, to, body, branch, tree=None):
275
"""Compose a request to merge this directive.
277
:param mail_client: The mail client to use for composing this request.
278
:param to: The address to compose the request to.
279
:param branch: The Branch that was used to produce this directive.
280
:param tree: The Tree (if any) for the Branch used to produce this
283
basename = self.get_disk_name(branch)
285
if self.message is not None:
286
subject += self.message
288
revision = branch.repository.get_revision(self.revision_id)
289
subject += revision.get_summary()
290
if getattr(mail_client, 'supports_body', False):
292
for hook in self.hooks['merge_request_body']:
293
params = MergeRequestBodyParams(body, orig_body, self,
294
to, basename, subject, branch,
297
elif len(self.hooks['merge_request_body']) > 0:
298
trace.warning('Cannot run merge_request_body hooks because mail'
299
' client %s does not support message bodies.',
300
mail_client.__class__.__name__)
301
mail_client.compose_merge_request(to, subject,
302
''.join(self.to_lines()),
306
class MergeDirective(_BaseMergeDirective):
308
"""A request to perform a merge into a branch.
310
Designed to be serialized and mailed. It provides all the information
311
needed to perform a merge automatically, by providing at minimum a revision
312
bundle or the location of a branch.
314
The serialization format is robust against certain common forms of
315
deterioration caused by mailing.
317
The format is also designed to be patch-compatible. If the directive
318
includes a diff or revision bundle, it should be possible to apply it
319
directly using the standard patch program.
322
_format_string = 'Bazaar merge directive format 1'
324
def __init__(self, revision_id, testament_sha1, time, timezone,
325
target_branch, patch=None, patch_type=None,
326
source_branch=None, message=None, bundle=None):
329
:param revision_id: The revision to merge
330
:param testament_sha1: The sha1 of the testament of the revision to
332
:param time: The current POSIX timestamp time
333
:param timezone: The timezone offset
334
:param target_branch: The branch to apply the merge to
335
:param patch: The text of a diff or bundle
336
:param patch_type: None, "diff" or "bundle", depending on the contents
338
:param source_branch: A public location to merge the revision from
339
:param message: The message to use when committing this merge
341
_BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
342
timezone, target_branch, patch, source_branch, message)
343
if patch_type not in (None, 'diff', 'bundle'):
344
raise ValueError(patch_type)
345
if patch_type != 'bundle' and source_branch is None:
346
raise errors.NoMergeSource()
347
if patch_type is not None and patch is None:
348
raise errors.PatchMissing(patch_type)
349
self.patch_type = patch_type
351
def clear_payload(self):
353
self.patch_type = None
355
def get_raw_bundle(self):
359
if self.patch_type == 'bundle':
364
bundle = property(_bundle)
367
def from_lines(klass, lines):
368
"""Deserialize a MergeRequest from an iterable of lines
370
:param lines: An iterable of lines
371
:return: a MergeRequest
373
line_iter = iter(lines)
375
for line in line_iter:
376
if line.startswith('# Bazaar merge directive format '):
377
return _format_registry.get(line[2:].rstrip())._from_lines(
379
firstline = firstline or line.strip()
380
raise errors.NotAMergeDirective(firstline)
383
def _from_lines(klass, line_iter):
384
stanza = rio.read_patch_stanza(line_iter)
385
patch_lines = list(line_iter)
386
if len(patch_lines) == 0:
390
patch = ''.join(patch_lines)
392
bundle_serializer.read_bundle(StringIO(patch))
393
except (errors.NotABundle, errors.BundleNotSupported,
397
patch_type = 'bundle'
398
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
400
for key in ('revision_id', 'testament_sha1', 'target_branch',
401
'source_branch', 'message'):
403
kwargs[key] = stanza.get(key)
406
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
407
return MergeDirective(time=time, timezone=timezone,
408
patch_type=patch_type, patch=patch, **kwargs)
411
lines = self._to_lines()
412
if self.patch is not None:
413
lines.extend(self.patch.splitlines(True))
417
def _generate_bundle(repository, revision_id, ancestor_id):
419
bundle_serializer.write_bundle(repository, revision_id,
420
ancestor_id, s, '0.9')
423
def get_merge_request(self, repository):
424
"""Provide data for performing a merge
426
Returns suggested base, suggested target, and patch verification status
428
return None, self.revision_id, 'inapplicable'
431
class MergeDirective2(_BaseMergeDirective):
433
_format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
435
def __init__(self, revision_id, testament_sha1, time, timezone,
436
target_branch, patch=None, source_branch=None, message=None,
437
bundle=None, base_revision_id=None):
438
if source_branch is None and bundle is None:
439
raise errors.NoMergeSource()
440
_BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
441
timezone, target_branch, patch, source_branch, message)
443
self.base_revision_id = base_revision_id
445
def _patch_type(self):
446
if self.bundle is not None:
448
elif self.patch is not None:
453
patch_type = property(_patch_type)
455
def clear_payload(self):
459
def get_raw_bundle(self):
460
if self.bundle is None:
463
return self.bundle.decode('base-64')
466
def _from_lines(klass, line_iter):
467
stanza = rio.read_patch_stanza(line_iter)
471
start = line_iter.next()
472
except StopIteration:
475
if start.startswith('# Begin patch'):
477
for line in line_iter:
478
if line.startswith('# Begin bundle'):
481
patch_lines.append(line)
484
patch = ''.join(patch_lines)
485
if start is not None:
486
if start.startswith('# Begin bundle'):
487
bundle = ''.join(line_iter)
489
raise errors.IllegalMergeDirectivePayload(start)
490
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
492
for key in ('revision_id', 'testament_sha1', 'target_branch',
493
'source_branch', 'message', 'base_revision_id'):
495
kwargs[key] = stanza.get(key)
498
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
499
kwargs['base_revision_id'] =\
500
kwargs['base_revision_id'].encode('utf-8')
501
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
505
lines = self._to_lines(base_revision=True)
506
if self.patch is not None:
507
lines.append('# Begin patch\n')
508
lines.extend(self.patch.splitlines(True))
509
if self.bundle is not None:
510
lines.append('# Begin bundle\n')
511
lines.extend(self.bundle.splitlines(True))
515
def from_objects(klass, repository, revision_id, time, timezone,
516
target_branch, include_patch=True, include_bundle=True,
517
local_target_branch=None, public_branch=None, message=None,
518
base_revision_id=None):
519
"""Generate a merge directive from various objects
521
:param repository: The repository containing the revision
522
:param revision_id: The revision to merge
523
:param time: The POSIX timestamp of the date the request was issued.
524
:param timezone: The timezone of the request
525
:param target_branch: The url of the branch to merge into
526
:param include_patch: If true, include a preview patch
527
:param include_bundle: If true, include a bundle
528
:param local_target_branch: a local copy of the target branch
529
:param public_branch: location of a public branch containing the target
531
:param message: Message to use when committing the merge
532
:return: The merge directive
534
The public branch is always used if supplied. If no bundle is
535
included, the public branch must be supplied, and will be verified.
537
If the message is not supplied, the message from revision_id will be
542
repository.lock_write()
543
locked.append(repository)
544
t_revision_id = revision_id
545
if revision_id == 'null:':
547
t = testament.StrictTestament3.from_revision(repository,
549
submit_branch = _mod_branch.Branch.open(target_branch)
550
submit_branch.lock_read()
551
locked.append(submit_branch)
552
if submit_branch.get_public_branch() is not None:
553
target_branch = submit_branch.get_public_branch()
554
submit_revision_id = submit_branch.last_revision()
555
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
556
graph = repository.get_graph(submit_branch.repository)
557
ancestor_id = graph.find_unique_lca(revision_id,
559
if base_revision_id is None:
560
base_revision_id = ancestor_id
561
if (include_patch, include_bundle) != (False, False):
562
repository.fetch(submit_branch.repository, submit_revision_id)
564
patch = klass._generate_diff(repository, revision_id,
570
bundle = klass._generate_bundle(repository, revision_id,
571
ancestor_id).encode('base-64')
575
if public_branch is not None and not include_bundle:
576
public_branch_obj = _mod_branch.Branch.open(public_branch)
577
public_branch_obj.lock_read()
578
locked.append(public_branch_obj)
579
if not public_branch_obj.repository.has_revision(
581
raise errors.PublicBranchOutOfDate(public_branch,
583
testament_sha1 = t.as_sha1()
585
for entry in reversed(locked):
587
return klass(revision_id, testament_sha1, time, timezone,
588
target_branch, patch, public_branch, message, bundle,
591
def _verify_patch(self, repository):
592
calculated_patch = self._generate_diff(repository, self.revision_id,
593
self.base_revision_id)
594
# Convert line-endings to UNIX
595
stored_patch = re.sub('\r\n?', '\n', self.patch)
596
calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
597
# Strip trailing whitespace
598
calculated_patch = re.sub(' *\n', '\n', calculated_patch)
599
stored_patch = re.sub(' *\n', '\n', stored_patch)
600
return (calculated_patch == stored_patch)
602
def get_merge_request(self, repository):
603
"""Provide data for performing a merge
605
Returns suggested base, suggested target, and patch verification status
607
verified = self._maybe_verify(repository)
608
return self.base_revision_id, self.revision_id, verified
610
def _maybe_verify(self, repository):
611
if self.patch is not None:
612
if self._verify_patch(repository):
617
return 'inapplicable'
620
class MergeDirectiveFormatRegistry(registry.Registry):
622
def register(self, directive, format_string=None):
623
if format_string is None:
624
format_string = directive._format_string
625
registry.Registry.register(self, format_string, directive)
628
_format_registry = MergeDirectiveFormatRegistry()
629
_format_registry.register(MergeDirective)
630
_format_registry.register(MergeDirective2)
631
# 0.19 never existed. It got renamed to 0.90. But by that point, there were
632
# already merge directives in the wild that used 0.19. Registering with the old
633
# format string to retain compatibility with those merge directives.
634
_format_registry.register(MergeDirective2,
635
'Bazaar merge directive format 2 (Bazaar 0.19)')