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