-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhero.py
executable file
·140 lines (110 loc) · 3.77 KB
/
hero.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import os
import configparser
from getpass import getpass
from github import Github
from git import Repo
def _get_creds(conf):
"""Get credentials from a file or stdin.
"""
if conf and os.path.isfile(conf):
config = configparser.ConfigParser()
config.read(conf)
usr, pwd = config['login']['username'], config['login']['password']
else:
usr, pwd = input("Username: "), getpass("Password: ")
return usr, pwd
def login(conf=None):
"""Authenticate with Github using their API (for a higher rate limit).
"""
g = Github(*_get_creds(conf))
limit = g.get_rate_limit().rate.remaining
return g.get_user(), limit
def _depaginate(pagable):
"""This extension of the Github API hides pagination handling for a
pagable object.
This function returns a generator that is per-page lazy.
"""
results = []
page = 0
buf = None
# the github api is weird
while buf == None or len(buf) > 0:
buf = pagable.get_page(page)
page += 1
for e in buf:
yield e
def traverse_repos(root, cb, skip=False):
"""Traverse repositories of github using the Github API.
Runs cb on each user's repositories.
If skip is True, then that user's repositories are not processed.
"""
# convert the repo callback to a user callback
def _user_cb(user):
for repo in _depaginate(user.get_repos()):
cb(user, repo)
traverse_users(root, _user_cb, skip)
def traverse_users(root, cb, skip, visited=set()):
"""Traverse Github users breadth-first using the Github API.
Runs cb on each user.
If skip is True, then that user's repositories are not processed.
"""
# avoid cycles
visited.add(root.login)
if not skip:
cb(root)
for user in _depaginate(root.get_following()):
if not user.login in visited:
traverse_users(user, cb, False)
def apply_diff(filename, diff):
"""Apply a diff to a file.
"""
# TODO
with open(filename, 'w') as fp:
fp.write(diff)
def clone(repo, path):
"""Clone a given repository object to the specified path.
"""
try:
shutil.rmtree(path)
except FileNotFoundError:
pass
finally:
logging.debug("cloning {}".format(repo.clone_url))
repo_local = Repo.clone_from(repo.clone_url, path)
def commit_and_pr(local, remote,
username, documents,
cmsg="auto", cargs={}):
local.index.add(documents)
local.index.commit(cmsg)
local.remote('origin').push()
args_pr = {'title': "automatic PR",
'body': "automatically generated pull request",
'head': "{}:master".format(username),
'base': "master"}
args_pr.update(cargs)
logging.debug("making PR")
remote.create_pull(**args_pr)
def auto_pr(user, repo_remote, work_path, updater, cmsg="auto", cargs={}):
"""Clone a repository, check for changes to be made, then fork and make a
PR if needed.
Runs updater on repository path, expecting a list of file diffs.
"""
# clone original repository
clone(repo_remote, work_path)
# determine changes
file_changes = updater(work_path)
# fork repo if diffs generated
if file_changes:
repo_fork = user.create_fork(repo_remote)
# clone forked repository
clone(repo_fork, work_path)
for name, diff in file_changes.items():
# apply changes to file
apply_diff(filename, diff)
# commit and PR
if repo_fork.index.diff(None): # TODO: retest the need for this
commit_and_pr(repo_fork, repo_remote,
user.login, list(file_changes.keys()),
cmsg, cargs)
# clean up
repo_fork.delete()