/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
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
21
from ... import (
0.431.33 by Jelmer Vernooij
Fix URLs from gitlab.
22
    branch as _mod_branch,
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
23
    controldir,
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
24
    errors,
25
    urlutils,
26
    )
0.432.4 by Jelmer Vernooij
Some work on gitlab.
27
from ...config import AuthenticationConfig
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
28
from ...git.urls import git_url_to_bzr_url
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
29
from ...sixish import PY3
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
30
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
31
from .propose import (
0.432.2 by Jelmer Vernooij
Publish command sort of works.
32
    Hoster,
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
33
    MergeProposal,
0.432.2 by Jelmer Vernooij
Publish command sort of works.
34
    MergeProposalBuilder,
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
35
    MergeProposalExists,
0.431.36 by Jelmer Vernooij
Fix import.
36
    NoMergeProposal,
0.431.38 by Jelmer Vernooij
Add NoSuchProject.
37
    NoSuchProject,
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
38
    PrerequisiteBranchUnsupported,
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
39
    UnsupportedHoster,
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
40
    )
41
42
0.431.17 by Jelmer Vernooij
Try harder to avoid detecting any URL as a GitLab URL.
43
class NotGitLabUrl(errors.BzrError):
44
45
    _fmt = "Not a GitLab URL: %(url)s"
46
47
    def __init__(self, url):
48
        errors.BzrError.__init__(self)
49
        self.url = url
50
51
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
52
class DifferentGitLabInstances(errors.BzrError):
53
54
    _fmt = ("Can't create merge proposals across GitLab instances: "
55
            "%(source_host)s and %(target_host)s")
56
57
    def __init__(self, source_host, target_host):
58
        self.source_host = source_host
59
        self.target_host = target_host
60
61
0.432.10 by Jelmer Vernooij
More test fixes.
62
class GitLabLoginMissing(errors.BzrError):
63
64
    _fmt = ("Please log into GitLab")
65
66
0.431.59 by Jelmer Vernooij
Add gitlab-login command.
67
def default_config_path():
68
    from breezy.config import config_dir
69
    import os
70
    return os.path.join(config_dir(), 'gitlab.conf')
71
72
73
def store_gitlab_token(name, url, private_token):
74
    """Store a GitLab token in a configuration file."""
75
    import configparser
76
    config = configparser.ConfigParser()
77
    path = default_config_path()
78
    config.read([path])
79
    config.add_section(name)
80
    config[name]['url'] = url
81
    config[name]['private_token'] = private_token
82
    with open(path, 'w') as f:
83
        config.write(f)
84
85
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
86
def iter_tokens():
87
    import configparser
88
    from gitlab.config import _DEFAULT_FILES
89
    config = configparser.ConfigParser()
90
    config.read(_DEFAULT_FILES + [default_config_path()])
91
    for name, section in config.items():
92
        yield name, section
93
94
0.432.10 by Jelmer Vernooij
More test fixes.
95
def connect_gitlab(host):
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
96
    from gitlab import Gitlab, GitlabGetError
0.432.4 by Jelmer Vernooij
Some work on gitlab.
97
    auth = AuthenticationConfig()
98
0.432.10 by Jelmer Vernooij
More test fixes.
99
    url = 'https://%s' % host
100
    credentials = auth.get_credentials('https', host)
0.432.4 by Jelmer Vernooij
Some work on gitlab.
101
    if credentials is None:
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
102
        for name, section in iter_tokens():
0.432.10 by Jelmer Vernooij
More test fixes.
103
            if section.get('url') == url:
104
                credentials = section
105
                break
106
        else:
0.431.10 by Jelmer Vernooij
Various other fixes.
107
            try:
108
                return Gitlab(url)
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
109
            except GitlabGetError:
0.431.10 by Jelmer Vernooij
Various other fixes.
110
                raise GitLabLoginMissing()
0.432.10 by Jelmer Vernooij
More test fixes.
111
    else:
112
        credentials['url'] = url
113
    return Gitlab(**credentials)
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
114
115
116
def parse_gitlab_url(branch):
117
    url = urlutils.split_segment_parameters(branch.user_url)[0]
118
    (scheme, user, password, host, port, path) = urlutils.parse_url(
119
        url)
0.431.17 by Jelmer Vernooij
Try harder to avoid detecting any URL as a GitLab URL.
120
    if scheme not in ('git+ssh', 'https', 'http'):
121
        raise NotGitLabUrl(branch.user_url)
122
    if not host:
123
        raise NotGitLabUrl(branch.user_url)
0.432.10 by Jelmer Vernooij
More test fixes.
124
    path = path.strip('/')
0.432.11 by Jelmer Vernooij
Fix some tests.
125
    if path.endswith('.git'):
126
        path = path[:-4]
0.432.10 by Jelmer Vernooij
More test fixes.
127
    return host, path, branch.name
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
128
129
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
130
class GitLabMergeProposal(MergeProposal):
131
132
    def __init__(self, mr):
133
        self._mr = mr
134
135
    @property
136
    def url(self):
137
        return self._mr.web_url
138
139
    def get_description(self):
140
        return self._mr.description
141
142
    def set_description(self, description):
143
        self._mr.description = description
144
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
145
    def _branch_url_from_project(self, project_id, branch_name):
146
        project = self._mr.manager.gitlab.projects.get(project_id)
147
        return gitlab_url_to_bzr_url(project.http_url_to_repo, branch_name)
148
149
    def get_source_branch_url(self):
150
        return self._branch_url_from_project(
151
            self._mr.source_project_id, self._mr.source_branch)
152
153
    def get_target_branch_url(self):
154
        return self._branch_url_from_project(
155
            self._mr.target_project_id, self._mr.target_branch)
156
0.431.46 by Jelmer Vernooij
Add MergeProposal.is_merged.
157
    def is_merged(self):
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
158
        return (self._mr.state == 'merged')
0.431.46 by Jelmer Vernooij
Add MergeProposal.is_merged.
159
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
160
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
161
def gitlab_url_to_bzr_url(url, name):
162
    if not PY3:
163
        name = name.encode('utf-8')
164
    return urlutils.join_segment_parameters(
165
            git_url_to_bzr_url(url), {"branch": name})
166
167
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
168
class GitLab(Hoster):
169
    """GitLab hoster implementation."""
170
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
171
    supports_merge_proposal_labels = True
172
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
173
    def __repr__(self):
174
        return "<GitLab(%r)>" % self.gl.url
175
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
176
    def __init__(self, gl):
177
        self.gl = gl
178
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
179
    def get_push_url(self, branch):
180
        (host, project_name, branch_name) = parse_gitlab_url(branch)
181
        project = self.gl.projects.get(project_name)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
182
        return gitlab_url_to_bzr_url(
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
183
            project.ssh_url_to_repo, branch_name)
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
184
0.431.20 by Jelmer Vernooij
publish -> publish_derived.
185
    def publish_derived(self, local_branch, base_branch, name, project=None,
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
186
                        owner=None, revision_id=None, overwrite=False,
187
                        allow_lossy=True):
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
188
        import gitlab
0.432.4 by Jelmer Vernooij
Some work on gitlab.
189
        (host, base_project, base_branch_name) = parse_gitlab_url(base_branch)
0.432.10 by Jelmer Vernooij
More test fixes.
190
        self.gl.auth()
0.431.38 by Jelmer Vernooij
Add NoSuchProject.
191
        try:
192
            base_project = self.gl.projects.get(base_project)
193
        except gitlab.GitlabGetError as e:
194
            if e.response_code == 404:
195
                raise NoSuchProject(base_project)
196
            else:
197
                raise
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
198
        if owner is None:
0.432.10 by Jelmer Vernooij
More test fixes.
199
            owner = self.gl.user.username
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
200
        if project is None:
0.431.30 by Jelmer Vernooij
s/name/path.
201
            project = base_project.path
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
202
        try:
0.432.10 by Jelmer Vernooij
More test fixes.
203
            target_project = self.gl.projects.get('%s/%s' % (owner, project))
0.431.30 by Jelmer Vernooij
s/name/path.
204
        except gitlab.GitlabGetError as e:
205
            if e.response_code == 404:
206
                target_project = base_project.forks.create({})
207
            else:
208
                raise
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
209
        remote_repo_url = git_url_to_bzr_url(target_project.ssh_url_to_repo)
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
210
        remote_dir = controldir.ControlDir.open(remote_repo_url)
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
211
        try:
212
            push_result = remote_dir.push_branch(local_branch, revision_id=revision_id,
213
                overwrite=overwrite, name=name)
214
        except errors.NoRoundtrippingSupport:
215
            if not allow_lossy:
216
                raise
217
            push_result = remote_dir.push_branch(local_branch, revision_id=revision_id,
218
                overwrite=overwrite, name=name, lossy=True)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
219
        public_url = gitlab_url_to_bzr_url(
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
220
            target_project.http_url_to_repo, name)
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
221
        return push_result.target_branch, public_url
0.432.4 by Jelmer Vernooij
Some work on gitlab.
222
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
223
    def get_derived_branch(self, base_branch, name, project=None, owner=None):
224
        import gitlab
225
        (host, base_project, base_branch_name) = parse_gitlab_url(base_branch)
226
        self.gl.auth()
0.431.38 by Jelmer Vernooij
Add NoSuchProject.
227
        try:
228
            base_project = self.gl.projects.get(base_project)
229
        except gitlab.GitlabGetError as e:
230
            if e.response_code == 404:
231
                raise NoSuchProject(base_project)
232
            else:
233
                raise
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
234
        if owner is None:
235
            owner = self.gl.user.username
236
        if project is None:
0.431.30 by Jelmer Vernooij
s/name/path.
237
            project = base_project.path
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
238
        try:
239
            target_project = self.gl.projects.get('%s/%s' % (owner, project))
240
        except gitlab.GitlabGetError as e:
241
            if e.response_code == 404:
242
                raise errors.NotBranchError('%s/%s/%s' % (self.gl.url, owner, project))
243
            raise
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
244
        return _mod_branch.Branch.open(gitlab_url_to_bzr_url(
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
245
                target_project.ssh_url_to_repo, name))
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
246
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
247
    def get_proposer(self, source_branch, target_branch):
248
        return GitlabMergeProposalBuilder(self.gl, source_branch, target_branch)
249
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
250
    def get_proposal(self, source_branch, target_branch):
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
251
        import gitlab
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
252
        (source_host, source_project_name, source_branch_name) = (
253
            parse_gitlab_url(source_branch))
254
        (target_host, target_project_name, target_branch_name) = (
255
            parse_gitlab_url(target_branch))
256
        if source_host != target_host:
257
            raise DifferentGitLabInstances(source_host, target_host)
258
        self.gl.auth()
259
        source_project = self.gl.projects.get(source_project_name)
260
        target_project = self.gl.projects.get(target_project_name)
0.431.43 by Jelmer Vernooij
Handle 403s during proposal listing.
261
        try:
262
            for mr in target_project.mergerequests.list(state='all'):
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
263
                if (mr.source_project_id != source_project.id or
264
                    mr.source_branch != source_branch_name or
265
                    mr.target_project_id != target_project.id or
266
                    mr.target_branch != target_branch_name):
0.431.43 by Jelmer Vernooij
Handle 403s during proposal listing.
267
                    continue
268
                return GitLabMergeProposal(mr)
269
        except gitlab.GitlabListError as e:
270
            if e.response_code == 403:
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
271
                raise errors.PermissionDenied(e.error_message)
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
272
        raise NoMergeProposal()
273
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
274
    def hosts(self, branch):
275
        try:
276
            (host, project, branch_name) = parse_gitlab_url(branch)
277
        except NotGitLabUrl:
278
            return False
279
        return (self.gl.url == ('https://%s' % host))
280
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
281
    @classmethod
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
282
    def probe(cls, branch):
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
283
        try:
284
            (host, project, branch_name) = parse_gitlab_url(branch)
0.431.17 by Jelmer Vernooij
Try harder to avoid detecting any URL as a GitLab URL.
285
        except NotGitLabUrl:
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
286
            raise UnsupportedHoster(branch)
0.432.4 by Jelmer Vernooij
Some work on gitlab.
287
        import gitlab
0.431.43 by Jelmer Vernooij
Handle 403s during proposal listing.
288
        import requests.exceptions
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
289
        try:
0.432.4 by Jelmer Vernooij
Some work on gitlab.
290
            gl = connect_gitlab(host)
0.431.10 by Jelmer Vernooij
Various other fixes.
291
            gl.auth()
0.431.43 by Jelmer Vernooij
Handle 403s during proposal listing.
292
        except requests.exceptions.SSLError:
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
293
            # Well, I guess it could be..
0.431.43 by Jelmer Vernooij
Handle 403s during proposal listing.
294
            raise UnsupportedHoster(branch)
0.432.4 by Jelmer Vernooij
Some work on gitlab.
295
        except gitlab.GitlabGetError:
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
296
            raise UnsupportedHoster(branch)
0.431.10 by Jelmer Vernooij
Various other fixes.
297
        except gitlab.GitlabHttpError as e:
0.431.27 by Jelmer Vernooij
Catch 503 errors.
298
            if e.response_code in (404, 405, 503):
0.431.10 by Jelmer Vernooij
Various other fixes.
299
                raise UnsupportedHoster(branch)
300
            else:
301
                raise
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
302
        return cls(gl)
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
303
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
304
    @classmethod
305
    def iter_instances(cls):
306
        from gitlab import Gitlab
307
        for name, credentials in iter_tokens():
308
            if 'url' not in credentials:
309
                continue
310
            gl = Gitlab(**credentials)
311
            yield cls(gl)
312
313
    def iter_my_proposals(self):
314
        self.gl.auth()
315
        for mp in self.gl.mergerequests.list(owner=self.gl.user.username):
316
            yield GitLabMergeProposal(mp)
317
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
318
0.432.2 by Jelmer Vernooij
Publish command sort of works.
319
class GitlabMergeProposalBuilder(MergeProposalBuilder):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
320
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
321
    def __init__(self, gl, source_branch, target_branch):
322
        self.gl = gl
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
323
        self.source_branch = source_branch
324
        (self.source_host, self.source_project_name, self.source_branch_name) = (
325
            parse_gitlab_url(source_branch))
326
        self.target_branch = target_branch
327
        (self.target_host, self.target_project_name, self.target_branch_name) = (
328
            parse_gitlab_url(target_branch))
329
        if self.source_host != self.target_host:
330
            raise DifferentGitLabInstances(self.source_host, self.target_host)
331
332
    def get_infotext(self):
333
        """Determine the initial comment for the merge proposal."""
334
        info = []
335
        info.append("Gitlab instance: %s\n" % self.target_host)
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
336
        info.append("Source: %s\n" % self.source_branch.user_url)
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
337
        info.append("Target: %s\n" % self.target_branch.user_url)
338
        return ''.join(info)
339
340
    def get_initial_body(self):
341
        """Get a body for the proposal for the user to modify.
342
343
        :return: a str or None.
344
        """
345
        return None
346
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
347
    def create_proposal(self, description, reviewers=None, labels=None,
348
                        prerequisite_branch=None):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
349
        """Perform the submission."""
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
350
        if prerequisite_branch is not None:
351
            raise PrerequisiteBranchUnsupported(self)
0.431.16 by Jelmer Vernooij
gitlab: Report when a merge proposal already exists.
352
        import gitlab
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
353
        # TODO(jelmer): Support reviewers
0.432.10 by Jelmer Vernooij
More test fixes.
354
        self.gl.auth()
355
        source_project = self.gl.projects.get(self.source_project_name)
356
        target_project = self.gl.projects.get(self.target_project_name)
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
357
        # TODO(jelmer): Allow setting title explicitly
358
        title = description.splitlines()[0]
359
        # TODO(jelmer): Allow setting allow_collaboration field
360
        # TODO(jelmer): Allow setting milestone field
361
        # TODO(jelmer): Allow setting squash field
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
362
        kwargs = {
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
363
            'title': title,
364
            'target_project_id': target_project.id,
365
            'source_branch': self.source_branch_name,
366
            'target_branch': self.target_branch_name,
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
367
            'description': description}
368
        if labels:
369
            kwargs['labels'] = ','.join(labels)
0.431.16 by Jelmer Vernooij
gitlab: Report when a merge proposal already exists.
370
        try:
371
            merge_request = source_project.mergerequests.create(kwargs)
372
        except gitlab.GitlabCreateError as e:
0.431.34 by Jelmer Vernooij
Cope with gitlab 403.
373
            if e.response_code == 403:
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
374
                raise errors.PermissionDenied(e.error_message)
0.431.16 by Jelmer Vernooij
gitlab: Report when a merge proposal already exists.
375
            if e.response_code == 409:
376
                raise MergeProposalExists(self.source_branch.user_url)
377
            raise
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
378
        return GitLabMergeProposal(merge_request)
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
379
380
381
def register_gitlab_instance(shortname, url):
382
    """Register a gitlab instance.
383
384
    :param shortname: Short name (e.g. "gitlab")
385
    :param url: URL to the gitlab instance
386
    """
387
    from breezy.bugtracker import (
388
        tracker_registry,
389
        ProjectIntegerBugTracker,
390
        )
391
    tracker_registry.register(
392
        shortname, ProjectIntegerBugTracker(
393
            shortname, url + '/{project}/issues/{id}'))