13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
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,
28
26
revision as _mod_revision,
34
from bzrlib.bundle import (
35
serializer as bundle_serializer,
37
from bzrlib.email_message import EmailMessage
40
class MergeRequestBodyParams(object):
41
"""Parameter object for the merge_request_body hook."""
43
def __init__(self, body, orig_body, directive, to, basename, subject,
46
self.orig_body = orig_body
47
self.directive = directive
51
self.basename = basename
52
self.subject = subject
55
class MergeDirectiveHooks(hooks.Hooks):
56
"""Hooks for MergeDirective classes."""
59
hooks.Hooks.__init__(self)
60
self.create_hook(hooks.HookPoint('merge_request_body',
61
"Called with a MergeRequestBodyParams when a body is needed for"
62
" a merge request. Callbacks must return a body. If more"
63
" than one callback is registered, the output of one callback is"
64
" provided to the next.", (1, 15, 0), False))
67
class BaseMergeDirective(object):
31
from bzrlib.bundle import serializer as bundle_serializer
34
class MergeDirective(object):
68
36
"""A request to perform a merge into a branch.
70
This is the base class that all merge directive implementations
73
:cvar multiple_output_files: Whether or not this merge directive
74
stores a set of revisions in more than one file
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.
77
hooks = MergeDirectiveHooks()
79
multiple_output_files = False
50
_format_string = 'Bazaar merge directive format 1'
81
52
def __init__(self, revision_id, testament_sha1, time, timezone,
82
target_branch, patch=None, source_branch=None, message=None,
53
target_branch, patch=None, patch_type=None,
54
source_branch=None, message=None):
86
57
:param revision_id: The revision to merge
90
61
:param timezone: The timezone offset
91
62
:param target_branch: The branch to apply the merge to
92
63
:param patch: The text of a diff or bundle
64
:param patch_type: None, "diff" or "bundle", depending on the contents
93
66
:param source_branch: A public location to merge the revision from
94
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)
96
74
self.revision_id = revision_id
97
75
self.testament_sha1 = testament_sha1
99
77
self.timezone = timezone
100
78
self.target_branch = target_branch
101
79
self.patch = patch
80
self.patch_type = patch_type
102
81
self.source_branch = source_branch
103
82
self.message = message
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)
105
120
def to_lines(self):
106
121
"""Serialize as a list of lines
108
123
:return: a list of lines
110
raise NotImplementedError(self.to_lines)
113
"""Serialize as a set of files.
115
:return: List of tuples with filename and contents as lines
117
raise NotImplementedError(self.to_files)
119
def get_raw_bundle(self):
120
"""Return the bundle for this merge directive.
122
:return: bundle text or None if there is no bundle
126
def _to_lines(self, base_revision=False):
127
"""Serialize as a list of lines
129
:return: a list of lines
131
125
time_str = timestamp.format_patch_date(self.time, self.timezone)
132
126
stanza = rio.Stanza(revision_id=self.revision_id, timestamp=time_str,
133
127
target_branch=self.target_branch,
135
129
for key in ('source_branch', 'message'):
136
130
if self.__dict__[key] is not None:
137
131
stanza.add(key, self.__dict__[key])
139
stanza.add('base_revision_id', self.base_revision_id)
140
132
lines = ['# ' + self._format_string + '\n']
141
133
lines.extend(rio.to_patch_lines(stanza))
142
134
lines.append('# \n')
135
if self.patch is not None:
136
lines.extend(self.patch.splitlines(True))
145
def write_to_directory(self, path):
146
"""Write this merge directive to a series of files in a directory.
148
:param path: Filesystem path to write to
150
raise NotImplementedError(self.write_to_directory)
153
def from_objects(klass, repository, revision_id, time, timezone,
154
target_branch, patch_type='bundle',
155
local_target_branch=None, public_branch=None, message=None):
156
"""Generate a merge directive from various objects
158
:param repository: The repository containing the revision
159
:param revision_id: The revision to merge
160
:param time: The POSIX timestamp of the date the request was issued.
161
:param timezone: The timezone of the request
162
:param target_branch: The url of the branch to merge into
163
:param patch_type: 'bundle', 'diff' or None, depending on the type of
165
:param local_target_branch: a local copy of the target branch
166
:param public_branch: location of a public branch containing the target
168
:param message: Message to use when committing the merge
169
:return: The merge directive
171
The public branch is always used if supplied. If the patch_type is
172
not 'bundle', the public branch must be supplied, and will be verified.
174
If the message is not supplied, the message from revision_id will be
177
t_revision_id = revision_id
178
if revision_id == _mod_revision.NULL_REVISION:
180
t = testament.StrictTestament3.from_revision(repository, t_revision_id)
181
submit_branch = _mod_branch.Branch.open(target_branch)
182
if submit_branch.get_public_branch() is not None:
183
target_branch = submit_branch.get_public_branch()
184
if patch_type is None:
187
submit_revision_id = submit_branch.last_revision()
188
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
189
repository.fetch(submit_branch.repository, submit_revision_id)
190
graph = repository.get_graph()
191
ancestor_id = graph.find_unique_lca(revision_id,
193
type_handler = {'bundle': klass._generate_bundle,
194
'diff': klass._generate_diff,
195
None: lambda x, y, z: None }
196
patch = type_handler[patch_type](repository, revision_id,
199
if public_branch is not None and patch_type != 'bundle':
200
public_branch_obj = _mod_branch.Branch.open(public_branch)
201
if not public_branch_obj.repository.has_revision(revision_id):
202
raise errors.PublicBranchOutOfDate(public_branch,
205
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
206
patch, patch_type, public_branch, message)
208
def get_disk_name(self, branch):
209
"""Generate a suitable basename for storing this directive on disk
211
:param branch: The Branch this merge directive was generated fro
214
revno, revision_id = branch.last_revision_info()
215
if self.revision_id == revision_id:
218
revno = branch.get_revision_id_to_revno_map().get(self.revision_id,
220
nick = re.sub('(\W+)', '-', branch.nick).strip('-')
221
return '%s-%s' % (nick, '.'.join(str(n) for n in revno))
224
def _generate_diff(repository, revision_id, ancestor_id):
225
tree_1 = repository.revision_tree(ancestor_id)
226
tree_2 = repository.revision_tree(revision_id)
228
diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
232
def _generate_bundle(repository, revision_id, ancestor_id):
234
bundle_serializer.write_bundle(repository, revision_id,
238
139
def to_signed(self, branch):
239
140
"""Serialize as a signed string.
254
155
:return: an email message
256
157
mail_from = branch.get_config().username()
158
message = Message.Message()
159
message['To'] = mail_to
160
message['From'] = mail_from
257
161
if self.message is not None:
258
subject = self.message
162
message['Subject'] = self.message
260
164
revision = branch.repository.get_revision(self.revision_id)
261
subject = revision.message
165
message['Subject'] = revision.message
263
167
body = self.to_signed(branch)
265
169
body = ''.join(self.to_lines())
266
message = EmailMessage(mail_from, mail_to, subject, body)
170
message.set_payload(body)
269
def install_revisions(self, target_repo):
270
"""Install revisions and return the target revision"""
271
if not target_repo.has_revision(self.revision_id):
272
if self.patch_type == 'bundle':
273
info = bundle_serializer.read_bundle(
274
StringIO(self.get_raw_bundle()))
275
# We don't use the bundle's target revision, because
276
# MergeDirective.revision_id is authoritative.
278
info.install_revisions(target_repo, stream_input=False)
279
except errors.RevisionNotPresent:
280
# At least one dependency isn't present. Try installing
281
# missing revisions from the submit branch
284
_mod_branch.Branch.open(self.target_branch)
285
except errors.NotBranchError:
286
raise errors.TargetNotBranch(self.target_branch)
287
missing_revisions = []
288
bundle_revisions = set(r.revision_id for r in
290
for revision in info.real_revisions:
291
for parent_id in revision.parent_ids:
292
if (parent_id not in bundle_revisions and
293
not target_repo.has_revision(parent_id)):
294
missing_revisions.append(parent_id)
295
# reverse missing revisions to try to get heads first
297
unique_missing_set = set()
298
for revision in reversed(missing_revisions):
299
if revision in unique_missing_set:
301
unique_missing.append(revision)
302
unique_missing_set.add(revision)
303
for missing_revision in unique_missing:
304
target_repo.fetch(submit_branch.repository,
306
info.install_revisions(target_repo, stream_input=False)
308
source_branch = _mod_branch.Branch.open(self.source_branch)
309
target_repo.fetch(source_branch.repository, self.revision_id)
310
return self.revision_id
312
def compose_merge_request(self, mail_client, to, body, branch, tree=None):
313
"""Compose a request to merge this directive.
315
:param mail_client: The mail client to use for composing this request.
316
:param to: The address to compose the request to.
317
:param branch: The Branch that was used to produce this directive.
318
:param tree: The Tree (if any) for the Branch used to produce this
321
basename = self.get_disk_name(branch)
323
if self.message is not None:
324
subject += self.message
326
revision = branch.repository.get_revision(self.revision_id)
327
subject += revision.get_summary()
328
if getattr(mail_client, 'supports_body', False):
330
for hook in self.hooks['merge_request_body']:
331
params = MergeRequestBodyParams(body, orig_body, self,
332
to, basename, subject, branch,
335
elif len(self.hooks['merge_request_body']) > 0:
336
trace.warning('Cannot run merge_request_body hooks because mail'
337
' client %s does not support message bodies.',
338
mail_client.__class__.__name__)
339
mail_client.compose_merge_request(to, subject,
340
''.join(self.to_lines()),
344
class MergeDirective(BaseMergeDirective):
346
"""A request to perform a merge into a branch.
348
Designed to be serialized and mailed. It provides all the information
349
needed to perform a merge automatically, by providing at minimum a revision
350
bundle or the location of a branch.
352
The serialization format is robust against certain common forms of
353
deterioration caused by mailing.
355
The format is also designed to be patch-compatible. If the directive
356
includes a diff or revision bundle, it should be possible to apply it
357
directly using the standard patch program.
360
_format_string = 'Bazaar merge directive format 1'
362
def __init__(self, revision_id, testament_sha1, time, timezone,
363
target_branch, patch=None, patch_type=None,
364
source_branch=None, message=None, bundle=None):
367
:param revision_id: The revision to merge
368
:param testament_sha1: The sha1 of the testament of the revision to
370
:param time: The current POSIX timestamp time
371
:param timezone: The timezone offset
372
:param target_branch: The branch to apply the merge to
373
:param patch: The text of a diff or bundle
374
:param patch_type: None, "diff" or "bundle", depending on the contents
376
:param source_branch: A public location to merge the revision from
377
:param message: The message to use when committing this merge
379
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
380
timezone, target_branch, patch, source_branch, message)
381
if patch_type not in (None, 'diff', 'bundle'):
382
raise ValueError(patch_type)
383
if patch_type != 'bundle' and source_branch is None:
384
raise errors.NoMergeSource()
385
if patch_type is not None and patch is None:
386
raise errors.PatchMissing(patch_type)
387
self.patch_type = patch_type
389
def clear_payload(self):
391
self.patch_type = None
393
def get_raw_bundle(self):
397
if self.patch_type == 'bundle':
402
bundle = property(_bundle)
405
def from_lines(klass, lines):
406
"""Deserialize a MergeRequest from an iterable of lines
408
:param lines: An iterable of lines
409
:return: a MergeRequest
411
line_iter = iter(lines)
413
for line in line_iter:
414
if line.startswith('# Bazaar merge directive format '):
415
return _format_registry.get(line[2:].rstrip())._from_lines(
417
firstline = firstline or line.strip()
418
raise errors.NotAMergeDirective(firstline)
421
def _from_lines(klass, line_iter):
422
stanza = rio.read_patch_stanza(line_iter)
423
patch_lines = list(line_iter)
424
if len(patch_lines) == 0:
428
patch = ''.join(patch_lines)
430
bundle_serializer.read_bundle(StringIO(patch))
431
except (errors.NotABundle, errors.BundleNotSupported,
435
patch_type = 'bundle'
436
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
438
for key in ('revision_id', 'testament_sha1', 'target_branch',
439
'source_branch', 'message'):
441
kwargs[key] = stanza.get(key)
444
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
445
return MergeDirective(time=time, timezone=timezone,
446
patch_type=patch_type, patch=patch, **kwargs)
449
lines = self._to_lines()
450
if self.patch is not None:
451
lines.extend(self.patch.splitlines(True))
455
def _generate_bundle(repository, revision_id, ancestor_id):
457
bundle_serializer.write_bundle(repository, revision_id,
458
ancestor_id, s, '0.9')
461
def get_merge_request(self, repository):
462
"""Provide data for performing a merge
464
Returns suggested base, suggested target, and patch verification status
466
return None, self.revision_id, 'inapplicable'
469
class MergeDirective2(BaseMergeDirective):
471
_format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
473
def __init__(self, revision_id, testament_sha1, time, timezone,
474
target_branch, patch=None, source_branch=None, message=None,
475
bundle=None, base_revision_id=None):
476
if source_branch is None and bundle is None:
477
raise errors.NoMergeSource()
478
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
479
timezone, target_branch, patch, source_branch, message)
481
self.base_revision_id = base_revision_id
483
def _patch_type(self):
484
if self.bundle is not None:
486
elif self.patch is not None:
491
patch_type = property(_patch_type)
493
def clear_payload(self):
497
def get_raw_bundle(self):
498
if self.bundle is None:
501
return self.bundle.decode('base-64')
504
def _from_lines(klass, line_iter):
505
stanza = rio.read_patch_stanza(line_iter)
509
start = line_iter.next()
510
except StopIteration:
513
if start.startswith('# Begin patch'):
515
for line in line_iter:
516
if line.startswith('# Begin bundle'):
519
patch_lines.append(line)
522
patch = ''.join(patch_lines)
523
if start is not None:
524
if start.startswith('# Begin bundle'):
525
bundle = ''.join(line_iter)
527
raise errors.IllegalMergeDirectivePayload(start)
528
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
530
for key in ('revision_id', 'testament_sha1', 'target_branch',
531
'source_branch', 'message', 'base_revision_id'):
533
kwargs[key] = stanza.get(key)
536
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
537
kwargs['base_revision_id'] =\
538
kwargs['base_revision_id'].encode('utf-8')
539
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
543
lines = self._to_lines(base_revision=True)
544
if self.patch is not None:
545
lines.append('# Begin patch\n')
546
lines.extend(self.patch.splitlines(True))
547
if self.bundle is not None:
548
lines.append('# Begin bundle\n')
549
lines.extend(self.bundle.splitlines(True))
553
174
def from_objects(klass, repository, revision_id, time, timezone,
554
target_branch, include_patch=True, include_bundle=True,
555
local_target_branch=None, public_branch=None, message=None,
556
base_revision_id=None):
175
target_branch, patch_type='bundle',
176
local_target_branch=None, public_branch=None, message=None):
557
177
"""Generate a merge directive from various objects
559
179
:param repository: The repository containing the revision
561
181
:param time: The POSIX timestamp of the date the request was issued.
562
182
:param timezone: The timezone of the request
563
183
:param target_branch: The url of the branch to merge into
564
:param include_patch: If true, include a preview patch
565
:param include_bundle: If true, include a bundle
184
:param patch_type: 'bundle', 'diff' or None, depending on the type of
566
186
:param local_target_branch: a local copy of the target branch
567
187
:param public_branch: location of a public branch containing the target
569
189
:param message: Message to use when committing the merge
570
190
:return: The merge directive
572
The public branch is always used if supplied. If no bundle is
573
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.
575
195
If the message is not supplied, the message from revision_id will be
576
196
used for the commit.
580
repository.lock_write()
581
locked.append(repository)
582
t_revision_id = revision_id
583
if revision_id == 'null:':
585
t = testament.StrictTestament3.from_revision(repository,
587
submit_branch = _mod_branch.Branch.open(target_branch)
588
submit_branch.lock_read()
589
locked.append(submit_branch)
590
if submit_branch.get_public_branch() is not None:
591
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:
592
205
submit_revision_id = submit_branch.last_revision()
593
submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
594
graph = repository.get_graph(submit_branch.repository)
595
ancestor_id = graph.find_unique_lca(revision_id,
597
if base_revision_id is None:
598
base_revision_id = ancestor_id
599
if (include_patch, include_bundle) != (False, False):
600
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':
602
221
patch = klass._generate_diff(repository, revision_id,
608
bundle = klass._generate_bundle(repository, revision_id,
609
ancestor_id).encode('base-64')
613
if public_branch is not None and not include_bundle:
224
if public_branch is not None and patch_type != 'bundle':
614
225
public_branch_obj = _mod_branch.Branch.open(public_branch)
615
public_branch_obj.lock_read()
616
locked.append(public_branch_obj)
617
if not public_branch_obj.repository.has_revision(
226
if not public_branch_obj.repository.has_revision(revision_id):
619
227
raise errors.PublicBranchOutOfDate(public_branch,
621
testament_sha1 = t.as_sha1()
623
for entry in reversed(locked):
625
return klass(revision_id, testament_sha1, time, timezone,
626
target_branch, patch, public_branch, message, bundle,
629
def _verify_patch(self, repository):
630
calculated_patch = self._generate_diff(repository, self.revision_id,
631
self.base_revision_id)
632
# Convert line-endings to UNIX
633
stored_patch = re.sub('\r\n?', '\n', self.patch)
634
calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
635
# Strip trailing whitespace
636
calculated_patch = re.sub(' *\n', '\n', calculated_patch)
637
stored_patch = re.sub(' *\n', '\n', stored_patch)
638
return (calculated_patch == stored_patch)
640
def get_merge_request(self, repository):
641
"""Provide data for performing a merge
643
Returns suggested base, suggested target, and patch verification status
645
verified = self._maybe_verify(repository)
646
return self.base_revision_id, self.revision_id, verified
648
def _maybe_verify(self, repository):
649
if self.patch is not None:
650
if self._verify_patch(repository):
655
return 'inapplicable'
658
class MergeDirectiveFormatRegistry(registry.Registry):
660
def register(self, directive, format_string=None):
661
if format_string is None:
662
format_string = directive._format_string
663
registry.Registry.register(self, format_string, directive)
666
_format_registry = MergeDirectiveFormatRegistry()
667
_format_registry.register(MergeDirective)
668
_format_registry.register(MergeDirective2)
669
# 0.19 never existed. It got renamed to 0.90. But by that point, there were
670
# already merge directives in the wild that used 0.19. Registering with the old
671
# format string to retain compatibility with those merge directives.
672
_format_registry.register(MergeDirective2,
673
'Bazaar merge directive format 2 (Bazaar 0.19)')
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,