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
return '%s-%s' % (branch.nick, '.'.join(str(n) for n in revno))
154
def _generate_diff(repository, revision_id, ancestor_id):
155
tree_1 = repository.revision_tree(ancestor_id)
156
tree_2 = repository.revision_tree(revision_id)
158
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
162
def _generate_bundle(repository, revision_id, ancestor_id):
164
bundle_serializer.write_bundle(repository, revision_id,
168
def to_signed(self, branch):
169
"""Serialize as a signed string.
171
:param branch: The source branch, to get the signing strategy
174
my_gpg = gpg.GPGStrategy(branch.get_config())
175
return my_gpg.sign(''.join(self.to_lines()))
177
def to_email(self, mail_to, branch, sign=False):
178
"""Serialize as an email message.
180
:param mail_to: The address to mail the message to
181
:param branch: The source branch, to get the signing strategy and
183
:param sign: If True, gpg-sign the email
184
:return: an email message
186
mail_from = branch.get_config().username()
187
if self.message is not None:
188
subject = self.message
190
revision = branch.repository.get_revision(self.revision_id)
191
subject = revision.message
193
body = self.to_signed(branch)
195
body = ''.join(self.to_lines())
196
message = EmailMessage(mail_from, mail_to, subject, body)
199
def install_revisions(self, target_repo):
200
"""Install revisions and return the target revision"""
201
if not target_repo.has_revision(self.revision_id):
202
if self.patch_type == 'bundle':
203
info = bundle_serializer.read_bundle(
204
StringIO(self.get_raw_bundle()))
205
# We don't use the bundle's target revision, because
206
# MergeDirective.revision_id is authoritative.
208
info.install_revisions(target_repo, stream_input=False)
209
except errors.RevisionNotPresent:
210
# At least one dependency isn't present. Try installing
211
# missing revisions from the submit branch
212
submit_branch = _mod_branch.Branch.open(self.target_branch)
213
missing_revisions = []
214
bundle_revisions = set(r.revision_id for r in
216
for revision in info.real_revisions:
217
for parent_id in revision.parent_ids:
218
if (parent_id not in bundle_revisions and
219
not target_repo.has_revision(parent_id)):
220
missing_revisions.append(parent_id)
221
# reverse missing revisions to try to get heads first
223
unique_missing_set = set()
224
for revision in reversed(missing_revisions):
225
if revision in unique_missing_set:
227
unique_missing.append(revision)
228
unique_missing_set.add(revision)
229
for missing_revision in unique_missing:
230
target_repo.fetch(submit_branch.repository,
232
info.install_revisions(target_repo, stream_input=False)
234
source_branch = _mod_branch.Branch.open(self.source_branch)
235
target_repo.fetch(source_branch.repository, self.revision_id)
236
return self.revision_id
239
class MergeDirective(_BaseMergeDirective):
241
"""A request to perform a merge into a branch.
243
Designed to be serialized and mailed. It provides all the information
244
needed to perform a merge automatically, by providing at minimum a revision
245
bundle or the location of a branch.
247
The serialization format is robust against certain common forms of
248
deterioration caused by mailing.
250
The format is also designed to be patch-compatible. If the directive
251
includes a diff or revision bundle, it should be possible to apply it
252
directly using the standard patch program.
255
_format_string = 'Bazaar merge directive format 1'
257
def __init__(self, revision_id, testament_sha1, time, timezone,
258
target_branch, patch=None, patch_type=None,
259
source_branch=None, message=None, bundle=None):
262
:param revision_id: The revision to merge
263
:param testament_sha1: The sha1 of the testament of the revision to
265
:param time: The current POSIX timestamp time
266
:param timezone: The timezone offset
267
:param target_branch: The branch to apply the merge to
268
:param patch: The text of a diff or bundle
269
:param patch_type: None, "diff" or "bundle", depending on the contents
271
:param source_branch: A public location to merge the revision from
272
:param message: The message to use when committing this merge
274
_BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
275
timezone, target_branch, patch, source_branch, message)
276
if patch_type not in (None, 'diff', 'bundle'):
277
raise ValueError(patch_type)
278
if patch_type != 'bundle' and source_branch is None:
279
raise errors.NoMergeSource()
280
if patch_type is not None and patch is None:
281
raise errors.PatchMissing(patch_type)
282
self.patch_type = patch_type
284
def clear_payload(self):
286
self.patch_type = None
288
def get_raw_bundle(self):
292
if self.patch_type == 'bundle':
297
bundle = property(_bundle)
300
def from_lines(klass, lines):
301
"""Deserialize a MergeRequest from an iterable of lines
303
:param lines: An iterable of lines
304
:return: a MergeRequest
306
line_iter = iter(lines)
307
for line in line_iter:
308
if line.startswith('# Bazaar merge directive format '):
312
raise errors.NotAMergeDirective(lines[0])
314
raise errors.NotAMergeDirective('')
315
return _format_registry.get(line[2:].rstrip())._from_lines(line_iter)
318
def _from_lines(klass, line_iter):
319
stanza = rio.read_patch_stanza(line_iter)
320
patch_lines = list(line_iter)
321
if len(patch_lines) == 0:
325
patch = ''.join(patch_lines)
327
bundle_serializer.read_bundle(StringIO(patch))
328
except (errors.NotABundle, errors.BundleNotSupported,
332
patch_type = 'bundle'
333
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
335
for key in ('revision_id', 'testament_sha1', 'target_branch',
336
'source_branch', 'message'):
338
kwargs[key] = stanza.get(key)
341
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
342
return MergeDirective(time=time, timezone=timezone,
343
patch_type=patch_type, patch=patch, **kwargs)
346
lines = self._to_lines()
347
if self.patch is not None:
348
lines.extend(self.patch.splitlines(True))
352
def _generate_bundle(repository, revision_id, ancestor_id):
354
bundle_serializer.write_bundle(repository, revision_id,
355
ancestor_id, s, '0.9')
358
def get_merge_request(self, repository):
359
"""Provide data for performing a merge
361
Returns suggested base, suggested target, and patch verification status
363
return None, self.revision_id, 'inapplicable'
366
class MergeDirective2(_BaseMergeDirective):
368
_format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
370
def __init__(self, revision_id, testament_sha1, time, timezone,
371
target_branch, patch=None, source_branch=None, message=None,
372
bundle=None, base_revision_id=None):
373
if source_branch is None and bundle is None:
374
raise errors.NoMergeSource()
375
_BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
376
timezone, target_branch, patch, source_branch, message)
378
self.base_revision_id = base_revision_id
380
def _patch_type(self):
381
if self.bundle is not None:
383
elif self.patch is not None:
388
patch_type = property(_patch_type)
390
def clear_payload(self):
394
def get_raw_bundle(self):
395
if self.bundle is None:
398
return self.bundle.decode('base-64')
401
def _from_lines(klass, line_iter):
402
stanza = rio.read_patch_stanza(line_iter)
406
start = line_iter.next()
407
except StopIteration:
410
if start.startswith('# Begin patch'):
412
for line in line_iter:
413
if line.startswith('# Begin bundle'):
416
patch_lines.append(line)
419
patch = ''.join(patch_lines)
420
if start is not None:
421
if start.startswith('# Begin bundle'):
422
bundle = ''.join(line_iter)
424
raise errors.IllegalMergeDirectivePayload(start)
425
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
427
for key in ('revision_id', 'testament_sha1', 'target_branch',
428
'source_branch', 'message', 'base_revision_id'):
430
kwargs[key] = stanza.get(key)
433
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
434
kwargs['base_revision_id'] =\
435
kwargs['base_revision_id'].encode('utf-8')
436
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
440
lines = self._to_lines(base_revision=True)
441
if self.patch is not None:
442
lines.append('# Begin patch\n')
443
lines.extend(self.patch.splitlines(True))
444
if self.bundle is not None:
445
lines.append('# Begin bundle\n')
446
lines.extend(self.bundle.splitlines(True))
450
def from_objects(klass, repository, revision_id, time, timezone,
451
target_branch, include_patch=True, include_bundle=True,
452
local_target_branch=None, public_branch=None, message=None,
453
base_revision_id=None):
454
"""Generate a merge directive from various objects
456
:param repository: The repository containing the revision
457
:param revision_id: The revision to merge
458
:param time: The POSIX timestamp of the date the request was issued.
459
:param timezone: The timezone of the request
460
:param target_branch: The url of the branch to merge into
461
:param include_patch: If true, include a preview patch
462
:param include_bundle: If true, include a bundle
463
:param local_target_branch: a local copy of the target branch
464
:param public_branch: location of a public branch containing the target
466
:param message: Message to use when committing the merge
467
:return: The merge directive
469
The public branch is always used if supplied. If no bundle is
470
included, the public branch must be supplied, and will be verified.
472
If the message is not supplied, the message from revision_id will be
477
repository.lock_write()
478
locked.append(repository)
479
t_revision_id = revision_id
480
if revision_id == 'null:':
482
t = testament.StrictTestament3.from_revision(repository,
484
submit_branch = _mod_branch.Branch.open(target_branch)
485
submit_branch.lock_read()
486
locked.append(submit_branch)
487
if submit_branch.get_public_branch() is not None:
488
target_branch = submit_branch.get_public_branch()
489
submit_revision_id = submit_branch.last_revision()
490
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
491
graph = repository.get_graph(submit_branch.repository)
492
ancestor_id = graph.find_unique_lca(revision_id,
494
if base_revision_id is None:
495
base_revision_id = ancestor_id
496
if (include_patch, include_bundle) != (False, False):
497
repository.fetch(submit_branch.repository, submit_revision_id)
499
patch = klass._generate_diff(repository, revision_id,
505
bundle = klass._generate_bundle(repository, revision_id,
506
ancestor_id).encode('base-64')
510
if public_branch is not None and not include_bundle:
511
public_branch_obj = _mod_branch.Branch.open(public_branch)
512
public_branch_obj.lock_read()
513
locked.append(public_branch_obj)
514
if not public_branch_obj.repository.has_revision(
516
raise errors.PublicBranchOutOfDate(public_branch,
519
for entry in reversed(locked):
521
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
522
patch, public_branch, message, bundle, base_revision_id)
524
def _verify_patch(self, repository):
525
calculated_patch = self._generate_diff(repository, self.revision_id,
526
self.base_revision_id)
527
# Convert line-endings to UNIX
528
stored_patch = re.sub('\r\n?', '\n', self.patch)
529
calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
530
# Strip trailing whitespace
531
calculated_patch = re.sub(' *\n', '\n', calculated_patch)
532
stored_patch = re.sub(' *\n', '\n', stored_patch)
533
return (calculated_patch == stored_patch)
535
def get_merge_request(self, repository):
536
"""Provide data for performing a merge
538
Returns suggested base, suggested target, and patch verification status
540
verified = self._maybe_verify(repository)
541
return self.base_revision_id, self.revision_id, verified
543
def _maybe_verify(self, repository):
544
if self.patch is not None:
545
if self._verify_patch(repository):
550
return 'inapplicable'
553
class MergeDirectiveFormatRegistry(registry.Registry):
555
def register(self, directive, format_string=None):
556
if format_string is None:
557
format_string = directive._format_string
558
registry.Registry.register(self, format_string, directive)
561
_format_registry = MergeDirectiveFormatRegistry()
562
_format_registry.register(MergeDirective)
563
_format_registry.register(MergeDirective2)
564
_format_registry.register(MergeDirective2,
565
'Bazaar merge directive format 2 (Bazaar 0.19)')