/brz/remove-bazaar

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