63
49
:param timezone: The timezone offset
64
50
:param target_branch: The branch to apply the merge to
65
51
:param patch: The text of a diff or bundle
66
:param patch_type: None, "diff" or "bundle", depending on the contents
68
52
:param source_branch: A public location to merge the revision from
69
53
:param message: The message to use when committing this merge
71
assert patch_type in (None, 'diff', 'bundle')
72
if patch_type != 'bundle' and source_branch is None:
73
raise errors.NoMergeSource()
74
if patch_type is not None and patch is None:
75
raise errors.PatchMissing(patch_type)
76
55
self.revision_id = revision_id
77
56
self.testament_sha1 = testament_sha1
79
58
self.timezone = timezone
80
59
self.target_branch = target_branch
82
self.patch_type = patch_type
83
61
self.source_branch = source_branch
84
62
self.message = message
87
def from_lines(klass, lines):
88
"""Deserialize a MergeRequest from an iterable of lines
90
:param lines: An iterable of lines
91
:return: a MergeRequest
93
line_iter = iter(lines)
94
for line in line_iter:
95
if line.startswith('# ' + klass._format_string):
99
raise errors.NotAMergeDirective(lines[0])
101
raise errors.NotAMergeDirective('')
102
stanza = rio.read_patch_stanza(line_iter)
103
patch_lines = list(line_iter)
104
if len(patch_lines) == 0:
108
patch = ''.join(patch_lines)
110
bundle_serializer.read_bundle(StringIO(patch))
111
except (errors.NotABundle, errors.BundleNotSupported,
115
patch_type = 'bundle'
116
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
118
for key in ('revision_id', 'testament_sha1', 'target_branch',
119
'source_branch', 'message'):
121
kwargs[key] = stanza.get(key)
124
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
125
return MergeDirective(time=time, timezone=timezone,
126
patch_type=patch_type, patch=patch, **kwargs)
64
def _to_lines(self, base_revision=False):
129
65
"""Serialize as a list of lines
131
67
:return: a list of lines
137
73
for key in ('source_branch', 'message'):
138
74
if self.__dict__[key] is not None:
139
75
stanza.add(key, self.__dict__[key])
77
stanza.add('base_revision_id', self.base_revision_id)
140
78
lines = ['# ' + self._format_string + '\n']
141
79
lines.extend(rio.to_patch_lines(stanza))
142
80
lines.append('# \n')
143
if self.patch is not None:
144
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,
147
154
def to_signed(self, branch):
148
155
"""Serialize as a signed string.
178
185
message.set_payload(body)
182
def from_objects(klass, repository, revision_id, time, timezone,
183
target_branch, patch_type='bundle',
184
local_target_branch=None, public_branch=None, message=None):
185
"""Generate a merge directive from various objects
187
:param repository: The repository containing the revision
188
:param revision_id: The revision to merge
189
:param time: The POSIX timestamp of the date the request was issued.
190
:param timezone: The timezone of the request
191
:param target_branch: The url of the branch to merge into
192
:param patch_type: 'bundle', 'diff' or None, depending on the type of
194
:param local_target_branch: a local copy of the target branch
195
:param public_branch: location of a public branch containing the target
197
:param message: Message to use when committing the merge
198
:return: The merge directive
200
The public branch is always used if supplied. If the patch_type is
201
not 'bundle', the public branch must be supplied, and will be verified.
203
If the message is not supplied, the message from revision_id will be
206
t_revision_id = revision_id
207
if revision_id == 'null:':
209
t = testament.StrictTestament3.from_revision(repository, t_revision_id)
210
submit_branch = _mod_branch.Branch.open(target_branch)
211
if submit_branch.get_public_branch() is not None:
212
target_branch = submit_branch.get_public_branch()
213
if patch_type is None:
216
submit_revision_id = submit_branch.last_revision()
217
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
218
repository.fetch(submit_branch.repository, submit_revision_id)
219
graph = repository.get_graph()
220
ancestor_id = graph.find_unique_lca(revision_id,
222
type_handler = {'bundle': klass._generate_bundle,
223
'diff': klass._generate_diff,
224
None: lambda x, y, z: None }
225
patch = type_handler[patch_type](repository, revision_id,
227
if patch_type == 'bundle':
229
bundle_serializer.write_bundle(repository, revision_id,
232
elif patch_type == 'diff':
233
patch = klass._generate_diff(repository, revision_id,
236
if public_branch is not None and patch_type != 'bundle':
237
public_branch_obj = _mod_branch.Branch.open(public_branch)
238
if not public_branch_obj.repository.has_revision(revision_id):
239
raise errors.PublicBranchOutOfDate(public_branch,
242
return MergeDirective(revision_id, t.as_sha1(), time, timezone,
243
target_branch, patch, patch_type, public_branch,
247
def _generate_diff(repository, revision_id, ancestor_id):
248
tree_1 = repository.revision_tree(ancestor_id)
249
tree_2 = repository.revision_tree(revision_id)
251
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
255
def _generate_bundle(repository, revision_id, ancestor_id):
257
bundle_serializer.write_bundle(repository, revision_id,
261
188
def install_revisions(self, target_repo):
262
189
"""Install revisions and return the target revision"""
263
190
if not target_repo.has_revision(self.revision_id):
264
191
if self.patch_type == 'bundle':
265
info = bundle_serializer.read_bundle(StringIO(self.patch))
192
info = bundle_serializer.read_bundle(
193
StringIO(self.get_raw_bundle()))
266
194
# We don't use the bundle's target revision, because
267
195
# MergeDirective.revision_id is authoritative.
268
196
info.install_revisions(target_repo)
270
198
source_branch = _mod_branch.Branch.open(self.source_branch)
271
199
target_repo.fetch(source_branch.repository, self.revision_id)
272
200
return self.revision_id
203
class MergeDirective(_BaseMergeDirective):
205
"""A request to perform a merge into a branch.
207
Designed to be serialized and mailed. It provides all the information
208
needed to perform a merge automatically, by providing at minimum a revision
209
bundle or the location of a branch.
211
The serialization format is robust against certain common forms of
212
deterioration caused by mailing.
214
The format is also designed to be patch-compatible. If the directive
215
includes a diff or revision bundle, it should be possible to apply it
216
directly using the standard patch program.
219
_format_string = 'Bazaar merge directive format 1'
221
def __init__(self, revision_id, testament_sha1, time, timezone,
222
target_branch, patch=None, patch_type=None,
223
source_branch=None, message=None, bundle=None):
226
:param revision_id: The revision to merge
227
:param testament_sha1: The sha1 of the testament of the revision to
229
:param time: The current POSIX timestamp time
230
:param timezone: The timezone offset
231
:param target_branch: The branch to apply the merge to
232
:param patch: The text of a diff or bundle
233
:param patch_type: None, "diff" or "bundle", depending on the contents
235
:param source_branch: A public location to merge the revision from
236
:param message: The message to use when committing this merge
238
_BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
239
timezone, target_branch, patch, source_branch, message)
240
assert patch_type in (None, 'diff', 'bundle'), patch_type
241
if patch_type != 'bundle' and source_branch is None:
242
raise errors.NoMergeSource()
243
if patch_type is not None and patch is None:
244
raise errors.PatchMissing(patch_type)
245
self.patch_type = patch_type
247
def clear_payload(self):
249
self.patch_type = None
251
def get_raw_bundle(self):
255
if self.patch_type == 'bundle':
260
bundle = property(_bundle)
263
def from_lines(klass, lines):
264
"""Deserialize a MergeRequest from an iterable of lines
266
:param lines: An iterable of lines
267
:return: a MergeRequest
269
line_iter = iter(lines)
270
for line in line_iter:
271
if line.startswith('# Bazaar merge directive format '):
275
raise errors.NotAMergeDirective(lines[0])
277
raise errors.NotAMergeDirective('')
278
return _format_registry.get(line[2:].rstrip())._from_lines(line_iter)
281
def _from_lines(klass, line_iter):
282
stanza = rio.read_patch_stanza(line_iter)
283
patch_lines = list(line_iter)
284
if len(patch_lines) == 0:
288
patch = ''.join(patch_lines)
290
bundle_serializer.read_bundle(StringIO(patch))
291
except (errors.NotABundle, errors.BundleNotSupported,
295
patch_type = 'bundle'
296
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
298
for key in ('revision_id', 'testament_sha1', 'target_branch',
299
'source_branch', 'message'):
301
kwargs[key] = stanza.get(key)
304
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
305
return MergeDirective(time=time, timezone=timezone,
306
patch_type=patch_type, patch=patch, **kwargs)
309
lines = self._to_lines()
310
if self.patch is not None:
311
lines.extend(self.patch.splitlines(True))
315
def _generate_bundle(repository, revision_id, ancestor_id):
317
bundle_serializer.write_bundle(repository, revision_id,
318
ancestor_id, s, '0.9')
321
def get_merge_request(self, repository):
322
"""Provide data for performing a merge
324
Returns suggested base, suggested target, and patch verification status
326
return None, self.revision_id, 'inapplicable'
329
class MergeDirective2(_BaseMergeDirective):
331
_format_string = 'Bazaar merge directive format 2 (Bazaar 0.18)'
333
def __init__(self, revision_id, testament_sha1, time, timezone,
334
target_branch, patch=None, source_branch=None, message=None,
335
bundle=None, base_revision_id=None):
336
if source_branch is None and bundle is None:
337
raise errors.NoMergeSource()
338
_BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
339
timezone, target_branch, patch, source_branch, message)
341
self.base_revision_id = base_revision_id
343
def _patch_type(self):
344
if self.bundle is not None:
346
elif self.patch is not None:
351
patch_type = property(_patch_type)
353
def clear_payload(self):
357
def get_raw_bundle(self):
358
if self.bundle is None:
361
return self.bundle.decode('base-64')
364
def _from_lines(klass, line_iter):
365
stanza = rio.read_patch_stanza(line_iter)
369
start = line_iter.next()
370
except StopIteration:
373
if start.startswith('# Begin patch'):
375
for line in line_iter:
376
if line.startswith('# Begin bundle'):
379
patch_lines.append(line)
382
patch = ''.join(patch_lines)
383
if start is not None:
384
if start.startswith('# Begin bundle'):
385
bundle = ''.join(line_iter)
387
raise errors.IllegalMergeDirectivePayload(start)
388
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
390
for key in ('revision_id', 'testament_sha1', 'target_branch',
391
'source_branch', 'message', 'base_revision_id'):
393
kwargs[key] = stanza.get(key)
396
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
397
kwargs['base_revision_id'] =\
398
kwargs['base_revision_id'].encode('utf-8')
399
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
403
lines = self._to_lines(base_revision=True)
404
if self.patch is not None:
405
lines.append('# Begin patch\n')
406
lines.extend(self.patch.splitlines(True))
407
if self.bundle is not None:
408
lines.append('# Begin bundle\n')
409
lines.extend(self.bundle.splitlines(True))
413
def from_objects(klass, repository, revision_id, time, timezone,
414
target_branch, include_patch=True, include_bundle=True,
415
local_target_branch=None, public_branch=None, message=None,
416
base_revision_id=None):
417
"""Generate a merge directive from various objects
419
:param repository: The repository containing the revision
420
:param revision_id: The revision to merge
421
:param time: The POSIX timestamp of the date the request was issued.
422
:param timezone: The timezone of the request
423
:param target_branch: The url of the branch to merge into
424
:param include_patch: If true, include a preview patch
425
:param include_bundle: If true, include a bundle
426
:param local_target_branch: a local copy of the target branch
427
:param public_branch: location of a public branch containing the target
429
:param message: Message to use when committing the merge
430
:return: The merge directive
432
The public branch is always used if supplied. If no bundle is
433
included, the public branch must be supplied, and will be verified.
435
If the message is not supplied, the message from revision_id will be
440
repository.lock_write()
441
locked.append(repository)
442
t_revision_id = revision_id
443
if revision_id == 'null:':
445
t = testament.StrictTestament3.from_revision(repository,
447
submit_branch = _mod_branch.Branch.open(target_branch)
448
submit_branch.lock_read()
449
locked.append(submit_branch)
450
if submit_branch.get_public_branch() is not None:
451
target_branch = submit_branch.get_public_branch()
452
submit_revision_id = submit_branch.last_revision()
453
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
454
graph = repository.get_graph(submit_branch.repository)
455
ancestor_id = graph.find_unique_lca(revision_id,
457
if base_revision_id is None:
458
base_revision_id = ancestor_id
459
if (include_patch, include_bundle) != (False, False):
460
repository.fetch(submit_branch.repository, submit_revision_id)
462
patch = klass._generate_diff(repository, revision_id,
468
bundle = klass._generate_bundle(repository, revision_id,
469
ancestor_id).encode('base-64')
473
if public_branch is not None and not include_bundle:
474
public_branch_obj = _mod_branch.Branch.open(public_branch)
475
public_branch_obj.lock_read()
476
locked.append(public_branch_obj)
477
if not public_branch_obj.repository.has_revision(
479
raise errors.PublicBranchOutOfDate(public_branch,
482
for entry in reversed(locked):
484
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
485
patch, public_branch, message, bundle, base_revision_id)
487
def _verify_patch(self, repository):
488
calculated_patch = self._generate_diff(repository, self.revision_id,
489
self.base_revision_id)
490
# Convert line-endings to UNIX
491
stored_patch = re.sub('\r\n?', '\n', self.patch)
492
# Strip trailing whitespace
493
calculated_patch = re.sub(' *\n', '\n', calculated_patch)
494
stored_patch = re.sub(' *\n', '\n', stored_patch)
495
return (calculated_patch == stored_patch)
497
def get_merge_request(self, repository):
498
"""Provide data for performing a merge
500
Returns suggested base, suggested target, and patch verification status
502
verified = self._maybe_verify(repository)
503
return self.base_revision_id, self.revision_id, verified
505
def _maybe_verify(self, repository):
506
if self.patch is not None:
507
if self._verify_patch(repository):
512
return 'inapplicable'
515
class MergeDirectiveFormatRegistry(registry.Registry):
517
def register(self, directive):
518
registry.Registry.register(self, directive._format_string, directive)
521
_format_registry = MergeDirectiveFormatRegistry()
522
_format_registry.register(MergeDirective)
523
_format_registry.register(MergeDirective2)