/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.46 by Jelmer Vernooij
Add MergeProposal.is_merged.
145
    def is_merged(self):
146
        return (self._mr.attributes['state'] == 'merged')
147
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
148
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
149
def gitlab_url_to_bzr_url(url, name):
150
    if not PY3:
151
        name = name.encode('utf-8')
152
    return urlutils.join_segment_parameters(
7211.13.7 by Jelmer Vernooij
Fix formatting.
153
        git_url_to_bzr_url(url), {"branch": name})
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
154
155
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
156
class GitLab(Hoster):
157
    """GitLab hoster implementation."""
158
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
159
    supports_merge_proposal_labels = True
160
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
161
    def __repr__(self):
162
        return "<GitLab(%r)>" % self.gl.url
163
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
164
    def __init__(self, gl):
165
        self.gl = gl
166
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
167
    def get_push_url(self, branch):
168
        (host, project_name, branch_name) = parse_gitlab_url(branch)
169
        project = self.gl.projects.get(project_name)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
170
        return gitlab_url_to_bzr_url(
171
            project.attributes['ssh_url_to_repo'], branch_name)
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
172
0.431.20 by Jelmer Vernooij
publish -> publish_derived.
173
    def publish_derived(self, local_branch, base_branch, name, project=None,
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
174
                        owner=None, revision_id=None, overwrite=False,
175
                        allow_lossy=True):
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
176
        import gitlab
0.432.4 by Jelmer Vernooij
Some work on gitlab.
177
        (host, base_project, base_branch_name) = parse_gitlab_url(base_branch)
0.432.10 by Jelmer Vernooij
More test fixes.
178
        self.gl.auth()
0.431.38 by Jelmer Vernooij
Add NoSuchProject.
179
        try:
180
            base_project = self.gl.projects.get(base_project)
181
        except gitlab.GitlabGetError as e:
182
            if e.response_code == 404:
183
                raise NoSuchProject(base_project)
184
            else:
185
                raise
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
186
        if owner is None:
0.432.10 by Jelmer Vernooij
More test fixes.
187
            owner = self.gl.user.username
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
188
        if project is None:
0.431.30 by Jelmer Vernooij
s/name/path.
189
            project = base_project.path
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
190
        try:
0.432.10 by Jelmer Vernooij
More test fixes.
191
            target_project = self.gl.projects.get('%s/%s' % (owner, project))
0.431.30 by Jelmer Vernooij
s/name/path.
192
        except gitlab.GitlabGetError as e:
193
            if e.response_code == 404:
194
                target_project = base_project.forks.create({})
195
            else:
196
                raise
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
197
        remote_repo_url = git_url_to_bzr_url(target_project.attributes['ssh_url_to_repo'])
198
        remote_dir = controldir.ControlDir.open(remote_repo_url)
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
199
        try:
7211.13.7 by Jelmer Vernooij
Fix formatting.
200
            push_result = remote_dir.push_branch(
201
                local_branch, revision_id=revision_id, overwrite=overwrite,
202
                name=name)
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
203
        except errors.NoRoundtrippingSupport:
204
            if not allow_lossy:
205
                raise
7211.13.7 by Jelmer Vernooij
Fix formatting.
206
            push_result = remote_dir.push_branch(
207
                local_branch, revision_id=revision_id, overwrite=overwrite,
208
                name=name, lossy=True)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
209
        public_url = gitlab_url_to_bzr_url(
210
            target_project.attributes['http_url_to_repo'], name)
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
211
        return push_result.target_branch, public_url
0.432.4 by Jelmer Vernooij
Some work on gitlab.
212
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
213
    def get_derived_branch(self, base_branch, name, project=None, owner=None):
214
        import gitlab
215
        (host, base_project, base_branch_name) = parse_gitlab_url(base_branch)
216
        self.gl.auth()
0.431.38 by Jelmer Vernooij
Add NoSuchProject.
217
        try:
218
            base_project = self.gl.projects.get(base_project)
219
        except gitlab.GitlabGetError as e:
220
            if e.response_code == 404:
221
                raise NoSuchProject(base_project)
222
            else:
223
                raise
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
224
        if owner is None:
225
            owner = self.gl.user.username
226
        if project is None:
0.431.30 by Jelmer Vernooij
s/name/path.
227
            project = base_project.path
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
228
        try:
229
            target_project = self.gl.projects.get('%s/%s' % (owner, project))
230
        except gitlab.GitlabGetError as e:
231
            if e.response_code == 404:
232
                raise errors.NotBranchError('%s/%s/%s' % (self.gl.url, owner, project))
233
            raise
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
234
        return _mod_branch.Branch.open(gitlab_url_to_bzr_url(
7211.13.7 by Jelmer Vernooij
Fix formatting.
235
            target_project.attributes['ssh_url_to_repo'], name))
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
236
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
237
    def get_proposer(self, source_branch, target_branch):
238
        return GitlabMergeProposalBuilder(self.gl, source_branch, target_branch)
239
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
240
    def get_proposal(self, source_branch, target_branch):
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
241
        import gitlab
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
242
        (source_host, source_project_name, source_branch_name) = (
243
            parse_gitlab_url(source_branch))
244
        (target_host, target_project_name, target_branch_name) = (
245
            parse_gitlab_url(target_branch))
246
        if source_host != target_host:
247
            raise DifferentGitLabInstances(source_host, target_host)
248
        self.gl.auth()
249
        source_project = self.gl.projects.get(source_project_name)
250
        target_project = self.gl.projects.get(target_project_name)
0.431.43 by Jelmer Vernooij
Handle 403s during proposal listing.
251
        try:
252
            for mr in target_project.mergerequests.list(state='all'):
7211.13.7 by Jelmer Vernooij
Fix formatting.
253
                attrs = mr.attributes
254
                if (attrs['source_project_id'] != source_project.id or
255
                        attrs['source_branch'] != source_branch_name or
256
                        attrs['target_project_id'] != target_project.id or
257
                        attrs['target_branch'] != target_branch_name):
0.431.43 by Jelmer Vernooij
Handle 403s during proposal listing.
258
                    continue
259
                return GitLabMergeProposal(mr)
260
        except gitlab.GitlabListError as e:
261
            if e.response_code == 403:
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
262
                raise errors.PermissionDenied(e.error_message)
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
263
        raise NoMergeProposal()
264
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
265
    def hosts(self, branch):
266
        try:
267
            (host, project, branch_name) = parse_gitlab_url(branch)
268
        except NotGitLabUrl:
269
            return False
270
        return (self.gl.url == ('https://%s' % host))
271
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
272
    @classmethod
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
273
    def probe(cls, branch):
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
274
        try:
275
            (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.
276
        except NotGitLabUrl:
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
277
            raise UnsupportedHoster(branch)
0.432.4 by Jelmer Vernooij
Some work on gitlab.
278
        import gitlab
0.431.43 by Jelmer Vernooij
Handle 403s during proposal listing.
279
        import requests.exceptions
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
280
        try:
0.432.4 by Jelmer Vernooij
Some work on gitlab.
281
            gl = connect_gitlab(host)
0.431.10 by Jelmer Vernooij
Various other fixes.
282
            gl.auth()
0.431.43 by Jelmer Vernooij
Handle 403s during proposal listing.
283
        except requests.exceptions.SSLError:
7211.13.7 by Jelmer Vernooij
Fix formatting.
284
            # Well, I guess it could be..
0.431.43 by Jelmer Vernooij
Handle 403s during proposal listing.
285
            raise UnsupportedHoster(branch)
0.432.4 by Jelmer Vernooij
Some work on gitlab.
286
        except gitlab.GitlabGetError:
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
287
            raise UnsupportedHoster(branch)
0.431.10 by Jelmer Vernooij
Various other fixes.
288
        except gitlab.GitlabHttpError as e:
0.431.27 by Jelmer Vernooij
Catch 503 errors.
289
            if e.response_code in (404, 405, 503):
0.431.10 by Jelmer Vernooij
Various other fixes.
290
                raise UnsupportedHoster(branch)
291
            else:
292
                raise
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
293
        return cls(gl)
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
294
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
295
    @classmethod
296
    def iter_instances(cls):
297
        from gitlab import Gitlab
298
        for name, credentials in iter_tokens():
299
            if 'url' not in credentials:
300
                continue
301
            gl = Gitlab(**credentials)
302
            yield cls(gl)
303
304
    def iter_my_proposals(self):
305
        self.gl.auth()
306
        for mp in self.gl.mergerequests.list(owner=self.gl.user.username):
307
            yield GitLabMergeProposal(mp)
308
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
309
0.432.2 by Jelmer Vernooij
Publish command sort of works.
310
class GitlabMergeProposalBuilder(MergeProposalBuilder):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
311
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
312
    def __init__(self, gl, source_branch, target_branch):
313
        self.gl = gl
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
314
        self.source_branch = source_branch
315
        (self.source_host, self.source_project_name, self.source_branch_name) = (
316
            parse_gitlab_url(source_branch))
317
        self.target_branch = target_branch
318
        (self.target_host, self.target_project_name, self.target_branch_name) = (
319
            parse_gitlab_url(target_branch))
320
        if self.source_host != self.target_host:
321
            raise DifferentGitLabInstances(self.source_host, self.target_host)
322
323
    def get_infotext(self):
324
        """Determine the initial comment for the merge proposal."""
325
        info = []
326
        info.append("Gitlab instance: %s\n" % self.target_host)
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
327
        info.append("Source: %s\n" % self.source_branch.user_url)
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
328
        info.append("Target: %s\n" % self.target_branch.user_url)
329
        return ''.join(info)
330
331
    def get_initial_body(self):
332
        """Get a body for the proposal for the user to modify.
333
334
        :return: a str or None.
335
        """
336
        return None
337
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
338
    def create_proposal(self, description, reviewers=None, labels=None,
339
                        prerequisite_branch=None):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
340
        """Perform the submission."""
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
341
        if prerequisite_branch is not None:
342
            raise PrerequisiteBranchUnsupported(self)
0.431.16 by Jelmer Vernooij
gitlab: Report when a merge proposal already exists.
343
        import gitlab
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
344
        # TODO(jelmer): Support reviewers
0.432.10 by Jelmer Vernooij
More test fixes.
345
        self.gl.auth()
346
        source_project = self.gl.projects.get(self.source_project_name)
347
        target_project = self.gl.projects.get(self.target_project_name)
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
348
        # TODO(jelmer): Allow setting title explicitly
349
        title = description.splitlines()[0]
350
        # TODO(jelmer): Allow setting allow_collaboration field
351
        # TODO(jelmer): Allow setting milestone field
352
        # TODO(jelmer): Allow setting squash field
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
353
        kwargs = {
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
354
            'title': title,
355
            'target_project_id': target_project.id,
356
            'source_branch': self.source_branch_name,
357
            'target_branch': self.target_branch_name,
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
358
            'description': description}
359
        if labels:
360
            kwargs['labels'] = ','.join(labels)
0.431.16 by Jelmer Vernooij
gitlab: Report when a merge proposal already exists.
361
        try:
362
            merge_request = source_project.mergerequests.create(kwargs)
363
        except gitlab.GitlabCreateError as e:
0.431.34 by Jelmer Vernooij
Cope with gitlab 403.
364
            if e.response_code == 403:
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
365
                raise errors.PermissionDenied(e.error_message)
0.431.16 by Jelmer Vernooij
gitlab: Report when a merge proposal already exists.
366
            if e.response_code == 409:
367
                raise MergeProposalExists(self.source_branch.user_url)
368
            raise
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
369
        return GitLabMergeProposal(merge_request)
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
370
371
372
def register_gitlab_instance(shortname, url):
373
    """Register a gitlab instance.
374
375
    :param shortname: Short name (e.g. "gitlab")
376
    :param url: URL to the gitlab instance
377
    """
378
    from breezy.bugtracker import (
379
        tracker_registry,
380
        ProjectIntegerBugTracker,
381
        )
382
    tracker_registry.register(
383
        shortname, ProjectIntegerBugTracker(
384
            shortname, url + '/{project}/issues/{id}'))