diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg_status.py	Fri Jul 24 14:42:08 2020 -0700
@@ -0,0 +1,214 @@
+from dataclasses import dataclass, field
+import datetime
+import json
+import logging
+from pathlib import Path
+import time
+import traceback
+from typing import Dict, Optional, Tuple
+
+import cyclone.httpserver
+import cyclone.sse
+import cyclone.web
+from cycloneerr import PrettyErrorHandler
+from dateutil.parser import parse
+from dateutil.tz import tzlocal
+import docopt
+from ruamel.yaml import YAML
+from standardservice.logsetup import log, verboseLogging
+import treq
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.utils import getProcessOutput, _UnexpectedErrorOutput
+import tzlocal
+
+local = tzlocal.get_localzone()
+githubOwner = 'drewp'
+
+
+@inlineCallbacks
+def runHg(cwd, args):
+    if args[0] not in ['push']:
+        args.extend(['-T', 'json'])
+    j = yield getProcessOutput('/usr/local/bin/hg', args, path=cwd)
+    returnValue(json.loads(j) if j else None)
+
+
+@dataclass
+class Repo:
+    path: Path
+    github: bool
+    _cache: Dict[str, Tuple[float, object]] = field(default_factory=dict)
+
+    def _isStale(self, group) -> Optional[object]:
+        now = time.time()
+        if group not in self._cache:
+            return True
+        if now > self._cache[group][0] + 86400:
+            return True
+        print('fresh')
+        return False
+
+    def _save(self, group, obj):
+        now = time.time()
+        self._cache[group] = (now, obj)
+
+    def _get(self, group):
+        print('get')
+        return self._cache[group][1]
+
+    @inlineCallbacks
+    def getStatus(self):
+        if self._isStale('status'):
+            try:
+                statusResp = yield runHg(self.path, ['status'])
+            except Exception as e:
+                status = {'error': repr(e)}
+            else:
+                unknowns = len([row for row in statusResp if row['status'] == '?'])
+                status = {'unknown': unknowns, 'changed': len(statusResp) - unknowns}
+            self._save('status', status)
+        returnValue(self._get('status'))
+
+    @inlineCallbacks
+    def getLatestHgCommit(self):
+        if self._isStale('log'):
+            rows = yield runHg(self.path, ['log', '--limit', '1'])
+            commit = rows[0]
+            sec = commit['date'][0]
+            t = datetime.datetime.fromtimestamp(sec, local)
+            self._save('log', {'email': commit['user'], 't': t.isoformat(), 'message': commit['desc']})
+        returnValue(self._get('log'))
+
+    @inlineCallbacks
+    def getLatestGithubCommit(self):
+        if self._isStale('github'):
+            resp = yield treq.get(f'https://api.github.com/repos/{githubOwner}/{self.path.name}/commits?per_page=1',
+                                  timeout=5,
+                                  headers={
+                                      'User-agent': 'reposync by github.com/drewp',
+                                      'Accept': 'application/vnd.github.v3+json'
+                                  })
+            ret = yield treq.json_content(resp)
+            commit = ret[0]['commit']
+            t = parse(commit['committer']['date']).astimezone(local).isoformat()
+            self._save('github', {'email': commit['committer']['email'], 't': t, 'message': commit['message']})
+        returnValue(self._get('github'))
+
+    @inlineCallbacks
+    def clearGithubMaster(self):
+        '''bang(pts/13):/tmp/reset% git init
+Initialized empty Git repository in /tmp/reset/.git/
+then github set current to a new branch called 'clearing' with https://developer.github.com/v3/repos/#update-a-repository
+bang(pts/13):/tmp/reset% git remote add origin git@github.com:drewp/href.git
+bang(pts/13):/tmp/reset% git push origin :master
+To github.com:drewp/href.git
+ - [deleted]         master
+maybe --set-upstream origin
+bang(pts/13):/tmp/reset% git remote set-branches origin master
+?
+then push
+then github setdefault to master
+then github delete clearing 
+'''
+
+    @inlineCallbacks
+    def pushToGithub(self):
+        if not self.github:
+            raise ValueError
+        yield runHg(self.path, ['bookmark', '--rev', 'default', 'master'])
+        out = yield runHg(self.path, ['push', f'git+ssh://git@github.com/{githubOwner}/{self.path.name}.git'])
+        print(f'out fompushh {out}')
+
+
+class GithubSync(PrettyErrorHandler, cyclone.web.RequestHandler):
+
+    @inlineCallbacks
+    def post(self):
+        try:
+            path = self.get_argument('repo')
+            repo = [r for r in self.settings.repos if str(r.path) == path][0]
+            yield repo.pushToGithub()
+        except Exception:
+            traceback.print_exc()
+            raise
+
+
+class Statuses(cyclone.sse.SSEHandler):
+
+    def update(self, key, data):
+        self.sendEvent(json.dumps({'key': key, 'update': data}).encode('utf8'))
+
+    def bind(self):
+        self.toProcess = self.settings.repos[:]
+        reactor.callLater(0, self.runOne)
+
+    @inlineCallbacks
+    def runOne(self):
+        if not self.toProcess:
+            print('done')
+            return
+        repo = self.toProcess.pop(0)
+
+        try:
+            update = {
+                'path': str(repo.path),
+                'github': repo.github,
+                'status': (yield repo.getStatus()),
+                'hgLatest': (yield repo.getLatestHgCommit())
+            }
+            if repo.github:
+                update['githubLatest'] = (yield repo.getLatestGithubCommit())
+            self.update(str(repo.path), update)
+        except Exception:
+            log.warn(f'not reporting on {repo}')
+            traceback.print_exc()
+        reactor.callLater(0, self.runOne)
+
+
+def main():
+    args = docopt.docopt('''
+Usage:
+  hg_status.py [options]
+
+Options:
+  -v, --verbose  more logging
+''')
+    verboseLogging(args['--verbose'])
+
+    import sys
+    sys.path.append('/usr/lib/python3/dist-packages')
+    import OpenSSL
+
+    yaml = YAML(typ='safe')
+    config = yaml.load(open('config.yaml'))
+    repos = [Repo(Path(row['dir']), row['github']) for row in config['hg_repos']]
+
+    class Application(cyclone.web.Application):
+
+        def __init__(self):
+            handlers = [
+                (r"/()", cyclone.web.StaticFileHandler, {
+                    'path': '.',
+                    'default_filename': 'index.html'
+                }),
+                (r'/build/(bundle\.js)', cyclone.web.StaticFileHandler, {
+                    'path': './build/'
+                }),
+                (r'/status/events', Statuses),
+                (r'/githubSync', GithubSync),
+            ]
+            cyclone.web.Application.__init__(
+                self,
+                handlers,
+                repos=repos,
+                debug=args['--verbose'],
+                template_path='.',
+            )
+
+    reactor.listenTCP(10001, Application())
+    reactor.run()
+
+
+if __name__ == '__main__':
+    main()