mirror of
https://github.com/hugsy/gef.git
synced 2026-10-11 21:49:56 +00:00
## Description This PR - removes unmaintained `shellcode` commands, tests, and documentation - (finally) switches to Ruff instead of Pylint (still handled by `pre-commit`) - fixes a few ruff/ty-found issues
211 lines
6.2 KiB
Python
211 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
|
|
"""
|
|
Small script to generate the changelog for a new release. It uses information from
|
|
both git and Github to create the changelog in Markdown, which can be simply copy/pasted
|
|
to the Github release page.
|
|
|
|
The script requires a Github token to be set in the environment variable `GITHUB_REPO_TOKEN`.
|
|
As the name implies, only scope `repo` is required for the token (which can be generated
|
|
from https://github.com/settings/tokens/new).
|
|
"""
|
|
|
|
import argparse
|
|
import datetime
|
|
import os
|
|
import pathlib
|
|
import subprocess
|
|
import tempfile
|
|
|
|
import requests
|
|
|
|
__author__ = "@_hugsy_"
|
|
__version__ = 0.1
|
|
__licence__ = "MIT"
|
|
__file__ = "new-release.py"
|
|
__desc__ = "Generate a new release for a Github project."
|
|
__usage__ = f"""{__file__} v{__version__}\nby {__author__} under {__licence__}\nsyntax: {__file__} [options] args"""
|
|
|
|
|
|
REPOSITORY = "hugsy/gef"
|
|
GITHUB_TOKEN = os.getenv("GITHUB_REPO_TOKEN")
|
|
DEBUG = False
|
|
OUTPUT_FILE = pathlib.Path(tempfile.gettempdir()) / "CHANGELOG.md"
|
|
GITHUB_NEW_TOKEN_URL = "https://github.com/settings/personal-access-tokens/new"
|
|
|
|
|
|
def dbg(x: str):
|
|
if DEBUG:
|
|
print(x)
|
|
return
|
|
|
|
|
|
def shell(x: str) -> str:
|
|
dbg(f" executing: {x}")
|
|
return subprocess.check_output(x, shell=True).strip().decode("utf8")
|
|
|
|
|
|
def generate_changelog(args: argparse.Namespace) -> bool:
|
|
"""Generate the changelog for the new release."""
|
|
latest_tag = shell("git describe --tags --abbrev=0")
|
|
|
|
print(f"Creating changelog for {args.version} in {args.output_file.name}")
|
|
content = f"# Changelog: {args.version} - {args.codename}{os.linesep}{os.linesep}"
|
|
|
|
dbg("Adding commit summary...")
|
|
content += f"## Highlights of `{args.codename}`{os.linesep}{os.linesep}"
|
|
content += f"{os.linesep}{os.linesep}"
|
|
|
|
dbg("Adding contributor summary...")
|
|
content += f"## Contributors{os.linesep}{os.linesep}"
|
|
contributor_names = shell(
|
|
f"git log {latest_tag}..HEAD --pretty=format:'%aN' | sort -u"
|
|
).splitlines()
|
|
commits = {}
|
|
for author in contributor_names:
|
|
author_commits = shell(
|
|
f'git log {latest_tag}..HEAD --pretty=format:"%h" --author="{author}"'
|
|
).splitlines()
|
|
commits[author] = len(author_commits)
|
|
total_commits = sum(commits.values())
|
|
|
|
content += f"| Author | Number of commits | {os.linesep}"
|
|
content += f"|:--|--:| {os.linesep}"
|
|
commits_sorted = dict(sorted(commits.items(), key=lambda item: -item[1]))
|
|
for author in commits_sorted:
|
|
content += f"| {author} | {commits[author]}|{os.linesep}"
|
|
content += f"{os.linesep}{os.linesep}"
|
|
|
|
dbg("Adding Github info...")
|
|
url = f"https://api.github.com/repos/{args.repository}/issues?state=closed&milestone.title=Release%3a%20{args.version}"
|
|
js = requests.get(url, headers={"Authorization": f"token {args.token}"}).json()
|
|
prs = {x["number"]: x["html_url"] for x in js if "pull" in x["html_url"]}
|
|
issues = {x["number"]: x["html_url"] for x in js if "issues" in x["html_url"]}
|
|
closed_prs_item = " • ".join([f" [{nb}]({url}) " for nb, url in prs.items()])
|
|
closed_issues_item = " • ".join(
|
|
[f" [{nb}]({url}) " for nb, url in issues.items()]
|
|
)
|
|
content += f"""
|
|
## Closed Issues
|
|
|
|
* {len(issues)} issues closed ({closed_issues_item})
|
|
|
|
|
|
## Closed Pull Requests
|
|
|
|
* {len(prs)} PRs closed ({closed_prs_item})
|
|
|
|
"""
|
|
|
|
dbg("Adding commit summary...")
|
|
log = shell(
|
|
f"""git log {latest_tag}..HEAD --pretty=format:' * %cs [%h](https://github.com/{args.repository}/commit/%H) • *%aN* • %s ' --reverse"""
|
|
)
|
|
diff = shell(f"""git diff --no-color --stat {latest_tag}..HEAD""")
|
|
content += f"""
|
|
## Commit details
|
|
|
|
<details>
|
|
<summary>
|
|
{total_commits} commits since <b>{latest_tag}</b>
|
|
</summary>
|
|
|
|
### Commit log
|
|
|
|
{log}
|
|
|
|
### File diff
|
|
|
|
```diff
|
|
{diff}
|
|
```
|
|
|
|
</details>
|
|
|
|
"""
|
|
args.output_file.write_text(content)
|
|
print(f"Done, the changelog file was written to `{args.output_file.name}`")
|
|
|
|
if args.push_release:
|
|
shell(
|
|
f"""git tag --annotate "{args.version}" --message "Release {args.version} - {args.codename}" --sign"""
|
|
)
|
|
shell(f"""git push origin "{args.version}" """)
|
|
|
|
return True
|
|
|
|
|
|
def test_token(args) -> bool:
|
|
url = f"https://api.github.com/repos/{args.repository}/issues"
|
|
req = requests.get(url, headers={"Authorization": f"token {args.token}"})
|
|
if req.status_code != 200:
|
|
print(f"test_token(): return HTTP code {req.status_code}")
|
|
print(
|
|
f"Get a token from {GITHUB_NEW_TOKEN_URL} with 'Read-Only' privilege on Issues for the {args.repository} repository"
|
|
)
|
|
return req.status_code == 200
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(
|
|
usage=__usage__, description=__desc__, prog=__file__
|
|
)
|
|
parser.add_argument(
|
|
"--debug", action="store_true", default=DEBUG, help="Enable debug output"
|
|
)
|
|
parser.add_argument(
|
|
"-r",
|
|
"--repository",
|
|
type=str,
|
|
default=REPOSITORY,
|
|
help="Specify the repository (default: '%(default)s')",
|
|
)
|
|
parser.add_argument(
|
|
"-t",
|
|
"--token",
|
|
dest="token",
|
|
type=str,
|
|
metavar="TOKEN",
|
|
default=GITHUB_TOKEN,
|
|
help="Specify the Github token to use (requires `repo` scope)",
|
|
)
|
|
parser.add_argument(
|
|
"-o",
|
|
"--output-file",
|
|
type=pathlib.Path,
|
|
default=OUTPUT_FILE.absolute(),
|
|
metavar="/path/to/output_file.md",
|
|
help=f"Specify the output file (default: '{OUTPUT_FILE}')",
|
|
)
|
|
parser.add_argument(
|
|
"--version",
|
|
type=str,
|
|
default=datetime.date.today().strftime("%Y.%m"),
|
|
help="Specify the version number (default: '%(default)s')",
|
|
)
|
|
parser.add_argument(
|
|
"--codename",
|
|
type=str,
|
|
default="GENERATE_ME",
|
|
help="Specify the version codename (default: '%(default)s')",
|
|
)
|
|
parser.add_argument(
|
|
"--push-release",
|
|
action="store_true",
|
|
default=False,
|
|
help="Create the new tag and publish the release on Github",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
DEBUG = args.debug
|
|
|
|
if args.codename == "GENERATE_ME":
|
|
args.codename = shell("random-word").title()
|
|
|
|
if not test_token(args):
|
|
dbg("The GITHUB_TOKEN is not valid")
|
|
exit(1)
|
|
|
|
generate_changelog(args)
|
|
exit(0)
|