Mercurial > repos > shellac > sam_consensus_v3
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 ) |