1
# Copyright (C) 2018 Breezy Developers
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.
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.
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
17
"""Support for GitHub."""
19
from __future__ import absolute_import
23
from .propose import (
29
PrerequisiteBranchUnsupported,
34
branch as _mod_branch,
39
version_string as breezy_version,
41
from ...config import AuthenticationConfig, GlobalStack, config_dir
42
from ...git.urls import git_url_to_bzr_url
43
from ...i18n import gettext
44
from ...sixish import PY3
45
from ...trace import note
46
from ...lazy_import import lazy_import
47
lazy_import(globals(), """
48
from github import Github
52
def store_github_token(scheme, host, token):
53
with open(os.path.join(config_dir(), 'github.conf'), 'w') as f:
57
def retrieve_github_token(scheme, host):
58
path = os.path.join(config_dir(), 'github.conf')
59
if not os.path.exists(path):
61
with open(path, 'r') as f:
62
return f.read().strip()
65
def determine_title(description):
66
return description.splitlines()[0]
69
class NotGitHubUrl(errors.BzrError):
71
_fmt = "Not a GitHub URL: %(url)s"
73
def __init__(self, url):
74
errors.BzrError.__init__(self)
81
user_agent = "Breezy/%s" % breezy_version
83
auth = AuthenticationConfig()
85
credentials = auth.get_credentials('https', 'github.com')
86
if credentials is not None:
87
return Github(credentials['user'], credentials['password'],
88
user_agent=user_agent)
90
# TODO(jelmer): token = auth.get_token('https', 'github.com')
91
token = retrieve_github_token('https', 'github.com')
93
return Github(token, user_agent=user_agent)
95
note('Accessing GitHub anonymously. To log in, run \'brz gh-login\'.')
96
return Github(user_agent=user_agent)
99
class GitHubMergeProposal(MergeProposal):
101
def __init__(self, pr):
106
return self._pr.html_url
108
def get_description(self):
111
def set_description(self, description):
112
self._pr.edit(body=description, title=determine_title(description))
115
return self._pr.merged
118
def parse_github_url(branch):
119
url = urlutils.split_segment_parameters(branch.user_url)[0]
120
(scheme, user, password, host, port, path) = urlutils.parse_url(
122
if host != 'github.com':
123
raise NotGitHubUrl(url)
124
(owner, repo_name) = path.strip('/').split('/')
125
if repo_name.endswith('.git'):
126
repo_name = repo_name[:-4]
127
return owner, repo_name, branch.name
130
def github_url_to_bzr_url(url, branch_name):
132
branch_name = branch_name.encode('utf-8')
133
return urlutils.join_segment_parameters(
134
git_url_to_bzr_url(url), {"branch": branch_name})
137
class GitHub(Hoster):
139
supports_merge_proposal_labels = True
145
self.gh = connect_github()
147
def publish_derived(self, local_branch, base_branch, name, project=None,
148
owner=None, revision_id=None, overwrite=False,
151
base_owner, base_project, base_branch_name = parse_github_url(base_branch)
152
base_repo = self.gh.get_repo('%s/%s' % (base_owner, base_project))
154
owner = self.gh.get_user().login
156
project = base_repo.name
158
remote_repo = self.gh.get_repo('%s/%s' % (owner, project))
160
except github.UnknownObjectException:
161
base_repo = self.gh.get_repo('%s/%s' % (base_owner, base_project))
162
if owner == self.gh.get_user().login:
163
owner_obj = self.gh.get_user()
165
owner_obj = self.gh.get_organization(owner)
166
remote_repo = owner_obj.create_fork(base_repo)
167
note(gettext('Forking new repository %s from %s') %
168
(remote_repo.html_url, base_repo.html_url))
170
note(gettext('Reusing existing repository %s') % remote_repo.html_url)
171
remote_dir = controldir.ControlDir.open(git_url_to_bzr_url(remote_repo.ssh_url))
173
push_result = remote_dir.push_branch(
174
local_branch, revision_id=revision_id, overwrite=overwrite,
176
except errors.NoRoundtrippingSupport:
179
push_result = remote_dir.push_branch(
180
local_branch, revision_id=revision_id,
181
overwrite=overwrite, name=name, lossy=True)
182
return push_result.target_branch, github_url_to_bzr_url(
183
remote_repo.html_url, name)
185
def get_push_url(self, branch):
186
owner, project, branch_name = parse_github_url(branch)
187
repo = self.gh.get_repo('%s/%s' % (owner, project))
188
return github_url_to_bzr_url(repo.ssh_url, branch_name)
190
def get_derived_branch(self, base_branch, name, project=None, owner=None):
192
base_owner, base_project, base_branch_name = parse_github_url(base_branch)
193
base_repo = self.gh.get_repo('%s/%s' % (base_owner, base_project))
195
owner = self.gh.get_user().login
197
project = base_repo.name
199
remote_repo = self.gh.get_repo('%s/%s' % (owner, project))
200
full_url = github_url_to_bzr_url(remote_repo.ssh_url, name)
201
return _mod_branch.Branch.open(full_url)
202
except github.UnknownObjectException:
203
raise errors.NotBranchError('https://github.com/%s/%s' % (owner, project))
205
def get_proposer(self, source_branch, target_branch):
206
return GitHubMergeProposalBuilder(self.gh, source_branch, target_branch)
208
def get_proposal(self, source_branch, target_branch):
209
(source_owner, source_repo_name, source_branch_name) = (
210
parse_github_url(source_branch))
211
(target_owner, target_repo_name, target_branch_name) = (
212
parse_github_url(target_branch))
213
target_repo = self.gh.get_repo("%s/%s" % (target_owner, target_repo_name))
214
for pull in target_repo.get_pulls(head=target_branch_name):
215
if pull.head.ref != source_branch_name:
217
if (pull.head.repo.owner.login != source_owner or
218
pull.head.repo.name != source_repo_name):
220
return GitHubMergeProposal(pull)
221
raise NoMergeProposal()
223
def hosts(self, branch):
225
parse_github_url(branch)
232
def probe(cls, branch):
234
parse_github_url(branch)
236
raise UnsupportedHoster(branch)
240
class GitHubMergeProposalBuilder(MergeProposalBuilder):
242
def __init__(self, gh, source_branch, target_branch):
244
self.source_branch = source_branch
245
self.target_branch = target_branch
246
(self.target_owner, self.target_repo_name, self.target_branch_name) = (
247
parse_github_url(self.target_branch))
248
(self.source_owner, self.source_repo_name, self.source_branch_name) = (
249
parse_github_url(self.source_branch))
251
def get_infotext(self):
252
"""Determine the initial comment for the merge proposal."""
254
info.append("Merge %s into %s:%s\n" % (
255
self.source_branch_name, self.target_owner,
256
self.target_branch_name))
257
info.append("Source: %s\n" % self.source_branch.user_url)
258
info.append("Target: %s\n" % self.target_branch.user_url)
261
def get_initial_body(self):
262
"""Get a body for the proposal for the user to modify.
264
:return: a str or None.
268
def create_proposal(self, description, reviewers=None, labels=None,
269
prerequisite_branch=None):
270
"""Perform the submission."""
271
if prerequisite_branch is not None:
272
raise PrerequisiteBranchUnsupported(self)
274
# TODO(jelmer): Probe for right repo name
275
if self.target_repo_name.endswith('.git'):
276
self.target_repo_name = self.target_repo_name[:-4]
277
target_repo = self.gh.get_repo("%s/%s" % (self.target_owner, self.target_repo_name))
278
# TODO(jelmer): Allow setting title explicitly?
279
title = determine_title(description)
280
# TOOD(jelmer): Set maintainers_can_modify?
282
pull_request = target_repo.create_pull(
283
title=title, body=description,
284
head="%s:%s" % (self.source_owner, self.source_branch_name),
285
base=self.target_branch_name)
286
except github.GithubException as e:
288
raise MergeProposalExists(self.source_branch.user_url)
291
for reviewer in reviewers:
292
pull_request.assignees.append(
293
self.gh.get_user(reviewer))
296
pull_request.issue.labels.append(label)
297
return GitHubMergeProposal(pull_request)