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