comparison env/lib/python3.9/site-packages/planemo/github_util.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 """Utilities for interacting with Github."""
2 from __future__ import absolute_import
3
4 import io
5 import os
6 import stat
7 import tarfile
8 import tempfile
9 from distutils.dir_util import copy_tree
10 from pathlib import Path
11
12 import requests
13
14 from planemo import git
15 from planemo.io import (
16 communicate,
17 IS_OS_X,
18 )
19
20 try:
21 import github
22 has_github_lib = True
23 except ImportError:
24 github = None
25 has_github_lib = False
26
27 GH_VERSION = "1.5.0"
28
29 NO_GITHUB_DEP_ERROR = ("Cannot use github functionality - "
30 "PyGithub library not available.")
31 FAILED_TO_DOWNLOAD_GH = "No gh executable available and it could not be installed."
32 DEFAULT_REMOTE_NAME = 'planemo-remote'
33
34
35 def get_github_config(ctx, allow_anonymous=False):
36 """Return a :class:`planemo.github_util.GithubConfig` for given configuration."""
37 global_github_config = _get_raw_github_config(ctx)
38 return GithubConfig(global_github_config, allow_anonymous=allow_anonymous)
39
40
41 def clone_fork_branch(ctx, target, path, remote_name=DEFAULT_REMOTE_NAME, **kwds):
42 """Clone, fork, and branch a repository ahead of building a pull request."""
43 git.checkout(
44 ctx,
45 target,
46 path,
47 branch=kwds.get("branch", None),
48 remote="origin",
49 from_branch="master"
50 )
51 if kwds.get("fork"):
52 try:
53 fork(ctx, path, remote_name=remote_name, **kwds)
54 except Exception:
55 pass
56 if 'GITHUB_USER' in os.environ:
57 # On CI systems fork doesn't add a local remote under circumstances I don't quite understand,
58 # but that's probably linked to https://github.com/cli/cli/issues/2722
59 cmd = ['git', 'remote', 'add', remote_name, f"https://github.com/{os.environ['GITHUB_USER']}/{os.path.basename(target)}"]
60 try:
61 communicate(cmd, cwd=path)
62 except RuntimeError:
63 # Can add the remote only once
64 pass
65 return remote_name
66
67
68 def fork(ctx, path, remote_name=DEFAULT_REMOTE_NAME, **kwds):
69 """Fork the target repository using ``gh``."""
70 gh_path = ensure_gh(ctx, **kwds)
71 gh_env = get_gh_env(ctx, path, **kwds)
72 cmd = [gh_path, "repo", "fork", '--remote=true', '--remote-name', remote_name]
73 communicate(cmd, cwd=path, env=gh_env)
74 return remote_name
75
76
77 def get_or_create_repository(ctx, owner, repo, dry_run=True, **kwds):
78 """Clones or creates a repository and returns path on disk"""
79 target = os.path.realpath(tempfile.mkdtemp())
80 remote_repo = "https://github.com/{owner}/{repo}".format(owner=owner, repo=repo)
81 try:
82 ctx.log('Cloning {}'.format(remote_repo))
83 git.clone(ctx, src=remote_repo, dest=target)
84 except Exception:
85 ctx.log('Creating repository {}'.format(remote_repo))
86 target = create_repository(ctx, owner=owner, repo=repo, dest=target, dry_run=dry_run)
87 return target
88
89
90 def create_repository(ctx, owner, repo, dest, dry_run, **kwds):
91 gh_path = ensure_gh(ctx, **kwds)
92 gh_env = get_gh_env(ctx, dry_run=dry_run, **kwds)
93 cmd = [gh_path, 'repo', 'create', '-y', '--public', "{owner}/{repo}".format(owner=owner, repo=repo)]
94 if dry_run:
95 "Would run command '{}'".format(" ".join(cmd))
96 git.init(ctx, dest)
97 return dest
98 communicate(cmd, env=gh_env, cwd=dest)
99 return os.path.join(dest, repo)
100
101
102 def rm_dir_contents(directory, ignore_dirs=(".git")):
103 directory = Path(directory)
104 for item in directory.iterdir():
105 if item.name not in ignore_dirs:
106 if item.is_dir():
107 rm_dir_contents(item)
108 else:
109 item.unlink()
110
111
112 def add_dir_contents_to_repo(ctx, from_dir, target_dir, target_repository_path, version, dry_run, notes=""):
113 ctx.log("From {} to {}".format(from_dir, target_repository_path))
114 rm_dir_contents(target_repository_path)
115 copy_tree(from_dir, target_repository_path)
116 git.add(ctx, target_repository_path, target_repository_path)
117 message = "Update for version {version}".format(version=version)
118 if notes:
119 message += "\n{notes}".format(notes=notes)
120 git.commit(ctx, repo_path=target_repository_path, message=message)
121 if not dry_run:
122 git.push(ctx, target_repository_path)
123
124
125 def assert_new_version(ctx, version, owner, repo):
126 remote_repo = "https://github.com/{owner}/{repo}".format(owner=owner, repo=repo)
127 try:
128 tags_and_versions = git.ls_remote(ctx, remote_repo=remote_repo)
129 if "refs/tags/v{}".format(version) in tags_and_versions or "refs/tags/{}".format(version) in tags_and_versions:
130 raise Exception("Version '{}' for {}/{} exists already. Please change the version.".format(version, owner, repo))
131 except RuntimeError:
132 # repo doesn't exist
133 pass
134
135
136 def changelog_in_repo(target_repository_path):
137 changelog = []
138 for path in os.listdir(target_repository_path):
139 if 'changelog.md' in path.lower():
140 header_seen = False
141 header_chars = ('---', '===', '~~~')
142 with(open(os.path.join(target_repository_path, path))) as changelog_fh:
143 for line in changelog_fh:
144 if line.startswith(header_chars):
145 if header_seen:
146 return "\n".join(changelog[:-1])
147 else:
148 header_seen = True
149 return "\n".join(changelog)
150
151
152 def create_release(ctx, from_dir, target_dir, owner, repo, version, dry_run, notes="", **kwds):
153 assert_new_version(ctx, version, owner=owner, repo=repo)
154 target_repository_path = get_or_create_repository(ctx, owner=owner, repo=repo, dry_run=dry_run)
155 add_dir_contents_to_repo(ctx, from_dir, target_dir, target_repository_path, version=version, dry_run=dry_run, notes=notes)
156 gh_path = ensure_gh(ctx, **kwds)
157 gh_env = get_gh_env(ctx, dry_run=dry_run, **kwds)
158 cmd = [
159 gh_path,
160 'release',
161 '-R',
162 "{}/{}".format(owner, repo),
163 'create',
164 "v{version}".format(version=version),
165 '--title',
166 str(version),
167 ]
168 cmd.extend(['--notes', notes or changelog_in_repo(target_repository_path)])
169 if not dry_run:
170 communicate(cmd, env=gh_env)
171 else:
172 ctx.log("Would run command '{}'".format(" ".join(cmd)))
173
174
175 def pull_request(ctx, path, message=None, repo=None, **kwds):
176 """Create a pull request against the origin of the path using ``gh``."""
177 gh_path = ensure_gh(ctx, **kwds)
178 gh_env = get_gh_env(ctx, path, **kwds)
179 cmd = [gh_path, "pr", "create"]
180 if message is None:
181 cmd.append('--fill')
182 else:
183 lines = message.splitlines()
184 cmd.extend(['--title', lines[0]])
185 if len(lines) > 1:
186 cmd.extend(["--body", "\n".join(lines[1:])])
187 if repo:
188 cmd.extend(['--repo', repo])
189 communicate(cmd, env=gh_env)
190
191
192 def get_gh_env(ctx, path=None, dry_run=False, **kwds):
193 """Return a environment dictionary to run gh with given user and repository target."""
194 if path is None:
195 env = {}
196 else:
197 env = git.git_env_for(path).copy()
198 if not dry_run:
199 github_config = _get_raw_github_config(ctx)
200 if github_config is not None:
201 if "access_token" in github_config:
202 env["GITHUB_TOKEN"] = github_config["access_token"]
203
204 return env
205
206
207 def ensure_gh(ctx, **kwds):
208 """Ensure gh is available for planemo
209
210 This method will ensure ``gh`` is installed at the correct version.
211
212 For more information on ``gh`` checkout https://cli.github.com/
213 """
214 planemo_gh_path = os.path.join(ctx.workspace, f"gh-{GH_VERSION}")
215 if not os.path.exists(planemo_gh_path):
216 _try_download_gh(planemo_gh_path)
217
218 if not os.path.exists(planemo_gh_path):
219 raise Exception(FAILED_TO_DOWNLOAD_GH)
220
221 return planemo_gh_path
222
223
224 def _try_download_gh(planemo_gh_path):
225 link = _gh_link()
226 path = Path(planemo_gh_path)
227 resp = requests.get(link)
228 with tarfile.open(fileobj=io.BytesIO(resp.content)) as tf, path.open('wb') as outfile:
229 for member in tf.getmembers():
230 if member.name.endswith('bin/gh'):
231 outfile.write(tf.extractfile(member).read())
232 path.chmod(path.stat().st_mode | stat.S_IEXEC)
233
234
235 def _get_raw_github_config(ctx):
236 """Return a :class:`planemo.github_util.GithubConfig` for given configuration."""
237 if "github" not in ctx.global_config:
238 if "GITHUB_TOKEN" in os.environ:
239 return {
240 "access_token": os.environ["GITHUB_TOKEN"],
241 }
242 if "github" not in ctx.global_config:
243 raise Exception("github account not found in planemo config and GITHUB_TOKEN environment variables unset")
244 return ctx.global_config["github"]
245
246
247 class GithubConfig(object):
248 """Abstraction around a Github account.
249
250 Required to use ``github`` module methods that require authorization.
251 """
252
253 def __init__(self, config, allow_anonymous=False):
254 if not has_github_lib:
255 raise Exception(NO_GITHUB_DEP_ERROR)
256 if "access_token" not in config:
257 if not allow_anonymous:
258 raise Exception("github authentication unavailable")
259 github_object = github.Github()
260 else:
261 github_object = github.Github(config["access_token"])
262 self._github = github_object
263
264
265 def _gh_link():
266 if IS_OS_X:
267 template_link = "https://github.com/cli/cli/releases/download/v%s/gh_%s_macOS_amd64.tar.gz"
268 else:
269 template_link = "https://github.com/cli/cli/releases/download/v%s/gh_%s_linux_amd64.tar.gz"
270 return template_link % (GH_VERSION, GH_VERSION)
271
272
273 def publish_as_gist_file(ctx, path, name="index"):
274 """Publish a gist.
275
276 More information on gists at http://gist.github.com/.
277 """
278 github_config = get_github_config(ctx, allow_anonymous=False)
279 user = github_config._github.get_user()
280 with open(path, "r") as fh:
281 content = fh.read()
282 content_file = github.InputFileContent(content)
283 gist = user.create_gist(False, {name: content_file})
284 return gist.files[name].raw_url
285
286
287 def get_repository_object(ctx, name):
288 github_object = get_github_config(ctx, allow_anonymous=True)
289 return github_object._github.get_repo(name)
290
291
292 __all__ = (
293 "add_dir_contents_to_repo",
294 "clone_fork_branch",
295 "create_release",
296 "ensure_gh",
297 "fork",
298 "get_github_config",
299 "get_gh_env",
300 "get_or_create_repository",
301 "publish_as_gist_file",
302 )