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