194
202
If the message is not supplied, the message from revision_id will be
195
203
used for the commit.
197
t_revision_id = revision_id
198
if revision_id == _mod_revision.NULL_REVISION:
200
t = testament.StrictTestament3.from_revision(repository, t_revision_id)
201
if local_target_branch is None:
202
submit_branch = _mod_branch.Branch.open(target_branch)
204
submit_branch = local_target_branch
205
t = testament.StrictTestament3.from_revision(repository, revision_id)
206
submit_branch = _mod_branch.Branch.open(target_branch)
205
207
if submit_branch.get_public_branch() is not None:
206
208
target_branch = submit_branch.get_public_branch()
207
209
if patch_type is None:
210
212
submit_revision_id = submit_branch.last_revision()
211
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
212
213
repository.fetch(submit_branch.repository, submit_revision_id)
213
graph = repository.get_graph()
214
ancestor_id = graph.find_unique_lca(revision_id,
214
ancestor_id = _mod_revision.common_ancestor(revision_id,
216
217
type_handler = {'bundle': klass._generate_bundle,
217
218
'diff': klass._generate_diff,
218
None: lambda x, y, z: None}
219
None: lambda x, y, z: None }
219
220
patch = type_handler[patch_type](repository, revision_id,
222
if public_branch is not None and patch_type != 'bundle':
223
public_branch_obj = _mod_branch.Branch.open(public_branch)
224
if not public_branch_obj.repository.has_revision(revision_id):
225
raise errors.PublicBranchOutOfDate(public_branch,
228
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
229
patch, patch_type, public_branch, message)
231
def get_disk_name(self, branch):
232
"""Generate a suitable basename for storing this directive on disk
234
:param branch: The Branch this merge directive was generated fro
237
revno, revision_id = branch.last_revision_info()
238
if self.revision_id == revision_id:
242
revno = branch.revision_id_to_dotted_revno(self.revision_id)
243
except errors.NoSuchRevision:
245
nick = re.sub('(\\W+)', '-', branch.nick).strip('-')
246
return '%s-%s' % (nick, '.'.join(str(n) for n in revno))
222
if patch_type == 'bundle':
224
bundle_serializer.write_bundle(repository, revision_id,
227
elif patch_type == 'diff':
228
patch = klass._generate_diff(repository, revision_id,
231
if public_branch is not None and patch_type != 'bundle':
232
public_branch_obj = _mod_branch.Branch.open(public_branch)
233
if not public_branch_obj.repository.has_revision(revision_id):
234
raise errors.PublicBranchOutOfDate(public_branch,
237
return MergeDirective(revision_id, t.as_sha1(), time, timezone,
238
target_branch, patch, patch_type, public_branch,
249
242
def _generate_diff(repository, revision_id, ancestor_id):
250
243
tree_1 = repository.revision_tree(ancestor_id)
251
244
tree_2 = repository.revision_tree(revision_id)
253
246
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
254
247
return s.getvalue()
257
250
def _generate_bundle(repository, revision_id, ancestor_id):
259
252
bundle_serializer.write_bundle(repository, revision_id,
261
254
return s.getvalue()
263
def to_signed(self, branch):
264
"""Serialize as a signed string.
266
:param branch: The source branch, to get the signing strategy
269
my_gpg = gpg.GPGStrategy(branch.get_config_stack())
270
return my_gpg.sign(b''.join(self.to_lines()), gpg.MODE_CLEAR)
272
def to_email(self, mail_to, branch, sign=False):
273
"""Serialize as an email message.
275
:param mail_to: The address to mail the message to
276
:param branch: The source branch, to get the signing strategy and
278
:param sign: If True, gpg-sign the email
279
:return: an email message
281
mail_from = branch.get_config_stack().get('email')
282
if self.message is not None:
283
subject = self.message
285
revision = branch.repository.get_revision(self.revision_id)
286
subject = revision.message
288
body = self.to_signed(branch)
290
body = b''.join(self.to_lines())
291
message = email_message.EmailMessage(mail_from, mail_to, subject,
295
256
def install_revisions(self, target_repo):
296
257
"""Install revisions and return the target revision"""
297
258
if not target_repo.has_revision(self.revision_id):
298
259
if self.patch_type == 'bundle':
299
info = bundle_serializer.read_bundle(
300
BytesIO(self.get_raw_bundle()))
260
info = bundle_serializer.read_bundle(StringIO(self.patch))
301
261
# We don't use the bundle's target revision, because
302
262
# MergeDirective.revision_id is authoritative.
304
info.install_revisions(target_repo, stream_input=False)
305
except errors.RevisionNotPresent:
306
# At least one dependency isn't present. Try installing
307
# missing revisions from the submit branch
310
_mod_branch.Branch.open(self.target_branch)
311
except errors.NotBranchError:
312
raise errors.TargetNotBranch(self.target_branch)
313
missing_revisions = []
314
bundle_revisions = set(r.revision_id for r in
316
for revision in info.real_revisions:
317
for parent_id in revision.parent_ids:
318
if (parent_id not in bundle_revisions
319
and not target_repo.has_revision(parent_id)):
320
missing_revisions.append(parent_id)
321
# reverse missing revisions to try to get heads first
323
unique_missing_set = set()
324
for revision in reversed(missing_revisions):
325
if revision in unique_missing_set:
327
unique_missing.append(revision)
328
unique_missing_set.add(revision)
329
for missing_revision in unique_missing:
330
target_repo.fetch(submit_branch.repository,
332
info.install_revisions(target_repo, stream_input=False)
263
info.install_revisions(target_repo)
334
265
source_branch = _mod_branch.Branch.open(self.source_branch)
335
266
target_repo.fetch(source_branch.repository, self.revision_id)
336
267
return self.revision_id
338
def compose_merge_request(self, mail_client, to, body, branch, tree=None):
339
"""Compose a request to merge this directive.
341
:param mail_client: The mail client to use for composing this request.
342
:param to: The address to compose the request to.
343
:param branch: The Branch that was used to produce this directive.
344
:param tree: The Tree (if any) for the Branch used to produce this
347
basename = self.get_disk_name(branch)
349
if self.message is not None:
350
subject += self.message
352
revision = branch.repository.get_revision(self.revision_id)
353
subject += revision.get_summary()
354
if getattr(mail_client, 'supports_body', False):
356
for hook in self.hooks['merge_request_body']:
357
params = MergeRequestBodyParams(body, orig_body, self,
358
to, basename, subject, branch,
361
elif len(self.hooks['merge_request_body']) > 0:
362
trace.warning('Cannot run merge_request_body hooks because mail'
363
' client %s does not support message bodies.',
364
mail_client.__class__.__name__)
365
mail_client.compose_merge_request(to, subject,
366
b''.join(self.to_lines()),
370
class MergeDirective(BaseMergeDirective):
372
"""A request to perform a merge into a branch.
374
Designed to be serialized and mailed. It provides all the information
375
needed to perform a merge automatically, by providing at minimum a revision
376
bundle or the location of a branch.
378
The serialization format is robust against certain common forms of
379
deterioration caused by mailing.
381
The format is also designed to be patch-compatible. If the directive
382
includes a diff or revision bundle, it should be possible to apply it
383
directly using the standard patch program.
386
_format_string = b'Bazaar merge directive format 1'
388
def __init__(self, revision_id, testament_sha1, time, timezone,
389
target_branch, patch=None, patch_type=None,
390
source_branch=None, message=None, bundle=None):
393
:param revision_id: The revision to merge
394
:param testament_sha1: The sha1 of the testament of the revision to
396
:param time: The current POSIX timestamp time
397
:param timezone: The timezone offset
398
:param target_branch: Location of the branch to apply the merge to
399
:param patch: The text of a diff or bundle
400
:param patch_type: None, "diff" or "bundle", depending on the contents
402
:param source_branch: A public location to merge the revision from
403
:param message: The message to use when committing this merge
405
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
406
timezone, target_branch, patch, source_branch, message)
407
if patch_type not in (None, 'diff', 'bundle'):
408
raise ValueError(patch_type)
409
if patch_type != 'bundle' and source_branch is None:
410
raise errors.NoMergeSource()
411
if patch_type is not None and patch is None:
412
raise errors.PatchMissing(patch_type)
413
self.patch_type = patch_type
415
def clear_payload(self):
417
self.patch_type = None
419
def get_raw_bundle(self):
423
if self.patch_type == 'bundle':
428
bundle = property(_bundle)
431
def from_lines(klass, lines):
432
"""Deserialize a MergeRequest from an iterable of lines
434
:param lines: An iterable of lines
435
:return: a MergeRequest
437
line_iter = iter(lines)
439
for line in line_iter:
440
if line.startswith(b'# Bazaar merge directive format '):
441
return _format_registry.get(line[2:].rstrip())._from_lines(
443
firstline = firstline or line.strip()
444
raise errors.NotAMergeDirective(firstline)
447
def _from_lines(klass, line_iter):
448
stanza = rio.read_patch_stanza(line_iter)
449
patch_lines = list(line_iter)
450
if len(patch_lines) == 0:
454
patch = b''.join(patch_lines)
456
bundle_serializer.read_bundle(BytesIO(patch))
457
except (errors.NotABundle, errors.BundleNotSupported,
461
patch_type = 'bundle'
462
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
464
for key in ('revision_id', 'testament_sha1', 'target_branch',
465
'source_branch', 'message'):
467
kwargs[key] = stanza.get(key)
470
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
471
if 'testament_sha1' in kwargs:
472
kwargs['testament_sha1'] = kwargs['testament_sha1'].encode('ascii')
473
return MergeDirective(time=time, timezone=timezone,
474
patch_type=patch_type, patch=patch, **kwargs)
477
lines = self._to_lines()
478
if self.patch is not None:
479
lines.extend(self.patch.splitlines(True))
483
def _generate_bundle(repository, revision_id, ancestor_id):
485
bundle_serializer.write_bundle(repository, revision_id,
486
ancestor_id, s, '0.9')
489
def get_merge_request(self, repository):
490
"""Provide data for performing a merge
492
Returns suggested base, suggested target, and patch verification status
494
return None, self.revision_id, 'inapplicable'
497
class MergeDirective2(BaseMergeDirective):
499
_format_string = b'Bazaar merge directive format 2 (Bazaar 0.90)'
501
def __init__(self, revision_id, testament_sha1, time, timezone,
502
target_branch, patch=None, source_branch=None, message=None,
503
bundle=None, base_revision_id=None):
504
if source_branch is None and bundle is None:
505
raise errors.NoMergeSource()
506
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
507
timezone, target_branch, patch, source_branch, message)
509
self.base_revision_id = base_revision_id
511
def _patch_type(self):
512
if self.bundle is not None:
514
elif self.patch is not None:
519
patch_type = property(_patch_type)
521
def clear_payload(self):
525
def get_raw_bundle(self):
526
if self.bundle is None:
529
return base64.b64decode(self.bundle)
532
def _from_lines(klass, line_iter):
533
stanza = rio.read_patch_stanza(line_iter)
537
start = next(line_iter)
538
except StopIteration:
541
if start.startswith(b'# Begin patch'):
543
for line in line_iter:
544
if line.startswith(b'# Begin bundle'):
547
patch_lines.append(line)
550
patch = b''.join(patch_lines)
551
if start is not None:
552
if start.startswith(b'# Begin bundle'):
553
bundle = b''.join(line_iter)
555
raise IllegalMergeDirectivePayload(start)
556
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
558
for key in ('revision_id', 'testament_sha1', 'target_branch',
559
'source_branch', 'message', 'base_revision_id'):
561
kwargs[key] = stanza.get(key)
564
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
565
kwargs['base_revision_id'] =\
566
kwargs['base_revision_id'].encode('utf-8')
567
if 'testament_sha1' in kwargs:
568
kwargs['testament_sha1'] = kwargs['testament_sha1'].encode('ascii')
569
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
573
lines = self._to_lines(base_revision=True)
574
if self.patch is not None:
575
lines.append(b'# Begin patch\n')
576
lines.extend(self.patch.splitlines(True))
577
if self.bundle is not None:
578
lines.append(b'# Begin bundle\n')
579
lines.extend(self.bundle.splitlines(True))
583
def from_objects(klass, repository, revision_id, time, timezone,
584
target_branch, include_patch=True, include_bundle=True,
585
local_target_branch=None, public_branch=None, message=None,
586
base_revision_id=None):
587
"""Generate a merge directive from various objects
589
:param repository: The repository containing the revision
590
:param revision_id: The revision to merge
591
:param time: The POSIX timestamp of the date the request was issued.
592
:param timezone: The timezone of the request
593
:param target_branch: The url of the branch to merge into
594
:param include_patch: If true, include a preview patch
595
:param include_bundle: If true, include a bundle
596
:param local_target_branch: the target branch, either itself or a local copy
597
:param public_branch: location of a public branch containing
599
:param message: Message to use when committing the merge
600
:return: The merge directive
602
The public branch is always used if supplied. If no bundle is
603
included, the public branch must be supplied, and will be verified.
605
If the message is not supplied, the message from revision_id will be
608
with contextlib.ExitStack() as exit_stack:
609
exit_stack.enter_context(repository.lock_write())
610
t_revision_id = revision_id
611
if revision_id == b'null:':
613
t = testament.StrictTestament3.from_revision(repository,
615
if local_target_branch is None:
616
submit_branch = _mod_branch.Branch.open(target_branch)
618
submit_branch = local_target_branch
619
exit_stack.enter_context(submit_branch.lock_read())
620
if submit_branch.get_public_branch() is not None:
621
target_branch = submit_branch.get_public_branch()
622
submit_revision_id = submit_branch.last_revision()
623
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
624
graph = repository.get_graph(submit_branch.repository)
625
ancestor_id = graph.find_unique_lca(revision_id,
627
if base_revision_id is None:
628
base_revision_id = ancestor_id
629
if (include_patch, include_bundle) != (False, False):
630
repository.fetch(submit_branch.repository, submit_revision_id)
632
patch = klass._generate_diff(repository, revision_id,
638
bundle = base64.b64encode(klass._generate_bundle(repository, revision_id,
643
if public_branch is not None and not include_bundle:
644
public_branch_obj = _mod_branch.Branch.open(public_branch)
645
exit_stack.enter_context(public_branch_obj.lock_read())
646
if not public_branch_obj.repository.has_revision(
648
raise errors.PublicBranchOutOfDate(public_branch,
650
testament_sha1 = t.as_sha1()
651
return klass(revision_id, testament_sha1, time, timezone,
652
target_branch, patch, public_branch, message, bundle,
655
def _verify_patch(self, repository):
656
calculated_patch = self._generate_diff(repository, self.revision_id,
657
self.base_revision_id)
658
# Convert line-endings to UNIX
659
stored_patch = re.sub(b'\r\n?', b'\n', self.patch)
660
calculated_patch = re.sub(b'\r\n?', b'\n', calculated_patch)
661
# Strip trailing whitespace
662
calculated_patch = re.sub(b' *\n', b'\n', calculated_patch)
663
stored_patch = re.sub(b' *\n', b'\n', stored_patch)
664
return (calculated_patch == stored_patch)
666
def get_merge_request(self, repository):
667
"""Provide data for performing a merge
669
Returns suggested base, suggested target, and patch verification status
671
verified = self._maybe_verify(repository)
672
return self.base_revision_id, self.revision_id, verified
674
def _maybe_verify(self, repository):
675
if self.patch is not None:
676
if self._verify_patch(repository):
681
return 'inapplicable'
684
class MergeDirectiveFormatRegistry(registry.Registry):
686
def register(self, directive, format_string=None):
687
if format_string is None:
688
format_string = directive._format_string
689
registry.Registry.register(self, format_string, directive)
692
_format_registry = MergeDirectiveFormatRegistry()
693
_format_registry.register(MergeDirective)
694
_format_registry.register(MergeDirective2)
695
# 0.19 never existed. It got renamed to 0.90. But by that point, there were
696
# already merge directives in the wild that used 0.19. Registering with the old
697
# format string to retain compatibility with those merge directives.
698
_format_registry.register(MergeDirective2,
699
b'Bazaar merge directive format 2 (Bazaar 0.19)')