gitsrht: Store visibility as enum instead of varchar

Add a 'visibility' enum type to the database and use it for the
repository.visibility column.

This required changes to scm.sr.ht code. Instead of updating scm.sr.ht,
most of the scm.sr.ht code that git.sr.ht uses was moved to git.sr.ht.
This commit is contained in:
Adnan Maolood 2022-02-24 19:26:22 -05:00 committed by Drew DeVault
parent cd8225a534
commit ad42bf4448
27 changed files with 451 additions and 194 deletions

View File

@ -3,7 +3,6 @@ package model
import (
"context"
"database/sql"
"fmt"
"strconv"
"time"
@ -23,28 +22,15 @@ type Repository struct {
Description *string `json:"description"`
Readme *string `json:"readme"`
Path string
OwnerID int
RawVisibility string
Path string
OwnerID int
Visibility Visibility
alias string
repo *RepoWrapper
fields *database.ModelFields
}
func (r *Repository) Visibility() Visibility {
visMap := map[string]Visibility{
"public": VisibilityPublic,
"unlisted": VisibilityUnlisted,
"private": VisibilityPrivate,
}
vis, ok := visMap[r.RawVisibility]
if !ok {
panic(fmt.Errorf("Invalid repo visibility %s", r.RawVisibility)) // Invariant
}
return vis
}
func (r *Repository) Repo() *RepoWrapper {
if r.repo != nil {
return r.repo
@ -94,7 +80,7 @@ func (r *Repository) Fields() *database.ModelFields {
{"updated", "updated", &r.Updated},
{"name", "name", &r.Name},
{"description", "description", &r.Description},
{"visibility", "visibility", &r.RawVisibility},
{"visibility", "visibility", &r.Visibility},
{"readme", "readme", &r.Readme},
// Always fetch:

View File

@ -115,19 +115,6 @@ func (r *mutationResolver) CreateRepository(ctx context.Context, name string, vi
}()
if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
vismap := map[model.Visibility]string{
model.VisibilityPublic: "public",
model.VisibilityUnlisted: "unlisted",
model.VisibilityPrivate: "private",
}
var (
dvis string
ok bool
)
if dvis, ok = vismap[visibility]; !ok {
panic(fmt.Errorf("Unknown visibility %s", visibility)) // Invariant
}
cloneStatus := CloneNone
if cloneURL != nil {
cloneStatus = CloneInProgress
@ -144,9 +131,9 @@ func (r *mutationResolver) CreateRepository(ctx context.Context, name string, vi
) RETURNING
id, created, updated, name, description, visibility,
path, owner_id;
`, name, description, repoPath, dvis, user.UserID, cloneStatus)
`, name, description, repoPath, visibility, user.UserID, cloneStatus)
if err := row.Scan(&repo.ID, &repo.Created, &repo.Updated, &repo.Name,
&repo.Description, &repo.RawVisibility,
&repo.Description, &repo.Visibility,
&repo.Path, &repo.OwnerID); err != nil {
if strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
return valid.Errorf(ctx, "name", "A repository with this name already exists.")
@ -184,7 +171,7 @@ func (r *mutationResolver) CreateRepository(ctx context.Context, name string, vi
}
export := path.Join(repoPath, "git-daemon-export-ok")
if repo.Visibility() != model.VisibilityPrivate {
if repo.Visibility != model.VisibilityPrivate {
_, err := os.Create(export)
if err != nil {
return err
@ -361,7 +348,7 @@ func (r *mutationResolver) UpdateRepository(ctx context.Context, id int, input m
valid.OptionalString("visibility", func(vis string) {
valid.Expect(model.Visibility(vis).IsValid(),
"Invalid visibility '%s'", vis)
query = query.Set(`visibility`, strings.ToLower(vis)) // TODO: Can we use uppercase here?
query = query.Set(`visibility`, vis)
})
valid.NullableString("readme", func(readme *string) {
@ -386,7 +373,7 @@ func (r *mutationResolver) UpdateRepository(ctx context.Context, id int, input m
row := query.RunWith(tx).QueryRowContext(ctx)
if err := row.Scan(&repo.ID, &repo.Created, &repo.Updated,
&repo.Name, &repo.Description, &repo.RawVisibility,
&repo.Name, &repo.Description, &repo.Visibility,
&repo.Path, &repo.OwnerID); err != nil {
if err == sql.ErrNoRows {
return fmt.Errorf("No repository by ID %d found for this user", id)
@ -418,7 +405,7 @@ func (r *mutationResolver) UpdateRepository(ctx context.Context, id int, input m
}
export := path.Join(repo.Path, "git-daemon-export-ok")
if repo.Visibility() == model.VisibilityPrivate {
if repo.Visibility == model.VisibilityPrivate {
err := os.Remove(export)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
@ -476,7 +463,7 @@ func (r *mutationResolver) DeleteRepository(ctx context.Context, id int) (*model
`, id, auth.ForContext(ctx).UserID)
if err := row.Scan(&repo.ID, &repo.Created, &repo.Updated,
&repo.Name, &repo.Description, &repo.RawVisibility,
&repo.Name, &repo.Description, &repo.Visibility,
&repo.Path, &repo.OwnerID); err != nil {
if err == sql.ErrNoRows {
return fmt.Errorf("No repository by ID %d found for this user", id)
@ -1302,7 +1289,7 @@ func (r *userResolver) Repositories(ctx context.Context, obj *model.User, cursor
sq.Or{
sq.Expr(`? IN (access.user_id, repo.owner_id)`,
auth.ForContext(ctx).UserID),
sq.Expr(`repo.visibility = 'public'`),
sq.Expr(`repo.visibility = 'PUBLIC'`),
},
sq.Expr(`repo.owner_id = ?`, obj.ID),
})

View File

@ -138,7 +138,7 @@ func fetchRepositoriesByID(ctx context.Context) func(ids []int) ([]*model.Reposi
sq.Expr(`repo.id = ANY(?)`, pq.Array(ids)),
sq.Or{
sq.Expr(`? IN (access.user_id, repo.owner_id)`, auser.UserID),
sq.Expr(`repo.visibility != 'private'`),
sq.Expr(`repo.visibility != 'PRIVATE'`),
},
})
if rows, err = query.RunWith(tx).QueryContext(ctx); err != nil {
@ -205,7 +205,7 @@ func fetchRepositoriesByOwnerRepoName(ctx context.Context) func([]OwnerRepoName)
Where(sq.Or{
sq.Expr(`? IN (access.user_id, repo.owner_id)`,
auth.ForContext(ctx).UserID),
sq.Expr(`repo.visibility != 'private'`),
sq.Expr(`repo.visibility != 'PRIVATE'`),
})
if rows, err = query.RunWith(tx).QueryContext(ctx); err != nil {
panic(err)
@ -272,7 +272,7 @@ func fetchRepositoriesByOwnerIDRepoName(ctx context.Context) func([]OwnerIDRepoN
Where(sq.Or{
sq.Expr(`? IN (access.user_id, repo.owner_id)`,
auth.ForContext(ctx).UserID),
sq.Expr(`repo.visibility != 'private'`),
sq.Expr(`repo.visibility != 'PRIVATE'`),
})
if rows, err = query.RunWith(tx).QueryContext(ctx); err != nil {
panic(err)

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"strings"
"time"
"git.sr.ht/~sircmpwn/core-go/auth"
@ -35,7 +36,7 @@ func DeliverLegacyRepoCreate(ctx context.Context, repo *model.Repository) {
Updated: repo.Created,
Name: repo.Name,
Description: repo.Description,
Visibility: repo.RawVisibility,
Visibility: strings.ToLower(repo.Visibility.String()),
}
// TODO: User groups
@ -69,7 +70,7 @@ func DeliverLegacyRepoUpdate(ctx context.Context, repo *model.Repository) {
Updated: repo.Created,
Name: repo.Name,
Description: repo.Description,
Visibility: repo.RawVisibility,
Visibility: strings.ToLower(repo.Visibility.String()),
}
// TODO: User groups

View File

@ -11,13 +11,12 @@ from prometheus_client import CollectorRegistry, Gauge
from prometheus_client.context_managers import Timer
from srht.config import cfg
from srht.database import DbSession
from gitsrht.types import Artifact, User, Repository, RepoVisibility
from gitsrht.types import Artifact, User, Repository
from minio import Minio
from datetime import datetime, timedelta
db = DbSession(cfg("git.sr.ht", "connection-string"))
db.init()
repo_api = gr.GitRepoApi()
registry = CollectorRegistry()
tg = Gauge("gitsrht_periodic_time",

View File

@ -152,7 +152,7 @@ func main() {
// Fetch the necessary info from SQL. This first query fetches:
//
// 1. Repository information, such as visibility (public|unlisted|private)
// 1. Repository information, such as visibility (PUBLIC|UNLISTED|PRIVATE)
// 2. Information about the repository owner's account
// 3. Information about the pusher's account
// 4. Any access control policies for that repo that apply to the pusher
@ -266,7 +266,7 @@ func main() {
repoOwnerId = pusherId
repoOwnerName = pusherName
repoVisibility = "private"
repoVisibility = "PRIVATE"
query := client.GraphQLQuery{
Query: `
@ -339,11 +339,11 @@ func main() {
} else {
if accessGrant == nil {
switch repoVisibility {
case "public":
case "PUBLIC":
fallthrough
case "unlisted":
case "UNLISTED":
hasAccess = ACCESS_READ
case "private":
case "PRIVATE":
fallthrough
default:
hasAccess = ACCESS_NONE

View File

@ -232,7 +232,7 @@ func (submitter GitBuildSubmitter) GetCommitNote() string {
}
func (submitter GitBuildSubmitter) GetCloneUrl() string {
if submitter.Visibility == "private" {
if submitter.Visibility == "PRIVATE" {
origin := strings.ReplaceAll(submitter.GitOrigin, "http://", "")
origin = strings.ReplaceAll(origin, "https://", "")
// Use SSH URL

93
gitsrht/access.py Normal file
View File

@ -0,0 +1,93 @@
from datetime import datetime
from enum import IntFlag
from flask import abort, current_app, request, redirect, url_for
from gitsrht.types import Access, AccessMode, Repository, Redirect, User, RepoVisibility
from srht.database import db
from srht.oauth import current_user
import sqlalchemy as sa
import sqlalchemy_utils as sau
from sqlalchemy.ext.declarative import declared_attr
from enum import Enum
class UserAccess(IntFlag):
none = 0
read = 1
write = 2
manage = 4
def get_repo(owner_name, repo_name):
if owner_name[0] == "~":
user = User.query.filter(User.username == owner_name[1:]).first()
if user:
repo = Repository.query.filter(Repository.owner_id == user.id)\
.filter(Repository.name == repo_name).first()
else:
repo = None
if user and not repo:
repo = (Redirect.query
.filter(Redirect.owner_id == user.id)
.filter(Redirect.name == repo_name)
).first()
return user, repo
else:
# TODO: organizations
return None, None
def get_repo_or_redir(owner, repo):
owner, repo = get_repo(owner, repo)
if not repo:
abort(404)
if not has_access(repo, UserAccess.read):
abort(401)
if isinstance(repo, Redirect):
view_args = request.view_args
if not "repo" in view_args or not "owner" in view_args:
return redirect(url_for(".summary",
owner=repo.new_repo.owner.canonical_name,
repo=repo.new_repo.name))
view_args["owner"] = repo.new_repo.owner.canonical_name
view_args["repo"] = repo.new_repo.name
abort(redirect(url_for(request.endpoint, **view_args)))
return owner, repo
def get_access(repo, user=None):
# Note: when updating push access logic, also update git.sr.ht/gitsrht-shell
if not user:
user = current_user
if not repo:
return UserAccess.none
if isinstance(repo, Redirect):
# Just pretend they have full access for long enough to do the redirect
return UserAccess.read | UserAccess.write | UserAccess.manage
if not user:
if repo.visibility == RepoVisibility.PUBLIC or \
repo.visibility == RepoVisibility.UNLISTED:
return UserAccess.read
return UserAccess.none
if repo.owner_id == user.id:
return UserAccess.read | UserAccess.write | UserAccess.manage
acl = Access.query.filter(
Access.user_id == user.id,
Access.repo_id == repo.id).first()
if acl:
acl.updated = datetime.utcnow()
db.session.commit()
if acl.mode == AccessMode.ro:
return UserAccess.read
else:
return UserAccess.read | UserAccess.write
if repo.visibility == RepoVisibility.PRIVATE:
return UserAccess.none
return UserAccess.read
def has_access(repo, access, user=None):
return access in get_access(repo, user)
def check_access(owner_name, repo_name, access):
owner, repo = get_repo(owner_name, repo_name)
if not owner or not repo:
abort(404)
a = get_access(repo)
if not access in a:
abort(403)
return owner, repo

View File

@ -0,0 +1,37 @@
"""Add visibility enum
Revision ID: 64fcd80183c8
Revises: 38952f52f32d
Create Date: 2022-02-24 12:29:23.314019
"""
# revision identifiers, used by Alembic.
revision = '64fcd80183c8'
down_revision = '38952f52f32d'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.execute("""
UPDATE repository SET visibility = 'private' WHERE visibility = 'autocreated';
CREATE TYPE visibility AS ENUM (
'PUBLIC',
'PRIVATE',
'UNLISTED'
);
ALTER TABLE repository
ALTER COLUMN visibility TYPE visibility USING upper(visibility)::visibility;
""")
def downgrade():
op.execute("""
ALTER TABLE repository
ALTER COLUMN visibility TYPE varchar USING lower(visibility::varchar);
DROP TYPE visibility;
""")

View File

@ -4,25 +4,24 @@ import stat
from functools import lru_cache
from gitsrht import urls
from gitsrht.git import commit_time, commit_links, trim_commit, signature_time
from gitsrht.repos import GitRepoApi
from gitsrht.service import oauth_service, webhooks_notify
from gitsrht.types import Access, Redirect, Repository, User
from scmsrht.flask import ScmSrhtFlask
from gitsrht.types import User
from srht.config import cfg
from srht.database import DbSession
from srht.flask import session
from srht.database import db, DbSession
from srht.flask import SrhtFlask, session
from jinja2 import FileSystemLoader, ChoiceLoader
from werkzeug.urls import url_quote
db = DbSession(cfg("git.sr.ht", "connection-string"))
db.init()
class GitApp(ScmSrhtFlask):
class GitApp(SrhtFlask):
def __init__(self):
super().__init__("git.sr.ht", __name__,
access_class=Access, redirect_class=Redirect,
repository_class=Repository, user_class=User,
repo_api=GitRepoApi(), oauth_service=oauth_service)
oauth_service=oauth_service)
from gitsrht.blueprints.auth import auth
from gitsrht.blueprints.public import public
from gitsrht.blueprints.api import register_api
from gitsrht.blueprints.api.plumbing import plumbing
from gitsrht.blueprints.api.porcelain import porcelain
@ -32,6 +31,9 @@ class GitApp(ScmSrhtFlask):
from gitsrht.blueprints.repo import repo
from srht.graphql import gql_blueprint
self.register_blueprint(auth)
self.register_blueprint(public)
register_api(self)
self.register_blueprint(plumbing)
self.register_blueprint(porcelain)
@ -68,6 +70,16 @@ class GitApp(ScmSrhtFlask):
"path_join": os.path.join,
"stat": stat,
"trim_commit": trim_commit,
"lookup_user": self.lookup_user
}
choices = [self.jinja_loader, FileSystemLoader(os.path.join(
os.path.dirname(__file__), "templates"))]
self.jinja_loader = ChoiceLoader(choices)
self.url_map.strict_slashes = False
def lookup_user(self, email):
return User.query.filter(User.email == email).one_or_none()
app = GitApp()

View File

@ -1,7 +1,7 @@
import pkg_resources
from flask import abort
from scmsrht.access import UserAccess, get_access
from scmsrht.types import Repository, User
from gitsrht.access import UserAccess, get_access
from gitsrht.types import Repository, User
from srht.flask import csrf_bypass
from srht.oauth import current_token, oauth

View File

@ -1,7 +1,7 @@
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
import gitsrht.repos as repos
from gitsrht.access import UserAccess
from gitsrht.types import Access, Repository, User, RepoVisibility
from gitsrht.webhooks import UserWebhook
from gitsrht.blueprints.api import get_user, get_repo
from srht.api import paginated_response
@ -22,12 +22,12 @@ def repos_by_user_GET(username):
.filter(Repository.owner_id == user.id))
if user.id != current_token.user_id:
repos = (repos
.outerjoin(Access._get_current_object(),
.outerjoin(Access,
Access.repo_id == Repository.id)
.filter(or_(
Access.user_id == current_token.user_id,
and_(
Repository.visibility == RepoVisibility.public,
Repository.visibility == RepoVisibility.PUBLIC,
Access.id.is_(None))
)))
return paginated_response(Repository.id, repos)
@ -37,7 +37,7 @@ def repos_by_user_GET(username):
def repos_POST():
valid = Validation(request)
user = current_token.user
resp = current_app.repo_api.create_repo(valid, user)
resp = repos.create_repo(valid, user)
if not valid.ok:
return valid.response
# Convert visibility back to lowercase
@ -103,7 +103,7 @@ 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)
repos.delete_repo(repo, user)
return {}, 204
@info.route("/api/repos/<reponame>/readme", defaults={"username": None})

View File

@ -10,7 +10,7 @@ from gitsrht.repos import upload_artifact
from gitsrht.webhooks import RepoWebhook
from io import BytesIO
from itertools import groupby
from scmsrht.access import UserAccess
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

View File

@ -7,7 +7,7 @@ from gitsrht.git import Repository as GitRepository, strip_pgp_signature
from gitsrht.repos import delete_artifact, upload_artifact
from gitsrht.types import Artifact
from minio import Minio
from scmsrht.access import check_access, get_repo_or_redir, UserAccess
from gitsrht.access import check_access, UserAccess
from srht.config import cfg
from srht.database import db
from srht.oauth import loginrequired

View File

@ -0,0 +1,22 @@
import os
from flask import Blueprint, request
from gitsrht.access import get_repo, has_access, UserAccess
from urllib.parse import urlparse, unquote
auth = Blueprint("auth", __name__)
@auth.route("/authorize")
def authorize_http_access():
original_uri = request.headers.get("X-Original-URI")
original_uri = urlparse(original_uri)
path = unquote(original_uri.path)
original_path = os.path.normpath(path).split('/')
if len(original_path) < 3:
return "authorized", 200
owner, repo = original_path[1], original_path[2]
owner, repo = get_repo(owner, repo)
if not repo:
return "unauthorized", 403
if not has_access(repo, UserAccess.read):
return "unauthorized", 403
return "authorized", 200

View File

@ -13,7 +13,7 @@ from flask import Blueprint, render_template, abort, request, url_for, session
from flask import redirect
from gitsrht.git import Repository as GitRepository, commit_time, diffstat
from gitsrht.git import get_log
from scmsrht.access import get_repo_or_redir
from gitsrht.access import get_repo_or_redir
from srht.config import cfg, cfgi, cfgb
from srht.oauth import loginrequired, current_user
from srht.validation import Validation

View File

@ -1,6 +1,7 @@
import pygit2
from flask import Blueprint, current_app, request, render_template, abort
from flask import redirect, url_for
import gitsrht.repos as repos
from gitsrht.git import Repository as GitRepository
from srht.config import cfg
from srht.database import db
@ -8,11 +9,8 @@ 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.types import Access, User
from gitsrht.access import check_access, UserAccess, AccessMode
from gitsrht.types import Access, User, Redirect
import shutil
import os
@ -28,10 +26,8 @@ def create_GET():
@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)
resp = repos.create_repo(valid)
if not valid.ok:
return render_template("create.html", **valid.kwargs)
@ -51,10 +47,8 @@ def clone():
@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)
resp = repos.clone_repo(valid)
if not valid.ok:
return render_template("clone.html", **valid.kwargs)
return redirect(url_for("repo.summary",
@ -64,7 +58,7 @@ def clone_POST():
@loginrequired
def settings_info(owner_name, repo_name):
owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
if isinstance(repo, BaseRedirectMixin):
if isinstance(repo, Redirect):
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)
@ -73,7 +67,7 @@ def settings_info(owner_name, repo_name):
@loginrequired
def settings_info_POST(owner_name, repo_name):
owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
if isinstance(repo, BaseRedirectMixin):
if isinstance(repo, Redirect):
repo = repo.new_repo
valid = Validation(request)
@ -101,7 +95,7 @@ def settings_info_POST(owner_name, repo_name):
@loginrequired
def settings_rename(owner_name, repo_name):
owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
if isinstance(repo, BaseRedirectMixin):
if isinstance(repo, Redirect):
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)
@ -110,7 +104,7 @@ def settings_rename(owner_name, repo_name):
@loginrequired
def settings_rename_POST(owner_name, repo_name):
owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
if isinstance(repo, BaseRedirectMixin):
if isinstance(repo, Redirect):
repo = repo.new_repo
valid = Validation(request)
@ -138,7 +132,7 @@ def settings_rename_POST(owner_name, repo_name):
@loginrequired
def settings_access(owner_name, repo_name):
owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
if isinstance(repo, BaseRedirectMixin):
if isinstance(repo, Redirect):
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)
@ -147,7 +141,7 @@ def settings_access(owner_name, repo_name):
@loginrequired
def settings_access_POST(owner_name, repo_name):
owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
if isinstance(repo, BaseRedirectMixin):
if isinstance(repo, Redirect):
repo = repo.new_repo
valid = Validation(request)
username = valid.require("user", friendly_name="User")
@ -190,7 +184,7 @@ def settings_access_POST(owner_name, repo_name):
@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):
if isinstance(repo, Redirect):
repo = repo.new_repo
grant = (Access.query
.filter(Access.repo_id == repo.id, Access.id == grant_id)
@ -206,7 +200,7 @@ def settings_access_revoke_POST(owner_name, repo_name, grant_id):
@loginrequired
def settings_delete(owner_name, repo_name):
owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
if isinstance(repo, BaseRedirectMixin):
if isinstance(repo, Redirect):
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)
@ -214,14 +208,12 @@ def settings_delete(owner_name, repo_name):
@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):
if isinstance(repo, Redirect):
# 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)
repos.delete_repo(repo)
session["notice"] = "{}/{} was deleted.".format(
owner.canonical_name, repo.name)
return redirect("/" + owner.canonical_name)

View File

@ -0,0 +1,54 @@
from flask import Blueprint, current_app, request
from flask import render_template, abort
from srht.config import cfg
from srht.flask import paginate_query
from srht.oauth import current_user
from srht.search import search_by
from gitsrht.types import Access, Repository, User, RepoVisibility
from sqlalchemy import and_, or_
public = Blueprint('public', __name__)
@public.route("/")
def index():
if current_user:
repos = (Repository.query
.filter(Repository.owner_id == current_user.id)
.order_by(Repository.updated.desc())
.limit(10)).all()
return render_template("dashboard.html", repos=repos)
return render_template("index.html")
@public.route("/~<username>")
@public.route("/~<username>/")
def user_index(username):
user = User.query.filter(User.username == username).first()
if not user:
abort(404)
terms = request.args.get("search")
repos = (Repository.query
.filter(Repository.owner_id == user.id))
if current_user and current_user.id != user.id:
repos = (repos
.outerjoin(Access,
Access.repo_id == Repository.id)
.filter(or_(
Access.user_id == current_user.id,
Repository.visibility == RepoVisibility.PUBLIC,
)))
elif not current_user:
repos = repos.filter(Repository.visibility == RepoVisibility.PUBLIC)
search_error = None
try:
repos = search_by(repos, terms,
[Repository.name, Repository.description])
except ValueError as ex:
search_error = str(ex)
repos = repos.order_by(Repository.updated.desc())
repos, pagination = paginate_query(repos)
return render_template("user.html",
user=user, repos=repos,
search=terms, search_error=search_error, **pagination)

View File

@ -19,7 +19,7 @@ from jinja2.utils import url_quote, escape
from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import guess_lexer, guess_lexer_for_filename, TextLexer
from scmsrht.access import get_repo, get_repo_or_redir
from gitsrht.access import get_repo, get_repo_or_redir
from scmsrht.formatting import get_formatted_readme, get_highlighted_file
from scmsrht.urls import get_clone_urls
from srht.config import cfg, get_origin

View File

@ -6,7 +6,6 @@ import shutil
import subprocess
from gitsrht.types import Artifact, Repository, Redirect
from minio import Minio
from scmsrht.repos.repository import RepoVisibility
from srht.config import cfg
from srht.database import db
from srht.graphql import exec_gql, GraphQLError
@ -78,85 +77,82 @@ def upload_artifact(valid, repo, commit, f, filename):
db.session.add(artifact)
return artifact
# TODO: Remove repo API wrapper class
def get_repo_path(owner, repo_name):
return os.path.join(repos_path, "~" + owner.username, repo_name)
class GitRepoApi():
def get_repo_path(self, owner, repo_name):
return os.path.join(repos_path, "~" + owner.username, repo_name)
def create_repo(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 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
# Convert the visibility to uppercase. This is needed for the REST API
# TODO: Remove this when the REST API is phased out
if visibility is not None:
visibility = visibility.upper()
# Convert the visibility to uppercase. This is needed for the REST API
# TODO: Remove this when the REST API is phased out
if visibility is not None:
visibility = visibility.upper()
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
}
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
}
description
visibility
}
""", valid=valid, user=user, name=repo_name,
description=description, visibility=visibility)
}
""", valid=valid, user=user, name=repo_name,
description=description, visibility=visibility)
if not valid.ok:
return None
return resp["createRepository"]
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
def clone_repo(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
}
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)
}
""", valid=valid, name=name, visibility=visibility,
description=description, cloneUrl=cloneUrl)
if not valid.ok:
return None
return resp["createRepository"]
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)
def delete_repo(repo, user=None):
exec_gql("git.sr.ht", """
mutation DeleteRepository($id: Int!) {
deleteRepository(id: $id) { id }
}
""", user=user, id=repo.id)

View File

@ -0,0 +1,5 @@
{% extends "layout.html" %}
{% block body %}
{% block content %}{% endblock %}
{% endblock %}

View File

@ -30,9 +30,9 @@
owner=current_user.canonical_name,
repo=repo.name)}}"
>~{{current_user.username}}/{{repo.name}}</a>
{% if repo.visibility.value != 'public' %}
{% if repo.visibility.value != 'PUBLIC' %}
<small class="pull-right">
{{ repo.visibility.value }}
{{ repo.visibility.value.lower() }}
</small>
{% endif %}
</h4>

View File

@ -2,7 +2,7 @@
{% block head %}
{% block repohead %}
{% endblock %}
{% if repo.visibility.value =='unlisted' %}
{% if repo.visibility.value == 'UNLISTED' %}
<meta name="robots" content="noindex">
{% endif %}
{# VCS meta tags #}
@ -44,18 +44,18 @@
>{{owner.canonical_name}}</a>/<wbr>{{repo.name}}
</h2>
<ul class="nav nav-tabs">
{% if repo.visibility.value != "public" %}
{% if repo.visibility.value != "PUBLIC" %}
<li
class="nav-item nav-text vis-{{repo.visibility.value.lower()}}"
{% if repo.visibility.value == "unlisted" %}
{% if repo.visibility.value == "UNLISTED" %}
title="This repository is only visible to those who know the URL."
{% elif repo.visibility.value == "private" %}
{% elif repo.visibility.value == "PRIVATE" %}
title="This repository is only visible to those who were invited to view it."
{% endif %}
>
{% if repo.visibility.value == "unlisted" %}
{% if repo.visibility.value == "UNLISTED" %}
Unlisted
{% elif repo.visibility.value == "private" %}
{% elif repo.visibility.value == "PRIVATE" %}
Private
{% endif %}
</li>

View File

@ -43,7 +43,7 @@
type="radio"
name="visibility"
value="PUBLIC"
{{ "checked" if repo.visibility.value == "public" else "" }}
{{ "checked" if repo.visibility.value == "PUBLIC" else "" }}
> Public
</label>
</div>
@ -57,7 +57,7 @@
type="radio"
name="visibility"
value="UNLISTED"
{{ "checked" if repo.visibility.value == "unlisted" else "" }}
{{ "checked" if repo.visibility.value == "UNLISTED" else "" }}
> Unlisted
</label>
</div>
@ -71,7 +71,7 @@
type="radio"
name="visibility"
value="PRIVATE"
{{ "checked" if repo.visibility.value == "private" else "" }}
{{ "checked" if repo.visibility.value == "PRIVATE" else "" }}
> Private
</label>
</div>

View File

@ -92,7 +92,7 @@
<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 %}" />
<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>
@ -121,7 +121,7 @@
</div>
</div>
{% if current_user == repo.owner and not license
and repo.visibility.value == 'public' %}
and repo.visibility.value == 'PUBLIC' %}
<div class="alert alert-danger">
<strong>Heads up!</strong> We couldn't find a license file for your
repository, which means that it may not be possible for others to use this

View File

@ -62,9 +62,9 @@
<a href="/~{{user.username}}/{{repo.name}}">
~{{user.username}}/{{repo.name}}
</a>
{% if repo.visibility.value != 'public' %}
{% if repo.visibility.value != 'PUBLIC' %}
<small class="pull-right">
{{ repo.visibility.value }}
{{ repo.visibility.value.lower() }}
</small>
{% endif %}
</h4>

View File

@ -7,8 +7,7 @@ from sqlalchemy.dialects import postgresql
from srht.database import Base
from srht.oauth import ExternalUserMixin, ExternalOAuthTokenMixin
from gitsrht.git import Repository as GitRepository
from scmsrht.repos import BaseAccessMixin, BaseRedirectMixin
from scmsrht.repos import RepoVisibility
from enum import Enum
class User(Base, ExternalUserMixin):
pass
@ -16,11 +15,86 @@ class User(Base, ExternalUserMixin):
class OAuthToken(Base, ExternalOAuthTokenMixin):
pass
class Access(Base, BaseAccessMixin):
pass
class AccessMode(Enum):
ro = 'ro'
rw = 'rw'
class Redirect(Base, BaseRedirectMixin):
pass
class Access(Base):
@declared_attr
def __tablename__(cls):
return "access"
@declared_attr
def __table_args__(cls):
return (
sa.UniqueConstraint('user_id', 'repo_id',
name="uq_access_user_id_repo_id"),
)
id = sa.Column(sa.Integer, primary_key=True)
created = sa.Column(sa.DateTime, nullable=False)
updated = sa.Column(sa.DateTime, nullable=False)
mode = sa.Column(sau.ChoiceType(AccessMode, impl=sa.String()),
nullable=False, default=AccessMode.ro)
@declared_attr
def user_id(cls):
return sa.Column(sa.Integer, sa.ForeignKey('user.id'), nullable=False)
@declared_attr
def user(cls):
return sa.orm.relationship('User', backref='access_grants')
@declared_attr
def repo_id(cls):
return sa.Column(sa.Integer,
sa.ForeignKey('repository.id', ondelete="CASCADE"),
nullable=False)
@declared_attr
def repo(cls):
return sa.orm.relationship('Repository',
backref=sa.orm.backref('access_grants', cascade="all, delete"))
def __repr__(self):
return '<Access {} {}->{}:{}>'.format(
self.id, self.user_id, self.repo_id, self.mode)
class Redirect(Base):
@declared_attr
def __tablename__(cls):
return "redirect"
id = sa.Column(sa.Integer, primary_key=True)
created = sa.Column(sa.DateTime, nullable=False)
name = sa.Column(sa.Unicode(256), nullable=False)
path = sa.Column(sa.Unicode(1024))
@declared_attr
def owner_id(cls):
return sa.Column(sa.Integer, sa.ForeignKey('user.id'), nullable=False)
@declared_attr
def owner(cls):
return sa.orm.relationship('User')
@declared_attr
def new_repo_id(cls):
return sa.Column(
sa.Integer,
sa.ForeignKey('repository.id', ondelete="CASCADE"),
nullable=False)
@declared_attr
def new_repo(cls):
return sa.orm.relationship('Repository')
class RepoVisibility(Enum):
# NOTE: SQLAlchemy uses the enum member names, not the values.
# The values are used by templates. Therfore, we capitalize both.
PUBLIC = 'PUBLIC'
PRIVATE = 'PRIVATE'
UNLISTED = 'UNLISTED'
class Repository(Base):
@declared_attr
@ -41,10 +115,7 @@ class Repository(Base):
name = sa.Column(sa.Unicode(256), nullable=False)
description = sa.Column(sa.Unicode(1024))
path = sa.Column(sa.Unicode(1024))
visibility = sa.Column(
sau.ChoiceType(RepoVisibility, impl=sa.String()),
nullable=False,
default=RepoVisibility.public)
visibility = sa.Column(postgresql.ENUM(RepoVisibility), nullable=False)
readme = sa.Column(sa.Unicode)
clone_status = sa.Column(postgresql.ENUM(
'NONE', 'IN_PROGRESS', 'COMPLETE', 'ERROR'), nullable=False)
@ -58,6 +129,8 @@ class Repository(Base):
def owner(cls):
return sa.orm.relationship('User', backref=sa.orm.backref('repos'))
# This is only used by the REST API
# TODO: Remove this when the REST API is phased out
def to_dict(self):
return {
"id": self.id,
@ -66,7 +139,7 @@ class Repository(Base):
"name": self.name,
"owner": self.owner.to_dict(short=True),
"description": self.description,
"visibility": self.visibility,
"visibility": self.visibility.value.lower(),
}
@property