diff --git a/.copier-answers.yml b/.copier-answers.yml index a979300..9309776 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 0.15.23 +_commit: 0.16.5 _src_path: gh:pawamoy/copier-pdm author_email: pawamoy@pm.me author_fullname: Timothée Mazzucotelli diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58919a9..b8964d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,11 +2,9 @@ name: ci on: push: - branches: - - main pull_request: branches: - - main + - main defaults: run: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1f699e2..c1f92ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,21 +1,25 @@ name: release on: push +permissions: + contents: write jobs: - github_release: + release: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') steps: - name: Checkout uses: actions/checkout@v3 + - name: Fetch all tags + run: git fetch --depth=1 --tags - name: Setup Python uses: actions/setup-python@v4 - name: Install git-changelog run: pip install git-changelog - name: Prepare release notes run: git-changelog --release-notes > release-notes.md - - name: Create GitHub release + - name: Create release uses: softprops/action-gh-release@v1 with: body_path: release-notes.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 262c547..7c80a6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [2.1.0](https://github.com/pawamoy/git-changelog/releases/tag/2.1.0) - 2023-08-04 + +[Compare with 2.0.0](https://github.com/pawamoy/git-changelog/compare/2.0.0...2.1.0) + +### Features + +- Add Bitbucket provider ([5d793e5](https://github.com/pawamoy/git-changelog/commit/5d793e540be4fe5a648742c285c0762c111537ee) by Sven Axelsson). + +### Code Refactoring + +- Stop using deprecated `datetime.utcfromtimestamp` (Python 3.12) ([1f3ed5d](https://github.com/pawamoy/git-changelog/commit/1f3ed5da94e2a7c8938645977e6a7a0ffde7f713) by Sven Axelsson). + ## [2.0.0](https://github.com/pawamoy/git-changelog/releases/tag/2.0.0) - 2023-07-03 [Compare with 1.0.1](https://github.com/pawamoy/git-changelog/compare/1.0.1...2.0.0) diff --git a/Makefile b/Makefile index 686de67..7e8de7c 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .DEFAULT_GOAL := help SHELL := bash DUTY := $(if $(VIRTUAL_ENV),,pdm run) duty -export PDM_MULTIRUN_VERSIONS ?= 3.8 3.9 3.10 3.11 +export PDM_MULTIRUN_VERSIONS ?= 3.8 3.9 3.10 3.11 3.12 args = $(foreach a,$($(subst -,_,$1)_args),$(if $(value $a),$a="$($a)")) check_quality_args = files diff --git a/README.md b/README.md index a04cfb9..ae893e3 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Automatic Changelog generator using Jinja2 templates. From git logs to change lo Built-in [Angular][angular-convention], [Conventional Commit][conventional-commit], [Atom][atom-convention] and basic conventions. - Git service/provider agnostic, plus references parsing (issues, commits, etc.). - Built-in [GitHub][github-refs] and [Gitlab][gitlab-refs] support. + Built-in [GitHub][github-refs], [Gitlab][gitlab-refs] and [Bitbucket][bitbucket-refs] support. - Understands [Semantic Versioning][semantic-versioning]: major/minor/patch for versions and commits. Guesses next version based on last commits. @@ -44,6 +44,7 @@ Automatic Changelog generator using Jinja2 templates. From git logs to change lo [conventional-commit]: https://www.conventionalcommits.org/en/v1.0.0/ [github-refs]: https://help.github.com/articles/autolinked-references-and-urls/ [gitlab-refs]: https://docs.gitlab.com/ce/user/markdown.html#special-gitlab-references +[bitbucket-refs]: https://support.atlassian.com/bitbucket-cloud/docs/markup-comments [git-trailers]: https://git-scm.com/docs/git-interpret-trailers [issue-14]: https://github.com/pawamoy/git-changelog/issues/14 @@ -168,7 +169,7 @@ options: insertion marker -->. -o, --output OUTPUT Output to given file. Default: stdout. -r, --parse-refs Parse provider-specific references in commit messages - (GitHub/GitLab issues, PRs, etc.). Default: False. + (GitHub/GitLab/Bitbucket issues, PRs, etc.). Default: False. -R, --release-notes Output release notes to stdout based on the last entry in the changelog. Default: False. -I, --input INPUT Read from given file when creating release notes. diff --git a/config/ruff.toml b/config/ruff.toml index 3c6f4ea..ea50c97 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -1,4 +1,4 @@ -target-version = "py37" +target-version = "py38" line-length = 132 exclude = [ "fixtures", @@ -71,6 +71,7 @@ ignore = [ "PLR0915", # Too many statements "SLF001", # Private member accessed "TRY003", # Avoid specifying long messages outside the exception class + "UP032", # Use f-string instead of `format` call ] [per-file-ignores] diff --git a/docs/usage.md b/docs/usage.md index 4454e52..668f1fd 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -23,7 +23,7 @@ Update a changelog in-place, overwriting and updating the "Unreleased" section, using the Angular commit message convention and the Keep A Changelog template (default): ```bash -git-changelog -io CHANGELOG.md -c angular +git-changelog -io CHANGELOG.md -c angular ``` Same thing, but now you're ready to tag so you tell *git-changelog* @@ -43,10 +43,10 @@ git-changelog -Tbio CHANGELOG.md -c angular -s build,deps,fix,feat,refactor ``` Generate a changelog using a custom template, -and parsing provider-specific references (GitHub/GitLab): +and parsing provider-specific references (GitHub/GitLab/Bitbucket): ```bash -git-changelog -rt path:./templates/changelog.md.jinja +git-changelog -rt path:./templates/changelog.md.jinja ``` Author's favorite, from Python: @@ -113,14 +113,14 @@ If a commit message summary (the first line of the message) with a particular word/prefix (case-insensitive), it is added to the corresponding section: -Type | Section -------- | ------- -`add` | Added -`fix` | Fixed -`change` | Changed -`remove` | Removed -`merge` | Merged -`doc` | Documented +Type | Section +---------|----------- +`add` | Added +`fix` | Fixed +`change` | Changed +`remove` | Removed +`merge` | Merged +`doc` | Documented ### Angular/Karma convention @@ -137,20 +137,20 @@ It expects the following format for commit messages: The types and corresponding sections *git-changelog* recognizes are: -Type | Section -------------- | ------- -`build` | Build -`chore` | Chore -`ci` | Continuous Integration -`deps` | Dependencies -`doc(s)` | Docs -`feat` | Features -`fix` | Bug Fixes -`perf` | Performance Improvements -`ref(actor)` | Code Refactoring -`revert` | Reverts -`style` | Style -`test(s)` | Tests +Type | Section +-------------|------------------------- +`build` | Build +`chore` | Chore +`ci` | Continuous Integration +`deps` | Dependencies +`doc(s)` | Docs +`feat` | Features +`fix` | Bug Fixes +`perf` | Performance Improvements +`ref(actor)` | Code Refactoring +`revert` | Reverts +`style` | Style +`test(s)` | Tests Breaking changes are detected by searching for `^break(s|ing changes?)?[ :]` in the commit message body. @@ -196,7 +196,7 @@ You can get inspiration from ## Understand the relationship with SemVer -[Semver](semver), or Semantic Versioning, helps users of tools and libraries +[Semver][semver], or Semantic Versioning, helps users of tools and libraries understand the impact of version changes. To quote SemVer itself: > Given a version number MAJOR.MINOR.PATCH, increment the: @@ -226,7 +226,7 @@ to find additional information. ### Provider-specific references -*git-changelog* will detect when you are using GitHub or GitLab +*git-changelog* will detect when you are using GitHub, GitLab or Bitbucket by checking the `origin` remote configured in your local clone (or the remote indicated by the value of the `GIT_CHANGELOG_REMOTE` environment variable). @@ -281,9 +281,9 @@ Part of epic #5: https://agile-software.com/super/project/epics/5 ``` As you can see, compared to provider-specific references, -trailers are written out explicitely, so it's a bit more work, +trailers are written out explicitly, so it's a bit more work, but this ensures your changelog can be rendered correctly *anywhere*, -not just on GitHub or GitLab, and without pre/post-processing. +not just on GitHub, GitLab or Bitbucket, and without pre/post-processing. Trailers are rendered in the Keep A Changelog template. If the value is an URL, a link is created with the token as title. diff --git a/duties.py b/duties.py index 945d000..17f6909 100644 --- a/duties.py +++ b/duties.py @@ -48,10 +48,10 @@ def merge(d1: Any, d2: Any) -> Any: # noqa: D103 def mkdocs_config() -> str: # noqa: D103 - from mkdocs import utils + import mergedeep - # patch YAML loader to merge arrays - utils.merge = merge + # force YAML loader to merge arrays + mergedeep.merge = merge if "+insiders" in pkgversion("mkdocs-material"): return "mkdocs.insiders.yml" @@ -103,6 +103,7 @@ def check_quality(ctx: Context) -> None: ctx.run( ruff.check(*PY_SRC_LIST, config="config/ruff.toml"), title=pyprefix("Checking code quality"), + command=f"ruff check --config config/ruff.toml {PY_SRC}", ) @@ -120,7 +121,11 @@ def check_dependencies(ctx: Context) -> None: allow_overrides=False, ) - ctx.run(safety.check(requirements), title="Checking dependencies") + ctx.run( + safety.check(requirements), + title="Checking dependencies", + command="pdm export -f requirements --without-hashes | safety check --stdin", + ) @duty @@ -132,7 +137,12 @@ def check_docs(ctx: Context) -> None: """ Path("htmlcov").mkdir(parents=True, exist_ok=True) Path("htmlcov/index.html").touch(exist_ok=True) - ctx.run(mkdocs.build(strict=True, config_file=mkdocs_config()), title=pyprefix("Building documentation")) + config = mkdocs_config() + ctx.run( + mkdocs.build(strict=True, config_file=config, verbose=True), + title=pyprefix("Building documentation"), + command=f"mkdocs build -vsf {config}", + ) @duty @@ -145,6 +155,7 @@ def check_types(ctx: Context) -> None: ctx.run( mypy.run(*PY_SRC_LIST, config_file="config/mypy.ini"), title=pyprefix("Type-checking"), + command=f"mypy --config-file config/mypy.ini {PY_SRC}", ) @@ -159,8 +170,9 @@ def check_api(ctx: Context) -> None: griffe_check = lazy(g_check, name="griffe.check") ctx.run( - griffe_check("git_changelog", search_paths=["src"]), + griffe_check("git_changelog", search_paths=["src"], color=True), title="Checking for API breaking changes", + command="griffe check -ssrc git_changelog", nofail=True, ) @@ -276,4 +288,5 @@ def test(ctx: Context, match: str = "") -> None: ctx.run( pytest.run("-n", "auto", "tests", config_file="config/pytest.ini", select=match, color="yes"), title=pyprefix("Running tests"), + command=f"pytest -c config/pytest.ini -n auto -k{match!r} --color=yes tests", ) diff --git a/mkdocs.yml b/mkdocs.yml index 5551136..077c4f1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,6 +6,12 @@ repo_name: "pawamoy/git-changelog" site_dir: "site" watch: [mkdocs.yml, README.md, CONTRIBUTING.md, CHANGELOG.md, src/git_changelog] copyright: Copyright © 2020 Timothée Mazzucotelli +edit_uri: edit/main/docs/ + +validation: + omitted_files: warn + absolute_links: warn + unrecognized_links: warn nav: - Home: diff --git a/pyproject.toml b/pyproject.toml index a587709..1495db4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ docs = [ "black>=23.1", "markdown-callouts>=0.2", "markdown-exec>=0.5", - "mkdocs>=1.3", + "mkdocs>=1.5", "mkdocs-coverage>=0.2", "mkdocs-gen-files>=0.3", "mkdocs-git-committers-plugin-2>=1.1", diff --git a/src/git_changelog/__init__.py b/src/git_changelog/__init__.py index 51fc713..3af14ec 100644 --- a/src/git_changelog/__init__.py +++ b/src/git_changelog/__init__.py @@ -5,6 +5,6 @@ from __future__ import annotations -from git_changelog.build import Changelog, Commit, GitHub, GitLab +from git_changelog.build import Bitbucket, Changelog, Commit, GitHub, GitLab -__all__: list[str] = ["Changelog", "Commit", "GitHub", "GitLab"] +__all__: list[str] = ["Bitbucket", "Changelog", "Commit", "GitHub", "GitLab"] diff --git a/src/git_changelog/build.py b/src/git_changelog/build.py index 5a889fc..26d123f 100644 --- a/src/git_changelog/build.py +++ b/src/git_changelog/build.py @@ -20,7 +20,7 @@ CommitConvention, ConventionalCommitConvention, ) -from git_changelog.providers import GitHub, GitLab, ProviderRefParser +from git_changelog.providers import Bitbucket, GitHub, GitLab, ProviderRefParser if TYPE_CHECKING: from pathlib import Path @@ -197,6 +197,8 @@ def __init__( provider = GitHub(namespace, project, url=provider_url) elif "gitlab" in provider_url: provider = GitLab(namespace, project, url=provider_url) + elif "bitbucket" in provider_url: + provider = Bitbucket(namespace, project, url=provider_url) self.remote_url: str = remote_url self.provider = provider diff --git a/src/git_changelog/cli.py b/src/git_changelog/cli.py index 244609e..36357f4 100644 --- a/src/git_changelog/cli.py +++ b/src/git_changelog/cli.py @@ -171,7 +171,7 @@ def get_parser() -> argparse.ArgumentParser: action="store_true", dest="parse_refs", default=False, - help="Parse provider-specific references in commit messages (GitHub/GitLab issues, PRs, etc.). Default: %(default)s.", + help="Parse provider-specific references in commit messages (GitHub/GitLab/Bitbucket issues, PRs, etc.). Default: %(default)s.", ) parser.add_argument( "-R", @@ -330,10 +330,10 @@ def build_and_render( last_released = _latest(lines, re.compile(version_regex)) if last_released: # check if the latest version is already in the changelog - if ( - last_released == changelog.versions_list[0].tag - or last_released == changelog.versions_list[0].planned_tag - ): + if last_released in [ + changelog.versions_list[0].tag, + changelog.versions_list[0].planned_tag, + ]: raise ValueError(f"Version {last_released} already in changelog") changelog.versions_list = _unreleased( changelog.versions_list, diff --git a/src/git_changelog/commit.py b/src/git_changelog/commit.py index 26987bb..bb6c3d8 100644 --- a/src/git_changelog/commit.py +++ b/src/git_changelog/commit.py @@ -6,7 +6,7 @@ from abc import ABC, abstractmethod from collections import defaultdict from contextlib import suppress -from datetime import datetime +from datetime import datetime, timezone from typing import TYPE_CHECKING, Any, ClassVar, Pattern if TYPE_CHECKING: @@ -59,11 +59,11 @@ def __init__( if not author_date: author_date = datetime.now() # noqa: DTZ005 elif isinstance(author_date, str): - author_date = datetime.utcfromtimestamp(float(author_date)) # noqa: DTZ004 + author_date = datetime.fromtimestamp(float(author_date), tz=timezone.utc) if not committer_date: committer_date = datetime.now() # noqa: DTZ005 elif isinstance(committer_date, str): - committer_date = datetime.utcfromtimestamp(float(committer_date)) # noqa: DTZ004 + committer_date = datetime.fromtimestamp(float(committer_date), tz=timezone.utc) self.hash: str = commit_hash self.author_name: str = author_name diff --git a/src/git_changelog/providers.py b/src/git_changelog/providers.py index cd5f654..9c971bc 100644 --- a/src/git_changelog/providers.py +++ b/src/git_changelog/providers.py @@ -302,3 +302,78 @@ def get_tag_url(self, tag: str = "") -> str: # noqa: D102 def get_compare_url(self, base: str, target: str) -> str: # noqa: D102 (use parent docstring) return self.build_ref_url("commits_ranges", {"ref": f"{base}...{target}"}) + + +class Bitbucket(ProviderRefParser): + """A parser for the Bitbucket references.""" + + url: str = "https://bitbucket.org" + project_url: str = "{base_url}/{namespace}/{project}" + tag_url: str = "{base_url}/{namespace}/{project}/commits/tag/{ref}" + + commit_min_length = 8 + commit_max_length = 40 + + REF: ClassVar[dict[str, RefDef]] = { + "issues": RefDef( + regex=re.compile(RefRe.BB + RefRe.NP + "?issue\\s*" + RefRe.ID.format(symbol="#"), re.I), + url_string="{base_url}/{namespace}/{project}/issues/{ref}", + ), + "merge_requests": RefDef( + regex=re.compile(RefRe.BB + RefRe.NP + "?pull request\\s*" + RefRe.ID.format(symbol=r"#"), re.I), + url_string="{base_url}/{namespace}/{project}/pull-request/{ref}", + ), + "commits": RefDef( + regex=re.compile( + RefRe.BB + + r"(?:{np}@)?{commit}{ba}".format( + np=RefRe.NP, + commit=RefRe.COMMIT.format(min=commit_min_length, max=commit_max_length), + ba=RefRe.BA, + ), + re.I, + ), + url_string="{base_url}/{namespace}/{project}/commits/{ref}", + ), + "commits_ranges": RefDef( + regex=re.compile( + RefRe.BB + + r"(?:{np}@)?{commit_range}".format( + np=RefRe.NP, + commit_range=RefRe.COMMIT_RANGE.format(min=commit_min_length, max=commit_max_length), + ), + re.I, + ), + url_string="{base_url}/{namespace}/{project}/branches/compare/{ref}#diff", + ), + "mentions": RefDef( + regex=re.compile(RefRe.BB + RefRe.MENTION, re.I), + url_string="{base_url}/{ref}", + ), + } + + def __init__(self, namespace: str, project: str, url: str = url): + """Initialization method. + + Arguments: + namespace: The Bitbucket namespace. + project: The Bitbucket project. + url: The Bitbucket URL. + """ + self.namespace: str = namespace + self.project: str = project + self.url: str = url # (shadowing but uses class' as default) + + def build_ref_url(self, ref_type: str, match_dict: dict[str, str]) -> str: # noqa: D102 (use parent docstring) + match_dict["base_url"] = self.url + if not match_dict.get("namespace"): + match_dict["namespace"] = self.namespace + if not match_dict.get("project"): + match_dict["project"] = self.project + return super().build_ref_url(ref_type, match_dict) + + def get_tag_url(self, tag: str = "") -> str: # noqa: D102 + return self.tag_url.format(base_url=self.url, namespace=self.namespace, project=self.project, ref=tag) + + def get_compare_url(self, base: str, target: str) -> str: # noqa: D102 (use parent docstring) + return self.build_ref_url("commits_ranges", {"ref": f"{target}..{base}"}) diff --git a/tests/test_providers.py b/tests/test_providers.py index 885b8a3..6829dbc 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -24,6 +24,8 @@ A snippet: $45 Some labels: ~18, ~bug, ~"multi word label" Some milestones: %2, %version1, %"awesome version" +A Bitbucket PR: Pull request #1 +A Bitbucket issue: Issue #1 """ @@ -39,3 +41,10 @@ def test_gitlab_issue_parsing() -> None: gitlab = git_changelog.GitLab("pawamoy", "git-changelog") for ref in gitlab.REF: assert gitlab.get_refs(ref, text) + + +def test_bitbucket_issue_parsing() -> None: + """Bitbucket issues are correctly parsed.""" + bitbucket = git_changelog.Bitbucket("pawamoy", "git-changelog") + for ref in bitbucket.REF: + assert bitbucket.get_refs(ref, text)