From 0c6d71f71951e752be094ae44a4fff10137a2501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Thu, 14 Sep 2023 02:15:10 +0200 Subject: [PATCH] initial --- .gitignore | 176 ++++++++++++++++++++++++++++ .pylintrc | 5 + .vscode/settings.json | 6 + .vscode/tasks.json | 61 ++++++++++ README.md | 0 pydiffchecker/__init__.py | 0 pydiffchecker/cli.py | 22 ++++ pydiffchecker/git_version.py | 28 +++++ pydiffchecker/helper.py | 16 +++ pydiffchecker/line_shift_checker.py | 70 +++++++++++ pydiffchecker/version.py | 9 ++ setup.py | 23 ++++ tox.ini | 2 + 13 files changed, 418 insertions(+) create mode 100644 .gitignore create mode 100644 .pylintrc create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 README.md create mode 100644 pydiffchecker/__init__.py create mode 100644 pydiffchecker/cli.py create mode 100644 pydiffchecker/git_version.py create mode 100644 pydiffchecker/helper.py create mode 100644 pydiffchecker/line_shift_checker.py create mode 100644 pydiffchecker/version.py create mode 100644 setup.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad4a1f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,176 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..e2e488e --- /dev/null +++ b/.pylintrc @@ -0,0 +1,5 @@ +[FORMAT] +max-line-length=120 + +[MESSAGES CONTROL] +disable=missing-module-docstring,missing-class-docstring,missing-function-docstring diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..598edf4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "python.linting.mypyEnabled": true, + "python.linting.pylintEnabled": true, + "python.linting.flake8Enabled": true, + "python.linting.enabled": true +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..008b104 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,61 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "create venv", + "type": "shell", + "command": "python3", + "args": [ + "-m", + "venv", + "${workspaceFolder}/venv" + ], + "group": { + "kind": "build" + }, + "problemMatcher": [], + "presentation": { + "focus": true + } + }, + { + "label": "destroy venv", + "type": "shell", + "command": "rm", + "args": [ + "-rf", + "${workspaceFolder}/venv" + ], + "group": { + "kind": "build" + }, + "problemMatcher": [], + "presentation": { + "focus": true + } + }, + { + "label": "install pydiffchecker", + "type": "shell", + "dependsOn": [ + "create venv" + ], + "command": "${workspaceFolder}/venv/bin/python3", + "args": [ + "-m", + "pip", + "install", + "-e", + "${workspaceFolder}" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [], + "presentation": { + "focus": true + } + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/pydiffchecker/__init__.py b/pydiffchecker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pydiffchecker/cli.py b/pydiffchecker/cli.py new file mode 100644 index 0000000..1cbb0f1 --- /dev/null +++ b/pydiffchecker/cli.py @@ -0,0 +1,22 @@ +import argparse +import json +from .version import get_version +from .line_shift_checker import LineShiftChecker + + +def main(): + parser = argparse.ArgumentParser(description='Diff checker') + + parser.add_argument('-v', + '--version', + action='version', + version=f'v{get_version()}') + parser.add_argument('revision_since') + parser.add_argument('revision_until') + + args = parser.parse_args() + + line_shift_checker = LineShiftChecker(args.revision_since, args.revision_until) + out = line_shift_checker.get_shifted_lines() + + print(json.dumps(out, indent=4)) diff --git a/pydiffchecker/git_version.py b/pydiffchecker/git_version.py new file mode 100644 index 0000000..d1a067b --- /dev/null +++ b/pydiffchecker/git_version.py @@ -0,0 +1,28 @@ +import subprocess + +GIT_DESCRIBE_COMMAND = ['git', 'describe', '--tags', '--long', '--dirty'] +VERSION_FORMAT = '{tag}.dev{commitcount}+{gitsha}{dirty}' + + +def get_version() -> str: + version = subprocess.check_output(GIT_DESCRIBE_COMMAND).decode().strip() + return format_version(version=version) + + +def format_version(version: str) -> str: + parts = version.split('-') + dirty = len(parts) == 4 + tag, count, sha = parts[:3] + tag = tag.lstrip('v') + if count == '0' and not dirty: + return tag + return VERSION_FORMAT.format( + tag=tag, + commitcount=count, + gitsha=sha.lstrip('g'), + dirty='dirty' if dirty else '' + ) + + +if __name__ == '__main__': + print(get_version()) diff --git a/pydiffchecker/helper.py b/pydiffchecker/helper.py new file mode 100644 index 0000000..f9d4e08 --- /dev/null +++ b/pydiffchecker/helper.py @@ -0,0 +1,16 @@ +import subprocess +from typing import Iterator + + +def subprocess_readlines(cmd, cwd=None) -> Iterator[str]: + process = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE) + + for line in process.stdout: + line = line.decode().rstrip(r'\n') + + yield line + + process.wait() + + if process.returncode != 0: + raise subprocess.CalledProcessError(process.returncode, cmd) diff --git a/pydiffchecker/line_shift_checker.py b/pydiffchecker/line_shift_checker.py new file mode 100644 index 0000000..0b28962 --- /dev/null +++ b/pydiffchecker/line_shift_checker.py @@ -0,0 +1,70 @@ +import re +from typing import List, Dict +from .helper import subprocess_readlines + + +class LineShiftChecker: + DIFF_BLOCK_REGEX = r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@' + + def __init__(self, revision_since, revision_until) -> None: + self.revision_since = revision_since + self.revision_until = revision_until + + def get_shifted_lines(self) -> Dict[str, Dict]: + shifted_lines = {} + + for file_info in self.__get_changed_files(): + shifted_lines[file_info['src']] = self.__get_shifted_lines_in_file(file_info) + + return shifted_lines + + def __get_changed_files(self) -> List[Dict]: + process_output = subprocess_readlines(['git', 'diff', '--name-status', '--diff-filter=MR', + self.revision_since, self.revision_until]) + + file_list = [] + for line in process_output: + raw_file_info = line.split() + file_list.append({ + 'src': raw_file_info[1], + 'dst': raw_file_info[2] if len(raw_file_info) > 2 else raw_file_info[1], + }) + + return file_list + + def __get_shifted_lines_in_file(self, file_info) -> 'Dict[str, str | None]': + lines_in_source_file = self.__count_lines_in_source_file(file_info['dst']) + process_output = subprocess_readlines(['git', 'diff', f'-U{lines_in_source_file}', + self.revision_since, self.revision_until, '--', + file_info['src'], file_info['dst']]) + + diff_started = False + shifted_lines = {} + for line in process_output: + matches = re.search(LineShiftChecker.DIFF_BLOCK_REGEX, line) + if matches: + old_start = int(matches.group(1)) + new_start = int(matches.group(3)) + diff_started = True + continue + + if not diff_started: + continue + + if line.startswith(' '): + shifted_lines[f'{file_info["src"]}:{old_start}'] = f'{file_info["dst"]}:{new_start}' + old_start += 1 + new_start += 1 + elif line.startswith('+'): + new_start += 1 + elif line.startswith('-'): + shifted_lines[f'{file_info["src"]}:{old_start}'] = None + old_start += 1 + + assert lines_in_source_file == len(shifted_lines) + + return shifted_lines + + def __count_lines_in_source_file(self, file) -> int: + process_output = subprocess_readlines(['git', 'show', f'{self.revision_since}:{file}']) + return sum(1 for _ in process_output) diff --git a/pydiffchecker/version.py b/pydiffchecker/version.py new file mode 100644 index 0000000..38c4eaa --- /dev/null +++ b/pydiffchecker/version.py @@ -0,0 +1,9 @@ +from importlib.metadata import version, PackageNotFoundError + + +def get_version(): + try: + return version('pydiffchecker') + except PackageNotFoundError: + # package is not installed + return '0.0.0' diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7ce08c2 --- /dev/null +++ b/setup.py @@ -0,0 +1,23 @@ +import setuptools + +from pydiffchecker.git_version import get_version + +with open('README.md', 'r', encoding='utf-8') as fh: + long_description = fh.read() + +setuptools.setup( + name='pydiffchecker', + version=get_version(), + url='https://git.esoko.eu/bence/pydiffchecker.git', + author='Bence Pocze', + author_email='bence@pocze.ch', + description='Wrappers for git diff', + long_description=long_description, + long_description_content_type='text/markdown', + packages=setuptools.find_packages(), + entry_points={ + 'console_scripts': ['pydiffchecker=pydiffchecker.cli:main'], + }, + python_requires='>=3.8', + install_requires=[] +) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..6deafc2 --- /dev/null +++ b/tox.ini @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120