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