Skip to content

Commit

Permalink
Implement clone and pull_request commands to ease PRs.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jmchilton committed Sep 20, 2016
1 parent 024c291 commit e925ba1
Show file tree
Hide file tree
Showing 10 changed files with 286 additions and 22 deletions.
2 changes: 1 addition & 1 deletion planemo/commands/cmd_brew.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
@options.brew_option()
@command_function
def cli(ctx, path, brew=None):
"""Install tool requirements using brew. (**Experimental**)
"""Install tool requirements using brew.
An experimental approach to versioning brew recipes will be used.
See full discussion on the homebrew-science issues page here -
Expand Down
69 changes: 69 additions & 0 deletions planemo/commands/cmd_clone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Module describing the planemo ``recipe_init`` command."""
import click

from planemo import github_util
from planemo import options
from planemo.cli import command_function
from planemo.config import planemo_option


CLONE_GITHUB_TARGETS = {
"tools-iuc": "galaxyproject/tools-iuc",
"tools-devteam": "galaxyproject/tools-devteam",
"galaxy": "galaxyproject/galaxy",
"planemo": "galaxyproject/planemo",
"tools-galaxyp": "galaxyproteomics/tools-galaxyp",
"bioconda-recipes": "bioconda/bioconda-recipes",
"homebrew-science": "Homebrew/homebrew-science",
"workflows": "common-workflow-language/workflows",
}


def clone_target_arg():
"""Represent target to clone/branch."""
return click.argument(
"target",
metavar="TARGET",
type=click.STRING,
)


@click.command('clone')
@planemo_option(
"--fork/--skip_fork",
default=True,
is_flag=True,
)
@planemo_option(
"--branch",
type=click.STRING,
default=None,
help="Create a named branch on result."
)
@clone_target_arg()
@options.optional_project_arg(exists=None, default="__NONE__")
@command_function
def cli(ctx, target, path, **kwds):
"""Short-cut to quickly clone, fork, and branch a relevant Github repo.
For instance, the following will clone, fork, and branch the tools-iuc
repository to allow a subsequent pull request to fix a problem with bwa.
::
$ 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."
These changes do require that a github username and password are
specified in ~/.planemo.yml.
"""
if target in CLONE_GITHUB_TARGETS:
target = "https://github.com/%s" % CLONE_GITHUB_TARGETS[target]
# Pretty hacky that this path isn't treated as None.
if path is None or path.endswith("__NONE__"):
path = target.split("/")[-1]
github_util.clone_fork_branch(ctx, target, path, **kwds)
5 changes: 3 additions & 2 deletions planemo/commands/cmd_conda_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@
# @options.skip_install_option() # TODO
@command_function
def cli(ctx, path, **kwds):
"""How to activate conda environment for tool.
"""Activate a conda environment for tool.
Source output to activate a conda environment for this tool.
Source the output of this command to activate a conda environment for this
tool.
% . <(planemo conda_env bowtie2.xml)
% which bowtie2
Expand Down
38 changes: 38 additions & 0 deletions planemo/commands/cmd_pull_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Module describing the planemo ``recipe_init`` command."""
import click

from planemo import github_util
from planemo import options
from planemo.cli import command_function
from planemo.config import planemo_option


@click.command('pull_request')
@planemo_option(
"-m",
"--message",
type=click.STRING,
default=None,
help="Message describing the pull request to create."
)
@options.optional_project_arg(exists=None)
@command_function
def cli(ctx, path, message=None, **kwds):
"""Short-cut to quickly create a pull request for a relevant Github repo.
For instance, the following will clone, fork, and branch the tools-iuc
repository to allow this pull request to issues against the repository.
::
$ 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."
These changes do require that a github username and password are
specified in ~/.planemo.yml.
"""
github_util.pull_request(ctx, path, message=message, **kwds)
3 changes: 1 addition & 2 deletions planemo/galaxy/test/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ def build(self):


class StructuredData(BaseStructuredData):
""" Abstraction around Galaxy's structured test data output.
"""
"""Abstraction around Galaxy's structured test data output."""

def __init__(self, json_path):
if not json_path or not os.path.exists(json_path):
Expand Down
41 changes: 37 additions & 4 deletions planemo/git.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
""" Utilities for interacting with git using planemo abstractions.
"""
"""Utilities for interacting with git using planemo abstractions."""
import os
import subprocess

from six import text_type

from planemo import io


def git_env_for(path):
"""Setup env dictionary to target specified git repo with git commands."""
env = {
"GIT_WORK_DIR": path,
"GIT_DIR": os.path.join(path, ".git")
}
return env


def checkout(ctx, remote_repo, local_path, branch=None, remote="origin", from_branch="master"):
"""Checkout a new branch from a remote repository."""
env = git_env_for(local_path)
if not os.path.exists(local_path):
io.communicate(command_clone(ctx, remote_repo, local_path))
else:
io.communicate(["git", "fetch", remote], env=env)

if branch:
io.communicate(["git", "checkout", "%s/%s" % (remote, from_branch), "-b", branch], env=env)
else:
io.communicate(["git", "merge", "--ff-only", "%s/%s" % (remote, from_branch)], env=env)


def command_clone(ctx, src, dest, bare=False, branch=None):
""" Take in ctx to allow more configurability down the road.
"""Produce a command-line string to clone a repository.
Take in ``ctx`` to allow more configurability down the road.
"""
bare_arg = ""
if bare:
Expand All @@ -21,6 +46,7 @@ def command_clone(ctx, src, dest, bare=False, branch=None):


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


def clone(*args, **kwds):
"""Clone a git repository.
See :func:`command_clone` for description of arguments.
"""
command = command_clone(*args, **kwds)
return io.shell(command)
return io.communicate(command)


def rev(ctx, directory):
Expand All @@ -48,11 +78,14 @@ def rev(ctx, directory):


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


def rev_if_git(ctx, directory):
"""Determine git revision (or ``None``)."""
try:
the_rev = rev(ctx, directory)
is_dirtry = is_rev_dirty(ctx, directory)
Expand Down
127 changes: 123 additions & 4 deletions planemo/github_util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
"""
"""
"""Utilities for interacting with Github."""
from __future__ import absolute_import

import os

from galaxy.tools.deps.commands import which

from planemo import git
from planemo.io import (
communicate,
IS_OS_X,
untar_to,
)

try:
import github
Expand All @@ -8,29 +19,137 @@
github = None
has_github_lib = False

HUB_VERSION = "2.2.8"

NO_GITHUB_DEP_ERROR = ("Cannot use github functionality - "
"PyGithub library not available.")
FAILED_TO_DOWNLOAD_HUB = "No hub executable available and it could not be installed."


def get_github_config(ctx):
"""Return a :class:`planemo.github_util.GithubConfig` for given configuration."""
global_github_config = _get_raw_github_config(ctx)
return None if global_github_config is None else GithubConfig(global_github_config)


def clone_fork_branch(ctx, target, path, **kwds):
"""Clone, fork, and branch a repository ahead of building a pull request."""
git.checkout(
ctx,
target,
path,
branch=kwds.get("branch", None),
remote="origin",
from_branch="master"
)
if kwds.get("fork"):
fork(ctx, path, **kwds)


def fork(ctx, path, **kwds):
"""Fork the target repository using ``hub``."""
hub_path = ensure_hub(ctx, **kwds)
hub_env = get_hub_env(ctx, path, **kwds)
cmd = [hub_path, "fork"]
communicate(cmd, env=hub_env)


def pull_request(ctx, path, message=None, **kwds):
"""Create a pull request against the origin of the path using ``hub``."""
hub_path = ensure_hub(ctx, **kwds)
hub_env = get_hub_env(ctx, path, **kwds)
cmd = [hub_path, "pull-request"]
if message is not None:
cmd.extend(["-m", message])
communicate(cmd, env=hub_env)


def get_hub_env(ctx, path, **kwds):
"""Return a environment dictionary to run hub with given user and repository target."""
env = git.git_env_for(path).copy()
github_config = _get_raw_github_config(ctx)
if github_config is not None:
if "username" in github_config:
env["GITHUB_USER"] = github_config["username"]
if "password" in github_config:
env["GITHUB_PASSWORD"] = github_config["password"]

return env


def ensure_hub(ctx, **kwds):
"""Ensure ``hub`` is on the system ``PATH``.
This method will ensure ``hub`` is installed if it isn't available.
For more information on ``hub`` checkout ...
"""
hub_path = which("hub")
if not hub_path:
planemo_hub_path = os.path.join(ctx.workspace, "hub")
if not os.path.exists(planemo_hub_path):
_try_download_hub(planemo_hub_path)

if not os.path.exists(planemo_hub_path):
raise Exception(FAILED_TO_DOWNLOAD_HUB)

hub_path = planemo_hub_path
return hub_path


def _try_download_hub(planemo_hub_path):
link = _hub_link()
# Strip URL base and .tgz at the end.
basename = link.split("/")[-1].rsplit(".", 1)[0]
untar_to(link, tar_args="-zxvf - %s/bin/hub -O > '%s'" % (basename, planemo_hub_path))
communicate(["chmod", "+x", planemo_hub_path])


def _get_raw_github_config(ctx):
"""Return a :class:`planemo.github_util.GithubConfig` for given configuration."""
if "github" not in ctx.global_config:
return None
global_github_config = ctx.global_config["github"]
return GithubConfig(global_github_config)
return ctx.global_config["github"]


class GithubConfig(object):
"""Abstraction around a Github account.
Required to use ``github`` module methods that require authorization.
"""

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


def _hub_link():
if IS_OS_X:
template_link = "https://github.com/github/hub/releases/download/v%s/hub-darwin-amd64-%s.tgz"
else:
template_link = "https://github.com/github/hub/releases/download/v%s/hub-linux-amd64-%s.tgz"
return template_link % (HUB_VERSION, HUB_VERSION)


def publish_as_gist_file(ctx, path, name="index"):
"""Publish a gist.
More information on gists at http://gist.github.com/.
"""
github_config = get_github_config(ctx)
user = github_config._github.get_user()
content = open(path, "r").read()
content_file = github.InputFileContent(content)
gist = user.create_gist(False, {name: content_file})
return gist.files[name].raw_url


__all__ = [
"clone_fork_branch",
"ensure_hub",
"fork",
"get_github_config",
"get_hub_env",
"publish_as_gist_file",
]
Loading

0 comments on commit e925ba1

Please sign in to comment.