/brz/remove-bazaar

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