== Create a new merge proposal == Branch merge proposals can be created through the API. >>> from canonical.launchpad.testing.pages import webservice_for_person >>> from canonical.launchpad.webapp.interfaces import OAuthPermission >>> from lazr.restful.testing.webservice import pprint_entry >>> login('admin@canonical.com') >>> target = factory.makeBranch() >>> from canonical.launchpad.webapp.servers import WebServiceTestRequest >>> request = WebServiceTestRequest(version="beta") >>> request.processInputs() >>> from lazr.restful.utils import get_current_web_service_request >>> request = get_current_web_service_request() >>> def fix_url(url): ... """Convert a browser request to a web service client request. ... This is a bit of a hack, but it's the simplest way to get a ... URL that the web service client will respect.""" ... return url.replace("launchpad.dev/api/", "api.launchpad.dev/") >>> target_url = fix_url(str(canonical_url( ... target, request=request, rootsite='api'))) >>> source = factory.makeBranchTargetBranch(target.target) >>> source_url = fix_url(str( ... canonical_url(source, request=request, rootsite='api'))) >>> prerequisite = factory.makeBranchTargetBranch(target.target) >>> prerequisite_url = fix_url(str(canonical_url( ... prerequisite, request=request, rootsite='api'))) >>> registrant = source.registrant >>> reviewer_url = fix_url(str(canonical_url( ... factory.makePerson(), request=request, rootsite='api'))) >>> logout() >>> registrant_webservice = webservice_for_person( ... registrant, permission=OAuthPermission.WRITE_PUBLIC) >>> bmp_result = registrant_webservice.named_post( ... source_url, 'createMergeProposal', target_branch=target_url, ... prerequisite_branch=prerequisite_url, ... initial_comment='Merge\nit!', needs_review=True, ... commit_message='It was merged!\n', reviewers=[reviewer_url], ... review_types=['green']) >>> bmp_url = bmp_result.getHeader('Location') >>> bmp = registrant_webservice.get(bmp_url).jsonBody() >>> pprint_entry(bmp) address: u'mp+...@code.launchpad.dev' all_comments_collection_link: u'http://api.launchpad.dev/devel/~.../+merge/.../all_comments' commit_message: u'It was merged!\n' date_created: u'...' date_merged: None date_queued: None date_review_requested: u'...' date_reviewed: None description: u'Merge\nit!' merge_reporter_link: None merged_revno: None prerequisite_branch_link: u'http://api.launchpad.dev/devel/~...' preview_diff_link: None private: False queue_position: None queue_status: u'Needs review' queued_revid: None queuer_link: None registrant_link: u'http://api.launchpad.dev/devel/~person-name...' resource_type_link: u'http://api.launchpad.dev/devel/#branch_merge_proposal' reviewed_revid: None reviewer_link: None self_link: u'http://api.launchpad.dev/devel/~.../+merge/...' source_branch_link: u'http://api.launchpad.dev/devel/~...' superseded_by_link: None supersedes_link: None target_branch_link: u'http://api.launchpad.dev/devel/~...' votes_collection_link: u'http://api.launchpad.dev/devel/~.../+merge/.../votes' web_link: u'http://code.../~.../+merge/...' If we try and create the merge proposal again, we should get a ValueError. >>> print registrant_webservice.named_post( ... source_url, 'createMergeProposal', target_branch=target_url, ... prerequisite_branch=prerequisite_url, ... initial_comment='Merge\nit!', needs_review=True, ... commit_message='It was merged!\n', reviewers=[reviewer_url], ... review_types=['green']) HTTP/1.1 400 Bad Request ... There is already a branch merge proposal registered for branch ... to land on ... that is still active. Our review request is listed in the votes collection. >>> votes = webservice.get( ... bmp['votes_collection_link']).jsonBody() >>> pprint_entry(votes['entries'][0]) branch_merge_proposal_link: u'http://api.launchpad.dev/devel/~.../+merge/...' comment_link: None date_created: u'...' is_pending: True registrant_link: u'http://api.launchpad.dev/devel/~person-name...' resource_type_link: u'http://api.launchpad.dev/devel/#code_review_vote_reference' review_type: u'green' reviewer_link: u'http://api.launchpad.dev/devel/~person-name...' self_link: u'http://api.launchpad.dev/devel/~...' == Get an existing merge proposal == Branch merge proposals can be fetched through the API. >>> login('admin@canonical.com') >>> from lp.code.tests.helpers import ( ... make_merge_proposal_without_reviewers) >>> fixit_proposal = make_merge_proposal_without_reviewers(factory) >>> fixit_proposal.source_branch.owner.name = 'source' >>> fixit_proposal.source_branch.name = 'fix-it' >>> fixit_proposal.target_branch.owner.name = 'target' >>> fixit_proposal.target_branch.name = 'trunk' >>> fooix = fixit_proposal.source_branch.product >>> fooix.name = 'fooix' >>> from lp.code.enums import CodeReviewVote >>> comment = factory.makeCodeReviewComment( ... subject='Looks good', body='This is great work', ... vote=CodeReviewVote.APPROVE, vote_tag='code', ... merge_proposal=fixit_proposal) >>> comment2 = factory.makeCodeReviewComment( ... subject='Not really', body='This is mediocre work.', ... vote=CodeReviewVote.ABSTAIN, parent=comment, ... merge_proposal=fixit_proposal) >>> transaction.commit() >>> proposal_url = fix_url(canonical_url( ... fixit_proposal, request=request, rootsite='api')) >>> new_person = factory.makePerson() >>> target_owner = fixit_proposal.target_branch.owner >>> logout() We use the webservice as an unrelated, unprivileged user. >>> webservice = webservice_for_person( ... new_person, permission=OAuthPermission.READ_PUBLIC) >>> merge_proposal = webservice.get(proposal_url).jsonBody() >>> pprint_entry(merge_proposal) address: u'mp+...@code.launchpad.dev' all_comments_collection_link: u'http://.../~source/fooix/fix-it/+merge/.../all_comments' commit_message: None date_created: ... date_merged: None date_queued: None date_review_requested: None date_reviewed: None description: None merge_reporter_link: None merged_revno: None prerequisite_branch_link: None preview_diff_link: None private: False queue_position: None queue_status: u'Work in progress' queued_revid: None queuer_link: None registrant_link: u'http://.../~person-name...' resource_type_link: u'http://.../#branch_merge_proposal' reviewed_revid: None reviewer_link: None self_link: u'http://.../~source/fooix/fix-it/+merge/...' source_branch_link: u'http://.../~source/fooix/fix-it' superseded_by_link: None supersedes_link: None target_branch_link: u'http://.../~target/fooix/trunk' votes_collection_link: u'http://.../~source/fooix/fix-it/+merge/.../votes' web_link: u'http://code.../~source/fooix/fix-it/+merge/...' == Read the comments == The comments on a branch merge proposal are exposed through the API. >>> all_comments = webservice.get( ... merge_proposal['all_comments_collection_link']).jsonBody() >>> print len(all_comments['entries']) 2 >>> pprint_entry(all_comments['entries'][0]) as_quoted_email: u'> This is great work' author_link: u'http://api.launchpad.dev/devel/~...' branch_merge_proposal_link: u'http://.../~source/fooix/fix-it/+merge/...' date_created: u'...' id: ... message_body: u'This is great work' resource_type_link: u'http://.../#code_review_comment' self_link: u'http://.../~source/fooix/fix-it/+merge/.../comments/...' title: u'Comment on proposed merge of lp://dev/~source/fooix/fix-it into lp://dev/~target/fooix/trunk' vote: u'Approve' vote_tag: u'code' web_link: u'http://code.../~source/fooix/fix-it/+merge/.../comments/...' >>> comment_2 = webservice.named_get( ... merge_proposal['self_link'], 'getComment', id=2).jsonBody() >>> pprint_entry(comment_2) as_quoted_email: u'> This is mediocre work.' author_link: u'http://api.launchpad.dev/devel/~...' branch_merge_proposal_link: u'http://.../~source/fooix/fix-it/+merge/...' date_created: u'...' id: ... message_body: u'This is mediocre work.' resource_type_link: u'http://.../#code_review_comment' self_link: u'http://.../~source/fooix/fix-it/+merge/.../comments/...' title: ... vote: u'Abstain' vote_tag: None web_link: u'http://code.../~source/fooix/fix-it/+merge/.../comments/...' == Check the votes == The votes on a branch merge proposal can be checked through the API. >>> votes = webservice.get( ... merge_proposal['votes_collection_link']).jsonBody()['entries'] >>> print len(votes) 2 >>> pprint_entry(votes[0]) branch_merge_proposal_link: u'http://.../~source/fooix/fix-it/+merge/...' comment_link: u'http://.../~source/fooix/fix-it/+merge/.../comments/...' date_created: u'...' is_pending: False registrant_link: u'http://.../~person-name...' resource_type_link: u'http://.../#code_review_vote_reference' review_type: u'code' reviewer_link: u'http://.../~person-name...' self_link: u'http://.../~source/fooix/fix-it/+merge/.../+review/...' == Performing a Review == A review can be performed through the API. A review can be requested of the person 'target'. >>> reviewer_webservice = webservice_for_person( ... target_owner, permission=OAuthPermission.WRITE_PUBLIC) >>> person = webservice.get('/~target').jsonBody() >>> reviewer = reviewer_webservice.named_post( ... merge_proposal['self_link'], 'nominateReviewer', ... reviewer=person['self_link'], review_type='code') >>> print reviewer HTTP/1.1 200 Ok ... >>> reviewer_entry = reviewer.jsonBody() >>> pprint_entry(reviewer_entry) branch_merge_proposal_link: u'http://.../~source/fooix/fix-it/+merge/...' comment_link: None date_created: u'...' is_pending: True registrant_link: u'http://.../~target' resource_type_link: u'http://.../#code_review_vote_reference' review_type: u'code' reviewer_link: u'http://.../~target' self_link: u'http://.../~source/fooix/fix-it/+merge/.../+review/...' >>> vote = reviewer_webservice.get(reviewer_entry['self_link']) >>> print vote HTTP/1.1 200 Ok ... Now the code review should be made. >>> comment_result = reviewer_webservice.named_post( ... merge_proposal['self_link'], 'createComment', ... subject='Great work', content='This is great work', ... vote=CodeReviewVote.APPROVE.title, review_type='code') >>> comment_link = comment_result.getHeader('Location') >>> comment = reviewer_webservice.get(comment_link).jsonBody() >>> pprint_entry(comment) as_quoted_email: u'> This is great work' author_link: u'http://api.launchpad.dev/devel/~...' branch_merge_proposal_link: u'http://.../~source/fooix/fix-it/+merge/...' date_created: u'...' id: ... message_body: u'This is great work' resource_type_link: u'http://.../#code_review_comment' self_link: u'http://.../~source/fooix/fix-it/+merge/.../comments/...' title: ... vote: u'Approve' vote_tag: u'code' web_link: u'http://code.../~source/fooix/fix-it/+merge/.../comments/...' In fact, now that the votes indicate approval, we might as well set the merge proposal status to "Approved" as well. >>> _unused = reviewer_webservice.named_post( ... merge_proposal['self_link'], 'setStatus', ... status=u'Approved', revid=u'25') >>> merge_proposal = reviewer_webservice.get( ... merge_proposal['self_link']).jsonBody() >>> print merge_proposal['queue_status'] Approved >>> print merge_proposal['reviewed_revid'] 25 However, there may have been breakage in the branch, and we need to revert back to "Work In Progress" and not specify the revision_id. >>> _unused = reviewer_webservice.named_post( ... merge_proposal['self_link'], 'setStatus', ... status=u'Work in progress') >>> merge_proposal = reviewer_webservice.get( ... merge_proposal['self_link']).jsonBody() >>> print merge_proposal['queue_status'] Work in progress >>> print merge_proposal['reviewed_revid'] None == Updating the preview diff == The merge proposal can now be updated with the diff that reflects what the merge would look like if the source branch was merged into the target branch. >>> diff_content = '''\ ... === modified file 'fooix.txt' ... --- fooix.txt\t2009-01-01 12:00:00 +0000 ... +++ fooix.txt\t2009-02-02 12:34:56 +0000 ... @@ -206,7 +206,7 @@ ... original ... -removed ... +added ... ''' >>> response = reviewer_webservice.named_post( ... merge_proposal['self_link'], 'updatePreviewDiff', ... diff_content=diff_content, ... source_revision_id='rev-a', ... target_revision_id='rev-b', conflicts='oh, no conflicts') >>> print response HTTP/1.1 200 Ok ... Content-Type: application/json Vary: ... ... The diff is now visible through the merge proposal. >>> merge_proposal = webservice.get(proposal_url).jsonBody() >>> preview_diff = webservice.get( ... merge_proposal['preview_diff_link']).jsonBody() >>> pprint_entry(preview_diff) added_lines_count: 0 branch_merge_proposal_link: u'http://.../~source/fooix/fix-it/+merge/...' conflicts: u'oh, no conflicts' diff_lines_count: 7 diff_text_link: u'http://.../~source/fooix/fix-it/+merge/.../+preview-diff/diff_text' diffstat: {u'fooix.txt': [0, 0]} prerequisite_revision_id: None removed_lines_count: 0 resource_type_link: u'http://.../#preview_diff' self_link: u'http://.../~source/fooix/fix-it/+merge/.../+preview-diff' source_revision_id: u'rev-a' stale: True target_revision_id: u'rev-b' It is possible that the diff will be empty. >>> response = reviewer_webservice.named_post( ... merge_proposal['self_link'], 'updatePreviewDiff', ... diff_content='', ... diff_stat='', source_revision_id='rev-c', ... target_revision_id='rev-d', conflicts=None) >>> print response HTTP/1.1 200 Ok ... Content-Type: application/json Vary: ... ... == Getting a Project's Pending Merge Proposals == It is possible to view all of a project's merge proposals or filter the proposals by their status. >>> def print_proposal(proposal): ... print proposal['self_link'] + ' - ' + \ ... proposal['queue_status'] >>> proposals = webservice.named_get( ... '/fooix', 'getMergeProposals').jsonBody() >>> for proposal in proposals['entries']: ... print_proposal(proposal) http://.../~source/fooix/fix-it/+merge/... - Work in progress Or I can look for anything that is approved. >>> login('admin@canonical.com') >>> from lp.code.enums import BranchMergeProposalStatus >>> fixit_proposal.approveBranch(fixit_proposal.target_branch.owner, '1') >>> logout() >>> def print_proposals(webservice, url, status=None): ... proposals = webservice.named_get( ... url, 'getMergeProposals', ... status=status).jsonBody() ... for proposal in proposals['entries']: ... print_proposal(proposal) >>> print_proposals( ... webservice, url='/fooix', ... status=[BranchMergeProposalStatus.CODE_APPROVED.title]) http://.../~source/fooix/fix-it/+merge/... - Approved If the branch is private it is not visible to an unpriveleged user. >>> login('admin@canonical.com') >>> from zope.security.proxy import removeSecurityProxy >>> removeSecurityProxy(fixit_proposal.source_branch).explicitly_private = True >>> branch_owner = fixit_proposal.source_branch.owner >>> logout() >>> print_proposals( ... webservice, url='/fooix', ... status=[BranchMergeProposalStatus.CODE_APPROVED.title]) If we get a webservice for the owner of the source branch, then they can see the proposal if they have allowed the API to access private bits. >>> service = webservice_for_person( ... branch_owner, permission=OAuthPermission.READ_PRIVATE) >>> print_proposals( ... service, url='/fooix', ... status=[BranchMergeProposalStatus.CODE_APPROVED.title]) http://.../~source/fooix/fix-it/+merge/... - Approved >>> login('admin@canonical.com') >>> removeSecurityProxy(fixit_proposal.source_branch).explicitly_private = False >>> logout() == Getting a Person's Pending Merge Proposals == It is possible to view all of a person's merge proposals or filter their proposals by their status. >>> proposals = webservice.named_get('/~source', 'getMergeProposals', ... ).jsonBody() >>> print_proposals(service, url='/~source') http://.../~source/fooix/fix-it/+merge/... - Approved The person's proposals can also be filtered by status. >>> login('admin@canonical.com') >>> fixit_proposal.rejectBranch(fixit_proposal.target_branch.owner, '1') >>> logout() >>> print_proposals(webservice, url='/~source', ... status=[BranchMergeProposalStatus.REJECTED.title]) http://.../~source/fooix/fix-it/+merge/... - Rejected == Getting a Project Group's Merge Proposals == Getting the merge proposals for a project group will get all the proposals for all the projects that are part of the project group. >>> login('admin@canonical.com') >>> project = factory.makeProject(name='widgets') >>> fooix.project = project >>> blob = factory.makeProduct(name='blob', project=project) >>> proposal = factory.makeBranchMergeProposal( ... product=blob, set_state=BranchMergeProposalStatus.NEEDS_REVIEW) >>> proposal.source_branch.owner.name = 'mary' >>> proposal.source_branch.name = 'bar' >>> logout() By default only work in progress, needs review and approved proposals are returned. >>> print_proposals(webservice, url='/widgets') http://.../~mary/blob/bar/+merge/... - Needs review The proposals can also be filtered by status. >>> print_proposals(webservice, url='/widgets', ... status=[BranchMergeProposalStatus.REJECTED.title]) http://.../~source/fooix/fix-it/+merge/... - Rejected == Getting Merge Proposals a Person has been Asked To Review == It's good to be able to find out which proposals you have been asked to review. >>> login('admin@canonical.com') >>> from lp.code.enums import BranchMergeProposalStatus First we create a review owned by someone else and requested of 'target' which is the one we want the method to return. >>> source_branch = factory.makeBranch(owner=branch_owner, ... product=blob, name="foo") >>> target_branch = factory.makeBranch(owner=target_owner, ... product=blob, name="bar") >>> proposal = factory.makeBranchMergeProposal( ... target_branch=target_branch, ... product=blob, set_state=BranchMergeProposalStatus.NEEDS_REVIEW, ... registrant=branch_owner, source_branch=source_branch) >>> proposal.nominateReviewer(target_owner, branch_owner) And then we propose a merge the other way, so that the owner is target, but they have not been asked to review, meaning that the method shouldn't return this review. >>> proposal = factory.makeBranchMergeProposal( ... target_branch=source_branch, ... product=blob, set_state=BranchMergeProposalStatus.NEEDS_REVIEW, ... registrant=target_owner, source_branch=target_branch) >>> proposal.nominateReviewer(branch_owner, target_owner) >>> logout() >>> proposals = webservice.named_get('/~target', 'getRequestedReviews' ... ).jsonBody() >>> for proposal in proposals['entries']: ... print_proposal(proposal) http://.../~source/blob/foo/+merge/4 - Needs review