1
# Copyright (C) 2007-2011 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
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
18
from StringIO import StringIO
21
from bzrlib import lazy_import
22
lazy_import.lazy_import(globals(), """
24
branch as _mod_branch,
31
revision as _mod_revision,
37
from bzrlib.bundle import (
38
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):
71
"""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
80
hooks = MergeDirectiveHooks()
82
multiple_output_files = False
84
def __init__(self, revision_id, testament_sha1, time, timezone,
85
target_branch, patch=None, source_branch=None,
86
message=None, bundle=None):
89
:param revision_id: The revision to merge
90
:param testament_sha1: The sha1 of the testament of the revision to
92
:param time: The current POSIX timestamp time
93
:param timezone: The timezone offset
94
:param target_branch: Location of branch to apply the merge to
95
:param patch: The text of a diff or bundle
96
:param source_branch: A public location to merge the revision from
97
:param message: The message to use when committing this merge
99
self.revision_id = revision_id
100
self.testament_sha1 = testament_sha1
102
self.timezone = timezone
103
self.target_branch = target_branch
105
self.source_branch = source_branch
106
self.message = message
109
"""Serialize as a list of lines
111
:return: a list of lines
113
raise NotImplementedError(self.to_lines)
116
"""Serialize as a set of files.
118
:return: List of tuples with filename and contents as lines
120
raise NotImplementedError(self.to_files)
122
def get_raw_bundle(self):
123
"""Return the bundle for this merge directive.
125
:return: bundle text or None if there is no bundle
129
def _to_lines(self, base_revision=False):
130
"""Serialize as a list of lines
132
:return: a list of lines
134
time_str = timestamp.format_patch_date(self.time, self.timezone)
135
stanza = rio.Stanza(revision_id=self.revision_id, timestamp=time_str,
136
target_branch=self.target_branch,
137
testament_sha1=self.testament_sha1)
138
for key in ('source_branch', 'message'):
139
if self.__dict__[key] is not None:
140
stanza.add(key, self.__dict__[key])
142
stanza.add('base_revision_id', self.base_revision_id)
143
lines = ['# ' + self._format_string + '\n']
144
lines.extend(rio.to_patch_lines(stanza))
148
def write_to_directory(self, path):
149
"""Write this merge directive to a series of files in a directory.
151
:param path: Filesystem path to write to
153
raise NotImplementedError(self.write_to_directory)
156
def from_objects(klass, repository, revision_id, time, timezone,
157
target_branch, patch_type='bundle',
158
local_target_branch=None, public_branch=None, message=None):
159
"""Generate a merge directive from various objects
161
:param repository: The repository containing the revision
162
:param revision_id: The revision to merge
163
:param time: The POSIX timestamp of the date the request was issued.
164
:param timezone: The timezone of the request
165
:param target_branch: The url of the branch to merge into
166
:param patch_type: 'bundle', 'diff' or None, depending on the type of
168
:param local_target_branch: the submit branch, either itself or a local copy
169
:param public_branch: location of a public branch containing
171
:param message: Message to use when committing the merge
172
:return: The merge directive
174
The public branch is always used if supplied. If the patch_type is
175
not 'bundle', the public branch must be supplied, and will be verified.
177
If the message is not supplied, the message from revision_id will be
180
t_revision_id = revision_id
181
if revision_id == _mod_revision.NULL_REVISION:
183
t = testament.StrictTestament3.from_revision(repository, t_revision_id)
184
if local_target_branch is None:
185
submit_branch = _mod_branch.Branch.open(target_branch)
187
submit_branch = local_target_branch
188
if submit_branch.get_public_branch() is not None:
189
target_branch = submit_branch.get_public_branch()
190
if patch_type is None:
193
submit_revision_id = submit_branch.last_revision()
194
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
195
repository.fetch(submit_branch.repository, submit_revision_id)
196
graph = repository.get_graph()
197
ancestor_id = graph.find_unique_lca(revision_id,
199
type_handler = {'bundle': klass._generate_bundle,
200
'diff': klass._generate_diff,
201
None: lambda x, y, z: None }
202
patch = type_handler[patch_type](repository, revision_id,
205
if public_branch is not None and patch_type != 'bundle':
206
public_branch_obj = _mod_branch.Branch.open(public_branch)
207
if not public_branch_obj.repository.has_revision(revision_id):
208
raise errors.PublicBranchOutOfDate(public_branch,
211
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
212
patch, patch_type, public_branch, message)
214
def get_disk_name(self, branch):
215
"""Generate a suitable basename for storing this directive on disk
217
:param branch: The Branch this merge directive was generated fro
220
revno, revision_id = branch.last_revision_info()
221
if self.revision_id == revision_id:
224
revno = branch.get_revision_id_to_revno_map().get(self.revision_id,
226
nick = re.sub('(\W+)', '-', branch.nick).strip('-')
227
return '%s-%s' % (nick, '.'.join(str(n) for n in revno))
230
def _generate_diff(repository, revision_id, ancestor_id):
231
tree_1 = repository.revision_tree(ancestor_id)
232
tree_2 = repository.revision_tree(revision_id)
234
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
238
def _generate_bundle(repository, revision_id, ancestor_id):
240
bundle_serializer.write_bundle(repository, revision_id,
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_stack())
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,
276
def install_revisions(self, target_repo):
277
"""Install revisions and return the target revision"""
278
if not target_repo.has_revision(self.revision_id):
279
if self.patch_type == 'bundle':
280
info = bundle_serializer.read_bundle(
281
StringIO(self.get_raw_bundle()))
282
# We don't use the bundle's target revision, because
283
# MergeDirective.revision_id is authoritative.
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)
315
source_branch = _mod_branch.Branch.open(self.source_branch)
316
target_repo.fetch(source_branch.repository, self.revision_id)
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)')