15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
from email import Message
18
19
from StringIO import StringIO
21
21
from bzrlib import (
22
22
branch as _mod_branch,
27
26
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):
31
from bzrlib.bundle import serializer as bundle_serializer
34
class MergeDirective(object):
36
"""A request to perform a merge into a branch.
38
Designed to be serialized and mailed. It provides all the information
39
needed to perform a merge automatically, by providing at minimum a revision
40
bundle or the location of a branch.
42
The serialization format is robust against certain common forms of
43
deterioration caused by mailing.
45
The format is also designed to be patch-compatible. If the directive
46
includes a diff or revision bundle, it should be possible to apply it
47
directly using the standard patch program.
50
_format_string = 'Bazaar merge directive format 1'
40
52
def __init__(self, revision_id, testament_sha1, time, timezone,
41
target_branch, patch=None, source_branch=None, message=None,
53
target_branch, patch=None, patch_type=None,
54
source_branch=None, message=None):
45
57
:param revision_id: The revision to merge
49
61
:param timezone: The timezone offset
50
62
:param target_branch: The branch to apply the merge to
51
63
:param patch: The text of a diff or bundle
64
:param patch_type: None, "diff" or "bundle", depending on the contents
52
66
:param source_branch: A public location to merge the revision from
53
67
:param message: The message to use when committing this merge
69
assert patch_type in (None, 'diff', 'bundle')
70
if patch_type != 'bundle' and source_branch is None:
71
raise errors.NoMergeSource()
72
if patch_type is not None and patch is None:
73
raise errors.PatchMissing(patch_type)
55
74
self.revision_id = revision_id
56
75
self.testament_sha1 = testament_sha1
58
77
self.timezone = timezone
59
78
self.target_branch = target_branch
80
self.patch_type = patch_type
61
81
self.source_branch = source_branch
62
82
self.message = message
64
def _to_lines(self, base_revision=False):
85
def from_lines(klass, lines):
86
"""Deserialize a MergeRequest from an iterable of lines
88
:param lines: An iterable of lines
89
:return: a MergeRequest
91
line_iter = iter(lines)
92
for line in line_iter:
93
if line.startswith('# ' + klass._format_string):
96
raise errors.NotAMergeDirective(lines[0])
97
stanza = rio.read_patch_stanza(line_iter)
98
patch_lines = list(line_iter)
99
if len(patch_lines) == 0:
102
patch = ''.join(patch_lines)
104
bundle_serializer.read_bundle(StringIO(patch))
105
except errors.NotABundle:
108
patch_type = 'bundle'
109
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
111
for key in ('revision_id', 'testament_sha1', 'target_branch',
112
'source_branch', 'message'):
114
kwargs[key] = stanza.get(key)
117
return MergeDirective(time=time, timezone=timezone,
118
patch_type=patch_type, patch=patch, **kwargs)
65
121
"""Serialize as a list of lines
67
123
:return: a list of lines
73
129
for key in ('source_branch', 'message'):
74
130
if self.__dict__[key] is not None:
75
131
stanza.add(key, self.__dict__[key])
77
stanza.add('base_revision_id', self.base_revision_id)
78
132
lines = ['# ' + self._format_string + '\n']
79
133
lines.extend(rio.to_patch_lines(stanza))
80
134
lines.append('# \n')
135
if self.patch is not None:
136
lines.extend(self.patch.splitlines(True))
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)
140
def _generate_diff(repository, revision_id, ancestor_id):
141
tree_1 = repository.revision_tree(ancestor_id)
142
tree_2 = repository.revision_tree(revision_id)
144
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
148
def _generate_bundle(repository, revision_id, ancestor_id):
150
bundle_serializer.write_bundle(repository, revision_id,
154
139
def to_signed(self, branch):
155
140
"""Serialize as a signed string.
170
155
:return: an email message
172
157
mail_from = branch.get_config().username()
158
message = Message.Message()
159
message['To'] = mail_to
160
message['From'] = mail_from
173
161
if self.message is not None:
174
subject = self.message
162
message['Subject'] = self.message
176
164
revision = branch.repository.get_revision(self.revision_id)
177
subject = revision.message
165
message['Subject'] = revision.message
179
167
body = self.to_signed(branch)
181
169
body = ''.join(self.to_lines())
182
message = EmailMessage(mail_from, mail_to, subject, body)
170
message.set_payload(body)
185
def install_revisions(self, target_repo):
186
"""Install revisions and return the target revision"""
187
if not target_repo.has_revision(self.revision_id):
188
if self.patch_type == 'bundle':
189
info = bundle_serializer.read_bundle(
190
StringIO(self.get_raw_bundle()))
191
# We don't use the bundle's target revision, because
192
# MergeDirective.revision_id is authoritative.
193
info.install_revisions(target_repo)
195
source_branch = _mod_branch.Branch.open(self.source_branch)
196
target_repo.fetch(source_branch.repository, self.revision_id)
197
return self.revision_id
200
class MergeDirective(_BaseMergeDirective):
202
"""A request to perform a merge into a branch.
204
Designed to be serialized and mailed. It provides all the information
205
needed to perform a merge automatically, by providing at minimum a revision
206
bundle or the location of a branch.
208
The serialization format is robust against certain common forms of
209
deterioration caused by mailing.
211
The format is also designed to be patch-compatible. If the directive
212
includes a diff or revision bundle, it should be possible to apply it
213
directly using the standard patch program.
216
_format_string = 'Bazaar merge directive format 1'
218
def __init__(self, revision_id, testament_sha1, time, timezone,
219
target_branch, patch=None, patch_type=None,
220
source_branch=None, message=None, bundle=None):
223
:param revision_id: The revision to merge
224
:param testament_sha1: The sha1 of the testament of the revision to
226
:param time: The current POSIX timestamp time
227
:param timezone: The timezone offset
228
:param target_branch: The branch to apply the merge to
229
:param patch: The text of a diff or bundle
230
:param patch_type: None, "diff" or "bundle", depending on the contents
232
:param source_branch: A public location to merge the revision from
233
:param message: The message to use when committing this merge
235
_BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
236
timezone, target_branch, patch, source_branch, message)
237
assert patch_type in (None, 'diff', 'bundle'), patch_type
238
if patch_type != 'bundle' and source_branch is None:
239
raise errors.NoMergeSource()
240
if patch_type is not None and patch is None:
241
raise errors.PatchMissing(patch_type)
242
self.patch_type = patch_type
244
def clear_payload(self):
246
self.patch_type = None
248
def get_raw_bundle(self):
252
if self.patch_type == 'bundle':
257
bundle = property(_bundle)
260
def from_lines(klass, lines):
261
"""Deserialize a MergeRequest from an iterable of lines
263
:param lines: An iterable of lines
264
:return: a MergeRequest
266
line_iter = iter(lines)
267
for line in line_iter:
268
if line.startswith('# Bazaar merge directive format '):
272
raise errors.NotAMergeDirective(lines[0])
274
raise errors.NotAMergeDirective('')
275
return _format_registry.get(line[2:].rstrip())._from_lines(line_iter)
278
def _from_lines(klass, line_iter):
279
stanza = rio.read_patch_stanza(line_iter)
280
patch_lines = list(line_iter)
281
if len(patch_lines) == 0:
285
patch = ''.join(patch_lines)
287
bundle_serializer.read_bundle(StringIO(patch))
288
except (errors.NotABundle, errors.BundleNotSupported,
292
patch_type = 'bundle'
293
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
295
for key in ('revision_id', 'testament_sha1', 'target_branch',
296
'source_branch', 'message'):
298
kwargs[key] = stanza.get(key)
301
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
302
return MergeDirective(time=time, timezone=timezone,
303
patch_type=patch_type, patch=patch, **kwargs)
306
lines = self._to_lines()
307
if self.patch is not None:
308
lines.extend(self.patch.splitlines(True))
312
def _generate_bundle(repository, revision_id, ancestor_id):
314
bundle_serializer.write_bundle(repository, revision_id,
315
ancestor_id, s, '0.9')
318
def get_merge_request(self, repository):
319
"""Provide data for performing a merge
321
Returns suggested base, suggested target, and patch verification status
323
return None, self.revision_id, 'inapplicable'
326
class MergeDirective2(_BaseMergeDirective):
328
_format_string = 'Bazaar merge directive format 2 (Bazaar 0.19)'
330
def __init__(self, revision_id, testament_sha1, time, timezone,
331
target_branch, patch=None, source_branch=None, message=None,
332
bundle=None, base_revision_id=None):
333
if source_branch is None and bundle is None:
334
raise errors.NoMergeSource()
335
_BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
336
timezone, target_branch, patch, source_branch, message)
338
self.base_revision_id = base_revision_id
340
def _patch_type(self):
341
if self.bundle is not None:
343
elif self.patch is not None:
348
patch_type = property(_patch_type)
350
def clear_payload(self):
354
def get_raw_bundle(self):
355
if self.bundle is None:
358
return self.bundle.decode('base-64')
361
def _from_lines(klass, line_iter):
362
stanza = rio.read_patch_stanza(line_iter)
366
start = line_iter.next()
367
except StopIteration:
370
if start.startswith('# Begin patch'):
372
for line in line_iter:
373
if line.startswith('# Begin bundle'):
376
patch_lines.append(line)
379
patch = ''.join(patch_lines)
380
if start is not None:
381
if start.startswith('# Begin bundle'):
382
bundle = ''.join(line_iter)
384
raise errors.IllegalMergeDirectivePayload(start)
385
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
387
for key in ('revision_id', 'testament_sha1', 'target_branch',
388
'source_branch', 'message', 'base_revision_id'):
390
kwargs[key] = stanza.get(key)
393
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
394
kwargs['base_revision_id'] =\
395
kwargs['base_revision_id'].encode('utf-8')
396
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
400
lines = self._to_lines(base_revision=True)
401
if self.patch is not None:
402
lines.append('# Begin patch\n')
403
lines.extend(self.patch.splitlines(True))
404
if self.bundle is not None:
405
lines.append('# Begin bundle\n')
406
lines.extend(self.bundle.splitlines(True))
410
174
def from_objects(klass, repository, revision_id, time, timezone,
411
target_branch, include_patch=True, include_bundle=True,
412
local_target_branch=None, public_branch=None, message=None,
413
base_revision_id=None):
175
target_branch, patch_type='bundle',
176
local_target_branch=None, public_branch=None, message=None):
414
177
"""Generate a merge directive from various objects
416
179
:param repository: The repository containing the revision
418
181
:param time: The POSIX timestamp of the date the request was issued.
419
182
:param timezone: The timezone of the request
420
183
:param target_branch: The url of the branch to merge into
421
:param include_patch: If true, include a preview patch
422
:param include_bundle: If true, include a bundle
184
:param patch_type: 'bundle', 'diff' or None, depending on the type of
423
186
:param local_target_branch: a local copy of the target branch
424
187
:param public_branch: location of a public branch containing the target
426
189
:param message: Message to use when committing the merge
427
190
:return: The merge directive
429
The public branch is always used if supplied. If no bundle is
430
included, the public branch must be supplied, and will be verified.
192
The public branch is always used if supplied. If the patch_type is
193
not 'bundle', the public branch must be supplied, and will be verified.
432
195
If the message is not supplied, the message from revision_id will be
433
196
used for the commit.
437
repository.lock_write()
438
locked.append(repository)
439
t_revision_id = revision_id
440
if revision_id == 'null:':
442
t = testament.StrictTestament3.from_revision(repository,
444
submit_branch = _mod_branch.Branch.open(target_branch)
445
submit_branch.lock_read()
446
locked.append(submit_branch)
447
if submit_branch.get_public_branch() is not None:
448
target_branch = submit_branch.get_public_branch()
198
t = testament.StrictTestament3.from_revision(repository, revision_id)
199
submit_branch = _mod_branch.Branch.open(target_branch)
200
if submit_branch.get_public_branch() is not None:
201
target_branch = submit_branch.get_public_branch()
202
if patch_type is None:
449
205
submit_revision_id = submit_branch.last_revision()
450
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
451
graph = repository.get_graph(submit_branch.repository)
452
ancestor_id = graph.find_unique_lca(revision_id,
454
if base_revision_id is None:
455
base_revision_id = ancestor_id
456
if (include_patch, include_bundle) != (False, False):
457
repository.fetch(submit_branch.repository, submit_revision_id)
206
repository.fetch(submit_branch.repository, submit_revision_id)
207
ancestor_id = _mod_revision.common_ancestor(revision_id,
210
type_handler = {'bundle': klass._generate_bundle,
211
'diff': klass._generate_diff,
212
None: lambda x, y, z: None }
213
patch = type_handler[patch_type](repository, revision_id,
215
if patch_type == 'bundle':
217
bundle_serializer.write_bundle(repository, revision_id,
220
elif patch_type == 'diff':
459
221
patch = klass._generate_diff(repository, revision_id,
465
bundle = klass._generate_bundle(repository, revision_id,
466
ancestor_id).encode('base-64')
470
if public_branch is not None and not include_bundle:
224
if public_branch is not None and patch_type != 'bundle':
471
225
public_branch_obj = _mod_branch.Branch.open(public_branch)
472
public_branch_obj.lock_read()
473
locked.append(public_branch_obj)
474
if not public_branch_obj.repository.has_revision(
226
if not public_branch_obj.repository.has_revision(revision_id):
476
227
raise errors.PublicBranchOutOfDate(public_branch,
479
for entry in reversed(locked):
481
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
482
patch, public_branch, message, bundle, base_revision_id)
484
def _verify_patch(self, repository):
485
calculated_patch = self._generate_diff(repository, self.revision_id,
486
self.base_revision_id)
487
# Convert line-endings to UNIX
488
stored_patch = re.sub('\r\n?', '\n', self.patch)
489
# Strip trailing whitespace
490
calculated_patch = re.sub(' *\n', '\n', calculated_patch)
491
stored_patch = re.sub(' *\n', '\n', stored_patch)
492
return (calculated_patch == stored_patch)
494
def get_merge_request(self, repository):
495
"""Provide data for performing a merge
497
Returns suggested base, suggested target, and patch verification status
499
verified = self._maybe_verify(repository)
500
return self.base_revision_id, self.revision_id, verified
502
def _maybe_verify(self, repository):
503
if self.patch is not None:
504
if self._verify_patch(repository):
509
return 'inapplicable'
512
class MergeDirectiveFormatRegistry(registry.Registry):
514
def register(self, directive):
515
registry.Registry.register(self, directive._format_string, directive)
518
_format_registry = MergeDirectiveFormatRegistry()
519
_format_registry.register(MergeDirective)
520
_format_registry.register(MergeDirective2)
230
return MergeDirective(revision_id, t.as_sha1(), time, timezone,
231
target_branch, patch, patch_type, public_branch,
235
def _generate_diff(repository, revision_id, ancestor_id):
236
tree_1 = repository.revision_tree(ancestor_id)
237
tree_2 = repository.revision_tree(revision_id)
239
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
243
def _generate_bundle(repository, revision_id, ancestor_id):
245
bundle_serializer.write_bundle(repository, revision_id,