zendev-commit 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zendev/commit/__init__.py +572 -0
- zendev/commit/py.typed +0 -0
- zendev/conventional/__init__.py +145 -0
- zendev/conventional/py.typed +0 -0
- zendev/data/LICENSE.gitmoji +21 -0
- zendev/data/emoji-conventions.toml +80 -0
- zendev/data/gitmojis.json +605 -0
- zendev/gitmoji/__init__.py +182 -0
- zendev/gitmoji/py.typed +0 -0
- zendev_commit-0.2.0.dist-info/METADATA +119 -0
- zendev_commit-0.2.0.dist-info/RECORD +13 -0
- zendev_commit-0.2.0.dist-info/WHEEL +4 -0
- zendev_commit-0.2.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Offline gitmoji catalog and official-header parsing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import tomllib
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from functools import cache
|
|
10
|
+
from importlib.resources import files
|
|
11
|
+
|
|
12
|
+
from zendev.conventional import ParseIssue
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"EmojiConvention",
|
|
16
|
+
"Gitmoji",
|
|
17
|
+
"GitmojiCommit",
|
|
18
|
+
"GitmojiMatch",
|
|
19
|
+
"load_emoji_conventions",
|
|
20
|
+
"load_gitmojis",
|
|
21
|
+
"match_gitmoji",
|
|
22
|
+
"parse_gitmoji_commit",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class Gitmoji:
|
|
28
|
+
emoji: str
|
|
29
|
+
code: str
|
|
30
|
+
description: str
|
|
31
|
+
name: str
|
|
32
|
+
semver: str | None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class EmojiConvention:
|
|
37
|
+
"""A Gitmoji intention paired with its canonical zendev commit type."""
|
|
38
|
+
|
|
39
|
+
type: str
|
|
40
|
+
gitmoji: Gitmoji
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, slots=True)
|
|
44
|
+
class GitmojiMatch:
|
|
45
|
+
gitmoji: Gitmoji
|
|
46
|
+
token: str
|
|
47
|
+
remainder: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class GitmojiCommit:
|
|
52
|
+
intention: Gitmoji
|
|
53
|
+
token: str
|
|
54
|
+
scope: str | None
|
|
55
|
+
message: str
|
|
56
|
+
body: str | None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@cache
|
|
60
|
+
def load_gitmojis() -> tuple[Gitmoji, ...]:
|
|
61
|
+
catalog_path = files("zendev").joinpath("data/gitmojis.json")
|
|
62
|
+
payload = json.loads(catalog_path.read_text(encoding="utf-8"))
|
|
63
|
+
return tuple(
|
|
64
|
+
Gitmoji(
|
|
65
|
+
emoji=item["emoji"],
|
|
66
|
+
code=item["code"],
|
|
67
|
+
description=item["description"],
|
|
68
|
+
name=item["name"],
|
|
69
|
+
semver=item["semver"],
|
|
70
|
+
)
|
|
71
|
+
for item in payload["gitmojis"]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@cache
|
|
76
|
+
def load_emoji_conventions() -> tuple[EmojiConvention, ...]:
|
|
77
|
+
"""Load and validate the complete, strict emoji-to-type convention."""
|
|
78
|
+
|
|
79
|
+
mapping_path = files("zendev").joinpath("data/emoji-conventions.toml")
|
|
80
|
+
payload = tomllib.loads(mapping_path.read_text(encoding="utf-8"))
|
|
81
|
+
types = payload.get("types")
|
|
82
|
+
if not isinstance(types, dict):
|
|
83
|
+
raise ValueError("emoji-conventions.toml must contain a string-to-string [types] table.")
|
|
84
|
+
type_mapping = {name: value for name, value in types.items() if isinstance(name, str) and isinstance(value, str)}
|
|
85
|
+
if len(type_mapping) != len(types):
|
|
86
|
+
raise ValueError("emoji-conventions.toml must contain a string-to-string [types] table.")
|
|
87
|
+
|
|
88
|
+
catalog = load_gitmojis()
|
|
89
|
+
catalog_names = {item.name for item in catalog}
|
|
90
|
+
mapped_names = set(type_mapping)
|
|
91
|
+
if mapped_names != catalog_names:
|
|
92
|
+
missing = ", ".join(sorted(catalog_names - mapped_names)) or "none"
|
|
93
|
+
extra = ", ".join(sorted(mapped_names - catalog_names)) or "none"
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"Emoji convention must cover the Gitmoji catalog exactly (missing: {missing}; extra: {extra})."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
type_names = tuple(type_mapping[item.name] for item in catalog)
|
|
99
|
+
if any(re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", type_name) is None for type_name in type_names):
|
|
100
|
+
raise ValueError("Emoji convention types must be lowercase words separated by single hyphens.")
|
|
101
|
+
if len(set(type_names)) != len(type_names):
|
|
102
|
+
raise ValueError("Emoji convention types must be unique.")
|
|
103
|
+
|
|
104
|
+
return tuple(EmojiConvention(type=type_mapping[item.name], gitmoji=item) for item in catalog)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@cache
|
|
108
|
+
def _token_index() -> tuple[tuple[str, Gitmoji], ...]:
|
|
109
|
+
tokens: dict[str, Gitmoji] = {}
|
|
110
|
+
for gitmoji in load_gitmojis():
|
|
111
|
+
tokens[gitmoji.emoji] = gitmoji
|
|
112
|
+
tokens[gitmoji.code] = gitmoji
|
|
113
|
+
without_variation_selector = gitmoji.emoji.replace("\ufe0f", "")
|
|
114
|
+
tokens.setdefault(without_variation_selector, gitmoji)
|
|
115
|
+
return tuple(sorted(tokens.items(), key=lambda item: len(item[0]), reverse=True))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def match_gitmoji(text: str) -> GitmojiMatch | None:
|
|
119
|
+
for token, gitmoji in _token_index():
|
|
120
|
+
if text.startswith(token) and len(text) > len(token) and text[len(token)].isspace():
|
|
121
|
+
return GitmojiMatch(gitmoji=gitmoji, token=token, remainder=text[len(token) :].lstrip())
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def parse_gitmoji_commit(text: str) -> tuple[GitmojiCommit | None, ParseIssue | None]:
|
|
126
|
+
"""Parse the official gitmoji title form and an optional Git body."""
|
|
127
|
+
|
|
128
|
+
lines = text.splitlines()
|
|
129
|
+
if not lines or not lines[0]:
|
|
130
|
+
return None, ParseIssue("empty-message", "The commit message is empty.")
|
|
131
|
+
match = match_gitmoji(lines[0])
|
|
132
|
+
if match is None:
|
|
133
|
+
return None, ParseIssue(
|
|
134
|
+
"invalid-gitmoji",
|
|
135
|
+
"Expected an official gitmoji Unicode token or shortcode.",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
remainder = match.remainder
|
|
139
|
+
scope: str | None = None
|
|
140
|
+
if remainder.startswith("("):
|
|
141
|
+
closing = remainder.find(")")
|
|
142
|
+
if closing <= 1:
|
|
143
|
+
return None, ParseIssue("invalid-gitmoji-scope", "The gitmoji scope must be non-empty.")
|
|
144
|
+
scope = remainder[1:closing]
|
|
145
|
+
remainder = remainder[closing + 1 :]
|
|
146
|
+
if remainder.startswith(":"):
|
|
147
|
+
if len(remainder) == 1 or not remainder[1].isspace():
|
|
148
|
+
return None, ParseIssue(
|
|
149
|
+
"invalid-gitmoji-separator",
|
|
150
|
+
"A colon after the gitmoji scope must be followed by a space.",
|
|
151
|
+
)
|
|
152
|
+
remainder = remainder[1:]
|
|
153
|
+
elif not remainder or not remainder[0].isspace():
|
|
154
|
+
return None, ParseIssue(
|
|
155
|
+
"invalid-gitmoji-separator",
|
|
156
|
+
"The gitmoji scope and message must be separated by whitespace or ': '.",
|
|
157
|
+
)
|
|
158
|
+
elif remainder.startswith(":"):
|
|
159
|
+
if len(remainder) == 1 or not remainder[1].isspace():
|
|
160
|
+
return None, ParseIssue(
|
|
161
|
+
"invalid-gitmoji-separator",
|
|
162
|
+
"A gitmoji colon must be followed by a space.",
|
|
163
|
+
)
|
|
164
|
+
remainder = remainder[1:]
|
|
165
|
+
message = remainder.lstrip()
|
|
166
|
+
if not message:
|
|
167
|
+
return None, ParseIssue("missing-gitmoji-message", "The gitmoji message is required.")
|
|
168
|
+
|
|
169
|
+
if len(lines) > 1 and lines[1] != "":
|
|
170
|
+
return None, ParseIssue(
|
|
171
|
+
"missing-header-separator",
|
|
172
|
+
"The body must begin one blank line after the gitmoji message.",
|
|
173
|
+
line=2,
|
|
174
|
+
)
|
|
175
|
+
body = "\n".join(lines[2:]).strip() if len(lines) > 2 else ""
|
|
176
|
+
return GitmojiCommit(
|
|
177
|
+
intention=match.gitmoji,
|
|
178
|
+
token=match.token,
|
|
179
|
+
scope=scope,
|
|
180
|
+
message=message,
|
|
181
|
+
body=body or None,
|
|
182
|
+
), None
|
zendev/gitmoji/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: zendev-commit
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Commit-message conventions and interactive commit workflows
|
|
5
|
+
Project-URL: Homepage, https://github.com/zendev-lab/zendev
|
|
6
|
+
Project-URL: Issues, https://github.com/zendev-lab/zendev/issues
|
|
7
|
+
Project-URL: Repository, https://github.com/zendev-lab/zendev
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: questionary>=2.1
|
|
11
|
+
Requires-Dist: typer>=0.27
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# zendev-commit
|
|
15
|
+
|
|
16
|
+
`zendev-commit` owns commit profiles, Conventional Commits and Gitmoji
|
|
17
|
+
validation, the interactive commit flow, and its vendored data. It can be
|
|
18
|
+
installed and used without the complete `zendev` toolkit.
|
|
19
|
+
|
|
20
|
+
```console
|
|
21
|
+
$ uv add --dev zendev-commit
|
|
22
|
+
$ uvx --from zendev-commit zendev-commit --help
|
|
23
|
+
$ uvx --from zendev-commit zendev-commit-msg --help
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`zendev-commit-msg` validates complete commit messages. `zendev-commit`
|
|
27
|
+
interactively builds a message and invokes `git commit`.
|
|
28
|
+
|
|
29
|
+
## Python API
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from zendev.commit import CommitProfile, validate_commit_message
|
|
33
|
+
|
|
34
|
+
result = validate_commit_message(
|
|
35
|
+
"feat(parser): accept empty input",
|
|
36
|
+
profile=CommitProfile.CONVENTIONAL,
|
|
37
|
+
)
|
|
38
|
+
assert result.valid
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Profiles
|
|
42
|
+
|
|
43
|
+
Configure the default profile in the consuming repository:
|
|
44
|
+
|
|
45
|
+
```toml
|
|
46
|
+
[tool.zendev.commit]
|
|
47
|
+
profile = "conventional"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The `--profile` option overrides repository configuration. `auto` reads the
|
|
51
|
+
nearest `pyproject.toml` and falls back to `zendev`.
|
|
52
|
+
|
|
53
|
+
| Profile | Contract |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `zendev` | A Gitmoji emoji or shortcode paired with its canonical commit type. |
|
|
56
|
+
| `conventional` | Conventional Commits 1.0.0, including scopes, breaking changes, bodies, and footers. |
|
|
57
|
+
| `gitmoji` | The Gitmoji title shape with Unicode or shortcode intentions and optional scope/body. |
|
|
58
|
+
|
|
59
|
+
Examples accepted by the default profile:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
🎉 init: begin a project
|
|
63
|
+
✨ feat: add export
|
|
64
|
+
🐛 fix(parser): handle null token
|
|
65
|
+
:memo: docs: update README
|
|
66
|
+
🚀 deploy: publish the package
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Git-generated `Merge`, `Revert`, `fixup!`, `squash!`, `amend!`, and `reword!`
|
|
70
|
+
messages are accepted.
|
|
71
|
+
|
|
72
|
+
## Commit hook
|
|
73
|
+
|
|
74
|
+
With `.pre-commit-config.yaml`:
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
repos:
|
|
78
|
+
- repo: https://github.com/zendev-lab/zendev
|
|
79
|
+
rev: v0.1.0
|
|
80
|
+
hooks:
|
|
81
|
+
- id: zendev-commit-msg
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
With `prek.toml`:
|
|
85
|
+
|
|
86
|
+
```toml
|
|
87
|
+
[[repos]]
|
|
88
|
+
repo = "https://github.com/zendev-lab/zendev"
|
|
89
|
+
rev = "v0.1.0"
|
|
90
|
+
hooks = [
|
|
91
|
+
{ id = "zendev-commit-msg" },
|
|
92
|
+
]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Install the Git hook:
|
|
96
|
+
|
|
97
|
+
```console
|
|
98
|
+
$ uvx prek install --hook-type commit-msg
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
An explicit command is also available:
|
|
102
|
+
|
|
103
|
+
```console
|
|
104
|
+
$ uvx --from zendev-commit zendev-commit-msg --profile conventional .git/COMMIT_EDITMSG
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Gitmoji data
|
|
108
|
+
|
|
109
|
+
The default profile uses the vendored catalog in
|
|
110
|
+
[`src/zendev/data/gitmojis.json`](./src/zendev/data/gitmojis.json) and the
|
|
111
|
+
reviewable pairing table in
|
|
112
|
+
[`src/zendev/data/emoji-conventions.toml`](./src/zendev/data/emoji-conventions.toml).
|
|
113
|
+
Validation is deterministic and does not access the network.
|
|
114
|
+
|
|
115
|
+
The retained upstream license is in
|
|
116
|
+
[`src/zendev/data/LICENSE.gitmoji`](./src/zendev/data/LICENSE.gitmoji).
|
|
117
|
+
Maintainers should follow the
|
|
118
|
+
[vendored-data procedure](../../CONTRIBUTING.md#vendored-gitmoji-data) when
|
|
119
|
+
refreshing the catalog.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
zendev/commit/__init__.py,sha256=B46lCIchjlCFtZEu7wP8LQ7vIbzxYnmw6lAAmswmrzw,18458
|
|
2
|
+
zendev/commit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
zendev/conventional/__init__.py,sha256=2FrhUa9r8pLctJy_3E0XF9u0x3ztt19854Q49HBeWlI,4130
|
|
4
|
+
zendev/conventional/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
zendev/data/LICENSE.gitmoji,sha256=Rj862ib3jsO1Zs4S-gmYLmDl-MBubo3sHzK1WcT01aY,1075
|
|
6
|
+
zendev/data/emoji-conventions.toml,sha256=yqdFA84-QxJtmfvjhaadLwqbozcyD9m2Z7YeyugirL4,1873
|
|
7
|
+
zendev/data/gitmojis.json,sha256=t-8tSHnRPufn-Bd3hP_bAACC0fIvQmF_R_u7fK3yMWc,14924
|
|
8
|
+
zendev/gitmoji/__init__.py,sha256=QCzrKJLOUBqUrmgQ7nlzrGnMMnWX9dXg3cdWZH_c33w,6239
|
|
9
|
+
zendev/gitmoji/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
zendev_commit-0.2.0.dist-info/METADATA,sha256=KPbCHZNTMQhp3FJSaTxSo6B7gSj4hwMD8DNcAQLgAtg,3182
|
|
11
|
+
zendev_commit-0.2.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
12
|
+
zendev_commit-0.2.0.dist-info/entry_points.txt,sha256=BHU_xD0A8mJquP3qenSFeZiKG4horu78PzFES9kM0c4,97
|
|
13
|
+
zendev_commit-0.2.0.dist-info/RECORD,,
|