13
13
# You should have received a copy of the GNU General Public License
14
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
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
from email import Message
18
19
from StringIO import StringIO
21
from bzrlib import lazy_import
22
lazy_import.lazy_import(globals(), """
23
21
from bzrlib import (
24
22
branch as _mod_branch,
31
26
revision as _mod_revision,
37
31
from bzrlib.bundle import (
38
32
serializer as bundle_serializer,
43
class MergeRequestBodyParams(object):
44
"""Parameter object for the merge_request_body hook."""
46
def __init__(self, body, orig_body, directive, to, basename, subject,
49
self.orig_body = orig_body
50
self.directive = directive
54
self.basename = basename
55
self.subject = subject
58
class MergeDirectiveHooks(hooks.Hooks):
59
"""Hooks for MergeDirective classes."""
62
hooks.Hooks.__init__(self, "bzrlib.merge_directive", "BaseMergeDirective.hooks")
63
self.add_hook('merge_request_body',
64
"Called with a MergeRequestBodyParams when a body is needed for"
65
" a merge request. Callbacks must return a body. If more"
66
" than one callback is registered, the output of one callback is"
67
" provided to the next.", (1, 15, 0))
70
class BaseMergeDirective(object):
36
class MergeDirective(object):
71
38
"""A request to perform a merge into a branch.
73
This is the base class that all merge directive implementations
76
:cvar multiple_output_files: Whether or not this merge directive
77
stores a set of revisions in more than one file
40
Designed to be serialized and mailed. It provides all the information
41
needed to perform a merge automatically, by providing at minimum a revision
42
bundle or the location of a branch.
44
The serialization format is robust against certain common forms of
45
deterioration caused by mailing.
47
The format is also designed to be patch-compatible. If the directive
48
includes a diff or revision bundle, it should be possible to apply it
49
directly using the standard patch program.
80
hooks = MergeDirectiveHooks()
82
multiple_output_files = False
52
_format_string = 'Bazaar merge directive format 1'
84
54
def __init__(self, revision_id, testament_sha1, time, timezone,
85
target_branch, patch=None, source_branch=None, message=None,
55
target_branch, patch=None, patch_type=None,
56
source_branch=None, message=None):
89
59
:param revision_id: The revision to merge
239
254
return s.getvalue()
241
def to_signed(self, branch):
242
"""Serialize as a signed string.
244
:param branch: The source branch, to get the signing strategy
247
my_gpg = gpg.GPGStrategy(branch.get_config())
248
return my_gpg.sign(''.join(self.to_lines()))
250
def to_email(self, mail_to, branch, sign=False):
251
"""Serialize as an email message.
253
:param mail_to: The address to mail the message to
254
:param branch: The source branch, to get the signing strategy and
256
:param sign: If True, gpg-sign the email
257
:return: an email message
259
mail_from = branch.get_config().username()
260
if self.message is not None:
261
subject = self.message
263
revision = branch.repository.get_revision(self.revision_id)
264
subject = revision.message
266
body = self.to_signed(branch)
268
body = ''.join(self.to_lines())
269
message = email_message.EmailMessage(mail_from, mail_to, subject,
273
256
def install_revisions(self, target_repo):
274
257
"""Install revisions and return the target revision"""
275
258
if not target_repo.has_revision(self.revision_id):
276
259
if self.patch_type == 'bundle':
277
info = bundle_serializer.read_bundle(
278
StringIO(self.get_raw_bundle()))
260
info = bundle_serializer.read_bundle(StringIO(self.patch))
279
261
# We don't use the bundle's target revision, because
280
262
# MergeDirective.revision_id is authoritative.
282
info.install_revisions(target_repo, stream_input=False)
283
except errors.RevisionNotPresent:
284
# At least one dependency isn't present. Try installing
285
# missing revisions from the submit branch
288
_mod_branch.Branch.open(self.target_branch)
289
except errors.NotBranchError:
290
raise errors.TargetNotBranch(self.target_branch)
291
missing_revisions = []
292
bundle_revisions = set(r.revision_id for r in
294
for revision in info.real_revisions:
295
for parent_id in revision.parent_ids:
296
if (parent_id not in bundle_revisions and
297
not target_repo.has_revision(parent_id)):
298
missing_revisions.append(parent_id)
299
# reverse missing revisions to try to get heads first
301
unique_missing_set = set()
302
for revision in reversed(missing_revisions):
303
if revision in unique_missing_set:
305
unique_missing.append(revision)
306
unique_missing_set.add(revision)
307
for missing_revision in unique_missing:
308
target_repo.fetch(submit_branch.repository,
310
info.install_revisions(target_repo, stream_input=False)
263
info.install_revisions(target_repo)
312
265
source_branch = _mod_branch.Branch.open(self.source_branch)
313
266
target_repo.fetch(source_branch.repository, self.revision_id)
314
267
return self.revision_id
316
def compose_merge_request(self, mail_client, to, body, branch, tree=None):
317
"""Compose a request to merge this directive.
319
:param mail_client: The mail client to use for composing this request.
320
:param to: The address to compose the request to.
321
:param branch: The Branch that was used to produce this directive.
322
:param tree: The Tree (if any) for the Branch used to produce this
325
basename = self.get_disk_name(branch)
327
if self.message is not None:
328
subject += self.message
330
revision = branch.repository.get_revision(self.revision_id)
331
subject += revision.get_summary()
332
if getattr(mail_client, 'supports_body', False):
334
for hook in self.hooks['merge_request_body']:
335
params = MergeRequestBodyParams(body, orig_body, self,
336
to, basename, subject, branch,
339
elif len(self.hooks['merge_request_body']) > 0:
340
trace.warning('Cannot run merge_request_body hooks because mail'
341
' client %s does not support message bodies.',
342
mail_client.__class__.__name__)
343
mail_client.compose_merge_request(to, subject,
344
''.join(self.to_lines()),
348
class MergeDirective(BaseMergeDirective):
350
"""A request to perform a merge into a branch.
352
Designed to be serialized and mailed. It provides all the information
353
needed to perform a merge automatically, by providing at minimum a revision
354
bundle or the location of a branch.
356
The serialization format is robust against certain common forms of
357
deterioration caused by mailing.
359
The format is also designed to be patch-compatible. If the directive
360
includes a diff or revision bundle, it should be possible to apply it
361
directly using the standard patch program.
364
_format_string = 'Bazaar merge directive format 1'
366
def __init__(self, revision_id, testament_sha1, time, timezone,
367
target_branch, patch=None, patch_type=None,
368
source_branch=None, message=None, bundle=None):
371
:param revision_id: The revision to merge
372
:param testament_sha1: The sha1 of the testament of the revision to
374
:param time: The current POSIX timestamp time
375
:param timezone: The timezone offset
376
:param target_branch: The branch to apply the merge to
377
:param patch: The text of a diff or bundle
378
:param patch_type: None, "diff" or "bundle", depending on the contents
380
:param source_branch: A public location to merge the revision from
381
:param message: The message to use when committing this merge
383
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
384
timezone, target_branch, patch, source_branch, message)
385
if patch_type not in (None, 'diff', 'bundle'):
386
raise ValueError(patch_type)
387
if patch_type != 'bundle' and source_branch is None:
388
raise errors.NoMergeSource()
389
if patch_type is not None and patch is None:
390
raise errors.PatchMissing(patch_type)
391
self.patch_type = patch_type
393
def clear_payload(self):
395
self.patch_type = None
397
def get_raw_bundle(self):
401
if self.patch_type == 'bundle':
406
bundle = property(_bundle)
409
def from_lines(klass, lines):
410
"""Deserialize a MergeRequest from an iterable of lines
412
:param lines: An iterable of lines
413
:return: a MergeRequest
415
line_iter = iter(lines)
417
for line in line_iter:
418
if line.startswith('# Bazaar merge directive format '):
419
return _format_registry.get(line[2:].rstrip())._from_lines(
421
firstline = firstline or line.strip()
422
raise errors.NotAMergeDirective(firstline)
425
def _from_lines(klass, line_iter):
426
stanza = rio.read_patch_stanza(line_iter)
427
patch_lines = list(line_iter)
428
if len(patch_lines) == 0:
432
patch = ''.join(patch_lines)
434
bundle_serializer.read_bundle(StringIO(patch))
435
except (errors.NotABundle, errors.BundleNotSupported,
439
patch_type = 'bundle'
440
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
442
for key in ('revision_id', 'testament_sha1', 'target_branch',
443
'source_branch', 'message'):
445
kwargs[key] = stanza.get(key)
448
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
449
return MergeDirective(time=time, timezone=timezone,
450
patch_type=patch_type, patch=patch, **kwargs)
453
lines = self._to_lines()
454
if self.patch is not None:
455
lines.extend(self.patch.splitlines(True))
459
def _generate_bundle(repository, revision_id, ancestor_id):
461
bundle_serializer.write_bundle(repository, revision_id,
462
ancestor_id, s, '0.9')
465
def get_merge_request(self, repository):
466
"""Provide data for performing a merge
468
Returns suggested base, suggested target, and patch verification status
470
return None, self.revision_id, 'inapplicable'
473
class MergeDirective2(BaseMergeDirective):
475
_format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
477
def __init__(self, revision_id, testament_sha1, time, timezone,
478
target_branch, patch=None, source_branch=None, message=None,
479
bundle=None, base_revision_id=None):
480
if source_branch is None and bundle is None:
481
raise errors.NoMergeSource()
482
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
483
timezone, target_branch, patch, source_branch, message)
485
self.base_revision_id = base_revision_id
487
def _patch_type(self):
488
if self.bundle is not None:
490
elif self.patch is not None:
495
patch_type = property(_patch_type)
497
def clear_payload(self):
501
def get_raw_bundle(self):
502
if self.bundle is None:
505
return self.bundle.decode('base-64')
508
def _from_lines(klass, line_iter):
509
stanza = rio.read_patch_stanza(line_iter)
513
start = line_iter.next()
514
except StopIteration:
517
if start.startswith('# Begin patch'):
519
for line in line_iter:
520
if line.startswith('# Begin bundle'):
523
patch_lines.append(line)
526
patch = ''.join(patch_lines)
527
if start is not None:
528
if start.startswith('# Begin bundle'):
529
bundle = ''.join(line_iter)
531
raise errors.IllegalMergeDirectivePayload(start)
532
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
534
for key in ('revision_id', 'testament_sha1', 'target_branch',
535
'source_branch', 'message', 'base_revision_id'):
537
kwargs[key] = stanza.get(key)
540
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
541
kwargs['base_revision_id'] =\
542
kwargs['base_revision_id'].encode('utf-8')
543
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
547
lines = self._to_lines(base_revision=True)
548
if self.patch is not None:
549
lines.append('# Begin patch\n')
550
lines.extend(self.patch.splitlines(True))
551
if self.bundle is not None:
552
lines.append('# Begin bundle\n')
553
lines.extend(self.bundle.splitlines(True))
557
def from_objects(klass, repository, revision_id, time, timezone,
558
target_branch, include_patch=True, include_bundle=True,
559
local_target_branch=None, public_branch=None, message=None,
560
base_revision_id=None):
561
"""Generate a merge directive from various objects
563
:param repository: The repository containing the revision
564
:param revision_id: The revision to merge
565
:param time: The POSIX timestamp of the date the request was issued.
566
:param timezone: The timezone of the request
567
:param target_branch: The url of the branch to merge into
568
:param include_patch: If true, include a preview patch
569
:param include_bundle: If true, include a bundle
570
:param local_target_branch: a local copy of the target branch
571
:param public_branch: location of a public branch containing the target
573
:param message: Message to use when committing the merge
574
:return: The merge directive
576
The public branch is always used if supplied. If no bundle is
577
included, the public branch must be supplied, and will be verified.
579
If the message is not supplied, the message from revision_id will be
584
repository.lock_write()
585
locked.append(repository)
586
t_revision_id = revision_id
587
if revision_id == 'null:':
589
t = testament.StrictTestament3.from_revision(repository,
591
submit_branch = _mod_branch.Branch.open(target_branch)
592
submit_branch.lock_read()
593
locked.append(submit_branch)
594
if submit_branch.get_public_branch() is not None:
595
target_branch = submit_branch.get_public_branch()
596
submit_revision_id = submit_branch.last_revision()
597
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
598
graph = repository.get_graph(submit_branch.repository)
599
ancestor_id = graph.find_unique_lca(revision_id,
601
if base_revision_id is None:
602
base_revision_id = ancestor_id
603
if (include_patch, include_bundle) != (False, False):
604
repository.fetch(submit_branch.repository, submit_revision_id)
606
patch = klass._generate_diff(repository, revision_id,
612
bundle = klass._generate_bundle(repository, revision_id,
613
ancestor_id).encode('base-64')
617
if public_branch is not None and not include_bundle:
618
public_branch_obj = _mod_branch.Branch.open(public_branch)
619
public_branch_obj.lock_read()
620
locked.append(public_branch_obj)
621
if not public_branch_obj.repository.has_revision(
623
raise errors.PublicBranchOutOfDate(public_branch,
625
testament_sha1 = t.as_sha1()
627
for entry in reversed(locked):
629
return klass(revision_id, testament_sha1, time, timezone,
630
target_branch, patch, public_branch, message, bundle,
633
def _verify_patch(self, repository):
634
calculated_patch = self._generate_diff(repository, self.revision_id,
635
self.base_revision_id)
636
# Convert line-endings to UNIX
637
stored_patch = re.sub('\r\n?', '\n', self.patch)
638
calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
639
# Strip trailing whitespace
640
calculated_patch = re.sub(' *\n', '\n', calculated_patch)
641
stored_patch = re.sub(' *\n', '\n', stored_patch)
642
return (calculated_patch == stored_patch)
644
def get_merge_request(self, repository):
645
"""Provide data for performing a merge
647
Returns suggested base, suggested target, and patch verification status
649
verified = self._maybe_verify(repository)
650
return self.base_revision_id, self.revision_id, verified
652
def _maybe_verify(self, repository):
653
if self.patch is not None:
654
if self._verify_patch(repository):
659
return 'inapplicable'
662
class MergeDirectiveFormatRegistry(registry.Registry):
664
def register(self, directive, format_string=None):
665
if format_string is None:
666
format_string = directive._format_string
667
registry.Registry.register(self, format_string, directive)
670
_format_registry = MergeDirectiveFormatRegistry()
671
_format_registry.register(MergeDirective)
672
_format_registry.register(MergeDirective2)
673
# 0.19 never existed. It got renamed to 0.90. But by that point, there were
674
# already merge directives in the wild that used 0.19. Registering with the old
675
# format string to retain compatibility with those merge directives.
676
_format_registry.register(MergeDirective2,
677
'Bazaar merge directive format 2 (Bazaar 0.19)')