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, 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 = email_message.EmailMessage(mail_from, mail_to, subject,
273
def install_revisions(self, target_repo):
274
"""Install revisions and return the target revision"""
275
if not target_repo.has_revision(self.revision_id):
276
if self.patch_type == 'bundle':
277
info = bundle_serializer.read_bundle(
278
StringIO(self.get_raw_bundle()))
279
# We don't use the bundle's target revision, because
280
# 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)
312
source_branch = _mod_branch.Branch.open(self.source_branch)
313
target_repo.fetch(source_branch.repository, self.revision_id)
314
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)')