Skip to content

Commit 2b9eca5

Browse files
authored
Merge pull request #569 from jmchilton/github
Implement ``clone`` and ``pull_request`` commands to ease PRs.
2 parents 1961077 + e925ba1 commit 2b9eca5

10 files changed

Lines changed: 286 additions & 22 deletions

File tree

planemo/commands/cmd_brew.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
@options.brew_option()
1717
@command_function
1818
def cli(ctx, path, brew=None):
19-
"""Install tool requirements using brew. (**Experimental**)
19+
"""Install tool requirements using brew.
2020
2121
An experimental approach to versioning brew recipes will be used.
2222
See full discussion on the homebrew-science issues page here -

planemo/commands/cmd_clone.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Module describing the planemo ``recipe_init`` command."""
2+
import click
3+
4+
from planemo import github_util
5+
from planemo import options
6+
from planemo.cli import command_function
7+
from planemo.config import planemo_option
8+
9+
10+
CLONE_GITHUB_TARGETS = {
11+
"tools-iuc": "galaxyproject/tools-iuc",
12+
"tools-devteam": "galaxyproject/tools-devteam",
13+
"galaxy": "galaxyproject/galaxy",
14+
"planemo": "galaxyproject/planemo",
15+
"tools-galaxyp": "galaxyproteomics/tools-galaxyp",
16+
"bioconda-recipes": "bioconda/bioconda-recipes",
17+
"homebrew-science": "Homebrew/homebrew-science",
18+
"workflows": "common-workflow-language/workflows",
19+
}
20+
21+
22+
def clone_target_arg():
23+
"""Represent target to clone/branch."""
24+
return click.argument(
25+
"target",
26+
metavar="TARGET",
27+
type=click.STRING,
28+
)
29+
30+
31+
@click.command('clone')
32+
@planemo_option(
33+
"--fork/--skip_fork",
34+
default=True,
35+
is_flag=True,
36+
)
37+
@planemo_option(
38+
"--branch",
39+
type=click.STRING,
40+
default=None,
41+
help="Create a named branch on result."
42+
)
43+
@clone_target_arg()
44+
@options.optional_project_arg(exists=None, default="__NONE__")
45+
@command_function
46+
def cli(ctx, target, path, **kwds):
47+
"""Short-cut to quickly clone, fork, and branch a relevant Github repo.
48+
49+
For instance, the following will clone, fork, and branch the tools-iuc
50+
repository to allow a subsequent pull request to fix a problem with bwa.
51+
52+
::
53+
54+
$ planemo clone --branch bwa-fix tools-iuc
55+
$ cd tools-iuc
56+
$ # Make changes.
57+
$ git add -p # Add desired changes.
58+
$ git commit -m "Fix bwa problem."
59+
$ planemo pull_request -m "Fix bwa problem."
60+
61+
These changes do require that a github username and password are
62+
specified in ~/.planemo.yml.
63+
"""
64+
if target in CLONE_GITHUB_TARGETS:
65+
target = "https://github.com/%s" % CLONE_GITHUB_TARGETS[target]
66+
# Pretty hacky that this path isn't treated as None.
67+
if path is None or path.endswith("__NONE__"):
68+
path = target.split("/")[-1]
69+
github_util.clone_fork_branch(ctx, target, path, **kwds)

planemo/commands/cmd_conda_env.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@
3535
# @options.skip_install_option() # TODO
3636
@command_function
3737
def cli(ctx, path, **kwds):
38-
"""How to activate conda environment for tool.
38+
"""Activate a conda environment for tool.
3939
40-
Source output to activate a conda environment for this tool.
40+
Source the output of this command to activate a conda environment for this
41+
tool.
4142
4243
% . <(planemo conda_env bowtie2.xml)
4344
% which bowtie2
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Module describing the planemo ``recipe_init`` command."""
2+
import click
3+
4+
from planemo import github_util
5+
from planemo import options
6+
from planemo.cli import command_function
7+
from planemo.config import planemo_option
8+
9+
10+
@click.command('pull_request')
11+
@planemo_option(
12+
"-m",
13+
"--message",
14+
type=click.STRING,
15+
default=None,
16+
help="Message describing the pull request to create."
17+
)
18+
@options.optional_project_arg(exists=None)
19+
@command_function
20+
def cli(ctx, path, message=None, **kwds):
21+
"""Short-cut to quickly create a pull request for a relevant Github repo.
22+
23+
For instance, the following will clone, fork, and branch the tools-iuc
24+
repository to allow this pull request to issues against the repository.
25+
26+
::
27+
28+
$ planemo clone --branch bwa-fix tools-iuc
29+
$ cd tools-iuc
30+
$ # Make changes.
31+
$ git add -p # Add desired changes.
32+
$ git commit -m "Fix bwa problem."
33+
$ planemo pull_request -m "Fix bwa problem."
34+
35+
These changes do require that a github username and password are
36+
specified in ~/.planemo.yml.
37+
"""
38+
github_util.pull_request(ctx, path, message=message, **kwds)

planemo/galaxy/test/structures.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ def build(self):
6262

6363

6464
class StructuredData(BaseStructuredData):
65-
""" Abstraction around Galaxy's structured test data output.
66-
"""
65+
"""Abstraction around Galaxy's structured test data output."""
6766

6867
def __init__(self, json_path):
6968
if not json_path or not os.path.exists(json_path):

planemo/git.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,39 @@
1-
""" Utilities for interacting with git using planemo abstractions.
2-
"""
1+
"""Utilities for interacting with git using planemo abstractions."""
2+
import os
33
import subprocess
44

55
from six import text_type
66

77
from planemo import io
88

99

10+
def git_env_for(path):
11+
"""Setup env dictionary to target specified git repo with git commands."""
12+
env = {
13+
"GIT_WORK_DIR": path,
14+
"GIT_DIR": os.path.join(path, ".git")
15+
}
16+
return env
17+
18+
19+
def checkout(ctx, remote_repo, local_path, branch=None, remote="origin", from_branch="master"):
20+
"""Checkout a new branch from a remote repository."""
21+
env = git_env_for(local_path)
22+
if not os.path.exists(local_path):
23+
io.communicate(command_clone(ctx, remote_repo, local_path))
24+
else:
25+
io.communicate(["git", "fetch", remote], env=env)
26+
27+
if branch:
28+
io.communicate(["git", "checkout", "%s/%s" % (remote, from_branch), "-b", branch], env=env)
29+
else:
30+
io.communicate(["git", "merge", "--ff-only", "%s/%s" % (remote, from_branch)], env=env)
31+
32+
1033
def command_clone(ctx, src, dest, bare=False, branch=None):
11-
""" Take in ctx to allow more configurability down the road.
34+
"""Produce a command-line string to clone a repository.
35+
36+
Take in ``ctx`` to allow more configurability down the road.
1237
"""
1338
bare_arg = ""
1439
if bare:
@@ -21,6 +46,7 @@ def command_clone(ctx, src, dest, bare=False, branch=None):
2146

2247

2348
def diff(ctx, directory, range):
49+
"""Produce a list of diff-ed files for commit range."""
2450
cmd_template = "cd '%s' && git diff --name-only '%s'"
2551
cmd = cmd_template % (directory, range)
2652
stdout, _ = io.communicate(
@@ -30,8 +56,12 @@ def diff(ctx, directory, range):
3056

3157

3258
def clone(*args, **kwds):
59+
"""Clone a git repository.
60+
61+
See :func:`command_clone` for description of arguments.
62+
"""
3363
command = command_clone(*args, **kwds)
34-
return io.shell(command)
64+
return io.communicate(command)
3565

3666

3767
def rev(ctx, directory):
@@ -48,11 +78,14 @@ def rev(ctx, directory):
4878

4979

5080
def is_rev_dirty(ctx, directory):
81+
"""Check if specified git repository has uncommitted changes."""
82+
# TODO: Use ENV instead of cd.
5183
cmd = "cd '%s' && git diff --quiet" % directory
5284
return io.shell(cmd) != 0
5385

5486

5587
def rev_if_git(ctx, directory):
88+
"""Determine git revision (or ``None``)."""
5689
try:
5790
the_rev = rev(ctx, directory)
5891
is_dirtry = is_rev_dirty(ctx, directory)

planemo/github_util.py

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
1-
"""
2-
"""
1+
"""Utilities for interacting with Github."""
2+
from __future__ import absolute_import
3+
4+
import os
5+
6+
from galaxy.tools.deps.commands import which
7+
8+
from planemo import git
9+
from planemo.io import (
10+
communicate,
11+
IS_OS_X,
12+
untar_to,
13+
)
314

415
try:
516
import github
@@ -8,29 +19,137 @@
819
github = None
920
has_github_lib = False
1021

22+
HUB_VERSION = "2.2.8"
23+
1124
NO_GITHUB_DEP_ERROR = ("Cannot use github functionality - "
1225
"PyGithub library not available.")
26+
FAILED_TO_DOWNLOAD_HUB = "No hub executable available and it could not be installed."
1327

1428

1529
def get_github_config(ctx):
30+
"""Return a :class:`planemo.github_util.GithubConfig` for given configuration."""
31+
global_github_config = _get_raw_github_config(ctx)
32+
return None if global_github_config is None else GithubConfig(global_github_config)
33+
34+
35+
def clone_fork_branch(ctx, target, path, **kwds):
36+
"""Clone, fork, and branch a repository ahead of building a pull request."""
37+
git.checkout(
38+
ctx,
39+
target,
40+
path,
41+
branch=kwds.get("branch", None),
42+
remote="origin",
43+
from_branch="master"
44+
)
45+
if kwds.get("fork"):
46+
fork(ctx, path, **kwds)
47+
48+
49+
def fork(ctx, path, **kwds):
50+
"""Fork the target repository using ``hub``."""
51+
hub_path = ensure_hub(ctx, **kwds)
52+
hub_env = get_hub_env(ctx, path, **kwds)
53+
cmd = [hub_path, "fork"]
54+
communicate(cmd, env=hub_env)
55+
56+
57+
def pull_request(ctx, path, message=None, **kwds):
58+
"""Create a pull request against the origin of the path using ``hub``."""
59+
hub_path = ensure_hub(ctx, **kwds)
60+
hub_env = get_hub_env(ctx, path, **kwds)
61+
cmd = [hub_path, "pull-request"]
62+
if message is not None:
63+
cmd.extend(["-m", message])
64+
communicate(cmd, env=hub_env)
65+
66+
67+
def get_hub_env(ctx, path, **kwds):
68+
"""Return a environment dictionary to run hub with given user and repository target."""
69+
env = git.git_env_for(path).copy()
70+
github_config = _get_raw_github_config(ctx)
71+
if github_config is not None:
72+
if "username" in github_config:
73+
env["GITHUB_USER"] = github_config["username"]
74+
if "password" in github_config:
75+
env["GITHUB_PASSWORD"] = github_config["password"]
76+
77+
return env
78+
79+
80+
def ensure_hub(ctx, **kwds):
81+
"""Ensure ``hub`` is on the system ``PATH``.
82+
83+
This method will ensure ``hub`` is installed if it isn't available.
84+
85+
For more information on ``hub`` checkout ...
86+
"""
87+
hub_path = which("hub")
88+
if not hub_path:
89+
planemo_hub_path = os.path.join(ctx.workspace, "hub")
90+
if not os.path.exists(planemo_hub_path):
91+
_try_download_hub(planemo_hub_path)
92+
93+
if not os.path.exists(planemo_hub_path):
94+
raise Exception(FAILED_TO_DOWNLOAD_HUB)
95+
96+
hub_path = planemo_hub_path
97+
return hub_path
98+
99+
100+
def _try_download_hub(planemo_hub_path):
101+
link = _hub_link()
102+
# Strip URL base and .tgz at the end.
103+
basename = link.split("/")[-1].rsplit(".", 1)[0]
104+
untar_to(link, tar_args="-zxvf - %s/bin/hub -O > '%s'" % (basename, planemo_hub_path))
105+
communicate(["chmod", "+x", planemo_hub_path])
106+
107+
108+
def _get_raw_github_config(ctx):
109+
"""Return a :class:`planemo.github_util.GithubConfig` for given configuration."""
16110
if "github" not in ctx.global_config:
17111
return None
18-
global_github_config = ctx.global_config["github"]
19-
return GithubConfig(global_github_config)
112+
return ctx.global_config["github"]
20113

21114

22115
class GithubConfig(object):
116+
"""Abstraction around a Github account.
117+
118+
Required to use ``github`` module methods that require authorization.
119+
"""
23120

24121
def __init__(self, config):
25122
if not has_github_lib:
26123
raise Exception(NO_GITHUB_DEP_ERROR)
27124
self._github = github.Github(config["username"], config["password"])
28125

29126

127+
def _hub_link():
128+
if IS_OS_X:
129+
template_link = "https://github.com/github/hub/releases/download/v%s/hub-darwin-amd64-%s.tgz"
130+
else:
131+
template_link = "https://github.com/github/hub/releases/download/v%s/hub-linux-amd64-%s.tgz"
132+
return template_link % (HUB_VERSION, HUB_VERSION)
133+
134+
30135
def publish_as_gist_file(ctx, path, name="index"):
136+
"""Publish a gist.
137+
138+
More information on gists at http://gist.github.com/.
139+
"""
31140
github_config = get_github_config(ctx)
32141
user = github_config._github.get_user()
33142
content = open(path, "r").read()
34143
content_file = github.InputFileContent(content)
35144
gist = user.create_gist(False, {name: content_file})
36145
return gist.files[name].raw_url
146+
147+
148+
__all__ = [
149+
"clone_fork_branch",
150+
"ensure_hub",
151+
"fork",
152+
"get_github_config",
153+
"get_hub_env",
154+
"publish_as_gist_file",
155+
]

0 commit comments

Comments
 (0)