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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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
213
submit_branch = _mod_branch.Branch.open(self.target_branch)
214
missing_revisions = []
215
bundle_revisions = set(r.revision_id for r in
217
for revision in info.real_revisions:
218
for parent_id in revision.parent_ids:
219
if (parent_id not in bundle_revisions and
220
not target_repo.has_revision(parent_id)):
221
missing_revisions.append(parent_id)
222
# reverse missing revisions to try to get heads first
224
unique_missing_set = set()
225
for revision in reversed(missing_revisions):
226
if revision in unique_missing_set:
228
unique_missing.append(revision)
229
unique_missing_set.add(revision)
230
for missing_revision in unique_missing:
231
target_repo.fetch(submit_branch.repository,
233
info.install_revisions(target_repo, stream_input=False)
235
source_branch = _mod_branch.Branch.open(self.source_branch)
236
target_repo.fetch(source_branch.repository, self.revision_id)
237
return self.revision_id
240
class MergeDirective(_BaseMergeDirective):
242
"""A request to perform a merge into a branch.
244
Designed to be serialized and mailed. It provides all the information
245
needed to perform a merge automatically, by providing at minimum a revision
246
bundle or the location of a branch.
248
The serialization format is robust against certain common forms of
249
deterioration caused by mailing.
251
The format is also designed to be patch-compatible. If the directive
252
includes a diff or revision bundle, it should be possible to apply it
253
directly using the standard patch program.
256
_format_string = 'Bazaar merge directive format 1'
258
def __init__(self, revision_id, testament_sha1, time, timezone,
259
target_branch, patch=None, patch_type=None,
260
source_branch=None, message=None, bundle=None):
263
:param revision_id: The revision to merge
264
:param testament_sha1: The sha1 of the testament of the revision to
266
:param time: The current POSIX timestamp time
267
:param timezone: The timezone offset
268
:param target_branch: The branch to apply the merge to
269
:param patch: The text of a diff or bundle
270
:param patch_type: None, "diff" or "bundle", depending on the contents
272
:param source_branch: A public location to merge the revision from
273
:param message: The message to use when committing this merge
275
_BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
276
timezone, target_branch, patch, source_branch, message)
277
if patch_type not in (None, 'diff', 'bundle'):
278
raise ValueError(patch_type)
279
if patch_type != 'bundle' and source_branch is None:
280
raise errors.NoMergeSource()
281
if patch_type is not None and patch is None:
282
raise errors.PatchMissing(patch_type)
283
self.patch_type = patch_type
285
def clear_payload(self):
287
self.patch_type = None
289
def get_raw_bundle(self):
293
if self.patch_type == 'bundle':
298
bundle = property(_bundle)
301
def from_lines(klass, lines):
302
"""Deserialize a MergeRequest from an iterable of lines
304
:param lines: An iterable of lines
305
:return: a MergeRequest
307
line_iter = iter(lines)
308
for line in line_iter:
309
if line.startswith('# Bazaar merge directive format '):
313
raise errors.NotAMergeDirective(lines[0])
315
raise errors.NotAMergeDirective('')
316
return _format_registry.get(line[2:].rstrip())._from_lines(line_iter)
319
def _from_lines(klass, line_iter):
320
stanza = rio.read_patch_stanza(line_iter)
321
patch_lines = list(line_iter)
322
if len(patch_lines) == 0:
326
patch = ''.join(patch_lines)
328
bundle_serializer.read_bundle(StringIO(patch))
329
except (errors.NotABundle, errors.BundleNotSupported,
333
patch_type = 'bundle'
334
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
336
for key in ('revision_id', 'testament_sha1', 'target_branch',
337
'source_branch', 'message'):
339
kwargs[key] = stanza.get(key)
342
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
343
return MergeDirective(time=time, timezone=timezone,
344
patch_type=patch_type, patch=patch, **kwargs)
347
lines = self._to_lines()
348
if self.patch is not None:
349
lines.extend(self.patch.splitlines(True))
353
def _generate_bundle(repository, revision_id, ancestor_id):
355
bundle_serializer.write_bundle(repository, revision_id,
356
ancestor_id, s, '0.9')
359
def get_merge_request(self, repository):
360
"""Provide data for performing a merge
362
Returns suggested base, suggested target, and patch verification status
364
return None, self.revision_id, 'inapplicable'
367
class MergeDirective2(_BaseMergeDirective):
369
_format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
371
def __init__(self, revision_id, testament_sha1, time, timezone,
372
target_branch, patch=None, source_branch=None, message=None,
373
bundle=None, base_revision_id=None):
374
if source_branch is None and bundle is None:
375
raise errors.NoMergeSource()
376
_BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
377
timezone, target_branch, patch, source_branch, message)
379
self.base_revision_id = base_revision_id
381
def _patch_type(self):
382
if self.bundle is not None:
384
elif self.patch is not None:
389
patch_type = property(_patch_type)
391
def clear_payload(self):
395
def get_raw_bundle(self):
396
if self.bundle is None:
399
return self.bundle.decode('base-64')
402
def _from_lines(klass, line_iter):
403
stanza = rio.read_patch_stanza(line_iter)
407
start = line_iter.next()
408
except StopIteration:
411
if start.startswith('# Begin patch'):
413
for line in line_iter:
414
if line.startswith('# Begin bundle'):
417
patch_lines.append(line)
420
patch = ''.join(patch_lines)
421
if start is not None:
422
if start.startswith('# Begin bundle'):
423
bundle = ''.join(line_iter)
425
raise errors.IllegalMergeDirectivePayload(start)
426
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
428
for key in ('revision_id', 'testament_sha1', 'target_branch',
429
'source_branch', 'message', 'base_revision_id'):
431
kwargs[key] = stanza.get(key)
434
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
435
kwargs['base_revision_id'] =\
436
kwargs['base_revision_id'].encode('utf-8')
437
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
441
lines = self._to_lines(base_revision=True)
442
if self.patch is not None:
443
lines.append('# Begin patch\n')
444
lines.extend(self.patch.splitlines(True))
445
if self.bundle is not None:
446
lines.append('# Begin bundle\n')
447
lines.extend(self.bundle.splitlines(True))
451
def from_objects(klass, repository, revision_id, time, timezone,
452
target_branch, include_patch=True, include_bundle=True,
453
local_target_branch=None, public_branch=None, message=None,
454
base_revision_id=None):
455
"""Generate a merge directive from various objects
457
:param repository: The repository containing the revision
458
:param revision_id: The revision to merge
459
:param time: The POSIX timestamp of the date the request was issued.
460
:param timezone: The timezone of the request
461
:param target_branch: The url of the branch to merge into
462
:param include_patch: If true, include a preview patch
463
:param include_bundle: If true, include a bundle
464
:param local_target_branch: a local copy of the target branch
465
:param public_branch: location of a public branch containing the target
467
:param message: Message to use when committing the merge
468
:return: The merge directive
470
The public branch is always used if supplied. If no bundle is
471
included, the public branch must be supplied, and will be verified.
473
If the message is not supplied, the message from revision_id will be
478
repository.lock_write()
479
locked.append(repository)
480
t_revision_id = revision_id
481
if revision_id == 'null:':
483
t = testament.StrictTestament3.from_revision(repository,
485
submit_branch = _mod_branch.Branch.open(target_branch)
486
submit_branch.lock_read()
487
locked.append(submit_branch)
488
if submit_branch.get_public_branch() is not None:
489
target_branch = submit_branch.get_public_branch()
490
submit_revision_id = submit_branch.last_revision()
491
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
492
graph = repository.get_graph(submit_branch.repository)
493
ancestor_id = graph.find_unique_lca(revision_id,
495
if base_revision_id is None:
496
base_revision_id = ancestor_id
497
if (include_patch, include_bundle) != (False, False):
498
repository.fetch(submit_branch.repository, submit_revision_id)
500
patch = klass._generate_diff(repository, revision_id,
506
bundle = klass._generate_bundle(repository, revision_id,
507
ancestor_id).encode('base-64')
511
if public_branch is not None and not include_bundle:
512
public_branch_obj = _mod_branch.Branch.open(public_branch)
513
public_branch_obj.lock_read()
514
locked.append(public_branch_obj)
515
if not public_branch_obj.repository.has_revision(
517
raise errors.PublicBranchOutOfDate(public_branch,
520
for entry in reversed(locked):
522
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
523
patch, public_branch, message, bundle, base_revision_id)
525
def _verify_patch(self, repository):
526
calculated_patch = self._generate_diff(repository, self.revision_id,
527
self.base_revision_id)
528
# Convert line-endings to UNIX
529
stored_patch = re.sub('\r\n?', '\n', self.patch)
530
calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
531
# Strip trailing whitespace
532
calculated_patch = re.sub(' *\n', '\n', calculated_patch)
533
stored_patch = re.sub(' *\n', '\n', stored_patch)
534
return (calculated_patch == stored_patch)
536
def get_merge_request(self, repository):
537
"""Provide data for performing a merge
539
Returns suggested base, suggested target, and patch verification status
541
verified = self._maybe_verify(repository)
542
return self.base_revision_id, self.revision_id, verified
544
def _maybe_verify(self, repository):
545
if self.patch is not None:
546
if self._verify_patch(repository):
551
return 'inapplicable'
554
class MergeDirectiveFormatRegistry(registry.Registry):
556
def register(self, directive, format_string=None):
557
if format_string is None:
558
format_string = directive._format_string
559
registry.Registry.register(self, format_string, directive)
562
_format_registry = MergeDirectiveFormatRegistry()
563
_format_registry.register(MergeDirective)
564
_format_registry.register(MergeDirective2)
565
_format_registry.register(MergeDirective2,
566
'Bazaar merge directive format 2 (Bazaar 0.19)')