1
# Copyright (C) 2007-2010 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,
30
revision as _mod_revision,
36
from bzrlib.bundle import (
37
serializer as bundle_serializer,
39
from bzrlib.email_message import EmailMessage
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)
63
self.create_hook(hooks.HookPoint('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), False))
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, message=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: The 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: a local copy of the target branch
169
:param public_branch: location of a public branch containing the target
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
submit_branch = _mod_branch.Branch.open(target_branch)
185
if submit_branch.get_public_branch() is not None:
186
target_branch = submit_branch.get_public_branch()
187
if patch_type is None:
190
submit_revision_id = submit_branch.last_revision()
191
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
192
repository.fetch(submit_branch.repository, submit_revision_id)
193
graph = repository.get_graph()
194
ancestor_id = graph.find_unique_lca(revision_id,
196
type_handler = {'bundle': klass._generate_bundle,
197
'diff': klass._generate_diff,
198
None: lambda x, y, z: None }
199
patch = type_handler[patch_type](repository, revision_id,
202
if public_branch is not None and patch_type != 'bundle':
203
public_branch_obj = _mod_branch.Branch.open(public_branch)
204
if not public_branch_obj.repository.has_revision(revision_id):
205
raise errors.PublicBranchOutOfDate(public_branch,
208
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
209
patch, patch_type, public_branch, message)
211
def get_disk_name(self, branch):
212
"""Generate a suitable basename for storing this directive on disk
214
:param branch: The Branch this merge directive was generated fro
217
revno, revision_id = branch.last_revision_info()
218
if self.revision_id == revision_id:
221
revno = branch.get_revision_id_to_revno_map().get(self.revision_id,
223
nick = re.sub('(\W+)', '-', branch.nick).strip('-')
224
return '%s-%s' % (nick, '.'.join(str(n) for n in revno))
227
def _generate_diff(repository, revision_id, ancestor_id):
228
tree_1 = repository.revision_tree(ancestor_id)
229
tree_2 = repository.revision_tree(revision_id)
231
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
235
def _generate_bundle(repository, revision_id, ancestor_id):
237
bundle_serializer.write_bundle(repository, revision_id,
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 = EmailMessage(mail_from, mail_to, subject, body)
272
def install_revisions(self, target_repo):
273
"""Install revisions and return the target revision"""
274
if not target_repo.has_revision(self.revision_id):
275
if self.patch_type == 'bundle':
276
info = bundle_serializer.read_bundle(
277
StringIO(self.get_raw_bundle()))
278
# We don't use the bundle's target revision, because
279
# MergeDirective.revision_id is authoritative.
281
info.install_revisions(target_repo, stream_input=False)
282
except errors.RevisionNotPresent:
283
# At least one dependency isn't present. Try installing
284
# missing revisions from the submit branch
287
_mod_branch.Branch.open(self.target_branch)
288
except errors.NotBranchError:
289
raise errors.TargetNotBranch(self.target_branch)
290
missing_revisions = []
291
bundle_revisions = set(r.revision_id for r in
293
for revision in info.real_revisions:
294
for parent_id in revision.parent_ids:
295
if (parent_id not in bundle_revisions and
296
not target_repo.has_revision(parent_id)):
297
missing_revisions.append(parent_id)
298
# reverse missing revisions to try to get heads first
300
unique_missing_set = set()
301
for revision in reversed(missing_revisions):
302
if revision in unique_missing_set:
304
unique_missing.append(revision)
305
unique_missing_set.add(revision)
306
for missing_revision in unique_missing:
307
target_repo.fetch(submit_branch.repository,
309
info.install_revisions(target_repo, stream_input=False)
311
source_branch = _mod_branch.Branch.open(self.source_branch)
312
target_repo.fetch(source_branch.repository, self.revision_id)
313
return self.revision_id
315
def compose_merge_request(self, mail_client, to, body, branch, tree=None):
316
"""Compose a request to merge this directive.
318
:param mail_client: The mail client to use for composing this request.
319
:param to: The address to compose the request to.
320
:param branch: The Branch that was used to produce this directive.
321
:param tree: The Tree (if any) for the Branch used to produce this
324
basename = self.get_disk_name(branch)
326
if self.message is not None:
327
subject += self.message
329
revision = branch.repository.get_revision(self.revision_id)
330
subject += revision.get_summary()
331
if getattr(mail_client, 'supports_body', False):
333
for hook in self.hooks['merge_request_body']:
334
params = MergeRequestBodyParams(body, orig_body, self,
335
to, basename, subject, branch,
338
elif len(self.hooks['merge_request_body']) > 0:
339
trace.warning('Cannot run merge_request_body hooks because mail'
340
' client %s does not support message bodies.',
341
mail_client.__class__.__name__)
342
mail_client.compose_merge_request(to, subject,
343
''.join(self.to_lines()),
347
class MergeDirective(BaseMergeDirective):
349
"""A request to perform a merge into a branch.
351
Designed to be serialized and mailed. It provides all the information
352
needed to perform a merge automatically, by providing at minimum a revision
353
bundle or the location of a branch.
355
The serialization format is robust against certain common forms of
356
deterioration caused by mailing.
358
The format is also designed to be patch-compatible. If the directive
359
includes a diff or revision bundle, it should be possible to apply it
360
directly using the standard patch program.
363
_format_string = 'Bazaar merge directive format 1'
365
def __init__(self, revision_id, testament_sha1, time, timezone,
366
target_branch, patch=None, patch_type=None,
367
source_branch=None, message=None, bundle=None):
370
:param revision_id: The revision to merge
371
:param testament_sha1: The sha1 of the testament of the revision to
373
:param time: The current POSIX timestamp time
374
:param timezone: The timezone offset
375
:param target_branch: The branch to apply the merge to
376
:param patch: The text of a diff or bundle
377
:param patch_type: None, "diff" or "bundle", depending on the contents
379
:param source_branch: A public location to merge the revision from
380
:param message: The message to use when committing this merge
382
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
383
timezone, target_branch, patch, source_branch, message)
384
if patch_type not in (None, 'diff', 'bundle'):
385
raise ValueError(patch_type)
386
if patch_type != 'bundle' and source_branch is None:
387
raise errors.NoMergeSource()
388
if patch_type is not None and patch is None:
389
raise errors.PatchMissing(patch_type)
390
self.patch_type = patch_type
392
def clear_payload(self):
394
self.patch_type = None
396
def get_raw_bundle(self):
400
if self.patch_type == 'bundle':
405
bundle = property(_bundle)
408
def from_lines(klass, lines):
409
"""Deserialize a MergeRequest from an iterable of lines
411
:param lines: An iterable of lines
412
:return: a MergeRequest
414
line_iter = iter(lines)
416
for line in line_iter:
417
if line.startswith('# Bazaar merge directive format '):
418
return _format_registry.get(line[2:].rstrip())._from_lines(
420
firstline = firstline or line.strip()
421
raise errors.NotAMergeDirective(firstline)
424
def _from_lines(klass, line_iter):
425
stanza = rio.read_patch_stanza(line_iter)
426
patch_lines = list(line_iter)
427
if len(patch_lines) == 0:
431
patch = ''.join(patch_lines)
433
bundle_serializer.read_bundle(StringIO(patch))
434
except (errors.NotABundle, errors.BundleNotSupported,
438
patch_type = 'bundle'
439
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
441
for key in ('revision_id', 'testament_sha1', 'target_branch',
442
'source_branch', 'message'):
444
kwargs[key] = stanza.get(key)
447
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
448
return MergeDirective(time=time, timezone=timezone,
449
patch_type=patch_type, patch=patch, **kwargs)
452
lines = self._to_lines()
453
if self.patch is not None:
454
lines.extend(self.patch.splitlines(True))
458
def _generate_bundle(repository, revision_id, ancestor_id):
460
bundle_serializer.write_bundle(repository, revision_id,
461
ancestor_id, s, '0.9')
464
def get_merge_request(self, repository):
465
"""Provide data for performing a merge
467
Returns suggested base, suggested target, and patch verification status
469
return None, self.revision_id, 'inapplicable'
472
class MergeDirective2(BaseMergeDirective):
474
_format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
476
def __init__(self, revision_id, testament_sha1, time, timezone,
477
target_branch, patch=None, source_branch=None, message=None,
478
bundle=None, base_revision_id=None):
479
if source_branch is None and bundle is None:
480
raise errors.NoMergeSource()
481
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
482
timezone, target_branch, patch, source_branch, message)
484
self.base_revision_id = base_revision_id
486
def _patch_type(self):
487
if self.bundle is not None:
489
elif self.patch is not None:
494
patch_type = property(_patch_type)
496
def clear_payload(self):
500
def get_raw_bundle(self):
501
if self.bundle is None:
504
return self.bundle.decode('base-64')
507
def _from_lines(klass, line_iter):
508
stanza = rio.read_patch_stanza(line_iter)
512
start = line_iter.next()
513
except StopIteration:
516
if start.startswith('# Begin patch'):
518
for line in line_iter:
519
if line.startswith('# Begin bundle'):
522
patch_lines.append(line)
525
patch = ''.join(patch_lines)
526
if start is not None:
527
if start.startswith('# Begin bundle'):
528
bundle = ''.join(line_iter)
530
raise errors.IllegalMergeDirectivePayload(start)
531
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
533
for key in ('revision_id', 'testament_sha1', 'target_branch',
534
'source_branch', 'message', 'base_revision_id'):
536
kwargs[key] = stanza.get(key)
539
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
540
kwargs['base_revision_id'] =\
541
kwargs['base_revision_id'].encode('utf-8')
542
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
546
lines = self._to_lines(base_revision=True)
547
if self.patch is not None:
548
lines.append('# Begin patch\n')
549
lines.extend(self.patch.splitlines(True))
550
if self.bundle is not None:
551
lines.append('# Begin bundle\n')
552
lines.extend(self.bundle.splitlines(True))
556
def from_objects(klass, repository, revision_id, time, timezone,
557
target_branch, include_patch=True, include_bundle=True,
558
local_target_branch=None, public_branch=None, message=None,
559
base_revision_id=None):
560
"""Generate a merge directive from various objects
562
:param repository: The repository containing the revision
563
:param revision_id: The revision to merge
564
:param time: The POSIX timestamp of the date the request was issued.
565
:param timezone: The timezone of the request
566
:param target_branch: The url of the branch to merge into
567
:param include_patch: If true, include a preview patch
568
:param include_bundle: If true, include a bundle
569
:param local_target_branch: a local copy of the target branch
570
:param public_branch: location of a public branch containing the target
572
:param message: Message to use when committing the merge
573
:return: The merge directive
575
The public branch is always used if supplied. If no bundle is
576
included, the public branch must be supplied, and will be verified.
578
If the message is not supplied, the message from revision_id will be
583
repository.lock_write()
584
locked.append(repository)
585
t_revision_id = revision_id
586
if revision_id == 'null:':
588
t = testament.StrictTestament3.from_revision(repository,
590
submit_branch = _mod_branch.Branch.open(target_branch)
591
submit_branch.lock_read()
592
locked.append(submit_branch)
593
if submit_branch.get_public_branch() is not None:
594
target_branch = submit_branch.get_public_branch()
595
submit_revision_id = submit_branch.last_revision()
596
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
597
graph = repository.get_graph(submit_branch.repository)
598
ancestor_id = graph.find_unique_lca(revision_id,
600
if base_revision_id is None:
601
base_revision_id = ancestor_id
602
if (include_patch, include_bundle) != (False, False):
603
repository.fetch(submit_branch.repository, submit_revision_id)
605
patch = klass._generate_diff(repository, revision_id,
611
bundle = klass._generate_bundle(repository, revision_id,
612
ancestor_id).encode('base-64')
616
if public_branch is not None and not include_bundle:
617
public_branch_obj = _mod_branch.Branch.open(public_branch)
618
public_branch_obj.lock_read()
619
locked.append(public_branch_obj)
620
if not public_branch_obj.repository.has_revision(
622
raise errors.PublicBranchOutOfDate(public_branch,
624
testament_sha1 = t.as_sha1()
626
for entry in reversed(locked):
628
return klass(revision_id, testament_sha1, time, timezone,
629
target_branch, patch, public_branch, message, bundle,
632
def _verify_patch(self, repository):
633
calculated_patch = self._generate_diff(repository, self.revision_id,
634
self.base_revision_id)
635
# Convert line-endings to UNIX
636
stored_patch = re.sub('\r\n?', '\n', self.patch)
637
calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
638
# Strip trailing whitespace
639
calculated_patch = re.sub(' *\n', '\n', calculated_patch)
640
stored_patch = re.sub(' *\n', '\n', stored_patch)
641
return (calculated_patch == stored_patch)
643
def get_merge_request(self, repository):
644
"""Provide data for performing a merge
646
Returns suggested base, suggested target, and patch verification status
648
verified = self._maybe_verify(repository)
649
return self.base_revision_id, self.revision_id, verified
651
def _maybe_verify(self, repository):
652
if self.patch is not None:
653
if self._verify_patch(repository):
658
return 'inapplicable'
661
class MergeDirectiveFormatRegistry(registry.Registry):
663
def register(self, directive, format_string=None):
664
if format_string is None:
665
format_string = directive._format_string
666
registry.Registry.register(self, format_string, directive)
669
_format_registry = MergeDirectiveFormatRegistry()
670
_format_registry.register(MergeDirective)
671
_format_registry.register(MergeDirective2)
672
# 0.19 never existed. It got renamed to 0.90. But by that point, there were
673
# already merge directives in the wild that used 0.19. Registering with the old
674
# format string to retain compatibility with those merge directives.
675
_format_registry.register(MergeDirective2,
676
'Bazaar merge directive format 2 (Bazaar 0.19)')