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