361 lines
9 KiB
Python
Executable file
361 lines
9 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
|
|
|
|
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)
|
|
|
|
log.info('Generate changelog from git commits')
|
|
|
|
versions = []
|
|
tag_commits = dict(
|
|
(tag.commit.binsha, tag)
|
|
for tag in repo.tags
|
|
)
|
|
|
|
|
|
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
|
|
|
|
|
|
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 or
|
|
clean_deb_version(
|
|
repo.git.describe('--always', '--tags')
|
|
)
|
|
)
|
|
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']
|
|
release_notes_lines.extend([
|
|
'* {0}\n'.format(message)
|
|
for message in versions[0]['messages']
|
|
])
|
|
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)
|