gitdch/gitdch
Benjamin Renard 95a013e74b
All checks were successful
Run tests / test-precommit (push) Successful in 56s
Code cleaning
2024-04-22 19:20:25 +02:00

328 lines
9.6 KiB
Python
Executable file

#!/usr/bin/env python3
""" Generate Debian package changelog from git """
import argparse
import logging
import os
import re
import sys
import textwrap
import git
from git.exc import GitCommandError
VERSION = "0.0"
DEFAULT_GIT_PATCH = "./"
DEFAULT_CODE_NAME = "unstable"
DEFAULT_URGENCY = "medium"
parser = argparse.ArgumentParser(description=f"{__doc__} (version: {VERSION})")
parser.add_argument("-d", "--debug", action="store_true", help="Show debug messages")
parser.add_argument("-v", "--verbose", action="store_true", help="Show verbose messages")
parser.add_argument("-w", "--warning", action="store_true", help="Show warning messages")
parser.add_argument("-l", "--log-file", help="Log file path")
parser.add_argument(
"-q",
"--quiet",
action="store_true",
help="Quiet mode: do not log on console (only if log file is provided)",
)
parser.add_argument(
"-p",
"--path",
help=f"Git repository path (default: {DEFAULT_GIT_PATCH})",
default=DEFAULT_GIT_PATCH,
)
parser.add_argument(
"-o",
"--output",
help="Generated Debian changelog output path (default: stdout)",
)
parser.add_argument(
"-A",
"--append",
action="store_true",
help=(
"Append mode: if the output changelog file already exists, append "
"generated changelog lines at the beginning of the file (optional, "
"default: overwriting the file)"
),
)
parser.add_argument("-n", "--package-name", help="Package name")
parser.add_argument(
"-V",
"--version",
help=("Current version (default: autodetected using git describe " "--always --tags)"),
)
parser.add_argument(
"--version-suffix",
help="Suffix for autodetected version",
)
parser.add_argument(
"-c",
"--code-name",
help=f"Debian code name (default: {DEFAULT_CODE_NAME})",
default=DEFAULT_CODE_NAME,
)
parser.add_argument(
"-u",
"--urgency",
help=f"Package urgency (default: {DEFAULT_URGENCY})",
default=DEFAULT_URGENCY,
)
parser.add_argument(
"-N",
"--maintainer-name",
help="Maintainer name (default: last commit author name)",
)
parser.add_argument(
"-E",
"--maintainer-email",
help="Maintainer email (default: last commit author email)",
)
parser.add_argument(
"-R",
"--release-notes",
help="Specify an optional Markdown release notes output path",
)
parser.add_argument(
"--revision",
help=(
"Specify the revision to use to generate the changelog (see "
"git-rev-parse for viable options, optional, default: generate the "
"changelog with all commits of the current branch) "
),
)
parser.add_argument(
"-C",
"--clean-tags-regex",
action="append",
type=re.compile,
help=(
"Clean tags regex: you could specify regex to clean tag names when "
'computing package versions. For instance, to drop a "-eeXXX" suffix '
'of tag names, specify -C "\\-ee[0-9]{3}$" (optional, multiple regex '
"allowed)"
),
default=[],
)
parser.add_argument(
"-x",
"--exclude",
action="append",
type=re.compile,
help=(
"Commit exclusion regex: you could specify regex to exclude some "
"commits from generated changelog entries. For instance, to exclude "
'commits with message starting with "CI: ", specify -x "^CI: " '
"(optional, multiple regex allowed)"
),
default=[],
)
options = parser.parse_args()
if not options.package_name:
parser.error("You must provide package name using -n/--package-name parameter")
# Initialize log
log = logging.getLogger()
logformat = logging.Formatter(
f"%(asctime)s - {os.path.basename(sys.argv[0])} - %(levelname)s : %(message)s"
)
# Set root logger to DEBUG (filtering done by handlers)
log.setLevel(logging.DEBUG)
log_level = None
if options.debug:
log_level = logging.DEBUG
elif options.verbose:
log_level = logging.INFO
elif options.warning:
log_level = logging.WARNING
if options.log_file:
log_file = logging.FileHandler(options.log_file)
log_file.setFormatter(logformat)
log_file.setLevel(log_level if log_level is not None else logging.INFO)
log.addHandler(log_file)
if not options.quiet or not options.log_file:
logconsole = logging.StreamHandler()
logconsole.setLevel(log_level if log_level is not None else logging.FATAL)
logconsole.setFormatter(logformat)
log.addHandler(logconsole)
repo = git.Repo(options.path)
def clean_deb_version(version_name):
"""Clean debian version name"""
version_name = re.sub("^[^0-9]*", "", version_name)
for clean_regex in options.clean_tags_regex:
version_name = clean_regex.sub("", version_name)
if options.version_suffix:
version_name += options.version_suffix
return version_name
if not options.version:
log.info("Detect current version from git tags & commits")
options.version = clean_deb_version(repo.git.describe("--always", "--tags"))
log.info("Current version detected: %s", options.version)
if options.output and options.append and not options.revision:
log.info(
"Append mode enabled but no revision specify, try to detect it from "
"last modification of the changelog file"
)
try:
last_change_commit = next(repo.iter_commits(paths=options.output))
# pylint: disable=consider-using-f-string
options.revision = f"{last_change_commit}..HEAD"
log.info(
'Last change commit of the output file is "%s": use revision "%s"',
last_change_commit,
options.revision,
)
except StopIteration:
log.warning(
"Fail to auto-detect last change commit of changelog file: it "
"seem not tracked. Continue without revision."
)
except GitCommandError:
log.warning(
"Fail to auto-detect last change commit of changelog file. May "
"be it's outside of the git repository. Continue without "
"revision."
)
# Reset repo object of to avoid BrokenPipeError
repo = git.Repo(options.path)
log.info("Generate changelog from git commits")
versions = []
tag_commits = {tag.commit.binsha: tag for tag in repo.tags}
def add_version():
"""Add version info"""
global messages # pylint: disable=global-statement
if not version_commit:
return
if not messages:
messages = [f"Release version {version}"]
log.info("Add version %s:\n - %s", version, "\n - ".join(messages))
versions.append({"name": version, "tag": tag, "commit": version_commit, "messages": messages})
tag = None
version_commit = None
version = options.version
messages = []
for commit in repo.iter_commits(rev=options.revision):
log.debug("Commit %s (%s)", commit, commit.summary)
if commit.binsha in tag_commits:
new_tag = tag_commits[commit.binsha]
log.debug("Reach new tag %s", new_tag)
add_version()
tag = new_tag
version = clean_deb_version(tag.name)
version_commit = commit
messages = []
log.debug("Iter commits for version %s", version)
if version_commit is None:
version_commit = commit
excluded = False
for regex in options.exclude:
if regex.search(commit.summary):
excluded = True
log.debug(
'Exclude commit %s ("%s", match with "%s")',
commit,
commit.summary,
regex.pattern,
)
if not excluded:
messages.append(commit.summary)
add_version()
log.info("%d versions found", len(versions))
changelog_lines = []
for version in versions:
# pylint: disable=consider-using-f-string
changelog_lines.append(
"{package} ({version}) {code_name}; urgency={urgency}\n\n".format(
package=options.package_name,
version=version["name"],
code_name=options.code_name,
urgency=options.urgency,
)
)
for message in version["messages"]:
for idx, line in enumerate(textwrap.wrap(message, 76, break_long_words=True)):
# pylint: disable=consider-using-f-string
changelog_lines.append("{}{}\n".format(" * " if not idx else " ", line))
# pylint: disable=consider-using-f-string
changelog_lines.append(
"\n -- {name} <{email}> {date}\n\n".format(
name=(
options.maintainer_name
if options.maintainer_name
else version["commit"].author.name
),
email=(
options.maintainer_email
if options.maintainer_email
else version["commit"].author.email
),
date=version["commit"].committed_datetime.strftime("%a, %d %b %Y %H:%M:%S %z"),
)
)
if options.output:
log.info("Write generated Debian changelog in file %s", options.output)
if options.append and os.path.exists(options.output):
with open(options.output, encoding="utf8") as fd:
changelog_lines += [""]
changelog_lines += fd.readlines()
with open(options.output, "w", encoding="utf8") as fd:
fd.writelines(changelog_lines)
else:
print("".join(changelog_lines))
if options.release_notes:
log.info("Generate Markdown release notes")
release_notes_lines = ["# Changelog:\n\n"]
if versions:
release_notes_lines.extend([f"* {message}\n" for message in versions[0]["messages"]])
else:
release_notes_lines.extend([f"* Release version {options.version}\n"])
log.info("Write generated Markdown release notes in file %s", options.release_notes)
with open(options.release_notes, "w", encoding="utf8") as fd:
fd.writelines(release_notes_lines)