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
22
branch as _mod_branch,
28
revision as _mod_revision,
34
from bzrlib.bundle import (
35
serializer as bundle_serializer,
37
from bzrlib.email_message import EmailMessage
40
class MergeRequestBodyParams(object):
41
"""Parameter object for the merge_request_body hook."""
43
def __init__(self, body, orig_body, directive, to, basename, subject,
46
self.orig_body = orig_body
47
self.directive = directive
51
self.basename = basename
52
self.subject = subject
55
class MergeDirectiveHooks(hooks.Hooks):
56
"""Hooks for MergeDirective classes."""
59
hooks.Hooks.__init__(self)
60
self.create_hook(hooks.HookPoint('merge_request_body',
61
"Called with a MergeRequestBodyParams when a body is needed for"
62
" a merge request. Callbacks must return a body. If more"
63
" than one callback is registered, the output of one callback is"
64
" provided to the next.", (1, 15, 0), False))
67
class BaseMergeDirective(object):
68
"""A request to perform a merge into a branch.
70
This is the base class that all merge directive implementations
73
:cvar multiple_output_files: Whether or not this merge directive
74
stores a set of revisions in more than one file
77
hooks = MergeDirectiveHooks()
79
multiple_output_files = False
81
def __init__(self, revision_id, testament_sha1, time, timezone,
82
target_branch, patch=None, source_branch=None, message=None,
86
:param revision_id: The revision to merge
87
:param testament_sha1: The sha1 of the testament of the revision to
89
:param time: The current POSIX timestamp time
90
:param timezone: The timezone offset
91
:param target_branch: The branch to apply the merge to
92
:param patch: The text of a diff or bundle
93
:param source_branch: A public location to merge the revision from
94
:param message: The message to use when committing this merge
96
self.revision_id = revision_id
97
self.testament_sha1 = testament_sha1
99
self.timezone = timezone
100
self.target_branch = target_branch
102
self.source_branch = source_branch
103
self.message = message
106
"""Serialize as a list of lines
108
:return: a list of lines
110
raise NotImplementedError(self.to_lines)
113
"""Serialize as a set of files.
115
:return: List of tuples with filename and contents as lines
117
raise NotImplementedError(self.to_files)
119
def get_raw_bundle(self):
120
"""Return the bundle for this merge directive.
122
:return: bundle text or None if there is no bundle
126
def _to_lines(self, base_revision=False):
127
"""Serialize as a list of lines
129
:return: a list of lines
131
time_str = timestamp.format_patch_date(self.time, self.timezone)
132
stanza = rio.Stanza(revision_id=self.revision_id, timestamp=time_str,
133
target_branch=self.target_branch,
134
testament_sha1=self.testament_sha1)
135
for key in ('source_branch', 'message'):
136
if self.__dict__[key] is not None:
137
stanza.add(key, self.__dict__[key])
139
stanza.add('base_revision_id', self.base_revision_id)
140
lines = ['# ' + self._format_string + '\n']
141
lines.extend(rio.to_patch_lines(stanza))
145
def write_to_directory(self, path):
146
"""Write this merge directive to a series of files in a directory.
148
:param path: Filesystem path to write to
150
raise NotImplementedError(self.write_to_directory)
153
def from_objects(klass, repository, revision_id, time, timezone,
154
target_branch, patch_type='bundle',
155
local_target_branch=None, public_branch=None, message=None):
156
"""Generate a merge directive from various objects
158
:param repository: The repository containing the revision
159
:param revision_id: The revision to merge
160
:param time: The POSIX timestamp of the date the request was issued.
161
:param timezone: The timezone of the request
162
:param target_branch: The url of the branch to merge into
163
:param patch_type: 'bundle', 'diff' or None, depending on the type of
165
:param local_target_branch: a local copy of the target branch
166
:param public_branch: location of a public branch containing the target
168
:param message: Message to use when committing the merge
169
:return: The merge directive
171
The public branch is always used if supplied. If the patch_type is
172
not 'bundle', the public branch must be supplied, and will be verified.
174
If the message is not supplied, the message from revision_id will be
177
t_revision_id = revision_id
178
if revision_id == _mod_revision.NULL_REVISION:
180
t = testament.StrictTestament3.from_revision(repository, t_revision_id)
181
submit_branch = _mod_branch.Branch.open(target_branch)
182
if submit_branch.get_public_branch() is not None:
183
target_branch = submit_branch.get_public_branch()
184
if patch_type is None:
187
submit_revision_id = submit_branch.last_revision()
188
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
189
repository.fetch(submit_branch.repository, submit_revision_id)
190
graph = repository.get_graph()
191
ancestor_id = graph.find_unique_lca(revision_id,
193
type_handler = {'bundle': klass._generate_bundle,
194
'diff': klass._generate_diff,
195
None: lambda x, y, z: None }
196
patch = type_handler[patch_type](repository, revision_id,
199
if public_branch is not None and patch_type != 'bundle':
200
public_branch_obj = _mod_branch.Branch.open(public_branch)
201
if not public_branch_obj.repository.has_revision(revision_id):
202
raise errors.PublicBranchOutOfDate(public_branch,
205
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
206
patch, patch_type, public_branch, message)
208
def get_disk_name(self, branch):
209
"""Generate a suitable basename for storing this directive on disk
211
:param branch: The Branch this merge directive was generated fro
214
revno, revision_id = branch.last_revision_info()
215
if self.revision_id == revision_id:
218
revno = branch.get_revision_id_to_revno_map().get(self.revision_id,
220
nick = re.sub('(\W+)', '-', branch.nick).strip('-')
221
return '%s-%s' % (nick, '.'.join(str(n) for n in revno))
224
def _generate_diff(repository, revision_id, ancestor_id):
225
tree_1 = repository.revision_tree(ancestor_id)
226
tree_2 = repository.revision_tree(revision_id)
228
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
232
def _generate_bundle(repository, revision_id, ancestor_id):
234
bundle_serializer.write_bundle(repository, revision_id,
238
def to_signed(self, branch):
239
"""Serialize as a signed string.
241
:param branch: The source branch, to get the signing strategy
244
my_gpg = gpg.GPGStrategy(branch.get_config())
245
return my_gpg.sign(''.join(self.to_lines()))
247
def to_email(self, mail_to, branch, sign=False):
248
"""Serialize as an email message.
250
:param mail_to: The address to mail the message to
251
:param branch: The source branch, to get the signing strategy and
253
:param sign: If True, gpg-sign the email
254
:return: an email message
256
mail_from = branch.get_config().username()
257
if self.message is not None:
258
subject = self.message
260
revision = branch.repository.get_revision(self.revision_id)
261
subject = revision.message
263
body = self.to_signed(branch)
265
body = ''.join(self.to_lines())
266
message = EmailMessage(mail_from, mail_to, subject, body)
269
def install_revisions(self, target_repo):
270
"""Install revisions and return the target revision"""
271
if not target_repo.has_revision(self.revision_id):
272
if self.patch_type == 'bundle':
273
info = bundle_serializer.read_bundle(
274
StringIO(self.get_raw_bundle()))
275
# We don't use the bundle's target revision, because
276
# MergeDirective.revision_id is authoritative.
278
info.install_revisions(target_repo, stream_input=False)
279
except errors.RevisionNotPresent:
280
# At least one dependency isn't present. Try installing
281
# missing revisions from the submit branch
284
_mod_branch.Branch.open(self.target_branch)
285
except errors.NotBranchError:
286
raise errors.TargetNotBranch(self.target_branch)
287
missing_revisions = []
288
bundle_revisions = set(r.revision_id for r in
290
for revision in info.real_revisions:
291
for parent_id in revision.parent_ids:
292
if (parent_id not in bundle_revisions and
293
not target_repo.has_revision(parent_id)):
294
missing_revisions.append(parent_id)
295
# reverse missing revisions to try to get heads first
297
unique_missing_set = set()
298
for revision in reversed(missing_revisions):
299
if revision in unique_missing_set:
301
unique_missing.append(revision)
302
unique_missing_set.add(revision)
303
for missing_revision in unique_missing:
304
target_repo.fetch(submit_branch.repository,
306
info.install_revisions(target_repo, stream_input=False)
308
source_branch = _mod_branch.Branch.open(self.source_branch)
309
target_repo.fetch(source_branch.repository, self.revision_id)
310
return self.revision_id
312
def compose_merge_request(self, mail_client, to, body, branch, tree=None):
313
"""Compose a request to merge this directive.
315
:param mail_client: The mail client to use for composing this request.
316
:param to: The address to compose the request to.
317
:param branch: The Branch that was used to produce this directive.
318
:param tree: The Tree (if any) for the Branch used to produce this
321
basename = self.get_disk_name(branch)
323
if self.message is not None:
324
subject += self.message
326
revision = branch.repository.get_revision(self.revision_id)
327
subject += revision.get_summary()
328
if getattr(mail_client, 'supports_body', False):
330
for hook in self.hooks['merge_request_body']:
331
params = MergeRequestBodyParams(body, orig_body, self,
332
to, basename, subject, branch,
335
elif len(self.hooks['merge_request_body']) > 0:
336
trace.warning('Cannot run merge_request_body hooks because mail'
337
' client %s does not support message bodies.',
338
mail_client.__class__.__name__)
339
mail_client.compose_merge_request(to, subject,
340
''.join(self.to_lines()),
344
class MergeDirective(BaseMergeDirective):
346
"""A request to perform a merge into a branch.
348
Designed to be serialized and mailed. It provides all the information
349
needed to perform a merge automatically, by providing at minimum a revision
350
bundle or the location of a branch.
352
The serialization format is robust against certain common forms of
353
deterioration caused by mailing.
355
The format is also designed to be patch-compatible. If the directive
356
includes a diff or revision bundle, it should be possible to apply it
357
directly using the standard patch program.
360
_format_string = 'Bazaar merge directive format 1'
362
def __init__(self, revision_id, testament_sha1, time, timezone,
363
target_branch, patch=None, patch_type=None,
364
source_branch=None, message=None, bundle=None):
367
:param revision_id: The revision to merge
368
:param testament_sha1: The sha1 of the testament of the revision to
370
:param time: The current POSIX timestamp time
371
:param timezone: The timezone offset
372
:param target_branch: The branch to apply the merge to
373
:param patch: The text of a diff or bundle
374
:param patch_type: None, "diff" or "bundle", depending on the contents
376
:param source_branch: A public location to merge the revision from
377
:param message: The message to use when committing this merge
379
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
380
timezone, target_branch, patch, source_branch, message)
381
if patch_type not in (None, 'diff', 'bundle'):
382
raise ValueError(patch_type)
383
if patch_type != 'bundle' and source_branch is None:
384
raise errors.NoMergeSource()
385
if patch_type is not None and patch is None:
386
raise errors.PatchMissing(patch_type)
387
self.patch_type = patch_type
389
def clear_payload(self):
391
self.patch_type = None
393
def get_raw_bundle(self):
397
if self.patch_type == 'bundle':
402
bundle = property(_bundle)
405
def from_lines(klass, lines):
406
"""Deserialize a MergeRequest from an iterable of lines
408
:param lines: An iterable of lines
409
:return: a MergeRequest
411
line_iter = iter(lines)
413
for line in line_iter:
414
if line.startswith('# Bazaar merge directive format '):
415
return _format_registry.get(line[2:].rstrip())._from_lines(
417
firstline = firstline or line.strip()
418
raise errors.NotAMergeDirective(firstline)
421
def _from_lines(klass, line_iter):
422
stanza = rio.read_patch_stanza(line_iter)
423
patch_lines = list(line_iter)
424
if len(patch_lines) == 0:
428
patch = ''.join(patch_lines)
430
bundle_serializer.read_bundle(StringIO(patch))
431
except (errors.NotABundle, errors.BundleNotSupported,
435
patch_type = 'bundle'
436
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
438
for key in ('revision_id', 'testament_sha1', 'target_branch',
439
'source_branch', 'message'):
441
kwargs[key] = stanza.get(key)
444
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
445
return MergeDirective(time=time, timezone=timezone,
446
patch_type=patch_type, patch=patch, **kwargs)
449
lines = self._to_lines()
450
if self.patch is not None:
451
lines.extend(self.patch.splitlines(True))
455
def _generate_bundle(repository, revision_id, ancestor_id):
457
bundle_serializer.write_bundle(repository, revision_id,
458
ancestor_id, s, '0.9')
461
def get_merge_request(self, repository):
462
"""Provide data for performing a merge
464
Returns suggested base, suggested target, and patch verification status
466
return None, self.revision_id, 'inapplicable'
469
class MergeDirective2(BaseMergeDirective):
471
_format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
473
def __init__(self, revision_id, testament_sha1, time, timezone,
474
target_branch, patch=None, source_branch=None, message=None,
475
bundle=None, base_revision_id=None):
476
if source_branch is None and bundle is None:
477
raise errors.NoMergeSource()
478
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
479
timezone, target_branch, patch, source_branch, message)
481
self.base_revision_id = base_revision_id
483
def _patch_type(self):
484
if self.bundle is not None:
486
elif self.patch is not None:
491
patch_type = property(_patch_type)
493
def clear_payload(self):
497
def get_raw_bundle(self):
498
if self.bundle is None:
501
return self.bundle.decode('base-64')
504
def _from_lines(klass, line_iter):
505
stanza = rio.read_patch_stanza(line_iter)
509
start = line_iter.next()
510
except StopIteration:
513
if start.startswith('# Begin patch'):
515
for line in line_iter:
516
if line.startswith('# Begin bundle'):
519
patch_lines.append(line)
522
patch = ''.join(patch_lines)
523
if start is not None:
524
if start.startswith('# Begin bundle'):
525
bundle = ''.join(line_iter)
527
raise errors.IllegalMergeDirectivePayload(start)
528
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
530
for key in ('revision_id', 'testament_sha1', 'target_branch',
531
'source_branch', 'message', 'base_revision_id'):
533
kwargs[key] = stanza.get(key)
536
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
537
kwargs['base_revision_id'] =\
538
kwargs['base_revision_id'].encode('utf-8')
539
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
543
lines = self._to_lines(base_revision=True)
544
if self.patch is not None:
545
lines.append('# Begin patch\n')
546
lines.extend(self.patch.splitlines(True))
547
if self.bundle is not None:
548
lines.append('# Begin bundle\n')
549
lines.extend(self.bundle.splitlines(True))
553
def from_objects(klass, repository, revision_id, time, timezone,
554
target_branch, include_patch=True, include_bundle=True,
555
local_target_branch=None, public_branch=None, message=None,
556
base_revision_id=None):
557
"""Generate a merge directive from various objects
559
:param repository: The repository containing the revision
560
:param revision_id: The revision to merge
561
:param time: The POSIX timestamp of the date the request was issued.
562
:param timezone: The timezone of the request
563
:param target_branch: The url of the branch to merge into
564
:param include_patch: If true, include a preview patch
565
:param include_bundle: If true, include a bundle
566
:param local_target_branch: a local copy of the target branch
567
:param public_branch: location of a public branch containing the target
569
:param message: Message to use when committing the merge
570
:return: The merge directive
572
The public branch is always used if supplied. If no bundle is
573
included, the public branch must be supplied, and will be verified.
575
If the message is not supplied, the message from revision_id will be
580
repository.lock_write()
581
locked.append(repository)
582
t_revision_id = revision_id
583
if revision_id == 'null:':
585
t = testament.StrictTestament3.from_revision(repository,
587
submit_branch = _mod_branch.Branch.open(target_branch)
588
submit_branch.lock_read()
589
locked.append(submit_branch)
590
if submit_branch.get_public_branch() is not None:
591
target_branch = submit_branch.get_public_branch()
592
submit_revision_id = submit_branch.last_revision()
593
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
594
graph = repository.get_graph(submit_branch.repository)
595
ancestor_id = graph.find_unique_lca(revision_id,
597
if base_revision_id is None:
598
base_revision_id = ancestor_id
599
if (include_patch, include_bundle) != (False, False):
600
repository.fetch(submit_branch.repository, submit_revision_id)
602
patch = klass._generate_diff(repository, revision_id,
608
bundle = klass._generate_bundle(repository, revision_id,
609
ancestor_id).encode('base-64')
613
if public_branch is not None and not include_bundle:
614
public_branch_obj = _mod_branch.Branch.open(public_branch)
615
public_branch_obj.lock_read()
616
locked.append(public_branch_obj)
617
if not public_branch_obj.repository.has_revision(
619
raise errors.PublicBranchOutOfDate(public_branch,
621
testament_sha1 = t.as_sha1()
623
for entry in reversed(locked):
625
return klass(revision_id, testament_sha1, time, timezone,
626
target_branch, patch, public_branch, message, bundle,
629
def _verify_patch(self, repository):
630
calculated_patch = self._generate_diff(repository, self.revision_id,
631
self.base_revision_id)
632
# Convert line-endings to UNIX
633
stored_patch = re.sub('\r\n?', '\n', self.patch)
634
calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
635
# Strip trailing whitespace
636
calculated_patch = re.sub(' *\n', '\n', calculated_patch)
637
stored_patch = re.sub(' *\n', '\n', stored_patch)
638
return (calculated_patch == stored_patch)
640
def get_merge_request(self, repository):
641
"""Provide data for performing a merge
643
Returns suggested base, suggested target, and patch verification status
645
verified = self._maybe_verify(repository)
646
return self.base_revision_id, self.revision_id, verified
648
def _maybe_verify(self, repository):
649
if self.patch is not None:
650
if self._verify_patch(repository):
655
return 'inapplicable'
658
class MergeDirectiveFormatRegistry(registry.Registry):
660
def register(self, directive, format_string=None):
661
if format_string is None:
662
format_string = directive._format_string
663
registry.Registry.register(self, format_string, directive)
666
_format_registry = MergeDirectiveFormatRegistry()
667
_format_registry.register(MergeDirective)
668
_format_registry.register(MergeDirective2)
669
# 0.19 never existed. It got renamed to 0.90. But by that point, there were
670
# already merge directives in the wild that used 0.19. Registering with the old
671
# format string to retain compatibility with those merge directives.
672
_format_registry.register(MergeDirective2,
673
'Bazaar merge directive format 2 (Bazaar 0.19)')