Completed
Pull Request — master (#45)
by Gonzalo
46s
created

create_changelog()   B

Complexity

Conditions 6

Size

Total Lines 65

Duplication

Lines 0
Ratio 0 %

Importance

Changes 9
Bugs 0 Features 1
Metric Value
cc 6
c 9
b 0
f 1
dl 0
loc 65
rs 7.6697

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
# -*- coding: utf-8 -*-
2
# -----------------------------------------------------------------------------
3
# Copyright (c) The Spyder Development Team
4
#
5
# Licensed under the terms of the MIT License
6
# (See LICENSE.txt for details)
7
# -----------------------------------------------------------------------------
8
"""Build a list of issues and pull requests per Github milestone."""
9
10
from __future__ import print_function
11
12
# Standard library imports
13
import argparse
14
import getpass
15
import re
16
import sys
17
import time
18
19
# Third party imports
20
from jinja2 import Template
21
22
# Local imports
23
# yapf: disable
24
from loghub.repo import GitHubRepo
25
from loghub.templates import (CHANGELOG_GROUPS_TEMPLATE_PATH,
26
                              CHANGELOG_TEMPLATE_PATH,
27
                              RELEASE_GROUPS_TEMPLATE_PATH,
28
                              RELEASE_TEMPLATE_PATH)
29
30
# yapf: enable
31
32
PY2 = sys.version[0] == '2'
33
34
35
def main():
36
    """Main script."""
37
    parse_arguments(skip=False)
38
39
40
def parse_arguments(skip=False):
41
    """Parse CLI arguments."""
42
    # Cli options
43
    parser = argparse.ArgumentParser(
44
        description='Script to print the list of issues and pull requests '
45
        'closed in a given milestone')
46
    parser.add_argument(
47
        'repository',
48
        help="Repository name to generate the Changelog for, in the form "
49
        "user/repo or org/repo (e.g. spyder-ide/spyder)")
50
    parser.add_argument(
51
        '-m',
52
        '--milestone',
53
        action="store",
54
        dest="milestone",
55
        default='',
56
        help="Github milestone to get issues and pull requests for")
57
    parser.add_argument(
58
        '-ilg',
59
        '--issue-label-group',
60
        action="append",
61
        nargs='+',
62
        dest="issue_label_groups",
63
        help="Groups the generated issues by the specified label. This option"
64
        "Takes 1 or 2 arguments, where the first one is the label to "
65
        "match and the second one is the label to print on the final"
66
        "output")
67
    parser.add_argument(
68
        '-ilr',
69
        '--issue-label-regex',
70
        action="store",
71
        dest="issue_label_regex",
72
        default='',
73
        help="Label issue filter using a regular expression filter")
74
    parser.add_argument(
75
        '-plr',
76
        '--pr-label-regex',
77
        action="store",
78
        dest="pr_label_regex",
79
        default='',
80
        help="Label pull requets filter using a regular expression filter")
81
    parser.add_argument(
82
        '-st',
83
        '--since-tag',
84
        action="store",
85
        dest="since_tag",
86
        default='',
87
        help="Github issues and pull requests since tag")
88
    parser.add_argument(
89
        '-ut',
90
        '--until-tag',
91
        action="store",
92
        dest="until_tag",
93
        default='',
94
        help="Github issues and pull requests until tag")
95
    parser.add_argument(
96
        '-b',
97
        '--branch',
98
        action="store",
99
        dest="branch",
100
        default='',
101
        help="Github base branch for merged PRs")
102
    parser.add_argument(
103
        '-f',
104
        '--format',
105
        action="store",
106
        dest="output_format",
107
        default='changelog',
108
        help="Format for print, either 'changelog' (for "
109
        "Changelog.md file) or 'release' (for the Github "
110
        "Releases page). Default is 'changelog'. The "
111
        "'release' option doesn't generate Markdown "
112
        "hyperlinks.")
113
    parser.add_argument(
114
        '--template',
115
        action="store",
116
        dest="template",
117
        default='',
118
        help="Use a custom Jinja2 template file ")
119
    parser.add_argument(
120
        '-u',
121
        '--user',
122
        action="store",
123
        dest="user",
124
        default='',
125
        help="Github user name")
126
    parser.add_argument(
127
        '-p',
128
        '--password',
129
        action="store",
130
        dest="password",
131
        default='',
132
        help="Github user password")
133
    parser.add_argument(
134
        '-t',
135
        '--token',
136
        action="store",
137
        dest="token",
138
        default='',
139
        help="Github access token")
140
    options = parser.parse_args()
141
142
    username = options.user
143
    password = options.password
144
    milestone = options.milestone
145
    issue_label_groups = options.issue_label_groups
146
147
    if username and not password:
148
        password = getpass.getpass()
149
150
    # Check if repo given
151
    if not options.repository:
152
        print('LOGHUB: Please define a repository name to this script. '
153
              'See its help')
154
        sys.exit(1)
155
156
    # Check if milestone or tag given
157
    if not milestone and not options.since_tag:
158
        print('\nLOGHUB: Querying all issues\n')
159
    elif milestone:
160
        print('\nLOGHUB: Querying issues for milestone {0}'
161
              '\n'.format(milestone))
162
163
    new_issue_label_groups = []
164
    if issue_label_groups:
165
        for item in issue_label_groups:
166
            dic = {}
167
            if len(item) == 1:
168
                dic['label'] = item[0]
169
                dic['name'] = item[0]
170
            elif len(item) == 2:
171
                dic['label'] = item[0]
172
                dic['name'] = item[1]
173
            else:
174
                print('LOGHUB: Issue label group takes 1 or 2 arguments\n')
175
                sys.exit(1)
176
            new_issue_label_groups.append(dic)
177
178
    if not skip:
179
        create_changelog(
180
            repo=options.repository,
181
            username=username,
182
            password=password,
183
            token=options.token,
184
            milestone=milestone,
185
            since_tag=options.since_tag,
186
            until_tag=options.until_tag,
187
            branch=options.branch,
188
            issue_label_regex=options.issue_label_regex,
189
            pr_label_regex=options.pr_label_regex,
190
            output_format=options.output_format,
191
            template_file=options.template,
192
            issue_label_groups=new_issue_label_groups)
193
194
    return options
195
196
197
def filter_issues_prs_regex(issues, issue_label_regex, pr_label_regex):
198
    """Filter issues by issue and pr regex."""
199
    filtered_issues, filtered_prs = [], []
200
    issue_pattern = re.compile(issue_label_regex)
201
    pr_pattern = re.compile(pr_label_regex)
202
203
    for issue in issues:
204
        is_pr = bool(issue.get('pull_request'))
205
        is_issue = not is_pr
206
        labels = ' '.join(issue.get('loghub_label_names'))
207
208
        if is_issue and issue_label_regex:
209
            issue_valid = bool(issue_pattern.search(labels))
210
            if issue_valid:
211
                filtered_issues.append(issue)
212
        elif is_pr and pr_label_regex:
213
            pr_valid = bool(pr_pattern.search(labels))
214
            if pr_valid:
215
                filtered_prs.append(issue)
216
        elif is_issue and not issue_label_regex:
217
            filtered_issues.append(issue)
218
        elif is_pr and not pr_label_regex:
219
            filtered_prs.append(issue)
220
221
    return filtered_issues, filtered_prs
222
223
224
def filter_issue_label_groups(issues, issue_label_groups):
225
    """"""
226
    new_filtered_issues = []
227
    if issue_label_groups:
228
        for issue in issues:
229
            for label_group_dic in issue_label_groups:
230
                labels = issue.get('loghub_label_names')
231
                label = label_group_dic['label']
232
                if label in labels:
233
                    new_filtered_issues.append(issue)
234
    else:
235
        new_filtered_issues = issues
236
237
    return new_filtered_issues
238
239
240
def create_changelog(repo=None,
241
                     username=None,
242
                     password=None,
243
                     token=None,
244
                     milestone=None,
245
                     since_tag=None,
246
                     until_tag=None,
247
                     branch=None,
248
                     output_format='changelog',
249
                     issue_label_regex='',
250
                     pr_label_regex='',
251
                     template_file=None,
252
                     issue_label_groups=None):
253
    """Create changelog data."""
254
    # Instantiate Github API
255
    gh = GitHubRepo(
256
        username=username,
257
        password=password,
258
        token=token,
259
        repo=repo, )
260
261
    version = until_tag or None
262
    milestone_number = None
263
    closed_at = None
264
    since = None
265
    until = None
266
267
    # Set milestone or from tag
268
    if milestone and not since_tag:
269
        milestone_data = gh.milestone(milestone)
270
        milestone_number = milestone_data['number']
271
        closed_at = milestone_data['closed_at']
272
        version = milestone.replace('v', '')
273
    elif not milestone and since_tag:
274
        since = gh.tag(since_tag)['tagger']['date']
275
        if until_tag:
276
            until = gh.tag(until_tag)['tagger']['date']
277
            closed_at = until
278
279
    # This returns issues and pull requests
280
    issues = gh.issues(
281
        milestone=milestone_number,
282
        state='closed',
283
        since=since,
284
        until=until,
285
        branch=branch, )
286
287
    # Filter by regex if available
288
    filtered_issues, filtered_prs = filter_issues_prs_regex(issues,
289
                                                            issue_label_regex,
290
                                                            pr_label_regex)
291
292
    # If issue label grouping, filter issues
293
    new_filtered_issues = filter_issue_label_groups(filtered_issues,
294
                                                    issue_label_groups)
295
296
    return format_changelog(
297
        repo,
298
        new_filtered_issues,
299
        filtered_prs,
300
        version,
301
        closed_at=closed_at,
302
        output_format=output_format,
303
        template_file=template_file,
304
        issue_label_groups=issue_label_groups)
305
306
307
def format_changelog(repo,
308
                     issues,
309
                     prs,
310
                     version,
311
                     closed_at=None,
312
                     output_format='changelog',
313
                     output_file='CHANGELOG.temp',
314
                     template_file=None,
315
                     issue_label_groups=None):
316
    """Create changelog data."""
317
    # Header
318
    if version and version[0] == 'v':
319
        version = version.replace('v', '')
320
    else:
321
        version = '<RELEASE_VERSION>'
322
323
    if closed_at:
324
        close_date = closed_at.split('T')[0]
325
    else:
326
        close_date = time.strftime("%Y/%m/%d")
327
328
    # Load template
329
    if template_file:
330
        filepath = template_file
331
    else:
332
        if issue_label_groups:
333
            if output_format == 'changelog':
334
                filepath = CHANGELOG_GROUPS_TEMPLATE_PATH
335
            else:
336
                filepath = RELEASE_GROUPS_TEMPLATE_PATH
337
        else:
338
            if output_format == 'changelog':
339
                filepath = CHANGELOG_TEMPLATE_PATH
340
            else:
341
                filepath = RELEASE_TEMPLATE_PATH
342
343
    with open(filepath) as f:
344
        data = f.read()
345
346
    repo_owner, repo_name = repo.split('/')
347
    template = Template(data)
348
    rendered = template.render(
349
        issues=issues,
350
        pull_requests=prs,
351
        version=version,
352
        close_date=close_date,
353
        repo_full_name=repo,
354
        repo_owner=repo_owner,
355
        repo_name=repo_name,
356
        issue_label_groups=issue_label_groups, )
357
358
    print('#' * 79)
359
    print(rendered)
360
    print('#' * 79)
361
362
    with open(output_file, 'w') as f:
363
        f.write(rendered)
364
365
    return rendered
366
367
368
if __name__ == '__main__':  # yapf: disable
369
    main()
370