/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
7490.144.1 by Jelmer Vernooij
Support providing the actual proposal to MergeProposalExists.
21
import re
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
22
import time
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
23
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
24
from ... import (
7340.1.1 by Martin
Fix use of config_dir in propose plugin
25
    bedding,
0.431.33 by Jelmer Vernooij
Fix URLs from gitlab.
26
    branch as _mod_branch,
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
27
    controldir,
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
28
    errors,
29
    urlutils,
30
    )
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
31
from ...git.urls import git_url_to_bzr_url
7380.1.2 by Jelmer Vernooij
Review comments.
32
from ...trace import mutter
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
33
from ...transport import get_transport
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
34
7408.3.1 by Jelmer Vernooij
Move propose module into core.
35
from ...propose import (
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
36
    determine_title,
0.432.2 by Jelmer Vernooij
Publish command sort of works.
37
    Hoster,
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
38
    MergeProposal,
0.432.2 by Jelmer Vernooij
Publish command sort of works.
39
    MergeProposalBuilder,
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
40
    MergeProposalExists,
0.431.38 by Jelmer Vernooij
Add NoSuchProject.
41
    NoSuchProject,
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
42
    PrerequisiteBranchUnsupported,
7490.123.1 by Jelmer Vernooij
Fix missing import.
43
    SourceNotDerivedFromTarget,
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
7490.117.3 by Jelmer Vernooij
Raise detailed error when source branch is not derived from target branch.
79
class GitLabUnprocessable(errors.BzrError):
80
81
    _fmt = "GitLab can not process request: %(error)s."
82
83
    def __init__(self, error):
84
        errors.BzrError.__init__(self, error=error)
85
86
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
87
class DifferentGitLabInstances(errors.BzrError):
88
89
    _fmt = ("Can't create merge proposals across GitLab instances: "
90
            "%(source_host)s and %(target_host)s")
91
92
    def __init__(self, source_host, target_host):
93
        self.source_host = source_host
94
        self.target_host = target_host
95
96
0.432.10 by Jelmer Vernooij
More test fixes.
97
class GitLabLoginMissing(errors.BzrError):
98
99
    _fmt = ("Please log into GitLab")
100
101
7296.10.2 by Jelmer Vernooij
More fixes.
102
class GitlabLoginError(errors.BzrError):
103
104
    _fmt = ("Error logging in: %(error)s")
105
106
    def __init__(self, error):
107
        self.error = error
108
109
7490.96.1 by Jelmer Vernooij
Raise proper exception when getting a 409 back from GitLab.
110
class GitLabConflict(errors.BzrError):
111
7490.106.1 by Jelmer Vernooij
Avoid use of reserved 'msg' argument for BrzError.
112
    _fmt = "Conflict during operation: %(reason)s"
7490.96.1 by Jelmer Vernooij
Raise proper exception when getting a 409 back from GitLab.
113
7490.106.1 by Jelmer Vernooij
Avoid use of reserved 'msg' argument for BrzError.
114
    def __init__(self, reason):
115
        errors.BzrError(self, reason=reason)
7490.96.1 by Jelmer Vernooij
Raise proper exception when getting a 409 back from GitLab.
116
117
7490.67.1 by Jelmer Vernooij
Raise proper exception when forking isn't available for a project.
118
class ForkingDisabled(errors.BzrError):
119
120
    _fmt = ("Forking on project %(project)s is disabled.")
121
122
    def __init__(self, project):
123
        self.project = project
124
125
7490.144.1 by Jelmer Vernooij
Support providing the actual proposal to MergeProposalExists.
126
class MergeRequestConflict(Exception):
127
    """Raised when a merge requests conflicts."""
128
129
    def __init__(self, reason):
130
        self.reason = reason
7490.10.1 by Jelmer Vernooij
Fix handling of 409s for gitlab.
131
132
7490.143.1 by Jelmer Vernooij
Add dedicated exception for project creation timeout.
133
class ProjectCreationTimeout(errors.BzrError):
134
7490.143.2 by Jelmer Vernooij
Propagate timeout.
135
    _fmt = ("Timeout (%(timeout)ds) while waiting for project "
136
            "%(project)s to be created.")
7490.143.1 by Jelmer Vernooij
Add dedicated exception for project creation timeout.
137
7490.143.2 by Jelmer Vernooij
Propagate timeout.
138
    def __init__(self, project, timeout):
7490.143.1 by Jelmer Vernooij
Add dedicated exception for project creation timeout.
139
        self.project = project
7490.143.2 by Jelmer Vernooij
Propagate timeout.
140
        self.timeout = timeout
7490.143.1 by Jelmer Vernooij
Add dedicated exception for project creation timeout.
141
142
0.431.59 by Jelmer Vernooij
Add gitlab-login command.
143
def default_config_path():
7340.1.1 by Martin
Fix use of config_dir in propose plugin
144
    return os.path.join(bedding.config_dir(), 'gitlab.conf')
0.431.59 by Jelmer Vernooij
Add gitlab-login command.
145
146
147
def store_gitlab_token(name, url, private_token):
148
    """Store a GitLab token in a configuration file."""
149
    import configparser
150
    config = configparser.ConfigParser()
151
    path = default_config_path()
152
    config.read([path])
153
    config.add_section(name)
154
    config[name]['url'] = url
155
    config[name]['private_token'] = private_token
156
    with open(path, 'w') as f:
157
        config.write(f)
158
159
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
160
def iter_tokens():
161
    import configparser
162
    config = configparser.ConfigParser()
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
163
    config.read(
164
        [os.path.expanduser(p) for p in _DEFAULT_FILES] +
165
        [default_config_path()])
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
166
    for name, section in config.items():
167
        yield name, section
168
169
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
170
def get_credentials_by_url(url):
171
    for name, credentials in iter_tokens():
172
        if 'url' not in credentials:
173
            continue
174
        if credentials['url'].rstrip('/') == url.rstrip('/'):
175
            return credentials
176
    else:
177
        return None
178
179
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
180
def parse_gitlab_url(url):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
181
    (scheme, user, password, host, port, path) = urlutils.parse_url(
182
        url)
0.431.17 by Jelmer Vernooij
Try harder to avoid detecting any URL as a GitLab URL.
183
    if scheme not in ('git+ssh', 'https', 'http'):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
184
        raise NotGitLabUrl(url)
0.431.17 by Jelmer Vernooij
Try harder to avoid detecting any URL as a GitLab URL.
185
    if not host:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
186
        raise NotGitLabUrl(url)
0.432.10 by Jelmer Vernooij
More test fixes.
187
    path = path.strip('/')
0.432.11 by Jelmer Vernooij
Fix some tests.
188
    if path.endswith('.git'):
189
        path = path[:-4]
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
190
    return host, path
191
192
193
def parse_gitlab_branch_url(branch):
7441.1.1 by Jelmer Vernooij
Add strip_segment_parameters function.
194
    url = urlutils.strip_segment_parameters(branch.user_url)
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
195
    host, path = parse_gitlab_url(url)
0.432.10 by Jelmer Vernooij
More test fixes.
196
    return host, path, branch.name
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
197
198
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
199
def parse_gitlab_merge_request_url(url):
200
    (scheme, user, password, host, port, path) = urlutils.parse_url(
201
        url)
202
    if scheme not in ('git+ssh', 'https', 'http'):
203
        raise NotGitLabUrl(url)
204
    if not host:
205
        raise NotGitLabUrl(url)
206
    path = path.strip('/')
207
    parts = path.split('/')
7490.23.1 by Jelmer Vernooij
Add support for newer style gitlab merge proposal URLs.
208
    if len(parts) < 2:
209
        raise NotMergeRequestUrl(host, url)
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
210
    if parts[-2] != 'merge_requests':
211
        raise NotMergeRequestUrl(host, url)
7490.23.1 by Jelmer Vernooij
Add support for newer style gitlab merge proposal URLs.
212
    if parts[-3] == '-':
213
        project_name = '/'.join(parts[:-3])
214
    else:
215
        project_name = '/'.join(parts[:-2])
216
    return host, project_name, int(parts[-1])
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
217
218
7490.117.2 by Jelmer Vernooij
Include body when receiving an unexpected HTTP Status from GitLab.
219
def _unexpected_status(path, response):
220
    raise errors.UnexpectedHttpStatus(
221
        path, response.status, response.data.decode('utf-8', 'replace'))
222
223
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
224
class GitLabMergeProposal(MergeProposal):
225
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
226
    def __init__(self, gl, mr):
227
        self.gl = gl
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
228
        self._mr = mr
229
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
230
    def _update(self, **kwargs):
231
        self.gl._update_merge_request(self._mr['project_id'], self._mr['iid'], kwargs)
232
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
233
    def __repr__(self):
234
        return "<%s at %r>" % (type(self).__name__, self._mr['web_url'])
235
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
236
    @property
237
    def url(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
238
        return self._mr['web_url']
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
239
240
    def get_description(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
241
        return self._mr['description']
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
242
243
    def set_description(self, description):
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
244
        self._update(description=description, title=determine_title(description))
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
245
7296.8.2 by Jelmer Vernooij
Add feature flag for commit message.
246
    def get_commit_message(self):
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
247
        return self._mr.get('merge_commit_message')
248
249
    def set_commit_message(self, message):
250
        raise errors.UnsupportedOperation(self.set_commit_message, self)
7296.8.2 by Jelmer Vernooij
Add feature flag for commit message.
251
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
252
    def _branch_url_from_project(self, project_id, branch_name):
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
253
        if project_id is None:
254
            return None
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
255
        project = self.gl._get_project(project_id)
7296.10.3 by Jelmer Vernooij
More fixes.
256
        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.
257
258
    def get_source_branch_url(self):
259
        return self._branch_url_from_project(
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
260
            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.
261
7490.65.1 by Jelmer Vernooij
Add functions fo retrieving merge proposal source revision.
262
    def get_source_revision(self):
263
        from breezy.git.mapping import default_mapping
7490.65.2 by Jelmer Vernooij
Fix bugs, add release note.
264
        sha = self._mr['sha']
265
        if sha is None:
266
            return None
267
        return default_mapping.revision_id_foreign_to_bzr(sha.encode('ascii'))
7490.65.1 by Jelmer Vernooij
Add functions fo retrieving merge proposal source revision.
268
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
269
    def get_target_branch_url(self):
270
        return self._branch_url_from_project(
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
271
            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.
272
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
273
    def _get_project_name(self, project_id):
274
        source_project = self.gl._get_project(project_id)
275
        return source_project['path_with_namespace']
276
277
    def get_source_project(self):
278
        return self._get_project_name(self._mr['source_project_id'])
279
280
    def get_target_project(self):
281
        return self._get_project_name(self._mr['target_project_id'])
282
0.431.46 by Jelmer Vernooij
Add MergeProposal.is_merged.
283
    def is_merged(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
284
        return (self._mr['state'] == 'merged')
0.431.46 by Jelmer Vernooij
Add MergeProposal.is_merged.
285
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
286
    def is_closed(self):
287
        return (self._mr['state'] == 'closed')
288
289
    def reopen(self):
7405.2.1 by Jelmer Vernooij
Fix reopen behaviour for gitlab.
290
        return self._update(state_event='reopen')
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
291
7260.2.1 by Jelmer Vernooij
Implement .close on merge proposals.
292
    def close(self):
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
293
        self._update(state_event='close')
7260.2.1 by Jelmer Vernooij
Implement .close on merge proposals.
294
7296.9.1 by Jelmer Vernooij
Add 'brz land' subcommand.
295
    def merge(self, commit_message=None):
296
        # https://docs.gitlab.com/ee/api/merge_requests.html#accept-mr
297
        self._mr.merge(merge_commit_message=commit_message)
298
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
299
    def can_be_merged(self):
300
        if self._mr['merge_status'] == 'cannot_be_merged':
301
            return False
302
        elif self._mr['merge_status'] == 'can_be_merged':
303
            return True
7490.30.2 by Jelmer Vernooij
Support cannot_be_merged_rechecked
304
        elif self._mr['merge_status'] in (
305
                'unchecked', 'cannot_be_merged_recheck'):
306
            # See https://gitlab.com/gitlab-org/gitlab/-/commit/7517105303c for
307
            # an explanation of the distinction between unchecked and
308
            # cannot_be_merged_recheck
7490.30.1 by Jelmer Vernooij
Support the 'unchecked' value for can_be_merged.
309
            return None
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
310
        else:
311
            raise ValueError(self._mr['merge_status'])
312
7414.4.1 by Jelmer Vernooij
Add a MergeProposal.get_merged_by method.
313
    def get_merged_by(self):
7414.4.2 by Jelmer Vernooij
Fix gitlab / github merged_by fetching.
314
        user = self._mr.get('merged_by')
315
        if user is None:
316
            return None
317
        return user['username']
7414.4.1 by Jelmer Vernooij
Add a MergeProposal.get_merged_by method.
318
7414.4.3 by Jelmer Vernooij
Add MergeProposal.get_merged_at.
319
    def get_merged_at(self):
320
        merged_at = self._mr.get('merged_at')
321
        if merged_at is None:
322
            return None
7414.4.4 by Jelmer Vernooij
Use iso8601 module.
323
        import iso8601
324
        return iso8601.parse_date(merged_at)
7414.4.3 by Jelmer Vernooij
Add MergeProposal.get_merged_at.
325
7490.52.1 by Jelmer Vernooij
Add MergeProposal.post_comment.
326
    def post_comment(self, body):
327
        kwargs = {'body': body}
328
        self.gl._post_merge_request_note(
329
            self._mr['project_id'], self._mr['iid'], kwargs)
330
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
331
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
332
def gitlab_url_to_bzr_url(url, name):
7408.2.1 by Jelmer Vernooij
Use standard functions for creating Git URLs.
333
    return git_url_to_bzr_url(url, branch=name)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
334
335
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
336
class GitLab(Hoster):
337
    """GitLab hoster implementation."""
338
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
339
    supports_merge_proposal_labels = True
7296.8.2 by Jelmer Vernooij
Add feature flag for commit message.
340
    supports_merge_proposal_commit_message = False
7490.3.9 by Jelmer Vernooij
Add supports_allow_collaboration flag.
341
    supports_allow_collaboration = True
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
342
    merge_proposal_description_format = 'markdown'
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
343
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
344
    def __repr__(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
345
        return "<GitLab(%r)>" % self.base_url
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
346
7260.1.1 by Jelmer Vernooij
Add .base_url property to Hoster.
347
    @property
348
    def base_url(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
349
        return self.transport.base
350
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
351
    @property
352
    def base_hostname(self):
353
        return urlutils.parse_url(self.base_url)[3]
354
7490.137.1 by Jelmer Vernooij
Attempt to find correct name for any GitLab projects that may have been renamed.
355
    def _find_correct_project_name(self, path):
356
        try:
357
            resp = self.transport.request(
358
                'GET', urlutils.join(self.base_url, path),
359
                headers=self.headers)
360
        except errors.RedirectRequested as e:
361
            return urlutils.parse_url(e.target)[-1].strip('/')
362
        if resp.status != 200:
363
            _unexpected_status(path, resp)
364
        return None
365
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
366
    def _api_request(self, method, path, fields=None, body=None):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
367
        return self.transport.request(
368
            method, urlutils.join(self.base_url, 'api', 'v4', path),
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
369
            headers=self.headers, fields=fields, body=body)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
370
371
    def __init__(self, transport, private_token):
372
        self.transport = transport
373
        self.headers = {"Private-Token": private_token}
374
        self.check()
375
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
376
    def _get_user(self, username):
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
377
        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.
378
        response = self._api_request('GET', path)
379
        if response.status == 404:
380
            raise KeyError('no such user %s' % username)
381
        if response.status == 200:
382
            return json.loads(response.data)
7490.117.2 by Jelmer Vernooij
Include body when receiving an unexpected HTTP Status from GitLab.
383
        _unexpected_status(path, response)
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
384
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
385
    def _get_user_by_email(self, email):
386
        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.
387
        response = self._api_request('GET', path)
388
        if response.status == 404:
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
389
            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.
390
        if response.status == 200:
391
            ret = json.loads(response.data)
392
            if len(ret) != 1:
393
                raise ValueError('unexpected number of results; %r' % ret)
394
            return ret[0]
7490.117.2 by Jelmer Vernooij
Include body when receiving an unexpected HTTP Status from GitLab.
395
        _unexpected_status(path, response)
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
396
7490.137.1 by Jelmer Vernooij
Attempt to find correct name for any GitLab projects that may have been renamed.
397
    def _get_project(self, project_name, _redirect_checked=False):
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
398
        path = 'projects/%s' % urlutils.quote(str(project_name), '')
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
399
        response = self._api_request('GET', path)
400
        if response.status == 404:
7490.137.1 by Jelmer Vernooij
Attempt to find correct name for any GitLab projects that may have been renamed.
401
            if not _redirect_checked:
402
                project_name = self._find_correct_project_name(project_name)
403
                if project_name is not None:
404
                    return self._get_project(project_name, _redirect_checked=True)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
405
            raise NoSuchProject(project_name)
406
        if response.status == 200:
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
407
            return json.loads(response.data)
7490.117.2 by Jelmer Vernooij
Include body when receiving an unexpected HTTP Status from GitLab.
408
        _unexpected_status(path, response)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
409
7490.102.1 by Jelmer Vernooij
Fix support for owner in GitLab proposals.
410
    def _fork_project(self, project_name, timeout=50, interval=5, owner=None):
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
411
        path = 'projects/%s/fork' % urlutils.quote(str(project_name), '')
7490.102.1 by Jelmer Vernooij
Fix support for owner in GitLab proposals.
412
        fields = {}
413
        if owner is not None:
414
            fields['namespace'] = owner
415
        response = self._api_request('POST', path, fields=fields)
7490.67.1 by Jelmer Vernooij
Raise proper exception when forking isn't available for a project.
416
        if response.status == 404:
417
            raise ForkingDisabled(project_name)
7490.96.1 by Jelmer Vernooij
Raise proper exception when getting a 409 back from GitLab.
418
        if response.status == 409:
7490.96.2 by Jelmer Vernooij
Avoid async for now.
419
            resp = json.loads(response.data)
420
            raise GitLabConflict(resp.get('message'))
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
421
        if response.status not in (200, 201):
7490.117.2 by Jelmer Vernooij
Include body when receiving an unexpected HTTP Status from GitLab.
422
            _unexpected_status(path, response)
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
423
        # The response should be valid JSON, but let's ignore it
7397.1.1 by Jelmer Vernooij
Fix project forking.
424
        project = json.loads(response.data)
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
425
        # Spin and wait until import_status for new project
426
        # is complete.
7380.1.2 by Jelmer Vernooij
Review comments.
427
        deadline = time.time() + timeout
7397.1.1 by Jelmer Vernooij
Fix project forking.
428
        while project['import_status'] not in ('finished', 'none'):
7380.1.2 by Jelmer Vernooij
Review comments.
429
            mutter('import status is %s', project['import_status'])
430
            if time.time() > deadline:
7490.143.2 by Jelmer Vernooij
Propagate timeout.
431
                raise ProjectCreationTimeout(
432
                    project['path_with_namespace'], timeout)
7380.1.2 by Jelmer Vernooij
Review comments.
433
            time.sleep(interval)
7397.1.1 by Jelmer Vernooij
Fix project forking.
434
            project = self._get_project(project['path_with_namespace'])
435
        return project
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
436
7490.105.1 by Jelmer Vernooij
Add some hoster metadata fields.
437
    def get_current_user(self):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
438
        return self._current_user['username']
439
7490.105.1 by Jelmer Vernooij
Add some hoster metadata fields.
440
    def get_user_url(self, username):
441
        return urlutils.join(self.base_url, username)
442
7408.1.1 by Jelmer Vernooij
Use paging to iterate over all gitlab pull requests.
443
    def _list_paged(self, path, parameters=None, per_page=None):
444
        if parameters is None:
445
            parameters = {}
446
        else:
447
            parameters = dict(parameters.items())
448
        if per_page:
7408.1.3 by Jelmer Vernooij
Support pagination for github.
449
            parameters['per_page'] = str(per_page)
7408.1.1 by Jelmer Vernooij
Use paging to iterate over all gitlab pull requests.
450
        page = "1"
451
        while page:
452
            parameters['page'] = page
453
            response = self._api_request(
454
                'GET', path + '?' +
455
                ';'.join(['%s=%s' % item for item in parameters.items()]))
456
            if response.status == 403:
457
                raise errors.PermissionDenied(response.text)
458
            if response.status != 200:
7490.117.2 by Jelmer Vernooij
Include body when receiving an unexpected HTTP Status from GitLab.
459
                _unexpected_status(path, response)
7408.1.1 by Jelmer Vernooij
Use paging to iterate over all gitlab pull requests.
460
            page = response.getheader("X-Next-Page")
461
            for entry in json.loads(response.data):
462
                yield entry
463
7490.141.1 by Jelmer Vernooij
Expose owner/author in API.
464
    def _list_merge_requests(self, author=None, project=None, state=None):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
465
        if project is not None:
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
466
            path = 'projects/%s/merge_requests' % urlutils.quote(str(project), '')
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
467
        else:
468
            path = 'merge_requests'
469
        parameters = {}
470
        if state:
471
            parameters['state'] = state
7490.141.1 by Jelmer Vernooij
Expose owner/author in API.
472
        if author:
7490.141.2 by Jelmer Vernooij
Fix typo.
473
            parameters['author_username'] = urlutils.quote(author, '')
7408.1.2 by Jelmer Vernooij
Set default page size to 50.
474
        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.
475
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
476
    def _get_merge_request(self, project, merge_id):
477
        path = 'projects/%s/merge_requests/%d' % (urlutils.quote(str(project), ''), merge_id)
478
        response = self._api_request('GET', path)
479
        if response.status == 403:
480
            raise errors.PermissionDenied(response.text)
481
        if response.status != 200:
7490.117.2 by Jelmer Vernooij
Include body when receiving an unexpected HTTP Status from GitLab.
482
            _unexpected_status(path, response)
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
483
        return json.loads(response.data)
484
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
485
    def _list_projects(self, owner):
486
        path = 'users/%s/projects' % urlutils.quote(str(owner), '')
487
        parameters = {}
488
        return self._list_paged(path, parameters, per_page=DEFAULT_PAGE_SIZE)
489
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
490
    def _update_merge_request(self, project_id, iid, mr):
491
        path = 'projects/%s/merge_requests/%s' % (
492
            urlutils.quote(str(project_id), ''), iid)
493
        response = self._api_request('PUT', path, fields=mr)
494
        if response.status == 200:
495
            return json.loads(response.data)
7490.119.1 by Jelmer Vernooij
Properly raise PermissionDenied when updating GitLab merge requests.
496
        if response.status == 403:
497
            raise errors.PermissionDenied(response.text)
7490.117.2 by Jelmer Vernooij
Include body when receiving an unexpected HTTP Status from GitLab.
498
        _unexpected_status(path, response)
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
499
7490.52.1 by Jelmer Vernooij
Add MergeProposal.post_comment.
500
    def _post_merge_request_note(self, project_id, iid, kwargs):
501
        path = 'projects/%s/merge_requests/%s/notes' % (
502
            urlutils.quote(str(project_id), ''), iid)
503
        response = self._api_request('POST', path, fields=kwargs)
504
        if response.status == 201:
505
            json.loads(response.data)
506
            return
7490.118.1 by Jelmer Vernooij
Raise PermissionDenied when posting a comments results in a 403.
507
        if response.status == 403:
508
            raise errors.PermissionDenied(response.text)
7490.117.2 by Jelmer Vernooij
Include body when receiving an unexpected HTTP Status from GitLab.
509
        _unexpected_status(path, response)
7490.52.1 by Jelmer Vernooij
Add MergeProposal.post_comment.
510
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
511
    def _create_mergerequest(
512
            self, title, source_project_id, target_project_id,
7296.10.3 by Jelmer Vernooij
More fixes.
513
            source_branch_name, target_branch_name, description,
7490.14.1 by Jelmer Vernooij
Various git fixes.
514
            labels=None, allow_collaboration=False):
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
515
        path = 'projects/%s/merge_requests' % source_project_id
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
516
        fields = {
517
            'title': title,
518
            'source_branch': source_branch_name,
519
            'target_branch': target_branch_name,
520
            'target_project_id': target_project_id,
521
            'description': description,
7490.14.1 by Jelmer Vernooij
Various git fixes.
522
            'allow_collaboration': allow_collaboration,
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
523
            }
524
        if labels:
525
            fields['labels'] = labels
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
526
        response = self._api_request('POST', path, fields=fields)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
527
        if response.status == 403:
528
            raise errors.PermissionDenied(response.text)
529
        if response.status == 409:
7490.144.1 by Jelmer Vernooij
Support providing the actual proposal to MergeProposalExists.
530
            raise MergeRequestConflict(json.loads(response.data))
7490.117.3 by Jelmer Vernooij
Raise detailed error when source branch is not derived from target branch.
531
        if response.status == 422:
532
            data = json.loads(response.data)
533
            raise GitLabUnprocessable(data['error'])
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
534
        if response.status != 201:
7490.117.2 by Jelmer Vernooij
Include body when receiving an unexpected HTTP Status from GitLab.
535
            _unexpected_status(path, response)
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
536
        return json.loads(response.data)
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
537
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
538
    def get_push_url(self, branch):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
539
        (host, project_name, branch_name) = parse_gitlab_branch_url(branch)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
540
        project = self._get_project(project_name)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
541
        return gitlab_url_to_bzr_url(
7296.10.3 by Jelmer Vernooij
More fixes.
542
            project['ssh_url_to_repo'], branch_name)
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
543
0.431.20 by Jelmer Vernooij
publish -> publish_derived.
544
    def publish_derived(self, local_branch, base_branch, name, project=None,
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
545
                        owner=None, revision_id=None, overwrite=False,
7489.4.2 by Jelmer Vernooij
Plumb through tag_selector.
546
                        allow_lossy=True, tag_selector=None):
7490.137.1 by Jelmer Vernooij
Attempt to find correct name for any GitLab projects that may have been renamed.
547
        (host, base_project_name, base_branch_name) = parse_gitlab_branch_url(base_branch)
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
548
        if owner is None:
7490.141.1 by Jelmer Vernooij
Expose owner/author in API.
549
            owner = base_branch.get_config_stack().get('fork-namespace')
550
        if owner is None:
7490.105.1 by Jelmer Vernooij
Add some hoster metadata fields.
551
            owner = self.get_current_user()
7490.137.1 by Jelmer Vernooij
Attempt to find correct name for any GitLab projects that may have been renamed.
552
        base_project = self._get_project(base_project_name)
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
553
        if project is None:
7490.137.1 by Jelmer Vernooij
Attempt to find correct name for any GitLab projects that may have been renamed.
554
            project = base_project['path']
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
555
        try:
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
556
            target_project = self._get_project('%s/%s' % (owner, project))
557
        except NoSuchProject:
7490.137.1 by Jelmer Vernooij
Attempt to find correct name for any GitLab projects that may have been renamed.
558
            target_project = self._fork_project(
559
                base_project['path_with_namespace'], owner=owner)
7296.10.3 by Jelmer Vernooij
More fixes.
560
        remote_repo_url = git_url_to_bzr_url(target_project['ssh_url_to_repo'])
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
561
        remote_dir = controldir.ControlDir.open(remote_repo_url)
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
562
        try:
7211.13.7 by Jelmer Vernooij
Fix formatting.
563
            push_result = remote_dir.push_branch(
564
                local_branch, revision_id=revision_id, overwrite=overwrite,
7489.4.2 by Jelmer Vernooij
Plumb through tag_selector.
565
                name=name, tag_selector=tag_selector)
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
566
        except errors.NoRoundtrippingSupport:
567
            if not allow_lossy:
568
                raise
7211.13.7 by Jelmer Vernooij
Fix formatting.
569
            push_result = remote_dir.push_branch(
570
                local_branch, revision_id=revision_id, overwrite=overwrite,
7489.4.2 by Jelmer Vernooij
Plumb through tag_selector.
571
                name=name, lossy=True, tag_selector=tag_selector)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
572
        public_url = gitlab_url_to_bzr_url(
7296.10.3 by Jelmer Vernooij
More fixes.
573
            target_project['http_url_to_repo'], name)
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
574
        return push_result.target_branch, public_url
0.432.4 by Jelmer Vernooij
Some work on gitlab.
575
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
576
    def get_derived_branch(self, base_branch, name, project=None, owner=None):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
577
        (host, base_project, base_branch_name) = parse_gitlab_branch_url(base_branch)
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
578
        if owner is None:
7490.105.1 by Jelmer Vernooij
Add some hoster metadata fields.
579
            owner = self.get_current_user()
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
580
        if project is None:
7296.10.3 by Jelmer Vernooij
More fixes.
581
            project = self._get_project(base_project)['path']
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
582
        try:
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
583
            target_project = self._get_project('%s/%s' % (owner, project))
584
        except NoSuchProject:
585
            raise errors.NotBranchError('%s/%s/%s' % (self.base_url, owner, project))
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
586
        return _mod_branch.Branch.open(gitlab_url_to_bzr_url(
7296.10.3 by Jelmer Vernooij
More fixes.
587
            target_project['ssh_url_to_repo'], name))
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
588
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
589
    def get_proposer(self, source_branch, target_branch):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
590
        return GitlabMergeProposalBuilder(self, source_branch, target_branch)
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
591
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
592
    def iter_proposals(self, source_branch, target_branch, status):
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
593
        (source_host, source_project_name, source_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
594
            parse_gitlab_branch_url(source_branch))
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
595
        (target_host, target_project_name, target_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
596
            parse_gitlab_branch_url(target_branch))
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
597
        if source_host != target_host:
598
            raise DifferentGitLabInstances(source_host, target_host)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
599
        source_project = self._get_project(source_project_name)
600
        target_project = self._get_project(target_project_name)
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
601
        state = mp_status_to_status(status)
7360.1.4 by Jelmer Vernooij
Fix retrieval of proposals from gitlab.
602
        for mr in self._list_merge_requests(
7296.10.3 by Jelmer Vernooij
More fixes.
603
                project=target_project['id'], state=state):
604
            if (mr['source_project_id'] != source_project['id'] or
605
                    mr['source_branch'] != source_branch_name or
606
                    mr['target_project_id'] != target_project['id'] or
607
                    mr['target_branch'] != target_branch_name):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
608
                continue
609
            yield GitLabMergeProposal(self, mr)
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
610
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
611
    def hosts(self, branch):
612
        try:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
613
            (host, project, branch_name) = parse_gitlab_branch_url(branch)
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
614
        except NotGitLabUrl:
615
            return False
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
616
        return self.base_hostname == host
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
617
618
    def check(self):
619
        response = self._api_request('GET', 'user')
620
        if response.status == 200:
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
621
            self._current_user = json.loads(response.data)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
622
            return
7296.10.2 by Jelmer Vernooij
More fixes.
623
        if response == 401:
7296.10.8 by Jelmer Vernooij
Remove json attribute from Response object, consistent with urllib3 API.
624
            if json.loads(response.data) == {"message": "401 Unauthorized"}:
7296.10.2 by Jelmer Vernooij
More fixes.
625
                raise GitLabLoginMissing()
626
            else:
627
                raise GitlabLoginError(response.text)
7490.86.2 by Jelmer Vernooij
If there is no github login, then don't list github as supported.
628
        raise UnsupportedHoster(self.base_url)
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
629
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
630
    @classmethod
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
631
    def probe_from_url(cls, url, possible_transports=None):
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
632
        try:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
633
            (host, project) = parse_gitlab_url(url)
0.431.17 by Jelmer Vernooij
Try harder to avoid detecting any URL as a GitLab URL.
634
        except NotGitLabUrl:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
635
            raise UnsupportedHoster(url)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
636
        transport = get_transport(
637
            'https://%s' % host, possible_transports=possible_transports)
7359.1.2 by Jelmer Vernooij
Some fixes for gitlab API.
638
        credentials = get_credentials_by_url(transport.base)
639
        if credentials is not None:
640
            return cls(transport, credentials.get('private_token'))
641
        raise UnsupportedHoster(url)
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
642
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
643
    @classmethod
644
    def iter_instances(cls):
645
        for name, credentials in iter_tokens():
646
            if 'url' not in credentials:
647
                continue
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
648
            yield cls(
649
                get_transport(credentials['url']),
650
                private_token=credentials.get('private_token'))
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
651
7490.141.1 by Jelmer Vernooij
Expose owner/author in API.
652
    def iter_my_proposals(self, status='open', author=None):
653
        if author is None:
654
            author = self.get_current_user()
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
655
        state = mp_status_to_status(status)
7490.141.1 by Jelmer Vernooij
Expose owner/author in API.
656
        for mp in self._list_merge_requests(author=author, state=state):
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
657
            yield GitLabMergeProposal(self, mp)
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
658
7490.141.1 by Jelmer Vernooij
Expose owner/author in API.
659
    def iter_my_forks(self, owner=None):
660
        if owner is not None:
661
            owner = self.get_current_user()
662
        for project in self._list_projects(owner=owner):
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
663
            base_project = project.get('forked_from_project')
7414.5.2 by Jelmer Vernooij
Change iter_my_projects to iter_my_forks.
664
            if not base_project:
665
                continue
666
            yield project['path_with_namespace']
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
667
7296.9.1 by Jelmer Vernooij
Add 'brz land' subcommand.
668
    def get_proposal_by_url(self, url):
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
669
        try:
670
            (host, project, merge_id) = parse_gitlab_merge_request_url(url)
671
        except NotGitLabUrl:
672
            raise UnsupportedHoster(url)
7296.9.4 by Jelmer Vernooij
Fix dealing with non-gitlab sites.
673
        except NotMergeRequestUrl as e:
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
674
            if self.base_hostname == e.host:
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
675
                raise
676
            else:
677
                raise UnsupportedHoster(url)
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
678
        if self.base_hostname != host:
7296.9.3 by Jelmer Vernooij
Support finding merge proposals by URL on GitLab instances.
679
            raise UnsupportedHoster(url)
7360.1.4 by Jelmer Vernooij
Fix retrieval of proposals from gitlab.
680
        project = self._get_project(project)
7490.23.2 by Jelmer Vernooij
More gitlab fixes.
681
        mr = self._get_merge_request(project['path_with_namespace'], merge_id)
682
        return GitLabMergeProposal(self, mr)
7296.9.1 by Jelmer Vernooij
Add 'brz land' subcommand.
683
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
684
    def delete_project(self, project):
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
685
        path = 'projects/%s' % urlutils.quote(str(project), '')
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
686
        response = self._api_request('DELETE', path)
687
        if response.status == 404:
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
688
            raise NoSuchProject(project)
689
        if response.status != 202:
7490.117.2 by Jelmer Vernooij
Include body when receiving an unexpected HTTP Status from GitLab.
690
            _unexpected_status(path, response)
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
691
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
692
0.432.2 by Jelmer Vernooij
Publish command sort of works.
693
class GitlabMergeProposalBuilder(MergeProposalBuilder):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
694
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
695
    def __init__(self, gl, source_branch, target_branch):
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
696
        self.gl = gl
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
697
        self.source_branch = source_branch
698
        (self.source_host, self.source_project_name, self.source_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
699
            parse_gitlab_branch_url(source_branch))
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
700
        self.target_branch = target_branch
701
        (self.target_host, self.target_project_name, self.target_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
702
            parse_gitlab_branch_url(target_branch))
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
703
        if self.source_host != self.target_host:
704
            raise DifferentGitLabInstances(self.source_host, self.target_host)
705
706
    def get_infotext(self):
707
        """Determine the initial comment for the merge proposal."""
708
        info = []
709
        info.append("Gitlab instance: %s\n" % self.target_host)
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
710
        info.append("Source: %s\n" % self.source_branch.user_url)
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
711
        info.append("Target: %s\n" % self.target_branch.user_url)
712
        return ''.join(info)
713
714
    def get_initial_body(self):
715
        """Get a body for the proposal for the user to modify.
716
717
        :return: a str or None.
718
        """
719
        return None
720
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
721
    def create_proposal(self, description, reviewers=None, labels=None,
7467.3.1 by Jelmer Vernooij
Add a work_in_progress flag.
722
                        prerequisite_branch=None, commit_message=None,
7490.6.1 by Jelmer Vernooij
Add allow-collaboration flag.
723
                        work_in_progress=False, allow_collaboration=False):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
724
        """Perform the submission."""
7296.8.1 by Jelmer Vernooij
Add commit-message option to 'brz propose'.
725
        # https://docs.gitlab.com/ee/api/merge_requests.html#create-mr
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
726
        if prerequisite_branch is not None:
727
            raise PrerequisiteBranchUnsupported(self)
7296.8.1 by Jelmer Vernooij
Add commit-message option to 'brz propose'.
728
        # 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.
729
        source_project = self.gl._get_project(self.source_project_name)
730
        target_project = self.gl._get_project(self.target_project_name)
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
731
        # TODO(jelmer): Allow setting title explicitly
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
732
        title = determine_title(description)
7467.3.1 by Jelmer Vernooij
Add a work_in_progress flag.
733
        if work_in_progress:
734
            title = 'WIP: %s' % title
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
735
        # TODO(jelmer): Allow setting milestone field
736
        # TODO(jelmer): Allow setting squash field
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
737
        kwargs = {
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
738
            'title': title,
7296.10.3 by Jelmer Vernooij
More fixes.
739
            'source_project_id': source_project['id'],
740
            'target_project_id': target_project['id'],
7371.4.4 by Jelmer Vernooij
Pull in more fixes from janitor.
741
            'source_branch_name': self.source_branch_name,
742
            'target_branch_name': self.target_branch_name,
7490.6.1 by Jelmer Vernooij
Add allow-collaboration flag.
743
            'description': description,
744
            'allow_collaboration': allow_collaboration}
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
745
        if labels:
746
            kwargs['labels'] = ','.join(labels)
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
747
        if reviewers:
748
            kwargs['assignee_ids'] = []
749
            for reviewer in reviewers:
750
                if '@' in reviewer:
751
                    user = self.gl._get_user_by_email(reviewer)
752
                else:
753
                    user = self.gl._get_user(reviewer)
754
                kwargs['assignee_ids'].append(user['id'])
7490.10.1 by Jelmer Vernooij
Fix handling of 409s for gitlab.
755
        try:
756
            merge_request = self.gl._create_mergerequest(**kwargs)
7490.144.1 by Jelmer Vernooij
Support providing the actual proposal to MergeProposalExists.
757
        except MergeRequestConflict as e:
758
            m = re.fullmatch(
759
                r'Another open merge request already exists for '
760
                r'this source branch: \!([0-9]+)',
761
                e.reason['message'][0])
762
            if m:
763
                merge_id = int(m.group(1))
764
                mr = self.gl._get_merge_request(
765
                    target_project['path_with_namespace'], merge_id)
766
                raise MergeProposalExists(
767
                    self.source_branch.user_url, GitLabMergeProposal(self.gl, mr))
768
            raise Exception('conflict: %r' % e.reason)
7490.117.3 by Jelmer Vernooij
Raise detailed error when source branch is not derived from target branch.
769
        except GitLabUnprocessable as e:
770
            if e.error == [
771
                    "Source project is not a fork of the target project"]:
772
                raise SourceNotDerivedFromTarget(
773
                    self.source_branch, self.target_branch)
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
774
        return GitLabMergeProposal(self.gl, merge_request)
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
775
776
777
def register_gitlab_instance(shortname, url):
778
    """Register a gitlab instance.
779
780
    :param shortname: Short name (e.g. "gitlab")
781
    :param url: URL to the gitlab instance
782
    """
783
    from breezy.bugtracker import (
784
        tracker_registry,
785
        ProjectIntegerBugTracker,
786
        )
787
    tracker_registry.register(
788
        shortname, ProjectIntegerBugTracker(
789
            shortname, url + '/{project}/issues/{id}'))