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
27
revision as _mod_revision,
34
32
from bzrlib.bundle import (
35
33
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):
68
"""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
77
hooks = MergeDirectiveHooks()
79
multiple_output_files = False
37
class _BaseMergeDirective(object):
81
39
def __init__(self, revision_id, testament_sha1, time, timezone,
82
40
target_branch, patch=None, source_branch=None, message=None,
196
124
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,
127
if public_branch is not None and patch_type != 'bundle':
128
public_branch_obj = _mod_branch.Branch.open(public_branch)
129
if not public_branch_obj.repository.has_revision(revision_id):
130
raise errors.PublicBranchOutOfDate(public_branch,
205
133
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
206
134
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
137
def _generate_diff(repository, revision_id, ancestor_id):
225
138
tree_1 = repository.revision_tree(ancestor_id)
274
190
StringIO(self.get_raw_bundle()))
275
191
# We don't use the bundle's target revision, because
276
192
# 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)
193
info.install_revisions(target_repo)
308
195
source_branch = _mod_branch.Branch.open(self.source_branch)
309
196
target_repo.fetch(source_branch.repository, self.revision_id)
310
197
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):
200
class MergeDirective(_BaseMergeDirective):
346
202
"""A request to perform a merge into a branch.
376
232
:param source_branch: A public location to merge the revision from
377
233
:param message: The message to use when committing this merge
379
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
235
_BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
380
236
timezone, target_branch, patch, source_branch, message)
381
if patch_type not in (None, 'diff', 'bundle'):
382
raise ValueError(patch_type)
237
assert patch_type in (None, 'diff', 'bundle'), patch_type
383
238
if patch_type != 'bundle' and source_branch is None:
384
239
raise errors.NoMergeSource()
385
240
if patch_type is not None and patch is None:
458
315
ancestor_id, s, '0.9')
459
316
return s.getvalue()
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)'
319
class MergeDirective2(_BaseMergeDirective):
321
_format_string = 'Bazaar merge directive format 2 (Bazaar 0.18)'
473
323
def __init__(self, revision_id, testament_sha1, time, timezone,
474
324
target_branch, patch=None, source_branch=None, message=None,
475
bundle=None, base_revision_id=None):
476
326
if source_branch is None and bundle is None:
477
327
raise errors.NoMergeSource()
478
BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
328
_BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
479
329
timezone, target_branch, patch, source_branch, message)
480
330
self.bundle = bundle
481
self.base_revision_id = base_revision_id
483
332
def _patch_type(self):
484
333
if self.bundle is not None:
528
377
time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
530
379
for key in ('revision_id', 'testament_sha1', 'target_branch',
531
'source_branch', 'message', 'base_revision_id'):
380
'source_branch', 'message'):
533
382
kwargs[key] = stanza.get(key)
536
385
kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
537
kwargs['base_revision_id'] =\
538
kwargs['base_revision_id'].encode('utf-8')
539
386
return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
542
389
def to_lines(self):
543
lines = self._to_lines(base_revision=True)
390
lines = self._to_lines()
544
391
if self.patch is not None:
545
392
lines.append('# Begin patch\n')
546
393
lines.extend(self.patch.splitlines(True))
561
407
:param time: The POSIX timestamp of the date the request was issued.
562
408
:param timezone: The timezone of the request
563
409
: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
410
:param patch_type: 'bundle', 'diff' or None, depending on the type of
566
412
:param local_target_branch: a local copy of the target branch
567
413
:param public_branch: location of a public branch containing the target
569
415
:param message: Message to use when committing the merge
570
416
: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.
418
The public branch is always used if supplied. If the patch_type is
419
not 'bundle', the public branch must be supplied, and will be verified.
575
421
If the message is not supplied, the message from revision_id will be
576
422
used for the commit.
589
435
locked.append(submit_branch)
590
436
if submit_branch.get_public_branch() is not None:
591
437
target_branch = submit_branch.get_public_branch()
592
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)
602
patch = klass._generate_diff(repository, revision_id,
438
if patch_type is None:
608
bundle = klass._generate_bundle(repository, revision_id,
609
ancestor_id).encode('base-64')
442
submit_revision_id = submit_branch.last_revision()
443
submit_revision_id = _mod_revision.ensure_null(
445
repository.fetch(submit_branch.repository, submit_revision_id)
446
graph = repository.get_graph()
447
ancestor_id = graph.find_unique_lca(revision_id,
449
if patch_type in ('bundle', 'diff'):
450
patch = klass._generate_diff(repository, revision_id,
452
if patch_type == 'bundle':
453
bundle = klass._generate_bundle(repository, revision_id,
454
ancestor_id).encode('base-64')
613
if public_branch is not None and not include_bundle:
614
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(
619
raise errors.PublicBranchOutOfDate(public_branch,
621
testament_sha1 = t.as_sha1()
458
if public_branch is not None and patch_type != 'bundle':
459
public_branch_obj = _mod_branch.Branch.open(public_branch)
460
public_branch_obj.lock_read()
461
locked.append(public_branch_obj)
462
if not public_branch_obj.repository.has_revision(
464
raise errors.PublicBranchOutOfDate(public_branch,
623
467
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'
469
return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
470
patch, public_branch, message, bundle)
658
472
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)
474
def register(self, directive):
475
registry.Registry.register(self, directive._format_string, directive)
666
478
_format_registry = MergeDirectiveFormatRegistry()
667
479
_format_registry.register(MergeDirective)
668
480
_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)')