Patch
This commit is contained in:
		
							parent
							
								
									ee40c905d4
								
							
						
					
					
						commit
						a5aac9a337
					
				@ -31,7 +31,8 @@ dependencies = [
 | 
				
			|||||||
    "emoji",
 | 
					    "emoji",
 | 
				
			||||||
    "aiofiles",
 | 
					    "aiofiles",
 | 
				
			||||||
    "PyJWT",
 | 
					    "PyJWT",
 | 
				
			||||||
    "multiavatar"
 | 
					    "multiavatar",
 | 
				
			||||||
 | 
					    "gitpython",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[tool.setuptools.packages.find]
 | 
					[tool.setuptools.packages.find]
 | 
				
			||||||
 | 
				
			|||||||
@ -52,6 +52,7 @@ 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"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -184,12 +185,9 @@ class Application(BaseApplication):
 | 
				
			|||||||
        self.router.add_view("/settings/repositories/repository/{name}/update.html", RepositoriesUpdateView)
 | 
					        self.router.add_view("/settings/repositories/repository/{name}/update.html", RepositoriesUpdateView)
 | 
				
			||||||
        self.router.add_view("/settings/repositories/respository/{name}/delete.html", RepositoriesDeleteView)
 | 
					        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)
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ class RepositoryService(BaseService):
 | 
				
			|||||||
    async def exists(self, user_uid, name, **kwargs):
 | 
					    async def exists(self, user_uid, name, **kwargs):
 | 
				
			||||||
        kwargs["user_uid"] = user_uid
 | 
					        kwargs["user_uid"] = user_uid
 | 
				
			||||||
        kwargs["name"] = name
 | 
					        kwargs["name"] = name
 | 
				
			||||||
        return await self.exists(**kwargs)
 | 
					        return await super().exists(**kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def init(self, user_uid, name):
 | 
					    async def init(self, user_uid, name):
 | 
				
			||||||
        repository_path = await self.services.user.get_repository_path(user_uid)
 | 
					        repository_path = await self.services.user.get_repository_path(user_uid)
 | 
				
			||||||
@ -21,16 +21,14 @@ class RepositoryService(BaseService):
 | 
				
			|||||||
            stderr=asyncio.subprocess.PIPE
 | 
					            stderr=asyncio.subprocess.PIPE
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        stdout, stderr = await process.communicate()
 | 
					        stdout, stderr = await process.communicate()
 | 
				
			||||||
        if process.returncode == 0:
 | 
					        return process.returncode == 0
 | 
				
			||||||
            print(f"Bare Git repository created at: {repo_path}")
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            print(f"Error creating repository: {stderr.decode().strip()}")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def create(self, user_uid, name,is_private=False):
 | 
					    async def create(self, user_uid, name,is_private=False):
 | 
				
			||||||
        if await self.exists(user_uid=user_uid, name=name):
 | 
					        if await self.exists(user_uid=user_uid, name=name):
 | 
				
			||||||
            return False 
 | 
					            return False 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not await self.init(user_uid=user_uid, name=name):
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        model = await self.new()
 | 
					        model = await self.new()
 | 
				
			||||||
        model["user_uid"] = user_uid
 | 
					        model["user_uid"] = user_uid
 | 
				
			||||||
 | 
				
			|||||||
@ -43,10 +43,7 @@ class UserService(BaseService):
 | 
				
			|||||||
        return self.mapper.get_admin_uids()
 | 
					        return self.mapper.get_admin_uids()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def get_repository_path(self, user_uid):
 | 
					    async def get_repository_path(self, user_uid):
 | 
				
			||||||
        path = pathlib.Path(f"./drive/repositories/{user_uid}")
 | 
					        return pathlib.Path(f"./drive/repositories/{user_uid}")
 | 
				
			||||||
        if not path.exists():
 | 
					 | 
				
			||||||
            return None
 | 
					 | 
				
			||||||
        return path
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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")
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										472
									
								
								src/snek/sgit.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										472
									
								
								src/snek/sgit.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,472 @@
 | 
				
			|||||||
 | 
					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)
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user