Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new generate_changelog script #5642

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .structure-config
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dockerfile/
scripts/testing/
scripts/jinja-render
scripts/makebumpver
scripts/generate_changelog
scripts/rhel_version.py
scripts/rhel_version.py.j2
dracut/.shellcheckrc
Expand Down
206 changes: 206 additions & 0 deletions scripts/generate_changelog
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#!/usr/bin/python3
#
# generate_changelog - Generate changelog from the git history, so this could
# be used to describe new releases.
#
# Copyright (C) 2009-2024 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import argparse
import subprocess
import re

VERBOSE = False


def parse_args():
"""Parse arguments"""
parser = argparse.ArgumentParser(description="""Generate changelog from a git history between
two commits.""",
epilog="""Without arguments the changelog is generated from
last two versions. Otherwise versions could be specified as
arguments.""")

parser.add_argument("-f", "--start-version", dest="start_version", default=None,
metavar="START PACKAGE VERSION",
help="From package version number (e.g. anaconda-41.15)")
parser.add_argument("-t", "--stop-version", dest="stop_version", default=None,
metavar="STOP PACKAGE VERSION",
help="Target package version number (e.g. anaconda-41.16)")
parser.add_argument("-v", dest="verbose", action="store_true", default=False,
help="Enable verbose mode")

return parser.parse_args()


def git_describe_call(from_commit=None):
"""Call git describe command.

:returns: latest tag name
"""
cmd_args = ["git", "describe", "--tags", "--abbrev=0"]
if from_commit:
cmd_args.append(from_commit)

ret = subprocess.run(cmd_args, capture_output=True, check=True)
return ret.stdout.decode().strip()


def get_start_version(commit):
"""Find start version from a git.

This version will be used as start for the changelog generation.

:param str commit: Start looking for version from the given commit.
"""
to_version = commit
# Get last version starting from commit before the last version.
return git_describe_call(f"{to_version}~")


def get_stop_version():
"""Find last version from a git.

Detect version which will be end for the changelog generation.
"""
return git_describe_call()


def get_git_history(start_version, stop_version):
"""Get log between given commits.

:returns: list of lines for each commit
"""
ret = subprocess.run(['git', 'log', '--no-merges', '--pretty=oneline',
f"{start_version}..{stop_version}~"],
capture_output=True, check=True)
return ret.stdout.decode().strip().split('\n')


def get_git_commit(commit, field):
"""Get commit detail."""
ret = subprocess.run(['git', 'log', '-1', f"--pretty=format:{field}", commit],
capture_output=True, check=True)
return ret.stdout.decode().strip('\n').split('\n')


def get_commit_detail(commit, field):
"""Get git commit detail."""
commit = get_git_commit(commit, field)

if len(commit) == 1 and commit[0].find('@') != -1:
commit = [commit[0].split('@')[0]]
elif len(commit) == 1:
commit = [commit[0]]
else:
commit = [x for x in commit if x != '']

return commit


def extract_bugs_from_commit(body):
"""Extract bugs from the commit."""
issues = set()

for bodyline in body:
# based on recommendation
# https://source.redhat.com/groups/public/release-engineering/release_engineering_rcm_wiki/using_gitbz
m = re.search(r"^(Resolves|Related|Reverts):\ +(\w+-\d+).*$", bodyline)
if not m:
continue

action = m.group(1)
bug = m.group(2)

if action and bug:
# store the bug to output list if checking is disabled and continue
issues.add(f"{action}: {bug}")

return issues


def pretify_line(summary, author, bugs):
"""Return line for the output generator."""
ret = f"- {summary.strip()} ({author})"

for b in bugs:
ret += f"\n {b}"

return ret


def generate_changelog(start_version, stop_version):
"""Generate changelog from a git history.

Changelog will be in format:
summary (author)
"""
commits = get_git_history(start_version, stop_version)

if VERBOSE:
print(f"Processing commits: \n{'\n'.join(commits)}\n")

rpm_log = []

for line in commits:
if not line:
continue
fields = line.split(' ')
commit = fields[0]

summary = get_commit_detail(commit, "%s")[0]
body = get_commit_detail(commit, "%b")
author = get_commit_detail(commit, "%aE")[0]

if re.match(r".*(#infra).*", summary) or re.match(r"infra: .*", summary):
if VERBOSE:
print(f"*** Ignoring (#infra) commit {commit}\n")
continue

if re.match(r".*(build\(deps-dev\)).*", summary):
if VERBOSE:
print(f"*** Ignoring (deps-dev) commit {commit}\n")
continue

if re.match(r".*(#test).*", summary):
if VERBOSE:
print(f"*** Ignoring (#test) commit {commit}\n")
continue

bugs = extract_bugs_from_commit(body)

rpm_log.append(pretify_line(summary, author, bugs))

return rpm_log


def main():
"""Main functions."""
global VERBOSE
args = parse_args()

VERBOSE = args.verbose

stop_version = args.stop_version or get_stop_version()
start_version = args.start_version or get_start_version(stop_version)

if VERBOSE:
print(f"Starting changelog generation from: {start_version} to: {stop_version}")

print("\n".join(generate_changelog(start_version, stop_version)))


if __name__ == "__main__":
main()