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