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)