Mercurial > code > home > repos > reposync
changeset 4:f714a6a7842c
start new web view for hgand github syncing
author | drewp@bigasterisk.com |
---|---|
date | Fri, 24 Jul 2020 14:42:08 -0700 |
parents | 4077903a9520 |
children | 7cbabd0e9226 |
files | .style.yapf .vscode/settings.json Dockerfile deploy.yaml entrypoint.sh hg_status.py index.html quietsync requirements.txt root-hgrc skaffold.yaml sync.py |
diffstat | 11 files changed, 470 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.style.yapf Fri Jul 24 14:42:08 2020 -0700 @@ -0,0 +1,3 @@ +[style] +based_on_style = google +column_limit = 130
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.vscode/settings.json Fri Jul 24 14:42:08 2020 -0700 @@ -0,0 +1,4 @@ +{ + "python.pythonPath": "/home/drewp/.venvs/reposync/bin/python", + "python.formatting.provider": "yapf" +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Dockerfile Fri Jul 24 14:42:08 2020 -0700 @@ -0,0 +1,27 @@ +FROM bang5:5000/base_x86 + +WORKDIR /opt + +RUN apt-get install -y python3.8 libpython3.8-dev python3-openssl python3-cffi-backend +RUN python3.8 -m pip install mercurial + +RUN python3.8 -m pip install -U pip +COPY requirements.txt ./ +RUN python3.8 -m pip install --index-url https://projects.bigasterisk.com/ --extra-index-url https://pypi.org/simple -r requirements.txt +RUN python3.8 -m pip install -U 'https://github.com/drewp/cyclone/archive/python3.zip?v3' +RUN python3.8 -m pip install -U cffi +RUN python3.8 -m pip install 'https://foss.heptapod.net/mercurial/hg-git/-/archive/branch/default/hg-git-branch-default.zip' +RUN groupadd --gid 1000 drewp && useradd --uid 501 --gid 1000 drewp + +COPY root-hgrc /root/.hgrc +COPY entrypoint.sh config.yaml hg_status.py index.html ./ +COPY dot-ssh/* /root/.ssh/ +#USER drewp + +ENV TZ=America/Los_Angeles +ENV LANG=en_US.UTF-8 + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + + +CMD ["/bin/sh", "entrypoint.sh"] \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/deploy.yaml Fri Jul 24 14:42:08 2020 -0700 @@ -0,0 +1,45 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: reposync +spec: + replicas: 1 + selector: + matchLabels: + app: reposync + template: + metadata: + labels: + app: reposync + spec: + containers: + - name: reposync + image: bang5:5000/reposync_image + imagePullPolicy: "Always" + securityContext: {capabilities: {add: [SYS_PTRACE]}} + ports: + - containerPort: 10001 + volumeMounts: + - {name: my, mountPath: /my} + volumes: + - { name: my, persistentVolumeClaim: { claimName: my } } + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: "kubernetes.io/hostname" + operator: In + values: ["bang"] +--- +apiVersion: v1 +kind: Service +metadata: + name: reposync +spec: + ports: + - {port: 80, targetPort: 10001, name: http} + selector: + app: reposync + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/entrypoint.sh Fri Jul 24 14:42:08 2020 -0700 @@ -0,0 +1,5 @@ +#!/bin/sh + +echo "nameserver 10.43.0.10 8.8.8.8" > /etc/resolv.conf + +exec python3.8 hg_status.py -v \ No newline at end of file
--- /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()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/index.html Fri Jul 24 14:42:08 2020 -0700 @@ -0,0 +1,124 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>reposync</title> + <style> + body { + background: #1f1f1f; + color: white; + font-family: monospace; + } + th { + text-align: left; + white-space: nowrap; + } + td.github { + white-space: nowrap; + opacity: 0.5; + } + td.github.on { + opacity: 1; + } + span.check { + font-size: 200%; + } + </style> + </head> + <body> + <h1>repo statuses</h1> + + <script type="module"> + import Litedom from "https://bigasterisk.com/lib/litedom/0.12.1/litedom.es.js"; + Litedom({ + tagName: "bigast-loginbar", + template: `<span>{this.responseHtml}</span>`, + data: { responseHtml: "..." }, + async created() { + const resp = await fetch("/_loginBar"); + this.data.responseHtml = await resp.text(); + }, + }); + </script> + + <script type="module"> + import Litedom from "https://bigasterisk.com/lib/litedom/0.12.1/litedom.es.js"; + const repos = [{ dir: "dir1", summary: "ok" }]; + Litedom({ + tagName: "reposync-top", + template: `<div id="top"> + <table> + <tr :for="repo in this.repos"> + <th>{repo.path}</th> + <td> {repo.status.error || ''} + <span :if="repo.status.changed==0 && repo.status.unknown==0"> + clean + </span> + <span :else> + changed {repo.status.changed}, unknown {repo.status.unknown} + </span> + </td> + <td :class="github: true; on: repo.github"> + <span class="check" :if="repo.github">☑</span> + <span class="check" :else>☐</span> github + <span :if="repo.github"> + <table style="display: inline-block"> + <tr><td>github latest</td><td>{repo.githubLatest.t}</td></tr> + <tr><td>home latest</td><td>{repo.hgLatest.t}</td></tr> + </table> + </span> + </td> + </tr> + </table> + </div>`, + data: { repos: [] }, + created() { + const statuses = new EventSource("status/events"); + statuses.addEventListener("message", (ev) => { + const update = JSON.parse(ev.data); + update.update.repoDir = update.key; + + var found = false; + this.data.repos.forEach((r) => { + if (r.repoDir == update.key) { + found = true; + Object.assign(r, update.update); + } + }); + if (!found) { + this.data.repos.push(update.update); + this.data.repos.sort((a, b) => (a.repoDir > b.repoDir ? 1 : -1)); + } + }); + }, + }); + </script> + + <reposync-top></reposync-top> + <bigast-loginbar></bigast-loginbar> + <pre> + repo1 clean synced to github github up to date [sync now] + dirty [msg____] [commit] + + repo1 is a link to my hgserve + + GET /status/events + + GET /recent?repo=/my/dir + GET /recent?all + GET /recent/events?repo=/my/dir|all + + POST /githubSync?all + POST /githubSync?repo=/my/dir + + GET /homepage -> a CE table of all changes using /recent + + each event are a json message: + key: string + update: replace the data for that key + Here, keys are repo paths. + + </pre> + </body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/quietsync Fri Jul 24 14:42:08 2020 -0700 @@ -0,0 +1,11 @@ +#!/bin/zsh +eval `keychain --quiet --eval id_ecdsa` +cd `dirname $0` +bin/python sync.py |& egrep -v "^(Running....(darcs|git)|INFO|Nothing to pull|Comparing final state|Contents match|Everything up)" | egrep -v "will be deprecated" | egrep -v "^$" + +for repo (traps blender-addons streamed-graph bigast-front-door rdf_elements webfilter) { + cd /my/repo/${repo}/ + hg up + hg bookmark -r default master + hg push git+ssh://git@github.com/drewp/${repo}.git --quiet +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/requirements.txt Fri Jul 24 14:42:08 2020 -0700 @@ -0,0 +1,13 @@ +cffi +cyclone +docopt +prometheus_client==0.7.1 +pyopenssl==19.0.0 +python-dateutil +requests +ruamel.yaml +treq +twisted[tls]==19.2.1 +tzlocal +cycloneerr==0.4.0 +standardservice==0.6.0 \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/root-hgrc Fri Jul 24 14:42:08 2020 -0700 @@ -0,0 +1,6 @@ +[trusted] +users = drewp + +[extensions] +hgext.bookmarks = +hggit = /usr/local/lib/python3.8/dist-packages/hggit \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/skaffold.yaml Fri Jul 24 14:42:08 2020 -0700 @@ -0,0 +1,18 @@ +apiVersion: skaffold/v2beta5 +kind: Config +metadata: + name: reposync +build: + tagPolicy: + dateTime: + format: "2006-01-02_15-04-05" + timezone: "Local" + artifacts: + - image: bang5:5000/reposync_image + sync: # files that could be patched sans python restart + infer: + - index.html +deploy: + kubectl: + manifests: + - deploy.yaml