Mercurial > code > home > repos > reposync
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() |