Skip to content

Commit e925ba1

Browse files
committed
Implement clone and pull_request commands to ease PRs.
My vision of #477. ``` $ planemo clone --branch bwa-fix tools-iuc $ cd tools-iuc $ # Make changes. $ git add -p # Add desired changes. $ git commit -m "Fix bwa problem." $ planemo pull_request -m "Fix bwa problem." ``` I don't know about this - part of me likes it because the tutorials can be so clean and so complete, but part of me thinks it obsecures important things developers need to know to be effective. I think regardless it is solid library functionality to add - potentially useful for things like bioconda recipes and the Bioconductor work.
1 parent 024c291 commit e925ba1

File tree

10 files changed

+286
-22
lines changed

10 files changed

+286
-22
lines changed

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)