275 lines
9.8 KiB
Python
275 lines
9.8 KiB
Python
import base64
|
|
import json
|
|
import pygit2
|
|
from flask import Blueprint, current_app, request, send_file, abort
|
|
from gitsrht.blueprints.repo import lookup_ref, collect_refs
|
|
from gitsrht.types import Artifact
|
|
from gitsrht.git import Repository as GitRepository, commit_time, annotate_tree
|
|
from gitsrht.git import get_log
|
|
from gitsrht.repos import upload_artifact
|
|
from gitsrht.webhooks import RepoWebhook
|
|
from io import BytesIO
|
|
from itertools import groupby
|
|
from gitsrht.access import UserAccess
|
|
from gitsrht.blueprints.api import get_user, get_repo
|
|
from srht.api import paginated_response
|
|
from srht.database import db
|
|
from srht.oauth import current_token, oauth
|
|
from srht.validation import Validation
|
|
|
|
porcelain = Blueprint("api_porcelain", __name__)
|
|
|
|
# See also gitsrht-update-hook/types.go
|
|
def commit_to_dict(c):
|
|
return {
|
|
"id": str(c.id),
|
|
"short_id": c.short_id,
|
|
"author": {
|
|
"email": c.author.email,
|
|
"name": c.author.name,
|
|
},
|
|
"committer": {
|
|
"email": c.committer.email,
|
|
"name": c.committer.name,
|
|
},
|
|
"timestamp": commit_time(c),
|
|
"message": c.message,
|
|
"tree": str(c.tree_id),
|
|
"parents": [str(p.id) for p in c.parents],
|
|
"signature": {
|
|
"signature": base64.b64encode(c.gpg_signature[0]).decode(),
|
|
"data": base64.b64encode(c.gpg_signature[1]).decode(),
|
|
} if c.gpg_signature[0] else None
|
|
}
|
|
|
|
def tree_to_dict(t):
|
|
return {
|
|
"id": str(t.id),
|
|
"short_id": t.short_id,
|
|
"entries": [
|
|
{
|
|
"name": e.raw_name.decode("utf-8", "replace"),
|
|
"id": str(e.id),
|
|
"type": (e.type_str if hasattr(e, "type_str") else e.type),
|
|
"mode": e.filemode,
|
|
} for e in t
|
|
]
|
|
}
|
|
|
|
def ref_to_dict(artifacts, ref):
|
|
target = ref.target.hex
|
|
return {
|
|
"target": target,
|
|
"name": ref.raw_name.decode("utf-8", "replace"),
|
|
"artifacts": [a.to_dict() for a in artifacts.get(target, [])],
|
|
}
|
|
|
|
@porcelain.route("/api/repos/<reponame>/refs", defaults={"username": None})
|
|
@porcelain.route("/api/<username>/repos/<reponame>/refs")
|
|
@oauth("data:read")
|
|
def repo_refs_GET(username, reponame):
|
|
user = get_user(username)
|
|
repo = get_repo(user, reponame)
|
|
|
|
with GitRepository(repo.path) as git_repo:
|
|
# TODO: pagination
|
|
refs = list(git_repo.raw_listall_references())
|
|
targets = [git_repo.references[ref].target.hex for ref in refs]
|
|
artifacts = (Artifact.query
|
|
.filter(Artifact.user_id == repo.owner_id)
|
|
.filter(Artifact.repo_id == repo.id)
|
|
.filter(Artifact.commit.in_(targets))).all()
|
|
artifacts = {
|
|
key: list(value) for key, value in
|
|
groupby(artifacts, key=lambda a: a.commit)
|
|
}
|
|
return {
|
|
"next": None,
|
|
"results": [
|
|
ref_to_dict(artifacts, git_repo.references[ref]) for ref in refs
|
|
],
|
|
"total": len(refs),
|
|
"results_per_page": len(refs),
|
|
}
|
|
|
|
@porcelain.route("/api/repos/<reponame>/artifacts/<path:refname>", defaults={"username": None}, methods=["POST"])
|
|
@porcelain.route("/api/<username>/repos/<reponame>/artifacts/<path:refname>", methods=["POST"])
|
|
@oauth("data:write")
|
|
def repo_refs_by_name_POST(username, reponame, refname):
|
|
user = get_user(username)
|
|
repo = get_repo(user, reponame, needs=UserAccess.manage)
|
|
|
|
with GitRepository(repo.path) as git_repo:
|
|
try:
|
|
tag = git_repo.revparse_single(refname)
|
|
except KeyError:
|
|
abort(404)
|
|
except ValueError:
|
|
abort(404)
|
|
if isinstance(tag, pygit2.Commit):
|
|
target = tag.oid.hex
|
|
else:
|
|
target = tag.target.hex
|
|
valid = Validation(request)
|
|
f = request.files.get("file")
|
|
valid.expect(f, "File is required", field="file")
|
|
if not valid.ok:
|
|
return valid.response
|
|
artifact = upload_artifact(valid, repo, target, f, f.filename)
|
|
if not valid.ok:
|
|
return valid.response
|
|
db.session.commit()
|
|
return artifact.to_dict()
|
|
|
|
# dear god, this routing
|
|
@porcelain.route("/api/repos/<reponame>/log",
|
|
defaults={"username": None, "ref": None, "path": ""})
|
|
@porcelain.route("/api/repos/<reponame>/log/<path:ref>",
|
|
defaults={"username": None, "path": ""})
|
|
@porcelain.route("/api/repos/<reponame>/log/<ref>/<path:path>",
|
|
defaults={"username": None})
|
|
@porcelain.route("/api/<username>/repos/<reponame>/log",
|
|
defaults={"ref": None, "path": ""})
|
|
@porcelain.route("/api/<username>/repos/<reponame>/log/<path:ref>",
|
|
defaults={"path": ""})
|
|
@porcelain.route("/api/<username>/repos/<reponame>/log/<ref>/<path:path>")
|
|
@oauth("data:read")
|
|
def repo_commits_GET(username, reponame, ref, path):
|
|
user = get_user(username)
|
|
repo = get_repo(user, reponame)
|
|
|
|
commits_per_page=50
|
|
with GitRepository(repo.path) as git_repo:
|
|
if git_repo.is_empty:
|
|
return { "next": None, "results": [],
|
|
"total": 0, "results_per_page": commits_per_page }
|
|
commit, ref, path = lookup_ref(git_repo, ref, path)
|
|
start = request.args.get("start")
|
|
if start:
|
|
commit = git_repo.get(start)
|
|
commits = get_log(git_repo, commit, commits_per_page=commits_per_page)
|
|
next_id = None
|
|
if len(commits) > commits_per_page:
|
|
next_id = str(commits[-1].id)
|
|
return {
|
|
"next": next_id,
|
|
"results": [commit_to_dict(c) for c in commits],
|
|
# TODO: Track total commits per repo per branch
|
|
"total": -1,
|
|
"results_per_page": commits_per_page
|
|
}
|
|
|
|
@porcelain.route("/api/repos/<reponame>/tree",
|
|
defaults={"username": None, "ref": None, "path": ""})
|
|
@porcelain.route("/api/repos/<reponame>/tree/<path:ref>",
|
|
defaults={"username": None, "path": ""})
|
|
@porcelain.route("/api/repos/<reponame>/tree/<ref>/<path:path>",
|
|
defaults={"username": None})
|
|
@porcelain.route("/api/<username>/repos/<reponame>/tree",
|
|
defaults={"ref": None, "path": ""})
|
|
@porcelain.route("/api/<username>/repos/<reponame>/tree/<path:ref>",
|
|
defaults={"path": ""})
|
|
@porcelain.route("/api/<username>/repos/<reponame>/tree/<ref>/<path:path>")
|
|
@oauth("data:read")
|
|
def repo_tree_GET(username, reponame, ref, path):
|
|
user = get_user(username)
|
|
repo = get_repo(user, reponame)
|
|
|
|
with GitRepository(repo.path) as git_repo:
|
|
commit, ref, path = lookup_ref(git_repo, ref, path)
|
|
if isinstance(commit, pygit2.Commit):
|
|
tree = commit.tree
|
|
elif isinstance(commit, pygit2.Tree):
|
|
tree = commit
|
|
else:
|
|
abort(404)
|
|
|
|
path = [p for p in path.split("/") if p]
|
|
for part in path:
|
|
if not tree or part not in tree:
|
|
abort(404)
|
|
entry = tree[part]
|
|
etype = (entry.type_str
|
|
if hasattr(entry, "type_str") else entry.type)
|
|
if etype == "blob":
|
|
abort(404)
|
|
tree = git_repo.get(entry.id)
|
|
if not tree:
|
|
abort(404)
|
|
return tree_to_dict(tree)
|
|
|
|
@porcelain.route("/api/repos/<reponame>/blob/<path:ref>",
|
|
defaults={"username": None, "path": ""})
|
|
@porcelain.route("/api/repos/<reponame>/blob/<ref>/<path:path>",
|
|
defaults={"username": None})
|
|
@porcelain.route("/api/<username>/blob/<reponame>/blob/<path:ref>",
|
|
defaults={"path": ""})
|
|
@porcelain.route("/api/<username>/repos/<reponame>/blob/<ref>/<path:path>")
|
|
@oauth("data:read")
|
|
def repo_blob_GET(username, reponame, ref, path):
|
|
user = get_user(username)
|
|
repo = get_repo(user, reponame)
|
|
|
|
if "/" in ref:
|
|
ref, _, path = ref.partition("/")
|
|
|
|
with GitRepository(repo.path) as git_repo:
|
|
# lookup_ref will cycle through the path to separate
|
|
# the actual ref from the actual path
|
|
commit, ref, path = lookup_ref(git_repo, ref, path)
|
|
if not commit:
|
|
abort(404)
|
|
|
|
entry = None
|
|
if isinstance(commit, pygit2.Blob):
|
|
blob = commit
|
|
else:
|
|
blob = None
|
|
tree = commit.tree
|
|
path = path.split("/")
|
|
for part in path:
|
|
if part == "":
|
|
continue
|
|
if part not in tree:
|
|
abort(404)
|
|
entry = tree[part]
|
|
etype = (entry.type_str
|
|
if hasattr(entry, "type_str") else entry.type)
|
|
if etype == "blob":
|
|
tree = annotate_tree(git_repo, tree, commit)
|
|
commit = next(e.commit for e in tree if e.name == entry.name)
|
|
blob = git_repo.get(entry.id)
|
|
break
|
|
tree = git_repo.get(entry.id)
|
|
if not blob:
|
|
abort(404)
|
|
|
|
attachment_filename = entry.name if entry else None
|
|
if not attachment_filename:
|
|
if path:
|
|
attachment_filename = path.split("/")[-1]
|
|
else:
|
|
attachment_filename = blob.id.hex + ".bin"
|
|
|
|
return send_file(BytesIO(blob.data),
|
|
as_attachment=blob.is_binary,
|
|
download_name=attachment_filename,
|
|
mimetype="text/plain" if not blob.is_binary
|
|
else "application/x-octet-stream")
|
|
|
|
|
|
def _webhook_filters(query, username, reponame):
|
|
user = get_user(username)
|
|
repo = get_repo(user, reponame)
|
|
return query.filter(RepoWebhook.Subscription.repo_id == repo.id)
|
|
|
|
def _webhook_create(sub, valid, username, reponame):
|
|
user = get_user(username)
|
|
repo = get_repo(user, reponame)
|
|
sub.repo_id = repo.id
|
|
sub.sync = valid.optional("sync", cls=bool, default=False)
|
|
return sub
|
|
|
|
RepoWebhook.api_routes(porcelain, "/api/<username>/repos/<reponame>",
|
|
filters=_webhook_filters, create=_webhook_create)
|