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

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

class CmdError(Exception):

def system(cmd, *args):
    cmd_actual = cmd %tuple("'%s'" %(a.replace("'", "\\'"), ) for a in args)
    res = os.system(cmd_actual)
    if res != 0:
        raise CmdError("%r exited with status %s" %(cmd_actual, res))
    return 0

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"]
        system("git clone %s %s", remote_base_repo, remote_base_name)
        did_create = True

        base_sha = pull_data["base"]["sha"]
        system("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, )
    system("git remote add -t %s %s %s",
           remote_branch, local_branch, remote_repo)
    system("git fetch %s %s:%s", remote_repo, remote_branch, local_branch)
    system("git checkout %s", local_branch)
    if did_create:
        print "repository cloned and pull request applied to '%s/'" %(

if __name__ == "__main__":