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