123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- #!/usr/bin/env python
- """Generate a new changelog entry.
- Usage
- =====
- To generate a new changelog entry::
- scripts/new-change
- This will open up a file in your editor (via the ``EDITOR`` env var).
- You'll see this template::
- # Type should be one of: feature, bugfix
- type:
- # Category is the high level feature area.
- # This can be a service identifier (e.g ``s3``),
- # or something like: Paginator.
- category:
- # A brief description of the change. You can
- # use github style references to issues such as
- # "fixes #489", "boto/boto3#100", etc. These
- # will get automatically replaced with the correct
- # link.
- description:
- Fill in the appropriate values, save and exit the editor.
- Make sure to commit these changes as part of your pull request.
- If, when your editor is open, you decide don't don't want to add a changelog
- entry, save an empty file and no entry will be generated.
- You can then use the ``scripts/gen-changelog`` to generate the
- CHANGELOG.rst file.
- """
- import argparse
- import json
- import os
- import random
- import re
- import string
- import subprocess
- import sys
- import tempfile
- VALID_CHARS = set(string.ascii_letters + string.digits)
- CHANGES_DIR = os.path.join(
- os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '.changes'
- )
- TEMPLATE = """\
- # Type should be one of: feature, bugfix, enhancement, api-change
- # feature: A larger feature or change in behavior, usually resulting in a
- # minor version bump.
- # bugfix: Fixing a bug in an existing code path.
- # enhancement: Small change to an underlying implementation detail.
- # api-change: Changes to a modeled API.
- type: {change_type}
- # Category is the high level feature area.
- # This can be a service identifier (e.g ``s3``),
- # or something like: Paginator.
- category: {category}
- # A brief description of the change. You can
- # use github style references to issues such as
- # "fixes #489", "boto/boto3#100", etc. These
- # will get automatically replaced with the correct
- # link.
- description: {description}
- """
- def new_changelog_entry(args):
- # Changelog values come from one of two places.
- # Either all values are provided on the command line,
- # or we open a text editor and let the user provide
- # enter their values.
- if all_values_provided(args):
- parsed_values = {
- 'type': args.change_type,
- 'category': args.category,
- 'description': args.description,
- }
- else:
- parsed_values = get_values_from_editor(args)
- if has_empty_values(parsed_values):
- sys.stderr.write(
- "Empty changelog values received, skipping entry creation.\n"
- )
- return 1
- replace_issue_references(parsed_values, args.repo)
- write_new_change(parsed_values)
- return 0
- def has_empty_values(parsed_values):
- return not (
- parsed_values.get('type')
- and parsed_values.get('category')
- and parsed_values.get('description')
- )
- def all_values_provided(args):
- return args.change_type and args.category and args.description
- def get_values_from_editor(args):
- with tempfile.NamedTemporaryFile('w') as f:
- contents = TEMPLATE.format(
- change_type=args.change_type,
- category=args.category,
- description=args.description,
- )
- f.write(contents)
- f.flush()
- env = os.environ
- editor = env.get('VISUAL', env.get('EDITOR', 'vim'))
- p = subprocess.Popen(f'{editor} {f.name}', shell=True)
- p.communicate()
- with open(f.name) as f:
- filled_in_contents = f.read()
- parsed_values = parse_filled_in_contents(filled_in_contents)
- return parsed_values
- def replace_issue_references(parsed, repo_name):
- description = parsed['description']
- def linkify(match):
- number = match.group()[1:]
- return '`{} <https://github.com/{}/issues/{}>`__'.format(
- match.group(), repo_name, number
- )
- new_description = re.sub(r'#\d+', linkify, description)
- parsed['description'] = new_description
- def write_new_change(parsed_values):
- if not os.path.isdir(CHANGES_DIR):
- os.makedirs(CHANGES_DIR)
- # Assume that new changes go into the next release.
- dirname = os.path.join(CHANGES_DIR, 'next-release')
- if not os.path.isdir(dirname):
- os.makedirs(dirname)
- # Need to generate a unique filename for this change.
- # We'll try a couple things until we get a unique match.
- category = parsed_values['category']
- short_summary = ''.join(filter(lambda x: x in VALID_CHARS, category))
- filename = '{type_name}-{summary}'.format(
- type_name=parsed_values['type'], summary=short_summary
- )
- possible_filename = os.path.join(
- dirname, f'{filename}-{str(random.randint(1, 100000))}.json'
- )
- while os.path.isfile(possible_filename):
- possible_filename = os.path.join(
- dirname, f'{filename}-{str(random.randint(1, 100000))}.json'
- )
- with open(possible_filename, 'w') as f:
- f.write(json.dumps(parsed_values, indent=2) + "\n")
- def parse_filled_in_contents(contents):
- """Parse filled in file contents and returns parsed dict.
- Return value will be::
- {
- "type": "bugfix",
- "category": "category",
- "description": "This is a description"
- }
- """
- if not contents.strip():
- return {}
- parsed = {}
- lines = iter(contents.splitlines())
- for line in lines:
- line = line.strip()
- if line.startswith('#'):
- continue
- if 'type' not in parsed and line.startswith('type:'):
- parsed['type'] = line.split(':')[1].strip()
- elif 'category' not in parsed and line.startswith('category:'):
- parsed['category'] = line.split(':')[1].strip()
- elif 'description' not in parsed and line.startswith('description:'):
- # Assume that everything until the end of the file is part
- # of the description, so we can break once we pull in the
- # remaining lines.
- first_line = line.split(':')[1].strip()
- full_description = '\n'.join([first_line] + list(lines))
- parsed['description'] = full_description.strip()
- break
- return parsed
- def main():
- parser = argparse.ArgumentParser()
- parser.add_argument(
- '-t',
- '--type',
- dest='change_type',
- default='',
- choices=('bugfix', 'feature', 'enhancement', 'api-change'),
- )
- parser.add_argument('-c', '--category', dest='category', default='')
- parser.add_argument('-d', '--description', dest='description', default='')
- parser.add_argument(
- '-r',
- '--repo',
- default='boto/boto3',
- help='Optional repo name, e.g: boto/boto3',
- )
- args = parser.parse_args()
- sys.exit(new_changelog_entry(args))
- if __name__ == '__main__':
- main()
|