1. David Wolever
  2. dotfiles


dotfiles / bin / pullpull

#!/usr/bin/env python
# pullpull: simplify pulling github pull requests into a local repository:
#   $ pullpull -c https://github.com/user/reponame/pull/123
#   Cloning into reponame...
#   ...
#   Switched to branch 'pull-123'
#   repository cloned and pull request applied to 'reponame/'
#   $

import re
import os
import sys
import json
import urllib2
import urlparse
import subprocess as sp

pull_re = re.compile("/(.*?)/(.*?)/pull/([0-9]+)")

class CmdError(Exception):

def _escape_cmd(cmd, args):
    return cmd %tuple("'%s'" %(a.replace("'", "\\'"), ) for a in args)

def _call(cmd, args):
    cmd_actual = _escape_cmd(cmd, args)
    print cmd_actual
    p = sp.Popen(cmd_actual, shell=True, stdout=sp.PIPE, stderr=sp.PIPE)
    stdout, stderr = p.communicate()
    return p.returncode, stdout, stderr

def call_nocheck(cmd, *args):
    return _call(cmd, args)

def call(cmd, *args):
    res, stdout, stderr = _call(cmd, args)
    if res != 0:
        raise CmdError("%r exited with status %s and stderr:\n%s"
                       %(_escape_cmd(cmd, args), res, stderr))
    return stdout

def main(argv):
    argv = list(argv)
    if len(argv) < 2:
        print "usage: %s PULL_REQUEST_URL" %(argv[0], )
        print "for example:"
        print "    %s https://github.com/user/repo/pull/123" %(argv[0], )
        return 1
    create = "-c" in argv
    if create:
    pull_url = urlparse.urlsplit(argv[1])
    match = pull_re.search(pull_url.path)
    if not match:
        print "error: invalid pull request path: %r" %(pull_url.path, )
        print "expected pull request path format: /:user/:repo/pull/:number"
        return 1
    user, repo, num = match.groups()

    api_url = "https://api.github.com/repos/%s/%s/pulls/%s" %(user, repo, num)
    pull_data_str = urllib2.urlopen(api_url).read()
        pull_data = json.loads(pull_data_str)
    except ValueError as e:
        print "error: %r returned invalid json: %r" %(api_url, e)
        return 1

    # Double check that we're actually in a git repo...
    did_create = False
    cur_path = os.getcwd()
    while cur_path != "/":
        if os.path.exists(os.path.join(cur_path, ".git")):
        cur_path = os.path.dirname(cur_path)
        if not create:
            print "error: not in a git repo (use -c to create one)"
            return 1
        remote_base_repo = pull_data["base"]["repo"]["git_url"]
        remote_base_name = pull_data["base"]["repo"]["name"]
        call("git clone %s %s", remote_base_repo, remote_base_name)
        did_create = True

        base_sha = pull_data["base"]["sha"]
        call("git show %s >/dev/null", base_sha)
    except CmdError as e:
        expected_url = pull_data["base"]["repo"]["git_url"]
        print "error: %s" %(e, )
        print "HINT:"
        print "- is this repository a clone of %s?" %(expected_url, )
        print "- is it up to date? (does it contain %s)?" %(base_sha, )
        return 1

    remote_repo = pull_data["head"]["repo"]["git_url"]
    remote_branch = pull_data["head"]["ref"]
    local_branch = "pull-%s" %(num, )

    branch_exists = call("git branch --list %s", local_branch)
    if branch_exists:
        overwrite = raw_input("branch %r already exists. overwrite? (Y/n) "
                              %(local_branch, ))
        if overwrite.lower()[:1] == "n":
            print "aborting."
            return 1
        call("git checkout --quiet master")
        call("git branch -D %s", local_branch)

    remote_exists_code, _, _ = call_nocheck("git remote show %s", local_branch)
    if remote_exists_code == 0:
        call("git remote remove %s", local_branch)
    call("git remote add -t %s %s %s", remote_branch, local_branch, remote_repo)
    call("git fetch %s %s:%s", remote_repo, remote_branch, local_branch)
    call("git checkout %s", local_branch)

    if did_create:
        print "repository cloned and pull request applied to '%s/'" %(

if __name__ == "__main__":