comparison hg_status.py @ 4:f714a6a7842c

start new web view for hgand github syncing
author drewp@bigasterisk.com
date Fri, 24 Jul 2020 14:42:08 -0700
parents
children db4037285592
comparison
equal deleted inserted replaced
3:4077903a9520 4:f714a6a7842c
1 from dataclasses import dataclass, field
2 import datetime
3 import json
4 import logging
5 from pathlib import Path
6 import time
7 import traceback
8 from typing import Dict, Optional, Tuple
9
10 import cyclone.httpserver
11 import cyclone.sse
12 import cyclone.web
13 from cycloneerr import PrettyErrorHandler
14 from dateutil.parser import parse
15 from dateutil.tz import tzlocal
16 import docopt
17 from ruamel.yaml import YAML
18 from standardservice.logsetup import log, verboseLogging
19 import treq
20 from twisted.internet import reactor
21 from twisted.internet.defer import inlineCallbacks, returnValue
22 from twisted.internet.utils import getProcessOutput, _UnexpectedErrorOutput
23 import tzlocal
24
25 local = tzlocal.get_localzone()
26 githubOwner = 'drewp'
27
28
29 @inlineCallbacks
30 def runHg(cwd, args):
31 if args[0] not in ['push']:
32 args.extend(['-T', 'json'])
33 j = yield getProcessOutput('/usr/local/bin/hg', args, path=cwd)
34 returnValue(json.loads(j) if j else None)
35
36
37 @dataclass
38 class Repo:
39 path: Path
40 github: bool
41 _cache: Dict[str, Tuple[float, object]] = field(default_factory=dict)
42
43 def _isStale(self, group) -> Optional[object]:
44 now = time.time()
45 if group not in self._cache:
46 return True
47 if now > self._cache[group][0] + 86400:
48 return True
49 print('fresh')
50 return False
51
52 def _save(self, group, obj):
53 now = time.time()
54 self._cache[group] = (now, obj)
55
56 def _get(self, group):
57 print('get')
58 return self._cache[group][1]
59
60 @inlineCallbacks
61 def getStatus(self):
62 if self._isStale('status'):
63 try:
64 statusResp = yield runHg(self.path, ['status'])
65 except Exception as e:
66 status = {'error': repr(e)}
67 else:
68 unknowns = len([row for row in statusResp if row['status'] == '?'])
69 status = {'unknown': unknowns, 'changed': len(statusResp) - unknowns}
70 self._save('status', status)
71 returnValue(self._get('status'))
72
73 @inlineCallbacks
74 def getLatestHgCommit(self):
75 if self._isStale('log'):
76 rows = yield runHg(self.path, ['log', '--limit', '1'])
77 commit = rows[0]
78 sec = commit['date'][0]
79 t = datetime.datetime.fromtimestamp(sec, local)
80 self._save('log', {'email': commit['user'], 't': t.isoformat(), 'message': commit['desc']})
81 returnValue(self._get('log'))
82
83 @inlineCallbacks
84 def getLatestGithubCommit(self):
85 if self._isStale('github'):
86 resp = yield treq.get(f'https://api.github.com/repos/{githubOwner}/{self.path.name}/commits?per_page=1',
87 timeout=5,
88 headers={
89 'User-agent': 'reposync by github.com/drewp',
90 'Accept': 'application/vnd.github.v3+json'
91 })
92 ret = yield treq.json_content(resp)
93 commit = ret[0]['commit']
94 t = parse(commit['committer']['date']).astimezone(local).isoformat()
95 self._save('github', {'email': commit['committer']['email'], 't': t, 'message': commit['message']})
96 returnValue(self._get('github'))
97
98 @inlineCallbacks
99 def clearGithubMaster(self):
100 '''bang(pts/13):/tmp/reset% git init
101 Initialized empty Git repository in /tmp/reset/.git/
102 then github set current to a new branch called 'clearing' with https://developer.github.com/v3/repos/#update-a-repository
103 bang(pts/13):/tmp/reset% git remote add origin git@github.com:drewp/href.git
104 bang(pts/13):/tmp/reset% git push origin :master
105 To github.com:drewp/href.git
106 - [deleted] master
107 maybe --set-upstream origin
108 bang(pts/13):/tmp/reset% git remote set-branches origin master
109 ?
110 then push
111 then github setdefault to master
112 then github delete clearing
113 '''
114
115 @inlineCallbacks
116 def pushToGithub(self):
117 if not self.github:
118 raise ValueError
119 yield runHg(self.path, ['bookmark', '--rev', 'default', 'master'])
120 out = yield runHg(self.path, ['push', f'git+ssh://git@github.com/{githubOwner}/{self.path.name}.git'])
121 print(f'out fompushh {out}')
122
123
124 class GithubSync(PrettyErrorHandler, cyclone.web.RequestHandler):
125
126 @inlineCallbacks
127 def post(self):
128 try:
129 path = self.get_argument('repo')
130 repo = [r for r in self.settings.repos if str(r.path) == path][0]
131 yield repo.pushToGithub()
132 except Exception:
133 traceback.print_exc()
134 raise
135
136
137 class Statuses(cyclone.sse.SSEHandler):
138
139 def update(self, key, data):
140 self.sendEvent(json.dumps({'key': key, 'update': data}).encode('utf8'))
141
142 def bind(self):
143 self.toProcess = self.settings.repos[:]
144 reactor.callLater(0, self.runOne)
145
146 @inlineCallbacks
147 def runOne(self):
148 if not self.toProcess:
149 print('done')
150 return
151 repo = self.toProcess.pop(0)
152
153 try:
154 update = {
155 'path': str(repo.path),
156 'github': repo.github,
157 'status': (yield repo.getStatus()),
158 'hgLatest': (yield repo.getLatestHgCommit())
159 }
160 if repo.github:
161 update['githubLatest'] = (yield repo.getLatestGithubCommit())
162 self.update(str(repo.path), update)
163 except Exception:
164 log.warn(f'not reporting on {repo}')
165 traceback.print_exc()
166 reactor.callLater(0, self.runOne)
167
168
169 def main():
170 args = docopt.docopt('''
171 Usage:
172 hg_status.py [options]
173
174 Options:
175 -v, --verbose more logging
176 ''')
177 verboseLogging(args['--verbose'])
178
179 import sys
180 sys.path.append('/usr/lib/python3/dist-packages')
181 import OpenSSL
182
183 yaml = YAML(typ='safe')
184 config = yaml.load(open('config.yaml'))
185 repos = [Repo(Path(row['dir']), row['github']) for row in config['hg_repos']]
186
187 class Application(cyclone.web.Application):
188
189 def __init__(self):
190 handlers = [
191 (r"/()", cyclone.web.StaticFileHandler, {
192 'path': '.',
193 'default_filename': 'index.html'
194 }),
195 (r'/build/(bundle\.js)', cyclone.web.StaticFileHandler, {
196 'path': './build/'
197 }),
198 (r'/status/events', Statuses),
199 (r'/githubSync', GithubSync),
200 ]
201 cyclone.web.Application.__init__(
202 self,
203 handlers,
204 repos=repos,
205 debug=args['--verbose'],
206 template_path='.',
207 )
208
209 reactor.listenTCP(10001, Application())
210 reactor.run()
211
212
213 if __name__ == '__main__':
214 main()