136
73
for key in ('source_branch', 'message'):
137
74
if self.__dict__[key] is not None:
138
75
stanza.add(key, self.__dict__[key])
77
stanza.add('base_revision_id', self.base_revision_id)
139
78
lines = ['# ' + self._format_string + '\n']
140
79
lines.extend(rio.to_patch_lines(stanza))
141
80
lines.append('# \n')
142
if self.patch is not None:
143
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,
146
154
def to_signed(self, branch):
147
155
"""Serialize as a signed string.
162
170
:return: an email message
164
172
mail_from = branch.get_config().username()
165
message = Message.Message()
166
message['To'] = mail_to
167
message['From'] = mail_from
168
173
if self.message is not None:
169
message['Subject'] = self.message
174
subject = self.message
171
176
revision = branch.repository.get_revision(self.revision_id)
172
message['Subject'] = revision.message
177
subject = revision.message
174
179
body = self.to_signed(branch)
176
181
body = ''.join(self.to_lines())
177
message.set_payload(body)
182
message = EmailMessage(mail_from, mail_to, subject, body)
181
def from_objects(klass, repository, revision_id, time, timezone,
182
target_branch, patch_type='bundle',
183
local_target_branch=None, public_branch=None, message=None):
184
"""Generate a merge directive from various objects
186
:param repository: The repository containing the revision
187
:param revision_id: The revision to merge
188
:param time: The POSIX timestamp of the date the request was issued.
189
:param timezone: The timezone of the request
190
:param target_branch: The url of the branch to merge into
191
:param patch_type: 'bundle', 'diff' or None, depending on the type of
193
:param local_target_branch: a local copy of the target branch
194
:param public_branch: location of a public branch containing the target
196
:param message: Message to use when committing the merge
197
:return: The merge directive
199
The public branch is always used if supplied. If the patch_type is
200
not 'bundle', the public branch must be supplied, and will be verified.
202
If the message is not supplied, the message from revision_id will be
205
t = testament.StrictTestament3.from_revision(repository, revision_id)
206
submit_branch = _mod_branch.Branch.open(target_branch)
207
if submit_branch.get_public_branch() is not None:
208
target_branch = submit_branch.get_public_branch()
209
if patch_type is None:
212
submit_revision_id = submit_branch.last_revision()
213
repository.fetch(submit_branch.repository, submit_revision_id)
214
ancestor_id = _mod_revision.common_ancestor(revision_id,
217
type_handler = {'bundle': klass._generate_bundle,
218
'diff': klass._generate_diff,
219
None: lambda x, y, z: None }
220
patch = type_handler[patch_type](repository, revision_id,
222
if patch_type == 'bundle':
224
bundle_serializer.write_bundle(repository, revision_id,
227
elif patch_type == 'diff':
228
patch = klass._generate_diff(repository, revision_id,
231
if public_branch is not None and patch_type != 'bundle':
232
public_branch_obj = _mod_branch.Branch.open(public_branch)
233
if not public_branch_obj.repository.has_revision(revision_id):
234
raise errors.PublicBranchOutOfDate(public_branch,
237
return MergeDirective(revision_id, t.as_sha1(), time, timezone,
238
target_branch, patch, patch_type, public_branch,
242
def _generate_diff(repository, revision_id, ancestor_id):
243
tree_1 = repository.revision_tree(ancestor_id)
244
tree_2 = repository.revision_tree(revision_id)
246
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
250
def _generate_bundle(repository, revision_id, ancestor_id):
252
bundle_serializer.write_bundle(repository, revision_id,
256
185
def install_revisions(self, target_repo):
257
186
"""Install revisions and return the target revision"""
258
187
if not target_repo.has_revision(self.revision_id):
259
188
if self.patch_type == 'bundle':
260
info = bundle_serializer.read_bundle(StringIO(self.patch))
189
info = bundle_serializer.read_bundle(
190
StringIO(self.get_raw_bundle()))
261
191
# We don't use the bundle's target revision, because
262
192
# MergeDirective.revision_id is authoritative.
263
info.install_revisions(target_repo)
194
info.install_revisions(target_repo, stream_input=False)
195
except errors.RevisionNotPresent:
196
# At least one dependency isn't present. Try installing
197
# missing revisions from the submit branch
198
submit_branch = _mod_branch.Branch.open(self.target_branch)
199
missing_revisions = []
200
bundle_revisions = set(r.revision_id for r in
202
for revision in info.real_revisions:
203
for parent_id in revision.parent_ids:
204
if (parent_id not in bundle_revisions and
205
not target_repo.has_revision(parent_id)):
206
missing_revisions.append(parent_id)
207
# reverse missing revisions to try to get heads first
209
unique_missing_set = set()
210
for revision in reversed(missing_revisions):
211
if revision in unique_missing_set:
213
unique_missing.append(revision)
214
unique_missing_set.add(revision)
215
for missing_revision in unique_missing:
216
target_repo.fetch(submit_branch.repository,
218
info.install_revisions(target_repo, stream_input=False)
265
220
source_branch = _mod_branch.Branch.open(self.source_branch)
266
221
target_repo.fetch(source_branch.repository, self.revision_id)
267
222
return self.revision_id
225
class MergeDirective(_BaseMergeDirective):
227
"""A request to perform a merge into a branch.
229
Designed to be serialized and mailed. It provides all the information
230
needed to perform a merge automatically, by providing at minimum a revision
231
bundle or the location of a branch.
233
The serialization format is robust against certain common forms of
234
deterioration caused by mailing.
236
The format is also designed to be patch-compatible. If the directive
237
includes a diff or revision bundle, it should be possible to apply it
238
directly using the standard patch program.
241
_format_string = 'Bazaar merge directive format 1'
243
def __init__(self, revision_id, testament_sha1, time, timezone,
244
target_branch, patch=None, patch_type=None,
245
source_branch=None, message=None, bundle=None):
248
:param revision_id: The revision to merge
249
:param testament_sha1: The sha1 of the testament of the revision to
251
:param time: The current POSIX timestamp time
252
:param timezone: The timezone offset
253
:param target_branch: The branch to apply the merge to
254
:param patch: The text of a diff or bundle
255
:param patch_type: None, "diff" or "bundle", depending on the contents
257
:param source_branch: A public location to merge the revision from
258
:param message: The message to use when committing this merge
260
_BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
261
timezone, target_branch, patch, source_branch, message)
262
assert patch_type in (None, 'diff', 'bundle'), patch_type
263
if patch_type != 'bundle' and source_branch is None:
264
raise errors.NoMergeSource()
265
if patch_type is not None and patch is None:
266
raise errors.PatchMissing(patch_type)
267
self.patch_type = patch_type
269
def clear_payload(self):
271
self.patch_type = None
273
def get_raw_bundle(self):
277
if self.patch_type == 'bundle':
282
bundle = property(_bundle)
285
def from_lines(klass, lines):
286
"""Deserialize a MergeRequest from an iterable of lines
288
:param lines: An iterable of lines
289
:return: a MergeRequest
291
line_iter = iter(lines)
292
for line in line_iter:
293
if line.startswith('# Bazaar merge directive format '):
297
raise errors.NotAMergeDirective(lines[0])
299
raise errors.NotAMergeDirective('')
300
return _format_registry.get(line[2:].rstrip())._from_lines(line_iter)
303
def _from_lines(klass, line_iter):
304
stanza = rio.read_patch_stanza(line_iter)
305
patch_lines = list(line_iter)
306
if len(patch_lines) == 0:
310
patch = ''.join(patch_lines)
312
bundle_serializer.read_bundle(StringIO(patch))
313
except (errors.NotABundle, errors.BundleNotSupported,
317
patch_type = 'bundle'
318
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
320
for key in ('revision_id', 'testament_sha1', 'target_branch',
321
'source_branch', 'message'):
323
kwargs[key] = stanza.get(key)
326
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
327
return MergeDirective(time=time, timezone=timezone,
328
patch_type=patch_type, patch=patch, **kwargs)
331
lines = self._to_lines()
332
if self.patch is not None:
333
lines.extend(self.patch.splitlines(True))
337
def _generate_bundle(repository, revision_id, ancestor_id):
339
bundle_serializer.write_bundle(repository, revision_id,
340
ancestor_id, s, '0.9')
343
def get_merge_request(self, repository):
344
"""Provide data for performing a merge
346
Returns suggested base, suggested target, and patch verification status
348
return None, self.revision_id, 'inapplicable'
351
class MergeDirective2(_BaseMergeDirective):
353
_format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
355
def __init__(self, revision_id, testament_sha1, time, timezone,
356
target_branch, patch=None, source_branch=None, message=None,
357
bundle=None, base_revision_id=None):
358
if source_branch is None and bundle is None:
359
raise errors.NoMergeSource()
360
_BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
361
timezone, target_branch, patch, source_branch, message)
363
self.base_revision_id = base_revision_id
365
def _patch_type(self):
366
if self.bundle is not None:
368
elif self.patch is not None:
373
patch_type = property(_patch_type)
375
def clear_payload(self):
379
def get_raw_bundle(self):
380
if self.bundle is None:
383
return self.bundle.decode('base-64')
386
def _from_lines(klass, line_iter):
387
stanza = rio.read_patch_stanza(line_iter)
391
start = line_iter.next()
392
except StopIteration:
395
if start.startswith('# Begin patch'):
397
for line in line_iter:
398
if line.startswith('# Begin bundle'):
401
patch_lines.append(line)
404
patch = ''.join(patch_lines)
405
if start is not None:
406
if start.startswith('# Begin bundle'):
407
bundle = ''.join(line_iter)
409
raise errors.IllegalMergeDirectivePayload(start)
410
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
412
for key in ('revision_id', 'testament_sha1', 'target_branch',
413
'source_branch', 'message', 'base_revision_id'):
415
kwargs[key] = stanza.get(key)
418
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
419
kwargs['base_revision_id'] =\
420
kwargs['base_revision_id'].encode('utf-8')
421
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
425
lines = self._to_lines(base_revision=True)
426
if self.patch is not None:
427
lines.append('# Begin patch\n')
428
lines.extend(self.patch.splitlines(True))
429
if self.bundle is not None:
430
lines.append('# Begin bundle\n')
431
lines.extend(self.bundle.splitlines(True))
435
def from_objects(klass, repository, revision_id, time, timezone,
436
target_branch, include_patch=True, include_bundle=True,
437
local_target_branch=None, public_branch=None, message=None,
438
base_revision_id=None):
439
"""Generate a merge directive from various objects
441
:param repository: The repository containing the revision
442
:param revision_id: The revision to merge
443
:param time: The POSIX timestamp of the date the request was issued.
444
:param timezone: The timezone of the request
445
:param target_branch: The url of the branch to merge into
446
:param include_patch: If true, include a preview patch
447
:param include_bundle: If true, include a bundle
448
:param local_target_branch: a local copy of the target branch
449
:param public_branch: location of a public branch containing the target
451
:param message: Message to use when committing the merge
452
:return: The merge directive
454
The public branch is always used if supplied. If no bundle is
455
included, the public branch must be supplied, and will be verified.
457
If the message is not supplied, the message from revision_id will be
462
repository.lock_write()
463
locked.append(repository)
464
t_revision_id = revision_id
465
if revision_id == 'null:':
467
t = testament.StrictTestament3.from_revision(repository,
469
submit_branch = _mod_branch.Branch.open(target_branch)
470
submit_branch.lock_read()
471
locked.append(submit_branch)
472
if submit_branch.get_public_branch() is not None:
473
target_branch = submit_branch.get_public_branch()
474
submit_revision_id = submit_branch.last_revision()
475
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
476
graph = repository.get_graph(submit_branch.repository)
477
ancestor_id = graph.find_unique_lca(revision_id,
479
if base_revision_id is None:
480
base_revision_id = ancestor_id
481
if (include_patch, include_bundle) != (False, False):
482
repository.fetch(submit_branch.repository, submit_revision_id)
484
patch = klass._generate_diff(repository, revision_id,
490
bundle = klass._generate_bundle(repository, revision_id,
491
ancestor_id).encode('base-64')
495
if public_branch is not None and not include_bundle:
496
public_branch_obj = _mod_branch.Branch.open(public_branch)
497
public_branch_obj.lock_read()
498
locked.append(public_branch_obj)
499
if not public_branch_obj.repository.has_revision(
501
raise errors.PublicBranchOutOfDate(public_branch,
504
for entry in reversed(locked):
506
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
507
patch, public_branch, message, bundle, base_revision_id)
509
def _verify_patch(self, repository):
510
calculated_patch = self._generate_diff(repository, self.revision_id,
511
self.base_revision_id)
512
# Convert line-endings to UNIX
513
stored_patch = re.sub('\r\n?', '\n', self.patch)
514
calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
515
# Strip trailing whitespace
516
calculated_patch = re.sub(' *\n', '\n', calculated_patch)
517
stored_patch = re.sub(' *\n', '\n', stored_patch)
518
return (calculated_patch == stored_patch)
520
def get_merge_request(self, repository):
521
"""Provide data for performing a merge
523
Returns suggested base, suggested target, and patch verification status
525
verified = self._maybe_verify(repository)
526
return self.base_revision_id, self.revision_id, verified
528
def _maybe_verify(self, repository):
529
if self.patch is not None:
530
if self._verify_patch(repository):
535
return 'inapplicable'
538
class MergeDirectiveFormatRegistry(registry.Registry):
540
def register(self, directive, format_string=None):
541
if format_string is None:
542
format_string = directive._format_string
543
registry.Registry.register(self, format_string, directive)
546
_format_registry = MergeDirectiveFormatRegistry()
547
_format_registry.register(MergeDirective)
548
_format_registry.register(MergeDirective2)
549
_format_registry.register(MergeDirective2,
550
'Bazaar merge directive format 2 (Bazaar 0.19)')