40
from ...git.urls import git_url_to_bzr_url
40
from ...git.refs import ref_to_branch_name
41
41
from ...lazy_import import lazy_import
42
42
lazy_import(globals(), """
43
43
from breezy.plugins.launchpad import (
48
47
from launchpadlib import uris
74
73
if url.startswith('lp:'):
76
regex = re.compile(r'([a-z]*\+)*(bzr\+ssh|http|ssh|git|https)'
77
r'://(bazaar|git).*\.launchpad\.net')
75
regex = re.compile('([a-z]*\+)*(bzr\+ssh|http|ssh|git|https)'
76
'://(bazaar|git).*.launchpad.net')
78
77
return bool(regex.match(url))
113
112
if self._mp.source_branch:
114
113
return self._mp.source_branch.bzr_identity
116
return git_url_to_bzr_url(
115
branch_name = ref_to_branch_name(
116
self._mp.source_git_path.encode('utf-8'))
117
return urlutils.join_segment_parameters(
117
118
self._mp.source_git_repository.git_identity,
118
ref=self._mp.source_git_path.encode('utf-8'))
120
def get_source_revision(self):
121
if self._mp.source_branch:
122
last_scanned_id = self._mp.source_branch.last_scanned_id
124
return last_scanned_id.encode('utf-8')
128
from breezy.git.mapping import default_mapping
129
git_repo = self._mp.source_git_repository
130
git_ref = git_repo.getRefByPath(path=self._mp.source_git_path)
131
sha = git_ref.commit_sha1
134
return default_mapping.revision_id_foreign_to_bzr(
119
{"branch": branch_name})
137
121
def get_target_branch_url(self):
138
122
if self._mp.target_branch:
139
123
return self._mp.target_branch.bzr_identity
141
return git_url_to_bzr_url(
125
branch_name = ref_to_branch_name(
126
self._mp.target_git_path.encode('utf-8'))
127
return urlutils.join_segment_parameters(
142
128
self._mp.target_git_repository.git_identity,
143
ref=self._mp.target_git_path.encode('utf-8'))
129
{"branch": branch_name})
149
135
def is_merged(self):
150
136
return (self._mp.queue_status == 'Merged')
153
return (self._mp.queue_status in ('Rejected', 'Superseded'))
156
self._mp.setStatus(status='Needs review')
158
138
def get_description(self):
159
139
return self._mp.description
162
142
self._mp.description = description
163
143
self._mp.lp_save()
165
def get_commit_message(self):
166
return self._mp.commit_message
168
def set_commit_message(self, commit_message):
169
self._mp.commit_message = commit_message
173
146
self._mp.setStatus(status='Rejected')
175
def can_be_merged(self):
176
if not self._mp.preview_diff:
179
return not bool(self._mp.preview_diff.conflicts)
181
def get_merged_by(self):
182
merge_reporter = self._mp.merge_reporter
183
if merge_reporter is None:
185
return merge_reporter.name
187
def get_merged_at(self):
188
return self._mp.date_merged
190
def merge(self, commit_message=None):
191
target_branch = _mod_branch.Branch.open(
192
self.get_target_branch_url())
193
source_branch = _mod_branch.Branch.open(
194
self.get_source_branch_url())
195
# TODO(jelmer): Ideally this would use a memorytree, but merge doesn't
197
# tree = target_branch.create_memorytree()
198
tmpdir = tempfile.mkdtemp()
200
tree = target_branch.create_checkout(
201
to_location=tmpdir, lightweight=True)
202
tree.merge_from_branch(source_branch)
203
tree.commit(commit_message or self._mp.commit_message)
205
shutil.rmtree(tmpdir)
207
def post_comment(self, body):
208
self._mp.createComment(content=body)
211
149
class Launchpad(Hoster):
212
150
"""The Launchpad hosting service."""
216
154
# https://bugs.launchpad.net/launchpad/+bug/397676
217
155
supports_merge_proposal_labels = False
219
supports_merge_proposal_commit_message = True
221
supports_allow_collaboration = False
223
merge_proposal_description_format = 'plain'
225
def __init__(self, service_root):
226
self._api_base_url = service_root
227
self._launchpad = None
231
if self._api_base_url == uris.LPNET_SERVICE_ROOT:
233
return 'Launchpad at %s' % self.base_url
237
if self._launchpad is None:
238
self._launchpad = lp_api.connect_launchpad(self._api_base_url, version='devel')
239
return self._launchpad
157
def __init__(self, staging=False):
158
self._staging = staging
160
lp_base_url = uris.STAGING_SERVICE_ROOT
162
lp_base_url = uris.LPNET_SERVICE_ROOT
163
self.launchpad = lp_api.connect_launchpad(lp_base_url)
242
166
def base_url(self):
243
return lp_api.uris.web_root_for_service_root(self._api_base_url)
167
return lp_api.uris.web_root_for_service_root(
168
str(self.launchpad._root_uri))
245
170
def __repr__(self):
246
return "Launchpad(service_root=%s)" % self._api_base_url
248
def get_current_user(self):
249
return self.launchpad.me.name
251
def get_user_url(self, username):
252
return self.launchpad.people[username].web_link
171
return "Launchpad(staging=%s)" % self._staging
254
173
def hosts(self, branch):
255
174
# TODO(jelmer): staging vs non-staging?
256
175
return plausible_launchpad_url(branch.user_url)
259
def probe_from_url(cls, url, possible_transports=None):
260
if plausible_launchpad_url(url):
261
return Launchpad(uris.LPNET_SERVICE_ROOT)
262
raise UnsupportedHoster(url)
178
def probe(cls, branch):
179
if plausible_launchpad_url(branch.user_url):
181
raise UnsupportedHoster(branch)
264
183
def _get_lp_git_ref_from_branch(self, branch):
265
184
url, params = urlutils.split_segment_parameters(branch.user_url)
293
212
return "~%s/%s" % (owner, project)
295
214
def _publish_git(self, local_branch, base_path, name, owner, project=None,
296
revision_id=None, overwrite=False, allow_lossy=True,
215
revision_id=None, overwrite=False, allow_lossy=True):
298
216
to_path = self._get_derived_git_path(base_path, owner, project)
299
217
to_transport = get_transport("git+ssh://git.launchpad.net/" + to_path)
306
224
if dir_to is None:
308
226
br_to = local_branch.create_clone_on_transport(
309
to_transport, revision_id=revision_id, name=name,
310
tag_selector=tag_selector)
227
to_transport, revision_id=revision_id, name=name)
311
228
except errors.NoRoundtrippingSupport:
312
229
br_to = local_branch.create_clone_on_transport(
313
230
to_transport, revision_id=revision_id, name=name,
314
lossy=True, tag_selector=tag_selector)
317
234
dir_to = dir_to.push_branch(
318
local_branch, revision_id, overwrite=overwrite, name=name,
319
tag_selector=tag_selector)
235
local_branch, revision_id, overwrite=overwrite, name=name)
320
236
except errors.NoRoundtrippingSupport:
321
237
if not allow_lossy:
323
239
dir_to = dir_to.push_branch(
324
240
local_branch, revision_id, overwrite=overwrite, name=name,
325
lossy=True, tag_selector=tag_selector)
326
242
br_to = dir_to.target_branch
328
244
"https://git.launchpad.net/%s/+ref/%s" % (to_path, name))
349
265
def _publish_bzr(self, local_branch, base_branch, name, owner,
350
266
project=None, revision_id=None, overwrite=False,
351
allow_lossy=True, tag_selector=None):
352
268
to_path = self._get_derived_bzr_path(base_branch, name, owner, project)
353
269
to_transport = get_transport("lp:" + to_path)
360
276
if dir_to is None:
361
277
br_to = local_branch.create_clone_on_transport(
362
to_transport, revision_id=revision_id, tag_selector=tag_selector)
278
to_transport, revision_id=revision_id)
364
280
br_to = dir_to.push_branch(
365
local_branch, revision_id, overwrite=overwrite,
366
tag_selector=tag_selector).target_branch
281
local_branch, revision_id, overwrite=overwrite).target_branch
367
282
return br_to, ("https://code.launchpad.net/" + to_path)
369
284
def _split_url(self, url):
381
296
def publish_derived(self, local_branch, base_branch, name, project=None,
382
297
owner=None, revision_id=None, overwrite=False,
383
allow_lossy=True, tag_selector=None):
384
299
"""Publish a branch to the site, derived from base_branch.
386
301
:param base_branch: branch to derive the new branch from
399
314
return self._publish_bzr(
400
315
local_branch, base_branch, name, project=project, owner=owner,
401
316
revision_id=revision_id, overwrite=overwrite,
402
allow_lossy=allow_lossy, tag_selector=tag_selector)
317
allow_lossy=allow_lossy)
403
318
elif base_vcs == 'git':
404
319
return self._publish_git(
405
320
local_branch, base_path, name, project=project, owner=owner,
406
321
revision_id=revision_id, overwrite=overwrite,
407
allow_lossy=allow_lossy, tag_selector=tag_selector)
322
allow_lossy=allow_lossy)
409
324
raise AssertionError('not a valid Launchpad URL')
471
386
def iter_instances(cls):
472
credential_store = lp_api.get_credential_store()
473
for service_root in set(uris.service_roots.values()):
474
auth_engine = lp_api.get_auth_engine(service_root)
475
creds = credential_store.load(auth_engine.unique_consumer_id)
476
if creds is not None:
477
yield cls(service_root)
479
389
def iter_my_proposals(self, status='open'):
480
390
statuses = status_to_lp_mp_statuses(status)
481
391
for mp in self.launchpad.me.getMergeProposals(status=statuses):
482
392
yield LaunchpadMergeProposal(mp)
484
def iter_my_forks(self):
485
# Launchpad doesn't really have the concept of "forks"
488
def get_proposal_by_url(self, url):
489
# Launchpad doesn't have a way to find a merge proposal by URL.
490
(scheme, user, password, host, port, path) = urlutils.parse_url(
492
LAUNCHPAD_CODE_DOMAINS = [
493
('code.%s' % domain) for domain in lp_uris.LAUNCHPAD_DOMAINS.values()]
494
if host not in LAUNCHPAD_CODE_DOMAINS:
495
raise UnsupportedHoster(url)
496
# TODO(jelmer): Check if this is a launchpad URL. Otherwise, raise
498
# See https://api.launchpad.net/devel/#branch_merge_proposal
500
# https://api.launchpad.net/devel/~<author.name>/<project.name>/<branch.name>/+merge/<id>
501
api_url = str(self.launchpad._root_uri) + path
502
mp = self.launchpad.load(api_url)
503
return LaunchpadMergeProposal(mp)
506
395
class LaunchpadBazaarMergeProposalBuilder(MergeProposalBuilder):
508
def __init__(self, lp_host, source_branch, target_branch,
397
def __init__(self, lp_host, source_branch, target_branch, message=None,
509
398
staging=None, approve=None, fixes=None):
512
401
:param source_branch: The branch to propose for merging.
513
402
:param target_branch: The branch to merge into.
403
:param message: The commit message to use. (May be None.)
514
404
:param staging: If True, propose the merge against staging instead of
516
406
:param approve: If True, mark the new proposal as approved immediately.
531
421
self.target_branch = target_branch
532
422
self.target_branch_lp = self.launchpad.branches.getByUrl(
533
423
url=target_branch.user_url)
424
self.commit_message = message
534
425
self.approve = approve
535
426
self.fixes = fixes
537
428
def get_infotext(self):
538
429
"""Determine the initial comment for the merge proposal."""
430
if self.commit_message is not None:
431
return self.commit_message.strip().encode('utf-8')
539
432
info = ["Source: %s\n" % self.source_branch_lp.bzr_identity]
540
433
info.append("Target: %s\n" % self.target_branch_lp.bzr_identity)
541
434
return ''.join(info)
568
461
def check_proposal(self):
569
462
"""Check that the submission is sensible."""
570
463
if self.source_branch_lp.self_link == self.target_branch_lp.self_link:
571
raise errors.CommandError(
464
raise errors.BzrCommandError(
572
465
'Source and target branches must be different.')
573
466
for mp in self.source_branch_lp.landing_targets:
574
467
if mp.queue_status in ('Merged', 'Rejected'):
587
480
revid=self.source_branch.last_revision())
589
482
def create_proposal(self, description, reviewers=None, labels=None,
590
prerequisite_branch=None, commit_message=None,
591
work_in_progress=False, allow_collaboration=False):
483
prerequisite_branch=None):
592
484
"""Perform the submission."""
594
raise LabelsUnsupported(self)
486
raise LabelsUnsupported()
595
487
if prerequisite_branch is not None:
596
488
prereq = self.launchpad.branches.getByUrl(
597
489
url=prerequisite_branch.user_url)
600
492
if reviewers is None:
604
for reviewer in reviewers:
606
reviewer_obj = self.launchpad.people.getByEmail(email=reviewer)
608
reviewer_obj = self.launchpad.people[reviewer]
609
reviewer_objs.append(reviewer_obj)
611
495
mp = _call_webservice(
612
496
self.source_branch_lp.createMergeProposal,
613
497
target_branch=self.target_branch_lp,
614
498
prerequisite_branch=prereq,
615
499
initial_comment=description.strip(),
616
commit_message=commit_message,
617
needs_review=(not work_in_progress),
618
reviewers=[reviewer.self_link for reviewer in reviewer_objs],
619
review_types=['' for reviewer in reviewer_objs])
500
commit_message=self.commit_message,
501
reviewers=[self.launchpad.people[reviewer].self_link
502
for reviewer in reviewers],
503
review_types=[None for reviewer in reviewers])
620
504
except WebserviceFailure as e:
622
506
if (b'There is already a branch merge proposal '
638
522
class LaunchpadGitMergeProposalBuilder(MergeProposalBuilder):
640
def __init__(self, lp_host, source_branch, target_branch,
524
def __init__(self, lp_host, source_branch, target_branch, message=None,
641
525
staging=None, approve=None, fixes=None):
644
528
:param source_branch: The branch to propose for merging.
645
529
:param target_branch: The branch to merge into.
530
:param message: The commit message to use. (May be None.)
646
531
:param staging: If True, propose the merge against staging instead of
648
533
:param approve: If True, mark the new proposal as approved immediately.
664
549
self.target_branch = target_branch
665
550
(self.target_repo_lp, self.target_branch_lp) = (
666
551
self.lp_host._get_lp_git_ref_from_branch(target_branch))
552
self.commit_message = message
667
553
self.approve = approve
668
554
self.fixes = fixes
670
556
def get_infotext(self):
671
557
"""Determine the initial comment for the merge proposal."""
558
if self.commit_message is not None:
559
return self.commit_message.strip().encode('utf-8')
672
560
info = ["Source: %s\n" % self.source_branch.user_url]
673
561
info.append("Target: %s\n" % self.target_branch.user_url)
674
562
return ''.join(info)
701
589
def check_proposal(self):
702
590
"""Check that the submission is sensible."""
703
591
if self.source_branch_lp.self_link == self.target_branch_lp.self_link:
704
raise errors.CommandError(
592
raise errors.BzrCommandError(
705
593
'Source and target branches must be different.')
706
594
for mp in self.source_branch_lp.landing_targets:
707
595
if mp.queue_status in ('Merged', 'Rejected'):
721
609
revid=self.source_branch.last_revision())
723
611
def create_proposal(self, description, reviewers=None, labels=None,
724
prerequisite_branch=None, commit_message=None,
725
work_in_progress=False, allow_collaboration=False):
612
prerequisite_branch=None):
726
613
"""Perform the submission."""
728
raise LabelsUnsupported(self)
615
raise LabelsUnsupported()
729
616
if prerequisite_branch is not None:
730
617
(prereq_repo_lp, prereq_branch_lp) = (
731
618
self.lp_host._get_lp_git_ref_from_branch(prerequisite_branch))
739
626
merge_target=self.target_branch_lp,
740
627
merge_prerequisite=prereq_branch_lp,
741
628
initial_comment=description.strip(),
742
commit_message=commit_message,
743
needs_review=(not work_in_progress),
629
commit_message=self.commit_message,
744
631
reviewers=[self.launchpad.people[reviewer].self_link
745
632
for reviewer in reviewers],
746
633
review_types=[None for reviewer in reviewers])
764
651
def modified_files(old_tree, new_tree):
765
652
"""Return a list of paths in the new tree with modified contents."""
766
for change in new_tree.iter_changes(old_tree):
767
if change.changed_content and change.kind[1] == 'file':
653
for f, (op, path), c, v, p, n, (ok, k), e in new_tree.iter_changes(
655
if c and k == 'file':