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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
from email import Message
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
18
from StringIO import StringIO
21
from bzrlib import lazy_import
22
lazy_import.lazy_import(globals(), """
21
23
from bzrlib import (
22
24
branch as _mod_branch,
26
31
revision as _mod_revision,
31
37
from bzrlib.bundle import (
32
38
serializer as bundle_serializer,
36
class MergeDirective(object):
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):
38
71
"""A request to perform a merge into a branch.
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.
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
52
_format_string = 'Bazaar merge directive format 1'
80
hooks = MergeDirectiveHooks()
82
multiple_output_files = False
54
84
def __init__(self, revision_id, testament_sha1, time, timezone,
55
target_branch, patch=None, patch_type=None,
56
source_branch=None, message=None):
85
target_branch, patch=None, source_branch=None,
86
message=None, bundle=None):
59
89
:param revision_id: The revision to merge
259
242
return s.getvalue()
244
def to_signed(self, branch):
245
"""Serialize as a signed string.
247
:param branch: The source branch, to get the signing strategy
250
my_gpg = gpg.GPGStrategy(branch.get_config())
251
return my_gpg.sign(''.join(self.to_lines()))
253
def to_email(self, mail_to, branch, sign=False):
254
"""Serialize as an email message.
256
:param mail_to: The address to mail the message to
257
:param branch: The source branch, to get the signing strategy and
259
:param sign: If True, gpg-sign the email
260
:return: an email message
262
mail_from = branch.get_config().username()
263
if self.message is not None:
264
subject = self.message
266
revision = branch.repository.get_revision(self.revision_id)
267
subject = revision.message
269
body = self.to_signed(branch)
271
body = ''.join(self.to_lines())
272
message = email_message.EmailMessage(mail_from, mail_to, subject,
261
276
def install_revisions(self, target_repo):
262
277
"""Install revisions and return the target revision"""
263
278
if not target_repo.has_revision(self.revision_id):
264
279
if self.patch_type == 'bundle':
265
info = bundle_serializer.read_bundle(StringIO(self.patch))
280
info = bundle_serializer.read_bundle(
281
StringIO(self.get_raw_bundle()))
266
282
# We don't use the bundle's target revision, because
267
283
# MergeDirective.revision_id is authoritative.
268
info.install_revisions(target_repo)
285
info.install_revisions(target_repo, stream_input=False)
286
except errors.RevisionNotPresent:
287
# At least one dependency isn't present. Try installing
288
# missing revisions from the submit branch
291
_mod_branch.Branch.open(self.target_branch)
292
except errors.NotBranchError:
293
raise errors.TargetNotBranch(self.target_branch)
294
missing_revisions = []
295
bundle_revisions = set(r.revision_id for r in
297
for revision in info.real_revisions:
298
for parent_id in revision.parent_ids:
299
if (parent_id not in bundle_revisions and
300
not target_repo.has_revision(parent_id)):
301
missing_revisions.append(parent_id)
302
# reverse missing revisions to try to get heads first
304
unique_missing_set = set()
305
for revision in reversed(missing_revisions):
306
if revision in unique_missing_set:
308
unique_missing.append(revision)
309
unique_missing_set.add(revision)
310
for missing_revision in unique_missing:
311
target_repo.fetch(submit_branch.repository,
313
info.install_revisions(target_repo, stream_input=False)
270
315
source_branch = _mod_branch.Branch.open(self.source_branch)
271
316
target_repo.fetch(source_branch.repository, self.revision_id)
272
317
return self.revision_id
319
def compose_merge_request(self, mail_client, to, body, branch, tree=None):
320
"""Compose a request to merge this directive.
322
:param mail_client: The mail client to use for composing this request.
323
:param to: The address to compose the request to.
324
:param branch: The Branch that was used to produce this directive.
325
:param tree: The Tree (if any) for the Branch used to produce this
328
basename = self.get_disk_name(branch)
330
if self.message is not None:
331
subject += self.message
333
revision = branch.repository.get_revision(self.revision_id)
334
subject += revision.get_summary()
335
if getattr(mail_client, 'supports_body', False):
337
for hook in self.hooks['merge_request_body']:
338
params = MergeRequestBodyParams(body, orig_body, self,
339
to, basename, subject, branch,
342
elif len(self.hooks['merge_request_body']) > 0:
343
trace.warning('Cannot run merge_request_body hooks because mail'
344
' client %s does not support message bodies.',
345
mail_client.__class__.__name__)
346
mail_client.compose_merge_request(to, subject,
347
''.join(self.to_lines()),
351
class MergeDirective(BaseMergeDirective):
353
"""A request to perform a merge into a branch.
355
Designed to be serialized and mailed. It provides all the information
356
needed to perform a merge automatically, by providing at minimum a revision
357
bundle or the location of a branch.
359
The serialization format is robust against certain common forms of
360
deterioration caused by mailing.
362
The format is also designed to be patch-compatible. If the directive
363
includes a diff or revision bundle, it should be possible to apply it
364
directly using the standard patch program.
367
_format_string = 'Bazaar merge directive format 1'
369
def __init__(self, revision_id, testament_sha1, time, timezone,
370
target_branch, patch=None, patch_type=None,
371
source_branch=None, message=None, bundle=None):
374
:param revision_id: The revision to merge
375
:param testament_sha1: The sha1 of the testament of the revision to
377
:param time: The current POSIX timestamp time
378
:param timezone: The timezone offset
379
:param target_branch: Location of the branch to apply the merge to
380
:param patch: The text of a diff or bundle
381
:param patch_type: None, "diff" or "bundle", depending on the contents
383
:param source_branch: A public location to merge the revision from
384
:param message: The message to use when committing this merge
386
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
387
timezone, target_branch, patch, source_branch, message)
388
if patch_type not in (None, 'diff', 'bundle'):
389
raise ValueError(patch_type)
390
if patch_type != 'bundle' and source_branch is None:
391
raise errors.NoMergeSource()
392
if patch_type is not None and patch is None:
393
raise errors.PatchMissing(patch_type)
394
self.patch_type = patch_type
396
def clear_payload(self):
398
self.patch_type = None
400
def get_raw_bundle(self):
404
if self.patch_type == 'bundle':
409
bundle = property(_bundle)
412
def from_lines(klass, lines):
413
"""Deserialize a MergeRequest from an iterable of lines
415
:param lines: An iterable of lines
416
:return: a MergeRequest
418
line_iter = iter(lines)
420
for line in line_iter:
421
if line.startswith('# Bazaar merge directive format '):
422
return _format_registry.get(line[2:].rstrip())._from_lines(
424
firstline = firstline or line.strip()
425
raise errors.NotAMergeDirective(firstline)
428
def _from_lines(klass, line_iter):
429
stanza = rio.read_patch_stanza(line_iter)
430
patch_lines = list(line_iter)
431
if len(patch_lines) == 0:
435
patch = ''.join(patch_lines)
437
bundle_serializer.read_bundle(StringIO(patch))
438
except (errors.NotABundle, errors.BundleNotSupported,
442
patch_type = 'bundle'
443
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
445
for key in ('revision_id', 'testament_sha1', 'target_branch',
446
'source_branch', 'message'):
448
kwargs[key] = stanza.get(key)
451
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
452
return MergeDirective(time=time, timezone=timezone,
453
patch_type=patch_type, patch=patch, **kwargs)
456
lines = self._to_lines()
457
if self.patch is not None:
458
lines.extend(self.patch.splitlines(True))
462
def _generate_bundle(repository, revision_id, ancestor_id):
464
bundle_serializer.write_bundle(repository, revision_id,
465
ancestor_id, s, '0.9')
468
def get_merge_request(self, repository):
469
"""Provide data for performing a merge
471
Returns suggested base, suggested target, and patch verification status
473
return None, self.revision_id, 'inapplicable'
476
class MergeDirective2(BaseMergeDirective):
478
_format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
480
def __init__(self, revision_id, testament_sha1, time, timezone,
481
target_branch, patch=None, source_branch=None, message=None,
482
bundle=None, base_revision_id=None):
483
if source_branch is None and bundle is None:
484
raise errors.NoMergeSource()
485
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
486
timezone, target_branch, patch, source_branch, message)
488
self.base_revision_id = base_revision_id
490
def _patch_type(self):
491
if self.bundle is not None:
493
elif self.patch is not None:
498
patch_type = property(_patch_type)
500
def clear_payload(self):
504
def get_raw_bundle(self):
505
if self.bundle is None:
508
return self.bundle.decode('base-64')
511
def _from_lines(klass, line_iter):
512
stanza = rio.read_patch_stanza(line_iter)
516
start = line_iter.next()
517
except StopIteration:
520
if start.startswith('# Begin patch'):
522
for line in line_iter:
523
if line.startswith('# Begin bundle'):
526
patch_lines.append(line)
529
patch = ''.join(patch_lines)
530
if start is not None:
531
if start.startswith('# Begin bundle'):
532
bundle = ''.join(line_iter)
534
raise errors.IllegalMergeDirectivePayload(start)
535
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
537
for key in ('revision_id', 'testament_sha1', 'target_branch',
538
'source_branch', 'message', 'base_revision_id'):
540
kwargs[key] = stanza.get(key)
543
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
544
kwargs['base_revision_id'] =\
545
kwargs['base_revision_id'].encode('utf-8')
546
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
550
lines = self._to_lines(base_revision=True)
551
if self.patch is not None:
552
lines.append('# Begin patch\n')
553
lines.extend(self.patch.splitlines(True))
554
if self.bundle is not None:
555
lines.append('# Begin bundle\n')
556
lines.extend(self.bundle.splitlines(True))
560
def from_objects(klass, repository, revision_id, time, timezone,
561
target_branch, include_patch=True, include_bundle=True,
562
local_target_branch=None, public_branch=None, message=None,
563
base_revision_id=None):
564
"""Generate a merge directive from various objects
566
:param repository: The repository containing the revision
567
:param revision_id: The revision to merge
568
:param time: The POSIX timestamp of the date the request was issued.
569
:param timezone: The timezone of the request
570
:param target_branch: The url of the branch to merge into
571
:param include_patch: If true, include a preview patch
572
:param include_bundle: If true, include a bundle
573
:param local_target_branch: the target branch, either itself or a local copy
574
:param public_branch: location of a public branch containing
576
:param message: Message to use when committing the merge
577
:return: The merge directive
579
The public branch is always used if supplied. If no bundle is
580
included, the public branch must be supplied, and will be verified.
582
If the message is not supplied, the message from revision_id will be
587
repository.lock_write()
588
locked.append(repository)
589
t_revision_id = revision_id
590
if revision_id == 'null:':
592
t = testament.StrictTestament3.from_revision(repository,
594
if local_target_branch is None:
595
submit_branch = _mod_branch.Branch.open(target_branch)
597
submit_branch = local_target_branch
598
submit_branch.lock_read()
599
locked.append(submit_branch)
600
if submit_branch.get_public_branch() is not None:
601
target_branch = submit_branch.get_public_branch()
602
submit_revision_id = submit_branch.last_revision()
603
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
604
graph = repository.get_graph(submit_branch.repository)
605
ancestor_id = graph.find_unique_lca(revision_id,
607
if base_revision_id is None:
608
base_revision_id = ancestor_id
609
if (include_patch, include_bundle) != (False, False):
610
repository.fetch(submit_branch.repository, submit_revision_id)
612
patch = klass._generate_diff(repository, revision_id,
618
bundle = klass._generate_bundle(repository, revision_id,
619
ancestor_id).encode('base-64')
623
if public_branch is not None and not include_bundle:
624
public_branch_obj = _mod_branch.Branch.open(public_branch)
625
public_branch_obj.lock_read()
626
locked.append(public_branch_obj)
627
if not public_branch_obj.repository.has_revision(
629
raise errors.PublicBranchOutOfDate(public_branch,
631
testament_sha1 = t.as_sha1()
633
for entry in reversed(locked):
635
return klass(revision_id, testament_sha1, time, timezone,
636
target_branch, patch, public_branch, message, bundle,
639
def _verify_patch(self, repository):
640
calculated_patch = self._generate_diff(repository, self.revision_id,
641
self.base_revision_id)
642
# Convert line-endings to UNIX
643
stored_patch = re.sub('\r\n?', '\n', self.patch)
644
calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
645
# Strip trailing whitespace
646
calculated_patch = re.sub(' *\n', '\n', calculated_patch)
647
stored_patch = re.sub(' *\n', '\n', stored_patch)
648
return (calculated_patch == stored_patch)
650
def get_merge_request(self, repository):
651
"""Provide data for performing a merge
653
Returns suggested base, suggested target, and patch verification status
655
verified = self._maybe_verify(repository)
656
return self.base_revision_id, self.revision_id, verified
658
def _maybe_verify(self, repository):
659
if self.patch is not None:
660
if self._verify_patch(repository):
665
return 'inapplicable'
668
class MergeDirectiveFormatRegistry(registry.Registry):
670
def register(self, directive, format_string=None):
671
if format_string is None:
672
format_string = directive._format_string
673
registry.Registry.register(self, format_string, directive)
676
_format_registry = MergeDirectiveFormatRegistry()
677
_format_registry.register(MergeDirective)
678
_format_registry.register(MergeDirective2)
679
# 0.19 never existed. It got renamed to 0.90. But by that point, there were
680
# already merge directives in the wild that used 0.19. Registering with the old
681
# format string to retain compatibility with those merge directives.
682
_format_registry.register(MergeDirective2,
683
'Bazaar merge directive format 2 (Bazaar 0.19)')