├── README.md └── git-pull-request /README.md: -------------------------------------------------------------------------------- 1 | git pull-request 2 | ================ 3 | 4 | Automatically check out github pull requests into their own branch. 5 | 6 | Installation 7 | ------------ 8 | 9 | Copy the script to somewhere in your ``PATH`` and make it executable. 10 | 11 | Set github.token to your github OAUTH token 12 | i.e. git config --global github.token OAUTH-TOKEN 13 | 14 | If you need Python 3 support be sure to use the ``python3`` branch. 15 | 16 | Usage 17 | ----- 18 | 19 | git pull-request [] 20 | 21 | Options 22 | ------- 23 | 24 | -h, --help 25 | Display this message and exit 26 | 27 | -r , --repo 28 | Use this github repo instead of the 'remote origin' or 'github.repo' 29 | git config settings. Needs to be in "user/repository" form 30 | 31 | License 32 | ======= 33 | 34 | Copyright (C) 2011-2013 by Andreas Gohr 35 | 36 | Permission is hereby granted, free of charge, to any person obtaining a copy 37 | of this software and associated documentation files (the "Software"), to deal 38 | in the Software without restriction, including without limitation the rights 39 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 40 | copies of the Software, and to permit persons to whom the Software is 41 | furnished to do so, subject to the following conditions: 42 | 43 | The above copyright notice and this permission notice shall be included in 44 | all copies or substantial portions of the Software. 45 | 46 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 47 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 48 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 49 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 50 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 51 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 52 | THE SOFTWARE. 53 | -------------------------------------------------------------------------------- /git-pull-request: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | """git pull-request 4 | 5 | Automatically check out github pull requests into their own branch. 6 | 7 | Usage: 8 | 9 | git pull-request 10 | 11 | When a PR# is specified, it will be fetched, otherwise a list of PRs 12 | in the remote branch will be shown. 13 | 14 | Options: 15 | 16 | -h, --help 17 | Display this message and exit 18 | 19 | -r , --repo , --remote 20 | Use this github repo instead of the 'remote origin' or 'github.repo' 21 | git config settings. Full form needs to be "user/repository", otherwise 22 | it is assumed to be a short remote alias name 23 | 24 | -g, --git 25 | Use git_url instead of http URL for fetching 26 | 27 | --copy 28 | List the Copyright details 29 | 30 | Copyright (C) 2011 by Andreas Gohr 31 | """ 32 | 33 | copyright = """ 34 | Copyright (C) 2011 by Andreas Gohr 35 | 36 | Permission is hereby granted, free of charge, to any person obtaining a copy 37 | of this software and associated documentation files (the "Software"), to deal 38 | in the Software without restriction, including without limitation the rights 39 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 40 | copies of the Software, and to permit persons to whom the Software is 41 | furnished to do so, subject to the following conditions: 42 | 43 | The above copyright notice and this permission notice shall be included in 44 | all copies or substantial portions of the Software. 45 | 46 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 47 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 48 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 49 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 50 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 51 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 52 | THE SOFTWARE. 53 | """ 54 | 55 | import sys 56 | import getopt 57 | import json 58 | import urllib2 59 | import os 60 | import re 61 | import pipes 62 | 63 | 64 | def main(): 65 | repo, remote = None, None 66 | git_method = 'html_url' 67 | 68 | # parse command line options 69 | try: 70 | opts, args = getopt.getopt(sys.argv[1:], "hr:g", ["help", "repo:", "copy","git"]) 71 | except getopt.error, msg: 72 | print msg 73 | print "for help use --help" 74 | sys.exit(2) 75 | # process options 76 | for o, a in opts: 77 | if o in ("-h", "--help"): 78 | print __doc__ 79 | sys.exit(0) 80 | elif o in ("--copy"): 81 | print copyright 82 | sys.exit(0) 83 | elif o in ("-g", "--git"): 84 | git_method = 'git_url' 85 | elif o in ("-r", "--repo", "--remote"): 86 | if re.search('/', a): 87 | repo = a 88 | else: 89 | remote = a 90 | 91 | 92 | # attempt to get token from git config 93 | token = os.popen("git config --get github.token").read().rstrip() 94 | # try to get repo name from git config: 95 | if not repo and not remote: 96 | repo = os.popen('git config --get github.repo').read().strip() 97 | 98 | if repo and not re.search('/', repo): 99 | # if repo is not a full repo, assume it is remote alias 100 | remote = repo 101 | repo = None 102 | 103 | # else try to get remote from git config: 104 | if not repo: 105 | if not remote: 106 | remote = os.popen('git config --get github.remote').read().strip() 107 | if not remote: 108 | remote = 'origin' 109 | 110 | if remote: 111 | # get full repo name from remote url 112 | escaped = pipes.quote(remote) 113 | origin = os.popen("git config --get remote.%s.url" % escaped).read() 114 | origin = re.sub("(\.git)?\s*$", "", origin) 115 | m = re.search(r"\bgithub\.com[:/]([^/]+/[^/]+)$", origin) 116 | if(m is not None): 117 | repo = m.group(1) 118 | 119 | if not repo: 120 | print color_text("Failed to determine github repository name", 'red', True) 121 | print "The repository is usually automatically detected from your remote." 122 | print "By default the remote is assumed to be 'origin', but you can override this by" 123 | print "using the -r parameter or specifying the github.remote option in your config" 124 | print "" 125 | print " git config --global github.remote upstream" 126 | print "" 127 | print "If your remote url doesn't point to github, you can specify the repository on" 128 | print "the command line using the -r parameter, by specifying either a remote or" 129 | print "the full repository name (user/repo), or configure it using" 130 | print "" 131 | print " git config github.repo /" 132 | sys.exit(1) 133 | 134 | # process arguments 135 | if len(args): 136 | ret = fetch(repo, token, args[0], git_method) 137 | else: 138 | ret = show(repo, token) 139 | 140 | sys.exit(ret) 141 | 142 | 143 | def display(pr): 144 | """Nicely display info about a given pull request 145 | """ 146 | print "%s - %s" % (color_text('REQUEST %s' % pr.get('number'), 'green'), pr.get('title')) 147 | print " %s" % (color_text(pr['head']['label'], 'yellow')) 148 | print " by %s %s" % (pr['user'].get('login'), color_text(pr.get('created_at')[:10], 'red')) 149 | print " %s" % (color_text(pr.get('html_url'), 'blue')) 150 | print 151 | 152 | 153 | def show(repo, token): 154 | """List open pull requests 155 | 156 | Queries the github API for open pull requests in the current repo 157 | """ 158 | print "loading open pull requests for %s..." % (repo) 159 | print 160 | url = "https://api.github.com/repos/%s/pulls" % (repo) 161 | 162 | if len(token): 163 | headers = {'User-Agent': 'git-pull-request', 'Authorization': 'token %s' % token} 164 | else: 165 | headers = {'User-Agent': 'git-pull-request'} 166 | 167 | req = urllib2.Request( 168 | url, headers=headers) 169 | try: 170 | response = urllib2.urlopen(req) 171 | except urllib2.HTTPError, msg: 172 | print "error loading pull requests for repo %s: %s" % (repo, msg) 173 | if msg.code == 404: 174 | # GH replies with 404 when a repo is not found or private and we request without OAUTH 175 | print "if this is a private repo, please set github.token to a valid GH oauth token" 176 | exit(1) 177 | 178 | data = response.read() 179 | if not data: 180 | print "failed to speak with github." 181 | return 3 182 | 183 | data = json.loads(data) 184 | # print json.dumps(data,sort_keys=True, indent=4) 185 | 186 | for pr in data: 187 | display(pr) 188 | return 0 189 | 190 | 191 | def fetch(repo, token, pullreq, git_method): 192 | print "loading pull request info for request %s..." % (pullreq) 193 | print 194 | url = "https://api.github.com/repos/%s/pulls/%s" % (repo, pullreq) 195 | 196 | if len(token): 197 | headers = {'User-Agent': 'git-pull-request', 'Authorization': 'token %s' % token} 198 | else: 199 | headers = {'User-Agent': 'git-pull-request'} 200 | 201 | req = urllib2.Request( 202 | url, headers=headers) 203 | try: 204 | response = urllib2.urlopen(req) 205 | except urllib2.HTTPError, msg: 206 | print "error loading pull requests for repo %s: %s" % (repo, msg) 207 | if msg.code == 404: 208 | # GH replies with 404 when a repo is not found or private and we request without OAUTH 209 | print "if this is a private repo, please set github.token to a valid GH oauth token" 210 | exit(1) 211 | 212 | data = response.read() 213 | if not data: 214 | print "failed to speak with github." 215 | return 3 216 | 217 | data = json.loads(data) 218 | pr = data 219 | print json.dumps(pr, sort_keys=True, indent=4, separators=(',', ': ')) 220 | if pr['head']['repo'] is None: 221 | print("remote repository for this pull request " 222 | "does not exist anymore.") 223 | return 6 224 | display(pr) 225 | 226 | local = pipes.quote('pull-request-%s' % (pullreq)) 227 | branch = os.popen("git branch|grep '^*'|awk '{print $2}'").read().strip() 228 | if(branch != pr['base']['ref'] and branch != local): 229 | print color_text("The pull request is based on branch '%s' but you're on '%s' currently" % (pr['base']['ref'], branch), 'red', True) 230 | return 4 231 | 232 | ret = os.system('git branch %s' % (local)) 233 | ret = os.system('git checkout %s' % (local)) 234 | if(ret != 0): 235 | print "Failed to create/switch branch" 236 | return 5 237 | 238 | 239 | print "pulling from %s (%s)" % (pr['head']['repo'][git_method], pr['head']['ref']) 240 | 241 | url = pipes.quote(pr['head']['repo'][git_method]) 242 | ref = pipes.quote(pr['head']['ref']) 243 | print 'git pull %s %s' % (url, ref) 244 | ret = os.system('git pull %s %s' % (url, ref)) 245 | if(ret != 0): 246 | print color_text("branch %s no longer exists." % ref, 'red') 247 | os.system('git checkout %s' % branch) 248 | os.system('git branch -D %s' % local) 249 | exit(1) 250 | 251 | print 252 | print color_text("done. examine changes and merge into master if good", 'green') 253 | 254 | return 0 255 | 256 | 257 | def color_text(text, color_name, bold=False): 258 | """Return the given text in ANSI colors 259 | 260 | From http://travelingfrontiers.wordpress.com/2010/08/22/how-to-add-colors-to-linux-command-line-output/ 261 | """ 262 | colors = ( 263 | 'black', 'red', 'green', 'yellow', 264 | 'blue', 'magenta', 'cyan', 'white' 265 | ) 266 | 267 | if not sys.stdout.isatty(): 268 | return text 269 | 270 | if color_name in colors: 271 | return '\033[{0};{1}m{2}\033[0m'.format( 272 | int(bold), 273 | colors.index(color_name) + 30, 274 | text) 275 | else: 276 | return text 277 | 278 | if __name__ == "__main__": 279 | main() 280 | --------------------------------------------------------------------------------