/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,
7490.14.1 by Jelmer Vernooij
Various git fixes.
398
            labels=None, allow_collaboration=False):
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,
7490.14.1 by Jelmer Vernooij
Various git fixes.
406
            'allow_collaboration': allow_collaboration,
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
407
            }
408
        if labels:
409
            fields['labels'] = labels
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
410
        response = self._api_request('POST', path, fields=fields)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
411
        if response.status == 403:
412
            raise errors.PermissionDenied(response.text)
413
        if response.status == 409:
7490.10.1 by Jelmer Vernooij
Fix handling of 409s for gitlab.
414
            raise MergeRequestExists()
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
415
        if response.status != 201:
416
            raise errors.InvalidHttpResponse(path, response.text)
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
417
        return json.loads(response.data)
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
418
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
419
    def get_push_url(self, branch):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
420
        (host, project_name, branch_name) = parse_gitlab_branch_url(branch)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
421
        project = self._get_project(project_name)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
422
        return gitlab_url_to_bzr_url(
7296.10.3 by Jelmer Vernooij
More fixes.
423
            project['ssh_url_to_repo'], branch_name)
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
424
0.431.20 by Jelmer Vernooij
publish -> publish_derived.
425
    def publish_derived(self, local_branch, base_branch, name, project=None,
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
426
                        owner=None, revision_id=None, overwrite=False,
7489.4.2 by Jelmer Vernooij
Plumb through tag_selector.
427
                        allow_lossy=True, tag_selector=None):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
428
        (host, base_project, base_branch_name) = parse_gitlab_branch_url(base_branch)
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
429
        if owner is None:
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
430
            owner = self._get_logged_in_username()
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
431
        if project is None:
7296.10.3 by Jelmer Vernooij
More fixes.
432
            project = self._get_project(base_project)['path']
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
433
        try:
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
434
            target_project = self._get_project('%s/%s' % (owner, project))
435
        except NoSuchProject:
436
            target_project = self._fork_project(base_project)
7296.10.3 by Jelmer Vernooij
More fixes.
437
        remote_repo_url = git_url_to_bzr_url(target_project['ssh_url_to_repo'])
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
438
        remote_dir = controldir.ControlDir.open(remote_repo_url)
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
439
        try:
7211.13.7 by Jelmer Vernooij
Fix formatting.
440
            push_result = remote_dir.push_branch(
441
                local_branch, revision_id=revision_id, overwrite=overwrite,
7489.4.2 by Jelmer Vernooij
Plumb through tag_selector.
442
                name=name, tag_selector=tag_selector)
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
443
        except errors.NoRoundtrippingSupport:
444
            if not allow_lossy:
445
                raise
7211.13.7 by Jelmer Vernooij
Fix formatting.
446
            push_result = remote_dir.push_branch(
447
                local_branch, revision_id=revision_id, overwrite=overwrite,
7489.4.2 by Jelmer Vernooij
Plumb through tag_selector.
448
                name=name, lossy=True, tag_selector=tag_selector)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
449
        public_url = gitlab_url_to_bzr_url(
7296.10.3 by Jelmer Vernooij
More fixes.
450
            target_project['http_url_to_repo'], name)
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
451
        return push_result.target_branch, public_url
0.432.4 by Jelmer Vernooij
Some work on gitlab.
452
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
453
    def get_derived_branch(self, base_branch, name, project=None, owner=None):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
454
        (host, base_project, base_branch_name) = parse_gitlab_branch_url(base_branch)
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
455
        if owner is None:
7296.10.3 by Jelmer Vernooij
More fixes.
456
            owner = self._get_logged_in_username()
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
457
        if project is None:
7296.10.3 by Jelmer Vernooij
More fixes.
458
            project = self._get_project(base_project)['path']
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
459
        try:
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
460
            target_project = self._get_project('%s/%s' % (owner, project))
461
        except NoSuchProject:
462
            raise errors.NotBranchError('%s/%s/%s' % (self.base_url, owner, project))
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
463
        return _mod_branch.Branch.open(gitlab_url_to_bzr_url(
7296.10.3 by Jelmer Vernooij
More fixes.
464
            target_project['ssh_url_to_repo'], name))
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
465
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
466
    def get_proposer(self, source_branch, target_branch):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
467
        return GitlabMergeProposalBuilder(self, source_branch, target_branch)
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
468
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
469
    def iter_proposals(self, source_branch, target_branch, status):
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
470
        (source_host, source_project_name, source_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
471
            parse_gitlab_branch_url(source_branch))
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
472
        (target_host, target_project_name, target_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
473
            parse_gitlab_branch_url(target_branch))
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
474
        if source_host != target_host:
475
            raise DifferentGitLabInstances(source_host, target_host)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
476
        source_project = self._get_project(source_project_name)
477
        target_project = self._get_project(target_project_name)
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
478
        state = mp_status_to_status(status)
7360.1.4 by Jelmer Vernooij
Fix retrieval of proposals from gitlab.
479
        for mr in self._list_merge_requests(
7296.10.3 by Jelmer Vernooij
More fixes.
480
                project=target_project['id'], state=state):
481
            if (mr['source_project_id'] != source_project['id'] or
482
                    mr['source_branch'] != source_branch_name or
483
                    mr['target_project_id'] != target_project['id'] or
484
                    mr['target_branch'] != target_branch_name):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
485
                continue
486
            yield GitLabMergeProposal(self, mr)
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
487
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
488
    def hosts(self, branch):
489
        try:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
490
            (host, project, branch_name) = parse_gitlab_branch_url(branch)
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
491
        except NotGitLabUrl:
492
            return False
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
493
        return (self.base_url == ('https://%s' % host))
494
495
    def check(self):
496
        response = self._api_request('GET', 'user')
497
        if response.status == 200:
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
498
            self._current_user = json.loads(response.data)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
499
            return
7296.10.2 by Jelmer Vernooij
More fixes.
500
        if response == 401:
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
501
            if json.loads(response.data) == {"message": "401 Unauthorized"}:
7296.10.2 by Jelmer Vernooij
More fixes.
502
                raise GitLabLoginMissing()
503
            else:
504
                raise GitlabLoginError(response.text)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
505
        raise UnsupportedHoster(url)
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
506
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
507
    @classmethod
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
508
    def probe_from_url(cls, url, possible_transports=None):
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
509
        try:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
510
            (host, project) = parse_gitlab_url(url)
0.431.17 by Jelmer Vernooij
Try harder to avoid detecting any URL as a GitLab URL.
511
        except NotGitLabUrl:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
512
            raise UnsupportedHoster(url)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
513
        transport = get_transport(
514
            'https://%s' % host, possible_transports=possible_transports)
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
515
        credentials = get_credentials_by_url(transport.base)
516
        if credentials is not None:
517
            return cls(transport, credentials.get('private_token'))
518
        raise UnsupportedHoster(url)
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
519
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
520
    @classmethod
521
    def iter_instances(cls):
522
        for name, credentials in iter_tokens():
523
            if 'url' not in credentials:
524
                continue
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
525
            yield cls(
526
                get_transport(credentials['url']),
527
                private_token=credentials.get('private_token'))
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
528
0.431.66 by Jelmer Vernooij
Add support for status argument.
529
    def iter_my_proposals(self, status='open'):
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
530
        state = mp_status_to_status(status)
7296.10.9 by Jelmer Vernooij
Fix method name spacing.
531
        for mp in self._list_merge_requests(
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
532
                owner=self._get_logged_in_username(), state=state):
533
            yield GitLabMergeProposal(self, mp)
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
534
7414.5.2 by Jelmer Vernooij
Change iter_my_projects to iter_my_forks.
535
    def iter_my_forks(self):
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
536
        for project in self._list_projects(owner=self._get_logged_in_username()):
537
            base_project = project.get('forked_from_project')
7414.5.2 by Jelmer Vernooij
Change iter_my_projects to iter_my_forks.
538
            if not base_project:
539
                continue
540
            yield project['path_with_namespace']
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
541
7296.9.1 by Jelmer Vernooij
Add 'brz land' subcommand.
542
    def get_proposal_by_url(self, url):
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
543
        try:
544
            (host, project, merge_id) = parse_gitlab_merge_request_url(url)
545
        except NotGitLabUrl:
546
            raise UnsupportedHoster(url)
7296.9.4 by Jelmer Vernooij
Fix dealing with non-gitlab sites.
547
        except NotMergeRequestUrl as e:
7360.1.4 by Jelmer Vernooij
Fix retrieval of proposals from gitlab.
548
            if self.base_url == ('https://%s' % e.host):
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
549
                raise
550
            else:
551
                raise UnsupportedHoster(url)
7360.1.4 by Jelmer Vernooij
Fix retrieval of proposals from gitlab.
552
        if self.base_url != ('https://%s' % host):
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
553
            raise UnsupportedHoster(url)
7360.1.4 by Jelmer Vernooij
Fix retrieval of proposals from gitlab.
554
        project = self._get_project(project)
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
555
        mr = project.mergerequests.get(merge_id)
556
        return GitLabMergeProposal(mr)
7296.9.1 by Jelmer Vernooij
Add 'brz land' subcommand.
557
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
558
    def delete_project(self, project):
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
559
        path = 'projects/%s' % urlutils.quote(str(project), '')
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
560
        response = self._api_request('DELETE', path)
561
        if response.status == 404:
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
562
            raise NoSuchProject(project)
563
        if response.status != 202:
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
564
            raise errors.InvalidHttpResponse(path, response.text)
565
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
566
0.432.2 by Jelmer Vernooij
Publish command sort of works.
567
class GitlabMergeProposalBuilder(MergeProposalBuilder):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
568
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
569
    def __init__(self, gl, source_branch, target_branch):
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
570
        self.gl = gl
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
571
        self.source_branch = source_branch
572
        (self.source_host, self.source_project_name, self.source_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
573
            parse_gitlab_branch_url(source_branch))
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
574
        self.target_branch = target_branch
575
        (self.target_host, self.target_project_name, self.target_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
576
            parse_gitlab_branch_url(target_branch))
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
577
        if self.source_host != self.target_host:
578
            raise DifferentGitLabInstances(self.source_host, self.target_host)
579
580
    def get_infotext(self):
581
        """Determine the initial comment for the merge proposal."""
582
        info = []
583
        info.append("Gitlab instance: %s\n" % self.target_host)
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
584
        info.append("Source: %s\n" % self.source_branch.user_url)
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
585
        info.append("Target: %s\n" % self.target_branch.user_url)
586
        return ''.join(info)
587
588
    def get_initial_body(self):
589
        """Get a body for the proposal for the user to modify.
590
591
        :return: a str or None.
592
        """
593
        return None
594
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
595
    def create_proposal(self, description, reviewers=None, labels=None,
7467.3.1 by Jelmer Vernooij
Add a work_in_progress flag.
596
                        prerequisite_branch=None, commit_message=None,
7490.6.1 by Jelmer Vernooij
Add allow-collaboration flag.
597
                        work_in_progress=False, allow_collaboration=False):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
598
        """Perform the submission."""
7296.8.1 by Jelmer Vernooij
Add commit-message option to 'brz propose'.
599
        # https://docs.gitlab.com/ee/api/merge_requests.html#create-mr
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
600
        if prerequisite_branch is not None:
601
            raise PrerequisiteBranchUnsupported(self)
7296.8.1 by Jelmer Vernooij
Add commit-message option to 'brz propose'.
602
        # 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.
603
        source_project = self.gl._get_project(self.source_project_name)
604
        target_project = self.gl._get_project(self.target_project_name)
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
605
        # TODO(jelmer): Allow setting title explicitly
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
606
        title = determine_title(description)
7467.3.1 by Jelmer Vernooij
Add a work_in_progress flag.
607
        if work_in_progress:
608
            title = 'WIP: %s' % title
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
609
        # TODO(jelmer): Allow setting milestone field
610
        # TODO(jelmer): Allow setting squash field
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
611
        kwargs = {
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
612
            'title': title,
7296.10.3 by Jelmer Vernooij
More fixes.
613
            'source_project_id': source_project['id'],
614
            'target_project_id': target_project['id'],
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
615
            'source_branch_name': self.source_branch_name,
616
            'target_branch_name': self.target_branch_name,
7490.6.1 by Jelmer Vernooij
Add allow-collaboration flag.
617
            'description': description,
618
            'allow_collaboration': allow_collaboration}
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
619
        if labels:
620
            kwargs['labels'] = ','.join(labels)
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
621
        if reviewers:
622
            kwargs['assignee_ids'] = []
623
            for reviewer in reviewers:
624
                if '@' in reviewer:
625
                    user = self.gl._get_user_by_email(reviewer)
626
                else:
627
                    user = self.gl._get_user(reviewer)
628
                kwargs['assignee_ids'].append(user['id'])
7490.10.1 by Jelmer Vernooij
Fix handling of 409s for gitlab.
629
        try:
630
            merge_request = self.gl._create_mergerequest(**kwargs)
631
        except MergeRequestExists:
632
            raise ProposalExists(self.source_branch.user_url)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
633
        return GitLabMergeProposal(self.gl, merge_request)
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
634
635
636
def register_gitlab_instance(shortname, url):
637
    """Register a gitlab instance.
638
639
    :param shortname: Short name (e.g. "gitlab")
640
    :param url: URL to the gitlab instance
641
    """
642
    from breezy.bugtracker import (
643
        tracker_registry,
644
        ProjectIntegerBugTracker,
645
        )
646
    tracker_registry.register(
647
        shortname, ProjectIntegerBugTracker(
648
            shortname, url + '/{project}/issues/{id}'))