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