1
# Copyright (C) 2007 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,
27
revision as _mod_revision,
32
from bzrlib.bundle import (
33
serializer as bundle_serializer,
35
from bzrlib.email_message import EmailMessage
38
class _BaseMergeDirective(object):
40
def __init__(self, revision_id, testament_sha1, time, timezone,
41
target_branch, patch=None, source_branch=None, message=None,
45
:param revision_id: The revision to merge
46
:param testament_sha1: The sha1 of the testament of the revision to
48
:param time: The current POSIX timestamp time
49
:param timezone: The timezone offset
50
:param target_branch: The branch to apply the merge to
51
:param patch: The text of a diff or bundle
52
:param source_branch: A public location to merge the revision from
53
:param message: The message to use when committing this merge
55
self.revision_id = revision_id
56
self.testament_sha1 = testament_sha1
58
self.timezone = timezone
59
self.target_branch = target_branch
61
self.source_branch = source_branch
62
self.message = message
64
def _to_lines(self, base_revision=False):
65
"""Serialize as a list of lines
67
:return: a list of lines
69
time_str = timestamp.format_patch_date(self.time, self.timezone)
70
stanza = rio.Stanza(revision_id=self.revision_id, timestamp=time_str,
71
target_branch=self.target_branch,
72
testament_sha1=self.testament_sha1)
73
for key in ('source_branch', 'message'):
74
if self.__dict__[key] is not None:
75
stanza.add(key, self.__dict__[key])
77
stanza.add('base_revision_id', self.base_revision_id)
78
lines = ['# ' + self._format_string + '\n']
79
lines.extend(rio.to_patch_lines(stanza))
84
def from_objects(klass, repository, revision_id, time, timezone,
85
target_branch, patch_type='bundle',
86
local_target_branch=None, public_branch=None, message=None):
87
"""Generate a merge directive from various objects
89
:param repository: The repository containing the revision
90
:param revision_id: The revision to merge
91
:param time: The POSIX timestamp of the date the request was issued.
92
:param timezone: The timezone of the request
93
:param target_branch: The url of the branch to merge into
94
:param patch_type: 'bundle', 'diff' or None, depending on the type of
96
:param local_target_branch: a local copy of the target branch
97
:param public_branch: location of a public branch containing the target
99
:param message: Message to use when committing the merge
100
:return: The merge directive
102
The public branch is always used if supplied. If the patch_type is
103
not 'bundle', the public branch must be supplied, and will be verified.
105
If the message is not supplied, the message from revision_id will be
108
t_revision_id = revision_id
109
if revision_id == _mod_revision.NULL_REVISION:
111
t = testament.StrictTestament3.from_revision(repository, t_revision_id)
112
submit_branch = _mod_branch.Branch.open(target_branch)
113
if submit_branch.get_public_branch() is not None:
114
target_branch = submit_branch.get_public_branch()
115
if patch_type is None:
118
submit_revision_id = submit_branch.last_revision()
119
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
120
repository.fetch(submit_branch.repository, submit_revision_id)
121
graph = repository.get_graph()
122
ancestor_id = graph.find_unique_lca(revision_id,
124
type_handler = {'bundle': klass._generate_bundle,
125
'diff': klass._generate_diff,
126
None: lambda x, y, z: None }
127
patch = type_handler[patch_type](repository, revision_id,
130
if public_branch is not None and patch_type != 'bundle':
131
public_branch_obj = _mod_branch.Branch.open(public_branch)
132
if not public_branch_obj.repository.has_revision(revision_id):
133
raise errors.PublicBranchOutOfDate(public_branch,
136
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
137
patch, patch_type, public_branch, message)
139
def get_disk_name(self, branch):
140
"""Generate a suitable basename for storing this directive on disk
142
:param branch: The Branch this merge directive was generated fro
145
revno, revision_id = branch.last_revision_info()
146
if self.revision_id == revision_id:
149
revno = branch.get_revision_id_to_revno_map().get(self.revision_id,
151
nick = re.sub('(\W+)', '-', branch.nick).strip('-')
152
return '%s-%s' % (nick, '.'.join(str(n) for n in revno))
155
def _generate_diff(repository, revision_id, ancestor_id):
156
tree_1 = repository.revision_tree(ancestor_id)
157
tree_2 = repository.revision_tree(revision_id)
159
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
163
def _generate_bundle(repository, revision_id, ancestor_id):
165
bundle_serializer.write_bundle(repository, revision_id,
169
def to_signed(self, branch):
170
"""Serialize as a signed string.
172
:param branch: The source branch, to get the signing strategy
175
my_gpg = gpg.GPGStrategy(branch.get_config())
176
return my_gpg.sign(''.join(self.to_lines()))
178
def to_email(self, mail_to, branch, sign=False):
179
"""Serialize as an email message.
181
:param mail_to: The address to mail the message to
182
:param branch: The source branch, to get the signing strategy and
184
:param sign: If True, gpg-sign the email
185
:return: an email message
187
mail_from = branch.get_config().username()
188
if self.message is not None:
189
subject = self.message
191
revision = branch.repository.get_revision(self.revision_id)
192
subject = revision.message
194
body = self.to_signed(branch)
196
body = ''.join(self.to_lines())
197
message = EmailMessage(mail_from, mail_to, subject, body)
200
def install_revisions(self, target_repo):
201
"""Install revisions and return the target revision"""
202
if not target_repo.has_revision(self.revision_id):
203
if self.patch_type == 'bundle':
204
info = bundle_serializer.read_bundle(
205
StringIO(self.get_raw_bundle()))
206
# We don't use the bundle's target revision, because
207
# MergeDirective.revision_id is authoritative.
209
info.install_revisions(target_repo, stream_input=False)
210
except errors.RevisionNotPresent:
211
# At least one dependency isn't present. Try installing
212
# missing revisions from the submit branch
215
_mod_branch.Branch.open(self.target_branch)
216
except errors.NotBranchError:
217
raise errors.TargetNotBranch(self.target_branch)
218
missing_revisions = []
219
bundle_revisions = set(r.revision_id for r in
221
for revision in info.real_revisions:
222
for parent_id in revision.parent_ids:
223
if (parent_id not in bundle_revisions and
224
not target_repo.has_revision(parent_id)):
225
missing_revisions.append(parent_id)
226
# reverse missing revisions to try to get heads first
228
unique_missing_set = set()
229
for revision in reversed(missing_revisions):
230
if revision in unique_missing_set:
232
unique_missing.append(revision)
233
unique_missing_set.add(revision)
234
for missing_revision in unique_missing:
235
target_repo.fetch(submit_branch.repository,
237
info.install_revisions(target_repo, stream_input=False)
239
source_branch = _mod_branch.Branch.open(self.source_branch)
240
target_repo.fetch(source_branch.repository, self.revision_id)
241
return self.revision_id
244
class MergeDirective(_BaseMergeDirective):
246
"""A request to perform a merge into a branch.
248
Designed to be serialized and mailed. It provides all the information
249
needed to perform a merge automatically, by providing at minimum a revision
250
bundle or the location of a branch.
252
The serialization format is robust against certain common forms of
253
deterioration caused by mailing.
255
The format is also designed to be patch-compatible. If the directive
256
includes a diff or revision bundle, it should be possible to apply it
257
directly using the standard patch program.
260
_format_string = 'Bazaar merge directive format 1'
262
def __init__(self, revision_id, testament_sha1, time, timezone,
263
target_branch, patch=None, patch_type=None,
264
source_branch=None, message=None, bundle=None):
267
:param revision_id: The revision to merge
268
:param testament_sha1: The sha1 of the testament of the revision to
270
:param time: The current POSIX timestamp time
271
:param timezone: The timezone offset
272
:param target_branch: The branch to apply the merge to
273
:param patch: The text of a diff or bundle
274
:param patch_type: None, "diff" or "bundle", depending on the contents
276
:param source_branch: A public location to merge the revision from
277
:param message: The message to use when committing this merge
279
_BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
280
timezone, target_branch, patch, source_branch, message)
281
if patch_type not in (None, 'diff', 'bundle'):
282
raise ValueError(patch_type)
283
if patch_type != 'bundle' and source_branch is None:
284
raise errors.NoMergeSource()
285
if patch_type is not None and patch is None:
286
raise errors.PatchMissing(patch_type)
287
self.patch_type = patch_type
289
def clear_payload(self):
291
self.patch_type = None
293
def get_raw_bundle(self):
297
if self.patch_type == 'bundle':
302
bundle = property(_bundle)
305
def from_lines(klass, lines):
306
"""Deserialize a MergeRequest from an iterable of lines
308
:param lines: An iterable of lines
309
:return: a MergeRequest
311
line_iter = iter(lines)
312
for line in line_iter:
313
if line.startswith('# Bazaar merge directive format '):
317
raise errors.NotAMergeDirective(lines[0])
319
raise errors.NotAMergeDirective('')
320
return _format_registry.get(line[2:].rstrip())._from_lines(line_iter)
323
def _from_lines(klass, line_iter):
324
stanza = rio.read_patch_stanza(line_iter)
325
patch_lines = list(line_iter)
326
if len(patch_lines) == 0:
330
patch = ''.join(patch_lines)
332
bundle_serializer.read_bundle(StringIO(patch))
333
except (errors.NotABundle, errors.BundleNotSupported,
337
patch_type = 'bundle'
338
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
340
for key in ('revision_id', 'testament_sha1', 'target_branch',
341
'source_branch', 'message'):
343
kwargs[key] = stanza.get(key)
346
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
347
return MergeDirective(time=time, timezone=timezone,
348
patch_type=patch_type, patch=patch, **kwargs)
351
lines = self._to_lines()
352
if self.patch is not None:
353
lines.extend(self.patch.splitlines(True))
357
def _generate_bundle(repository, revision_id, ancestor_id):
359
bundle_serializer.write_bundle(repository, revision_id,
360
ancestor_id, s, '0.9')
363
def get_merge_request(self, repository):
364
"""Provide data for performing a merge
366
Returns suggested base, suggested target, and patch verification status
368
return None, self.revision_id, 'inapplicable'
371
class MergeDirective2(_BaseMergeDirective):
373
_format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
375
def __init__(self, revision_id, testament_sha1, time, timezone,
376
target_branch, patch=None, source_branch=None, message=None,
377
bundle=None, base_revision_id=None):
378
if source_branch is None and bundle is None:
379
raise errors.NoMergeSource()
380
_BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
381
timezone, target_branch, patch, source_branch, message)
383
self.base_revision_id = base_revision_id
385
def _patch_type(self):
386
if self.bundle is not None:
388
elif self.patch is not None:
393
patch_type = property(_patch_type)
395
def clear_payload(self):
399
def get_raw_bundle(self):
400
if self.bundle is None:
403
return self.bundle.decode('base-64')
406
def _from_lines(klass, line_iter):
407
stanza = rio.read_patch_stanza(line_iter)
411
start = line_iter.next()
412
except StopIteration:
415
if start.startswith('# Begin patch'):
417
for line in line_iter:
418
if line.startswith('# Begin bundle'):
421
patch_lines.append(line)
424
patch = ''.join(patch_lines)
425
if start is not None:
426
if start.startswith('# Begin bundle'):
427
bundle = ''.join(line_iter)
429
raise errors.IllegalMergeDirectivePayload(start)
430
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
432
for key in ('revision_id', 'testament_sha1', 'target_branch',
433
'source_branch', 'message', 'base_revision_id'):
435
kwargs[key] = stanza.get(key)
438
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
439
kwargs['base_revision_id'] =\
440
kwargs['base_revision_id'].encode('utf-8')
441
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
445
lines = self._to_lines(base_revision=True)
446
if self.patch is not None:
447
lines.append('# Begin patch\n')
448
lines.extend(self.patch.splitlines(True))
449
if self.bundle is not None:
450
lines.append('# Begin bundle\n')
451
lines.extend(self.bundle.splitlines(True))
455
def from_objects(klass, repository, revision_id, time, timezone,
456
target_branch, include_patch=True, include_bundle=True,
457
local_target_branch=None, public_branch=None, message=None,
458
base_revision_id=None):
459
"""Generate a merge directive from various objects
461
:param repository: The repository containing the revision
462
:param revision_id: The revision to merge
463
:param time: The POSIX timestamp of the date the request was issued.
464
:param timezone: The timezone of the request
465
:param target_branch: The url of the branch to merge into
466
:param include_patch: If true, include a preview patch
467
:param include_bundle: If true, include a bundle
468
:param local_target_branch: a local copy of the target branch
469
:param public_branch: location of a public branch containing the target
471
:param message: Message to use when committing the merge
472
:return: The merge directive
474
The public branch is always used if supplied. If no bundle is
475
included, the public branch must be supplied, and will be verified.
477
If the message is not supplied, the message from revision_id will be
482
repository.lock_write()
483
locked.append(repository)
484
t_revision_id = revision_id
485
if revision_id == 'null:':
487
t = testament.StrictTestament3.from_revision(repository,
489
submit_branch = _mod_branch.Branch.open(target_branch)
490
submit_branch.lock_read()
491
locked.append(submit_branch)
492
if submit_branch.get_public_branch() is not None:
493
target_branch = submit_branch.get_public_branch()
494
submit_revision_id = submit_branch.last_revision()
495
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
496
graph = repository.get_graph(submit_branch.repository)
497
ancestor_id = graph.find_unique_lca(revision_id,
499
if base_revision_id is None:
500
base_revision_id = ancestor_id
501
if (include_patch, include_bundle) != (False, False):
502
repository.fetch(submit_branch.repository, submit_revision_id)
504
patch = klass._generate_diff(repository, revision_id,
510
bundle = klass._generate_bundle(repository, revision_id,
511
ancestor_id).encode('base-64')
515
if public_branch is not None and not include_bundle:
516
public_branch_obj = _mod_branch.Branch.open(public_branch)
517
public_branch_obj.lock_read()
518
locked.append(public_branch_obj)
519
if not public_branch_obj.repository.has_revision(
521
raise errors.PublicBranchOutOfDate(public_branch,
524
for entry in reversed(locked):
526
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
527
patch, public_branch, message, bundle, base_revision_id)
529
def _verify_patch(self, repository):
530
calculated_patch = self._generate_diff(repository, self.revision_id,
531
self.base_revision_id)
532
# Convert line-endings to UNIX
533
stored_patch = re.sub('\r\n?', '\n', self.patch)
534
calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
535
# Strip trailing whitespace
536
calculated_patch = re.sub(' *\n', '\n', calculated_patch)
537
stored_patch = re.sub(' *\n', '\n', stored_patch)
538
return (calculated_patch == stored_patch)
540
def get_merge_request(self, repository):
541
"""Provide data for performing a merge
543
Returns suggested base, suggested target, and patch verification status
545
verified = self._maybe_verify(repository)
546
return self.base_revision_id, self.revision_id, verified
548
def _maybe_verify(self, repository):
549
if self.patch is not None:
550
if self._verify_patch(repository):
555
return 'inapplicable'
558
class MergeDirectiveFormatRegistry(registry.Registry):
560
def register(self, directive, format_string=None):
561
if format_string is None:
562
format_string = directive._format_string
563
registry.Registry.register(self, format_string, directive)
566
_format_registry = MergeDirectiveFormatRegistry()
567
_format_registry.register(MergeDirective)
568
_format_registry.register(MergeDirective2)
569
_format_registry.register(MergeDirective2,
570
'Bazaar merge directive format 2 (Bazaar 0.19)')