gitdch/gitdch
Benjamin Renard 1f2ae28cf0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Try to auto-detect revision in append mode if not specified
2022-12-13 10:45:28 +01:00

394 lines
10 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='{0} (version: {1})'.format(__doc__, 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',
action="store",
type=str,
dest="logfile",
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',
type=str,
dest='git_path',
help='Git repository path (default: %s)' % DEFAULT_GIT_PATCH,
default=DEFAULT_GIT_PATCH
)
parser.add_argument(
'-o',
'--output',
type=str,
dest='output',
help='Generated Debian changelog output path (default: stdout)',
)
parser.add_argument(
'-A',
'--append',
action='store_true',
dest='append',
help=(
'Append mode: if the output changelog file already exists, append '
'generated changelog lines at the begining of the file (optional, '
'default: overwriting the file)'
)
)
parser.add_argument(
'-n',
'--package-name',
type=str,
dest='package_name',
help='Package name'
)
parser.add_argument(
'-V',
'--version',
type=str,
dest='version',
help=(
'Currrent version (default: autodetected using git describe '
'--always --tags)')
)
parser.add_argument(
'--version-suffix',
type=str,
dest='version_suffix',
help='Suffix for autodetected version'
)
parser.add_argument(
'-c',
'--code-name',
type=str,
dest='code_name',
help='Debian code name (default: %s)' % DEFAULT_CODE_NAME,
default=DEFAULT_CODE_NAME
)
parser.add_argument(
'-u',
'--urgency',
type=str,
dest='urgency',
help='Package urgency (default: %s)' % DEFAULT_URGENCY,
default=DEFAULT_URGENCY
)
parser.add_argument(
'-N',
'--maintainer-name',
type=str,
dest='maintainer_name',
help='Maintainer name (default: last commit author name)'
)
parser.add_argument(
'-E',
'--maintainer-email',
type=str,
dest='maintainer_email',
help='Maintainer email (default: last commit author email)'
)
parser.add_argument(
'-R',
'--release-notes',
type=str,
dest='release_notes',
help='Specify an optional Markdown release notes output path'
)
parser.add_argument(
'--revision',
type=str,
dest='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) '
),
default=None
)
parser.add_argument(
'-C', '--clean-tags-regex',
action='append',
type=re.compile,
dest='clean_tags_regex',
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,
dest='exclude',
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(
'%(asctime)s - {} - %(levelname)s : %(message)s'.format(
os.path.basename(sys.argv[0])
)
)
# 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.logfile:
logfile = logging.FileHandler(options.logfile)
logfile.setFormatter(logformat)
logfile.setLevel(log_level if log_level is not None else logging.INFO)
log.addHandler(logfile)
if not options.quiet or not options.logfile:
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.git_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('Currrent 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 = '{0}..HEAD'.format(last_change_commit)
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.git_path)
log.info('Generate changelog from git commits')
versions = []
tag_commits = dict(
(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 = ['Release version {0}'.format(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(
'{0}{1}\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, 'r', 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([
'* {0}\n'.format(message)
for message in versions[0]['messages']
])
else:
release_notes_lines.extend([
'* Release version {0}\n'.format(options.version)
])
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)