181
202
If the message is not supplied, the message from revision_id will be
182
203
used for the commit.
184
t_revision_id = revision_id
185
if revision_id == _mod_revision.NULL_REVISION:
187
t = testament.StrictTestament3.from_revision(repository, t_revision_id)
188
if local_target_branch is None:
189
submit_branch = _mod_branch.Branch.open(target_branch)
191
submit_branch = local_target_branch
205
t = testament.StrictTestament3.from_revision(repository, revision_id)
206
submit_branch = _mod_branch.Branch.open(target_branch)
192
207
if submit_branch.get_public_branch() is not None:
193
208
target_branch = submit_branch.get_public_branch()
194
209
if patch_type is None:
197
212
submit_revision_id = submit_branch.last_revision()
198
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
199
213
repository.fetch(submit_branch.repository, submit_revision_id)
200
graph = repository.get_graph()
201
ancestor_id = graph.find_unique_lca(revision_id,
214
ancestor_id = _mod_revision.common_ancestor(revision_id,
203
217
type_handler = {'bundle': klass._generate_bundle,
204
218
'diff': klass._generate_diff,
205
219
None: lambda x, y, z: None }
206
220
patch = type_handler[patch_type](repository, revision_id,
209
if public_branch is not None and patch_type != 'bundle':
210
public_branch_obj = _mod_branch.Branch.open(public_branch)
211
if not public_branch_obj.repository.has_revision(revision_id):
212
raise errors.PublicBranchOutOfDate(public_branch,
215
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
216
patch, patch_type, public_branch, message)
218
def get_disk_name(self, branch):
219
"""Generate a suitable basename for storing this directive on disk
221
:param branch: The Branch this merge directive was generated fro
224
revno, revision_id = branch.last_revision_info()
225
if self.revision_id == revision_id:
229
revno = branch.revision_id_to_dotted_revno(self.revision_id)
230
except errors.NoSuchRevision:
232
nick = re.sub('(\\W+)', '-', branch.nick).strip('-')
233
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,
236
242
def _generate_diff(repository, revision_id, ancestor_id):
237
243
tree_1 = repository.revision_tree(ancestor_id)
238
244
tree_2 = repository.revision_tree(revision_id)
240
246
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
241
247
return s.getvalue()
244
250
def _generate_bundle(repository, revision_id, ancestor_id):
246
252
bundle_serializer.write_bundle(repository, revision_id,
248
254
return s.getvalue()
250
def to_signed(self, branch):
251
"""Serialize as a signed string.
253
:param branch: The source branch, to get the signing strategy
256
my_gpg = gpg.GPGStrategy(branch.get_config_stack())
257
return my_gpg.sign(b''.join(self.to_lines()), gpg.MODE_CLEAR)
259
def to_email(self, mail_to, branch, sign=False):
260
"""Serialize as an email message.
262
:param mail_to: The address to mail the message to
263
:param branch: The source branch, to get the signing strategy and
265
:param sign: If True, gpg-sign the email
266
:return: an email message
268
mail_from = branch.get_config_stack().get('email')
269
if self.message is not None:
270
subject = self.message
272
revision = branch.repository.get_revision(self.revision_id)
273
subject = revision.message
275
body = self.to_signed(branch)
277
body = b''.join(self.to_lines())
278
message = email_message.EmailMessage(mail_from, mail_to, subject,
282
256
def install_revisions(self, target_repo):
283
257
"""Install revisions and return the target revision"""
284
258
if not target_repo.has_revision(self.revision_id):
285
259
if self.patch_type == 'bundle':
286
info = bundle_serializer.read_bundle(
287
BytesIO(self.get_raw_bundle()))
260
info = bundle_serializer.read_bundle(StringIO(self.patch))
288
261
# We don't use the bundle's target revision, because
289
262
# MergeDirective.revision_id is authoritative.
291
info.install_revisions(target_repo, stream_input=False)
292
except errors.RevisionNotPresent:
293
# At least one dependency isn't present. Try installing
294
# missing revisions from the submit branch
297
_mod_branch.Branch.open(self.target_branch)
298
except errors.NotBranchError:
299
raise errors.TargetNotBranch(self.target_branch)
300
missing_revisions = []
301
bundle_revisions = set(r.revision_id for r in
303
for revision in info.real_revisions:
304
for parent_id in revision.parent_ids:
305
if (parent_id not in bundle_revisions and
306
not target_repo.has_revision(parent_id)):
307
missing_revisions.append(parent_id)
308
# reverse missing revisions to try to get heads first
310
unique_missing_set = set()
311
for revision in reversed(missing_revisions):
312
if revision in unique_missing_set:
314
unique_missing.append(revision)
315
unique_missing_set.add(revision)
316
for missing_revision in unique_missing:
317
target_repo.fetch(submit_branch.repository,
319
info.install_revisions(target_repo, stream_input=False)
263
info.install_revisions(target_repo)
321
265
source_branch = _mod_branch.Branch.open(self.source_branch)
322
266
target_repo.fetch(source_branch.repository, self.revision_id)
323
267
return self.revision_id
325
def compose_merge_request(self, mail_client, to, body, branch, tree=None):
326
"""Compose a request to merge this directive.
328
:param mail_client: The mail client to use for composing this request.
329
:param to: The address to compose the request to.
330
:param branch: The Branch that was used to produce this directive.
331
:param tree: The Tree (if any) for the Branch used to produce this
334
basename = self.get_disk_name(branch)
336
if self.message is not None:
337
subject += self.message
339
revision = branch.repository.get_revision(self.revision_id)
340
subject += revision.get_summary()
341
if getattr(mail_client, 'supports_body', False):
343
for hook in self.hooks['merge_request_body']:
344
params = MergeRequestBodyParams(body, orig_body, self,
345
to, basename, subject, branch,
348
elif len(self.hooks['merge_request_body']) > 0:
349
trace.warning('Cannot run merge_request_body hooks because mail'
350
' client %s does not support message bodies.',
351
mail_client.__class__.__name__)
352
mail_client.compose_merge_request(to, subject,
353
b''.join(self.to_lines()),
357
class MergeDirective(BaseMergeDirective):
359
"""A request to perform a merge into a branch.
361
Designed to be serialized and mailed. It provides all the information
362
needed to perform a merge automatically, by providing at minimum a revision
363
bundle or the location of a branch.
365
The serialization format is robust against certain common forms of
366
deterioration caused by mailing.
368
The format is also designed to be patch-compatible. If the directive
369
includes a diff or revision bundle, it should be possible to apply it
370
directly using the standard patch program.
373
_format_string = b'Bazaar merge directive format 1'
375
def __init__(self, revision_id, testament_sha1, time, timezone,
376
target_branch, patch=None, patch_type=None,
377
source_branch=None, message=None, bundle=None):
380
:param revision_id: The revision to merge
381
:param testament_sha1: The sha1 of the testament of the revision to
383
:param time: The current POSIX timestamp time
384
:param timezone: The timezone offset
385
:param target_branch: Location of the branch to apply the merge to
386
:param patch: The text of a diff or bundle
387
:param patch_type: None, "diff" or "bundle", depending on the contents
389
:param source_branch: A public location to merge the revision from
390
:param message: The message to use when committing this merge
392
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
393
timezone, target_branch, patch, source_branch, message)
394
if patch_type not in (None, 'diff', 'bundle'):
395
raise ValueError(patch_type)
396
if patch_type != 'bundle' and source_branch is None:
397
raise errors.NoMergeSource()
398
if patch_type is not None and patch is None:
399
raise errors.PatchMissing(patch_type)
400
self.patch_type = patch_type
402
def clear_payload(self):
404
self.patch_type = None
406
def get_raw_bundle(self):
410
if self.patch_type == 'bundle':
415
bundle = property(_bundle)
418
def from_lines(klass, lines):
419
"""Deserialize a MergeRequest from an iterable of lines
421
:param lines: An iterable of lines
422
:return: a MergeRequest
424
line_iter = iter(lines)
426
for line in line_iter:
427
if line.startswith(b'# Bazaar merge directive format '):
428
return _format_registry.get(line[2:].rstrip())._from_lines(
430
firstline = firstline or line.strip()
431
raise errors.NotAMergeDirective(firstline)
434
def _from_lines(klass, line_iter):
435
stanza = rio.read_patch_stanza(line_iter)
436
patch_lines = list(line_iter)
437
if len(patch_lines) == 0:
441
patch = b''.join(patch_lines)
443
bundle_serializer.read_bundle(BytesIO(patch))
444
except (errors.NotABundle, errors.BundleNotSupported,
448
patch_type = 'bundle'
449
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
451
for key in ('revision_id', 'testament_sha1', 'target_branch',
452
'source_branch', 'message'):
454
kwargs[key] = stanza.get(key)
457
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
458
return MergeDirective(time=time, timezone=timezone,
459
patch_type=patch_type, patch=patch, **kwargs)
462
lines = self._to_lines()
463
if self.patch is not None:
464
lines.extend(self.patch.splitlines(True))
468
def _generate_bundle(repository, revision_id, ancestor_id):
470
bundle_serializer.write_bundle(repository, revision_id,
471
ancestor_id, s, '0.9')
474
def get_merge_request(self, repository):
475
"""Provide data for performing a merge
477
Returns suggested base, suggested target, and patch verification status
479
return None, self.revision_id, 'inapplicable'
482
class MergeDirective2(BaseMergeDirective):
484
_format_string = b'Bazaar merge directive format 2 (Bazaar 0.90)'
486
def __init__(self, revision_id, testament_sha1, time, timezone,
487
target_branch, patch=None, source_branch=None, message=None,
488
bundle=None, base_revision_id=None):
489
if source_branch is None and bundle is None:
490
raise errors.NoMergeSource()
491
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
492
timezone, target_branch, patch, source_branch, message)
494
self.base_revision_id = base_revision_id
496
def _patch_type(self):
497
if self.bundle is not None:
499
elif self.patch is not None:
504
patch_type = property(_patch_type)
506
def clear_payload(self):
510
def get_raw_bundle(self):
511
if self.bundle is None:
514
return base64.b64decode(self.bundle)
517
def _from_lines(klass, line_iter):
518
stanza = rio.read_patch_stanza(line_iter)
522
start = next(line_iter)
523
except StopIteration:
526
if start.startswith(b'# Begin patch'):
528
for line in line_iter:
529
if line.startswith(b'# Begin bundle'):
532
patch_lines.append(line)
535
patch = b''.join(patch_lines)
536
if start is not None:
537
if start.startswith(b'# Begin bundle'):
538
bundle = b''.join(line_iter)
540
raise errors.IllegalMergeDirectivePayload(start)
541
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
543
for key in ('revision_id', 'testament_sha1', 'target_branch',
544
'source_branch', 'message', 'base_revision_id'):
546
kwargs[key] = stanza.get(key)
549
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
550
kwargs['base_revision_id'] =\
551
kwargs['base_revision_id'].encode('utf-8')
552
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
556
lines = self._to_lines(base_revision=True)
557
if self.patch is not None:
558
lines.append(b'# Begin patch\n')
559
lines.extend(self.patch.splitlines(True))
560
if self.bundle is not None:
561
lines.append(b'# Begin bundle\n')
562
lines.extend(self.bundle.splitlines(True))
566
def from_objects(klass, repository, revision_id, time, timezone,
567
target_branch, include_patch=True, include_bundle=True,
568
local_target_branch=None, public_branch=None, message=None,
569
base_revision_id=None):
570
"""Generate a merge directive from various objects
572
:param repository: The repository containing the revision
573
:param revision_id: The revision to merge
574
:param time: The POSIX timestamp of the date the request was issued.
575
:param timezone: The timezone of the request
576
:param target_branch: The url of the branch to merge into
577
:param include_patch: If true, include a preview patch
578
:param include_bundle: If true, include a bundle
579
:param local_target_branch: the target branch, either itself or a local copy
580
:param public_branch: location of a public branch containing
582
:param message: Message to use when committing the merge
583
:return: The merge directive
585
The public branch is always used if supplied. If no bundle is
586
included, the public branch must be supplied, and will be verified.
588
If the message is not supplied, the message from revision_id will be
593
repository.lock_write()
594
locked.append(repository)
595
t_revision_id = revision_id
596
if revision_id == b'null:':
598
t = testament.StrictTestament3.from_revision(repository,
600
if local_target_branch is None:
601
submit_branch = _mod_branch.Branch.open(target_branch)
603
submit_branch = local_target_branch
604
submit_branch.lock_read()
605
locked.append(submit_branch)
606
if submit_branch.get_public_branch() is not None:
607
target_branch = submit_branch.get_public_branch()
608
submit_revision_id = submit_branch.last_revision()
609
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
610
graph = repository.get_graph(submit_branch.repository)
611
ancestor_id = graph.find_unique_lca(revision_id,
613
if base_revision_id is None:
614
base_revision_id = ancestor_id
615
if (include_patch, include_bundle) != (False, False):
616
repository.fetch(submit_branch.repository, submit_revision_id)
618
patch = klass._generate_diff(repository, revision_id,
624
bundle = base64.b64encode(klass._generate_bundle(repository, revision_id,
629
if public_branch is not None and not include_bundle:
630
public_branch_obj = _mod_branch.Branch.open(public_branch)
631
public_branch_obj.lock_read()
632
locked.append(public_branch_obj)
633
if not public_branch_obj.repository.has_revision(
635
raise errors.PublicBranchOutOfDate(public_branch,
637
testament_sha1 = t.as_sha1()
639
for entry in reversed(locked):
641
return klass(revision_id, testament_sha1, time, timezone,
642
target_branch, patch, public_branch, message, bundle,
645
def _verify_patch(self, repository):
646
calculated_patch = self._generate_diff(repository, self.revision_id,
647
self.base_revision_id)
648
# Convert line-endings to UNIX
649
stored_patch = re.sub(b'\r\n?', b'\n', self.patch)
650
calculated_patch = re.sub(b'\r\n?', b'\n', calculated_patch)
651
# Strip trailing whitespace
652
calculated_patch = re.sub(b' *\n', b'\n', calculated_patch)
653
stored_patch = re.sub(b' *\n', b'\n', stored_patch)
654
return (calculated_patch == stored_patch)
656
def get_merge_request(self, repository):
657
"""Provide data for performing a merge
659
Returns suggested base, suggested target, and patch verification status
661
verified = self._maybe_verify(repository)
662
return self.base_revision_id, self.revision_id, verified
664
def _maybe_verify(self, repository):
665
if self.patch is not None:
666
if self._verify_patch(repository):
671
return 'inapplicable'
674
class MergeDirectiveFormatRegistry(registry.Registry):
676
def register(self, directive, format_string=None):
677
if format_string is None:
678
format_string = directive._format_string
679
registry.Registry.register(self, format_string, directive)
682
_format_registry = MergeDirectiveFormatRegistry()
683
_format_registry.register(MergeDirective)
684
_format_registry.register(MergeDirective2)
685
# 0.19 never existed. It got renamed to 0.90. But by that point, there were
686
# already merge directives in the wild that used 0.19. Registering with the old
687
# format string to retain compatibility with those merge directives.
688
_format_registry.register(MergeDirective2,
689
b'Bazaar merge directive format 2 (Bazaar 0.19)')