├── .githasdiff.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── README.md ├── hellogopher │ ├── Dockerfile │ ├── README.md │ └── main.go └── helloworld │ ├── Dockerfile │ ├── README.md │ └── main.py ├── githasdiff.py ├── release.sh └── tests ├── __init__.py ├── test_githasdiff.py └── testdata └── data.json /.githasdiff.json: -------------------------------------------------------------------------------- 1 | { 2 | "githasdiff": { 3 | "include": [ 4 | "githasdiff.py", 5 | "tests/*" 6 | ] 7 | }, 8 | "helloworld": { 9 | "include": [ 10 | "examples/helloworld/*.py", 11 | "examples/helloworld/Dockerfile" 12 | ] 13 | }, 14 | "hellogopher": { 15 | "include": [ 16 | "examples/hellogopher/*.go", 17 | "examples/hellogopher/Dockerfile" 18 | ] 19 | }, 20 | "exclude": [ 21 | "*.md", 22 | ".gitignore", 23 | "LICENSE" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://raw.githubusercontent.com/dvcs/gitignore/master/templates/Python.gitignore 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | 133 | # pytype static type analyzer 134 | .pytype/ 135 | 136 | # https://raw.githubusercontent.com/dvcs/gitignore/master/templates/Go.gitignore 137 | 138 | # Binaries for programs and plugins 139 | *.exe 140 | *.exe~ 141 | *.dll 142 | *.so 143 | *.dylib 144 | 145 | # Test binary, built with `go test -c` 146 | *.test 147 | 148 | # Output of the go coverage tool, specifically when used with LiteIDE 149 | *.out 150 | 151 | # Dependency directories (remove the comment below to include it) 152 | # vendor/ 153 | 154 | # editor 155 | *.swp 156 | 157 | # project 158 | # githasdiff release 159 | githasdiff 160 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | git: 4 | depth: 2 5 | 6 | before_install: 7 | - curl -L https://github.com/pyanderson/githasdiff/releases/download/1.0.4/githasdiff > ./githasdiff 8 | - chmod +x ./githasdiff 9 | 10 | jobs: 11 | include: 12 | - name: "githasdiff tests" 13 | python: 3.8 14 | script: ./githasdiff githasdiff python -m unittest -v 15 | - name: "helloworld build and run" 16 | services: 17 | - docker 18 | script: 19 | - ./githasdiff helloworld docker build -t githasdiff:helloworld ./examples/helloworld/ 20 | - ./githasdiff helloworld docker run githasdiff:helloworld 21 | - name: "hellogopher build and run" 22 | services: 23 | - docker 24 | script: 25 | - ./githasdiff hellogopher docker build -t githasdiff:hellogopher ./examples/hellogopher/ 26 | - ./githasdiff hellogopher docker run githasdiff:hellogopher 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Anderson de Sousa Lima 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # githasdiff 2 | [![Build Status](https://travis-ci.org/pyanderson/githasdiff.svg?branch=master)](https://travis-ci.org/pyanderson/githasdiff) 3 | 4 | Small python script to search for changes using [fnmatch](https://docs.python.org/3/library/fnmatch.html) patterns to filter `git diff HEAD~`. The principal objective is make build processes faster checking whats changes before build projects/services. 5 | 6 | - The script exits with 0 when changes are found and 1 otherwise. 7 | - If script receives a `command`, it will be executed and exits with `command` exit code. 8 | 9 | Inspired by [dockerfiles test script](https://github.com/jessfraz/dockerfiles/blob/master/test.sh) from **Jess Frazelle**. 10 | 11 | 12 | ## Configuration 13 | Create a json file named `.githasdiff.json` with `include` and `exclude` patterns for each project/service/build: 14 | 15 | ```json 16 | { 17 | "project_a": { 18 | "include": [ 19 | "project_a/*.py" 20 | ], 21 | "exclude": [ 22 | "project_a/extra_scripts/*.py" 23 | ] 24 | } 25 | } 26 | ``` 27 | 28 | Global patterns can be defined in same file: 29 | 30 | ```json 31 | { 32 | "include": [ 33 | "*.py" 34 | ], 35 | "exclude": [ 36 | "*.md" 37 | ] 38 | } 39 | ``` 40 | 41 | ### Observations: 42 | - Global patterns will be always used to search for changes. 43 | - `exclude` has priority over `include` patterns, so first exclude, then matches. 44 | - If `include` patterns list is omitted, then script will considerate `["*"]` as `include` list pattern. 45 | 46 | It's also possible use an env var `GITHASDIFF_FILE` to set the path to json config file, and an env var `GITHASDIFF_COMMAND` to set command to check for diff. 47 | 48 | ## Install 49 | 50 | ```bash 51 | curl -L https://github.com/pyanderson/githasdiff/releases/download/1.0.4/githasdiff > ./githasdiff 52 | chmod +x ./githasdiff 53 | ``` 54 | 55 | ## Run 56 | 57 | Using if/else: 58 | 59 | ```bash 60 | if ./githasdiff project_a; then docker build -t project_a project_a/; else exit 0; fi 61 | ``` 62 | 63 | Using the `command` as args: 64 | 65 | ```bash 66 | ./githasdiff project_a docker build -t project_a project_a/ 67 | ``` 68 | 69 | ## Examples 70 | 71 | Check [.githasdiff.json](.githasdiff.json) for configuration and [.travis.yml](.travis.yml) for running. 72 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | - [Hello World!](helloworld/) 4 | - [Hello Gopher!](hellogopher/) 5 | -------------------------------------------------------------------------------- /examples/hellogopher/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14-alpine3.11 2 | 3 | WORKDIR /app 4 | 5 | COPY main.go main.go 6 | 7 | RUN go build ./ 8 | 9 | CMD ["./app"] 10 | -------------------------------------------------------------------------------- /examples/hellogopher/README.md: -------------------------------------------------------------------------------- 1 | # Hello Gopher! 2 | 3 | ## Build 4 | 5 | ```bash 6 | docker build -t githasdiff:hellogopher . 7 | ``` 8 | 9 | ## Run 10 | 11 | ```bash 12 | docker run githasdiff:hellogopher 13 | # Hello Gopher! 14 | ``` 15 | -------------------------------------------------------------------------------- /examples/hellogopher/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | // this comment it's just to force the build 7 | fmt.Println("Hello Gopher!") 8 | } 9 | -------------------------------------------------------------------------------- /examples/helloworld/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine3.11 2 | 3 | WORKDIR /app 4 | 5 | COPY main.py main.py 6 | 7 | CMD ["python", "main.py"] 8 | -------------------------------------------------------------------------------- /examples/helloworld/README.md: -------------------------------------------------------------------------------- 1 | # Hello World! 2 | 3 | ## Build 4 | 5 | ```bash 6 | docker build -t githasdiff:helloworld . 7 | ``` 8 | 9 | ## Run 10 | 11 | ```bash 12 | docker run githasdiff:helloworld 13 | # Hello World! 14 | ``` 15 | -------------------------------------------------------------------------------- /examples/helloworld/main.py: -------------------------------------------------------------------------------- 1 | # this comment it's just to force the build 2 | print('Hello World!') 3 | -------------------------------------------------------------------------------- /githasdiff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import json 4 | import os 5 | from subprocess import check_output 6 | from fnmatch import fnmatch 7 | 8 | 9 | def get_files(): 10 | '''Get all changed files.''' 11 | cmd = os.getenv('GITHASDIFF_COMMAND', 'git diff HEAD~ --name-only').split() 12 | return [n for n in check_output(cmd).decode('utf-8').split('\n') if n] 13 | 14 | 15 | def load_patterns(name, patterns_path='.githasdiff.json'): 16 | with open(patterns_path, 'r') as jfile: 17 | data = json.load(jfile) 18 | include = data.get(name, {}).get('include', []) + data.get('include', []) 19 | exclude = data.get(name, {}).get('exclude', []) + data.get('exclude', []) 20 | if len(include) == 0: 21 | include = ['*'] 22 | return include, exclude 23 | 24 | 25 | def has_diff(files, include, exclude): 26 | files = [n for n in files if not any(fnmatch(n, p) for p in exclude)] 27 | return any(fnmatch(n, p) for n in files for p in include) 28 | 29 | 30 | def run(name, command='', patterns_path='.githasdiff.json'): 31 | files = get_files() 32 | try: 33 | include, exclude = load_patterns(name, patterns_path) 34 | except FileNotFoundError: 35 | include, exclude = [], [] 36 | if has_diff(files, include, exclude): 37 | print('has diff!') 38 | if command: 39 | print('running command: ' + command) 40 | return os.system(command) >> 8 41 | return 0 42 | print('has not diff!') 43 | if command: 44 | return 0 45 | return 1 46 | 47 | 48 | if __name__ == '__main__': 49 | parser = argparse.ArgumentParser(description='Search for changes.') 50 | parser.add_argument('project') 51 | args, command = parser.parse_known_args() 52 | patterns_path = os.getenv('GITHASDIFF_FILE', '.githasdiff.json') 53 | exit(run(args.project, ' '.join(command), patterns_path)) 54 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | dos2unix -n githasdiff.py githasdiff 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyanderson/githasdiff/b00479ed23fbc93af27f056af20bb477b8b2e104/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_githasdiff.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | 4 | # local imports 5 | from githasdiff import has_diff, load_patterns, run 6 | 7 | 8 | class TestFunctions(unittest.TestCase): 9 | def test_load_patterns(self): 10 | include, exclude = load_patterns('project', 'tests/testdata/data.json') 11 | assert 'foo/*' in include 12 | assert '*.py' in include 13 | assert 'foo/tests/*' in exclude 14 | assert '*.md' in exclude 15 | assert '*.rst' in exclude 16 | 17 | def test_has_diff(self): 18 | include = ['foo/*'] 19 | exclude = ['*.md', 'tests/*'] 20 | assert has_diff(['foo/main.py'], include, exclude) 21 | assert has_diff(['foo/tests/test_foo.py'], include, exclude) 22 | assert not has_diff(['foo/READEME.md'], include, exclude) 23 | assert not has_diff(['tests/test_foo.py'], include, exclude) 24 | 25 | def test_run(self): 26 | with patch('githasdiff.get_files') as mock: 27 | mock.return_value = ['main.py'] 28 | assert not run('project', patterns_path='tests/testdata/data.json') 29 | 30 | def test_run_with_command(self): 31 | with patch('githasdiff.get_files') as mock: 32 | mock.return_value = ['main.py'] 33 | assert not run('project', 'ls', 'tests/testdata/data.json') 34 | -------------------------------------------------------------------------------- /tests/testdata/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "include": [ 4 | "foo/*" 5 | ], 6 | "exclude": [ 7 | "foo/tests/*" 8 | ] 9 | }, 10 | "include": [ 11 | "*.py" 12 | ], 13 | "exclude": [ 14 | "*.md", 15 | "*.rst" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------