gha-utils 4.1.3__tar.gz → 4.2.0__tar.gz
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.
Potentially problematic release.
This version of gha-utils might be problematic. Click here for more details.
- {gha_utils-4.1.3 → gha_utils-4.2.0}/PKG-INFO +2 -1
- {gha_utils-4.1.3 → gha_utils-4.2.0}/gha_utils/__init__.py +1 -1
- {gha_utils-4.1.3 → gha_utils-4.2.0}/gha_utils/changelog.py +1 -1
- {gha_utils-4.1.3 → gha_utils-4.2.0}/gha_utils/cli.py +94 -31
- gha_utils-4.2.0/gha_utils/mailmap.py +166 -0
- {gha_utils-4.1.3 → gha_utils-4.2.0}/gha_utils/metadata.py +10 -10
- {gha_utils-4.1.3 → gha_utils-4.2.0}/gha_utils.egg-info/PKG-INFO +2 -1
- {gha_utils-4.1.3 → gha_utils-4.2.0}/gha_utils.egg-info/requires.txt +1 -0
- {gha_utils-4.1.3 → gha_utils-4.2.0}/pyproject.toml +11 -2
- gha_utils-4.1.3/gha_utils/mailmap.py +0 -100
- {gha_utils-4.1.3 → gha_utils-4.2.0}/gha_utils/__main__.py +0 -0
- {gha_utils-4.1.3 → gha_utils-4.2.0}/gha_utils/py.typed +0 -0
- {gha_utils-4.1.3 → gha_utils-4.2.0}/gha_utils.egg-info/SOURCES.txt +0 -0
- {gha_utils-4.1.3 → gha_utils-4.2.0}/gha_utils.egg-info/dependency_links.txt +0 -0
- {gha_utils-4.1.3 → gha_utils-4.2.0}/gha_utils.egg-info/entry_points.txt +0 -0
- {gha_utils-4.1.3 → gha_utils-4.2.0}/gha_utils.egg-info/top_level.txt +0 -0
- {gha_utils-4.1.3 → gha_utils-4.2.0}/readme.md +0 -0
- {gha_utils-4.1.3 → gha_utils-4.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: gha-utils
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.0
|
|
4
4
|
Summary: ⚙️ CLI helpers for GitHub Actions + reuseable workflows
|
|
5
5
|
Author-email: Kevin Deldycke <kevin@deldycke.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/kdeldycke/workflows
|
|
@@ -47,6 +47,7 @@ Classifier: Typing :: Typed
|
|
|
47
47
|
Requires-Python: >=3.8.6
|
|
48
48
|
Description-Content-Type: text/markdown
|
|
49
49
|
Requires-Dist: backports.strenum~=1.3.1; python_version < "3.11"
|
|
50
|
+
Requires-Dist: boltons~=24.0.0
|
|
50
51
|
Requires-Dist: bump-my-version~=0.24.0
|
|
51
52
|
Requires-Dist: click-extra~=4.8.3
|
|
52
53
|
Requires-Dist: packaging~=24.1
|
|
@@ -56,13 +56,41 @@ def file_writer(filepath):
|
|
|
56
56
|
writer.close()
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
def
|
|
60
|
-
"""
|
|
61
|
-
|
|
59
|
+
def generate_header(ctx: Context) -> str:
|
|
60
|
+
"""Generate metadata to be left as comments to the top of a file generated by
|
|
61
|
+
this CLI.
|
|
62
|
+
"""
|
|
63
|
+
header = (
|
|
62
64
|
f"# Generated by {ctx.command_path} v{__version__}"
|
|
63
65
|
" - https://github.com/kdeldycke/workflows\n"
|
|
64
|
-
f"# Timestamp: {datetime.now().isoformat()}
|
|
66
|
+
f"# Timestamp: {datetime.now().isoformat()}\n"
|
|
65
67
|
)
|
|
68
|
+
logging.debug(f"Generated header:\n{header}")
|
|
69
|
+
return header
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def remove_header(content: str) -> str:
|
|
73
|
+
"""Return the same content provided, but without the blank lines and header metadata generated by the function above."""
|
|
74
|
+
logging.debug(f"Removing header from:\n{content}")
|
|
75
|
+
lines = []
|
|
76
|
+
still_in_header = True
|
|
77
|
+
for line in content.splitlines():
|
|
78
|
+
if still_in_header:
|
|
79
|
+
# We are still in the header as long as we have blank lines or we have
|
|
80
|
+
# comment lines matching the format produced by the method above.
|
|
81
|
+
if not line.strip() or line.startswith((
|
|
82
|
+
"# Generated by ",
|
|
83
|
+
"# Timestamp: ",
|
|
84
|
+
)):
|
|
85
|
+
continue
|
|
86
|
+
else:
|
|
87
|
+
still_in_header = False
|
|
88
|
+
# We are past the header, so keep all the lines: we have nothing left to remove.
|
|
89
|
+
lines.append(line)
|
|
90
|
+
|
|
91
|
+
headerless_content = "\n".join(lines)
|
|
92
|
+
logging.debug(f"Result of header removal:\n{headerless_content}")
|
|
93
|
+
return headerless_content
|
|
66
94
|
|
|
67
95
|
|
|
68
96
|
@extra_group
|
|
@@ -95,10 +123,12 @@ def metadata(ctx, format, overwrite, output_path):
|
|
|
95
123
|
"""Dump project metadata to a file.
|
|
96
124
|
|
|
97
125
|
By default the metadata produced are displayed directly to the console output.
|
|
98
|
-
So `gha-utils metadata` is the same as a call to `gha-utils metadata -`. To have
|
|
99
|
-
in a file on disk, specify the output file like so:
|
|
126
|
+
So `gha-utils metadata` is the same as a call to `gha-utils metadata -`. To have
|
|
127
|
+
the results written in a file on disk, specify the output file like so:
|
|
128
|
+
`gha-utils metadata dump.txt`.
|
|
100
129
|
|
|
101
|
-
For GitHub you want to output to the standard environment file pointed to by the
|
|
130
|
+
For GitHub you want to output to the standard environment file pointed to by the
|
|
131
|
+
`$GITHUB_OUTPUT` variable. I.e.:
|
|
102
132
|
|
|
103
133
|
$ gha-utils metadata --format github "$GITHUB_OUTPUT"
|
|
104
134
|
"""
|
|
@@ -170,43 +200,76 @@ def changelog(ctx, source, changelog_path):
|
|
|
170
200
|
f.write(content)
|
|
171
201
|
|
|
172
202
|
|
|
173
|
-
@gha_utils.command(short_help="
|
|
203
|
+
@gha_utils.command(short_help="Update Git's .mailmap file with missing contributors")
|
|
174
204
|
@option(
|
|
175
205
|
"--source",
|
|
176
|
-
type=
|
|
206
|
+
type=file_path(readable=True, resolve_path=True),
|
|
177
207
|
default=".mailmap",
|
|
178
|
-
help="
|
|
208
|
+
help="Mailmap source file to use as reference for contributors identities that "
|
|
209
|
+
"are already grouped.",
|
|
210
|
+
)
|
|
211
|
+
@option(
|
|
212
|
+
"--create-if-missing/--skip-if-missing",
|
|
213
|
+
is_flag=True,
|
|
214
|
+
default=True,
|
|
215
|
+
help="Create the destination mailmap file if it is not found. Or skip the update "
|
|
216
|
+
"process entirely if if does not already exists in the first place. This option "
|
|
217
|
+
f"is ignore if the destination is to print the result to {sys.stdout.name}.",
|
|
179
218
|
)
|
|
180
219
|
@argument(
|
|
181
|
-
"
|
|
220
|
+
"destination_mailmap",
|
|
182
221
|
type=file_path(writable=True, resolve_path=True, allow_dash=True),
|
|
183
222
|
default="-",
|
|
184
223
|
)
|
|
185
224
|
@pass_context
|
|
186
|
-
def
|
|
187
|
-
"""Update a ``.mailmap`` file with missing contributors found in Git commit
|
|
225
|
+
def mailmap_sync(ctx, source, create_if_missing, destination_mailmap):
|
|
226
|
+
"""Update a ``.mailmap`` file with all missing contributors found in Git commit
|
|
227
|
+
history.
|
|
188
228
|
|
|
189
|
-
By default the
|
|
190
|
-
|
|
229
|
+
By default the ``.mailmap`` at the root of the repository is read and its content
|
|
230
|
+
is reused as reference, so identities already aliased in there are preserved and
|
|
231
|
+
used as initial mapping. Only missing contributors not found in this initial mapping
|
|
232
|
+
are added.
|
|
191
233
|
|
|
192
|
-
|
|
193
|
-
|
|
234
|
+
The resulting updated mapping is printed to the console output. So a bare call to
|
|
235
|
+
`gha-utils mailmap-sync` is the same as a call to
|
|
236
|
+
`gha-utils mailmap-sync --source .mailmap -`.
|
|
194
237
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"""
|
|
198
|
-
initial_content = None
|
|
199
|
-
if source:
|
|
200
|
-
logging.info(f"Read initial mapping from {source}")
|
|
201
|
-
initial_content = source.read_text(encoding="utf-8")
|
|
238
|
+
To have the updated mapping written to a file, specify the output file like so:
|
|
239
|
+
`gha-utils mailmap-sync .mailmap`.
|
|
202
240
|
|
|
203
|
-
|
|
204
|
-
|
|
241
|
+
The updated results are sorted. But no attempts are made at regrouping new
|
|
242
|
+
contributors. SO you have to edit entries by hand to regroup them
|
|
243
|
+
"""
|
|
244
|
+
mailmap = Mailmap()
|
|
205
245
|
|
|
206
|
-
if
|
|
207
|
-
logging.info(f"
|
|
246
|
+
if source.exists():
|
|
247
|
+
logging.info(f"Read initial mapping from {source}")
|
|
248
|
+
content = remove_header(source.read_text(encoding="utf-8"))
|
|
249
|
+
mailmap.parse(content)
|
|
250
|
+
else:
|
|
251
|
+
logging.debug(f"Mailmap source file {source} does not exists.")
|
|
252
|
+
|
|
253
|
+
mailmap.update_from_git()
|
|
254
|
+
new_content = mailmap.render()
|
|
255
|
+
|
|
256
|
+
if is_stdout(destination_mailmap):
|
|
257
|
+
logging.info(f"Print updated results to {sys.stdout.name}.")
|
|
258
|
+
logging.debug(
|
|
259
|
+
"Ignore the "
|
|
260
|
+
+ ("--create-if-missing" if create_if_missing else "--skip-if-missing")
|
|
261
|
+
+ " option."
|
|
262
|
+
)
|
|
208
263
|
else:
|
|
209
|
-
logging.info(f"Save updated results to {
|
|
264
|
+
logging.info(f"Save updated results to {destination_mailmap}")
|
|
265
|
+
if not create_if_missing and not destination_mailmap.exists():
|
|
266
|
+
logging.warning(
|
|
267
|
+
f"{destination_mailmap} does not exists, stop the sync process."
|
|
268
|
+
)
|
|
269
|
+
ctx.exit()
|
|
270
|
+
if content == new_content:
|
|
271
|
+
logging.warning("Nothing to update, stop the sync process.")
|
|
272
|
+
ctx.exit()
|
|
210
273
|
|
|
211
|
-
with file_writer(
|
|
212
|
-
f.write(
|
|
274
|
+
with file_writer(destination_mailmap) as f:
|
|
275
|
+
f.write(generate_header(ctx) + new_content)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
|
|
2
|
+
#
|
|
3
|
+
# This program is Free Software; you can redistribute it and/or
|
|
4
|
+
# modify it under the terms of the GNU General Public License
|
|
5
|
+
# as published by the Free Software Foundation; either version 2
|
|
6
|
+
# of the License, or (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program; if not, write to the Free Software
|
|
15
|
+
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import sys
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from functools import cached_property
|
|
23
|
+
from subprocess import run
|
|
24
|
+
|
|
25
|
+
from boltons.iterutils import unique # type: ignore[import-untyped]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(order=True, frozen=True)
|
|
29
|
+
class Record:
|
|
30
|
+
"""A mailmap identity mapping entry."""
|
|
31
|
+
|
|
32
|
+
# Mapping is define as the first field so we have natural sorting,
|
|
33
|
+
# whatever the value of the pre_comment is.
|
|
34
|
+
canonical: str = ""
|
|
35
|
+
aliases: set[str] = field(default_factory=set)
|
|
36
|
+
pre_comment: str = ""
|
|
37
|
+
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
"""Render the record with pre-comments first, followed by the identity mapping.
|
|
40
|
+
|
|
41
|
+
Sort all entries in the mapping without case-sensitivity, but keep the first in
|
|
42
|
+
its place as the canonical identity.
|
|
43
|
+
"""
|
|
44
|
+
lines = []
|
|
45
|
+
if self.pre_comment:
|
|
46
|
+
lines.append(self.pre_comment)
|
|
47
|
+
if self.canonical:
|
|
48
|
+
lines.append(
|
|
49
|
+
" ".join((self.canonical, *sorted(self.aliases, key=str.casefold)))
|
|
50
|
+
)
|
|
51
|
+
return "\n".join(lines)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Mailmap:
|
|
55
|
+
"""Helpers to manipulate ``.mailmap`` files.
|
|
56
|
+
|
|
57
|
+
``.mailmap`` `file format is documented on Git website
|
|
58
|
+
<https://git-scm.com/docs/gitmailmap>`_.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
records: list[Record] = list()
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def split_identities(mapping: str) -> tuple[str, set[str]]:
|
|
65
|
+
"""Split a mapping of identities and normalize them."""
|
|
66
|
+
identities = []
|
|
67
|
+
for identity in map(str.strip, mapping.split(">")):
|
|
68
|
+
# Skip blank strings produced by uneven spaces.
|
|
69
|
+
if not identity:
|
|
70
|
+
continue
|
|
71
|
+
assert identity.count("<") == 1, f"Unexpected email format in {identity!r}"
|
|
72
|
+
name, email = identity.split("<", maxsplit=1)
|
|
73
|
+
identities.append(f"{name.strip()} <{email}>")
|
|
74
|
+
|
|
75
|
+
assert len(identities), f"No identities found in {mapping!r}"
|
|
76
|
+
|
|
77
|
+
identities = list(unique(identities))
|
|
78
|
+
return identities[0], set(identities[1:])
|
|
79
|
+
|
|
80
|
+
def parse(self, content: str) -> None:
|
|
81
|
+
"""Parse mailmap content and add it to the current list of records.
|
|
82
|
+
|
|
83
|
+
Each non-empty, non-comment line is considered a mapping entry.
|
|
84
|
+
|
|
85
|
+
The preceding lines of a mapping entry are kept attached to it as pre-comments,
|
|
86
|
+
so the layout will be preserved on rendering, during which records are sorted.
|
|
87
|
+
"""
|
|
88
|
+
logging.debug(f"Parsing:\n{content}")
|
|
89
|
+
pre_lines = []
|
|
90
|
+
for line in map(str.strip, content.splitlines()):
|
|
91
|
+
# Comment lines are added as-is.
|
|
92
|
+
if line.startswith("#"):
|
|
93
|
+
pre_lines.append(line)
|
|
94
|
+
# Blank lines are added as-is.
|
|
95
|
+
elif not line:
|
|
96
|
+
pre_lines.append(line)
|
|
97
|
+
# Mapping entry, which mark the end of a block, so add it to the list
|
|
98
|
+
# mailmap records.
|
|
99
|
+
else:
|
|
100
|
+
canonical, aliases = self.split_identities(line)
|
|
101
|
+
record = Record(
|
|
102
|
+
pre_comment="\n".join(pre_lines),
|
|
103
|
+
canonical=canonical,
|
|
104
|
+
aliases=aliases,
|
|
105
|
+
)
|
|
106
|
+
logging.debug(record)
|
|
107
|
+
pre_lines = []
|
|
108
|
+
self.records.append(record)
|
|
109
|
+
|
|
110
|
+
def find(self, identity: str) -> bool:
|
|
111
|
+
"""Returns ``True`` if the provided identity matched any record."""
|
|
112
|
+
identity_token = identity.lower()
|
|
113
|
+
for record in self.records:
|
|
114
|
+
# Identity matching is case insensitive:
|
|
115
|
+
# https://git-scm.com/docs/gitmailmap#_syntax
|
|
116
|
+
if identity_token in map(str.lower, (record.canonical, *record.aliases)):
|
|
117
|
+
return True
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
@cached_property
|
|
121
|
+
def git_contributors(self) -> set[str]:
|
|
122
|
+
"""Returns the set of all constributors found in the Git commit history.
|
|
123
|
+
|
|
124
|
+
No normalization happens: all variations of authors and committers strings
|
|
125
|
+
attached to all commits are considered.
|
|
126
|
+
|
|
127
|
+
For format output syntax, see:
|
|
128
|
+
https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt-emaNem
|
|
129
|
+
"""
|
|
130
|
+
contributors = set()
|
|
131
|
+
|
|
132
|
+
git_cli = ("git", "log", "--pretty=format:%aN <%aE>%n%cN <%cE>")
|
|
133
|
+
logging.debug(f"Run: {' '.join(git_cli)}")
|
|
134
|
+
process = run(git_cli, capture_output=True, encoding="utf-8")
|
|
135
|
+
|
|
136
|
+
# Parse git CLI output.
|
|
137
|
+
if process.returncode:
|
|
138
|
+
sys.exit(process.stderr)
|
|
139
|
+
for line in map(str.strip, process.stdout.splitlines()):
|
|
140
|
+
if line:
|
|
141
|
+
contributors.add(line)
|
|
142
|
+
|
|
143
|
+
logging.debug(
|
|
144
|
+
"Authors and committers found in Git history:\n"
|
|
145
|
+
+ "\n".join(sorted(contributors, key=str.casefold))
|
|
146
|
+
)
|
|
147
|
+
return contributors
|
|
148
|
+
|
|
149
|
+
def update_from_git(self) -> None:
|
|
150
|
+
"""Add to internal records all missing contributors found in commit history.
|
|
151
|
+
|
|
152
|
+
This method will refrain from adding contributors already registered as aliases.
|
|
153
|
+
"""
|
|
154
|
+
for contributor in self.git_contributors:
|
|
155
|
+
if not self.find(contributor):
|
|
156
|
+
record = Record(canonical=contributor)
|
|
157
|
+
logging.info(f"Add new identity {record}")
|
|
158
|
+
self.records.append(record)
|
|
159
|
+
else:
|
|
160
|
+
logging.debug(f"Ignore existing identity {contributor}")
|
|
161
|
+
|
|
162
|
+
def render(self) -> str:
|
|
163
|
+
"""Render internal records in Mailmap format."""
|
|
164
|
+
return "\n".join(
|
|
165
|
+
map(str, sorted(self.records, key=lambda r: r.canonical.casefold()))
|
|
166
|
+
)
|
|
@@ -120,7 +120,7 @@ from bumpversion.config.files import find_config_file # type: ignore[import-unt
|
|
|
120
120
|
from bumpversion.show import resolve_name # type: ignore[import-untyped]
|
|
121
121
|
from packaging.specifiers import SpecifierSet
|
|
122
122
|
from packaging.version import Version
|
|
123
|
-
from pydriller import Commit, Git, Repository # type: ignore[import]
|
|
123
|
+
from pydriller import Commit, Git, Repository # type: ignore[import-untyped]
|
|
124
124
|
from pyproject_metadata import ConfigurationError, StandardMetadata
|
|
125
125
|
from wcmatch.glob import (
|
|
126
126
|
BRACE,
|
|
@@ -283,7 +283,7 @@ class Metadata:
|
|
|
283
283
|
context = json.loads(os.environ["GITHUB_CONTEXT"])
|
|
284
284
|
logging.debug("--- GitHub context ---")
|
|
285
285
|
logging.debug(json.dumps(context, indent=4))
|
|
286
|
-
return context
|
|
286
|
+
return context # type:ignore[no-any-return]
|
|
287
287
|
|
|
288
288
|
def git_stash_count(self, git_repo: Git) -> int:
|
|
289
289
|
"""Returns the number of stashes."""
|
|
@@ -441,8 +441,8 @@ class Metadata:
|
|
|
441
441
|
return None
|
|
442
442
|
|
|
443
443
|
if bool(os.environ.get("GITHUB_BASE_REF")):
|
|
444
|
-
return WorkflowEvent.pull_request
|
|
445
|
-
return WorkflowEvent.push
|
|
444
|
+
return WorkflowEvent.pull_request # type: ignore[no-any-return]
|
|
445
|
+
return WorkflowEvent.push # type: ignore[no-any-return]
|
|
446
446
|
|
|
447
447
|
@cached_property
|
|
448
448
|
def commit_range(self) -> tuple[str, str] | None:
|
|
@@ -475,7 +475,7 @@ class Metadata:
|
|
|
475
475
|
if not self.github_context or not self.event_type:
|
|
476
476
|
return None
|
|
477
477
|
# Pull request event.
|
|
478
|
-
if self.event_type in (
|
|
478
|
+
if self.event_type in ( # type: ignore[unreachable]
|
|
479
479
|
WorkflowEvent.pull_request,
|
|
480
480
|
WorkflowEvent.pull_request_target,
|
|
481
481
|
):
|
|
@@ -601,7 +601,7 @@ class Metadata:
|
|
|
601
601
|
|
|
602
602
|
@property
|
|
603
603
|
def is_python_project(self):
|
|
604
|
-
"""Returns ``
|
|
604
|
+
"""Returns ``True`` if repository is a Python project.
|
|
605
605
|
|
|
606
606
|
Presence of a ``pyproject.toml`` file is not enough, as 3rd party tools can use
|
|
607
607
|
that file to store their own configuration.
|
|
@@ -792,7 +792,7 @@ class Metadata:
|
|
|
792
792
|
if self.new_commits_matrix:
|
|
793
793
|
details = self.new_commits_matrix.get("include")
|
|
794
794
|
if details:
|
|
795
|
-
version = details[0].get("current_version")
|
|
795
|
+
version = details[0].get("current_version")
|
|
796
796
|
return version
|
|
797
797
|
|
|
798
798
|
@cached_property
|
|
@@ -805,18 +805,18 @@ class Metadata:
|
|
|
805
805
|
# This script is only designed for at most 1 release in the list of new
|
|
806
806
|
# commits.
|
|
807
807
|
assert len(details) == 1
|
|
808
|
-
version = details[0].get("current_version")
|
|
808
|
+
version = details[0].get("current_version")
|
|
809
809
|
return version
|
|
810
810
|
|
|
811
811
|
@cached_property
|
|
812
812
|
def is_sphinx(self) -> bool:
|
|
813
|
-
"""Returns
|
|
813
|
+
"""Returns ``True`` if the Sphinx config file is present."""
|
|
814
814
|
# The Sphinx config file is present, that's enough for us.
|
|
815
815
|
return self.sphinx_conf_path.exists() and self.sphinx_conf_path.is_file()
|
|
816
816
|
|
|
817
817
|
@cached_property
|
|
818
818
|
def active_autodoc(self) -> bool:
|
|
819
|
-
"""Returns
|
|
819
|
+
"""Returns ``True`` if there are active Sphinx extensions."""
|
|
820
820
|
if self.is_sphinx:
|
|
821
821
|
# Look for list of active Sphinx extensions.
|
|
822
822
|
for node in ast.parse(self.sphinx_conf_path.read_bytes()).body:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: gha-utils
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.0
|
|
4
4
|
Summary: ⚙️ CLI helpers for GitHub Actions + reuseable workflows
|
|
5
5
|
Author-email: Kevin Deldycke <kevin@deldycke.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/kdeldycke/workflows
|
|
@@ -47,6 +47,7 @@ Classifier: Typing :: Typed
|
|
|
47
47
|
Requires-Python: >=3.8.6
|
|
48
48
|
Description-Content-Type: text/markdown
|
|
49
49
|
Requires-Dist: backports.strenum~=1.3.1; python_version < "3.11"
|
|
50
|
+
Requires-Dist: boltons~=24.0.0
|
|
50
51
|
Requires-Dist: bump-my-version~=0.24.0
|
|
51
52
|
Requires-Dist: click-extra~=4.8.3
|
|
52
53
|
Requires-Dist: packaging~=24.1
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
# Docs: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
|
|
3
3
|
name = "gha-utils"
|
|
4
|
-
version = "4.
|
|
4
|
+
version = "4.2.0"
|
|
5
5
|
# Python versions and their status: https://devguide.python.org/versions/
|
|
6
6
|
# XXX 3.8 is not enough and we need to bump requirements to 3.8.6 because of:
|
|
7
7
|
# https://github.com/clbarnes/backports.strenum/issues/1
|
|
@@ -72,6 +72,7 @@ classifiers = [
|
|
|
72
72
|
]
|
|
73
73
|
dependencies = [
|
|
74
74
|
"backports.strenum ~= 1.3.1 ; python_version < '3.11'",
|
|
75
|
+
"boltons ~= 24.0.0",
|
|
75
76
|
"bump-my-version ~= 0.24.0",
|
|
76
77
|
"click-extra ~= 4.8.3",
|
|
77
78
|
"packaging ~= 24.1",
|
|
@@ -94,8 +95,16 @@ gha-utils = "gha_utils.__main__:main"
|
|
|
94
95
|
[tool.setuptools.packages.find]
|
|
95
96
|
include = ["gha_utils"]
|
|
96
97
|
|
|
98
|
+
[tool.mypy]
|
|
99
|
+
warn_unused_configs = true
|
|
100
|
+
warn_redundant_casts = true
|
|
101
|
+
warn_unused_ignores = true
|
|
102
|
+
warn_return_any = true
|
|
103
|
+
warn_unreachable = true
|
|
104
|
+
pretty = true
|
|
105
|
+
|
|
97
106
|
[tool.bumpversion]
|
|
98
|
-
current_version = "4.
|
|
107
|
+
current_version = "4.2.0"
|
|
99
108
|
allow_dirty = true
|
|
100
109
|
ignore_missing_files = true
|
|
101
110
|
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
|
|
2
|
-
#
|
|
3
|
-
# This program is Free Software; you can redistribute it and/or
|
|
4
|
-
# modify it under the terms of the GNU General Public License
|
|
5
|
-
# as published by the Free Software Foundation; either version 2
|
|
6
|
-
# of the License, or (at your option) any later version.
|
|
7
|
-
#
|
|
8
|
-
# This program is distributed in the hope that it will be useful,
|
|
9
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
-
# GNU General Public License for more details.
|
|
12
|
-
#
|
|
13
|
-
# You should have received a copy of the GNU General Public License
|
|
14
|
-
# along with this program; if not, write to the Free Software
|
|
15
|
-
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
import logging
|
|
20
|
-
import sys
|
|
21
|
-
from functools import cached_property
|
|
22
|
-
from subprocess import run
|
|
23
|
-
from textwrap import dedent
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class Mailmap:
|
|
27
|
-
"""Helpers to manipulate ``.mailmap`` file.
|
|
28
|
-
|
|
29
|
-
The ``.mailmap`` files expected to be found in the root of repository.
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
def __init__(self, initial_mailmap: str | None = None) -> None:
|
|
33
|
-
if not initial_mailmap:
|
|
34
|
-
# Initialize empty .mailmap with pointers to reference documentation.
|
|
35
|
-
self.content = dedent(
|
|
36
|
-
"""\
|
|
37
|
-
# Format is:
|
|
38
|
-
# Preferred Name <preferred e-mail> Other Name <other e-mail>
|
|
39
|
-
#
|
|
40
|
-
# Reference: https://git-scm.com/docs/git-blame#_mapping_authors
|
|
41
|
-
""",
|
|
42
|
-
)
|
|
43
|
-
else:
|
|
44
|
-
self.content = initial_mailmap
|
|
45
|
-
logging.debug(f"Initial content set to:\n{self.content}")
|
|
46
|
-
|
|
47
|
-
@cached_property
|
|
48
|
-
def git_contributors(self) -> set[str]:
|
|
49
|
-
"""Returns the set of all constributors found in the Git commit history.
|
|
50
|
-
|
|
51
|
-
No normalization happens: all variations of authors and committers strings attached to all commits are considered.
|
|
52
|
-
|
|
53
|
-
For format output syntax, see:
|
|
54
|
-
https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt-emaNem
|
|
55
|
-
"""
|
|
56
|
-
contributors = set()
|
|
57
|
-
|
|
58
|
-
git_cli = ("git", "log", "--pretty=format:%aN <%aE>%n%cN <%cE>")
|
|
59
|
-
logging.debug(f"Run: {' '.join(git_cli)}")
|
|
60
|
-
process = run(git_cli, capture_output=True, encoding="utf-8")
|
|
61
|
-
|
|
62
|
-
# Parse git CLI output.
|
|
63
|
-
if process.returncode:
|
|
64
|
-
sys.exit(process.stderr)
|
|
65
|
-
for line in process.stdout.splitlines():
|
|
66
|
-
if line.strip():
|
|
67
|
-
contributors.add(line)
|
|
68
|
-
|
|
69
|
-
logging.debug(
|
|
70
|
-
"Authors and committers found in Git history:\n"
|
|
71
|
-
+ "\n".join(sorted(contributors, key=str.casefold))
|
|
72
|
-
)
|
|
73
|
-
return contributors
|
|
74
|
-
|
|
75
|
-
def updated_map(self):
|
|
76
|
-
"""Add all missing contributors from commit history to mailmap.
|
|
77
|
-
|
|
78
|
-
This method will refrain from adding contributors already registered as aliases.
|
|
79
|
-
"""
|
|
80
|
-
# Extract comments in .mailmap header and keep mapping lines.
|
|
81
|
-
header_comments = []
|
|
82
|
-
mappings = set()
|
|
83
|
-
for line in self.content.splitlines():
|
|
84
|
-
if line.startswith("#"):
|
|
85
|
-
header_comments.append(line)
|
|
86
|
-
elif line.strip():
|
|
87
|
-
mappings.add(line)
|
|
88
|
-
|
|
89
|
-
# Add all missing contributors to the mail mapping.
|
|
90
|
-
for contributor in self.git_contributors:
|
|
91
|
-
if contributor not in self.content:
|
|
92
|
-
logging.debug(f"{contributor!r} not found in original content, add it.")
|
|
93
|
-
mappings.add(contributor)
|
|
94
|
-
|
|
95
|
-
# Render content in .mailmap format.
|
|
96
|
-
return (
|
|
97
|
-
"\n".join(header_comments)
|
|
98
|
-
+ "\n\n"
|
|
99
|
-
+ "\n".join(sorted(mappings, key=str.casefold))
|
|
100
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|