/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
1
# Copyright (C) 2018 Breezy Developers
2
#
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License
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
16
0.434.1 by Jelmer Vernooij
Use absolute_import.
17
"""Support for GitLab."""
18
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
19
import json
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
20
import os
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
21
import time
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
22
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
23
from ... import (
7340.1.1 by Martin
Fix use of config_dir in propose plugin
24
    bedding,
0.431.33 by Jelmer Vernooij
Fix URLs from gitlab.
25
    branch as _mod_branch,
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
26
    controldir,
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
27
    errors,
28
    urlutils,
29
    )
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
30
from ...git.urls import git_url_to_bzr_url
7380.1.2 by Jelmer Vernooij
Review comments.
31
from ...trace import mutter
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
32
from ...transport import get_transport
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
33
7408.3.1 by Jelmer Vernooij
Move propose module into core.
34
from ...propose import (
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
35
    determine_title,
0.432.2 by Jelmer Vernooij
Publish command sort of works.
36
    Hoster,
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
37
    MergeProposal,
0.432.2 by Jelmer Vernooij
Publish command sort of works.
38
    MergeProposalBuilder,
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
39
    MergeProposalExists,
0.431.38 by Jelmer Vernooij
Add NoSuchProject.
40
    NoSuchProject,
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
41
    PrerequisiteBranchUnsupported,
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
42
    UnsupportedHoster,
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
43
    )
44
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
45
46
_DEFAULT_FILES = ['/etc/python-gitlab.cfg', '~/.python-gitlab.cfg']
7408.1.2 by Jelmer Vernooij
Set default page size to 50.
47
DEFAULT_PAGE_SIZE = 50
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
48
49
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
50
def mp_status_to_status(status):
51
    return {
52
        'all': 'all',
53
        'open': 'opened',
54
        'merged': 'merged',
55
        'closed': 'closed'}[status]
56
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
57
0.431.17 by Jelmer Vernooij
Try harder to avoid detecting any URL as a GitLab URL.
58
class NotGitLabUrl(errors.BzrError):
59
60
    _fmt = "Not a GitLab URL: %(url)s"
61
62
    def __init__(self, url):
63
        errors.BzrError.__init__(self)
64
        self.url = url
65
66
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
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
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
77
class DifferentGitLabInstances(errors.BzrError):
78
79
    _fmt = ("Can't create merge proposals across GitLab instances: "
80
            "%(source_host)s and %(target_host)s")
81
82
    def __init__(self, source_host, target_host):
83
        self.source_host = source_host
84
        self.target_host = target_host
85
86
0.432.10 by Jelmer Vernooij
More test fixes.
87
class GitLabLoginMissing(errors.BzrError):
88
89
    _fmt = ("Please log into GitLab")
90
91
7296.10.2 by Jelmer Vernooij
More fixes.
92
class GitlabLoginError(errors.BzrError):
93
94
    _fmt = ("Error logging in: %(error)s")
95
96
    def __init__(self, error):
97
        self.error = error
98
99
7490.10.1 by Jelmer Vernooij
Fix handling of 409s for gitlab.
100
class MergeRequestExists(Exception):
101
    """Raised when a merge requests already exists."""
102
103
0.431.59 by Jelmer Vernooij
Add gitlab-login command.
104
def default_config_path():
7340.1.1 by Martin
Fix use of config_dir in propose plugin
105
    return os.path.join(bedding.config_dir(), 'gitlab.conf')
0.431.59 by Jelmer Vernooij
Add gitlab-login command.
106
107
108
def store_gitlab_token(name, url, private_token):
109
    """Store a GitLab token in a configuration file."""
110
    import configparser
111
    config = configparser.ConfigParser()
112
    path = default_config_path()
113
    config.read([path])
114
    config.add_section(name)
115
    config[name]['url'] = url
116
    config[name]['private_token'] = private_token
117
    with open(path, 'w') as f:
118
        config.write(f)
119
120
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
121
def iter_tokens():
122
    import configparser
123
    config = configparser.ConfigParser()
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
124
    config.read(
125
        [os.path.expanduser(p) for p in _DEFAULT_FILES] +
126
        [default_config_path()])
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
127
    for name, section in config.items():
128
        yield name, section
129
130
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
131
def get_credentials_by_url(url):
132
    for name, credentials in iter_tokens():
133
        if 'url' not in credentials:
134
            continue
135
        if credentials['url'].rstrip('/') == url.rstrip('/'):
136
            return credentials
137
    else:
138
        return None
139
140
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
141
def parse_gitlab_url(url):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
142
    (scheme, user, password, host, port, path) = urlutils.parse_url(
143
        url)
0.431.17 by Jelmer Vernooij
Try harder to avoid detecting any URL as a GitLab URL.
144
    if scheme not in ('git+ssh', 'https', 'http'):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
145
        raise NotGitLabUrl(url)
0.431.17 by Jelmer Vernooij
Try harder to avoid detecting any URL as a GitLab URL.
146
    if not host:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
147
        raise NotGitLabUrl(url)
0.432.10 by Jelmer Vernooij
More test fixes.
148
    path = path.strip('/')
0.432.11 by Jelmer Vernooij
Fix some tests.
149
    if path.endswith('.git'):
150
        path = path[:-4]
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
151
    return host, path
152
153
154
def parse_gitlab_branch_url(branch):
7441.1.1 by Jelmer Vernooij
Add strip_segment_parameters function.
155
    url = urlutils.strip_segment_parameters(branch.user_url)
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
156
    host, path = parse_gitlab_url(url)
0.432.10 by Jelmer Vernooij
More test fixes.
157
    return host, path, branch.name
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
158
159
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
160
def parse_gitlab_merge_request_url(url):
161
    (scheme, user, password, host, port, path) = urlutils.parse_url(
162
        url)
163
    if scheme not in ('git+ssh', 'https', 'http'):
164
        raise NotGitLabUrl(url)
165
    if not host:
166
        raise NotGitLabUrl(url)
167
    path = path.strip('/')
168
    parts = path.split('/')
7490.23.1 by Jelmer Vernooij
Add support for newer style gitlab merge proposal URLs.
169
    if len(parts) < 2:
170
        raise NotMergeRequestUrl(host, url)
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
171
    if parts[-2] != 'merge_requests':
172
        raise NotMergeRequestUrl(host, url)
7490.23.1 by Jelmer Vernooij
Add support for newer style gitlab merge proposal URLs.
173
    if parts[-3] == '-':
174
        project_name = '/'.join(parts[:-3])
175
    else:
176
        project_name = '/'.join(parts[:-2])
177
    return host, project_name, int(parts[-1])
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
178
179
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
180
class GitLabMergeProposal(MergeProposal):
181
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
182
    def __init__(self, gl, mr):
183
        self.gl = gl
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
184
        self._mr = mr
185
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
186
    def _update(self, **kwargs):
187
        self.gl._update_merge_request(self._mr['project_id'], self._mr['iid'], kwargs)
188
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
189
    def __repr__(self):
190
        return "<%s at %r>" % (type(self).__name__, self._mr['web_url'])
191
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
192
    @property
193
    def url(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
194
        return self._mr['web_url']
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
195
196
    def get_description(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
197
        return self._mr['description']
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
198
199
    def set_description(self, description):
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
200
        self._update(description=description, title=determine_title(description))
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
201
7296.8.2 by Jelmer Vernooij
Add feature flag for commit message.
202
    def get_commit_message(self):
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
203
        return self._mr.get('merge_commit_message')
204
205
    def set_commit_message(self, message):
206
        raise errors.UnsupportedOperation(self.set_commit_message, self)
7296.8.2 by Jelmer Vernooij
Add feature flag for commit message.
207
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
208
    def _branch_url_from_project(self, project_id, branch_name):
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
209
        if project_id is None:
210
            return None
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
211
        project = self.gl._get_project(project_id)
7296.10.3 by Jelmer Vernooij
More fixes.
212
        return gitlab_url_to_bzr_url(project['http_url_to_repo'], branch_name)
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
213
214
    def get_source_branch_url(self):
215
        return self._branch_url_from_project(
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
216
            self._mr['source_project_id'], self._mr['source_branch'])
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
217
218
    def get_target_branch_url(self):
219
        return self._branch_url_from_project(
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
220
            self._mr['target_project_id'], self._mr['target_branch'])
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
221
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
222
    def _get_project_name(self, project_id):
223
        source_project = self.gl._get_project(project_id)
224
        return source_project['path_with_namespace']
225
226
    def get_source_project(self):
227
        return self._get_project_name(self._mr['source_project_id'])
228
229
    def get_target_project(self):
230
        return self._get_project_name(self._mr['target_project_id'])
231
0.431.46 by Jelmer Vernooij
Add MergeProposal.is_merged.
232
    def is_merged(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
233
        return (self._mr['state'] == 'merged')
0.431.46 by Jelmer Vernooij
Add MergeProposal.is_merged.
234
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
235
    def is_closed(self):
236
        return (self._mr['state'] == 'closed')
237
238
    def reopen(self):
7405.2.1 by Jelmer Vernooij
Fix reopen behaviour for gitlab.
239
        return self._update(state_event='reopen')
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
240
7260.2.1 by Jelmer Vernooij
Implement .close on merge proposals.
241
    def close(self):
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
242
        self._update(state_event='close')
7260.2.1 by Jelmer Vernooij
Implement .close on merge proposals.
243
7296.9.1 by Jelmer Vernooij
Add 'brz land' subcommand.
244
    def merge(self, commit_message=None):
245
        # https://docs.gitlab.com/ee/api/merge_requests.html#accept-mr
246
        self._mr.merge(merge_commit_message=commit_message)
247
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
248
    def can_be_merged(self):
249
        if self._mr['merge_status'] == 'cannot_be_merged':
250
            return False
251
        elif self._mr['merge_status'] == 'can_be_merged':
252
            return True
253
        else:
254
            raise ValueError(self._mr['merge_status'])
255
7414.4.1 by Jelmer Vernooij
Add a MergeProposal.get_merged_by method.
256
    def get_merged_by(self):
7414.4.2 by Jelmer Vernooij
Fix gitlab / github merged_by fetching.
257
        user = self._mr.get('merged_by')
258
        if user is None:
259
            return None
260
        return user['username']
7414.4.1 by Jelmer Vernooij
Add a MergeProposal.get_merged_by method.
261
7414.4.3 by Jelmer Vernooij
Add MergeProposal.get_merged_at.
262
    def get_merged_at(self):
263
        merged_at = self._mr.get('merged_at')
264
        if merged_at is None:
265
            return None
7414.4.4 by Jelmer Vernooij
Use iso8601 module.
266
        import iso8601
267
        return iso8601.parse_date(merged_at)
7414.4.3 by Jelmer Vernooij
Add MergeProposal.get_merged_at.
268
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
269
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
270
def gitlab_url_to_bzr_url(url, name):
7408.2.1 by Jelmer Vernooij
Use standard functions for creating Git URLs.
271
    return git_url_to_bzr_url(url, branch=name)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
272
273
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
274
class GitLab(Hoster):
275
    """GitLab hoster implementation."""
276
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
277
    supports_merge_proposal_labels = True
7296.8.2 by Jelmer Vernooij
Add feature flag for commit message.
278
    supports_merge_proposal_commit_message = False
7490.3.9 by Jelmer Vernooij
Add supports_allow_collaboration flag.
279
    supports_allow_collaboration = True
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
280
    merge_proposal_description_format = 'markdown'
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
281
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
282
    def __repr__(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
283
        return "<GitLab(%r)>" % self.base_url
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
284
7260.1.1 by Jelmer Vernooij
Add .base_url property to Hoster.
285
    @property
286
    def base_url(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
287
        return self.transport.base
288
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
289
    @property
290
    def base_hostname(self):
291
        return urlutils.parse_url(self.base_url)[3]
292
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
293
    def _api_request(self, method, path, fields=None, body=None):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
294
        return self.transport.request(
295
            method, urlutils.join(self.base_url, 'api', 'v4', path),
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
296
            headers=self.headers, fields=fields, body=body)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
297
298
    def __init__(self, transport, private_token):
299
        self.transport = transport
300
        self.headers = {"Private-Token": private_token}
301
        self.check()
302
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
303
    def _get_user(self, username):
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
304
        path = 'users/%s' % urlutils.quote(str(username), '')
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
305
        response = self._api_request('GET', path)
306
        if response.status == 404:
307
            raise KeyError('no such user %s' % username)
308
        if response.status == 200:
309
            return json.loads(response.data)
310
        raise errors.InvalidHttpResponse(path, response.text)
311
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
312
    def _get_user_by_email(self, email):
313
        path = 'users?search=%s' % urlutils.quote(str(email), '')
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
314
        response = self._api_request('GET', path)
315
        if response.status == 404:
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
316
            raise KeyError('no such user %s' % email)
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
317
        if response.status == 200:
318
            ret = json.loads(response.data)
319
            if len(ret) != 1:
320
                raise ValueError('unexpected number of results; %r' % ret)
321
            return ret[0]
322
        raise errors.InvalidHttpResponse(path, response.text)
323
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
324
    def _get_project(self, project_name):
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
325
        path = 'projects/%s' % urlutils.quote(str(project_name), '')
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
326
        response = self._api_request('GET', path)
327
        if response.status == 404:
328
            raise NoSuchProject(project_name)
329
        if response.status == 200:
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
330
            return json.loads(response.data)
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
331
        raise errors.InvalidHttpResponse(path, response.text)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
332
7380.1.2 by Jelmer Vernooij
Review comments.
333
    def _fork_project(self, project_name, timeout=50, interval=5):
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
334
        path = 'projects/%s/fork' % urlutils.quote(str(project_name), '')
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
335
        response = self._api_request('POST', path)
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
336
        if response.status not in (200, 201):
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
337
            raise errors.InvalidHttpResponse(path, response.text)
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
338
        # The response should be valid JSON, but let's ignore it
7397.1.1 by Jelmer Vernooij
Fix project forking.
339
        project = json.loads(response.data)
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
340
        # Spin and wait until import_status for new project
341
        # is complete.
7380.1.2 by Jelmer Vernooij
Review comments.
342
        deadline = time.time() + timeout
7397.1.1 by Jelmer Vernooij
Fix project forking.
343
        while project['import_status'] not in ('finished', 'none'):
7380.1.2 by Jelmer Vernooij
Review comments.
344
            mutter('import status is %s', project['import_status'])
345
            if time.time() > deadline:
346
                raise Exception('timeout waiting for project to become available')
347
            time.sleep(interval)
7397.1.1 by Jelmer Vernooij
Fix project forking.
348
            project = self._get_project(project['path_with_namespace'])
349
        return project
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
350
351
    def _get_logged_in_username(self):
352
        return self._current_user['username']
353
7408.1.1 by Jelmer Vernooij
Use paging to iterate over all gitlab pull requests.
354
    def _list_paged(self, path, parameters=None, per_page=None):
355
        if parameters is None:
356
            parameters = {}
357
        else:
358
            parameters = dict(parameters.items())
359
        if per_page:
7408.1.3 by Jelmer Vernooij
Support pagination for github.
360
            parameters['per_page'] = str(per_page)
7408.1.1 by Jelmer Vernooij
Use paging to iterate over all gitlab pull requests.
361
        page = "1"
362
        while page:
363
            parameters['page'] = page
364
            response = self._api_request(
365
                'GET', path + '?' +
366
                ';'.join(['%s=%s' % item for item in parameters.items()]))
367
            if response.status == 403:
368
                raise errors.PermissionDenied(response.text)
369
            if response.status != 200:
370
                raise errors.InvalidHttpResponse(path, response.text)
371
            page = response.getheader("X-Next-Page")
372
            for entry in json.loads(response.data):
373
                yield entry
374
7296.10.9 by Jelmer Vernooij
Fix method name spacing.
375
    def _list_merge_requests(self, owner=None, project=None, state=None):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
376
        if project is not None:
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
377
            path = 'projects/%s/merge_requests' % urlutils.quote(str(project), '')
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
378
        else:
379
            path = 'merge_requests'
380
        parameters = {}
381
        if state:
382
            parameters['state'] = state
383
        if owner:
384
            parameters['owner_id'] = urlutils.quote(owner, '')
7408.1.2 by Jelmer Vernooij
Set default page size to 50.
385
        return self._list_paged(path, parameters, per_page=DEFAULT_PAGE_SIZE)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
386
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
387
    def _get_merge_request(self, project, merge_id):
388
        path = 'projects/%s/merge_requests/%d' % (urlutils.quote(str(project), ''), merge_id)
389
        response = self._api_request('GET', path)
390
        if response.status == 403:
391
            raise errors.PermissionDenied(response.text)
392
        if response.status != 200:
393
            raise errors.InvalidHttpResponse(path, response.text)
394
        return json.loads(response.data)
395
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
396
    def _list_projects(self, owner):
397
        path = 'users/%s/projects' % urlutils.quote(str(owner), '')
398
        parameters = {}
399
        return self._list_paged(path, parameters, per_page=DEFAULT_PAGE_SIZE)
400
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
401
    def _update_merge_request(self, project_id, iid, mr):
402
        path = 'projects/%s/merge_requests/%s' % (
403
            urlutils.quote(str(project_id), ''), iid)
404
        response = self._api_request('PUT', path, fields=mr)
405
        if response.status == 200:
406
            return json.loads(response.data)
407
        raise errors.InvalidHttpResponse(path, response.text)
408
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
409
    def _create_mergerequest(
410
            self, title, source_project_id, target_project_id,
7296.10.3 by Jelmer Vernooij
More fixes.
411
            source_branch_name, target_branch_name, description,
7490.14.1 by Jelmer Vernooij
Various git fixes.
412
            labels=None, allow_collaboration=False):
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
413
        path = 'projects/%s/merge_requests' % source_project_id
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
414
        fields = {
415
            'title': title,
416
            'source_branch': source_branch_name,
417
            'target_branch': target_branch_name,
418
            'target_project_id': target_project_id,
419
            'description': description,
7490.14.1 by Jelmer Vernooij
Various git fixes.
420
            'allow_collaboration': allow_collaboration,
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
421
            }
422
        if labels:
423
            fields['labels'] = labels
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
424
        response = self._api_request('POST', path, fields=fields)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
425
        if response.status == 403:
426
            raise errors.PermissionDenied(response.text)
427
        if response.status == 409:
7490.10.1 by Jelmer Vernooij
Fix handling of 409s for gitlab.
428
            raise MergeRequestExists()
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
429
        if response.status != 201:
430
            raise errors.InvalidHttpResponse(path, response.text)
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
431
        return json.loads(response.data)
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
432
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
433
    def get_push_url(self, branch):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
434
        (host, project_name, branch_name) = parse_gitlab_branch_url(branch)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
435
        project = self._get_project(project_name)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
436
        return gitlab_url_to_bzr_url(
7296.10.3 by Jelmer Vernooij
More fixes.
437
            project['ssh_url_to_repo'], branch_name)
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
438
0.431.20 by Jelmer Vernooij
publish -> publish_derived.
439
    def publish_derived(self, local_branch, base_branch, name, project=None,
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
440
                        owner=None, revision_id=None, overwrite=False,
7489.4.2 by Jelmer Vernooij
Plumb through tag_selector.
441
                        allow_lossy=True, tag_selector=None):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
442
        (host, base_project, base_branch_name) = parse_gitlab_branch_url(base_branch)
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
443
        if owner is None:
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
444
            owner = self._get_logged_in_username()
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
445
        if project is None:
7296.10.3 by Jelmer Vernooij
More fixes.
446
            project = self._get_project(base_project)['path']
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
447
        try:
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
448
            target_project = self._get_project('%s/%s' % (owner, project))
449
        except NoSuchProject:
450
            target_project = self._fork_project(base_project)
7296.10.3 by Jelmer Vernooij
More fixes.
451
        remote_repo_url = git_url_to_bzr_url(target_project['ssh_url_to_repo'])
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
452
        remote_dir = controldir.ControlDir.open(remote_repo_url)
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
453
        try:
7211.13.7 by Jelmer Vernooij
Fix formatting.
454
            push_result = remote_dir.push_branch(
455
                local_branch, revision_id=revision_id, overwrite=overwrite,
7489.4.2 by Jelmer Vernooij
Plumb through tag_selector.
456
                name=name, tag_selector=tag_selector)
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
457
        except errors.NoRoundtrippingSupport:
458
            if not allow_lossy:
459
                raise
7211.13.7 by Jelmer Vernooij
Fix formatting.
460
            push_result = remote_dir.push_branch(
461
                local_branch, revision_id=revision_id, overwrite=overwrite,
7489.4.2 by Jelmer Vernooij
Plumb through tag_selector.
462
                name=name, lossy=True, tag_selector=tag_selector)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
463
        public_url = gitlab_url_to_bzr_url(
7296.10.3 by Jelmer Vernooij
More fixes.
464
            target_project['http_url_to_repo'], name)
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
465
        return push_result.target_branch, public_url
0.432.4 by Jelmer Vernooij
Some work on gitlab.
466
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
467
    def get_derived_branch(self, base_branch, name, project=None, owner=None):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
468
        (host, base_project, base_branch_name) = parse_gitlab_branch_url(base_branch)
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
469
        if owner is None:
7296.10.3 by Jelmer Vernooij
More fixes.
470
            owner = self._get_logged_in_username()
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
471
        if project is None:
7296.10.3 by Jelmer Vernooij
More fixes.
472
            project = self._get_project(base_project)['path']
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
473
        try:
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
474
            target_project = self._get_project('%s/%s' % (owner, project))
475
        except NoSuchProject:
476
            raise errors.NotBranchError('%s/%s/%s' % (self.base_url, owner, project))
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
477
        return _mod_branch.Branch.open(gitlab_url_to_bzr_url(
7296.10.3 by Jelmer Vernooij
More fixes.
478
            target_project['ssh_url_to_repo'], name))
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
479
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
480
    def get_proposer(self, source_branch, target_branch):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
481
        return GitlabMergeProposalBuilder(self, source_branch, target_branch)
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
482
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
483
    def iter_proposals(self, source_branch, target_branch, status):
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
484
        (source_host, source_project_name, source_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
485
            parse_gitlab_branch_url(source_branch))
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
486
        (target_host, target_project_name, target_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
487
            parse_gitlab_branch_url(target_branch))
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
488
        if source_host != target_host:
489
            raise DifferentGitLabInstances(source_host, target_host)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
490
        source_project = self._get_project(source_project_name)
491
        target_project = self._get_project(target_project_name)
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
492
        state = mp_status_to_status(status)
7360.1.4 by Jelmer Vernooij
Fix retrieval of proposals from gitlab.
493
        for mr in self._list_merge_requests(
7296.10.3 by Jelmer Vernooij
More fixes.
494
                project=target_project['id'], state=state):
495
            if (mr['source_project_id'] != source_project['id'] or
496
                    mr['source_branch'] != source_branch_name or
497
                    mr['target_project_id'] != target_project['id'] or
498
                    mr['target_branch'] != target_branch_name):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
499
                continue
500
            yield GitLabMergeProposal(self, mr)
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
501
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
502
    def hosts(self, branch):
503
        try:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
504
            (host, project, branch_name) = parse_gitlab_branch_url(branch)
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
505
        except NotGitLabUrl:
506
            return False
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
507
        return self.base_hostname == host
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
508
509
    def check(self):
510
        response = self._api_request('GET', 'user')
511
        if response.status == 200:
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
512
            self._current_user = json.loads(response.data)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
513
            return
7296.10.2 by Jelmer Vernooij
More fixes.
514
        if response == 401:
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
515
            if json.loads(response.data) == {"message": "401 Unauthorized"}:
7296.10.2 by Jelmer Vernooij
More fixes.
516
                raise GitLabLoginMissing()
517
            else:
518
                raise GitlabLoginError(response.text)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
519
        raise UnsupportedHoster(url)
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
520
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
521
    @classmethod
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
522
    def probe_from_url(cls, url, possible_transports=None):
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
523
        try:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
524
            (host, project) = parse_gitlab_url(url)
0.431.17 by Jelmer Vernooij
Try harder to avoid detecting any URL as a GitLab URL.
525
        except NotGitLabUrl:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
526
            raise UnsupportedHoster(url)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
527
        transport = get_transport(
528
            'https://%s' % host, possible_transports=possible_transports)
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
529
        credentials = get_credentials_by_url(transport.base)
530
        if credentials is not None:
531
            return cls(transport, credentials.get('private_token'))
532
        raise UnsupportedHoster(url)
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
533
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
534
    @classmethod
535
    def iter_instances(cls):
536
        for name, credentials in iter_tokens():
537
            if 'url' not in credentials:
538
                continue
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
539
            yield cls(
540
                get_transport(credentials['url']),
541
                private_token=credentials.get('private_token'))
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
542
0.431.66 by Jelmer Vernooij
Add support for status argument.
543
    def iter_my_proposals(self, status='open'):
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
544
        state = mp_status_to_status(status)
7296.10.9 by Jelmer Vernooij
Fix method name spacing.
545
        for mp in self._list_merge_requests(
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
546
                owner=self._get_logged_in_username(), state=state):
547
            yield GitLabMergeProposal(self, mp)
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
548
7414.5.2 by Jelmer Vernooij
Change iter_my_projects to iter_my_forks.
549
    def iter_my_forks(self):
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
550
        for project in self._list_projects(owner=self._get_logged_in_username()):
551
            base_project = project.get('forked_from_project')
7414.5.2 by Jelmer Vernooij
Change iter_my_projects to iter_my_forks.
552
            if not base_project:
553
                continue
554
            yield project['path_with_namespace']
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
555
7296.9.1 by Jelmer Vernooij
Add 'brz land' subcommand.
556
    def get_proposal_by_url(self, url):
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
557
        try:
558
            (host, project, merge_id) = parse_gitlab_merge_request_url(url)
559
        except NotGitLabUrl:
560
            raise UnsupportedHoster(url)
7296.9.4 by Jelmer Vernooij
Fix dealing with non-gitlab sites.
561
        except NotMergeRequestUrl as e:
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
562
            if self.base_hostname == e.host:
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
563
                raise
564
            else:
565
                raise UnsupportedHoster(url)
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
566
        if self.base_hostname != host:
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
567
            raise UnsupportedHoster(url)
7360.1.4 by Jelmer Vernooij
Fix retrieval of proposals from gitlab.
568
        project = self._get_project(project)
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
569
        mr = self._get_merge_request(project['path_with_namespace'], merge_id)
570
        return GitLabMergeProposal(self, mr)
7296.9.1 by Jelmer Vernooij
Add 'brz land' subcommand.
571
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
572
    def delete_project(self, project):
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
573
        path = 'projects/%s' % urlutils.quote(str(project), '')
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
574
        response = self._api_request('DELETE', path)
575
        if response.status == 404:
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
576
            raise NoSuchProject(project)
577
        if response.status != 202:
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
578
            raise errors.InvalidHttpResponse(path, response.text)
579
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
580
0.432.2 by Jelmer Vernooij
Publish command sort of works.
581
class GitlabMergeProposalBuilder(MergeProposalBuilder):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
582
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
583
    def __init__(self, gl, source_branch, target_branch):
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
584
        self.gl = gl
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
585
        self.source_branch = source_branch
586
        (self.source_host, self.source_project_name, self.source_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
587
            parse_gitlab_branch_url(source_branch))
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
588
        self.target_branch = target_branch
589
        (self.target_host, self.target_project_name, self.target_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
590
            parse_gitlab_branch_url(target_branch))
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
591
        if self.source_host != self.target_host:
592
            raise DifferentGitLabInstances(self.source_host, self.target_host)
593
594
    def get_infotext(self):
595
        """Determine the initial comment for the merge proposal."""
596
        info = []
597
        info.append("Gitlab instance: %s\n" % self.target_host)
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
598
        info.append("Source: %s\n" % self.source_branch.user_url)
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
599
        info.append("Target: %s\n" % self.target_branch.user_url)
600
        return ''.join(info)
601
602
    def get_initial_body(self):
603
        """Get a body for the proposal for the user to modify.
604
605
        :return: a str or None.
606
        """
607
        return None
608
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
609
    def create_proposal(self, description, reviewers=None, labels=None,
7467.3.1 by Jelmer Vernooij
Add a work_in_progress flag.
610
                        prerequisite_branch=None, commit_message=None,
7490.6.1 by Jelmer Vernooij
Add allow-collaboration flag.
611
                        work_in_progress=False, allow_collaboration=False):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
612
        """Perform the submission."""
7296.8.1 by Jelmer Vernooij
Add commit-message option to 'brz propose'.
613
        # https://docs.gitlab.com/ee/api/merge_requests.html#create-mr
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
614
        if prerequisite_branch is not None:
615
            raise PrerequisiteBranchUnsupported(self)
7296.8.1 by Jelmer Vernooij
Add commit-message option to 'brz propose'.
616
        # Note that commit_message is ignored, since Gitlab doesn't support it.
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
617
        source_project = self.gl._get_project(self.source_project_name)
618
        target_project = self.gl._get_project(self.target_project_name)
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
619
        # TODO(jelmer): Allow setting title explicitly
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
620
        title = determine_title(description)
7467.3.1 by Jelmer Vernooij
Add a work_in_progress flag.
621
        if work_in_progress:
622
            title = 'WIP: %s' % title
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
623
        # TODO(jelmer): Allow setting milestone field
624
        # TODO(jelmer): Allow setting squash field
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
625
        kwargs = {
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
626
            'title': title,
7296.10.3 by Jelmer Vernooij
More fixes.
627
            'source_project_id': source_project['id'],
628
            'target_project_id': target_project['id'],
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
629
            'source_branch_name': self.source_branch_name,
630
            'target_branch_name': self.target_branch_name,
7490.6.1 by Jelmer Vernooij
Add allow-collaboration flag.
631
            'description': description,
632
            'allow_collaboration': allow_collaboration}
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
633
        if labels:
634
            kwargs['labels'] = ','.join(labels)
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
635
        if reviewers:
636
            kwargs['assignee_ids'] = []
637
            for reviewer in reviewers:
638
                if '@' in reviewer:
639
                    user = self.gl._get_user_by_email(reviewer)
640
                else:
641
                    user = self.gl._get_user(reviewer)
642
                kwargs['assignee_ids'].append(user['id'])
7490.10.1 by Jelmer Vernooij
Fix handling of 409s for gitlab.
643
        try:
644
            merge_request = self.gl._create_mergerequest(**kwargs)
645
        except MergeRequestExists:
7490.25.1 by Jelmer Vernooij
Fix raising of ProposalExists.
646
            raise MergeProposalExists(self.source_branch.user_url)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
647
        return GitLabMergeProposal(self.gl, merge_request)
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
648
649
650
def register_gitlab_instance(shortname, url):
651
    """Register a gitlab instance.
652
653
    :param shortname: Short name (e.g. "gitlab")
654
    :param url: URL to the gitlab instance
655
    """
656
    from breezy.bugtracker import (
657
        tracker_registry,
658
        ProjectIntegerBugTracker,
659
        )
660
    tracker_registry.register(
661
        shortname, ProjectIntegerBugTracker(
662
            shortname, url + '/{project}/issues/{id}'))