Completed
Push — master ( b9438d...274b03 )
by Gonzalo
9s
created

create_changelog()   B

Complexity

Conditions 6

Size

Total Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

Changes 10
Bugs 0 Features 1
Metric Value
cc 6
c 10
b 0
f 1
dl 0
loc 64
rs 7.69

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_prs_by_regex(issues, pr_label_regex):
198
    """Filter prs by issue regex."""
199
    filtered_prs = []
200
    pr_pattern = re.compile(pr_label_regex)
201
202
    for issue in issues:
203
        is_pr = bool(issue.get('pull_request'))
204
        labels = ' '.join(issue.get('loghub_label_names'))
205
206
        if is_pr and pr_label_regex:
207
            pr_valid = bool(pr_pattern.search(labels))
208
            if pr_valid:
209
                filtered_prs.append(issue)
210
        if is_pr and not pr_label_regex:
211
            filtered_prs.append(issue)
212
213
    return filtered_prs
214
215
216
def filter_issues_by_regex(issues, issue_label_regex):
217
    """Filter issues by issue regex."""
218
    filtered_issues = []
219
    issue_pattern = re.compile(issue_label_regex)
220
221
    for issue in issues:
222
        is_pr = bool(issue.get('pull_request'))
223
        is_issue = not is_pr
224
        labels = ' '.join(issue.get('loghub_label_names'))
225
226
        if is_issue and issue_label_regex:
227
            issue_valid = bool(issue_pattern.search(labels))
228
            if issue_valid:
229
                filtered_issues.append(issue)
230
        elif is_issue and not issue_label_regex:
231
            filtered_issues.append(issue)
232
233
    return filtered_issues
234
235
236
def filter_issue_label_groups(issues, issue_label_groups):
237
    """Filter issues by the label groups."""
238
    new_filtered_issues = []
239
    if issue_label_groups:
240
        for issue in issues:
241
            for label_group_dic in issue_label_groups:
242
                labels = issue.get('loghub_label_names')
243
                label = label_group_dic['label']
244
                if label in labels:
245
                    new_filtered_issues.append(issue)
246
    else:
247
        new_filtered_issues = issues
248
249
    return new_filtered_issues
250
251
252
def create_changelog(repo=None,
253
                     username=None,
254
                     password=None,
255
                     token=None,
256
                     milestone=None,
257
                     since_tag=None,
258
                     until_tag=None,
259
                     branch=None,
260
                     output_format='changelog',
261
                     issue_label_regex='',
262
                     pr_label_regex='',
263
                     template_file=None,
264
                     issue_label_groups=None):
265
    """Create changelog data."""
266
    # Instantiate Github API
267
    gh = GitHubRepo(
268
        username=username,
269
        password=password,
270
        token=token,
271
        repo=repo, )
272
273
    version = until_tag or None
274
    milestone_number = None
275
    closed_at = None
276
    since = None
277
    until = None
278
279
    # Set milestone or from tag
280
    if milestone and not since_tag:
281
        milestone_data = gh.milestone(milestone)
282
        milestone_number = milestone_data['number']
283
        closed_at = milestone_data['closed_at']
284
        version = milestone.replace('v', '')
285
    elif not milestone and since_tag:
286
        since = gh.tag(since_tag)['tagger']['date']
287
        if until_tag:
288
            until = gh.tag(until_tag)['tagger']['date']
289
            closed_at = until
290
291
    # This returns issues and pull requests
292
    issues = gh.issues(
293
        milestone=milestone_number,
294
        state='closed',
295
        since=since,
296
        until=until,
297
        branch=branch, )
298
299
    # Filter by regex if available
300
    filtered_prs = filter_prs_by_regex(issues, pr_label_regex)
301
    filtered_issues = filter_issues_by_regex(issues, issue_label_regex)
302
303
    # If issue label grouping, filter issues
304
    new_filtered_issues = filter_issue_label_groups(filtered_issues,
305
                                                    issue_label_groups)
306
307
    return format_changelog(
308
        repo,
309
        new_filtered_issues,
310
        filtered_prs,
311
        version,
312
        closed_at=closed_at,
313
        output_format=output_format,
314
        template_file=template_file,
315
        issue_label_groups=issue_label_groups)
316
317
318
def format_changelog(repo,
319
                     issues,
320
                     prs,
321
                     version,
322
                     closed_at=None,
323
                     output_format='changelog',
324
                     output_file='CHANGELOG.temp',
325
                     template_file=None,
326
                     issue_label_groups=None):
327
    """Create changelog data."""
328
    # Header
329
    if version and version[0] == 'v':
330
        version = version.replace('v', '')
331
    else:
332
        version = '<RELEASE_VERSION>'
333
334
    if closed_at:
335
        close_date = closed_at.split('T')[0]
336
    else:
337
        close_date = time.strftime("%Y/%m/%d")
338
339
    # Load template
340
    if template_file:
341
        filepath = template_file
342
    else:
343
        if issue_label_groups:
344
            if output_format == 'changelog':
345
                filepath = CHANGELOG_GROUPS_TEMPLATE_PATH
346
            else:
347
                filepath = RELEASE_GROUPS_TEMPLATE_PATH
348
        else:
349
            if output_format == 'changelog':
350
                filepath = CHANGELOG_TEMPLATE_PATH
351
            else:
352
                filepath = RELEASE_TEMPLATE_PATH
353
354
    with open(filepath) as f:
355
        data = f.read()
356
357
    repo_owner, repo_name = repo.split('/')
358
    template = Template(data)
359
    rendered = template.render(
360
        issues=issues,
361
        pull_requests=prs,
362
        version=version,
363
        close_date=close_date,
364
        repo_full_name=repo,
365
        repo_owner=repo_owner,
366
        repo_name=repo_name,
367
        issue_label_groups=issue_label_groups, )
368
369
    print('#' * 79)
370
    print(rendered)
371
    print('#' * 79)
372
373
    with open(output_file, 'w') as f:
374
        f.write(rendered)
375
376
    return rendered
377
378
379
if __name__ == '__main__':  # yapf: disable
380
    main()
381