~edwargix/git.sr.ht

db41e6d1f630a0ddfbf0807ca73d3d9a81cda222 — Drew DeVault 8 years ago a9288d2
Add basic builds.sr.ht integration
A alembic.ini.example => alembic.ini.example +58 -0
@@ 0,0 1,58 @@
# A generic, single database configuration.

[alembic]
# path to migration scripts
script_location = gitsrht/alembic

# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s

# max length of characters to apply to the
# "slug" field
#truncate_slug_length = 40

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false

sqlalchemy.url = postgres://postgres@localhost/git.sr.ht

# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

A config.ini.example => config.ini.example +58 -0
@@ 0,0 1,58 @@
#
# git.sr.ht config

[server]
#
# Specifies the protocol (usually http or https) meta.sr.ht runs with.
protocol=http
#
# Specifies the domain name meta.sr.ht is running on.
domain=localhost:5001
#
# A secret key to encrypt session cookies with.
secret-key=CHANGEME

[debug]
#
# Address and port to bind the debug server to.
debug-host=0.0.0.0
debug-port=5001

[sr.ht]
#
# Configures the SQLAlchemy connection string for the database.
connection-string=postgresql://postgres@localhost/git.sr.ht
#
# The name of your network of sr.ht-based sites
site-name=sr.ht

[network]
#
# Location of other sites in your network
#
# This isn't a hardcoded list, add or remove entries as you like. The upstream
# sites do know about each other and will omit integrations if you leave out
# the relevant site. Only meta is required.
meta=http://meta.sr.ht.local
git=http://git.sr.ht.local
builds=http://builds.sr.ht.local

[cgit]
remote=http://cgit.local
repos=/var/lib/git/

[git.sr.ht]
git-user=git:git
post-update-script=/usr/bin/git-srht-update-hook

[meta.sr.ht]
#
# Register an OAuth client for meta.sr.ht and fill in these details with it
oauth-client-id=
oauth-client-secret=

[builds.sr.ht]
#
# Fill this in with the oauth client ID builds.sr.ht uses for builds.sr.ht
# integration
oauth-client-id=

M git-srht-update-hook => git-srht-update-hook +22 -27
@@ 5,41 5,36 @@ from srht.database import DbSession
db = DbSession(cfg("sr.ht", "connection-string"))
from gitsrht.types import User, Repository
db.init()
from gitsrht.worker import do_post_update
from configparser import ConfigParser
from datetime import datetime
from pygit2 import Repository as GitRepository
import shlex
import subprocess
import sys

config = ConfigParser()
with open("config") as f:
    config.readfp(f)
op = sys.argv[0]

repo_id = config.get("srht", "repo-id")
if not repo_id:
    sys.exit(0)
repo_id = int(repo_id)
if op == "hooks/post-update":
    refs = sys.argv[1:]

repo = Repository.query.filter(Repository.id == repo_id).first()
if not repo:
    sys.exit(0)
    config = ConfigParser()
    with open("config") as f:
        config.readfp(f)

# Might do this later:
#result = subprocess.run([
#        "git", "ls-tree", "--full-tree", "-r", "-l", shlex.quote(sys.argv[3]), "|",
#        "sort", "-k", "4", "-n", "-r", "|", "head", "-1"
#    ], shell=True)
#
#if result.returncode != 0:
#    sys.exit(0)
#parts = result.stdout.decode().split(' ')
#size = int(parts[3])
#max_size = cfg("git.sr.ht", "max-file-size", default=52428800)
#if size > max_size:
#    print("{} is over the maximum file size ({} MiB).", parts[4], max_size // 1048576)
#    sys.exit(1)
    repo_id = config.get("srht", "repo-id")
    if not repo_id:
        sys.exit(0)
    repo_id = int(repo_id)

repo.updated = datetime.utcnow()
db.session.commit()
    repo = Repository.query.filter(Repository.id == repo_id).first()
    if not repo:
        sys.exit(0)

# TODO: fire webhooks
    repo.updated = datetime.utcnow()
    db.session.commit()

    git_repo = GitRepository(repo.path)
    for ref in refs:
        target = git_repo.lookup_reference(ref).target
        do_post_update(repo, git_repo, target)

M gitsrht/app.py => gitsrht/app.py +7 -3
@@ 27,10 27,14 @@ try:
except:
    pass

meta_sr_ht = cfg("network", "meta")
meta_client_id = cfg("meta.sr.ht", "oauth-client-id")
builds_sr_ht = cfg("builds.sr.ht", "oauth-client-id")

def oauth_url(return_to):
    return "{}/oauth/authorize?client_id={}&scopes=profile,keys&state={}".format(
        cfg("network", "meta"),
        cfg("meta.sr.ht", "oauth-client-id"),
    return "{}/oauth/authorize?client_id={}&scopes=profile,keys{}&state={}".format(
        meta_sr_ht, meta_client_id,
        "," + builds_sr_ht + "/jobs:write" if builds_sr_ht else "",
        urllib.parse.quote_plus(return_to))

from gitsrht.blueprints.auth import auth

M gitsrht/blueprints/auth.py => gitsrht/blueprints/auth.py +4 -1
@@ 3,6 3,7 @@ from flask_login import login_user, logout_user
from sqlalchemy import or_
from srht.config import cfg
from srht.flask import DATE_FORMAT
from srht.oauth import OAuthScope
from srht.database import db
from gitsrht.types import User
from datetime import datetime


@@ 24,7 25,8 @@ def oauth_callback():
    exchange = request.args.get("exchange")
    scopes = request.args.get("scopes")
    state = request.args.get("state")
    if scopes != "profile:read,keys:read":
    _scopes = [OAuthScope(s) for s in scopes.split(",")]
    if not OAuthScope("profile:read") in _scopes or not OAuthScope("keys:read") in _scopes:
        return render_template("oauth-error.html",
            details="git.sr.ht requires profile and key access at a mininum to function correctly. " +
                "To use git.sr.ht, try again and do not untick these permissions.")


@@ 65,6 67,7 @@ def oauth_callback():
    user.paid = json.get("paid")
    user.oauth_token = token
    user.oauth_token_expires = expires
    user.oauth_token_scopes = scopes
    db.session.commit()

    login_user(user)

M gitsrht/templates/index.html => gitsrht/templates/index.html +1 -1
@@ 9,7 9,7 @@
        Welcome back, {{ current_user.username }}!
      </p>
      <p>
        <a href="/create">Create repository »</a>
        <a href="/create">Create new repository »</a>
      </p>
      <div class="alert alert-danger">
        Notice: this site is a work in progress.

M gitsrht/types/user.py => gitsrht/types/user.py +1 -0
@@ 10,6 10,7 @@ class User(Base):
    updated = sa.Column(sa.DateTime, nullable=False)
    oauth_token = sa.Column(sa.String(256), nullable=False)
    oauth_token_expires = sa.Column(sa.DateTime, nullable=False)
    oauth_token_scopes = sa.Column(sa.String, nullable=False, default="")
    email = sa.Column(sa.String(256), nullable=False)
    paid = sa.Column(sa.Boolean, nullable=False)


A gitsrht/worker.py => gitsrht/worker.py +71 -0
@@ 0,0 1,71 @@
from srht.config import cfg, load_config, loaded
if not loaded():
    load_config("git")
from srht.database import DbSession, db
if not hasattr(db, "session"):
    db = DbSession(cfg("sr.ht", "connection-string"))
    import gitsrht.types
    db.init()

import requests
from celery import Celery
from pygit2 import Commit
from srht.oauth import OAuthScope
#from buildsrht.manifest import Manifest

worker = Celery('git', broker=cfg("git.sr.ht", "redis"))
builds_sr_ht = cfg("network", "builds")
builds_client_id = cfg("builds.sr.ht", "oauth-client-id")
git_sr_ht = cfg("server", "protocol") + "://" + cfg("server", "domain")

@worker.task
def do_webhook(url, payload, headers=None):
    r = requests.post(url, json=payload, headers=headers)
    # TODO: Store the response somewhere I guess
    print(r.status_code)
    try:
        print(r.json())
    except:
        pass

def do_post_update(repo, git_repo, ref):
    commit = git_repo.get(ref)
    if not commit or not isinstance(commit, Commit):
        return

    # builds.sr.ht
    if builds_sr_ht:
        manifest = None
        if ".build.yml" in commit.tree:
            manifest = commit.tree[".build.yml"]
        if ".build.yaml" in commit.tree:
            manifest = commit.tree[".build.yaml"]
        # TODO: More complex build manifests
        if manifest:
            manifest = git_repo.get(manifest.id)
            manifest = manifest.data.decode()
            # TODO: parse manifest and print errors here, and update the repo URL to match the ref
            #manifest = Manifest(manifest)
            token = repo.owner.oauth_token
            scopes = repo.owner.oauth_token_scopes
            scopes = [OAuthScope(s) for s in scopes.split(",")]
            if not any(s for s in scopes
                    if s.client_id == builds_client_id and s.access == 'write'):
                print("Warning: log out and back in on the website to enable builds integration")
            else:
                do_webhook.delay(builds_sr_ht + "/api/jobs", {
                    "manifest": manifest,
                    # TODO: orgs
                    "tags": ["~" + repo.owner.username, repo.name],
                    "note": "[{}]({}) {} <{}>".format(
                        # TODO: cgit replacement
                        str(commit.id)[:7],
                        "{}/{}/{}/commit?id={}".format(
                            git_sr_ht,
                            "~" + repo.owner.username,
                            repo.name,
                            str(commit.id)),
                        commit.author.name,
                        commit.author.email
                    )
                }, { "Authorization": "token " + token })

M scripts/symlink-update-hook.py => scripts/symlink-update-hook.py +13 -5
@@ 9,9 9,17 @@ import os

post_update = cfg("git.sr.ht", "post-update-script")

def migrate(path, link):
    if not os.path.exists(path) \
            or not os.path.islink(path) \
            or os.readlink(path) != link:
        if os.path.exists(path):
            os.remove(path)
        os.symlink(link, path)
        return True
    return False

for repo in Repository.query.all():
    hook = os.path.join(repo.path, "hooks", "update")
    if not os.path.islink(hook) or os.readlink(hook) != post_update:
        print("Migrating {}".format(repo.name))
        os.remove(hook)
        os.symlink(post_update, hook)
    if migrate(os.path.join(repo.path, "hooks", "update"), post_update) \
        or migrate(os.path.join(repo.path, "hooks", "post-update"), post_update):
        print("Migrated {}".format(repo.name))