| #!/usr/bin/env python |
| # This file is borrowed from the aws/aws-cli project with the following modifications: |
| # - Add a 'deprecation' category, and validation for the category value |
| # - Modify the 'linkify' method to use Markdown syntax instead of reStructuredText (rst) |
| # - Better error reporting when one or more of the fields are empty |
| # - Change filename format to use a the SHA1 digest of the content instead of a random number |
| # - Change references to aws/cli to aws/aws-sdk-java |
| """Generate a new changelog entry. |
| |
| Usage |
| ===== |
| |
| To generate a new changelog entry:: |
| |
| bin/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, deprecation, removal, documentation |
| type: {change_type} |
| |
| # The marketing name of the service this change applies to |
| # e.g: AWS CodeCommit, Amazon DynamoDB |
| # or "AWS SDK for Java v2" if it's an SDK change to the core, runtime etc |
| category: {category} |
| |
| |
| The description of the change. Feel free to use Markdown here. |
| description: {description} |
| |
| Fill in the appropriate values, save and exit the editor. |
| |
| 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. |
| |
| """ |
| import argparse |
| import hashlib |
| import json |
| import os |
| import re |
| import string |
| import subprocess |
| import sys |
| import tempfile |
| |
| from changelog.git import stage_file |
| |
| VALID_CHARS = set(string.ascii_letters + string.digits) |
| CHANGES_DIR = os.path.join( |
| os.path.dirname(os.path.dirname(os.path.abspath(__file__))), |
| '.changes' |
| ) |
| CHANGE_PARTS = ['type', 'category', 'description'] |
| TEMPLATE = """\ |
| # Type should be one of: feature, bugfix, deprecation, removal, documentation |
| type: {change_type} |
| |
| # The marketing name of the service this change applies to |
| # e.g: AWS CodeCommit, Amazon DynamoDB |
| # or "AWS SDK for Java v2" if it's an SDK change to the core, runtime etc |
| category: {category} |
| |
| # Your GitHub username (without '@') to be included in the CHANGELOG. |
| # Every contribution counts and we would like to recognize |
| # your contribution! |
| # Leave it empty if you would prefer not to be mentioned. |
| contributor: {contributor} |
| |
| The description of the change. Feel free to use Markdown here. |
| 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, |
| 'contributor': args.contributor |
| } |
| else: |
| parsed_values = get_values_from_editor(args) |
| missing_parts = get_missing_parts(parsed_values) |
| if len(missing_parts) > 0: |
| sys.stderr.write( |
| "No values provided for: %s. Skipping entry creation.\n" % missing_parts) |
| return 1 |
| |
| replace_issue_references(parsed_values, args.repo) |
| filename = write_new_change(parsed_values) |
| return stage_file(filename) |
| |
| def get_missing_parts(parsed_values): |
| return [p for p in CHANGE_PARTS if not parsed_values.get(p)] |
| |
| |
| def all_values_provided(args): |
| return args.change_type and args.category and args.description and args.contributor |
| |
| |
| 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, |
| contributor=args.contributor |
| ) |
| f.write(contents) |
| f.flush() |
| env = os.environ |
| editor = env.get('VISUAL', env.get('EDITOR', 'vim')) |
| p = subprocess.Popen('%s %s' % (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 ( |
| '[%s](https://github.com/%s/issues/%s)' % ( |
| match.group(), repo_name, number)) |
| |
| new_description = re.sub('(?<!\[)#\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. |
| category = parsed_values['category'] |
| |
| contributor = parsed_values['contributor'] |
| if contributor and contributor.strip: |
| contributor = remove_prefix(contributor, '@') |
| parsed_values['contributor'] = contributor |
| |
| short_summary = ''.join(filter(lambda x: x in VALID_CHARS, category)) |
| contents = json.dumps(parsed_values, indent=4) + "\n" |
| contents_digest = hashlib.sha1(contents.encode('utf-8')).hexdigest() |
| filename = '{type_name}-{summary}-{digest}.json'.format( |
| type_name=parsed_values['type'], |
| summary=short_summary, |
| digest=contents_digest[0:7]) |
| filename = os.path.join(dirname, filename) |
| with open(filename, 'w') as f: |
| f.write(contents) |
| return filename |
| |
| def remove_prefix(text, prefix): |
| if text.startswith(prefix): |
| return text[len(prefix):] |
| return text |
| |
| 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:'): |
| t = line[len('type:'):].strip() |
| if t not in ['feature', 'bugfix', 'deprecation', 'removal', |
| 'documentation']: |
| raise Exception("Unsupported category %s" % t) |
| parsed['type'] = t |
| elif 'category' not in parsed and line.startswith('category:'): |
| parsed['category'] = line[len('category:'):].strip() |
| elif 'contributor' not in parsed and line.startswith('contributor:'): |
| parsed['contributor'] = line[len('contributor:'):].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[len('description:'):].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', |
| 'deprecation', 'documentation')) |
| parser.add_argument('-c', '--category', dest='category', |
| default='') |
| parser.add_argument('-u', '--contributor', dest='contributor', |
| default='') |
| parser.add_argument('-d', '--description', dest='description', |
| default='') |
| parser.add_argument('-r', '--repo', default='aws/aws-sdk-java-v2', |
| help='Optional repo name, e.g: aws/aws-sdk-java') |
| args = parser.parse_args() |
| sys.exit(new_changelog_entry(args)) |
| |
| |
| if __name__ == '__main__': |
| main() |