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