Completed
Pull Request — master (#14)
by Gonzalo
58s
created

create_changelog()   D

Complexity

Conditions 8

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 8
c 2
b 0
f 1
dl 0
loc 37
rs 4
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 datetime
15
import getpass
16
import sys
17
import time
18
19
# Local imports
20
from loghub.external.github import GitHub
21
22
# TEMPLATES
23
ISSUE_LONG = "* [Issue {number}](https://github.com/{repo}/issues/{number})"
24
ISSUE_SHORT = "* Issue #{number}"
25
PR_LONG = "* [PR {number}](https://github.com/{repo}/pull/{number})"
26
PR_SHORT = "* PR #{number}"
27
28
29
def main():
30
    """Main script."""
31
    # Cli options
32
    parser = argparse.ArgumentParser(
33
        description='Script to print the list of issues and pull requests '
34
        'closed in a given milestone')
35
    parser.add_argument(
36
        'repository',
37
        help="Repository name to generate the Changelog for, in the form "
38
        "user/repo or org/repo (e.g. spyder-ide/spyder)")
39
    parser.add_argument(
40
        '-m',
41
        '--milestone',
42
        action="store",
43
        dest="milestone",
44
        default='',
45
        help="Github milestone to get issues and pull requests for")
46
    parser.add_argument(
47
        '-st',
48
        '--since-tag',
49
        action="store",
50
        dest="since_tag",
51
        default='',
52
        help="Github issues and pull requests since tag")
53
    parser.add_argument(
54
        '-ut',
55
        '--until-tag',
56
        action="store",
57
        dest="until_tag",
58
        default='',
59
        help="Github issues and pull requests until tag")
60
    parser.add_argument(
61
        '-f',
62
        '--format',
63
        action="store",
64
        dest="output_format",
65
        default='changelog',
66
        help="Format for print, either 'changelog' (for "
67
        "Changelog.md file) or 'release' (for the Github "
68
        "Releases page). Default is 'changelog'. The "
69
        "'release' option doesn't generate Markdown "
70
        "hyperlinks.")
71
    parser.add_argument(
72
        '-u',
73
        '--user',
74
        action="store",
75
        dest="user",
76
        default='',
77
        help="Github user name")
78
    parser.add_argument(
79
        '-p',
80
        '--password',
81
        action="store",
82
        dest="password",
83
        default='',
84
        help="Github user password")
85
    options = parser.parse_args()
86
87
    # Check if repo given
88
    if not options.repository:
89
        print('Please define a repository name to this script. See its help')
90
        sys.exit(1)
91
92
    # Check if milestone or tag given
93
    if not options.milestone and not options.since_tag:
94
        print('Please pass a milestone or a tag to this script. See its help')
95
        sys.exit(1)
96
97
    create_changelog(
98
        repo=options.repository,
99
        username=options.user,
100
        password=options.password,
101
        milestone=options.milestone,
102
        since_tag=options.since_tag,
103
        until_tag=options.until_tag,
104
        output_format=options.output_format, )
105
106
107
def create_changelog(repo, username, password, milestone, since_tag, until_tag,
108
                     output_format):
109
    """Create changelog data."""
110
    if username and not password:
111
        password = getpass.getpass()
112
113
    # Instantiate Github API
114
    gh = GitHubRepo(username=username, password=password, repo=repo)
115
116
    version = until_tag or None
117
    milestone_number = None
118
    closed_at = None
119
    since = None
120
    until = None
121
122
    # Set milestone or from tag
123
    if milestone and not since_tag:
124
        milestone_data = gh.milestone(milestone)
125
        milestone_number = milestone_data['number']
126
        closed_at = milestone_data['closed_at']
127
        version = milestone.replace('v', '')
128
    elif not milestone and since_tag:
129
        since = gh.tag(since_tag)['tagger']['date']
130
        if until_tag:
131
            until = gh.tag(until_tag)['tagger']['date']
132
            closed_at = until
133
134
    # This returns issues and pull requests
135
    issues = gh.issues(
136
        milestone=milestone_number, state='closed', since=since, until=until)
137
138
    format_changelog(
139
        repo,
140
        issues,
141
        version,
142
        closed_at=closed_at,
143
        output_format=output_format)
144
145
146
def format_changelog(repo,
147
                     issues,
148
                     version,
149
                     closed_at=None,
150
                     output_format='changelog',
151
                     output_file='CHANGELOG.temp'):
152
    """Create changelog data."""
153
    lines = []
154
155
    # Header
156
    if version and version[0] == 'v':
157
        version = version.replace('v', '')
158
    else:
159
        '<RELEASE_VERSION>'
160
161
    if closed_at:
162
        close_date = closed_at.split('T')[0]
163
    else:
164
        close_date = time.strftime("%Y/%m/%d")
165
166
    quotes = '"' if version and ' ' in version else ''
167
    header = '## Version {q}{version}{q} ({date})\n'.format(
168
        version=version, date=close_date, q=quotes)
169
170
    lines.append(header)
171
    lines.append('### Bugs fixed\n')
172
173
    # --- Issues
174
    number_of_issues = 0
175
    issue_lines = ['**Issues**\n']
176
    for i in issues:
177
        pr = i.get('pull_request', '')
178
        if not pr:
179
            number_of_issues += 1
180
            number = i['number']
181
            if output_format == 'changelog':
182
                issue_link = ISSUE_LONG.format(number=number, repo=repo)
183
            else:
184
                issue_link = ISSUE_SHORT.format(number=number)
185
            issue_lines.append(issue_link + ' - ' + i['title'])
186
187
    tense = 'was' if number_of_issues == 1 else 'were'
188
    plural = '' if number_of_issues == 1 else 's'
189
    issue_lines.append('\nIn this release {number} issue{plural} {tense} '
190
                       'closed\n'.format(
191
                           number=number_of_issues, tense=tense,
192
                           plural=plural))
193
    if number_of_issues > 0:
194
        lines = lines + issue_lines
195
196
    # --- Pull requests
197
    number_of_prs = 0
198
    pr_lines = ['**Pull requests**\n']
199
    for i in issues:
200
        pr = i.get('pull_request', '')
201
        if pr:
202
            number_of_prs += 1
203
            number = i['number']
204
            if output_format == 'changelog':
205
                pr_link = PR_LONG.format(number=number, repo=repo)
206
            else:
207
                pr_link = PR_SHORT.format(number=number)
208
            pr_lines.append(pr_link + ' - ' + i['title'])
209
    tense = 'was' if number_of_prs == 1 else 'were'
210
    plural = '' if number_of_prs == 1 else 's'
211
    pr_lines.append('\nIn this release {number} pull request{plural} {tense} '
212
                    'merged\n'.format(
213
                        number=number_of_prs, tense=tense, plural=plural))
214
    if number_of_prs > 0:
215
        lines = lines + pr_lines
216
217
    # Print everything
218
    for line in lines:
219
        print(line)
220
221
    # Write to file
222
    with open(output_file, 'w') as f:
223
        for line in lines:
224
            print(line, file=f)
225
226
227
class GitHubRepo(object):
228
    """Github repository wrapper."""
229
230
    def __init__(self, username=None, password=None, repo=None):
231
        """Github repository wrapper."""
232
        self.gh = GitHub(username=username, password=password)
233
        repo_organization, repo_name = repo.split('/')
234
        self.repo = self.gh.repos(repo_organization)(repo_name)
235
236
    def tags(self):
237
        """Return all tags."""
238
        return self.repo('git')('refs')('tags').get()
239
240
    def tag(self, tag_name):
241
        """Get tag information."""
242
        refs = self.repo('git')('refs')('tags').get()
243
        sha = -1
244
        for ref in refs:
245
            ref_name = 'refs/tags/{tag}'.format(tag=tag_name)
246
            if 'object' in ref and ref['ref'] == ref_name:
247
                sha = ref['object']['sha']
248
                break
249
250
        if sha == -1:
251
            print("You didn't pass a valid tag name!")
252
            sys.exit(1)
253
254
        return self.repo('git')('tags')(sha).get()
255
256
    def milestones(self):
257
        """Return all milestones."""
258
        return self.repo.milestones.get(state='all')
259
260
    def milestone(self, milestone_title):
261
        """Return milestone with given title."""
262
        milestones = self.milestones()
263
        milestone_number = -1
264
        for milestone in milestones:
265
            if milestone['title'] == milestone_title:
266
                milestone_number = milestone['number']
267
                break
268
269
        if milestone_number == -1:
270
            print("You didn't pass a valid milestone name!")
271
            sys.exit(1)
272
273
        return milestone
274
275
    def issues(self,
276
               milestone=None,
277
               state=None,
278
               assignee=None,
279
               creator=None,
280
               mentioned=None,
281
               labels=None,
282
               sort=None,
283
               direction=None,
284
               since=None,
285
               until=None):
286
        """Return Issues and Pull Requests."""
287
        page = 1
288
        issues = []
289
        while True:
290
            result = self.repo.issues.get(page=page,
291
                                          per_page=100,
292
                                          milestone=milestone,
293
                                          state=state,
294
                                          assignee=assignee,
295
                                          creator=creator,
296
                                          mentioned=mentioned,
297
                                          labels=labels,
298
                                          sort=sort,
299
                                          firection=direction,
300
                                          since=since)
301
            if len(result) > 0:
302
                issues += result
303
                page = page + 1
304
            else:
305
                break
306
307
        # If until was provided, fix it!
308
        if until:
309
            until_date = self.str_to_date(until)
310
            for issue in issues[:]:
311
                close_date = self.str_to_date(issue['closed_at'])
312
                if close_date > until_date:
313
                    issues.remove(issue)
314
315
        return issues
316
317
    @staticmethod
318
    def str_to_date(string):
319
        """Convert ISO date string to datetime object."""
320
        parts = string.split('T')
321
        date_parts = parts[0]
322
        time_parts = parts[1][:-1]
323
        year, month, day = [int(i) for i in date_parts.split('-')]
324
        hour, minutes, seconds = [int(i) for i in time_parts.split(':')]
325
        return datetime.datetime(year, month, day, hour, minutes, seconds)
326
327
328
if __name__ == '__main__':
329
    main()
330