diff --git a/gira/__main__.py b/gira/__main__.py index d34b8bf..03f3090 100644 --- a/gira/__main__.py +++ b/gira/__main__.py @@ -1,44 +1,42 @@ +import argparse import logging import sys from pathlib import Path -import click - from . import config as config_parser from . import gira, logger -@click.command() -@click.option("-r", "--ref", "ref", type=str) -@click.option("-c", "--config", "config", type=str) -@click.option("-v", "--verbose", "verbose", type=bool, is_flag=True) -@click.option( - "-f", - "--format", - "format", - type=str, - default="commit", - help="Output format: commit, detail, markdown", -) -@click.argument("args", nargs=-1) -def main(config: str, ref: str, verbose: bool, format: str, args: list[str]) -> int: - logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO, stream=sys.stderr) +def main() -> int: + """Prepare gira to run with arguments from command line. Return exit code.""" + parser = argparse.ArgumentParser(description="Gira - Git Dependencies Analyzer") + parser.add_argument("-r", "--ref", type=str) + parser.add_argument("-c", "--config", type=str) + parser.add_argument("-v", "--verbose", action="store_true") + parser.add_argument( + "-f", "--format", type=str, default="commit", help="Output format: commit, detail, markdown" + ) + parser.add_argument("args", nargs=argparse.REMAINDER) + args = parser.parse_args(sys.argv[1:]) + + logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO, stream=sys.stderr) logging.getLogger("urllib3").setLevel(logging.WARNING) - conf = config_parser.from_file(Path(config) if config else None) + logger.debug(f"Arguments: {args}") + conf = config_parser.from_file(Path(args.config) if args.config else None) if not conf.observe and not conf.submodules: logger.error("No observed dependencies found in gira configuration file") return 1 stream = sys.stdout - precommit = len(args) > 0 and args[0] == ".git/COMMIT_EDITMSG" + precommit = len(args.args) > 0 and args.args[0] == ".git/COMMIT_EDITMSG" if precommit: - commit_msg_file = args[0] + commit_msg_file = args.args[0] logger.debug(f"Outputting to commit message file {commit_msg_file}") stream = Path(commit_msg_file).open("at", newline="\n") try: - gira.gira(conf, format=format, stream=stream, ref=ref) + gira.gira(conf, format=args.format, stream=stream, ref=args.ref) return 0 except Exception as e: logger.debug(e, stack_info=True) diff --git a/gira/cache.py b/gira/cache.py index f58c6c6..08ab765 100644 --- a/gira/cache.py +++ b/gira/cache.py @@ -10,31 +10,36 @@ CACHE_DIR = Path(".gira_cache") -def cache(project: str, url: str) -> pygit2.Repository: - """Return commit messages between two revisions a and b""" - repo_dir = CACHE_DIR / (project + ".git") +def cache(name: str, url: str) -> pygit2.Repository: + """Cache a git repository by its url ane name and return a pygit2.Repository object to it""" + repo_dir = CACHE_DIR / (name + ".git") if not CACHE_DIR.exists(): CACHE_DIR.mkdir() - if not url.startswith("http") and not url.startswith("git@"): + # add a protocol and .git suffix if missing + if "://" not in url and not url.startswith("git@"): url = f"https://{url}" if not url.endswith(".git"): url = f"{url}.git" # use the binary for remote url to avoid issues with ssh keys if not repo_dir.exists(): - logger.debug(f"Cloning {project} with url {url} to {repo_dir}") - subprocess.run(["git", "clone", "--bare", url, repo_dir], check=True, capture_output=True) + logger.debug(f"Cloning {name} with url {url} to {repo_dir}") + subprocess.run( + ["git", "clone", "--bare", url, str(repo_dir)], check=True, capture_output=True + ) else: logger.debug("Fetching from origin") - subprocess.run(["git", "fetch", "origin"], cwd=repo_dir, check=True, capture_output=True) + subprocess.run( + ["git", "fetch", "origin"], cwd=str(repo_dir), check=True, capture_output=True + ) return repo.Repo(repo_dir, ref="HEAD", bare=True) -def messages(project: str, url: str, a: str, b: str) -> list[str]: +def messages(name: str, url: str, a: str, b: str) -> list[str]: """Return commit messages between two revisions a and b for cached git repository @deprecated use cache() and repo.messages() instead. """ - repo = cache(project, url) + repo = cache(name, url) return repo.messages(a, b) diff --git a/gira/deps.py b/gira/deps.py index 6ef94d0..8717761 100644 --- a/gira/deps.py +++ b/gira/deps.py @@ -8,7 +8,6 @@ import tomllib as toml else: import tomli as toml - from pathlib import Path version_re = re.compile(r"""([0-9]+\.[0-9]+[^"',]*)""") @@ -60,7 +59,7 @@ def parse_pytoml(content: str, observed: dict[str, str]) -> dict[str, str]: dependencies[name] = "v" + version_match.group(1) if _section(parsed, "tool.poetry.dependencies"): - """The developer chould decide not to version poetry.lock so we need to parse pyproject.toml + """The developer could decide not to version poetry.lock so we need to parse pyproject.toml Example: [tool.poetry.dependencies] @@ -69,7 +68,7 @@ def parse_pytoml(content: str, observed: dict[str, str]) -> dict[str, str]: pymavlink = "^2.4.20" ruff = {version="*", optional=true} """ - for dependency, value in _section(parsed, "tool.poetry.dependencies"): + for dependency, value in _section(parsed, "tool.poetry.dependencies").items(): if dependency not in observed: continue version = "" @@ -117,8 +116,7 @@ def parse_package_lock(content: str, Set: set[str]) -> dict[str, str]: "@fontsource/roboto": "^4.2.3", "@fontsource/titillium-web": "^4.5.8", """ - # TODO: implement - return {} + raise NotImplementedError("Not implemented parsing dependencies from package.json") def parse_pubspec_yaml(content: str, observed: dict[str, str]) -> dict[str, str]: diff --git a/gira/repo.py b/gira/repo.py index 5dcee65..6c477c6 100644 --- a/gira/repo.py +++ b/gira/repo.py @@ -18,7 +18,10 @@ def __init__(self, path: Path, ref: Optional[str] = "", bare: bool = False): self.path = path self.repo = pygit2.Repository(str(path), pygit2.GIT_REPOSITORY_OPEN_BARE if bare else 0) self.bare = bare - self.ref = self._check_ref(ref) + try: + self.ref = self._check_ref(ref) + except KeyError as e: + raise RuntimeError(f"Revision {e.args[0]} does not exist") self._submodules = None @property @@ -92,10 +95,8 @@ def _check_ref(self, ref: Optional[str]): return "refs/tags/" + ref except KeyError: return "refs/heads/" + ref - if self.repo and len(self.repo.diff("HEAD")) == 0: return "HEAD^" - return "HEAD" def messages(self, a: str, b: Optional[str] = None): diff --git a/pyproject.toml b/pyproject.toml index 2cacf96..82a0c96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ readme = "README.md" license.text = "MIT" authors = [{name = "Tomas Peterka", email = "tomas.peterka@dronetag.cz"},] dependencies = [ - "click", "pygit2", "tomli; python_version < '3.11'", "pyyaml", diff --git a/run_tests.sh b/run_tests.sh new file mode 100644 index 0000000..26ee1e8 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +# make sure the binaries are available +which git +which envsubst +which gira + +cd tests +. run.sh diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..61a1174 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,19 @@ +# Tests + +Here are tests that deal with git repositories. The "remote" is TARed in remote/ +folder and extracted at the begging of the test. Since file:// remotes must be +specified with absolute path then we have the local-template repository where +$GIRA_TEST_ROOT is being replaced (with pretty much just value of $PWD). + + +## Modify remote + +We kept conveniently whole dep1 repository so you can just unTAR it, make commits +and then TAR it again. All of this just to avoid having sub-repositories in gira +repository. + + +## Test workings + +Tests do `git init` and then make commits/changes so the .git repository does not +need to be part of gira repository. diff --git a/tests/local-template/poetry/poetry.lock b/tests/local-template/poetry/poetry.lock new file mode 100644 index 0000000..89c8ffc --- /dev/null +++ b/tests/local-template/poetry/poetry.lock @@ -0,0 +1,39 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "dep1" +version = "1.0.0" +description = "The dummy dependency" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "dummy.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "dummy-1.0.0.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "future" +version = "0.18.3" +description = "Clean single-source support for Python 3 and 2" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"}, +] diff --git a/tests/local-template/poetry/pyproject.toml b/tests/local-template/poetry/pyproject.toml new file mode 100644 index 0000000..e294d9c --- /dev/null +++ b/tests/local-template/poetry/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "gira-test" +version = "0.0.0" +repository = "https://github.com/dronetag/gira" +authors = [ + "Tomas Peterka ", +] +description = "Dummy pytoml" +readme = "README.md" +license = "MIT" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[tool.poetry.dependencies] +python = "^3.8" +dep1 = "1.0.0" +wheel = "*" +pytest = {version="*", optional=true} + +[tool.poetry.extras] +dev = ["wheel", "pytest"] + +[tool.gira.observe] +dep1 = "file://${GIRA_TEST_ROOT}/remote/dep1/.git" diff --git a/tests/local-template/pubspec.yaml b/tests/local-template/pubspec.yaml new file mode 100644 index 0000000..e8a3d02 --- /dev/null +++ b/tests/local-template/pubspec.yaml @@ -0,0 +1,25 @@ +name: gira_test_app +description: >- + Test of pubspec.yaml changes + +version: 0.0.0 +homepage: https://www.dronetag.cz +repository: https://github.com/dronetag/gira + +publish_to: none + +environment: + sdk: ">=3.2.4 <4.0.0" + flutter: ">=3.16.7" + +dependencies: + analyzer: ^5.13.0 + dep1: + git: + url: file:///${GIRA_TEST_ROOT}/remote/dep1/.git + ref: v1.0.0 + non-internal-dep2: ^0.9.3+5 + +gira: + observe: + dep1: "file:///${GIRA_TEST_ROOT}/remote/dep1/.git" diff --git a/tests/local-template/pyproject.toml b/tests/local-template/pyproject.toml new file mode 100644 index 0000000..92ec5b0 --- /dev/null +++ b/tests/local-template/pyproject.toml @@ -0,0 +1,23 @@ +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "gira-test" +version = "0.0.0" +requires-python = ">=3.8" +description = "GIRA places JIRA tickets from your dependencies into your commit message" +readme = "README.md" +license.text = "MIT" +authors = [{name = "Tomas Peterka", email = "tomas.peterka@dronetag.cz"},] +dependencies = [ + "click", + "pygit2", + "tomli; python_version < '3.11'", + "pyyaml", + "jira", + "dep1==1.0.0", +] + +[tool.gira.observe] +dep1 = "file://${GIRA_TEST_ROOT}/remote/dep1/.git" diff --git a/tests/local-template/west.yaml b/tests/local-template/west.yaml new file mode 100644 index 0000000..3464488 --- /dev/null +++ b/tests/local-template/west.yaml @@ -0,0 +1,19 @@ +manifest: + version: 0.7 + + remotes: + - name: local + url-base: file://${GIRA_TEST_ROOT}/remote + + defaults: + remote: local + + projects: + - name: dep1 + repo-path: libs/dep1 + revision: v1.0.0 + import: true + + gira: + observe: + dep1: "file://${GIRA_TEST_ROOT}/remote/dep1/.git" diff --git a/tests/remote/dep1.tar b/tests/remote/dep1.tar new file mode 100644 index 0000000..9efbf8f Binary files /dev/null and b/tests/remote/dep1.tar differ diff --git a/tests/run.sh b/tests/run.sh new file mode 100644 index 0000000..25d4fa6 --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,111 @@ +set -e + +export GIRA_TEST_ROOT=$PWD + +#### Prepare the test ####################### +## Untar git remote for the tests +pushd remote +rm -rf dep1 +tar -xf dep1.tar +popd + +rm -rf local +mkdir -p local/poetry +envsubst < local-template/poetry/pyproject.toml > local/poetry/pyproject.toml +envsubst < local-template/poetry/poetry.lock > local/poetry/poetry.lock +envsubst < local-template/pyproject.toml > local/pyproject.toml +envsubst < local-template/pubspec.yaml > local/pubspec.yaml +envsubst < local-template/west.yaml > local/west.yaml + + +pushd local +git init +git add . +git commit -m "Initial commit" + + +############################################# +## run tests +echo "Test poetry/poetry.lock" +git reset --hard +rm -rf .gira_cache output.txt +sed -i 's/1.0.0/1.1.0/g' poetry/poetry.lock +gira > output.txt +grep OCD-1234 output.txt +grep -v OCD-567 output.txt + + +echo "Test poetry/pyproject.toml" +git reset --hard +rm -rf .gira_cache output.txt +sed -i 's/1.0.0/1.1.1/g' poetry/pyproject.toml +gira -c poetry/pyproject.toml > output.txt +grep OCD-1234 output.txt +grep OCD-567 output.txt + + +echo "Test pyproject.toml" +git reset --hard +rm -rf .gira_cache output.txt +sed -i 's/1.0.0/1.1.0/g' pyproject.toml +gira > output.txt +grep OCD-1234 output.txt +grep -v OCD-567 output.txt + + +echo "Test pubspec.yaml" +git reset --hard +rm -rf .gira_cache output.txt +sed -i 's/1.0.0/1.1.1/g' pubspec.yaml +gira -c pubspec.yaml > output.txt +grep OCD-1234 output.txt +grep OCD-567 output.txt + + +echo "Test pubspec.yaml" +git reset --hard +rm -rf .gira_cache output.txt +sed -i 's/1.0.0/1.1.0/g' west.yaml +gira -c west.yaml > output.txt +grep OCD-1234 output.txt +grep -v OCD-567 output.txt + +echo "Test moving from 1.0.0 to 1.1.0 and then to 1.1.1" +git reset --hard +rm -rf .gira_cache output.txt +sed -i 's/1.0.0/1.1.0/g' west.yaml +gira -c west.yaml > output.txt +grep OCD-1234 output.txt +grep -v OCD-567 output.txt +# now move to 1.1.1 +sed -i 's/1.1.0/1.1.1/g' west.yaml +gira -c west.yaml > output.txt +grep OCD-1234 output.txt +grep OCD-567 output.txt + + +echo "Test pre-commit" +git reset --hard +rm -rf .gira_cache output.txt +sed -i 's/1.0.0/1.1.1/g' west.yaml +echo "" > .git/COMMIT_EDITMSG # clear the commit message +gira -c west.yaml .git/COMMIT_EDITMSG +grep OCD-1234 .git/COMMIT_EDITMSG # gira should output there instead of stdout +grep OCD-567 .git/COMMIT_EDITMSG # gira should output there instead of stdout + +echo "should stay" > randomFile.txt +gira -c west.yaml randomFile.txt # gira must not override anything else than the commit message file +grep "should stay" randomFile.txt # gira should not touch the file +grep -v OCD-1234 randomFile.txt +grep -v OCD-567 randomFile.txt + +popd + +############################################# +## cleanup local +rm -rf local + +## cleanup remote +pushd remote +rm -rf dep1 +popd