/brz/remove-bazaar

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