M gitsrht/app.py => gitsrht/app.py +4 -1
@@ 23,13 23,16 @@ class GitApp(ScmSrhtFlask):
repository_class=Repository, user_class=User,
repo_api=GitRepoApi(), oauth_service=oauth_service)
- from gitsrht.blueprints.api import plumbing, porcelain
+ from gitsrht.blueprints.api import register_api
+ from gitsrht.blueprints.api.plumbing import plumbing
+ from gitsrht.blueprints.api.porcelain import porcelain
from gitsrht.blueprints.artifacts import artifacts
from gitsrht.blueprints.email import mail
from gitsrht.blueprints.manage import manage
from gitsrht.blueprints.repo import repo
from srht.graphql import gql_blueprint
+ register_api(self)
self.register_blueprint(plumbing)
self.register_blueprint(porcelain)
self.register_blueprint(mail)
M gitsrht/blueprints/api/__init__.py => gitsrht/blueprints/api/__init__.py +48 -2
@@ 1,2 1,48 @@
-from gitsrht.blueprints.api.plumbing import plumbing
-from gitsrht.blueprints.api.porcelain import porcelain
+import pkg_resources
+from flask import abort
+from scmsrht.access import UserAccess, get_access
+from scmsrht.types import Repository, User
+from srht.flask import csrf_bypass
+from srht.oauth import current_token, oauth
+
+def get_user(username):
+ user = None
+ if username == None:
+ user = current_token.user
+ elif username.startswith("~"):
+ user = User.query.filter(User.username == username[1:]).one_or_none()
+ if not user:
+ abort(404)
+ return user
+
+def get_repo(owner, reponame, needs=UserAccess.read):
+ repo = (Repository.query
+ .filter(Repository.owner_id == owner.id)
+ .filter(Repository.name == reponame)).one_or_none()
+ if not repo:
+ abort(404)
+ access = get_access(repo, user=current_token.user)
+ if needs not in access:
+ abort(403)
+ return repo
+
+def register_api(app):
+ from gitsrht.blueprints.api.info import info
+
+ app.register_blueprint(info)
+ csrf_bypass(info)
+
+ @app.route("/api/version")
+ def version():
+ try:
+ dist = pkg_resources.get_distribution("gitsrht")
+ return { "version": dist.version }
+ except:
+ return { "version": "unknown" }
+
+ @app.route("/api/user/<username>")
+ @app.route("/api/user", defaults={"username": None})
+ @oauth(None)
+ def user_GET(username):
+ user = get_user(username)
+ return user.to_dict()
A gitsrht/blueprints/api/info.py => gitsrht/blueprints/api/info.py +158 -0
@@ 0,0 1,158 @@
+from flask import Blueprint, Response, current_app, request
+from scmsrht.access import UserAccess
+from scmsrht.repos import RepoVisibility
+from scmsrht.types import Access, Repository, User
+from gitsrht.blueprints.api import get_user, get_repo
+from srht.api import paginated_response
+from srht.database import db
+from srht.graphql import exec_gql
+from srht.oauth import current_token, oauth
+from srht.validation import Validation
+from sqlalchemy import and_, or_
+
+info = Blueprint("api_info", __name__)
+
+@info.route("/api/repos", defaults={"username": None})
+@info.route("/api/<username>/repos")
+@oauth("info:read")
+def repos_by_user_GET(username):
+ user = get_user(username)
+ repos = (Repository.query
+ .filter(Repository.owner_id == user.id)
+ .filter(Repository.visibility != RepoVisibility.autocreated))
+ if user.id != current_token.user_id:
+ repos = (repos
+ .outerjoin(Access._get_current_object(),
+ Access.repo_id == Repository.id)
+ .filter(or_(
+ Access.user_id == current_token.user_id,
+ and_(
+ Repository.visibility == RepoVisibility.public,
+ Access.id.is_(None))
+ )))
+ return paginated_response(Repository.id, repos)
+
+@info.route("/api/repos", methods=["POST"])
+@oauth("info:write")
+def repos_POST():
+ valid = Validation(request)
+ user = current_token.user
+ resp = current_app.repo_api.create_repo(valid, user)
+ if not valid.ok:
+ return valid.response
+ return resp, 201
+
+@info.route("/api/repos/<reponame>", defaults={"username": None})
+@info.route("/api/<username>/repos/<reponame>")
+@oauth("info:read")
+def repos_by_name_GET(username, reponame):
+ user = get_user(username)
+ repo = get_repo(user, reponame)
+ return repo.to_dict()
+
+@info.route("/api/repos/<reponame>", methods=["PUT"])
+@oauth("info:write")
+def repos_by_name_PUT(reponame):
+ valid = Validation(request)
+ user = current_token.user
+ repo = get_repo(user, reponame, needs=UserAccess.manage)
+
+ rewrite = lambda value: None if value == "" else value
+ input = {
+ key: rewrite(valid.source[key]) for key in [
+ "name", "description", "visibility",
+ ] if valid.source.get(key) is not None
+ }
+
+ # Visibility must be uppercase
+ if "visibility" in input:
+ input["visibility"] = input["visibility"].upper()
+
+ resp = exec_gql(current_app.site, """
+ mutation UpdateRepository($id: Int!, $input: RepoInput!) {
+ updateRepository(id: $id, input: $input) {
+ id
+ created
+ updated
+ name
+ owner {
+ canonicalName
+ ... on User {
+ name: username
+ }
+ }
+ description
+ visibility
+ }
+ }
+ """, user=user, valid=valid, id=repo.id, input=input)
+
+ if not valid.ok:
+ return valid.response
+
+ resp = resp["updateRepository"]
+ # Convert visibility back to lowercase
+ resp["visibility"] = resp["visibility"].lower()
+ return resp
+
+@info.route("/api/repos/<reponame>", methods=["DELETE"])
+@oauth("info:write")
+def repos_by_name_DELETE(reponame):
+ user = current_token.user
+ repo = get_repo(user, reponame, needs=UserAccess.manage)
+ repo_id = repo.id
+ current_app.repo_api.delete_repo(repo, user)
+ return {}, 204
+
+@info.route("/api/repos/<reponame>/readme", defaults={"username": None})
+@info.route("/api/<username>/repos/<reponame>/readme")
+@oauth("info:read")
+def repos_by_name_readme_GET(username, reponame):
+ user = get_user(username)
+ repo = get_repo(user, reponame)
+
+ if repo.readme is None:
+ return {}, 404
+ else:
+ return Response(repo.readme, mimetype="text/plain")
+
+@info.route("/api/repos/<reponame>/readme", methods=["PUT"])
+@oauth("info:write")
+def repos_by_name_readme_PUT(reponame):
+ user = current_token.user
+ repo = get_repo(user, reponame, needs=UserAccess.manage)
+
+ valid = Validation(request)
+ if request.content_type != 'text/html':
+ return valid.error("not text/html", field="content-type")
+
+ readme = None
+ try:
+ readme = request.data.decode("utf-8")
+ except:
+ return valid.error("README files must be UTF-8 encoded", field="body")
+
+ resp = exec_gql(current_app.site, """
+ mutation UpdateRepository($id: Int!, $readme: String!) {
+ updateRepository(id: $id, input: {readme: $readme}) { id }
+ }
+ """, user=user, valid=valid, id=repo.id, readme=readme)
+
+ if not valid.ok:
+ return valid.response
+ return {}, 204
+
+@info.route("/api/repos/<reponame>/readme", methods=["DELETE"])
+@oauth("info:write")
+def repos_by_name_readme_DELETE(reponame):
+ user = current_token.user
+ repo = get_repo(user, reponame, needs=UserAccess.manage)
+ valid = Validation(request)
+ exec_gql(current_app.site, """
+ mutation UpdateRepository($id: Int!) {
+ updateRepository(id: $id, input: {readme: null}) { id }
+ }
+ """, user=user, valid=valid, id=repo.id)
+ if not valid.ok:
+ return valid.response
+ return {}, 204
M gitsrht/blueprints/api/plumbing.py => gitsrht/blueprints/api/plumbing.py +1 -1
@@ 3,7 3,7 @@ import binascii
import pygit2
from flask import Blueprint, Response, abort, request
from gitsrht.git import Repository as GitRepository
-from scmsrht.blueprints.api import get_user, get_repo
+from gitsrht.blueprints.api import get_user, get_repo
from srht.oauth import oauth
plumbing = Blueprint("api_plumbing", __name__)
M gitsrht/blueprints/api/porcelain.py => gitsrht/blueprints/api/porcelain.py +1 -1
@@ 11,7 11,7 @@ from gitsrht.webhooks import RepoWebhook
from io import BytesIO
from itertools import groupby
from scmsrht.access import UserAccess
-from scmsrht.blueprints.api import get_user, get_repo
+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
M gitsrht/blueprints/manage.py => gitsrht/blueprints/manage.py +227 -30
@@ 1,47 1,244 @@
import pygit2
-from flask import Blueprint, request, render_template
+from flask import Blueprint, current_app, request, render_template, abort
from flask import redirect, url_for
from gitsrht.git import Repository as GitRepository
+from srht.config import cfg
from srht.database import db
-from srht.oauth import loginrequired
+from srht.flask import session
+from srht.graphql import exec_gql, GraphQLError
+from srht.oauth import current_user, loginrequired, UserType
from srht.validation import Validation
from scmsrht.access import check_access, UserAccess
+from scmsrht.repos.access import AccessMode
from scmsrht.repos.redirect import BaseRedirectMixin
from scmsrht.repos.repository import RepoVisibility
-from scmsrht.webhooks import UserWebhook
+from scmsrht.types import Access, User
+import shutil
+import os
-manage = Blueprint('manage_git', __name__)
+manage = Blueprint('manage', __name__)
-@manage.route("/<owner_name>/<repo_name>/settings/info_git", methods=["POST"])
+@manage.route("/create")
@loginrequired
-def settings_info_git_POST(owner_name, repo_name):
+def create_GET():
+ another = request.args.get("another")
+ name = request.args.get("name")
+ return render_template("create.html", another=another, repo_name=name)
+
+@manage.route("/create", methods=["POST"])
+@loginrequired
+def create_POST():
+ if not current_app.repo_api:
+ abort(501)
+ valid = Validation(request)
+ resp = current_app.repo_api.create_repo(valid)
+ if not valid.ok:
+ return render_template("create.html", **valid.kwargs)
+
+ another = valid.optional("another")
+ if another == "on":
+ return redirect("/create?another")
+ else:
+ return redirect(url_for("repo.summary",
+ owner=current_user.canonical_name, repo=resp["name"]))
+
+@manage.route("/clone")
+@loginrequired
+def clone():
+ another = request.args.get("another")
+ return render_template("clone.html", another=another, visibility="UNLISTED")
+
+@manage.route("/clone", methods=["POST"])
+@loginrequired
+def clone_POST():
+ if not current_app.repo_api:
+ abort(501)
+ valid = Validation(request)
+ resp = current_app.repo_api.clone_repo(valid)
+ if not valid.ok:
+ return render_template("clone.html", **valid.kwargs)
+ return redirect(url_for("repo.summary",
+ owner=current_user.canonical_name, repo=resp["name"]))
+
+@manage.route("/<owner_name>/<repo_name>/settings/info")
+@loginrequired
+def settings_info(owner_name, repo_name):
+ owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
+ if isinstance(repo, BaseRedirectMixin):
+ return redirect(url_for(".settings_info",
+ owner_name=owner_name, repo_name=repo.new_repo.name))
+ return render_template("settings_info.html", owner=owner, repo=repo)
+
+@manage.route("/<owner_name>/<repo_name>/settings/info", methods=["POST"])
+@loginrequired
+def settings_info_POST(owner_name, repo_name):
owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
if isinstance(repo, BaseRedirectMixin):
repo = repo.new_repo
+
+ # TODO: GraphQL mutation to set default branch name
valid = Validation(request)
- desc = valid.optional("description", default=repo.description)
- visibility = valid.optional("visibility",
- cls=RepoVisibility,
- default=repo.visibility)
branch = valid.optional("default_branch_name")
- with GitRepository(repo.path) as git_repo:
- new_default_branch = None
- if branch:
- try:
- new_default_branch = git_repo.branches.get(branch)
- except pygit2.InvalidSpecError:
+ if branch:
+ with GitRepository(repo.path) as git_repo:
+ new_default_branch = git_repo.branches.get(branch)
+ if new_default_branch:
+ head_ref = git_repo.lookup_reference("HEAD")
+ head_ref.set_target(new_default_branch.name)
+ else:
valid.error(f"Branch {branch} not found", field="default_branch_name")
- if not valid.ok:
- return render_template("settings_info.html",
- owner=owner, repo=repo, **valid.kwargs)
- if new_default_branch:
- head_ref = git_repo.lookup_reference("HEAD")
- head_ref.set_target(new_default_branch.name)
-
- repo.visibility = visibility
- repo.description = desc
- UserWebhook.deliver(UserWebhook.Events.repo_update,
- repo.to_dict(), UserWebhook.Subscription.user_id == repo.owner_id)
- db.session.commit()
- return redirect(url_for("manage.settings_info",
- owner_name=owner_name, repo_name=repo_name))
+ return render_template("settings_info.html",
+ owner=owner, repo=repo, **valid.kwargs)
+
+ rewrite = lambda value: None if value == "" else value
+ input = {
+ key: rewrite(valid.source[key]) for key in [
+ "description", "visibility",
+ ] if valid.source.get(key) is not None
+ }
+
+ resp = exec_gql("git.sr.ht", """
+ mutation UpdateRepository($id: Int!, $input: RepoInput!) {
+ updateRepository(id: $id, input: $input) { id }
+ }
+ """, valid=valid, id=repo.id, input=input)
+ if not valid.ok:
+ return render_template("settings_info.html",
+ owner=owner, repo=repo, **valid.kwargs)
+
+ return redirect(url_for("manage.settings_info",
+ owner_name=owner_name, repo_name=repo_name))
+
+@manage.route("/<owner_name>/<repo_name>/settings/rename")
+@loginrequired
+def settings_rename(owner_name, repo_name):
+ owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
+ if isinstance(repo, BaseRedirectMixin):
+ return redirect(url_for(".settings_rename",
+ owner_name=owner_name, repo_name=repo.new_repo.name))
+ return render_template("settings_rename.html", owner=owner, repo=repo)
+
+@manage.route("/<owner_name>/<repo_name>/settings/rename", methods=["POST"])
+@loginrequired
+def settings_rename_POST(owner_name, repo_name):
+ owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
+ if isinstance(repo, BaseRedirectMixin):
+ repo = repo.new_repo
+
+ valid = Validation(request)
+ name = valid.require("name")
+ if not valid.ok:
+ return render_template("settings_rename.html", owner=owner, repo=repo,
+ **valid.kwargs)
+
+ resp = None
+ try:
+ resp = exec_gql("git.sr.ht", """
+ mutation RenameRepository($id: Int!, $name: String!) {
+ updateRepository(id: $id, input: {name: $name}) {
+ name
+ }
+ }
+ """, id=repo.id, name=name)
+ except GraphQLError as e:
+ for err in e.errors:
+ valid.error(err["message"], field="name")
+
+ if not valid.ok:
+ return render_template("settings_rename.html", owner=owner, repo=repo,
+ **valid.kwargs)
+ resp = resp["updateRepository"]
+ return redirect(url_for("repo.summary",
+ owner=owner_name, repo=resp["name"]))
+
+@manage.route("/<owner_name>/<repo_name>/settings/access")
+@loginrequired
+def settings_access(owner_name, repo_name):
+ owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
+ if isinstance(repo, BaseRedirectMixin):
+ return redirect(url_for(".settings_manage",
+ owner_name=owner_name, repo_name=repo.new_repo.name))
+ return render_template("settings_access.html", owner=owner, repo=repo)
+
+@manage.route("/<owner_name>/<repo_name>/settings/access", methods=["POST"])
+@loginrequired
+def settings_access_POST(owner_name, repo_name):
+ owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
+ if isinstance(repo, BaseRedirectMixin):
+ repo = repo.new_repo
+ valid = Validation(request)
+ username = valid.require("user", friendly_name="User")
+ mode = valid.optional("access", cls=AccessMode, default=AccessMode.ro)
+ if not valid.ok:
+ return render_template("settings_access.html",
+ owner=owner, repo=repo, **valid.kwargs)
+ # TODO: Group access
+ if username[0] == "~":
+ username = username[1:]
+ try:
+ user = current_app.oauth_service.lookup_user(username)
+ except:
+ user = None
+ valid.expect(user, "User not found.", field="user")
+ valid.expect(not user or user.id != current_user.id,
+ "You can't adjust your own access controls. You always have full read/write access.",
+ field="user")
+ valid.expect(not user or user.user_type != UserType.unconfirmed,
+ "This account has not been confirmed yet.", field="user")
+ valid.expect(not user or user.user_type != UserType.suspended,
+ "This account has been suspended.", field="user")
+ if not valid.ok:
+ return render_template("settings_access.html",
+ owner=owner, repo=repo, **valid.kwargs)
+ grant = (Access.query
+ .filter(Access.repo_id == repo.id, Access.user_id == user.id)
+ ).first()
+ if not grant:
+ grant = Access()
+ grant.repo_id = repo.id
+ grant.user_id = user.id
+ db.session.add(grant)
+ grant.mode = mode
+ db.session.commit()
+ return redirect(url_for("manage.settings_access",
+ owner_name=owner.canonical_name, repo_name=repo.name))
+
+@manage.route("/<owner_name>/<repo_name>/settings/access/revoke/<grant_id>", methods=["POST"])
+@loginrequired
+def settings_access_revoke_POST(owner_name, repo_name, grant_id):
+ owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
+ if isinstance(repo, BaseRedirectMixin):
+ repo = repo.new_repo
+ grant = (Access.query
+ .filter(Access.repo_id == repo.id, Access.id == grant_id)
+ ).first()
+ if not grant:
+ abort(404)
+ db.session.delete(grant)
+ db.session.commit()
+ return redirect("/{}/{}/settings/access".format(
+ owner.canonical_name, repo.name))
+
+@manage.route("/<owner_name>/<repo_name>/settings/delete")
+@loginrequired
+def settings_delete(owner_name, repo_name):
+ owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
+ if isinstance(repo, BaseRedirectMixin):
+ return redirect(url_for(".settings_delete",
+ owner_name=owner_name, repo_name=repo.new_repo.name))
+ return render_template("settings_delete.html", owner=owner, repo=repo)
+
+@manage.route("/<owner_name>/<repo_name>/settings/delete", methods=["POST"])
+@loginrequired
+def settings_delete_POST(owner_name, repo_name):
+ if not current_app.repo_api:
+ abort(501)
+ owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
+ if isinstance(repo, BaseRedirectMixin):
+ # Normally we'd redirect but we don't want to fuck up some other repo
+ abort(404)
+ repo_id = repo.id
+ current_app.repo_api.delete_repo(repo)
+ session["notice"] = "{}/{} was deleted.".format(
+ owner.canonical_name, repo.name)
+ return redirect("/" + owner.canonical_name)
M gitsrht/repos.py => gitsrht/repos.py +65 -38
@@ 1,12 1,15 @@
import hashlib
import os.path
import pygit2
+import re
+import shutil
import subprocess
from gitsrht.types import Artifact, Repository, Redirect
from minio import Minio
-from scmsrht.repos import SimpleRepoApi
+from scmsrht.repos.repository import RepoVisibility
from srht.config import cfg
from srht.database import db
+from srht.graphql import exec_gql, GraphQLError
from werkzeug.utils import secure_filename
repos_path = cfg("git.sr.ht", "repos")
@@ 75,44 78,68 @@ def upload_artifact(valid, repo, commit, f, filename):
db.session.add(artifact)
return artifact
-class GitRepoApi(SimpleRepoApi):
- def __init__(self):
- super().__init__(repos_path,
- redirect_class=Redirect,
- repository_class=Repository)
+# TODO: Remove repo API wrapper class
- def do_init_repo(self, owner, repo):
- # Note: update gitsrht-shell when changing this,
- # do_clone_repo(), or _repo_config_init()
- git_repo = pygit2.init_repository(repo.path, bare=True,
- flags=pygit2.GIT_REPOSITORY_INIT_BARE |
- pygit2.GIT_REPOSITORY_INIT_MKPATH)
- self._repo_config_init(repo, git_repo)
+class GitRepoApi():
+ def get_repo_path(self, owner, repo_name):
+ return os.path.join(repos_path, "~" + owner.username, repo_name)
- def _repo_config_init(self, repo, git_repo):
- git_repo.config["srht.repo-id"] = repo.id
- # We handle this ourselves in the post-update hook, and git's
- # default behaviour is to print a large notice and reject the push entirely
- git_repo.config["receive.denyDeleteCurrent"] = "ignore"
- git_repo.config["receive.advertisePushOptions"] = True
- os.unlink(os.path.join(repo.path, "info", "exclude"))
- os.unlink(os.path.join(repo.path, "hooks", "README.sample"))
- os.unlink(os.path.join(repo.path, "description"))
- os.symlink(post_update, os.path.join(repo.path, "hooks", "pre-receive"))
- os.symlink(post_update, os.path.join(repo.path, "hooks", "update"))
- os.symlink(post_update, os.path.join(repo.path, "hooks", "post-update"))
+ def create_repo(self, valid, user=None):
+ repo_name = valid.require("name", friendly_name="Name")
+ description = valid.optional("description")
+ visibility = valid.optional("visibility")
+ if not valid.ok:
+ return None
- def do_delete_repo(self, repo):
- from gitsrht.webhooks import RepoWebhook
- RepoWebhook.Subscription.query.filter(
- RepoWebhook.Subscription.repo_id == repo.id).delete()
- # TODO: Should we delete these asynchronously?
- for artifact in (Artifact.query
- .filter(Artifact.user_id == repo.owner_id)
- .filter(Artifact.repo_id == repo.id)):
- delete_artifact(artifact)
- super().do_delete_repo(repo)
+ resp = exec_gql("git.sr.ht", """
+ mutation CreateRepository($name: String!, $visibility: Visibility = PUBLIC, $description: String) {
+ createRepository(name: $name, visibility: $visibility, description: $description) {
+ id
+ created
+ updated
+ name
+ owner {
+ canonicalName
+ ... on User {
+ name: username
+ }
+ }
+ description
+ visibility
+ }
+ }
+ """, valid=valid, user=user, name=repo_name, description=description, visibility=visibility)
- def do_clone_repo(self, source, repo):
- git_repo = pygit2.clone_repository(source, repo.path, bare=True)
- self._repo_config_init(repo, git_repo)
+ print(valid)
+
+ if not valid.ok:
+ return None
+ return resp["createRepository"]
+
+ def clone_repo(self, valid):
+ cloneUrl = valid.require("cloneUrl", friendly_name="Clone URL")
+ name = valid.require("name", friendly_name="Name")
+ description = valid.optional("description")
+ visibility = valid.optional("visibility")
+ if not valid.ok:
+ return None
+
+ resp = exec_gql("git.sr.ht", """
+ mutation CreateRepository($name: String!, $visibility: Visibility = UNLISTED, $description: String, $cloneUrl: String) {
+ createRepository(name: $name, visibility: $visibility, description: $description, cloneUrl: $cloneUrl) {
+ name
+ }
+ }
+ """, valid=valid, name=name, visibility=visibility,
+ description=description, cloneUrl=cloneUrl)
+
+ if not valid.ok:
+ return None
+ return resp["createRepository"]
+
+ def delete_repo(self, repo, user=None):
+ exec_gql("git.sr.ht", """
+ mutation DeleteRepository($id: Int!) {
+ deleteRepository(id: $id) { id }
+ }
+ """, user=user, id=repo.id)
A gitsrht/templates/clone.html => gitsrht/templates/clone.html +105 -0
@@ 0,0 1,105 @@
+{% extends "base.html" %}
+
+{% block content %}
+<div class="container">
+ <div class="row">
+ <section class="col-md-6">
+ <h3 id="create">Clone existing repository</h3>
+ <form method="POST" action="/clone">
+ {{csrf_token()}}
+ <div class="form-group">
+ <label for="cloneUrl">Clone URL</label>
+ <input
+ {% if another %}
+ autofocus
+ {% endif %}
+ type="text"
+ name="cloneUrl"
+ id="cloneUrl"
+ class="form-control {{valid.cls("cloneUrl")}}"
+ value="{{ cloneUrl or "" }}" />
+ {{valid.summary("cloneUrl")}}
+ </div>
+ <div class="form-group">
+ <label for="name">Name</label>
+ <input
+ type="text"
+ name="name"
+ id="name"
+ class="form-control {{valid.cls("name")}}"
+ value="{{ name or "" }}" />
+ {{valid.summary("name")}}
+ </div>
+ <div class="form-group">
+ <label for="description">Description</label>
+ <input
+ type="text"
+ name="description"
+ id="description"
+ class="form-control {{valid.cls("description")}}"
+ value="{{ description or "" }}" />
+ {{valid.summary("description")}}
+ </div>
+ <fieldset class="form-group">
+ <legend>Visibility</legend>
+ <div class="form-check form-check-inline">
+ <label
+ class="form-check-label"
+ title="Publically visible and listed on your profile"
+ >
+ <input
+ class="form-check-input"
+ type="radio"
+ name="visibility"
+ value="PUBLIC"
+ {{ "checked" if visibility == "PUBLIC" else "" }}
+ > Public
+ </label>
+ </div>
+ <div class="form-check form-check-inline">
+ <label
+ class="form-check-label"
+ title="Visible to anyone with the link, but not shown on your profile"
+ >
+ <input
+ class="form-check-input"
+ type="radio"
+ name="visibility"
+ value="UNLISTED"
+ {{ "checked" if visibility == "UNLISTED" else "" }}
+ > Unlisted
+ </label>
+ </div>
+ <div class="form-check form-check-inline">
+ <label
+ class="form-check-label"
+ title="Only visible to you and your collaborators"
+ >
+ <input
+ class="form-check-input"
+ type="radio"
+ name="visibility"
+ value="PRIVATE"
+ {{ "checked" if visibility == "PRIVATE" else "" }}
+ > Private
+ </label>
+ </div>
+ </fieldset>
+ <button type="submit" class="btn btn-primary">
+ Clone {{icon("caret-right")}}
+ </button>
+ <label class="form-check-label" style="margin-left: 2rem">
+ <input
+ class="form-check-input"
+ type="checkbox"
+ name="another"
+ style="position: relative; top: 2px;"
+ {% if another %}
+ checked
+ {% endif %}> Clone another?
+ </label>
+ </form>
+ </section>
+ </div>
+</div>
+{% endblock %}
A gitsrht/templates/create.html => gitsrht/templates/create.html +97 -0
@@ 0,0 1,97 @@
+{% extends "base.html" %}
+
+{% block content %}
+<div class="container">
+ <div class="row">
+ <section class="col-md-6">
+ <h3 id="create">Create new repository</h3>
+ <form method="POST" action="/create">
+ {{csrf_token()}}
+ <div class="form-group">
+ <label for="name">Name</label>
+ <input
+ {% if another %}
+ autofocus
+ {% endif %}
+ type="text"
+ name="name"
+ id="name"
+ class="form-control {{valid.cls("name")}}"
+ value="{{ repo_name or "" }}" />
+ {{valid.summary("name")}}
+ </div>
+ <div class="form-group">
+ <label for="description">Description</label>
+ <input
+ type="text"
+ name="description"
+ id="description"
+ class="form-control {{valid.cls("description")}}"
+ value="{{ repo_description or "" }}" />
+ {{valid.summary("description")}}
+ </div>
+ <fieldset class="form-group">
+ <legend>Visibility</legend>
+ <div class="form-check form-check-inline">
+ <label
+ class="form-check-label"
+ title="Publically visible and listed on your profile"
+ >
+ <input
+ class="form-check-input"
+ type="radio"
+ name="visibility"
+ value="PUBLIC"
+ checked> Public
+ </label>
+ </div>
+ <div class="form-check form-check-inline">
+ <label
+ class="form-check-label"
+ title="Visible to anyone with the link, but not shown on your profile"
+ >
+ <input
+ class="form-check-input"
+ type="radio"
+ name="visibility"
+ value="UNLISTED"> Unlisted
+ </label>
+ </div>
+ <div class="form-check form-check-inline">
+ <label
+ class="form-check-label"
+ title="Only visible to you and your collaborators"
+ >
+ <input
+ class="form-check-input"
+ type="radio"
+ name="visibility"
+ value="PRIVATE"> Private
+ </label>
+ </div>
+ </fieldset>
+ <button type="submit" class="btn btn-primary">
+ Create {{icon("caret-right")}}
+ </button>
+ <label class="form-check-label" style="margin-left: 2rem">
+ <input
+ class="form-check-input"
+ type="checkbox"
+ name="another"
+ style="position: relative; top: 2px;"
+ {% if another %}
+ checked
+ {% endif %}> Create another?
+ </label>
+ </form>
+ </section>
+ <section class="col-md-8">
+ <div class="alert alert-info">
+ <strong>Did you know?</strong> If you remember how our clone URLs are
+ written, you can push to repositories that don't exist — they'll
+ be automatically created for you.
+ </div>
+ </section>
+ </div>
+</div>
+{% endblock %}
M gitsrht/templates/dashboard.html => gitsrht/templates/dashboard.html +55 -5
@@ 1,6 1,56 @@
-{% extends "bases/scmdashboard.html" %}
-{% block welcome_user %}
- Welcome back, {{ current_user.username }}! This is your git hosting service.
- Documentation for its use is
- <a href="https://man.sr.ht/git.sr.ht">available here</a>.
+{% extends "base.html" %}
+
+{% block content %}
+<div class="container">
+ <div class="row">
+ <div class="col-md-4">
+ <p>
+ Welcome back, {{ current_user.username }}! This is your git hosting service.
+ Documentation for its use is
+ <a href="https://man.sr.ht/git.sr.ht">available here</a>.
+ </p>
+ <a
+ href="{{url_for('manage.create_GET')}}"
+ class="btn btn-primary btn-block"
+ >Create new repository {{icon("caret-right")}}</a>
+ <a
+ href="{{url_for('manage.clone')}}"
+ class="btn btn-default btn-block"
+ >Clone existing repository {{icon("caret-right")}}</a>
+ </div>
+ <div class="col-md-8">
+ <hr class="d-md-none" />
+ {% if repos and len(repos) %}
+ <div class="event-list">
+ {% for repo in repos %}
+ <div class="event">
+ <h4>
+ <a
+ href="{{url_for('repo.summary',
+ owner=current_user.canonical_name,
+ repo=repo.name)}}"
+ >~{{current_user.username}}/{{repo.name}}</a>
+ {% if repo.visibility.value != 'public' %}
+ <small class="pull-right">
+ {{ repo.visibility.value }}
+ </small>
+ {% endif %}
+ </h4>
+ {% if repo.description %}
+ <p>{{ repo.description }}</p>
+ {% endif %}
+ </div>
+ {% endfor %}
+ </div>
+ <a
+ href="{{url_for('public.user_index',
+ username=current_user.username)}}"
+ class="btn btn-default pull-right"
+ >More on your profile {{icon("caret-right")}}</a>
+ {% else %}
+ <p class="text-muted">You don't have any repositories.</p>
+ {% endif %}
+ </div>
+ </div>
+</div>
{% endblock %}
M gitsrht/templates/settings_info.html => gitsrht/templates/settings_info.html +106 -27
@@ 1,30 1,109 @@
-{% set info_action=url_for("manage_git.settings_info_git_POST",
- owner_name=owner.canonical_name, repo_name=repo.name) %}
-{% extends "bases/scmsettings_info.html" %}
+{% extends "settings.html" %}
-{% block extrafields %}
-<div class="form-group">
- <label for="default_branch_name">
- Default branch
- </label>
- <select
- class="form-control {{valid.cls('default_branch_name')}}"
- id="default_branch_name"
- name="default_branch_name"
- {% if repo.git_repo.is_empty %}disabled{% endif %}
- >
- {% set default_branch_name = repo.git_repo.default_branch_name() or "" %}
- {% for branch in repo.git_repo.raw_listall_branches() %}
- {% set branch = branch.decode("utf-8", "replace") %}
- <option
- value="{{branch}}"
- {% if branch == default_branch_name %}
- selected
- {% endif %}>{{branch}}</option>
- {% else %}
- <option>No branches</option>
- {% endfor %}
- </select>
- {{valid.summary('default_branch_name')}}
+{% block content %}
+<div class="row">
+ <div class="col-md-8">
+ <form method="POST" action="{{url_for("manage.settings_info_POST",
+ owner_name=owner.canonical_name, repo_name=repo.name)}}">
+ {{csrf_token()}}
+ <div class="form-group">
+ <label for="name" style="display: block">
+ Repository name
+ <a
+ href="/{{ owner.canonical_name }}/{{ repo.name }}/settings/rename"
+ class="pull-right"
+ >Rename?</a>
+ </label>
+ <input
+ type="text"
+ class="form-control"
+ id="name"
+ value="{{repo.name}}"
+ readonly />
+ </div>
+ <div class="form-group">
+ <label for="description">
+ Description
+ </label>
+ <input
+ type="text"
+ class="form-control"
+ id="description"
+ name="description"
+ value="{{repo.description or ""}}" />
+ </div>
+ <fieldset class="form-group">
+ <div class="form-check form-check-inline">
+ <label
+ class="form-check-label"
+ title="Publically visible and listed on your profile"
+ >
+ <input
+ class="form-check-input"
+ type="radio"
+ name="visibility"
+ value="PUBLIC"
+ {{ "checked" if repo.visibility.value == "public" else "" }}
+ > Public
+ </label>
+ </div>
+ <div class="form-check form-check-inline">
+ <label
+ class="form-check-label"
+ title="Visible to anyone with the link, but not shown on your profile"
+ >
+ <input
+ class="form-check-input"
+ type="radio"
+ name="visibility"
+ value="UNLISTED"
+ {{ "checked" if repo.visibility.value == "unlisted" else "" }}
+ > Unlisted
+ </label>
+ </div>
+ <div class="form-check form-check-inline">
+ <label
+ class="form-check-label"
+ title="Only visible to you and your collaborators"
+ >
+ <input
+ class="form-check-input"
+ type="radio"
+ name="visibility"
+ value="PRIVATE"
+ {{ "checked" if repo.visibility.value == "private" else "" }}
+ > Private
+ </label>
+ </div>
+ </fieldset>
+ <div class="form-group">
+ <label for="default_branch_name">
+ Default branch
+ </label>
+ <select
+ class="form-control {{valid.cls('default_branch_name')}}"
+ id="default_branch_name"
+ name="default_branch_name"
+ {% if repo.git_repo.is_empty %}disabled{% endif %}
+ >
+ {% set default_branch_name = repo.git_repo.default_branch_name() or "" %}
+ {% for branch in repo.git_repo.raw_listall_branches() %}
+ {% set branch = branch.decode("utf-8", "replace") %}
+ <option
+ value="{{branch}}"
+ {% if branch == default_branch_name %}
+ selected
+ {% endif %}>{{branch}}</option>
+ {% else %}
+ <option>No branches</option>
+ {% endfor %}
+ </select>
+ {{valid.summary('default_branch_name')}}
+ </div>
+ <button type="submit" class="btn btn-primary pull-right">
+ Save {{icon("caret-right")}}
+ </button>
+ </form>
+ </div>
</div>
{% endblock %}
M gitsrht/templates/summary.html => gitsrht/templates/summary.html +4 -1
@@ 89,7 89,10 @@
{% elif current_user != repo.owner %}
<form method="POST" action="{{url_for('manage.clone_POST')}}">
{{csrf_token()}}
- <input type="hidden" name="source_repo_id" value="{{repo.id}}" />
+ <input type="hidden" name="cloneUrl" value="{{(repo|clone_urls)[0]}}" />
+ <input type="hidden" name="name" value="{{repo.name}}" />
+ <input type="hidden" name="description" value="Clone of {{(repo|clone_urls)[0]}}" />
+ <input type="hidden" name="visibility" value="{% if repo.visibility.value == 'private' %}PRIVATE{% else %}UNLISTED{% endif %}" />
<button type="submit" class="btn btn-primary btn-block">
Clone repo to your account {{icon('caret-right')}}
</button>
M gitsrht/webhooks.py => gitsrht/webhooks.py +0 -1
@@ 8,7 8,6 @@ if not hasattr(db, "session"):
from srht.webhook import Event
from srht.webhook.celery import CeleryWebhook, make_worker
from srht.metrics import RedisQueueCollector
-from scmsrht.webhooks import UserWebhook
import sqlalchemy as sa
webhook_broker = cfg("git.sr.ht", "webhooks")