Compare commits
No commits in common. "a5aac9a33701e3d4852fba13520771e6de82aac0" and "c56bf4fb49c986e9b653f635a81937a3ef433a5e" have entirely different histories.
a5aac9a337
...
c56bf4fb49
@ -31,8 +31,7 @@ dependencies = [
|
|||||||
"emoji",
|
"emoji",
|
||||||
"aiofiles",
|
"aiofiles",
|
||||||
"PyJWT",
|
"PyJWT",
|
||||||
"multiavatar",
|
"multiavatar"
|
||||||
"gitpython",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import uvloop
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import asyncio
|
|
||||||
from snek.app import Application
|
from snek.app import Application
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Run the web application.")
|
parser = argparse.ArgumentParser(description="Run the web application.")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--port",
|
"--port",
|
||||||
|
|||||||
@ -39,10 +39,6 @@ from snek.view.logout import LogoutView
|
|||||||
from snek.view.register import RegisterView
|
from snek.view.register import RegisterView
|
||||||
from snek.view.rpc import RPCView
|
from snek.view.rpc import RPCView
|
||||||
from snek.view.search_user import SearchUserView
|
from snek.view.search_user import SearchUserView
|
||||||
from snek.view.settings.repositories import RepositoriesIndexView
|
|
||||||
from snek.view.settings.repositories import RepositoriesCreateView
|
|
||||||
from snek.view.settings.repositories import RepositoriesUpdateView
|
|
||||||
from snek.view.settings.repositories import RepositoriesDeleteView
|
|
||||||
from snek.view.settings.index import SettingsIndexView
|
from snek.view.settings.index import SettingsIndexView
|
||||||
from snek.view.settings.profile import SettingsProfileView
|
from snek.view.settings.profile import SettingsProfileView
|
||||||
from snek.view.stats import StatsView
|
from snek.view.stats import StatsView
|
||||||
@ -52,7 +48,6 @@ from snek.view.upload import UploadView
|
|||||||
from snek.view.user import UserView
|
from snek.view.user import UserView
|
||||||
from snek.view.web import WebView
|
from snek.view.web import WebView
|
||||||
from snek.webdav import WebdavApplication
|
from snek.webdav import WebdavApplication
|
||||||
from snek.sgit import GitApplication
|
|
||||||
|
|
||||||
SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
|
SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
|
||||||
|
|
||||||
@ -180,14 +175,13 @@ class Application(BaseApplication):
|
|||||||
self.router.add_view("/drive/{drive}.json", DriveView)
|
self.router.add_view("/drive/{drive}.json", DriveView)
|
||||||
self.router.add_view("/stats.json", StatsView)
|
self.router.add_view("/stats.json", StatsView)
|
||||||
self.router.add_view("/user/{user}.html", UserView)
|
self.router.add_view("/user/{user}.html", UserView)
|
||||||
self.router.add_view("/settings/repositories/index.html", RepositoriesIndexView)
|
|
||||||
self.router.add_view("/settings/repositories/create.html", RepositoriesCreateView)
|
|
||||||
self.router.add_view("/settings/repositories/repository/{name}/update.html", RepositoriesUpdateView)
|
|
||||||
self.router.add_view("/settings/repositories/respository/{name}/delete.html", RepositoriesDeleteView)
|
|
||||||
self.webdav = WebdavApplication(self)
|
self.webdav = WebdavApplication(self)
|
||||||
self.git = GitApplication(self)
|
|
||||||
self.add_subapp("/webdav", self.webdav)
|
self.add_subapp("/webdav", self.webdav)
|
||||||
self.add_subapp("/git",self.git)
|
|
||||||
|
self.add_subapp(
|
||||||
|
"/docs",
|
||||||
|
DocsApplication(path=pathlib.Path(__file__).parent.joinpath("docs")),
|
||||||
|
)
|
||||||
|
|
||||||
#self.router.add_get("/{file_path:.*}", self.static_handler)
|
#self.router.add_get("/{file_path:.*}", self.static_handler)
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,6 @@ from snek.mapper.drive_item import DriveItemMapper
|
|||||||
from snek.mapper.notification import NotificationMapper
|
from snek.mapper.notification import NotificationMapper
|
||||||
from snek.mapper.user import UserMapper
|
from snek.mapper.user import UserMapper
|
||||||
from snek.mapper.user_property import UserPropertyMapper
|
from snek.mapper.user_property import UserPropertyMapper
|
||||||
from snek.mapper.repository import RepositoryMapper
|
|
||||||
from snek.system.object import Object
|
from snek.system.object import Object
|
||||||
|
|
||||||
|
|
||||||
@ -24,7 +23,6 @@ def get_mappers(app=None):
|
|||||||
"drive_item": DriveItemMapper(app=app),
|
"drive_item": DriveItemMapper(app=app),
|
||||||
"drive": DriveMapper(app=app),
|
"drive": DriveMapper(app=app),
|
||||||
"user_property": UserPropertyMapper(app=app),
|
"user_property": UserPropertyMapper(app=app),
|
||||||
"repository": RepositoryMapper(app=app),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
from snek.model.repository import RepositoryModel
|
|
||||||
from snek.system.mapper import BaseMapper
|
|
||||||
|
|
||||||
|
|
||||||
class RepositoryMapper(BaseMapper):
|
|
||||||
model_class = RepositoryModel
|
|
||||||
table_name = "repository"
|
|
||||||
@ -10,7 +10,6 @@ from snek.model.drive_item import DriveItemModel
|
|||||||
from snek.model.notification import NotificationModel
|
from snek.model.notification import NotificationModel
|
||||||
from snek.model.user import UserModel
|
from snek.model.user import UserModel
|
||||||
from snek.model.user_property import UserPropertyModel
|
from snek.model.user_property import UserPropertyModel
|
||||||
from snek.model.repository import RepositoryModel
|
|
||||||
from snek.system.object import Object
|
from snek.system.object import Object
|
||||||
|
|
||||||
|
|
||||||
@ -26,7 +25,6 @@ def get_models():
|
|||||||
"drive": DriveModel,
|
"drive": DriveModel,
|
||||||
"notification": NotificationModel,
|
"notification": NotificationModel,
|
||||||
"user_property": UserPropertyModel,
|
"user_property": UserPropertyModel,
|
||||||
"repository": RepositoryModel,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
from snek.model.user import UserModel
|
|
||||||
from snek.system.model import BaseModel, ModelField
|
|
||||||
|
|
||||||
|
|
||||||
class RepositoryModel(BaseModel):
|
|
||||||
|
|
||||||
user_uid = ModelField(name="user_uid", required=True, kind=str)
|
|
||||||
|
|
||||||
name = ModelField(name="name", required=True, kind=str)
|
|
||||||
|
|
||||||
is_private = ModelField(name="is_private", required=False, kind=bool)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -11,7 +11,6 @@ from snek.service.socket import SocketService
|
|||||||
from snek.service.user import UserService
|
from snek.service.user import UserService
|
||||||
from snek.service.user_property import UserPropertyService
|
from snek.service.user_property import UserPropertyService
|
||||||
from snek.service.util import UtilService
|
from snek.service.util import UtilService
|
||||||
from snek.service.repository import RepositoryService
|
|
||||||
from snek.system.object import Object
|
from snek.system.object import Object
|
||||||
|
|
||||||
|
|
||||||
@ -30,7 +29,6 @@ def get_services(app):
|
|||||||
"drive": DriveService(app=app),
|
"drive": DriveService(app=app),
|
||||||
"drive_item": DriveItemService(app=app),
|
"drive_item": DriveItemService(app=app),
|
||||||
"user_property": UserPropertyService(app=app),
|
"user_property": UserPropertyService(app=app),
|
||||||
"repository": RepositoryService(app=app),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
from snek.system.service import BaseService
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
class RepositoryService(BaseService):
|
|
||||||
mapper_name = "repository"
|
|
||||||
|
|
||||||
async def exists(self, user_uid, name, **kwargs):
|
|
||||||
kwargs["user_uid"] = user_uid
|
|
||||||
kwargs["name"] = name
|
|
||||||
return await super().exists(**kwargs)
|
|
||||||
|
|
||||||
async def init(self, user_uid, name):
|
|
||||||
repository_path = await self.services.user.get_repository_path(user_uid)
|
|
||||||
if not repository_path.exists():
|
|
||||||
repository_path.mkdir(parents=True)
|
|
||||||
repository_path = repository_path.joinpath(name)
|
|
||||||
command = ['git', 'init', '--bare', repository_path]
|
|
||||||
process = await asyncio.subprocess.create_subprocess_exec(
|
|
||||||
*command,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE
|
|
||||||
)
|
|
||||||
stdout, stderr = await process.communicate()
|
|
||||||
return process.returncode == 0
|
|
||||||
|
|
||||||
async def create(self, user_uid, name,is_private=False):
|
|
||||||
if await self.exists(user_uid=user_uid, name=name):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not await self.init(user_uid=user_uid, name=name):
|
|
||||||
return False
|
|
||||||
|
|
||||||
model = await self.new()
|
|
||||||
model["user_uid"] = user_uid
|
|
||||||
model["name"] = name
|
|
||||||
model["is_private"] = is_private
|
|
||||||
return await self.save(model)
|
|
||||||
@ -42,9 +42,6 @@ class UserService(BaseService):
|
|||||||
def get_admin_uids(self):
|
def get_admin_uids(self):
|
||||||
return self.mapper.get_admin_uids()
|
return self.mapper.get_admin_uids()
|
||||||
|
|
||||||
async def get_repository_path(self, user_uid):
|
|
||||||
return pathlib.Path(f"./drive/repositories/{user_uid}")
|
|
||||||
|
|
||||||
async def get_static_path(self, user_uid):
|
async def get_static_path(self, user_uid):
|
||||||
path = pathlib.Path(f"./drive/{user_uid}/snek/static")
|
path = pathlib.Path(f"./drive/{user_uid}/snek/static")
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
|
|||||||
472
src/snek/sgit.py
472
src/snek/sgit.py
@ -1,472 +0,0 @@
|
|||||||
import os
|
|
||||||
import aiohttp
|
|
||||||
from aiohttp import web
|
|
||||||
import git
|
|
||||||
import shutil
|
|
||||||
import json
|
|
||||||
import tempfile
|
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import base64
|
|
||||||
import pathlib
|
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
||||||
logger = logging.getLogger('git_server')
|
|
||||||
|
|
||||||
class GitApplication(web.Application):
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
self.parent = parent
|
|
||||||
super().__init__(client_max_size=100*1024*1024)
|
|
||||||
self.REPO_DIR = "drive/repositories/3177f85e-dbb3-4406-993e-3d3748fea545"
|
|
||||||
self.USERS = {
|
|
||||||
'x': 'x',
|
|
||||||
'bob': 'bobpass',
|
|
||||||
}
|
|
||||||
self.add_routes([
|
|
||||||
web.post('/create/{repo_name}', self.create_repository),
|
|
||||||
web.delete('/delete/{repo_name}', self.delete_repository),
|
|
||||||
web.get('/clone/{repo_name}', self.clone_repository),
|
|
||||||
web.post('/push/{repo_name}', self.push_repository),
|
|
||||||
web.post('/pull/{repo_name}', self.pull_repository),
|
|
||||||
web.get('/status/{repo_name}', self.status_repository),
|
|
||||||
web.get('/list', self.list_repositories),
|
|
||||||
web.get('/branches/{repo_name}', self.list_branches),
|
|
||||||
web.post('/branches/{repo_name}', self.create_branch),
|
|
||||||
web.get('/log/{repo_name}', self.commit_log),
|
|
||||||
web.get('/file/{repo_name}/{file_path:.*}', self.file_content),
|
|
||||||
web.get('/{path:.+}/info/refs', self.git_smart_http),
|
|
||||||
web.post('/{path:.+}/git-upload-pack', self.git_smart_http),
|
|
||||||
web.post('/{path:.+}/git-receive-pack', self.git_smart_http),
|
|
||||||
web.get('/{repo_name}.git/info/refs', self.git_smart_http),
|
|
||||||
web.post('/{repo_name}.git/git-upload-pack', self.git_smart_http),
|
|
||||||
web.post('/{repo_name}.git/git-receive-pack', self.git_smart_http),
|
|
||||||
])
|
|
||||||
|
|
||||||
async def check_basic_auth(self, request):
|
|
||||||
# For now, always return the fixed user and path
|
|
||||||
return "retoor", pathlib.Path(self.REPO_DIR)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def require_auth(handler):
|
|
||||||
async def wrapped(self, request, *args, **kwargs):
|
|
||||||
username, repository_path = await self.check_basic_auth(request)
|
|
||||||
if not username or not repository_path:
|
|
||||||
return web.Response(status=401, headers={'WWW-Authenticate': 'Basic'}, text='Authentication required')
|
|
||||||
request['username'] = username
|
|
||||||
request['repository_path'] = repository_path
|
|
||||||
return await handler(self, request, *args, **kwargs)
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
def repo_path(self, repository_path, repo_name):
|
|
||||||
return repository_path.joinpath(repo_name + '.git')
|
|
||||||
|
|
||||||
def check_repo_exists(self, repository_path, repo_name):
|
|
||||||
repo_dir = self.repo_path(repository_path, repo_name)
|
|
||||||
if not os.path.exists(repo_dir):
|
|
||||||
return web.Response(text="Repository not found", status=404)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@require_auth
|
|
||||||
async def create_repository(self, request):
|
|
||||||
username = request['username']
|
|
||||||
repo_name = request.match_info['repo_name']
|
|
||||||
repository_path = request['repository_path']
|
|
||||||
if not repo_name or '/' in repo_name or '..' in repo_name:
|
|
||||||
return web.Response(text="Invalid repository name", status=400)
|
|
||||||
repo_dir = self.repo_path(repository_path, repo_name)
|
|
||||||
if os.path.exists(repo_dir):
|
|
||||||
return web.Response(text="Repository already exists", status=400)
|
|
||||||
try:
|
|
||||||
git.Repo.init(repo_dir, bare=True)
|
|
||||||
logger.info(f"Created repository: {repo_name} for user {username}")
|
|
||||||
return web.Response(text=f"Created repository {repo_name}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error creating repository {repo_name}: {str(e)}")
|
|
||||||
return web.Response(text=f"Error creating repository: {str(e)}", status=500)
|
|
||||||
|
|
||||||
@require_auth
|
|
||||||
async def delete_repository(self, request):
|
|
||||||
username = request['username']
|
|
||||||
repo_name = request.match_info['repo_name']
|
|
||||||
repository_path = request['repository_path']
|
|
||||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
|
||||||
if error_response:
|
|
||||||
return error_response
|
|
||||||
try:
|
|
||||||
shutil.rmtree(self.repo_path(repository_path, repo_name))
|
|
||||||
logger.info(f"Deleted repository: {repo_name} for user {username}")
|
|
||||||
return web.Response(text=f"Deleted repository {repo_name}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error deleting repository {repo_name}: {str(e)}")
|
|
||||||
return web.Response(text=f"Error deleting repository: {str(e)}", status=500)
|
|
||||||
|
|
||||||
@require_auth
|
|
||||||
async def clone_repository(self, request):
|
|
||||||
username = request['username']
|
|
||||||
repo_name = request.match_info['repo_name']
|
|
||||||
repository_path = request['repository_path']
|
|
||||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
|
||||||
if error_response:
|
|
||||||
return error_response
|
|
||||||
host = request.host
|
|
||||||
clone_url = f"http://{host}/{repo_name}.git"
|
|
||||||
response_data = {
|
|
||||||
"repository": repo_name,
|
|
||||||
"clone_command": f"git clone {clone_url}",
|
|
||||||
"clone_url": clone_url
|
|
||||||
}
|
|
||||||
return web.json_response(response_data)
|
|
||||||
|
|
||||||
@require_auth
|
|
||||||
async def push_repository(self, request):
|
|
||||||
username = request['username']
|
|
||||||
repo_name = request.match_info['repo_name']
|
|
||||||
repository_path = request['repository_path']
|
|
||||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
|
||||||
if error_response:
|
|
||||||
return error_response
|
|
||||||
try:
|
|
||||||
data = await request.json()
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return web.Response(text="Invalid JSON data", status=400)
|
|
||||||
commit_message = data.get('commit_message', 'Update from server')
|
|
||||||
branch = data.get('branch', 'main')
|
|
||||||
changes = data.get('changes', [])
|
|
||||||
if not changes:
|
|
||||||
return web.Response(text="No changes provided", status=400)
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
|
||||||
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
|
||||||
for change in changes:
|
|
||||||
file_path = os.path.join(temp_dir, change.get('file', ''))
|
|
||||||
content = change.get('content', '')
|
|
||||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
|
||||||
with open(file_path, 'w') as f:
|
|
||||||
f.write(content)
|
|
||||||
temp_repo.git.add(A=True)
|
|
||||||
if not temp_repo.config_reader().has_section('user'):
|
|
||||||
temp_repo.config_writer().set_value("user", "name", "Git Server").release()
|
|
||||||
temp_repo.config_writer().set_value("user", "email", "git@server.local").release()
|
|
||||||
temp_repo.index.commit(commit_message)
|
|
||||||
origin = temp_repo.remote('origin')
|
|
||||||
origin.push(refspec=f"{branch}:{branch}")
|
|
||||||
logger.info(f"Pushed to repository: {repo_name} for user {username}")
|
|
||||||
return web.Response(text=f"Successfully pushed changes to {repo_name}")
|
|
||||||
|
|
||||||
@require_auth
|
|
||||||
async def pull_repository(self, request):
|
|
||||||
username = request['username']
|
|
||||||
repo_name = request.match_info['repo_name']
|
|
||||||
repository_path = request['repository_path']
|
|
||||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
|
||||||
if error_response:
|
|
||||||
return error_response
|
|
||||||
try:
|
|
||||||
data = await request.json()
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
data = {}
|
|
||||||
remote_url = data.get('remote_url')
|
|
||||||
branch = data.get('branch', 'main')
|
|
||||||
if not remote_url:
|
|
||||||
return web.Response(text="Remote URL is required", status=400)
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
|
||||||
try:
|
|
||||||
local_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
|
||||||
remote_name = "pull_source"
|
|
||||||
try:
|
|
||||||
remote = local_repo.create_remote(remote_name, remote_url)
|
|
||||||
except git.GitCommandError:
|
|
||||||
remote = local_repo.remote(remote_name)
|
|
||||||
remote.set_url(remote_url)
|
|
||||||
remote.fetch()
|
|
||||||
local_repo.git.merge(f"{remote_name}/{branch}")
|
|
||||||
origin = local_repo.remote('origin')
|
|
||||||
origin.push()
|
|
||||||
logger.info(f"Pulled to repository {repo_name} from {remote_url} for user {username}")
|
|
||||||
return web.Response(text=f"Successfully pulled changes from {remote_url} to {repo_name}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error pulling to {repo_name}: {str(e)}")
|
|
||||||
return web.Response(text=f"Error pulling changes: {str(e)}", status=500)
|
|
||||||
|
|
||||||
@require_auth
|
|
||||||
async def status_repository(self, request):
|
|
||||||
username = request['username']
|
|
||||||
repo_name = request.match_info['repo_name']
|
|
||||||
repository_path = request['repository_path']
|
|
||||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
|
||||||
if error_response:
|
|
||||||
return error_response
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
|
||||||
try:
|
|
||||||
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
|
||||||
branches = [b.name for b in temp_repo.branches]
|
|
||||||
active_branch = temp_repo.active_branch.name
|
|
||||||
commits = []
|
|
||||||
for commit in list(temp_repo.iter_commits(max_count=5)):
|
|
||||||
commits.append({
|
|
||||||
"id": commit.hexsha,
|
|
||||||
"author": f"{commit.author.name} <{commit.author.email}>",
|
|
||||||
"date": commit.committed_datetime.isoformat(),
|
|
||||||
"message": commit.message
|
|
||||||
})
|
|
||||||
files = []
|
|
||||||
for root, dirs, filenames in os.walk(temp_dir):
|
|
||||||
if '.git' in root:
|
|
||||||
continue
|
|
||||||
for filename in filenames:
|
|
||||||
full_path = os.path.join(root, filename)
|
|
||||||
rel_path = os.path.relpath(full_path, temp_dir)
|
|
||||||
files.append(rel_path)
|
|
||||||
status_info = {
|
|
||||||
"repository": repo_name,
|
|
||||||
"branches": branches,
|
|
||||||
"active_branch": active_branch,
|
|
||||||
"recent_commits": commits,
|
|
||||||
"files": files
|
|
||||||
}
|
|
||||||
return web.json_response(status_info)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error getting status for {repo_name}: {str(e)}")
|
|
||||||
return web.Response(text=f"Error getting repository status: {str(e)}", status=500)
|
|
||||||
|
|
||||||
@require_auth
|
|
||||||
async def list_repositories(self, request):
|
|
||||||
username = request['username']
|
|
||||||
try:
|
|
||||||
repos = []
|
|
||||||
user_dir = self.REPO_DIR
|
|
||||||
if os.path.exists(user_dir):
|
|
||||||
for item in os.listdir(user_dir):
|
|
||||||
item_path = os.path.join(user_dir, item)
|
|
||||||
if os.path.isdir(item_path) and item.endswith('.git'):
|
|
||||||
repos.append(item[:-4])
|
|
||||||
if request.query.get('format') == 'json':
|
|
||||||
return web.json_response({"repositories": repos})
|
|
||||||
else:
|
|
||||||
return web.Response(text="\n".join(repos) if repos else "No repositories found")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error listing repositories: {str(e)}")
|
|
||||||
return web.Response(text=f"Error listing repositories: {str(e)}", status=500)
|
|
||||||
|
|
||||||
@require_auth
|
|
||||||
async def list_branches(self, request):
|
|
||||||
username = request['username']
|
|
||||||
repo_name = request.match_info['repo_name']
|
|
||||||
repository_path = request['repository_path']
|
|
||||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
|
||||||
if error_response:
|
|
||||||
return error_response
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
|
||||||
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
|
||||||
branches = [b.name for b in temp_repo.branches]
|
|
||||||
return web.json_response({"branches": branches})
|
|
||||||
|
|
||||||
@require_auth
|
|
||||||
async def create_branch(self, request):
|
|
||||||
username = request['username']
|
|
||||||
repo_name = request.match_info['repo_name']
|
|
||||||
repository_path = request['repository_path']
|
|
||||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
|
||||||
if error_response:
|
|
||||||
return error_response
|
|
||||||
try:
|
|
||||||
data = await request.json()
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return web.Response(text="Invalid JSON data", status=400)
|
|
||||||
branch_name = data.get('branch_name')
|
|
||||||
start_point = data.get('start_point', 'HEAD')
|
|
||||||
if not branch_name:
|
|
||||||
return web.Response(text="Branch name is required", status=400)
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
|
||||||
try:
|
|
||||||
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
|
||||||
temp_repo.git.branch(branch_name, start_point)
|
|
||||||
temp_repo.git.push('origin', branch_name)
|
|
||||||
logger.info(f"Created branch {branch_name} in repository {repo_name} for user {username}")
|
|
||||||
return web.Response(text=f"Created branch {branch_name}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error creating branch {branch_name} in {repo_name}: {str(e)}")
|
|
||||||
return web.Response(text=f"Error creating branch: {str(e)}", status=500)
|
|
||||||
|
|
||||||
@require_auth
|
|
||||||
async def commit_log(self, request):
|
|
||||||
username = request['username']
|
|
||||||
repo_name = request.match_info['repo_name']
|
|
||||||
repository_path = request['repository_path']
|
|
||||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
|
||||||
if error_response:
|
|
||||||
return error_response
|
|
||||||
try:
|
|
||||||
limit = int(request.query.get('limit', 10))
|
|
||||||
branch = request.query.get('branch', 'main')
|
|
||||||
except ValueError:
|
|
||||||
return web.Response(text="Invalid limit parameter", status=400)
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
|
||||||
try:
|
|
||||||
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
|
||||||
commits = []
|
|
||||||
try:
|
|
||||||
for commit in list(temp_repo.iter_commits(branch, max_count=limit)):
|
|
||||||
commits.append({
|
|
||||||
"id": commit.hexsha,
|
|
||||||
"short_id": commit.hexsha[:7],
|
|
||||||
"author": f"{commit.author.name} <{commit.author.email}>",
|
|
||||||
"date": commit.committed_datetime.isoformat(),
|
|
||||||
"message": commit.message.strip()
|
|
||||||
})
|
|
||||||
except git.GitCommandError as e:
|
|
||||||
if "unknown revision or path" in str(e):
|
|
||||||
commits = []
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
return web.json_response({
|
|
||||||
"repository": repo_name,
|
|
||||||
"branch": branch,
|
|
||||||
"commits": commits
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error getting commit log for {repo_name}: {str(e)}")
|
|
||||||
return web.Response(text=f"Error getting commit log: {str(e)}", status=500)
|
|
||||||
|
|
||||||
@require_auth
|
|
||||||
async def file_content(self, request):
|
|
||||||
username = request['username']
|
|
||||||
repo_name = request.match_info['repo_name']
|
|
||||||
file_path = request.match_info.get('file_path', '')
|
|
||||||
branch = request.query.get('branch', 'main')
|
|
||||||
repository_path = request['repository_path']
|
|
||||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
|
||||||
if error_response:
|
|
||||||
return error_response
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
|
||||||
try:
|
|
||||||
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
|
||||||
try:
|
|
||||||
temp_repo.git.checkout(branch)
|
|
||||||
except git.GitCommandError:
|
|
||||||
return web.Response(text=f"Branch '{branch}' not found", status=404)
|
|
||||||
file_full_path = os.path.join(temp_dir, file_path)
|
|
||||||
if not os.path.exists(file_full_path):
|
|
||||||
return web.Response(text=f"File '{file_path}' not found", status=404)
|
|
||||||
if os.path.isdir(file_full_path):
|
|
||||||
files = os.listdir(file_full_path)
|
|
||||||
return web.json_response({
|
|
||||||
"repository": repo_name,
|
|
||||||
"path": file_path,
|
|
||||||
"type": "directory",
|
|
||||||
"contents": files
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
with open(file_full_path, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
return web.Response(text=content)
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
return web.Response(text=f"Cannot display binary file content for '{file_path}'", status=400)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error getting file content from {repo_name}: {str(e)}")
|
|
||||||
return web.Response(text=f"Error getting file content: {str(e)}", status=500)
|
|
||||||
|
|
||||||
@require_auth
|
|
||||||
async def git_smart_http(self, request):
|
|
||||||
username = request['username']
|
|
||||||
repository_path = request['repository_path']
|
|
||||||
path = request.path
|
|
||||||
async def get_repository_path():
|
|
||||||
req_path = path.lstrip('/')
|
|
||||||
if req_path.endswith('/info/refs'):
|
|
||||||
repo_name = req_path[:-len('/info/refs')]
|
|
||||||
elif req_path.endswith('/git-upload-pack'):
|
|
||||||
repo_name = req_path[:-len('/git-upload-pack')]
|
|
||||||
elif req_path.endswith('/git-receive-pack'):
|
|
||||||
repo_name = req_path[:-len('/git-receive-pack')]
|
|
||||||
else:
|
|
||||||
repo_name = req_path
|
|
||||||
if repo_name.endswith('.git'):
|
|
||||||
repo_name = repo_name[:-4]
|
|
||||||
repo_name = repo_name.lstrip('git/')
|
|
||||||
repo_dir = repository_path.joinpath(repo_name + '.git')
|
|
||||||
logger.info(f"Resolved repo path: {repo_dir}")
|
|
||||||
return repo_dir
|
|
||||||
async def handle_info_refs(service):
|
|
||||||
repo_path = await get_repository_path()
|
|
||||||
|
|
||||||
logger.info(f"handle_info_refs: {repo_path}")
|
|
||||||
if not os.path.exists(repo_path):
|
|
||||||
return web.Response(text="Repository not found", status=404)
|
|
||||||
cmd = [service, '--stateless-rpc', '--advertise-refs', str(repo_path)]
|
|
||||||
try:
|
|
||||||
process = await asyncio.create_subprocess_exec(
|
|
||||||
*cmd,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE
|
|
||||||
)
|
|
||||||
stdout, stderr = await process.communicate()
|
|
||||||
if process.returncode != 0:
|
|
||||||
logger.error(f"Git command failed: {stderr.decode()}")
|
|
||||||
return web.Response(text=f"Git error: {stderr.decode()}", status=500)
|
|
||||||
response = web.StreamResponse(
|
|
||||||
status=200,
|
|
||||||
reason='OK',
|
|
||||||
headers={
|
|
||||||
'Content-Type': f'application/x-{service}-advertisement',
|
|
||||||
'Cache-Control': 'no-cache'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
await response.prepare(request)
|
|
||||||
packet = f"# service={service}\n"
|
|
||||||
length = len(packet) + 4
|
|
||||||
header = f"{length:04x}"
|
|
||||||
await response.write(f"{header}{packet}0000".encode())
|
|
||||||
await response.write(stdout)
|
|
||||||
return response
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error handling info/refs: {str(e)}")
|
|
||||||
return web.Response(text=f"Server error: {str(e)}", status=500)
|
|
||||||
async def handle_service_rpc(service):
|
|
||||||
repo_path = await get_repository_path()
|
|
||||||
logger.info(f"handle_service_rpc: {repo_path}")
|
|
||||||
if not os.path.exists(repo_path):
|
|
||||||
return web.Response(text="Repository not found", status=404)
|
|
||||||
if not request.headers.get('Content-Type') == f'application/x-{service}-request':
|
|
||||||
return web.Response(text="Invalid Content-Type", status=403)
|
|
||||||
body = await request.read()
|
|
||||||
cmd = [service, '--stateless-rpc', str(repo_path)]
|
|
||||||
try:
|
|
||||||
process = await asyncio.create_subprocess_exec(
|
|
||||||
*cmd,
|
|
||||||
stdin=asyncio.subprocess.PIPE,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE
|
|
||||||
)
|
|
||||||
stdout, stderr = await process.communicate(input=body)
|
|
||||||
if process.returncode != 0:
|
|
||||||
logger.error(f"Git command failed: {stderr.decode()}")
|
|
||||||
return web.Response(text=f"Git error: {stderr.decode()}", status=500)
|
|
||||||
return web.Response(
|
|
||||||
body=stdout,
|
|
||||||
content_type=f'application/x-{service}-result'
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error handling service RPC: {str(e)}")
|
|
||||||
return web.Response(text=f"Server error: {str(e)}", status=500)
|
|
||||||
if request.method == 'GET' and path.endswith('/info/refs'):
|
|
||||||
service = request.query.get('service')
|
|
||||||
if service in ('git-upload-pack', 'git-receive-pack'):
|
|
||||||
return await handle_info_refs(service)
|
|
||||||
else:
|
|
||||||
return web.Response(text="Smart HTTP requires service parameter", status=400)
|
|
||||||
elif request.method == 'POST' and '/git-upload-pack' in path:
|
|
||||||
return await handle_service_rpc('git-upload-pack')
|
|
||||||
elif request.method == 'POST' and '/git-receive-pack' in path:
|
|
||||||
return await handle_service_rpc('git-receive-pack')
|
|
||||||
return web.Response(text="Not found", status=404)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
import uvloop
|
|
||||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
|
||||||
logger.info("Using uvloop for improved performance")
|
|
||||||
except ImportError:
|
|
||||||
logger.info("uvloop not available, using standard event loop")
|
|
||||||
app = GitApplication()
|
|
||||||
logger.info("Starting Git server on port 8080")
|
|
||||||
web.run_app(app, port=8080)
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
{% extends 'settings/index.html' %}
|
|
||||||
|
|
||||||
{% block header_text %}<h1><i class="fa-solid fa-plus"></i> Create Repository</h1>{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
div,input,label,button{
|
|
||||||
padding-bottom: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
form {
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
label { font-weight: bold; display: flex; align-items: center; gap: 0.5rem;}
|
|
||||||
input[type="text"] {
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid #ccc; border-radius: 5px;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
button, a.button {
|
|
||||||
background: #198754; color: #fff; border: none; border-radius: 5px;
|
|
||||||
padding: 0.1rem 0.8rem; text-decoration: none; cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
font-size: 1rem; display: inline-flex; align-items: center; gap: 0.4rem;
|
|
||||||
}
|
|
||||||
.
|
|
||||||
.cancel {
|
|
||||||
background: #6c757d;
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.container { max-width: 98vw; }
|
|
||||||
form { padding: 1rem; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<form action="/settings/repositories/create.html" method="post">
|
|
||||||
<div>
|
|
||||||
<label for="name"><i class="fa-solid fa-book"></i> Name</label>
|
|
||||||
<input type="text" id="name" name="name" required placeholder="Repository name">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="is_private" value="1">
|
|
||||||
<i class="fa-solid fa-lock"></i> Private
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<button type="submit"><i class="fa-solid fa-plus"></i> Create</button>
|
|
||||||
<button onclick="history.back()" class="cancel"><i class="fa-solid fa-arrow-left"></i> Back</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Delete Repository</title>
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
|
||||||
<style>
|
|
||||||
body { font-family: sans-serif; margin: 2rem; }
|
|
||||||
.container { max-width: 400px; margin: 0 auto; }
|
|
||||||
.confirm-box {
|
|
||||||
background: #ffe5e8;
|
|
||||||
border: 1.5px solid #dc3545;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
.repo-name {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin: 1rem 0;
|
|
||||||
color: #dc3545;
|
|
||||||
}
|
|
||||||
.actions {
|
|
||||||
display: flex; gap: 1rem; justify-content: center; margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
button, a {
|
|
||||||
background: #dc3545; color: #fff;
|
|
||||||
border: none; border-radius: 5px; padding: 0.6rem 1.2rem;
|
|
||||||
font-size: 1rem; cursor: pointer;
|
|
||||||
display: flex; align-items: center; gap: 0.5rem; text-decoration: none; justify-content: center;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
.cancel {
|
|
||||||
background: #6c757d;
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.container { max-width: 98vw; }
|
|
||||||
.confirm-box { padding: 1rem; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1><i class="fa-solid fa-trash-can"></i> Delete Repository</h1>
|
|
||||||
<div class="confirm-box">
|
|
||||||
<div>
|
|
||||||
<i class="fa-solid fa-triangle-exclamation" style="font-size:2rem; color:#dc3545;"></i>
|
|
||||||
</div>
|
|
||||||
<p>Are you sure you want to <strong>delete</strong> the following repository?</p>
|
|
||||||
<div class="repo-name"><i class="fa-solid fa-book"></i> my-first-repo</div>
|
|
||||||
<form action="/repositories/delete" method="post" style="margin-top:1.5rem;">
|
|
||||||
<input type="hidden" name="id" value="1">
|
|
||||||
<div class="actions">
|
|
||||||
<button type="submit"><i class="fa-solid fa-trash"></i> Yes, delete</button>
|
|
||||||
<a href="repositories.html" class="cancel"><i class="fa-solid fa-ban"></i> Cancel</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
{% extends 'settings/index.html' %}
|
|
||||||
|
|
||||||
{% block header_text %}<h1><i class="fa-solid fa-database"></i> Repositories</h1>{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Repositories - List</title>
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
|
||||||
<style>
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.repo-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
.repo-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.repo-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 220px;
|
|
||||||
}
|
|
||||||
.repo-name {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.repo-row { flex-direction: column; align-items: stretch; }
|
|
||||||
.actions { justify-content: flex-start; }
|
|
||||||
}
|
|
||||||
.topbar {
|
|
||||||
display: flex;
|
|
||||||
/* justify-content: flex-end;*/
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
button, a.button {
|
|
||||||
background: #198754; color: #fff; border: none; border-radius: 5px;
|
|
||||||
padding: 0.4rem 0.8rem; text-decoration: none; cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
font-size: 1rem; display: inline-flex; align-items: center; gap: 0.4rem;
|
|
||||||
}
|
|
||||||
.button.delete { background: #dc3545; }
|
|
||||||
.button.edit { background: #0d6efd; }
|
|
||||||
.button.clone { background: #6c757d; }
|
|
||||||
.button.browse { background: #ffc107; color: #212529; }
|
|
||||||
.button.create { background: #20c997; margin-left: 0.5rem; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<div class="topbar">
|
|
||||||
<a class="button create" href="/settings/repositories/create.html">
|
|
||||||
<i class="fa-solid fa-plus"></i> New Repository
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<section class="repo-list">
|
|
||||||
<!-- Example repository entries; replace with your templating/iteration -->
|
|
||||||
{% for repo in repositories %}
|
|
||||||
<div class="repo-row">
|
|
||||||
<div class="repo-info">
|
|
||||||
<span class="repo-name"><i class="fa-solid fa-book"></i> {{ repo.name }}</span>
|
|
||||||
|
|
||||||
<span title="Public">
|
|
||||||
<i class="fa-solid {% if repo.is_private %}fa-lock{% else %}fa-lock-open{% endif %}"></i>
|
|
||||||
{% if repo.is_private %}Private{% else %}Public{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<a class="button browse" href="/repositories/{{ user.username }}/{{ repo.name }}" target="_blank">
|
|
||||||
<i class="fa-solid fa-folder-open"></i> Browse
|
|
||||||
</a>
|
|
||||||
<a class="button clone" href="/repositories/{{ user.username }}/{{ repo.name }}/clone">
|
|
||||||
<i class="fa-solid fa-code-branch"></i> Clone
|
|
||||||
</a>
|
|
||||||
<a class="button edit" href="/settings/repositories/repository/{{ repo.name }}/update.html">
|
|
||||||
<i class="fa-solid fa-pen"></i> Edit
|
|
||||||
</a>
|
|
||||||
<a class="button delete" href="/settings/repositories/{{ repo.name }}/delete.html">
|
|
||||||
<i class="fa-solid fa-trash"></i> Delete
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
<!-- ... -->
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
{% endblock %}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
{% extends "settings/index.html" %}
|
|
||||||
|
|
||||||
{% block header_text %}<h1><i class="fa-solid fa-pen"></i> Update Repository</h1>{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
|
||||||
<style>
|
|
||||||
form {
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
label { font-weight: bold; display: flex; align-items: center; gap: 0.5rem;}
|
|
||||||
button {
|
|
||||||
background: #0d6efd; color: #fff;
|
|
||||||
border: none; border-radius: 5px; padding: 0.6rem 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem; display: inline-flex; align-items: center; gap: 0.4rem;
|
|
||||||
}
|
|
||||||
.cancel {
|
|
||||||
background: #6c757d;
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.container { max-width: 98vw; }
|
|
||||||
form { padding: 1rem; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="container">
|
|
||||||
<form method="post">
|
|
||||||
<!-- Assume hidden id for backend use -->
|
|
||||||
<input type="hidden" name="id" value="{{ repository.id }}">
|
|
||||||
<div>
|
|
||||||
<label for="name"><i class="fa-solid fa-book"></i> Name</label>
|
|
||||||
<input type="text" id="name" name="name" value="{{ repository.name }}" readonly>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="is_private" value="1" {% if repository.is_private %}checked{% endif %}>
|
|
||||||
<i class="fa-solid fa-lock"></i> Private
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<button type="submit"><i class="fa-solid fa-pen"></i> Update</button>
|
|
||||||
<button onclick="history.back()" class="cancel"><i class="fa-solid fa-arrow-left"></i>Cancel</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@ -3,7 +3,7 @@
|
|||||||
<h2>You</h2>
|
<h2>You</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a class="no-select" href="/settings/profile.html">Profile</a></li>
|
<li><a class="no-select" href="/settings/profile.html">Profile</a></li>
|
||||||
<li><a class="no-select" href="/settings/repositories/index.html">Repositories</a></li>
|
<li><a class="no-select" href="/settings/gists.html">Gists</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@ -1,68 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from aiohttp import web
|
|
||||||
|
|
||||||
from snek.system.view import BaseFormView
|
|
||||||
import pathlib
|
|
||||||
|
|
||||||
class RepositoriesIndexView(BaseFormView):
|
|
||||||
|
|
||||||
login_required = True
|
|
||||||
|
|
||||||
async def get(self):
|
|
||||||
|
|
||||||
user_uid = self.session.get("uid")
|
|
||||||
|
|
||||||
repositories = []
|
|
||||||
async for repository in self.services.repository.find(user_uid=user_uid):
|
|
||||||
repositories.append(repository.record)
|
|
||||||
|
|
||||||
return await self.render_template("settings/repositories/index.html", {"repositories": repositories})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class RepositoriesCreateView(BaseFormView):
|
|
||||||
|
|
||||||
login_required = True
|
|
||||||
|
|
||||||
async def get(self):
|
|
||||||
|
|
||||||
return await self.render_template("settings/repositories/create.html")
|
|
||||||
|
|
||||||
async def post(self):
|
|
||||||
data = await self.request.post()
|
|
||||||
repository = await self.services.repository.create(user_uid=self.session.get("uid"), name=data['name'], is_private=int(data.get('is_private',0)))
|
|
||||||
return web.HTTPFound("/settings/repositories/index.html")
|
|
||||||
|
|
||||||
class RepositoriesUpdateView(BaseFormView):
|
|
||||||
|
|
||||||
login_required = True
|
|
||||||
|
|
||||||
async def get(self):
|
|
||||||
|
|
||||||
repository = await self.services.repository.get(
|
|
||||||
user_uid=self.session.get("uid"), name=self.request.match_info["name"]
|
|
||||||
)
|
|
||||||
if not repository:
|
|
||||||
return web.HTTPNotFound()
|
|
||||||
return await self.render_template("settings/repositories/update.html", {"repository": repository.record})
|
|
||||||
|
|
||||||
async def post(self):
|
|
||||||
data = await self.request.post()
|
|
||||||
repository = await self.services.repository.get(
|
|
||||||
user_uid=self.session.get("uid"), name=self.request.match_info["name"]
|
|
||||||
)
|
|
||||||
repository['is_private'] = int(data.get('is_private',0))
|
|
||||||
await self.services.repository.save(repository)
|
|
||||||
return web.HTTPFound("/settings/repositories/index.html")
|
|
||||||
|
|
||||||
class RepositoriesDeleteView(BaseFormView):
|
|
||||||
|
|
||||||
login_required = True
|
|
||||||
|
|
||||||
async def get(self):
|
|
||||||
|
|
||||||
return await self.render_template("settings/repositories/delete.html")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user