~edwargix/git.sr.ht

cbccdd195329ea1b0cf360fb6448857557853716 — Drew DeVault 7 years ago 76d2f1c
Implement renaming repositories
M git-srht-keys => git-srht-keys +16 -1
@@ 11,6 11,12 @@ from grp import getgrnam
from srht.config import cfg, cfgi, load_config
load_config("git")

root = (
    cfg("server", "protocol") +
    "://" +
    cfg("server", "domain")
)

_log = None
try:
    _log = open("/var/log/git-srht-push", "a")


@@ 102,7 108,7 @@ def shell():

    from srht.database import DbSession
    db = DbSession(cfg("sr.ht", "connection-string"))
    from gitsrht.types import User, Repository, RepoVisibility
    from gitsrht.types import User, Repository, RepoVisibility, Redirect
    from gitsrht.access import has_access, UserAccess
    db.init()



@@ 128,6 134,15 @@ def shell():

    repo = Repository.query.filter(Repository.path == path).first()
    if not repo:
        repo = Redirect.query.filter(Redirect.path == path).first()
        if repo:
            repo = repo.new_repo
            sys.stderr.write("\n\033[93m\tNOTICE\033[0m\n")
            sys.stderr.write("\tThis repository has moved:\n")
            # TODO: orgs
            sys.stderr.write("\t{}/~{}/{}\n".format(
                root, repo.owner.username, repo.name))
            sys.stderr.write("\tPlease update your remote.\n\n")
        sys.exit(128)
    
    if cmd[0] == "git-receive-pack":

M gitsrht/access.py => gitsrht/access.py +10 -1
@@ 1,6 1,7 @@
from flask import abort
from enum import IntFlag
from flask_login import current_user
from gitsrht.types import User, Repository, RepoVisibility
from gitsrht.types import User, Repository, RepoVisibility, Redirect

class UserAccess(IntFlag):
    none = 0


@@ 16,6 17,11 @@ def get_repo(owner_name, repo_name):
                .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


@@ 27,6 33,9 @@ def get_access(repo, user=None):
    # TODO: ACLs
    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:

A gitsrht/alembic/versions/f81294e9c2cf_add_redirect_table.py => gitsrht/alembic/versions/f81294e9c2cf_add_redirect_table.py +30 -0
@@ 0,0 1,30 @@
"""Add redirect table

Revision ID: f81294e9c2cf
Revises: daf28fd9e001
Create Date: 2017-11-12 14:41:59.099778

"""

# revision identifiers, used by Alembic.
revision = 'f81294e9c2cf'
down_revision = 'daf28fd9e001'

from alembic import op
import sqlalchemy as sa


def upgrade():
    op.create_table('redirect',
        sa.Column('id', sa.Integer, primary_key=True),
        sa.Column('created', sa.DateTime, nullable=False),
        sa.Column('name', sa.Unicode(256), nullable=False),
        sa.Column('owner_id', sa.Integer, sa.ForeignKey('user.id'), nullable=False),
        sa.Column('path', sa.Unicode(1024)),
        sa.Column('new_repo_id', 
                sa.Integer,
                sa.ForeignKey('repository.id'),
                nullable=False))

def downgrade():
    op.drop_table('redirect')

M gitsrht/blueprints/api.py => gitsrht/blueprints/api.py +7 -2
@@ 1,5 1,5 @@
from flask import Blueprint, request, abort
from gitsrht.types import Repository, RepoVisibility, User, Webhook
from flask import Blueprint, request, redirect, abort, url_for
from gitsrht.types import Repository, RepoVisibility, User, Webhook, Redirect
from gitsrht.access import UserAccess, has_access, get_repo
from gitsrht.blueprints.public import check_repo
from gitsrht.repos import create_repo


@@ 81,6 81,9 @@ def repos_username_GET(owner):
@api.route("/api/repos/~<owner>/<name>")
def repos_by_name_GET(owner, name):
    user, repo = check_repo(owner, name)
    if isinstance(repo, Redirect):
        return redirect(url_for(".repos_by_name_GET",
            owner=owner, name=repo.new_repo.name))
    return repo_json(repo)

def prop(valid, resource, prop, **kwargs):


@@ 92,6 95,8 @@ def prop(valid, resource, prop, **kwargs):
@oauth("repos:write")
def repos_by_name_PUT(oauth_token, owner, name):
    user, repo = check_repo(owner, name, authorized=oauth_token.user)
    if isinstance(repo, Redirect):
        abort(404)
    valid = Validation(request)
    prop(valid, repo, "visibility", cls=RepoVisibility)
    prop(valid, repo, "description", cls=str)

M gitsrht/blueprints/manage.py => gitsrht/blueprints/manage.py +38 -3
@@ 1,12 1,13 @@
from flask import Blueprint, request, render_template, redirect, session
from flask import Blueprint, request, render_template
from flask import redirect, session, url_for
from flask_login import current_user
from srht.config import cfg
from srht.database import db
from srht.validation import Validation
from gitsrht.types import Repository, RepoVisibility
from gitsrht.types import Repository, RepoVisibility, Redirect
from gitsrht.decorators import loginrequired
from gitsrht.access import check_access, UserAccess
from gitsrht.repos import create_repo
from gitsrht.repos import create_repo, rename_repo
import shutil

manage = Blueprint('manage', __name__)


@@ 36,34 37,68 @@ def create():
@loginrequired
def settings_info(owner_name, repo_name):
    owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
    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)

@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, Redirect):
        repo = repo.new_repo
    valid = Validation(request)
    desc = valid.optional("description", default=repo.description)
    repo.description = desc
    db.session.commit()
    return redirect("/{}/{}/settings/info".format(owner_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, 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)

@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, Redirect):
        repo = repo.new_repo
    valid = Validation(request)
    repo = rename_repo(owner, repo, valid)
    if not repo:
        return valid.response
    return redirect("/{}/{}".format(owner_name, repo.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, 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)

@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, 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)

@manage.route("/<owner_name>/<repo_name>/settings/delete", methods=["POST"])
@loginrequired
def settings_delete_POST(owner_name, repo_name):
    owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
    if isinstance(repo, Redirect):
        repo = repo.new_repo
    shutil.rmtree(repo.path)
    db.session.delete(repo)
    db.session.commit()

M gitsrht/blueprints/public.py => gitsrht/blueprints/public.py +18 -7
@@ 1,8 1,9 @@
from flask import Blueprint, Response, request, render_template, abort, stream_with_context
from flask import Blueprint, Response, request, redirect, url_for
from flask import render_template, abort, stream_with_context
from flask_login import current_user
import requests
from srht.config import cfg
from gitsrht.types import User, Repository, RepoVisibility
from gitsrht.types import User, Repository, RepoVisibility, Redirect
from gitsrht.access import UserAccess, has_access, get_repo
from sqlalchemy import or_



@@ 39,6 40,10 @@ def check_repo(user, repo, authorized=current_user):
@public.route("/<owner_name>/<repo_name>/<path:cgit_path>")
def cgit_passthrough(owner_name, repo_name, cgit_path):
    owner, repo = get_repo(owner_name, repo_name)
    if isinstance(repo, Redirect):
        return redirect(url_for(".cgit_passthrough",
            owner_name=owner_name, repo_name=repo.new_repo.name,
            cgit_path=cgit_path))
    if not has_access(repo, UserAccess.read):
        abort(404)
    r = requests.get("{}/{}".format(upstream, request.full_path))


@@ 70,12 75,18 @@ def cgit_passthrough(owner_name, repo_name, cgit_path):
            cgit_html=text, owner=owner, repo=repo,
            has_access=has_access, UserAccess=UserAccess)

@public.route("/<owner_name>/<repo_name>/patch", defaults={"path": None})
@public.route("/<owner_name>/<repo_name>/patch/<path:path>")
@public.route("/<owner_name>/<repo_name>/plain/<path:path>")
@public.route("/<owner_name>/<repo_name>/snapshot/<path:path>")
def cgit_plain(owner_name, repo_name, path):
@public.route("/<owner_name>/<repo_name>/<op>", defaults={"path": None})
@public.route("/<owner_name>/<repo_name>/<op>/<path:path>")
@public.route("/<owner_name>/<repo_name>/<op>/<path:path>")
@public.route("/<owner_name>/<repo_name>/<op>/<path:path>")
def cgit_plain(owner_name, repo_name, op, path):
    if not op in ["patch", "plain", "snapshot"]:
        abort(404)
    owner, repo = get_repo(owner_name, repo_name)
    if isinstance(repo, Redirect):
        return redirect(url_for(".cgit_plain",
            owner_name=owner_name, repo_name=repo.new_repo.name,
            op=op, path=path))
    if not has_access(repo, UserAccess.read):
        abort(404)
    r = requests.get("{}/{}".format(upstream, request.full_path), stream=True)

M gitsrht/repos.py => gitsrht/repos.py +40 -8
@@ 1,26 1,31 @@
import subprocess
from srht.database import db
from srht.config import cfg
from gitsrht.types import Repository, RepoVisibility
from gitsrht.types import Repository, RepoVisibility, Redirect
import re
import os

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

def validate_name(valid, owner, repo_name):
    if not valid.ok:
        return
    valid.expect(re.match(r'^[a-z._-][a-z0-9._-]*$', repo_name),
            "Name must match [a-z._-][a-z0-9._-]*", field="name")
    existing = (Repository.query
            .filter(Repository.owner_id == owner.id)
            .filter(Repository.name.ilike("%" + repo_name + "%"))
            .first())
    valid.expect(not existing, "This name is already in use.", field="name")

def create_repo(valid, owner):
    repo_name = valid.require("name", friendly_name="Name")
    valid.expect(not repo_name or re.match(r'^[a-z._-][a-z0-9._-]*$', repo_name),
            "Name must match [a-z._-][a-z0-9._-]*", field="name")
    description = valid.optional("description")
    visibility = valid.optional("visibility",
            default="public",
            cls=RepoVisibility)
    repos = Repository.query.filter(Repository.owner_id == owner.id)\
            .order_by(Repository.updated.desc()).all()
    valid.expect(not repo_name or not repo_name in [r.name for r in repos],
            "This name is already in use.", field="name")

    validate_name(valid, owner, repo_name)
    if not valid.ok:
        return None



@@ 41,3 46,30 @@ def create_repo(valid, owner):
    subprocess.run(["ln", "-s", post_update, os.path.join(repo.path, "hooks", "update")])
    subprocess.run(["ln", "-s", post_update, os.path.join(repo.path, "hooks", "post-update")])
    return repo

def rename_repo(owner, repo, valid):
    repo_name = valid.require("name")
    valid.expect(repo.name != repo_name,
            "This is the same name as before.", field="name")
    if not valid.ok:
        return None
    validate_name(valid, owner, repo_name)
    if not valid.ok:
        return None

    _redirect = Redirect()
    _redirect.name = repo.name
    _redirect.path = repo.path
    _redirect.owner_id = repo.owner_id
    _redirect.new_repo_id = repo.id
    db.session.add(_redirect)

    new_path = os.path.join(repos_path, "~" + owner.username, repo_name)

    subprocess.run(["mv", repo.path, new_path])

    repo.path = new_path
    repo.name = repo_name
    db.session.commit()

    return repo

M gitsrht/templates/settings_info.html => gitsrht/templates/settings_info.html +6 -2
@@ 4,8 4,12 @@
  <div class="col-md-6">
    <form method="POST">
      <div class="form-group">
        <label for="name">
          Repository name <span class="text-muted">(you can't edit this)</span>
        <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"

A gitsrht/templates/settings_rename.html => gitsrht/templates/settings_rename.html +32 -0
@@ 0,0 1,32 @@
{% extends "settings.html" %}
{% block content %}
<div class="row">
  <div class="col-md-6">
    <h3>Rename Repository</h3>
    <form method="POST">
      <div class="form-group {{valid.cls('name')}}">
        <label for="name" style="display: block">
          New repository name:
        </label>
        <input
          type="text"
          class="form-control"
          id="name"
          name="name"
          value="{{name or repo.name}}" />
        {{valid.summary('name')}}
      </div>
      <p>
        This will change the canonical URL of your repository. A redirect from
        the old URL to the new one will be in place until you create another
        repository with the same name. You will have to manually update your
        local git remotes, submodules, etc.
        {# TODO: Set up redirects for git access too #}
      </p>
      <button type="submit" class="btn btn-default pull-right">
        Rename repository
      </button>
    </form>
  </div>
</div>
{% endblock %}

M gitsrht/templates/tabs.html => gitsrht/templates/tabs.html +5 -0
@@ 11,6 11,11 @@
    href="/{{ owner.canonical_name }}/{{ repo.name }}"
  >« back</a>
</li>
{% if request.path.endswith("/rename") %}
<li class="nav-item">
  {{ link("/rename", "rename") }}
</li>
{% endif %}
<li class="nav-item">
  {{ link("/info", "info") }}
</li>

M gitsrht/types/__init__.py => gitsrht/types/__init__.py +1 -0
@@ 2,3 2,4 @@ from .user import User
from .repository import Repository, RepoVisibility
from .oauthtoken import OAuthToken
from .webhook import Webhook
from .redirect import Redirect

A gitsrht/types/redirect.py => gitsrht/types/redirect.py +18 -0
@@ 0,0 1,18 @@
import sqlalchemy as sa
import sqlalchemy_utils as sau
from srht.database import Base
from enum import Enum

class Redirect(Base):
    __tablename__ = 'redirect'
    id = sa.Column(sa.Integer, primary_key=True)
    created = sa.Column(sa.DateTime, nullable=False)
    name = sa.Column(sa.Unicode(256), nullable=False)
    owner_id = sa.Column(sa.Integer, sa.ForeignKey('user.id'), nullable=False)
    owner = sa.orm.relationship('User')
    path = sa.Column(sa.Unicode(1024))
    new_repo_id = sa.Column(
            sa.Integer,
            sa.ForeignKey('repository.id'),
            nullable=False)
    new_repo = sa.orm.relationship('Repository')