/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 (
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):
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
192
        self._update(description=description, title=description.splitlines()[0])
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):
263
    if not PY3:
264
        name = name.encode('utf-8')
7408.2.1 by Jelmer Vernooij
Use standard functions for creating Git URLs.
265
    return git_url_to_bzr_url(url, branch=name)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
266
267
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
268
class GitLab(Hoster):
269
    """GitLab hoster implementation."""
270
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
271
    supports_merge_proposal_labels = True
7296.8.2 by Jelmer Vernooij
Add feature flag for commit message.
272
    supports_merge_proposal_commit_message = False
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
273
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
274
    def __repr__(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
275
        return "<GitLab(%r)>" % self.base_url
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
276
7260.1.1 by Jelmer Vernooij
Add .base_url property to Hoster.
277
    @property
278
    def base_url(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
279
        return self.transport.base
280
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
281
    def _api_request(self, method, path, fields=None, body=None):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
282
        return self.transport.request(
283
            method, urlutils.join(self.base_url, 'api', 'v4', path),
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
284
            headers=self.headers, fields=fields, body=body)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
285
286
    def __init__(self, transport, private_token):
287
        self.transport = transport
288
        self.headers = {"Private-Token": private_token}
289
        self.check()
290
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
291
    def _get_user(self, username):
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
292
        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.
293
        response = self._api_request('GET', path)
294
        if response.status == 404:
295
            raise KeyError('no such user %s' % username)
296
        if response.status == 200:
297
            return json.loads(response.data)
298
        raise errors.InvalidHttpResponse(path, response.text)
299
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
300
    def _get_user_by_email(self, email):
301
        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.
302
        response = self._api_request('GET', path)
303
        if response.status == 404:
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
304
            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.
305
        if response.status == 200:
306
            ret = json.loads(response.data)
307
            if len(ret) != 1:
308
                raise ValueError('unexpected number of results; %r' % ret)
309
            return ret[0]
310
        raise errors.InvalidHttpResponse(path, response.text)
311
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
312
    def _get_project(self, project_name):
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
313
        path = 'projects/%s' % urlutils.quote(str(project_name), '')
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
314
        response = self._api_request('GET', path)
315
        if response.status == 404:
316
            raise NoSuchProject(project_name)
317
        if response.status == 200:
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
318
            return json.loads(response.data)
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
319
        raise errors.InvalidHttpResponse(path, response.text)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
320
7380.1.2 by Jelmer Vernooij
Review comments.
321
    def _fork_project(self, project_name, timeout=50, interval=5):
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
322
        path = 'projects/%s/fork' % urlutils.quote(str(project_name), '')
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
323
        response = self._api_request('POST', path)
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
324
        if response.status not in (200, 201):
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
325
            raise errors.InvalidHttpResponse(path, response.text)
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
326
        # The response should be valid JSON, but let's ignore it
7397.1.1 by Jelmer Vernooij
Fix project forking.
327
        project = json.loads(response.data)
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
328
        # Spin and wait until import_status for new project
329
        # is complete.
7380.1.2 by Jelmer Vernooij
Review comments.
330
        deadline = time.time() + timeout
7397.1.1 by Jelmer Vernooij
Fix project forking.
331
        while project['import_status'] not in ('finished', 'none'):
7380.1.2 by Jelmer Vernooij
Review comments.
332
            mutter('import status is %s', project['import_status'])
333
            if time.time() > deadline:
334
                raise Exception('timeout waiting for project to become available')
335
            time.sleep(interval)
7397.1.1 by Jelmer Vernooij
Fix project forking.
336
            project = self._get_project(project['path_with_namespace'])
337
        return project
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
338
339
    def _get_logged_in_username(self):
340
        return self._current_user['username']
341
7408.1.1 by Jelmer Vernooij
Use paging to iterate over all gitlab pull requests.
342
    def _list_paged(self, path, parameters=None, per_page=None):
343
        if parameters is None:
344
            parameters = {}
345
        else:
346
            parameters = dict(parameters.items())
347
        if per_page:
7408.1.3 by Jelmer Vernooij
Support pagination for github.
348
            parameters['per_page'] = str(per_page)
7408.1.1 by Jelmer Vernooij
Use paging to iterate over all gitlab pull requests.
349
        page = "1"
350
        while page:
351
            parameters['page'] = page
352
            response = self._api_request(
353
                'GET', path + '?' +
354
                ';'.join(['%s=%s' % item for item in parameters.items()]))
355
            if response.status == 403:
356
                raise errors.PermissionDenied(response.text)
357
            if response.status != 200:
358
                raise errors.InvalidHttpResponse(path, response.text)
359
            page = response.getheader("X-Next-Page")
360
            for entry in json.loads(response.data):
361
                yield entry
362
7296.10.9 by Jelmer Vernooij
Fix method name spacing.
363
    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.
364
        if project is not None:
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
365
            path = 'projects/%s/merge_requests' % urlutils.quote(str(project), '')
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
366
        else:
367
            path = 'merge_requests'
368
        parameters = {}
369
        if state:
370
            parameters['state'] = state
371
        if owner:
372
            parameters['owner_id'] = urlutils.quote(owner, '')
7408.1.2 by Jelmer Vernooij
Set default page size to 50.
373
        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.
374
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
375
    def _list_projects(self, owner):
376
        path = 'users/%s/projects' % urlutils.quote(str(owner), '')
377
        parameters = {}
378
        return self._list_paged(path, parameters, per_page=DEFAULT_PAGE_SIZE)
379
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
380
    def _update_merge_request(self, project_id, iid, mr):
381
        path = 'projects/%s/merge_requests/%s' % (
382
            urlutils.quote(str(project_id), ''), iid)
383
        response = self._api_request('PUT', path, fields=mr)
384
        if response.status == 200:
385
            return json.loads(response.data)
386
        raise errors.InvalidHttpResponse(path, response.text)
387
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
388
    def _create_mergerequest(
389
            self, title, source_project_id, target_project_id,
7296.10.3 by Jelmer Vernooij
More fixes.
390
            source_branch_name, target_branch_name, description,
391
            labels=None):
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
392
        path = 'projects/%s/merge_requests' % source_project_id
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
393
        fields = {
394
            'title': title,
395
            'source_branch': source_branch_name,
396
            'target_branch': target_branch_name,
397
            'target_project_id': target_project_id,
398
            'description': description,
399
            }
400
        if labels:
401
            fields['labels'] = labels
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
402
        response = self._api_request('POST', path, fields=fields)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
403
        if response.status == 403:
404
            raise errors.PermissionDenied(response.text)
405
        if response.status == 409:
406
            raise MergeProposalExists(self.source_branch.user_url)
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
407
        if response.status != 201:
408
            raise errors.InvalidHttpResponse(path, response.text)
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
409
        return json.loads(response.data)
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
410
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
411
    def get_push_url(self, branch):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
412
        (host, project_name, branch_name) = parse_gitlab_branch_url(branch)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
413
        project = self._get_project(project_name)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
414
        return gitlab_url_to_bzr_url(
7296.10.3 by Jelmer Vernooij
More fixes.
415
            project['ssh_url_to_repo'], branch_name)
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
416
0.431.20 by Jelmer Vernooij
publish -> publish_derived.
417
    def publish_derived(self, local_branch, base_branch, name, project=None,
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
418
                        owner=None, revision_id=None, overwrite=False,
419
                        allow_lossy=True):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
420
        (host, base_project, base_branch_name) = parse_gitlab_branch_url(base_branch)
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
421
        if owner is None:
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
422
            owner = self._get_logged_in_username()
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
423
        if project is None:
7296.10.3 by Jelmer Vernooij
More fixes.
424
            project = self._get_project(base_project)['path']
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
425
        try:
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
426
            target_project = self._get_project('%s/%s' % (owner, project))
427
        except NoSuchProject:
428
            target_project = self._fork_project(base_project)
7296.10.3 by Jelmer Vernooij
More fixes.
429
        remote_repo_url = git_url_to_bzr_url(target_project['ssh_url_to_repo'])
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
430
        remote_dir = controldir.ControlDir.open(remote_repo_url)
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
431
        try:
7211.13.7 by Jelmer Vernooij
Fix formatting.
432
            push_result = remote_dir.push_branch(
433
                local_branch, revision_id=revision_id, overwrite=overwrite,
434
                name=name)
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
435
        except errors.NoRoundtrippingSupport:
436
            if not allow_lossy:
437
                raise
7211.13.7 by Jelmer Vernooij
Fix formatting.
438
            push_result = remote_dir.push_branch(
439
                local_branch, revision_id=revision_id, overwrite=overwrite,
440
                name=name, lossy=True)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
441
        public_url = gitlab_url_to_bzr_url(
7296.10.3 by Jelmer Vernooij
More fixes.
442
            target_project['http_url_to_repo'], name)
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
443
        return push_result.target_branch, public_url
0.432.4 by Jelmer Vernooij
Some work on gitlab.
444
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
445
    def get_derived_branch(self, base_branch, name, project=None, owner=None):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
446
        (host, base_project, base_branch_name) = parse_gitlab_branch_url(base_branch)
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
447
        if owner is None:
7296.10.3 by Jelmer Vernooij
More fixes.
448
            owner = self._get_logged_in_username()
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
449
        if project is None:
7296.10.3 by Jelmer Vernooij
More fixes.
450
            project = self._get_project(base_project)['path']
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
451
        try:
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
452
            target_project = self._get_project('%s/%s' % (owner, project))
453
        except NoSuchProject:
454
            raise errors.NotBranchError('%s/%s/%s' % (self.base_url, owner, project))
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
455
        return _mod_branch.Branch.open(gitlab_url_to_bzr_url(
7296.10.3 by Jelmer Vernooij
More fixes.
456
            target_project['ssh_url_to_repo'], name))
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
457
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
458
    def get_proposer(self, source_branch, target_branch):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
459
        return GitlabMergeProposalBuilder(self, source_branch, target_branch)
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
460
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
461
    def iter_proposals(self, source_branch, target_branch, status):
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
462
        (source_host, source_project_name, source_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
463
            parse_gitlab_branch_url(source_branch))
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
464
        (target_host, target_project_name, target_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
465
            parse_gitlab_branch_url(target_branch))
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
466
        if source_host != target_host:
467
            raise DifferentGitLabInstances(source_host, target_host)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
468
        source_project = self._get_project(source_project_name)
469
        target_project = self._get_project(target_project_name)
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
470
        state = mp_status_to_status(status)
7360.1.4 by Jelmer Vernooij
Fix retrieval of proposals from gitlab.
471
        for mr in self._list_merge_requests(
7296.10.3 by Jelmer Vernooij
More fixes.
472
                project=target_project['id'], state=state):
473
            if (mr['source_project_id'] != source_project['id'] or
474
                    mr['source_branch'] != source_branch_name or
475
                    mr['target_project_id'] != target_project['id'] or
476
                    mr['target_branch'] != target_branch_name):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
477
                continue
478
            yield GitLabMergeProposal(self, mr)
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
479
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
480
    def hosts(self, branch):
481
        try:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
482
            (host, project, branch_name) = parse_gitlab_branch_url(branch)
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
483
        except NotGitLabUrl:
484
            return False
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
485
        return (self.base_url == ('https://%s' % host))
486
487
    def check(self):
488
        response = self._api_request('GET', 'user')
489
        if response.status == 200:
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
490
            self._current_user = json.loads(response.data)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
491
            return
7296.10.2 by Jelmer Vernooij
More fixes.
492
        if response == 401:
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
493
            if json.loads(response.data) == {"message": "401 Unauthorized"}:
7296.10.2 by Jelmer Vernooij
More fixes.
494
                raise GitLabLoginMissing()
495
            else:
496
                raise GitlabLoginError(response.text)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
497
        raise UnsupportedHoster(url)
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
498
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
499
    @classmethod
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
500
    def probe_from_url(cls, url, possible_transports=None):
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
501
        try:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
502
            (host, project) = parse_gitlab_url(url)
0.431.17 by Jelmer Vernooij
Try harder to avoid detecting any URL as a GitLab URL.
503
        except NotGitLabUrl:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
504
            raise UnsupportedHoster(url)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
505
        transport = get_transport(
506
            'https://%s' % host, possible_transports=possible_transports)
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
507
        credentials = get_credentials_by_url(transport.base)
508
        if credentials is not None:
509
            return cls(transport, credentials.get('private_token'))
510
        raise UnsupportedHoster(url)
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
511
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
512
    @classmethod
513
    def iter_instances(cls):
514
        for name, credentials in iter_tokens():
515
            if 'url' not in credentials:
516
                continue
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
517
            yield cls(
518
                get_transport(credentials['url']),
519
                private_token=credentials.get('private_token'))
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
520
0.431.66 by Jelmer Vernooij
Add support for status argument.
521
    def iter_my_proposals(self, status='open'):
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
522
        state = mp_status_to_status(status)
7296.10.9 by Jelmer Vernooij
Fix method name spacing.
523
        for mp in self._list_merge_requests(
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
524
                owner=self._get_logged_in_username(), state=state):
525
            yield GitLabMergeProposal(self, mp)
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
526
7414.5.2 by Jelmer Vernooij
Change iter_my_projects to iter_my_forks.
527
    def iter_my_forks(self):
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
528
        for project in self._list_projects(owner=self._get_logged_in_username()):
529
            base_project = project.get('forked_from_project')
7414.5.2 by Jelmer Vernooij
Change iter_my_projects to iter_my_forks.
530
            if not base_project:
531
                continue
532
            yield project['path_with_namespace']
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
533
7296.9.1 by Jelmer Vernooij
Add 'brz land' subcommand.
534
    def get_proposal_by_url(self, url):
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
535
        try:
536
            (host, project, merge_id) = parse_gitlab_merge_request_url(url)
537
        except NotGitLabUrl:
538
            raise UnsupportedHoster(url)
7296.9.4 by Jelmer Vernooij
Fix dealing with non-gitlab sites.
539
        except NotMergeRequestUrl as e:
7360.1.4 by Jelmer Vernooij
Fix retrieval of proposals from gitlab.
540
            if self.base_url == ('https://%s' % e.host):
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
541
                raise
542
            else:
543
                raise UnsupportedHoster(url)
7360.1.4 by Jelmer Vernooij
Fix retrieval of proposals from gitlab.
544
        if self.base_url != ('https://%s' % host):
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
545
            raise UnsupportedHoster(url)
7360.1.4 by Jelmer Vernooij
Fix retrieval of proposals from gitlab.
546
        project = self._get_project(project)
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
547
        mr = project.mergerequests.get(merge_id)
548
        return GitLabMergeProposal(mr)
7296.9.1 by Jelmer Vernooij
Add 'brz land' subcommand.
549
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
550
    def delete_project(self, project):
551
        path = 'projects/%s' % urlutils.quote(str(project_name), '')
552
        response = self._api_request('DELETE', path)
553
        if response.status == 404:
554
            raise NoSuchProject(project_name)
555
        if response.status != 200:
556
            raise errors.InvalidHttpResponse(path, response.text)
557
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
558
0.432.2 by Jelmer Vernooij
Publish command sort of works.
559
class GitlabMergeProposalBuilder(MergeProposalBuilder):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
560
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
561
    def __init__(self, gl, source_branch, target_branch):
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
562
        self.gl = gl
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
563
        self.source_branch = source_branch
564
        (self.source_host, self.source_project_name, self.source_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
565
            parse_gitlab_branch_url(source_branch))
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
566
        self.target_branch = target_branch
567
        (self.target_host, self.target_project_name, self.target_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
568
            parse_gitlab_branch_url(target_branch))
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
569
        if self.source_host != self.target_host:
570
            raise DifferentGitLabInstances(self.source_host, self.target_host)
571
572
    def get_infotext(self):
573
        """Determine the initial comment for the merge proposal."""
574
        info = []
575
        info.append("Gitlab instance: %s\n" % self.target_host)
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
576
        info.append("Source: %s\n" % self.source_branch.user_url)
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
577
        info.append("Target: %s\n" % self.target_branch.user_url)
578
        return ''.join(info)
579
580
    def get_initial_body(self):
581
        """Get a body for the proposal for the user to modify.
582
583
        :return: a str or None.
584
        """
585
        return None
586
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
587
    def create_proposal(self, description, reviewers=None, labels=None,
7296.8.1 by Jelmer Vernooij
Add commit-message option to 'brz propose'.
588
                        prerequisite_branch=None, commit_message=None):
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
597
        title = description.splitlines()[0]
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}'))