├── .gitignore ├── README.md ├── robot.py ├── settings.py ├── testfile.txt └── users.txt /.gitignore: -------------------------------------------------------------------------------- 1 | settings.py 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Whitespace Bot - by [Gun.io](http://gun.io) 2 | ## Making GitHub Better - With Robots 3 | ## Rich Jones - rich@gun.io 4 | 5 | # About 6 | WhitespaceBot finds trailing whitespace and destroys it, then sends you a pull request! 7 | It gives you a .gitignore if you didn't have one either. 8 | -------------------------------------------------------------------------------- /robot.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import settings 3 | import simplejson 4 | import subprocess 5 | import sys 6 | import argparse 7 | import time 8 | from random import choice 9 | import os 10 | from os.path import join 11 | import shutil 12 | import urllib2 13 | import json 14 | 15 | #pseudo 16 | # take name from list 17 | # scan names for most names most popular repo 18 | # fork it - POST /repos/:user/:repo/forks 19 | # clone it 20 | # switch branch 21 | # fix it 22 | # commit it! 23 | # push it 24 | # submit pull req 25 | # remove name from list 26 | 27 | 28 | def main(): 29 | parser = argparse.ArgumentParser(description='Whitespace annihilating GitHub robot.\nBy Rich Jones - Gun.io - rich@gun.io') 30 | parser.add_argument('-u', '--users', help='A text file with usernames.', default='users.txt') 31 | parser.add_argument('-o', '--old-users', help='A text file with usernames.', default='old_users.txt') 32 | parser.add_argument('-c', '--count', help='The maximum number of total requests to make.', default=999999) 33 | parser.add_argument('-v', '--verbose', help='Make this sucker loud? (True/False)', default=True) 34 | args = parser.parse_args() 35 | 36 | auth = (settings.username, settings.password) 37 | 38 | old_users_file = args.old_users 39 | old_users = load_user_list(old_users_file) 40 | 41 | users = args.users 42 | new_users = load_user_list(users) 43 | user = get_user(users) 44 | 45 | #XXX: Potential deal breaker in here! 46 | count = 0 47 | while user in old_users: 48 | print "We've already done that user!" 49 | user = get_user(users) 50 | count = count + 1 51 | if count > len(new_users): 52 | return 53 | 54 | repos = 'https://api.github.com/users/' + user + '/repos' 55 | r = requests.get(repos, auth=auth) 56 | 57 | if (r.status_code == 200): 58 | resp = simplejson.loads(r.content) 59 | topwatch = 0 60 | top_repo = '' 61 | for repo in resp: 62 | if repo['watchers'] > topwatch: 63 | top_repo = repo['name'] 64 | topwatch = repo['watchers'] 65 | print dir(repo) 66 | 67 | print user + "'s most watched repo is " + top_repo + " with " + str(topwatch) + " watchers. Forking." 68 | 69 | repo = top_repo 70 | print "GitHub Forking.." 71 | clone_url = fork_repo(user, repo) 72 | print "Waiting.." 73 | time.sleep(30) 74 | print "Cloning.." 75 | cloned = clone_repo(clone_url) 76 | if not cloned: 77 | return 78 | print "Changing branch.." 79 | branched = change_branch(repo) 80 | print "Fixing repo.." 81 | fixed = fix_repo(repo) 82 | print "Comitting.." 83 | commited = commit_repo(repo) 84 | print "Pushing.." 85 | pushed = push_commit(repo) 86 | print "Submitting pull request.." 87 | submitted = submit_pull_request(user, repo) 88 | print "Delting local repo.." 89 | deleted = delete_local_repo(repo) 90 | print "Olding user.." 91 | old = save_user(old_users_file, user) 92 | 93 | 94 | def save_user(old_users_file, user): 95 | with open(old_users_file, "a") as id_file: 96 | id_file.write(user + '\n') 97 | return True 98 | 99 | 100 | def load_user_list(old_users): 101 | text_file = open(old_users, "r") 102 | old = text_file.readlines() 103 | text_file.close() 104 | x = 0 105 | for hid in old: 106 | old[x] = hid.rstrip() 107 | x = x + 1 108 | return old 109 | 110 | 111 | def get_user(users): 112 | text_file = open(users, "r") 113 | u = text_file.readlines() 114 | text_file.close() 115 | return choice(u).rstrip() 116 | 117 | 118 | def fork_repo(user, repo): 119 | url = 'https://api.github.com/repos/' + user + '/' + repo + '/forks' 120 | auth = (settings.username, settings.password) 121 | r = requests.post(url, auth=auth) 122 | if (r.status_code == 201): 123 | resp = simplejson.loads(r.content) 124 | return resp['ssh_url'] 125 | else: 126 | return None 127 | 128 | 129 | def clone_repo(clone_url): 130 | try: 131 | args = ['/usr/bin/git', 'clone', clone_url] 132 | p = subprocess.Popen(args) 133 | p.wait() 134 | return True 135 | except Exception, e: 136 | return False 137 | 138 | 139 | def change_branch(repo): 140 | #XXX fuck this 141 | gitdir = os.path.join(settings.pwd, repo, ".git") 142 | repo = os.path.join(settings.pwd, repo) 143 | 144 | try: 145 | args = ['/usr/bin/git', '--git-dir', gitdir, '--work-tree', repo, 'branch', 'clean'] 146 | p = subprocess.Popen(args) 147 | p.wait() 148 | args = ['/usr/bin/git', '--git-dir', gitdir, '--work-tree', repo, 'checkout', 'clean'] 149 | p = subprocess.Popen(args) 150 | p.wait() 151 | return True 152 | except Exception, e: 153 | return False 154 | 155 | 156 | def fix_repo(repo): 157 | gitdir = os.path.join(settings.pwd, repo, ".git") 158 | repo = os.path.join(settings.pwd, repo) 159 | for root, dirs, files in os.walk(repo): 160 | for f in files: 161 | path = os.path.join(root, f) 162 | 163 | # gotta be a way more pythonic way of doing this 164 | banned = ['.git', '.py', '.yaml', '.patch', '.hs', '.occ', '.md', '.markdown', '.mdown'] 165 | cont = False 166 | for b in banned: 167 | if b in path: 168 | cont = True 169 | if cont: 170 | continue 171 | 172 | p = subprocess.Popen(['file', '-bi', path], stdout=subprocess.PIPE) 173 | 174 | while True: 175 | o = p.stdout.readline() 176 | if o == '': 177 | break 178 | #XXX: Motherfucking OSX is a super shitty and not real operating system 179 | #XXX: and doesn't do file -bi properly 180 | if 'text' in o: 181 | q = subprocess.Popen(['sed', '-i', 's/[ \\t]*$//', path]) 182 | q.wait() 183 | args = ['/usr/bin/git', '--git-dir', gitdir, '--work-tree', repo, 'add', path] 184 | pee = subprocess.Popen(args) 185 | pee.wait() 186 | if o == '' and p.poll() != None: break 187 | 188 | git_ignore = os.path.join(repo, '.gitignore') 189 | if not os.path.exists(git_ignore): 190 | ignorefile = open(git_ignore, 'w') 191 | ignore = '# Compiled source #\n' + \ 192 | '###################\n' + \ 193 | '*.com\n' + \ 194 | '*.class\n' + \ 195 | '*.dll\n' + \ 196 | '*.exe\n' + \ 197 | '*.o\n' + \ 198 | '*.so\n' + \ 199 | '*.pyc\n\n' + \ 200 | '# Numerous always-ignore extensions\n' + \ 201 | '###################\n' + \ 202 | '*.diff\n' + \ 203 | '*.err\n' + \ 204 | '*.orig\n' + \ 205 | '*.log\n' + \ 206 | '*.rej\n' + \ 207 | '*.swo\n' + \ 208 | '*.swp\n' + \ 209 | '*.vi\n' + \ 210 | '*~\n\n' + \ 211 | '*.sass-cache\n' + \ 212 | '# Folders to ignore\n' + \ 213 | '###################\n' + \ 214 | '.hg\n' + \ 215 | '.svn\n' + \ 216 | '.CVS\n' + 217 | '# OS or Editor folders\n' + \ 218 | '###################\n' + \ 219 | '.DS_Store\n' + \ 220 | 'Icon?\n' + \ 221 | 'Thumbs.db\n' + \ 222 | 'ehthumbs.db\n' + \ 223 | 'nbproject\n' + \ 224 | '.cache\n' + \ 225 | '.project\n' + \ 226 | '.settings\n' + \ 227 | '.tmproj\n' + \ 228 | '*.esproj\n' + \ 229 | '*.sublime-project\n' + \ 230 | '*.sublime-workspace\n' + \ 231 | '# Dreamweaver added files\n' + \ 232 | '###################\n' + \ 233 | '_notes\n' + \ 234 | 'dwsync.xml\n' + \ 235 | '# Komodo\n' + \ 236 | '###################\n' + \ 237 | '*.komodoproject\n' + \ 238 | '.komodotools\n' 239 | ignorefile.write(ignore) 240 | ignorefile.close() 241 | try: 242 | args = ['/usr/bin/git', '--git-dir', gitdir, '--work-tree', repo, 'add', git_ignore] 243 | p = subprocess.Popen(args) 244 | p.wait() 245 | return True 246 | except Exception, e: 247 | return False 248 | 249 | return True 250 | 251 | 252 | def commit_repo(repo): 253 | gitdir = os.path.join(settings.pwd, repo, ".git") 254 | repo = os.path.join(settings.pwd, repo) 255 | 256 | try: 257 | message = "Remove whitespace [Gun.io WhitespaceBot]" 258 | args = ['/usr/bin/git', '--git-dir', gitdir, '--work-tree', repo, 'commit', '-m', message] 259 | p = subprocess.Popen(args) 260 | p.wait() 261 | return True 262 | except Exception, e: 263 | print e 264 | return False 265 | 266 | 267 | def push_commit(repo): 268 | gitdir = os.path.join(settings.pwd, repo, ".git") 269 | repo = os.path.join(settings.pwd, repo) 270 | try: 271 | args = ['/usr/bin/git', '--git-dir', gitdir, '--work-tree', repo, 'push', 'origin', 'clean'] 272 | p = subprocess.Popen(args) 273 | p.wait() 274 | return True 275 | except Exception, e: 276 | print e 277 | return False 278 | 279 | 280 | def basic_authorization(user, password): 281 | s = user + ":" + password 282 | return "Basic " + s.encode("base64").rstrip() 283 | 284 | 285 | def submit_pull_request(user, repo): 286 | auth = (settings.username, settings.password) 287 | url = 'https://api.github.com/repos/' + user + '/' + repo + '/pulls' 288 | params = {'title': 'Hi! I cleaned up your code for you!', 'body': 'Hi' 289 | + ' there!\n\nThis is WhitespaceBot. I\'m an [open-source](https://github.com/Gunio/WhitespaceBot) robot that' 290 | + ' removes trailing white space in your code, and gives you a gitignore file if you didn\'t have one! ' + 291 | ' \n\nWhy whitespace? Whitespace is an eyesore for developers who use text editors with dark themes. It\'s not ' + 292 | ' a huge deal, but it\'s a bit annoying if you use Vim in a terminal. Really, I\'m just a proof of ' + 293 | ' concept - GitHub\'s V3 API allows robots to automatically improve open source projects, and that\'s really' + 294 | ' cool. Hopefully, somebody, maybe you!, will fork me and make me even more useful. My owner is ' + 295 | '[funding a bounty](http://gun.io/open/12/add-security-flaw-fixing-features-to-whitespacebot) to anybody ' + 296 | 'who can add security fixing features to me. ' + 297 | '\n\nI\'ve only cleaned your most popular project, and I\'ve added you to a list of users not to contact ' + 298 | 'again, so you won\'t get any more pull requests from me unless you ask. If I\'m misbehaving, please email my ' + 299 | 'owner and tell him to turn me off! If this is pull request is of no use to you, please just ignore it.\n\n' + 300 | 'Thanks!\nWhiteSpacebot from [Gun.io](http://gun.io).', 301 | 'base': 'master', 'head': 'GunioRobot:clean'} 302 | 303 | req = urllib2.Request(url, 304 | headers={ 305 | "Authorization": basic_authorization(settings.username, settings.password), 306 | "Content-Type": "application/json", 307 | "Accept": "*/*", 308 | "User-Agent": "WhitespaceRobot/Gunio", 309 | }, 310 | data=json.dumps(params)) 311 | f = urllib2.urlopen(req) 312 | return True 313 | 314 | 315 | def delete_local_repo(repo): 316 | repo = os.path.join(settings.pwd, repo) 317 | try: 318 | shutil.rmtree(repo) 319 | return True 320 | except Exception, e: 321 | return False 322 | 323 | 324 | if __name__ == '__main__': 325 | sys.exit(main()) 326 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | # Put your User/Pass in here! 2 | 3 | username='YourUsername' 4 | password='YourPassword' 5 | -------------------------------------------------------------------------------- /testfile.txt: -------------------------------------------------------------------------------- 1 | This file has 2 | a lot of trailing 3 | whitespace 4 | 5 | 6 | 7 | and lots 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | of blank lines! 16 | -------------------------------------------------------------------------------- /users.txt: -------------------------------------------------------------------------------- 1 | Miserlou 2 | --------------------------------------------------------------------------------