Merge lp:~jelmer/brz/land into lp:brz

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: 7301
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz/land
Merge into: lp:brz
Diff against target: 265 lines (+129/-1)
7 files modified
breezy/plugins/propose/__init__.py (+1/-0)
breezy/plugins/propose/cmds.py (+13/-0)
breezy/plugins/propose/github.py (+8/-0)
breezy/plugins/propose/gitlabs.py (+44/-0)
breezy/plugins/propose/launchpad.py (+36/-0)
breezy/plugins/propose/propose.py (+24/-1)
doc/en/release-notes/brz-3.1.txt (+3/-0)
To merge this branch: bzr merge lp:~jelmer/brz/land
Reviewer Review Type Date Requested Status
Jelmer Vernooij Approve
Review via email: mp+366609@code.launchpad.net

Commit message

Add a 'brz land' subcommand.

Description of the change

Add a 'brz land' subcommand.

This allows merging existing merge proposals on Launchpad, GitHub and GitLab.

To post a comment you must log in.
Jelmer Vernooij (jelmer) :
review: Approve
lp:~jelmer/brz/land updated
7301. By Jelmer Vernooij on 2019-06-03

Merge trunk.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'breezy/plugins/propose/__init__.py'
2--- breezy/plugins/propose/__init__.py 2019-01-02 19:21:51 +0000
3+++ breezy/plugins/propose/__init__.py 2019-06-03 23:27:22 +0000
4@@ -21,6 +21,7 @@
5 from ...commands import plugin_cmds
6
7 plugin_cmds.register_lazy("cmd_propose_merge", ["propose"], __name__ + ".cmds")
8+plugin_cmds.register_lazy("cmd_land_merge_proposal", ["land"], __name__ + ".cmds")
9 plugin_cmds.register_lazy("cmd_publish_derived", ['publish'], __name__ + ".cmds")
10 plugin_cmds.register_lazy("cmd_find_merge_proposal", ['find-proposal'], __name__ + ".cmds")
11 plugin_cmds.register_lazy("cmd_github_login", ["gh-login"], __name__ + ".cmds")
12
13=== modified file 'breezy/plugins/propose/cmds.py'
14--- breezy/plugins/propose/cmds.py 2019-04-27 15:53:53 +0000
15+++ breezy/plugins/propose/cmds.py 2019-06-03 23:27:22 +0000
16@@ -340,3 +340,16 @@
17 for instance in hoster_cls.iter_instances():
18 for mp in instance.iter_my_proposals(status=status):
19 self.outf.write('%s\n' % mp.url)
20+
21+
22+class cmd_land_merge_proposal(Command):
23+ __doc__ = """Land a merge proposal."""
24+
25+ takes_args = ['url']
26+ takes_options = [
27+ Option('message', help='Commit message to use.', type=str)]
28+
29+ def run(self, url, message=None):
30+ from .propose import get_proposal_by_url
31+ proposal = get_proposal_by_url(url)
32+ proposal.merge(commit_message=message)
33
34=== modified file 'breezy/plugins/propose/github.py'
35--- breezy/plugins/propose/github.py 2019-04-27 16:03:23 +0000
36+++ breezy/plugins/propose/github.py 2019-06-03 23:27:22 +0000
37@@ -135,6 +135,10 @@
38 def close(self):
39 self._pr.edit(state='closed')
40
41+ def merge(self, commit_message=None):
42+ # https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button
43+ self._pr.merge(commit_message=commit_message)
44+
45
46 def parse_github_url(url):
47 (scheme, user, password, host, port, path) = urlutils.parse_url(
48@@ -321,6 +325,10 @@
49 for issue in self.gh.search_issues(query=' '.join(query)):
50 yield GitHubMergeProposal(issue.as_pull_request())
51
52+ @convert_github_error
53+ def get_proposal_by_url(self, url):
54+ raise UnsupportedHoster(url)
55+
56
57 class GitHubMergeProposalBuilder(MergeProposalBuilder):
58
59
60=== modified file 'breezy/plugins/propose/gitlabs.py'
61--- breezy/plugins/propose/gitlabs.py 2019-04-27 16:03:23 +0000
62+++ breezy/plugins/propose/gitlabs.py 2019-06-03 23:27:22 +0000
63@@ -54,6 +54,16 @@
64 self.url = url
65
66
67+class NotMergeRequestUrl(errors.BzrError):
68+
69+ _fmt = "Not a merge proposal URL: %(url)s"
70+
71+ def __init__(self, host, url):
72+ errors.BzrError.__init__(self)
73+ self.host = host
74+ self.url = url
75+
76+
77 class DifferentGitLabInstances(errors.BzrError):
78
79 _fmt = ("Can't create merge proposals across GitLab instances: "
80@@ -129,6 +139,20 @@
81 return host, path, branch.name
82
83
84+def parse_gitlab_merge_request_url(url):
85+ (scheme, user, password, host, port, path) = urlutils.parse_url(
86+ url)
87+ if scheme not in ('git+ssh', 'https', 'http'):
88+ raise NotGitLabUrl(url)
89+ if not host:
90+ raise NotGitLabUrl(url)
91+ path = path.strip('/')
92+ parts = path.split('/')
93+ if parts[-2] != 'merge_requests':
94+ raise NotMergeRequestUrl(host, url)
95+ return host, '/'.join(parts[:-2]), int(parts[-1])
96+
97+
98 class GitLabMergeProposal(MergeProposal):
99
100 def __init__(self, mr):
101@@ -167,6 +191,10 @@
102 self._mr.state_event = 'close'
103 self._mr.save()
104
105+ def merge(self, commit_message=None):
106+ # https://docs.gitlab.com/ee/api/merge_requests.html#accept-mr
107+ self._mr.merge(merge_commit_message=commit_message)
108+
109
110 def gitlab_url_to_bzr_url(url, name):
111 if not PY3:
112@@ -334,6 +362,22 @@
113 owner=self.gl.user.username, state=state):
114 yield GitLabMergeProposal(mp)
115
116+ def get_proposal_by_url(self, url):
117+ try:
118+ (host, project, merge_id) = parse_gitlab_merge_request_url(url)
119+ except NotGitLabUrl:
120+ raise UnsupportedHoster(url)
121+ except NotMergeRequestUrl as e:
122+ if self.gl.url == ('https://%s' % e.host):
123+ raise
124+ else:
125+ raise UnsupportedHoster(url)
126+ if self.gl.url != ('https://%s' % host):
127+ raise UnsupportedHoster(url)
128+ project = self.gl.projects.get(project)
129+ mr = project.mergerequests.get(merge_id)
130+ return GitLabMergeProposal(mr)
131+
132
133 class GitlabMergeProposalBuilder(MergeProposalBuilder):
134
135
136=== modified file 'breezy/plugins/propose/launchpad.py'
137--- breezy/plugins/propose/launchpad.py 2019-06-03 22:38:47 +0000
138+++ breezy/plugins/propose/launchpad.py 2019-06-03 23:27:22 +0000
139@@ -20,6 +20,8 @@
140 from __future__ import absolute_import
141
142 import re
143+import shutil
144+import tempfile
145
146 from .propose import (
147 Hoster,
148@@ -152,6 +154,23 @@
149 def close(self):
150 self._mp.setStatus(status='Rejected')
151
152+ def merge(self, commit_message=None):
153+ target_branch = _mod_branch.Branch.open(
154+ self.get_target_branch_url())
155+ source_branch = _mod_branch.Branch.open(
156+ self.get_source_branch_url())
157+ # TODO(jelmer): Ideally this would use a memorytree, but merge doesn't
158+ # support that yet.
159+ # tree = target_branch.create_memorytree()
160+ tmpdir = tempfile.mkdtemp()
161+ try:
162+ tree = target_branch.create_checkout(
163+ to_location=tmpdir, lightweight=True)
164+ tree.merge_from_branch(source_branch)
165+ tree.commit(commit_message or self._mp.commit_message)
166+ finally:
167+ shutil.rmtree(tmpdir)
168+
169
170 class Launchpad(Hoster):
171 """The Launchpad hosting service."""
172@@ -400,6 +419,23 @@
173 for mp in self.launchpad.me.getMergeProposals(status=statuses):
174 yield LaunchpadMergeProposal(mp)
175
176+ def get_proposal_by_url(self, url):
177+ # Launchpad doesn't have a way to find a merge proposal by URL.
178+ (scheme, user, password, host, port, path) = urlutils.parse_url(
179+ url)
180+ LAUNCHPAD_CODE_DOMAINS = [
181+ ('code.%s' % domain) for domain in lp_api.LAUNCHPAD_DOMAINS.values()]
182+ if host not in LAUNCHPAD_CODE_DOMAINS:
183+ raise UnsupportedHoster(url)
184+ # TODO(jelmer): Check if this is a launchpad URL. Otherwise, raise
185+ # UnsupportedHoster
186+ # See https://api.launchpad.net/devel/#branch_merge_proposal
187+ # the syntax is:
188+ # https://api.launchpad.net/devel/~<author.name>/<project.name>/<branch.name>/+merge/<id>
189+ api_url = str(self.launchpad._root_uri) + path
190+ mp = self.launchpad.load(api_url)
191+ return LaunchpadMergeProposal(mp)
192+
193
194 class LaunchpadBazaarMergeProposalBuilder(MergeProposalBuilder):
195
196
197=== modified file 'breezy/plugins/propose/propose.py'
198--- breezy/plugins/propose/propose.py 2019-04-27 16:03:23 +0000
199+++ breezy/plugins/propose/propose.py 2019-06-03 23:27:22 +0000
200@@ -135,6 +135,10 @@
201 """Check whether this merge proposal has been merged."""
202 raise NotImplementedError(self.is_merged)
203
204+ def merge(self, commit_message=None):
205+ """Merge this merge proposal."""
206+ raise NotImplementedError(self.merge)
207+
208
209 class MergeProposalBuilder(object):
210 """Merge proposal creator.
211@@ -225,7 +229,7 @@
212 raise NotImplementedError(self.get_proposer)
213
214 def iter_proposals(self, source_branch, target_branch, status='open'):
215- """Get a merge proposal for a specified branch tuple.
216+ """Get the merge proposals for a specified branch tuple.
217
218 :param source_branch: Source branch
219 :param target_branch: Target branch
220@@ -234,6 +238,15 @@
221 """
222 raise NotImplementedError(self.iter_proposals)
223
224+ def get_proposal_by_url(self, url):
225+ """Retrieve a branch proposal by URL.
226+
227+ :param url: Merge proposal URL.
228+ :return: MergeProposal object
229+ :raise UnsupportedHoster: Hoster does not support this URL
230+ """
231+ raise NotImplementedError(self.get_proposal_by_url)
232+
233 def hosts(self, branch):
234 """Return true if this hoster hosts given branch."""
235 raise NotImplementedError(self.hosts)
236@@ -289,6 +302,16 @@
237 raise UnsupportedHoster(branch)
238
239
240+def get_proposal_by_url(url):
241+ for name, hoster_cls in hosters.items():
242+ for instance in hoster_cls.iter_instances():
243+ try:
244+ return instance.get_proposal_by_url(url)
245+ except UnsupportedHoster:
246+ pass
247+ raise UnsupportedHoster(url)
248+
249+
250 hosters = registry.Registry()
251 hosters.register_lazy(
252 "launchpad", "breezy.plugins.propose.launchpad",
253
254=== modified file 'doc/en/release-notes/brz-3.1.txt'
255--- doc/en/release-notes/brz-3.1.txt 2019-06-03 22:38:47 +0000
256+++ doc/en/release-notes/brz-3.1.txt 2019-06-03 23:27:22 +0000
257@@ -21,6 +21,9 @@
258
259 .. New commands, options, etc that users may wish to try out.
260
261+ * A new ``brz land`` command can merge merge proposals on Launchpad,
262+ GitHub and GitLab sites. (Jelmer Vernooń≥, #1816213)
263+
264 * The 'patch' command is now bundled with brz.
265 Imported from bzrtools by Aaron Bentley. (Jelmer Vernooń≥)
266

Subscribers

People subscribed via source and target branches