├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Pipfile ├── Pipfile.lock ├── README.md ├── git ├── __init__.py ├── __version__.py ├── exceptions.py ├── logging.py └── vendor │ └── git-2.4.3.tar ├── setup.py └── tests ├── __init__.py └── git_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/vim,python 2 | 3 | ### Python ### 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.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 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | ENV/ 98 | env.bak/ 99 | venv.bak/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | .dmypy.json 114 | dmypy.json 115 | 116 | ### Python Patch ### 117 | .venv/ 118 | 119 | ### Python.VirtualEnv Stack ### 120 | # Virtualenv 121 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 122 | [Bb]in 123 | [Ii]nclude 124 | [Ll]ib 125 | [Ll]ib64 126 | [Ll]ocal 127 | [Ss]cripts 128 | pyvenv.cfg 129 | pip-selfcheck.json 130 | 131 | ### Vim ### 132 | # Swap 133 | [._]*.s[a-v][a-z] 134 | [._]*.sw[a-p] 135 | [._]s[a-rt-v][a-z] 136 | [._]ss[a-gi-z] 137 | [._]sw[a-p] 138 | 139 | # Session 140 | Session.vim 141 | 142 | # Temporary 143 | .netrwhist 144 | *~ 145 | # Auto-generated tag files 146 | tags 147 | # Persistent undo 148 | [._]*.un~ 149 | 150 | 151 | # End of https://www.gitignore.io/api/vim,python 152 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '2.7' 4 | - '3.6' 5 | install: 6 | - 'pip install pipenv' 7 | - 'pipenv sync' 8 | script: 'python -m nose' 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Pull Requests 2 | 3 | Here are some reasons why a pull request may not be merged: 4 | 5 | 1. It hasn’t been reviewed. 6 | 2. It doesn’t include specs for new functionality. 7 | 3. It doesn’t include documentation for new functionality. 8 | 4. It changes behavior without changing the relevant documentation, comments, or specs. 9 | 5. It changes behavior of an existing public API, breaking backward compatibility. 10 | 6. It breaks the tests on a supported platform. 11 | 7. It doesn’t merge cleanly (requiring Git rebasing and conflict resolution). 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jacopo Scrinzi 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.in 2 | include *.lock 3 | include LICENSE 4 | include Pipfile 5 | include git/vendor/*.tar 6 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | lambda-git = {editable = true, path = "."} 8 | 9 | [dev-packages] 10 | mock = ">=2.0.0" 11 | nose = ">=1.3.7" 12 | 13 | [requires] 14 | python_version = "2.7" 15 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "35257cacdfd1b599a5eed30fb80a40f4a1575e6d12d5b558642208aaaa9fec0c" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "2.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "lambda-git": { 20 | "editable": true, 21 | "path": "." 22 | } 23 | }, 24 | "develop": { 25 | "mock": { 26 | "hashes": [ 27 | "sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1", 28 | "sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba" 29 | ], 30 | "index": "pypi", 31 | "version": "==2.0.0" 32 | }, 33 | "nose": { 34 | "hashes": [ 35 | "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", 36 | "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a", 37 | "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" 38 | ], 39 | "index": "pypi", 40 | "version": "==1.3.7" 41 | }, 42 | "pbr": { 43 | "hashes": [ 44 | "sha256:1be135151a0da949af8c5d0ee9013d9eafada71237eb80b3ba8896b4f12ec5dc", 45 | "sha256:cf36765bf2218654ae824ec8e14257259ba44e43b117fd573c8d07a9895adbdd" 46 | ], 47 | "version": "==4.3.0" 48 | }, 49 | "six": { 50 | "hashes": [ 51 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 52 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" 53 | ], 54 | "version": "==1.11.0" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lambda-git 2 | A git binary installed through PyPI for AWS Lambda - inspired by the JavaScript package [lambda-git](https://github.com/pimterry/lambda-git). 3 | 4 | ## Installation 5 | 6 | ```shell 7 | $ pip install lambda-git 8 | ``` 9 | 10 | ## Usage 11 | 12 | To use this, just require it and call `exec_command`. E.g: 13 | 14 | ```python 15 | import git 16 | 17 | git.exec_command('init') # will run 'git init' 18 | ``` 19 | 20 | 21 | ### Executing commands in a specific path: 22 | 23 | AWS Lambda give you only `/tmp` as working directory. This package by default will execute all commands in `/tmp`, but it can be overridden by passing `cwd`. 24 | 25 | ```python 26 | import git 27 | 28 | new_repo_path = '/tmp/my-new-repo' 29 | os.mkdir(new_repo_path) 30 | git.exec_command('init', cwd=new_repo_path) 31 | ``` 32 | 33 | ### Executing commands with separate environment: 34 | 35 | By default every git command will be executed with the system environment, but it can be overridden by setting `env`. 36 | 37 | ```python 38 | import git 39 | 40 | commit_env = os.environ 41 | commit_env['GIT_AUTHOR_NAME'] = 'My Name' 42 | commit_env['GIT_AUTHOR_EMAIL'] = 'me@email.com' 43 | commit_env['GIT_COMMITTER_NAME'] = 'My Name' 44 | commit_env['GIT_COMMITTER_EMAIL'] = 'me@email.com' 45 | 46 | new_repo_path = '/tmp/my-new-repo' 47 | 48 | git.exec_command('add', '.', cwd=new_repo_path) 49 | git.exec_command('commit', '-m "first commit"', cwd=new_repo_path, env=commit_env) 50 | ``` 51 | 52 | ### Testing 53 | 54 | ```shell 55 | $ python -m nose 56 | ``` 57 | 58 | ## Contributing 59 | 60 | This repository is [open to contributions](CONTRIBUTING.md). 61 | -------------------------------------------------------------------------------- /git/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import tarfile 4 | from distutils.spawn import find_executable 5 | 6 | from .exceptions import GitExecutionError 7 | from .logging import LOGGER 8 | 9 | 10 | PKG_PATH = os.path.dirname(os.path.realpath(__file__)) 11 | VENDOR_PATH = os.path.realpath('{}/vendor'.format(PKG_PATH)) 12 | GIT_VERSION = '2.4.3' 13 | GIT_TAR_FILE = '{}/git-{}.tar'.format(VENDOR_PATH, GIT_VERSION) 14 | TMP_PATH = '/tmp' 15 | BIN_PATH = os.path.join(TMP_PATH, 'usr/bin') 16 | GIT_TEMPLATE_DIR = os.path.join(TMP_PATH, 'usr/share/git-core/templates') 17 | GIT_EXEC_PATH = os.path.join(TMP_PATH, 'usr/libexec/git-core') 18 | LD_LIBRARY_PATH = os.path.join(TMP_PATH, 'usr/lib64') 19 | GIT_BINARY = '{}/usr/bin/git'.format(TMP_PATH) 20 | 21 | 22 | if not find_executable('git'): 23 | LOGGER.info('git not found installing using local copy') 24 | if not os.path.isfile(GIT_BINARY): 25 | LOGGER.info('extracting git tarball') 26 | tar = tarfile.open(GIT_TAR_FILE) 27 | tar.extractall(path=TMP_PATH) 28 | tar.close() 29 | 30 | LOGGER.info('setting up environment variables') 31 | os.environ['PATH'] += ':{}'.format(BIN_PATH) 32 | os.environ['GIT_TEMPLATE_DIR'] = GIT_TEMPLATE_DIR 33 | os.environ['GIT_EXEC_PATH'] = GIT_EXEC_PATH 34 | os.environ['LD_LIBRARY_PATH'] = LD_LIBRARY_PATH 35 | 36 | 37 | def exec_command(*args, **kwargs): 38 | options = dict({'cwd': '/tmp', 'env': os.environ}, **kwargs) 39 | command = ['git'] + list(args) 40 | LOGGER.info('executing git command: "{}"'.format(' '.join(command))) 41 | p = subprocess.Popen(command, stdout=subprocess.PIPE, 42 | stderr=subprocess.PIPE, cwd=options['cwd'], 43 | env=options['env']) 44 | stdout, stderr = p.communicate() 45 | if p.returncode != 0: 46 | LOGGER.error('git failed with {} returncode'.format(p.returncode)) 47 | raise GitExecutionError( 48 | 'command={} returncode={} stdout="{}" ' 49 | 'stderr="{}"'.format(command, p.returncode, stdout, stderr) 50 | ) 51 | return stdout, stderr 52 | -------------------------------------------------------------------------------- /git/__version__.py: -------------------------------------------------------------------------------- 1 | # __ __ __ __ ____ ____ __ ___ ____ ____ 2 | # ( ) /__\ ( \/ ( _ ( _ \ /__\ / __(_ _(_ _) 3 | # )(__ /(__)\ ) ( ) _ <)(_) /(__)\ ( (_-._)(_ )( 4 | # (____(__)(__(_/\/\_(____(____(__)(__) \___(____)(__) 5 | 6 | __title__ = 'lambda-git' 7 | __description__ = 'A package to install git in AWS Lambda.' 8 | __url__ = 'https://github.com/eredi93/lambda-git' 9 | __version__ = '0.1.1' 10 | __author__ = 'Jacopo Scrinzi' 11 | __author_email__ = 'scrinzi.jacopo@gmail.com' 12 | __license__ = 'MIT' 13 | -------------------------------------------------------------------------------- /git/exceptions.py: -------------------------------------------------------------------------------- 1 | class GitExecutionError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /git/logging.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import logging 3 | 4 | logging.basicConfig() 5 | LOGGER = logging.getLogger('git') 6 | LOGGER.setLevel(logging.INFO) 7 | -------------------------------------------------------------------------------- /git/vendor/git-2.4.3.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eredi93/lambda-git/503e24e9b237d0555800e4833e28d3637ec7f802/git/vendor/git-2.4.3.tar -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import setuptools 3 | 4 | 5 | ROOT = os.path.dirname(__file__) 6 | 7 | 8 | def get_file_content(file_name): 9 | with open(file_name, 'r') as fh: 10 | content = fh.read() 11 | return content 12 | 13 | 14 | about = {} 15 | exec(get_file_content(os.path.join(ROOT, 'git', '__version__.py')), about) 16 | 17 | 18 | # package_data={'': ['*.tar']}, 19 | setuptools.setup( 20 | name=about['__title__'], 21 | version=about['__version__'], 22 | description=about['__description__'], 23 | long_description=get_file_content('README.md'), 24 | long_description_content_type='text/markdown', 25 | author=about['__author__'], 26 | author_email=about['__author_email__'], 27 | url=about['__url__'], 28 | packages=setuptools.find_packages(), 29 | include_package_data=True, 30 | classifiers=[ 31 | 'Programming Language :: Python :: 2', 32 | 'Programming Language :: Python :: 2.7', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 3.6', 35 | 'License :: OSI Approved :: MIT License', 36 | 'Operating System :: OS Independent', 37 | ], 38 | ) 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eredi93/lambda-git/503e24e9b237d0555800e4833e28d3637ec7f802/tests/__init__.py -------------------------------------------------------------------------------- /tests/git_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import mock 3 | import sys 4 | import unittest 5 | 6 | import git 7 | 8 | if (sys.version_info > (3, 0)): 9 | from io import StringIO 10 | from importlib import reload 11 | else: 12 | from StringIO import StringIO 13 | 14 | 15 | class GitTestCase(unittest.TestCase): 16 | @mock.patch('git.tarfile.open') 17 | @mock.patch('git.os') 18 | @mock.patch('distutils.spawn.find_executable') 19 | def test_git_installation_on_import(self, find_executable_mock, os_mock, 20 | open_tarfile_mock): 21 | tar = mock.MagicMock() 22 | current_path = os.environ['PATH'] 23 | 24 | find_executable_mock.return_value = None 25 | os_mock.path.return_value.isfile.return_value = False 26 | open_tarfile_mock.return_value = tar 27 | 28 | reload(git) 29 | 30 | open_tarfile_mock.assert_called_with(git.GIT_TAR_FILE) 31 | tar.extractall.assert_called_with(path=git.TMP_PATH) 32 | tar.close.assert_called_with() 33 | 34 | path = '{}:{}'.format(current_path, git.BIN_PATH) 35 | self.assertEqual(os.environ['PATH'], path) 36 | self.assertEqual(os.environ['GIT_TEMPLATE_DIR'], git.GIT_TEMPLATE_DIR) 37 | self.assertEqual(os.environ['GIT_EXEC_PATH'], git.GIT_EXEC_PATH) 38 | self.assertEqual(os.environ['LD_LIBRARY_PATH'], git.LD_LIBRARY_PATH) 39 | 40 | @mock.patch('git.subprocess.PIPE') 41 | @mock.patch('git.subprocess.Popen') 42 | def test_exec_command(self, PopenMock, PipeMock): 43 | branch_name = 'js/my_new_branch' 44 | 45 | PopenMock.return_value.communicate.return_value = ('output', 'error') 46 | PopenMock.return_value.returncode = 0 47 | PipeMock.return_value = StringIO() 48 | 49 | git.exec_command('checkout', '-b', branch_name) 50 | 51 | PopenMock.assert_called_with(['git', 'checkout', '-b', branch_name], 52 | stdout=PipeMock, stderr=PipeMock, 53 | cwd='/tmp', env=os.environ) 54 | --------------------------------------------------------------------------------