/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(
7211.13.7 by Jelmer Vernooij
Fix formatting.
165
        git_url_to_bzr_url(url), {"branch": name})
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
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:
7211.13.7 by Jelmer Vernooij
Fix formatting.
212
            push_result = remote_dir.push_branch(
213
                local_branch, revision_id=revision_id, overwrite=overwrite,
214
                name=name)
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
215
        except errors.NoRoundtrippingSupport:
216
            if not allow_lossy:
217
                raise
7211.13.7 by Jelmer Vernooij
Fix formatting.
218
            push_result = remote_dir.push_branch(
219
                local_branch, revision_id=revision_id, overwrite=overwrite,
220
                name=name, lossy=True)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
221
        public_url = gitlab_url_to_bzr_url(
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
222
            target_project.http_url_to_repo, name)
0.432.5 by Jelmer Vernooij
Fix publishing to gitlab.
223
        return push_result.target_branch, public_url
0.432.4 by Jelmer Vernooij
Some work on gitlab.
224
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
225
    def get_derived_branch(self, base_branch, name, project=None, owner=None):
226
        import gitlab
227
        (host, base_project, base_branch_name) = parse_gitlab_url(base_branch)
228
        self.gl.auth()
0.431.38 by Jelmer Vernooij
Add NoSuchProject.
229
        try:
230
            base_project = self.gl.projects.get(base_project)
231
        except gitlab.GitlabGetError as e:
232
            if e.response_code == 404:
233
                raise NoSuchProject(base_project)
234
            else:
235
                raise
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
236
        if owner is None:
237
            owner = self.gl.user.username
238
        if project is None:
0.431.30 by Jelmer Vernooij
s/name/path.
239
            project = base_project.path
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
240
        try:
241
            target_project = self.gl.projects.get('%s/%s' % (owner, project))
242
        except gitlab.GitlabGetError as e:
243
            if e.response_code == 404:
244
                raise errors.NotBranchError('%s/%s/%s' % (self.gl.url, owner, project))
245
            raise
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
246
        return _mod_branch.Branch.open(gitlab_url_to_bzr_url(
7233.3.2 by Jelmer Vernooij
Merge lp:brz-propose.
247
            target_project.ssh_url_to_repo, name))
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
248
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
249
    def get_proposer(self, source_branch, target_branch):
250
        return GitlabMergeProposalBuilder(self.gl, source_branch, target_branch)
251
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
252
    def get_proposal(self, source_branch, target_branch):
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
253
        import gitlab
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
254
        (source_host, source_project_name, source_branch_name) = (
255
            parse_gitlab_url(source_branch))
256
        (target_host, target_project_name, target_branch_name) = (
257
            parse_gitlab_url(target_branch))
258
        if source_host != target_host:
259
            raise DifferentGitLabInstances(source_host, target_host)
260
        self.gl.auth()
261
        source_project = self.gl.projects.get(source_project_name)
262
        target_project = self.gl.projects.get(target_project_name)
0.431.43 by Jelmer Vernooij
Handle 403s during proposal listing.
263
        try:
264
            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.
265
                if (mr.source_project_id != source_project.id or
7233.3.2 by Jelmer Vernooij
Merge lp:brz-propose.
266
                        mr.source_branch != source_branch_name or
267
                        mr.target_project_id != target_project.id or
268
                        mr.target_branch != target_branch_name):
0.431.43 by Jelmer Vernooij
Handle 403s during proposal listing.
269
                    continue
270
                return GitLabMergeProposal(mr)
271
        except gitlab.GitlabListError as e:
272
            if e.response_code == 403:
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
273
                raise errors.PermissionDenied(e.error_message)
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
274
        raise NoMergeProposal()
275
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
276
    def hosts(self, branch):
277
        try:
278
            (host, project, branch_name) = parse_gitlab_url(branch)
279
        except NotGitLabUrl:
280
            return False
281
        return (self.gl.url == ('https://%s' % host))
282
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
283
    @classmethod
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
284
    def probe(cls, branch):
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
285
        try:
286
            (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.
287
        except NotGitLabUrl:
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
288
            raise UnsupportedHoster(branch)
0.432.4 by Jelmer Vernooij
Some work on gitlab.
289
        import gitlab
0.431.43 by Jelmer Vernooij
Handle 403s during proposal listing.
290
        import requests.exceptions
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
291
        try:
0.432.4 by Jelmer Vernooij
Some work on gitlab.
292
            gl = connect_gitlab(host)
0.431.10 by Jelmer Vernooij
Various other fixes.
293
            gl.auth()
0.431.43 by Jelmer Vernooij
Handle 403s during proposal listing.
294
        except requests.exceptions.SSLError:
7211.13.7 by Jelmer Vernooij
Fix formatting.
295
            # Well, I guess it could be..
0.431.43 by Jelmer Vernooij
Handle 403s during proposal listing.
296
            raise UnsupportedHoster(branch)
0.432.4 by Jelmer Vernooij
Some work on gitlab.
297
        except gitlab.GitlabGetError:
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
298
            raise UnsupportedHoster(branch)
0.431.10 by Jelmer Vernooij
Various other fixes.
299
        except gitlab.GitlabHttpError as e:
0.431.27 by Jelmer Vernooij
Catch 503 errors.
300
            if e.response_code in (404, 405, 503):
0.431.10 by Jelmer Vernooij
Various other fixes.
301
                raise UnsupportedHoster(branch)
302
            else:
303
                raise
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
304
        return cls(gl)
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
305
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
306
    @classmethod
307
    def iter_instances(cls):
308
        from gitlab import Gitlab
309
        for name, credentials in iter_tokens():
310
            if 'url' not in credentials:
311
                continue
312
            gl = Gitlab(**credentials)
313
            yield cls(gl)
314
315
    def iter_my_proposals(self):
316
        self.gl.auth()
317
        for mp in self.gl.mergerequests.list(owner=self.gl.user.username):
318
            yield GitLabMergeProposal(mp)
319
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
320
0.432.2 by Jelmer Vernooij
Publish command sort of works.
321
class GitlabMergeProposalBuilder(MergeProposalBuilder):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
322
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
323
    def __init__(self, gl, source_branch, target_branch):
324
        self.gl = gl
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
325
        self.source_branch = source_branch
326
        (self.source_host, self.source_project_name, self.source_branch_name) = (
327
            parse_gitlab_url(source_branch))
328
        self.target_branch = target_branch
329
        (self.target_host, self.target_project_name, self.target_branch_name) = (
330
            parse_gitlab_url(target_branch))
331
        if self.source_host != self.target_host:
332
            raise DifferentGitLabInstances(self.source_host, self.target_host)
333
334
    def get_infotext(self):
335
        """Determine the initial comment for the merge proposal."""
336
        info = []
337
        info.append("Gitlab instance: %s\n" % self.target_host)
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
338
        info.append("Source: %s\n" % self.source_branch.user_url)
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
339
        info.append("Target: %s\n" % self.target_branch.user_url)
340
        return ''.join(info)
341
342
    def get_initial_body(self):
343
        """Get a body for the proposal for the user to modify.
344
345
        :return: a str or None.
346
        """
347
        return None
348
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
349
    def create_proposal(self, description, reviewers=None, labels=None,
350
                        prerequisite_branch=None):
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
351
        """Perform the submission."""
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
352
        if prerequisite_branch is not None:
353
            raise PrerequisiteBranchUnsupported(self)
0.431.16 by Jelmer Vernooij
gitlab: Report when a merge proposal already exists.
354
        import gitlab
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
355
        # TODO(jelmer): Support reviewers
0.432.10 by Jelmer Vernooij
More test fixes.
356
        self.gl.auth()
357
        source_project = self.gl.projects.get(self.source_project_name)
358
        target_project = self.gl.projects.get(self.target_project_name)
0.431.5 by Jelmer Vernooij
Initial work on gitlab support.
359
        # TODO(jelmer): Allow setting title explicitly
360
        title = description.splitlines()[0]
361
        # TODO(jelmer): Allow setting allow_collaboration field
362
        # TODO(jelmer): Allow setting milestone field
363
        # TODO(jelmer): Allow setting squash field
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
364
        kwargs = {
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
365
            'title': title,
366
            'target_project_id': target_project.id,
367
            'source_branch': self.source_branch_name,
368
            'target_branch': self.target_branch_name,
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
369
            'description': description}
370
        if labels:
371
            kwargs['labels'] = ','.join(labels)
0.431.16 by Jelmer Vernooij
gitlab: Report when a merge proposal already exists.
372
        try:
373
            merge_request = source_project.mergerequests.create(kwargs)
374
        except gitlab.GitlabCreateError as e:
0.431.34 by Jelmer Vernooij
Cope with gitlab 403.
375
            if e.response_code == 403:
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
376
                raise errors.PermissionDenied(e.error_message)
0.431.16 by Jelmer Vernooij
gitlab: Report when a merge proposal already exists.
377
            if e.response_code == 409:
378
                raise MergeProposalExists(self.source_branch.user_url)
379
            raise
0.431.39 by Jelmer Vernooij
Extend the merge proposal abstraction a bit.
380
        return GitLabMergeProposal(merge_request)
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
381
382
383
def register_gitlab_instance(shortname, url):
384
    """Register a gitlab instance.
385
386
    :param shortname: Short name (e.g. "gitlab")
387
    :param url: URL to the gitlab instance
388
    """
389
    from breezy.bugtracker import (
390
        tracker_registry,
391
        ProjectIntegerBugTracker,
392
        )
393
    tracker_registry.register(
394
        shortname, ProjectIntegerBugTracker(
395
            shortname, url + '/{project}/issues/{id}'))