/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('/')
7490.23.1 by Jelmer Vernooij
Add support for newer style gitlab merge proposal URLs.
172
    if len(parts) < 2:
173
        raise NotMergeRequestUrl(host, url)
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
174
    if parts[-2] != 'merge_requests':
175
        raise NotMergeRequestUrl(host, url)
7490.23.1 by Jelmer Vernooij
Add support for newer style gitlab merge proposal URLs.
176
    if parts[-3] == '-':
177
        project_name = '/'.join(parts[:-3])
178
    else:
179
        project_name = '/'.join(parts[:-2])
180
    return host, project_name, int(parts[-1])
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
181
182
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
183
class GitLabMergeProposal(MergeProposal):
184
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
185
    def __init__(self, gl, mr):
186
        self.gl = gl
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
187
        self._mr = mr
188
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
189
    def _update(self, **kwargs):
190
        self.gl._update_merge_request(self._mr['project_id'], self._mr['iid'], kwargs)
191
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
192
    def __repr__(self):
193
        return "<%s at %r>" % (type(self).__name__, self._mr['web_url'])
194
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
195
    @property
196
    def url(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
197
        return self._mr['web_url']
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
198
199
    def get_description(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
200
        return self._mr['description']
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
201
202
    def set_description(self, description):
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
203
        self._update(description=description, title=determine_title(description))
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
204
7296.8.2 by Jelmer Vernooij
Add feature flag for commit message.
205
    def get_commit_message(self):
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
206
        return self._mr.get('merge_commit_message')
207
208
    def set_commit_message(self, message):
209
        raise errors.UnsupportedOperation(self.set_commit_message, self)
7296.8.2 by Jelmer Vernooij
Add feature flag for commit message.
210
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
211
    def _branch_url_from_project(self, project_id, branch_name):
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
212
        if project_id is None:
213
            return None
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
214
        project = self.gl._get_project(project_id)
7296.10.3 by Jelmer Vernooij
More fixes.
215
        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.
216
217
    def get_source_branch_url(self):
218
        return self._branch_url_from_project(
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
219
            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.
220
221
    def get_target_branch_url(self):
222
        return self._branch_url_from_project(
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
223
            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.
224
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
225
    def _get_project_name(self, project_id):
226
        source_project = self.gl._get_project(project_id)
227
        return source_project['path_with_namespace']
228
229
    def get_source_project(self):
230
        return self._get_project_name(self._mr['source_project_id'])
231
232
    def get_target_project(self):
233
        return self._get_project_name(self._mr['target_project_id'])
234
0.431.46 by Jelmer Vernooij
Add MergeProposal.is_merged.
235
    def is_merged(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
236
        return (self._mr['state'] == 'merged')
0.431.46 by Jelmer Vernooij
Add MergeProposal.is_merged.
237
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
238
    def is_closed(self):
239
        return (self._mr['state'] == 'closed')
240
241
    def reopen(self):
7405.2.1 by Jelmer Vernooij
Fix reopen behaviour for gitlab.
242
        return self._update(state_event='reopen')
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
243
7260.2.1 by Jelmer Vernooij
Implement .close on merge proposals.
244
    def close(self):
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
245
        self._update(state_event='close')
7260.2.1 by Jelmer Vernooij
Implement .close on merge proposals.
246
7296.9.1 by Jelmer Vernooij
Add 'brz land' subcommand.
247
    def merge(self, commit_message=None):
248
        # https://docs.gitlab.com/ee/api/merge_requests.html#accept-mr
249
        self._mr.merge(merge_commit_message=commit_message)
250
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
251
    def can_be_merged(self):
252
        if self._mr['merge_status'] == 'cannot_be_merged':
253
            return False
254
        elif self._mr['merge_status'] == 'can_be_merged':
255
            return True
256
        else:
257
            raise ValueError(self._mr['merge_status'])
258
7414.4.1 by Jelmer Vernooij
Add a MergeProposal.get_merged_by method.
259
    def get_merged_by(self):
7414.4.2 by Jelmer Vernooij
Fix gitlab / github merged_by fetching.
260
        user = self._mr.get('merged_by')
261
        if user is None:
262
            return None
263
        return user['username']
7414.4.1 by Jelmer Vernooij
Add a MergeProposal.get_merged_by method.
264
7414.4.3 by Jelmer Vernooij
Add MergeProposal.get_merged_at.
265
    def get_merged_at(self):
266
        merged_at = self._mr.get('merged_at')
267
        if merged_at is None:
268
            return None
7414.4.4 by Jelmer Vernooij
Use iso8601 module.
269
        import iso8601
270
        return iso8601.parse_date(merged_at)
7414.4.3 by Jelmer Vernooij
Add MergeProposal.get_merged_at.
271
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
272
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
273
def gitlab_url_to_bzr_url(url, name):
274
    if not PY3:
275
        name = name.encode('utf-8')
7408.2.1 by Jelmer Vernooij
Use standard functions for creating Git URLs.
276
    return git_url_to_bzr_url(url, branch=name)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
277
278
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
279
class GitLab(Hoster):
280
    """GitLab hoster implementation."""
281
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
282
    supports_merge_proposal_labels = True
7296.8.2 by Jelmer Vernooij
Add feature flag for commit message.
283
    supports_merge_proposal_commit_message = False
7490.3.9 by Jelmer Vernooij
Add supports_allow_collaboration flag.
284
    supports_allow_collaboration = True
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
285
    merge_proposal_description_format = 'markdown'
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
286
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
287
    def __repr__(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
288
        return "<GitLab(%r)>" % self.base_url
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
289
7260.1.1 by Jelmer Vernooij
Add .base_url property to Hoster.
290
    @property
291
    def base_url(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
292
        return self.transport.base
293
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
294
    @property
295
    def base_hostname(self):
296
        return urlutils.parse_url(self.base_url)[3]
297
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
298
    def _api_request(self, method, path, fields=None, body=None):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
299
        return self.transport.request(
300
            method, urlutils.join(self.base_url, 'api', 'v4', path),
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
301
            headers=self.headers, fields=fields, body=body)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
302
303
    def __init__(self, transport, private_token):
304
        self.transport = transport
305
        self.headers = {"Private-Token": private_token}
306
        self.check()
307
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
308
    def _get_user(self, username):
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
309
        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.
310
        response = self._api_request('GET', path)
311
        if response.status == 404:
312
            raise KeyError('no such user %s' % username)
313
        if response.status == 200:
314
            return json.loads(response.data)
315
        raise errors.InvalidHttpResponse(path, response.text)
316
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
317
    def _get_user_by_email(self, email):
318
        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.
319
        response = self._api_request('GET', path)
320
        if response.status == 404:
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
321
            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.
322
        if response.status == 200:
323
            ret = json.loads(response.data)
324
            if len(ret) != 1:
325
                raise ValueError('unexpected number of results; %r' % ret)
326
            return ret[0]
327
        raise errors.InvalidHttpResponse(path, response.text)
328
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
329
    def _get_project(self, project_name):
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
330
        path = 'projects/%s' % urlutils.quote(str(project_name), '')
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
331
        response = self._api_request('GET', path)
332
        if response.status == 404:
333
            raise NoSuchProject(project_name)
334
        if response.status == 200:
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
335
            return json.loads(response.data)
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
336
        raise errors.InvalidHttpResponse(path, response.text)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
337
7380.1.2 by Jelmer Vernooij
Review comments.
338
    def _fork_project(self, project_name, timeout=50, interval=5):
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
339
        path = 'projects/%s/fork' % urlutils.quote(str(project_name), '')
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
340
        response = self._api_request('POST', path)
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
341
        if response.status not in (200, 201):
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
342
            raise errors.InvalidHttpResponse(path, response.text)
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
343
        # The response should be valid JSON, but let's ignore it
7397.1.1 by Jelmer Vernooij
Fix project forking.
344
        project = json.loads(response.data)
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
345
        # Spin and wait until import_status for new project
346
        # is complete.
7380.1.2 by Jelmer Vernooij
Review comments.
347
        deadline = time.time() + timeout
7397.1.1 by Jelmer Vernooij
Fix project forking.
348
        while project['import_status'] not in ('finished', 'none'):
7380.1.2 by Jelmer Vernooij
Review comments.
349
            mutter('import status is %s', project['import_status'])
350
            if time.time() > deadline:
351
                raise Exception('timeout waiting for project to become available')
352
            time.sleep(interval)
7397.1.1 by Jelmer Vernooij
Fix project forking.
353
            project = self._get_project(project['path_with_namespace'])
354
        return project
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
355
356
    def _get_logged_in_username(self):
357
        return self._current_user['username']
358
7408.1.1 by Jelmer Vernooij
Use paging to iterate over all gitlab pull requests.
359
    def _list_paged(self, path, parameters=None, per_page=None):
360
        if parameters is None:
361
            parameters = {}
362
        else:
363
            parameters = dict(parameters.items())
364
        if per_page:
7408.1.3 by Jelmer Vernooij
Support pagination for github.
365
            parameters['per_page'] = str(per_page)
7408.1.1 by Jelmer Vernooij
Use paging to iterate over all gitlab pull requests.
366
        page = "1"
367
        while page:
368
            parameters['page'] = page
369
            response = self._api_request(
370
                'GET', path + '?' +
371
                ';'.join(['%s=%s' % item for item in parameters.items()]))
372
            if response.status == 403:
373
                raise errors.PermissionDenied(response.text)
374
            if response.status != 200:
375
                raise errors.InvalidHttpResponse(path, response.text)
376
            page = response.getheader("X-Next-Page")
377
            for entry in json.loads(response.data):
378
                yield entry
379
7296.10.9 by Jelmer Vernooij
Fix method name spacing.
380
    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.
381
        if project is not None:
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
382
            path = 'projects/%s/merge_requests' % urlutils.quote(str(project), '')
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
383
        else:
384
            path = 'merge_requests'
385
        parameters = {}
386
        if state:
387
            parameters['state'] = state
388
        if owner:
389
            parameters['owner_id'] = urlutils.quote(owner, '')
7408.1.2 by Jelmer Vernooij
Set default page size to 50.
390
        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.
391
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
392
    def _get_merge_request(self, project, merge_id):
393
        path = 'projects/%s/merge_requests/%d' % (urlutils.quote(str(project), ''), merge_id)
394
        response = self._api_request('GET', path)
395
        if response.status == 403:
396
            raise errors.PermissionDenied(response.text)
397
        if response.status != 200:
398
            raise errors.InvalidHttpResponse(path, response.text)
399
        return json.loads(response.data)
400
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
401
    def _list_projects(self, owner):
402
        path = 'users/%s/projects' % urlutils.quote(str(owner), '')
403
        parameters = {}
404
        return self._list_paged(path, parameters, per_page=DEFAULT_PAGE_SIZE)
405
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
406
    def _update_merge_request(self, project_id, iid, mr):
407
        path = 'projects/%s/merge_requests/%s' % (
408
            urlutils.quote(str(project_id), ''), iid)
409
        response = self._api_request('PUT', path, fields=mr)
410
        if response.status == 200:
411
            return json.loads(response.data)
412
        raise errors.InvalidHttpResponse(path, response.text)
413
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
414
    def _create_mergerequest(
415
            self, title, source_project_id, target_project_id,
7296.10.3 by Jelmer Vernooij
More fixes.
416
            source_branch_name, target_branch_name, description,
7490.14.1 by Jelmer Vernooij
Various git fixes.
417
            labels=None, allow_collaboration=False):
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
418
        path = 'projects/%s/merge_requests' % source_project_id
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
419
        fields = {
420
            'title': title,
421
            'source_branch': source_branch_name,
422
            'target_branch': target_branch_name,
423
            'target_project_id': target_project_id,
424
            'description': description,
7490.14.1 by Jelmer Vernooij
Various git fixes.
425
            'allow_collaboration': allow_collaboration,
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
426
            }
427
        if labels:
428
            fields['labels'] = labels
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
429
        response = self._api_request('POST', path, fields=fields)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
430
        if response.status == 403:
431
            raise errors.PermissionDenied(response.text)
432
        if response.status == 409:
7490.10.1 by Jelmer Vernooij
Fix handling of 409s for gitlab.
433
            raise MergeRequestExists()
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
434
        if response.status != 201:
435
            raise errors.InvalidHttpResponse(path, response.text)
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
436
        return json.loads(response.data)
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
437
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
438
    def get_push_url(self, branch):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
439
        (host, project_name, branch_name) = parse_gitlab_branch_url(branch)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
440
        project = self._get_project(project_name)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
441
        return gitlab_url_to_bzr_url(
7296.10.3 by Jelmer Vernooij
More fixes.
442
            project['ssh_url_to_repo'], branch_name)
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
443
0.431.20 by Jelmer Vernooij
publish -> publish_derived.
444
    def publish_derived(self, local_branch, base_branch, name, project=None,
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
445
                        owner=None, revision_id=None, overwrite=False,
7489.4.2 by Jelmer Vernooij
Plumb through tag_selector.
446
                        allow_lossy=True, tag_selector=None):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
447
        (host, base_project, base_branch_name) = parse_gitlab_branch_url(base_branch)
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
448
        if owner is None:
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
449
            owner = self._get_logged_in_username()
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
450
        if project is None:
7296.10.3 by Jelmer Vernooij
More fixes.
451
            project = self._get_project(base_project)['path']
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
452
        try:
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
453
            target_project = self._get_project('%s/%s' % (owner, project))
454
        except NoSuchProject:
455
            target_project = self._fork_project(base_project)
7296.10.3 by Jelmer Vernooij
More fixes.
456
        remote_repo_url = git_url_to_bzr_url(target_project['ssh_url_to_repo'])
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
457
        remote_dir = controldir.ControlDir.open(remote_repo_url)
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
458
        try:
7211.13.7 by Jelmer Vernooij
Fix formatting.
459
            push_result = remote_dir.push_branch(
460
                local_branch, revision_id=revision_id, overwrite=overwrite,
7489.4.2 by Jelmer Vernooij
Plumb through tag_selector.
461
                name=name, tag_selector=tag_selector)
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
462
        except errors.NoRoundtrippingSupport:
463
            if not allow_lossy:
464
                raise
7211.13.7 by Jelmer Vernooij
Fix formatting.
465
            push_result = remote_dir.push_branch(
466
                local_branch, revision_id=revision_id, overwrite=overwrite,
7489.4.2 by Jelmer Vernooij
Plumb through tag_selector.
467
                name=name, lossy=True, tag_selector=tag_selector)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
468
        public_url = gitlab_url_to_bzr_url(
7296.10.3 by Jelmer Vernooij
More fixes.
469
            target_project['http_url_to_repo'], name)
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
470
        return push_result.target_branch, public_url
0.432.4 by Jelmer Vernooij
Some work on gitlab.
471
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
472
    def get_derived_branch(self, base_branch, name, project=None, owner=None):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
473
        (host, base_project, base_branch_name) = parse_gitlab_branch_url(base_branch)
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
474
        if owner is None:
7296.10.3 by Jelmer Vernooij
More fixes.
475
            owner = self._get_logged_in_username()
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
476
        if project is None:
7296.10.3 by Jelmer Vernooij
More fixes.
477
            project = self._get_project(base_project)['path']
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
478
        try:
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
479
            target_project = self._get_project('%s/%s' % (owner, project))
480
        except NoSuchProject:
481
            raise errors.NotBranchError('%s/%s/%s' % (self.base_url, owner, project))
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
482
        return _mod_branch.Branch.open(gitlab_url_to_bzr_url(
7296.10.3 by Jelmer Vernooij
More fixes.
483
            target_project['ssh_url_to_repo'], name))
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
484
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
485
    def get_proposer(self, source_branch, target_branch):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
486
        return GitlabMergeProposalBuilder(self, source_branch, target_branch)
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
487
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
488
    def iter_proposals(self, source_branch, target_branch, status):
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
489
        (source_host, source_project_name, source_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
490
            parse_gitlab_branch_url(source_branch))
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
491
        (target_host, target_project_name, target_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
492
            parse_gitlab_branch_url(target_branch))
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
493
        if source_host != target_host:
494
            raise DifferentGitLabInstances(source_host, target_host)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
495
        source_project = self._get_project(source_project_name)
496
        target_project = self._get_project(target_project_name)
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
497
        state = mp_status_to_status(status)
7360.1.4 by Jelmer Vernooij
Fix retrieval of proposals from gitlab.
498
        for mr in self._list_merge_requests(
7296.10.3 by Jelmer Vernooij
More fixes.
499
                project=target_project['id'], state=state):
500
            if (mr['source_project_id'] != source_project['id'] or
501
                    mr['source_branch'] != source_branch_name or
502
                    mr['target_project_id'] != target_project['id'] or
503
                    mr['target_branch'] != target_branch_name):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
504
                continue
505
            yield GitLabMergeProposal(self, mr)
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
506
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
507
    def hosts(self, branch):
508
        try:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
509
            (host, project, branch_name) = parse_gitlab_branch_url(branch)
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
510
        except NotGitLabUrl:
511
            return False
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
512
        return self.base_hostname == host
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
513
514
    def check(self):
515
        response = self._api_request('GET', 'user')
516
        if response.status == 200:
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
517
            self._current_user = json.loads(response.data)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
518
            return
7296.10.2 by Jelmer Vernooij
More fixes.
519
        if response == 401:
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
520
            if json.loads(response.data) == {"message": "401 Unauthorized"}:
7296.10.2 by Jelmer Vernooij
More fixes.
521
                raise GitLabLoginMissing()
522
            else:
523
                raise GitlabLoginError(response.text)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
524
        raise UnsupportedHoster(url)
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
525
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
526
    @classmethod
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
527
    def probe_from_url(cls, url, possible_transports=None):
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
528
        try:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
529
            (host, project) = parse_gitlab_url(url)
0.431.17 by Jelmer Vernooij
Try harder to avoid detecting any URL as a GitLab URL.
530
        except NotGitLabUrl:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
531
            raise UnsupportedHoster(url)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
532
        transport = get_transport(
533
            'https://%s' % host, possible_transports=possible_transports)
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
534
        credentials = get_credentials_by_url(transport.base)
535
        if credentials is not None:
536
            return cls(transport, credentials.get('private_token'))
537
        raise UnsupportedHoster(url)
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
538
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
539
    @classmethod
540
    def iter_instances(cls):
541
        for name, credentials in iter_tokens():
542
            if 'url' not in credentials:
543
                continue
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
544
            yield cls(
545
                get_transport(credentials['url']),
546
                private_token=credentials.get('private_token'))
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
547
0.431.66 by Jelmer Vernooij
Add support for status argument.
548
    def iter_my_proposals(self, status='open'):
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
549
        state = mp_status_to_status(status)
7296.10.9 by Jelmer Vernooij
Fix method name spacing.
550
        for mp in self._list_merge_requests(
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
551
                owner=self._get_logged_in_username(), state=state):
552
            yield GitLabMergeProposal(self, mp)
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
553
7414.5.2 by Jelmer Vernooij
Change iter_my_projects to iter_my_forks.
554
    def iter_my_forks(self):
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
555
        for project in self._list_projects(owner=self._get_logged_in_username()):
556
            base_project = project.get('forked_from_project')
7414.5.2 by Jelmer Vernooij
Change iter_my_projects to iter_my_forks.
557
            if not base_project:
558
                continue
559
            yield project['path_with_namespace']
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
560
7296.9.1 by Jelmer Vernooij
Add 'brz land' subcommand.
561
    def get_proposal_by_url(self, url):
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
562
        try:
563
            (host, project, merge_id) = parse_gitlab_merge_request_url(url)
564
        except NotGitLabUrl:
565
            raise UnsupportedHoster(url)
7296.9.4 by Jelmer Vernooij
Fix dealing with non-gitlab sites.
566
        except NotMergeRequestUrl as e:
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
567
            if self.base_hostname == e.host:
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
568
                raise
569
            else:
570
                raise UnsupportedHoster(url)
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
571
        if self.base_hostname != host:
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
572
            raise UnsupportedHoster(url)
7360.1.4 by Jelmer Vernooij
Fix retrieval of proposals from gitlab.
573
        project = self._get_project(project)
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
574
        mr = self._get_merge_request(project['path_with_namespace'], merge_id)
575
        return GitLabMergeProposal(self, mr)
7296.9.1 by Jelmer Vernooij
Add 'brz land' subcommand.
576
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
577
    def delete_project(self, project):
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
578
        path = 'projects/%s' % urlutils.quote(str(project), '')
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
579
        response = self._api_request('DELETE', path)
580
        if response.status == 404:
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
581
            raise NoSuchProject(project)
582
        if response.status != 202:
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
583
            raise errors.InvalidHttpResponse(path, response.text)
584
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
585
0.432.2 by Jelmer Vernooij
Publish command sort of works.
586
class GitlabMergeProposalBuilder(MergeProposalBuilder):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
587
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
588
    def __init__(self, gl, source_branch, target_branch):
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
589
        self.gl = gl
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
590
        self.source_branch = source_branch
591
        (self.source_host, self.source_project_name, self.source_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
592
            parse_gitlab_branch_url(source_branch))
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
593
        self.target_branch = target_branch
594
        (self.target_host, self.target_project_name, self.target_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
595
            parse_gitlab_branch_url(target_branch))
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
596
        if self.source_host != self.target_host:
597
            raise DifferentGitLabInstances(self.source_host, self.target_host)
598
599
    def get_infotext(self):
600
        """Determine the initial comment for the merge proposal."""
601
        info = []
602
        info.append("Gitlab instance: %s\n" % self.target_host)
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
603
        info.append("Source: %s\n" % self.source_branch.user_url)
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
604
        info.append("Target: %s\n" % self.target_branch.user_url)
605
        return ''.join(info)
606
607
    def get_initial_body(self):
608
        """Get a body for the proposal for the user to modify.
609
610
        :return: a str or None.
611
        """
612
        return None
613
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
614
    def create_proposal(self, description, reviewers=None, labels=None,
7467.3.1 by Jelmer Vernooij
Add a work_in_progress flag.
615
                        prerequisite_branch=None, commit_message=None,
7490.6.1 by Jelmer Vernooij
Add allow-collaboration flag.
616
                        work_in_progress=False, allow_collaboration=False):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
617
        """Perform the submission."""
7296.8.1 by Jelmer Vernooij
Add commit-message option to 'brz propose'.
618
        # https://docs.gitlab.com/ee/api/merge_requests.html#create-mr
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
619
        if prerequisite_branch is not None:
620
            raise PrerequisiteBranchUnsupported(self)
7296.8.1 by Jelmer Vernooij
Add commit-message option to 'brz propose'.
621
        # 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.
622
        source_project = self.gl._get_project(self.source_project_name)
623
        target_project = self.gl._get_project(self.target_project_name)
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
624
        # TODO(jelmer): Allow setting title explicitly
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
625
        title = determine_title(description)
7467.3.1 by Jelmer Vernooij
Add a work_in_progress flag.
626
        if work_in_progress:
627
            title = 'WIP: %s' % title
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
628
        # TODO(jelmer): Allow setting milestone field
629
        # TODO(jelmer): Allow setting squash field
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
630
        kwargs = {
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
631
            'title': title,
7296.10.3 by Jelmer Vernooij
More fixes.
632
            'source_project_id': source_project['id'],
633
            'target_project_id': target_project['id'],
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
634
            'source_branch_name': self.source_branch_name,
635
            'target_branch_name': self.target_branch_name,
7490.6.1 by Jelmer Vernooij
Add allow-collaboration flag.
636
            'description': description,
637
            'allow_collaboration': allow_collaboration}
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
638
        if labels:
639
            kwargs['labels'] = ','.join(labels)
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
640
        if reviewers:
641
            kwargs['assignee_ids'] = []
642
            for reviewer in reviewers:
643
                if '@' in reviewer:
644
                    user = self.gl._get_user_by_email(reviewer)
645
                else:
646
                    user = self.gl._get_user(reviewer)
647
                kwargs['assignee_ids'].append(user['id'])
7490.10.1 by Jelmer Vernooij
Fix handling of 409s for gitlab.
648
        try:
649
            merge_request = self.gl._create_mergerequest(**kwargs)
650
        except MergeRequestExists:
7490.25.1 by Jelmer Vernooij
Fix raising of ProposalExists.
651
            raise MergeProposalExists(self.source_branch.user_url)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
652
        return GitLabMergeProposal(self.gl, merge_request)
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
653
654
655
def register_gitlab_instance(shortname, url):
656
    """Register a gitlab instance.
657
658
    :param shortname: Short name (e.g. "gitlab")
659
    :param url: URL to the gitlab instance
660
    """
661
    from breezy.bugtracker import (
662
        tracker_registry,
663
        ProjectIntegerBugTracker,
664
        )
665
    tracker_registry.register(
666
        shortname, ProjectIntegerBugTracker(
667
            shortname, url + '/{project}/issues/{id}'))