xref: /aosp_15_r20/external/aws-sdk-java-v2/scripts/new-change (revision 8a52c7834d808308836a99fc2a6e0ed8db339086)
1#!/usr/bin/env python
2# This file is borrowed from the aws/aws-cli project with the following modifications:
3#  - Add a 'deprecation' category, and validation for the category value
4#  - Modify the 'linkify' method to use Markdown syntax instead of reStructuredText (rst)
5#  - Better error reporting when one or more of the fields are empty
6#  - Change filename format to use a the SHA1 digest of the content instead of a random number
7#  - Change references to aws/cli to aws/aws-sdk-java
8"""Generate a new changelog entry.
9
10Usage
11=====
12
13To generate a new changelog entry::
14
15    bin/new-change
16
17This will open up a file in your editor (via the ``EDITOR`` env  var).
18You'll see this template::
19    # Type should be one of: feature, bugfix, deprecation, removal, documentation
20    type: {change_type}
21
22    # The marketing name of the service this change applies to
23    # e.g: AWS CodeCommit, Amazon DynamoDB
24    # or "AWS SDK for Java v2" if it's an SDK change to the core, runtime etc
25    category: {category}
26
27
28    The description of the change. Feel free to use Markdown here.
29    description: {description}
30
31Fill in the appropriate values, save and exit the editor.
32
33If, when your editor is open, you decide don't don't want to add a changelog
34entry, save an empty file and no entry will be generated.
35
36"""
37import argparse
38import hashlib
39import json
40import os
41import re
42import string
43import subprocess
44import sys
45import tempfile
46
47from changelog.git import stage_file
48
49VALID_CHARS = set(string.ascii_letters + string.digits)
50CHANGES_DIR = os.path.join(
51    os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
52    '.changes'
53)
54CHANGE_PARTS = ['type', 'category', 'description']
55TEMPLATE = """\
56# Type should be one of: feature, bugfix, deprecation, removal, documentation
57type: {change_type}
58
59# The marketing name of the service this change applies to
60# e.g: AWS CodeCommit, Amazon DynamoDB
61# or "AWS SDK for Java v2" if it's an SDK change to the core, runtime etc
62category: {category}
63
64# Your GitHub username (without '@') to be included in the CHANGELOG.
65# Every contribution counts and we would like to recognize
66# your contribution!
67# Leave it empty if you would prefer not to be mentioned.
68contributor: {contributor}
69
70The description of the change. Feel free to use Markdown here.
71description: {description}
72"""
73
74
75def new_changelog_entry(args):
76    # Changelog values come from one of two places.
77    # Either all values are provided on the command line,
78    # or we open a text editor and let the user provide
79    # enter their values.
80    if all_values_provided(args):
81        parsed_values = {
82            'type': args.change_type,
83            'category': args.category,
84            'description': args.description,
85            'contributor': args.contributor
86        }
87    else:
88        parsed_values = get_values_from_editor(args)
89        missing_parts = get_missing_parts(parsed_values)
90        if len(missing_parts) > 0:
91            sys.stderr.write(
92                "No values provided for: %s. Skipping entry creation.\n" % missing_parts)
93            return 1
94
95    replace_issue_references(parsed_values, args.repo)
96    filename = write_new_change(parsed_values)
97    return stage_file(filename)
98
99def get_missing_parts(parsed_values):
100    return [p for p in CHANGE_PARTS if not parsed_values.get(p)]
101
102
103def all_values_provided(args):
104    return args.change_type and args.category and args.description and args.contributor
105
106
107def get_values_from_editor(args):
108    with tempfile.NamedTemporaryFile('w') as f:
109        contents = TEMPLATE.format(
110            change_type=args.change_type,
111            category=args.category,
112            description=args.description,
113            contributor=args.contributor
114        )
115        f.write(contents)
116        f.flush()
117        env = os.environ
118        editor = env.get('VISUAL', env.get('EDITOR', 'vim'))
119        p = subprocess.Popen('%s %s' % (editor, f.name), shell=True)
120        p.communicate()
121        with open(f.name) as f:
122            filled_in_contents = f.read()
123            parsed_values = parse_filled_in_contents(filled_in_contents)
124            return parsed_values
125
126
127def replace_issue_references(parsed, repo_name):
128    description = parsed['description']
129
130    def linkify(match):
131        number = match.group()[1:]
132        return (
133            '[%s](https://github.com/%s/issues/%s)' % (
134                match.group(), repo_name, number))
135
136    new_description = re.sub('(?<!\[)#\d+', linkify, description)
137    parsed['description'] = new_description
138
139def write_new_change(parsed_values):
140    if not os.path.isdir(CHANGES_DIR):
141        os.makedirs(CHANGES_DIR)
142    # Assume that new changes go into the next release.
143    dirname = os.path.join(CHANGES_DIR, 'next-release')
144    if not os.path.isdir(dirname):
145        os.makedirs(dirname)
146    # Need to generate a unique filename for this change.
147    category = parsed_values['category']
148
149    contributor = parsed_values['contributor']
150    if contributor and contributor.strip:
151        contributor = remove_prefix(contributor, '@')
152        parsed_values['contributor'] = contributor
153
154    short_summary = ''.join(filter(lambda x: x in VALID_CHARS, category))
155    contents = json.dumps(parsed_values, indent=4) + "\n"
156    contents_digest = hashlib.sha1(contents.encode('utf-8')).hexdigest()
157    filename = '{type_name}-{summary}-{digest}.json'.format(
158        type_name=parsed_values['type'],
159        summary=short_summary,
160        digest=contents_digest[0:7])
161    filename = os.path.join(dirname, filename)
162    with open(filename, 'w') as f:
163        f.write(contents)
164    return filename
165
166def remove_prefix(text, prefix):
167    if text.startswith(prefix):
168        return text[len(prefix):]
169    return text
170
171def parse_filled_in_contents(contents):
172    """Parse filled in file contents and returns parsed dict.
173
174    Return value will be::
175        {
176          "type": "bugfix",
177          "category": "category",
178          "description": "This is a description"
179        }
180
181    """
182    if not contents.strip():
183        return {}
184    parsed = {}
185    lines = iter(contents.splitlines())
186    for line in lines:
187        line = line.strip()
188        if line.startswith('#'):
189            continue
190        if 'type' not in parsed and line.startswith('type:'):
191            t = line[len('type:'):].strip()
192            if t not in ['feature', 'bugfix', 'deprecation', 'removal',
193                         'documentation']:
194                raise Exception("Unsupported category %s" % t)
195            parsed['type'] = t
196        elif 'category' not in parsed and line.startswith('category:'):
197            parsed['category'] = line[len('category:'):].strip()
198        elif 'contributor' not in parsed and line.startswith('contributor:'):
199            parsed['contributor'] = line[len('contributor:'):].strip()
200        elif 'description' not in parsed and line.startswith('description:'):
201            # Assume that everything until the end of the file is part
202            # of the description, so we can break once we pull in the
203            # remaining lines.
204            first_line = line[len('description:'):].strip()
205            full_description = '\n'.join([first_line] + list(lines))
206            parsed['description'] = full_description.strip()
207            break
208    return parsed
209
210
211def main():
212    parser = argparse.ArgumentParser()
213    parser.add_argument('-t', '--type', dest='change_type',
214                        default='', choices=('bugfix', 'feature',
215                                             'deprecation', 'documentation'))
216    parser.add_argument('-c', '--category', dest='category',
217                        default='')
218    parser.add_argument('-u', '--contributor', dest='contributor',
219                        default='')
220    parser.add_argument('-d', '--description', dest='description',
221                        default='')
222    parser.add_argument('-r', '--repo', default='aws/aws-sdk-java-v2',
223                        help='Optional repo name, e.g: aws/aws-sdk-java')
224    args = parser.parse_args()
225    sys.exit(new_changelog_entry(args))
226
227
228if __name__ == '__main__':
229    main()
230