├── gitmask ├── lib │ ├── __init__.py │ ├── scm │ │ ├── github_utils.py │ │ ├── github_backend.py │ │ ├── github_repo.py │ │ └── github_refs_container.py │ ├── common │ │ ├── string.py │ │ └── git.py │ ├── gitmask_protocol.py │ └── gitmask_receive_pack_handler.py ├── version.py ├── git-info-refs.py └── git-receive-pack.py ├── docs ├── noun_hacker_2481442.png └── noun_hacker_2481442.svg ├── Pipfile ├── setup.py ├── package.json ├── LICENSE ├── .gitignore ├── .circleci └── config.yml ├── serverless.yml ├── Pipfile.lock └── README.md /gitmask/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/noun_hacker_2481442.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnalogJ/gitmask/HEAD/docs/noun_hacker_2481442.png -------------------------------------------------------------------------------- /gitmask/lib/scm/github_utils.py: -------------------------------------------------------------------------------- 1 | 2 | def auth_remote_url(authToken, org, repo): 3 | return "https://{0}:@github.com/{1}/{2}.git".format(authToken, org, repo) 4 | 5 | -------------------------------------------------------------------------------- /gitmask/lib/common/string.py: -------------------------------------------------------------------------------- 1 | def remove_prefix(text, prefix): 2 | if text.startswith(prefix): 3 | return text[len(prefix):] 4 | return text # or whatever 5 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | dulwich = "*" 10 | pygithub = "*" 11 | 12 | [requires] 13 | python_version = "3.7" 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_namespace_packages 2 | 3 | 4 | setup( 5 | name='gitmask', 6 | 7 | version='1', 8 | 9 | description='', 10 | long_description='', 11 | 12 | author='Jason Kulatunga', 13 | author_email='jason@thesparktree.com', 14 | 15 | license='MIT', 16 | 17 | packages=find_namespace_packages(include=['gitmask.*', 'gitmask.lib.*']), 18 | zip_safe=False, 19 | ) 20 | -------------------------------------------------------------------------------- /gitmask/version.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def handler(event, context): 5 | body = { 6 | "message": "Go Serverless v1.0! Your function executed successfully!", 7 | "input": event 8 | } 9 | 10 | response = { 11 | "statusCode": 200, 12 | "body": json.dumps(body) 13 | } 14 | 15 | return response 16 | 17 | # Use this code if you don't use the http event with the LAMBDA-PROXY 18 | # integration 19 | """ 20 | return { 21 | "message": "Go Serverless v1.0! Your function executed successfully!", 22 | "event": event 23 | } 24 | """ 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitmask", 3 | "version": "2.0.0", 4 | "description": "Gitmask v2 using Python", 5 | "main": "handler.py", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 0" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/AnalogJ/gitmask-v2.git" 12 | }, 13 | "author": "Jason Kulatunga", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/AnalogJ/gitmask-v2/issues" 17 | }, 18 | "homepage": "https://github.com/AnalogJ/gitmask-v2#readme", 19 | "dependencies": { 20 | "serverless-apigw-binary": "^0.4.4", 21 | "serverless-apigwy-binary": "^1.0.0", 22 | "serverless-offline-python": "^3.22.2", 23 | "serverless-python-requirements": "^5.1.0" 24 | }, 25 | "devDependencies": { 26 | "serverless-domain-manager": "^4.1.1", 27 | "serverless-prune-plugin": "^1.4.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jason Kulatunga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /gitmask/git-info-refs.py: -------------------------------------------------------------------------------- 1 | from dulwich.protocol import Protocol 2 | from gitmask.lib.scm.github_backend import GithubBackend 3 | from gitmask.lib.gitmask_receive_pack_handler import GitmaskReceivePackHandler 4 | import io 5 | 6 | def handler(event, context): 7 | print(event) 8 | owner = event['pathParameters']['org'] 9 | reponame = event['pathParameters']['repo'].replace('.git', '') 10 | 11 | # from receive_pack(path=".", inf=None, outf=None): https://github.com/dulwich/dulwich/blob/master/dulwich/porcelain.py#L1137 12 | repo_fullpath = "{0}/{1}".format(owner, reponame) 13 | 14 | inf = io.BytesIO() 15 | outf = io.BytesIO() 16 | 17 | backend = GithubBackend() 18 | 19 | def send_fn(data): 20 | outf.write(data) 21 | # outf.flush() 22 | 23 | proto = Protocol(inf.read, send_fn) 24 | handler = GitmaskReceivePackHandler(backend, [repo_fullpath], proto) 25 | handler.handle_info_refs() 26 | 27 | # send receive pack handler response to client 28 | 29 | response = { 30 | "statusCode": 200, 31 | "headers": {'Content-Type': "application/x-{0}-advertisement".format(event['queryStringParameters']['service'])}, 32 | "body": outf.getvalue().decode("utf-8") 33 | } 34 | 35 | return response 36 | -------------------------------------------------------------------------------- /gitmask/lib/gitmask_protocol.py: -------------------------------------------------------------------------------- 1 | # inject data into 2 | import io 3 | from gitmask.lib.common.string import remove_prefix 4 | from dulwich.protocol import (Protocol, pkt_line, SIDE_BAND_CHANNEL_PROGRESS) 5 | from dulwich.server import (extract_capabilities, FileSystemBackend, ReceivePackHandler) 6 | 7 | def inject(payload_bytes, inject_bytes): 8 | 9 | if payload_bytes.endswith(b'0000'): 10 | # the protocol payload has already been closed 11 | # we need to reopen it. Remove termination bytes 12 | payload_bytes = payload_bytes[:-4] 13 | 14 | 15 | resp_outf = io.BytesIO() 16 | def send_fn(data): 17 | resp_outf.write(data) 18 | 19 | resp_proto = Protocol(io.BytesIO().read, send_fn) 20 | 21 | resp_proto.write_sideband(SIDE_BAND_CHANNEL_PROGRESS, inject_bytes) 22 | 23 | # write termination block 24 | resp_proto.write_pkt_line(None) 25 | 26 | # append the new messages to the open payload 27 | return (payload_bytes + resp_outf.getvalue()) 28 | 29 | 30 | def decode_branch(payload_bytes): 31 | inf = io.BytesIO(payload_bytes) 32 | header_proto = Protocol(inf.read, lambda *args: None) #noop for output, we can ignore. 33 | ref, __ignore__ = extract_capabilities(header_proto.read_pkt_line()) 34 | return remove_prefix(ref.decode("utf-8") .split(' ')[2], 'refs/heads/') 35 | -------------------------------------------------------------------------------- /gitmask/lib/scm/github_backend.py: -------------------------------------------------------------------------------- 1 | from dulwich.server import Backend 2 | from dulwich import log_utils 3 | from dulwich.errors import NotGitRepository 4 | from dulwich.repo import Repo 5 | from github import Github 6 | 7 | # from github_repo import GithubRepo 8 | 9 | import os 10 | from gitmask.lib.scm.github_repo import GithubRepo 11 | 12 | logger = log_utils.getLogger(__name__) 13 | 14 | class GithubBackend(Backend): 15 | """Backend looking up Git repositories from scm""" 16 | 17 | def __init__(self): 18 | super(GithubBackend, self).__init__() 19 | # or using an access token 20 | self.scm_client = Github(os.environ.get('GITHUB_ACCESS_TOKEN')) 21 | 22 | 23 | def open_repository(self, repo_fullname): 24 | logger.debug('opening github repository at %s', repo_fullname) 25 | 26 | try: 27 | # check if repo exists 28 | repo = self.scm_client.get_repo(repo_fullname) 29 | return GithubRepo(repo_fullname) 30 | except: 31 | raise NotGitRepository("Github Repository %r does not exist" % (repo_fullname)) 32 | 33 | # if __name__ == "__main__": 34 | # backend = GithubBackend() 35 | # repo = backend.open_repository("AnalogJ/gitmask") 36 | # print(sorted(repo.get_refs().items())) 37 | # print(sorted(repo.refs.get_symrefs().items())) 38 | -------------------------------------------------------------------------------- /gitmask/lib/scm/github_repo.py: -------------------------------------------------------------------------------- 1 | from dulwich.repo import BaseRepo 2 | from dulwich.object_store import MemoryObjectStore 3 | from gitmask.lib.scm.github_refs_container import GithubRefsContainer 4 | import os 5 | from github import Github 6 | 7 | # Based on BaseRepo(object): https://github.com/dulwich/dulwich/blob/master/dulwich/repo.py#L273 8 | # and MemoryRepo(BaseRepo): https://github.com/dulwich/dulwich/blob/master/dulwich/repo.py#L1384 9 | # and Repo(BaseRepo): https://github.com/dulwich/dulwich/blob/master/dulwich/repo.py#L912 10 | class GithubRepo(BaseRepo): 11 | """A git repository backed by local disk. 12 | To open an existing repository, call the contructor with 13 | the path of the repository. 14 | To create a new repository, use the Repo.init class method. 15 | """ 16 | 17 | def __init__(self, repo_fullname): 18 | self.scm_client = Github(os.environ.get('GITHUB_ACCESS_TOKEN')) 19 | self.repo_fullname = repo_fullname 20 | self.scm_client.get_repo(self.repo_fullname) # throws error if repo does not exist. 21 | 22 | 23 | self._reflog = [] 24 | refs_container = GithubRefsContainer(self.repo_fullname) 25 | BaseRepo.__init__(self, MemoryObjectStore(), refs_container) 26 | self._named_files = {} 27 | self.bare = True 28 | self._config = None 29 | self._description = None 30 | 31 | # if __name__ == "__main__": 32 | # repo = GithubRepo("AnalogJ/gitmask") 33 | # print(sorted(repo.get_refs().items())) 34 | # print(sorted(repo.refs.get_symrefs().items())) 35 | -------------------------------------------------------------------------------- /gitmask/lib/scm/github_refs_container.py: -------------------------------------------------------------------------------- 1 | from dulwich.refs import DictRefsContainer 2 | from github import Github 3 | import os 4 | 5 | 6 | 7 | # based on RefsContainer(object): https://github.com/dulwich/dulwich/blob/master/dulwich/refs.py#L95 8 | # and DictRefsContainer(RefsContainer): https://github.com/dulwich/dulwich/blob/master/dulwich/refs.py#L390 9 | class GithubRefsContainer(DictRefsContainer): 10 | """A container for refs.""" 11 | 12 | def __init__(self, repo_fullname, logger=None): 13 | self.scm_client = Github(os.environ.get('GITHUB_ACCESS_TOKEN')) 14 | self.repo_fullname = repo_fullname 15 | repo = self.scm_client.get_repo(self.repo_fullname) 16 | 17 | # from https://github.com/dulwich/dulwich/blob/master/dulwich/tests/test_refs.py#L331 18 | # dict(_TEST_REFS) 19 | # Dict of refs that we expect all RefsContainerTests subclasses to define. 20 | # _TEST_REFS = { 21 | # b'HEAD': b'42d06bd4b77fed026b154d16493e5deab78f02ec', 22 | # b'refs/heads/40-char-ref-aaaaaaaaaaaaaaaaaa': 23 | # b'42d06bd4b77fed026b154d16493e5deab78f02ec', 24 | # b'refs/heads/master': b'42d06bd4b77fed026b154d16493e5deab78f02ec', 25 | # b'refs/heads/packed': b'42d06bd4b77fed026b154d16493e5deab78f02ec', 26 | # b'refs/tags/refs-0.1': b'df6800012397fb85c56e7418dd4eb9405dee075c', 27 | # b'refs/tags/refs-0.2': b'3ec9c43c84ff242e3ef4a9fc5bc111fd780a76a8', 28 | # b'refs/heads/loop': b'ref: refs/heads/loop', 29 | # } 30 | 31 | refs = {} 32 | for ghRefs in repo.get_git_refs(): 33 | refs[bytes(ghRefs.ref, 'utf-8')] = bytes(ghRefs.object.sha, 'utf-8') 34 | # TODO: check the repo base branch 35 | if ghRefs.ref == 'refs/heads/master': 36 | refs[bytes('HEAD', 'utf-8')] = bytes(ghRefs.object.sha, 'utf-8') 37 | 38 | super(GithubRefsContainer, self).__init__(dict(refs), logger=logger) 39 | 40 | 41 | 42 | if __name__ == "__main__": 43 | container = GithubRefsContainer("AnalogJ/gitmask") 44 | print(container.get_symrefs()) 45 | # print(container.allkeys()) 46 | # print(container._refs) 47 | -------------------------------------------------------------------------------- /docs/noun_hacker_2481442.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | /lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | 132 | # Distribution / packaging 133 | .Python 134 | env/ 135 | build/ 136 | develop-eggs/ 137 | dist/ 138 | downloads/ 139 | eggs/ 140 | .eggs/ 141 | /lib/ 142 | lib64/ 143 | parts/ 144 | sdist/ 145 | var/ 146 | *.egg-info/ 147 | .installed.cfg 148 | *.egg 149 | 150 | # Serverless directories 151 | .serverless 152 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | # The primary container is an instance of the first image listed. The job's commands run in this container. 5 | docker: 6 | - image: circleci/python:3.7.0-node 7 | steps: 8 | - checkout 9 | - run: 10 | name: Update npm 11 | command: 'sudo npm install -g npm@latest' 12 | - run: 13 | name: Install serverless 14 | command: 'sudo npm install -g serverless' 15 | - restore_cache: 16 | key: dependency-cache-{{ checksum "package.json" }} 17 | - run: 18 | name: Install npm 19 | command: npm install 20 | - save_cache: 21 | key: dependency-cache-{{ checksum "package.json" }} 22 | paths: 23 | - node_modules 24 | - run: 25 | name: List npm installed packages 26 | command: npm ls || true 27 | test: 28 | docker: 29 | - image: circleci/python:3.7.0-node 30 | steps: 31 | - checkout 32 | - restore_cache: 33 | key: dependency-cache-{{ checksum "package.json" }} 34 | - run: 35 | name: Test 36 | command: npm test 37 | # - run: 38 | # name: Generate code coverage 39 | # command: './node_modules/.bin/nyc report --reporter=text-lcov' 40 | # - store_artifacts: 41 | # path: test-results.xml 42 | # prefix: tests 43 | # - store_artifacts: 44 | # path: coverage 45 | # prefix: coverage 46 | deploy: 47 | docker: 48 | - image: circleci/python:3.7.0-node 49 | steps: 50 | - checkout 51 | - run: 52 | name: Install serverless 53 | command: 'sudo npm install -g serverless' 54 | - restore_cache: 55 | key: dependency-cache-{{ checksum "package.json" }} 56 | - run: 57 | name: Deploy 58 | command: | 59 | 60 | serverless create_domain 61 | serverless deploy --conceal --verbose --stage $CIRCLE_BRANCH --region us-east-1 62 | - run: 63 | name: Create Deployment 64 | command: | 65 | 66 | curl --user "x:${GITHUB_CIRCLECI_DEPLOYMENT_STATUS_TOKEN}" https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/deployments \ 67 | -H "Expect:" \ 68 | -H 'Content-Type: text/json; charset=utf-8' \ 69 | -d @- << EOF 70 | { 71 | "ref":"${CIRCLE_SHA1}", 72 | "auto_merge":false, 73 | "payload":"", 74 | "required_contexts": [], 75 | "description":"Deploy request from CircleCI", 76 | "environment":"${CIRCLE_BRANCH}" 77 | } 78 | EOF 79 | 80 | workflows: 81 | version: 2 82 | build_and_test: 83 | jobs: 84 | - build 85 | - test: 86 | requires: 87 | - build 88 | - deploy: 89 | requires: 90 | - test 91 | filters: 92 | branches: 93 | only: 94 | - master 95 | - beta 96 | -------------------------------------------------------------------------------- /gitmask/lib/common/git.py: -------------------------------------------------------------------------------- 1 | from dulwich.porcelain import (clone, pull, update_head, branch_list, active_branch) 2 | import subprocess 3 | import os 4 | import io 5 | 6 | def repo_receive_pack(repo_path, payload_bytes): 7 | outf = io.BytesIO() 8 | 9 | env = os.environ.copy() 10 | # env['GIT_TRACE'] = '1' 11 | p = subprocess.Popen(['git-receive-pack', "--stateless-rpc", repo_path], cwd=repo_path, env=env, stdout=subprocess.PIPE, 12 | stdin=subprocess.PIPE) 13 | 14 | p.stdin.write(payload_bytes) 15 | p.stdin.flush() 16 | 17 | # TODO: throw an error if process fails. don't try to continue. 18 | 19 | for line in p.stdout: 20 | outf.write(line) 21 | 22 | return outf.getvalue() 23 | 24 | def repo_clone(git_remote, repo_path, is_bare=False): 25 | # '--depth', '1', 26 | 27 | cmd = ['git', 'clone'] 28 | if is_bare: 29 | cmd.append('--bare') 30 | cmd.append(git_remote) 31 | cmd.append(repo_path) 32 | 33 | process = subprocess.check_call(cmd) 34 | 35 | 36 | def repo_fetch(repo_path): 37 | process_fetch = subprocess.check_call(['git', 'fetch', '--all'], cwd=repo_path) 38 | 39 | def repo_checkout(repo_path, branch_name): 40 | process = subprocess.check_call(['git', 'checkout', branch_name], cwd=repo_path) 41 | 42 | def repo_squash_commits(repo_path, dest_branch, squashed_branch, packfile_branch): 43 | 44 | process = subprocess.check_call(['git', 'checkout', '-b', squashed_branch, dest_branch], cwd=repo_path) 45 | 46 | process_merge = subprocess.check_call(['git', 'merge', '--squash', packfile_branch], cwd=repo_path) 47 | 48 | cust_env = os.environ.copy() 49 | cust_env['GIT_COMMITTER_NAME'] = 'ghost' 50 | cust_env['GIT_COMMITTER_EMAIL'] = 'ghost@users.noreply.github.com' 51 | cust_env['GIT_AUTHOR_NAME'] = 'ghost' 52 | cust_env['GIT_AUTHOR_EMAIL'] = 'ghost@users.noreply.github.com' 53 | 54 | # make a squashed commit on the squashed_branch 55 | process_add = subprocess.check_call(['git', 'commit', '-am', 'gitmask.com anonymous commit'], cwd=repo_path, env=cust_env) 56 | 57 | 58 | def repo_push(repo_path, remote_name, branch_name): 59 | process = subprocess.check_call(['git', 'push', remote_name, branch_name], cwd=repo_path) 60 | 61 | def repo_remote_add(repo_path, remote_name, remote_url): 62 | process = subprocess.check_call(['git', 'remote', 'add', remote_name, remote_url], cwd=repo_path) 63 | 64 | 65 | # 66 | # 67 | # def clone_repo(gitRemote, destination, branch, depth=1): 68 | # branch_ref = bytes('refs/heads/{0}'.format(branch), 'utf-8') 69 | # 70 | # local_repo = clone(gitRemote, destination, bare=False) 71 | # pull(destination, refspecs=[b'HEAD', branch_ref]) 72 | # 73 | # print("=====AFTER=====>", branch_list(destination)) 74 | # update_head(destination, branch_ref, False) 75 | # local_repo.reset_index(local_repo[branch_ref].tree) 76 | # local_repo.refs.set_symbolic_ref(b'HEAD', branch_ref) 77 | # print("ACTIVE BRANCH====>", active_branch(destination)) 78 | 79 | # def checkout_branch(repoPath, branchName): 80 | # return execGitCmd(`git checkout ${branchName}`, repoPath) 81 | 82 | -------------------------------------------------------------------------------- /gitmask/lib/gitmask_receive_pack_handler.py: -------------------------------------------------------------------------------- 1 | from dulwich.server import ( 2 | ReceivePackHandler, 3 | extract_capabilities 4 | ) 5 | from dulwich.protocol import ( # noqa: F401 6 | CAPABILITY_AGENT, 7 | CAPABILITY_ATOMIC, 8 | CAPABILITIES_REF, 9 | CAPABILITY_DELETE_REFS, 10 | CAPABILITY_OFS_DELTA, 11 | CAPABILITY_QUIET, 12 | CAPABILITY_REPORT_STATUS, 13 | CAPABILITY_SIDE_BAND_64K, 14 | ZERO_SHA, 15 | ) 16 | 17 | from dulwich.errors import ( 18 | GitProtocolError 19 | ) 20 | 21 | from typing import Iterable 22 | 23 | # modified from ReceivePackHandler(PackHandler): https://github.com/dulwich/dulwich/blob/master/dulwich/server.py#L896 24 | class GitmaskReceivePackHandler(ReceivePackHandler): 25 | """Protocol handler for downloading a pack from the client.""" 26 | 27 | def __init__(self, backend, args, proto, http_req=None, 28 | advertise_refs=False): 29 | super(GitmaskReceivePackHandler, self).__init__( 30 | backend, args, proto, http_req=http_req) 31 | self.repo = backend.open_repository(args[0]) 32 | self.advertise_refs = advertise_refs 33 | 34 | @classmethod 35 | def capabilities(cls) -> Iterable[bytes]: 36 | return [ 37 | CAPABILITY_REPORT_STATUS, 38 | CAPABILITY_DELETE_REFS, 39 | CAPABILITY_QUIET, 40 | CAPABILITY_ATOMIC, 41 | CAPABILITY_SIDE_BAND_64K 42 | ] 43 | 44 | 45 | def set_client_capabilities(self, caps: Iterable[bytes]) -> None: 46 | allowable_caps = set(self.innocuous_capabilities()) 47 | allowable_caps.update(self.capabilities()) 48 | for cap in caps: 49 | if cap.startswith(CAPABILITY_AGENT + b'='): 50 | continue 51 | if cap not in allowable_caps: 52 | raise GitProtocolError('Client asked for capability %r that ' 53 | 'was not advertised.' % cap) 54 | for cap in self.required_capabilities(): 55 | if cap not in caps: 56 | raise GitProtocolError('Client does not support required ' 57 | 'capability %r.' % cap) 58 | self._client_capabilities = set(caps) 59 | # logger.info('Client capabilities: %s', caps) 60 | 61 | 62 | # modified from handle(self): https://github.com/dulwich/dulwich/blob/master/dulwich/server.py#L1001 63 | def handle_info_refs(self): 64 | refs = sorted(self.repo.get_refs().items()) 65 | 66 | if not refs: 67 | refs = [(CAPABILITIES_REF, ZERO_SHA)] 68 | 69 | self.proto.write_pkt_line(b'# service=git-receive-pack\n') 70 | self.proto.write_pkt_line(None) 71 | self.proto.write_pkt_line( 72 | refs[0][1] + b' ' + refs[0][0] + b'\0' + 73 | self.capability_line(self.capabilities()) + b' agent=git/gitmask\n') 74 | 75 | for i in range(1, len(refs)): 76 | ref = refs[i] 77 | self.proto.write_pkt_line(ref[1] + b' ' + ref[0] + b'\n') 78 | self.proto.write_pkt_line(None) 79 | 80 | def handle_receive_pack(self): 81 | client_refs = [] 82 | ref = self.proto.read_pkt_line() 83 | 84 | # if ref is none then client doesnt want to send us anything.. 85 | if ref is None: 86 | return 87 | 88 | ref, caps = extract_capabilities(ref) 89 | self.set_client_capabilities(caps) 90 | 91 | # client will now send us a list of (oldsha, newsha, ref) 92 | while ref: 93 | client_refs.append(ref.split()) 94 | ref = self.proto.read_pkt_line() 95 | 96 | # backend can now deal with this refs and read a pack using self.read 97 | status = self._apply_pack(client_refs) 98 | print("========status========") 99 | print(status) 100 | self._on_post_receive(client_refs) 101 | 102 | # when we have read all the pack from the client, send a status report 103 | # if the client asked for it 104 | if self.has_capability(CAPABILITY_REPORT_STATUS): 105 | self._report_status(status) 106 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Serverless! 2 | # 3 | # This file is the main config file for your service. 4 | # It's very minimal at this point and uses default values. 5 | # You can always add more config options for more control. 6 | # We've included some commented out config examples here. 7 | # Just uncomment any of them to get that config option. 8 | # 9 | # For full config options, check the docs: 10 | # docs.serverless.com 11 | # 12 | # Happy Coding! 13 | 14 | service: ${env:GITMASK_SERVICE, 'gitmask-api'} # NOTE: update this with your service name 15 | #app: your-app-name 16 | #tenant: your-tenant-name 17 | 18 | # You can pin your service to only deploy with a specific Serverless version 19 | # Check out our docs for more details 20 | # frameworkVersion: "=X.X.X" 21 | 22 | provider: 23 | name: aws 24 | runtime: python3.7 25 | logRetentionInDays: 60 26 | cfLogs: true 27 | stage: 'beta' 28 | region: us-east-1 29 | timeout: 40 # optional, default is 6 30 | memorySize: 128 31 | environment: 32 | GITMASK_SERVICE: ${self:service} 33 | DOMAIN: git.gitmask.com 34 | DEPLOY_SHA: ${env:CIRCLE_SHA1} 35 | STAGE: ${opt:stage, self:provider.stage} 36 | GITHUB_USER: ${env:GITHUB_USER, 'unmasked'} 37 | GITHUB_API_TOKEN: ${env:GITHUB_API_TOKEN} 38 | DEBUG: gitmask:* 39 | 40 | apiGateway: 41 | binaryMediaTypes: 42 | - 'application/x-git-receive-pack-request' 43 | - 'application/x-git-receive-pack-result' 44 | - '*/*' 45 | 46 | custom: 47 | pythonRequirements: 48 | dockerizePip: non-linux 49 | apigwBinary: 50 | types: #list of mime-types 51 | - 'application/x-git-receive-pack-request' 52 | - 'application/x-git-receive-pack-result' 53 | variables: 54 | master: 55 | cleanup_enabled: true 56 | debug: '' 57 | api_path: 'v1' 58 | beta: 59 | cleanup_enabled: false 60 | debug: gitmask:* 61 | api_path: 'beta' 62 | local: 63 | cleanup_enabled: false 64 | debug: gitmask:* 65 | api_path: 'local' 66 | prune: 67 | automatic: true 68 | number: 2 69 | 70 | customDomain: 71 | domainName: git.gitmask.com 72 | basePath: ${self:custom.variables.${opt:stage, self:provider.stage}.api_path} 73 | stage: ${self:provider.stage} 74 | createRoute53Record: false 75 | certificateName: '*.gitmask.com' 76 | 77 | functions: 78 | version: 79 | handler: gitmask/version.handler 80 | events: 81 | - http: 82 | path: version 83 | method: get 84 | cors: true 85 | 86 | # V2 Backend Methods 87 | git-info-refs: 88 | handler: gitmask/git-info-refs.handler 89 | events: 90 | - http: 91 | # https://beta.gitmask.com/gh/AnalogJ/capsulecd.git 92 | path: gh/{org}/{repo}/info/refs 93 | method: get 94 | cors: true 95 | 96 | git-receive-pack: 97 | handler: gitmask/git-receive-pack.handler 98 | layers: 99 | - arn:aws:lambda:us-east-1:553035198032:layer:git:11 100 | events: 101 | - http: 102 | # https://beta.gitmask.com/gh/AnalogJ/capsulecd.git 103 | path: gh/{org}/{repo}/git-receive-pack 104 | method: post 105 | cors: true 106 | # request: 107 | # contentHandling: CONVERT_TO_BINARY 108 | 109 | 110 | plugins: 111 | - serverless-python-requirements 112 | - serverless-offline-python 113 | - serverless-prune-plugin 114 | - serverless-domain-manager 115 | # you can add packaging information here 116 | package: 117 | # individually: true # required for https://github.com/FidelLimited/serverless-plugin-optimize 118 | # only the following paths will be included in the resulting artifact which will be uploaded. Without specific include everything in the current folder will be included 119 | include: 120 | - gitmask 121 | - functions 122 | - opt 123 | # The following paths will be excluded from the resulting artifact. If both include and exclude are defined we first apply the include, then the exclude so files are guaranteed to be excluded 124 | exclude: 125 | - tmp 126 | - .git 127 | - .idea 128 | - no 129 | - opt 130 | - venv 131 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "063e01fe74a669a3a0450984032df9b30cf845cfd1f5ff8e3bb764cf598b0d2f" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", 22 | "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" 23 | ], 24 | "version": "==2020.4.5.1" 25 | }, 26 | "chardet": { 27 | "hashes": [ 28 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 29 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 30 | ], 31 | "version": "==3.0.4" 32 | }, 33 | "deprecated": { 34 | "hashes": [ 35 | "sha256:525ba66fb5f90b07169fdd48b6373c18f1ee12728ca277ca44567a367d9d7f74", 36 | "sha256:a766c1dccb30c5f6eb2b203f87edd1d8588847709c78589e1521d769addc8218" 37 | ], 38 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 39 | "version": "==1.2.10" 40 | }, 41 | "dulwich": { 42 | "hashes": [ 43 | "sha256:10699277c6268d0c16febe141a5b1c1a6e9744f3144c2d2de1706f4b1adafe63", 44 | "sha256:267160904e9a1cb6c248c5efc53597a35d038ecc6f60bdc4546b3053bed11982", 45 | "sha256:4e3aba5e4844e7c700721c1fc696987ea820ee3528a03604dc4e74eff4196826", 46 | "sha256:60bb2c2c92f5025c1b53a556304008f0f624c98ae36f22d870e056b2d4236c11", 47 | "sha256:dddae02d372fc3b5cfb0046d0f62246ef281fa0c088df7601ab5916607add94b", 48 | "sha256:f00d132082b8fcc2eb0d722abc773d4aeb5558c1475d7edd1f0f571146c29db9", 49 | "sha256:f74561c448bfb6f04c07de731c1181ae4280017f759b0bb04fa5770aa84ca850" 50 | ], 51 | "index": "pypi", 52 | "version": "==0.19.16" 53 | }, 54 | "idna": { 55 | "hashes": [ 56 | "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", 57 | "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" 58 | ], 59 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 60 | "version": "==2.9" 61 | }, 62 | "pygithub": { 63 | "hashes": [ 64 | "sha256:8375a058ec651cc0774244a3bc7395cf93617298735934cdd59e5bcd9a1df96e", 65 | "sha256:d2d17d1e3f4474e070353f201164685a95b5a92f5ee0897442504e399c7bc249" 66 | ], 67 | "index": "pypi", 68 | "version": "==1.51" 69 | }, 70 | "pyjwt": { 71 | "hashes": [ 72 | "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", 73 | "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" 74 | ], 75 | "version": "==1.7.1" 76 | }, 77 | "requests": { 78 | "hashes": [ 79 | "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", 80 | "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" 81 | ], 82 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 83 | "version": "==2.23.0" 84 | }, 85 | "urllib3": { 86 | "hashes": [ 87 | "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", 88 | "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" 89 | ], 90 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 91 | "version": "==1.25.9" 92 | }, 93 | "wrapt": { 94 | "hashes": [ 95 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" 96 | ], 97 | "version": "==1.12.1" 98 | } 99 | }, 100 | "develop": {} 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |