9c4f3c4303
Change-Id: Idbf382d83a9af05b17b8def7154c7390b6a7a5b0
318 lines
10 KiB
Python
318 lines
10 KiB
Python
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
"""Show the changes that will be included in the release.
|
|
"""
|
|
|
|
import argparse
|
|
import atexit
|
|
import json
|
|
import logging
|
|
import os
|
|
import os.path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
import requests
|
|
|
|
from openstack_releases import gitutils
|
|
from openstack_releases import yamlutils
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
def git_log(workdir, repo, git_range, extra_args=[]):
|
|
results = {
|
|
'range': git_range,
|
|
'logs': []
|
|
}
|
|
cmd = ['git', 'log', '--no-color']
|
|
cmd.extend(extra_args)
|
|
if isinstance(git_range, str):
|
|
cmd.append(git_range)
|
|
elif isinstance(git_range, tuple):
|
|
cmd.append('{start}..{end}'.format(
|
|
start=git_range[0], end=git_range[1]))
|
|
else:
|
|
cmd.extend(git_range)
|
|
LOG.debug('\n' + ' '.join(cmd) + '\n')
|
|
output = subprocess.check_output(cmd, cwd=os.path.join(workdir, repo))
|
|
results['logs'] = [el for el in output.decode('utf-8').split("\n") if el]
|
|
LOG.debug(results)
|
|
return results
|
|
|
|
|
|
def filter_results(content,
|
|
ignore_no_results=False,
|
|
ignore_errors=False,
|
|
ignore_not_yet_released=False,
|
|
ignore_all=False):
|
|
filtered = []
|
|
for repo in content:
|
|
removed = False
|
|
if ignore_all and \
|
|
(repo['error'] or
|
|
repo['not_yet_released'] or
|
|
not repo['commits'] or
|
|
not repo['commits'].get('logs', None)):
|
|
removed = True
|
|
LOG.debug("ignoring all")
|
|
elif ignore_errors and repo['error']:
|
|
removed = True
|
|
LOG.debug("ignoring errors")
|
|
elif ignore_not_yet_released and repo['not_yet_released']:
|
|
removed = True
|
|
LOG.debug("ignoring not yet released")
|
|
elif ignore_no_results and (not repo.get('commits', None) or
|
|
not repo.get('commits', None).get(
|
|
'logs', None)):
|
|
LOG.debug("ignoring no results")
|
|
removed = True
|
|
if not removed:
|
|
filtered.append(repo)
|
|
else:
|
|
LOG.debug('repo {} will be ignored'.format(repo['repo']))
|
|
LOG.debug(repo)
|
|
return filtered
|
|
|
|
|
|
def generate_output(content, output_format='std',
|
|
ignore_no_results=False,
|
|
ignore_errors=False,
|
|
ignore_not_yet_released=False,
|
|
ignore_all=False):
|
|
filtered = filter_results(content, ignore_no_results, ignore_errors,
|
|
ignore_not_yet_released, ignore_all)
|
|
if output_format == 'json':
|
|
return json.dumps(filtered, indent=4)
|
|
if output_format == 'yaml':
|
|
return yamlutils.dumps(filtered)
|
|
if output_format == 'std':
|
|
out = []
|
|
for repo in filtered:
|
|
out.append(
|
|
'\033[1m\033[91m[ Unreleased changes in '
|
|
'{rep} ({br}) ]\033[0m'.format(rep=repo['repo'],
|
|
br=repo['branch'])
|
|
)
|
|
|
|
if repo['not_yet_released'] or repo['error']:
|
|
out.append(repo['msg'])
|
|
continue
|
|
|
|
range_msg = 'Changes between {start} and {end}'.format(
|
|
start=repo['commits']['range'][0],
|
|
end=repo['commits']['range'][1])
|
|
out.append(range_msg)
|
|
if repo['commits'].get('logs', None):
|
|
out.append("\n".join(repo['commits']['logs']))
|
|
out.append('')
|
|
return '\n'.join(out)
|
|
|
|
|
|
def main():
|
|
if not sys.stdout.encoding:
|
|
# Wrap sys.stdout with a writer that knows how to handle
|
|
# encoding Unicode data.
|
|
import codecs
|
|
wrapped_stdout = codecs.getwriter('UTF-8')(sys.stdout)
|
|
sys.stdout = wrapped_stdout
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
'--no-cleanup',
|
|
dest='cleanup',
|
|
default=True,
|
|
action='store_false',
|
|
help='do not remove temporary files',
|
|
)
|
|
parser.add_argument(
|
|
"-v",
|
|
"--verbosity",
|
|
action="count",
|
|
help="increase output verbosity",
|
|
default=0)
|
|
parser.add_argument(
|
|
"--ignore-no-results",
|
|
action='store_true',
|
|
default=False,
|
|
help="Ignore projects without difference between the HEAD and "
|
|
"the retrieved previous tag. "
|
|
"They will be ignored in the command output.")
|
|
parser.add_argument(
|
|
"--ignore-errors",
|
|
action='store_true',
|
|
default=False,
|
|
help="Ignore projects in error (repos not found).")
|
|
parser.add_argument(
|
|
"--ignore-not-yet-released",
|
|
action='store_true',
|
|
default=False,
|
|
help="Ignore projects not yet released (previous tag not found).")
|
|
parser.add_argument(
|
|
"-I",
|
|
"--ignore-all",
|
|
action='store_true',
|
|
default=False,
|
|
help="Ignore projects without difference between the HEAD and "
|
|
"previous tag, projects not yet released, projects in error. "
|
|
"Similar to call command with "
|
|
"`--ignore-no-results --ignore-errors --ignore-not-yet-released`"
|
|
" They will be ignored in the command output.")
|
|
parser.add_argument(
|
|
"-f",
|
|
"--format",
|
|
choices=['std', 'json', 'yaml'],
|
|
default='std',
|
|
help="Output format")
|
|
parser.add_argument(
|
|
'branch',
|
|
help=('Branch to analyze'),
|
|
)
|
|
parser.add_argument(
|
|
'repos',
|
|
nargs='*',
|
|
help=('Repos to analyze, '
|
|
'repo should be e.g. openstack/glance'),
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
log_level = logging.ERROR
|
|
if args.verbosity >= 3:
|
|
log_level = logging.DEBUG
|
|
elif args.verbosity >= 2:
|
|
log_level = logging.INFO
|
|
elif args.verbosity >= 1:
|
|
log_level = logging.WARNING
|
|
|
|
# Set up logging, including making some loggers quiet.
|
|
logging.basicConfig(
|
|
format='%(levelname)7s: %(message)s',
|
|
stream=sys.stdout,
|
|
level=log_level,
|
|
)
|
|
logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
|
|
|
|
workdir = tempfile.mkdtemp(prefix='releases-')
|
|
LOG.debug('creating temporary files in %s' % workdir)
|
|
|
|
def cleanup_workdir():
|
|
if args.cleanup:
|
|
shutil.rmtree(workdir, True)
|
|
else:
|
|
LOG.info('not cleaning up %s' % workdir)
|
|
atexit.register(cleanup_workdir)
|
|
|
|
# Remove any inherited PAGER environment variable to avoid
|
|
# blocking the output waiting for input.
|
|
os.environ['PAGER'] = ''
|
|
output = []
|
|
|
|
for repo in args.repos:
|
|
repo = f'openstack/{repo}' if 'openstack/' not in repo else repo
|
|
current = {
|
|
'repo': repo,
|
|
'branch': args.branch,
|
|
'commits': None,
|
|
'error': False,
|
|
'not_yet_released': False,
|
|
'msg': ''
|
|
}
|
|
url = 'https://opendev.org/{}'.format(repo)
|
|
res = requests.get(url)
|
|
if res.status_code == 404:
|
|
current.update({'error': True})
|
|
current.update(
|
|
{'msg': "fatal: repository '{}' not found".format(url)})
|
|
output.append(current)
|
|
continue
|
|
|
|
# Start by checking out master, always. We need the repo
|
|
# checked out before we can tell if the stable branch
|
|
# really exists.
|
|
gitutils.clone_repo(
|
|
workdir,
|
|
repo,
|
|
branch='master',
|
|
)
|
|
|
|
# Set some git configuration values to allow us to perform
|
|
# local operations like tagging.
|
|
gitutils.ensure_basic_git_config(
|
|
workdir, repo,
|
|
{'user.email': 'openstack-infra@lists.openstack.org',
|
|
'user.name': 'OpenStack Proposal Bot'},
|
|
)
|
|
|
|
# Determine which branch we should actually be looking
|
|
# at. Assume any series for which there is no stable
|
|
# branch will be on 'master'.
|
|
branch = args.branch.replace('stable/', '')
|
|
if gitutils.stable_branch_exists(workdir, repo, branch):
|
|
branch = 'stable/' + branch
|
|
else:
|
|
branch = 'master'
|
|
|
|
if branch != 'master':
|
|
# Check out the repo again to the right branch if we
|
|
# didn't get it the first time.
|
|
gitutils.clone_repo(
|
|
workdir,
|
|
repo,
|
|
branch=branch,
|
|
)
|
|
|
|
# look at the previous tag for the parent of the commit
|
|
# getting the new release
|
|
previous_tag = gitutils.get_latest_tag(workdir, repo, always=False)
|
|
|
|
if not previous_tag:
|
|
current.update({'not_yet_released': True})
|
|
current.update(
|
|
{'msg': '{} has not yet been released'.format(repo)})
|
|
output.append(current)
|
|
continue
|
|
|
|
start_range = previous_tag
|
|
head_sha = gitutils.get_head(workdir, repo)
|
|
|
|
if not start_range:
|
|
current.update({'not_yet_released': True})
|
|
current.update(
|
|
{'msg': '{} has not yet been released'.format(repo)})
|
|
output.append(current)
|
|
continue
|
|
|
|
commits = git_log(workdir, repo, (start_range, head_sha),
|
|
extra_args=[
|
|
'--no-color',
|
|
'--no-merges',
|
|
'--graph',
|
|
'--format=%h %ci %s'])
|
|
|
|
current.update({'commits': commits})
|
|
output.append(current)
|
|
|
|
LOG.debug(output)
|
|
|
|
out = generate_output(output, output_format=args.format,
|
|
ignore_no_results=args.ignore_no_results,
|
|
ignore_errors=args.ignore_errors,
|
|
ignore_not_yet_released=args.ignore_not_yet_released,
|
|
ignore_all=args.ignore_all)
|
|
print("".join(out))
|
|
return 0
|