M api/graph/model/repository.go => api/graph/model/repository.go +4 -18
@@ 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:
M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +8 -21
@@ 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),
})
M api/loaders/middleware.go => api/loaders/middleware.go +3 -3
@@ 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)
M api/webhooks/legacy.go => api/webhooks/legacy.go +3 -2
@@ 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
M gitsrht-periodic => gitsrht-periodic +1 -2
@@ 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",
M gitsrht-shell/main.go => gitsrht-shell/main.go +5 -5
@@ 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
M gitsrht-update-hook/submitter.go => gitsrht-update-hook/submitter.go +1 -1
@@ 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
A gitsrht/access.py => gitsrht/access.py +93 -0
@@ 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
A gitsrht/alembic/versions/64fcd80183c8_add_visibility_enum.py => gitsrht/alembic/versions/64fcd80183c8_add_visibility_enum.py +37 -0
@@ 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;
+ """)
M gitsrht/app.py => gitsrht/app.py +21 -9
@@ 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()
M gitsrht/blueprints/api/__init__.py => gitsrht/blueprints/api/__init__.py +2 -2
@@ 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
M gitsrht/blueprints/api/info.py => gitsrht/blueprints/api/info.py +7 -7
@@ 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})
M gitsrht/blueprints/api/porcelain.py => gitsrht/blueprints/api/porcelain.py +1 -1
@@ 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
M gitsrht/blueprints/artifacts.py => gitsrht/blueprints/artifacts.py +1 -1
@@ 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
A gitsrht/blueprints/auth.py => gitsrht/blueprints/auth.py +22 -0
@@ 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
M gitsrht/blueprints/email.py => gitsrht/blueprints/email.py +1 -1
@@ 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
M gitsrht/blueprints/manage.py => gitsrht/blueprints/manage.py +15 -23
@@ 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)
A gitsrht/blueprints/public.py => gitsrht/blueprints/public.py +54 -0
@@ 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)
M gitsrht/blueprints/repo.py => gitsrht/blueprints/repo.py +1 -1
@@ 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
M gitsrht/repos.py => gitsrht/repos.py +67 -71
@@ 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(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 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
- # 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)
A gitsrht/templates/base.html => gitsrht/templates/base.html +5 -0
@@ 0,0 1,5 @@
+{% extends "layout.html" %}
+
+{% block body %}
+{% block content %}{% endblock %}
+{% endblock %}
M gitsrht/templates/dashboard.html => gitsrht/templates/dashboard.html +2 -2
@@ 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>
M gitsrht/templates/repo.html => gitsrht/templates/repo.html +6 -6
@@ 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>
M gitsrht/templates/settings_info.html => gitsrht/templates/settings_info.html +3 -3
@@ 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>
M gitsrht/templates/summary.html => gitsrht/templates/summary.html +2 -2
@@ 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
M gitsrht/templates/user.html => gitsrht/templates/user.html +2 -2
@@ 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>
M gitsrht/types/__init__.py => gitsrht/types/__init__.py +84 -11
@@ 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